77876c9dc523f675f0b8f7607f2c56a40c319974
[odoo/odoo.git] / addons / web_diagram / static / src / js / diagram.js
1 /*---------------------------------------------------------
2  * OpenERP diagram library
3  *---------------------------------------------------------*/
4
5 openerp.web_diagram = function (instance) {
6 var QWeb = instance.web.qweb,
7       _t = instance.web._t,
8      _lt = instance.web._lt;
9 instance.web.views.add('diagram', 'instance.web.DiagramView');
10 instance.web.DiagramView = instance.web.View.extend({
11     display_name: _lt('Diagram'),
12     view_type: 'diagram',
13     searchable: false,
14     init: function(parent, dataset, view_id, options) {
15         var self = this;
16         this._super(parent);
17         this.set_default_options(options);
18         this.view_manager = parent;
19         this.dataset = dataset;
20         this.model = this.dataset.model;
21         this.view_id = view_id;
22         this.domain = this.dataset._domain || [];
23         this.context = {};
24         this.ids = this.dataset.ids;
25         this.on('pager_action_executed', self, self.pager_action_trigger);
26     },
27
28     view_loading: function(r) {
29         return this.load_diagram(r);
30     },
31
32     toTitleCase: function(str) {
33         return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
34     },
35
36     load_diagram: function(result) {
37         var self = this;
38         if(this.ids && this.ids.length) {
39             this.id = this.ids[self.dataset.index || 0];
40         }
41
42         this.fields_view = result,
43         this.view_id = this.fields_view.view_id,
44         this.fields = this.fields_view.fields,
45         this.nodes = this.fields_view.arch.children[0],
46         this.connectors = this.fields_view.arch.children[1],
47         this.node = this.nodes.attrs.object,
48         this.connector = this.connectors.attrs.object;
49         this.labels = _.filter(this.fields_view.arch.children, function(label) {
50             return label.tag == "label";
51         });
52
53         this.$el.html(QWeb.render("DiagramView", {'widget': self}));
54         this.$el.addClass(this.fields_view.arch.attrs['class']);
55
56         _.each(self.labels,function(label){
57             html_label = '<p style="padding: 4px;">' + label.attrs.string + "</p>";
58             self.$el.find('.oe_diagram_header').append(html_label);
59         })
60
61         this.init_pager();
62
63         // New Node,Edge
64         this.$el.find('#new_node.oe_diagram_button_new').click(function(){self.add_node();});
65
66         if(this.id) {
67             self.get_diagram_info();
68         }
69         this.trigger('diagram_view_loaded', result);
70     },
71
72     get_diagram_info: function() {
73         var self = this;
74         var params = {
75             'id': this.id,
76             'model': this.model,
77             'node': this.node,
78             'connector': this.connector,
79             'bgcolor': this.nodes.attrs.bgcolor,
80             'shape': this.nodes.attrs.shape,
81             'src_node': this.connectors.attrs.source,
82             'des_node': this.connectors.attrs.destination,
83             'label': this.connectors.attrs.label || false,
84             'visible_nodes': [],
85             'invisible_nodes': [],
86             'node_fields': [],
87             'connectors': [],
88             'connectors_fields': []
89         };
90
91         _.each(this.nodes.children, function(child) {
92             if(child.attrs.invisible == '1')
93                 params['invisible_nodes'].push(child.attrs.name);
94             else {
95                 params['visible_nodes'].push(child.attrs.name);
96                 params['node_fields'].push(self.fields[child.attrs.name]['string']|| this.toTitleCase(child.attrs.name));
97             }
98         });
99
100         _.each(this.connectors.children, function(conn) {
101             params['connectors_fields'].push(self.fields[conn.attrs.name]['string']|| this.toTitleCase(conn.attrs.name));
102             params['connectors'].push(conn.attrs.name);
103         });
104         this.rpc(
105             '/web_diagram/diagram/get_diagram_info',params).done(function(result) {
106                 self.draw_diagram(result);
107             }
108         );
109     },
110
111     on_diagram_loaded: function(record) {
112         var id_record = record['id'];
113         if (id_record) {
114             this.id = id_record;
115             this.get_diagram_info();
116             this.do_push_state({id: id_record});
117         } else {
118             this.do_push_state({});
119         }
120     },
121
122     do_load_state: function(state, warm) {
123         if (state && state.id) {
124             if (!this.dataset.get_id_index(state.id)) {
125                 this.dataset.ids.push(state.id);
126             }
127             this.dataset.select_id(state.id);
128             if (warm) {
129                 this.do_show();
130             }
131         }
132      },
133
134     // Set-up the drawing elements of the diagram
135     draw_diagram: function(result) {
136         var self = this;
137         var res_nodes  = result['nodes'];
138         var res_edges  = result['conn'];
139         this.parent_field = result.parent_field;
140         this.$el.find('h3.oe_diagram_title').text(result.name);
141
142         var id_to_node = {};
143
144
145         var style = {
146             edge_color: "#A0A0A0",
147             edge_label_color: "#555",
148             edge_label_font_size: 10,
149             edge_width: 2,
150             edge_spacing: 100,
151             edge_loop_radius: 100,
152
153             node_label_color: "#333",
154             node_label_font_size: 12,
155             node_outline_color: "#333",
156             node_outline_width: 1,
157             node_selected_color: "#0097BE",
158             node_selected_width: 2,
159             node_size_x: 110,
160             node_size_y: 80,
161             connector_active_color: "#FFF",
162             connector_radius: 4,
163             
164             close_button_radius: 8,
165             close_button_color: "#333",
166             close_button_x_color: "#FFF",
167
168             gray: "#DCDCDC",
169             white: "#FFF",
170             
171             viewport_margin: 50
172         };
173
174         // remove previous diagram
175         var canvas = self.$el.find('div.oe_diagram_diagram')
176                              .empty().get(0);
177
178         var r  = new Raphael(canvas, '100%','100%');
179
180         var graph  = new CuteGraph(r,style,canvas.parentNode);
181
182         _.each(res_nodes, function(node) {
183             var n = new CuteNode(
184                 graph,
185                 node.x + 50,  //FIXME the +50 should be in the layout algorithm
186                 node.y + 50,
187                 CuteGraph.wordwrap(node.name, 14),
188                 node.shape === 'rectangle' ? 'rect' : 'circle',
189                 node.color === 'white' ? style.white : style.gray);
190
191             n.id = node.id;
192             id_to_node[node.id] = n;
193         });
194
195         _.each(res_edges, function(edge) {
196             var e =  new CuteEdge(
197                 graph,
198                 CuteGraph.wordwrap(edge.signal, 32),
199                 id_to_node[edge.s_id],
200                 id_to_node[edge.d_id] || id_to_node[edge.s_id]  );  //WORKAROUND
201             e.id = edge.id;
202         });
203
204         CuteNode.double_click_callback = function(cutenode){
205             self.edit_node(cutenode.id);
206         };
207         var i = 0;
208         CuteNode.destruction_callback = function(cutenode){
209             if(!confirm(_t("Deleting this node cannot be undone.\nIt will also delete all connected transitions.\n\nAre you sure ?"))){
210                 return $.Deferred().reject().promise();
211             }
212             return new instance.web.DataSet(self,self.node).unlink([cutenode.id]);
213         };
214         CuteEdge.double_click_callback = function(cuteedge){
215             self.edit_connector(cuteedge.id);
216         };
217
218         CuteEdge.creation_callback = function(node_start, node_end){
219             return {label:_t("")};
220         };
221         CuteEdge.new_edge_callback = function(cuteedge){
222             self.add_connector(cuteedge.get_start().id,
223                                cuteedge.get_end().id,
224                                cuteedge);
225         };
226         CuteEdge.destruction_callback = function(cuteedge){
227             if(!confirm(_t("Deleting this transition cannot be undone.\n\nAre you sure ?"))){
228                 return $.Deferred().reject().promise();
229             }
230             return new instance.web.DataSet(self,self.connector).unlink([cuteedge.id]);
231         };
232
233     },
234
235     // Creates a popup to edit the content of the node with id node_id
236     edit_node: function(node_id){
237         var self = this;
238         var title = _t('Activity');
239         var pop = new instance.web.form.FormOpenPopup(self);
240
241         pop.show_element(
242                 self.node,
243                 node_id,
244                 self.context || self.dataset.context,
245                 {
246                     title: _t("Open: ") + title
247                 }
248             );
249
250         pop.on('write_completed', self, function() {
251             self.dataset.read_index(_.keys(self.fields_view.fields)).then(self.on_diagram_loaded);
252             });
253         
254         var form_fields = [self.parent_field];
255         var form_controller = pop.view_form;
256
257        form_controller.on("load_record", self, function(){
258             _.each(form_fields, function(fld) {
259                 if (!(fld in form_controller.fields)) { return; }
260                 var field = form_controller.fields[fld];
261                 field.$input.prop('disabled', true);
262                 field.$drop_down.unbind();
263             });
264          });
265
266        
267     },
268
269     // Creates a popup to add a node to the diagram
270     add_node: function(){
271         var self = this;
272         var title = _t('Activity');
273         var pop = new instance.web.form.SelectCreatePopup(self);
274         pop.select_element(
275             self.node,
276             {
277                 title: _t("Create:") + title,
278                 initial_view: 'form',
279                 disable_multiple_selection: true
280             },
281             self.dataset.domain,
282             self.context || self.dataset.context
283         );
284         pop.on("elements_selected", self, function(element_ids) {
285             self.dataset.read_index(_.keys(self.fields_view.fields)).then(self.on_diagram_loaded);
286         });
287
288         var form_controller = pop.view_form;
289         var form_fields = [this.parent_field];
290
291         form_controller.on("load_record", self, function(){
292             _.each(form_fields, function(fld) {
293                 if (!(fld in form_controller.fields)) { return; }
294                 var field = form_controller.fields[fld];
295                 field.set_value(self.id);
296                 field.dirty = true;
297             });
298         });
299     },
300
301     // Creates a popup to edit the connector of id connector_id
302     edit_connector: function(connector_id){
303         var self = this;
304         var title = _t('Transition');
305         var pop = new instance.web.form.FormOpenPopup(self);
306         pop.show_element(
307             self.connector,
308             parseInt(connector_id,10),      //FIXME Isn't connector_id supposed to be an int ?
309             self.context || self.dataset.context,
310             {
311                 title: _t("Open: ") + title
312             }
313         );
314         pop.on('write_completed', self, function() {
315             self.dataset.read_index(_.keys(self.fields_view.fields)).then(self.on_diagram_loaded);
316         });
317     },
318
319     // Creates a popup to add a connector from node_source_id to node_dest_id.
320     // dummy_cuteedge if not null, will be removed form the graph after the popup is closed.
321     add_connector: function(node_source_id, node_dest_id, dummy_cuteedge){
322         var self = this;
323         var title = _t('Transition');
324         var pop = new instance.web.form.SelectCreatePopup(self);
325
326         pop.select_element(
327             self.connector,
328             {
329                 title: _t("Create:") + title,
330                 initial_view: 'form',
331                 disable_multiple_selection: true
332             },
333             this.dataset.domain,
334             this.context || this.dataset.context
335         );
336         pop.on("elements_selected", self, function(element_ids) {
337             self.dataset.read_index(_.keys(self.fields_view.fields)).then(self.on_diagram_loaded);
338         });
339         // We want to destroy the dummy edge after a creation cancel. This destroys it even if we save the changes.
340         // This is not a problem since the diagram is completely redrawn on saved changes.
341         pop.$el.parents('.modal').on('hidden.bs.modal', function (e){
342             if(dummy_cuteedge){
343                 dummy_cuteedge.remove();
344             }
345         });
346
347         var form_controller = pop.view_form;
348
349
350        form_controller.on("load_record", self, function(){
351             form_controller.fields[self.connectors.attrs.source].set_value(node_source_id);
352             form_controller.fields[self.connectors.attrs.source].dirty = true;
353             form_controller.fields[self.connectors.attrs.destination].set_value(node_dest_id);
354             form_controller.fields[self.connectors.attrs.destination].dirty = true;
355        });
356     },
357     
358     do_hide: function () {
359         if (this.$pager) {
360             this.$pager.hide();
361         }
362         this._super();
363     },
364     
365     init_pager: function() {
366         var self = this;
367         if (this.$pager)
368             this.$pager.remove();
369         
370         this.$pager = $(QWeb.render("DiagramView.pager", {'widget':self})).hide();
371         if (this.options.$pager) {
372             this.$pager.appendTo(this.options.$pager);
373         } else {
374             this.$el.find('.oe_diagram_pager').replaceWith(this.$pager);
375         }
376         this.$pager.on('click','a[data-pager-action]',function() {
377             var action = $(this).data('pager-action');
378             self.execute_pager_action(action);
379         });
380         this.do_update_pager();
381     },
382     
383     pager_action_trigger: function(){
384         var loaded = this.dataset.read_index(_.keys(this.fields_view.fields))
385                 .then(this.on_diagram_loaded);
386         this.do_update_pager();
387         return loaded;
388     },
389     
390     execute_pager_action: function(action) {
391             switch (action) {
392                 case 'first':
393                     this.dataset.index = 0;
394                     break;
395                 case 'previous':
396                     this.dataset.previous();
397                     break;
398                 case 'next':
399                     this.dataset.next();
400                     break;
401                 case 'last':
402                     this.dataset.index = this.dataset.ids.length - 1;
403                     break;
404             }
405             this.trigger('pager_action_executed');
406
407     },
408
409     do_update_pager: function(hide_index) {
410         this.$pager.toggle(this.dataset.ids.length > 1);
411         if (hide_index) {
412             $(".oe_diagram_pager_state", this.$pager).html("");
413         } else {
414             $(".oe_diagram_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
415         }
416     },
417
418     do_show: function() {
419         this.do_push_state({});
420         return $.when(this._super(), this.execute_pager_action('reload'));
421     }
422 });
423 };
424
425 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: