mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-05-02 04:23:57 +00:00
join wizard
This commit is contained in:
189
packages/web/src/modals/InsertJoinModal.js
Normal file
189
packages/web/src/modals/InsertJoinModal.js
Normal file
@@ -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 (
|
||||||
|
<ModalBase modalState={modalState}>
|
||||||
|
<ModalHeader modalState={modalState}>Insert join</ModalHeader>
|
||||||
|
<ModalContent>
|
||||||
|
<FlexLine>
|
||||||
|
<FlexColumn>
|
||||||
|
<Label>Existing table</Label>
|
||||||
|
<TableControl
|
||||||
|
rows={sources}
|
||||||
|
focusOnCreate
|
||||||
|
selectedIndex={sourceIndex}
|
||||||
|
setSelectedIndex={setSourceIndex}
|
||||||
|
onKeyDown={sourceKeyDown}
|
||||||
|
tableRef={sourceRef}
|
||||||
|
>
|
||||||
|
<TableColumn fieldName="alias" header="Alias" />
|
||||||
|
<TableColumn fieldName="name" header="Name" />
|
||||||
|
</TableControl>
|
||||||
|
</FlexColumn>
|
||||||
|
<FlexColumn>
|
||||||
|
<Label>New table</Label>
|
||||||
|
<TableControl
|
||||||
|
rows={targets}
|
||||||
|
selectedIndex={targetIndex}
|
||||||
|
setSelectedIndex={setTargetIndex}
|
||||||
|
tableRef={targetRef}
|
||||||
|
onKeyDown={targetKeyDown}
|
||||||
|
>
|
||||||
|
<TableColumn fieldName="baseColumns" header="Column from" />
|
||||||
|
<TableColumn fieldName="refTable" header="Table to" />
|
||||||
|
<TableColumn fieldName="refColumns" header="Column to" />
|
||||||
|
{/* <TableColumn fieldName="constraintName" header="Foreign key" /> */}
|
||||||
|
</TableControl>
|
||||||
|
</FlexColumn>
|
||||||
|
<FlexColumn>
|
||||||
|
<Label>Join</Label>
|
||||||
|
<TableControl
|
||||||
|
rows={JOIN_TYPES.map((name) => ({ name }))}
|
||||||
|
selectedIndex={joinIndex}
|
||||||
|
setSelectedIndex={setJoinIndex}
|
||||||
|
tableRef={joinRef}
|
||||||
|
onKeyDown={joinKeyDown}
|
||||||
|
>
|
||||||
|
<TableColumn fieldName="name" header="Join type" />
|
||||||
|
</TableControl>
|
||||||
|
<Label>Alias</Label>
|
||||||
|
<TextField
|
||||||
|
value={alias}
|
||||||
|
onChange={(e) => setAlias(e.target.value)}
|
||||||
|
editorRef={aliasRef}
|
||||||
|
onKeyDown={aliasKeyDown}
|
||||||
|
/>
|
||||||
|
</FlexColumn>
|
||||||
|
</FlexLine>
|
||||||
|
<SqlWrapper>
|
||||||
|
<SqlEditor value={sqlPreview} engine={engine} readOnly />
|
||||||
|
</SqlWrapper>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<FormStyledButton
|
||||||
|
value="OK"
|
||||||
|
onClick={() => {
|
||||||
|
modalState.close();
|
||||||
|
onInsert(sqlPreview);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormStyledButton type="button" value="Close" onClick={modalState.close} />
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalBase>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,9 +2,12 @@ import React from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import AceEditor from 'react-ace';
|
import AceEditor from 'react-ace';
|
||||||
import useDimensions from '../utility/useDimensions';
|
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 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`
|
const Wrapper = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -55,118 +58,50 @@ export default function SqlEditor({
|
|||||||
|
|
||||||
const currentEditorRef = editorRef || ownEditorRef;
|
const currentEditorRef = editorRef || ownEditorRef;
|
||||||
|
|
||||||
React.useEffect(() => {
|
useCodeCompletion({
|
||||||
if (!tabVisible) return;
|
conid,
|
||||||
|
database,
|
||||||
setCompleters([]);
|
tabVisible,
|
||||||
addCompleter({
|
currentEditorRef,
|
||||||
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]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if ((tabVisible || focusOnCreate) && currentEditorRef.current && currentEditorRef.current.editor)
|
if ((tabVisible || focusOnCreate) && currentEditorRef.current && currentEditorRef.current.editor)
|
||||||
currentEditorRef.current.editor.focus();
|
currentEditorRef.current.editor.focus();
|
||||||
}, [tabVisible, focusOnCreate]);
|
}, [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) => (
|
||||||
|
<InsertJoinModal
|
||||||
|
sql={currentEditorRef.current.editor.getValue()}
|
||||||
|
modalState={modalState}
|
||||||
|
engine={engine}
|
||||||
|
dbinfo={dbinfo}
|
||||||
|
onInsert={(text) => {
|
||||||
|
const editor = currentEditorRef.current.editor;
|
||||||
|
editor.session.insert(editor.getCursorPosition(), text);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onKeyDown) onKeyDown(data, hash, keyString, keyCode, event);
|
||||||
|
},
|
||||||
|
[onKeyDown]
|
||||||
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (onKeyDown && currentEditorRef.current) {
|
if ((onKeyDown || !readOnly) && currentEditorRef.current) {
|
||||||
currentEditorRef.current.editor.keyBinding.addKeyboardHandler(onKeyDown);
|
currentEditorRef.current.editor.keyBinding.addKeyboardHandler(handleKeyDown);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
currentEditorRef.current.editor.keyBinding.removeKeyboardHandler(onKeyDown);
|
currentEditorRef.current.editor.keyBinding.removeKeyboardHandler(handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [onKeyDown]);
|
}, [handleKeyDown]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper ref={containerRef}>
|
<Wrapper ref={containerRef}>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function analyseQuerySources(sql, sourceNames) {
|
|||||||
name: word,
|
name: word,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (/^(where)|(inner)|(left)|(right)$/i.test(postWord)) {
|
if (/^(where)|(inner)|(left)|(right)|(on)$/i.test(postWord)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
res.push({
|
res.push({
|
||||||
|
|||||||
123
packages/web/src/sqleditor/useCodeCompletion.js
Normal file
123
packages/web/src/sqleditor/useCodeCompletion.js
Normal file
@@ -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]);
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ export default function ObjectListControl({ collection = [], title, showIfEmpty
|
|||||||
<TableColumn
|
<TableColumn
|
||||||
fieldName="displayName"
|
fieldName="displayName"
|
||||||
header="Name"
|
header="Name"
|
||||||
formatter={col => <AppObjectControl data={col} makeAppObj={makeAppObj} component="span" />}
|
formatter={(col) => <AppObjectControl data={col} makeAppObj={makeAppObj} component="span" />}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</TableControl>
|
</TableControl>
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import keycodes from './keycodes';
|
||||||
|
|
||||||
const Table = styled.table`
|
const Table = styled.table`
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
user-select: ${(props) =>
|
||||||
|
// @ts-ignore
|
||||||
|
props.focusable ? 'none' : ''};
|
||||||
|
// outline: none;
|
||||||
`;
|
`;
|
||||||
const TableHead = styled.thead``;
|
const TableHead = styled.thead``;
|
||||||
const TableBody = styled.tbody``;
|
const TableBody = styled.tbody``;
|
||||||
const TableHeaderRow = styled.tr``;
|
const TableHeaderRow = styled.tr``;
|
||||||
const TableBodyRow = styled.tr``;
|
const TableBodyRow = styled.tr`
|
||||||
|
background-color: ${(props) =>
|
||||||
|
// @ts-ignore
|
||||||
|
props.isSelected ? '#ccccff' : ''};
|
||||||
|
`;
|
||||||
const TableHeaderCell = styled.td`
|
const TableHeaderCell = styled.td`
|
||||||
border: 1px solid #e8eef4;
|
border: 1px solid #e8eef4;
|
||||||
background-color: #e8eef4;
|
background-color: #e8eef4;
|
||||||
@@ -30,26 +39,70 @@ function format(row, col) {
|
|||||||
return row[fieldName];
|
return row[fieldName];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TableControl({ rows = [], children }) {
|
export default function TableControl({
|
||||||
console.log('children', children);
|
rows = [],
|
||||||
|
children,
|
||||||
|
focusOnCreate = false,
|
||||||
|
onKeyDown = undefined,
|
||||||
|
tabIndex = -1,
|
||||||
|
setSelectedIndex = undefined,
|
||||||
|
selectedIndex = undefined,
|
||||||
|
tableRef = undefined,
|
||||||
|
}) {
|
||||||
const columns = (children instanceof Array ? _.flatten(children) : [children])
|
const columns = (children instanceof Array ? _.flatten(children) : [children])
|
||||||
.filter(child => child && child.props && child.props.fieldName)
|
.filter((child) => child && child.props && child.props.fieldName)
|
||||||
.map(child => child.props);
|
.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 (
|
return (
|
||||||
<Table>
|
<Table
|
||||||
|
ref={currentTableRef}
|
||||||
|
onKeyDown={selectedIndex != null ? handleKeyDown : undefined}
|
||||||
|
tabIndex={selectedIndex != null ? tabIndex : undefined}
|
||||||
|
// @ts-ignore
|
||||||
|
focusable={selectedIndex != null}
|
||||||
|
>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableHeaderRow>
|
<TableHeaderRow>
|
||||||
{columns.map(x => (
|
{columns.map((x) => (
|
||||||
<TableHeaderCell key={x.fieldName}>{x.header}</TableHeaderCell>
|
<TableHeaderCell key={x.fieldName}>{x.header}</TableHeaderCell>
|
||||||
))}
|
))}
|
||||||
</TableHeaderRow>
|
</TableHeaderRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{rows.map((row, index) => (
|
{rows.map((row, index) => (
|
||||||
<TableBodyRow key={index}>
|
<TableBodyRow
|
||||||
{columns.map(col => (
|
key={index}
|
||||||
|
// @ts-ignore
|
||||||
|
isSelected={index == selectedIndex}
|
||||||
|
onClick={
|
||||||
|
selectedIndex != null
|
||||||
|
? () => {
|
||||||
|
setSelectedIndex(index);
|
||||||
|
currentTableRef.current.focus();
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{columns.map((col) => (
|
||||||
<TableBodyCell key={col.fieldName}>{format(row, col)}</TableBodyCell>
|
<TableBodyCell key={col.fieldName}>{format(row, col)}</TableBodyCell>
|
||||||
))}
|
))}
|
||||||
</TableBodyRow>
|
</TableBodyRow>
|
||||||
|
|||||||
Reference in New Issue
Block a user