Fixed following issues in ERD:

1) After opening an existing project, the first table is already selected but the edit, clone, delete buttons are disabled.
 2) ERD project title gets changed when 2 ERD projects are open & anyone of it edited.
 3) Closing the ERD tab does not ask for a confirmation pop-up.
 4) Shortcut for 'Show more/Fewer details' is missing.
 5) Deleting the primary key does not delete associated links.
 6) The long table & schema name are getting out of the box.
 7) The long table name in the notes pop-up needs re-alignment.
 8) The same table name present in ERD/canvas is allowed in Add Table dialogue. Added validation in the dialog.
 9) Download image option is added, but it is not perfect yet. Image icons (table, schema, etc.) are not showing up.
 10) Rename panel option should be disabled by default. It should be enabled for the tools which implement rename functionality.
 11) The Toolbar is not visible in Safari for the ERD tool.

refs #1802
pull/39/head
Aditya Toshniwal 2021-01-25 17:29:54 +05:30 committed by Akshay Joshi
parent 446c2a19a5
commit 13db981445
20 changed files with 589 additions and 185 deletions

View File

@ -46,6 +46,7 @@
"prop-types": "^15.7.2",
"raw-loader": "^3.1.0",
"resize-observer-polyfill": "^1.5.1",
"resolve-url-loader": "^3.1.2",
"sass": "^1.24.4",
"sass-loader": "^7.1.0",
"sass-resources-loader": "^2.0.0",
@ -87,6 +88,7 @@
"dagre": "^0.8.4",
"dropzone": "^5.5.1",
"exports-loader": "~0.7.0",
"html2canvas": "^1.0.0-rc.7",
"immutability-helper": "^3.0.0",
"imports-loader": "^0.8.0",
"ip-address": "^5.8.9",

View File

@ -30,7 +30,7 @@ define([
height: 600,
showTitle: true,
isClosable: true,
isRenamable: true,
isRenamable: false,
isPrivate: false,
url: '',
icon: '',

View File

@ -41,11 +41,11 @@ export default function BaseChart({type='line', id, options, data, redraw=false,
options: optionsMerged,
});
props.onInit && props.onInit(chartObj.current);
}
};
const destroyChart = function() {
chartObj.current && chartObj.current.destroy();
}
};
useEffect(()=>{
initChart();
@ -72,7 +72,7 @@ export default function BaseChart({type='line', id, options, data, redraw=false,
destroyChart();
initChart();
}
}, [redraw])
}, [redraw]);
return (
<canvas id={id} ref={chartRef}></canvas>

View File

@ -180,7 +180,7 @@ define('pgadmin.datagrid', [
name: 'frm_datagrid',
showTitle: true,
isCloseable: true,
isRenameable: true,
isRenamable: true,
isPrivate: true,
url: 'about:blank',
});

View File

@ -78,11 +78,11 @@ export function setQueryToolDockerTitle(panel, is_query_tool, panel_title, is_fi
export function set_renamable_option(panel, is_file) {
if(is_file || is_file == 'true') {
panel._isRenamable = false;
panel.renamable(false);
$('.conn-info-dd').hide();
$('.connection-data').css({pointerEvents: 'none', cursor: 'arrow'});
} else {
panel._isRenamable = true;
panel.renamable(true);
}
}

View File

@ -288,6 +288,24 @@ class ERDModule(PgAdminModule):
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'show_details',
gettext('Show more/fewer details'),
'keyboardshortcut',
{
'alt': True,
'shift': False,
'control': True,
'key': {
'key_code': 84,
'char': 't'
}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'zoom_to_fit',

View File

@ -212,14 +212,14 @@ export default class ERDCore {
let sourcePort = sourceNode.getPort(portName);
/* Create the port if not there */
if(!sourcePort) {
sourcePort = sourceNode.addPort(this.getNewPort(type, null, {name:portName, alignment:PortModelAlignment.RIGHT}));
sourcePort = sourceNode.addPort(this.getNewPort(type, null, {name:portName, subtype: 'one', alignment:PortModelAlignment.RIGHT}));
}
portName = targetNode.getPortName(data.local_column_attnum);
let targetPort = targetNode.getPort(portName);
/* Create the port if not there */
if(!targetPort) {
targetPort = targetNode.addPort(this.getNewPort(type, null, {name:portName, alignment:PortModelAlignment.RIGHT}));
targetPort = targetNode.addPort(this.getNewPort(type, null, {name:portName, subtype: 'many', alignment:PortModelAlignment.RIGHT}));
}
/* Link the ports */

View File

@ -50,7 +50,7 @@ export default class TableDialog {
return 'entity_dialog';
}
getDataModel(attributes, colTypes, schemas, sVersion) {
getDataModel(attributes, existingTables, colTypes, schemas, sVersion) {
let dialogObj = this;
let columnsModel = this.pgBrowser.DataModel.extend({
idAttribute: 'attnum',
@ -694,6 +694,10 @@ export default class TableDialog {
msg = gettext('Table name cannot be empty.');
this.errorModel.set('name', msg);
return msg;
} else if(_.findIndex(existingTables, (table)=>table[0]==schema&&table[1]==name) >= 0) {
msg = gettext('Table name already exists.');
this.errorModel.set('name', msg);
return msg;
}
this.errorModel.unset('name');
if (
@ -705,6 +709,8 @@ export default class TableDialog {
return msg;
}
this.errorModel.unset('schema');
return null;
},
});
@ -731,9 +737,9 @@ export default class TableDialog {
return Alertify[dialogName];
}
show(title, attributes, colTypes, schemas, sVersion, callback) {
show(title, attributes, existingTables, colTypes, schemas, sVersion, callback) {
let dialogTitle = title || gettext('Unknown');
const dialog = this.createOrGetDialog('table_dialog');
dialog(dialogTitle, this.getDataModel(attributes, colTypes, schemas, sVersion), callback).resizeTo(this.pgBrowser.stdW.md, this.pgBrowser.stdH.md);
dialog(dialogTitle, this.getDataModel(attributes, existingTables, colTypes, schemas, sVersion), callback).resizeTo(this.pgBrowser.stdW.md, this.pgBrowser.stdH.md);
}
}

View File

@ -9,10 +9,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash';
import BodyWidget from './ui_components/BodyWidget';
import getDialog, {transformToSupported} from './dialogs';
import Alertify from 'pgadmin.alertifyjs';
import pgWindow from 'sources/window';
import pgAdmin from 'sources/pgadmin';
export default class ERDTool {
constructor(container, params) {
@ -20,10 +23,28 @@ export default class ERDTool {
this.params = params;
}
getPreferencesForModule() {
}
render() {
/* Mount the React ERD tool to the container */
let panel = null;
_.each(pgWindow.pgAdmin.Browser.docker.findPanels('frm_erdtool'), function(p) {
if (p.isVisible()) {
panel = p;
}
});
ReactDOM.render(
<BodyWidget params={this.params} getDialog={getDialog} transformToSupported={transformToSupported} pgAdmin={pgWindow.pgAdmin} alertify={Alertify} />,
<BodyWidget
params={this.params}
getDialog={getDialog}
transformToSupported={transformToSupported}
pgWindow={pgWindow}
pgAdmin={pgAdmin}
panel={panel}
alertify={Alertify} />,
this.container
);
}

View File

@ -9,12 +9,12 @@
import React from 'react';
import {
RightAngleLinkModel,
RightAngleLinkWidget,
DefaultLinkFactory,
PortModelAlignment,
LinkWidget,
PointModel
RightAngleLinkModel,
RightAngleLinkWidget,
DefaultLinkFactory,
PortModelAlignment,
LinkWidget,
PointModel,
} from '@projectstorm/react-diagrams';
import {Point} from '@projectstorm/geometry';
import _ from 'lodash';
@ -24,7 +24,7 @@ export const OneToManyModel = {
local_column_attnum: undefined,
referenced_table_uid: undefined,
referenced_column_attnum: undefined,
}
};
export class OneToManyLinkModel extends RightAngleLinkModel {
constructor({data, ...options}) {
@ -33,7 +33,7 @@ export class OneToManyLinkModel extends RightAngleLinkModel {
width: 1,
class: 'link-onetomany',
locked: true,
...options
...options,
});
this._data = {
@ -62,13 +62,13 @@ export class OneToManyLinkModel extends RightAngleLinkModel {
'local_column': _.find(target.columns, (col)=>data.local_column_attnum == col.attnum).name,
'referenced': _.find(source.columns, (col)=>data.referenced_column_attnum == col.attnum).name,
}],
}
};
}
serialize() {
return {
...super.serialize(),
data: this.getData()
data: this.getData(),
};
}
}
@ -83,13 +83,13 @@ const CustomLinkEndWidget = props => {
<circle className="svg-link-ele svg-otom-circle" cx="0" cy="16" r={props.width*1.75} strokeWidth={props.width} />
<polyline className="svg-link-ele" points="-8,0 0,15 0,0 0,30 0,15 8,0" fill="none" strokeWidth={props.width} />
</>
)
);
} else if (type == 'one') {
return (
<polyline className="svg-link-ele" points="-8,15 0,15 0,0 0,30 0,15 8,15" fill="none" strokeWidth={props.width} />
)
);
}
}
};
return (
<g transform={'translate(' + point.getPosition().x + ', ' + point.getPosition().y + ')'}>
@ -111,21 +111,21 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
let degree = 0;
let tx = 0, ty = 0;
switch(alignment) {
case PortModelAlignment.BOTTOM:
ty = -offset;
break;
case PortModelAlignment.LEFT:
degree = 90;
tx = offset
break;
case PortModelAlignment.TOP:
degree = 180;
ty = offset;
break;
case PortModelAlignment.RIGHT:
degree = -90;
tx = -offset;
break;
case PortModelAlignment.BOTTOM:
ty = -offset;
break;
case PortModelAlignment.LEFT:
degree = 90;
tx = offset;
break;
case PortModelAlignment.TOP:
degree = 180;
ty = offset;
break;
case PortModelAlignment.RIGHT:
degree = -90;
tx = -offset;
break;
}
return [degree, tx, ty];
}
@ -146,8 +146,8 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
point: point,
rotation: rotation,
tx: tx,
ty: ty
}
ty: ty,
};
}
generateCustomEndWidget({type, point, rotation, tx, ty}) {
@ -183,7 +183,7 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
}
handleMove = function(event) {
this.props.link.getTargetPort()
this.props.link.getTargetPort();
this.draggingEvent(event, this.dragging_index);
this.props.link.fireEvent({}, 'positionChanged');
}.bind(this);
@ -220,7 +220,7 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
this.props.link.addPoint(
new PointModel({
link: this.props.link,
position: new Point(onePoint.point.getX(), manyPoint.point.getY())
position: new Point(onePoint.point.getX(), manyPoint.point.getY()),
})
);
}
@ -246,7 +246,7 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
onMouseEnter: (event) => {
this.setState({ selected: true });
this.props.link.lastHoverIndexOfPath = j;
}
},
},
j
)

