Added support for the 'Add to macros' feature and fixed various usability issues. #4735

pull/7506/head
Rohit Bhati 2024-05-24 15:30:31 +05:30 committed by GitHub
parent 36a71dc7fa
commit 4e3ec91d23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 284 additions and 56 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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;
} }

View File

@ -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>;
}); });

View File

@ -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
}; };

View File

@ -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()

View File

@ -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}

View File

@ -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={{

View File

@ -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
}; };

View File

@ -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,
};

View File

@ -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':

View File

@ -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)