diff --git a/docs/en_US/erd_tool.rst b/docs/en_US/erd_tool.rst index 954c169cf1f..ea1a8c81583 100644 --- a/docs/en_US/erd_tool.rst +++ b/docs/en_US/erd_tool.rst @@ -207,6 +207,22 @@ The table node shows table details in a graphical representation: * you can click on the node and drag to move on the canvas. * Upon double click on the table node or by clicking the edit button from the toolbar, the table dialog opens where you can change the table details. Refer :ref:`table dialog ` for information on different fields. +The One to One Link Dialog +*************************** + +.. image:: images/erd_11_dialog.png + :alt: ERD tool 1-1 dialog + :align: center + +The one to one link dialog allows you to: + +* Add a one to one relationship between two tables. +* *Local Table* is the table that references a table and has the *one* end point. +* *Local Column* the column that references. +* *Select Constraint* To implement one to one relationship, the *Local Column* must have primaty key or unique constraint. The default is a unique constraint. Please note that this field is visible only when the selected *Local Column* does not have either of the mentioned constraints. +* *Referenced Table* is the table that is being referred and has the *one* end point. +* *Referenced Column* the column that is being referred. + The One to Many Link Dialog *************************** diff --git a/docs/en_US/images/erd_11_dialog.png b/docs/en_US/images/erd_11_dialog.png new file mode 100644 index 00000000000..bd14d0f28b3 Binary files /dev/null and b/docs/en_US/images/erd_11_dialog.png differ diff --git a/web/pgadmin/tools/erd/__init__.py b/web/pgadmin/tools/erd/__init__.py index 99f58eea4f3..8416646e9b0 100644 --- a/web/pgadmin/tools/erd/__init__.py +++ b/web/pgadmin/tools/erd/__init__.py @@ -246,6 +246,24 @@ def register_preferences(self): fields=shortcut_fields ) + self.preference.register( + 'keyboard_shortcuts', + 'one_to_one', + gettext('One to one link'), + 'keyboardshortcut', + { + 'alt': True, + 'shift': False, + 'control': True, + 'key': { + 'key_code': 66, + 'char': 'b' + } + }, + category_label=PREF_LABEL_KEYBOARD_SHORTCUTS, + fields=shortcut_fields + ) + self.preference.register( 'keyboard_shortcuts', 'one_to_many', diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ERDConstants.js b/web/pgadmin/tools/erd/static/js/erd_tool/ERDConstants.js index 558a927115e..f7f6cd3d8b2 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/ERDConstants.js +++ b/web/pgadmin/tools/erd/static/js/erd_tool/ERDConstants.js @@ -9,6 +9,7 @@ export const ERD_EVENTS = { CLONE_NODE: 'CLONE_NODE', DELETE_NODE: 'DELETE_NODE', SHOW_NOTE: 'SHOW_NOTE', + ONE_TO_ONE: 'ONE_TO_ONE', ONE_TO_MANY: 'ONE_TO_MANY', MANY_TO_MANY: 'MANY_TO_MANY', AUTO_DISTRIBUTE: 'AUTO_DISTRIBUTE', diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js b/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js index 37dce9ab660..aeaa076e135 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js +++ b/web/pgadmin/tools/erd/static/js/erd_tool/ERDCore.js @@ -22,6 +22,8 @@ import ForeignKeySchema from '../../../../../browser/server_groups/servers/datab import diffArray from 'diff-arrays-of-objects'; import TableSchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/static/js/table.ui'; import ColumnSchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui'; +import UniqueConstraintSchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.ui'; +import PrimaryKeySchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.ui'; import { boundingBoxFromPolygons } from '@projectstorm/geometry'; export default class ERDCore { @@ -337,18 +339,19 @@ export default class ERDCore { let tableData = tableNode.getData(); /* Remove the links if column dropped or primary key removed */ _.differenceWith(oldTableData.columns, tableData.columns, function(existing, incoming) { - if(existing.attnum == incoming.attnum && existing.is_primary_key && !incoming.is_primary_key) { - return false; - } return existing.attnum == incoming.attnum; }).forEach((col)=>{ - let existPort = tableNode.getPort(tableNode.getPortName(col.attnum)); - if(existPort) { - Object.values(existPort.getLinks()).forEach((link)=>{ - self.removeOneToManyLink(link); - }); - tableNode.removePort(existPort); - } + this.getLeftRightPorts(tableNode, col.attnum).forEach(port => { + if (port) { + Object.values(port.getLinks()).forEach(link => { + self.removeOneToManyLink(link); + }); + tableNode.removePort(port); + } + }); + }); + Object.values(tableNode.getLinks()).forEach(link=>{ + link.fireEvent({},'updateLink'); }); } @@ -482,6 +485,31 @@ export default class ERDCore { columns: [col], }) ); + // Below logic is to add one to one relationship + if(onetomanyData.constraint_type === 'primary_key') { + let newPk = new PrimaryKeySchema({},{}); + let pkCol = {}; + let column = _.find(targetNode.getColumns(), (colm)=>colm.attnum==onetomanyData.local_column_attnum); + column.is_primary_key = true; + pkCol.column =column.name; + tableData.primary_key = tableData.primary_key || []; + tableData.primary_key.push( + newPk.getNewData({ + columns: [pkCol] + }) + ); + + } else if (onetomanyData.constraint_type === 'unique') { + let newUk = new UniqueConstraintSchema({},{}); + let ukCol = {}; + ukCol.column = _.find(targetNode.getColumns(), (colm)=>colm.attnum==onetomanyData.local_column_attnum).name; + tableData.unique_constraint = tableData.unique_constraint || []; + tableData.unique_constraint.push( + newUk.getNewData({ + columns: [ukCol] + }) + ); + } targetNode.setData(tableData); let newLink = this.addLink(onetomanyData, 'onetomany'); this.clearSelection(); diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx index 51bc3d02ea3..5154cf44809 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx +++ b/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx @@ -145,7 +145,7 @@ export default class ERDTool extends React.Component { _.bindAll(this, ['onLoadDiagram', 'onSaveDiagram', 'onSQLClick', 'onImageClick', 'onAddNewNode', 'onEditTable', 'onCloneNode', 'onDeleteNode', 'onNoteClick', - 'onNoteClose', 'onOneToManyClick', 'onManyToManyClick', 'onAutoDistribute', 'onDetailsToggle', + 'onNoteClose', 'onOneToOneClick', 'onOneToManyClick', 'onManyToManyClick', 'onAutoDistribute', 'onDetailsToggle', 'onChangeColors', 'onDropNode', 'onNotationChange', 'closePanel' ]); @@ -220,6 +220,7 @@ export default class ERDTool extends React.Component { this.eventBus.registerListener(ERD_EVENTS.CLONE_NODE, this.onCloneNode); this.eventBus.registerListener(ERD_EVENTS.DELETE_NODE, this.onDeleteNode); this.eventBus.registerListener(ERD_EVENTS.SHOW_NOTE, this.onNoteClick); + this.eventBus.registerListener(ERD_EVENTS.ONE_TO_ONE, this.onOneToOneClick); this.eventBus.registerListener(ERD_EVENTS.ONE_TO_MANY, this.onOneToManyClick); this.eventBus.registerListener(ERD_EVENTS.MANY_TO_MANY, this.onManyToManyClick); this.eventBus.registerListener(ERD_EVENTS.AUTO_DISTRIBUTE, this.onAutoDistribute); @@ -265,6 +266,9 @@ export default class ERDTool extends React.Component { [this.state.preferences.add_edit_note, ()=>{ this.eventBus.fireEvent(ERD_EVENTS.SHOW_NOTE); }], + [this.state.preferences.one_to_one, ()=>{ + this.eventBus.fireEvent(ERD_EVENTS.ONE_TO_ONE); + }], [this.state.preferences.one_to_many, ()=>{ this.eventBus.fireEvent(ERD_EVENTS.ONE_TO_MANY); }], @@ -397,7 +401,7 @@ export default class ERDTool extends React.Component { serverInfo, callback }); }; - } else if(dialogName === 'onetomany_dialog' || dialogName === 'manytomany_dialog') { + } else if(dialogName === 'onetomany_dialog' || dialogName === 'manytomany_dialog' || dialogName === 'onetoone_dialog') { return (title, attributes, callback)=>{ this.erdDialogs.showRelationDialog(dialogName, { title, attributes, tableNodes: this.diagram.getModel().getNodesDict(), @@ -429,6 +433,17 @@ export default class ERDTool extends React.Component { if(this.diagram.anyDuplicateNodeName(newData, oldData)) { return gettext('Table name already exists'); } + // If a column that is part of a foreign key is removed, the foreign key constraint should also be removed. + _.differenceWith(oldData.columns, newData.columns, function(existing, incoming) { + return existing.attnum == incoming.attnum; + }).forEach(colm=>{ + newData.foreign_key?.forEach((theFkRow, index)=>{ + let fkCols = theFkRow.columns[0]; + if (fkCols.local_column === colm.name) { + newData.foreign_key.splice(index,1); + } + }); + }); node.setData(newData); this.diagram.syncTableLinks(node, oldData); this.diagram.repaint(); @@ -774,6 +789,14 @@ export default class ERDTool extends React.Component { }, 1000); } + onOneToOneClick() { + let dialog = this.getDialog('onetoone_dialog'); + let initData = {local_table_uid: this.diagram.getSelectedNodes()[0].getID()}; + dialog(gettext('One to one relation'), initData, (newData)=>{ + this.diagram.addOneToManyLink(newData); + }); + } + onOneToManyClick() { let dialog = this.getDialog('onetomany_dialog'); let initData = {local_table_uid: this.diagram.getSelectedNodes()[0].getID()}; diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/components/MainToolBar.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/components/MainToolBar.jsx index 200b6f250c9..a2bc518379d 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/components/MainToolBar.jsx +++ b/web/pgadmin/tools/erd/static/js/erd_tool/components/MainToolBar.jsx @@ -54,6 +54,7 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati 'save': true, 'edit-table': true, 'clone-table': true, + 'one-to-one': true, 'one-to-many': true, 'many-to-many': true, 'show-note': true, @@ -121,6 +122,7 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati [ERD_EVENTS.SINGLE_NODE_SELECTED, (selected)=>{ setDisableButton('edit-table', !selected); setDisableButton('clone-table', !selected); + setDisableButton('one-to-one', !selected); setDisableButton('one-to-many', !selected); setDisableButton('many-to-many', !selected); setDisableButton('show-note', !selected); @@ -210,12 +212,17 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati }} /> - 1M} + 1 - 1} + shortcut={preferences.one_to_one} disabled={buttonsDisabled['one-to-one']} + onClick={()=>{ + eventBus.fireEvent(ERD_EVENTS.ONE_TO_ONE); + }} /> + 1 - M} shortcut={preferences.one_to_many} disabled={buttonsDisabled['one-to-many']} onClick={()=>{ eventBus.fireEvent(ERD_EVENTS.ONE_TO_MANY); }} /> - MM} + M - M} shortcut={preferences.many_to_many} disabled={buttonsDisabled['many-to-many']} onClick={()=>{ eventBus.fireEvent(ERD_EVENTS.MANY_TO_MANY); diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/OneToOneDialog.js b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/OneToOneDialog.js new file mode 100644 index 00000000000..eb1b233ee98 --- /dev/null +++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/OneToOneDialog.js @@ -0,0 +1,106 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import { isEmptyString } from 'sources/validators'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; +import _ from 'lodash'; + +class OneToOneSchema extends BaseUISchema { + constructor(fieldOptions={}, initValues={}, localTableData={}) { + super({ + local_table_uid: undefined, + local_column_attnum: undefined, + referenced_table_uid: undefined, + referenced_column_attnum: undefined, + constraint_type: undefined, + ...initValues, + }); + this.fieldOptions = fieldOptions; + this.localTableData = localTableData; + } + + isVisible (state) { + let colName = _.find(this.localTableData.getData().columns, col => col.attnum === state.local_column_attnum)?.name; + let {pkCols, ukCols} = this.localTableData.getConstraintCols(); + return !((pkCols.includes(colName) || ukCols.includes(colName)) || isEmptyString(state.local_column_attnum)); + } + get baseFields() { + return [{ + id: 'local_table_uid', label: gettext('Local Table'), + type: 'select', readonly: true, controlProps: {allowClear: false}, + options: this.fieldOptions.local_table_uid, + },{ + id: 'local_column_attnum', label: gettext('Local Column'), + type: 'select', options: this.fieldOptions.local_column_attnum, + controlProps: {allowClear: false}, noEmpty: true, + },{ + id: 'constraint_type', label: gettext('Select constraint'), + type: 'toggle', deps: ['local_column_attnum'], + options: [ + {label: 'Primary Key', value: 'primary_key'}, + {label: 'Unique', value: 'unique'}, + ], + visible: this.isVisible, + depChange: (state, source)=>{ + if (source[0] === 'local_column_attnum' && this.isVisible(state)) { + return {constraint_type: 'unique'}; + } else if (source[0] === 'local_column_attnum') { + return {constraint_type: ''}; + } + }, helpMessage: gettext('A constraint is required to implement One to One relationship.') + }, { + id: 'referenced_table_uid', label: gettext('Referenced Table'), + type: 'select', options: this.fieldOptions.referenced_table_uid, + controlProps: {allowClear: false}, noEmpty: true, + },{ + id: 'referenced_column_attnum', label: gettext('Referenced Column'), + controlProps: {allowClear: false}, deps: ['referenced_table_uid'], noEmpty: true, + type: (state)=>({ + type: 'select', + options: state.referenced_table_uid ? ()=>this.fieldOptions.getRefColumns(state.referenced_table_uid) : [], + optionsReloadBasis: state.referenced_table_uid, + }), + }]; + } + + validate(state, setError) { + let tableData = this.localTableData.getData(); + if (tableData.primary_key.length && state.constraint_type === 'primary_key') { + setError('constraint_type', gettext('Primary key already exists, please select different constraint.')); + return true; + } + return false; + } +} + +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'}); + }); + + return new OneToOneSchema({ + local_table_uid: tablesData, + local_column_attnum: tableNodesDict[attributes.local_table_uid].getColumns().map((col)=>{ + return { + value: col.attnum, label: col.name, 'image': 'icon-column', + }; + }), + referenced_table_uid: tablesData, + getRefColumns: (uid)=>{ + return tableNodesDict[uid].getColumns().map((col)=>{ + return { + value: col.attnum, label: col.name, 'image': 'icon-column', + }; + }); + }, + }, attributes, tableNodesDict[attributes.local_table_uid]); +} diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.jsx index f46d43ebe1f..5241cea750d 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.jsx +++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.jsx @@ -10,6 +10,7 @@ import {getTableDialogSchema} from './TableDialog'; import {getOneToManyDialogSchema} from './OneToManyDialog'; import {getManyToManyDialogSchema} from './ManyToManyDialog'; +import {getOneToOneDialogSchema} from './OneToOneDialog'; import pgAdmin from 'sources/pgadmin'; import SchemaView from '../../../../../../static/js/SchemaView'; @@ -67,6 +68,8 @@ export default class ERDDialogs { schema = getOneToManyDialogSchema(params.attributes, params.tableNodes); } else if(dialogName === 'manytomany_dialog') { schema = getManyToManyDialogSchema(params.attributes, params.tableNodes); + } else if(dialogName === 'onetoone_dialog') { + schema = getOneToOneDialogSchema(params.attributes, params.tableNodes); } this.modal.showModal(params.title, (closeModal)=>{ diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx index a3a0f073437..424e3fa94de 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx +++ b/web/pgadmin/tools/erd/static/js/erd_tool/links/OneToManyLink.jsx @@ -45,6 +45,10 @@ export class OneToManyLinkModel extends RightAngleLinkModel { this._data = { ...data, }; + this._linkPointType = { + sourceType: 'one', + targetType: 'many' + }; } getData() { @@ -77,6 +81,22 @@ export class OneToManyLinkModel extends RightAngleLinkModel { data: this.getData(), }; } + + setPointType(nodesDict) { + let data = this.getData(); + let target = nodesDict[data['local_table_uid']].getData(); + let colName = _.find(target.columns, (col)=>data.local_column_attnum == col.attnum).name; + let {pkCols=[], ukCols=[]} = nodesDict[data['local_table_uid']].getConstraintCols(); + let targetType = pkCols.includes(colName) || ukCols.includes(colName) ? 'one' : 'many'; + this._linkPointType = { + ...this._linkPointType, + targetType, + }; + } + + getPointType() { + return this._linkPointType; + } } const svgLinkSelected = keyframes` @@ -173,6 +193,22 @@ CustomLinkEndWidget.propTypes = { export class OneToManyLinkWidget extends RightAngleLinkWidget { constructor(props) { super(props); + this.state = {}; + this.setPointType(); + this.updateLinkListener = this.props.link.registerListener({ + updateLink: ()=>{ + this.setPointType(); + this.setState({}); + } + }); + } + + componentWillUnmount() { + this.props.link.deregisterListener(this.updateLinkListener); + } + + setPointType() { + this.props.link.setPointType(this.props.diagramEngine.getModel().getNodesDict()); } endPointTranslation(alignment) { @@ -259,9 +295,10 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget { //ensure id is present for all points on the path let points = this.props.link.getPoints(); let paths = []; + let {sourceType, targetType} = this.props.link.getPointType(); - let onePoint = this.addCustomWidgetPoint('one', this.props.link.getSourcePort(), points[0]); - let manyPoint = this.addCustomWidgetPoint('many', this.props.link.getTargetPort(), points[points.length-1]); + let onePoint = this.addCustomWidgetPoint(sourceType, this.props.link.getSourcePort(), points[0]); + let manyPoint = this.addCustomWidgetPoint(targetType, this.props.link.getTargetPort(), points[points.length-1]); if (!this.state.canDrag && points.length > 2) { // Those points and its position only will be moved diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx index a1f412737ce..a499969688f 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx +++ b/web/pgadmin/tools/erd/static/js/erd_tool/nodes/TableNode.jsx @@ -44,6 +44,7 @@ export class TableNodeModel extends DefaultNodeModel { is_promise: Boolean(otherInfo.data?.then || (otherInfo.metadata?.data_failed && !otherInfo.data)), }; this._data = null; + this._constraintCols = {}; if(otherInfo.data?.then) { otherInfo.data.then((data)=>{ /* Once the data is available, it is no more a promise */ @@ -53,6 +54,7 @@ export class TableNodeModel extends DefaultNodeModel { data_failed: false, is_promise: false, }; + this.generateOnetoOneData(data); this.fireEvent(this._metadata, 'dataAvaiable'); this.fireEvent({}, 'nodeUpdated'); this.fireEvent({}, 'selectionChanged'); @@ -69,6 +71,7 @@ export class TableNodeModel extends DefaultNodeModel { columns: [], ...otherInfo.data, }; + this.generateOnetoOneData(otherInfo.data); } } @@ -132,6 +135,7 @@ export class TableNodeModel extends DefaultNodeModel { setData(data) { this._data = data; + this.generateOnetoOneData(data); this.fireEvent({}, 'nodeUpdated'); } @@ -164,6 +168,34 @@ export class TableNodeModel extends DefaultNodeModel { }, }; } + + setConstraintCols(colsData) { + this._constraintCols = colsData; + } + + getConstraintCols() { + return this._constraintCols; + } + + generateOnetoOneData = (tableData) => { + if (tableData){ + let ukCols = [], pkCols = []; + (tableData.unique_constraint||[]).forEach((uk)=>{ + if(uk.columns.length === 1){ + ukCols.push(...uk.columns.map((c)=>c.column)); + } + }); + (tableData.primary_key||[]).forEach((pk)=>{ + if(pk.columns.length === 1){ + pkCols.push(...pk.columns.map((c)=>c.column)); + } + }); + this.setConstraintCols({ + ukCols, + pkCols + }); + } + }; } function RowIcon({icon}) { @@ -239,7 +271,7 @@ export class TableNodeWidget extends React.Component { show_details: true, }; - this.props.node.registerListener({ + this.tableNodeEventListener = this.props.node.registerListener({ toggleDetails: (event) => { this.setState({show_details: event.show_details}); }, @@ -256,6 +288,10 @@ export class TableNodeWidget extends React.Component { }); } + componentWillUnmount() { + this.props.node.deregisterListener(this.tableNodeEventListener); + } + generateColumn(col, localFkCols, localUkCols) { let leftPort = this.props.node.getPort(this.props.node.getPortName(col.attnum, PortModelAlignment.LEFT)); let rightPort = this.props.node.getPort(this.props.node.getPortName(col.attnum, PortModelAlignment.RIGHT)); diff --git a/web/regression/javascript/erd/onetomany_link_spec.js b/web/regression/javascript/erd/onetomany_link_spec.js index f3863928b9b..fa44de59c96 100644 --- a/web/regression/javascript/erd/onetomany_link_spec.js +++ b/web/regression/javascript/erd/onetomany_link_spec.js @@ -15,6 +15,7 @@ import { import OneToManyPortModel from 'pgadmin.tools.erd/erd_tool/ports/OneToManyPort'; import {OneToManyLinkModel, OneToManyLinkWidget, OneToManyLinkFactory} from 'pgadmin.tools.erd/erd_tool/links/OneToManyLink'; +import ERDModel from 'pgadmin.tools.erd/erd_tool/ERDModel'; import { render } from '@testing-library/react'; import Theme from '../../../pgadmin/static/js/Theme'; @@ -108,14 +109,14 @@ describe('ERD OneToManyLinkModel', ()=>{ describe('ERD OneToManyLinkWidget', ()=>{ let linkFactory = new OneToManyLinkFactory(); + let model = new ERDModel(); let engine = { getFactoryForLink: ()=>linkFactory, + getModel: ()=>model }; let link = null; beforeEach(()=>{ - - link = new OneToManyLinkModel({ color: '#000', data: { @@ -129,6 +130,45 @@ describe('ERD OneToManyLinkWidget', ()=>{ link.setTargetPort(new OneToManyPortModel({options: {}})); }); + jest.spyOn(model, 'getNodes').mockReturnValue([ + { + name: 'test1', + getID: function() { + return 'id1'; + }, + getData: function(){ return { + 'name': 'table1', + 'schema': 'erd1', + 'columns': [ + {'name': 'col11', attnum: 0}, + {'name': 'col12', attnum: 1}, + ], + };}, + getConstraintCols: function(){ return { + ukCols: [], + pkCols: [] + };} + }, + { + name: 'test2', + getID: function() { + return 'id2'; + }, + getData: function(){ return { + 'name': 'table2', + 'schema': 'erd2', + 'columns': [ + {'name': 'col21', attnum: 0}, + {'name': 'col22', attnum: 1}, + ], + };}, + getConstraintCols: function(){ return { + ukCols: [], + pkCols: [] + };} + }, + ]); + it('render', ()=>{ let linkWidget = render(