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 ( + +
{pureName}
+ + {(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