diff --git a/cypress.d.ts b/cypress.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ab1d185af891a3d302b785869e6893ff1f946b1 --- /dev/null +++ b/cypress.d.ts @@ -0,0 +1,10 @@ +import { mount } from "cypress/react"; + +declare global { + namespace Cypress { + interface Chainable { + mount: typeof mount; + login(): Chainable<void>; + } + } +} diff --git a/package-lock.json b/package-lock.json index a86596c7794ac77131cccb097e2aebb62121d810..3ddd7d77ccc63d90fc8944f0ca68b3b67bdde462 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ess-ics/ce-ui-common", - "version": "15.5.0", + "version": "16.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ess-ics/ce-ui-common", - "version": "15.5.0", + "version": "16.0.0", "dependencies": { "@fontsource/titillium-web": "^5.0.22", "@mui/x-data-grid-pro": "^6.5.0", diff --git a/package.json b/package.json index 085d1d50fb580c29a254410185678e771529691e..294573bbcb18a1e1f213608bf2c5e9a3b65c77cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ess-ics/ce-ui-common", - "version": "15.5.0", + "version": "16.0.0", "private": true, "type": "module", "main": "dist/index.js", diff --git a/src/components/auth/Login.jsx b/src/components/auth/Login.jsx index 6c66c77e548fdb3426b893054e83e7ef57c2d47f..5dd5d273742f8637350d0e2876df6d6fe0f475d2 100644 --- a/src/components/auth/Login.jsx +++ b/src/components/auth/Login.jsx @@ -130,9 +130,9 @@ export const LoginDialog = styled( return ( <Dialog DialogProps={{ - fullWidth: true + fullWidth: true, + open: open }} - open={open} onClose={handleClose} className={`${className} CeuiLoginDialog`} content={ diff --git a/src/components/common/Dialog/Dialog.cy.jsx b/src/components/common/Dialog/Dialog.cy.tsx similarity index 94% rename from src/components/common/Dialog/Dialog.cy.jsx rename to src/components/common/Dialog/Dialog.cy.tsx index 19c13a1f62e8e11c20682a0d76466bd43f45d349..49b9d612d60c35eb9c914d1f47ce3e8c7fbd2cd1 100644 --- a/src/components/common/Dialog/Dialog.cy.jsx +++ b/src/components/common/Dialog/Dialog.cy.tsx @@ -1,4 +1,5 @@ import { composeStories } from "@storybook/react"; +import { mount } from "cypress/react"; import * as stories from "../../../stories/common/Dialog/Dialog.stories"; const { BasicDialog, Confirmation, DangerActionDialog } = @@ -6,14 +7,14 @@ const { BasicDialog, Confirmation, DangerActionDialog } = describe("SimpleDialog", () => { it("should close on clicking close", () => { - cy.mount(<BasicDialog />); + mount(<BasicDialog />); cy.findByText(/some title/i).should("exist"); cy.findByRole("button", { name: /close/i }).click(); cy.findByText(/some title/i).should("not.exist"); }); it("should close on escape", () => { - cy.mount(<BasicDialog />); + mount(<BasicDialog />); cy.findByText(/some title/i) .should("exist") @@ -26,7 +27,7 @@ const closedText = "You closed the dialog!"; describe("ConfirmationDialog", () => { it("should close on clicking close icon", () => { - cy.mount(<Confirmation />); + mount(<Confirmation />); cy.findByText(/some title/i).should("exist"); cy.findByRole("button", { name: /close/i }).click(); @@ -34,7 +35,7 @@ describe("ConfirmationDialog", () => { cy.findByText(closedText).should("exist"); }); it("should close on escape", () => { - cy.mount(<Confirmation />); + mount(<Confirmation />); cy.findByText(/some title/i) .should("exist") @@ -43,7 +44,7 @@ describe("ConfirmationDialog", () => { cy.findByText(closedText).should("exist"); }); it("should close and when clicking cancel", () => { - cy.mount(<Confirmation />); + mount(<Confirmation />); cy.findByText(/some title/i).should("exist"); cy.findByRole("button", { name: /cancel/i }).click(); @@ -54,7 +55,7 @@ describe("ConfirmationDialog", () => { describe("DangerActionDialog", () => { const mountDangerActionDialog = () => { - cy.mount(<DangerActionDialog />); + mount(<DangerActionDialog />); cy.contains("Open Danger Action Dialog").click(); }; diff --git a/src/components/common/Dialog/Dialog.jsx b/src/components/common/Dialog/Dialog.tsx similarity index 72% rename from src/components/common/Dialog/Dialog.jsx rename to src/components/common/Dialog/Dialog.tsx index c002e0c9e845dd5b01ec90cc7085f20628d87d50..bacf66b53158db0e5ea7f040fb91a5c9d9c3fca0 100644 --- a/src/components/common/Dialog/Dialog.jsx +++ b/src/components/common/Dialog/Dialog.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { ReactNode, useCallback, useState } from "react"; import { Button, Dialog as MuiDialog, @@ -13,52 +13,47 @@ import { Grid, TextField, LinearProgress, - Alert + Alert, + DialogProps as DialogPropsMUI } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; -/** - * The title component for an ESS dialog, featuring a text/custom area and a close button - * nestled inside a colored box. - */ -const DialogTitle = styled(({ onClose, className, children }) => { - return ( - <Stack - className={className} - flexDirection="row" - justifyContent="space-between" - alignItems="center" - backgroundColor="primary.main" - color="essWhite.main" - paddingX={2} - paddingY={1} - > - {typeof children === "string" ? ( - <Typography>{children}</Typography> - ) : ( - <>{children}</> - )} - <IconButton - variant="outlined" - onClick={onClose} - aria-label="Close Dialog" - edge="end" - > - <CloseIcon - fontSize="medium" - color="essWhite" - /> - </IconButton> - </Stack> - ); -})({}); +interface DialogTitleProps { + onClose: () => void; + className?: string; + children: ReactNode | string; +} -/** - * @name renderActionsCallback - * @function - * @param {function} onClose - callback fired when the component requests to be closed - * @returns {object} JSX component - */ +const DialogTitle = ({ onClose, className, children }: DialogTitleProps) => ( + <Stack + className={className} + flexDirection="row" + justifyContent="space-between" + alignItems="center" + sx={{ + backgroundColor: "primary.main", + color: "essWhite.main", + paddingX: 2, + paddingY: 1 + }} + > + {typeof children === "string" ? ( + <Typography>{children}</Typography> + ) : ( + <>{children}</> + )} + <IconButton + onClick={onClose} + aria-label="Close Dialog" + edge="end" + > + <CloseIcon + fontSize="medium" + sx={{ color: "essWhite.main" }} + /> + </IconButton> + </Stack> +); /** * A generic MUI Dialog stylized for ESS that provides: @@ -76,26 +71,35 @@ const DialogTitle = styled(({ onClose, className, children }) => { * @param {string|object} title - Title string or custom component * @param {string|object} content - Content string or custom component * @param {renderActionsCallback} renderActions - render function that receives the onClose callback and returns the dialog footer/actions - * @param {boolean} open return true or false * @param {function} onClose callback fired when the component requests to be closed * @param {object} DialogProps MUI Dialog component props; this might be how you change the * width of the modal via fullWidth=true, maxWidth="md", for example. See MUI documentation. * @param {string} className containing css classname */ + +export interface DialogProps { + title: string | ReactNode; + content: string | ReactNode; + DialogProps: DialogPropsMUI; + className?: string; + isLoading?: boolean; + error?: string; + onClose: () => void; + renderActions?: (onClose: () => void) => ReactNode; +} + export const Dialog = ({ title, content, - renderActions, - open, - onClose, - DialogProps = {}, className, isLoading, - error -}) => { + error, + DialogProps, + renderActions, + onClose +}: DialogProps) => { return ( <MuiDialog - open={open} onClose={onClose} fullWidth maxWidth="sm" @@ -125,14 +129,22 @@ export const Dialog = ({ ); }; +interface ConfirmationDialogButtonsProps { + confirmText: string; + cancelText: string; + isConfirmDisabled?: boolean; + onConfirm: () => void; + onClose: () => void; +} + const ConfirmationDialogButtons = styled( ({ - onConfirm, - onClose, confirmText, cancelText, - isConfirmDisabled = false - }) => { + isConfirmDisabled = false, + onConfirm, + onClose + }: ConfirmationDialogButtonsProps) => { const handleConfirm = useCallback(() => { if (onConfirm) { onConfirm(); @@ -187,23 +199,36 @@ const ConfirmationDialogButtons = styled( * @param {string|object} props.title - Title string or custom component * @param {string|object} props.content - Content string or custom component * @param {renderActionsCallback} props.renderActions - render function that receives the onClose callback and returns the dialog footer/actions - * @param {boolean} props.open return true or false * @param {function} props.onClose callback fired when the component requests to be closed * @param {object} props.DialogProps MUI Dialog component props; this might be how you change the * width of the modal via fullWidth=true, maxWidth="md", for example. See MUI documentation. * @param {string} props.className containing css classname */ + +export interface ConfirmationDialogProps extends DialogProps { + confirmText?: string; + cancelText?: string; + isConfirmDisabled?: boolean; + onConfirm: () => void; +} + export const ConfirmationDialog = styled( ({ + title, + content, onConfirm, onClose, confirmText = "Yes", cancelText = "No", - isConfirmDisabled, - ...props - }) => { + isConfirmDisabled = false, + DialogProps + }: ConfirmationDialogProps) => { return ( <Dialog + title={title} + content={content} + DialogProps={DialogProps} + onClose={onClose} renderActions={() => ( <ConfirmationDialogButtons onConfirm={onConfirm} @@ -213,28 +238,30 @@ export const ConfirmationDialog = styled( isConfirmDisabled={isConfirmDisabled} /> )} - onClose={onClose} - {...props} /> ); } )({}); -export const ConfirmDangerActionDialog = ({ ...props }) => { - const { - title, - text, - confirmText, - valueToCheck, - open, - onClose, - onConfirm, - isLoading, - error - } = props; +export interface ConfirmDangerActionDialogProps + extends ConfirmationDialogProps { + valueToCheck: string; +} + +export const ConfirmDangerActionDialog = ({ + title, + content, + confirmText, + valueToCheck, + DialogProps, + onClose, + onConfirm, + isLoading, + error +}: ConfirmDangerActionDialogProps) => { const [isConfirmDisabled, setIsConfirmDisabled] = useState(true); - const handleInputChange = (event) => { + const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { const inputText = event.target.value; setIsConfirmDisabled(inputText !== valueToCheck); }; @@ -250,6 +277,7 @@ export const ConfirmDangerActionDialog = ({ ...props }) => { confirmText={confirmText} cancelText="Cancel" onClose={handleClose} + DialogProps={DialogProps} content={ <Grid container @@ -260,9 +288,9 @@ export const ConfirmDangerActionDialog = ({ ...props }) => { xs={12} > {typeof content === "string" ? ( - <DialogContentText>{text}</DialogContentText> + <DialogContentText>{content}</DialogContentText> ) : ( - <Box>{text}</Box> + <Box>{content}</Box> )} </Grid> <Grid diff --git a/src/components/common/Dialog/index.js b/src/components/common/Dialog/index.js deleted file mode 100644 index 296be34e900265e42422ee85c4f9951de082712b..0000000000000000000000000000000000000000 --- a/src/components/common/Dialog/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import { - Dialog, - ConfirmationDialog, - ConfirmDangerActionDialog -} from "./Dialog"; - -export { Dialog, ConfirmationDialog, ConfirmDangerActionDialog }; diff --git a/src/components/common/Dialog/index.ts b/src/components/common/Dialog/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f6d6189fce7ffb8027aab49dcbd001a4a86752b7 --- /dev/null +++ b/src/components/common/Dialog/index.ts @@ -0,0 +1,17 @@ +import { + Dialog, + ConfirmationDialog, + ConfirmDangerActionDialog, + DialogProps, + ConfirmationDialogProps, + ConfirmDangerActionDialogProps +} from "./Dialog"; + +export { + Dialog, + ConfirmationDialog, + ConfirmDangerActionDialog, + type DialogProps, + type ConfirmationDialogProps, + type ConfirmDangerActionDialogProps +}; diff --git a/src/components/common/ExpandableViewer/ExpandableViewer.jsx b/src/components/common/ExpandableViewer/ExpandableViewer.jsx index 6d0820677fe097bfb92ee1c5fda9e94d334eae54..20c8a70d22bff97260db5ad7032c949c32ca6874 100644 --- a/src/components/common/ExpandableViewer/ExpandableViewer.jsx +++ b/src/components/common/ExpandableViewer/ExpandableViewer.jsx @@ -88,7 +88,6 @@ export const ExpandableViewer = styled( ) : ( <> <Dialog - open={dialogOpen} onClose={handleClose} title={title} content={ @@ -101,6 +100,7 @@ export const ExpandableViewer = styled( } className="CeuiExpandableViewerDialog" DialogProps={{ + open: dialogOpen, fullWidth: true, maxWidth: "xl", ...DialogProps diff --git a/src/hooks/useTypingTimer.ts b/src/hooks/useTypingTimer.ts index 421e401d21268f9f4384dcfb38b535bb9b61550e..a39430647fd5af38d5949df0e46a22e4bcef2212 100644 --- a/src/hooks/useTypingTimer.ts +++ b/src/hooks/useTypingTimer.ts @@ -1,7 +1,8 @@ import { KeyboardEvent, useState } from "react"; export const useTypingTimer = ({ init = "", interval = 750 } = {}) => { - const [typingTimer, setTypingTimer] = useState<NodeJS.Timeout>(); + const [typingTimer, setTypingTimer] = + useState<ReturnType<typeof setTimeout>>(); const [value, setValue] = useState(init); const doneTypingInterval = interval; // ms diff --git a/src/stories/common/Dialog/Dialog.stories.jsx b/src/stories/common/Dialog/Dialog.stories.tsx similarity index 72% rename from src/stories/common/Dialog/Dialog.stories.jsx rename to src/stories/common/Dialog/Dialog.stories.tsx index 7fa9e976b79d2c6fdb68ca0c62d1ad30c4e2ef59..50db852f172c6fddf73c5cd5a88eb8f714a804eb 100644 --- a/src/stories/common/Dialog/Dialog.stories.jsx +++ b/src/stories/common/Dialog/Dialog.stories.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, ReactNode } from "react"; import { Alert, Button, Stack, Typography } from "@mui/material"; import { Box } from "@mui/system"; import AddBoxIcon from "@mui/icons-material/AddBox"; @@ -20,7 +20,11 @@ const CLOSED = "CLOSED"; const loremString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam dignissim aliquam libero, sit amet porttitor felis iaculis a. Nunc consequat urna vitae tellus consequat, a egestas enim dictum. Morbi sodales pharetra sagittis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed velit ex, venenatis id vulputate egestas, molestie id libero. Curabitur eu tincidunt metus. Mauris ultricies tristique quam, ut cursus tortor. Nullam feugiat elementum nulla, nec faucibus lorem sodales quis. Suspendisse sollicitudin sollicitudin sapien, a bibendum dolor rhoncus consectetur. Nunc interdum et turpis vitae vehicula. Donec egestas vitae mauris id scelerisque. Cras suscipit magna nec purus vehicula ultrices. Fusce quam nibh, iaculis in maximus a, ultrices eget tortor. Etiam ex massa, vulputate condimentum fermentum eu, convallis non metus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam dignissim aliquam libero, sit amet porttitor felis iaculis a. Nunc consequat urna vitae tellus consequat, a egestas enim dictum. Morbi sodales pharetra sagittis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed velit ex, venenatis id vulputate egestas, molestie id libero. Curabitur eu tincidunt metus. Mauris ultricies tristique quam, ut cursus tortor. Nullam feugiat elementum nulla, nec faucibus lorem sodales quis. Suspendisse sollicitudin sollicitudin sapien, a bibendum dolor rhoncus consectetur. Nunc interdum et turpis vitae vehicula. Donec egestas vitae mauris id scelerisque. Cras suscipit magna nec purus vehicula ultrices. Fusce quam nibh, iaculis in maximus a, ultrices eget tortor. Etiam ex massa, vulputate condimentum fermentum eu, convallis non metus."; -const CustomTitle = ({ title }) => { +interface CustomTitleProps { + title: string | ReactNode; +} + +const CustomTitle = ({ title }: CustomTitleProps) => { return ( <Stack flexDirection="row" @@ -30,7 +34,7 @@ const CustomTitle = ({ title }) => { <AddBoxIcon fontSize="large" /> <Typography variant="button" - verticalalign="center" + sx={{ verticalalign: "center" }} > {title} </Typography> @@ -38,17 +42,27 @@ const CustomTitle = ({ title }) => { ); }; -const CustomContent = ({ content }) => { +interface CustomContentProps { + content: string | ReactNode; +} + +const CustomContent = ({ content }: CustomContentProps) => { return <Typography fontFamily="monospace">{content}</Typography>; }; +interface DialogProps { + title: string | ReactNode; + titleIsCustomComponent: boolean; + content: string | ReactNode; + contentIsCustomComponent: boolean; +} + export const BasicDialog = ({ title, titleIsCustomComponent, content, - contentIsCustomComponent, - ...props -}) => { + contentIsCustomComponent +}: DialogProps) => { const [state, setState] = useState(OPEN); return ( @@ -60,7 +74,7 @@ export const BasicDialog = ({ Show Dialog </Button> <Dialog - open={state === OPEN} + DialogProps={{ open: state === OPEN }} onClose={() => setState(IDLE)} title={titleIsCustomComponent ? <CustomTitle title={title} /> : title} content={ @@ -70,7 +84,6 @@ export const BasicDialog = ({ content ) } - {...props} /> </> ); @@ -87,9 +100,8 @@ const ConfirmationTemplate = ({ title, titleIsCustomComponent, content, - contentIsCustomComponent, - ...props -}) => { + contentIsCustomComponent +}: DialogProps) => { const [state, setState] = useState(OPEN); const onConfirm = () => setState(CONFIRMED); const onClose = () => setState(CLOSED); @@ -113,8 +125,10 @@ const ConfirmationTemplate = ({ </Box> <ConfirmationDialog {...{ onConfirm, onClose }} - open={state === OPEN} + DialogProps={{ open: state === OPEN }} title={titleIsCustomComponent ? <CustomTitle title={title} /> : title} + cancelText="Cancel" + confirmText="Confirm" content={ contentIsCustomComponent ? ( <CustomContent content={content} /> @@ -122,13 +136,20 @@ const ConfirmationTemplate = ({ content ) } - {...props} /> </> ); }; -export const Confirmation = (args) => <ConfirmationTemplate {...args} />; +interface ConfirmationDialogProps extends DialogProps { + confirmText: string; + cancelText: string; + isConfirmDisabled: boolean; +} + +export const Confirmation = (args: ConfirmationDialogProps) => ( + <ConfirmationTemplate {...args} /> +); Confirmation.args = { ...BasicDialog.args, confirmText: "Confirm", @@ -136,7 +157,17 @@ Confirmation.args = { isConfirmDisabled: false }; -export const DangerActionComponent = ({ ...props }) => { +interface DangerActionDialogProps extends ConfirmationDialogProps { + valueToCheck: string; +} + +const DangerActionComponent = ({ + title, + content, + confirmText, + valueToCheck, + ...props +}: DangerActionDialogProps) => { const [state, setState] = useState(IDLE); return ( @@ -154,7 +185,11 @@ export const DangerActionComponent = ({ ...props }) => { ) : null} </Box> <ConfirmDangerActionDialog - open={state === OPEN} + title={title} + content={content} + confirmText={confirmText} + valueToCheck={valueToCheck} + DialogProps={{ open: state === OPEN }} onConfirm={() => setState(CONFIRMED)} onClose={() => setState(CLOSED)} {...props} @@ -163,10 +198,25 @@ export const DangerActionComponent = ({ ...props }) => { ); }; -export const DangerActionDialog = (args) => <DangerActionComponent {...args} />; +export const DangerActionDialog = ({ + title, + content, + confirmText, + valueToCheck, + ...args +}: DangerActionDialogProps) => ( + <DangerActionComponent + title={title} + content={content} + confirmText={confirmText} + valueToCheck={valueToCheck} + {...args} + /> +); DangerActionDialog.args = { title: "Delete IOC type", - text: " Type in the complete IOC type name to confirm that you really want to delete the IOC type.", + content: + " Type in the complete IOC type name to confirm that you really want to delete the IOC type.", confirmText: "Delete", valueToCheck: "ec234-67hdj" }; @@ -188,11 +238,23 @@ const getCustomComponent = () => ( </> ); -export const DangerActionDialogWithCustomContent = (args) => ( - <DangerActionComponent {...args} /> +export const DangerActionDialogWithCustomContent = ({ + title, + content, + confirmText, + valueToCheck, + ...args +}: DangerActionDialogProps) => ( + <DangerActionComponent + title={title} + content={content} + confirmText={confirmText} + valueToCheck={valueToCheck} + {...args} + /> ); DangerActionDialogWithCustomContent.args = { ...DangerActionDialog.args, - text: getCustomComponent() + content: getCustomComponent() }; diff --git a/tsconfig.json b/tsconfig.json index 7d887f1405288181631d0ed188bfe4d538e6fd3a..189f7c7abc7bf61077f8b75003af4f7bb29ede5f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,13 +12,14 @@ "noEmit": true, "declaration": true, "jsx": "react-jsx", - "typeRoots": ["./dist/index.d.ts", "node_modules/@types"], + "typeRoots": ["./dist/index.d.ts", "node_modules/@types", "node_modules"], /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "types": ["cypress", "@testing-library/cypress"] }, - "include": ["src"] + "include": ["src", "cypress.d.ts"] }