Added the ability to search for tables and automatically bring them into view in the ERD tool. #4306
parent
08379d6ae0
commit
986ba41ba9
|
|
@ -86,6 +86,9 @@ Editing Options
|
||||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||||
| Icon | Behavior | Shortcut |
|
| Icon | Behavior | Shortcut |
|
||||||
+======================+===================================================================================================+================+
|
+======================+===================================================================================================+================+
|
||||||
|
| *Search table* | Click to search for a table in the diagram. Selecting a table from the search results will bring | Option/Alt + |
|
||||||
|
| | it into view and highlight it. | Ctrl + F |
|
||||||
|
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||||
| *Add table* | Click this button to add a new table to the diagram. On clicking, this will open a table dialog | Option/Alt + |
|
| *Add table* | Click this button to add a new table to the diagram. On clicking, this will open a table dialog | Option/Alt + |
|
||||||
| | where you can put the table details. | Ctrl + A |
|
| | where you can put the table details. | Ctrl + A |
|
||||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||||
|
|
@ -109,11 +112,14 @@ Table Relationship Options
|
||||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||||
| Icon | Behavior | Shortcut |
|
| Icon | Behavior | Shortcut |
|
||||||
+======================+===================================================================================================+================+
|
+======================+===================================================================================================+================+
|
||||||
| *1M* | Click this button to open a one-to-many relationship dialog to add a relationship between the | Option/Alt + |
|
| *1-1* | Click this button to open a one-to-one relationship dialog to add a relationship between the | Option/Alt + |
|
||||||
|
| | two tables. The selected table becomes the referencing table. | Ctrl + B |
|
||||||
|
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||||
|
| *1-M* | Click this button to open a one-to-many relationship dialog to add a relationship between the | Option/Alt + |
|
||||||
| | two tables. The selected table becomes the referencing table and will have the *many* endpoint of | Ctrl + O |
|
| | two tables. The selected table becomes the referencing table and will have the *many* endpoint of | Ctrl + O |
|
||||||
| | the link. | |
|
| | the link. | |
|
||||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||||
| *MM* | Click this button to open a many-to-many relationship dialog to add a relationship between the | Option/Alt + |
|
| *M-M* | Click this button to open a many-to-many relationship dialog to add a relationship between the | Option/Alt + |
|
||||||
| | two tables. This option will create a new table based on the selected columns for the two relating| Ctrl + M |
|
| | two tables. This option will create a new table based on the selected columns for the two relating| Ctrl + M |
|
||||||
| | tables and link them. | |
|
| | tables and link them. | |
|
||||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 679 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
|
@ -20,6 +20,7 @@ Bundled PostgreSQL Utilities
|
||||||
New features
|
New features
|
||||||
************
|
************
|
||||||
|
|
||||||
|
| `Issue #4306 <https://github.com/pgadmin-org/pgadmin4/issues/4306>`_ - Added the ability to search for tables and automatically bring them into view in the ERD tool.
|
||||||
| `Issue #6698 <https://github.com/pgadmin-org/pgadmin4/issues/6698>`_ - Add support for setting image download resolution in the ERD tool.
|
| `Issue #6698 <https://github.com/pgadmin-org/pgadmin4/issues/6698>`_ - Add support for setting image download resolution in the ERD tool.
|
||||||
| `Issue #7885 <https://github.com/pgadmin-org/pgadmin4/issues/7885>`_ - Add support for displaying detailed Citus query plans instead of 'Custom Scan' placeholder.
|
| `Issue #7885 <https://github.com/pgadmin-org/pgadmin4/issues/7885>`_ - Add support for displaying detailed Citus query plans instead of 'Custom Scan' placeholder.
|
||||||
| `Issue #8912 <https://github.com/pgadmin-org/pgadmin4/issues/8912>`_ - Add support for formatting .pgerd ERD project file.
|
| `Issue #8912 <https://github.com/pgadmin-org/pgadmin4/issues/8912>`_ - Add support for formatting .pgerd ERD project file.
|
||||||
|
|
@ -34,4 +35,6 @@ Bug fixes
|
||||||
|
|
||||||
| `Issue #8504 <https://github.com/pgadmin-org/pgadmin4/issues/8504>`_ - Fixed an issue where data output column resize is not sticking in Safari.
|
| `Issue #8504 <https://github.com/pgadmin-org/pgadmin4/issues/8504>`_ - Fixed an issue where data output column resize is not sticking in Safari.
|
||||||
| `Issue #9117 <https://github.com/pgadmin-org/pgadmin4/issues/9117>`_ - Fixed an issue where Schema Diff does not ignore Tablespace for indexes.
|
| `Issue #9117 <https://github.com/pgadmin-org/pgadmin4/issues/9117>`_ - Fixed an issue where Schema Diff does not ignore Tablespace for indexes.
|
||||||
|
| `Issue #9132 <https://github.com/pgadmin-org/pgadmin4/issues/9132>`_ - Fixed an issue where the 2FA window redirected to the login page after session expiration.
|
||||||
|
| `Issue #9240 <https://github.com/pgadmin-org/pgadmin4/issues/9240>`_ - Fixed an issue where the Debian build process failed with a "Sphinx module not found" error when using a Python virtual environment.
|
||||||
| `Issue #9304 <https://github.com/pgadmin-org/pgadmin4/issues/9304>`_ - Fixed an issue that prevented assigning multiple users to an RLS policy.
|
| `Issue #9304 <https://github.com/pgadmin-org/pgadmin4/issues/9304>`_ - Fixed an issue that prevented assigning multiple users to an RLS policy.
|
||||||
|
|
@ -295,7 +295,7 @@ const StyleDialog = styled(Dialog)(({theme}) => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function ModalContainer({ id, title, content, dialogHeight, dialogWidth, onClose, fullScreen = false, isFullWidth = false, showFullScreen = false, isResizeable = false, minHeight = MIN_HEIGHT, minWidth = MIN_WIDTH, showTitle=true }) {
|
function ModalContainer({ id, title, content, dialogHeight, dialogWidth, onClose, fullScreen = false, isFullWidth = false, showFullScreen = false, isResizeable = false, minHeight = MIN_HEIGHT, minWidth = MIN_WIDTH, showTitle=true, ...props }) {
|
||||||
let useModalRef = useModal();
|
let useModalRef = useModal();
|
||||||
let closeModal = (_e, reason) => {
|
let closeModal = (_e, reason) => {
|
||||||
if(reason == 'backdropClick' && showTitle) {
|
if(reason == 'backdropClick' && showTitle) {
|
||||||
|
|
@ -321,6 +321,7 @@ function ModalContainer({ id, title, content, dialogHeight, dialogWidth, onClose
|
||||||
fullScreen={isFullScreen}
|
fullScreen={isFullScreen}
|
||||||
fullWidth={isFullWidth}
|
fullWidth={isFullWidth}
|
||||||
disablePortal
|
disablePortal
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
{ showTitle &&
|
{ showTitle &&
|
||||||
<DialogTitle className='modal-drag-area'>
|
<DialogTitle className='modal-drag-area'>
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,24 @@ class ERDModule(PgAdminModule):
|
||||||
fields=shortcut_fields
|
fields=shortcut_fields
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.preference.register(
|
||||||
|
'keyboard_shortcuts',
|
||||||
|
'search_table',
|
||||||
|
gettext('Search table'),
|
||||||
|
'keyboardshortcut',
|
||||||
|
{
|
||||||
|
'alt': True,
|
||||||
|
'shift': False,
|
||||||
|
'control': True,
|
||||||
|
'key': {
|
||||||
|
'key_code': 70,
|
||||||
|
'char': 'f'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
|
||||||
|
fields=shortcut_fields
|
||||||
|
)
|
||||||
|
|
||||||
self.preference.register(
|
self.preference.register(
|
||||||
'keyboard_shortcuts',
|
'keyboard_shortcuts',
|
||||||
'add_table',
|
'add_table',
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,13 @@ export const ERD_EVENTS = {
|
||||||
TRIGGER_SHOW_SQL: 'TRIGGER_SHOW_SQL',
|
TRIGGER_SHOW_SQL: 'TRIGGER_SHOW_SQL',
|
||||||
SHOW_SQL: 'SHOW_SQL',
|
SHOW_SQL: 'SHOW_SQL',
|
||||||
DOWNLOAD_IMAGE: 'DOWNLOAD_IMAGE',
|
DOWNLOAD_IMAGE: 'DOWNLOAD_IMAGE',
|
||||||
|
|
||||||
|
SEARCH_NODE: 'SEARCH_NODE',
|
||||||
ADD_NODE: 'ADD_NODE',
|
ADD_NODE: 'ADD_NODE',
|
||||||
EDIT_NODE: 'EDIT_NODE',
|
EDIT_NODE: 'EDIT_NODE',
|
||||||
CLONE_NODE: 'CLONE_NODE',
|
CLONE_NODE: 'CLONE_NODE',
|
||||||
DELETE_NODE: 'DELETE_NODE',
|
DELETE_NODE: 'DELETE_NODE',
|
||||||
|
|
||||||
SHOW_NOTE: 'SHOW_NOTE',
|
SHOW_NOTE: 'SHOW_NOTE',
|
||||||
ONE_TO_ONE: 'ONE_TO_ONE',
|
ONE_TO_ONE: 'ONE_TO_ONE',
|
||||||
ONE_TO_MANY: 'ONE_TO_MANY',
|
ONE_TO_MANY: 'ONE_TO_MANY',
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import { useApplicationState } from '../../../../../../settings/static/Applicati
|
||||||
import { connectServerModal, connectServer } from '../../../../../sqleditor/static/js/components/connectServer';
|
import { connectServerModal, connectServer } from '../../../../../sqleditor/static/js/components/connectServer';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { FileManagerUtils } from '../../../../../../misc/file_manager/static/js/components/FileManager';
|
import { FileManagerUtils } from '../../../../../../misc/file_manager/static/js/components/FileManager';
|
||||||
|
import SearchNode from './SearchNode';
|
||||||
|
|
||||||
/* Custom react-diagram action for keyboard events */
|
/* Custom react-diagram action for keyboard events */
|
||||||
export class KeyboardShortcutAction extends Action {
|
export class KeyboardShortcutAction extends Action {
|
||||||
|
|
@ -178,9 +179,9 @@ export default class ERDTool extends React.Component {
|
||||||
this.eventBus = new EventBus();
|
this.eventBus = new EventBus();
|
||||||
|
|
||||||
_.bindAll(this, ['onLoadDiagram', 'onSaveDiagram', 'onSQLClick',
|
_.bindAll(this, ['onLoadDiagram', 'onSaveDiagram', 'onSQLClick',
|
||||||
'onImageClick', 'onAddNewNode', 'onEditTable', 'onCloneNode', 'onDeleteNode', 'onNoteClick',
|
'onImageClick', 'onSearchNode', 'onAddNewNode', 'onEditTable', 'onCloneNode', 'onDeleteNode', 'onNoteClick',
|
||||||
'onNoteClose', 'onOneToOneClick', 'onOneToManyClick', 'onManyToManyClick', 'onAutoDistribute', 'onDetailsToggle',
|
'onNoteClose', 'onOneToOneClick', 'onOneToManyClick', 'onManyToManyClick', 'onAutoDistribute', 'onDetailsToggle',
|
||||||
'onChangeColors', 'onDropNode', 'onNotationChange', 'closePanel'
|
'onChangeColors', 'onDropNode', 'onNotationChange', 'closePanel', 'scrollToNode'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.diagram.zoomToFit = this.diagram.zoomToFit.bind(this.diagram);
|
this.diagram.zoomToFit = this.diagram.zoomToFit.bind(this.diagram);
|
||||||
|
|
@ -249,6 +250,7 @@ export default class ERDTool extends React.Component {
|
||||||
this.eventBus.registerListener(ERD_EVENTS.SAVE_DIAGRAM, this.onSaveDiagram);
|
this.eventBus.registerListener(ERD_EVENTS.SAVE_DIAGRAM, this.onSaveDiagram);
|
||||||
this.eventBus.registerListener(ERD_EVENTS.SHOW_SQL, this.onSQLClick);
|
this.eventBus.registerListener(ERD_EVENTS.SHOW_SQL, this.onSQLClick);
|
||||||
this.eventBus.registerListener(ERD_EVENTS.DOWNLOAD_IMAGE, this.onImageClick);
|
this.eventBus.registerListener(ERD_EVENTS.DOWNLOAD_IMAGE, this.onImageClick);
|
||||||
|
this.eventBus.registerListener(ERD_EVENTS.SEARCH_NODE, this.onSearchNode);
|
||||||
this.eventBus.registerListener(ERD_EVENTS.ADD_NODE, this.onAddNewNode);
|
this.eventBus.registerListener(ERD_EVENTS.ADD_NODE, this.onAddNewNode);
|
||||||
this.eventBus.registerListener(ERD_EVENTS.EDIT_NODE, this.onEditTable);
|
this.eventBus.registerListener(ERD_EVENTS.EDIT_NODE, this.onEditTable);
|
||||||
this.eventBus.registerListener(ERD_EVENTS.CLONE_NODE, this.onCloneNode);
|
this.eventBus.registerListener(ERD_EVENTS.CLONE_NODE, this.onCloneNode);
|
||||||
|
|
@ -285,6 +287,9 @@ export default class ERDTool extends React.Component {
|
||||||
[this.state.preferences.download_image, ()=>{
|
[this.state.preferences.download_image, ()=>{
|
||||||
this.eventBus.fireEvent(ERD_EVENTS.DOWNLOAD_IMAGE);
|
this.eventBus.fireEvent(ERD_EVENTS.DOWNLOAD_IMAGE);
|
||||||
}],
|
}],
|
||||||
|
[this.state.preferences.search_table, ()=>{
|
||||||
|
this.eventBus.fireEvent(ERD_EVENTS.SEARCH_NODE);
|
||||||
|
}],
|
||||||
[this.state.preferences.add_table, ()=>{
|
[this.state.preferences.add_table, ()=>{
|
||||||
this.eventBus.fireEvent(ERD_EVENTS.ADD_NODE);
|
this.eventBus.fireEvent(ERD_EVENTS.ADD_NODE);
|
||||||
}],
|
}],
|
||||||
|
|
@ -488,12 +493,69 @@ export default class ERDTool extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scrollToNode(node) {
|
||||||
|
const engine = this.diagram.getEngine();
|
||||||
|
const model = engine.getModel();
|
||||||
|
const container = this.canvasEle;
|
||||||
|
if (!node || !container) return;
|
||||||
|
|
||||||
|
const { x, y } = node.getPosition();
|
||||||
|
const zoom = model.getZoomLevel() / 100;
|
||||||
|
const offsetX = model.getOffsetX();
|
||||||
|
const offsetY = model.getOffsetY();
|
||||||
|
|
||||||
|
const viewportWidth = container.clientWidth;
|
||||||
|
const viewportHeight = container.clientHeight;
|
||||||
|
|
||||||
|
const nodeWidth = node.width; // Approximate width of a table node
|
||||||
|
const nodeHeight = node.height; // Approximate height of a table node
|
||||||
|
|
||||||
|
// Node screen bounds
|
||||||
|
const nodeLeft = x * zoom + offsetX;
|
||||||
|
const nodeRight = nodeLeft + nodeWidth * zoom;
|
||||||
|
const nodeTop = y * zoom + offsetY;
|
||||||
|
const nodeBottom = nodeTop + nodeHeight * zoom;
|
||||||
|
|
||||||
|
let newOffsetX = offsetX;
|
||||||
|
let newOffsetY = offsetY;
|
||||||
|
|
||||||
|
// Check horizontal visibility
|
||||||
|
if (nodeLeft < 0) {
|
||||||
|
newOffsetX += -nodeLeft + 20; // 20px padding
|
||||||
|
} else if (nodeRight > viewportWidth) {
|
||||||
|
newOffsetX -= nodeRight - viewportWidth + 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check vertical visibility
|
||||||
|
if (nodeHeight * zoom >= viewportHeight) {
|
||||||
|
// Node taller than viewport: snap top of node to top of viewport
|
||||||
|
newOffsetY = offsetY + viewportHeight / 2 - (nodeHeight * zoom) / 2;
|
||||||
|
newOffsetY = offsetY - (nodeTop - 20); // aligns top
|
||||||
|
} else {
|
||||||
|
// Node fits in viewport: ensure fully visible
|
||||||
|
if (nodeTop < 0) {
|
||||||
|
newOffsetY += -nodeTop + 20;
|
||||||
|
} else if (nodeBottom > viewportHeight) {
|
||||||
|
newOffsetY -= nodeBottom - viewportHeight + 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update offset only if needed
|
||||||
|
if (newOffsetX !== offsetX || newOffsetY !== offsetY) {
|
||||||
|
model.setOffset(newOffsetX, newOffsetY);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.diagram.repaint();
|
||||||
|
node.setSelected(true);
|
||||||
|
node.fireEvent({}, 'highlightFlash');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
addEditTable(node) {
|
addEditTable(node) {
|
||||||
let dialog = this.getDialog('table_dialog');
|
let dialog = this.getDialog('table_dialog');
|
||||||
if(node) {
|
if(node) {
|
||||||
let [schema, table] = node.getSchemaTableName();
|
|
||||||
let oldData = node.getData();
|
let oldData = node.getData();
|
||||||
dialog(gettext('Table: %s (%s)', _.escape(table),_.escape(schema)), oldData, false, (newData)=>{
|
dialog(gettext('Table: %s', node.getDisplayName()), oldData, false, (newData)=>{
|
||||||
if(this.diagram.anyDuplicateNodeName(newData, oldData)) {
|
if(this.diagram.anyDuplicateNodeName(newData, oldData)) {
|
||||||
return gettext('Table name already exists');
|
return gettext('Table name already exists');
|
||||||
}
|
}
|
||||||
|
|
@ -560,6 +622,12 @@ export default class ERDTool extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSearchNode() {
|
||||||
|
this.context.showModal(gettext('Search'), (closeModal)=>(
|
||||||
|
<SearchNode tableNodes={this.diagram.getModel().getNodesDict()} onClose={closeModal} scrollToNode={this.scrollToNode} />
|
||||||
|
), {id: 'id-erd-search-node', showTitle: false, disableRestoreFocus: true});
|
||||||
|
}
|
||||||
|
|
||||||
onAddNewNode() {
|
onAddNewNode() {
|
||||||
this.addEditTable();
|
this.addEditTable();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,7 @@ export default function FloatingNote({open, onClose, anchorEl, rows, noteNode})
|
||||||
|
|
||||||
const header = useMemo(()=>{
|
const header = useMemo(()=>{
|
||||||
if(noteNode) {
|
if(noteNode) {
|
||||||
let [schema, name] = noteNode.getSchemaTableName();
|
return noteNode.getDisplayName();
|
||||||
return `${name} (${schema})`;
|
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import ImageRoundedIcon from '@mui/icons-material/ImageRounded';
|
||||||
import FormatColorFillRoundedIcon from '@mui/icons-material/FormatColorFillRounded';
|
import FormatColorFillRoundedIcon from '@mui/icons-material/FormatColorFillRounded';
|
||||||
import FormatColorTextRoundedIcon from '@mui/icons-material/FormatColorTextRounded';
|
import FormatColorTextRoundedIcon from '@mui/icons-material/FormatColorTextRounded';
|
||||||
import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined';
|
import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined';
|
||||||
|
import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined';
|
||||||
|
|
||||||
import { PgMenu, PgMenuItem, usePgMenuGroup } from '../../../../../../static/js/components/Menu';
|
import { PgMenu, PgMenuItem, usePgMenuGroup } from '../../../../../../static/js/components/Menu';
|
||||||
import gettext from 'sources/gettext';
|
import gettext from 'sources/gettext';
|
||||||
|
|
@ -201,6 +202,11 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati
|
||||||
}} />
|
}} />
|
||||||
</PgButtonGroup>
|
</PgButtonGroup>
|
||||||
<PgButtonGroup size="small">
|
<PgButtonGroup size="small">
|
||||||
|
<PgIconButton title={gettext('Search Table')} icon={<SearchOutlinedIcon />}
|
||||||
|
shortcut={preferences.search_table}
|
||||||
|
onClick={()=>{
|
||||||
|
eventBus.fireEvent(ERD_EVENTS.SEARCH_NODE);
|
||||||
|
}} />
|
||||||
<PgIconButton title={gettext('Add Table')} icon={<AddBoxIcon />}
|
<PgIconButton title={gettext('Add Table')} icon={<AddBoxIcon />}
|
||||||
shortcut={preferences.add_table}
|
shortcut={preferences.add_table}
|
||||||
onClick={()=>{
|
onClick={()=>{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// pgAdmin 4 - PostgreSQL Tools
|
||||||
|
//
|
||||||
|
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
||||||
|
// This software is released under the PostgreSQL Licence
|
||||||
|
//
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { InputSelect } from '../../../../../../static/js/components/FormComponents';
|
||||||
|
|
||||||
|
|
||||||
|
export default function SearchNode({tableNodes, onClose, scrollToNode}) {
|
||||||
|
const onSelectChange = (val) => {
|
||||||
|
let node = tableNodes[val];
|
||||||
|
if(node) {
|
||||||
|
scrollToNode(node);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputSelect
|
||||||
|
options={Object.values(tableNodes).map(node => ({
|
||||||
|
value: node.getID(),
|
||||||
|
label: node.getDisplayName(),
|
||||||
|
}))}
|
||||||
|
onChange={onSelectChange}
|
||||||
|
autoFocus
|
||||||
|
placeholder="Select a table"
|
||||||
|
openMenuOnFocus
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchNode.propTypes = {
|
||||||
|
tableNodes: PropTypes.object.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
@ -50,8 +50,7 @@ class ManyToManySchema extends BaseUISchema {
|
||||||
export function getManyToManyDialogSchema(attributes, tableNodesDict) {
|
export function getManyToManyDialogSchema(attributes, tableNodesDict) {
|
||||||
let tablesData = [];
|
let tablesData = [];
|
||||||
_.forEach(tableNodesDict, (node, uid)=>{
|
_.forEach(tableNodesDict, (node, uid)=>{
|
||||||
let [schema, name] = node.getSchemaTableName();
|
tablesData.push({value: uid, label: node.getDisplayName(), image: 'icon-table'});
|
||||||
tablesData.push({value: uid, label: `(${schema}) ${name}`, image: 'icon-table'});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return new ManyToManySchema({
|
return new ManyToManySchema({
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,7 @@ class OneToManySchema extends BaseUISchema {
|
||||||
export function getOneToManyDialogSchema(attributes, tableNodesDict) {
|
export function getOneToManyDialogSchema(attributes, tableNodesDict) {
|
||||||
let tablesData = [];
|
let tablesData = [];
|
||||||
_.forEach(tableNodesDict, (node, uid)=>{
|
_.forEach(tableNodesDict, (node, uid)=>{
|
||||||
let [schema, name] = node.getSchemaTableName();
|
tablesData.push({value: uid, label: node.getDisplayName(), image: 'icon-table'});
|
||||||
tablesData.push({value: uid, label: `(${schema}) ${name}`, image: 'icon-table'});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return new OneToManySchema({
|
return new OneToManySchema({
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,7 @@ class OneToOneSchema extends BaseUISchema {
|
||||||
export function getOneToOneDialogSchema(attributes, tableNodesDict) {
|
export function getOneToOneDialogSchema(attributes, tableNodesDict) {
|
||||||
let tablesData = [];
|
let tablesData = [];
|
||||||
_.forEach(tableNodesDict, (node, uid)=>{
|
_.forEach(tableNodesDict, (node, uid)=>{
|
||||||
let [schema, name] = node.getSchemaTableName();
|
tablesData.push({value: uid, label: node.getDisplayName(), image: 'icon-table'});
|
||||||
tablesData.push({value: uid, label: `(${schema}) ${name}`, image: 'icon-table'});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return new OneToOneSchema({
|
return new OneToOneSchema({
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,7 @@ export function getTableDialogSchema(attributes, isNew, tableNodesDict, colTypes
|
||||||
references: ()=>{
|
references: ()=>{
|
||||||
let retOpts = [];
|
let retOpts = [];
|
||||||
_.forEach(tableNodesDict, (node, uid)=>{
|
_.forEach(tableNodesDict, (node, uid)=>{
|
||||||
let [schema, name] = node.getSchemaTableName();
|
retOpts.push({value: uid, label: node.getDisplayName()});
|
||||||
retOpts.push({value: uid, label: `(${schema}) ${name}`});
|
|
||||||
});
|
});
|
||||||
return retOpts;
|
return retOpts;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,10 @@ export class TableNodeModel extends DefaultNodeModel {
|
||||||
return [this._data.schema, this._data.name];
|
return [this._data.schema, this._data.name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDisplayName() {
|
||||||
|
return `(${this._data.schema}) ${this._data.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
Object.values(this.getPorts()).forEach((port)=>{
|
Object.values(this.getPorts()).forEach((port)=>{
|
||||||
port.removeAllLinks();
|
port.removeAllLinks();
|
||||||
|
|
@ -218,6 +222,19 @@ const StyledDiv = styled('div')(({theme})=>({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: `${TABLE_WIDTH}px`,
|
width: `${TABLE_WIDTH}px`,
|
||||||
fontSize: '0.8em',
|
fontSize: '0.8em',
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
|
||||||
|
'&.flash': {
|
||||||
|
animation: 'flash 2s ease-in-out',
|
||||||
|
},
|
||||||
|
'@keyframes flash': {
|
||||||
|
'0%': {
|
||||||
|
boxShadow: `0 0 10px 5px ${theme.palette.primary.main}`,
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
'& .TableNode-tableContent': {
|
'& .TableNode-tableContent': {
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
|
|
@ -263,10 +280,21 @@ const StyledDiv = styled('div')(({theme})=>({
|
||||||
padding: '0.125rem 0.25rem',
|
padding: '0.125rem 0.25rem',
|
||||||
wordBreak: 'break-all',
|
wordBreak: 'break-all',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'&:last-child': {
|
||||||
|
borderBottom: 'none',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'&.TableNode-tableNodeSelected': {
|
'&.TableNode-tableNodeSelected': {
|
||||||
borderColor: theme.palette.primary.main,
|
'& .TableNode-tableToolbar': {
|
||||||
|
borderColor: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
'& .TableNode-tableContent': {
|
||||||
|
borderLeftColor: theme.palette.primary.main,
|
||||||
|
borderRightColor: theme.palette.primary.main,
|
||||||
|
borderBottomColor: theme.palette.primary.main,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -276,6 +304,7 @@ export class TableNodeWidget extends React.Component {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
show_details: true,
|
show_details: true,
|
||||||
|
flash: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.tableNodeEventListener = this.props.node.registerListener({
|
this.tableNodeEventListener = this.props.node.registerListener({
|
||||||
|
|
@ -291,6 +320,12 @@ export class TableNodeWidget extends React.Component {
|
||||||
dataAvaiable: ()=>{
|
dataAvaiable: ()=>{
|
||||||
/* Just re-render */
|
/* Just re-render */
|
||||||
this.setState({});
|
this.setState({});
|
||||||
|
},
|
||||||
|
highlightFlash: ()=>{
|
||||||
|
this.setState({flash: true});
|
||||||
|
setTimeout(()=>{
|
||||||
|
this.setState({flash: false});
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -368,13 +403,21 @@ export class TableNodeWidget extends React.Component {
|
||||||
(tableData.unique_constraint||[]).forEach((uk)=>{
|
(tableData.unique_constraint||[]).forEach((uk)=>{
|
||||||
localUkCols.push(...uk.columns.map((c)=>c.column));
|
localUkCols.push(...uk.columns.map((c)=>c.column));
|
||||||
});
|
});
|
||||||
const styles = {
|
const contentStyles = {
|
||||||
backgroundColor: tableMetaData.fillColor,
|
backgroundColor: tableMetaData.fillColor,
|
||||||
color: tableMetaData.textColor,
|
color: tableMetaData.textColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let classList = ['TableNode-tableNode'];
|
||||||
|
if(this.props.node.isSelected()) {
|
||||||
|
classList.push('TableNode-tableNodeSelected');
|
||||||
|
}
|
||||||
|
if(this.state.flash) {
|
||||||
|
classList.push('flash');
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<StyledDiv className={['TableNode-tableNode', (this.props.node.isSelected() ? 'TableNode-tableNodeSelected': '')].join(' ')}
|
<StyledDiv className={classList.join(' ')}
|
||||||
onDoubleClick={()=>{this.props.node.fireEvent({}, 'editTable');}} style={styles}>
|
onDoubleClick={()=>{this.props.node.fireEvent({}, 'editTable');}}>
|
||||||
<div className={'TableNode-tableToolbar'}>
|
<div className={'TableNode-tableToolbar'}>
|
||||||
<PgIconButton size="xs" title={gettext('Show Details')} icon={this.state.show_details ? <VisibilityRoundedIcon /> : <VisibilityOffRoundedIcon />}
|
<PgIconButton size="xs" title={gettext('Show Details')} icon={this.state.show_details ? <VisibilityRoundedIcon /> : <VisibilityOffRoundedIcon />}
|
||||||
onClick={this.toggleShowDetails} onDoubleClick={(e)=>{e.stopPropagation();}} />
|
onClick={this.toggleShowDetails} onDoubleClick={(e)=>{e.stopPropagation();}} />
|
||||||
|
|
@ -386,7 +429,7 @@ export class TableNodeWidget extends React.Component {
|
||||||
}}
|
}}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
<div className='TableNode-tableContent'>
|
<div className='TableNode-tableContent' style={contentStyles}>
|
||||||
{tableMetaData.is_promise &&
|
{tableMetaData.is_promise &&
|
||||||
<div className='TableNode-tableSection'>
|
<div className='TableNode-tableSection'>
|
||||||
{!tableMetaData.data_failed && <div className='TableNode-tableNameText'>{gettext('Fetching...')}</div>}
|
{!tableMetaData.data_failed && <div className='TableNode-tableNameText'>{gettext('Fetching...')}</div>}
|
||||||
|
|
|
||||||
|
|
@ -65,18 +65,25 @@ describe('SchemaView', ()=>{
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
simulateValidData = async ()=>{
|
simulateValidData = async ()=>{
|
||||||
|
// Wait for focus
|
||||||
|
await act(async ()=>{
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
});
|
||||||
await user.type(ctrl.container.querySelector('[name="field1"]'), 'val1');
|
await user.type(ctrl.container.querySelector('[name="field1"]'), 'val1');
|
||||||
await user.type(ctrl.container.querySelector('[name="field2"]'), '2');
|
await user.type(ctrl.container.querySelector('[name="field2"]'), '2');
|
||||||
await user.type(ctrl.container.querySelector('[name="field5"]'), 'val5');
|
await user.type(ctrl.container.querySelector('[name="field5"]'), 'val5');
|
||||||
/* Add a row */
|
/* Add a row */
|
||||||
await user.click(ctrl.container.querySelector('button[data-test="add-row"]'));
|
await user.click(ctrl.container.querySelector('button[data-test="add-row"]'));
|
||||||
await user.click(ctrl.container.querySelector('button[data-test="add-row"]'));
|
await user.click(ctrl.container.querySelector('button[data-test="add-row"]'));
|
||||||
|
// Wait for focus
|
||||||
|
await act(async ()=>{
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
});
|
||||||
await user.type(ctrl.container.querySelectorAll('[name="field5"]')[0], 'rval51');
|
await user.type(ctrl.container.querySelectorAll('[name="field5"]')[0], 'rval51');
|
||||||
await user.type(ctrl.container.querySelectorAll('[name="field5"]')[1], 'rval52');
|
await user.type(ctrl.container.querySelectorAll('[name="field5"]')[1], 'rval52');
|
||||||
// Wait for validations to run
|
// Wait for validations to run
|
||||||
await act(async ()=>{
|
await act(async ()=>{
|
||||||
await new Promise(resolve => setTimeout(resolve));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export class FakeNode {
|
||||||
getColumnAt(pos) {return _.find(this.getColumns()||[], (c)=>c.attnum==pos);}
|
getColumnAt(pos) {return _.find(this.getColumns()||[], (c)=>c.attnum==pos);}
|
||||||
remove() {/*This is intentional (SonarQube)*/}
|
remove() {/*This is intentional (SonarQube)*/}
|
||||||
getSchemaTableName() {return [this.data.schema, this.data.name];}
|
getSchemaTableName() {return [this.data.schema, this.data.name];}
|
||||||
|
getDisplayName() {return `(${this.data.schema}) ${this.data.name}`;}
|
||||||
cloneData(tabName) {
|
cloneData(tabName) {
|
||||||
let retVal = {...this.data};
|
let retVal = {...this.data};
|
||||||
retVal.name = tabName;
|
retVal.name = tabName;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ describe('ERD FloatingNote', ()=>{
|
||||||
getSchemaTableName: function() {
|
getSchemaTableName: function() {
|
||||||
return ['schema1', 'table1'];
|
return ['schema1', 'table1'];
|
||||||
},
|
},
|
||||||
|
getDisplayName: function() {
|
||||||
|
return '(schema1) table1';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue