Added support for the 'Add to macros' feature and fixed various usability issues. #4735
parent
36a71dc7fa
commit
4e3ec91d23
Binary file not shown.
After Width: | Height: | Size: 288 KiB |
|
@ -485,6 +485,12 @@ To create a macro, select the *Manage Macros* option from the *Macros* menu on t
|
||||||
:alt: Query Tool Manage Macros dialogue
|
:alt: Query Tool Manage Macros dialogue
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
|
To add a query to macros, write and select your query, then go to the *Macros* menu in the Query Tool and click *Add to macros*. Your query will be automatically saved to macros.
|
||||||
|
|
||||||
|
.. image:: images/query_tool_add_to_macro.png
|
||||||
|
:alt: Query Tool Add To Macros
|
||||||
|
:align: center
|
||||||
|
|
||||||
To delete a macro, select the macro on the *Manage Macros* dialogue, and then click the *Delete* button.
|
To delete a macro, select the macro on the *Manage Macros* dialogue, and then click the *Delete* button.
|
||||||
The server will prompt you for confirmation to delete the macro.
|
The server will prompt you for confirmation to delete the macro.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: ac2c2e27dc2d
|
||||||
|
Revises: ec0f11f9a4e6
|
||||||
|
Create Date: 2024-05-17 19:35:03.700104
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'ac2c2e27dc2d'
|
||||||
|
down_revision = 'ec0f11f9a4e6'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
meta = sa.MetaData()
|
||||||
|
meta.reflect(op.get_bind(), only=('user_macros',))
|
||||||
|
user_macros_table = sa.Table('user_macros', meta)
|
||||||
|
|
||||||
|
# Create a select statement
|
||||||
|
stmt = sa.select(
|
||||||
|
user_macros_table.columns.mid,
|
||||||
|
user_macros_table.columns.uid, user_macros_table.columns.name,
|
||||||
|
user_macros_table.columns.sql
|
||||||
|
)
|
||||||
|
# Fetch the data from the user_macros table
|
||||||
|
results = op.get_bind().execute(stmt).fetchall()
|
||||||
|
|
||||||
|
# Drop and re-create user macro table.
|
||||||
|
op.drop_table('user_macros')
|
||||||
|
op.create_table(
|
||||||
|
'user_macros',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True),
|
||||||
|
sa.Column('mid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('uid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=1024), nullable=False),
|
||||||
|
sa.Column('sql', sa.String()),
|
||||||
|
sa.ForeignKeyConstraint(['mid'], ['macros.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['uid'], ['user.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id',))
|
||||||
|
|
||||||
|
# Reflect the new table structure
|
||||||
|
meta.reflect(op.get_bind(), only=('user_macros',))
|
||||||
|
new_user_macros_table = sa.Table('user_macros', meta)
|
||||||
|
|
||||||
|
# Bulk insert the fetched data into the new user_macros table
|
||||||
|
op.bulk_insert(
|
||||||
|
new_user_macros_table,
|
||||||
|
[
|
||||||
|
{'mid': row[0], 'uid': row[1], 'name': row[2], 'sql': row[3]}
|
||||||
|
for row in results
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# pgAdmin only upgrades, downgrade not implemented.
|
||||||
|
pass
|
|
@ -33,7 +33,7 @@ import config
|
||||||
#
|
#
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
SCHEMA_VERSION = 39
|
SCHEMA_VERSION = 40
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
#
|
#
|
||||||
|
@ -433,11 +433,12 @@ class Macros(db.Model):
|
||||||
class UserMacros(db.Model):
|
class UserMacros(db.Model):
|
||||||
"""Define the macro for a particular user."""
|
"""Define the macro for a particular user."""
|
||||||
__tablename__ = 'user_macros'
|
__tablename__ = 'user_macros'
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
mid = db.Column(
|
mid = db.Column(
|
||||||
db.Integer, db.ForeignKey('macros.id'), primary_key=True
|
db.Integer, db.ForeignKey('macros.id'), nullable=True
|
||||||
)
|
)
|
||||||
uid = db.Column(
|
uid = db.Column(
|
||||||
db.Integer, db.ForeignKey(USER_ID), primary_key=True
|
db.Integer, db.ForeignKey(USER_ID)
|
||||||
)
|
)
|
||||||
name = db.Column(db.String(1024), nullable=False)
|
name = db.Column(db.String(1024), nullable=False)
|
||||||
sql = db.Column(db.Text(), nullable=False)
|
sql = db.Column(db.Text(), nullable=False)
|
||||||
|
|
|
@ -518,8 +518,13 @@ export default function DataGridView({
|
||||||
if(!props.canAddRow) {
|
if(!props.canAddRow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newRow = schemaRef.current.getNewData();
|
let newRow = schemaRef.current.getNewData();
|
||||||
|
|
||||||
|
const current_macros = schemaRef.current?._top?._sessData?.macro || null;
|
||||||
|
if (current_macros){
|
||||||
|
newRow = schemaRef.current.getNewData(current_macros);
|
||||||
|
}
|
||||||
|
|
||||||
if(props.expandEditOnAdd && props.canEdit) {
|
if(props.expandEditOnAdd && props.canEdit) {
|
||||||
newRowIndex.current = rows.length;
|
newRowIndex.current = rows.length;
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,11 +107,15 @@ export const PgMenuItem = applyStatics(MenuItem)(({hasCheck=false, checked=false
|
||||||
props.onClick(e);
|
props.onClick(e);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const keyVal = shortcutToString(shortcut, accesskey);
|
||||||
|
|
||||||
const dataLabel = typeof(children) == 'string' ? children : props.datalabel;
|
const dataLabel = typeof(children) == 'string' ? children : props.datalabel;
|
||||||
return <MenuItem {...props} onClick={onClick} data-label={dataLabel} data-checked={checked}>
|
return <MenuItem {...props} onClick={onClick} data-label={dataLabel} data-checked={checked}>
|
||||||
{hasCheck && <CheckIcon className={classes.checkIcon} style={checked ? {} : {visibility: 'hidden'}} data-label="CheckIcon"/>}
|
{hasCheck && <CheckIcon className={classes.checkIcon} style={checked ? {} : {visibility: 'hidden'}} data-label="CheckIcon"/>}
|
||||||
{children}
|
{children}
|
||||||
{(shortcut || accesskey) && <div className={classes.shortcut}>({shortcutToString(shortcut, accesskey)})</div>}
|
<div className={classes.shortcut}>
|
||||||
|
{keyVal ? `(${keyVal})` : ''}
|
||||||
|
</div>
|
||||||
</MenuItem>;
|
</MenuItem>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
//
|
//
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';
|
||||||
import { makeStyles } from '@mui/styles';
|
import { makeStyles } from '@mui/styles';
|
||||||
import FileCopyRoundedIcon from '@mui/icons-material/FileCopyRounded';
|
import FileCopyRoundedIcon from '@mui/icons-material/FileCopyRounded';
|
||||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||||
|
@ -62,7 +62,7 @@ CopyButton.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default function CodeMirror({className, currEditor, showCopyBtn=false, customKeyMap=[], ...props}) {
|
export default function CodeMirror({className, currEditor, showCopyBtn=false, customKeyMap=[], onTextSelect, ...props}) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const editor = useRef();
|
const editor = useRef();
|
||||||
const [[showFind, isReplace], setShowFind] = useState([false, false]);
|
const [[showFind, isReplace], setShowFind] = useState([false, false]);
|
||||||
|
@ -111,8 +111,36 @@ export default function CodeMirror({className, currEditor, showCopyBtn=false, cu
|
||||||
const onMouseEnter = useCallback(()=>{showCopyBtn && setShowCopy(true);}, []);
|
const onMouseEnter = useCallback(()=>{showCopyBtn && setShowCopy(true);}, []);
|
||||||
const onMouseLeave = useCallback(()=>{showCopyBtn && setShowCopy(false);}, []);
|
const onMouseLeave = useCallback(()=>{showCopyBtn && setShowCopy(false);}, []);
|
||||||
|
|
||||||
|
// Use to handle text selection events.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onTextSelect) return;
|
||||||
|
|
||||||
|
const handleSelection = () => {
|
||||||
|
const selectedText = window.getSelection().toString();
|
||||||
|
if (selectedText) {
|
||||||
|
onTextSelect(selectedText);
|
||||||
|
} else {
|
||||||
|
onTextSelect(''); // Reset if no text is selected
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyUp = () => {
|
||||||
|
handleSelection();
|
||||||
|
};
|
||||||
|
// Add event listeners for mouseup and keyup events to detect text selection.
|
||||||
|
document.addEventListener('mouseup', handleSelection);
|
||||||
|
document.addEventListener('keyup', handleKeyUp);
|
||||||
|
|
||||||
|
// Cleanup function to remove event listeners when component unmounts or onTextSelect changes.
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mouseup', handleSelection);
|
||||||
|
document.removeEventListener('keyup', handleKeyUp);
|
||||||
|
};
|
||||||
|
}, [onTextSelect]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(className, classes.root)} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} >
|
<div className={clsx(className, classes.root)} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||||
<Editor currEditor={currEditorWrap} customKeyMap={finalCustomKeyMap} {...props} />
|
<Editor currEditor={currEditorWrap} customKeyMap={finalCustomKeyMap} {...props} />
|
||||||
{showCopy && <CopyButton editor={editor.current} />}
|
{showCopy && <CopyButton editor={editor.current} />}
|
||||||
<FindDialog editor={editor.current} show={showFind} replace={isReplace} onClose={closeFind} />
|
<FindDialog editor={editor.current} show={showFind} replace={isReplace} onClose={closeFind} />
|
||||||
|
@ -126,4 +154,5 @@ CodeMirror.propTypes = {
|
||||||
className: CustomPropTypes.className,
|
className: CustomPropTypes.className,
|
||||||
showCopyBtn: PropTypes.bool,
|
showCopyBtn: PropTypes.bool,
|
||||||
customKeyMap: PropTypes.array,
|
customKeyMap: PropTypes.array,
|
||||||
|
onTextSelect:PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
|
@ -49,7 +49,7 @@ from pgadmin.tools.sqleditor.utils.query_tool_fs_utils import \
|
||||||
read_file_generator
|
read_file_generator
|
||||||
from pgadmin.tools.sqleditor.utils.filter_dialog import FilterDialog
|
from pgadmin.tools.sqleditor.utils.filter_dialog import FilterDialog
|
||||||
from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
|
from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
|
||||||
from pgadmin.tools.sqleditor.utils.macros import get_macros,\
|
from pgadmin.tools.sqleditor.utils.macros import get_macros, \
|
||||||
get_user_macros, set_macros
|
get_user_macros, set_macros
|
||||||
from pgadmin.utils.constants import MIMETYPE_APP_JS, \
|
from pgadmin.utils.constants import MIMETYPE_APP_JS, \
|
||||||
SERVER_CONNECTION_CLOSED, ERROR_MSG_TRANS_ID_NOT_FOUND, \
|
SERVER_CONNECTION_CLOSED, ERROR_MSG_TRANS_ID_NOT_FOUND, \
|
||||||
|
@ -130,6 +130,7 @@ class SqlEditorModule(PgAdminModule):
|
||||||
'sqleditor.clear_query_history',
|
'sqleditor.clear_query_history',
|
||||||
'sqleditor.get_macro',
|
'sqleditor.get_macro',
|
||||||
'sqleditor.get_macros',
|
'sqleditor.get_macros',
|
||||||
|
'sqleditor.get_user_macros',
|
||||||
'sqleditor.set_macros',
|
'sqleditor.set_macros',
|
||||||
'sqleditor.get_new_connection_data',
|
'sqleditor.get_new_connection_data',
|
||||||
'sqleditor.get_new_connection_servers',
|
'sqleditor.get_new_connection_servers',
|
||||||
|
@ -2692,3 +2693,15 @@ def update_macros(trans_id):
|
||||||
_, _, _, _, _ = check_transaction_status(trans_id)
|
_, _, _, _, _ = check_transaction_status(trans_id)
|
||||||
|
|
||||||
return set_macros()
|
return set_macros()
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route(
|
||||||
|
'/get_user_macros',
|
||||||
|
methods=["GET"], endpoint='get_user_macros'
|
||||||
|
)
|
||||||
|
@pga_login_required
|
||||||
|
def user_macros(json_resp=True):
|
||||||
|
"""
|
||||||
|
This method is used to fetch all user macros.
|
||||||
|
"""
|
||||||
|
return get_user_macros()
|
||||||
|
|
|
@ -50,6 +50,21 @@ function initConnection(api, params, passdata) {
|
||||||
return api.post(url_for('NODE-server.connect_id', params), passdata);
|
return api.post(url_for('NODE-server.connect_id', params), passdata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRandomName(existingNames) {
|
||||||
|
const maxNumber = existingNames.reduce((max, name) => {
|
||||||
|
const match = name.match(/\d+$/); // Extract the number from the name
|
||||||
|
if (match) {
|
||||||
|
const number = parseInt(match[0], 10);
|
||||||
|
return number > max ? number : max;
|
||||||
|
}
|
||||||
|
return max;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Generate the new name
|
||||||
|
const newName = `Macro ${maxNumber + 1}`;
|
||||||
|
return newName;
|
||||||
|
}
|
||||||
|
|
||||||
function setPanelTitle(docker, panelId, title, qtState, dirty=false) {
|
function setPanelTitle(docker, panelId, title, qtState, dirty=false) {
|
||||||
if(qtState.current_file) {
|
if(qtState.current_file) {
|
||||||
title = qtState.current_file.split('\\').pop().split('/').pop();
|
title = qtState.current_file.split('\\').pop().split('/').pop();
|
||||||
|
@ -194,6 +209,8 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [selectedText, setSelectedText] = useState('');
|
||||||
|
|
||||||
const setQtState = (state)=>{
|
const setQtState = (state)=>{
|
||||||
_setQtState((prev)=>({...prev,...evalFunc(null, state, prev)}));
|
_setQtState((prev)=>({...prev,...evalFunc(null, state, prev)}));
|
||||||
};
|
};
|
||||||
|
@ -250,7 +267,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
|
||||||
{
|
{
|
||||||
maximizable: true,
|
maximizable: true,
|
||||||
tabs: [
|
tabs: [
|
||||||
LayoutDocker.getPanel({id: PANELS.QUERY, title: gettext('Query'), content: <Query />}),
|
LayoutDocker.getPanel({id: PANELS.QUERY, title: gettext('Query'), content: <Query onTextSelect={(text) => setSelectedText(text)}/>}),
|
||||||
LayoutDocker.getPanel({id: PANELS.HISTORY, title: gettext('Query History'), content: <QueryHistory />,
|
LayoutDocker.getPanel({id: PANELS.HISTORY, title: gettext('Query History'), content: <QueryHistory />,
|
||||||
cached: undefined}),
|
cached: undefined}),
|
||||||
],
|
],
|
||||||
|
@ -798,6 +815,37 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
|
||||||
}, 850, 500);
|
}, 850, 500);
|
||||||
}, [qtState.preferences.browser]);
|
}, [qtState.preferences.browser]);
|
||||||
|
|
||||||
|
const onAddToMacros = () => {
|
||||||
|
if (selectedText){
|
||||||
|
let currMacros = qtState.params.macros;
|
||||||
|
const existingNames = currMacros.map(macro => macro.name);
|
||||||
|
const newName = getRandomName(existingNames);
|
||||||
|
let changed = [{ 'name': newName, 'sql': selectedText }];
|
||||||
|
|
||||||
|
api.put(
|
||||||
|
url_for('sqleditor.set_macros', {
|
||||||
|
'trans_id': qtState.params.trans_id,
|
||||||
|
}),
|
||||||
|
{ changed: changed }
|
||||||
|
)
|
||||||
|
.then(({ data: respData }) => {
|
||||||
|
const filteredData = respData.filter(m => Boolean(m.name));
|
||||||
|
setQtState(prev => ({
|
||||||
|
...prev,
|
||||||
|
params: {
|
||||||
|
...prev.params,
|
||||||
|
macros: filteredData,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setSelectedText('');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const onFilterClick = useCallback(()=>{
|
const onFilterClick = useCallback(()=>{
|
||||||
const onClose = ()=>docker.current.close('filter-dialog');
|
const onClose = ()=>docker.current.close('filter-dialog');
|
||||||
docker.current.openDialog({
|
docker.current.openDialog({
|
||||||
|
@ -884,8 +932,9 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
|
||||||
<MainToolBar
|
<MainToolBar
|
||||||
containerRef={containerRef}
|
containerRef={containerRef}
|
||||||
onManageMacros={onManageMacros}
|
onManageMacros={onManageMacros}
|
||||||
|
onAddToMacros={onAddToMacros}
|
||||||
onFilterClick={onFilterClick}
|
onFilterClick={onFilterClick}
|
||||||
/>), [containerRef.current, onManageMacros, onFilterClick])}
|
/>), [containerRef.current, onManageMacros, onFilterClick, onAddToMacros])}
|
||||||
<Layout
|
<Layout
|
||||||
getLayoutInstance={(obj)=>docker.current=obj}
|
getLayoutInstance={(obj)=>docker.current=obj}
|
||||||
defaultLayout={defaultLayout}
|
defaultLayout={defaultLayout}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import React from 'react';
|
||||||
import SchemaView from '../../../../../../static/js/SchemaView';
|
import SchemaView from '../../../../../../static/js/SchemaView';
|
||||||
import BaseUISchema from '../../../../../../static/js/SchemaView/base_schema.ui';
|
import BaseUISchema from '../../../../../../static/js/SchemaView/base_schema.ui';
|
||||||
import gettext from 'sources/gettext';
|
import gettext from 'sources/gettext';
|
||||||
import { QueryToolContext } from '../QueryToolComponent';
|
import { QueryToolContext, getRandomName } from '../QueryToolComponent';
|
||||||
import url_for from 'sources/url_for';
|
import url_for from 'sources/url_for';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
@ -23,17 +23,36 @@ class MacrosCollection extends BaseUISchema {
|
||||||
}
|
}
|
||||||
|
|
||||||
get idAttribute() {
|
get idAttribute() {
|
||||||
return 'mid';
|
return 'id';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Returns the new data row for the schema based on defaults and input */
|
||||||
|
getNewData(current_macros, data={}) {
|
||||||
|
let newRow = {};
|
||||||
|
this.fields.forEach((field)=>{
|
||||||
|
newRow[field.id] = this.defaults[field.id];
|
||||||
|
});
|
||||||
|
newRow = {
|
||||||
|
...newRow,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
if (current_macros){
|
||||||
|
// Extract an array of existing names from the 'macro' collection
|
||||||
|
const existingNames = current_macros.map(macro => macro.name);
|
||||||
|
const newName = getRandomName(existingNames);
|
||||||
|
newRow.name = newName;
|
||||||
|
}
|
||||||
|
return newRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
get baseFields() {
|
get baseFields() {
|
||||||
let obj = this;
|
let obj = this;
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'id', label: gettext('Key'), cell: 'select', noEmpty: true,
|
id: 'mid', label: gettext('Key'), cell: 'select', noEmpty: false,
|
||||||
width: 100, options: obj.keyOptions, optionsReloadBasis: obj.keyOptions.length,
|
width: 100, options: obj.keyOptions, optionsReloadBasis: obj.keyOptions.length,
|
||||||
controlProps: {
|
controlProps: {
|
||||||
allowClear: false,
|
allowClear: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -73,10 +92,14 @@ class MacrosSchema extends BaseUISchema {
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(state, setError) {
|
validate(state, setError) {
|
||||||
let allKeys = state.macro.map((m)=>m.id.toString());
|
let allKeys = state.macro.map((m) => m.mid ? m.mid.toString() : null).filter(key => key !== null);
|
||||||
|
let allNames = state.macro.map((m) => m.name ? m.name.toLowerCase() : null);
|
||||||
if(allKeys.length != new Set(allKeys).size) {
|
if(allKeys.length != new Set(allKeys).size) {
|
||||||
setError('macro', gettext('Key must be unique.'));
|
setError('macro', gettext('Key must be unique.'));
|
||||||
return true;
|
return true;
|
||||||
|
} else if(allNames.length != new Set(allNames).size) {
|
||||||
|
setError('macro', gettext('Name must be unique.'));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -89,26 +112,28 @@ const useStyles = makeStyles((theme)=>({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function getChangedMacros(macrosData, changeData) {
|
function getChangedMacros(userMacrosData, changeData) {
|
||||||
/* For backend, added, removed is changed. Convert all added, removed to changed. */
|
/* For backend, added, removed is changed. Convert all added, removed to changed. */
|
||||||
let changed = [];
|
let changed = [];
|
||||||
|
|
||||||
for (const m of (changeData.macro.changed || [])) {
|
for (const m of (changeData.macro.changed || [])) {
|
||||||
let newM = {...m};
|
let newM = {...m};
|
||||||
if('id' in m) {
|
if('id' in m) {
|
||||||
/* if key changed, clear prev and add new */
|
let em = _.find(userMacrosData, (d)=>d.id==m.id);
|
||||||
changed.push({id: m.mid, name: null, sql: null});
|
newM = {name: m.name ? (m.name) : em.name , sql: m.sql ? m.sql : em.sql, mid: m.mid ? m.mid : em.mid, ...m};
|
||||||
let em = _.find(macrosData, (d)=>d.mid==m.mid);
|
|
||||||
newM = {name: em.name, sql: em.sql, ...m};
|
|
||||||
} else {
|
} else {
|
||||||
newM.id = m.mid;
|
newM.id = m.mid;
|
||||||
}
|
}
|
||||||
delete newM.mid;
|
|
||||||
changed.push(newM);
|
changed.push(newM);
|
||||||
}
|
}
|
||||||
for (const m of (changeData.macro.deleted || [])) {
|
for (const m of (changeData.macro.deleted || [])) {
|
||||||
changed.push({id: m.id, name: null, sql: null});
|
changed.push({id: m.id, name: null, sql: null});
|
||||||
}
|
}
|
||||||
for (const m of (changeData.macro.added || [])) {
|
for (const m of (changeData.macro.added || [])) {
|
||||||
|
if (m.id && m.id !== 0){
|
||||||
|
m.mid = m.id;
|
||||||
|
delete m.id;
|
||||||
|
}
|
||||||
changed.push(m);
|
changed.push(m);
|
||||||
}
|
}
|
||||||
return changed;
|
return changed;
|
||||||
|
@ -118,30 +143,46 @@ export default function MacrosDialog({onClose, onSave}) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const queryToolCtx = React.useContext(QueryToolContext);
|
const queryToolCtx = React.useContext(QueryToolContext);
|
||||||
const [macrosData, setMacrosData] = React.useState([]);
|
const [macrosData, setMacrosData] = React.useState([]);
|
||||||
|
const [userMacrosData, setUserMacrosData] = React.useState([]);
|
||||||
const [macrosErr, setMacrosErr] = React.useState(null);
|
const [macrosErr, setMacrosErr] = React.useState(null);
|
||||||
|
|
||||||
React.useEffect(async ()=>{
|
React.useEffect(async ()=>{
|
||||||
try {
|
try {
|
||||||
let {data: respData} = await queryToolCtx.api.get(url_for('sqleditor.get_macros', {
|
// Fetch user macros data
|
||||||
'trans_id': queryToolCtx.params.trans_id,
|
let { data: userMacroRespData } = await queryToolCtx.api.get(url_for('sqleditor.get_user_macros'));
|
||||||
}));
|
setUserMacrosData(userMacroRespData);
|
||||||
/* Copying id to mid to track key id changes */
|
|
||||||
setMacrosData(respData.macro.map((m)=>({...m, mid: m.id})));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMacrosErr(error);
|
setMacrosErr(error);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
React.useEffect(async ()=>{
|
||||||
|
try {
|
||||||
|
// Fetch macros data
|
||||||
|
let {data: respData} = await queryToolCtx.api.get(url_for('sqleditor.get_macros', {
|
||||||
|
'trans_id': queryToolCtx.params.trans_id,
|
||||||
|
}));
|
||||||
|
/* Copying id to mid to track key id changes */
|
||||||
|
setMacrosData(respData.macro.map((m)=>({...m, mid: m.id})));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
setMacrosErr(error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const onSaveClick = (_isNew, changeData)=>{
|
const onSaveClick = (_isNew, changeData)=>{
|
||||||
return new Promise((resolve, reject)=>{
|
return new Promise((resolve, reject)=>{
|
||||||
const setMacros = async ()=>{
|
const setMacros = async ()=>{
|
||||||
try {
|
try {
|
||||||
let changed = getChangedMacros(macrosData, changeData);
|
let changed = getChangedMacros(userMacrosData, changeData);
|
||||||
let {data: respData} = await queryToolCtx.api.put(url_for('sqleditor.set_macros', {
|
let {data: respData} = await queryToolCtx.api.put(url_for('sqleditor.set_macros', {
|
||||||
'trans_id': queryToolCtx.params.trans_id,
|
'trans_id': queryToolCtx.params.trans_id,
|
||||||
}), {changed: changed});
|
}), {changed: changed});
|
||||||
resolve();
|
resolve();
|
||||||
onSave(respData.macro?.filter((m)=>Boolean(m.name)));
|
onSave(respData.filter((m) => Boolean(m.name)));
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
|
@ -159,6 +200,7 @@ export default function MacrosDialog({onClose, onSave}) {
|
||||||
if(keyOptions.length <= 0) {
|
if(keyOptions.length <= 0) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaView
|
<SchemaView
|
||||||
formType={'dialog'}
|
formType={'dialog'}
|
||||||
|
@ -166,7 +208,7 @@ export default function MacrosDialog({onClose, onSave}) {
|
||||||
if(macrosErr) {
|
if(macrosErr) {
|
||||||
return Promise.reject(macrosErr);
|
return Promise.reject(macrosErr);
|
||||||
}
|
}
|
||||||
return Promise.resolve({macro: macrosData.filter((m)=>Boolean(m.name))});
|
return Promise.resolve({macro: userMacrosData.filter((m)=>Boolean(m.name))});
|
||||||
}}
|
}}
|
||||||
schema={new MacrosSchema(keyOptions)}
|
schema={new MacrosSchema(keyOptions)}
|
||||||
viewHelperProps={{
|
viewHelperProps={{
|
||||||
|
|
|
@ -54,7 +54,7 @@ function autoCommitRollback(type, api, transId, value) {
|
||||||
return api.post(url, JSON.stringify(value));
|
return api.post(url, JSON.stringify(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
|
export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddToMacros}) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const eventBus = useContext(QueryToolEventsContext);
|
const eventBus = useContext(QueryToolEventsContext);
|
||||||
const queryToolCtx = useContext(QueryToolContext);
|
const queryToolCtx = useContext(QueryToolContext);
|
||||||
|
@ -633,6 +633,7 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
|
||||||
label={gettext('Macros Menu')}
|
label={gettext('Macros Menu')}
|
||||||
>
|
>
|
||||||
<PgMenuItem onClick={onManageMacros}>{gettext('Manage macros')}</PgMenuItem>
|
<PgMenuItem onClick={onManageMacros}>{gettext('Manage macros')}</PgMenuItem>
|
||||||
|
<PgMenuItem onClick={onAddToMacros}>{gettext('Add to macros')}</PgMenuItem>
|
||||||
<PgMenuDivider />
|
<PgMenuDivider />
|
||||||
{queryToolCtx.params?.macros?.map((m)=>{
|
{queryToolCtx.params?.macros?.map((m)=>{
|
||||||
return (
|
return (
|
||||||
|
@ -656,4 +657,5 @@ MainToolBar.propTypes = {
|
||||||
containerRef: CustomPropTypes.ref,
|
containerRef: CustomPropTypes.ref,
|
||||||
onFilterClick: PropTypes.func,
|
onFilterClick: PropTypes.func,
|
||||||
onManageMacros: PropTypes.func,
|
onManageMacros: PropTypes.func,
|
||||||
|
onAddToMacros: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { usePgAdmin } from '../../../../../../static/js/BrowserComponent';
|
||||||
import ConfirmPromotionContent from '../dialogs/ConfirmPromotionContent';
|
import ConfirmPromotionContent from '../dialogs/ConfirmPromotionContent';
|
||||||
import usePreferences from '../../../../../../preferences/static/js/store';
|
import usePreferences from '../../../../../../preferences/static/js/store';
|
||||||
import { getTitle } from '../../sqleditor_title';
|
import { getTitle } from '../../sqleditor_title';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
|
||||||
const useStyles = makeStyles(()=>({
|
const useStyles = makeStyles(()=>({
|
||||||
|
@ -61,7 +62,7 @@ async function registerAutocomplete(editor, api, transId) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Query() {
|
export default function Query({onTextSelect}) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const editor = React.useRef();
|
const editor = React.useRef();
|
||||||
const eventBus = useContext(QueryToolEventsContext);
|
const eventBus = useContext(QueryToolEventsContext);
|
||||||
|
@ -390,7 +391,6 @@ export default function Query() {
|
||||||
|
|
||||||
const change = useCallback(()=>{
|
const change = useCallback(()=>{
|
||||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.QUERY_CHANGED, editor.current.isDirty());
|
eventBus.fireEvent(QUERY_TOOL_EVENTS.QUERY_CHANGED, editor.current.isDirty());
|
||||||
|
|
||||||
if(!queryToolCtx.params.is_query_tool && editor.current.isDirty()){
|
if(!queryToolCtx.params.is_query_tool && editor.current.isDirty()){
|
||||||
if(queryToolCtx.preferences.sqleditor.view_edit_promotion_warning){
|
if(queryToolCtx.preferences.sqleditor.view_edit_promotion_warning){
|
||||||
checkViewEditDataPromotion();
|
checkViewEditDataPromotion();
|
||||||
|
@ -480,5 +480,11 @@ export default function Query() {
|
||||||
onChange={change}
|
onChange={change}
|
||||||
autocomplete={true}
|
autocomplete={true}
|
||||||
customKeyMap={shortcutOverrideKeys}
|
customKeyMap={shortcutOverrideKeys}
|
||||||
|
onTextSelect={onTextSelect}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Query.propTypes = {
|
||||||
|
onTextSelect: PropTypes.func,
|
||||||
|
};
|
|
@ -32,15 +32,15 @@ class TestMacros(BaseTestGenerator):
|
||||||
operation='set',
|
operation='set',
|
||||||
data={
|
data={
|
||||||
'changed': [
|
'changed': [
|
||||||
{'id': 1,
|
{'mid': 1,
|
||||||
'name': 'Test Macro 1',
|
'name': 'Test Macro 1',
|
||||||
'sql': 'SELECT 1;'
|
'sql': 'SELECT 1;'
|
||||||
},
|
},
|
||||||
{'id': 2,
|
{'mid': 2,
|
||||||
'name': 'Test Macro 2',
|
'name': 'Test Macro 2',
|
||||||
'sql': 'SELECT 2;'
|
'sql': 'SELECT 2;'
|
||||||
},
|
},
|
||||||
{'id': 3,
|
{'mid': 3,
|
||||||
'name': 'Test Macro 3',
|
'name': 'Test Macro 3',
|
||||||
'sql': 'SELECT 3;'
|
'sql': 'SELECT 3;'
|
||||||
},
|
},
|
||||||
|
@ -129,10 +129,11 @@ class TestMacros(BaseTestGenerator):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
for m in self.data['changed']:
|
for m in self.data['changed']:
|
||||||
|
if self.operation == 'set':
|
||||||
|
m['id'] = m['mid']
|
||||||
url = '/sqleditor/get_macros/{0}/{1}'.format(m['id'],
|
url = '/sqleditor/get_macros/{0}/{1}'.format(m['id'],
|
||||||
self.trans_id)
|
self.trans_id)
|
||||||
response = self.tester.get(url)
|
response = self.tester.get(url)
|
||||||
|
|
||||||
if self.operation == 'clear':
|
if self.operation == 'clear':
|
||||||
self.assertEqual(response.status_code, 410)
|
self.assertEqual(response.status_code, 410)
|
||||||
elif self.operation == 'set':
|
elif self.operation == 'set':
|
||||||
|
|
|
@ -27,7 +27,7 @@ def get_macros(macro_id, json_resp):
|
||||||
:param json_resp: Set True to return json response
|
:param json_resp: Set True to return json response
|
||||||
"""
|
"""
|
||||||
if macro_id:
|
if macro_id:
|
||||||
macro = UserMacros.query.filter_by(mid=macro_id,
|
macro = UserMacros.query.filter_by(id=macro_id,
|
||||||
uid=current_user.id).first()
|
uid=current_user.id).first()
|
||||||
if macro is None:
|
if macro is None:
|
||||||
return make_json_response(
|
return make_json_response(
|
||||||
|
@ -37,7 +37,8 @@ def get_macros(macro_id, json_resp):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return ajax_response(
|
return ajax_response(
|
||||||
response={'id': macro.mid,
|
response={'id': macro.id,
|
||||||
|
'mid':macro.mid,
|
||||||
'name': macro.name,
|
'name': macro.name,
|
||||||
'sql': macro.sql},
|
'sql': macro.sql},
|
||||||
status=200
|
status=200
|
||||||
|
@ -45,13 +46,12 @@ def get_macros(macro_id, json_resp):
|
||||||
else:
|
else:
|
||||||
macros = db.session.query(Macros.id, Macros.alt, Macros.control,
|
macros = db.session.query(Macros.id, Macros.alt, Macros.control,
|
||||||
Macros.key, Macros.key_code,
|
Macros.key, Macros.key_code,
|
||||||
UserMacros.name, UserMacros.sql
|
UserMacros.name, UserMacros.sql,
|
||||||
).outerjoin(
|
UserMacros.id).outerjoin(
|
||||||
UserMacros, and_(Macros.id == UserMacros.mid,
|
UserMacros, and_(Macros.id == UserMacros.mid,
|
||||||
UserMacros.uid == current_user.id)).all()
|
UserMacros.uid == current_user.id)).all()
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
for m in macros:
|
for m in macros:
|
||||||
key_label = 'Ctrl + ' + m[3] if m[2] is True else 'Alt + ' + m[3]
|
key_label = 'Ctrl + ' + m[3] if m[2] is True else 'Alt + ' + m[3]
|
||||||
data.append({'id': m[0], 'alt': m[1],
|
data.append({'id': m[0], 'alt': m[1],
|
||||||
|
@ -74,7 +74,8 @@ def get_user_macros():
|
||||||
This method is used to get all the user macros.
|
This method is used to get all the user macros.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
macros = db.session.query(UserMacros.name,
|
macros = db.session.query(UserMacros.id,
|
||||||
|
UserMacros.name,
|
||||||
Macros.id,
|
Macros.id,
|
||||||
Macros.alt, Macros.control,
|
Macros.alt, Macros.control,
|
||||||
Macros.key, Macros.key_code,
|
Macros.key, Macros.key_code,
|
||||||
|
@ -86,11 +87,17 @@ def get_user_macros():
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
for m in macros:
|
for m in macros:
|
||||||
key_label = 'Ctrl + ' + m[4] if m[3] is True else 'Alt + ' + m[4]
|
key_label = (
|
||||||
data.append({'name': m[0], 'id': m[1], 'key': m[4],
|
'Ctrl + ' + str(m[5])
|
||||||
'key_label': key_label, 'alt': 1 if m[2] else 0,
|
if m[4] is True
|
||||||
'control': 1 if m[3] else 0, 'key_code': m[5],
|
else 'Alt + ' + str(m[5])
|
||||||
'sql': m[6]})
|
if m[5] is not None
|
||||||
|
else ''
|
||||||
|
)
|
||||||
|
data.append({'id': m[0], 'name': m[1], 'mid': m[2], 'key': m[5],
|
||||||
|
'key_label': key_label, 'alt': 1 if m[3] else 0,
|
||||||
|
'control': 1 if m[4] else 0, 'key_code': m[6],
|
||||||
|
'sql': m[7]})
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -111,21 +118,21 @@ def set_macros():
|
||||||
)
|
)
|
||||||
|
|
||||||
for m in data['changed']:
|
for m in data['changed']:
|
||||||
if m['id']:
|
if m.get('id'):
|
||||||
macro = UserMacros.query.filter_by(
|
macro = UserMacros.query.filter_by(
|
||||||
uid=current_user.id,
|
uid=current_user.id,
|
||||||
mid=m['id']).first()
|
id=m['id']).first()
|
||||||
if macro:
|
if macro:
|
||||||
status, msg = update_macro(m, macro)
|
status, msg = update_macro(m, macro)
|
||||||
else:
|
else:
|
||||||
status, msg = create_macro(m)
|
status, msg = create_macro(m)
|
||||||
|
|
||||||
if not status:
|
if not status:
|
||||||
return make_json_response(
|
return make_json_response(
|
||||||
status=410, success=0, errormsg=msg
|
status=410, success=0, errormsg=msg
|
||||||
)
|
)
|
||||||
|
|
||||||
return get_macros(None, True)
|
return get_user_macros()
|
||||||
|
|
||||||
|
|
||||||
def create_macro(macro):
|
def create_macro(macro):
|
||||||
|
@ -146,7 +153,7 @@ def create_macro(macro):
|
||||||
try:
|
try:
|
||||||
new_macro = UserMacros(
|
new_macro = UserMacros(
|
||||||
uid=current_user.id,
|
uid=current_user.id,
|
||||||
mid=macro['id'],
|
mid=macro['mid'] if macro.get('mid') else None,
|
||||||
name=macro['name'],
|
name=macro['name'],
|
||||||
sql=macro['sql']
|
sql=macro['sql']
|
||||||
)
|
)
|
||||||
|
@ -168,6 +175,7 @@ def update_macro(data, macro):
|
||||||
|
|
||||||
name = data.get('name', None)
|
name = data.get('name', None)
|
||||||
sql = data.get('sql', None)
|
sql = data.get('sql', None)
|
||||||
|
mid = data.get('mid', None)
|
||||||
|
|
||||||
if (name or sql) and macro.sql and 'name' in data and name is None:
|
if (name or sql) and macro.sql and 'name' in data and name is None:
|
||||||
return False, gettext(
|
return False, gettext(
|
||||||
|
@ -177,11 +185,12 @@ def update_macro(data, macro):
|
||||||
"Could not find the required parameter (sql).")
|
"Could not find the required parameter (sql).")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if name or sql:
|
if name or sql or mid:
|
||||||
if name:
|
if name:
|
||||||
macro.name = name
|
macro.name = name
|
||||||
if sql:
|
if sql:
|
||||||
macro.sql = sql
|
macro.sql = sql
|
||||||
|
macro.mid = mid if mid != 0 else None
|
||||||
else:
|
else:
|
||||||
db.session.delete(macro)
|
db.session.delete(macro)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue