diff --git a/packages/web/src/modals/InsertJoinModal.js b/packages/web/src/modals/InsertJoinModal.js
new file mode 100644
index 000000000..5c83d031d
--- /dev/null
+++ b/packages/web/src/modals/InsertJoinModal.js
@@ -0,0 +1,189 @@
+import React from 'react';
+import ModalBase from './ModalBase';
+import { FormButtonRow } from '../utility/forms';
+import FormStyledButton from '../widgets/FormStyledButton';
+import SqlEditor from '../sqleditor/SqlEditor';
+import styled from 'styled-components';
+import keycodes from '../utility/keycodes';
+import ModalHeader from './ModalHeader';
+import ModalContent from './ModalContent';
+import ModalFooter from './ModalFooter';
+import analyseQuerySources from '../sqleditor/analyseQuerySources';
+import TableControl, { TableColumn } from '../utility/TableControl';
+import { TextField } from '../utility/inputs';
+
+const FlexLine = styled.div`
+ display: flex;
+ margin-bottom: 15px;
+`;
+
+const FlexColumn = styled.div`
+ margin: 5px;
+`;
+
+const Label = styled.div`
+ margin: 5px;
+`;
+
+const SqlWrapper = styled.div`
+ position: relative;
+ height: 80px;
+ width: 40vw;
+`;
+
+const JOIN_TYPES = ['INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN'];
+
+export default function InsertJoinModal({ sql, modalState, engine, dbinfo, onInsert }) {
+ const sources = React.useMemo(
+ () => analyseQuerySources(sql, [...dbinfo.tables.map((x) => x.pureName), ...dbinfo.views.map((x) => x.pureName)]),
+ [sql, dbinfo]
+ );
+
+ const [sourceIndex, setSourceIndex] = React.useState(0);
+ const [targetIndex, setTargetIndex] = React.useState(0);
+ const [joinIndex, setJoinIndex] = React.useState(0);
+ const [alias, setAlias] = React.useState('');
+ const sourceRef = React.useRef(null);
+ const targetRef = React.useRef(null);
+ const aliasRef = React.useRef(null);
+ const joinRef = React.useRef(null);
+
+ const targets = React.useMemo(() => {
+ const source = sources[sourceIndex];
+ if (!source) return [];
+ /** @type {import('@dbgate/types').TableInfo} */
+ const table = dbinfo.tables.find((x) => x.pureName == sources[sourceIndex].name);
+ if (!table) return [];
+ return [
+ ...table.foreignKeys.map((fk) => ({
+ baseColumns: fk.columns.map((x) => x.columnName).join(', '),
+ refTable: fk.refTableName,
+ refColumns: fk.columns.map((x) => x.refColumnName).join(', '),
+ constraintName: fk.constraintName,
+ columnMap: fk.columns,
+ })),
+ ...table.dependencies.map((fk) => ({
+ baseColumns: fk.columns.map((x) => x.refColumnName).join(', '),
+ refTable: fk.pureName,
+ refColumns: fk.columns.map((x) => x.columnName).join(', '),
+ constraintName: fk.constraintName,
+ columnMap: fk.columns.map((x) => ({
+ columnName: x.refColumnName,
+ refColumnName: x.columnName,
+ })),
+ })),
+ ];
+ }, [sourceIndex, sources]);
+
+ const sqlPreview = React.useMemo(() => {
+ const source = sources[sourceIndex];
+ const target = targets[targetIndex];
+ if (source && target) {
+ return `${JOIN_TYPES[joinIndex]} ${target.refTable}${alias ? ` ${alias}` : ''} ON ${target.columnMap
+ .map((col) => `${source.name}.${col.columnName} = ${alias || target.refTable}.${col.refColumnName}`)
+ .join(' AND ')}`;
+ }
+ return '';
+ }, [joinIndex, sources, targets, sourceIndex, targetIndex, alias]);
+
+ const sourceKeyDown = React.useCallback((event) => {
+ if (event.keyCode == keycodes.enter || event.keyCode == keycodes.rightArrow) {
+ targetRef.current.focus();
+ }
+ }, []);
+ const targetKeyDown = React.useCallback((event) => {
+ if (event.keyCode == keycodes.leftArrow) {
+ sourceRef.current.focus();
+ }
+ if (event.keyCode == keycodes.enter || event.keyCode == keycodes.rightArrow) {
+ joinRef.current.focus();
+ }
+ }, []);
+ const joinKeyDown = React.useCallback((event) => {
+ if (event.keyCode == keycodes.leftArrow) {
+ targetRef.current.focus();
+ }
+ if (event.keyCode == keycodes.enter || event.keyCode == keycodes.rightArrow) {
+ aliasRef.current.focus();
+ }
+ }, []);
+ const aliasKeyDown = React.useCallback((event) => {
+ if (event.keyCode == keycodes.enter) {
+ event.preventDefault();
+ modalState.close();
+ onInsert(sqlPreview);
+ }
+ }, []);
+
+ return (
+
+ Insert join
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* */}
+
+
+
+
+ ({ name }))}
+ selectedIndex={joinIndex}
+ setSelectedIndex={setJoinIndex}
+ tableRef={joinRef}
+ onKeyDown={joinKeyDown}
+ >
+
+
+
+ setAlias(e.target.value)}
+ editorRef={aliasRef}
+ onKeyDown={aliasKeyDown}
+ />
+
+
+
+
+
+
+
+
+ {
+ modalState.close();
+ onInsert(sqlPreview);
+ }}
+ />
+
+
+
+ );
+}
diff --git a/packages/web/src/sqleditor/SqlEditor.js b/packages/web/src/sqleditor/SqlEditor.js
index 1dc3b8a5c..fc19b31ba 100644
--- a/packages/web/src/sqleditor/SqlEditor.js
+++ b/packages/web/src/sqleditor/SqlEditor.js
@@ -2,9 +2,12 @@ import React from 'react';
import styled from 'styled-components';
import AceEditor from 'react-ace';
import useDimensions from '../utility/useDimensions';
-import { addCompleter, setCompleters } from 'ace-builds/src-noconflict/ext-language_tools';
-import { getDatabaseInfo } from '../utility/metadataLoaders';
import analyseQuerySources from './analyseQuerySources';
+import keycodes from '../utility/keycodes';
+import useCodeCompletion from './useCodeCompletion';
+import showModal from '../modals/showModal';
+import InsertJoinModal from '../modals/InsertJoinModal';
+import { getDatabaseInfo } from '../utility/metadataLoaders';
const Wrapper = styled.div`
position: absolute;
@@ -55,118 +58,50 @@ export default function SqlEditor({
const currentEditorRef = editorRef || ownEditorRef;
- React.useEffect(() => {
- if (!tabVisible) return;
-
- setCompleters([]);
- addCompleter({
- getCompletions: async function (editor, session, pos, prefix, callback) {
- const cursor = session.selection.cursor;
- const line = session.getLine(cursor.row).slice(0, cursor.column);
- const dbinfo = await getDatabaseInfo({ conid, database });
-
- let list = COMMON_KEYWORDS.map((word) => ({
- name: word,
- value: word,
- caption: word,
- meta: 'keyword',
- score: 800,
- }));
-
- if (/from\s*([a-zA-Z0-9_]*)?$/i.test(line)) {
- if (dbinfo) {
- list = [
- ...list,
- ...dbinfo.tables.map((x) => ({
- name: x.pureName,
- value: x.pureName,
- caption: x.pureName,
- meta: 'table',
- score: 1000,
- })),
- ...dbinfo.views.map((x) => ({
- name: x.pureName,
- value: x.pureName,
- caption: x.pureName,
- meta: 'view',
- score: 1000,
- })),
- ];
- }
- }
-
- const colMatch = line.match(/([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)?$/);
- if (colMatch && dbinfo) {
- const table = colMatch[1];
- const sources = analyseQuerySources(editor.getValue(), [
- ...dbinfo.tables.map((x) => x.pureName),
- ...dbinfo.views.map((x) => x.pureName),
- ]);
- const source = sources.find((x) => (x.alias || x.name) == table);
- if (source) {
- const table = dbinfo.tables.find((x) => x.pureName == source.name);
- if (table) {
- list = [
- ...list,
- ...table.columns.map((x) => ({
- name: x.columnName,
- value: x.columnName,
- caption: x.columnName,
- meta: 'column',
- score: 1000,
- })),
- ];
- }
- }
- }
-
- callback(null, list);
- },
- });
-
- const doLiveAutocomplete = function (e) {
- const editor = e.editor;
- var hasCompleter = editor.completer && editor.completer.activated;
- const session = editor.session;
- const cursor = session.selection.cursor;
- const line = session.getLine(cursor.row).slice(0, cursor.column);
-
- // We don't want to autocomplete with no prefix
- if (e.command.name === 'backspace') {
- // do not hide after backspace
- } else if (e.command.name === 'insertstring') {
- if (!hasCompleter || e.args == '.') {
- editor.execCommand('startAutocomplete');
- }
-
- // if (e.args == ' ' || e.args == '.') {
- // if (/from\s*$/i.test(line)) {
- // currentEditorRef.current.editor.execCommand('startAutocomplete');
- // }
- // }
- }
- };
-
- currentEditorRef.current.editor.commands.on('afterExec', doLiveAutocomplete);
-
- return () => {
- currentEditorRef.current.editor.commands.removeListener('afterExec', doLiveAutocomplete);
- };
- }, [tabVisible, conid, database, currentEditorRef.current]);
+ useCodeCompletion({
+ conid,
+ database,
+ tabVisible,
+ currentEditorRef,
+ });
React.useEffect(() => {
if ((tabVisible || focusOnCreate) && currentEditorRef.current && currentEditorRef.current.editor)
currentEditorRef.current.editor.focus();
}, [tabVisible, focusOnCreate]);
+ const handleKeyDown = React.useCallback(
+ async (data, hash, keyString, keyCode, event) => {
+ if (keyCode == keycodes.j && event.ctrlKey && !readOnly && tabVisible) {
+ event.preventDefault();
+ const dbinfo = await getDatabaseInfo({ conid, database });
+ showModal((modalState) => (
+ {
+ const editor = currentEditorRef.current.editor;
+ editor.session.insert(editor.getCursorPosition(), text);
+ }}
+ />
+ ));
+ }
+
+ if (onKeyDown) onKeyDown(data, hash, keyString, keyCode, event);
+ },
+ [onKeyDown]
+ );
+
React.useEffect(() => {
- if (onKeyDown && currentEditorRef.current) {
- currentEditorRef.current.editor.keyBinding.addKeyboardHandler(onKeyDown);
+ if ((onKeyDown || !readOnly) && currentEditorRef.current) {
+ currentEditorRef.current.editor.keyBinding.addKeyboardHandler(handleKeyDown);
}
return () => {
- currentEditorRef.current.editor.keyBinding.removeKeyboardHandler(onKeyDown);
+ currentEditorRef.current.editor.keyBinding.removeKeyboardHandler(handleKeyDown);
};
- }, [onKeyDown]);
+ }, [handleKeyDown]);
return (
diff --git a/packages/web/src/sqleditor/analyseQuerySources.js b/packages/web/src/sqleditor/analyseQuerySources.js
index f01ca9d70..bf19db833 100644
--- a/packages/web/src/sqleditor/analyseQuerySources.js
+++ b/packages/web/src/sqleditor/analyseQuerySources.js
@@ -19,7 +19,7 @@ export default function analyseQuerySources(sql, sourceNames) {
name: word,
});
}
- if (/^(where)|(inner)|(left)|(right)$/i.test(postWord)) {
+ if (/^(where)|(inner)|(left)|(right)|(on)$/i.test(postWord)) {
continue;
}
res.push({
diff --git a/packages/web/src/sqleditor/useCodeCompletion.js b/packages/web/src/sqleditor/useCodeCompletion.js
new file mode 100644
index 000000000..285b9436e
--- /dev/null
+++ b/packages/web/src/sqleditor/useCodeCompletion.js
@@ -0,0 +1,123 @@
+import React from 'react';
+import { addCompleter, setCompleters } from 'ace-builds/src-noconflict/ext-language_tools';
+import { getDatabaseInfo } from '../utility/metadataLoaders';
+import analyseQuerySources from './analyseQuerySources';
+
+const COMMON_KEYWORDS = [
+ 'select',
+ 'where',
+ 'update',
+ 'delete',
+ 'group',
+ 'order',
+ 'from',
+ 'by',
+ 'create',
+ 'table',
+ 'drop',
+ 'alter',
+ 'view',
+ 'execute',
+ 'procedure',
+];
+
+export default function useCodeCompletion({ conid, database, tabVisible, currentEditorRef }) {
+ React.useEffect(() => {
+ if (!tabVisible) return;
+
+ setCompleters([]);
+ addCompleter({
+ getCompletions: async function (editor, session, pos, prefix, callback) {
+ const cursor = session.selection.cursor;
+ const line = session.getLine(cursor.row).slice(0, cursor.column);
+ const dbinfo = await getDatabaseInfo({ conid, database });
+
+ let list = COMMON_KEYWORDS.map((word) => ({
+ name: word,
+ value: word,
+ caption: word,
+ meta: 'keyword',
+ score: 800,
+ }));
+
+ if (/(join)|(from)|(update)|(delete)|(insert)\s*([a-zA-Z0-9_]*)?$/i.test(line)) {
+ if (dbinfo) {
+ list = [
+ ...list,
+ ...dbinfo.tables.map((x) => ({
+ name: x.pureName,
+ value: x.pureName,
+ caption: x.pureName,
+ meta: 'table',
+ score: 1000,
+ })),
+ ...dbinfo.views.map((x) => ({
+ name: x.pureName,
+ value: x.pureName,
+ caption: x.pureName,
+ meta: 'view',
+ score: 1000,
+ })),
+ ];
+ }
+ }
+
+ const colMatch = line.match(/([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)?$/);
+ if (colMatch && dbinfo) {
+ const table = colMatch[1];
+ const sources = analyseQuerySources(editor.getValue(), [
+ ...dbinfo.tables.map((x) => x.pureName),
+ ...dbinfo.views.map((x) => x.pureName),
+ ]);
+ const source = sources.find((x) => (x.alias || x.name) == table);
+ if (source) {
+ const table = dbinfo.tables.find((x) => x.pureName == source.name);
+ if (table) {
+ list = [
+ ...list,
+ ...table.columns.map((x) => ({
+ name: x.columnName,
+ value: x.columnName,
+ caption: x.columnName,
+ meta: 'column',
+ score: 1000,
+ })),
+ ];
+ }
+ }
+ }
+
+ callback(null, list);
+ },
+ });
+
+ const doLiveAutocomplete = function (e) {
+ const editor = e.editor;
+ var hasCompleter = editor.completer && editor.completer.activated;
+ const session = editor.session;
+ const cursor = session.selection.cursor;
+ const line = session.getLine(cursor.row).slice(0, cursor.column);
+
+ // We don't want to autocomplete with no prefix
+ if (e.command.name === 'backspace') {
+ // do not hide after backspace
+ } else if (e.command.name === 'insertstring') {
+ if (!hasCompleter || e.args == '.') {
+ editor.execCommand('startAutocomplete');
+ }
+
+ // if (e.args == ' ' || e.args == '.') {
+ // if (/from\s*$/i.test(line)) {
+ // currentEditorRef.current.editor.execCommand('startAutocomplete');
+ // }
+ // }
+ }
+ };
+
+ currentEditorRef.current.editor.commands.on('afterExec', doLiveAutocomplete);
+
+ return () => {
+ currentEditorRef.current.editor.commands.removeListener('afterExec', doLiveAutocomplete);
+ };
+ }, [tabVisible, conid, database, currentEditorRef.current]);
+}
diff --git a/packages/web/src/utility/ObjectListControl.js b/packages/web/src/utility/ObjectListControl.js
index 899c2c949..3a2f853ed 100644
--- a/packages/web/src/utility/ObjectListControl.js
+++ b/packages/web/src/utility/ObjectListControl.js
@@ -37,7 +37,7 @@ export default function ObjectListControl({ collection = [], title, showIfEmpty
}
+ formatter={(col) => }
/>
{children}
diff --git a/packages/web/src/utility/TableControl.js b/packages/web/src/utility/TableControl.js
index 8ed20be9a..521de062f 100644
--- a/packages/web/src/utility/TableControl.js
+++ b/packages/web/src/utility/TableControl.js
@@ -1,15 +1,24 @@
import React from 'react';
import _ from 'lodash';
import styled from 'styled-components';
+import keycodes from './keycodes';
const Table = styled.table`
border-collapse: collapse;
width: 100%;
+ user-select: ${(props) =>
+ // @ts-ignore
+ props.focusable ? 'none' : ''};
+ // outline: none;
`;
const TableHead = styled.thead``;
const TableBody = styled.tbody``;
const TableHeaderRow = styled.tr``;
-const TableBodyRow = styled.tr``;
+const TableBodyRow = styled.tr`
+ background-color: ${(props) =>
+ // @ts-ignore
+ props.isSelected ? '#ccccff' : ''};
+`;
const TableHeaderCell = styled.td`
border: 1px solid #e8eef4;
background-color: #e8eef4;
@@ -30,26 +39,70 @@ function format(row, col) {
return row[fieldName];
}
-export default function TableControl({ rows = [], children }) {
- console.log('children', children);
-
+export default function TableControl({
+ rows = [],
+ children,
+ focusOnCreate = false,
+ onKeyDown = undefined,
+ tabIndex = -1,
+ setSelectedIndex = undefined,
+ selectedIndex = undefined,
+ tableRef = undefined,
+}) {
const columns = (children instanceof Array ? _.flatten(children) : [children])
- .filter(child => child && child.props && child.props.fieldName)
- .map(child => child.props);
+ .filter((child) => child && child.props && child.props.fieldName)
+ .map((child) => child.props);
+
+ const myTableRef = React.useRef(null);
+ const currentTableRef = tableRef || myTableRef;
+
+ React.useEffect(() => {
+ if (focusOnCreate) {
+ currentTableRef.current.focus();
+ }
+ }, []);
+
+ const handleKeyDown = React.useCallback((event) => {
+ if (event.keyCode == keycodes.downArrow) {
+ setSelectedIndex((i) => Math.min(i + 1, rows.length - 1));
+ }
+ if (event.keyCode == keycodes.upArrow) {
+ setSelectedIndex((i) => Math.max(0, i - 1));
+ }
+ if (onKeyDown) onKeyDown(event);
+ }, []);
return (
-
+
- {columns.map(x => (
+ {columns.map((x) => (
{x.header}
))}
{rows.map((row, index) => (
-
- {columns.map(col => (
+ {
+ setSelectedIndex(index);
+ currentTableRef.current.focus();
+ }
+ : undefined
+ }
+ >
+ {columns.map((col) => (
{format(row, col)}
))}