pgadmin4/web/pgadmin/static/js/helpers/Layout.jsx
Aditya Toshniwal b5b9ee46a1 1) Port query tool to React. Fixes #6131
2) Added status bar to the Query Tool. Fixes #3253
3) Ensure that row numbers should be visible in view when scrolling horizontally. Fixes #3989
4) Allow removing a single query history. Refs #4113
5) Partially fixed Macros usability issues. Ref #6969
6) Fixed an issue where the Query tool opens on minimum size if the user opens multiple query tool Window quickly. Fixes #6725
7) Relocate GIS Viewer Button to the Left Side of the Results Table. Fixes #6830
8) Fixed an issue where the connection bar is not visible. Fixes #7188
9) Fixed an issue where an Empty message popup after running a query. Fixes #7260
10) Ensure that Autocomplete should work after changing the connection. Fixes #7262
11) Fixed an issue where the copy and paste row does not work if the first column contains no data. Fixes #7294
2022-04-07 17:36:56 +05:30

315 lines
8.2 KiB
JavaScript

import React, { useRef, useMemo } from 'react';
import DockLayout from 'rc-dock';
import { makeStyles } from '@material-ui/styles';
import PropTypes from 'prop-types';
import EventBus from './EventBus';
import getApiInstance from '../api_instance';
import url_for from 'sources/url_for';
import { PgIconButton } from '../components/Buttons';
import CloseIcon from '@material-ui/icons/CloseRounded';
import gettext from 'sources/gettext';
import {ExpandDialogIcon, MinimizeDialogIcon } from '../components/ExternalIcon';
const useStyles = makeStyles((theme)=>({
docklayout: {
height: '100%',
'& .dock-tab-active': {
color: theme.otherVars.activeColor,
'&::hover': {
color: theme.otherVars.activeColor,
}
},
'& .dock-ink-bar': {
height: '3px',
backgroundColor: theme.otherVars.activeBorder,
color: theme.otherVars.activeColor,
'&.dock-ink-bar-animated': {
transition: 'none !important',
}
},
'& .dock-bar': {
paddingLeft: 0,
backgroundColor: theme.palette.background.default,
...theme.mixins.panelBorder.bottom,
'& .dock-nav-wrap': {
cursor: 'move',
}
},
'& .dock-panel': {
border: 'none',
'&.dragging': {
opacity: 0.6,
pointerEvents: 'visible',
},
'& .dock': {
borderRadius: 'inherit',
},
'&.dock-style-dialogs': {
borderRadius: theme.shape.borderRadius,
'&.dock-panel.dragging': {
opacity: 1,
},
'& .dock-ink-bar': {
height: '0px',
},
'& .dock-panel-drag-size-b-r': {
zIndex: 1020,
},
'& .dock-tab-active': {
color: theme.palette.text.primary,
fontWeight: 'bold',
'&::hover': {
color: theme.palette.text.primary,
}
},
},
'& .dock-tabpane': {
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}
},
'& .dock-tab': {
minWidth: 'unset',
borderBottom: 'none',
marginRight: 0,
background: 'unset',
fontWeight: 'unset',
color: theme.palette.text.primary,
'&::hover': {
color: 'unset',
},
'& > div': {
padding: '4px 10px',
},
'& .drag-initiator': {
display: 'flex',
'& .dock-tab-close-btn': {
color: theme.palette.text.primary,
position: 'unset',
marginLeft: '8px',
fontSize: '18px',
transition: 'none',
'&::before': {
content: '"\\00d7"',
position: 'relative',
top: '-5px',
}
}
}
},
'& .dock-extra-content': {
alignItems: 'center',
paddingRight: '10px',
},
'& .dock-vbox, & .dock-hbox .dock-vbox': {
'& .dock-divider': {
flexBasis: '1px',
transform: 'scaleY(8)',
'&::before': {
backgroundColor: theme.otherVars.borderColor,
display: 'block',
content: '""',
width: '100%',
transform: 'scaleY(0.125)',
height: '1px',
}
}
},
'& .dock-hbox, & .dock-vbox .dock-hbox': {
'& .dock-divider': {
flexBasis: '1px',
transform: 'scaleX(8)',
'&::before': {
backgroundColor: theme.otherVars.borderColor,
display: 'block',
content: '""',
height: '100%',
transform: 'scaleX(0.125)',
width: '1px',
}
}
},
'& .dock-content-animated': {
transition: 'none',
},
'& .dock-fbox': {
zIndex: 1060,
},
'& .dock-mbox': {
zIndex: 1080,
},
'& .drag-accept-reject::after': {
content: '',
}
}
}));
export const LayoutEventsContext = React.createContext();
export class LayoutHelper {
static getPanel(attrs) {
return {
cached: true,
group: 'default',
...attrs,
};
}
static close(docker, panelId) {
docker?.dockMove(docker.find(panelId), 'remove');
}
static focus(docker, panelId) {
docker?.updateTab(panelId, null, true);
}
static openDialog(docker, panelData, width=500, height=300) {
let panel = docker.find(panelData.id);
if(panel) {
docker.dockMove(panel, null, 'front');
} else {
let {width: lw, height: lh} = docker.getLayoutSize();
lw = (lw - width)/2;
lh = (lh - height)/2;
docker.dockMove({
x: lw,
y: lh,
w: width,
h: height,
tabs: [LayoutHelper.getPanel({
...panelData,
group: 'dialogs',
closable: false,
})],
}, null, 'float');
}
}
static isTabOpen(docker, panelId) {
return Boolean(docker.find(panelId));
}
static openTab(docker, panelData, refTabId, direction, forceRerender=false) {
let panel = docker.find(panelData.id);
if(panel) {
if(forceRerender) {
docker.updateTab(panelData.id, LayoutHelper.getPanel(panelData), true);
} else {
LayoutHelper.focus(docker, panelData.id);
}
} else {
let tgtPanel = docker.find(refTabId);
docker.dockMove(LayoutHelper.getPanel(panelData), tgtPanel, direction);
}
}
}
function saveLayout(layoutObj, layoutId) {
let api = getApiInstance();
if(!layoutId || !layoutObj) {
return;
}
const formData = new FormData();
formData.append('setting', layoutId);
formData.append('value', JSON.stringify(layoutObj.saveLayout()));
api.post(url_for('settings.store_bulk'), formData)
.catch(()=>{/* No need to throw error */});
}
function getDialogsGroup() {
return {
disableDock: true,
tabLocked: true,
floatable: 'singleTab',
panelExtra: (panelData, context) => (
<div>
<PgIconButton title={gettext('Close')} icon={<CloseIcon />} size="xs" noBorder onClick={()=>{
context.dockMove(panelData, null, 'remove');
}} />
</div>
)
};
}
function getDefaultGroup() {
return {
maximizable: false,
panelExtra: (panelData, context) => {
let icon = <ExpandDialogIcon style={{width: '0.7em'}}/>;
let title = gettext('Maximise');
if(panelData?.parent?.mode == 'maximize') {
icon = <MinimizeDialogIcon />;
title = gettext('Restore');
}
return <div>
<PgIconButton title={title} icon={icon} size="xs" noBorder onClick={()=>{
context.dockMove(panelData, null, 'maximize');
}} />
</div>;
}
};
}
export default function Layout({groups, getLayoutInstance, layoutId, savedLayout, ...props}) {
const classes = useStyles();
const layoutObj = useRef();
const defaultGroups = React.useMemo(()=>({
'dialogs': getDialogsGroup(),
'default': getDefaultGroup(),
...groups,
}), [groups]);
const layoutEventBus = React.useRef(new EventBus());
return useMemo(()=>(
<div className={classes.docklayout}>
<LayoutEventsContext.Provider value={layoutEventBus.current}>
<DockLayout
style={{
height: '100%',
}}
ref={(obj)=>{
layoutObj.current = obj;
if(layoutObj.current) {
layoutObj.current.resetLayout = ()=>{
layoutObj.current.loadLayout(props.defaultLayout);
saveLayout(layoutObj.current, layoutId);
};
}
getLayoutInstance?.(layoutObj.current);
try {
layoutObj.current?.loadLayout(JSON.parse(savedLayout));
} catch {
/* Fallback to default */
layoutObj.current?.loadLayout(props.defaultLayout);
}
}}
groups={defaultGroups}
onLayoutChange={(_l, currentTabId, direction)=>{
saveLayout(layoutObj.current, layoutId);
if(Object.values(LAYOUT_EVENTS).indexOf(direction) > -1) {
layoutEventBus.current.fireEvent(LAYOUT_EVENTS[direction.toUpperCase()], currentTabId);
}
}}
{...props}
/>
</LayoutEventsContext.Provider>
</div>
), []);
}
Layout.propTypes = {
groups: PropTypes.object,
getLayoutInstance: PropTypes.func,
};
export const LAYOUT_EVENTS = {
ACTIVE: 'active',
REMOVE: 'remove',
FLOAT: 'float',
FRONT: 'front',
MAXIMIZE: 'maximize',
MOVE: 'move',
};