Implemented React based modal provider to create models on the fly.

Also, use this to replace Alertify alert and confirm dialog.
This commit is contained in:
Aditya Toshniwal 2021-12-03 12:50:24 +05:30 committed by Akshay Joshi
parent 9c0c046a38
commit dfdaf7f6d1
7 changed files with 262 additions and 16 deletions

View File

@ -144,6 +144,7 @@
"react": "^17.0.1",
"react-aspen": "^1.1.0",
"react-dom": "^17.0.1",
"react-draggable": "^4.4.4",
"react-select": "^4.2.1",
"react-table": "^7.6.3",
"react-virtualized-auto-sizer": "^1.0.6",

View File

@ -167,7 +167,7 @@ basicSettings = createMuiTheme(basicSettings, {
top: 0,
zIndex: 9999,
}
}
},
},
transitions: {
duration: {
@ -199,6 +199,9 @@ basicSettings = createMuiTheme(basicSettings, {
},
MuiCheckbox: {
disableTouchRipple: true,
},
MuiDialogTitle: {
disableTypography: true,
}
},
});
@ -390,6 +393,22 @@ function getFinalTheme(baseTheme) {
color: baseTheme.palette.text.muted,
},
},
MuiDialogContent: {
root: {
padding: 0,
userSelect: 'text',
}
},
MuiDialogTitle: {
root: {
fontWeight: 'bold',
padding: '5px 10px',
cursor: 'move',
display: 'flex',
alignItems: 'center',
...mixins.panelBorder.bottom,
}
},
}
}, baseTheme);
}

View File

