workflow.js 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049
  1. // Licensed to Cloudera, Inc. under one
  2. // or more contributor license agreements. See the NOTICE file
  3. // distributed with this work for additional information
  4. // regarding copyright ownership. Cloudera, Inc. licenses this file
  5. // to you under the Apache License, Version 2.0 (the
  6. // "License"); you may not use this file except in compliance
  7. // with the License. You may obtain a copy of the License at
  8. //
  9. // http://www.apache.org/licenses/LICENSE-2.0
  10. //
  11. // Unless required by applicable law or agreed to in writing, software
  12. // distributed under the License is distributed on an "AS IS" BASIS,
  13. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. // See the License for the specific language governing permissions and
  15. // limitations under the License.
  16. var Registry = RegistryModule($);
  17. var Modal = ModalModule($, ko);
  18. /**
  19. * Import node module
  20. * Assists in the conversion of a jobsub node.
  21. * Assists in the population of available nodes.
  22. */
  23. var ImportNodeModule = function($) {
  24. var module = function(options) {
  25. var self = this;
  26. self.workflow = options.workflow;
  27. self.available_nodes = [];
  28. self.url = ko.computed(function() {
  29. return '/oozie/workflows/' + options.workflow.id() + '/jobsub/actions'
  30. });
  31. module.prototype.initialize.apply(self, arguments);
  32. return self;
  33. };
  34. $.extend(module.prototype, {
  35. initialize: function(options) {
  36. var self = this;
  37. if (options.nodes) {
  38. $.each(options.nodes, function(index, node) {
  39. self.available_nodes.push(node);
  40. });
  41. }
  42. },
  43. getAvailableNodes: function() {
  44. var self = this;
  45. return self.available_nodes;
  46. },
  47. loadAvailableNodes: function(options) {
  48. var self = this;
  49. var request = $.extend({
  50. url: self.url(),
  51. dataType: 'json',
  52. type: 'GET',
  53. success: $.noop,
  54. error: $.noop
  55. }, options || {});
  56. $.ajax(request);
  57. },
  58. convertNode: function(options, jobsub_id) {
  59. var self = this;
  60. var options = options || {};
  61. var request = $.extend({
  62. url: self.url(),
  63. type: 'POST',
  64. data: { 'jobsub_id': jobsub_id },
  65. success: $.noop,
  66. error: $.noop
  67. }, options);
  68. $.ajax(request);
  69. }
  70. });
  71. return module;
  72. };
  73. var ImportNode = ImportNodeModule($);
  74. var Node = NodeModule($, IdGeneratorTable, NodeFields);
  75. var StartNode = NodeModule($, IdGeneratorTable, NodeFields);
  76. $.extend(StartNode.prototype, Node.prototype, {
  77. /**
  78. * Same as addChild for nodes, except if we reach the end,
  79. * we add the end node to our child_links in the form of a
  80. * 'related' link and 'start' link.
  81. * We should always have a start link in an empty workflow!
  82. */
  83. replaceChild: function(child, replacement) {
  84. var self = this;
  85. var index = -1;
  86. $.each(self.child_links(), function(i, link) {
  87. if (link.child() == child.id()) {
  88. index = i;
  89. }
  90. });
  91. if (index > -1) {
  92. self.child_links.splice(index, 1);
  93. var links = self.child_links.splice(index);
  94. var link = {
  95. parent: ko.observable(self.id()),
  96. child: ko.observable(replacement.id()),
  97. name: ko.observable('to'),
  98. comment: ko.observable('')
  99. };
  100. self.child_links.push(link);
  101. $.each(links, function(index, link) {
  102. self.child_links.push(link);
  103. });
  104. }
  105. return index != -1;
  106. }
  107. });
  108. var ForkNode = NodeModule($, IdGeneratorTable, NodeFields);
  109. $.extend(ForkNode.prototype, Node.prototype, {
  110. // Join nodes are connected through 'related' links
  111. join: function() {
  112. var self = this;
  113. var join = null;
  114. $.each(self.child_links(), function(index, link) {
  115. if (link.name() == 'related') {
  116. join = self.registry.get(link.child());
  117. }
  118. });
  119. return join;
  120. },
  121. /**
  122. * Append a node to the current fork
  123. * Also adds join node to node.
  124. * When adding the join node, append will remove all the children from the join!
  125. * We need to make sure the join remembers its children since append will replace them.
  126. * NOTE: Cannot append a fork or decision! Use addChild or replaceChild instead!
  127. */
  128. append: function(node) {
  129. var self = this;
  130. if (node.node_type() != 'decision' && node.node_type() != 'fork') {
  131. var join = self.join();
  132. if (join.id() != node.id()) {
  133. var children = join.findChildren();
  134. self.addChild(node);
  135. node.append(join);
  136. // remember children
  137. $.each(children, function(index, child) {
  138. join.addChild(child);
  139. });
  140. }
  141. }
  142. },
  143. /**
  144. * Replace child node with another node
  145. * 1. Remove child if the replacement is related join
  146. * 2. Apply Node.replaceChild for all other causes
  147. * NOTE: can assume the only join this fork will ever see is the related join!
  148. * This is because the related will always be the closest join for children.
  149. * Also, the operations allowed that would make this all possible: detach, append;
  150. * are inherently going to remove any other joins if they exist.
  151. * This is also easier given nodes are contained within Forks!
  152. */
  153. replaceChild: function(child, replacement) {
  154. var self = this;
  155. var ret = true;
  156. if (self.join().id() == replacement.id()) {
  157. ret = self.removeChild(child);
  158. } else {
  159. ret = Node.prototype.replaceChild.apply(self, arguments);
  160. }
  161. var links = self.links().filter(function(element, index, arr) {
  162. return self.registry.get(element.child()).node_type() != 'join';
  163. });
  164. if (links.length < 2) {
  165. self.detach();
  166. self.join().detach();
  167. self.erase();
  168. self.join().erase();
  169. }
  170. return ret;
  171. },
  172. /**
  173. * Converts fork node into decision node in the following way:
  174. * 1. Copies contents of current fork node into a new decision node
  175. * 2. Detach fork node
  176. * 3. Erase fork node
  177. * 4. Append decision node to parent
  178. */
  179. convertToDecision: function() {
  180. var self = this;
  181. var join = self.join();
  182. var end = null;
  183. var child = join.findChildren()[0];
  184. // Replace join with decision end
  185. var decision_end_model = new NodeModel({
  186. id: IdGeneratorTable['decisionend'].nextId(),
  187. node_type: 'decisionend',
  188. workflow: self.workflow(),
  189. child_links: join.model.child_links
  190. });
  191. var decision_end_node = new Node(self._workflow, decision_end_model, self.registry);
  192. $.each(decision_end_model.child_links, function(index, link) {
  193. link.parent = decision_end_model.id;
  194. });
  195. var parents = join.findParents();
  196. $.each(parents, function(index, parent) {
  197. parent.replaceChild(join, decision_end_node);
  198. });
  199. // Replace fork with decision node
  200. var decision_model = new DecisionModel({
  201. id: IdGeneratorTable['decision'].nextId(),
  202. name: self.name(),
  203. description: self.description(),
  204. node_type: 'decision',
  205. workflow: self.workflow(),
  206. child_links: self.model.child_links
  207. });
  208. var default_link = {
  209. parent: decision_model.id,
  210. child: self._workflow.end(),
  211. name: 'default',
  212. comment: ''
  213. };
  214. decision_model.child_links.push(default_link);
  215. $.each(decision_model.child_links, function(index, link) {
  216. link.parent = decision_model.id;
  217. });
  218. var parents = self.findParents();
  219. var decision_node = new DecisionNode(self._workflow, decision_model, self.registry);
  220. decision_node.removeChild(join);
  221. decision_node.addChild(decision_end_node);
  222. $.each(parents, function(index, parent) {
  223. parent.replaceChild(self, decision_node);
  224. });
  225. // Get rid of fork and join in registry
  226. join.erase();
  227. self.erase();
  228. // Add decision and decision end to registry
  229. self.registry.add(decision_node.id(), decision_node);
  230. self.registry.add(decision_end_node.id(), decision_end_node);
  231. }
  232. });
  233. var DecisionNode = NodeModule($, IdGeneratorTable, NodeFields);
  234. $.extend(DecisionNode.prototype, ForkNode.prototype, {
  235. initialize: function(workflow, model, registry) {
  236. var self = this;
  237. var registry = registry;
  238. var end = null;
  239. },
  240. end: function() {
  241. var self = this;
  242. var end = null;
  243. $.each(self.child_links(), function(index, link) {
  244. if (link.name() == 'related') {
  245. end = self.registry.get(link.child());
  246. }
  247. });
  248. return end;
  249. },
  250. /**
  251. * Append a node to the current decision
  252. * Also appends end node to node.
  253. * NOTE: Cannot append a decision or fork! Use addChild or replaceChild instead!
  254. */
  255. append: function(node) {
  256. var self = this;
  257. if (node.node_type() != 'decision' && node.node_type() != 'fork') {
  258. var end = self.end();
  259. if (end.id() != node.id()) {
  260. var children = end.findChildren();
  261. self.addChild(node);
  262. node.append(end);
  263. // remember children
  264. $.each(children, function(index, child) {
  265. end.addChild(child);
  266. });
  267. }
  268. }
  269. },
  270. /**
  271. * Replace child node with another node
  272. * 1. Remove child if the replacement is end
  273. * 2. Apply Node.replaceChild for all other causes
  274. */
  275. replaceChild: function(child, replacement) {
  276. var self = this;
  277. var ret = true;
  278. var end = self.end();
  279. if (end && end.id() == replacement.id()) {
  280. ret = self.removeChild(child);
  281. } else {
  282. ret = Node.prototype.replaceChild.apply(self, arguments);
  283. }
  284. if (self.links().length < 2) {
  285. self.detach();
  286. end.detach();
  287. self.erase();
  288. end.erase();
  289. }
  290. return ret;
  291. }
  292. });
  293. /**
  294. * Workflow module
  295. */
  296. var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode, IdGeneratorTable) {
  297. var module = function(options) {
  298. var self = this;
  299. // @see http://knockoutjs.com/documentation/plugins-mapping.html
  300. var mapping = ko.mapping.fromJS(options.model, {
  301. ignore: ['initialize', 'toString', 'copy'],
  302. job_properties: {
  303. create: function(options) {
  304. var parent = options.parent;
  305. var subscribe = function(mapping) {
  306. mapping.name.subscribe(function(value) {
  307. parent.job_properties.valueHasMutated();
  308. });
  309. mapping.value.subscribe(function(value) {
  310. parent.job_properties.valueHasMutated();
  311. });
  312. };
  313. return map_params(options, subscribe);
  314. },
  315. update: function(options) {
  316. var parent = options.parent;
  317. var subscribe = function(mapping) {
  318. mapping.name.subscribe(function(value) {
  319. parent.job_properties.valueHasMutated();
  320. });
  321. mapping.value.subscribe(function(value) {
  322. parent.job_properties.valueHasMutated();
  323. });
  324. };
  325. return map_params(options, subscribe);
  326. }
  327. },
  328. parameters: {
  329. // Will receive individual objects to subscribe.
  330. // Containing array is mapped automagically
  331. create: function(options) {
  332. var parent = options.parent;
  333. var subscribe = function(mapping) {
  334. mapping.name.subscribe(function(value) {
  335. parent.parameters.valueHasMutated();
  336. });
  337. mapping.value.subscribe(function(value) {
  338. parent.parameters.valueHasMutated();
  339. });
  340. };
  341. return map_params(options, subscribe);
  342. },
  343. update: function(options) {
  344. var parent = options.parent;
  345. var subscribe = function(mapping) {
  346. mapping.name.subscribe(function(value) {
  347. parent.parameters.valueHasMutated();
  348. });
  349. mapping.value.subscribe(function(value) {
  350. parent.parameters.valueHasMutated();
  351. });
  352. };
  353. return map_params(options, subscribe);
  354. }
  355. }
  356. });
  357. $.extend(self, mapping);
  358. $.each(mapping['__ko_mapping__'].mappedProperties, function(key, value) {
  359. var key = key;
  360. self[key].subscribe(function(value) {
  361. self.is_dirty( true );
  362. self.model[key] = ko.mapping.toJS(value);
  363. });
  364. });
  365. self.model = options.model;
  366. self.registry = options.registry;
  367. self.options = options;
  368. self.el = $('#workflow');
  369. self.nodes = ko.observableArray([]);
  370. self.kill = null;
  371. self.is_dirty = ko.observable( false );
  372. self.loading = ko.observable( false );
  373. self.read_only = ko.observable( options.read_only || false );
  374. self.new_node = ko.observable();
  375. self.url = ko.computed(function() {
  376. return '/oozie/workflows/' + self.id()
  377. });
  378. // Events
  379. self.el.on('workflow:rebuild', function() {
  380. self.rebuild();
  381. });
  382. self.el.on('workflow:events:load', function() {
  383. self.dragAndDropEvents( options );
  384. });
  385. self.el.on('workflow:droppables:load', function() {
  386. self.droppables();
  387. });
  388. self.el.on('workflow:draggables:load', function() {
  389. self.draggables();
  390. });
  391. self.dragAndDropEvents( options );
  392. self.el.trigger('workflow:events:loaded');
  393. module.prototype.initialize.apply(self, arguments);
  394. return self;
  395. };
  396. $.extend(module.prototype, {
  397. // Normal stuff
  398. initialize: function(options) {
  399. var self = this;
  400. $.extend(self.options, options);
  401. if ('model' in options) {
  402. self.model = options.model;
  403. // Initialize nodes
  404. if (self.model.nodes) {
  405. self.registry.clear();
  406. $.each(self.model.nodes, function(index, node) {
  407. var NodeModel = NodeModelChooser(node.node_type);
  408. var model = new NodeModel(node);
  409. var temp = null;
  410. switch(node.node_type) {
  411. case 'start':
  412. temp = new StartNode(self, model, self.registry);
  413. break;
  414. case 'fork':
  415. temp = new ForkNode(self, model, self.registry);
  416. break;
  417. case 'decision':
  418. temp = new DecisionNode(self, model, self.registry);
  419. break;
  420. case 'kill':
  421. temp = self.kill = new Node(self, model, self.registry);
  422. break;
  423. default:
  424. temp = new Node(self, model, self.registry);
  425. break;
  426. }
  427. self.registry.add(temp.id(), temp);
  428. });
  429. }
  430. // Update data
  431. $.each(self.model, function (key, value) {
  432. if (key in self) {
  433. switch(key) {
  434. case 'job_properties':
  435. case 'parameters':
  436. // These may be serialized JSON data since that is how they are stored
  437. self[key].removeAll();
  438. var arr = $.parseJSON(value) || value;
  439. $.each(arr, function(index, obj) {
  440. var mapping = ko.mapping.fromJS(obj);
  441. mapping.name.subscribe(function(value) {
  442. self[key].valueHasMutated();
  443. });
  444. mapping.value.subscribe(function(value) {
  445. self[key].valueHasMutated();
  446. });
  447. self[key].push(mapping);
  448. });
  449. break;
  450. case 'nodes':
  451. break;
  452. default:
  453. self[key](value);
  454. break;
  455. }
  456. }
  457. });
  458. self.is_dirty( false );
  459. }
  460. if (!self.kill) {
  461. var kill_json = {
  462. "description": "",
  463. "workflow": self.id(),
  464. "child_links": [],
  465. "node_type": "kill",
  466. "message": "Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]",
  467. "name": "kill",
  468. "id": IdGeneratorTable['kill'].nextId()
  469. };
  470. var NodeModel = NodeModelChooser(kill_json.node_type);
  471. var model = new NodeModel(kill_json);
  472. self.kill = new Node(self, model, self.registry);
  473. self.registry.add(self.kill.id(), self.kill);
  474. }
  475. if ('read_only' in options) {
  476. self.read_only(options['read_only']);
  477. }
  478. },
  479. toString: function() {
  480. return '';
  481. var s = '[';
  482. $.each(self.registry.nodes, function(key, node) {
  483. s += node.model.toString() + ",\n";
  484. });
  485. return s + ']';
  486. },
  487. // Data manipulation
  488. toJSON: function() {
  489. var self = this;
  490. data = $.extend(true, {}, self.model);
  491. var nodes = [];
  492. $.each(self.registry.nodes, function(key, node) {
  493. // Create object with members from the actual model to address JSON.stringify bug
  494. // JSON.stringify does not pick up members specified in prototype prior to object creation.
  495. var model = {};
  496. for (var key in node.model) {
  497. model[key] = node.model[key];
  498. }
  499. nodes.push(model);
  500. });
  501. data['nodes'] = nodes;
  502. return JSON.stringify(data);
  503. },
  504. save: function( options ) {
  505. var self = this;
  506. var request = $.extend({
  507. url: self.url() + '/save',
  508. type: 'POST',
  509. data: { workflow: self.toJSON() },
  510. success: $.noop,
  511. error: $.noop
  512. }, options || {});
  513. $.ajax(request);
  514. },
  515. load: function( options ) {
  516. var self = this;
  517. var request = $.extend({
  518. url: self.url(),
  519. dataType: 'json',
  520. type: 'GET',
  521. success: $.noop,
  522. error: $.noop
  523. }, options || {});
  524. $.ajax(request);
  525. },
  526. reload: function(model) {
  527. var self = this;
  528. // Clear all children
  529. $.each(self.registry.nodes, function(index, node) {
  530. node.children.removeAll();
  531. });
  532. self.nodes.removeAll();
  533. self.initialize({model: model});
  534. self.rebuild();
  535. self.el.trigger('workflow:loaded');
  536. },
  537. addParameter: function(data, event) {
  538. var self = this;
  539. var prop = { name: ko.observable(""), value: ko.observable("") };
  540. // force bubble up to containing observable array.
  541. prop.name.subscribe(function(){
  542. self.parameters.valueHasMutated();
  543. });
  544. prop.value.subscribe(function(){
  545. self.parameters.valueHasMutated();
  546. });
  547. self.parameters.push(prop);
  548. },
  549. removeParameter: function(data, event) {
  550. var self = this;
  551. self.parameters.remove(data);
  552. },
  553. addJobProperty: function(data, event) {
  554. var self = this;
  555. var prop = { name: ko.observable(""), value: ko.observable("") };
  556. // force bubble up to containing observable array.
  557. prop.name.subscribe(function(){
  558. self.parameters.valueHasMutated();
  559. });
  560. prop.value.subscribe(function(){
  561. self.parameters.valueHasMutated();
  562. });
  563. self.job_properties.push(prop);
  564. },
  565. removeJobProperty: function(data, event) {
  566. var self = this;
  567. self.job_properties.remove(data);
  568. },
  569. // Workflow UI
  570. // Function to build nodes... recursively.
  571. build: function() {
  572. var self = this;
  573. var maximum = 100;
  574. var count = 0;
  575. var methodChooser = function(node, collection, skip_parents_check) {
  576. if (count++ >= maximum) {
  577. console.error('Hit maximum number of node recursion: ' + maximum);
  578. return null;
  579. }
  580. if (!node) {
  581. return node;
  582. }
  583. var parents = node.findParents();
  584. // Found end of decision node or found join!
  585. if (parents.length > 1 && !skip_parents_check) {
  586. return node;
  587. }
  588. switch(node.node_type()) {
  589. case 'start':
  590. case 'end':
  591. case 'kill':
  592. case 'fork':
  593. case 'join':
  594. case 'decision':
  595. case 'decisionend':
  596. return control(node, collection);
  597. default:
  598. return normal(node, collection);
  599. }
  600. };
  601. var normal = function(node, collection) {
  602. collection.push(node);
  603. var retNode = null;
  604. $.each(node.links(), function(index, link) {
  605. var next_node = self.registry.get(link.child());
  606. retNode = methodChooser(next_node, collection, false, true);
  607. });
  608. return retNode;
  609. };
  610. var control = function(node, collection, skip_parents_check) {
  611. switch(node.node_type()) {
  612. case 'start':
  613. case 'end':
  614. case 'kill':
  615. case 'join':
  616. case 'decisionend':
  617. return normal(node, collection, false, true);
  618. case 'fork':
  619. collection.push(node);
  620. // Wait for join.
  621. // Iterate through all children and add them to child collection.
  622. var join = null;
  623. $.each(node.links(), function(index, link) {
  624. var next_node = self.registry.get(link.child());
  625. var collection = ko.observableArray([]);
  626. node.children.push(collection);
  627. join = methodChooser(next_node, collection, false, true);
  628. });
  629. // Add join to collection, then find its single child.
  630. return methodChooser(join, collection, true, true);
  631. case 'decision':
  632. collection.push(node);
  633. // Waits for end, then runs through children of end node
  634. var end = null;
  635. $.each(node.links(), function(index, link) {
  636. var next_node = self.registry.get(link.child());
  637. var collection = ko.observableArray([]);
  638. node.children.push(collection);
  639. end = methodChooser(next_node, collection, true, true);
  640. });
  641. // Add end
  642. return methodChooser(node.end(), collection, true, true);
  643. default:
  644. // Should never get here.
  645. return node;
  646. }
  647. };
  648. methodChooser(self.registry.get(self.start()), self.nodes, false, true);
  649. $(".tooltip").remove();
  650. $("*[rel=tooltip]").tooltip();
  651. },
  652. rebuild: function() {
  653. var self = this;
  654. // Clear all children
  655. $.each(self.registry.nodes, function(index, node) {
  656. node.children.removeAll();
  657. });
  658. self.nodes.removeAll();
  659. // Rebuild
  660. self.build();
  661. self.draggables();
  662. self.droppables();
  663. self.el.trigger('workflow:rebuilt');
  664. },
  665. draggables: function() {
  666. var self = this;
  667. self.el.find('.node-action').each(function(index, el) {
  668. if (!$(el).hasClass('ui-draggable')) {
  669. $(el).find('.row-fluid').eq(0).css('cursor', 'move');
  670. $(el).draggable({
  671. containment: [ self.el.offset().left - 10, self.el.offset().top - 10,
  672. self.el.offset().left + self.el.outerWidth(), self.el.offset().top + self.el.outerHeight() ],
  673. refreshPositions: true,
  674. revert: true,
  675. zIndex: 1000,
  676. opacity: 0.45,
  677. revertDuration: 0,
  678. cancel: '.node-action-bar'
  679. });
  680. }
  681. });
  682. },
  683. droppables: function() {
  684. var self = this;
  685. self.el.find('.node-link').each(function(index, el) {
  686. $(el).droppable({
  687. 'hoverClass': 'node-link-hover',
  688. 'greedy': true,
  689. 'accept': '.node-action',
  690. 'tolerance': 'pointer'
  691. });
  692. });
  693. self.el.find('.node-decision-end').each(function(index, el) {
  694. $(el).droppable({
  695. 'hoverClass': 'node-link-hover',
  696. 'greedy': true,
  697. 'accept': '.node-action',
  698. 'tolerance': 'pointer'
  699. });
  700. });
  701. self.el.find('.node-fork .action').each(function(index, el) {
  702. $(el).droppable({
  703. 'hoverClass': 'node-fork-hover',
  704. 'greedy': true,
  705. 'accept': '.node-action',
  706. 'tolerance': 'pointer'
  707. });
  708. });
  709. self.el.find('.node-decision .action').each(function(index, el) {
  710. $(el).droppable({
  711. 'hoverClass': 'node-fork-hover',
  712. 'greedy': true,
  713. 'accept': '.node-action',
  714. 'tolerance': 'pointer'
  715. });
  716. });
  717. self.el.find('.node-action .action').each(function(index, el) {
  718. $(el).droppable({
  719. 'hoverClass': 'node-action-hover',
  720. 'greedy': true,
  721. 'accept': '.node-action',
  722. 'tolerance': 'pointer'
  723. });
  724. });
  725. },
  726. dragAndDropEvents: function( options ) {
  727. var self = this;
  728. var read_only_error_handler = options.read_only_error_handler;
  729. // Build event delegations.
  730. // Drop on node link
  731. self.el.on('drop', '.node-link', function(e, ui) {
  732. if (self.read_only()) {
  733. read_only_error_handler();
  734. return false;
  735. }
  736. // draggable should be a node.
  737. // droppable should be a link.
  738. var draggable = ko.contextFor(ui.draggable[0]).$data;
  739. var droppable = ko.contextFor(this).$data;
  740. // If newParent is fork, prepend to child instead.
  741. // This will make it so that we can drop and drop to the top of a node list within a fork.
  742. var newParent = self.registry.get(droppable.parent());
  743. if (newParent.id() != draggable.id() && !newParent.isChild(draggable)) {
  744. switch(newParent.node_type()) {
  745. case 'fork':
  746. case 'decision':
  747. // Children that are forks or decisions may be removed when we detach.
  748. // Remember children in this case to find correct node.
  749. var child = self.registry.get(droppable.child());
  750. var children_of_child = [];
  751. if (child.node_type() == 'fork' || child.node_type() == 'decision') {
  752. children_of_child = child.findChildren();
  753. }
  754. draggable.detach();
  755. // Make sure fork and decision still exist
  756. // Otherwise find child that replaced it
  757. var child_to_replace = child;
  758. if (child.node_type() == 'fork' || child.node_type() == 'decision') {
  759. if (!self.registry.get(child.id())) {
  760. // Guaranteed one because the fork is being removed right now.
  761. child_to_replace = $.grep(children_of_child, function(child_of_child, index) {
  762. return child_of_child.findParents().length > 0;
  763. })[0];
  764. }
  765. }
  766. newParent.replaceChild(child_to_replace, draggable);
  767. draggable.addChild(child_to_replace);
  768. break;
  769. case 'join':
  770. case 'decisionend':
  771. // Join and decisionend may disappear when we detach...
  772. // Remember its children and append to child.
  773. var parents = newParent.findParents();
  774. draggable.detach();
  775. if (newParent.findParents().length < 2) {
  776. $.each(parents, function(index, parent) {
  777. parent.append(draggable);
  778. });
  779. } else {
  780. newParent.append(draggable);
  781. }
  782. break;
  783. default:
  784. draggable.detach();
  785. newParent.append(draggable);
  786. break;
  787. }
  788. workflow.is_dirty( true );
  789. self.rebuild();
  790. }
  791. // Prevent bubbling events
  792. return false;
  793. });
  794. // Drop on fork
  795. self.el.on('drop', '.node-fork', function(e, ui) {
  796. if (self.read_only()) {
  797. read_only_error_handler();
  798. return false;
  799. }
  800. // draggable should be a node.
  801. // droppable should be a fork.
  802. var draggable = ko.contextFor(ui.draggable[0]).$data;
  803. var droppable = ko.contextFor(this).$data;
  804. if (!droppable.isChild(draggable) && droppable.id() != draggable.id()) {
  805. draggable.detach();
  806. droppable.append(draggable);
  807. self.rebuild();
  808. }
  809. // Prevent bubbling events
  810. return false;
  811. });
  812. // Drop on decision
  813. self.el.on('drop', '.node-decision', function(e, ui) {
  814. if (self.read_only()) {
  815. read_only_error_handler();
  816. return false;
  817. }
  818. // draggable should be a node.
  819. // droppable should be a fork.
  820. var draggable = ko.contextFor(ui.draggable[0]).$data;
  821. var droppable = ko.contextFor(this).$data;
  822. if (!droppable.isChild(draggable) && droppable.id() != draggable.id()) {
  823. draggable.detach();
  824. droppable.append(draggable);
  825. self.rebuild();
  826. }
  827. // Prevent bubbling events
  828. return false;
  829. });
  830. // Drop on action
  831. self.el.on('drop', '.node-action', function(e, ui) {
  832. if (self.read_only()) {
  833. read_only_error_handler();
  834. return false;
  835. }
  836. // draggable should be a node.
  837. // droppable should be a node.
  838. var draggable = ko.contextFor(ui.draggable[0]).$data;
  839. var droppable = ko.contextFor(this).$data;
  840. // Create a fork and join programatically.
  841. var newParents = droppable.findParents();
  842. // skip forking beneathe a decision node
  843. if (droppable.id() != draggable.id() && newParents.length == 1 && draggable.findParents().length <= 1) {
  844. var ForkModel = NodeModelChooser('fork');
  845. var JoinModel = NodeModelChooser('join');
  846. var fork = new ForkModel({
  847. id: IdGeneratorTable['fork'].nextId(),
  848. description: "",
  849. workflow: self.id,
  850. node_type: "fork",
  851. child_links: []
  852. });
  853. var forkNode = new ForkNode(self, fork, self.registry);
  854. var join = new JoinModel({
  855. id: IdGeneratorTable['join'].nextId(),
  856. description: "",
  857. workflow: self.id,
  858. node_type: "join",
  859. child_links: []
  860. });
  861. var joinNode = new Node(self, join, self.registry);
  862. self.registry.add(forkNode.id(), forkNode);
  863. self.registry.add(joinNode.id(), joinNode);
  864. forkNode.addChild(joinNode);
  865. // Handles fork creation.
  866. $.each(newParents, function(index, parent) {
  867. parent.replaceChild(droppable, forkNode);
  868. });
  869. draggable.detach();
  870. forkNode.append(draggable);
  871. forkNode.append(droppable);
  872. self.rebuild();
  873. }
  874. // Prevent bubbling events.
  875. return false;
  876. });
  877. }
  878. });
  879. return module;
  880. };
  881. var Workflow = WorkflowModule($, nodeModelChooser, Node, ForkNode, DecisionNode, IdGeneratorTable);