From d9ab452c494e8c6a53fb9e9def4e2089daa58d1b Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Fri, 29 Nov 2024 12:59:51 +0100 Subject: [PATCH 1/2] change: dialog and savedialog --- .../components/dialog/SaveDialogFooter.jsx | 58 +++++++ .../components/dialog/confirmationdialog.jsx | 60 ++----- src/web/components/dialog/content.jsx | 9 - src/web/components/dialog/dialog.jsx | 38 ++-- src/web/components/dialog/savedialog.jsx | 164 ++++++------------ 5 files changed, 147 insertions(+), 182 deletions(-) create mode 100644 src/web/components/dialog/SaveDialogFooter.jsx diff --git a/src/web/components/dialog/SaveDialogFooter.jsx b/src/web/components/dialog/SaveDialogFooter.jsx new file mode 100644 index 0000000000..dac4197169 --- /dev/null +++ b/src/web/components/dialog/SaveDialogFooter.jsx @@ -0,0 +1,58 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import MultiStepFooter from 'web/components/dialog/multistepfooter'; +import DialogFooter from 'web/components/dialog/twobuttonfooter'; +import PropTypes from 'web/utils/proptypes'; + +const SaveDialogFooter = ({ + multiStep, + isLoading, + prevDisabled, + nextDisabled, + buttonTitle, + currentStep, + setCurrentStep, + onClose, + handleSaveClick, +}) => { + return multiStep > 0 ? ( + + setCurrentStep(currentStep < multiStep ? currentStep + 1 : currentStep) + } + onLeftButtonClick={onClose} + onPreviousButtonClick={() => + setCurrentStep(currentStep > 0 ? currentStep - 1 : currentStep) + } + onRightButtonClick={handleSaveClick} + /> + ) : ( + + ); +}; + +SaveDialogFooter.propTypes = { + multiStep: PropTypes.number.isRequired, + isLoading: PropTypes.bool.isRequired, + prevDisabled: PropTypes.bool.isRequired, + nextDisabled: PropTypes.bool.isRequired, + buttonTitle: PropTypes.string.isRequired, + currentStep: PropTypes.number.isRequired, + setCurrentStep: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + handleSaveClick: PropTypes.func.isRequired, +}; + +export default SaveDialogFooter; diff --git a/src/web/components/dialog/confirmationdialog.jsx b/src/web/components/dialog/confirmationdialog.jsx index de6705427b..d135ff35d8 100644 --- a/src/web/components/dialog/confirmationdialog.jsx +++ b/src/web/components/dialog/confirmationdialog.jsx @@ -3,12 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import React, {useCallback} from 'react'; - import PropTypes from 'web/utils/proptypes'; -import Dialog from 'web/components/dialog/dialog'; import DialogContent from 'web/components/dialog/content'; +import Dialog from 'web/components/dialog/dialog'; import DialogTwoButtonFooter, { DELETE_ACTION, } from 'web/components/dialog/twobuttonfooter'; @@ -17,43 +15,6 @@ import useTranslation from 'web/hooks/useTranslation'; const DEFAULT_DIALOG_WIDTH = '400px'; -const ConfirmationDialogContent = ({ - content, - close, - rightButtonTitle, - onResumeClick, - loading, - rightButtonAction, -}) => { - const handleResume = useCallback(() => { - if (onResumeClick) { - onResumeClick(); - } - }, [onResumeClick]); - - return ( - - {content} - - - ); -}; - -ConfirmationDialogContent.propTypes = { - close: PropTypes.func.isRequired, - content: PropTypes.elementOrString, - rightButtonTitle: PropTypes.string, - onResumeClick: PropTypes.func.isRequired, - loading: PropTypes.bool, - rightButtonAction: PropTypes.oneOf([undefined, DELETE_ACTION]), -}; - const ConfirmationDialog = ({ width = DEFAULT_DIALOG_WIDTH, content, @@ -67,18 +28,23 @@ const ConfirmationDialog = ({ const [_] = useTranslation(); rightButtonTitle = rightButtonTitle || _('OK'); + return ( - - {({close}) => ( - - )} + } + > + {content} ); }; diff --git a/src/web/components/dialog/content.jsx b/src/web/components/dialog/content.jsx index ead07b591b..ab33e5c809 100644 --- a/src/web/components/dialog/content.jsx +++ b/src/web/components/dialog/content.jsx @@ -13,13 +13,4 @@ const DialogContent = styled.div` gap: 20px; `; -export const StickyFooter = styled.div` - position: sticky; - bottom: 0; - background-color: white; - padding: 20px 0; - z-index: 201; - margin-bottom: 20; -`; - export default DialogContent; diff --git a/src/web/components/dialog/dialog.jsx b/src/web/components/dialog/dialog.jsx index 5313da1deb..04a909e07b 100644 --- a/src/web/components/dialog/dialog.jsx +++ b/src/web/components/dialog/dialog.jsx @@ -4,6 +4,7 @@ */ import {Modal} from '@greenbone/opensight-ui-components-mantinev7'; +import {ScrollArea} from '@mantine/core'; import {isDefined, isFunction} from 'gmp/utils/identity'; import {useCallback, useEffect, useState} from 'react'; import styled from 'styled-components'; @@ -27,6 +28,10 @@ const DialogTitleButton = styled.button` `; const StyledModal = styled(Modal)` + position: relative; + left: ${({position}) => `${position.x}px`}; + z-index: ${MODAL_Z_INDEX}; + .mantine-Modal-content { display: flex; flex-direction: column; @@ -42,27 +47,30 @@ const StyledModal = styled(Modal)` flex: 1; } .mantine-Modal-body { - padding-bottom: 0px; - margin-bottom: 15px; - overflow-y: auto; + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; } .mantine-Modal-close { width: 2rem; height: 2rem; } +`; - position: relative; - left: ${({position}) => `${position.x}px`}; - z-index: ${MODAL_Z_INDEX}; - resize: both; +const StyledScrollArea = styled(ScrollArea)` + flex: 1; + overflow-y: auto; + padding-right: 18px; `; const Dialog = ({ children, title, + footer, + onClose, height = MODAL_HEIGHT, width = MODAL_WIDTH, - onClose, }) => { const [isResizing, setIsResizing] = useState(false); @@ -150,17 +158,21 @@ const Dialog = ({ height={height} position={position} > - {isFunction(children) - ? children({ - close: handleClose, - }) - : children} + + {isFunction(children) + ? children({ + close: handleClose, + }) + : children} + + {footer} ); }; Dialog.propTypes = { children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), + footer: PropTypes.node, title: PropTypes.string, width: PropTypes.string, onClose: PropTypes.func, diff --git a/src/web/components/dialog/savedialog.jsx b/src/web/components/dialog/savedialog.jsx index 867a103ccd..4893b6654c 100644 --- a/src/web/components/dialog/savedialog.jsx +++ b/src/web/components/dialog/savedialog.jsx @@ -3,35 +3,41 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import React, {useState, useEffect} from 'react'; +import {useEffect, useState} from 'react'; import {isDefined, isFunction} from 'gmp/utils/identity'; -import State from 'web/utils/state'; import PropTypes from 'web/utils/proptypes'; +import State from 'web/utils/state'; import ErrorBoundary from 'web/components/error/errorboundary'; import useTranslation from 'web/hooks/useTranslation'; +import DialogContent from 'web/components/dialog/content'; import Dialog from 'web/components/dialog/dialog'; -import DialogContent, {StickyFooter} from 'web/components/dialog/content'; import DialogError from 'web/components/dialog/error'; -import DialogFooter from 'web/components/dialog/twobuttonfooter'; -import MultiStepFooter from 'web/components/dialog/multistepfooter'; +import SaveDialogFooter from 'web/components/dialog/SaveDialogFooter'; -const SaveDialogContent = ({ +const SaveDialog = ({ + buttonTitle, + children, + defaultValues, error, multiStep = 0, + title, + values, + width = '40vw', + onClose, onError, onErrorClose, onSave, - ...props }) => { const [_] = useTranslation(); + buttonTitle = buttonTitle || _('Save'); + const [isLoading, setIsLoading] = useState(false); const [stateError, setStateError] = useState(undefined); - const [currentStep, setCurrentStep] = useState(0); const [prevDisabled, setPrevDisabled] = useState(true); @@ -40,8 +46,7 @@ const SaveDialogContent = ({ useEffect(() => { setPrevDisabled(currentStep === 0); setNextDisabled(currentStep === multiStep); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentStep]); + }, [currentStep, multiStep]); useEffect(() => { setStateError(error); @@ -58,16 +63,11 @@ const SaveDialogContent = ({ } }; - const handleStepChange = index => { - setCurrentStep(index); - }; - const handleSaveClick = state => { if (onSave && !isLoading) { const promise = onSave(state); if (isDefined(promise)) { setIsLoading(true); - // eslint-disable-next-line no-shadow return promise.catch(error => setError(error)); } } @@ -81,117 +81,57 @@ const SaveDialogContent = ({ } }; - const {buttonTitle, children, close, defaultValues, values} = props; - return ( {({state, onValueChange}) => { const childValues = {...state, ...values}; + return ( - - {stateError && ( - - )} - - {isFunction(children) - ? children({ - currentStep, - values: childValues, - onStepChange: handleStepChange, - onValueChange, - }) - : children} - - - {multiStep > 0 ? ( - - setCurrentStep( - currentStep < multiStep ? currentStep + 1 : currentStep, - ) - } - onLeftButtonClick={close} - onPreviousButtonClick={() => - setCurrentStep( - currentStep > 0 ? currentStep - 1 : currentStep, - ) - } - onRightButtonClick={() => handleSaveClick(childValues)} - /> - ) : ( - handleSaveClick(childValues)} + handleSaveClick(childValues)} + /> + } + > + + {stateError && ( + )} - - + + {isFunction(children) + ? children({ + currentStep, + values: childValues, + onValueChange, + }) + : children} + + + ); }} ); }; -SaveDialogContent.propTypes = { - buttonTitle: PropTypes.string, - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), - close: PropTypes.func.isRequired, - defaultValues: PropTypes.object, - error: PropTypes.string, - multiStep: PropTypes.number, - nextDisabled: PropTypes.bool, - prevDisabled: PropTypes.bool, - values: PropTypes.object, - onError: PropTypes.func, - onErrorClose: PropTypes.func, - onSave: PropTypes.func.isRequired, - onValueChange: PropTypes.func, -}; - -const SaveDialog = ({ - buttonTitle, - children, - defaultValues, - error, - multiStep = 0, - title, - values, - width = '40vw', - onClose, - onError, - onErrorClose, - onSave, -}) => { - const [_] = useTranslation(); - buttonTitle = buttonTitle || _('Save'); - return ( - - - {children} - - - ); -}; - SaveDialog.propTypes = { buttonTitle: PropTypes.string, - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), + children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, defaultValues: PropTypes.object, // default values for uncontrolled values error: PropTypes.string, // for errors controlled from parent (onErrorClose must be used if set) multiStep: PropTypes.number, @@ -205,5 +145,3 @@ SaveDialog.propTypes = { }; export default SaveDialog; - -// vim: set ts=2 sw=2 tw=80: From b3dc4d34f64a4e577b17d14e68a03f12a87099e2 Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Fri, 29 Nov 2024 15:34:42 +0100 Subject: [PATCH 2/2] add: test --- .../dialog/__tests__/SaveDialogFooter.jsx | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/web/components/dialog/__tests__/SaveDialogFooter.jsx diff --git a/src/web/components/dialog/__tests__/SaveDialogFooter.jsx b/src/web/components/dialog/__tests__/SaveDialogFooter.jsx new file mode 100644 index 0000000000..c73862983a --- /dev/null +++ b/src/web/components/dialog/__tests__/SaveDialogFooter.jsx @@ -0,0 +1,59 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; + +import {render, fireEvent, screen} from 'web/utils/testing'; +import SaveDialogFooter from '../SaveDialogFooter'; + +describe('SaveDialogFooter', () => { + const defaultProps = { + multiStep: 0, + isLoading: false, + prevDisabled: false, + nextDisabled: false, + buttonTitle: 'Save', + currentStep: 0, + setCurrentStep: testing.fn(), + onClose: testing.fn(), + handleSaveClick: testing.fn(), + }; + + test('renders DialogFooter when multiStep is 0', () => { + render(); + expect(screen.getByText('Save')).toBeInTheDocument(); + }); + + test('renders MultiStepFooter when multiStep is greater than 0', () => { + render(); + expect(screen.getByText('Save')).toBeInTheDocument(); + }); + + test('calls setCurrentStep with incremented value on next button click in MultiStepFooter', () => { + render(); + fireEvent.click(screen.getByTestId('dialog-next-button')); + expect(defaultProps.setCurrentStep).toHaveBeenCalledWith(1); + }); + + test('calls setCurrentStep with decremented value on previous button click in MultiStepFooter', () => { + render( + , + ); + fireEvent.click(screen.getByTestId('dialog-previous-button')); + expect(defaultProps.setCurrentStep).toHaveBeenCalledWith(1); + }); + + test('calls onClose when left button is clicked', () => { + render(); + fireEvent.click(screen.getByText('Cancel')); + expect(defaultProps.onClose).toHaveBeenCalled(); + }); + + test('calls handleSaveClick when right button is clicked', () => { + render(); + fireEvent.click(screen.getByText('Save')); + expect(defaultProps.handleSaveClick).toHaveBeenCalled(); + }); +});