You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

654 satır
28KB

  1. /**
  2. * Copyright 2013, 2014 IBM Corp.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. **/
  16. RED.editor = (function() {
  17. var editing_node = null;
  18. // TODO: should IMPORT/EXPORT get their own dialogs?
  19. function getCredentialsURL(nodeType, nodeID) {
  20. var dashedType = nodeType.replace(/\s+/g, '-');
  21. return 'credentials/' + dashedType + "/" + nodeID;
  22. }
  23. /**
  24. * Validate a node
  25. * @param node - the node being validated
  26. * @returns {boolean} whether the node is valid. Sets node.dirty if needed
  27. */
  28. function validateNode(node) {
  29. var oldValue = node.valid;
  30. node.valid = validateNodeProperties(node, node._def.defaults, node);
  31. if (node._def._creds) {
  32. node.valid = node.valid && validateNodeProperties(node, node._def.credentials, node._def._creds);
  33. }
  34. if (oldValue != node.valid) {
  35. node.dirty = true;
  36. }
  37. }
  38. /**
  39. * Validate a node's properties for the given set of property definitions
  40. * @param node - the node being validated
  41. * @param definition - the node property definitions (either def.defaults or def.creds)
  42. * @param properties - the node property values to validate
  43. * @returns {boolean} whether the node's properties are valid
  44. */
  45. function validateNodeProperties(node, definition, properties) {
  46. var isValid = true;
  47. for (var prop in definition) {
  48. if (!validateNodeProperty(node, definition, prop, properties[prop])) {
  49. isValid = false;
  50. }
  51. }
  52. return isValid;
  53. }
  54. /**
  55. * Validate a individual node property
  56. * @param node - the node being validated
  57. * @param definition - the node property definitions (either def.defaults or def.creds)
  58. * @param property - the property name being validated
  59. * @param value - the property value being validated
  60. * @returns {boolean} whether the node proprty is valid
  61. */
  62. function validateNodeProperty(node,definition,property,value) {
  63. var valid = true;
  64. if ("required" in definition[property] && definition[property].required) {
  65. valid = value !== "";
  66. }
  67. if (valid && "validate" in definition[property]) {
  68. valid = definition[property].validate.call(node,value);
  69. }
  70. if (valid && definition[property].type && RED.nodes.getType(definition[property].type) && !("validate" in definition[property])) {
  71. if (!value || value == "_ADD_") {
  72. valid = false;
  73. } else {
  74. var v = RED.nodes.node(value).valid;
  75. valid = (v==null || v);
  76. }
  77. }
  78. return valid;
  79. }
  80. /**
  81. * Called when the node's properties have changed.
  82. * Marks the node as dirty and needing a size check.
  83. * Removes any links to non-existant outputs.
  84. * @param node - the node that has been updated
  85. * @returns {array} the links that were removed due to this update
  86. */
  87. function updateNodeProperties(node) {
  88. node.resize = true;
  89. node.dirty = true;
  90. var removedLinks = [];
  91. if (node.outputs < node.ports.length) {
  92. while (node.outputs < node.ports.length) {
  93. node.ports.pop();
  94. }
  95. RED.nodes.eachLink(function(l) {
  96. if (l.source === node && l.sourcePort >= node.outputs) {
  97. removedLinks.push(l);
  98. }
  99. });
  100. for (var l=0;l<removedLinks.length;l++) {
  101. RED.nodes.removeLink(removedLinks[l]);
  102. }
  103. } else if (node.outputs > node.ports.length) {
  104. while (node.outputs > node.ports.length) {
  105. node.ports.push(node.ports.length);
  106. }
  107. }
  108. return removedLinks;
  109. }
  110. $( "#dialog" ).dialog({
  111. modal: true,
  112. autoOpen: false,
  113. closeOnEscape: false,
  114. width: 500,
  115. buttons: [
  116. {
  117. text: "Ok",
  118. click: function() {
  119. if (editing_node) {
  120. var changes = {};
  121. var changed = false;
  122. var wasDirty = RED.view.dirty();
  123. var d;
  124. if (editing_node._def.oneditsave) {
  125. var oldValues = {};
  126. for (d in editing_node._def.defaults) {
  127. if (editing_node._def.defaults.hasOwnProperty(d)) {
  128. if (typeof editing_node[d] === "string" || typeof editing_node[d] === "number") {
  129. oldValues[d] = editing_node[d];
  130. } else {
  131. oldValues[d] = $.extend(true,{},{v:editing_node[d]}).v;
  132. }
  133. }
  134. }
  135. var rc = editing_node._def.oneditsave.call(editing_node);
  136. if (rc === true) {
  137. changed = true;
  138. }
  139. for (d in editing_node._def.defaults) {
  140. if (editing_node._def.defaults.hasOwnProperty(d)) {
  141. if (oldValues[d] === null || typeof oldValues[d] === "string" || typeof oldValues[d] === "number") {
  142. if (oldValues[d] !== editing_node[d]) {
  143. changes[d] = oldValues[d];
  144. changed = true;
  145. }
  146. } else {
  147. if (JSON.stringify(oldValues[d]) !== JSON.stringify(editing_node[d])) {
  148. changes[d] = oldValues[d];
  149. changed = true;
  150. }
  151. }
  152. }
  153. }
  154. }
  155. if (editing_node._def.defaults) {
  156. for (d in editing_node._def.defaults) {
  157. if (editing_node._def.defaults.hasOwnProperty(d)) {
  158. var input = $("#node-input-"+d);
  159. var newValue;
  160. if (input.attr('type') === "checkbox") {
  161. newValue = input.prop('checked');
  162. } else {
  163. newValue = input.val();
  164. }
  165. if (newValue != null) {
  166. if (editing_node[d] != newValue) {
  167. if (editing_node._def.defaults[d].type) {
  168. if (newValue == "_ADD_") {
  169. newValue = "";
  170. }
  171. // Change to a related config node
  172. var configNode = RED.nodes.node(editing_node[d]);
  173. if (configNode) {
  174. var users = configNode.users;
  175. users.splice(users.indexOf(editing_node),1);
  176. }
  177. configNode = RED.nodes.node(newValue);
  178. if (configNode) {
  179. configNode.users.push(editing_node);
  180. }
  181. }
  182. changes[d] = editing_node[d];
  183. editing_node[d] = newValue;
  184. changed = true;
  185. }
  186. }
  187. }
  188. }
  189. }
  190. if (editing_node._def.credentials) {
  191. var prefix = 'node-input';
  192. var credDefinition = editing_node._def.credentials;
  193. var credsChanged = updateNodeCredentials(editing_node,credDefinition,prefix);
  194. changed = changed || credsChanged;
  195. }
  196. var removedLinks = updateNodeProperties(editing_node);
  197. if (changed) {
  198. var wasChanged = editing_node.changed;
  199. editing_node.changed = true;
  200. RED.view.dirty(true);
  201. RED.history.push({t:'edit',node:editing_node,changes:changes,links:removedLinks,dirty:wasDirty,changed:wasChanged});
  202. }
  203. editing_node.dirty = true;
  204. validateNode(editing_node);
  205. RED.view.redraw();
  206. } else if (RED.view.state() == RED.state.EXPORT) {
  207. if (/library/.test($( "#dialog" ).dialog("option","title"))) {
  208. //TODO: move this to RED.library
  209. var flowName = $("#node-input-filename").val();
  210. if (!/^\s*$/.test(flowName)) {
  211. $.post('library/flows/'+flowName,$("#node-input-filename").attr('nodes'),function() {
  212. RED.library.loadFlowLibrary();
  213. RED.notify("Saved nodes","success");
  214. });
  215. }
  216. }
  217. } else if (RED.view.state() == RED.state.IMPORT) {
  218. RED.view.importNodes($("#node-input-import").val());
  219. }
  220. $( this ).dialog( "close" );
  221. }
  222. },
  223. {
  224. text: "Cancel",
  225. click: function() {
  226. $( this ).dialog( "close" );
  227. }
  228. }
  229. ],
  230. resize: function(e,ui) {
  231. if (editing_node) {
  232. $(this).dialog('option',"sizeCache-"+editing_node.type,ui.size);
  233. }
  234. },
  235. open: function(e) {
  236. RED.keyboard.disable();
  237. if (editing_node) {
  238. var size = $(this).dialog('option','sizeCache-'+editing_node.type);
  239. if (size) {
  240. $(this).dialog('option','width',size.width);
  241. $(this).dialog('option','height',size.height);
  242. }
  243. }
  244. },
  245. close: function(e) {
  246. RED.keyboard.enable();
  247. if (RED.view.state() != RED.state.IMPORT_DRAGGING) {
  248. RED.view.state(RED.state.DEFAULT);
  249. }
  250. $( this ).dialog('option','height','auto');
  251. $( this ).dialog('option','width','500');
  252. if (editing_node) {
  253. RED.sidebar.info.refresh(editing_node);
  254. }
  255. RED.sidebar.config.refresh();
  256. editing_node = null;
  257. }
  258. });
  259. /**
  260. * Create a config-node select box for this property
  261. * @param node - the node being edited
  262. * @param property - the name of the field
  263. * @param type - the type of the config-node
  264. */
  265. function prepareConfigNodeSelect(node,property,type) {
  266. var input = $("#node-input-"+property);
  267. var node_def = RED.nodes.getType(type);
  268. input.replaceWith('<select style="width: 60%;" id="node-input-'+property+'"></select>');
  269. updateConfigNodeSelect(property,type,node[property]);
  270. var select = $("#node-input-"+property);
  271. select.after(' <a id="node-input-lookup-'+property+'" class="btn"><i class="icon icon-pencil"></i></a>');
  272. $('#node-input-lookup-'+property).click(function(e) {
  273. showEditConfigNodeDialog(property,type,select.find(":selected").val());
  274. e.preventDefault();
  275. });
  276. var label = "";
  277. var configNode = RED.nodes.node(node[property]);
  278. if (configNode && node_def.label) {
  279. if (typeof node_def.label == "function") {
  280. label = node_def.label.call(configNode);
  281. } else {
  282. label = node_def.label;
  283. }
  284. }
  285. input.val(label);
  286. }
  287. /**
  288. * Populate the editor dialog input field for this property
  289. * @param node - the node being edited
  290. * @param property - the name of the field
  291. * @param prefix - the prefix to use in the input element ids (node-input|node-config-input)
  292. */
  293. function preparePropertyEditor(node,property,prefix) {
  294. var input = $("#"+prefix+"-"+property);
  295. if (input.attr('type') === "checkbox") {
  296. input.prop('checked',node[property]);
  297. } else {
  298. var val = node[property];
  299. if (val == null) {
  300. val = "";
  301. }
  302. input.val(val);
  303. }
  304. }
  305. /**
  306. * Add an on-change handler to revalidate a node field
  307. * @param node - the node being edited
  308. * @param definition - the definition of the node
  309. * @param property - the name of the field
  310. * @param prefix - the prefix to use in the input element ids (node-input|node-config-input)
  311. */
  312. function attachPropertyChangeHandler(node,definition,property,prefix) {
  313. $("#"+prefix+"-"+property).change(function() {
  314. if (!validateNodeProperty(node, definition, property,this.value)) {
  315. $(this).addClass("input-error");
  316. } else {
  317. $(this).removeClass("input-error");
  318. }
  319. });
  320. }
  321. /**
  322. * Assign the value to each credential field
  323. * @param node
  324. * @param credDef
  325. * @param credData
  326. * @param prefix
  327. */
  328. function populateCredentialsInputs(node, credDef, credData, prefix) {
  329. var cred;
  330. for (cred in credDef) {
  331. if (credDef.hasOwnProperty(cred)) {
  332. if (credDef[cred].type == 'password') {
  333. if (credData[cred]) {
  334. $('#' + prefix + '-' + cred).val(credData[cred]);
  335. } else if (credData['has_' + cred]) {
  336. $('#' + prefix + '-' + cred).val('__PWRD__');
  337. }
  338. else {
  339. $('#' + prefix + '-' + cred).val('');
  340. }
  341. } else {
  342. preparePropertyEditor(credData, cred, prefix);
  343. }
  344. attachPropertyChangeHandler(node, credDef, cred, prefix);
  345. }
  346. }
  347. for (cred in credDef) {
  348. if (credDef.hasOwnProperty(cred)) {
  349. $("#" + prefix + "-" + cred).change();
  350. }
  351. }
  352. }
  353. /**
  354. * Update the node credentials from the edit form
  355. * @param node - the node containing the credentials
  356. * @param credDefinition - definition of the credentials
  357. * @param prefix - prefix of the input fields
  358. * @return {boolean} whether anything has changed
  359. */
  360. function updateNodeCredentials(node, credDefinition, prefix) {
  361. var changed = false;
  362. if(!node.credentials) {
  363. node.credentials = {_:{}};
  364. }
  365. for (var cred in credDefinition) {
  366. if (credDefinition.hasOwnProperty(cred)) {
  367. var input = $("#" + prefix + '-' + cred);
  368. var value = input.val();
  369. if (credDefinition[cred].type == 'password') {
  370. node.credentials['has_' + cred] = (value !== "");
  371. if (value == '__PWRD__') {
  372. continue;
  373. }
  374. changed = true;
  375. }
  376. if (value != node.credentials._[cred]) {
  377. node.credentials[cred] = value;
  378. changed = true;
  379. }
  380. }
  381. }
  382. return changed;
  383. }
  384. /**
  385. * Prepare all of the editor dialog fields
  386. * @param node - the node being edited
  387. * @param definition - the node definition
  388. * @param prefix - the prefix to use in the input element ids (node-input|node-config-input)
  389. */
  390. function prepareEditDialog(node,definition,prefix) {
  391. for (var d in definition.defaults) {
  392. if (definition.defaults.hasOwnProperty(d)) {
  393. if (definition.defaults[d].type) {
  394. prepareConfigNodeSelect(node,d,definition.defaults[d].type);
  395. } else {
  396. preparePropertyEditor(node,d,prefix);
  397. }
  398. attachPropertyChangeHandler(node,definition.defaults,d,prefix);
  399. }
  400. }
  401. var completePrepare = function() {
  402. if (definition.oneditprepare) {
  403. definition.oneditprepare.call(node);
  404. }
  405. for (var d in definition.defaults) {
  406. if (definition.defaults.hasOwnProperty(d)) {
  407. $("#"+prefix+"-"+d).change();
  408. }
  409. }
  410. }
  411. if (definition.credentials) {
  412. if (node.credentials) {
  413. populateCredentialsInputs(node, definition.credentials, node.credentials, prefix);
  414. completePrepare();
  415. } else {
  416. $.getJSON(getCredentialsURL(node.type, node.id), function (data) {
  417. node.credentials = data;
  418. node.credentials._ = $.extend(true,{},data);
  419. populateCredentialsInputs(node, definition.credentials, node.credentials, prefix);
  420. completePrepare();
  421. });
  422. }
  423. } else {
  424. completePrepare();
  425. }
  426. }
  427. function showEditDialog(node) {
  428. editing_node = node;
  429. RED.view.state(RED.state.EDITING);
  430. $("#dialog-form").html($("script[data-template-name='"+node.type+"']").html());
  431. prepareEditDialog(node,node._def,"node-input");
  432. $( "#dialog" ).dialog("option","title","Edit "+node.type+" node").dialog( "open" );
  433. }
  434. function showEditConfigNodeDialog(name,type,id) {
  435. var adding = (id == "_ADD_");
  436. var node_def = RED.nodes.getType(type);
  437. var configNode = RED.nodes.node(id);
  438. if (configNode == null) {
  439. configNode = {
  440. id: (1+Math.random()*4294967295).toString(16),
  441. _def: node_def,
  442. type: type
  443. }
  444. for (var d in node_def.defaults) {
  445. if (node_def.defaults[d].value) {
  446. configNode[d] = node_def.defaults[d].value;
  447. }
  448. }
  449. }
  450. $("#dialog-config-form").html($("script[data-template-name='"+type+"']").html());
  451. prepareEditDialog(configNode,node_def,"node-config-input");
  452. var buttons = $( "#node-config-dialog" ).dialog("option","buttons");
  453. if (adding) {
  454. if (buttons.length == 3) {
  455. buttons = buttons.splice(1);
  456. }
  457. buttons[0].text = "Add";
  458. $("#node-config-dialog-user-count").html("").hide();
  459. } else {
  460. if (buttons.length == 2) {
  461. buttons.unshift({
  462. class: 'leftButton',
  463. text: "Delete",
  464. click: function() {
  465. var configProperty = $(this).dialog('option','node-property');
  466. var configId = $(this).dialog('option','node-id');
  467. var configType = $(this).dialog('option','node-type');
  468. var configNode = RED.nodes.node(configId);
  469. var configTypeDef = RED.nodes.getType(configType);
  470. if (configTypeDef.ondelete) {
  471. configTypeDef.ondelete.call(RED.nodes.node(configId));
  472. }
  473. RED.nodes.remove(configId);
  474. for (var i=0;i<configNode.users.length;i++) {
  475. var user = configNode.users[i];
  476. for (var d in user._def.defaults) {
  477. if (user._def.defaults.hasOwnProperty(d) && user[d] == configId) {
  478. user[d] = "";
  479. }
  480. }
  481. validateNode(user);
  482. }
  483. updateConfigNodeSelect(configProperty,configType,"");
  484. RED.view.dirty(true);
  485. $( this ).dialog( "close" );
  486. RED.view.redraw();
  487. }
  488. });
  489. }
  490. buttons[1].text = "Update";
  491. $("#node-config-dialog-user-count").html(configNode.users.length+" node"+(configNode.users.length==1?" uses":"s use")+" this config").show();
  492. }
  493. $( "#node-config-dialog" ).dialog("option","buttons",buttons);
  494. $( "#node-config-dialog" )
  495. .dialog("option","node-adding",adding)
  496. .dialog("option","node-property",name)
  497. .dialog("option","node-id",configNode.id)
  498. .dialog("option","node-type",type)
  499. .dialog("option","title",(adding?"Add new ":"Edit ")+type+" config node")
  500. .dialog( "open" );
  501. }
  502. function updateConfigNodeSelect(name,type,value) {
  503. var select = $("#node-input-"+name);
  504. var node_def = RED.nodes.getType(type);
  505. select.children().remove();
  506. RED.nodes.eachConfig(function(config) {
  507. if (config.type == type) {
  508. var label = "";
  509. if (typeof node_def.label == "function") {
  510. label = node_def.label.call(config);
  511. } else {
  512. label = node_def.label;
  513. }
  514. select.append('<option value="'+config.id+'"'+(value==config.id?" selected":"")+'>'+label+'</option>');
  515. }
  516. });
  517. select.append('<option value="_ADD_"'+(value===""?" selected":"")+'>Add new '+type+'...</option>');
  518. window.setTimeout(function() { select.change();},50);
  519. }
  520. $( "#node-config-dialog" ).dialog({
  521. modal: true,
  522. autoOpen: false,
  523. width: 500,
  524. closeOnEscape: false,
  525. buttons: [
  526. {
  527. text: "Ok",
  528. click: function() {
  529. var configProperty = $(this).dialog('option','node-property');
  530. var configId = $(this).dialog('option','node-id');
  531. var configType = $(this).dialog('option','node-type');
  532. var configAdding = $(this).dialog('option','node-adding');
  533. var configTypeDef = RED.nodes.getType(configType);
  534. var configNode;
  535. var d;
  536. if (configAdding) {
  537. configNode = {type:configType,id:configId,users:[]};
  538. for (d in configTypeDef.defaults) {
  539. if (configTypeDef.defaults.hasOwnProperty(d)) {
  540. configNode[d] = $("#node-config-input-"+d).val();
  541. }
  542. }
  543. configNode.label = configTypeDef.label;
  544. configNode._def = configTypeDef;
  545. RED.nodes.add(configNode);
  546. updateConfigNodeSelect(configProperty,configType,configNode.id);
  547. } else {
  548. configNode = RED.nodes.node(configId);
  549. for (d in configTypeDef.defaults) {
  550. if (configTypeDef.defaults.hasOwnProperty(d)) {
  551. configNode[d] = $("#node-config-input-"+d).val();
  552. }
  553. }
  554. updateConfigNodeSelect(configProperty,configType,configId);
  555. }
  556. if (configTypeDef.credentials) {
  557. updateNodeCredentials(configNode,configTypeDef.credentials,"node-config-input");
  558. }
  559. if (configTypeDef.oneditsave) {
  560. configTypeDef.oneditsave.call(RED.nodes.node(configId));
  561. }
  562. validateNode(configNode);
  563. RED.view.dirty(true);
  564. $(this).dialog("close");
  565. }
  566. },
  567. {
  568. text: "Cancel",
  569. click: function() {
  570. var configType = $(this).dialog('option','node-type');
  571. var configId = $(this).dialog('option','node-id');
  572. var configAdding = $(this).dialog('option','node-adding');
  573. var configTypeDef = RED.nodes.getType(configType);
  574. if (configTypeDef.oneditcancel) {
  575. // TODO: what to pass as this to call
  576. if (configTypeDef.oneditcancel) {
  577. var cn = RED.nodes.node(configId);
  578. if (cn) {
  579. configTypeDef.oneditcancel.call(cn,false);
  580. } else {
  581. configTypeDef.oneditcancel.call({id:configId},true);
  582. }
  583. }
  584. }
  585. $( this ).dialog( "close" );
  586. }
  587. }
  588. ],
  589. resize: function(e,ui) {
  590. },
  591. open: function(e) {
  592. if (RED.view.state() != RED.state.EDITING) {
  593. RED.keyboard.disable();
  594. }
  595. },
  596. close: function(e) {
  597. $("#dialog-config-form").html("");
  598. if (RED.view.state() != RED.state.EDITING) {
  599. RED.keyboard.enable();
  600. }
  601. RED.sidebar.config.refresh();
  602. }
  603. });
  604. return {
  605. edit: showEditDialog,
  606. editConfig: showEditConfigNodeDialog,
  607. validateNode: validateNode,
  608. updateNodeProperties: updateNodeProperties // TODO: only exposed for edit-undo
  609. }
  610. })();