View File

@ -19,7 +19,7 @@ export class TableNodeModel extends DefaultNodeModel {
constructor({otherInfo, ...options}) {
super({
...options,
type: TYPE
type: TYPE,
});
this._note = otherInfo.note || '';
@ -60,22 +60,22 @@ export class TableNodeModel extends DefaultNodeModel {
cloneData(name) {
let newData = {
...this.getData()
...this.getData(),
};
if(name) {
newData['name'] = name
newData['name'] = name;
}
return newData;
}
setData(data) {
let self = this;
/* Remove the links if column dropped */
/* Remove the links if column dropped or primary key removed */
_.differenceWith(this._data.columns, data.columns, function(existing, incoming) {
return existing.attnum == incoming.attnum;
return existing.attnum == incoming.attnum && incoming.is_primary_key == true;
}).forEach((col)=>{
let existPort = self.getPort(self.getPortName(col.attnum));
if(existPort) {
if(existPort && existPort.getSubtype() == 'one') {
existPort.removeAllLinks();
self.removePort(existPort);
}
@ -109,7 +109,7 @@ export class TableNodeModel extends DefaultNodeModel {
otherInfo: {
data: this.getData(),
note: this.getNote(),
}
},
};
}
}
@ -119,8 +119,8 @@ export class TableNodeWidget extends React.Component {
super(props);
this.state = {
show_details: true
}
show_details: true,
};
this.props.node.registerListener({
toggleDetails: (event) => {
@ -143,13 +143,13 @@ export class TableNodeWidget extends React.Component {
</div>
<div className="ml-auto col-row-port">{this.generatePort(port)}</div>
</div>
)
);
}
generatePort = port => {
if(port) {
return (
<PortWidget engine={this.props.engine} port={port} key={port.getID()} className={"port-" + port.options.alignment} />
<PortWidget engine={this.props.engine} port={port} key={port.getID()} className={'port-' + port.options.alignment} />
);
}
return <></>;
@ -163,21 +163,21 @@ export class TableNodeWidget extends React.Component {
render() {
let node_data = this.props.node.getData();
return (
<div className={"table-node " + (this.props.node.isSelected() ? 'selected': '') } onDoubleClick={()=>{this.props.node.fireEvent({}, 'editNode')}}>
<div className={'table-node ' + (this.props.node.isSelected() ? 'selected': '') } onDoubleClick={()=>{this.props.node.fireEvent({}, 'editNode');}}>
<div className="table-toolbar">
<DetailsToggleButton className='btn-xs' showDetails={this.state.show_details} onClick={this.toggleShowDetails} onDoubleClick={(e)=>{e.stopPropagation();}} />
{this.props.node.getNote() &&
<IconButton icon="far fa-sticky-note" className="btn-xs btn-warning ml-auto" onClick={()=>{
this.props.node.fireEvent({}, 'showNote')
this.props.node.fireEvent({}, 'showNote');
}} title="Check note" />}
</div>
<div className="table-schema">
<span className="wcTabIcon icon-schema"></span>
{node_data.schema}
<div className="d-flex table-schema-data">
<div className="table-icon-schema"></div>
<div className="table-schema">{node_data.schema}</div>
</div>
<div className="table-name">
<span className="wcTabIcon icon-table"></span>
{node_data.name}
<div className="d-flex table-name-data">
<div><span className="wcTabIcon icon-table"></span></div>
<div className="table-name">{node_data.name}</div>
</div>
<div className="table-cols">
{_.map(node_data.columns, (col)=>this.generateColumn(col))}

View File

@ -16,6 +16,7 @@ const TYPE = 'onetomany';
export default class OneToManyPortModel extends PortModel {
constructor({options}) {
super({
subtype: 'notset',
...options,
type: TYPE,
});
@ -30,6 +31,22 @@ export default class OneToManyPortModel extends PortModel {
createLinkModel() {
return new OneToManyLinkModel({});
}
getSubtype() {
return this.options.subtype;
}
deserialize(event) {
super.deserialize(event);
this.options.subtype = event.data.subtype || 'notset';
}
serialize() {
return {
...super.serialize(),
subtype: this.options.subtype,
};
}
}
export class OneToManyPortFactory extends AbstractModelFactory {

View File

@ -12,6 +12,8 @@ import { CanvasWidget } from '@projectstorm/react-canvas-core';
import axios from 'axios';
import { Action, InputType } from '@projectstorm/react-canvas-core';
import PropTypes from 'prop-types';
import _ from 'lodash';
import html2canvas from 'html2canvas';
import ERDCore from '../ERDCore';
import ToolBar, {IconButton, DetailsToggleButton, ButtonGroup} from './ToolBar';
@ -22,22 +24,25 @@ import {setPanelTitle} from '../../erd_module';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import {showERDSqlTool} from 'tools/datagrid/static/js/show_query_tool';
import 'wcdocker';
/* Custom react-diagram action for keyboard events */
export class KeyboardShortcutAction extends Action {
constructor(shortcut_handlers=[]) {
super({
type: InputType.KEY_DOWN,
fire: ({ event })=>{
this.callHandler(event);
}
});
this.shortcuts = {};
super({
type: InputType.KEY_DOWN,
fire: ({ event })=>{
this.callHandler(event);
},
});
this.shortcuts = {};
for(let i=0; i<shortcut_handlers.length; i++){
let [key, handler] = shortcut_handlers[i];
for(let i=0; i<shortcut_handlers.length; i++){
let [key, handler] = shortcut_handlers[i];
if(key) {
this.shortcuts[this.shortcutKey(key.alt, key.control, key.shift, false, key.key.key_code)] = handler;
}
}
}
shortcutKey(altKey, ctrlKey, shiftKey, metaKey, keyCode) {
@ -69,9 +74,12 @@ export default class BodyWidget extends React.Component {
current_file: null,
dirty: false,
show_details: true,
is_new_tab: false,
preferences: {},
}
};
this.diagram = new ERDCore();
/* Flag for checking if user has opted for save before close */
this.closeOnSave = React.createRef();
this.fileInputRef = React.createRef();
this.diagramContainerRef = React.createRef();
this.canvasEle = null;
@ -83,6 +91,7 @@ export default class BodyWidget extends React.Component {
this.onSaveDiagram = this.onSaveDiagram.bind(this);
this.onSaveAsDiagram = this.onSaveAsDiagram.bind(this);
this.onSQLClick = this.onSQLClick.bind(this);
this.onImageClick = this.onImageClick.bind(this);
this.onAddNewNode = this.onAddNewNode.bind(this);
this.onEditNode = this.onEditNode.bind(this);
this.onCloneNode = this.onCloneNode.bind(this);
@ -133,7 +142,7 @@ export default class BodyWidget extends React.Component {
},
'editNode': (event) => {
this.addEditNode(event.node);
}
},
};
Object.keys(diagramEvents).forEach(eventName => {
this.diagram.registerModelEvent(eventName, diagramEvents[eventName]);
@ -157,9 +166,10 @@ export default class BodyWidget extends React.Component {
[this.state.preferences.one_to_many, this.onOneToManyClick],
[this.state.preferences.many_to_many, this.onManyToManyClick],
[this.state.preferences.auto_align, this.onAutoDistribute],
[this.state.preferences.show_details, this.onDetailsToggle],
[this.state.preferences.zoom_to_fit, this.diagram.zoomToFit],
[this.state.preferences.zoom_in, this.diagram.zoomIn],
[this.state.preferences.zoom_out, this.diagram.zoomOut]
[this.state.preferences.zoom_out, this.diagram.zoomOut],
]);
this.diagram.registerKeyAction(this.keyboardActionObj);
@ -182,11 +192,16 @@ export default class BodyWidget extends React.Component {
}
async componentDidMount() {
this.setLoading('Preparing');
this.setTitle(this.state.current_file);
this.setLoading(gettext('Preparing...'));
this.setState({
preferences: this.props.pgAdmin.Browser.get_preferences_for_module('erd')
}, this.registerKeyboardShortcuts);
preferences: this.props.pgWindow.pgAdmin.Browser.get_preferences_for_module('erd'),
is_new_tab: (this.props.pgWindow.pgAdmin.Browser.get_preferences_for_module('browser').new_browser_tab_open || '')
.includes('erd_tool'),
}, ()=>{
this.registerKeyboardShortcuts();
this.setTitle(this.state.current_file);
});
this.registerModelEvents();
this.realignGrid({
backgroundSize: '45px 45px',
@ -197,10 +212,19 @@ export default class BodyWidget extends React.Component {
this.props.pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:create_file', this.saveFile, this);
this.props.pgAdmin.Browser.onPreferencesChange('erd', () => {
this.setState({
preferences: this.props.pgAdmin.Browser.get_preferences_for_module('erd')
preferences: this.props.pgWindow.pgAdmin.Browser.get_preferences_for_module('erd'),
}, ()=>this.registerKeyboardShortcuts());
});
this.props.panel?.on(window.wcDocker?.EVENT.CLOSING, () => {
if(this.state.dirty) {
this.closeOnSave = false;
this.confirmBeforeClose();
return false;
}
return true;
});
let done = await this.initConnection();
if(!done) return;
@ -218,18 +242,78 @@ export default class BodyWidget extends React.Component {
}
}
confirmBeforeClose() {
let bodyObj = this;
this.props.alertify.confirmSave || this.props.alertify.dialog('confirmSave', function() {
return {
main: function(title, message) {
this.setHeader(title);
this.setContent(message);
},
setup: function() {
return {
buttons: [{
text: gettext('Cancel'),
key: 27, // ESC
invokeOnClose: true,
className: 'btn btn-secondary fa fa-lg fa-times pg-alertify-button',
}, {
text: gettext('Don\'t save'),
className: 'btn btn-secondary fa fa-lg fa-trash-alt pg-alertify-button',
}, {
text: gettext('Save'),
className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
}],
focus: {
element: 0,
select: false,
},
options: {
maximizable: false,
resizable: false,
},
};
},
callback: function(closeEvent) {
switch (closeEvent.index) {
case 0: // Cancel
//Do nothing.
break;
case 1: // Don't Save
bodyObj.closePanel();
break;
case 2: //Save
bodyObj.onSaveDiagram(false, true);
break;
}
},
};
});
this.props.alertify.confirmSave(gettext('Save changes?'), gettext('The diagram has changed. Do you want to save changes?'));
return false;
}
closePanel() {
window.onbeforeunload = null;
this.props.panel.off(wcDocker.EVENT.CLOSING);
this.props.pgWindow.pgAdmin.Browser.docker.removePanel(this.props.panel);
}
getDialog(dialogName) {
if(dialogName === 'entity_dialog') {
let existingTables = this.diagram.getModel().getNodes().map((node)=>{
return node.getSchemaTableName();
});
return (title, attributes, callback)=>{
this.props.getDialog(dialogName).show(
title, attributes, this.diagram.getCache('colTypes'), this.diagram.getCache('schemas'), this.state.server_version, callback
);
this.props.getDialog(dialogName).show(
title, attributes, existingTables, this.diagram.getCache('colTypes'), this.diagram.getCache('schemas'), this.state.server_version, callback
);
};
} else if(dialogName === 'onetomany_dialog' || dialogName === 'manytomany_dialog') {
return (title, attributes, callback)=>{
this.props.getDialog(dialogName).show(
title, attributes, this.diagram.getModel().getNodesDict(), this.state.server_version, callback
);
this.props.getDialog(dialogName).show(
title, attributes, this.diagram.getModel().getNodesDict(), this.state.server_version, callback
);
};
}
}
@ -289,20 +373,20 @@ export default class BodyWidget extends React.Component {
gettext('Delete ?'),
gettext('You have selected %s tables and %s links.', this.diagram.getSelectedNodes().length, this.diagram.getSelectedLinks().length)
+ '<br />' + gettext('Are you sure you want to delete ?'),
() => {
this.diagram.getSelectedNodes().forEach((node)=>{
node.setSelected(false);
node.remove();
});
this.diagram.getSelectedLinks().forEach((link)=>{
link.getTargetPort().remove();
link.getSourcePort().remove();
link.setSelected(false);
link.remove();
});
this.diagram.repaint();
},
() => {}
() => {
this.diagram.getSelectedNodes().forEach((node)=>{
node.setSelected(false);
node.remove();
});
this.diagram.getSelectedLinks().forEach((link)=>{
link.getTargetPort().remove();
link.getSourcePort().remove();
link.setSelected(false);
link.remove();
});
this.diagram.repaint();
},
() => {}
);
}
@ -312,11 +396,11 @@ export default class BodyWidget extends React.Component {
onDetailsToggle() {
this.setState((prevState)=>({
show_details: !prevState.show_details
show_details: !prevState.show_details,
}), ()=>{
this.diagram.getModel().getNodes().forEach((node)=>{
node.fireEvent({show_details: this.state.show_details}, 'toggleDetails');
})
});
});
}
@ -335,8 +419,9 @@ export default class BodyWidget extends React.Component {
}
openFile(fileName) {
this.setLoading(gettext('Loading project...'));
axios.post(url_for('sqleditor.load_file'), {
'file_name': decodeURI(fileName)
'file_name': decodeURI(fileName),
}).then((res)=>{
this.setState({
current_file: fileName,
@ -344,13 +429,17 @@ export default class BodyWidget extends React.Component {
});
this.setTitle(fileName);
this.diagram.deserialize(res.data);
this.diagram.clearSelection();
this.registerModelEvents();
}).catch((err)=>{
this.handleAxiosCatch(err);
}).then(()=>{
this.setLoading(null);
});
}
onSaveDiagram(isSaveAs=false) {
onSaveDiagram(isSaveAs=false, closeOnSave=false) {
this.closeOnSave = closeOnSave;
if(this.state.current_file && !isSaveAs) {
this.saveFile(this.state.current_file);
} else {
@ -370,9 +459,10 @@ export default class BodyWidget extends React.Component {
}
saveFile(fileName) {
this.setLoading(gettext('Saving...'));
axios.post(url_for('sqleditor.save_file'), {
'file_name': decodeURI(fileName),
'file_content': JSON.stringify(this.diagram.serialize(this.props.pgAdmin.Browser.utils.app_version_int))
'file_content': JSON.stringify(this.diagram.serialize(this.props.pgAdmin.Browser.utils.app_version_int)),
}).then(()=>{
this.props.alertify.success(gettext('Project saved successfully.'));
this.setState({
@ -380,7 +470,12 @@ export default class BodyWidget extends React.Component {
dirty: false,
});
this.setTitle(fileName);
this.setLoading(null);
if(this.closeOnSave) {
this.closePanel.call(this);
}
}).catch((err)=>{
this.setLoading(null);
this.handleAxiosCatch(err);
});
}
@ -395,14 +490,10 @@ export default class BodyWidget extends React.Component {
title = 'Untitled';
}
title = this.getCurrentProjectName(title) + (dirty ? '*': '');
if (this.new_browser_tab) {
if (this.state.is_new_tab) {
window.document.title = title;
} else {
_.each(this.props.pgAdmin.Browser.docker.findPanels('frm_erdtool'), function(p) {
if (p.isVisible()) {
setPanelTitle(p, title);
}
});
setPanelTitle(this.props.panel, title);
}
}
@ -414,7 +505,7 @@ export default class BodyWidget extends React.Component {
trans_id: this.props.params.trans_id,
sgid: this.props.params.sgid,
sid: this.props.params.sid,
did: this.props.params.did
did: this.props.params.did,
});
this.setLoading(gettext('Preparing the SQL...'));
@ -428,18 +519,53 @@ export default class BodyWidget extends React.Component {
sid: this.props.params.sid,
did: this.props.params.did,
stype: this.props.params.server_type,
}
};
let sqlId = `erd${this.props.params.trans_id}`;
localStorage.setItem(sqlId, sqlScript);
showERDSqlTool(parentData, sqlId, this.props.params.title, this.props.pgAdmin.DataGrid, this.props.alertify);
showERDSqlTool(parentData, sqlId, this.props.params.title, this.props.pgWindow.pgAdmin.DataGrid, this.props.alertify);
})
.catch((error)=>{
this.handleAxiosCatch(error);
})
.then(()=>{
this.setLoading(null);
})
});
}
onImageClick() {
this.setLoading(gettext('Preparing the image...'));
/* Change the styles for suiting html2canvas */
this.canvasEle.classList.add('html2canvas-reset');
this.canvasEle.style.width = this.canvasEle.scrollWidth + 'px';
this.canvasEle.style.height = this.canvasEle.scrollHeight + 'px';
html2canvas(this.canvasEle, {
width: this.canvasEle.scrollWidth + 10,
height: this.canvasEle.scrollHeight + 10,
scrollX: 0,
scrollY: 0,
useCORS: true,
allowTaint: true,
backgroundColor: window.getComputedStyle(this.canvasEle).backgroundColor,
}).then((canvas)=>{
let link = document.createElement('a');
link.setAttribute('href', canvas.toDataURL('image/png'));
link.setAttribute('download', this.getCurrentProjectName() + '.png');
link.click();
}).catch((err)=>{
console.error(err);
this.props.alertify.alert()
.set('title', gettext('Error'))
.set('message', err).show();
}).then(()=>{
/* Revert back to the original CSS styles */
this.canvasEle.classList.remove('html2canvas-reset');
this.canvasEle.style.width = '';
this.canvasEle.style.height = '';
this.setLoading(null);
});
}
onOneToManyClick() {
@ -473,8 +599,8 @@ export default class BodyWidget extends React.Component {
'name': `${right_table.getData().name}_${right_table.getColumnAt(newData.right_table_column_attnum).name}`,
'is_primary_key': false,
'attnum': 1,
}]
}
}],
};
let newNode = this.diagram.addNode(tableData);
this.diagram.clearSelection();
newNode.setSelected(true);
@ -484,7 +610,7 @@ export default class BodyWidget extends React.Component {
local_column_attnum: newNode.getColumns()[0].attnum,
referenced_table_uid: newData.left_table_uid,
referenced_column_attnum : newData.left_table_column_attnum,
}
};
this.diagram.addLink(linkData, 'onetomany');
linkData = {
@ -492,7 +618,7 @@ export default class BodyWidget extends React.Component {
local_column_attnum: newNode.getColumns()[1].attnum,
referenced_table_uid: newData.right_table_uid,
referenced_column_attnum : newData.right_table_column_attnum,
}
};
this.diagram.addLink(linkData, 'onetomany');
@ -505,7 +631,7 @@ export default class BodyWidget extends React.Component {
this.noteRefEle = this.diagram.getEngine().getNodeElement(noteNode);
this.setState({
note_node: noteNode,
note_open: true
note_open: true,
});
}
}
@ -528,14 +654,14 @@ export default class BodyWidget extends React.Component {
trans_id: this.props.params.trans_id,
sgid: this.props.params.sgid,
sid: this.props.params.sid,
did: this.props.params.did
did: this.props.params.did,
});
try {
let response = await axios.post(initUrl);
this.setState({
conn_status: CONNECT_STATUS.CONNECTED,
server_version: response.data.data.serverVersion
server_version: response.data.data.serverVersion,
});
return true;
} catch (error) {
@ -556,7 +682,7 @@ export default class BodyWidget extends React.Component {
trans_id: this.props.params.trans_id,
sgid: this.props.params.sgid,
sid: this.props.params.sid,
did: this.props.params.did
did: this.props.params.did,
});
try {
@ -579,7 +705,7 @@ export default class BodyWidget extends React.Component {
trans_id: this.props.params.trans_id,
sgid: this.props.params.sgid,
sid: this.props.params.sid,
did: this.props.params.did
did: this.props.params.did,
});
try {
@ -604,7 +730,7 @@ export default class BodyWidget extends React.Component {
<ButtonGroup>
<IconButton id="open-file" icon="fa fa-folder-open" onClick={this.onLoadDiagram} title={gettext('Load from file')}
shortcut={this.state.preferences.open_project}/>
<IconButton id="save-erd" icon="fa fa-save" onClick={()=>{this.onSaveDiagram()}} title={gettext('Save project')}
<IconButton id="save-erd" icon="fa fa-save" onClick={()=>{this.onSaveDiagram();}} title={gettext('Save project')}
shortcut={this.state.preferences.save_project} disabled={!this.state.dirty}/>
<IconButton id="save-as-erd" icon="fa fa-share-square" onClick={this.onSaveAsDiagram} title={gettext('Save as')}
shortcut={this.state.preferences.save_project_as}/>
@ -612,6 +738,8 @@ export default class BodyWidget extends React.Component {
<ButtonGroup>
<IconButton id="save-sql" icon="fa fa-file-code" onClick={this.onSQLClick} title={gettext('Generate SQL')}
shortcut={this.state.preferences.generate_sql}/>
<IconButton id="save-image" icon="fa fa-file-image" onClick={this.onImageClick} title={gettext('Generate SQL')}
shortcut={this.state.preferences.generate_sql}/>
</ButtonGroup>
<ButtonGroup>
<IconButton id="add-node" icon="fa fa-plus-square" onClick={this.onAddNewNode} title={gettext('Add table')}
@ -634,7 +762,8 @@ export default class BodyWidget extends React.Component {
shortcut={this.state.preferences.add_edit_note} disabled={!this.state.single_node_selected || this.state.single_link_selected}/>
<IconButton id="auto-align" icon="fa fa-magic" onClick={this.onAutoDistribute} title={gettext('Auto align')}
shortcut={this.state.preferences.auto_align} />
<DetailsToggleButton id="more-details" onClick={this.onDetailsToggle} showDetails={this.state.show_details} />
<DetailsToggleButton id="more-details" onClick={this.onDetailsToggle} showDetails={this.state.show_details}
shortcut={this.state.preferences.show_details} />
</ButtonGroup>
<ButtonGroup>
<IconButton id="zoom-to-fit" icon="fa fa-compress" onClick={this.diagram.zoomToFit} title={gettext('Zoom to fit')}
@ -654,7 +783,7 @@ export default class BodyWidget extends React.Component {
reference={this.noteRefEle} noteNode={this.state.note_node} appendTo={this.diagramContainerRef.current} rows={8}/>
<div className="diagram-container" ref={this.diagramContainerRef}>
<Loader message={this.state.loading_msg} autoEllipsis={true}/>
<CanvasWidget className="diagram-canvas flex-grow-1" ref={(ele)=>{this.canvasEle = ele?.ref?.current}} engine={this.diagram.getEngine()} />
<CanvasWidget className="diagram-canvas flex-grow-1" ref={(ele)=>{this.canvasEle = ele?.ref?.current;}} engine={this.diagram.getEngine()} />
</div>
</>
);
@ -672,10 +801,11 @@ BodyWidget.propTypes = {
title: PropTypes.string.isRequired,
bgcolor: PropTypes.string,
fgcolor: PropTypes.string,
gen: PropTypes.bool.isRequired
gen: PropTypes.bool.isRequired,
}),
getDialog: PropTypes.func.isRequired,
transformToSupported: PropTypes.func.isRequired,
pgWindow: PropTypes.object.isRequired,
pgAdmin: PropTypes.object.isRequired,
alertify: PropTypes.object.isRequired
alertify: PropTypes.object.isRequired,
};

View File

@ -16,7 +16,7 @@ export const STATUS = {
DISCONNECTED: 2,
CONNECTING: 3,
FAILED: 4,
}
};
/* The connection bar component */
export default function ConnectionBar({statusId, status, bgcolor, fgcolor, title}) {
@ -33,19 +33,19 @@ export default function ConnectionBar({statusId, status, bgcolor, fgcolor, title
+ (status == STATUS.CONNECTED ? 'icon-query-tool-connected' : '')
+ (status == (STATUS.DISCONNECTED || STATUS.FAILED) ? 'icon-query-tool-disconnected ' : '')
+ (status == STATUS.CONNECTING ? 'obtaining-conn' : '')}
aria-hidden="true" title="" role="img">
aria-hidden="true" title="" role="img">
</span>
</div>
<div className="connection-info btn-group" role="group" aria-label="">
<div className="editor-title"
style={{backgroundColor: bgcolor, color: fgcolor}}>
{status == STATUS.CONNECTING ? '(' + gettext('Obtaining connection...') + ') ' : ''}
{status == STATUS.FAILED ? '(' + gettext('Connection failed') + ') ' : ''}
{title}
{status == STATUS.CONNECTING ? '(' + gettext('Obtaining connection...') + ') ' : ''}
{status == STATUS.FAILED ? '(' + gettext('Connection failed') + ') ' : ''}
{title}
</div>
</div>
</div>
)
);
}
ConnectionBar.propTypes = {

View File

@ -51,13 +51,13 @@ export default function FloatingNote({open, onClose, reference, rows, noteNode,
</div>
</div>
</div>
)}
visible={open}
interactive={true}
animation={false}
reference={reference}
placement='auto-end'
{...tippyProps}
)}
visible={open}
interactive={true}
animation={false}
reference={reference}
placement='auto-end'
{...tippyProps}
/>
);
}

View File

@ -34,7 +34,7 @@ BaseIconButton.propTypes = {
text: PropTypes.string,
className: PropTypes.string,
ref: CustomPropTypes.ref,
}
};
/* The tooltip content to show shortcut details */
@ -47,10 +47,10 @@ export function Shortcut({shortcut}) {
return (
<div style={{justifyContent: 'center', marginTop: '0.125rem'}} className="d-flex">
{keys.map((key, i)=>{
return <div key={i} className="shortcut-key">{key}</div>
return <div key={i} className="shortcut-key">{key}</div>;
})}
</div>
)
);
}
const shortcutPropType = PropTypes.shape({
@ -85,7 +85,7 @@ export const IconButton = forwardRef((props, ref) => {
</Tippy>
);
} else {
return <BaseIconButton ref={ref} className='btn btn-sm btn-primary-icon' {...otherProps}/>
return <BaseIconButton ref={ref} className='btn btn-sm btn-primary-icon' {...otherProps}/>;
}
});
@ -93,21 +93,21 @@ IconButton.propTypes = {
title: PropTypes.string,
shortcut: shortcutPropType,
className: PropTypes.string,
}
};
/* Toggle button, icon changes based on value */
export function DetailsToggleButton({showDetails, ...props}) {
return (
<IconButton
icon={showDetails ? 'far fa-eye' : 'fas fa-low-vision'}
title={showDetails ? gettext('Show fewer details') : gettext("Show more details") }
title={showDetails ? gettext('Show fewer details') : gettext('Show more details') }
{...props} />
);
}
DetailsToggleButton.propTypes = {
showDetails: PropTypes.bool,
}
};
/* Button group container */
export function ButtonGroup({className, children}) {
@ -115,12 +115,12 @@ export function ButtonGroup({className, children}) {
<div className={'btn-group mr-1 ' + (className ? className : '')} role="group" aria-label="save group">
{children}
</div>
)
);
}
ButtonGroup.propTypes = {
className: PropTypes.string,
}
};
/* Toolbar container */
export default function ToolBar({id, children}) {
@ -128,9 +128,9 @@ export default function ToolBar({id, children}) {
<div id={id} className="editor-toolbar d-flex" role="toolbar" aria-label="">
{children}
</div>
)
);
}
ButtonGroup.propTypes = {
id: PropTypes.string,
}
};

View File

@ -28,6 +28,7 @@
position: relative;
width: 100%;
height: 100%;
min-height: 0;
}
.floating-note {
@ -56,6 +57,7 @@
}
.note-body {
word-break: break-all;
& textarea {
width: 100%;
border: none;
@ -69,11 +71,21 @@
}
}
.html2canvas-reset {
background-image: none !important;
overflow: auto !important;
& > svg, & > div {
transform: none !important;
}
}
.diagram-canvas{
width: 100%;
height: 100%;
color: $color-fg;
font-family: sans-serif;
background-color: $erd-canvas-bg;
background-image: $erd-bg-grid;
cursor: unset;
@ -85,6 +97,22 @@
width: 175px;
font-size: 0.8em;
.table-icon-schema {
background-image: url('~top/browser/server_groups/servers/databases/schemas/static/img/schema.svg') !important;
// background-repeat: no-repeat;
// // background-size: 20px !important;
// align-content: center;
// vertical-align: middle;
// height: 100%;
// background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIwLjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA2NCA2NCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNjQgNjQ7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDpub25lO3N0cm9rZTojOTk5OTk5O3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEwO30KPC9zdHlsZT4KPHBhdGggY2xhc3M9InN0MCIgZD0iTTIxLjksMjIuMmMwLDMuNS0yLjksNi40LTYuNCw2LjRzLTYuNC0yLjktNi40LTYuNHMyLjktNi40LDYuNC02LjRTMjEuOSwxOC43LDIxLjksMjIuMnogTTQuMiw1MWwyMi4xLTE5CgkgTTQ2LjUsNTEuMUwyNi40LDMyIE0zNy42LDQyLjZsMTEuNi0xMCBNNjIuNCw0NS4xTDQ5LjIsMzIuNyBNNjMsMUgxdjYyaDYyVjF6IE0xLDUxLjRoNjIiLz4KPC9zdmc+Cg==');
width: 20px;
height: 20px;
// background: transparent url('~top/browser/server_groups/servers/databases/schemas/static/img/schema.svg') no-repeat center center;
// height: 100%;
// width: 20px;
}
&.selected {
border-color: $input-focus-border-color;
box-shadow: $input-btn-focus-box-shadow;
@ -105,16 +133,24 @@
}
}
.table-schema {
.table-schema-data {
border-bottom: $border-width solid $erd-node-border-color;
padding: $erd-row-padding;
font-weight: bold;
& .table-schema {
font-weight: bold;
word-break: break-all;
}
}
.table-name {
.table-name-data {
border-bottom: $border-width*2 solid $erd-node-border-color;
padding: $erd-row-padding;
font-weight: bold;
& .table-name {
font-weight: bold;
word-break: break-all;
}
}
.table-cols {
@ -123,10 +159,7 @@
.col-row-data {
padding: $erd-row-padding;
width: 100%;
.col-name {
word-break: break-all;
}
word-break: break-all;
}
.col-row-port {
padding: 0;

View File

@ -73,7 +73,10 @@ describe('ERD TableNodeModel', ()=>{
});
describe('setData', ()=>{
let existPort = jasmine.createSpyObj('port', ['removeAllLinks']);
let existPort = jasmine.createSpyObj('port', {
'removeAllLinks': jasmine.createSpy('removeAllLinks'),
'getSubtype': 'notset',
});
beforeEach(()=>{
modelObj._data.columns = [
@ -93,6 +96,7 @@ describe('ERD TableNodeModel', ()=>{
});
it('add columns', ()=>{
spyOn(existPort, 'getSubtype').and.returnValue('many');
existPort.removeAllLinks.calls.reset();
modelObj.setData({
name: 'noname',
@ -118,29 +122,31 @@ describe('ERD TableNodeModel', ()=>{
});
it('update columns', ()=>{
spyOn(existPort, 'getSubtype').and.returnValue('many');
existPort.removeAllLinks.calls.reset();
modelObj.setData({
name: 'noname',
schema: 'erd',
columns: [
{name: 'col1', not_null:false, attnum: 0},
{name: 'col2updated', not_null:false, attnum: 1},
{name: 'col3', not_null:true, attnum: 2},
{name: 'col1', not_null:false, attnum: 0, is_primary_key: false},
{name: 'col2updated', not_null:false, attnum: 1, is_primary_key: false},
{name: 'col3', not_null:true, attnum: 2, is_primary_key: false},
],
});
expect(modelObj.getData()).toEqual({
name: 'noname',
schema: 'erd',
columns: [
{name: 'col1', not_null:false, attnum: 0},
{name: 'col2updated', not_null:false, attnum: 1},
{name: 'col3', not_null:true, attnum: 2},
{name: 'col1', not_null:false, attnum: 0, is_primary_key: false},
{name: 'col2updated', not_null:false, attnum: 1, is_primary_key: false},
{name: 'col3', not_null:true, attnum: 2, is_primary_key: false},
],
});
expect(existPort.removeAllLinks).not.toHaveBeenCalled();
});
it('remove columns', ()=>{
spyOn(existPort, 'getSubtype').and.returnValue('one');
existPort.removeAllLinks.calls.reset();
modelObj.setData({
name: 'noname',

View File

@ -41,6 +41,10 @@ let pgAdmin = {
},
};
let pgWindow = {
pgAdmin: pgAdmin,
};
let alertify = jasmine.createSpyObj('alertify', {
'success': null,
'error': null,
@ -124,7 +128,7 @@ describe('ERD BodyWidget', ()=>{
beforeEach(()=>{
jasmineEnzyme();
body = mount(<BodyWidget params={params} pgAdmin={pgAdmin} getDialog={getDialog} transformToSupported={()=>{}} alertify={alertify}/>);
body = mount(<BodyWidget params={params} pgAdmin={pgAdmin} pgWindow={pgWindow} getDialog={getDialog} transformToSupported={()=>{}} alertify={alertify}/>);
bodyInstance = body.instance();
});
@ -248,7 +252,7 @@ describe('ERD BodyWidget', ()=>{
bodyInstance.addEditNode();
expect(tableDialog.show).toHaveBeenCalled();
let saveCallback = tableDialog.show.calls.mostRecent().args[5];
let saveCallback = tableDialog.show.calls.mostRecent().args[6];
let newData = {key: 'value'};
saveCallback(newData);
expect(bodyInstance.diagram.addNode).toHaveBeenCalledWith(newData);
@ -263,7 +267,7 @@ describe('ERD BodyWidget', ()=>{
bodyInstance.addEditNode(node);
expect(tableDialog.show).toHaveBeenCalled();
saveCallback = tableDialog.show.calls.mostRecent().args[5];
saveCallback = tableDialog.show.calls.mostRecent().args[6];
newData = {key: 'value'};
saveCallback(newData);
expect(node.setData).toHaveBeenCalledWith(newData);

View File

@ -1281,6 +1281,14 @@ acorn@^7.0.0, acorn@^7.1.0, acorn@^7.1.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
adjust-sourcemap-loader@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz#5ae12fb5b7b1c585e80bbb5a63ec163a1a45e61e"
integrity sha512-YBrGyT2/uVQ/c6Rr+t6ZJXniY03YtHGMJQYal368burRGYKqhx9qGTWqcBU5s1CwYY9E/ri63RYyG1IacMZtqw==
dependencies:
loader-utils "^2.0.0"
regex-parser "^2.2.11"
after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
@ -1432,6 +1440,11 @@ argparse@^1.0.6, argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
arity-n@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/arity-n/-/arity-n-1.0.4.tgz#d9e76b11733e08569c0847ae7b39b2860b30b745"
integrity sha1-2edrEXM+CFacCEeuezmyhgswt0U=
arr-diff@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@ -1813,6 +1826,11 @@ base64-arraybuffer@0.1.5:
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
base64-arraybuffer@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
base64-js@^1.0.2, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@ -2437,16 +2455,16 @@ camelcase-keys@^2.0.0:
camelcase "^2.0.0"
map-obj "^1.0.0"
camelcase@5.3.1, camelcase@^5.0.0:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
camelcase@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=
camelcase@^5.0.0:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
caniuse-api@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
@ -2797,6 +2815,13 @@ component-inherit@0.0.3:
resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=
compose-function@3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/compose-function/-/compose-function-3.0.3.tgz#9ed675f13cc54501d30950a486ff6a7ba3ab185f"
integrity sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8=
dependencies:
arity-n "^1.0.4"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -2857,13 +2882,18 @@ content-type@~1.0.4:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
convert-source-map@^1.1.3, convert-source-map@^1.5.0, convert-source-map@^1.7.0:
convert-source-map@1.7.0, convert-source-map@^1.1.3, convert-source-map@^1.5.0, convert-source-map@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
dependencies:
safe-buffer "~5.1.1"
convert-source-map@^0.3.3:
version "0.3.5"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190"
integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA=
convert-source-map@~1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
@ -3051,6 +3081,13 @@ css-declaration-sorter@^4.0.1:
postcss "^7.0.1"
timsort "^0.3.0"
css-line-break@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.1.1.tgz#d5e9bdd297840099eb0503c7310fd34927a026ef"
integrity sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==
dependencies:
base64-arraybuffer "^0.2.0"
css-loader@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.0.tgz#42952ac22bca5d076978638e9813abce49b8f0cc"
@ -3119,6 +3156,16 @@ css-what@^4.0.0:
resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233"
integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==
css@^2.0.0:
version "2.2.4"
resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929"
integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==
dependencies:
inherits "^2.0.3"
source-map "^0.6.1"
source-map-resolve "^0.5.2"
urix "^0.1.0"
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
@ -3235,6 +3282,14 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
dependencies:
es5-ext "^0.10.50"
type "^1.0.1"
dagre@^0.8.4:
version "0.8.5"
resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
@ -3664,6 +3719,11 @@ emoji-regex@^7.0.1:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
emojis-list@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k=
emojis-list@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
@ -3870,6 +3930,32 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
es5-ext@^0.10.35, es5-ext@^0.10.50:
version "0.10.53"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
dependencies:
es6-iterator "~2.0.3"
es6-symbol "~3.1.3"
next-tick "~1.0.0"
es6-iterator@2.0.3, es6-iterator@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
dependencies:
d "1"
es5-ext "^0.10.35"
es6-symbol "^3.1.1"
es6-symbol@^3.1.1, es6-symbol@~3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
dependencies:
d "^1.0.1"
ext "^1.1.2"
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@ -4176,6 +4262,13 @@ ext-name@^5.0.0:
ext-list "^2.0.0"
sort-keys-length "^1.0.0"
ext@^1.1.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
integrity sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==
dependencies:
type "^2.0.0"
extend-shallow@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@ -4982,6 +5075,13 @@ html-escaper@^2.0.0:
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
html2canvas@^1.0.0-rc.7:
version "1.0.0-rc.7"
resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-rc.7.tgz#70c159ce0e63954a91169531894d08ad5627ac98"
integrity sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==
dependencies:
css-line-break "1.1.1"
htmlescape@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
@ -6167,6 +6267,15 @@ loader-runner@^2.4.0:
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==
loader-utils@1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==
dependencies:
big.js "^5.2.2"
emojis-list "^2.0.0"
json5 "^1.0.1"
loader-utils@1.4.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.1, loader-utils@^1.2.3, loader-utils@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
@ -6837,6 +6946,11 @@ neo-async@^2.5.0, neo-async@^2.6.1:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
next-tick@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@ -7854,6 +7968,15 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
postcss@7.0.21:
version "7.0.21"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.21.tgz#06bb07824c19c2021c5d056d5b10c35b989f7e17"
integrity sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==
dependencies:
chalk "^2.4.2"
source-map "^0.6.1"
supports-color "^6.1.0"
postcss@7.0.27:
version "7.0.27"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9"
@ -8260,6 +8383,11 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
regex-parser@^2.2.11:
version "2.2.11"
resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58"
integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==
regexp.prototype.flags@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
@ -8374,6 +8502,22 @@ resolve-from@^4.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
resolve-url-loader@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-3.1.2.tgz#235e2c28e22e3e432ba7a5d4e305c59a58edfc08"
integrity sha512-QEb4A76c8Mi7I3xNKXlRKQSlLBwjUV/ULFMP+G7n3/7tJZ8MG5wsZ3ucxP1Jz8Vevn6fnJsxDx9cIls+utGzPQ==
dependencies:
adjust-sourcemap-loader "3.0.0"
camelcase "5.3.1"
compose-function "3.0.3"
convert-source-map "1.7.0"
es6-iterator "2.0.3"
loader-utils "1.2.3"
postcss "7.0.21"
rework "1.0.1"
rework-visit "1.0.0"
source-map "0.6.1"
resolve-url@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@ -8412,6 +8556,19 @@ ret@~0.1.10:
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
rework-visit@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/rework-visit/-/rework-visit-1.0.0.tgz#9945b2803f219e2f7aca00adb8bc9f640f842c9a"
integrity sha1-mUWygD8hni96ygCtuLyfZA+ELJo=
rework@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/rework/-/rework-1.0.1.tgz#30806a841342b54510aa4110850cd48534144aa7"
integrity sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=
dependencies:
convert-source-map "^0.3.3"
css "^2.0.0"
rfdc@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2"
@ -8890,7 +9047,7 @@ source-list-map@^2.0.0:
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
source-map-resolve@^0.5.0:
source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
version "0.5.3"
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
@ -8919,16 +9076,16 @@ source-map@0.5.0:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.0.tgz#0fe96503ac86a5adb5de63f4e412ae4872cdbe86"
integrity sha1-D+llA6yGpa213mP05BKuSHLNvoY=
source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.3:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@^0.7.3:
version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
@ -9653,6 +9810,16 @@ type-is@~1.6.17, type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
type@^1.0.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
type@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f"
integrity sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"