pgadmin4/web/pgadmin/static/js/helpers/ModalProvider.jsx

336 lines
11 KiB
JavaScript

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { Box, Dialog, DialogContent, DialogTitle, Paper } from '@mui/material';
import React, { useState, useMemo } from 'react';
import { getEpoch } from 'sources/utils';
import { DefaultButton, PgIconButton, PrimaryButton } from '../components/Buttons';
import Draggable from 'react-draggable';
import CloseIcon from '@mui/icons-material/CloseRounded';
import CustomPropTypes from '../custom_prop_types';
import PropTypes from 'prop-types';
import gettext from 'sources/gettext';
import HTMLReactParser from 'html-react-parser';
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
import { Rnd } from 'react-rnd';
import { ExpandDialogIcon, MinimizeDialogIcon } from '../components/ExternalIcon';
import { styled } from '@mui/material/styles';
export const ModalContext = React.createContext({});
const MIN_HEIGHT = 190;
const MIN_WIDTH = 500;
const StyledBox = styled(Box)(({theme}) => ({
'& .Alert-footer': {
display: 'flex',
justifyContent: 'flex-end',
padding: '0.5rem',
...theme.mixins.panelBorder.top,
},
'& .Alert-margin': {
marginLeft: '0.25rem',
},
}));
export function useModal() {
return React.useContext(ModalContext);
}
function renderExtraButtons(button) {
switch(button.type) {
case 'primary':
return <PrimaryButton className='Alert-margin' startIcon={button.icon} onClick={button.onclick}>{button.label}</PrimaryButton>;
case 'default':
return <DefaultButton className='Alert-margin' startIcon={button.icon} onClick={button.onclick}>{button.label}</DefaultButton>;
default:
return <DefaultButton className='Alert-margin' startIcon={button.icon} onClick={button.onclick}>{button.label}</DefaultButton>;
};
}
function AlertContent({ text, confirm, okLabel = gettext('OK'), cancelLabel = gettext('Cancel'), onOkClick, onCancelClick, extraButtons }) {
return (
<StyledBox display="flex" flexDirection="column" height="100%">
<Box flexGrow="1" p={2}>{typeof (text) == 'string' ? HTMLReactParser(text) : text}</Box>
<Box className='Alert-footer'>
{confirm &&
<DefaultButton startIcon={<CloseIcon />} onClick={onCancelClick} >{cancelLabel}</DefaultButton>
}
{
extraButtons?.length ?
extraButtons.map(button=>renderExtraButtons(button))
:
<PrimaryButton className='Alert-margin' startIcon={<CheckRoundedIcon />} onClick={onOkClick} autoFocus={true} >{okLabel}</PrimaryButton>
}
</Box>
</StyledBox>
);
}
AlertContent.propTypes = {
text: PropTypes.string,
confirm: PropTypes.bool,
onOkClick: PropTypes.func,
onCancelClick: PropTypes.func,
okLabel: PropTypes.string,
cancelLabel: PropTypes.string,
extraButtons: PropTypes.array
};
function alert(title, text, onOkClick, okLabel = gettext('OK')) {
// bind the modal provider before calling
this.showModal(title, (closeModal) => {
const onOkClickClose = () => {
onOkClick?.();
closeModal();
};
return (
<AlertContent text={text} onOkClick={onOkClickClose} okLabel={okLabel} />
);
});
}
function confirm(title, text, onOkClick, onCancelClick, okLabel = gettext('Yes'), cancelLabel = gettext('No'), extras = null) {
// bind the modal provider before calling
this.showModal(title, (closeModal) => {
const onCancelClickClose = () => {
onCancelClick?.();
closeModal();
};
const onOkClickClose = () => {
onOkClick?.();
closeModal();
};
const extraButtons = extras?.(closeModal);
return (
<AlertContent text={text} confirm onOkClick={onOkClickClose} onCancelClick={onCancelClickClose} okLabel={okLabel} cancelLabel={cancelLabel} extraButtons={extraButtons} />
);
});
}
export default function ModalProvider({ children }) {
const [modals, setModals] = React.useState([]);
const showModal = (title, content, modalOptions) => {
let id = getEpoch().toString() + crypto.getRandomValues(new Uint8Array(4));
setModals((prev) => [...prev, {
id: id,
title: title,
content: content,
...modalOptions,
}]);
};
const closeModal = (id) => {
setModals((prev) => {
return prev.filter((o) => o.id != id);
});
};
const fullScreenModal = (fullScreen) => {
setModals((prev) => [...prev, {
fullScreen: fullScreen,
}]);
};
const modalContextBase = {
showModal: showModal,
closeModal: closeModal,
fullScreenModal: fullScreenModal
};
const modalContext = React.useMemo(() => ({
...modalContextBase,
confirm: confirm.bind(modalContextBase),
alert: alert.bind(modalContextBase)
}), []);
return (
<ModalContext.Provider value={modalContext}>
{children}
{modals.map((modalOptions) => (
<ModalContainer key={modalOptions.id} {...modalOptions} />
))}
</ModalContext.Provider>
);
}
ModalProvider.propTypes = {
children: CustomPropTypes.children,
};
const StyledRnd = styled(Rnd)(({theme}) => ({
'&.Dialog-content': {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid ' + theme.otherVars.inputBorderColor,
borderRadius: theme.shape.borderRadius,
},
'&.Dialog-fullScreen': {
transform: 'none !important'
},
}));
function checkIsResizable(props) {
return props.isresizeable == 'true';
}
function setEnableResizing(props, resizeable) {
return props.isfullscreen == 'true' ? false : resizeable;
}
function PaperComponent({minHeight, minWidth, ...props}) {
let [dialogPosition, setDialogPosition] = useState(null);
let resizeable = checkIsResizable(props);
const setConditionalPosition = () => {
return props.isfullscreen == 'true' ? { x: 0, y: 0 } : dialogPosition && { x: dialogPosition.x, y: dialogPosition.y };
};
const y_position = window.innerHeight*0.02; // 2% of total height
const x_position = props.width ? (window.innerWidth/2) - (props.width/2) : (window.innerWidth/2) - (MIN_WIDTH/2);
return (
props.isresizeable == 'true' ?
<StyledRnd
size={props.isfullscreen == 'true' && { width: '100%', height: '100%' }}
className={'Dialog-content ' + ( props.isfullscreen == 'true' ? 'Dialog-fullScreen' : '')}
default={{
x: x_position,
y: y_position,
...(props.width && { width: props.width }),
...(props.height && { height: props.height }),
}}
minWidth = { minWidth || MIN_WIDTH }
minHeight = { minHeight || MIN_HEIGHT }
// {...(props.width && { minWidth: MIN_WIDTH })}
// {...(props.height && { minHeight: MIN_HEIGHT })}
bounds="window"
enableResizing={setEnableResizing(props, resizeable)}
position={setConditionalPosition()}
onDragStop={(e, position) => {
if (props.isfullscreen !== 'true') {
setDialogPosition({
...position,
});
}
}}
onResize={(e, direction, ref, delta, position) => {
setDialogPosition({
...position,
});
}}
dragHandleClassName="modal-drag-area"
>
<Paper {...props} style={{ width: '100%', height: '100%', maxHeight: '100%', maxWidth: '100%' }} />
</StyledRnd>
:
<Draggable cancel={'[class*="MuiDialogContent-root"]'}>
<Paper {...props} style={{ minWidth: '600px' }} />
</Draggable>
);
}
PaperComponent.propTypes = {
isfullscreen: PropTypes.string,
isresizeable: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
minWidth: PropTypes.number,
minHeight: PropTypes.number,
};
const StyleDialog = styled(Dialog)(({theme}) => ({
'& .Modal-container': {
backgroundColor: theme.palette.background.default
},
'& .Modal-titleBar': {
display: 'flex',
flexGrow: 1
},
'& .Modal-icon': {
fill: 'currentColor',
width: '1em',
height: '1em',
display: 'inline-block',
fontSize: '1.5rem',
transition: 'none',
flexShrink: 0,
userSelect: 'none',
},
'& .Modal-footer': {
display: 'flex',
justifyContent: 'flex-end',
padding: '0.5rem',
...theme.mixins.panelBorder?.top,
},
'& .Modal-iconButtonStyle': {
marginLeft: 'auto',
marginRight: '4px'
},
}));
function ModalContainer({ id, title, content, dialogHeight, dialogWidth, onClose, fullScreen = false, isFullWidth = false, showFullScreen = false, isResizeable = false, minHeight = MIN_HEIGHT, minWidth = MIN_WIDTH, showTitle=true }) {
let useModalRef = useModal();
let closeModal = (_e, reason) => {
if(reason == 'backdropClick' && showTitle) {
return;
}
useModalRef.closeModal(id);
if(reason == 'escapeKeyDown' || reason == undefined) {
onClose?.();
}
};
const [isFullScreen, setIsFullScreen] = useState(fullScreen);
return (
<StyleDialog
open={true}
onClose={closeModal}
PaperComponent={PaperComponent}
PaperProps={{ 'isfullscreen': isFullScreen.toString(), 'isresizeable': isResizeable.toString(), width: dialogWidth, height: dialogHeight, minHeight: minHeight, minWidth: minWidth }}
fullScreen={isFullScreen}
fullWidth={isFullWidth}
disablePortal
>
{ showTitle &&
<DialogTitle className='modal-drag-area'>
<Box className='Modal-titleBar'>
<Box sx={{ marginRight:'0.25rem', flexGrow: 1}}>{title}</Box>
{
showFullScreen && !isFullScreen &&
<Box className='Modal-iconButtonStyle'><PgIconButton title={gettext('Maximize')} icon={<ExpandDialogIcon className='Modal-icon' />} size="xs" noBorder onClick={() => { setIsFullScreen(!isFullScreen); }} /></Box>
}
{
showFullScreen && isFullScreen &&
<Box className='Modal-iconButtonStyle'><PgIconButton title={gettext('Minimize')} icon={<MinimizeDialogIcon className='Modal-icon' />} size="xs" noBorder onClick={() => { setIsFullScreen(!isFullScreen); }} /></Box>
}
<Box marginLeft="auto"><PgIconButton title={gettext('Close')} icon={<CloseIcon />} size="xs" noBorder onClick={closeModal} /></Box>
</Box>
</DialogTitle>
}
<DialogContent height="100%">
{useMemo(()=>{ return content(closeModal); }, [])}
</DialogContent>
</StyleDialog>
);
}
ModalContainer.propTypes = {
id: PropTypes.string,
title: CustomPropTypes.children,
content: PropTypes.func,
fullScreen: PropTypes.bool,
isFullWidth: PropTypes.bool,
showFullScreen: PropTypes.bool,
isResizeable: PropTypes.bool,
dialogHeight: PropTypes.number,
dialogWidth: PropTypes.number,
onClose: PropTypes.func,
minWidth: PropTypes.number,
minHeight: PropTypes.number,
showTitle: PropTypes.bool,
};