diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0b64067 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "chat.tools.terminal.autoApprove": { + "npx tsc": true + } +} diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 1edf65c..94f9a29 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -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" } diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json deleted file mode 100644 index d5b8f08..0000000 --- a/public/locales/fr/common.json +++ /dev/null @@ -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)" -} diff --git a/src/Sass/_errorLogs.scss b/src/Sass/_errorLogs.scss new file mode 100644 index 0000000..319114c --- /dev/null +++ b/src/Sass/_errorLogs.scss @@ -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; +} diff --git a/src/Sass/_expandableCell.scss b/src/Sass/_expandableCell.scss new file mode 100644 index 0000000..f4e579c --- /dev/null +++ b/src/Sass/_expandableCell.scss @@ -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; + } + } +} diff --git a/src/Sass/global.scss b/src/Sass/global.scss index f167216..443af00 100644 --- a/src/Sass/global.scss +++ b/src/Sass/global.scss @@ -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"; @@ -10,7 +10,7 @@ @import "../../node_modules/bootstrap/scss/mixins"; @import "../../node_modules/bootstrap/scss/root"; -// main bootstrap import +// main bootstrap import //@import "~bootstrap/scss/bootstrap.scss"; @import "../../node_modules/react-toastify/dist/ReactToastify"; @@ -26,8 +26,10 @@ @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 { - display: none; + display: none; } diff --git a/src/components/common/ExpandableCell.tsx b/src/components/common/ExpandableCell.tsx new file mode 100644 index 0000000..3ae6cd5 --- /dev/null +++ b/src/components/common/ExpandableCell.tsx @@ -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(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 ( +
+ + +
+
+ {children} +
+
+
+ ); +} diff --git a/src/components/common/Pagination.tsx b/src/components/common/Pagination.tsx index fd0ddc4..4b99ba9 100644 --- a/src/components/common/Pagination.tsx +++ b/src/components/common/Pagination.tsx @@ -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 { - data : Paginated - onChangePage : (page: number, pageSize: number) => void; - onUnselect?: () => void; + data: Paginated | null; + onChangePage: (page: number, pageSize: number) => void; + onUnselect?: () => void; } - -interface PaginationState { - + +function Pagination({ + data, + onChangePage, + onUnselect, +}: PaginationProps): JSX.Element { + const changePage = useCallback( + (page: number, pageSize: number) => { + onChangePage(page, pageSize); + if (onUnselect) onUnselect(); + }, + [onChangePage, onUnselect], + ); + + if (data === null) return <>; + + const { page, pageSize, totalPages, count } = data; + + const clickFirst = () => changePage(1, pageSize); + const clickPrevious = () => changePage(page - 1, pageSize); + const clickNext = () => changePage(page + 1, pageSize); + const clickLast = () => changePage(totalPages, pageSize); + + const PageSizeChange = (e: React.ChangeEvent) => { + const newPageSize = Number(e.currentTarget.value); + changePage(page, newPageSize); + }; + + const handlePageSelect = (e: React.ChangeEvent) => { + const newPage = Number(e.currentTarget.value); + if (1 <= newPage && newPage <= totalPages) { + changePage(newPage, pageSize); + } + }; + + const pageSizeOptions = [10, 25, 50, 100]; + + return ( +
+ + + + + + + + {" "} + of {totalPages} + + + + {count} Items + +
+ ); } - -class Pagination extends React.Component, PaginationState> { - state = { } - changePage( page : number, pageSize : number ){ - const {onChangePage, onUnselect} = this.props; - - onChangePage(page, pageSize); - if (onUnselect) { - onUnselect(); - } - } - - clickFirst = () => { - const { pageSize } = this.props.data; - - this.changePage(1, pageSize ); - } - - clickPrevious = () => { - const { page, pageSize } = this.props.data; - - 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) => { - const input = e.currentTarget; - const { page } = this.props.data; - - let newPageSize : number = +input.value; - - this.changePage(page, newPageSize); - } - - handlePageSelect = ( e : React.ChangeEvent) => - { - const { pageSize, totalPages } = this.props.data; - const input = e.currentTarget; - - const newPage = Number(input.value); - - if (1 <= newPage && newPage <= totalPages) { - this.changePage(newPage, pageSize); - } - } - - render() { - - 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 (
- - - - - - - of {data.totalPages} - - - {data.count} Items - -
); - } -} - -export default Pagination; \ No newline at end of file +export default Pagination; diff --git a/src/components/common/Permission.tsx b/src/components/common/Permission.tsx index 3c0d66f..470b1ad 100644 --- a/src/components/common/Permission.tsx +++ b/src/components/common/Permission.tsx @@ -1,22 +1,16 @@ import React from "react"; import authenticationService from "../../modules/frame/services/authenticationService"; - -interface PermissionProps{ - privilegeKey : string; - children: React.ReactNode; +interface PermissionProps { + privilegeKey: string; + children: React.ReactNode; } - -class Permission extends React.Component { - render() { - const { privilegeKey, children } = this.props; - const hasAccess = authenticationService.hasAccess( privilegeKey ); - if (hasAccess === false) - return ( <> ); - else - return ( <>{children} ); - } -} - -export default Permission; \ No newline at end of file +const Permission: React.FC = ({ privilegeKey, children }) => { + const hasAccess = authenticationService.hasAccess(privilegeKey); + + if (!hasAccess) return null; + return <>{children}; +}; + +export default Permission; diff --git a/src/components/common/Redirect.tsx b/src/components/common/Redirect.tsx index bd884d8..9ecdb40 100644 --- a/src/components/common/Redirect.tsx +++ b/src/components/common/Redirect.tsx @@ -1,21 +1,13 @@ -import React from "react"; +import { useEffect } from "react"; interface RedirectProps { - to : string + to: string; } -interface RedirectState { - +export default function Redirect({ to }: RedirectProps) { + useEffect(() => { + window.location.replace(to); + }, [to]); + + return null; } - -class Redirect extends React.Component { - render() { - const {to} = this.props; - - window.location.replace(to); - - return null; - } -} - -export default Redirect; \ No newline at end of file diff --git a/src/components/common/Select.tsx b/src/components/common/Select.tsx index 5439698..da4fec2 100644 --- a/src/components/common/Select.tsx +++ b/src/components/common/Select.tsx @@ -1,55 +1,75 @@ 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, - onChange?: (e: React.ChangeEvent) => 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; + includeLabel?: boolean; + name: string; + label: string; + error?: string; + value: unknown; + options?: Option[]; + includeBlankFirstEntry?: boolean; + onChange?: (e: React.ChangeEvent) => void; } -class Select extends React.Component { - render() { - const { includeLabel, name, label, error, value, options, includeBlankFirstEntry, onChange, ...rest } = this.props; +function GenerateValue(value: unknown) { + if (value === true) return "true"; + if (value === false) return "false"; + return value as string | number | readonly string[] | undefined; +} - const actualValue = GenerateValue( value); - - return ( -
- {(includeLabel===undefined || includeLabel===true) && } - {!options && } - {options && - - } - {error &&
{error}
} -
- ); - } -}; +export default function Select({ + includeLabel, + name, + label, + error, + value, + options, + includeBlankFirstEntry, + onChange, + ...rest +}: SelectProps) { + const { t } = useTranslation(); + const actualValue = GenerateValue(value); -export default Select; \ No newline at end of file + return ( +
+ {(includeLabel === undefined || includeLabel === true) && ( + + )} + {!options && ( + + )} + {options && ( + + )} + {error &&
{error}
} +
+ ); +} diff --git a/src/components/common/Tab.tsx b/src/components/common/Tab.tsx index 34a3449..5b1c4a6 100644 --- a/src/components/common/Tab.tsx +++ b/src/components/common/Tab.tsx @@ -1,18 +1,10 @@ -import React from 'react'; +import React from "react"; -interface TabProps{ - label: string; - children : JSX.Element | JSX.Element[]; +interface TabProps { + label: string; + children: React.ReactNode; } -class Tab extends React.Component { - render() { - return ( -
- -
- ); - } +export default function Tab({ label, children }: TabProps): JSX.Element { + return
{children}
; } - -export default Tab; \ No newline at end of file diff --git a/src/components/common/TabHeader.tsx b/src/components/common/TabHeader.tsx index d91d44f..4a212a3 100644 --- a/src/components/common/TabHeader.tsx +++ b/src/components/common/TabHeader.tsx @@ -1,36 +1,25 @@ -import React from 'react'; +import React, { useCallback } from "react"; -interface TabHeaderProps{ - isActive : boolean, - label : string - onClick : any; +interface TabHeaderProps { + isActive: boolean; + label: string; + onClick: (label: string) => void; } -class TabHeader extends React.Component { +export default function TabHeader({ + isActive, + label, + onClick, +}: TabHeaderProps) { + const handleClick = useCallback(() => onClick(label), [onClick, label]); - onClick = () => { - const { label, onClick } = this.props; - onClick(label); - }; + const className = isActive + ? "tab-list-item tab-list-active" + : "tab-list-item"; - render() { - const { - onClick, - props: { isActive, label }, - } = this; - - let className = "tab-list-item"; - - if (isActive === true) { - className += " tab-list-active"; - } - - return ( -
  • - {label} -
  • - ); - } + return ( +
  • + {label} +
  • + ); } - -export default TabHeader; \ No newline at end of file diff --git a/src/components/common/Table.tsx b/src/components/common/Table.tsx index e9f49cb..dee365d 100644 --- a/src/components/common/Table.tsx +++ b/src/components/common/Table.tsx @@ -1,70 +1,112 @@ -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 { - data: Paginated, - sortColumn? : Column, - selectedRow? : T; - onChangePage? : (page: number, pageSize : number) => {}; - onSort? : (sortColumn : Column) => void; - onSearch?: ( name: string, value: string) => void; - canEdit? : ( item : T ) => boolean; - canDelete?: ( item : T ) => boolean; - onDelete?: ( item? : T ) => void; - onSelectRow?: (item: T) => void; - onUnselectRow?: () => void; + data: Paginated; + sortColumn?: Column; + selectedRow?: T; + onChangePage?: (page: number, pageSize: number) => {}; + onSort?: (sortColumn: Column) => void; + onSearch?: (name: string, value: string) => void; + canEdit?: (item: T) => boolean; + canDelete?: (item: T) => boolean; + onDelete?: (item?: T) => void; + onSelectRow?: (item: T) => void; + onUnselectRow?: () => void; } -export interface TableProps extends PublishedTableProps { - keyName : string; - columns : Column[]; - editPath? : string; - onAuditParams?: ( item : T ) => AuditParams; - secondaryAudit? : boolean; +export interface TableProps extends PublishedTableProps { + keyName: string; + columns: Column[]; + editPath?: string; + onAuditParams?: (item: T) => AuditParams; + secondaryAudit?: boolean; } -interface TableState{ - debouncedOnSearch?: any -} +export default function Table(props: TableProps): JSX.Element { + const { + data, + keyName, + selectedRow, + columns, + sortColumn, + editPath, + canEdit, + canDelete, + onSort, + onChangePage, + onDelete, + onAuditParams, + onSelectRow, + secondaryAudit, + onUnselectRow, + onSearch, + } = props; -class Table extends Component, TableState> { - state : TableState = { + const [debouncedOnSearch, setDebouncedOnSearch] = useState(undefined); + + useEffect(() => { + const debounceDelay = 200; + if (!onSearch) { + setDebouncedOnSearch(undefined); + return; } - componentDidMount(): void { - const {onSearch } = this.props; - const debounceDelay = 200; - const debouncedOnSearch = onSearch === undefined ? undefined : debounce(onSearch, debounceDelay); + const d = debounce(onSearch, debounceDelay); + setDebouncedOnSearch(() => d); - this.setState( { - debouncedOnSearch, - }) - } + return () => { + if ((d as any).cancel) (d as any).cancel(); + }; + }, [onSearch]); - render() { - const { data, keyName, selectedRow, columns, sortColumn, editPath, canEdit, canDelete, onSort, onChangePage, onDelete, onAuditParams, onSelectRow, secondaryAudit, onUnselectRow } = this.props; - const { debouncedOnSearch } = this.state; + const showEdit = editPath != null && editPath !== ""; + const showDelete = onDelete != null; + const showAudit = onAuditParams != null; + const showSecondaryAudit = showAudit && secondaryAudit === true; - const showEdit = (editPath != null) && (editPath !== ""); - const showDelete = onDelete != null; - const showAudit = onAuditParams != null; - const showSecondaryAudit = showAudit && secondaryAudit === true; - - return ( - <> - - - - -
    - - ); - } + return ( + <> + + + + +
    + + ); } - -export default Table; \ No newline at end of file diff --git a/src/components/common/TableBody.tsx b/src/components/common/TableBody.tsx index 21fc6b4..db6af43 100644 --- a/src/components/common/TableBody.tsx +++ b/src/components/common/TableBody.tsx @@ -1,138 +1,170 @@ -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 +export interface AuditParams { + entityName: string; + primaryKey: string; } -export interface TableBodyProps{ - data : T[] | undefined; - keyName : string; - columns : Column[]; - editPath? : string; - selectedRow? : T; - canEdit? : ( item : T ) => boolean; - canDelete? : ( item : T ) => boolean; - onDelete?: ( item? : T ) => void; - onAuditParams?: ( item : T ) => AuditParams; - onSelectRow? : ( item : T ) => void; - showSecondaryAudit : boolean; +export interface TableBodyProps { + data: T[] | undefined; + keyName: string; + columns: Column[]; + editPath?: string; + selectedRow?: T; + canEdit?: (item: T) => boolean; + canDelete?: (item: T) => boolean; + onDelete?: (item?: T) => void; + onAuditParams?: (item: T) => AuditParams; + onSelectRow?: (item: T) => void; + showSecondaryAudit: boolean; } -class TableBody extends Component> { - resolvePath = ( path : string, args : string[]) => { - let modifiedPath = path; +export default function TableBody({ + data, + keyName, + selectedRow, + columns, + editPath, + canEdit, + canDelete, + onDelete, + onAuditParams, + onSelectRow, + showSecondaryAudit, +}: TableBodyProps): JSX.Element { + const resolvePath = (path: string, args: string[]) => { + let modifiedPath = path; + let index = 0; + while (index < args.length) { + modifiedPath = modifiedPath.replace("{" + index + "}", args[index]); + index++; + } + return modifiedPath; + }; - let index : number = 0; - while ( index < args.length ) - { - modifiedPath = modifiedPath.replace("{"+index+"}", args[index]) - index++; - } - - return modifiedPath; + const renderCell = (item: T, column: Column) => { + if (column.content) return column.content(item); + + const foundItem = deepFind(item, column.path || column.key); + + let columnContent: JSX.Element; + if (foundItem instanceof Date) { + columnContent = ; + } else if (typeof foundItem === "object") { + columnContent = <>; + } else { + columnContent = <>{foundItem}; } - renderCell = (item : T, column : Column) => { - const {keyName} = this.props; - if (column.content) return column.content(item); - - const foundItem = deepFind(item, column.path || column.key) - - let columnContent : JSX.Element; - - if (foundItem instanceof Date) { - columnContent = - } - else if (typeof foundItem === "object"){ - columnContent = <>; - } - else { - columnContent = <> - {foundItem} - ; - } - - const linkPath = column.link; - - if (linkPath !== undefined){ - const resolvedlinkPath = this.resolvePath( linkPath, [ (item as any)[keyName] ] ); - columnContent = {columnContent}; - } - - return <> - {columnContent} - ; - }; - - clickRow = ( value : T ) => - { - const { onSelectRow } = this.props; - - if (onSelectRow !== undefined) - onSelectRow( value ); + const linkPath = column.link; + if (linkPath !== undefined) { + const resolvedlinkPath = resolvePath(linkPath, [(item as any)[keyName]]); + columnContent = {columnContent}; } - createKey = (item : T, column : Column) => { - const { keyName } = this.props; + return <>{columnContent}; + }; - return (item as any)[keyName] + '_' + (column.path || column.key); - }; - - handleAuditParams = ( item : T, primaryOnly : boolean ) => { - const { onAuditParams } = this.props; - if (onAuditParams !== undefined) { - var auditParams = onAuditParams(item); - let json = JSON.stringify(auditParams); - var params = Buffer.from(json).toString('base64') ; + const clickRow = (value: T) => { + if (onSelectRow !== undefined) onSelectRow(value); + }; - var queryString = ""; - if (primaryOnly===false) - queryString += "?primaryOnly=" + primaryOnly; + const createKey = (item: T, column: Column) => { + return (item as any)[keyName] + "_" + (column.path || column.key); + }; - return "/audit/" + params + queryString; - } - - return ""; + const handleAuditParams = (item: T, primaryOnly: boolean) => { + if (onAuditParams !== undefined) { + const auditParams = onAuditParams(item); + const json = JSON.stringify(auditParams); + const params = Buffer.from(json).toString("base64"); + + let queryString = ""; + if (primaryOnly === false) queryString += "?primaryOnly=" + primaryOnly; + + return "/audit/" + params + queryString; } - 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 showAudit:boolean = onAuditParams !== undefined; + return ""; + }; + + const showDelete: boolean = onDelete != null; + const showEdit: boolean = editPath != null && editPath !== ""; + const showAudit: boolean = onAuditParams !== undefined; + + return ( + + {data?.map((item) => { + const classNames = selectedRow === item ? "table-primary" : ""; return ( - - {data?.map((item) => { - let classNames = ""; - if (selectedRow === item) - { - classNames+="table-primary"; - } - - return ( - {columns.map((column) => ( - this.clickRow(item)}>{this.renderCell(item, column)} - ))} - {showEdit && {(canEdit === undefined || canEdit(item)) && }} - {showDelete && {(canDelete === undefined || canDelete(item)) && }} - {showAudit && } - {showAudit && showSecondaryAudit && } - ) - })} - + + {columns.map((column) => ( + clickRow(item)}> + {renderCell(item, column)} + + ))} + {showEdit && ( + + {(canEdit === undefined || canEdit(item)) && ( + + )} + + )} + {showDelete && ( + + {(canDelete === undefined || canDelete(item)) && ( + + + + )} + + )} + {showAudit && ( + + + + + + )} + {showAudit && showSecondaryAudit && ( + + + + + + )} + ); - } + })} + + ); } - -export default TableBody; diff --git a/src/components/common/TableFooter.tsx b/src/components/common/TableFooter.tsx index 70391d0..36e8cd6 100644 --- a/src/components/common/TableFooter.tsx +++ b/src/components/common/TableFooter.tsx @@ -1,40 +1,51 @@ -import React, { Component } from "react"; +import React from "react"; import { Paginated } from "../../services/Paginated"; import Column from "./columns"; import Pagination from "./Pagination"; -export interface TableFooterProps{ - data : Paginated; - columns : Column[]; - showEdit : boolean; - showDelete : boolean; - showAudit : boolean; - showSecondaryAudit : boolean; - onChangePage?: (page: number, pageSize: number) => void; - onUnselectRow?: () => void; +export interface TableFooterProps { + data: Paginated; + columns: Column[]; + showEdit: boolean; + showDelete: boolean; + showAudit: boolean; + showSecondaryAudit: boolean; + onChangePage?: (page: number, pageSize: number) => void; + onUnselectRow?: () => void; } -class TableFooter extends Component> { - render() { - const { data, columns, showEdit, showDelete, showAudit, showSecondaryAudit, onChangePage, onUnselectRow} = this.props; - - let staticColumnCount = 0; - if (showEdit) staticColumnCount++; - if (showDelete) staticColumnCount++; - if (showAudit) staticColumnCount++; - if (showAudit && showSecondaryAudit) staticColumnCount++; - - let pagination = onChangePage === undefined ? undefined : ; +export default function TableFooter({ + data, + columns, + showEdit, + showDelete, + showAudit, + showSecondaryAudit, + onChangePage, + onUnselectRow, +}: TableFooterProps): JSX.Element | null { + let staticColumnCount = 0; + if (showEdit) staticColumnCount++; + if (showDelete) staticColumnCount++; + if (showAudit) staticColumnCount++; + if (showAudit && showSecondaryAudit) staticColumnCount++; - if (pagination) - return - - {pagination} - - + const pagination = + onChangePage === undefined ? undefined : ( + + ); - return <> - } + if (!pagination) return null; + + return ( + + + {pagination} + + + ); } - -export default TableFooter; diff --git a/src/components/common/TableHeader.tsx b/src/components/common/TableHeader.tsx index 6d4401b..2db03e8 100644 --- a/src/components/common/TableHeader.tsx +++ b/src/components/common/TableHeader.tsx @@ -1,103 +1,115 @@ +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"; -export interface TableHeaderProps{ - sortColumn? : Column; - columns : Column[]; - showDelete? : boolean; - showEdit? : boolean; - showAudit? : boolean; - showSecondaryAudit? : boolean; - onSort? : (sortColumn : Column) => void; - onSearch?: ( name: string, value: string) => void; +export interface TableHeaderProps { + sortColumn?: Column; + columns: Column[]; + showDelete?: boolean; + showEdit?: boolean; + showAudit?: boolean; + showSecondaryAudit?: boolean; + onSort?: (sortColumn: Column) => void; + onSearch?: (name: string, value: string) => void; } -class TableHeader extends Component> { - columnsMatch = ( left? : Column, right? : Column) => - { - if (left?.key !== right?.key) return false; - return true; - } - - raiseSort = (column : Column) => { - let sortColumn = this.props.sortColumn; +export default function TableHeader({ + sortColumn, + columns, + showEdit, + showDelete, + showAudit, + showSecondaryAudit, + onSort, + onSearch, +}: TableHeaderProps): JSX.Element { + const columnsMatch = useCallback((left?: Column, right?: Column) => { + return left?.key === right?.key; + }, []); - if (sortColumn) { - if (this.columnsMatch(column, sortColumn)){ - sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc"; - } - else { - sortColumn = column; - sortColumn.order = "asc"; - } + const raiseSort = useCallback( + (column: Column) => { + let sc = sortColumn; - const { onSort } = this.props; - - if (onSort) - onSort(sortColumn); + if (sc) { + if (columnsMatch(column, sc)) { + sc.order = sc.order === "asc" ? "desc" : "asc"; + } else { + sc = column; + sc.order = "asc"; } - }; - renderSortIcon = (column : Column) => { - const { sortColumn } = this.props; + if (onSort) onSort(sc); + } + }, + [sortColumn, onSort, columnsMatch], + ); - if (!sortColumn) return null; + const renderSortIcon = useCallback( + (column: Column) => { + if (!sortColumn) return null; + if (!columnsMatch(column, sortColumn)) return null; + return sortColumn.order === "asc" ? ( + + ) : ( + + ); + }, + [sortColumn, columnsMatch], + ); - if (!this.columnsMatch(column, sortColumn)) return null; + const changeSearch = useCallback( + (e: ChangeEvent) => { + if (onSearch) onSearch(e.target.name, e.target.value); + }, + [onSearch], + ); - if (sortColumn?.order === "asc") return + const searchRow = onSearch ? ( + + {columns.map((column) => ( + + {(column.searchable === undefined || column.searchable === true) && ( + + )} + + ))} + {showEdit && } + {showDelete && } + {showAudit && } + {showAudit && showSecondaryAudit && } + + ) : ( + <> + ); - return - }; - - changeSearch = (e: ChangeEvent) => - { - 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 = - {columns.map((column) => - - { - (column.searchable === undefined || column.searchable === true ) && - - } - - )} - {showEdit && } - {showDelete && } - {showAudit && } - {showAudit && showSecondaryAudit && } - ; - - return ( - - - {columns.map((column) => - this.raiseSort(column)}> - {column.label} {this.renderSortIcon(column)} - - )} - {showEdit && } - {showDelete && } - {showAudit && } - {showAudit && showSecondaryAudit && } - - {searchRow} - - ); - } + return ( + + + {columns.map((column) => ( + raiseSort(column)} + > + {column.label} {renderSortIcon(column)} + + ))} + {showEdit && } + {showDelete && } + {showAudit && } + {showAudit && showSecondaryAudit && } + + {searchRow} + + ); } - -export default TableHeader; diff --git a/src/components/common/TemplateEditor.tsx b/src/components/common/TemplateEditor.tsx index 22f7d27..ebc6a69 100644 --- a/src/components/common/TemplateEditor.tsx +++ b/src/components/common/TemplateEditor.tsx @@ -1,66 +1,67 @@ -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 { - type: string; - name: string; - guid: string | undefined; - id: string | bigint; +interface CustomFieldType { + type: string; + name: string; + guid: string | undefined; + id: string | bigint; } interface TemplateEditorProps { - className : string - name : string; - data : string; - showFields : boolean; - onChange : ( name : string, value : string ) => void; -} - -interface TemplateEditorState { - customfields: Array; - ready : boolean; + className: string; + name: string; + data: string; + showFields: boolean; + onChange: (name: string, value: string) => void; } -class TemplateEditor extends React.Component { - state = { - customfields : [], - ready: false +export default function TemplateEditor({ + className, + name, + data, + showFields, + onChange, +}: TemplateEditorProps) { + const [customfields, setCustomfields] = useState([]); + const [ready, setReady] = useState(false); + + useEffect(() => { + let mounted = true; + + async function load() { + const pageData = await customFieldsService.getFields(0, 10, "name", true); + + const fields: CustomFieldType[] = pageData.data.map((x: any) => ({ + type: "CustomField", + name: x.name, + guid: x.guid, + id: x.id, + })); + + if (mounted) { + setCustomfields(fields); + setReady(true); + } } - async componentWillMount() { - const pageData = await customFieldsService.getFields(0, 10, "name", true); + load(); - const customfields = pageData.data.map( - (x) => { - return { - type: "CustomField", - name: x.name, - guid: x.guid, - id: x.id - } - } - ); - - const { className, name, data, onChange } = this.props; + return () => { + mounted = false; + }; + }, []); - this.setState( { - ready : true, - customfields, - }); + if (!ready) return <>; - - } - - render() { - const { className, name, data, onChange } = this.props; - const { ready, customfields } = this.state; - - if (!ready) - return <> - - return ; - } + return ( + + ); } - -export default TemplateEditor; \ No newline at end of file diff --git a/src/components/common/TemplateFiller.tsx b/src/components/common/TemplateFiller.tsx index 3d95797..f93e687 100644 --- a/src/components/common/TemplateFiller.tsx +++ b/src/components/common/TemplateFiller.tsx @@ -1,184 +1,207 @@ 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"; import Loading from "./Loading"; interface TemplateFillerProps { - templateId? : GeneralIdRef; - formInstanceId?: GeneralIdRef; - onValidationChanged? : () => {}; + templateId?: GeneralIdRef; + formInstanceId?: GeneralIdRef; + onValidationChanged?: () => {}; } - + interface TemplateFillerState extends FormState { - customFields? : CustomField[], - template : { - name?: string; - templateId? : GeneralIdRef, - version? : bigint, - definition?: string; - } + customFields?: CustomField[]; + template: { + name?: string; + templateId?: GeneralIdRef; + version?: bigint; + definition?: string; + }; } - -class TemplateFiller extends Form { - state : TemplateFillerState = { - loaded: false, - customFields : undefined, - template : { - name: undefined, - templateId: undefined, - version: undefined, - definition: undefined, - }, - data : {}, - errors: {} + +class TemplateFiller extends Form< + TemplateFillerProps, + any, + TemplateFillerState +> { + state: TemplateFillerState = { + loaded: false, + customFields: undefined, + template: { + name: undefined, + templateId: undefined, + version: undefined, + definition: undefined, + }, + data: {}, + errors: {}, + }; + + schema = {}; + + componentDidMount(): void { + this.loadTemplate(); + } + + componentDidUpdate( + prevProps: Readonly, + prevState: Readonly, + 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 (prevErrorCount !== errorCount) { + this.props.onValidationChanged(); + } + } + } + + loadTemplate = async () => { + const { templateId, formInstanceId } = this.props; + let loadedData: any; + + if (templateId !== undefined) { + loadedData = await formsService.getForm(templateId?.id, templateId?.guid); + //Get the form definiton for the template provided by templateId and load. + + loadedData.templateId = undefined; + loadedData.customFieldValues = undefined; + loadedData.updatedVersion = undefined; + } else if (formInstanceId !== undefined) { + loadedData = await formsService.getFormInstance( + formInstanceId?.id, + formInstanceId?.guid, + ); + console.log("formInstanceId", loadedData); + } else { + loadedData = { + name: undefined, + id: undefined, + guid: undefined, + version: undefined, + definition: undefined, + customFieldDefinitions: undefined, + templateId: undefined, + customFieldValues: undefined, + updatedVersion: undefined, + }; } - schema = { + const { template, data } = this.state; + + template.name = loadedData.name; + template.templateId = MakeGeneralIdRef(loadedData.id, loadedData.guid); + template.version = loadedData.version; + template.definition = loadedData.definition; + const customFields = loadedData.customFieldDefinitions; + + this.setCustomFieldValues(data, loadedData.customFieldValues, customFields); + + this.setState({ loaded: true, template, customFields, data }); + }; + + 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]; + return this.renderCustomField(customField, false); + } + } else if (domNodeAsAny.name === "p") { + return ( +
    + {domToReact(domNodeAsAny.children, options)} +
    + ); + } + }, }; - componentDidMount(): void { - this.loadTemplate(); + 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) { + toast.error("There are errors on the form"); + throw new Error("There are errors on the form"); } - componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { - if ((prevProps.formInstanceId !== this.props.formInstanceId) || (prevProps.templateId !== this.props.templateId)) - this.loadTemplate(); + const customFieldValues = this.CustomFieldValues(); + if (formInstanceId !== undefined) { + if (templateId === undefined) throw Error("TemplateId cannot be null"); - 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(); - } - } - + if (version === undefined) throw Error("Version cannot be null"); + + const editFormInstance: EditFormInstance = { + formInstanceId, + templateId, + version, + customFieldValues, + }; + await formsService.putFormInstance(editFormInstance); + } else { + if (templateId !== undefined && version !== undefined) { + //const customFieldValues = this.CustomFieldValues(); + const formInstance: CreateFormInstance = { + templateId, + version, + customFieldValues, + }; + return await formsService.postFormInstance(formInstance); + } else throw new Error("template unknown"); } + } - loadTemplate = async () => { - const { templateId, formInstanceId } = this.props; - let loadedData : any; + render() { + const { loaded, template, customFields } = this.state; - if (templateId !== undefined){ - loadedData = await formsService.getForm(templateId?.id, templateId?.guid); - //Get the form definiton for the template provided by templateId and load. + let parsedDefinition: any; + if (template.definition) + parsedDefinition = this.parseDefinition( + template.definition, + customFields!, + ); + else parsedDefinition = <>; - loadedData.templateId = undefined; - loadedData.customFieldValues = undefined; - loadedData.updatedVersion = undefined; - } else if (formInstanceId !== undefined){ - loadedData = await formsService.getFormInstance(formInstanceId?.id, formInstanceId?.guid); - console.log("formInstanceId", loadedData); - } else { - loadedData = { - name: undefined, - id : undefined, - guid: undefined, - version: undefined, - definition: undefined, - customFieldDefinitions: undefined, - templateId : undefined, - customFieldValues : undefined, - updatedVersion : undefined - } - } - - const { template, data } = this.state; - - template.name = loadedData.name; - template.templateId = MakeGeneralIdRef(loadedData.id, loadedData.guid); - template.version = loadedData.version; - template.definition = loadedData.definition; - const customFields = loadedData.customFieldDefinitions; - - this.setCustomFieldValues(data, loadedData.customFieldValues, customFields ); - - this.setState({ loaded: true, template, customFields, data }); - } - - 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]; - return this.renderCustomField(customField, false); - } - } - else if (domNodeAsAny.name === "p"){ - return
    {domToReact(domNodeAsAny.children, options)}
    - } - } - } - - 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 ) - { - 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 (version === undefined) - throw Error("Version cannot be null"); - - const editFormInstance : EditFormInstance = { formInstanceId, templateId, version, customFieldValues }; - await formsService.putFormInstance(editFormInstance); - } - else { - if (templateId !== undefined && version !== undefined) - { - //const customFieldValues = this.CustomFieldValues(); - const formInstance : CreateFormInstance = { templateId, version, customFieldValues }; - return await formsService.postFormInstance(formInstance); - } - else - throw new Error("template unknown"); - } - } - - render() { - const { loaded, template, customFields } = this.state; - - let parsedDefinition : any; - if (template.definition) - parsedDefinition = this.parseDefinition(template.definition, customFields!); - else - parsedDefinition = <>; - - return ( - -
    - {parsedDefinition} -
    -
    - ); - } + return ( + +
    {parsedDefinition}
    +
    + ); + } } - -export default TemplateFiller; \ No newline at end of file + +export default TemplateFiller; diff --git a/src/components/common/ToggleSlider.tsx b/src/components/common/ToggleSlider.tsx index f7e60e6..75e8494 100644 --- a/src/components/common/ToggleSlider.tsx +++ b/src/components/common/ToggleSlider.tsx @@ -2,25 +2,26 @@ import React from "react"; import Toggle, { ToggleProps } from "react-toggle"; export interface ToggleSliderProps extends ToggleProps { - name: string; - label: string; - error: string; - readOnly?: boolean; - defaultChecked: boolean; + name: string; + label: string; + error: string; + readOnly?: boolean; + defaultChecked: boolean; } -class ToggleSlider extends React.Component { - render() { - const { name, label, error, readOnly, defaultChecked, ...rest } = this.props; - - return ( -
    - - - {error &&
    {error}
    } -
    - ); - } +export default function ToggleSlider({ + name, + label, + error, + readOnly, + defaultChecked, + ...rest +}: ToggleSliderProps) { + return ( +
    + + + {error &&
    {error}
    } +
    + ); } - -export default ToggleSlider; diff --git a/src/components/pickers/CustomFieldPicker.tsx b/src/components/pickers/CustomFieldPicker.tsx index c596070..3b35714 100644 --- a/src/components/pickers/CustomFieldPicker.tsx +++ b/src/components/pickers/CustomFieldPicker.tsx @@ -1,94 +1,97 @@ -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; - label: string; - error?: string; - value: any; - exclude? : CustomField[]; - onChange?: (name: string, id: GeneralIdRef, displayValue : string) => void; + name: string; + label: string; + error?: string; + value: any; + exclude?: CustomField[]; + 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([]); -class CustomFieldPicker extends React.Component { - state = { options: [] as Option[] }; - - async componentDidMount() { - 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 { - _id: x.id, - name: x.name, - }; - }); - - this.setState({ options }); - } - } - - GetOptionById = (value: string ) => { - const { options } = this.state; - - for( var option of options) - { - if (String(option._id) === value) - return option.name; - } - return ""; - } - - handleChange = (e: React.ChangeEvent) => { - const { onChange } = this.props; - const input = e.currentTarget; - - const generalIdRef = MakeGeneralIdRef(BigInt(input.value)); - const displayValue = this.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; - } + useEffect(() => { + async function load() { + const pagedData = await customFieldsService.getFields( + 0, + 10, + "name", + true, + ); + if (pagedData) { + const opts: Option[] = (pagedData.data as any[]).map( + (x: { id: any; name: any }) => ({ + _id: x.id, + name: x.name, + }), ); - - return filteredOptions; + setOptions(opts); + } } - render() { - const { name, label, error, value } = this.props; + load(); + }, []); - const filteredOptions = this.getFilteredOptions(); + const getOptionById = useCallback( + (val: string) => { + for (const option of options) { + if (String(option._id) === val) return option.name; + } + return ""; + }, + [options], + ); - return ( - + ); } - -export default CustomFieldPicker; diff --git a/src/components/pickers/DomainPicker.tsx b/src/components/pickers/DomainPicker.tsx index d2a80e3..282f0a1 100644 --- a/src/components/pickers/DomainPicker.tsx +++ b/src/components/pickers/DomainPicker.tsx @@ -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"; @@ -7,138 +7,129 @@ import { CustomFieldValue } from "../../modules/manager/glossary/services/glossa import MultiSelect from "../common/MultiSelect"; interface DomainPickerProps { - includeLabel?: boolean; - name: string; - label: string; - error?: string; - values: CustomFieldValue[]; - minEntries?: number; - maxEntries?: number; - onChange?: (name: string, value: CustomFieldValue[]) => void; + includeLabel?: boolean; + name: string; + label: string; + error?: string; + values: CustomFieldValue[]; + minEntries?: number; + maxEntries?: number; + 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([]); + const [selectedOptions, setSelectedOptions] = useState([]); -class DomainPicker extends React.Component { - state = { options: [] as Option[], selectedOptions: [] as Option[] }; + useEffect(() => { + async function load() { + const pagedData = await domainsService.getDomains(0, 10, "name", true); - async componentDidMount() { - const { values } = this.props; - const pagedData = await domainsService.getDomains(0, 10, "name", true); + if (pagedData) { + const opts: Option[] = pagedData.data.map( + (x: { id: any; name: any }) => ({ + _id: x.id, + name: x.name, + }), + ); - if (pagedData) { - const options: Option[] | undefined = pagedData.data.map((x: { id: any; name: any }) => { - return { - _id: x.id, - name: x.name, - }; - }); + const selected: Option[] = []; - const selectedOptions: 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); - } - } - - this.setState({ options, selectedOptions }); + if (values) { + for (const option of values) { + const foundOption = opts.filter( + (x) => + Number(x._id) === Number((option.value as GeneralIdRef).id), + )[0]; + if (foundOption) selected.push(foundOption); + } } + + setOptions(opts); + setSelectedOptions(selected); + } } - doOnChange = (newSelectedOptions: Option[]) => { - const { onChange } = this.props; + load(); + }, [values]); - const { name } = this.props; + const doOnChange = useCallback( + (newSelectedOptions: Option[]) => { + const vals: CustomFieldValue[] = newSelectedOptions.map((x) => ({ + value: MakeGeneralIdRef(x._id as unknown as bigint), + displayValue: x.name, + })); - var values: CustomFieldValue[] = newSelectedOptions.map((x) => { - return { - value: MakeGeneralIdRef(x._id as unknown as bigint), - displayValue: x.name, - }; - }); + if (onChange) onChange(name, vals); + }, + [onChange, name], + ); - if (onChange) onChange(name, values); - }; + const handleChange = useCallback( + (e: React.ChangeEvent) => { + const input = e.currentTarget; + const id: number = Number(input.value); - handleChange = (e: React.ChangeEvent) => { - let { options, selectedOptions } = this.state; - const input = e.currentTarget; + const newSelected = options.filter((x) => x._id === id); + setSelectedOptions(newSelected); + doOnChange(newSelected); + }, + [options, doOnChange], + ); - const id: number = Number(input.value); + const handleAdd = useCallback( + (item: Option) => { + const newSelected = [...selectedOptions, item]; + setSelectedOptions(newSelected); + doOnChange(newSelected); + }, + [selectedOptions, doOnChange], + ); - selectedOptions = options.filter((x) => x._id === id); + const handleDelete = useCallback( + (item: Option) => { + const newSelected = selectedOptions.filter((x) => x !== item); + setSelectedOptions(newSelected); + doOnChange(newSelected); + }, + [selectedOptions, doOnChange], + ); - this.setState({ selectedOptions }); - this.doOnChange(selectedOptions); - }; - - handleAdd = (item: Option) => { - const { selectedOptions } = this.state; - - 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 ( - // - ); - } else { - return ( - - ); - } - } + if (maxEntries === 1) { + const value = selectedOptions[0]?._id; + return ( + - ); - } + return ( + - ); - } else { - return ( - - ); - } - } + if (maxEntries === 1) { + const value = selectedOptions[0]?._id; + return ( + + useEffect(() => { + async function load() { + const pagedData = await sequenceService.getSequences(0, 10, "name", true); + if (pagedData) { + const opts: Option[] = (pagedData.data as any[]).map( + (x: { id: any; name: any }) => ({ + _id: x.id, + name: x.name, + }), ); + setOptions(opts); + } } -} -export default SequencePicker; + load(); + }, []); + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + const input = e.currentTarget; + const generalIdRef: GeneralIdRef = { + id: BigInt(input.value), + }; + + if (onChange) onChange(input.name, generalIdRef); + }, + [onChange], + ); + + return ( + - ); - } + return ( + - ); - } + return ( +