From 2abf16eeb1df06a06c0aebabaaceb1d28cc2b061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Tue, 16 Apr 2024 13:01:20 +0200 Subject: [PATCH] Change: Rework dialog components to use Modal as base Update Dialog component to use the Model component from opensight for improved UI. --- src/web/components/dialog/composercontent.jsx | 40 ++-- .../components/dialog/confirmationdialog.jsx | 44 ++-- src/web/components/dialog/content.jsx | 7 +- src/web/components/dialog/dialog.jsx | 218 ++---------------- src/web/components/dialog/errordialog.jsx | 31 +-- src/web/components/dialog/footer.jsx | 8 +- src/web/components/dialog/multistepfooter.jsx | 85 +++---- src/web/components/dialog/savedialog.jsx | 120 ++++------ src/web/components/dialog/twobuttonfooter.jsx | 21 +- 9 files changed, 174 insertions(+), 400 deletions(-) diff --git a/src/web/components/dialog/composercontent.jsx b/src/web/components/dialog/composercontent.jsx index 3c0fc46b60..e5401b04e1 100644 --- a/src/web/components/dialog/composercontent.jsx +++ b/src/web/components/dialog/composercontent.jsx @@ -20,19 +20,15 @@ import React from 'react'; import styled from 'styled-components'; -import _ from 'gmp/locale'; - import {NO_VALUE, YES_VALUE} from 'gmp/parser'; import PropTypes from 'web/utils/proptypes'; +import Theme from 'web/utils/theme'; import CheckBox from 'web/components/form/checkbox'; import FormGroup from 'web/components/form/formgroup'; -import Divider from 'web/components/layout/divider'; -import Layout from 'web/components/layout/layout'; - -import Theme from 'web/utils/theme'; +import useTranslation from 'web/hooks/useTranslation'; export const COMPOSER_CONTENT_DEFAULTS = { includeNotes: YES_VALUE, @@ -52,20 +48,24 @@ const FilterField = styled.div` `; const ComposerContent = ({ - filterFieldTitle = _( - 'To change the filter, please filter your results on the report page. This filter will not be stored as default.', - ), + filterFieldTitle, filterString, includeNotes, includeOverrides, onValueChange, -}) => ( - - - {filterString} - - - +}) => { + const [_] = useTranslation(); + filterFieldTitle = + filterFieldTitle || + _( + 'To change the filter, please filter your results on the report page. This filter will not be stored as default.', + ); + return ( + <> + + {filterString} + + - - - -); + + + ); +}; ComposerContent.propTypes = { filterFieldTitle: PropTypes.string, diff --git a/src/web/components/dialog/confirmationdialog.jsx b/src/web/components/dialog/confirmationdialog.jsx index 3b5ec6f128..47f6f44694 100644 --- a/src/web/components/dialog/confirmationdialog.jsx +++ b/src/web/components/dialog/confirmationdialog.jsx @@ -15,40 +15,36 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import React from 'react'; - -import _ from 'gmp/locale'; +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 ScrollableContent from 'web/components/dialog/scrollablecontent'; -import DialogTitle from 'web/components/dialog/title'; import DialogTwoButtonFooter from 'web/components/dialog/twobuttonfooter'; -const DEFAULT_DIALOG_WIDTH = '400px'; +import useTranslation from 'web/hooks/useTranslation'; -const ConfirmationDialogContent = props => { - const handleResume = () => { - const {onResumeClick} = props; +const DEFAULT_DIALOG_WIDTH = '400px'; +const ConfirmationDialogContent = ({ + content, + close, + rightButtonTitle, + onResumeClick, +}) => { + const handleResume = useCallback(() => { if (onResumeClick) { onResumeClick(); } - }; - - const {content, moveprops, title, rightButtonTitle} = props; + }, [onResumeClick]); return ( - - - {content} - + {content} @@ -58,7 +54,6 @@ const ConfirmationDialogContent = props => { ConfirmationDialogContent.propTypes = { close: PropTypes.func.isRequired, content: PropTypes.elementOrString, - moveprops: PropTypes.object, rightButtonTitle: PropTypes.string, title: PropTypes.string.isRequired, onResumeClick: PropTypes.func.isRequired, @@ -68,18 +63,19 @@ const ConfirmationDialog = ({ width = DEFAULT_DIALOG_WIDTH, content, title, - rightButtonTitle = _('OK'), + rightButtonTitle, onClose, onResumeClick, }) => { + const [_] = useTranslation(); + + rightButtonTitle = rightButtonTitle || _('OK'); return ( - - {({close, moveProps}) => ( + + {({close}) => ( @@ -98,5 +94,3 @@ ConfirmationDialog.propTypes = { }; export default ConfirmationDialog; - -// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/components/dialog/content.jsx b/src/web/components/dialog/content.jsx index 9e2c12685b..2aee0ca829 100644 --- a/src/web/components/dialog/content.jsx +++ b/src/web/components/dialog/content.jsx @@ -18,17 +18,12 @@ import styled from 'styled-components'; -import Theme from 'web/utils/theme'; - const DialogContent = styled.div` display: flex; flex-direction: column; height: inherit; padding: 0; - background: ${Theme.white}; - box-shadow: 5px 5px 10px ${Theme.mediumGray}; - border-radius: 3px; - border: 1px solid ${Theme.mediumGray}; + gap: 20px; `; export default DialogContent; diff --git a/src/web/components/dialog/dialog.jsx b/src/web/components/dialog/dialog.jsx index 77fd338437..0da1cab1f4 100644 --- a/src/web/components/dialog/dialog.jsx +++ b/src/web/components/dialog/dialog.jsx @@ -16,213 +16,39 @@ * along with this program. If not, see . */ -import React from 'react'; +import React, {useCallback} from 'react'; -import {KeyCode} from 'gmp/utils/event'; -import {isDefined} from 'gmp/utils/identity'; +import {Modal} from '@greenbone/opensight-ui-components'; -import PropTypes from 'web/utils/proptypes'; - -import Portal from '../portal/portal'; +import {isDefined, isFunction} from 'gmp/utils/identity'; -import DialogContainer from './container'; -import DialogOverlay from './overlay'; -import Resizer from './resizer'; +import PropTypes from 'web/utils/proptypes'; const DEFAULT_DIALOG_WIDTH = '800px'; -const DEFAULT_DIALOG_HEIGHT = undefined; // use auto height by default -const DEFAULT_DIALOG_MAX_HEIGHT = '400px'; -const DEFAULT_DIALOG_MIN_HEIGHT = 250; -const DEFAULT_DIALOG_MIN_WIDTH = 450; - -class Dialog extends React.Component { - constructor(...args) { - super(...args); - - this.dialogRef = React.createRef(); - - this.handleClose = this.handleClose.bind(this); - - this.onKeyDown = this.onKeyDown.bind(this); - this.onMouseDownMove = this.onMouseDownMove.bind(this); - this.onMouseDownResize = this.onMouseDownResize.bind(this); - this.onMouseMoveMove = this.onMouseMoveMove.bind(this); - this.onMouseMoveResize = this.onMouseMoveResize.bind(this); - this.onMouseUp = this.onMouseUp.bind(this); - - this.state = this.defaultState(); - } - - componentDidMount() { - document.addEventListener('keydown', this.onKeyDown); - } - - componentWillUnmount() { - document.removeEventListener('keydown', this.onKeyDown); - } - - defaultState() { - const {width, height} = this.props; - return { - width: isDefined(width) ? width : DEFAULT_DIALOG_WIDTH, - height: isDefined(height) ? height : DEFAULT_DIALOG_HEIGHT, - }; - } - - handleClose() { - const {onClose} = this.props; - if (onClose) { +const Dialog = ({children, title, width = DEFAULT_DIALOG_WIDTH, onClose}) => { + const handleClose = useCallback(() => { + if (isDefined(onClose)) { onClose(); } - } - - setDialogPosition(x, y) { - const {current: dialog} = this.dialogRef; - - dialog.style.position = 'absolute'; - dialog.style.left = `${x}px`; - dialog.style.top = `${y}px`; - dialog.style.margin = '0'; - } - - onKeyDown(event) { - if (event.keyCode === KeyCode.ESC) { - this.handleClose(); - event.preventDefault(); - } - } - - onMouseDownMove(event) { - const {current: dialog} = this.dialogRef; - // eslint-disable-next-line no-bitwise - if (event.buttons & 1 && dialog !== null) { - const box = dialog.getBoundingClientRect(); - this.relX = event.pageX - box.left; - this.relY = event.pageY - box.top; - - this.setDialogPosition(box.left, box.top); - - document.addEventListener('mousemove', this.onMouseMoveMove); - document.addEventListener('mouseup', this.onMouseUp); - event.preventDefault(); - } - } - - onMouseDownResize(event) { - // eslint-disable-next-line no-bitwise - if (event.buttons & 1) { - const {current: dialog} = this.dialogRef; - const box = dialog.getBoundingClientRect(); - - this.width = Math.round(box.width); - this.height = Math.round(box.height); - this.mousePosX = event.pageX; - this.mousePosY = event.pageY; - - this.setDialogPosition(box.left, box.top); - - document.addEventListener('mousemove', this.onMouseMoveResize); - document.addEventListener('mouseup', this.onMouseUp); - event.preventDefault(); - } - } - - onMouseMoveMove(event) { - const left = event.pageX - this.relX; - const top = event.pageY - this.relY; - - const rightBorder = window.outerWidth - DEFAULT_DIALOG_MIN_WIDTH; - const bottomBorder = window.outerHeight - DEFAULT_DIALOG_MIN_HEIGHT; - - this.setDialogPosition( - Math.min(Math.max(left, 0), rightBorder), - Math.min(Math.max(top, 0), bottomBorder), - ); - - event.preventDefault(); - } - - onMouseMoveResize(event) { - const { - minHeight = DEFAULT_DIALOG_MIN_HEIGHT, - minWidth = DEFAULT_DIALOG_MIN_WIDTH, - } = this.props; - - const differenceX = this.mousePosX - event.pageX; - const differenceY = this.mousePosY - event.pageY; - let newWidth = this.width - differenceX; - let newHeight = this.height - differenceY; - - if (newWidth < minWidth) { - newWidth = minWidth; - } - if (newHeight < minHeight) { - newHeight = minHeight; - } - - this.setState({ - width: newWidth, - height: newHeight, - }); - - event.preventDefault(); - } - - onMouseUp(event) { - document.removeEventListener('mousemove', this.onMouseMoveMove); - document.removeEventListener('mousemove', this.onMouseMoveResize); - document.removeEventListener('mouseup', this.onMouseUp); - event.preventDefault(); - } - - render() { - let {height, width} = this.state; - - const {children, resizable = true} = this.props; - - if (!resizable) { - height = 'auto'; - } - - const maxHeight = isDefined(height) ? undefined : DEFAULT_DIALOG_MAX_HEIGHT; - - return ( - - - - {children({ - close: this.handleClose, - moveProps: { - onMouseDown: this.onMouseDownMove, - }, - heightProps: { - maxHeight, - }, - })} - {resizable && } - - - - ); - } -} + }, [onClose]); + + return ( + + {isFunction(children) + ? children({ + close: handleClose, + }) + : children} + + ); +}; Dialog.propTypes = { - height: PropTypes.numberOrNumberString, - minHeight: PropTypes.numberOrNumberString, - minWidth: PropTypes.numberOrNumberString, - resizable: PropTypes.bool, + children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), + title: PropTypes.string, width: PropTypes.string, - onClose: PropTypes.func.isRequired, + onClose: PropTypes.func, }; export default Dialog; - -// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/components/dialog/errordialog.jsx b/src/web/components/dialog/errordialog.jsx index fc8df38384..0cb47e4e74 100644 --- a/src/web/components/dialog/errordialog.jsx +++ b/src/web/components/dialog/errordialog.jsx @@ -17,26 +17,19 @@ */ import React from 'react'; -import _ from 'gmp/locale'; - import PropTypes from 'web/utils/proptypes'; import Dialog from 'web/components/dialog/dialog'; import DialogContent from 'web/components/dialog/content'; -import ScrollableContent from 'web/components/dialog/scrollablecontent'; -import DialogTitle from 'web/components/dialog/title'; import DialogFooter from 'web/components/dialog/footer'; const DEFAULT_DIALOG_WIDTH = '400px'; -const ErrorDialogContent = ({moveprops, text, title, buttonTitle, close}) => { +const ErrorDialogContent = ({children, buttonTitle, onClose}) => { return ( - - - {text} - - + {children} + ); }; @@ -53,20 +46,16 @@ const ErrorDialog = ({ width = DEFAULT_DIALOG_WIDTH, text, title, - buttonTitle = _('OK'), + buttonTitle, onClose, }) => { + const [_] = useTranslation(); + buttonTitle = buttonTitle || _('OK'); return ( - - {({close, moveProps}) => ( - - )} + + + {text} + ); }; diff --git a/src/web/components/dialog/footer.jsx b/src/web/components/dialog/footer.jsx index 08af61cece..502b28d0c1 100644 --- a/src/web/components/dialog/footer.jsx +++ b/src/web/components/dialog/footer.jsx @@ -26,20 +26,21 @@ import Theme from 'web/utils/theme'; import Layout from 'web/components/layout/layout'; -import Button from './button'; +import Button from 'web/components/form/button'; export const DialogFooterLayout = styled(Layout)` border-width: 1px 0 0 0; border-style: solid; border-color: ${Theme.lightGray}; margin-top: 15px; - padding: 10px 20px 10px 20px; + padding: 10px 0px 0px 0px; `; const DialogFooter = ({ title, onClick, loading = false, + isLoading = loading, 'data-testid': dataTestId, }) => ( - @@ -55,6 +56,7 @@ const DialogFooter = ({ DialogFooter.propTypes = { 'data-testid': PropTypes.string, + isLoading: PropTypes.bool, loading: PropTypes.bool, title: PropTypes.string.isRequired, onClick: PropTypes.func, diff --git a/src/web/components/dialog/multistepfooter.jsx b/src/web/components/dialog/multistepfooter.jsx index 4e1d8e0958..e11f1ec825 100644 --- a/src/web/components/dialog/multistepfooter.jsx +++ b/src/web/components/dialog/multistepfooter.jsx @@ -20,26 +20,24 @@ import React from 'react'; import styled from 'styled-components'; -import _ from 'gmp/locale'; - import PropTypes from 'web/utils/proptypes'; import {DialogFooterLayout} from 'web/components/dialog/footer'; -import Button from './button'; -import LoadingButton from 'web/components/form/loadingbutton'; +import Button from 'web/components/form/button'; import Divider from 'web/components/layout/divider'; +import useTranslation from 'web/hooks/useTranslation'; const StyledLayout = styled(DialogFooterLayout)` justify-content: space-between; `; const MultiStepFooter = ({ - leftButtonTitle = _('Cancel'), + leftButtonTitle, nextButtonTitle = '🠮', previousButtonTitle = '🠬', - rightButtonTitle = _('Save'), + rightButtonTitle, onLeftButtonClick, onNextButtonClick, onPreviousButtonClick, @@ -47,44 +45,49 @@ const MultiStepFooter = ({ prevDisabled = false, nextDisabled = false, loading = false, -}) => ( - - - - - {previousButtonTitle} - - - {nextButtonTitle} - +}) => { + const [_] = useTranslation(); + leftButtonTitle = leftButtonTitle || _('Cancel'); + rightButtonTitle = rightButtonTitle || _('Save'); + return ( + - - -); + + + + + + + ); +}; MultiStepFooter.propTypes = { leftButtonTitle: PropTypes.string, diff --git a/src/web/components/dialog/savedialog.jsx b/src/web/components/dialog/savedialog.jsx index 456f1cf3aa..067c65270e 100644 --- a/src/web/components/dialog/savedialog.jsx +++ b/src/web/components/dialog/savedialog.jsx @@ -17,32 +17,31 @@ */ import React, {useState, useEffect} from 'react'; -import _ from 'gmp/locale'; - -import {isDefined} from 'gmp/utils/identity'; +import {isDefined, isFunction} from 'gmp/utils/identity'; import State from 'web/utils/state'; import PropTypes from 'web/utils/proptypes'; import ErrorBoundary from 'web/components/error/errorboundary'; -import Dialog from 'web/components/dialog/dialog'; -import DialogContent from 'web/components/dialog/content'; -import DialogError from 'web/components/dialog/error'; -import DialogFooter from 'web/components/dialog/twobuttonfooter'; -import DialogTitle from 'web/components/dialog/title'; -import MultiStepFooter from 'web/components/dialog/multistepfooter'; -import ScrollableContent from 'web/components/dialog/scrollablecontent'; +import useTranslation from 'web/hooks/useTranslation'; + +import Dialog from './dialog'; +import DialogContent from './content'; +import DialogError from './error'; +import DialogFooter from './twobuttonfooter'; +import MultiStepFooter from './multistepfooter'; const SaveDialogContent = ({ - onSave, error, multiStep = 0, onError, - onErrorClose = undefined, + onErrorClose, + onSave, ...props }) => { - const [loading, setLoading] = useState(false); + const [_] = useTranslation(); + const [isLoading, setLoading] = useState(false); const [stateError, setStateError] = useState(undefined); const [currentStep, setCurrentStep] = useState(0); @@ -76,12 +75,12 @@ const SaveDialogContent = ({ }; const handleSaveClick = state => { - if (onSave && !loading) { + if (onSave && !isLoading) { const promise = onSave(state); if (isDefined(promise)) { setLoading(true); // eslint-disable-next-line no-shadow - promise.catch(error => setError(error)); + return promise.catch(error => setError(error)); } } }; @@ -94,16 +93,7 @@ const SaveDialogContent = ({ } }; - const { - buttonTitle, - children, - close, - defaultValues, - moveProps, - heightProps, - title, - values, - } = props; + const {buttonTitle, children, close, defaultValues, values} = props; return ( @@ -111,26 +101,22 @@ const SaveDialogContent = ({ const childValues = {...state, ...values}; return ( - {stateError && ( )} - - {children({ - currentStep, - values: childValues, - onStepChange: handleStepChange, - onValueChange, - })} - + {isFunction(children) + ? children({ + currentStep, + values: childValues, + onStepChange: handleStepChange, + onValueChange, + }) + : children} {multiStep > 0 ? ( ) : ( handleSaveClick(childValues)} @@ -164,15 +150,13 @@ const SaveDialogContent = ({ SaveDialogContent.propTypes = { buttonTitle: PropTypes.string, + children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), close: PropTypes.func.isRequired, defaultValues: PropTypes.object, error: PropTypes.string, - heightProps: PropTypes.object, - moveProps: PropTypes.object, multiStep: PropTypes.number, nextDisabled: PropTypes.bool, prevDisabled: PropTypes.bool, - title: PropTypes.string.isRequired, values: PropTypes.object, onError: PropTypes.func, onErrorClose: PropTypes.func, @@ -181,59 +165,45 @@ SaveDialogContent.propTypes = { }; const SaveDialog = ({ - buttonTitle = _('Save'), + buttonTitle, children, defaultValues, error, - initialHeight, - minHeight, - minWidth, multiStep = 0, title, values, - width, + width = 'lg', onClose, onError, onErrorClose, onSave, }) => { + const [_] = useTranslation(); + buttonTitle = buttonTitle || _('Save'); return ( - - {({close, moveProps, heightProps}) => ( - - {children} - - )} + + + {children} + ); }; SaveDialog.propTypes = { buttonTitle: PropTypes.string, + children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), defaultValues: PropTypes.object, // default values for uncontrolled values error: PropTypes.string, // for errors controlled from parent (onErrorClose must be used if set) - initialHeight: PropTypes.string, - minHeight: PropTypes.numberOrNumberString, - minWidth: PropTypes.numberOrNumberString, multiStep: PropTypes.number, title: PropTypes.string.isRequired, values: PropTypes.object, // should be used for controlled values diff --git a/src/web/components/dialog/twobuttonfooter.jsx b/src/web/components/dialog/twobuttonfooter.jsx index 023021ec72..956ad13f93 100644 --- a/src/web/components/dialog/twobuttonfooter.jsx +++ b/src/web/components/dialog/twobuttonfooter.jsx @@ -18,19 +18,13 @@ import React from 'react'; -import styled from 'styled-components'; - import _ from 'gmp/locale'; import PropTypes from 'web/utils/proptypes'; import {DialogFooterLayout} from 'web/components/dialog/footer'; -import Button from './button'; - -const StyledLayout = styled(DialogFooterLayout)` - justify-content: space-between; -`; +import Button from 'web/components/form/button'; const DialogTwoButtonFooter = ({ leftButtonTitle = _('Cancel'), @@ -38,28 +32,29 @@ const DialogTwoButtonFooter = ({ onLeftButtonClick, onRightButtonClick, loading = false, + isLoading = loading, }) => ( - + - + ); DialogTwoButtonFooter.propTypes = { + isLoading: PropTypes.bool, leftButtonTitle: PropTypes.string, loading: PropTypes.bool, rightButtonTitle: PropTypes.string.isRequired,