More work on the massic refactor to functional react
This commit is contained in:
parent
471e239591
commit
8308515c9b
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"npx tsc": true
|
||||
}
|
||||
}
|
||||
@ -4,27 +4,50 @@
|
||||
"AuditLogs": "Audit Logs",
|
||||
"BlockedIPAddresses": "Blocked IP addresses",
|
||||
"BlockedIPs": "Blocked IPs",
|
||||
"Changes": "Changes",
|
||||
"ClientDomainManager": "Client Domain Manager",
|
||||
"ClientDomains": "Client Domains",
|
||||
"Comment": "Comment",
|
||||
"CustomFieldManager": "Custom Field Manager",
|
||||
"CustomFields": "Custom Fields",
|
||||
"DisplayName": "Display Name",
|
||||
"EntityDisplayName": "Entity Display Name",
|
||||
"e-print": "e-print",
|
||||
"e-suite": "e-suite",
|
||||
"ErrorLogs": "Error Logs",
|
||||
"ExceptionJson": "Exception JSON",
|
||||
"ExceptionLogs": "Exception Logs",
|
||||
"Forms": "Forms",
|
||||
"FormTemplateManager": "Form Template Manager",
|
||||
"Glossaries": "Glossaries",
|
||||
"GlossaryManager": "Glossary Manager",
|
||||
"Home": "Home",
|
||||
"Id": "Id",
|
||||
"Application": "Application",
|
||||
"Message": "Message",
|
||||
"ShowJSON": "Show JSON",
|
||||
"ShowStackTrace": "Show stack trace",
|
||||
"OccuredAt": "Occured At",
|
||||
"IPAddress": "IP Address",
|
||||
"IPAddressUnblocked": "IP Address '{{ip}}' unblocked.",
|
||||
"Loading": "Loading",
|
||||
"Name": "Name",
|
||||
"NumberOfAttempts": "Number Of Attempts",
|
||||
"NewValue": "New Value",
|
||||
"OldValue": "Old Value",
|
||||
"PressAgainToUnblock": "Press again to unblock",
|
||||
"Sequence": "Sequence",
|
||||
"SequenceManager": "Sequence Manager",
|
||||
"SiteManager": "Site Manager",
|
||||
"SpecificationManager": "Specification Manager",
|
||||
"StackTrace": "Stack Trace",
|
||||
"SsoManager": "Sso Manager",
|
||||
"Support": "Support",
|
||||
"SupportingData": "Supporting Data",
|
||||
"Timing": "Timing",
|
||||
"Type": "Type",
|
||||
"UnblockedInMinutes": "Unblocked In (Minutes)",
|
||||
"UserManager": "User Manager",
|
||||
"Users": "Users",
|
||||
"Name": "Name",
|
||||
"Loading": "Loading"
|
||||
"UserName": "User Name",
|
||||
"Users": "Users"
|
||||
}
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
{
|
||||
"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)"
|
||||
}
|
||||
26
src/Sass/_errorLogs.scss
Normal file
26
src/Sass/_errorLogs.scss
Normal file
@ -0,0 +1,26 @@
|
||||
.errorlogs-pre {
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
font-family: Menlo, Monaco, "Courier New", monospace;
|
||||
background: transparent; /* removed fixed white background to respect app theme */
|
||||
border: 1px solid transparent; /* keep layout without forcing a light border color */
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.errorlogs-ExceptionJson {
|
||||
max-width: 800px;
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.errorlogs-StackTraceJson {
|
||||
max-width: 500px;
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.errorlogs-SupportingData {
|
||||
max-width: 500px;
|
||||
max-height: 600px;
|
||||
}
|
||||
63
src/Sass/_expandableCell.scss
Normal file
63
src/Sass/_expandableCell.scss
Normal file
@ -0,0 +1,63 @@
|
||||
.expandable-cell {
|
||||
display: block; // stack: button on top, content below
|
||||
|
||||
.expandable-cell__button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
font-size: 1rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.expandable-cell__title {
|
||||
margin-right: 0.25rem;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.expandable-cell__content_defaults {
|
||||
max-width: 600px;
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.expandable-cell__content {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
|
||||
overflow-x: auto; /* horizontal scrollbar always visible if needed */
|
||||
overflow-y: auto; /* vertical scrollbar always visible if needed */
|
||||
transition: height 240ms ease;
|
||||
}
|
||||
|
||||
.expandable-cell__content-inner {
|
||||
transition: opacity 180ms ease;
|
||||
opacity: 1;
|
||||
min-width: max-content; /* allow long lines to trigger horizontal scroll */
|
||||
width: fit-content;
|
||||
/* Remove overflow properties from here */
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.expandable-cell__icon {
|
||||
display: inline-block;
|
||||
transform-origin: center;
|
||||
transition: transform 180ms ease;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
.expandable-cell__icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.is-open) {
|
||||
.expandable-cell__content-inner {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
@import "../../node_modules/bootstrap/scss/functions";
|
||||
|
||||
//default variable overrides
|
||||
@import './_esuiteVariables.scss';
|
||||
@import "./_esuiteVariables.scss";
|
||||
|
||||
@import "../../node_modules/bootstrap/scss/variables";
|
||||
@import "../../node_modules/bootstrap/scss/variables-dark";
|
||||
@ -26,6 +26,8 @@
|
||||
@import "./pill.scss";
|
||||
@import "./multiSelect.scss";
|
||||
@import "./horizionalTabs";
|
||||
@import "./_expandableCell.scss";
|
||||
@import "./_errorLogs.scss";
|
||||
|
||||
//Changes needed to make MS Edge behave the same as other browsers
|
||||
input::-ms-reveal {
|
||||
|
||||
69
src/components/common/ExpandableCell.tsx
Normal file
69
src/components/common/ExpandableCell.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
defaultCollapsed?: boolean;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
title?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function ExpandableCell({
|
||||
children,
|
||||
defaultCollapsed = true,
|
||||
className,
|
||||
contentClassName = "expandable-cell__content_defaults",
|
||||
title,
|
||||
}: Props) {
|
||||
const [open, setOpen] = useState(!defaultCollapsed);
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const contentHeight = contentRef.current
|
||||
? contentRef.current.scrollHeight
|
||||
: 0;
|
||||
const calcMaxHeight = open ? `${contentHeight}px` : "0px";
|
||||
|
||||
const classes = ["expandable-cell", className, open ? "is-open" : ""]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((s) => !s)}
|
||||
className="expandable-cell__button"
|
||||
aria-label={
|
||||
typeof title === "string"
|
||||
? `${open ? "Hide" : "Show"} ${title}`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{title ? <span className="expandable-cell__title">{title}</span> : null}
|
||||
<FontAwesomeIcon
|
||||
className="expandable-cell__icon"
|
||||
icon={faChevronRight}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={["expandable-cell__content", contentClassName]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
style={{ height: calcMaxHeight }}
|
||||
aria-hidden={!open}
|
||||
>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className="expandable-cell__content-inner"
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,109 +1,119 @@
|
||||
import React from "react";
|
||||
import { faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import React, { useCallback } from "react";
|
||||
import {
|
||||
faAngleDoubleLeft,
|
||||
faAngleLeft,
|
||||
faAngleRight,
|
||||
faAngleDoubleRight,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Paginated } from "../../services/Paginated";
|
||||
import Button, { ButtonType } from "./Button";
|
||||
import { InputType } from "./Input";
|
||||
|
||||
interface PaginationProps<T> {
|
||||
data : Paginated<T>
|
||||
data: Paginated<T> | null;
|
||||
onChangePage: (page: number, pageSize: number) => void;
|
||||
onUnselect?: () => void;
|
||||
}
|
||||
|
||||
interface PaginationState {
|
||||
|
||||
}
|
||||
|
||||
class Pagination<T> extends React.Component<PaginationProps<T>, PaginationState> {
|
||||
state = { }
|
||||
|
||||
changePage( page : number, pageSize : number ){
|
||||
const {onChangePage, onUnselect} = this.props;
|
||||
|
||||
function Pagination<T>({
|
||||
data,
|
||||
onChangePage,
|
||||
onUnselect,
|
||||
}: PaginationProps<T>): JSX.Element {
|
||||
const changePage = useCallback(
|
||||
(page: number, pageSize: number) => {
|
||||
onChangePage(page, pageSize);
|
||||
if (onUnselect) {
|
||||
onUnselect();
|
||||
}
|
||||
}
|
||||
if (onUnselect) onUnselect();
|
||||
},
|
||||
[onChangePage, onUnselect],
|
||||
);
|
||||
|
||||
clickFirst = () => {
|
||||
const { pageSize } = this.props.data;
|
||||
if (data === null) return <></>;
|
||||
|
||||
this.changePage(1, pageSize );
|
||||
}
|
||||
const { page, pageSize, totalPages, count } = data;
|
||||
|
||||
clickPrevious = () => {
|
||||
const { page, pageSize } = this.props.data;
|
||||
const clickFirst = () => changePage(1, pageSize);
|
||||
const clickPrevious = () => changePage(page - 1, pageSize);
|
||||
const clickNext = () => changePage(page + 1, pageSize);
|
||||
const clickLast = () => changePage(totalPages, pageSize);
|
||||
|
||||
this.changePage(page - 1, pageSize);
|
||||
}
|
||||
|
||||
clickNext = () => {
|
||||
const { page, pageSize } = this.props.data;
|
||||
|
||||
this.changePage(page + 1, pageSize);
|
||||
}
|
||||
|
||||
clickLast = () => {
|
||||
const { totalPages, pageSize } = this.props.data;
|
||||
|
||||
this.changePage(totalPages, pageSize);
|
||||
}
|
||||
|
||||
PageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const input = e.currentTarget;
|
||||
const { page } = this.props.data;
|
||||
|
||||
let newPageSize : number = +input.value;
|
||||
|
||||
this.changePage(page, newPageSize);
|
||||
}
|
||||
|
||||
handlePageSelect = ( e : React.ChangeEvent<HTMLInputElement>) =>
|
||||
{
|
||||
const { pageSize, totalPages } = this.props.data;
|
||||
const input = e.currentTarget;
|
||||
|
||||
const newPage = Number(input.value);
|
||||
const PageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newPageSize = Number(e.currentTarget.value);
|
||||
changePage(page, newPageSize);
|
||||
};
|
||||
|
||||
const handlePageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newPage = Number(e.currentTarget.value);
|
||||
if (1 <= newPage && newPage <= totalPages) {
|
||||
this.changePage(newPage, pageSize);
|
||||
}
|
||||
changePage(newPage, pageSize);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const pageSizeOptions = [10, 25, 50, 100];
|
||||
|
||||
const { data } = this.props;
|
||||
|
||||
if (data === null)
|
||||
return <></>
|
||||
|
||||
const pageSizeOptions = [{ _id: "10", name: "10" },
|
||||
{ _id: "25", name: "25" },
|
||||
{ _id: "50", name: "50" },
|
||||
{ _id: "100", name: "100" }];
|
||||
|
||||
return ( <div className="d-flex py-2 bg-body-tertiary pagination">
|
||||
return (
|
||||
<div className="d-flex py-2 bg-body-tertiary pagination">
|
||||
<span className="px-2">
|
||||
<select value={data.pageSize} className="form-select" onChange={this.PageSizeChange}>
|
||||
{pageSizeOptions.map(({ _id, name }) => (
|
||||
<option key={_id} value={_id}>
|
||||
{name}
|
||||
<select
|
||||
value={pageSize}
|
||||
className="form-select"
|
||||
onChange={PageSizeChange}
|
||||
>
|
||||
{pageSizeOptions.map((n) => (
|
||||
<option key={n} value={n}>
|
||||
{n}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
<span>
|
||||
<Button className="me-1" buttonType={ButtonType.primary} onClick={this.clickFirst} disabled={data.page < 2}><FontAwesomeIcon icon={faAngleDoubleLeft}/></Button>
|
||||
<Button className="me-1"buttonType={ButtonType.primary} onClick={this.clickPrevious} disabled={data.page < 2}><FontAwesomeIcon icon={faAngleLeft}/></Button>
|
||||
<span className="me-1"><input type={InputType.number} value={data.page} min={1} max={data.totalPages} onChange={this.handlePageSelect}/> of {data.totalPages}</span>
|
||||
<Button className="me-1" buttonType={ButtonType.primary} onClick={this.clickNext} disabled={data.page >= data.totalPages}><FontAwesomeIcon icon={faAngleRight}/></Button>
|
||||
<Button className="me-1" buttonType={ButtonType.primary} onClick={this.clickLast} disabled={data.page >= data.totalPages}><FontAwesomeIcon icon={faAngleDoubleRight}/></Button>
|
||||
<span className="me-1">{data.count} Items</span>
|
||||
<Button
|
||||
className="me-1"
|
||||
buttonType={ButtonType.primary}
|
||||
onClick={clickFirst}
|
||||
disabled={page < 2}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleDoubleLeft} />
|
||||
</Button>
|
||||
<Button
|
||||
className="me-1"
|
||||
buttonType={ButtonType.primary}
|
||||
onClick={clickPrevious}
|
||||
disabled={page < 2}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleLeft} />
|
||||
</Button>
|
||||
<span className="me-1">
|
||||
<input
|
||||
type={InputType.number}
|
||||
value={page}
|
||||
min={1}
|
||||
max={totalPages}
|
||||
onChange={handlePageSelect}
|
||||
/>{" "}
|
||||
of {totalPages}
|
||||
</span>
|
||||
</div>);
|
||||
}
|
||||
<Button
|
||||
className="me-1"
|
||||
buttonType={ButtonType.primary}
|
||||
onClick={clickNext}
|
||||
disabled={page >= totalPages}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleRight} />
|
||||
</Button>
|
||||
<Button
|
||||
className="me-1"
|
||||
buttonType={ButtonType.primary}
|
||||
onClick={clickLast}
|
||||
disabled={page >= totalPages}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleDoubleRight} />
|
||||
</Button>
|
||||
<span className="me-1">{count} Items</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Pagination;
|
||||
@ -1,22 +1,16 @@
|
||||
import React from "react";
|
||||
import authenticationService from "../../modules/frame/services/authenticationService";
|
||||
|
||||
|
||||
interface PermissionProps {
|
||||
privilegeKey: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
class Permission extends React.Component<PermissionProps> {
|
||||
render() {
|
||||
const { privilegeKey, children } = this.props;
|
||||
const Permission: React.FC<PermissionProps> = ({ privilegeKey, children }) => {
|
||||
const hasAccess = authenticationService.hasAccess(privilegeKey);
|
||||
|
||||
if (hasAccess === false)
|
||||
return ( <></> );
|
||||
else
|
||||
return ( <>{children}</> );
|
||||
}
|
||||
}
|
||||
if (!hasAccess) return null;
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default Permission;
|
||||
@ -1,21 +1,13 @@
|
||||
import React from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface RedirectProps {
|
||||
to : string
|
||||
to: string;
|
||||
}
|
||||
|
||||
interface RedirectState {
|
||||
|
||||
}
|
||||
|
||||
class Redirect extends React.Component<RedirectProps, RedirectState> {
|
||||
render() {
|
||||
const {to} = this.props;
|
||||
|
||||
export default function Redirect({ to }: RedirectProps) {
|
||||
useEffect(() => {
|
||||
window.location.replace(to);
|
||||
}, [to]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default Redirect;
|
||||
@ -1,43 +1,66 @@
|
||||
import React from "react";
|
||||
import Option from "./option";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Namespaces } from "../../i18n/i18n";
|
||||
|
||||
export interface SelectProps {
|
||||
includeLabel? : boolean,
|
||||
name : string,
|
||||
label : string,
|
||||
error? : string,
|
||||
value : unknown
|
||||
options? : Option[],
|
||||
includeBlankFirstEntry? : boolean,
|
||||
includeLabel?: boolean;
|
||||
name: string;
|
||||
label: string;
|
||||
error?: string;
|
||||
value: unknown;
|
||||
options?: Option[];
|
||||
includeBlankFirstEntry?: boolean;
|
||||
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
}
|
||||
|
||||
function GenerateValue( value : unknown )
|
||||
{
|
||||
let actualValue = value;
|
||||
|
||||
if (value === true)
|
||||
return "true";
|
||||
if (value === false)
|
||||
return "false";
|
||||
|
||||
return actualValue as string | number | readonly string[] | undefined;
|
||||
function GenerateValue(value: unknown) {
|
||||
if (value === true) return "true";
|
||||
if (value === false) return "false";
|
||||
return value as string | number | readonly string[] | undefined;
|
||||
}
|
||||
|
||||
class Select extends React.Component<SelectProps> {
|
||||
render() {
|
||||
const { includeLabel, name, label, error, value, options, includeBlankFirstEntry, onChange, ...rest } = this.props;
|
||||
|
||||
export default function Select({
|
||||
includeLabel,
|
||||
name,
|
||||
label,
|
||||
error,
|
||||
value,
|
||||
options,
|
||||
includeBlankFirstEntry,
|
||||
onChange,
|
||||
...rest
|
||||
}: SelectProps) {
|
||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||
const actualValue = GenerateValue(value);
|
||||
|
||||
return (
|
||||
<div className="form-group">
|
||||
{(includeLabel===undefined || includeLabel===true) && <label htmlFor={name}>{label}</label>}
|
||||
{!options && <select multiple {...rest} id={name} value={actualValue} className="form-control loading" name={name} onChange={onChange}>
|
||||
<option value="loading..." />
|
||||
</select>}
|
||||
{options &&
|
||||
<select {...rest} id={name} value={actualValue} className="form-control" name={name} onChange={onChange}>
|
||||
{(includeLabel === undefined || includeLabel === true) && (
|
||||
<label htmlFor={name}>{label}</label>
|
||||
)}
|
||||
{!options && (
|
||||
<select
|
||||
multiple
|
||||
{...rest}
|
||||
id={name}
|
||||
value={actualValue}
|
||||
className="form-control loading"
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
>
|
||||
<option value="loading...">{t("Loading")}...</option>
|
||||
</select>
|
||||
)}
|
||||
{options && (
|
||||
<select
|
||||
{...rest}
|
||||
id={name}
|
||||
value={actualValue}
|
||||
className="form-control"
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
>
|
||||
{includeBlankFirstEntry && <option value="" />}
|
||||
{options?.map(({ _id, name }) => (
|
||||
<option key={_id} value={_id}>
|
||||
@ -45,11 +68,8 @@ class Select extends React.Component<SelectProps> {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
}
|
||||
)}
|
||||
{error && <div className="alert alert-danger">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Select;
|
||||
@ -1,18 +1,10 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
interface TabProps {
|
||||
label: string;
|
||||
children : JSX.Element | JSX.Element[];
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
class Tab extends React.Component<TabProps> {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
export default function Tab({ label, children }: TabProps): JSX.Element {
|
||||
return <div data-tab-label={label}>{children}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default Tab;
|
||||
@ -1,36 +1,25 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
interface TabHeaderProps {
|
||||
isActive : boolean,
|
||||
label : string
|
||||
onClick : any;
|
||||
isActive: boolean;
|
||||
label: string;
|
||||
onClick: (label: string) => void;
|
||||
}
|
||||
|
||||
class TabHeader extends React.Component<TabHeaderProps> {
|
||||
|
||||
onClick = () => {
|
||||
const { label, onClick } = this.props;
|
||||
onClick(label);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
export default function TabHeader({
|
||||
isActive,
|
||||
label,
|
||||
onClick,
|
||||
props: { isActive, label },
|
||||
} = this;
|
||||
}: TabHeaderProps) {
|
||||
const handleClick = useCallback(() => onClick(label), [onClick, label]);
|
||||
|
||||
let className = "tab-list-item";
|
||||
|
||||
if (isActive === true) {
|
||||
className += " tab-list-active";
|
||||
}
|
||||
const className = isActive
|
||||
? "tab-list-item tab-list-active"
|
||||
: "tab-list-item";
|
||||
|
||||
return (
|
||||
<li className={className} onClick={onClick}>
|
||||
<li className={className} onClick={handleClick}>
|
||||
{label}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TabHeader;
|
||||
@ -1,14 +1,14 @@
|
||||
import { Component } from 'react';
|
||||
import { Paginated } from '../../services/Paginated';
|
||||
import Column from './columns';
|
||||
import TableBody, { AuditParams } from './TableBody';
|
||||
import TableFooter from './TableFooter';
|
||||
import TableHeader from './TableHeader';
|
||||
import debounce from 'lodash.debounce';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Paginated } from "../../services/Paginated";
|
||||
import Column from "./columns";
|
||||
import TableBody, { AuditParams } from "./TableBody";
|
||||
import TableFooter from "./TableFooter";
|
||||
import TableHeader from "./TableHeader";
|
||||
import debounce from "lodash.debounce";
|
||||
|
||||
export interface PublishedTableProps<T> {
|
||||
data: Paginated<T>,
|
||||
sortColumn? : Column<T>,
|
||||
data: Paginated<T>;
|
||||
sortColumn?: Column<T>;
|
||||
selectedRow?: T;
|
||||
onChangePage?: (page: number, pageSize: number) => {};
|
||||
onSort?: (sortColumn: Column<T>) => void;
|
||||
@ -28,43 +28,85 @@ export interface TableProps<T> extends PublishedTableProps<T> {
|
||||
secondaryAudit?: boolean;
|
||||
}
|
||||
|
||||
interface TableState{
|
||||
debouncedOnSearch?: any
|
||||
}
|
||||
export default function Table<T>(props: TableProps<T>): JSX.Element {
|
||||
const {
|
||||
data,
|
||||
keyName,
|
||||
selectedRow,
|
||||
columns,
|
||||
sortColumn,
|
||||
editPath,
|
||||
canEdit,
|
||||
canDelete,
|
||||
onSort,
|
||||
onChangePage,
|
||||
onDelete,
|
||||
onAuditParams,
|
||||
onSelectRow,
|
||||
secondaryAudit,
|
||||
onUnselectRow,
|
||||
onSearch,
|
||||
} = props;
|
||||
|
||||
class Table<T> extends Component<TableProps<T>, TableState> {
|
||||
state : TableState = {
|
||||
}
|
||||
const [debouncedOnSearch, setDebouncedOnSearch] = useState<any>(undefined);
|
||||
|
||||
componentDidMount(): void {
|
||||
const {onSearch } = this.props;
|
||||
useEffect(() => {
|
||||
const debounceDelay = 200;
|
||||
const debouncedOnSearch = onSearch === undefined ? undefined : debounce(onSearch, debounceDelay);
|
||||
|
||||
this.setState( {
|
||||
debouncedOnSearch,
|
||||
})
|
||||
if (!onSearch) {
|
||||
setDebouncedOnSearch(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, keyName, selectedRow, columns, sortColumn, editPath, canEdit, canDelete, onSort, onChangePage, onDelete, onAuditParams, onSelectRow, secondaryAudit, onUnselectRow } = this.props;
|
||||
const { debouncedOnSearch } = this.state;
|
||||
const d = debounce(onSearch, debounceDelay);
|
||||
setDebouncedOnSearch(() => d);
|
||||
|
||||
const showEdit = (editPath != null) && (editPath !== "");
|
||||
return () => {
|
||||
if ((d as any).cancel) (d as any).cancel();
|
||||
};
|
||||
}, [onSearch]);
|
||||
|
||||
const showEdit = editPath != null && editPath !== "";
|
||||
const showDelete = onDelete != null;
|
||||
const showAudit = onAuditParams != null;
|
||||
const showSecondaryAudit = showAudit && secondaryAudit === true;
|
||||
|
||||
return (
|
||||
<>
|
||||
<table className='table table-sm table-striped table-bordered'>
|
||||
<TableHeader columns={columns} sortColumn={sortColumn} showDelete={showDelete} showEdit={showEdit} showAudit={showAudit} showSecondaryAudit={showSecondaryAudit} onSort={onSort} onSearch={debouncedOnSearch} />
|
||||
<TableBody data={data.data} keyName={keyName} columns={columns} selectedRow={selectedRow} canEdit={canEdit} canDelete={canDelete} onDelete={onDelete} editPath={editPath} onAuditParams={onAuditParams} onSelectRow={onSelectRow} showSecondaryAudit={showSecondaryAudit}/>
|
||||
<TableFooter data={data} columns={columns} showDelete={showDelete} showEdit={showEdit} showAudit={showAudit} showSecondaryAudit={showSecondaryAudit} onChangePage={onChangePage} onUnselectRow={onUnselectRow} />
|
||||
<table className="table table-sm table-striped table-bordered">
|
||||
<TableHeader
|
||||
columns={columns}
|
||||
sortColumn={sortColumn}
|
||||
showDelete={showDelete}
|
||||
showEdit={showEdit}
|
||||
showAudit={showAudit}
|
||||
showSecondaryAudit={showSecondaryAudit}
|
||||
onSort={onSort}
|
||||
onSearch={debouncedOnSearch}
|
||||
/>
|
||||
<TableBody
|
||||
data={data.data}
|
||||
keyName={keyName}
|
||||
columns={columns}
|
||||
selectedRow={selectedRow}
|
||||
canEdit={canEdit}
|
||||
canDelete={canDelete}
|
||||
onDelete={onDelete}
|
||||
editPath={editPath}
|
||||
onAuditParams={onAuditParams}
|
||||
onSelectRow={onSelectRow}
|
||||
showSecondaryAudit={showSecondaryAudit}
|
||||
/>
|
||||
<TableFooter
|
||||
data={data}
|
||||
columns={columns}
|
||||
showDelete={showDelete}
|
||||
showEdit={showEdit}
|
||||
showAudit={showAudit}
|
||||
showSecondaryAudit={showSecondaryAudit}
|
||||
onChangePage={onChangePage}
|
||||
onUnselectRow={onUnselectRow}
|
||||
/>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Table;
|
||||
@ -1,17 +1,22 @@
|
||||
import React, { Component } from "react";
|
||||
import React from "react";
|
||||
import deepFind from "../../utils/deepfind";
|
||||
import Column from "./columns";
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faBook, faBookJournalWhills, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faBook,
|
||||
faBookJournalWhills,
|
||||
faEdit,
|
||||
faTrash,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { Link } from "react-router-dom";
|
||||
import ConfirmButton from "./ConfirmButton";
|
||||
import { Buffer } from 'buffer';
|
||||
import { Buffer } from "buffer";
|
||||
import Button, { ButtonType } from "./Button";
|
||||
import { DateView } from "./DateView";
|
||||
|
||||
export interface AuditParams {
|
||||
entityName : string,
|
||||
primaryKey : string
|
||||
entityName: string;
|
||||
primaryKey: string;
|
||||
}
|
||||
|
||||
export interface TableBodyProps<T> {
|
||||
@ -28,111 +33,138 @@ export interface TableBodyProps<T>{
|
||||
showSecondaryAudit: boolean;
|
||||
}
|
||||
|
||||
class TableBody<T> extends Component<TableBodyProps<T>> {
|
||||
resolvePath = ( path : string, args : string[]) => {
|
||||
export default function TableBody<T>({
|
||||
data,
|
||||
keyName,
|
||||
selectedRow,
|
||||
columns,
|
||||
editPath,
|
||||
canEdit,
|
||||
canDelete,
|
||||
onDelete,
|
||||
onAuditParams,
|
||||
onSelectRow,
|
||||
showSecondaryAudit,
|
||||
}: TableBodyProps<T>): JSX.Element {
|
||||
const resolvePath = (path: string, args: string[]) => {
|
||||
let modifiedPath = path;
|
||||
|
||||
let index : number = 0;
|
||||
while ( index < args.length )
|
||||
{
|
||||
modifiedPath = modifiedPath.replace("{"+index+"}", args[index])
|
||||
let index = 0;
|
||||
while (index < args.length) {
|
||||
modifiedPath = modifiedPath.replace("{" + index + "}", args[index]);
|
||||
index++;
|
||||
}
|
||||
|
||||
return modifiedPath;
|
||||
}
|
||||
};
|
||||
|
||||
renderCell = (item : T, column : Column<T>) => {
|
||||
const {keyName} = this.props;
|
||||
const renderCell = (item: T, column: Column<T>) => {
|
||||
if (column.content) return column.content(item);
|
||||
|
||||
const foundItem = deepFind(item, column.path || column.key)
|
||||
const foundItem = deepFind(item, column.path || column.key);
|
||||
|
||||
let columnContent: JSX.Element;
|
||||
|
||||
if (foundItem instanceof Date) {
|
||||
columnContent = <DateView value={foundItem}/>
|
||||
}
|
||||
else if (typeof foundItem === "object"){
|
||||
columnContent = <DateView value={foundItem} />;
|
||||
} else if (typeof foundItem === "object") {
|
||||
columnContent = <></>;
|
||||
}
|
||||
else {
|
||||
columnContent = <>
|
||||
{foundItem}
|
||||
</>;
|
||||
} else {
|
||||
columnContent = <>{foundItem}</>;
|
||||
}
|
||||
|
||||
const linkPath = column.link;
|
||||
|
||||
if (linkPath !== undefined) {
|
||||
const resolvedlinkPath = this.resolvePath( linkPath, [ (item as any)[keyName] ] );
|
||||
const resolvedlinkPath = resolvePath(linkPath, [(item as any)[keyName]]);
|
||||
columnContent = <Link to={resolvedlinkPath}>{columnContent}</Link>;
|
||||
}
|
||||
|
||||
return <>
|
||||
{columnContent}
|
||||
</>;
|
||||
return <>{columnContent}</>;
|
||||
};
|
||||
|
||||
clickRow = ( value : T ) =>
|
||||
{
|
||||
const { onSelectRow } = this.props;
|
||||
|
||||
if (onSelectRow !== undefined)
|
||||
onSelectRow( value );
|
||||
}
|
||||
|
||||
createKey = (item : T, column : Column<T>) => {
|
||||
const { keyName } = this.props;
|
||||
|
||||
return (item as any)[keyName] + '_' + (column.path || column.key);
|
||||
const clickRow = (value: T) => {
|
||||
if (onSelectRow !== undefined) onSelectRow(value);
|
||||
};
|
||||
|
||||
handleAuditParams = ( item : T, primaryOnly : boolean ) => {
|
||||
const { onAuditParams } = this.props;
|
||||
const createKey = (item: T, column: Column<T>) => {
|
||||
return (item as any)[keyName] + "_" + (column.path || column.key);
|
||||
};
|
||||
|
||||
const handleAuditParams = (item: T, primaryOnly: boolean) => {
|
||||
if (onAuditParams !== undefined) {
|
||||
var auditParams = onAuditParams(item);
|
||||
let json = JSON.stringify(auditParams);
|
||||
var params = Buffer.from(json).toString('base64') ;
|
||||
const auditParams = onAuditParams(item);
|
||||
const json = JSON.stringify(auditParams);
|
||||
const params = Buffer.from(json).toString("base64");
|
||||
|
||||
var queryString = "";
|
||||
if (primaryOnly===false)
|
||||
queryString += "?primaryOnly=" + primaryOnly;
|
||||
let queryString = "";
|
||||
if (primaryOnly === false) queryString += "?primaryOnly=" + primaryOnly;
|
||||
|
||||
return "/audit/" + params + queryString;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { data, keyName, selectedRow, columns, editPath, canEdit, canDelete, onDelete, onAuditParams, showSecondaryAudit } = this.props;
|
||||
const showDelete: boolean = onDelete != null;
|
||||
const showEdit:boolean = (editPath != null) && (editPath !== "");
|
||||
const showEdit: boolean = editPath != null && editPath !== "";
|
||||
const showAudit: boolean = onAuditParams !== undefined;
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
{data?.map((item) => {
|
||||
let classNames = "";
|
||||
if (selectedRow === item)
|
||||
{
|
||||
classNames+="table-primary";
|
||||
}
|
||||
const classNames = selectedRow === item ? "table-primary" : "";
|
||||
|
||||
return (<tr className={classNames} key={(item as any)[keyName]}>
|
||||
return (
|
||||
<tr className={classNames} key={(item as any)[keyName]}>
|
||||
{columns.map((column) => (
|
||||
<td key={this.createKey(item, column)} onClick={ () => this.clickRow(item)}>{this.renderCell(item, column)}</td>
|
||||
<td key={createKey(item, column)} onClick={() => clickRow(item)}>
|
||||
{renderCell(item, column)}
|
||||
</td>
|
||||
))}
|
||||
{showEdit && <td className="align-middle">{(canEdit === undefined || canEdit(item)) && <Button buttonType={ButtonType.primary} to={this.resolvePath( editPath!, [ (item as any)[keyName] ] )}><FontAwesomeIcon icon={faEdit}/></Button>}</td>}
|
||||
{showDelete && <td className="align-middle">{(canDelete === undefined || canDelete(item)) && <ConfirmButton buttonType={ButtonType.primary} keyValue={item} onClick={onDelete} confirmMessage={"Press again to delete"} ><FontAwesomeIcon icon={faTrash}/></ConfirmButton>}</td>}
|
||||
{showAudit && <td className="align-middle"><Link to={this.handleAuditParams(item, true)}><Button buttonType={ButtonType.primary}><FontAwesomeIcon icon={faBook}/></Button></Link></td>}
|
||||
{showAudit && showSecondaryAudit && <td className="align-middle"><Link to={this.handleAuditParams(item, false)}><Button buttonType={ButtonType.secondary}><FontAwesomeIcon icon={faBookJournalWhills}/></Button></Link></td>}
|
||||
</tr>)
|
||||
{showEdit && (
|
||||
<td className="align-middle">
|
||||
{(canEdit === undefined || canEdit(item)) && (
|
||||
<Button
|
||||
buttonType={ButtonType.primary}
|
||||
to={resolvePath(editPath!, [(item as any)[keyName]])}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
{showDelete && (
|
||||
<td className="align-middle">
|
||||
{(canDelete === undefined || canDelete(item)) && (
|
||||
<ConfirmButton
|
||||
buttonType={ButtonType.primary}
|
||||
keyValue={item}
|
||||
onClick={onDelete}
|
||||
confirmMessage={"Press again to delete"}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</ConfirmButton>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
{showAudit && (
|
||||
<td className="align-middle">
|
||||
<Link to={handleAuditParams(item, true)}>
|
||||
<Button buttonType={ButtonType.primary}>
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
</Button>
|
||||
</Link>
|
||||
</td>
|
||||
)}
|
||||
{showAudit && showSecondaryAudit && (
|
||||
<td className="align-middle">
|
||||
<Link to={handleAuditParams(item, false)}>
|
||||
<Button buttonType={ButtonType.secondary}>
|
||||
<FontAwesomeIcon icon={faBookJournalWhills} />
|
||||
</Button>
|
||||
</Link>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TableBody;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { Component } from "react";
|
||||
import React from "react";
|
||||
import { Paginated } from "../../services/Paginated";
|
||||
import Column from "./columns";
|
||||
import Pagination from "./Pagination";
|
||||
@ -14,27 +14,38 @@ export interface TableFooterProps<T>{
|
||||
onUnselectRow?: () => void;
|
||||
}
|
||||
|
||||
class TableFooter<T> extends Component<TableFooterProps<T>> {
|
||||
render() {
|
||||
const { data, columns, showEdit, showDelete, showAudit, showSecondaryAudit, onChangePage, onUnselectRow} = this.props;
|
||||
|
||||
export default function TableFooter<T>({
|
||||
data,
|
||||
columns,
|
||||
showEdit,
|
||||
showDelete,
|
||||
showAudit,
|
||||
showSecondaryAudit,
|
||||
onChangePage,
|
||||
onUnselectRow,
|
||||
}: TableFooterProps<T>): JSX.Element | null {
|
||||
let staticColumnCount = 0;
|
||||
if (showEdit) staticColumnCount++;
|
||||
if (showDelete) staticColumnCount++;
|
||||
if (showAudit) staticColumnCount++;
|
||||
if (showAudit && showSecondaryAudit) staticColumnCount++;
|
||||
|
||||
let pagination = onChangePage === undefined ? undefined : <Pagination data={data} onChangePage={onChangePage} onUnselect={onUnselectRow} />;
|
||||
const pagination =
|
||||
onChangePage === undefined ? undefined : (
|
||||
<Pagination
|
||||
data={data}
|
||||
onChangePage={onChangePage}
|
||||
onUnselect={onUnselectRow}
|
||||
/>
|
||||
);
|
||||
|
||||
if (pagination)
|
||||
return <tfoot>
|
||||
if (!pagination) return null;
|
||||
|
||||
return (
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={columns.length + staticColumnCount}>{pagination}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
|
||||
return <></>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TableFooter;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { ChangeEvent, useCallback } from "react";
|
||||
import { faSortAsc, faSortDesc } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ChangeEvent, Component } from "react";
|
||||
import Column from "./columns";
|
||||
import Input, { InputType } from "./Input";
|
||||
|
||||
@ -15,80 +15,95 @@ export interface TableHeaderProps<T>{
|
||||
onSearch?: (name: string, value: string) => void;
|
||||
}
|
||||
|
||||
class TableHeader<T> extends Component<TableHeaderProps<T>> {
|
||||
columnsMatch = ( left? : Column<T>, right? : Column<T>) =>
|
||||
{
|
||||
if (left?.key !== right?.key) return false;
|
||||
return true;
|
||||
export default function TableHeader<T>({
|
||||
sortColumn,
|
||||
columns,
|
||||
showEdit,
|
||||
showDelete,
|
||||
showAudit,
|
||||
showSecondaryAudit,
|
||||
onSort,
|
||||
onSearch,
|
||||
}: TableHeaderProps<T>): JSX.Element {
|
||||
const columnsMatch = useCallback((left?: Column<T>, right?: Column<T>) => {
|
||||
return left?.key === right?.key;
|
||||
}, []);
|
||||
|
||||
const raiseSort = useCallback(
|
||||
(column: Column<T>) => {
|
||||
let sc = sortColumn;
|
||||
|
||||
if (sc) {
|
||||
if (columnsMatch(column, sc)) {
|
||||
sc.order = sc.order === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
sc = column;
|
||||
sc.order = "asc";
|
||||
}
|
||||
|
||||
raiseSort = (column : Column<T>) => {
|
||||
let sortColumn = this.props.sortColumn;
|
||||
|
||||
if (sortColumn) {
|
||||
if (this.columnsMatch(column, sortColumn)){
|
||||
sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc";
|
||||
}
|
||||
else {
|
||||
sortColumn = column;
|
||||
sortColumn.order = "asc";
|
||||
if (onSort) onSort(sc);
|
||||
}
|
||||
},
|
||||
[sortColumn, onSort, columnsMatch],
|
||||
);
|
||||
|
||||
const { onSort } = this.props;
|
||||
|
||||
if (onSort)
|
||||
onSort(sortColumn);
|
||||
}
|
||||
};
|
||||
|
||||
renderSortIcon = (column : Column<T>) => {
|
||||
const { sortColumn } = this.props;
|
||||
|
||||
const renderSortIcon = useCallback(
|
||||
(column: Column<T>) => {
|
||||
if (!sortColumn) return null;
|
||||
if (!columnsMatch(column, sortColumn)) return null;
|
||||
return sortColumn.order === "asc" ? (
|
||||
<FontAwesomeIcon icon={faSortAsc} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faSortDesc} />
|
||||
);
|
||||
},
|
||||
[sortColumn, columnsMatch],
|
||||
);
|
||||
|
||||
if (!this.columnsMatch(column, sortColumn)) return null;
|
||||
const changeSearch = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (onSearch) onSearch(e.target.name, e.target.value);
|
||||
},
|
||||
[onSearch],
|
||||
);
|
||||
|
||||
if (sortColumn?.order === "asc") return <FontAwesomeIcon icon={faSortAsc}/>
|
||||
|
||||
return <FontAwesomeIcon icon={faSortDesc}/>
|
||||
};
|
||||
|
||||
changeSearch = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
{
|
||||
const {onSearch} = this.props;
|
||||
|
||||
if (onSearch)
|
||||
onSearch(e?.target?.name, e?.target?.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { columns, showEdit, showDelete, showAudit, showSecondaryAudit, onSearch } = this.props;
|
||||
|
||||
let searchRow = <></>;
|
||||
if (onSearch)
|
||||
searchRow = <tr>
|
||||
{columns.map((column) =>
|
||||
const searchRow = onSearch ? (
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
<th key={column.path || column.key}>
|
||||
{
|
||||
(column.searchable === undefined || column.searchable === true ) &&
|
||||
<Input name={column.path || column.key} label={""} error={""} type={InputType.text} onChange={this.changeSearch} />
|
||||
}
|
||||
</th>
|
||||
{(column.searchable === undefined || column.searchable === true) && (
|
||||
<Input
|
||||
name={column.path || column.key}
|
||||
label={""}
|
||||
error={""}
|
||||
type={InputType.text}
|
||||
onChange={changeSearch}
|
||||
/>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
{showEdit && <th></th>}
|
||||
{showDelete && <th></th>}
|
||||
{showAudit && <th></th>}
|
||||
{showAudit && showSecondaryAudit && <th></th>}
|
||||
</tr>;
|
||||
</tr>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((column) =>
|
||||
<th className="text-nowrap" key={column.path || column.key} scope="col" onClick={() => this.raiseSort(column)}>
|
||||
{column.label} {this.renderSortIcon(column)}
|
||||
{columns.map((column) => (
|
||||
<th
|
||||
className="text-nowrap"
|
||||
key={column.path || column.key}
|
||||
scope="col"
|
||||
onClick={() => raiseSort(column)}
|
||||
>
|
||||
{column.label} {renderSortIcon(column)}
|
||||
</th>
|
||||
)}
|
||||
))}
|
||||
{showEdit && <th scope="col"></th>}
|
||||
{showDelete && <th scope="col"></th>}
|
||||
{showAudit && <th scope="col"></th>}
|
||||
@ -98,6 +113,3 @@ class TableHeader<T> extends Component<TableHeaderProps<T>> {
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TableHeader;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import TextEditor from "./ckeditor/TextEditor";
|
||||
import customFieldsService from '../../modules/manager/customfields/services/customFieldsService';
|
||||
import customFieldsService from "../../modules/manager/customfields/services/customFieldsService";
|
||||
|
||||
interface customfieldType {
|
||||
interface CustomFieldType {
|
||||
type: string;
|
||||
name: string;
|
||||
guid: string | undefined;
|
||||
@ -10,57 +10,58 @@ interface customfieldType {
|
||||
}
|
||||
|
||||
interface TemplateEditorProps {
|
||||
className : string
|
||||
className: string;
|
||||
name: string;
|
||||
data: string;
|
||||
showFields: boolean;
|
||||
onChange: (name: string, value: string) => void;
|
||||
}
|
||||
|
||||
interface TemplateEditorState {
|
||||
customfields: Array<customfieldType>;
|
||||
ready : boolean;
|
||||
}
|
||||
export default function TemplateEditor({
|
||||
className,
|
||||
name,
|
||||
data,
|
||||
showFields,
|
||||
onChange,
|
||||
}: TemplateEditorProps) {
|
||||
const [customfields, setCustomfields] = useState<CustomFieldType[]>([]);
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
class TemplateEditor extends React.Component<TemplateEditorProps, TemplateEditorState> {
|
||||
state = {
|
||||
customfields : [],
|
||||
ready: false
|
||||
}
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
async componentWillMount() {
|
||||
async function load() {
|
||||
const pageData = await customFieldsService.getFields(0, 10, "name", true);
|
||||
|
||||
const customfields = pageData.data.map(
|
||||
(x) => {
|
||||
return {
|
||||
const fields: CustomFieldType[] = pageData.data.map((x: any) => ({
|
||||
type: "CustomField",
|
||||
name: x.name,
|
||||
guid: x.guid,
|
||||
id: x.id
|
||||
id: x.id,
|
||||
}));
|
||||
|
||||
if (mounted) {
|
||||
setCustomfields(fields);
|
||||
setReady(true);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!ready) return <></>;
|
||||
|
||||
return (
|
||||
<TextEditor
|
||||
className={className}
|
||||
name={name}
|
||||
data={data}
|
||||
onChange={onChange}
|
||||
customFields={customfields}
|
||||
/>
|
||||
);
|
||||
|
||||
const { className, name, data, onChange } = this.props;
|
||||
|
||||
this.setState( {
|
||||
ready : true,
|
||||
customfields,
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, name, data, onChange } = this.props;
|
||||
const { ready, customfields } = this.state;
|
||||
|
||||
if (!ready)
|
||||
return <></>
|
||||
|
||||
return <TextEditor className={className} name={name} data={data} onChange={onChange} customFields={customfields} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default TemplateEditor;
|
||||
@ -1,7 +1,10 @@
|
||||
import React from "react";
|
||||
import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef";
|
||||
import formsService, { CreateFormInstance, EditFormInstance } from "../../modules/manager/forms/services/formsService";
|
||||
import parse, { HTMLReactParserOptions, domToReact } from 'html-react-parser';
|
||||
import formsService, {
|
||||
CreateFormInstance,
|
||||
EditFormInstance,
|
||||
} from "../../modules/manager/forms/services/formsService";
|
||||
import parse, { HTMLReactParserOptions, domToReact } from "html-react-parser";
|
||||
import { CustomField } from "../../modules/manager/customfields/services/customFieldsService";
|
||||
import Form, { FormState } from "./Form";
|
||||
import { toast } from "react-toastify";
|
||||
@ -14,16 +17,20 @@ interface TemplateFillerProps {
|
||||
}
|
||||
|
||||
interface TemplateFillerState extends FormState {
|
||||
customFields? : CustomField[],
|
||||
customFields?: CustomField[];
|
||||
template: {
|
||||
name?: string;
|
||||
templateId? : GeneralIdRef,
|
||||
version? : bigint,
|
||||
templateId?: GeneralIdRef;
|
||||
version?: bigint;
|
||||
definition?: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class TemplateFiller extends Form<TemplateFillerProps, any, TemplateFillerState> {
|
||||
class TemplateFiller extends Form<
|
||||
TemplateFillerProps,
|
||||
any,
|
||||
TemplateFillerState
|
||||
> {
|
||||
state: TemplateFillerState = {
|
||||
loaded: false,
|
||||
customFields: undefined,
|
||||
@ -34,30 +41,34 @@ class TemplateFiller extends Form<TemplateFillerProps, any, TemplateFillerState>
|
||||
definition: undefined,
|
||||
},
|
||||
data: {},
|
||||
errors: {}
|
||||
}
|
||||
|
||||
schema = {
|
||||
errors: {},
|
||||
};
|
||||
|
||||
schema = {};
|
||||
|
||||
componentDidMount(): void {
|
||||
this.loadTemplate();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<TemplateFillerProps>, prevState: Readonly<TemplateFillerState>, snapshot?: any): void {
|
||||
if ((prevProps.formInstanceId !== this.props.formInstanceId) || (prevProps.templateId !== this.props.templateId))
|
||||
componentDidUpdate(
|
||||
prevProps: Readonly<TemplateFillerProps>,
|
||||
prevState: Readonly<TemplateFillerState>,
|
||||
snapshot?: any,
|
||||
): void {
|
||||
if (
|
||||
prevProps.formInstanceId !== this.props.formInstanceId ||
|
||||
prevProps.templateId !== this.props.templateId
|
||||
)
|
||||
this.loadTemplate();
|
||||
|
||||
if (this.props.onValidationChanged)
|
||||
{
|
||||
const prevErrorCount = Object.keys(prevState.errors).length > 0
|
||||
const errorCount = Object.keys(this.state.errors).length > 0
|
||||
if (this.props.onValidationChanged) {
|
||||
const prevErrorCount = Object.keys(prevState.errors).length > 0;
|
||||
const errorCount = Object.keys(this.state.errors).length > 0;
|
||||
|
||||
if (prevErrorCount !== errorCount) {
|
||||
this.props.onValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
loadTemplate = async () => {
|
||||
@ -72,7 +83,10 @@ class TemplateFiller extends Form<TemplateFillerProps, any, TemplateFillerState>
|
||||
loadedData.customFieldValues = undefined;
|
||||
loadedData.updatedVersion = undefined;
|
||||
} else if (formInstanceId !== undefined) {
|
||||
loadedData = await formsService.getFormInstance(formInstanceId?.id, formInstanceId?.guid);
|
||||
loadedData = await formsService.getFormInstance(
|
||||
formInstanceId?.id,
|
||||
formInstanceId?.guid,
|
||||
);
|
||||
console.log("formInstanceId", loadedData);
|
||||
} else {
|
||||
loadedData = {
|
||||
@ -84,8 +98,8 @@ class TemplateFiller extends Form<TemplateFillerProps, any, TemplateFillerState>
|
||||
customFieldDefinitions: undefined,
|
||||
templateId: undefined,
|
||||
customFieldValues: undefined,
|
||||
updatedVersion : undefined
|
||||
}
|
||||
updatedVersion: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const { template, data } = this.state;
|
||||
@ -99,66 +113,75 @@ class TemplateFiller extends Form<TemplateFillerProps, any, TemplateFillerState>
|
||||
this.setCustomFieldValues(data, loadedData.customFieldValues, customFields);
|
||||
|
||||
this.setState({ loaded: true, template, customFields, data });
|
||||
}
|
||||
};
|
||||
|
||||
parseDefinition = ( definition: string, customFieldDefinitions: CustomField[]) => {
|
||||
parseDefinition = (
|
||||
definition: string,
|
||||
customFieldDefinitions: CustomField[],
|
||||
) => {
|
||||
const options: HTMLReactParserOptions = {
|
||||
replace: (domNode) => {
|
||||
const domNodeAsAny: any = domNode;
|
||||
if (domNodeAsAny.name === "span") {
|
||||
if (domNodeAsAny.attribs.fieldtype === "CustomField")
|
||||
{
|
||||
const customField = customFieldDefinitions.filter( x => x.guid === domNodeAsAny.attribs.guid)[0];
|
||||
if (domNodeAsAny.attribs.fieldtype === "CustomField") {
|
||||
const customField = customFieldDefinitions.filter(
|
||||
(x) => x.guid === domNodeAsAny.attribs.guid,
|
||||
)[0];
|
||||
return this.renderCustomField(customField, false);
|
||||
}
|
||||
} else if (domNodeAsAny.name === "p") {
|
||||
return (
|
||||
<div className="p">
|
||||
{domToReact(domNodeAsAny.children, options)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else if (domNodeAsAny.name === "p"){
|
||||
return <div className="p">{domToReact(domNodeAsAny.children, options)}</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return parse(definition, options);
|
||||
}
|
||||
};
|
||||
|
||||
hasValidationErrors = (): boolean => {
|
||||
const { errors } = this.state;
|
||||
|
||||
const result = Object.keys(errors).length > 0;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
async Save() {
|
||||
const { errors } = this.state;
|
||||
const { templateId, version } = this.state.template;
|
||||
const { formInstanceId } = this.props;
|
||||
|
||||
if ( Object.keys(errors).length > 0 )
|
||||
{
|
||||
if (Object.keys(errors).length > 0) {
|
||||
toast.error("There are errors on the form");
|
||||
throw new Error("There are errors on the form");
|
||||
}
|
||||
|
||||
const customFieldValues = this.CustomFieldValues();
|
||||
if (formInstanceId !== undefined) {
|
||||
if (templateId === undefined)
|
||||
throw Error("TemplateId cannot be null");
|
||||
if (templateId === undefined) throw Error("TemplateId cannot be null");
|
||||
|
||||
if (version === undefined)
|
||||
throw Error("Version cannot be null");
|
||||
if (version === undefined) throw Error("Version cannot be null");
|
||||
|
||||
const editFormInstance : EditFormInstance = { formInstanceId, templateId, version, customFieldValues };
|
||||
const editFormInstance: EditFormInstance = {
|
||||
formInstanceId,
|
||||
templateId,
|
||||
version,
|
||||
customFieldValues,
|
||||
};
|
||||
await formsService.putFormInstance(editFormInstance);
|
||||
}
|
||||
else {
|
||||
if (templateId !== undefined && version !== undefined)
|
||||
{
|
||||
} else {
|
||||
if (templateId !== undefined && version !== undefined) {
|
||||
//const customFieldValues = this.CustomFieldValues();
|
||||
const formInstance : CreateFormInstance = { templateId, version, customFieldValues };
|
||||
const formInstance: CreateFormInstance = {
|
||||
templateId,
|
||||
version,
|
||||
customFieldValues,
|
||||
};
|
||||
return await formsService.postFormInstance(formInstance);
|
||||
}
|
||||
else
|
||||
throw new Error("template unknown");
|
||||
} else throw new Error("template unknown");
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,15 +190,15 @@ class TemplateFiller extends Form<TemplateFillerProps, any, TemplateFillerState>
|
||||
|
||||
let parsedDefinition: any;
|
||||
if (template.definition)
|
||||
parsedDefinition = this.parseDefinition(template.definition, customFields!);
|
||||
else
|
||||
parsedDefinition = <></>;
|
||||
parsedDefinition = this.parseDefinition(
|
||||
template.definition,
|
||||
customFields!,
|
||||
);
|
||||
else parsedDefinition = <></>;
|
||||
|
||||
return (
|
||||
<Loading loaded={loaded}>
|
||||
<div className="ck-content form-editor">
|
||||
{parsedDefinition}
|
||||
</div>
|
||||
<div className="ck-content form-editor">{parsedDefinition}</div>
|
||||
</Loading>
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,10 +9,14 @@ export interface ToggleSliderProps extends ToggleProps {
|
||||
defaultChecked: boolean;
|
||||
}
|
||||
|
||||
class ToggleSlider extends React.Component<ToggleSliderProps> {
|
||||
render() {
|
||||
const { name, label, error, readOnly, defaultChecked, ...rest } = this.props;
|
||||
|
||||
export default function ToggleSlider({
|
||||
name,
|
||||
label,
|
||||
error,
|
||||
readOnly,
|
||||
defaultChecked,
|
||||
...rest
|
||||
}: ToggleSliderProps) {
|
||||
return (
|
||||
<div className="form-group">
|
||||
<label htmlFor={name}>{label}</label>
|
||||
@ -21,6 +25,3 @@ class ToggleSlider extends React.Component<ToggleSliderProps> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ToggleSlider;
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import Select from "../common/Select";
|
||||
import Option from "../common/option";
|
||||
import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef";
|
||||
import customFieldsService, { CustomField } from "../../modules/manager/customfields/services/customFieldsService";
|
||||
import customFieldsService, {
|
||||
CustomField,
|
||||
} from "../../modules/manager/customfields/services/customFieldsService";
|
||||
|
||||
interface CustomFieldPickerProps {
|
||||
name: string;
|
||||
@ -13,82 +15,83 @@ interface CustomFieldPickerProps {
|
||||
onChange?: (name: string, id: GeneralIdRef, displayValue: string) => void;
|
||||
}
|
||||
|
||||
interface CustomFieldPickerState {
|
||||
options?: Option[];
|
||||
}
|
||||
export default function CustomFieldPicker({
|
||||
name,
|
||||
label,
|
||||
error,
|
||||
value,
|
||||
exclude,
|
||||
onChange,
|
||||
}: CustomFieldPickerProps) {
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
|
||||
class CustomFieldPicker extends React.Component<CustomFieldPickerProps, CustomFieldPickerState> {
|
||||
state = { options: [] as Option[] };
|
||||
|
||||
async componentDidMount() {
|
||||
const pagedData = await customFieldsService.getFields(0, 10, "name", true);
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const pagedData = await customFieldsService.getFields(
|
||||
0,
|
||||
10,
|
||||
"name",
|
||||
true,
|
||||
);
|
||||
if (pagedData) {
|
||||
const options: Option[] = (pagedData.data as any[]).map((x: { id: any; name: any }) => {
|
||||
return {
|
||||
const opts: Option[] = (pagedData.data as any[]).map(
|
||||
(x: { id: any; name: any }) => ({
|
||||
_id: x.id,
|
||||
name: x.name,
|
||||
};
|
||||
});
|
||||
|
||||
this.setState({ options });
|
||||
}),
|
||||
);
|
||||
setOptions(opts);
|
||||
}
|
||||
}
|
||||
|
||||
GetOptionById = (value: string ) => {
|
||||
const { options } = this.state;
|
||||
load();
|
||||
}, []);
|
||||
|
||||
for( var option of options)
|
||||
{
|
||||
if (String(option._id) === value)
|
||||
return option.name;
|
||||
const getOptionById = useCallback(
|
||||
(val: string) => {
|
||||
for (const option of options) {
|
||||
if (String(option._id) === val) return option.name;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
[options],
|
||||
);
|
||||
|
||||
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { onChange } = this.props;
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const input = e.currentTarget;
|
||||
|
||||
const generalIdRef = MakeGeneralIdRef(BigInt(input.value));
|
||||
const displayValue = this.GetOptionById(input.value);
|
||||
const displayValue = getOptionById(input.value);
|
||||
|
||||
if (onChange) onChange(input.name, generalIdRef, displayValue);
|
||||
};
|
||||
|
||||
getFilteredOptions = () => {
|
||||
const { exclude } = this.props;
|
||||
const { options } = this.state;
|
||||
|
||||
let filteredOptions = options.filter( (o) => {
|
||||
|
||||
if (exclude)
|
||||
{
|
||||
for( var exlcudedItem of exclude)
|
||||
{
|
||||
const idAsString : string = String(exlcudedItem.id);
|
||||
const oid : string = String(o._id);
|
||||
if (oid === idAsString) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
[onChange, getOptionById],
|
||||
);
|
||||
|
||||
return filteredOptions;
|
||||
const getFilteredOptions = useCallback(() => {
|
||||
return options.filter((o) => {
|
||||
if (exclude) {
|
||||
for (const excludedItem of exclude) {
|
||||
const idAsString: string = String(excludedItem.id);
|
||||
const oid: string = String(o._id);
|
||||
if (oid === idAsString) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [options, exclude]);
|
||||
|
||||
render() {
|
||||
const { name, label, error, value } = this.props;
|
||||
|
||||
const filteredOptions = this.getFilteredOptions();
|
||||
const filteredOptions = getFilteredOptions();
|
||||
|
||||
return (
|
||||
<Select name={name} label={label} error={error} value={value?.id} options={filteredOptions} includeBlankFirstEntry={true} onChange={this.handleChange} />
|
||||
<Select
|
||||
name={name}
|
||||
label={label}
|
||||
error={error}
|
||||
value={value?.id}
|
||||
options={filteredOptions}
|
||||
includeBlankFirstEntry={true}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomFieldPicker;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import Select from "../common/Select";
|
||||
import Option from "../common/option";
|
||||
import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef";
|
||||
@ -17,101 +17,95 @@ interface DomainPickerProps {
|
||||
onChange?: (name: string, value: CustomFieldValue[]) => void;
|
||||
}
|
||||
|
||||
interface DomainPickerState {
|
||||
options?: Option[];
|
||||
selectedOptions: Option[];
|
||||
}
|
||||
export default function DomainPicker({
|
||||
includeLabel,
|
||||
name,
|
||||
label,
|
||||
error,
|
||||
values,
|
||||
minEntries,
|
||||
maxEntries,
|
||||
onChange,
|
||||
}: DomainPickerProps) {
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
const [selectedOptions, setSelectedOptions] = useState<Option[]>([]);
|
||||
|
||||
class DomainPicker extends React.Component<DomainPickerProps, DomainPickerState> {
|
||||
state = { options: [] as Option[], selectedOptions: [] as Option[] };
|
||||
|
||||
async componentDidMount() {
|
||||
const { values } = this.props;
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const pagedData = await domainsService.getDomains(0, 10, "name", true);
|
||||
|
||||
if (pagedData) {
|
||||
const options: Option[] | undefined = pagedData.data.map((x: { id: any; name: any }) => {
|
||||
return {
|
||||
const opts: Option[] = pagedData.data.map(
|
||||
(x: { id: any; name: any }) => ({
|
||||
_id: x.id,
|
||||
name: x.name,
|
||||
};
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const selectedOptions: Option[] = [];
|
||||
const selected: Option[] = [];
|
||||
|
||||
if (values) {
|
||||
for (const option of values) {
|
||||
const foundOption = options.filter((x) => Number(x._id) === Number((option.value as GeneralIdRef).id))[0];
|
||||
selectedOptions.push(foundOption);
|
||||
const foundOption = opts.filter(
|
||||
(x) =>
|
||||
Number(x._id) === Number((option.value as GeneralIdRef).id),
|
||||
)[0];
|
||||
if (foundOption) selected.push(foundOption);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ options, selectedOptions });
|
||||
setOptions(opts);
|
||||
setSelectedOptions(selected);
|
||||
}
|
||||
}
|
||||
|
||||
doOnChange = (newSelectedOptions: Option[]) => {
|
||||
const { onChange } = this.props;
|
||||
load();
|
||||
}, [values]);
|
||||
|
||||
const { name } = this.props;
|
||||
|
||||
var values: CustomFieldValue[] = newSelectedOptions.map((x) => {
|
||||
return {
|
||||
const doOnChange = useCallback(
|
||||
(newSelectedOptions: Option[]) => {
|
||||
const vals: CustomFieldValue[] = newSelectedOptions.map((x) => ({
|
||||
value: MakeGeneralIdRef(x._id as unknown as bigint),
|
||||
displayValue: x.name,
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
if (onChange) onChange(name, values);
|
||||
};
|
||||
if (onChange) onChange(name, vals);
|
||||
},
|
||||
[onChange, name],
|
||||
);
|
||||
|
||||
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
let { options, selectedOptions } = this.state;
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const input = e.currentTarget;
|
||||
|
||||
const id: number = Number(input.value);
|
||||
|
||||
selectedOptions = options.filter((x) => x._id === id);
|
||||
const newSelected = options.filter((x) => x._id === id);
|
||||
setSelectedOptions(newSelected);
|
||||
doOnChange(newSelected);
|
||||
},
|
||||
[options, doOnChange],
|
||||
);
|
||||
|
||||
this.setState({ selectedOptions });
|
||||
this.doOnChange(selectedOptions);
|
||||
};
|
||||
const handleAdd = useCallback(
|
||||
(item: Option) => {
|
||||
const newSelected = [...selectedOptions, item];
|
||||
setSelectedOptions(newSelected);
|
||||
doOnChange(newSelected);
|
||||
},
|
||||
[selectedOptions, doOnChange],
|
||||
);
|
||||
|
||||
handleAdd = (item: Option) => {
|
||||
const { selectedOptions } = this.state;
|
||||
const handleDelete = useCallback(
|
||||
(item: Option) => {
|
||||
const newSelected = selectedOptions.filter((x) => x !== item);
|
||||
setSelectedOptions(newSelected);
|
||||
doOnChange(newSelected);
|
||||
},
|
||||
[selectedOptions, doOnChange],
|
||||
);
|
||||
|
||||
selectedOptions.push(item);
|
||||
|
||||
this.setState({ selectedOptions });
|
||||
this.doOnChange(selectedOptions);
|
||||
};
|
||||
|
||||
handleDelete = (item: Option) => {
|
||||
let { selectedOptions } = this.state;
|
||||
|
||||
selectedOptions = selectedOptions.filter((x) => x !== item);
|
||||
|
||||
this.setState({ selectedOptions });
|
||||
this.doOnChange(selectedOptions);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { includeLabel, name, label, error, values, maxEntries } = this.props;
|
||||
const { options, selectedOptions } = this.state;
|
||||
|
||||
// return (
|
||||
// <Select
|
||||
// name={name}
|
||||
// label={label}
|
||||
// error={error}
|
||||
// value={String(value?.id)}
|
||||
// options={options}
|
||||
// includeBlankFirstEntry={false}
|
||||
// onChange={this.handleChange}
|
||||
// />
|
||||
// );
|
||||
if (maxEntries == 1) {
|
||||
let value = selectedOptions[0]?._id;
|
||||
if (maxEntries === 1) {
|
||||
const value = selectedOptions[0]?._id;
|
||||
return (
|
||||
<Select
|
||||
includeLabel={includeLabel}
|
||||
@ -121,7 +115,7 @@ class DomainPicker extends React.Component<DomainPickerProps, DomainPickerState>
|
||||
value={value}
|
||||
options={options}
|
||||
includeBlankFirstEntry={true}
|
||||
onChange={this.handleChange}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@ -133,12 +127,9 @@ class DomainPicker extends React.Component<DomainPickerProps, DomainPickerState>
|
||||
error={error}
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
onAdd={this.handleAdd}
|
||||
onDelete={this.handleDelete}
|
||||
></MultiSelect>
|
||||
onAdd={handleAdd}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DomainPicker;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import Select from "../common/Select";
|
||||
import Option from "../common/option";
|
||||
import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef";
|
||||
@ -13,52 +13,62 @@ interface FormTemplatePickerProps {
|
||||
onChange?: (name: string, value: GeneralIdRef) => void;
|
||||
}
|
||||
|
||||
interface FormTemplatePickerState {
|
||||
options?: Option[];
|
||||
}
|
||||
export default function FormTemplatePicker({
|
||||
includeLabel,
|
||||
name,
|
||||
label,
|
||||
error,
|
||||
value,
|
||||
onChange,
|
||||
}: FormTemplatePickerProps) {
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
|
||||
class FormTemplatePicker extends React.Component<FormTemplatePickerProps, FormTemplatePickerState> {
|
||||
state = { options: [] as Option[] };
|
||||
|
||||
async componentDidMount() {
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const formTemplates = await formsService.getForms(0, 10, "name", true);
|
||||
if (formTemplates) {
|
||||
const options: Option[] | undefined = (formTemplates.data as any[]).map((x) => {
|
||||
return {
|
||||
const opts: Option[] = (formTemplates.data as any[]).map((x) => ({
|
||||
_id: x.id,
|
||||
name: x.name,
|
||||
};
|
||||
});
|
||||
|
||||
this.setState({ options });
|
||||
}));
|
||||
setOptions(opts);
|
||||
}
|
||||
}
|
||||
|
||||
doOnChange = (name: string, value: bigint) => {
|
||||
const { onChange } = this.props;
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const generalIdRef = MakeGeneralIdRef(value);
|
||||
if (onChange) onChange(name, generalIdRef);
|
||||
};
|
||||
const doOnChange = useCallback(
|
||||
(n: string, v: bigint) => {
|
||||
const generalIdRef = MakeGeneralIdRef(v);
|
||||
if (onChange) onChange(n, generalIdRef);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const input = e.currentTarget;
|
||||
this.doOnChange(input.name, BigInt(input.value));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { includeLabel, name, label, error, value } = this.props;
|
||||
const { options } = this.state;
|
||||
doOnChange(input.name, BigInt(input.value));
|
||||
},
|
||||
[doOnChange],
|
||||
);
|
||||
|
||||
let id = "";
|
||||
if (!((value === undefined || Number.isNaN(value.id) ))) {
|
||||
if (value !== undefined && !Number.isNaN(value.id)) {
|
||||
id = String(value.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<Select includeLabel={includeLabel} name={name} label={label} error={error} value={id} options={options} includeBlankFirstEntry={true} onChange={this.handleChange} />
|
||||
<Select
|
||||
includeLabel={includeLabel}
|
||||
name={name}
|
||||
label={label}
|
||||
error={error}
|
||||
value={id}
|
||||
options={options}
|
||||
includeBlankFirstEntry={true}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FormTemplatePicker;
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import Select from "../common/Select";
|
||||
import Option from "../common/option";
|
||||
import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef";
|
||||
import glossariesService, { CustomFieldValue, SystemGlossaries } from "../../modules/manager/glossary/services/glossaryService";
|
||||
import glossariesService, {
|
||||
CustomFieldValue,
|
||||
SystemGlossaries,
|
||||
} from "../../modules/manager/glossary/services/glossaryService";
|
||||
import MultiSelect from "../common/MultiSelect";
|
||||
|
||||
interface GlossaryPickerProps {
|
||||
@ -16,94 +19,96 @@ interface GlossaryPickerProps {
|
||||
onChange?: (name: string, values: CustomFieldValue[]) => void;
|
||||
}
|
||||
|
||||
interface GlossaryPickerState {
|
||||
options?: Option[];
|
||||
selectedOptions: Option[];
|
||||
}
|
||||
export default function GlossaryPicker({
|
||||
includeLabel,
|
||||
name,
|
||||
label,
|
||||
rootItem,
|
||||
error,
|
||||
values,
|
||||
maxEntries,
|
||||
onChange,
|
||||
}: GlossaryPickerProps) {
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
const [selectedOptions, setSelectedOptions] = useState<Option[]>([]);
|
||||
|
||||
class GlossaryPicker extends React.Component<GlossaryPickerProps, GlossaryPickerState> {
|
||||
state = {
|
||||
options: [] as Option[],
|
||||
selectedOptions: [] as Option[],
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
const { rootItem, values } = this.props;
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const actualRootItem = rootItem ?? SystemGlossaries;
|
||||
|
||||
const glossary = await glossariesService.getGlossaryItem(actualRootItem);
|
||||
|
||||
if (glossary) {
|
||||
const options: Option[] | undefined = glossary.children.map((x: { id: any; name: any }) => {
|
||||
return {
|
||||
const opts: Option[] = glossary.children.map(
|
||||
(x: { id: any; name: any }) => ({
|
||||
_id: x.id,
|
||||
name: x.name,
|
||||
};
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const selectedOptions: Option[] = [];
|
||||
const selected: Option[] = [];
|
||||
|
||||
if (values) {
|
||||
for (const option of values) {
|
||||
const foundOption = options.filter((x) => Number(x._id) === Number((option.value as GeneralIdRef).id))[0];
|
||||
selectedOptions.push(foundOption);
|
||||
const foundOption = opts.filter(
|
||||
(x) =>
|
||||
Number(x._id) === Number((option.value as GeneralIdRef).id),
|
||||
)[0];
|
||||
if (foundOption) selected.push(foundOption);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ options, selectedOptions });
|
||||
setOptions(opts);
|
||||
setSelectedOptions(selected);
|
||||
}
|
||||
}
|
||||
|
||||
doOnChange = (newSelectedOptions: Option[]) => {
|
||||
const { onChange } = this.props;
|
||||
load();
|
||||
}, [rootItem, values]);
|
||||
|
||||
const { name } = this.props;
|
||||
|
||||
var values: CustomFieldValue[] = newSelectedOptions.map((x) => {
|
||||
return {
|
||||
const doOnChange = useCallback(
|
||||
(newSelectedOptions: Option[]) => {
|
||||
const vals: CustomFieldValue[] = newSelectedOptions.map((x) => ({
|
||||
value: MakeGeneralIdRef(x._id as unknown as bigint),
|
||||
displayValue: x.name,
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
if (onChange) onChange(name, values);
|
||||
};
|
||||
if (onChange) onChange(name, vals);
|
||||
},
|
||||
[onChange, name],
|
||||
);
|
||||
|
||||
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
let { options, selectedOptions } = this.state;
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const input = e.currentTarget;
|
||||
|
||||
const id: number = Number(input.value);
|
||||
|
||||
selectedOptions = options.filter((x) => x._id === id);
|
||||
const newSelected = options.filter((x) => x._id === id);
|
||||
setSelectedOptions(newSelected);
|
||||
doOnChange(newSelected);
|
||||
},
|
||||
[options, doOnChange],
|
||||
);
|
||||
|
||||
this.setState({ selectedOptions });
|
||||
this.doOnChange(selectedOptions);
|
||||
};
|
||||
const handleAdd = useCallback(
|
||||
(item: Option) => {
|
||||
const newSelected = [...selectedOptions, item];
|
||||
setSelectedOptions(newSelected);
|
||||
doOnChange(newSelected);
|
||||
},
|
||||
[selectedOptions, doOnChange],
|
||||
);
|
||||
|
||||
handleAdd = (item: Option) => {
|
||||
const { selectedOptions } = this.state;
|
||||
const handleDelete = useCallback(
|
||||
(item: Option) => {
|
||||
const newSelected = selectedOptions.filter((x) => x !== item);
|
||||
setSelectedOptions(newSelected);
|
||||
doOnChange(newSelected);
|
||||
},
|
||||
[selectedOptions, doOnChange],
|
||||
);
|
||||
|
||||
selectedOptions.push(item);
|
||||
|
||||
this.setState({ selectedOptions });
|
||||
this.doOnChange(selectedOptions);
|
||||
};
|
||||
|
||||
handleDelete = (item: Option) => {
|
||||
let { selectedOptions } = this.state;
|
||||
|
||||
selectedOptions = selectedOptions.filter((x) => x !== item);
|
||||
|
||||
this.setState({ selectedOptions });
|
||||
this.doOnChange(selectedOptions);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { includeLabel, name, label, error, maxEntries } = this.props;
|
||||
const { options, selectedOptions } = this.state;
|
||||
|
||||
if (maxEntries == 1) {
|
||||
let value = selectedOptions[0]?._id;
|
||||
if (maxEntries === 1) {
|
||||
const value = selectedOptions[0]?._id;
|
||||
return (
|
||||
<Select
|
||||
includeLabel={includeLabel}
|
||||
@ -113,7 +118,7 @@ class GlossaryPicker extends React.Component<GlossaryPickerProps, GlossaryPicker
|
||||
value={value}
|
||||
options={options}
|
||||
includeBlankFirstEntry={true}
|
||||
onChange={this.handleChange}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@ -125,12 +130,9 @@ class GlossaryPicker extends React.Component<GlossaryPickerProps, GlossaryPicker
|
||||
error={error}
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
onAdd={this.handleAdd}
|
||||
onDelete={this.handleDelete}
|
||||
></MultiSelect>
|
||||
onAdd={handleAdd}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default GlossaryPicker;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import sequenceService from "../../modules/manager/sequence/services/sequenceService";
|
||||
import Select from "./../common/Select";
|
||||
import Option from "../common/option";
|
||||
@ -13,46 +13,55 @@ interface SequencePickerProps {
|
||||
onChange?: (name: string, value: GeneralIdRef) => void;
|
||||
}
|
||||
|
||||
interface SequencePickerState {
|
||||
options?: Option[];
|
||||
}
|
||||
export default function SequencePicker({
|
||||
includeLabel,
|
||||
name,
|
||||
label,
|
||||
error,
|
||||
value,
|
||||
onChange,
|
||||
}: SequencePickerProps) {
|
||||
const [options, setOptions] = useState<Option[] | undefined>(undefined);
|
||||
|
||||
class SequencePicker extends React.Component<SequencePickerProps, SequencePickerState> {
|
||||
state = { options: undefined };
|
||||
|
||||
async componentDidMount() {
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const pagedData = await sequenceService.getSequences(0, 10, "name", true);
|
||||
if (pagedData) {
|
||||
const options: Option[] = (pagedData.data as any[]).map((x: { id: any; name: any }) => {
|
||||
return {
|
||||
const opts: Option[] = (pagedData.data as any[]).map(
|
||||
(x: { id: any; name: any }) => ({
|
||||
_id: x.id,
|
||||
name: x.name,
|
||||
};
|
||||
});
|
||||
|
||||
this.setState({ options });
|
||||
}),
|
||||
);
|
||||
setOptions(opts);
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { onChange } = this.props;
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const input = e.currentTarget;
|
||||
|
||||
const generalIdRef: GeneralIdRef = {
|
||||
id: BigInt(input.value),
|
||||
};
|
||||
|
||||
if (onChange) onChange(input.name, generalIdRef);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { includeLabel, name, label, error, value } = this.props;
|
||||
const { options } = this.state;
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select includeLabel={includeLabel} name={name} label={label} error={error} value={String(value?.id)} options={options} includeBlankFirstEntry={true} onChange={this.handleChange} />
|
||||
<Select
|
||||
includeLabel={includeLabel}
|
||||
name={name}
|
||||
label={label}
|
||||
error={error}
|
||||
value={String(value?.id)}
|
||||
options={options}
|
||||
includeBlankFirstEntry={true}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SequencePicker;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import Select from "../common/Select";
|
||||
import Option from "../common/option";
|
||||
import { GeneralIdRef } from "../../utils/GeneralIdRef";
|
||||
@ -13,46 +13,57 @@ interface SsoProviderPickerProps {
|
||||
onChange?: (name: string, value: GeneralIdRef) => void;
|
||||
}
|
||||
|
||||
interface SsoProviderPickerState {
|
||||
options?: Option[];
|
||||
}
|
||||
export default function SsoProviderPicker({
|
||||
name,
|
||||
label,
|
||||
error,
|
||||
value,
|
||||
domain,
|
||||
onChange,
|
||||
}: SsoProviderPickerProps) {
|
||||
const [options, setOptions] = useState<Option[] | undefined>(undefined);
|
||||
|
||||
class SsoProviderPicker extends React.Component<SsoProviderPickerProps, SsoProviderPickerState> {
|
||||
state = { options: undefined };
|
||||
|
||||
async componentDidMount() {
|
||||
const pagedData = await ssoManagerService.getSsoProviders(0, 10, "name", true);
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const pagedData = await ssoManagerService.getSsoProviders(
|
||||
0,
|
||||
10,
|
||||
"name",
|
||||
true,
|
||||
);
|
||||
if (pagedData) {
|
||||
const options: Option[] = (pagedData.data as any[]).map(x => {
|
||||
return {
|
||||
const opts: Option[] = (pagedData.data as any[]).map((x) => ({
|
||||
_id: x.id,
|
||||
name: x.name,
|
||||
};
|
||||
});
|
||||
|
||||
this.setState({ options });
|
||||
}));
|
||||
setOptions(opts);
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { onChange } = this.props;
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const input = e.currentTarget;
|
||||
|
||||
const generalIdRef: GeneralIdRef = {
|
||||
id: BigInt(input.value),
|
||||
};
|
||||
|
||||
if (onChange) onChange(input.name, generalIdRef);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { name, label, error, value } = this.props;
|
||||
const { options } = this.state;
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select name={name} label={label} error={error} value={String(value?.id)} options={options} includeBlankFirstEntry={true} onChange={this.handleChange} />
|
||||
<Select
|
||||
name={name}
|
||||
label={label}
|
||||
error={error}
|
||||
value={String(value?.id)}
|
||||
options={options}
|
||||
includeBlankFirstEntry={true}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SsoProviderPicker;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import Select from "./../common/Select";
|
||||
import Option from "../common/option";
|
||||
import { GeneralIdRef } from "./../../utils/GeneralIdRef";
|
||||
@ -13,46 +13,52 @@ interface UserPickerProps {
|
||||
onChange?: (name: string, value: GeneralIdRef) => void;
|
||||
}
|
||||
|
||||
interface UserPickerState {
|
||||
options?: Option[];
|
||||
}
|
||||
export default function UserPicker({
|
||||
name,
|
||||
label,
|
||||
error,
|
||||
value,
|
||||
domain,
|
||||
onChange,
|
||||
}: UserPickerProps) {
|
||||
const [options, setOptions] = useState<Option[] | undefined>(undefined);
|
||||
|
||||
class UserPicker extends React.Component<UserPickerProps, UserPickerState> {
|
||||
state = { options: undefined };
|
||||
|
||||
async componentDidMount() {
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const pagedData = await userService.getUsers(0, 10, "name", true);
|
||||
if (pagedData) {
|
||||
const options: Option[] = (pagedData.data as any[]).map(x => {
|
||||
return {
|
||||
const opts: Option[] = (pagedData.data as any[]).map((x) => ({
|
||||
_id: x.id,
|
||||
name: x.displayName,
|
||||
};
|
||||
});
|
||||
|
||||
this.setState({ options });
|
||||
}));
|
||||
setOptions(opts);
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { onChange } = this.props;
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const input = e.currentTarget;
|
||||
|
||||
const generalIdRef: GeneralIdRef = {
|
||||
id: BigInt(input.value),
|
||||
};
|
||||
|
||||
if (onChange) onChange(input.name, generalIdRef);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { name, label, error, value } = this.props;
|
||||
const { options } = this.state;
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select name={name} label={label} error={error} value={String(value?.id)} options={options} includeBlankFirstEntry={true} onChange={this.handleChange} />
|
||||
<Select
|
||||
name={name}
|
||||
label={label}
|
||||
error={error}
|
||||
value={String(value?.id)}
|
||||
options={options}
|
||||
includeBlankFirstEntry={true}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UserPicker;
|
||||
|
||||
@ -1,114 +1,118 @@
|
||||
import React, { Component } from 'react';
|
||||
import Column from '../../components/common/columns';
|
||||
import { Paginated } from '../../services/Paginated';
|
||||
import withRouter from '../../utils/withRouter';
|
||||
import AuditTable from './components/auditTable';
|
||||
import auditService, { AuditLogEntry } from './services/auditService';
|
||||
import equal from "fast-deep-equal";
|
||||
import Loading from '../../components/common/Loading';
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import Column from "../../components/common/columns";
|
||||
import { Paginated } from "../../services/Paginated";
|
||||
import AuditTable from "./components/auditTable";
|
||||
import auditService, { AuditLogEntry } from "./services/auditService";
|
||||
import Loading from "../../components/common/Loading";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
|
||||
interface AuditState{
|
||||
loaded: boolean;
|
||||
pagedData : Paginated<AuditLogEntry>,
|
||||
sortColumn : Column<AuditLogEntry>,
|
||||
filters: Map<string, string>;
|
||||
}
|
||||
const useQuery = () => new URLSearchParams(useLocation().search);
|
||||
|
||||
class Audit extends Component< any, any, AuditState> {
|
||||
state = {
|
||||
loaded: false,
|
||||
pagedData : { page: 1,
|
||||
export default function Audit() {
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const query = useQuery();
|
||||
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [pagedData, setPagedData] = useState<Paginated<AuditLogEntry>>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
count: 0,
|
||||
totalPages: 1,
|
||||
data: [] },
|
||||
sortColumn: { key: "dateTime", label: "Timing", order: "desc" },
|
||||
filters: new Map<string, string>()
|
||||
}
|
||||
data: [],
|
||||
});
|
||||
|
||||
componentDidMount = () => {
|
||||
this.loadData();
|
||||
const [sortColumn, setSortColumn] = useState<Column<AuditLogEntry>>({
|
||||
key: "dateTime",
|
||||
label: "Timing",
|
||||
order: "desc",
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState<Map<string, string>>(new Map());
|
||||
|
||||
const doSearch = useCallback(async () => {
|
||||
const { page, pageSize } = pagedData;
|
||||
const auditId = params.auditId ?? "";
|
||||
const primaryOnly = query.get("primaryOnly");
|
||||
const isPrimaryOnly =
|
||||
primaryOnly === null || primaryOnly.toLowerCase() === "true";
|
||||
|
||||
const result = await auditService.getLog(
|
||||
auditId,
|
||||
isPrimaryOnly,
|
||||
page,
|
||||
pageSize,
|
||||
sortColumn.key,
|
||||
sortColumn.order === "asc",
|
||||
filters,
|
||||
);
|
||||
|
||||
if (result) {
|
||||
setPagedData(result);
|
||||
setLoaded(true);
|
||||
} else {
|
||||
setLoaded(false);
|
||||
}
|
||||
}, [pagedData, sortColumn, filters, params.auditId, query]);
|
||||
|
||||
const changePage = async (page: number, pageSize: number) => {
|
||||
setPagedData((prev) => ({
|
||||
...prev,
|
||||
page,
|
||||
pageSize,
|
||||
}));
|
||||
setLoaded(false);
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<any>, prevState: Readonly<any>, snapshot?: AuditState | undefined): void {
|
||||
if (!(equal(this.props?.router.location.search, prevProps?.router.location.search) && equal(this.props?.router.params.auditId, prevProps?.router.params.auditId)))
|
||||
{
|
||||
let {pagedData, sortColumn} = this.state;
|
||||
const onSort = async (column: Column<AuditLogEntry>) => {
|
||||
setSortColumn(column);
|
||||
setLoaded(false);
|
||||
};
|
||||
|
||||
pagedData = { page: 1,
|
||||
const onSearch = async (name: string, value: string) => {
|
||||
setFilters((prev) => {
|
||||
const updated = new Map(prev);
|
||||
updated.set(name, value);
|
||||
return updated;
|
||||
});
|
||||
setLoaded(false);
|
||||
};
|
||||
|
||||
// Load data on mount and whenever page/sort/filter changes
|
||||
useEffect(() => {
|
||||
doSearch();
|
||||
}, [doSearch]);
|
||||
|
||||
// Reset pagination + sort when URL search or auditId changes
|
||||
useEffect(() => {
|
||||
setPagedData({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
count: 0,
|
||||
totalPages: 1,
|
||||
data: [] };
|
||||
sortColumn = { key: "dateTime", label: "Timing", order: "desc" };
|
||||
data: [],
|
||||
});
|
||||
|
||||
this.setState({pagedData, sortColumn});
|
||||
setSortColumn({
|
||||
key: "dateTime",
|
||||
label: "Timing",
|
||||
order: "desc",
|
||||
});
|
||||
|
||||
this.changePage(pagedData.page, pagedData.pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
loadData = async () => {
|
||||
const { page, pageSize } = this.state.pagedData;
|
||||
|
||||
await this.changePage(page, pageSize);
|
||||
}
|
||||
|
||||
changePage = async(page: number, pageSize : number) =>{
|
||||
const { pagedData } = this.state;
|
||||
|
||||
pagedData.page = page;
|
||||
pagedData.pageSize = pageSize;
|
||||
|
||||
this.setState( {loaded: false, pagedData } );
|
||||
await this.doSearch();
|
||||
}
|
||||
|
||||
onSort = async(sortColumn : Column<AuditLogEntry>) => {
|
||||
this.setState({loaded: false, sortColumn});
|
||||
|
||||
await this.doSearch();
|
||||
}
|
||||
|
||||
onSearch = async ( name: string, value: string) => {
|
||||
const { filters } = this.state;
|
||||
filters.set(name, value);
|
||||
|
||||
this.setState( { filters });
|
||||
|
||||
await this.doSearch();
|
||||
};
|
||||
|
||||
doSearch = async () => {
|
||||
const {page, pageSize } = this.state.pagedData;
|
||||
const {sortColumn, filters } = this.state;
|
||||
const auditId = this.props?.router?.params?.auditId ?? "";
|
||||
var primaryOnly = this.props.router.query.get("primaryOnly");
|
||||
const isPrimaryOnly : boolean = (primaryOnly === null) || (primaryOnly.toLowerCase() === "true");
|
||||
|
||||
const pagedData = await auditService.getLog(auditId, isPrimaryOnly, page, pageSize, sortColumn.key, sortColumn.order === "asc", filters);
|
||||
if (pagedData) {
|
||||
this.setState({ loaded: true, pagedData });
|
||||
}
|
||||
else {
|
||||
this.setState({ loaded: false });
|
||||
}
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const {loaded, pagedData, sortColumn } = this.state;
|
||||
setLoaded(false);
|
||||
}, [location.search, params.auditId]);
|
||||
|
||||
return (
|
||||
<Loading loaded={loaded}>
|
||||
<div>
|
||||
<AuditTable data={pagedData} sortColumn={sortColumn} onChangePage={this.changePage} onSort={this.onSort} onSearch={this.onSearch}/>
|
||||
<AuditTable
|
||||
data={pagedData}
|
||||
sortColumn={sortColumn}
|
||||
onChangePage={changePage}
|
||||
onSort={onSort}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
</div>
|
||||
</Loading>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const HOCAudit = withRouter(Audit);
|
||||
|
||||
export default HOCAudit;
|
||||
@ -1,14 +1,16 @@
|
||||
import React from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import Column from "../../../components/common/columns";
|
||||
import Table, { PublishedTableProps } from "../../../components/common/Table";
|
||||
import { Paginated } from "../../../services/Paginated";
|
||||
import { AuditLogEntry } from "../services/auditService";
|
||||
import { Namespaces } from "../../../i18n/i18n";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface AuditFieldValues {
|
||||
OldDisplayName? : string,
|
||||
OldValue? : string,
|
||||
NewDisplayName? : string
|
||||
NewValue? : string
|
||||
OldDisplayName?: string;
|
||||
OldValue?: string;
|
||||
NewDisplayName?: string;
|
||||
NewValue?: string;
|
||||
}
|
||||
|
||||
interface AuditFieldChanges {
|
||||
@ -17,103 +19,143 @@ interface AuditFieldChanges{
|
||||
|
||||
interface AuditFieldChangeValues {
|
||||
fieldName: string;
|
||||
oldDisplayName? : string,
|
||||
oldValue? : string,
|
||||
newDisplayName? : string
|
||||
newValue? : string
|
||||
oldDisplayName?: string;
|
||||
oldValue?: string;
|
||||
newDisplayName?: string;
|
||||
newValue?: string;
|
||||
}
|
||||
|
||||
class AuditTable extends React.Component<PublishedTableProps<AuditLogEntry>> {
|
||||
fieldColumns : Column<AuditFieldChangeValues>[] = [
|
||||
{ key: "fieldName", label: "Field" },
|
||||
{ key: "oldDisplayName", label: "Old Value", content: (item) => {
|
||||
if (item.oldDisplayName !== undefined)
|
||||
return <>{item.oldDisplayName}</>
|
||||
return <>{item.oldValue !== undefined ? String(item.oldValue) : ""}</>
|
||||
} },
|
||||
{ key: "newDisplayName", label: "New Value", content: (item) => {
|
||||
if (item.newDisplayName !== undefined)
|
||||
return <>{item.newDisplayName}</>
|
||||
return <>{item.newValue !== undefined ? String(item.newValue) : ""}</>
|
||||
} },
|
||||
]
|
||||
export default function AuditTable(props: PublishedTableProps<AuditLogEntry>) {
|
||||
const { data, sortColumn, onChangePage, onSearch, onSort } = props;
|
||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||
|
||||
columns : Column<AuditLogEntry>[] = [
|
||||
{ key: "dateTime", label: "Timing", order: "asc", searchable: false },
|
||||
{ key: "userDisplayName", label: "User Name", order: "asc" },
|
||||
{ key: "comment", label: "Comment", order: "asc" },
|
||||
{ key: "entityDisplayName", label: "Entity Display Name", order: "asc", searchable: false },
|
||||
{ key: "type", label: "Type", order: "asc" },
|
||||
{ key: "displayName", label: "DisplayName", order: "asc" },
|
||||
|
||||
{ key: "fields", label: "Changes", order: "asc", searchable: false, content: (item)=>{
|
||||
if (item.type === "Delete" || item.type === "Purge")
|
||||
const fieldColumns: Column<AuditFieldChangeValues>[] = useMemo(
|
||||
() => [
|
||||
{ key: "fieldName", label: t("Field") },
|
||||
{
|
||||
if (item.displayName !== "")
|
||||
return <></>
|
||||
key: "oldDisplayName",
|
||||
label: t("OldValue"),
|
||||
content: (item) => {
|
||||
if (item.oldDisplayName !== undefined)
|
||||
return <>{item.oldDisplayName}</>;
|
||||
return (
|
||||
<>{item.oldValue !== undefined ? String(item.oldValue) : ""}</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "newDisplayName",
|
||||
label: t("NewValue"),
|
||||
content: (item) => {
|
||||
if (item.newDisplayName !== undefined)
|
||||
return <>{item.newDisplayName}</>;
|
||||
return (
|
||||
<>{item.newValue !== undefined ? String(item.newValue) : ""}</>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const columns: Column<AuditLogEntry>[] = useMemo(
|
||||
() => [
|
||||
{ key: "dateTime", label: t("Timing"), order: "asc", searchable: false },
|
||||
{ key: "userDisplayName", label: t("User Name"), order: "asc" },
|
||||
{ key: "comment", label: t("Comment"), order: "asc" },
|
||||
{
|
||||
key: "entityDisplayName",
|
||||
label: t("EntityDisplayName"),
|
||||
order: "asc",
|
||||
searchable: false,
|
||||
},
|
||||
{ key: "type", label: t("Type"), order: "asc" },
|
||||
{ key: "displayName", label: t("DisplayName"), order: "asc" },
|
||||
{
|
||||
key: "fields",
|
||||
label: t("Changes"),
|
||||
order: "asc",
|
||||
searchable: false,
|
||||
content: (item) => {
|
||||
if (
|
||||
(item.type === "Delete" || item.type === "Purge") &&
|
||||
item.displayName !== ""
|
||||
) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const fieldsObject: AuditFieldChanges = JSON.parse(item!.fields);
|
||||
const data: AuditFieldChangeValues[] = [];
|
||||
|
||||
let data : AuditFieldChangeValues[] = [];
|
||||
Object.keys(fieldsObject).forEach(
|
||||
(key, index) => {
|
||||
let dataItem : AuditFieldChangeValues =
|
||||
{
|
||||
Object.keys(fieldsObject).forEach((key) => {
|
||||
const dataItem: AuditFieldChangeValues = {
|
||||
fieldName: key,
|
||||
oldValue: fieldsObject[key].OldValue,
|
||||
oldDisplayName: fieldsObject[key].OldDisplayName,
|
||||
newValue: fieldsObject[key].NewValue,
|
||||
newDisplayName : fieldsObject[key].NewDisplayName
|
||||
}
|
||||
|
||||
newDisplayName: fieldsObject[key].NewDisplayName,
|
||||
};
|
||||
data.push(dataItem);
|
||||
});
|
||||
|
||||
let paginated : Paginated<AuditFieldChangeValues> = {
|
||||
const paginated: Paginated<AuditFieldChangeValues> = {
|
||||
count: 0,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
totalPages: 0,
|
||||
data: data
|
||||
}
|
||||
data,
|
||||
};
|
||||
|
||||
let fieldColumns : Column<AuditFieldChangeValues>[];
|
||||
fieldColumns = this.fieldColumns;
|
||||
|
||||
if (item.type === "Create" || item.type === "Restore")
|
||||
{
|
||||
fieldColumns = this.fieldColumns.filter( x => x.key !== "oldDisplayName");
|
||||
}
|
||||
|
||||
if (item.type === "Delete" || item.type === "Purge")
|
||||
{
|
||||
fieldColumns = this.fieldColumns.filter( x => x.key !== "newDisplayName");
|
||||
}
|
||||
|
||||
|
||||
return <Table data={ paginated } keyName="id" columns={fieldColumns} />;
|
||||
} },
|
||||
let displayColumns: Column<AuditFieldChangeValues>[] = [
|
||||
...fieldColumns,
|
||||
];
|
||||
|
||||
raiseSort = (sortColumn : Column<AuditLogEntry>) => {
|
||||
this.setState({sortColumn});
|
||||
if (this.props.onSort !== undefined)
|
||||
this.props.onSort(sortColumn);
|
||||
if (item.type === "Create" || item.type === "Restore") {
|
||||
displayColumns = displayColumns.filter(
|
||||
(x) => x.key !== "oldDisplayName",
|
||||
);
|
||||
}
|
||||
|
||||
handleAuditParams = (item: any) => {
|
||||
if (item.type === "Delete" || item.type === "Purge") {
|
||||
displayColumns = displayColumns.filter(
|
||||
(x) => x.key !== "newDisplayName",
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table data={paginated} keyName="id" columns={displayColumns} />
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[fieldColumns, t],
|
||||
);
|
||||
|
||||
const raiseSort = useCallback(
|
||||
(sortCol: Column<AuditLogEntry>) => {
|
||||
if (onSort !== undefined) onSort(sortCol);
|
||||
},
|
||||
[onSort],
|
||||
);
|
||||
|
||||
const handleAuditParams = useCallback((item: any) => {
|
||||
return {
|
||||
entityName: item.entityName,
|
||||
primaryKey : item.primaryKey
|
||||
}
|
||||
}
|
||||
primaryKey: item.primaryKey,
|
||||
};
|
||||
}, []);
|
||||
|
||||
render() {
|
||||
const { data, sortColumn, onChangePage, onSearch } = this.props;
|
||||
|
||||
return <Table data={ data } keyName="id" columns={this.columns} sortColumn={sortColumn} onSort={this.raiseSort} onChangePage={onChangePage} onSearch={onSearch} onAuditParams={this.handleAuditParams} secondaryAudit={true} />;
|
||||
return (
|
||||
<Table
|
||||
data={data}
|
||||
keyName="id"
|
||||
columns={columns}
|
||||
sortColumn={sortColumn}
|
||||
onSort={raiseSort}
|
||||
onChangePage={onChangePage}
|
||||
onSearch={onSearch}
|
||||
onAuditParams={handleAuditParams}
|
||||
secondaryAudit={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AuditTable;
|
||||
@ -1,81 +1,110 @@
|
||||
import React, { Component } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import Column from '../../components/common/columns';
|
||||
import { Paginated } from '../../services/Paginated';
|
||||
import BlockedIPsTable from './components/blockedIPsTable';
|
||||
import blockedIPsService, { BlockedIPEntry } from './services/blockedIPsService';
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import Column from "../../components/common/columns";
|
||||
import { Paginated } from "../../services/Paginated";
|
||||
import BlockedIPsTable from "./components/blockedIPsTable";
|
||||
import blockedIPsService, {
|
||||
BlockedIPEntry,
|
||||
} from "./services/blockedIPsService";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Namespaces } from "../../i18n/i18n";
|
||||
|
||||
interface BlockedIPsState {
|
||||
pagedData: Paginated<BlockedIPEntry>,
|
||||
sortColumn: Column<BlockedIPEntry>,
|
||||
filters: Map<string, string>;
|
||||
}
|
||||
|
||||
class BlockedIPs extends Component<any, any, BlockedIPsState> {
|
||||
state = {
|
||||
pagedData: {
|
||||
export default function BlockedIPs() {
|
||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||
const [pagedData, setPagedData] = useState<Paginated<BlockedIPEntry>>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
count: 0,
|
||||
totalPages: 1,
|
||||
data: []
|
||||
data: [],
|
||||
});
|
||||
|
||||
const [sortColumn, setSortColumn] = useState<Column<BlockedIPEntry>>({
|
||||
key: "ipAddress",
|
||||
label: t("IPAddress"),
|
||||
order: "asc",
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState<Map<string, string>>(
|
||||
() => new Map<string, string>(),
|
||||
);
|
||||
|
||||
const loadPage = useCallback(
|
||||
async (page: number, pageSize: number) => {
|
||||
const paged = await blockedIPsService.getBlockedIps(
|
||||
page,
|
||||
pageSize,
|
||||
sortColumn.key,
|
||||
sortColumn.order === "asc",
|
||||
filters,
|
||||
);
|
||||
|
||||
if (paged) setPagedData(paged);
|
||||
},
|
||||
sortColumn: { key: "ipAddress", label: "IP Address", order: "asc" },
|
||||
filters: new Map<string, string>()
|
||||
}
|
||||
[sortColumn, filters],
|
||||
);
|
||||
|
||||
componentDidMount = async () => {
|
||||
const { page, pageSize } = this.state.pagedData;
|
||||
useEffect(() => {
|
||||
loadPage(pagedData.page, pagedData.pageSize);
|
||||
}, []); // initial load only
|
||||
|
||||
await this.changePage(page, pageSize);
|
||||
}
|
||||
const handleSort = useCallback(
|
||||
async (col: Column<BlockedIPEntry>) => {
|
||||
setSortColumn(col);
|
||||
|
||||
changePage = async (page: number, pageSize: number) => {
|
||||
const { sortColumn, filters } = this.state;
|
||||
const paged = await blockedIPsService.getBlockedIps(
|
||||
pagedData.page,
|
||||
pagedData.pageSize,
|
||||
col.key,
|
||||
col.order === "asc",
|
||||
filters,
|
||||
);
|
||||
|
||||
const pagedData = await blockedIPsService.getBlockedIps(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters);
|
||||
if (pagedData) {
|
||||
this.setState({ pagedData });
|
||||
}
|
||||
}
|
||||
if (paged) setPagedData(paged);
|
||||
},
|
||||
[pagedData.page, pagedData.pageSize, filters],
|
||||
);
|
||||
|
||||
onSort = async (sortColumn: Column<BlockedIPEntry>) => {
|
||||
const { page, pageSize } = this.state.pagedData;
|
||||
const { filters } = this.state;
|
||||
const pagedData = await blockedIPsService.getBlockedIps(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters);
|
||||
if (pagedData) {
|
||||
this.setState({ pagedData, sortColumn });
|
||||
}
|
||||
}
|
||||
const handleSearch = useCallback(
|
||||
async (name: string, value: string) => {
|
||||
const newFilters = new Map(filters);
|
||||
newFilters.set(name, value);
|
||||
setFilters(newFilters);
|
||||
|
||||
onSearch = async (name: string, value: string) => {
|
||||
const { page, pageSize } = this.state.pagedData;
|
||||
const { sortColumn, filters } = this.state;
|
||||
filters.set(name, value);
|
||||
const paged = await blockedIPsService.getBlockedIps(
|
||||
pagedData.page,
|
||||
pagedData.pageSize,
|
||||
sortColumn.key,
|
||||
sortColumn.order === "asc",
|
||||
newFilters,
|
||||
);
|
||||
|
||||
const pagedData = await blockedIPsService.getBlockedIps(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters);
|
||||
if (pagedData) {
|
||||
this.setState({ filters, pagedData });
|
||||
}
|
||||
};
|
||||
if (paged) setPagedData(paged);
|
||||
},
|
||||
[filters, pagedData.page, pagedData.pageSize, sortColumn],
|
||||
);
|
||||
|
||||
onUnblock = async (item?: BlockedIPEntry) => {
|
||||
const handleUnblock = useCallback(
|
||||
async (item?: BlockedIPEntry) => {
|
||||
const response = await blockedIPsService.UnBlockIp(item?.ipAddress);
|
||||
if (response) {
|
||||
this.componentDidMount();
|
||||
toast.info(`IP Address '${item?.ipAddress}' Unblocked.`);
|
||||
await loadPage(pagedData.page, pagedData.pageSize);
|
||||
toast.info(t("IPAddressUnblocked", { ip: item?.ipAddress }));
|
||||
}
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { pagedData, sortColumn } = this.state;
|
||||
},
|
||||
[pagedData.page, pagedData.pageSize, loadPage, t],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BlockedIPsTable data={pagedData} sortColumn={sortColumn} onChangePage={this.changePage} onSort={this.onSort} onSearch={this.onSearch} onDelete={this.onUnblock} />
|
||||
<BlockedIPsTable
|
||||
data={pagedData}
|
||||
sortColumn={sortColumn}
|
||||
onChangePage={loadPage}
|
||||
onSort={handleSort}
|
||||
onSearch={handleSearch}
|
||||
onDelete={handleUnblock}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default BlockedIPs;
|
||||
|
||||
@ -1,39 +1,78 @@
|
||||
import { faUnlock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { ButtonType } from "../../../components/common/Button";
|
||||
import Column from "../../../components/common/columns";
|
||||
import ConfirmButton from "../../../components/common/ConfirmButton";
|
||||
import Table, { PublishedTableProps } from "../../../components/common/Table";
|
||||
import authentication from "../../frame/services/authenticationService";
|
||||
import { BlockedIPEntry } from "../services/blockedIPsService";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Namespaces } from "../../../i18n/i18n";
|
||||
|
||||
class BlockedIPsTable extends React.Component<PublishedTableProps<BlockedIPEntry>> {
|
||||
canUnblockBlockedIPAddress = authentication.hasAccess("UnlockIPAddress");
|
||||
export default function BlockedIPsTable(
|
||||
props: PublishedTableProps<BlockedIPEntry>,
|
||||
) {
|
||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||
const { data, sortColumn, onChangePage, onSearch, onSort, onDelete } = props;
|
||||
|
||||
columns : Column<BlockedIPEntry>[] = [
|
||||
{ key: "ipAddress", label: "IP Address", order: "asc" },
|
||||
{ key: "numberOfAttempts", label: "Number of Attempts", order: "asc", searchable: false },
|
||||
{ key: "blockedAt", label: "Date", order: "asc", searchable: false },
|
||||
{ key: "unblockedIn", label: "Unblocked In (Minutes)", order: "asc", searchable: false },
|
||||
const canUnblockBlockedIPAddress =
|
||||
authentication.hasAccess("UnlockIPAddress");
|
||||
|
||||
const columns: Column<BlockedIPEntry>[] = useMemo(
|
||||
() => [
|
||||
{ key: "ipAddress", label: t("IPAddress"), order: "asc" },
|
||||
{
|
||||
key: "action", label: "", searchable: false, content: (item) => {
|
||||
if (this.canUnblockBlockedIPAddress) return <><ConfirmButton buttonType={ButtonType.primary} keyValue={item} onClick={this.props.onDelete} confirmMessage={"Press again to unblock"} ><FontAwesomeIcon icon={faUnlock} /></ConfirmButton></>; return (<></>)
|
||||
}
|
||||
}
|
||||
];
|
||||
key: "numberOfAttempts",
|
||||
label: t("NumberOfAttempts"),
|
||||
order: "asc",
|
||||
searchable: false,
|
||||
},
|
||||
{ key: "blockedAt", label: t("Date"), order: "asc", searchable: false },
|
||||
{
|
||||
key: "unblockedIn",
|
||||
label: t("UnblockedInMinutes"),
|
||||
order: "asc",
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
key: "action",
|
||||
label: "",
|
||||
searchable: false,
|
||||
content: (item) =>
|
||||
canUnblockBlockedIPAddress ? (
|
||||
<ConfirmButton
|
||||
buttonType={ButtonType.primary}
|
||||
keyValue={item}
|
||||
onClick={onDelete}
|
||||
confirmMessage={t("PressAgainToUnblock")}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUnlock} />
|
||||
</ConfirmButton>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
},
|
||||
],
|
||||
[canUnblockBlockedIPAddress, onDelete, t],
|
||||
);
|
||||
|
||||
raiseSort = (sortColumn : Column<BlockedIPEntry>) => {
|
||||
this.setState({sortColumn});
|
||||
if (this.props.onSort !== undefined)
|
||||
this.props.onSort(sortColumn);
|
||||
}
|
||||
const raiseSort = useCallback(
|
||||
(sortCol: Column<BlockedIPEntry>) => {
|
||||
if (onSort !== undefined) onSort(sortCol);
|
||||
},
|
||||
[onSort],
|
||||
);
|
||||
|
||||
render() {
|
||||
const { data, sortColumn, onChangePage, onSearch } = this.props;
|
||||
|
||||
return <Table data={data} keyName="ipAddress" columns={this.columns} sortColumn={sortColumn} onSort={this.raiseSort} onChangePage={onChangePage} onSearch={onSearch}/>;
|
||||
return (
|
||||
<Table
|
||||
data={data}
|
||||
keyName="ipAddress"
|
||||
columns={columns}
|
||||
sortColumn={sortColumn}
|
||||
onSort={raiseSort}
|
||||
onChangePage={onChangePage}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BlockedIPsTable;
|
||||
@ -1,27 +1,122 @@
|
||||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import Column from "../../../components/common/columns";
|
||||
import Table, { PublishedTableProps } from "../../../components/common/Table";
|
||||
import { ErrorLog } from "../services/errorLogsService";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Namespaces } from "../../../i18n/i18n";
|
||||
|
||||
class ErrorLogsTable extends React.Component<PublishedTableProps<ErrorLog>> {
|
||||
columns: Column<ErrorLog>[] = [
|
||||
{ key: "id", label: "Id", order: "asc" },
|
||||
{ key: "application", label: "Application", order: "asc" },
|
||||
{ key: "message", label: "Message", order: "asc" },
|
||||
{ key: "occuredAt", label: "Occured At", order: "asc", searchable: false }
|
||||
];
|
||||
import ExpandableCell from "../../../components/common/ExpandableCell";
|
||||
import { max } from "date-fns";
|
||||
|
||||
raiseSort = (sortColumn: Column<ErrorLog>) => {
|
||||
this.setState({ sortColumn });
|
||||
if (this.props.onSort !== undefined)
|
||||
this.props.onSort(sortColumn);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, sortColumn, onChangePage, onSearch } = this.props;
|
||||
|
||||
return <Table data={data} keyName="id" columns={this.columns} sortColumn={sortColumn} onSort={this.raiseSort} onChangePage={onChangePage} onSearch={onSearch} />;
|
||||
export default function ErrorLogsTable(
|
||||
props: PublishedTableProps<ErrorLog>,
|
||||
): JSX.Element {
|
||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||
const { data, sortColumn, onChangePage, onSearch, onSort } = props;
|
||||
|
||||
const columns: Column<ErrorLog>[] = useMemo(
|
||||
() => [
|
||||
{ key: "id", label: t("Id"), order: "asc" },
|
||||
{ key: "application", label: t("Application"), order: "asc" },
|
||||
{ key: "message", label: t("Message"), order: "asc" },
|
||||
{
|
||||
key: "occuredAt",
|
||||
label: t("OccuredAt"),
|
||||
order: "asc",
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
key: "exceptionJson",
|
||||
label: t("ExceptionJson"),
|
||||
order: "asc",
|
||||
searchable: false,
|
||||
content: (item: ErrorLog) => {
|
||||
const raw = item.exceptionJson ?? "";
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
const pretty = JSON.stringify(parsed, null, 2)
|
||||
.replace(/\\r\\n/g, "\n")
|
||||
.replace(/\\n/g, "\n");
|
||||
return (
|
||||
<ExpandableCell
|
||||
title={t("ShowJSON")}
|
||||
contentClassName="errorlogs-ExceptionJson"
|
||||
>
|
||||
<pre className="errorlogs-pre">{pretty}</pre>
|
||||
</ExpandableCell>
|
||||
);
|
||||
} catch {
|
||||
const safe = raw.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n");
|
||||
return (
|
||||
<ExpandableCell title={t("ShowJSON")}>
|
||||
<pre className="errorlogs-pre">{safe}</pre>
|
||||
</ExpandableCell>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
//cjd- hiding the stack trace as this information is already availalbe in the exceptionJson field
|
||||
// {
|
||||
// key: "stackTrace",
|
||||
// label: t("StackTrace"),
|
||||
// order: "asc",
|
||||
// searchable: false,
|
||||
// content: (item: ErrorLog) => (
|
||||
// <ExpandableCell
|
||||
// title={t("ShowStackTrace")}
|
||||
// contentClassName="errorlogs-StackTraceJson"
|
||||
// >
|
||||
// <pre className="errorlogs-pre">{item.stackTrace ?? ""}</pre>
|
||||
// </ExpandableCell>
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
key: "supportingData",
|
||||
label: t("SupportingData"),
|
||||
order: "asc",
|
||||
searchable: false,
|
||||
content: (item: ErrorLog) => {
|
||||
const raw = item.supportingData ?? "";
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
const pretty = JSON.stringify(parsed, null, 2)
|
||||
.replace(/\\r\\n/g, "\n")
|
||||
.replace(/\\n/g, "\n");
|
||||
return (
|
||||
<ExpandableCell
|
||||
title={t("ShowJSON")}
|
||||
contentClassName="errorlogs-SupportingData"
|
||||
>
|
||||
<pre className="errorlogs-pre">{pretty}</pre>
|
||||
</ExpandableCell>
|
||||
);
|
||||
} catch {
|
||||
const safe = raw.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n");
|
||||
return (
|
||||
<ExpandableCell title={t("ShowJSON")}>
|
||||
<pre className="errorlogs-pre">{safe}</pre>
|
||||
</ExpandableCell>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
export default ErrorLogsTable;
|
||||
const raiseSort = (sortColumnParam: Column<ErrorLog>) => {
|
||||
if (onSort) onSort(sortColumnParam);
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
data={data}
|
||||
keyName="id"
|
||||
columns={columns}
|
||||
sortColumn={sortColumn}
|
||||
onSort={raiseSort}
|
||||
onChangePage={onChangePage}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,72 +1,101 @@
|
||||
import { Component } from 'react';
|
||||
import Column from '../../components/common/columns';
|
||||
import { Paginated } from '../../services/Paginated';
|
||||
import ErrorLogsTable from './components/errorLogsTable';
|
||||
import errorLogsService, { ErrorLog } from './services/errorLogsService';
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Column from "../../components/common/columns";
|
||||
import { Paginated } from "../../services/Paginated";
|
||||
import ErrorLogsTable from "./components/errorLogsTable";
|
||||
import errorLogsService, { ErrorLog } from "./services/errorLogsService";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Namespaces } from "../../i18n/i18n";
|
||||
|
||||
interface ErrorLogState {
|
||||
pagedData: Paginated<ErrorLog>,
|
||||
sortColumn: Column<ErrorLog>,
|
||||
filters: Map<string, string>;
|
||||
}
|
||||
|
||||
class ErrorLogs extends Component<any, any, ErrorLogState> {
|
||||
state = {
|
||||
pagedData: {
|
||||
const ErrorLogs = (): JSX.Element => {
|
||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||
const [pagedData, setPagedData] = useState<Paginated<ErrorLog>>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
count: 0,
|
||||
totalPages: 1,
|
||||
data: []
|
||||
data: [],
|
||||
});
|
||||
const [sortColumn, setSortColumn] = useState<Column<ErrorLog>>({
|
||||
key: "Id",
|
||||
label: t("Id"),
|
||||
order: "desc",
|
||||
});
|
||||
const [filters, setFilters] = useState<Map<string, string>>(
|
||||
new Map<string, string>(),
|
||||
);
|
||||
|
||||
const loadErrorLogs = useCallback(
|
||||
async (
|
||||
page: number,
|
||||
pageSize: number,
|
||||
column: Column<ErrorLog>,
|
||||
filterMap: Map<string, string>,
|
||||
) => {
|
||||
const data = await errorLogsService.getErrorLogs(
|
||||
page,
|
||||
pageSize,
|
||||
column.key,
|
||||
column.order === "asc",
|
||||
filterMap,
|
||||
);
|
||||
if (data) {
|
||||
setPagedData(data);
|
||||
}
|
||||
},
|
||||
sortColumn: { key: "Id", label: "Id", order: "desc" },
|
||||
filters: new Map<string, string>()
|
||||
}
|
||||
[],
|
||||
);
|
||||
|
||||
componentDidMount = async () => {
|
||||
const { page, pageSize } = this.state.pagedData;
|
||||
useEffect(() => {
|
||||
loadErrorLogs(pagedData.page, pagedData.pageSize, sortColumn, filters);
|
||||
}, [loadErrorLogs]);
|
||||
|
||||
await this.changePage(page, pageSize);
|
||||
}
|
||||
const changePage = useCallback(
|
||||
async (page: number, pageSize: number) => {
|
||||
await loadErrorLogs(page, pageSize, sortColumn, filters);
|
||||
},
|
||||
[loadErrorLogs, sortColumn, filters],
|
||||
);
|
||||
|
||||
changePage = async (page: number, pageSize: number) => {
|
||||
const { sortColumn, filters } = this.state;
|
||||
const onSort = useCallback(
|
||||
async (nextSortColumn: Column<ErrorLog>) => {
|
||||
setSortColumn(nextSortColumn);
|
||||
await loadErrorLogs(
|
||||
pagedData.page,
|
||||
pagedData.pageSize,
|
||||
nextSortColumn,
|
||||
filters,
|
||||
);
|
||||
},
|
||||
[loadErrorLogs, pagedData.page, pagedData.pageSize, filters],
|
||||
);
|
||||
|
||||
const pagedData = await errorLogsService.getErrorLogs(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters);
|
||||
if (pagedData) {
|
||||
this.setState({ pagedData });
|
||||
}
|
||||
}
|
||||
const onSearch = useCallback(
|
||||
async (name: string, value: string) => {
|
||||
const nextFilters = new Map(filters);
|
||||
nextFilters.set(name, value);
|
||||
setFilters(nextFilters);
|
||||
|
||||
onSort = async (sortColumn: Column<ErrorLog>) => {
|
||||
const { page, pageSize } = this.state.pagedData;
|
||||
const { filters } = this.state;
|
||||
const pagedData = await errorLogsService.getErrorLogs(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters);
|
||||
if (pagedData) {
|
||||
this.setState({ pagedData, sortColumn });
|
||||
}
|
||||
}
|
||||
|
||||
onSearch = async (name: string, value: string) => {
|
||||
const { page, pageSize } = this.state.pagedData;
|
||||
const { sortColumn, filters } = this.state;
|
||||
filters.set(name, value);
|
||||
|
||||
const pagedData = await errorLogsService.getErrorLogs(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters);
|
||||
if (pagedData) {
|
||||
this.setState({ filters, pagedData });
|
||||
}
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const { pagedData, sortColumn } = this.state;
|
||||
await loadErrorLogs(
|
||||
pagedData.page,
|
||||
pagedData.pageSize,
|
||||
sortColumn,
|
||||
nextFilters,
|
||||
);
|
||||
},
|
||||
[loadErrorLogs, pagedData.page, pagedData.pageSize, sortColumn, filters],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ErrorLogsTable data={pagedData} sortColumn={sortColumn} onChangePage={this.changePage} onSort={this.onSort} onSearch={this.onSearch} />
|
||||
<ErrorLogsTable
|
||||
data={pagedData}
|
||||
sortColumn={sortColumn}
|
||||
onChangePage={changePage}
|
||||
onSort={onSort}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ErrorLogs;
|
||||
|
||||
@ -4,31 +4,42 @@ import MapToJson from "../../../utils/MapToJson";
|
||||
|
||||
const apiEndpoint = "/exceptionlogs";
|
||||
|
||||
|
||||
export type ErrorLog =
|
||||
{
|
||||
export type ErrorLog = {
|
||||
id: bigint;
|
||||
application: string;
|
||||
aessage: string;
|
||||
occuredAt: Date;
|
||||
}
|
||||
stackTrace: string;
|
||||
supportingData: string;
|
||||
exceptionJson: string;
|
||||
};
|
||||
|
||||
export async function getErrorLogs(page: number, pageSize: number, sortKey: string, sortAscending: boolean, filters?: Map<string, string>): Promise<Paginated<ErrorLog>> {
|
||||
export async function getErrorLogs(
|
||||
page: number,
|
||||
pageSize: number,
|
||||
sortKey: string,
|
||||
sortAscending: boolean,
|
||||
filters?: Map<string, string>,
|
||||
): Promise<Paginated<ErrorLog>> {
|
||||
const filterString = MapToJson(filters);
|
||||
const response = await httpService.get<Paginated<ErrorLog>>(apiEndpoint + "/exceptionlogs",
|
||||
{ params: {
|
||||
const response = await httpService.get<Paginated<ErrorLog>>(
|
||||
apiEndpoint + "/exceptionlogs",
|
||||
{
|
||||
params: {
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
sortKey: sortKey,
|
||||
sortAscending: sortAscending,
|
||||
filters : filterString
|
||||
} } );
|
||||
filters: filterString,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response?.data;
|
||||
}
|
||||
|
||||
const errorLogsService = {
|
||||
getErrorLogs
|
||||
getErrorLogs,
|
||||
};
|
||||
|
||||
export default errorLogsService;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user