Commit c634ff12 authored by Jérome Perrin's avatar Jérome Perrin

Merge branch 'mr321' into feat/coding_style_check

merge rebased commits of nexedi/erp5/merge_requests/321

with:

`git cherry-pick nexedi/mr/321...47eba793~1`
parents 98597913 dda1824e
...@@ -9,13 +9,17 @@ ...@@ -9,13 +9,17 @@
<script src="renderjs.js" type="text/javascript"></script> <script src="renderjs.js" type="text/javascript"></script>
<script src="rsvp.js" type="text/javascript"></script> <script src="rsvp.js" type="text/javascript"></script>
--> -->
<!--
FIXME: Including jQuery twice cause the jsplumb to be loaded twice.
For now we assume that it has already been loaded at this point.
<script src="../lib/jquery.js" type="text/javascript"></script> <script src="../lib/jquery.js" type="text/javascript"></script>
-->
<script src="../lib/jquery-ui.js" type="text/javascript"></script> <script src="../lib/jquery-ui.js" type="text/javascript"></script>
<script src="../lib/jquery.jsplumb.js" type="text/javascript"></script> <script src="../lib/jquery.jsplumb.js" type="text/javascript"></script>
<script src="../lib/handlebars.min.js" type="text/javascript"></script> <script src="../lib/handlebars.min.js" type="text/javascript"></script>
<script src="../lib/springy.js" type="text/javascript"></script>
<script src="../dream/mixin_promise.js" type="text/javascript"></script> <script src="../dream/mixin_promise.js" type="text/javascript"></script>
<script src="springy.js" type="text/javascript"></script>
<script src="jsplumb.js" type="text/javascript"></script> <script src="jsplumb.js" type="text/javascript"></script>
<script id="node-template" type="text/x-handlebars-template"> <script id="node-template" type="text/x-handlebars-template">
......
/* =========================================================================== /* ===========================================================================
* Copyright 2013-2015 Nexedi SA and Contributors * Copyright 2013-2015 Nexedi SA and Contributors
* *
* This file is part of DREAM. * This file is part of DREAM.
* *
* DREAM is free software: you can redistribute it and/or modify * DREAM is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by * it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* DREAM is distributed in the hope that it will be useful, * DREAM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details. * GNU Lesser General Public License for more details.
* *
* You should have received a copy of the GNU Lesser General Public License * You should have received a copy of the GNU Lesser General Public License
* along with DREAM. If not, see <http://www.gnu.org/licenses/>. * along with DREAM. If not, see <http://www.gnu.org/licenses/>.
* ==========================================================================*/ * ==========================================================================*/
/*global console, window, RSVP, rJS, $, jsPlumb, Handlebars, /*global console, window, Node, RSVP, rJS, $, jsPlumb, Handlebars,
loopEventListener, promiseEventListener, DOMParser, Springy */ loopEventListener, promiseEventListener, DOMParser, Springy */
/*jslint unparam: true todo: true */ /*jslint vars: true unparam: true nomen: true todo: true */
(function(RSVP, rJS, $, jsPlumb, Handlebars, loopEventListener, promiseEventListener, DOMParser, Springy) { (function (RSVP, rJS, $, jsPlumb, Handlebars, loopEventListener, promiseEventListener, DOMParser, Springy) {
"use strict"; "use strict";
/* TODO: /* TODO:
* less dependancies ( promise event listner ? ) * less dependancies ( promise event listener ? )
* no more handlebars * no more handlebars
* id should not always be modifiable * id should not always be modifiable
* drop zoom level * drop zoom level
* rename draggable() * rename draggable()
* factorize node & edge popup edition * factorize node & edge popup edition
*/ */
/*jslint nomen: true */ var gadget_klass = rJS(window);
var gadget_klass = rJS(window), var domParser = new DOMParser();
domParser = new DOMParser(), var node_template_source = gadget_klass.__template_element.getElementById("node-template").innerHTML;
node_template_source = gadget_klass.__template_element.getElementById("node-template").innerHTML, var node_template = Handlebars.compile(node_template_source);
node_template = Handlebars.compile(node_template_source), var popup_edit_template = gadget_klass.__template_element.getElementById("popup-edit-template").innerHTML;
popup_edit_template = gadget_klass.__template_element.getElementById("popup-edit-template").innerHTML;
function layoutGraph(graph_data) {
function layoutGraph(graph_data) { // Promise returning the graph once springy calculated the layout.
// Promise returning the graph once springy calculated the layout. // If the graph already contain layout, return it as is.
// If the graph already contain layout, return it as is. function resolver(resolve, reject) {
function resolver(resolve, reject) { try {
try { var springy_graph = new Springy.Graph();
var springy_graph = new Springy.Graph(), var max_iterations = 100; // we stop layout after 100 iterations.
max_iterations = 100, // we stop layout after 100 iterations. var loop = 0;
loop = 0, var springy_nodes = {};
springy_nodes = {}, var drawn_nodes = {};
drawn_nodes = {}, var min_x = 100;
min_x=100, max_x=0, min_y=100, max_y=0; var max_x = 0;
// make a Springy graph with our graph var min_y = 100;
$.each(graph_data.node, function(key, value) { var max_y = 0;
if (value.coordinate) { // if graph is empty, no need to layout
// graph already has a layout, no need to layout again if (Object.keys(graph_data.edge).length === 0) {
return resolve(graph_data); resolve(graph_data);
} return;
springy_nodes[key] = springy_graph.newNode({node_id: key}); }
}); // make a Springy graph with our graph
$.each(graph_data.edge, function(key, value) { $.each(graph_data.node, function (key, value) {
springy_graph.newEdge(springy_nodes[value.source], springy_nodes[value.destination]); if (value.coordinate && value.coordinate.top && value.coordinate.left) {
}); // graph already has a layout, no need to layout again
resolve(graph_data);
return;
}
springy_nodes[key] = springy_graph.newNode({node_id: key});
});
$.each(graph_data.edge, function (ignore, value) {
springy_graph.newEdge(springy_nodes[value.source], springy_nodes[value.destination]);
});
var layout = new Springy.Layout.ForceDirected(springy_graph, 400.0, 400.0, 0.5);
var renderer;
renderer = new Springy.Renderer(
layout,
function clear() {
return;
},
function drawEdge() {
return;
},
function drawNode(node, p) {
drawn_nodes[node.data.node_id] = p;
loop += 1;
if (loop > max_iterations) {
renderer.stop();
}
},
function onRenderStop() {
// calculate the min and max of x and y
$.each(graph_data.node, function (key) {
if (drawn_nodes[key].x > max_x) {
max_x = drawn_nodes[key].x;
}
if (drawn_nodes[key].x < min_x) {
min_x = drawn_nodes[key].x;
}
if (drawn_nodes[key].y > max_y) {
max_y = drawn_nodes[key].y;
}
if (drawn_nodes[key].y < min_y) {
min_y = drawn_nodes[key].y;
}
});
// "resample" the positions from 0 to 1, the scale used by this gadget.
// We keep a 5% margin
$.each(graph_data.node, function (key) {
graph_data.node[key].coordinate = {
left: 0.05 + 0.9 * (drawn_nodes[key].x - min_x) / (max_x - min_x),
top: 0.05 + 0.9 * (drawn_nodes[key].y - min_y) / (max_y - min_y)
};
});
resolve(graph_data);
}
);
renderer.start();
} catch (e) {
reject(e);
}
}
return new RSVP.Promise(resolver);
}
var layout = new Springy.Layout.ForceDirected(springy_graph, 400.0, 400.0, 0.5); function loopJsplumbBind(gadget, type, callback) {
var renderer = new Springy.Renderer( //////////////////////////
layout, // Infinite event listener (promise is never resolved)
function clear() {}, // eventListener is removed when promise is cancelled/rejected
function drawEdge(edge, p1, p2) {}, //////////////////////////
function drawNode(node, p) { var handle_event_callback;
drawn_nodes[node.data.node_id] = p; var callback_promise;
if ( ++loop > max_iterations) { var jsplumb_instance = gadget.props.jsplumb_instance;
renderer.stop();
} function cancelResolver() {
}, if (callback_promise !== undefined && typeof callback_promise.cancel === "function") {
function onRenderStop() { callback_promise.cancel();
// calculate the min and max of x and y }
$.each(graph_data.node, function(key, value) { }
if (drawn_nodes[key].x > max_x) {
max_x = drawn_nodes[key].x; function canceller() {
} if (handle_event_callback !== undefined) {
if (drawn_nodes[key].x < min_x) { jsplumb_instance.unbind(type);
min_x = drawn_nodes[key].x; }
} cancelResolver();
if (drawn_nodes[key].y > max_y) {
max_y = drawn_nodes[key].y;
}
if (drawn_nodes[key].y < min_y) {
min_y = drawn_nodes[key].y;
}
});
// "resample" the positions from 0 to 1, the scale used by this gadget.
// We keep a 5% margin
$.each(graph_data.node, function(key, value) {
graph_data.node[key].coordinate = {
left: 0.05 + 0.9 * (drawn_nodes[key].x - min_x) / (max_x - min_x),
top: 0.05 + 0.9 * (drawn_nodes[key].y - min_y) / (max_y - min_y)
};
});
resolve(graph_data);
}
);
renderer.start();
} catch (e) {
reject(e);
}
}
return new RSVP.Promise(resolver);
}
function loopJsplumbBind(gadget, type, callback) {
//////////////////////////
// Infinite event listener (promise is never resolved)
// eventListener is removed when promise is cancelled/rejected
//////////////////////////
var handle_event_callback, callback_promise, jsplumb_instance = gadget.props.jsplumb_instance;
function cancelResolver() {
if (callback_promise !== undefined && typeof callback_promise.cancel === "function") {
callback_promise.cancel();
}
}
function canceller() {
if (handle_event_callback !== undefined) {
jsplumb_instance.unbind(type);
}
cancelResolver();
}
function resolver(resolve, reject) {
handle_event_callback = function() {
var args = arguments;
cancelResolver();
callback_promise = new RSVP.Queue().push(function() {
return callback.apply(jsplumb_instance, args);
}).push(undefined, function(error) {
if (!(error instanceof RSVP.CancellationError)) {
canceller();
reject(error);
}
});
};
jsplumb_instance.bind(type, handle_event_callback);
}
return new RSVP.Promise(resolver, canceller);
}
function getNodeId(gadget, element_id) {
// returns the ID of the node in the graph from its DOM element id
var node_id;
$.each(gadget.props.node_id_to_dom_element_id, function(k, v) {
if (v === element_id) {
node_id = k;
return false;
}
});
return node_id;
}
function generateNodeId(gadget, element) {
// Generate a node id
var n = 1,
class_def = gadget.props.data.class_definition[element._class],
id = class_def.short_id || element._class;
while (gadget.props.data.graph.node[id + n] !== undefined) {
n += 1;
}
return id + n;
}
function generateDomElementId(gadget_element) {
// Generate a probably unique DOM element ID.
var n = 1;
while ($(gadget_element).find("#DreamNode_" + n).length > 0) {
n += 1;
}
return "DreamNode_" + n;
}
function getDefaultEdgeClass(gadget) {
var class_definition = gadget.props.data.class_definition;
for (var key in class_definition) {
if (class_definition.hasOwnProperty(key) && class_definition[key]._class === 'edge') {
return key;
}
}
return "Dream.Edge";
}
function updateConnectionData(gadget, connection, remove) {
if (connection.ignoreEvent) {
// this hack is for edge edition. Maybe there I missed one thing and
// there is a better way.
return;
}
if (remove) {
delete gadget.props.data.graph.edge[connection.id];
} else {
var edge_data = gadget.props.data.graph.edge[connection.id] || {
_class: getDefaultEdgeClass(gadget)
};
edge_data.source = getNodeId(gadget, connection.sourceId);
edge_data.destination = getNodeId(gadget, connection.targetId);
gadget.props.data.graph.edge[connection.id] = edge_data;
}
gadget.notifyDataChanged();
}
function convertToAbsolutePosition(gadget, x, y) {
var zoom_level = gadget.props.zoom_level,
canvas_size_x = $(gadget.props.main).width(),
canvas_size_y = $(gadget.props.main).height(),
size_x = $(gadget.props.element).find(".dummy_window").width() * zoom_level,
size_y = $(gadget.props.element).find(".dummy_window").height() * zoom_level,
top = Math.floor(y * (canvas_size_y - size_y)) + "px",
left = Math.floor(x * (canvas_size_x - size_x)) + "px";
return [left, top];
}
function convertToRelativePosition(gadget, x, y) {
var zoom_level = gadget.props.zoom_level,
canvas_size_x = $(gadget.props.main).width(),
canvas_size_y = $(gadget.props.main).height(),
size_x = $(gadget.props.element).find(".dummy_window").width() * zoom_level,
size_y = $(gadget.props.element).find(".dummy_window").height() * zoom_level,
top = Math.max(Math.min(y.replace("px", "") / (canvas_size_y - size_y), 1), 0),
left = Math.max(Math.min(x.replace("px", "") / (canvas_size_x - size_x), 1), 0);
return [left, top];
}
function updateElementCoordinate(gadget, node_id, coordinate) {
var element_id = gadget.props.node_id_to_dom_element_id[node_id],
element,
relative_position;
if (coordinate === undefined) {
element = $(gadget.props.element).find("#" + element_id);
relative_position = convertToRelativePosition(gadget, element.css("left"), element.css("top"));
coordinate = {
left: relative_position[0],
top: relative_position[1]
};
}
gadget.props.data.graph.node[node_id].coordinate = coordinate;
gadget.notifyDataChanged();
return coordinate;
}
function draggable(gadget) {
var jsplumb_instance = gadget.props.jsplumb_instance,
stop = function(element) {
updateElementCoordinate(gadget, getNodeId(gadget, element.target.id));
};
// XXX This function should only touch the node element that we just added.
jsplumb_instance.draggable(jsplumb_instance.getSelector(".window"), {
containment: "parent",
grid: [10, 10],
stop: stop
});
jsplumb_instance.makeSource(jsplumb_instance.getSelector(".window"), {
filter: ".ep",
anchor: "Continuous",
connector: ["StateMachine", {
curviness: 20
}],
connectorStyle: {
strokeStyle: "#5c96bc",
lineWidth: 2,
outlineColor: "transparent",
outlineWidth: 4
}
});
jsplumb_instance.makeTarget(jsplumb_instance.getSelector(".window"), {
dropOptions: {
hoverClass: "dragHover"
},
anchor: "Continuous"
});
}
function updateNodeStyle(gadget, element_id) {
// Update node size according to the zoom level
// XXX does nothing for now
var zoom_level = gadget.props.zoom_level,
element = $(gadget.props.element).find("#" + element_id),
new_value;
$.each(gadget.props.style_attr_list, function(i, j) {
new_value = element.css(j).replace("px", "") * zoom_level + "px";
element.css(j, new_value);
});
}
function removeElement(gadget, node_id) {
var element_id = gadget.props.node_id_to_dom_element_id[node_id];
gadget.props.jsplumb_instance.removeAllEndpoints($(gadget.props.element).find("#" + element_id));
$(gadget.props.element).find("#" + element_id).remove();
delete gadget.props.data.graph.node[node_id];
delete gadget.props.node_id_to_dom_element_id[node_id];
$.each(gadget.props.data.graph.edge, function(k, v) {
if (node_id === v.source || node_id === v.destination) {
delete gadget.props.data.graph.edge[k];
}
});
gadget.notifyDataChanged();
}
function updateElementData(gadget, node_id, data) {
var element_id = gadget.props.node_id_to_dom_element_id[node_id],
new_id = data.id || data.data.id;
$(gadget.props.element).find("#" + element_id).text(data.data.name || new_id)
.attr("title", data.data.name || new_id)
.append('<div class="ep"></div></div>');
delete data.id;
$.extend(gadget.props.data.graph.node[node_id], data.data);
if (new_id && new_id !== node_id) {
gadget.props.data.graph.node[new_id] = gadget.props.data.graph.node[node_id];
delete gadget.props.data.graph.node[node_id];
gadget.props.node_id_to_dom_element_id[new_id] = gadget.props.node_id_to_dom_element_id[node_id];
delete gadget.props.node_id_to_dom_element_id[node_id];
delete gadget.props.data.graph.node[new_id].id;
$.each(gadget.props.data.graph.edge, function (k, v) {
if (v.source === node_id) {
v.source = new_id;
} }
if (v.destination === node_id) {
v.destination = new_id; function resolver(ignore, reject) {
handle_event_callback = function () {
var args = arguments;
cancelResolver();
callback_promise = new RSVP.Queue().push(function () {
return callback.apply(jsplumb_instance, args);
}).push(undefined, function (error) {
if (!(error instanceof RSVP.CancellationError)) {
canceller();
reject(error);
}
});
};
jsplumb_instance.bind(type, handle_event_callback);
} }
}); return new RSVP.Promise(resolver, canceller);
} }
gadget.notifyDataChanged();
} function getNodeId(gadget, element_id) {
// returns the ID of the node in the graph from its DOM element id
var node_id;
function addEdge(gadget, edge_id, edge_data) { $.each(gadget.props.node_id_to_dom_element_id, function (k, v) {
var overlays = [], if (v === element_id) {
connection; node_id = k;
if (edge_data.name) { return false;
overlays = [ }
["Label", { });
cssClass: "l1 component label", return node_id;
label: edge_data.name }
}]
]; function generateNodeId(gadget, element) {
} // Generate a node id
if (gadget.props.data.graph.node[edge_data.source] === undefined) { var n = 1;
throw new Error("Error adding edge " + edge_id + " Source " + edge_data.source + " does not exist"); var class_def = gadget.props.data.class_definition[element._class];
} var id = class_def.short_id || element._class;
if (gadget.props.data.graph.node[edge_data.destination] === undefined) { while (gadget.props.data.graph.node[id + n] !== undefined) {
throw new Error("Edge adding edge " + edge_id + " Destination " + edge_data.destination + " does not exist"); n += 1;
} }
// If an edge has this data: return id + n;
// { _class: 'Edge', }
// source: 'N1',
// destination: 'N2', function generateDomElementId(gadget_element) {
// jsplumb_source_endpoint: 'BottomCenter', // Generate a probably unique DOM element ID.
// jsplumb_destination_endpoint: 'LeftMiddle', var n = 1;
// jsplumb_connector: 'Flowchart' } while ($(gadget_element).find("#DreamNode_" + n).length > 0) {
// Then it is rendered using a flowchart connector. The difficulty is that n += 1;
// jsplumb does not let you configure the connector type on the edge, but }
// on the source endpoint. One solution seem to create all types of return "DreamNode_" + n;
// endpoints on nodes. }
if (edge_data.jsplumb_connector === "Flowchart") {
connection = gadget.props.jsplumb_instance.connect({ function getDefaultEdgeClass(gadget) {
uuids: [edge_data.source + ".flowChart" + edge_data.jsplumb_source_endpoint, var class_definition = gadget.props.data.class_definition;
edge_data.destination + ".flowChart" + edge_data.jsplumb_destination_endpoint var key;
], for (key in class_definition) {
overlays: overlays if (class_definition.hasOwnProperty(key) && class_definition[key]._class === "edge") {
}); return key;
} else { }
connection = gadget.props.jsplumb_instance.connect({ }
source: gadget.props.node_id_to_dom_element_id[edge_data.source], return "Dream.Edge";
target: gadget.props.node_id_to_dom_element_id[edge_data.destination], }
Connector: ["Bezier", {
curviness: 75 function updateConnectionData(gadget, connection, remove) {
}], if (connection.ignoreEvent) {
overlays: overlays // this hack is for edge edition. Maybe there I missed one thing and
}); // there is a better way.
} return;
// set data for 'connection' event that will be called "later" }
gadget.props.data.graph.edge[edge_id] = edge_data; if (remove) {
// jsplumb assigned an id, but we are controlling ids ourselves. delete gadget.props.data.graph.edge[connection.id];
connection.id = edge_id; } else {
} var edge_data = gadget.props.data.graph.edge[connection.id] || {
_class: getDefaultEdgeClass(gadget)
function expandSchema(class_definition, full_schema) { };
// minimal expanding of json schema, supports merging allOf and $ref edge_data.source = getNodeId(gadget, connection.sourceId);
// references edge_data.destination = getNodeId(gadget, connection.targetId);
// XXX this should probably be moved to fieldset ( and not handle gadget.props.data.graph.edge[connection.id] = edge_data;
// class_definition here) }
gadget.notifyDataChanged();
function resolveReference(ref, schema) { }
// 2 here is for #/
var i, ref_path = ref.substr(2, ref.length), function convertToAbsolutePosition(gadget, x, y) {
parts = ref_path.split("/"); var zoom_level = gadget.props.zoom_level;
for (i = 0; i < parts.length; i += 1) { var canvas_size_x = $(gadget.props.main).width();
schema = schema[parts[i]]; var canvas_size_y = $(gadget.props.main).height();
} var size_x = $(gadget.props.element).find(".dummy_window").width() * zoom_level;
return schema; var size_y = $(gadget.props.element).find(".dummy_window").height() * zoom_level;
} var top = Math.floor(y * (canvas_size_y - size_y)) + "px";
var left = Math.floor(x * (canvas_size_x - size_x)) + "px";
function clone(obj) { return [left, top];
return JSON.parse(JSON.stringify(obj)); }
}
function convertToRelativePosition(gadget, x, y) {
var referenced, var zoom_level = gadget.props.zoom_level;
i, var canvas_size_x = $(gadget.props.main).width();
property, var canvas_size_y = $(gadget.props.main).height();
expanded_class_definition = clone(class_definition) || {}; var size_x = $(gadget.props.element).find(".dummy_window").width() * zoom_level;
var size_y = $(gadget.props.element).find(".dummy_window").height() * zoom_level;
var top = Math.max(Math.min(y.replace("px", "") / (canvas_size_y - size_y), 1), 0);
if (!expanded_class_definition.properties) { var left = Math.max(Math.min(x.replace("px", "") / (canvas_size_x - size_x), 1), 0);
expanded_class_definition.properties = {}; return [left, top];
} }
// expand direct ref
if (class_definition.$ref) { function updateElementCoordinate(gadget, node_id, coordinate) {
referenced = expandSchema(resolveReference(class_definition.$ref, full_schema.class_definition), full_schema); var element_id = gadget.props.node_id_to_dom_element_id[node_id];
$.extend(expanded_class_definition, referenced); var element;
delete expanded_class_definition.$ref; var relative_position;
} if (coordinate === undefined) {
// expand ref in properties element = $(gadget.props.element).find("#" + element_id);
for (property in class_definition.properties) { relative_position = convertToRelativePosition(gadget, element.css("left"), element.css("top"));
if (class_definition.properties.hasOwnProperty(property)) { coordinate = {
if (class_definition.properties[property].$ref) { left: relative_position[0],
referenced = expandSchema(resolveReference(class_definition.properties[property].$ref, full_schema.class_definition), full_schema); top: relative_position[1]
$.extend(expanded_class_definition.properties[property], referenced); };
delete expanded_class_definition.properties[property].$ref; }
} else { gadget.props.data.graph.node[node_id].coordinate = coordinate;
if (class_definition.properties[property].type === "object") { gadget.notifyDataChanged();
// no reference, but we expand anyway because we need to recurse in case there is a ref in an object property return coordinate;
referenced = expandSchema(class_definition.properties[property], full_schema); }
$.extend(expanded_class_definition.properties[property], referenced);
} function draggable(gadget) {
} var jsplumb_instance = gadget.props.jsplumb_instance;
} var stop = function (element) {
} updateElementCoordinate(gadget, getNodeId(gadget, element.target.id));
if (class_definition.oneOf) { };
expanded_class_definition.oneOf = [];
for (i = 0; i < class_definition.oneOf.length; i += 1) { // XXX This function should only touch the node element that we just added.
expanded_class_definition.oneOf.push(expandSchema(class_definition.oneOf[i], full_schema)); jsplumb_instance.draggable(jsplumb_instance.getSelector(".window"), {
} containment: "parent",
} grid: [10, 10],
if (class_definition.allOf) { stop: stop
for (i = 0; i < class_definition.allOf.length; i += 1) { });
referenced = expandSchema(class_definition.allOf[i], full_schema); jsplumb_instance.makeSource(jsplumb_instance.getSelector(".window"), {
if (referenced.properties) { filter: ".ep",
$.extend(expanded_class_definition.properties, referenced.properties); anchor: "Continuous",
delete referenced.properties; connector: ["StateMachine", {
} curviness: 20
$.extend(expanded_class_definition, referenced); }],
} connectorStyle: {
if (expanded_class_definition.allOf) { strokeStyle: "#5c96bc",
delete expanded_class_definition.allOf; lineWidth: 2,
} outlineColor: "transparent",
} outlineWidth: 4
if (expanded_class_definition.$ref) { }
delete expanded_class_definition.$ref; });
} jsplumb_instance.makeTarget(jsplumb_instance.getSelector(".window"), {
return clone(expanded_class_definition); dropOptions: {
} hoverClass: "dragHover"
},
function openEdgeEditionDialog(gadget, connection) { anchor: "Continuous"
var edge_id = connection.id, });
edge_data = gadget.props.data.graph.edge[edge_id], }
edit_popup = $(gadget.props.element).find("#popup-edit-template"),
schema, function updateNodeStyle(gadget, element_id) {
fieldset_element, // Update node size according to the zoom level
delete_promise; // XXX does nothing for now
schema = expandSchema(gadget.props.data.class_definition[edge_data._class], gadget.props.data); var zoom_level = gadget.props.zoom_level;
// We do not edit source & destination on edge this way. var element = $(gadget.props.element).find("#" + element_id);
delete schema.properties.source; var new_value;
delete schema.properties.destination; $.each(gadget.props.style_attr_list, function (ignore, j) {
gadget.props.element.insertAdjacentHTML("beforeend", popup_edit_template); new_value = element.css(j).replace("px", "") * zoom_level + "px";
edit_popup = $(gadget.props.element).find("#edit-popup"); element.css(j, new_value);
edit_popup.find(".node_class").text(connection.name || connection._class); });
fieldset_element = edit_popup.find("fieldset")[0]; }
edit_popup.dialog();
edit_popup.show(); function removeElement(gadget, node_id) {
var element_id = gadget.props.node_id_to_dom_element_id[node_id];
function save_promise(fieldset_gadget, edge_id) { gadget.props.jsplumb_instance.removeAllEndpoints($(gadget.props.element).find("#" + element_id));
return RSVP.Queue().push(function() { $(gadget.props.element).find("#" + element_id).remove();
return promiseEventListener(edit_popup.find(".graph_editor_validate_button")[0], "click", false); delete gadget.props.data.graph.node[node_id];
}).push(function(evt) { delete gadget.props.node_id_to_dom_element_id[node_id];
var data = { $.each(gadget.props.data.graph.edge, function (k, v) {
id: $(evt.target[1]).val(), if (node_id === v.source || node_id === v.destination) {
data: {} delete gadget.props.data.graph.edge[k];
}; }
return fieldset_gadget.getContent().then(function(r) { });
$.extend(data.data, gadget.props.data.graph.edge[connection.id]); gadget.notifyDataChanged();
$.extend(data.data, r); }
// to redraw, we remove the edge and add again.
// but we want to disable events on connection, since event function updateElementData(gadget, node_id, data) {
// handling promise are executed asynchronously in undefined order, var element_id = gadget.props.node_id_to_dom_element_id[node_id];
// we cannot just remove and /then/ add, because the new edge is var new_id = data.id || data.data.id;
// added before the old is removed. $(gadget.props.element).find("#" + element_id).text(data.data.name || new_id)
connection.ignoreEvent = true; .attr("title", data.data.name || new_id)
gadget.props.jsplumb_instance.detach(connection); .append("<div class='ep'></div></div>");
addEdge(gadget, r.id, data.data);
}); delete data.id;
});
} $.extend(gadget.props.data.graph.node[node_id], data.data);
delete_promise = new RSVP.Queue().push(function() { if (new_id && new_id !== node_id) {
return promiseEventListener(edit_popup.find(".graph_editor_delete_button")[0], "click", false); gadget.props.data.graph.node[new_id] = gadget.props.data.graph.node[node_id];
}).push(function() { delete gadget.props.data.graph.node[node_id];
// connectionDetached event will remove the edge from data
gadget.props.jsplumb_instance.detach(connection); gadget.props.node_id_to_dom_element_id[new_id] = gadget.props.node_id_to_dom_element_id[node_id];
}); delete gadget.props.node_id_to_dom_element_id[node_id];
return gadget.declareGadget("../fieldset/index.html", {
element: fieldset_element, delete gadget.props.data.graph.node[new_id].id;
scope: "fieldset" $.each(gadget.props.data.graph.edge, function (ignore, v) {
}).push(function(fieldset_gadget) { if (v.source === node_id) {
return RSVP.all([fieldset_gadget, fieldset_gadget.render({ v.source = new_id;
value: edge_data, }
property_definition: schema if (v.destination === node_id) {
}, edge_id)]); v.destination = new_id;
}).push(function(fieldset_gadget) { }
edit_popup.dialog("open"); });
return fieldset_gadget[0]; }
}).push(function(fieldset_gadget) { gadget.notifyDataChanged();
fieldset_gadget.startService(); // XXX }
return fieldset_gadget;
}).push(function(fieldset_gadget) {
// Expose the dialog handling promise so that we can wait for it in function addEdge(gadget, edge_id, edge_data) {
// test. var overlays = [];
gadget.props.dialog_promise = RSVP.any([save_promise(fieldset_gadget, edge_id), delete_promise]); var connection;
return gadget.props.dialog_promise; if (edge_data.name) {
}).push(function() { overlays = [
edit_popup.dialog("close"); ["Label", {
edit_popup.remove(); cssClass: "l1 component label",
delete gadget.props.dialog_promise; label: edge_data.name
}); }]
} ];
}
function openNodeEditionDialog(gadget, element) { if (gadget.props.data.graph.node[edge_data.source] === undefined) {
var node_id = getNodeId(gadget, element.id), throw new Error("Error adding edge " + edge_id + " Source " + edge_data.source + " does not exist");
node_data = gadget.props.data.graph.node[node_id], }
node_edit_popup = $(gadget.props.element).find("#popup-edit-template"), if (gadget.props.data.graph.node[edge_data.destination] === undefined) {
schema, throw new Error("Edge adding edge " + edge_id + " Destination " + edge_data.destination + " does not exist");
fieldset_element, }
delete_promise; // If an edge has this data:
// If we have no definition for this, we do not allow edition. // { _class: 'Edge',
// XXX incorrect, we need to display this dialog to be able // source: 'N1',
// to delete a node // destination: 'N2',
if (gadget.props.data.class_definition[node_data._class] === undefined) { // jsplumb_source_endpoint: 'BottomCenter',
return; // jsplumb_destination_endpoint: 'LeftMiddle',
} // jsplumb_connector: 'Flowchart' }
schema = expandSchema(gadget.props.data.class_definition[node_data._class], gadget.props.data); // Then it is rendered using a flowchart connector. The difficulty is that
if (node_edit_popup.length !== 0) { // jsplumb does not let you configure the connector type on the edge, but
node_edit_popup.remove(); // on the source endpoint. One solution seem to create all types of
} // endpoints on nodes.
gadget.props.element.insertAdjacentHTML("beforeend", popup_edit_template); if (edge_data.jsplumb_connector === "Flowchart") {
node_edit_popup = $(gadget.props.element).find("#edit-popup"); connection = gadget.props.jsplumb_instance.connect({
// Set the name of the popup to the node class uuids: [
node_edit_popup.find(".node_class").text(node_data.name || node_data._class); edge_data.source + ".flowChart" + edge_data.jsplumb_source_endpoint,
fieldset_element = node_edit_popup.find("fieldset")[0]; edge_data.destination + ".flowChart" + edge_data.jsplumb_destination_endpoint
node_edit_popup.dialog(); ],
node_data.id = node_id; overlays: overlays
});
function save_promise(fieldset_gadget, node_id) { } else {
return RSVP.Queue().push(function() { connection = gadget.props.jsplumb_instance.connect({
return promiseEventListener(node_edit_popup.find(".graph_editor_validate_button")[0], "click", false); source: gadget.props.node_id_to_dom_element_id[edge_data.source],
}).push(function(evt) { target: gadget.props.node_id_to_dom_element_id[edge_data.destination],
var data = { Connector: ["Bezier", {
// XXX id should not be handled differently ... curviness: 75
id: $(evt.target[1]).val(), }],
data: {} overlays: overlays
}; });
return fieldset_gadget.getContent().then(function(r) { }
$.extend(data.data, r); // set data for 'connection' event that will be called "later"
updateElementData(gadget, node_id, data); gadget.props.data.graph.edge[edge_id] = edge_data;
}); // jsplumb assigned an id, but we are controlling ids ourselves.
}); connection.id = edge_id;
} }
delete_promise = new RSVP.Queue().push(function() {
return promiseEventListener(node_edit_popup.find(".graph_editor_delete_button")[0], "click", false); function expandSchema(class_definition, full_schema) {
}).push(function() { // minimal expanding of json schema, supports merging allOf and $ref
return removeElement(gadget, node_id); // references
}); // XXX this should probably be moved to fieldset ( and not handle
return gadget.declareGadget("../fieldset/index.html", { // class_definition here)
element: fieldset_element, function resolveReference(ref, schema) {
scope: "fieldset" var i;
}).push(function(fieldset_gadget) { var ref_path = ref.substr(2, ref.length); // 2 here is for #/
return RSVP.all([fieldset_gadget, fieldset_gadget.render({ var parts = ref_path.split("/");
value: node_data, for (i = 0; i < parts.length; i += 1) {
property_definition: schema schema = schema[parts[i]];
}, node_id)]); }
}).push(function(fieldset_gadget) { return schema;
node_edit_popup.dialog("open"); }
return fieldset_gadget[0];
}).push(function(fieldset_gadget) { function clone(obj) {
fieldset_gadget.startService(); // XXX this should not be needed anymore. return JSON.parse(JSON.stringify(obj));
return fieldset_gadget; }
}).push(function(fieldset_gadget) {
// Expose the dialog handling promise so that we can wait for it in var referenced;
// test. var i;
gadget.props.dialog_promise = RSVP.any([save_promise(fieldset_gadget, node_id), delete_promise]); var property;
return gadget.props.dialog_promise; var expanded_class_definition = clone(class_definition) || {};
}).push(function() {
node_edit_popup.dialog("close");
node_edit_popup.remove(); if (!expanded_class_definition.properties) {
delete gadget.props.dialog_promise; expanded_class_definition.properties = {};
}); }
} // expand direct ref
if (class_definition.$ref) {
function waitForNodeClick(gadget, node) { referenced = expandSchema(resolveReference(class_definition.$ref, full_schema.class_definition), full_schema);
gadget.props.nodes_click_monitor.monitor(loopEventListener(node, "dblclick", false, openNodeEditionDialog.bind(null, gadget, node))); $.extend(expanded_class_definition, referenced);
} delete expanded_class_definition.$ref;
}
function waitForConnection(gadget) { // expand ref in properties
return loopJsplumbBind(gadget, "connection", function(info, originalEvent) { for (property in class_definition.properties) {
updateConnectionData(gadget, info.connection, false); if (class_definition.properties.hasOwnProperty(property)) {
}); if (class_definition.properties[property].$ref) {
} referenced = expandSchema(resolveReference(class_definition.properties[property].$ref, full_schema.class_definition), full_schema);
$.extend(expanded_class_definition.properties[property], referenced);
function waitForConnectionDetached(gadget) { delete expanded_class_definition.properties[property].$ref;
return loopJsplumbBind(gadget, "connectionDetached", function(info, originalEvent) { } else {
updateConnectionData(gadget, info.connection, true); if (class_definition.properties[property].type === "object") {
}); // no reference, but we expand anyway because we need to recurse in case there is a ref in an object property
} referenced = expandSchema(class_definition.properties[property], full_schema);
$.extend(expanded_class_definition.properties[property], referenced);
function waitForConnectionClick(gadget) { }
return loopJsplumbBind(gadget, "click", function(connection) { }
return openEdgeEditionDialog(gadget, connection); }
}); }
} if (class_definition.oneOf) {
expanded_class_definition.oneOf = [];
function addNode(gadget, node_id, node_data) { for (i = 0; i < class_definition.oneOf.length; i += 1) {
var render_element = $(gadget.props.main), expanded_class_definition.oneOf.push(expandSchema(class_definition.oneOf[i], full_schema));
class_definition = gadget.props.data.class_definition[node_data._class], }
coordinate = node_data.coordinate, }
dom_element_id, if (class_definition.allOf) {
box, for (i = 0; i < class_definition.allOf.length; i += 1) {
absolute_position, referenced = expandSchema(class_definition.allOf[i], full_schema);
domElement; if (referenced.properties) {
$.extend(expanded_class_definition.properties, referenced.properties);
dom_element_id = generateDomElementId(gadget.props.element); delete referenced.properties;
gadget.props.node_id_to_dom_element_id[node_id] = dom_element_id; }
node_data.name = node_data.name || class_definition.name; $.extend(expanded_class_definition, referenced);
gadget.props.data.graph.node[node_id] = node_data; }
if (coordinate === undefined) { if (expanded_class_definition.allOf) {
coordinate = { delete expanded_class_definition.allOf;
top: 0, }
left: 0 }
}; if (expanded_class_definition.$ref) {
} delete expanded_class_definition.$ref;
node_data.coordinate = updateElementCoordinate(gadget, node_id, coordinate); }
/*jslint nomen: true*/ return clone(expanded_class_definition);
domElement = domParser.parseFromString(node_template({ }
"class": node_data._class.replace(".", "-"),
element_id: dom_element_id, function openEdgeEditionDialog(gadget, connection) {
title: node_data.name || node_data.id, var edge_id = connection.id;
name: node_data.name || node_data.id var edge_data = gadget.props.data.graph.edge[edge_id];
}), "text/html").querySelector(".window"); var edit_popup = $(gadget.props.element).find("#popup-edit-template");
render_element.append(domElement); var schema;
waitForNodeClick(gadget, domElement); var fieldset_element;
box = $(gadget.props.element).find("#" + dom_element_id); var delete_promise;
absolute_position = convertToAbsolutePosition(gadget, coordinate.left, coordinate.top); schema = expandSchema(gadget.props.data.class_definition[edge_data._class], gadget.props.data);
if (class_definition && class_definition.css) { // We do not edit source & destination on edge this way.
box.css(class_definition.css); delete schema.properties.source;
} delete schema.properties.destination;
box.css("top", absolute_position[1]); gadget.props.element.insertAdjacentHTML("beforeend", popup_edit_template);
box.css("left", absolute_position[0]); edit_popup = $(gadget.props.element).find("#edit-popup");
updateNodeStyle(gadget, dom_element_id); edit_popup.find(".node_class").text(connection.name || connection._class);
draggable(gadget); fieldset_element = edit_popup.find("fieldset")[0];
// XXX make only this element draggable. edit_popup.dialog();
// Add some flowchart endpoints edit_popup.show();
// TODO: add them all !
gadget.props.jsplumb_instance.addEndpoint(dom_element_id, { function save_promise(fieldset_gadget) {
isSource: true, return new RSVP.Queue().push(function () {
maxConnections: -1, return promiseEventListener(edit_popup.find(".graph_editor_validate_button")[0], "click", false);
connector: ["Flowchart", { }).push(function (evt) {
stub: [40, 60], var data = {
gap: 10, id: $(evt.target[1]).val(),
cornerRadius: 5, data: {}
alwaysRespectStubs: true };
}] return fieldset_gadget.getContent().then(function (r) {
}, { $.extend(data.data, gadget.props.data.graph.edge[connection.id]);
anchor: "BottomCenter", $.extend(data.data, r);
uuid: node_id + ".flowchartBottomCenter" // to redraw, we remove the edge and add again.
}); // but we want to disable events on connection, since event
gadget.props.jsplumb_instance.addEndpoint(dom_element_id, { // handling promise are executed asynchronously in undefined order,
isTarget: true, // we cannot just remove and /then/ add, because the new edge is
maxConnections: -1 // added before the old is removed.
}, { connection.ignoreEvent = true;
anchor: "LeftMiddle", gadget.props.jsplumb_instance.detach(connection);
uuid: node_id + ".flowChartLeftMiddle" addEdge(gadget, r.id, data.data);
}); });
gadget.notifyDataChanged(); });
} }
delete_promise = new RSVP.Queue().push(function () {
function waitForDrop(gadget) { return promiseEventListener(edit_popup.find(".graph_editor_delete_button")[0], "click", false);
var callback; }).push(function () {
// connectionDetached event will remove the edge from data
function canceller() { gadget.props.jsplumb_instance.detach(connection);
if (callback !== undefined) { });
gadget.props.main.removeEventListener("drop", callback, false); return gadget.declareGadget("../fieldset/index.html", {
} element: fieldset_element,
} scope: "fieldset"
/*jslint unparam: true*/ }).push(function (fieldset_gadget) {
function resolver(resolve, reject) { return RSVP.all([fieldset_gadget, fieldset_gadget.render({
callback = function(evt) { value: edge_data,
try { property_definition: schema
var class_name, offset = $(gadget.props.main).offset(), }, edge_id)]);
relative_position = convertToRelativePosition(gadget, evt.clientX - offset.left + "px", evt.clientY - offset.top + "px"); }).push(function (fieldset_gadget) {
try { edit_popup.dialog("open");
// html5 compliant browser return fieldset_gadget[0];
class_name = JSON.parse(evt.dataTransfer.getData("application/json")); }).push(function (fieldset_gadget) {
} catch (e) { fieldset_gadget.startService(); // XXX
// internet explorer return fieldset_gadget;
class_name = JSON.parse(evt.dataTransfer.getData("text")); }).push(function (fieldset_gadget) {
// Expose the dialog handling promise so that we can wait for it in
// test.
gadget.props.dialog_promise = RSVP.any([save_promise(fieldset_gadget, edge_id), delete_promise]);
return gadget.props.dialog_promise;
}).push(function () {
edit_popup.dialog("close");
edit_popup.remove();
delete gadget.props.dialog_promise;
});
}
function openNodeEditionDialog(gadget, element) {
var node_id = getNodeId(gadget, element.id);
var node_data = gadget.props.data.graph.node[node_id];
var node_edit_popup = $(gadget.props.element).find("#popup-edit-template");
var schema;
var fieldset_element;
var delete_promise;
// If we have no definition for this, we do not allow edition.
// XXX incorrect, we need to display this dialog to be able
// to delete a node
if (gadget.props.data.class_definition[node_data._class] === undefined) {
return false;
}
schema = expandSchema(gadget.props.data.class_definition[node_data._class], gadget.props.data);
if (node_edit_popup.length !== 0) {
node_edit_popup.remove();
}
gadget.props.element.insertAdjacentHTML("beforeend", popup_edit_template);
node_edit_popup = $(gadget.props.element).find("#edit-popup");
// Set the name of the popup to the node class
node_edit_popup.find(".node_class").text(node_data.name || node_data._class);
fieldset_element = node_edit_popup.find("fieldset")[0];
node_edit_popup.dialog();
node_data.id = node_id;
function save_promise(fieldset_gadget, node_id) {
return new RSVP.Queue().push(function () {
return promiseEventListener(
node_edit_popup.find(".graph_editor_validate_button")[0],
"click",
false
);
}).push(function (evt) {
var data = {
// XXX id should not be handled differently ...
id: $(evt.target[1]).val(),
data: {}
};
return fieldset_gadget.getContent().then(function (r) {
$.extend(data.data, r);
updateElementData(gadget, node_id, data);
});
});
}
delete_promise = new RSVP.Queue().push(function () {
return promiseEventListener(
node_edit_popup.find(".graph_editor_delete_button")[0],
"click",
false
);
}).push(function () {
return removeElement(gadget, node_id);
});
return gadget.declareGadget("../fieldset/index.html", {
element: fieldset_element,
scope: "fieldset"
}).push(function (fieldset_gadget) {
return RSVP.all([
fieldset_gadget,
fieldset_gadget.render(
{
value: node_data,
property_definition: schema
},
node_id
)
]);
}).push(function (fieldset_gadget) {
node_edit_popup.dialog("open");
return fieldset_gadget[0];
}).push(function (fieldset_gadget) {
fieldset_gadget.startService(); // XXX this should not be needed anymore.
return fieldset_gadget;
}).push(function (fieldset_gadget) {
// Expose the dialog handling promise so that we can wait for it in
// test.
gadget.props.dialog_promise = RSVP.any([save_promise(fieldset_gadget, node_id), delete_promise]);
return gadget.props.dialog_promise;
}).push(function () {
node_edit_popup.dialog("close");
node_edit_popup.remove();
delete gadget.props.dialog_promise;
});
}
function waitForConnection(gadget) {
return loopJsplumbBind(gadget, "connection", function (info) {
updateConnectionData(gadget, info.connection, false);
});
}
function waitForConnectionDetached(gadget) {
return loopJsplumbBind(gadget, "connectionDetached", function (info) {
updateConnectionData(gadget, info.connection, true);
});
}
function waitForConnectionClick(gadget) {
return loopJsplumbBind(gadget, "click", function (connection) {
return openEdgeEditionDialog(gadget, connection);
});
}
function addNode(gadget, node_id, node_data) {
var render_element = $(gadget.props.main);
var class_definition = gadget.props.data.class_definition[node_data._class];
var coordinate = node_data.coordinate;
var dom_element_id;
var box;
var absolute_position;
var domElement;
dom_element_id = generateDomElementId(gadget.props.element);
gadget.props.node_id_to_dom_element_id[node_id] = dom_element_id;
node_data.name = node_data.name || class_definition.name;
gadget.props.data.graph.node[node_id] = node_data;
if (coordinate === undefined) {
coordinate = {
top: 0,
left: 0
};
}
node_data.coordinate = updateElementCoordinate(gadget, node_id, coordinate);
domElement = domParser.parseFromString(node_template({
"class": node_data._class.replace(".", "-"),
element_id: dom_element_id,
title: node_data.name || node_data.id,
name: node_data.name || node_data.id
}), "text/html").querySelector(".window");
render_element.append(domElement);
box = $(gadget.props.element).find("#" + dom_element_id);
absolute_position = convertToAbsolutePosition(gadget, coordinate.left, coordinate.top);
if (class_definition && class_definition.css) {
box.css(class_definition.css);
}
box.css("top", absolute_position[1]);
box.css("left", absolute_position[0]);
updateNodeStyle(gadget, dom_element_id);
draggable(gadget);
// XXX make only this element draggable.
// Add some flowchart endpoints
// TODO: add them all !
gadget.props.jsplumb_instance.addEndpoint(dom_element_id, {
isSource: true,
maxConnections: -1,
connector: ["Flowchart", {
stub: [40, 60],
gap: 10,
cornerRadius: 5,
alwaysRespectStubs: true
}]
}, {
anchor: "BottomCenter",
uuid: node_id + ".flowchartBottomCenter"
});
gadget.props.jsplumb_instance.addEndpoint(dom_element_id, {
isTarget: true,
maxConnections: -1
}, {
anchor: "LeftMiddle",
uuid: node_id + ".flowChartLeftMiddle"
});
gadget.notifyDataChanged();
}
function waitForDrop(gadget) {
var callback;
function canceller() {
if (callback !== undefined) {
gadget.props.main.removeEventListener("drop", callback, false);
}
}
function resolver(ignore, reject) {
callback = function (evt) {
try {
var class_name;
var offset = $(gadget.props.main).offset();
var relative_position = convertToRelativePosition(gadget, evt.clientX - offset.left + "px", evt.clientY - offset.top + "px");
try {
// html5 compliant browser
class_name = JSON.parse(evt.dataTransfer.getData("application/json"));
} catch (error_from_drop) {
// internet explorer
class_name = JSON.parse(evt.dataTransfer.getData("text"));
}
addNode(gadget, generateNodeId(gadget, {
_class: class_name
}), {
coordinate: {
left: relative_position[0],
top: relative_position[1]
},
_class: class_name
});
} catch (e) {
reject(e);
}
};
gadget.props.main.addEventListener("drop", callback, false);
}
return RSVP.all([ // loopEventListener adds an event listener that will prevent default for
// dragover
loopEventListener(gadget.props.main, "dragover", false, function () {
return undefined;
}), new RSVP.Promise(resolver, canceller)
]);
}
gadget_klass.ready(function (g) {
g.props = {};
}).ready(function (g) {
return g.getElement().push(function (element) {
g.props.element = element;
});
}).ready(function (g) {
g.props.node_id_to_dom_element_id = {};
g.props.zoom_level = 1;
g.props.style_attr_list = ["width", "height", "padding-top", "line-height"];
g.getElement().then(function (element) {
g.props.element = element;
});
}).declareAcquiredMethod("notifyDataChanged", "notifyDataChanged")
.declareMethod("render", function (data) {
var gadget = this;
this.props.data = {};
if (data.key) {
// Gadget embedded in ERP5
this.props.erp5_key = data.key;
data = data.value;
}
this.props.main = this.props.element.querySelector(".graph_container");
/*
$(this.props.main).resizable({
resize : function (event, ui) {
jsplumb_instance.repaint(ui.helper);
} }
addNode(gadget, generateNodeId(gadget, {
_class: class_name
}), {
coordinate: {
left: relative_position[0],
top: relative_position[1]
},
_class: class_name
});
} catch (e) {
reject(e);
}
};
gadget.props.main.addEventListener("drop", callback, false);
}
return new RSVP.all([ // loopEventListener adds an event listener that will prevent default for
// dragover
loopEventListener(gadget.props.main, "dragover", false, function() {
return undefined;
}), RSVP.Promise(resolver, canceller)
]);
}
gadget_klass.ready(function (g) {
g.props = {};
})
.ready(function (g) {
return g.getElement().push(function (element) {
g.props.element = element;
});
})
.ready(function(g) {
g.props.node_id_to_dom_element_id = {};
g.props.zoom_level = 1;
g.props.style_attr_list = ["width", "height", "padding-top", "line-height"];
g.getElement().then(function(element) {
g.props.element = element;
});
})
.declareAcquiredMethod("notifyDataChanged", "notifyDataChanged")
.declareMethod("render", function(data) {
var gadget = this, jsplumb_instance;
this.props.data = {};
if (data.key) {
// Gadget embedded in ERP5
this.props.erp5_key = data.key;
data = data.value;
}
this.props.main = this.props.element.querySelector(".graph_container");
/*
$(this.props.main).resizable({
resize : function(event, ui) {
jsplumb_instance.repaint(ui.helper);
}
});
*/
if (data) {
this.props.data = JSON.parse(data);
// XXX how to make queue ??
return layoutGraph(this.props.data.graph).then(function(graph_data) {
gadget.props.data.graph = graph_data;
// load the data
$.each(gadget.props.data.graph.node, function(key, value) {
addNode(gadget, key, value);
});
$.each(gadget.props.data.graph.edge, function(key, value) {
addEdge(gadget, key, value);
}); });
}); */
} if (data) {
}) this.props.data = JSON.parse(data);
.declareMethod("getContent", function() {
var ret = {}; // XXX how to make queue ??
if (this.props.erp5_key) { return layoutGraph(this.props.data.graph).then(function (graph_data) {
// ERP5 gadget.props.data.graph = graph_data;
ret[this.props.erp5_key] = JSON.stringify(this.props.data); // load the data
return ret; $.each(gadget.props.data.graph.node, function (key, value) {
} addNode(gadget, key, value);
return JSON.stringify(this.props.data); });
}) $.each(gadget.props.data.graph.edge, function (key, value) {
.declareService(function() { addEdge(gadget, key, value);
var gadget = this, jsplumb_instance; });
this.props.main = this.props.element.querySelector(".graph_container"); });
this.props.jsplumb_instance = jsplumb_instance = jsPlumb.getInstance(); }
if (this.props.data) { })
// load the data .declareMethod("getContent", function () {
$.each(this.props.data.graph.node, function(key, value) { var ret = {};
addNode(gadget, key, value); if (this.props.erp5_key) {
}); // ERP5
$.each(this.props.data.graph.edge, function(key, value) { ret[this.props.erp5_key] = JSON.stringify(this.props.data);
addEdge(gadget, key, value); return ret;
}); }
} return JSON.stringify(this.props.data);
jsplumb_instance.setRenderMode(jsplumb_instance.SVG); })
jsplumb_instance.importDefaults({ .onEvent("dblclick", function (evt) {
HoverPaintStyle: { var node = evt.target;
strokeStyle: "#1e8151", if (
lineWidth: 2 (node.nodeType === Node.ELEMENT_NODE) &&
}, (node.tagName === "DIV") && node.classList.contains(["window"])
Endpoint: ["Dot", { ) {
radius: 2 return openNodeEditionDialog(this, node);
}], }
ConnectionOverlays: [ })
["Arrow", { .declareService(function () {
location: 1, var gadget = this;
id: "arrow", var jsplumb_instance;
length: 14, this.props.main = this.props.element.querySelector(".graph_container");
foldback: 0.8 this.props.jsplumb_instance = jsplumb_instance = jsPlumb.getInstance();
}] if (this.props.data) {
], // load the data
Container: this.props.main $.each(this.props.data.graph.node, function (key, value) {
}); addNode(gadget, key, value);
draggable(gadget); });
$.each(this.props.data.graph.edge, function (key, value) {
this.props.nodes_click_monitor = RSVP.Monitor(); addEdge(gadget, key, value);
return RSVP.all([waitForDrop(gadget), });
waitForConnection(gadget), }
waitForConnectionDetached(gadget), jsplumb_instance.setRenderMode(jsplumb_instance.SVG);
waitForConnectionClick(gadget), jsplumb_instance.importDefaults({
gadget.props.nodes_click_monitor HoverPaintStyle: {
]); strokeStyle: "#1e8151",
}); lineWidth: 2
},
})(RSVP, rJS, $, jsPlumb, Handlebars, loopEventListener, promiseEventListener, DOMParser, Springy); Endpoint: ["Dot", {
\ No newline at end of file radius: 2
}],
ConnectionOverlays: [
["Arrow", {
location: 1,
id: "arrow",
length: 14,
foldback: 0.8
}]
],
Container: this.props.main
});
draggable(gadget);
return RSVP.all([
waitForDrop(gadget),
waitForConnection(gadget),
waitForConnectionDetached(gadget),
waitForConnectionClick(gadget)
]);
});
}(RSVP, rJS, $, jsPlumb, Handlebars, loopEventListener, promiseEventListener, DOMParser, Springy));
\ No newline at end of file
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
<body> <body>
<div id="qunit"></div> <div id="qunit"></div>
<div id="qunit-fixture"></div> <div id="qunit-fixture">
<div id="test-element"/>
</div>
</body> </body>
</html> </html>
/*global window, document, rJS, JSON, QUnit, jQuery, RSVP, console, setTimeout /*jslint vars:true nomen:true */ /* these two options are for compatibility with jslint 2014-04-21 . We'll remove them once we switch to more recent jslint */
/*global window, document, rJS, JSON, QUnit, jQuery, RSVP, console, setTimeout */
*/ (function (rJS, JSON, QUnit, RSVP, $) {
(function(rJS, JSON, QUnit, RSVP, $) {
"use strict"; "use strict";
var start = QUnit.start, var start = QUnit.start;
stop = QUnit.stop, var stop = QUnit.stop;
test = QUnit.test, var test = QUnit.test;
equal = QUnit.equal, var equal = QUnit.equal;
ok = QUnit.ok, var ok = QUnit.ok;
error_handler = function(e) { var error_handler = function (e) {
window.console.error(e); window.console.error(e);
ok(false, e); ok(false, e);
}, };
sample_class_definition = { var sample_class_definition = {
edge: { edge: {
description: "Base definition for edge", description: "Base definition for edge",
properties: { properties: {
_class: { "_class": {
type: "string" type: "string"
}, },
destination: { destination: {
...@@ -25,7 +24,7 @@ ...@@ -25,7 +24,7 @@
name: { name: {
type: "string" type: "string"
}, },
required: [ "name", "_class", "source", "destination" ], required: ["name", "_class", "source", "destination"],
source: { source: {
type: "string" type: "string"
} }
...@@ -33,35 +32,35 @@ ...@@ -33,35 +32,35 @@
type: "object" type: "object"
}, },
"Example.Edge": { "Example.Edge": {
_class: "edge", "_class": "edge",
allOf: [ { allOf: [{
$ref: "#/edge" "$ref": "#/edge"
}, { }, {
properties: { properties: {
color: { color: {
"enum": [ "red", "green", "blue" ] "enum": ["red", "green", "blue"]
} }
} }
} ], }],
description: "An example edge with a color property" description: "An example edge with a color property"
}, },
"Example.Node": { "Example.Node": {
_class: "node", "_class": "node",
allOf: [ { allOf: [{
$ref: "#/node" "$ref": "#/node"
}, { }, {
properties: { properties: {
shape: { shape: {
type: "string" type: "string"
} }
} }
} ], }],
description: "An example node with a shape property" description: "An example node with a shape property"
}, },
node: { node: {
description: "Base definition for node", description: "Base definition for node",
properties: { properties: {
_class: { "_class": {
type: "string" type: "string"
}, },
coordinate: { coordinate: {
...@@ -74,14 +73,15 @@ ...@@ -74,14 +73,15 @@
name: { name: {
type: "string" type: "string"
}, },
required: [ "name", "_class" ] required: ["name", "_class"]
}, },
type: "object" type: "object"
} }
}, sample_graph = { };
var sample_graph = {
edge: { edge: {
edge1: { edge1: {
_class: "Example.Edge", "_class": "Example.Edge",
source: "N1", source: "N1",
destination: "N2", destination: "N2",
color: "blue" color: "blue"
...@@ -89,7 +89,7 @@ ...@@ -89,7 +89,7 @@
}, },
node: { node: {
N1: { N1: {
_class: "Example.Node", "_class": "Example.Node",
name: "Node 1", name: "Node 1",
coordinate: { coordinate: {
top: 0, top: 0,
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
shape: "square" shape: "square"
}, },
N2: { N2: {
_class: "Example.Node", "_class": "Example.Node",
name: "Node 2", name: "Node 2",
shape: "circle", shape: "circle",
coordinate: { coordinate: {
...@@ -107,127 +107,161 @@ ...@@ -107,127 +107,161 @@
} }
} }
} }
}, sample_graph_not_connected = { };
var sample_graph_no_node_coodinate = {
edge: {
edge1: {
"_class": "Example.Edge",
source: "N1",
destination: "N2",
color: "blue"
}
},
node: {
N1: {
"_class": "Example.Node",
name: "Node 1",
shape: "square"
},
N2: {
"_class": "Example.Node",
name: "Node 2",
shape: "circle"
}
}
};
var sample_graph_not_connected = {
edge: {}, edge: {},
node: { node: {
N1: { N1: {
_class: "Example.Node", "_class": "Example.Node",
name: "Node 1", name: "Node 1",
shape: "square" shape: "square"
}, },
N2: { N2: {
_class: "Example.Node", "_class": "Example.Node",
name: "Node 2", name: "Node 2",
shape: "circle" shape: "circle"
} }
} }
}, sample_data_graph = JSON.stringify({ };
var sample_data_graph = JSON.stringify({
class_definition: sample_class_definition, class_definition: sample_class_definition,
graph: sample_graph graph: sample_graph
}), sample_data_graph_not_connected = JSON.stringify({ });
var sample_data_graph_no_node_coordinate = JSON.stringify({
class_definition: sample_class_definition,
graph: sample_graph_no_node_coodinate
});
var sample_data_graph_not_connected = JSON.stringify({
class_definition: sample_class_definition, class_definition: sample_class_definition,
graph: sample_graph_not_connected graph: sample_graph_not_connected
}), sample_data_empty_graph = JSON.stringify({ });
var sample_data_empty_graph = JSON.stringify({
class_definition: sample_class_definition, class_definition: sample_class_definition,
graph: { graph: {
node: {}, node: {},
edge: {} edge: {}
} }
}); });
QUnit.config.testTimeout = 60000; QUnit.config.testTimeout = 60000;
rJS(window).ready(function(g) { rJS(window).ready(function (g) {
test("Sample graph can be loaded and output is equal to input", function() { test("Sample graph can be loaded and output is equal to input", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
return jsplumb_gadget.render(sample_data_graph); return jsplumb_gadget.render(sample_data_graph);
}).then(function() { }).then(function () {
return jsplumb_gadget.getContent(); return jsplumb_gadget.getContent();
}).then(function(content) { }).then(function (content) {
equal(content, sample_data_graph); equal(content, sample_data_graph);
}).fail(error_handler).always(start); }).fail(error_handler).always(start);
}); });
test("New node can be drag & dropped", function() { test("New node can be drag & dropped", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
function runTest() { function runTest() {
// XXX here I used getContent to have a promise, but there must be a // XXX here I used getContent to have a promise, but there must be a
// more elegant way. // more elegant way.
return jsplumb_gadget.getContent().then(function() { return jsplumb_gadget.getContent().then(function () {
// fake a drop event // fake a drop event
var e = new window.Event("drop"); var e = new window.Event("drop");
e.dataTransfer = { e.dataTransfer = {
getData: function(type) { getData: function (type) {
// make sure we are called properly // make sure we are called properly
equal("application/json", type, "The drag&dropped element must have data type application/json"); equal("application/json", type, "The drag&dropped element must have data type application/json");
return JSON.stringify("Example.Node"); return JSON.stringify("Example.Node");
} }
}; };
jsplumb_gadget.props.main.dispatchEvent(e); jsplumb_gadget.props.main.dispatchEvent(e);
}).then(function() { }).then(function () {
return jsplumb_gadget.getContent(); return jsplumb_gadget.getContent();
}).then(function(content) { }).then(function (content) {
var node, graph = JSON.parse(content).graph; var node;
var graph = JSON.parse(content).graph;
equal(1, Object.keys(graph.node).length, "There is one new node class"); equal(1, Object.keys(graph.node).length, "There is one new node class");
node = graph.node[Object.keys(graph.node)[0]]; node = graph.node[Object.keys(graph.node)[0]];
equal("Example.Node", node._class, "Node class is set to Example.?ode"); equal("Example.Node", node._class, "Node class is set to Example.Node");
}); });
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_empty_graph); jsplumb_gadget.render(sample_data_empty_graph);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("Node can be dragged", function() { test("Node can be dragged", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
function runTest() { function runTest() {
return jsplumb_gadget.getContent().then(function() { return jsplumb_gadget.getContent().then(function () {
// 100 and 60 are about 10% of the .graph_container div ( set by css, so this // 100 and 60 are about 10% of the .graph_container div ( set by css, so this
// might change ) // might change )
$("div[title='Node 1']").simulate("drag", { $("div[title='Node 1']").simulate("drag", {
dx: 100, dx: 100,
dy: 60 dy: 60
}); });
}).then(function() { }).then(function () {
return jsplumb_gadget.getContent(); return jsplumb_gadget.getContent();
}).then(function(content) { }).then(function (content) {
var graph = JSON.parse(content).graph, node_coordinate = graph.node.N1.coordinate; var graph = JSON.parse(content).graph;
var node_coordinate = graph.node.N1.coordinate;
// Since original coordinates where 0,0 we are now about 0.1,0.1 // Since original coordinates where 0,0 we are now about 0.1,0.1
// as we moved 10% // as we moved 10%
ok(node_coordinate.top - .1 < .1, "Top is ok"); ok(node_coordinate.top - 0.1 < 0.1, "Top is ok");
ok(node_coordinate.left - .1 < .1, "Left is ok"); ok(node_coordinate.left - 0.1 < 0.1, "Left is ok");
}); });
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph); jsplumb_gadget.render(sample_data_graph);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("Node properties can be edited", function() { test("Node properties can be edited", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
function runTest() { function runTest() {
return jsplumb_gadget.getContent().then(function() { return jsplumb_gadget.getContent().then(function () {
// click on a node to see display the popup // click on a node to see display the popup
$("div[title='Node 1']").simulate("dblclick"); $("div[title='Node 1']").simulate("dblclick");
// Promises that handle the dialog actions are not available // Promises that handle the dialog actions are not available
// immediately after clicking. // immediately after clicking.
var promise = RSVP.Promise(function(resolve) { var promise = new RSVP.Promise(function (resolve) {
var fillDialog = function() { function fillDialog() {
if (!jsplumb_gadget.props.dialog_promise) { if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later. // Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait // XXX this condition is actually incorrect. We need to wait
// for the event listener to have been registered for the // for the event listener to have been registered for the
// dialog buttons. This setTimeout is good enough for now. // dialog buttons. This setTimeout is good enough for now.
return setTimeout(fillDialog, 1e3); setTimeout(fillDialog, 1e3);
return;
} }
// check displayed values // check displayed values
equal($("input[name='id']").val(), "N1"); equal($("input[name='id']").val(), "N1");
...@@ -241,12 +275,13 @@ ...@@ -241,12 +275,13 @@
// resolve our test promise once the dialog handling promise is // resolve our test promise once the dialog handling promise is
// finished. // finished.
jsplumb_gadget.props.dialog_promise.then(resolve); jsplumb_gadget.props.dialog_promise.then(resolve);
}; }
fillDialog(); fillDialog();
}); });
return promise.then(function() { return promise.then(function () {
return jsplumb_gadget.getContent().then(function(content) { return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph, node = graph.node.N1; var graph = JSON.parse(content).graph;
var node = graph.node.N1;
equal("Modified Name", node.name, "Data is modified"); equal("Modified Name", node.name, "Data is modified");
equal("Modified Name", $("div#" + jsplumb_gadget.props.node_id_to_dom_element_id.N1).text(), "DOM is modified"); equal("Modified Name", $("div#" + jsplumb_gadget.props.node_id_to_dom_element_id.N1).text(), "DOM is modified");
equal(1, $("div[title='Modified Name']").length, "DOM title attribute is modified"); equal(1, $("div[title='Modified Name']").length, "DOM title attribute is modified");
...@@ -255,27 +290,29 @@ ...@@ -255,27 +290,29 @@
}); });
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph); jsplumb_gadget.render(sample_data_graph);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("Node can be connected", function() { test("Node can be connected", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
function runTest() { function runTest() {
return jsplumb_gadget.getContent().then(function(content) { return jsplumb_gadget.getContent().then(function (content) {
var node1 = jsplumb_gadget.props.main.querySelector("div[title='Node 1']"), node2 = jsplumb_gadget.props.main.querySelector("div[title='Node 2']"); var node1 = jsplumb_gadget.props.main.querySelector("div[title='Node 1']");
var node2 = jsplumb_gadget.props.main.querySelector("div[title='Node 2']");
equal(0, Object.keys(JSON.parse(content).graph.edge).length, "There are no edge at the beginning"); equal(0, Object.keys(JSON.parse(content).graph.edge).length, "There are no edge at the beginning");
jsplumb_gadget.props.jsplumb_instance.connect({ jsplumb_gadget.props.jsplumb_instance.connect({
source: node1.id, source: node1.id,
target: node2.id target: node2.id
}); });
}).then(function() { }).then(function () {
return jsplumb_gadget.getContent(); return jsplumb_gadget.getContent();
}).then(function(content) { }).then(function (content) {
var edge, graph = JSON.parse(content).graph; var edge;
var graph = JSON.parse(content).graph;
equal(2, Object.keys(graph.node).length, "We still have 2 nodes"); equal(2, Object.keys(graph.node).length, "We still have 2 nodes");
equal(1, Object.keys(graph.edge).length, "We have 1 edge"); equal(1, Object.keys(graph.edge).length, "We have 1 edge");
edge = graph.edge[Object.keys(graph.edge)[0]]; edge = graph.edge[Object.keys(graph.edge)[0]];
...@@ -286,42 +323,43 @@ ...@@ -286,42 +323,43 @@
}); });
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph_not_connected); jsplumb_gadget.render(sample_data_graph_not_connected);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("Node can be deleted", function() { test("Node can be deleted", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
function runTest() { function runTest() {
return jsplumb_gadget.getContent().then(function() { return jsplumb_gadget.getContent().then(function () {
equal(1, $("div[title='Node 1']").length, "node 1 is visible"); equal(1, $("div[title='Node 1']").length, "node 1 is visible");
equal(1, $("._jsPlumb_connector").length, "there is 1 connection"); equal(1, $("._jsPlumb_connector").length, "there is 1 connection");
// click on node 1 to see display the popup // click on node 1 to see display the popup
$("div[title='Node 1']").simulate("dblclick"); $("div[title='Node 1']").simulate("dblclick");
// Promises that handle the dialog actions are not available // Promises that handle the dialog actions are not available
// immediately after clicking. // immediately after clicking.
var promise = RSVP.Promise(function(resolve) { var promise = new RSVP.Promise(function (resolve) {
var waitForDialogAndDelete = function() { function waitForDialogAndDelete() {
if (!jsplumb_gadget.props.dialog_promise) { if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later. // Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait // XXX this condition is actually incorrect. We need to wait
// for the event listener to have been registered for the // for the event listener to have been registered for the
// dialog buttons. This setTimeout is good enough for now. // dialog buttons. This setTimeout is good enough for now.
return setTimeout(waitForDialogAndDelete, 1e3); setTimeout(waitForDialogAndDelete, 1e3);
return;
} }
equal(1, $("input[value='Delete']").length, "There should be one delete button"); equal(1, $("input[value='Delete']").length, "There should be one delete button");
$("input[value='Delete']").click(); $("input[value='Delete']").click();
// resolve our test promise once the dialog handling promise is // resolve our test promise once the dialog handling promise is
// finished. // finished.
jsplumb_gadget.props.dialog_promise.then(resolve); jsplumb_gadget.props.dialog_promise.then(resolve);
}; }
waitForDialogAndDelete(); waitForDialogAndDelete();
}); });
return promise.then(function() { return promise.then(function () {
return jsplumb_gadget.getContent().then(function(content) { return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph; var graph = JSON.parse(content).graph;
equal(1, Object.keys(graph.node).length, "node is removed from data"); equal(1, Object.keys(graph.node).length, "node is removed from data");
equal(0, Object.keys(graph.edge).length, "edge referencing this node is also removed"); equal(0, Object.keys(graph.edge).length, "edge referencing this node is also removed");
...@@ -332,29 +370,30 @@ ...@@ -332,29 +370,30 @@
}); });
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph); jsplumb_gadget.render(sample_data_graph);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("Node id can be changed (connections are updated and node can be edited afterwards)", function() { test("Node id can be changed (connections are updated and node can be edited afterwards)", function () {
var jsplumb_gadget; var jsplumb_gadget;
stop(); stop();
function runTest() { function runTest() {
return jsplumb_gadget.getContent().then(function() { return jsplumb_gadget.getContent().then(function () {
// click on a node to see display the popup // click on a node to see display the popup
$("div[title='Node 1']").simulate("dblclick"); $("div[title='Node 1']").simulate("dblclick");
// Promises that handle the dialog actions are not available // Promises that handle the dialog actions are not available
// immediately after clicking. // immediately after clicking.
var promise = RSVP.Promise(function(resolve) { var promise = new RSVP.Promise(function (resolve) {
var fillDialog = function() { function fillDialog() {
if (!jsplumb_gadget.props.dialog_promise) { if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later. // Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait // XXX this condition is actually incorrect. We need to wait
// for the event listener to have been registered for the // for the event listener to have been registered for the
// dialog buttons. This setTimeout is good enough for now. // dialog buttons. This setTimeout is good enough for now.
return setTimeout(fillDialog, 1e3); setTimeout(fillDialog, 1e3);
return;
} }
equal($("input[name='id']").val(), "N1"); equal($("input[name='id']").val(), "N1");
// change the id // change the id
...@@ -364,11 +403,11 @@ ...@@ -364,11 +403,11 @@
// resolve our test promise once the dialog handling promise is // resolve our test promise once the dialog handling promise is
// finished. // finished.
jsplumb_gadget.props.dialog_promise.then(resolve); jsplumb_gadget.props.dialog_promise.then(resolve);
}; }
fillDialog(); fillDialog();
}); });
return promise.then(function() { return promise.then(function () {
return jsplumb_gadget.getContent().then(function(content) { return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph; var graph = JSON.parse(content).graph;
equal(2, Object.keys(graph.node).length, "We still have two nodes"); equal(2, Object.keys(graph.node).length, "We still have two nodes");
ok(graph.node.N1b !== undefined, "Node Id changed"); ok(graph.node.N1b !== undefined, "Node Id changed");
...@@ -379,52 +418,55 @@ ...@@ -379,52 +418,55 @@
}); });
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_graph); jsplumb_gadget.render(sample_data_graph);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("New node can be edited", function() { test("New node can be edited", function () {
var jsplumb_gadget, node_id; var jsplumb_gadget;
var node_id;
stop(); stop();
function runTest() { function runTest() {
// XXX here I used getContent to have a promise, but there must be a // XXX here I used getContent to have a promise, but there must be a
// more elegant way. // more elegant way.
return jsplumb_gadget.getContent().then(function() { return jsplumb_gadget.getContent().then(function () {
// fake a drop event // fake a drop event
var e = new window.Event("drop"); var e = new window.Event("drop");
e.dataTransfer = { e.dataTransfer = {
getData: function(type) { getData: function (type) {
// make sure we are called properly // make sure we are called properly
equal("application/json", type, "The drag&dropped element must have data type application/json"); equal("application/json", type, "The drag&dropped element must have data type application/json");
return JSON.stringify("Example.Node"); return JSON.stringify("Example.Node");
} }
}; };
jsplumb_gadget.props.main.dispatchEvent(e); jsplumb_gadget.props.main.dispatchEvent(e);
}).then(function() { }).then(function () {
return jsplumb_gadget.getContent(); return jsplumb_gadget.getContent();
}).then(function(content) { }).then(function (content) {
var node, graph = JSON.parse(content).graph; var node;
var graph = JSON.parse(content).graph;
equal(1, Object.keys(graph.node).length); equal(1, Object.keys(graph.node).length);
node_id = Object.keys(graph.node)[0]; node_id = Object.keys(graph.node)[0];
node = graph.node[node_id]; node = graph.node[node_id];
equal("Example.Node", node._class); equal("Example.Node", node._class);
}).then(function() { }).then(function () {
// click the new node to see display the popup // click the new node to see display the popup
// XXX at the moment nodes have class window // XXX at the moment nodes have class window
equal(1, $("div.window").length, "We have a new node"); equal(1, $("div.window").length, "We have a new node");
$("div.window").simulate("dblclick"); $("div.window").simulate("dblclick");
// Promises that handle the dialog actions are not available // Promises that handle the dialog actions are not available
// immediately after clicking. // immediately after clicking.
var promise = RSVP.Promise(function(resolve) { var promise = new RSVP.Promise(function (resolve) {
var fillDialog = function() { function fillDialog() {
if (!jsplumb_gadget.props.dialog_promise) { if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later. // Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait // XXX this condition is actually incorrect. We need to wait
// for the event listener to have been registered for the // for the event listener to have been registered for the
// dialog buttons. This setTimeout is good enough for now. // dialog buttons. This setTimeout is good enough for now.
return setTimeout(fillDialog, 1e3); setTimeout(fillDialog, 1e3);
return;
} }
// check displayed values // check displayed values
equal($("input[name='id']").val(), node_id); equal($("input[name='id']").val(), node_id);
...@@ -438,12 +480,13 @@ ...@@ -438,12 +480,13 @@
// resolve our test promise once the dialog handling promise is // resolve our test promise once the dialog handling promise is
// finished. // finished.
jsplumb_gadget.props.dialog_promise.then(resolve); jsplumb_gadget.props.dialog_promise.then(resolve);
}; }
fillDialog(); fillDialog();
}); });
return promise.then(function() { return promise.then(function () {
return jsplumb_gadget.getContent().then(function(content) { return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph, node = graph.node[node_id]; var graph = JSON.parse(content).graph;
var node = graph.node[node_id];
equal("Modified Name", node.name, "Data is modified"); equal("Modified Name", node.name, "Data is modified");
equal("Modified Name", $("div.window").text(), "DOM is modified"); equal("Modified Name", $("div.window").text(), "DOM is modified");
}); });
...@@ -451,63 +494,66 @@ ...@@ -451,63 +494,66 @@
}); });
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_empty_graph); jsplumb_gadget.render(sample_data_empty_graph);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("New node can be deleted", function() { test("New node can be deleted", function () {
var jsplumb_gadget, node_id; var jsplumb_gadget;
var node_id;
stop(); stop();
function runTest() { function runTest() {
// XXX here I used getContent to have a promise, but there must be a // XXX here I used getContent to have a promise, but there must be a
// more elegant way. // more elegant way.
return jsplumb_gadget.getContent().then(function() { return jsplumb_gadget.getContent().then(function () {
// fake a drop event // fake a drop event
var e = new window.Event("drop"); var e = new window.Event("drop");
e.dataTransfer = { e.dataTransfer = {
getData: function(type) { getData: function (type) {
// make sure we are called properly // make sure we are called properly
equal("application/json", type, "The drag&dropped element must have data type application/json"); equal("application/json", type, "The drag&dropped element must have data type application/json");
return JSON.stringify("Example.Node"); return JSON.stringify("Example.Node");
} }
}; };
jsplumb_gadget.props.main.dispatchEvent(e); jsplumb_gadget.props.main.dispatchEvent(e);
}).then(function() { }).then(function () {
return jsplumb_gadget.getContent(); return jsplumb_gadget.getContent();
}).then(function(content) { }).then(function (content) {
var node, graph = JSON.parse(content).graph; var node;
var graph = JSON.parse(content).graph;
equal(1, Object.keys(graph.node).length); equal(1, Object.keys(graph.node).length);
node_id = Object.keys(graph.node)[0]; node_id = Object.keys(graph.node)[0];
node = graph.node[node_id]; node = graph.node[node_id];
equal("Example.Node", node._class); equal("Example.Node", node._class);
}).then(function() { }).then(function () {
// click the new node to see display the popup // click the new node to see display the popup
// XXX at the moment nodes have class window // XXX at the moment nodes have class window
equal(1, $("div.window").length, "We have a new node"); equal(1, $("div.window").length, "We have a new node");
$("div.window").simulate("dblclick"); $("div.window").simulate("dblclick");
// Promises that handle the dialog actions are not available // Promises that handle the dialog actions are not available
// immediately after clicking. // immediately after clicking.
var promise = RSVP.Promise(function(resolve) { var promise = new RSVP.Promise(function (resolve) {
var waitForDialogAndDelete = function() { function waitForDialogAndDelete() {
if (!jsplumb_gadget.props.dialog_promise) { if (!jsplumb_gadget.props.dialog_promise) {
// Dialog not ready. Let's retry later. // Dialog not ready. Let's retry later.
// XXX this condition is actually incorrect. We need to wait // XXX this condition is actually incorrect. We need to wait
// for the event listener to have been registered for the // for the event listener to have been registered for the
// dialog buttons. This setTimeout is good enough for now. // dialog buttons. This setTimeout is good enough for now.
return setTimeout(waitForDialogAndDelete, 1e3); setTimeout(waitForDialogAndDelete, 1e3);
return;
} }
equal(1, $("input[value='Delete']").length, "There should be one delete button"); equal(1, $("input[value='Delete']").length, "There should be one delete button");
$("input[value='Delete']").click(); $("input[value='Delete']").click();
// resolve our test promise once the dialog handling promise is // resolve our test promise once the dialog handling promise is
// finished. // finished.
jsplumb_gadget.props.dialog_promise.then(resolve); jsplumb_gadget.props.dialog_promise.then(resolve);
}; }
waitForDialogAndDelete(); waitForDialogAndDelete();
}); });
return promise.then(function() { return promise.then(function () {
return jsplumb_gadget.getContent().then(function(content) { return jsplumb_gadget.getContent().then(function (content) {
var graph = JSON.parse(content).graph; var graph = JSON.parse(content).graph;
equal(0, Object.keys(graph.node).length, "node is removed from data"); equal(0, Object.keys(graph.node).length, "node is removed from data");
equal(0, $("div.window").length, "DOM is modified"); equal(0, $("div.window").length, "DOM is modified");
...@@ -516,11 +562,31 @@ ...@@ -516,11 +562,31 @@
}); });
} }
g.declareGadget("./index.html", { g.declareGadget("./index.html", {
element: document.querySelector("#qunit-fixture") element: document.querySelector("#test-element")
}).then(function(new_gadget) { }).then(function (new_gadget) {
jsplumb_gadget = new_gadget; jsplumb_gadget = new_gadget;
jsplumb_gadget.render(sample_data_empty_graph); jsplumb_gadget.render(sample_data_empty_graph);
}).then(runTest).fail(error_handler).always(start); }).then(runTest).fail(error_handler).always(start);
}); });
test("Graph is automatically layout", function () {
var jsplumb_gadget;
stop();
g.declareGadget("./index.html", {
element: document.querySelector("#test-element")
}).then(function (new_gadget) {
jsplumb_gadget = new_gadget;
return jsplumb_gadget.render(sample_data_graph_no_node_coordinate);
}).then(function () {
return jsplumb_gadget.getContent();
}).then(function (content) {
/*jslint unparam: true */
$.each(JSON.parse(content).graph.node, function (ignore, node) {
ok(node.coordinate.top !== undefined, "Node have top coordinate");
ok((0 <= node.coordinate.top) && (node.coordinate.top <= 1), "Node top coordinate is between [0..1]");
ok(node.coordinate.left !== undefined, "Node have left coordinate");
ok((0 <= node.coordinate.left) && (node.coordinate.left <= 1), "Node left coordinate is between [0..1]");
});
}).fail(error_handler).always(start);
});
}); });
})(rJS, JSON, QUnit, RSVP, jQuery); }(rJS, JSON, QUnit, RSVP, jQuery));
\ No newline at end of file \ No newline at end of file
...@@ -14,16 +14,16 @@ ...@@ -14,16 +14,16 @@
}).declareMethod("render", function(options) { }).declareMethod("render", function(options) {
var select = this.element.getElementsByTagName("select")[0], i, template, tmp = ""; var select = this.element.getElementsByTagName("select")[0], i, template, tmp = "";
select.setAttribute("name", options.key); select.setAttribute("name", options.key);
for (i = 0; i < options.property_definition.enum.length; i += 1) { for (i = 0; i < options.property_definition['enum'].length; i += 1) {
if (options.property_definition.enum[i] === options.value) { if (options.property_definition['enum'][i] === options.value) {
template = selected_option_template; template = selected_option_template;
} else { } else {
template = option_template; template = option_template;
} }
// XXX value and text are always same in json schema // XXX value and text are always same in json schema
tmp += template({ tmp += template({
value: options.property_definition.enum[i], value: options.property_definition['enum'][i],
text: options.property_definition.enum[i] text: options.property_definition['enum'][i]
}); });
} }
select.innerHTML += tmp; select.innerHTML += tmp;
......
erp5_trade
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Zuite" module="Products.Zelenium.zuite"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>graph_editor_zuite</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>content_type</string> </key>
<value> <string>text/html</string> </value>
</item>
<item>
<key> <string>expand</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>testQunit</string> </value>
</item>
<item>
<key> <string>output_encoding</string> </key>
<value> <string>utf-8</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <unicode>Graph Editor Qunit Test</unicode> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<html>
<head><title>Graph Editor Qunit Test</title></head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="4">
Run existing qunit test in zelenium framework, to easily integrate it in current test suite.
</td></tr>
</thead><tbody>
<tr>
<td>open</td>
<td tal:content="string:${context/portal_url}/dream_graph_editor/jsplumb/test.html"></td>
<td></td>
</tr>
<tr>
<td>waitForTextPresent</td>
<td>Tests completed in </td>
<td>30000</td>
</tr>
<tr>
<td>assertText</td>
<td>css=#qunit-testresult span.failed</td>
<td>0</td>
</tr>
</tbody></table>
</body>
</html>
\ No newline at end of file
##############################################################################
#
# Copyright (c) 2017 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import unittest
from Products.ERP5Type.tests.ERP5TypeFunctionalTestCase import ERP5TypeFunctionalTestCase
class TestGraphEditor(ERP5TypeFunctionalTestCase):
run_only = "graph_editor_zuite"
def getBusinessTemplateList(self):
return (
'erp5_graph_editor',
'erp5_graph_editor_ui_test',
'erp5_ui_test_core',
)
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestGraphEditor))
return suite
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Test Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>testFunctionalGraphEditor</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>test.erp5.testFunctionalGraphEditor</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Test Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
</ZopeData>
erp5_graph_editor
erp5_ui_test_core
\ No newline at end of file
portal_tests/graph_editor_zuite
portal_tests/graph_editor_zuite/**
\ No newline at end of file
test.erp5.testFunctionalGraphEditor
\ No newline at end of file
erp5_full_text_mroonga_catalog
\ No newline at end of file
erp5_graph_editor_ui_test
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment