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
This commit is contained in:
Aditya Toshniwal
2022-04-07 17:36:56 +05:30
committed by Akshay Joshi
parent bf8e569bde
commit b5b9ee46a1
213 changed files with 11134 additions and 18830 deletions

View File

@@ -42,6 +42,7 @@ const useStyles = makeStyles((theme)=>({
}
},
iconButton: {
minWidth: 0,
padding: '3px 6px',
'&.MuiButton-sizeSmall, &.MuiButton-outlinedSizeSmall, &.MuiButton-containedSizeSmall': {
padding: '1px 4px',
@@ -59,6 +60,7 @@ const useStyles = makeStyles((theme)=>({
'&:hover': {
backgroundColor: theme.custom.icon.hoverMain,
color: theme.custom.icon.hoverContrastText,
borderColor: theme.custom.icon.borderColor,
},
},
splitButton: {
@@ -72,6 +74,7 @@ const useStyles = makeStyles((theme)=>({
xsButton: {
padding: '2px 1px',
height: '24px',
minWidth: '24px',
'& .MuiSvgIcon-root': {
height: '0.8em',
}
@@ -127,27 +130,29 @@ DefaultButton.propTypes = {
/* pgAdmin Icon button, takes Icon component as input */
export const PgIconButton = forwardRef(({icon, title, shortcut, accessKey, className, splitButton, style, color, ...props}, ref)=>{
export const PgIconButton = forwardRef(({icon, title, shortcut, className, splitButton, style, color, accesskey, ...props}, ref)=>{
const classes = useStyles();
let shortcutTitle = null;
if(accessKey || shortcut) {
shortcutTitle = <ShortcutTitle title={title} accessKey={accessKey} shortcut={shortcut}/>;
if(accesskey || shortcut) {
shortcutTitle = <ShortcutTitle title={title} accesskey={accesskey} shortcut={shortcut}/>;
}
/* Tooltip does not work for disabled items */
if(props.disabled) {
if(color == 'primary') {
return (
<PrimaryButton ref={ref} style={{minWidth: 0, ...style}}
className={clsx(classes.iconButton, (splitButton ? classes.splitButton : ''), className)} {...props}>
<PrimaryButton ref={ref} style={style}
className={clsx(classes.iconButton, (splitButton ? classes.splitButton : ''), className)}
accessKey={accesskey} {...props}>
{icon}
</PrimaryButton>
);
} else {
return (
<DefaultButton ref={ref} style={{minWidth: 0, ...style}}
className={clsx(classes.iconButton, classes.iconButtonDefault, (splitButton ? classes.splitButton : ''), className)} {...props}>
<DefaultButton ref={ref} style={style}
className={clsx(classes.iconButton, classes.iconButtonDefault, (splitButton ? classes.splitButton : ''), className)}
accessKey={accesskey} {...props}>
{icon}
</DefaultButton>
);
@@ -156,8 +161,9 @@ export const PgIconButton = forwardRef(({icon, title, shortcut, accessKey, class
if(color == 'primary') {
return (
<Tooltip title={shortcutTitle || title || ''} aria-label={title || ''}>
<PrimaryButton ref={ref} style={{minWidth: 0, ...style}}
className={clsx(classes.iconButton, (splitButton ? classes.splitButton : ''), className)} {...props}>
<PrimaryButton ref={ref} style={style}
className={clsx(classes.iconButton, (splitButton ? classes.splitButton : ''), className)}
accessKey={accesskey} {...props}>
{icon}
</PrimaryButton>
</Tooltip>
@@ -165,8 +171,9 @@ export const PgIconButton = forwardRef(({icon, title, shortcut, accessKey, class
} else {
return (
<Tooltip title={shortcutTitle || title || ''} aria-label={title || ''}>
<DefaultButton ref={ref} style={{minWidth: 0, ...style}}
className={clsx(classes.iconButton, classes.iconButtonDefault, (splitButton ? classes.splitButton : ''), className)} {...props}>
<DefaultButton ref={ref} style={style}
className={clsx(classes.iconButton, classes.iconButtonDefault, (splitButton ? classes.splitButton : ''), className)}
accessKey={accesskey} {...props}>
{icon}
</DefaultButton>
</Tooltip>
@@ -179,7 +186,7 @@ PgIconButton.propTypes = {
icon: CustomPropTypes.children,
title: PropTypes.string.isRequired,
shortcut: CustomPropTypes.shortcut,
accessKey: PropTypes.string,
accesskey: PropTypes.string,
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
style: PropTypes.object,
color: PropTypes.oneOf(['primary', 'default', undefined]),

View File

@@ -8,11 +8,11 @@
//////////////////////////////////////////////////////////////
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {default as OrigCodeMirror} from 'bundled_codemirror';
import OrigCodeMirror from 'bundled_codemirror';
import {useOnScreen} from 'sources/custom_hooks';
import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
import pgAdmin from 'sources/pgadmin';
import pgWindow from 'sources/window';
import gettext from 'sources/gettext';
import { Box, InputAdornment, makeStyles } from '@material-ui/core';
import clsx from 'clsx';
@@ -31,6 +31,11 @@ const useStyles = makeStyles((theme)=>({
root: {
position: 'relative',
},
hideCursor: {
'& .CodeMirror-cursors': {
display: 'none'
}
},
findDialog: {
position: 'absolute',
zIndex: 99,
@@ -70,31 +75,40 @@ function parseQuery(query, useRegex=false, matchCase=false) {
return query;
}
function getRegexFinder(query) {
return (stream) => {
query.lastIndex = stream.pos;
var match = query.exec(stream.string);
if (match && match.index == stream.pos) {
stream.pos += match[0].length || 1;
return 'searching';
} else if (match) {
stream.pos = match.index;
} else {
stream.skipToEnd();
}
};
}
function getPlainStringFinder(query, matchCase) {
return (stream) => {
var matchIndex = (matchCase ? stream.string : stream.string.toLowerCase()).indexOf(query, stream.pos);
if(matchIndex == -1) {
stream.skipToEnd();
} else if(matchIndex == stream.pos) {
stream.pos += query.length;
return 'searching';
} else {
stream.pos = matchIndex;
}
};
}
function searchOverlay(query, matchCase) {
return {
token: typeof query == 'string' ?
(stream) =>{
var matchIndex = (matchCase ? stream.string : stream.string.toLowerCase()).indexOf(query, stream.pos);
if(matchIndex == -1) {
stream.skipToEnd();
} else if(matchIndex == stream.pos) {
stream.pos += query.length;
return 'searching';
} else {
stream.pos = matchIndex;
}
} : (stream) => {
query.lastIndex = stream.pos;
var match = query.exec(stream.string);
if (match && match.index == stream.pos) {
stream.pos += match[0].length || 1;
return 'searching';
} else if (match) {
stream.pos = match.index;
} else {
stream.skipToEnd();
}
}
getPlainStringFinder(query, matchCase) : getRegexFinder(query)
};
}
@@ -260,28 +274,96 @@ FindDialog.propTypes = {
onClose: PropTypes.func,
};
function handleDrop(editor, e) {
var dropDetails = null;
try {
dropDetails = JSON.parse(e.dataTransfer.getData('text'));
/* Stop firefox from redirecting */
if(e.preventDefault) {
e.preventDefault();
}
if (e.stopPropagation) {
e.stopPropagation();
}
} catch(error) {
/* if parsing fails, it must be the drag internal of codemirror text */
return;
}
var cursor = editor.coordsChar({
left: e.x,
top: e.y,
});
editor.replaceRange(dropDetails.text, cursor);
editor.focus();
editor.setSelection({
...cursor,
ch: cursor.ch + dropDetails.cur.from,
},{
...cursor,
ch: cursor.ch +dropDetails.cur.to,
});
}
function calcFontSize(fontSize) {
if(fontSize) {
fontSize = parseFloat((Math.round(parseFloat(fontSize + 'e+2')) + 'e-2'));
let rounded = Number(fontSize);
if(rounded > 0) {
return rounded + 'em';
}
}
return '1em';
}
/* React wrapper for CodeMirror */
export default function CodeMirror({currEditor, name, value, options, events, readonly, disabled, className}) {
export default function CodeMirror({currEditor, name, value, options, events, readonly, disabled, className, autocomplete=false}) {
const taRef = useRef();
const editor = useRef();
const cmWrapper = useRef();
const isVisibleTrack = useRef();
const classes = useStyles();
const [[showFind, isReplace], setShowFind] = useState([false, false]);
const defaultOptions = {
tabindex: '0',
lineNumbers: true,
styleSelectedText: true,
mode: 'text/x-pgsql',
foldOptions: {
widget: '\u2026',
},
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
extraKeys: pgAdmin.Browser.editor_shortcut_keys,
dragDrop: false,
screenReaderLabel: gettext('SQL editor'),
};
const defaultOptions = useMemo(()=>{
let goLeftKey = 'Ctrl-Alt-Left',
goRightKey = 'Ctrl-Alt-Right',
commentKey = 'Ctrl-/';
if(isMac()) {
goLeftKey = 'Cmd-Alt-Left';
goRightKey = 'Cmd-Alt-Right';
commentKey = 'Cmd-/';
}
return {
tabindex: '0',
lineNumbers: true,
styleSelectedText: true,
mode: 'text/x-pgsql',
foldOptions: {
widget: '\u2026',
},
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
extraKeys: {
// Autocomplete sql command
...(autocomplete ? {
'Ctrl-Space': 'autocomplete',
}: {}),
'Alt-Up': 'goLineUp',
'Alt-Down': 'goLineDown',
// Move word by word left/right
[goLeftKey]: 'goGroupLeft',
[goRightKey]: 'goGroupRight',
// Allow user to delete Tab(s)
'Shift-Tab': 'indentLess',
//comment
[commentKey]: 'toggleComment',
},
dragDrop: true,
screenReaderLabel: gettext('SQL editor'),
};
});
useEffect(()=>{
const finalOptions = {...defaultOptions, ...options};
@@ -317,29 +399,61 @@ export default function CodeMirror({currEditor, name, value, options, events, re
setShowFind([false, false]);
setShowFind([true, true]);
}
}
},
'Cmd-G': false,
});
}
Object.keys(events||{}).forEach((eventName)=>{
editor.current.on(eventName, events[eventName]);
});
editor.current.on('drop', handleDrop);
initPreferences();
return ()=>{
editor.current?.toTextArea();
};
}, []);
const initPreferences = ()=>{
reflectPreferences();
pgWindow?.pgAdmin?.Browser?.onPreferencesChange('sqleditor', function() {
reflectPreferences();
});
};
const reflectPreferences = ()=>{
let pref = pgWindow?.pgAdmin?.Browser?.get_preferences_for_module('sqleditor') || {};
let wrapEle = editor?.current.getWrapperElement();
wrapEle && (wrapEle.style.fontSize = calcFontSize(pref.sql_font_size));
if(pref.plain_editor_mode) {
editor?.current.setOption('mode', 'text/plain');
/* Although not required, setting explicitly as codemirror will remove code folding only on next edit */
editor?.current.setOption('foldGutter', false);
} else {
editor?.current.setOption('mode', 'text/x-pgsql');
editor?.current.setOption('foldGutter', pref.code_folding);
}
editor?.current.setOption('indentWithTabs', pref.indent_with_tabs);
editor?.current.setOption('indentUnit', pref.tab_size);
editor?.current.setOption('tabSize', pref.tab_size);
editor?.current.setOption('lineWrapping', pref.wrap_code);
editor?.current.setOption('autoCloseBrackets', pref.insert_pair_brackets);
editor?.current.setOption('matchBrackets', pref.brace_matching);
editor?.current.refresh();
};
useEffect(()=>{
if(editor.current) {
if(disabled) {
cmWrapper.current.classList.add('cm_disabled');
editor.current.setOption('readOnly', 'nocursor');
editor.current.setOption('readOnly', true);
cmWrapper.current.classList.add(classes.hideCursor);
} else if(readonly) {
cmWrapper.current.classList.add('cm_disabled');
editor.current.setOption('readOnly', true);
editor.current.addKeyMap({'Tab': false});
editor.current.addKeyMap({'Shift-Tab': false});
cmWrapper.current.classList.add(classes.hideCursor);
} else {
cmWrapper.current.classList.remove('cm_disabled');
editor.current.setOption('readOnly', false);
@@ -392,4 +506,5 @@ CodeMirror.propTypes = {
readonly: PropTypes.bool,
disabled: PropTypes.bool,
className: CustomPropTypes.className,
autocomplete: PropTypes.bool,
};

View File

@@ -22,17 +22,16 @@ ExternalIcon.propTypes = {
Icon: PropTypes.elementType.isRequired,
};
export const QueryToolIcon = ()=><ExternalIcon Icon={QueryToolSvg} style={{height: '0.7em'}} />;
export const SaveDataIcon = ()=><ExternalIcon Icon={SaveDataSvg} style={{height: '0.7em'}} />;
export const QueryToolIcon = ()=><ExternalIcon Icon={QueryToolSvg} style={{height: '1rem'}} />;
export const SaveDataIcon = ()=><ExternalIcon Icon={SaveDataSvg} style={{height: '1rem'}} />;
export const PasteIcon = ()=><ExternalIcon Icon={PasteSvg} />;
export const FilterIcon = ()=><ExternalIcon Icon={FilterSvg} />;
export const CommitIcon = ()=><ExternalIcon Icon={CommitSvg} />;
export const RollbackIcon = ()=><ExternalIcon Icon={RollbackSvg} />;
export const ClearIcon = ()=><ExternalIcon Icon={ClearSvg} />;
export const ConnectedIcon = ()=><ExternalIcon Icon={ConnectedSvg} style={{height: '0.7em'}} />;
export const DisonnectedIcon = ()=><ExternalIcon Icon={DisconnectedSvg} style={{height: '0.7em'}} />;
export const ConnectedIcon = ()=><ExternalIcon Icon={ConnectedSvg} style={{height: '1rem'}} />;
export const DisonnectedIcon = ()=><ExternalIcon Icon={DisconnectedSvg} style={{height: '1rem'}} />;
export const RegexIcon = ()=><ExternalIcon Icon={RegexSvg} />;
export const FormatCaseIcon = ()=><ExternalIcon Icon={FormatCaseSvg} />;
export const ExpandDialogIcon = ()=><ExternalIcon Icon={Expand} style={{height: '1.2em'}} />;
export const MinimizeDialogIcon = ()=><ExternalIcon Icon={Collapse} style={{height: '1.4em'}} />;
export const ExpandDialogIcon = ()=><ExternalIcon Icon={Expand} style={{height: '1.2rem'}} />;
export const MinimizeDialogIcon = ()=><ExternalIcon Icon={Collapse} style={{height: '1.4rem'}} />;

View File

@@ -207,6 +207,7 @@ FormInputSQL.propTypes = {
helpMessage: PropTypes.string,
testcid: PropTypes.string,
value: PropTypes.string,
controlProps: PropTypes.object,
noLabel: PropTypes.bool,
change: PropTypes.func,
};
@@ -570,7 +571,7 @@ export function InputRadio({ helpid, value, onChange, controlProps, readonly, ..
disableRipple
{...props}
/>
}
label={controlProps.label}
className={(readonly || props.disabled) ? classes.readOnlySwitch : null}
@@ -821,6 +822,7 @@ export const InputSelect = forwardRef(({
const [[finalOptions, isLoading], setFinalOptions] = useState([[], true]);
const theme = useTheme();
/* React will always take options var as changed parameter. So,
We cannot run the below effect with options dependency as it will keep on
loading the options. optionsReloadBasis is helpful to avoid repeated
@@ -1174,20 +1176,21 @@ const useStylesFormFooter = makeStyles((theme) => ({
}));
/* The form footer used mostly for showing error */
export function FormFooterMessage(props) {
export function FormFooterMessage({style, ...props}) {
const classes = useStylesFormFooter();
if (!props.message) {
return <></>;
}
return (
<Box className={classes.root}>
<Box className={classes.root} style={style}>
<NotifierMessage {...props}></NotifierMessage>
</Box>
);
}
FormFooterMessage.propTypes = {
style: PropTypes.object,
message: PropTypes.string,
};

View File

@@ -13,7 +13,7 @@ import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
/* React wrapper for JsonEditor */
export default function JsonEditor({currEditor, value, options, className}) {
export default function JsonEditor({getEditor, value, options, className}) {
const eleRef = useRef();
const editor = useRef();
const defaultOptions = {
@@ -34,9 +34,10 @@ export default function JsonEditor({currEditor, value, options, className}) {
}
});
editor.current.setText(value);
currEditor && currEditor(editor.current);
getEditor?.(editor.current);
editor.current.focus();
return ()=>editor.current?.destroy();
/* Required by json editor */
eleRef.current.style.height = eleRef.current.offsetHeight + 'px';
}, []);
useMemo(() => {
@@ -53,7 +54,7 @@ export default function JsonEditor({currEditor, value, options, className}) {
}
JsonEditor.propTypes = {
currEditor: PropTypes.func,
getEditor: PropTypes.func,
value: PropTypes.string,
options: PropTypes.object,
className: CustomPropTypes.className,

View File

@@ -18,6 +18,9 @@ const useStyles = makeStyles((theme)=>({
'& .szh-menu': {
padding: '4px 0px',
zIndex: 1000,
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
border: `1px solid ${theme.otherVars.borderColor}`
},
'& .szh-menu__divider': {
margin: 0,

View File

@@ -4,8 +4,13 @@ import PropTypes from 'prop-types';
import { isMac } from '../keyboard_shortcuts';
import _ from 'lodash';
import CustomPropTypes from '../custom_prop_types';
import gettext from 'sources/gettext';
const useStyles = makeStyles((theme)=>({
shortcutTitle: {
width: '100%',
textAlign: 'center',
},
shortcut: {
justifyContent: 'center',
marginTop: '0.125rem',
@@ -19,10 +24,43 @@ const useStyles = makeStyles((theme)=>({
},
}));
export function getBrowserAccesskey() {
/* Ref: https://github.com/tillsanders/access-key-label-polyfill/ */
let ua = window.navigator.userAgent;
// macOS
if (ua.match(/macintosh/i)) {
// Firefox
if (ua.match(/firefox/i)) {
const firefoxVersion = ua.match(/firefox[\s/](\d+)/i);
// Firefox < v14
if (firefoxVersion[1] && parseInt(firefoxVersion[1], 10) < 14) {
return ['Ctrl'];
}
}
return ['Option', 'Ctrl'];
}
// Internet Explorer / Edge
if (ua.match(/msie|trident/i) || ua.match(/\sedg/i)) {
return ['Alt'];
}
// iOS / iPadOS
if (ua.match(/(ipod|iphone|ipad)/i)) {
// accesskeyLabel is supported > v14, but we're not checking for versions here, since we use
// feature support detection
return ['Option', 'Ctrl'];
}
// Fallback
// Note: Apparently, Chrome for Android is not even supporting accesskey, so be prepared.
return [gettext('Accesskey')];
}
export function shortcutToString(shortcut, accesskey=null, asArray=false) {
let keys = [];
if(accesskey) {
keys.push('Accesskey');
keys = getBrowserAccesskey();
keys.push(_.capitalize(accesskey?.toUpperCase()));
} else if(shortcut) {
shortcut.alt && keys.push((isMac() ? 'Option' : 'Alt'));
@@ -41,12 +79,12 @@ export function shortcutToString(shortcut, accesskey=null, asArray=false) {
}
/* The tooltip content to show shortcut details */
export default function ShortcutTitle({title, shortcut, accessKey}) {
export default function ShortcutTitle({title, shortcut, accesskey}) {
const classes = useStyles();
let keys = shortcutToString(shortcut, accessKey, true);
let keys = shortcutToString(shortcut, accesskey, true);
return (
<>
<div>{title}</div>
<div className={classes.shortcutTitle}>{title}</div>
<div className={classes.shortcut}>
{keys.map((key, i)=>{
return <div key={i} className={classes.key}>{key}</div>;
@@ -59,5 +97,5 @@ export default function ShortcutTitle({title, shortcut, accessKey}) {
ShortcutTitle.propTypes = {
title: PropTypes.string,
shortcut: CustomPropTypes.shortcut,
accessKey: PropTypes.string,
accesskey: PropTypes.string,
};