@ -15,6 +15,12 @@ import CustomPropTypes from '../custom_prop_types';
const useStyles = makeStyles((theme)=>({
primaryButton: {
'&.MuiButton-outlinedSizeSmall': {
height: '28px',
'& .MuiSvgIcon-root': {
height: '0.8em',
}
},
'&.Mui-disabled': {
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.primary.disabledMain,
@ -28,6 +34,13 @@ const useStyles = makeStyles((theme)=>({
backgroundColor: theme.palette.default.main,
color: theme.palette.default.contrastText,
border: '1px solid '+theme.palette.default.borderColor,
whiteSpace: 'nowrap',
'&.MuiButton-outlinedSizeSmall': {
height: '28px',
'& .MuiSvgIcon-root': {
height: '0.8em',
}
},
'&.Mui-disabled': {
color: theme.palette.default.disabledContrastText,
borderColor: theme.palette.default.disabledBorderColor
@ -52,35 +65,59 @@ const useStyles = makeStyles((theme)=>({
backgroundColor: theme.custom.icon.hoverMain,
color: theme.custom.icon.hoverContrastText,
}
},
xsButton: {
padding: '2px 1px',
height: '24px',
'& .MuiSvgIcon-root': {
height: '0.8em',
}
},
noBorder: {
border: 0,
}
}));
/* pgAdmin primary button */
export const PrimaryButton = forwardRef((props, ref)=>{
let {children, className, ...otherProps} = props;
let {children, className, size, noBorder, ...otherProps} = props;
const classes = useStyles();
let allClassName = [classes.primaryButton, className];
if(size == 'xs') {
size = undefined;
allClassName.push(classes.xsButton);
}
noBorder && allClassName.push(classes.noBorder);
return (
<Button ref={ref} variant="contained" color="primary" className={clsx(classes.primaryButton, className)} {...otherProps}>{children}</Button>
<Button ref={ref} variant="contained" color="primary" size={size} className={clsx(allClassName)} {...otherProps}>{children}</Button>
);
});
PrimaryButton.displayName = 'PrimaryButton';
PrimaryButton.propTypes = {
size: PropTypes.string,
noBorder: PropTypes.bool,
children: CustomPropTypes.children,
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
};
/* pgAdmin default button */
export const DefaultButton = forwardRef((props, ref)=>{
let {children, className, ...otherProps} = props;
let {children, className, size, noBorder, ...otherProps} = props;
const classes = useStyles();
let allClassName = [classes.defaultButton, className];
if(size == 'xs') {
size = undefined;
allClassName.push(classes.xsButton);
}
noBorder && allClassName.push(classes.noBorder);
return (
<Button ref={ref} variant="outlined" color="default" className={clsx(classes.defaultButton, className)} {...otherProps}>{children}</Button>
<Button ref={ref} variant="outlined" color="default" size={size} className={clsx(allClassName)} {...otherProps}>{children}</Button>
);
});
DefaultButton.displayName = 'DefaultButton';
DefaultButton.propTypes = {
size: PropTypes.string,
noBorder: PropTypes.bool,
children: CustomPropTypes.children,
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
};

View File

@ -0,0 +1,92 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { Box, Dialog, DialogContent, DialogTitle, Paper } from '@material-ui/core';
import React from 'react';
import {getEpoch} from 'sources/utils';
import { PgIconButton } from '../components/Buttons';
import Draggable from 'react-draggable';
import CloseIcon from '@material-ui/icons/CloseRounded';
import CustomPropTypes from '../custom_prop_types';
import PropTypes from 'prop-types';
const ModalContext = React.createContext({});
export function useModal() {
return React.useContext(ModalContext);
}
export default function ModalProvider({children}) {
const [modals, setModals] = React.useState([]);
const showModal = (title, content, modalOptions)=>{
let id = getEpoch().toString() + Math.random();
setModals((prev)=>[...prev, {
id: id,
title: title,
content: content,
...modalOptions,
}]);
};
const closeModal = (id)=>{
setModals((prev)=>{
return prev.filter((o)=>o.id!=id);
});
};
const modalContext = React.useMemo(()=>({
showModal: showModal,
closeModal: closeModal,
}), []);
return (
<ModalContext.Provider value={modalContext}>
{children}
{modals.map((modalOptions, i)=>(
<ModalContainer key={i} {...modalOptions}/>
))}
</ModalContext.Provider>
);
}
ModalProvider.propTypes = {
children: CustomPropTypes.children,
};
function PaperComponent(props) {
return (
<Draggable cancel={'[class*="MuiDialogContent-root"]'}>
<Paper {...props} style={{minWidth: '600px'}} />
</Draggable>
);
}
function ModalContainer({id, title, content}) {
let useModalRef = useModal();
let closeModal = ()=>useModalRef.closeModal(id);
return (
<Dialog
open={true}
onClose={closeModal}
PaperComponent={PaperComponent}
disableBackdropClick
>
<DialogTitle>
<Box marginRight="0.25rem">{title}</Box>
<Box marginLeft="auto"><PgIconButton icon={<CloseIcon />} size="xs" noBorder onClick={closeModal}/></Box>
</DialogTitle>
<DialogContent>
{content(closeModal)}
</DialogContent>
</Dialog>
);
}
ModalContainer.propTypes = {
id: PropTypes.string,
title: CustomPropTypes.children,
content: CustomPropTypes.children,
};

View File

@ -16,30 +16,54 @@ import CustomPropTypes from '../custom_prop_types';
import gettext from 'sources/gettext';
import pgWindow from 'sources/window';
import Alertify from 'pgadmin.alertifyjs';
import ModalProvider, { useModal } from './ModalProvider';
import { DefaultButton, PrimaryButton } from '../components/Buttons';
import { Box } from '@material-ui/core';
import { makeStyles } from '@material-ui/styles';
import HTMLReactParse from 'html-react-parser';
import CloseIcon from '@material-ui/icons/CloseRounded';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import PropTypes from 'prop-types';
const AUTO_HIDE_DURATION = 3000; // In milliseconds
let snackbarRef;
function SnackbarUtilsConfigurator() {
snackbarRef = useSnackbar();
return <></>;
}
let notifierInitialized = false;
export function initializeNotifier(notifierContainer) {
notifierInitialized = true;
const RefLoad = ()=>{
snackbarRef = useSnackbar();
return <></>;
};
ReactDOM.render(
<Theme>
<SnackbarProvider
maxSnack={30}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}>
<SnackbarUtilsConfigurator />
<RefLoad />
</SnackbarProvider>
</Theme>, notifierContainer
);
}
export const FinalNotifyContent = React.forwardRef(({children}, ref) => {
let modalRef;
let modalInitialized = false;
export function initializeModalProvider(modalContainer) {
modalInitialized = true;
const RefLoad = ()=>{
modalRef = useModal();
return <></>;
};
ReactDOM.render(
<Theme>
<ModalProvider>
<RefLoad />
</ModalProvider>
</Theme>, modalContainer
);
}
const FinalNotifyContent = React.forwardRef(({children}, ref) => {
return <SnackbarContent style= {{justifyContent:'end'}} ref={ref}>{children}</SnackbarContent>;
});
FinalNotifyContent.displayName = 'FinalNotifyContent';
@ -47,6 +71,38 @@ FinalNotifyContent.propTypes = {
children: CustomPropTypes.children,
};
const useAlertStyles = makeStyles((theme)=>({
footer: {
display: 'flex',
justifyContent: 'flex-end',
padding: '0.5rem',
...theme.mixins.panelBorder.top,
},
margin: {
marginLeft: '0.25rem',
}
}));
function AlertContent({text, confirm, onOkClick, onCancelClick}) {
const classes = useAlertStyles();
return (
<Box display="flex" flexDirection="column" height="100%">
<Box flexGrow="1" p={2}>{HTMLReactParse(text)}</Box>
<Box className={classes.footer}>
<DefaultButton startIcon={<CloseIcon />} onClick={onCancelClick} autoFocus={!confirm}>Close</DefaultButton>
{confirm &&
<PrimaryButton className={classes.margin} startIcon={<CheckRoundedIcon />} onClick={onOkClick} autoFocus={confirm}>OK</PrimaryButton>
}
</Box>
</Box>
);
}
AlertContent.propTypes = {
text: PropTypes.string,
confirm: PropTypes.bool,
onOkClick: PropTypes.func,
onCancelClick: PropTypes.func,
};
var Notifier = {
success(msg, autoHideDuration = AUTO_HIDE_DURATION) {
this._callNotify(msg, MESSAGE_TYPE.SUCCESS, autoHideDuration);
@ -170,7 +226,39 @@ var Notifier = {
Alertify.alert().show().set(
'message', msg.replace(new RegExp(/\r?\n/, 'g'), '<br />')
).set('title', promptmsg).set('closable', true);
}
},
alert: (title, text, onCancelClick)=>{
if(!modalInitialized) {
initializeModalProvider(document.getElementById('modalContainer'));
}
modalRef.showModal(title, (closeModal)=>{
const onCancelClickClose = ()=>{
onCancelClick && onCancelClick();
closeModal();
};
return (
<AlertContent text={text} onCancelClick={onCancelClickClose} />
);
});
},
confirm: (title, text, onOkClick, onCancelClick)=>{
if(!modalInitialized) {
initializeModalProvider(document.getElementById('modalContainer'));
}
modalRef.showModal(title, (closeModal)=>{
const onCancelClickClose = ()=>{
onCancelClick && onCancelClick();
closeModal();
};
const onOkClickClose = ()=>{
onOkClick && onOkClick();
closeModal();
};
return (
<AlertContent text={text} confirm onOkClick={onOkClickClose} onCancelClick={onCancelClickClose} />
);
});
},
};
if(window.frameElement) {

View File

@ -75,6 +75,7 @@
{% block body %}{% endblock %}
<div id="notifierContainer"></div>
<div id="modalContainer"></div>
<script type="application/javascript">
{% block init_script %}{% endblock %}
</script>

View File

@ -2907,7 +2907,7 @@ closest@^0.0.1:
dependencies:
matches-selector "0.0.1"
clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0:
clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0, clsx@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
@ -7782,6 +7782,14 @@ react-dom@^17.0.1:
object-assign "^4.1.1"
scheduler "^0.20.2"
react-draggable@^4.4.4:
version "4.4.4"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.4.tgz#5b26d9996be63d32d285a426f41055de87e59b2f"
integrity sha512-6e0WdcNLwpBx/YIDpoyd2Xb04PB0elrDrulKUgdrIlwuYvxh5Ok9M+F8cljm8kPXXs43PmMzek9RrB1b7mLMqA==
dependencies:
clsx "^1.1.1"
prop-types "^15.6.0"
react-input-autosize@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85"