diff --git a/packages/web/src/appobj/AppObjectCore.js b/packages/web/src/appobj/AppObjectCore.js
index e7824ef0e..0730ee7ac 100644
--- a/packages/web/src/appobj/AppObjectCore.js
+++ b/packages/web/src/appobj/AppObjectCore.js
@@ -67,6 +67,10 @@ export function AppObjectCore({
}}
theme={theme}
isBold={isBold}
+ draggable
+ onDragStart={(e) => {
+ e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
+ }}
{...other}
>
{prefix}
diff --git a/packages/web/src/designer/Designer.js b/packages/web/src/designer/Designer.js
new file mode 100644
index 000000000..665bed943
--- /dev/null
+++ b/packages/web/src/designer/Designer.js
@@ -0,0 +1,50 @@
+import React from 'react';
+import styled from 'styled-components';
+import DesignerTable from './DesignerTable';
+
+const Wrapper = styled.div`
+ flex: 1;
+`;
+
+export default function Designer({ value, onChange }) {
+ const { tables } = value || {};
+ const handleDrop = (e) => {
+ var data = e.dataTransfer.getData('app_object_drag_data');
+ e.preventDefault();
+ if (!data) return;
+ var json = JSON.parse(data);
+ onChange({
+ ...value,
+ tables: [...(tables || []), json],
+ });
+ // var objs = AppObject.createAppObjectInstances(json);
+ // let targetOffset = $(ev.target).offset();
+ // for (let obj of objs) {
+ // await this.props.model.addTable(obj, ev.clientX - targetOffset.left, ev.clientY - targetOffset.top);
+ // }
+ // this.changedModel();
+ };
+
+ const changeTable = React.useCallback(
+ (table, index) => {
+ onChange({
+ ...value,
+ tables: (tables || []).map((t, i) => (i == index ? table : t)),
+ });
+ },
+ [onChange]
+ );
+
+ return (
+ e.preventDefault()} onDrop={handleDrop}>
+ {(tables || []).map((table, index) => (
+
+ ))}
+
+ );
+}
diff --git a/packages/web/src/designer/DesignerTable.js b/packages/web/src/designer/DesignerTable.js
new file mode 100644
index 000000000..3ee2df78d
--- /dev/null
+++ b/packages/web/src/designer/DesignerTable.js
@@ -0,0 +1,112 @@
+import React from 'react';
+import styled from 'styled-components';
+import ColumnLabel from '../datagrid/ColumnLabel';
+
+const Wrapper = styled.div`
+ position: absolute;
+ background-color: white;
+`;
+
+const Header = styled.div`
+ font-weight: bold;
+ text-align: center;
+ padding: 2px;
+ background: lightblue;
+`;
+
+const ColumnsWrapper = styled.div`
+ max-height: 400px;
+ overflow-y: scroll;
+ width: 100%;
+`;
+
+export default function DesignerTable(props) {
+ const { pureName, columns, left, top, onChangeTable, index } = props;
+ const [movingPosition, setMovingPosition] = React.useState(null);
+ const movingPositionRef = React.useRef(null);
+
+ const moveStartXRef = React.useRef(null);
+ const moveStartYRef = React.useRef(null);
+
+ const handleMove = React.useCallback(
+ (e) => {
+ let diffX = e.clientX - moveStartXRef.current;
+ let diffY = e.clientY - moveStartYRef.current;
+ moveStartXRef.current = e.clientX;
+ moveStartYRef.current = e.clientY;
+
+ movingPositionRef.current = {
+ left: (movingPositionRef.current.left || 0) + diffX,
+ top: (movingPositionRef.current.top || 0) + diffY,
+ };
+ setMovingPosition(movingPositionRef.current);
+ // onChangeTable(
+ // {
+ // ...props,
+ // left: (left || 0) + diffX,
+ // top: (top || 0) + diffY,
+ // },
+ // index
+ // );
+ },
+ [onChangeTable]
+ );
+
+ const handleMoveEnd = React.useCallback((e) => {
+ setMovingPosition(null);
+
+ onChangeTable(
+ {
+ ...props,
+ left: movingPositionRef.current.left,
+ top: movingPositionRef.current.top,
+ },
+ index
+ );
+
+ // this.props.model.fixPositions();
+
+ // this.props.designer.changedModel(true);
+ }, []);
+
+ React.useEffect(() => {
+ if (movingPosition) {
+ document.addEventListener('mousemove', handleMove, true);
+ document.addEventListener('mouseup', handleMoveEnd, true);
+ return () => {
+ document.removeEventListener('mousemove', handleMove, true);
+ document.removeEventListener('mouseup', handleMoveEnd, true);
+ };
+ }
+ }, [movingPosition == null, handleMove, handleMoveEnd]);
+
+ const headerMouseDown = React.useCallback(
+ (e) => {
+ e.preventDefault();
+ moveStartXRef.current = e.clientX;
+ moveStartYRef.current = e.clientY;
+ movingPositionRef.current = { left, top };
+ setMovingPosition(movingPositionRef.current);
+ // setIsMoving(true);
+ },
+ [handleMove, handleMoveEnd]
+ );
+
+ return (
+
+
+
+ {(columns || []).map((column) => (
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/packages/web/src/designer/QueryDesigner.js b/packages/web/src/designer/QueryDesigner.js
new file mode 100644
index 000000000..53947a8f8
--- /dev/null
+++ b/packages/web/src/designer/QueryDesigner.js
@@ -0,0 +1,7 @@
+import React from 'react';
+import styled from 'styled-components';
+import Designer from './Designer';
+
+export default function QueryDesigner({ value, conid, database, engine, onChange, onKeyDown }) {
+ return ;
+}
diff --git a/packages/web/src/icons.js b/packages/web/src/icons.js
index ecc48052f..e247a3447 100644
--- a/packages/web/src/icons.js
+++ b/packages/web/src/icons.js
@@ -29,6 +29,7 @@ const iconNames = {
'icon sql-file': 'mdi mdi-file',
'icon web': 'mdi mdi-web',
'icon home': 'mdi mdi-home',
+ 'icon query-design': 'mdi mdi-vector-polyline-edit',
'icon edit': 'mdi mdi-pencil',
'icon delete': 'mdi mdi-delete',
@@ -68,6 +69,7 @@ const iconNames = {
'img markdown': 'mdi mdi-application color-red-7',
'img preview': 'mdi mdi-file-find color-red-7',
'img favorite': 'mdi mdi-star color-yellow-7',
+ 'img query-design': 'mdi mdi-vector-polyline-edit color-red-7',
'img free-table': 'mdi mdi-table color-green-7',
'img macro': 'mdi mdi-hammer-wrench',
diff --git a/packages/web/src/query/useNewQuery.js b/packages/web/src/query/useNewQuery.js
index ac2f33ca0..53cdd749d 100644
--- a/packages/web/src/query/useNewQuery.js
+++ b/packages/web/src/query/useNewQuery.js
@@ -27,3 +27,29 @@ export default function useNewQuery() {
{ editor: initialData }
);
}
+
+export function useNewQueryDesign() {
+ const openNewTab = useOpenNewTab();
+ const currentDatabase = useCurrentDatabase();
+
+ const connection = _.get(currentDatabase, 'connection') || {};
+ const database = _.get(currentDatabase, 'name');
+
+ const tooltip = `${connection.displayName || connection.server}\n${database}`;
+
+ return ({ title = undefined, initialData = undefined, ...props } = {}) =>
+ openNewTab(
+ {
+ title: title || 'Query',
+ icon: 'img query-design',
+ tooltip,
+ tabComponent: 'QueryDesignTab',
+ props: {
+ ...props,
+ conid: connection._id,
+ database,
+ },
+ },
+ { editor: initialData }
+ );
+}
diff --git a/packages/web/src/tabs/QueryDesignTab.js b/packages/web/src/tabs/QueryDesignTab.js
new file mode 100644
index 000000000..582eed5cc
--- /dev/null
+++ b/packages/web/src/tabs/QueryDesignTab.js
@@ -0,0 +1,172 @@
+import React from 'react';
+import _ from 'lodash';
+import ReactDOM from 'react-dom';
+import axios from '../utility/axios';
+
+import { useConnectionInfo } from '../utility/metadataLoaders';
+import SqlEditor from '../sqleditor/SqlEditor';
+import { useUpdateDatabaseForTab, useSetOpenedTabs } from '../utility/globalState';
+import QueryToolbar from '../query/QueryToolbar';
+import SocketMessagesView from '../query/SocketMessagesView';
+import { TabPage } from '../widgets/TabControl';
+import ResultTabs from '../sqleditor/ResultTabs';
+import { VerticalSplitter } from '../widgets/Splitter';
+import keycodes from '../utility/keycodes';
+import { changeTab } from '../utility/common';
+import useSocket from '../utility/SocketProvider';
+import SaveTabModal from '../modals/SaveTabModal';
+import useModalState from '../modals/useModalState';
+import sqlFormatter from 'sql-formatter';
+import useEditorData from '../utility/useEditorData';
+import applySqlTemplate from '../utility/applySqlTemplate';
+import LoadingInfo from '../widgets/LoadingInfo';
+import useExtensions from '../utility/useExtensions';
+import QueryDesigner from '../designer/QueryDesigner';
+
+export default function QueryDesignTab({
+ tabid,
+ conid,
+ database,
+ initialArgs,
+ tabVisible,
+ toolbarPortalRef,
+ ...other
+}) {
+ const [sessionId, setSessionId] = React.useState(null);
+ const [executeNumber, setExecuteNumber] = React.useState(0);
+ const setOpenedTabs = useSetOpenedTabs();
+ const socket = useSocket();
+ const [busy, setBusy] = React.useState(false);
+ const saveFileModalState = useModalState();
+ const extensions = useExtensions();
+ const { editorData, setEditorData, isLoading } = useEditorData({
+ tabid,
+ loadFromArgs:
+ initialArgs && initialArgs.sqlTemplate
+ ? () => applySqlTemplate(initialArgs.sqlTemplate, extensions, { conid, database, ...other })
+ : null,
+ });
+
+ const editorRef = React.useRef(null);
+
+ const handleSessionDone = React.useCallback(() => {
+ setBusy(false);
+ }, []);
+
+ React.useEffect(() => {
+ if (sessionId && socket) {
+ socket.on(`session-done-${sessionId}`, handleSessionDone);
+ return () => {
+ socket.off(`session-done-${sessionId}`, handleSessionDone);
+ };
+ }
+ }, [sessionId, socket]);
+
+ React.useEffect(() => {
+ changeTab(tabid, setOpenedTabs, (tab) => ({ ...tab, busy }));
+ }, [busy]);
+
+ useUpdateDatabaseForTab(tabVisible, conid, database);
+ const connection = useConnectionInfo({ conid });
+
+ const handleExecute = async () => {
+ if (busy) return;
+ setExecuteNumber((num) => num + 1);
+ const selectedText = editorRef.current.editor.getSelectedText();
+
+ let sesid = sessionId;
+ if (!sesid) {
+ const resp = await axios.post('sessions/create', {
+ conid,
+ database,
+ });
+ sesid = resp.data.sesid;
+ setSessionId(sesid);
+ }
+ setBusy(true);
+ await axios.post('sessions/execute-query', {
+ sesid,
+ sql: selectedText || editorData,
+ });
+ };
+
+ const handleCancel = () => {
+ axios.post('sessions/cancel', {
+ sesid: sessionId,
+ });
+ };
+
+ const handleKill = () => {
+ axios.post('sessions/kill', {
+ sesid: sessionId,
+ });
+ setSessionId(null);
+ setBusy(false);
+ };
+
+ const handleKeyDown = (data, hash, keyString, keyCode, event) => {
+ if (keyCode == keycodes.f5) {
+ event.preventDefault();
+ handleExecute();
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+ {sessionId && (
+
+
+
+
+
+ )}
+
+ {/* {toolbarPortalRef &&
+ toolbarPortalRef.current &&
+ tabVisible &&
+ ReactDOM.createPortal(
+ ,
+ toolbarPortalRef.current
+ )} */}
+
+ >
+ );
+}
+
+QueryDesignTab.allowAddToFavorites = (props) => true;
diff --git a/packages/web/src/tabs/index.js b/packages/web/src/tabs/index.js
index 577e26b45..a561058b1 100644
--- a/packages/web/src/tabs/index.js
+++ b/packages/web/src/tabs/index.js
@@ -12,6 +12,7 @@ import MarkdownEditorTab from './MarkdownEditorTab';
import MarkdownViewTab from './MarkdownViewTab';
import MarkdownPreviewTab from './MarkdownPreviewTab';
import FavoriteEditorTab from './FavoriteEditorTab';
+import QueryDesignTab from './QueryDesignTab';
export default {
TableDataTab,
@@ -28,4 +29,5 @@ export default {
MarkdownViewTab,
MarkdownPreviewTab,
FavoriteEditorTab,
+ QueryDesignTab,
};
diff --git a/packages/web/src/widgets/Toolbar.js b/packages/web/src/widgets/Toolbar.js
index a830cb12d..2306cc205 100644
--- a/packages/web/src/widgets/Toolbar.js
+++ b/packages/web/src/widgets/Toolbar.js
@@ -3,7 +3,7 @@ import useModalState from '../modals/useModalState';
import ConnectionModal from '../modals/ConnectionModal';
import styled from 'styled-components';
import ToolbarButton, { ToolbarButtonExternalImage } from './ToolbarButton';
-import useNewQuery from '../query/useNewQuery';
+import useNewQuery, { useNewQueryDesign } from '../query/useNewQuery';
import { useConfig, useFavorites } from '../utility/metadataLoaders';
import { useSetOpenedTabs, useOpenedTabs, useCurrentTheme, useSetCurrentTheme } from '../utility/globalState';
import useNewFreeTable from '../freetable/useNewFreeTable';
@@ -27,6 +27,7 @@ const ToolbarContainer = styled.div`
export default function ToolBar({ toolbarPortalRef }) {
const modalState = useModalState();
const newQuery = useNewQuery();
+ const newQueryDesign = useNewQueryDesign();
const newFreeTable = useNewFreeTable();
const config = useConfig();
// const toolbar = config.toolbar || [];
@@ -132,6 +133,9 @@ export default function ToolBar({ toolbarPortalRef }) {
New Query
+
+ Query Designer
+
Free table editor