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       |
 | 
			
		||||
   +======================+===================================================================================================+================+
 | 
			
		||||
   | *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 +   |
 | 
			
		||||
   |                      | where you can put the table details.                                                              | Ctrl + A       |
 | 
			
		||||
   +----------------------+---------------------------------------------------------------------------------------------------+----------------+
 | 
			
		||||
| 
						 | 
				
			
			@ -109,11 +112,14 @@ Table Relationship Options
 | 
			
		|||
   +----------------------+---------------------------------------------------------------------------------------------------+----------------+
 | 
			
		||||
   | 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       |
 | 
			
		||||
   |                      | 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       |
 | 
			
		||||
   |                      | 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
 | 
			
		||||
************
 | 
			
		||||
 | 
			
		||||
  | `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 #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.
 | 
			
		||||
| 
						 | 
				
			
			@ -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 #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.
 | 
			
		||||
| 
						 | 
				
			
			@ -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 closeModal = (_e, reason) => {
 | 
			
		||||
    if(reason == 'backdropClick' && showTitle) {
 | 
			
		||||
| 
						 | 
				
			
			@ -321,6 +321,7 @@ function ModalContainer({ id, title, content, dialogHeight, dialogWidth, onClose
 | 
			
		|||
      fullScreen={isFullScreen}
 | 
			
		||||
      fullWidth={isFullWidth}
 | 
			
		||||
      disablePortal
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      { showTitle &&
 | 
			
		||||
        <DialogTitle className='modal-drag-area'>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -158,6 +158,24 @@ class ERDModule(PgAdminModule):
 | 
			
		|||
            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(
 | 
			
		||||
            'keyboard_shortcuts',
 | 
			
		||||
            'add_table',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,10 +4,13 @@ export const ERD_EVENTS = {
 | 
			
		|||
  TRIGGER_SHOW_SQL: 'TRIGGER_SHOW_SQL',
 | 
			
		||||
  SHOW_SQL: 'SHOW_SQL',
 | 
			
		||||
  DOWNLOAD_IMAGE: 'DOWNLOAD_IMAGE',
 | 
			
		||||
 | 
			
		||||
  SEARCH_NODE: 'SEARCH_NODE',
 | 
			
		||||
  ADD_NODE: 'ADD_NODE',
 | 
			
		||||
  EDIT_NODE: 'EDIT_NODE',
 | 
			
		||||
  CLONE_NODE: 'CLONE_NODE',
 | 
			
		||||
  DELETE_NODE: 'DELETE_NODE',
 | 
			
		||||
 | 
			
		||||
  SHOW_NOTE: 'SHOW_NOTE',
 | 
			
		||||
  ONE_TO_ONE: 'ONE_TO_ONE',
 | 
			
		||||
  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 { useEffect } from 'react';
 | 
			
		||||
import { FileManagerUtils } from '../../../../../../misc/file_manager/static/js/components/FileManager';
 | 
			
		||||
import SearchNode from './SearchNode';
 | 
			
		||||
 | 
			
		||||
/* Custom react-diagram action for keyboard events */
 | 
			
		||||
export class KeyboardShortcutAction extends Action {
 | 
			
		||||
| 
						 | 
				
			
			@ -178,9 +179,9 @@ export default class ERDTool extends React.Component {
 | 
			
		|||
    this.eventBus = new EventBus();
 | 
			
		||||
 | 
			
		||||
    _.bindAll(this, ['onLoadDiagram', 'onSaveDiagram', 'onSQLClick',
 | 
			
		||||
      'onImageClick', 'onAddNewNode', 'onEditTable', 'onCloneNode', 'onDeleteNode', 'onNoteClick',
 | 
			
		||||
      'onImageClick', 'onSearchNode', 'onAddNewNode', 'onEditTable', 'onCloneNode', 'onDeleteNode', 'onNoteClick',
 | 
			
		||||
      'onNoteClose', 'onOneToOneClick', 'onOneToManyClick', 'onManyToManyClick', 'onAutoDistribute', 'onDetailsToggle',
 | 
			
		||||
      'onChangeColors', 'onDropNode', 'onNotationChange', 'closePanel'
 | 
			
		||||
      'onChangeColors', 'onDropNode', 'onNotationChange', 'closePanel', 'scrollToNode'
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    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.SHOW_SQL, this.onSQLClick);
 | 
			
		||||
    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.EDIT_NODE, this.onEditTable);
 | 
			
		||||
    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.eventBus.fireEvent(ERD_EVENTS.DOWNLOAD_IMAGE);
 | 
			
		||||
      }],
 | 
			
		||||
      [this.state.preferences.search_table, ()=>{
 | 
			
		||||
        this.eventBus.fireEvent(ERD_EVENTS.SEARCH_NODE);
 | 
			
		||||
      }],
 | 
			
		||||
      [this.state.preferences.add_table, ()=>{
 | 
			
		||||
        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) {
 | 
			
		||||
    let dialog = this.getDialog('table_dialog');
 | 
			
		||||
    if(node) {
 | 
			
		||||
      let [schema, table] = node.getSchemaTableName();
 | 
			
		||||
      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)) {
 | 
			
		||||
          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() {
 | 
			
		||||
    this.addEditTable();
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -62,8 +62,7 @@ export default function FloatingNote({open, onClose, anchorEl, rows, noteNode})
 | 
			
		|||
 | 
			
		||||
  const header = useMemo(()=>{
 | 
			
		||||
    if(noteNode) {
 | 
			
		||||
      let [schema, name] = noteNode.getSchemaTableName();
 | 
			
		||||
      return `${name} (${schema})`;
 | 
			
		||||
      return noteNode.getDisplayName();
 | 
			
		||||
    }
 | 
			
		||||
    return '';
 | 
			
		||||
  }, [open]);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,6 +28,7 @@ import ImageRoundedIcon from '@mui/icons-material/ImageRounded';
 | 
			
		|||
import FormatColorFillRoundedIcon from '@mui/icons-material/FormatColorFillRounded';
 | 
			
		||||
import FormatColorTextRoundedIcon from '@mui/icons-material/FormatColorTextRounded';
 | 
			
		||||
import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined';
 | 
			
		||||
import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined';
 | 
			
		||||
 | 
			
		||||
import { PgMenu, PgMenuItem, usePgMenuGroup } from '../../../../../../static/js/components/Menu';
 | 
			
		||||
import gettext from 'sources/gettext';
 | 
			
		||||
| 
						 | 
				
			
			@ -201,6 +202,11 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati
 | 
			
		|||
            }} />
 | 
			
		||||
        </PgButtonGroup>
 | 
			
		||||
        <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 />}
 | 
			
		||||
            shortcut={preferences.add_table}
 | 
			
		||||
            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) {
 | 
			
		||||
  let tablesData = [];
 | 
			
		||||
  _.forEach(tableNodesDict, (node, uid)=>{
 | 
			
		||||
    let [schema, name] = node.getSchemaTableName();
 | 
			
		||||
    tablesData.push({value: uid, label: `(${schema}) ${name}`, image: 'icon-table'});
 | 
			
		||||
    tablesData.push({value: uid, label: node.getDisplayName(), image: 'icon-table'});
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return new ManyToManySchema({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,8 +50,7 @@ class OneToManySchema extends BaseUISchema {
 | 
			
		|||
export function getOneToManyDialogSchema(attributes, tableNodesDict) {
 | 
			
		||||
  let tablesData = [];
 | 
			
		||||
  _.forEach(tableNodesDict, (node, uid)=>{
 | 
			
		||||
    let [schema, name] = node.getSchemaTableName();
 | 
			
		||||
    tablesData.push({value: uid, label: `(${schema}) ${name}`, image: 'icon-table'});
 | 
			
		||||
    tablesData.push({value: uid, label: node.getDisplayName(), image: 'icon-table'});
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return new OneToManySchema({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -83,8 +83,7 @@ class OneToOneSchema extends BaseUISchema {
 | 
			
		|||
export function getOneToOneDialogSchema(attributes, tableNodesDict) {
 | 
			
		||||
  let tablesData = [];
 | 
			
		||||
  _.forEach(tableNodesDict, (node, uid)=>{
 | 
			
		||||
    let [schema, name] = node.getSchemaTableName();
 | 
			
		||||
    tablesData.push({value: uid, label: `(${schema}) ${name}`, image: 'icon-table'});
 | 
			
		||||
    tablesData.push({value: uid, label: node.getDisplayName(), image: 'icon-table'});
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return new OneToOneSchema({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,8 +61,7 @@ export function getTableDialogSchema(attributes, isNew, tableNodesDict, colTypes
 | 
			
		|||
          references: ()=>{
 | 
			
		||||
            let retOpts = [];
 | 
			
		||||
            _.forEach(tableNodesDict, (node, uid)=>{
 | 
			
		||||
              let [schema, name] = node.getSchemaTableName();
 | 
			
		||||
              retOpts.push({value: uid, label: `(${schema}) ${name}`});
 | 
			
		||||
              retOpts.push({value: uid, label: node.getDisplayName()});
 | 
			
		||||
            });
 | 
			
		||||
            return retOpts;
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -147,6 +147,10 @@ export class TableNodeModel extends DefaultNodeModel {
 | 
			
		|||
    return [this._data.schema, this._data.name];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getDisplayName() {
 | 
			
		||||
    return `(${this._data.schema}) ${this._data.name}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  remove() {
 | 
			
		||||
    Object.values(this.getPorts()).forEach((port)=>{
 | 
			
		||||
      port.removeAllLinks();
 | 
			
		||||
| 
						 | 
				
			
			@ -218,6 +222,19 @@ const StyledDiv = styled('div')(({theme})=>({
 | 
			
		|||
    position: 'relative',
 | 
			
		||||
    width: `${TABLE_WIDTH}px`,
 | 
			
		||||
    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': {
 | 
			
		||||
      backgroundColor: theme.palette.background.default,
 | 
			
		||||
| 
						 | 
				
			
			@ -263,10 +280,21 @@ const StyledDiv = styled('div')(({theme})=>({
 | 
			
		|||
        padding: '0.125rem 0.25rem',
 | 
			
		||||
        wordBreak: 'break-all',
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      '&:last-child': {
 | 
			
		||||
        borderBottom: 'none',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  '&.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 = {
 | 
			
		||||
      show_details: true,
 | 
			
		||||
      flash: false,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.tableNodeEventListener = this.props.node.registerListener({
 | 
			
		||||
| 
						 | 
				
			
			@ -291,6 +320,12 @@ export class TableNodeWidget extends React.Component {
 | 
			
		|||
      dataAvaiable: ()=>{
 | 
			
		||||
        /* Just re-render */
 | 
			
		||||
        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)=>{
 | 
			
		||||
      localUkCols.push(...uk.columns.map((c)=>c.column));
 | 
			
		||||
    });
 | 
			
		||||
    const styles = {
 | 
			
		||||
    const contentStyles = {
 | 
			
		||||
      backgroundColor: tableMetaData.fillColor,
 | 
			
		||||
      color: tableMetaData.textColor,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let classList = ['TableNode-tableNode'];
 | 
			
		||||
    if(this.props.node.isSelected()) {
 | 
			
		||||
      classList.push('TableNode-tableNodeSelected');
 | 
			
		||||
    }
 | 
			
		||||
    if(this.state.flash) {
 | 
			
		||||
      classList.push('flash');
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <StyledDiv className={['TableNode-tableNode', (this.props.node.isSelected() ? 'TableNode-tableNodeSelected': '')].join(' ')}
 | 
			
		||||
        onDoubleClick={()=>{this.props.node.fireEvent({}, 'editTable');}} style={styles}>
 | 
			
		||||
      <StyledDiv className={classList.join(' ')}
 | 
			
		||||
        onDoubleClick={()=>{this.props.node.fireEvent({}, 'editTable');}}>
 | 
			
		||||
        <div className={'TableNode-tableToolbar'}>
 | 
			
		||||
          <PgIconButton size="xs" title={gettext('Show Details')} icon={this.state.show_details ? <VisibilityRoundedIcon /> : <VisibilityOffRoundedIcon />}
 | 
			
		||||
            onClick={this.toggleShowDetails} onDoubleClick={(e)=>{e.stopPropagation();}} />
 | 
			
		||||
| 
						 | 
				
			
			@ -386,7 +429,7 @@ export class TableNodeWidget extends React.Component {
 | 
			
		|||
              }}
 | 
			
		||||
            />}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='TableNode-tableContent'>
 | 
			
		||||
        <div className='TableNode-tableContent' style={contentStyles}>
 | 
			
		||||
          {tableMetaData.is_promise &&
 | 
			
		||||
          <div className='TableNode-tableSection'>
 | 
			
		||||
            {!tableMetaData.data_failed && <div className='TableNode-tableNameText'>{gettext('Fetching...')}</div>}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,18 +65,25 @@ describe('SchemaView', ()=>{
 | 
			
		|||
        });
 | 
			
		||||
      },
 | 
			
		||||
      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="field2"]'), '2');
 | 
			
		||||
        await user.type(ctrl.container.querySelector('[name="field5"]'), 'val5');
 | 
			
		||||
        /* 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"]'));
 | 
			
		||||
        // 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"]')[1], 'rval52');
 | 
			
		||||
        // Wait for validations to run
 | 
			
		||||
        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);}
 | 
			
		||||
  remove() {/*This is intentional (SonarQube)*/}
 | 
			
		||||
  getSchemaTableName() {return [this.data.schema, this.data.name];}
 | 
			
		||||
  getDisplayName() {return `(${this.data.schema}) ${this.data.name}`;}
 | 
			
		||||
  cloneData(tabName) {
 | 
			
		||||
    let retVal = {...this.data};
 | 
			
		||||
    retVal.name = tabName;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,9 @@ describe('ERD FloatingNote', ()=>{
 | 
			
		|||
      getSchemaTableName: function() {
 | 
			
		||||
        return ['schema1', 'table1'];
 | 
			
		||||
      },
 | 
			
		||||
      getDisplayName: function() {
 | 
			
		||||
        return '(schema1) table1';
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    const user = userEvent.setup();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue