diff --git a/packages/tools/src/nameTools.ts b/packages/tools/src/nameTools.ts index 9d22301bd..ef76f1d56 100644 --- a/packages/tools/src/nameTools.ts +++ b/packages/tools/src/nameTools.ts @@ -1,4 +1,4 @@ -import { DatabaseInfo, DatabaseInfoObjects } from 'dbgate-types'; +import { ColumnInfo, DatabaseInfo, DatabaseInfoObjects, TableInfo } from 'dbgate-types'; export function fullNameFromString(name) { const m = name.match(/\[([^\]]+)\]\.\[([^\]]+)\]/); @@ -45,3 +45,7 @@ export function findObjectLike( // @ts-ignore return dbinfo[objectTypeField].find((x) => equalStringLike(x.pureName, pureName)); } + +export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo) { + return (table.foreignKeys || []).find((fk) => fk.columns.find((col) => col.columnName == column.columnName)); +} diff --git a/packages/web/src/designer/Designer.js b/packages/web/src/designer/Designer.js index 2aea4017d..c91bb24b1 100644 --- a/packages/web/src/designer/Designer.js +++ b/packages/web/src/designer/Designer.js @@ -7,6 +7,7 @@ import useTheme from '../theme/useTheme'; import DesignerReference from './DesignerReference'; import cleanupDesignColumns from './cleanupDesignColumns'; import { isConnectedByReference } from './designerTools'; +import { getTableInfo } from '../utility/metadataLoaders'; const Wrapper = styled.div` flex: 1; @@ -20,7 +21,7 @@ const Canvas = styled.div` position: relative; `; -export default function Designer({ value, onChange }) { +export default function Designer({ value, onChange, conid, database }) { const { tables, references } = value || {}; const theme = useTheme(); @@ -39,10 +40,11 @@ export default function Designer({ value, onChange }) { json.designerId = uuidv1(); json.left = e.clientX - rect.left; json.top = e.clientY - rect.top; - onChange({ - ...value, - tables: [...(tables || []), json], - }); + + onChange((current) => ({ + ...current, + tables: [...(current.tables || []), json], + })); }; const changeTable = React.useCallback( @@ -70,6 +72,9 @@ export default function Designer({ value, onChange }) { onChange((current) => ({ ...current, tables: (current.tables || []).filter((x) => x.designerId != table.designerId), + references: (current.references || []).filter( + (x) => x.sourceId != table.designerId && x.targetId != table.designerId + ), })); }, [onChange] @@ -144,6 +149,45 @@ export default function Designer({ value, onChange }) { }); }; + const handleAddReferenceByColumn = async (designerId, foreignKey) => { + const toTable = await getTableInfo({ + conid, + database, + pureName: foreignKey.refTableName, + schemaName: foreignKey.refSchemaName, + }); + const newTableDesignerId = uuidv1(); + onChange((current) => { + const fromTable = (current.tables || []).find((x) => x.designerId == designerId); + if (!fromTable) return; + return { + ...current, + tables: [ + ...(current.tables || []), + { + ...toTable, + left: fromTable.left + 300, + top: fromTable.top + 50, + designerId: newTableDesignerId, + }, + ], + references: [ + ...(current.references || []), + { + designerId: uuidv1(), + sourceId: fromTable.designerId, + targetId: newTableDesignerId, + joinType: 'INNER JOIN', + columns: foreignKey.columns.map((col) => ({ + source: col.columnName, + target: col.refColumnName, + })), + }, + ], + }; + }); + }; + const handleSelectColumn = React.useCallback( (column) => { onChange((current) => ({ @@ -211,6 +255,7 @@ export default function Designer({ value, onChange }) { onCreateReference={handleCreateReference} onSelectColumn={handleSelectColumn} onChangeColumn={handleChangeColumn} + onAddReferenceByColumn={handleAddReferenceByColumn} table={table} onChangeTable={changeTable} onBringToFront={bringToFront} diff --git a/packages/web/src/designer/DesignerTable.js b/packages/web/src/designer/DesignerTable.js index b5c107661..5d04e8028 100644 --- a/packages/web/src/designer/DesignerTable.js +++ b/packages/web/src/designer/DesignerTable.js @@ -1,11 +1,16 @@ import React from 'react'; import styled from 'styled-components'; +import { findForeignKeyForColumn } from 'dbgate-tools'; import ColumnLabel from '../datagrid/ColumnLabel'; import { FontIcon } from '../icons'; import useTheme from '../theme/useTheme'; import DomTableRef from './DomTableRef'; import _ from 'lodash'; import { CheckboxField } from '../utility/inputs'; +import { useShowMenu } from '../modals/showMenu'; +import { DropDownMenuDivider, DropDownMenuItem } from '../modals/DropDownMenu'; +import useShowModal from '../modals/showModal'; +import InputTextModal from '../modals/InputTextModal'; const Wrapper = styled.div` position: absolute; @@ -80,12 +85,50 @@ const ColumnLine = styled.div` `} `; +function TableContextMenu({ remove, setTableAlias, removeTableAlias }) { + return ( + <> + Remove + + Set table alias + {!!removeTableAlias && Remove table alias} + + ); +} + +function ColumnContextMenu({ setSortOrder, addReference }) { + return ( + <> + setSortOrder(1)}>Sort ascending + setSortOrder(-1)}>Sort descending + setSortOrder(0)}>Unsort + {!!addReference && Add reference} + + ); +} + +function ColumnDesignerIcons({ column, designerId, designer }) { + const designerColumn = (designer.columns || []).find( + (x) => x.designerId == designerId && x.columnName == column.columnName + ); + if (!designerColumn) return null; + return ( + <> + {!!designerColumn.filter && } + {designerColumn.sortOrder > 0 && } + {designerColumn.sortOrder < 0 && } + {!!designerColumn.isGrouped && } + + ); +} + export default function DesignerTable({ table, onChangeTable, onBringToFront, onRemoveTable, onCreateReference, + onAddReferenceByColumn, onSelectColumn, onChangeColumn, sourceDragColumn, @@ -97,11 +140,13 @@ export default function DesignerTable({ setChangeToken, designer, }) { - const { pureName, columns, left, top, designerId } = table; + const { pureName, columns, left, top, designerId, alias } = table; const [movingPosition, setMovingPosition] = React.useState(null); const movingPositionRef = React.useRef(null); const theme = useTheme(); const domObjectsRef = React.useRef({}); + const showMenu = useShowMenu(); + const showModal = useShowModal(); const moveStartXRef = React.useRef(null); const moveStartYRef = React.useRef(null); @@ -185,6 +230,71 @@ export default function DesignerTable({ changeTokenDebounced.current(); }; + const handleSetTableAlias = () => { + showModal((modalState) => ( + { + onChangeTable({ + ...table, + alias: newAlias, + }); + }} + /> + )); + }; + + const handleHeaderContextMenu = (event) => { + event.preventDefault(); + showMenu( + event.pageX, + event.pageY, + onRemoveTable({ designerId })} + setTableAlias={handleSetTableAlias} + removeTableAlias={ + alias + ? () => + onChangeTable({ + ...table, + alias: null, + }) + : null + } + /> + ); + }; + + const handleColumnContextMenu = (column) => (event) => { + event.preventDefault(); + const foreignKey = findForeignKeyForColumn(table, column); + showMenu( + event.pageX, + event.pageY, + { + onChangeColumn( + { + ...column, + designerId, + }, + (col) => ({ ...col, sortOrder }) + ); + }} + addReference={ + foreignKey + ? () => { + onAddReferenceByColumn(designerId, foreignKey); + } + : null + } + /> + ); + }; + return ( onBringToFront(table)} ref={(dom) => dispatchDomColumn('', dom)} > -
- {pureName} +
+ {alias || pureName} onRemoveTable(table)} theme={theme}> @@ -204,6 +314,7 @@ export default function DesignerTable({ {(columns || []).map((column) => ( - + + ))} diff --git a/packages/web/src/designer/QueryDesigner.js b/packages/web/src/designer/QueryDesigner.js index f5f1969ae..031fe40ed 100644 --- a/packages/web/src/designer/QueryDesigner.js +++ b/packages/web/src/designer/QueryDesigner.js @@ -3,5 +3,5 @@ import styled from 'styled-components'; import Designer from './Designer'; export default function QueryDesigner({ value, conid, database, engine, onChange }) { - return ; + return ; } diff --git a/packages/web/src/designer/designerTools.ts b/packages/web/src/designer/designerTools.ts index 356b1fca8..a1061a1ff 100644 --- a/packages/web/src/designer/designerTools.ts +++ b/packages/web/src/designer/designerTools.ts @@ -34,7 +34,7 @@ export function findConnectingReference( tables2: DesignerTableInfo[], additionalCondition: (ref: DesignerReferenceInfo) => boolean ) { - for (const ref of designer.references) { + for (const ref of designer.references || []) { if (additionalCondition(ref) && referenceIsConnecting(ref, tables1, tables2)) { return ref; } @@ -119,6 +119,7 @@ export function isConnectedByReference( table2: { designerId: string }, withoutRef: { designerId: string } ) { + if (!designer.references) return false; const creator = new DesignerComponentCreator({ ...designer, references: withoutRef diff --git a/packages/web/src/icons.js b/packages/web/src/icons.js index e247a3447..14bf7424e 100644 --- a/packages/web/src/icons.js +++ b/packages/web/src/icons.js @@ -85,6 +85,8 @@ const iconNames = { 'img reference': 'mdi mdi-link-box', 'img link': 'mdi mdi-link', + 'img filter': 'mdi mdi-filter', + 'img group': 'mdi mdi-group', }; export function FontIcon({ icon, className = '', ...other }) { diff --git a/packages/web/src/tabs/QueryDesignTab.js b/packages/web/src/tabs/QueryDesignTab.js index 50f56b0c2..170811d32 100644 --- a/packages/web/src/tabs/QueryDesignTab.js +++ b/packages/web/src/tabs/QueryDesignTab.js @@ -102,7 +102,7 @@ export default function QueryDesignTab({ sesid, sql: sqlPreview, }); - }, [busy]); + }, [busy, conid, sessionId, database, sqlPreview]); const handleCancel = () => { axios.post('sessions/cancel', { @@ -118,12 +118,15 @@ export default function QueryDesignTab({ setBusy(false); }; - const handleKeyDown = React.useCallback((e) => { - if (e.keyCode == keycodes.f5) { - e.preventDefault(); - handleExecute(); - } - }, []); + const handleKeyDown = React.useCallback( + (e) => { + if (e.keyCode == keycodes.f5) { + e.preventDefault(); + handleExecute(); + } + }, + [handleExecute] + ); React.useEffect(() => { if (tabVisible) {