uiActions.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. /*
  2. op-mattermost provides an integration for Mattermost and Open Project.
  3. Copyright (C) 2020 to present , Girish M
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <https://www.gnu.org/licenses/>
  14. */
  15. // noinspection JSUnresolvedVariable
  16. class UIactions {
  17. constructor(opURL, mmURL, intURL) {
  18. const Util = require('./util');
  19. const Message = require('./message');
  20. this.moment = require('moment');
  21. this.util = new Util();
  22. this.message = new Message(mmURL);
  23. this.opURL = opURL;
  24. this.mmURL = mmURL;
  25. this.intURL = intURL;
  26. this.projectId = '';
  27. this.timeLogId = '';
  28. this.wpId = '';
  29. this.optLen = 3;
  30. this.opAuth = {
  31. username: 'apikey',
  32. password: process.env.OP_ACCESS_TOKEN
  33. }
  34. this.currentUser = '';
  35. this.customFieldForBillableHours = 'customField1';
  36. }
  37. showSelProject(req, res, axios, action) {
  38. axios({
  39. url: 'projects?sortBy=[["created_at","desc"]]',
  40. method: 'get',
  41. baseURL: this.opURL,
  42. auth: this.opAuth
  43. }).then((response) => {
  44. console.log("Projects obtained from OP: %o", response.data);
  45. let projectOptArray = [];
  46. response.data._embedded.elements.forEach(element => {
  47. projectOptArray.push({
  48. "text": element.name,
  49. "value": "opt" + element.id
  50. });
  51. });
  52. let projectOptJSON;
  53. if (req.body.text) {
  54. projectOptJSON = this.util.getProjectOptJSON(this.intURL, projectOptArray, action);
  55. }
  56. else {
  57. projectOptJSON = this.util.getProjectOptJSON(this.intURL, projectOptArray, action, 'update');
  58. }
  59. console.log("optArray for projects", projectOptJSON);
  60. res.set('Content-Type', 'application/json').send(JSON.stringify(projectOptJSON)).status(200);
  61. }).catch(error => {
  62. if(error.response.status === 401) {
  63. console.log("Unauthorized: ", error.response.data.message);
  64. res.send("**Unauthorized. Invalid OpenProject access token**").status(401);
  65. }
  66. else {
  67. console.log("Error in getting projects from OP", error.response.data.message);
  68. res.send("**Open Project server down!!**").status(500);
  69. }
  70. return false;
  71. });
  72. }
  73. showSelWP(req, res, axios, action, mode = '') {
  74. // noinspection JSUnresolvedVariable
  75. this.projectId = req.body.context.selected_option.slice(this.optLen);
  76. axios({
  77. url: 'projects/' + this.projectId + '/work_packages?sortBy=[["created_at","desc"]]',
  78. method: 'get',
  79. baseURL: this.opURL,
  80. auth: this.opAuth
  81. }).then((response) => {
  82. console.log("WP obtained from OP: %o", response.data);
  83. let wpOptArray = [];
  84. response.data._embedded.elements.forEach(element => {
  85. wpOptArray.push({
  86. "text": element.subject,
  87. "value": "opt" + element.id
  88. });
  89. });
  90. let wpOptJSON = this.util.getWpOptJSON(this.intURL, wpOptArray, action, mode);
  91. console.log("opt Array for WP: ", wpOptJSON);
  92. res.set('Content-Type', 'application/json').send(JSON.stringify(wpOptJSON)).status(200);
  93. }, (error) => {
  94. console.log("Request failed for /work_packages: %o", error.response.data.message);
  95. this.message.showMsg(req, res, axios, this.util.wpFetchErrMsg);
  96. return false;
  97. });
  98. }
  99. loadTimeLogDlg(req, res, axios) {
  100. this.wpId = req.body.context.selected_option.slice(this.optLen);
  101. axios({
  102. url: 'time_entries/form',
  103. method: 'post',
  104. baseURL: this.opURL,
  105. auth: this.opAuth,
  106. data: {
  107. "_links": {
  108. "workPackage": {
  109. "href": "/api/v3/work_packages/" + this.wpId
  110. }
  111. }
  112. }
  113. }).then((response) => {
  114. console.log("Activities obtained from OP: %o", response.data);
  115. this.customFieldForBillableHours = this.getCustomFieldForBillableHours(response.data._embedded.schema, 'billable');
  116. console.log("Custom field for billable hours is: ", this.customFieldForBillableHours);
  117. let activityOptArray = [];
  118. response.data._embedded.schema.activity._embedded.allowedValues.forEach(element => {
  119. activityOptArray.push({
  120. "text": element.name,
  121. "value": "opt" + element.id
  122. });
  123. });
  124. let logTimeDlgJSON = JSON.stringify(this.util.getLogTimeDlgObj(req.body.trigger_id, this.intURL, activityOptArray));
  125. console.log("logTimeDlgJSON: " + logTimeDlgJSON);
  126. axios.post(this.mmURL + 'actions/dialogs/open', logTimeDlgJSON).then(response => {
  127. console.log("Response from projects dialog: ", response);
  128. let updateMsg = JSON.stringify({
  129. "update": {
  130. "message": "Updated!",
  131. "props": {}
  132. },
  133. "ephemeral_text": "Opening time log dialog..."
  134. });
  135. res.type('application/json').send(updateMsg).status(200);
  136. }).catch(error => {
  137. console.log("Error while creating projects dialog", error.response.data.message);
  138. this.message.showMsg(req, res, axios, this.util.dlgCreateErrMsg);
  139. return false;
  140. });
  141. }).catch((error) => {
  142. console.log("Error in fetching activities: ", error);
  143. if(error.response.status === 403) {
  144. this.message.showMsg(req, res, axios, this.util.activityFetchErrMsg);
  145. }
  146. else {
  147. this.message.showMsg(req, res, axios, this.util.genericErrMsg);
  148. }
  149. return false;
  150. });
  151. }
  152. handleSubmission(req, res, axios) {
  153. if (req.body.submission) {
  154. const { spent_on, comments, spent_hours, billable_hours, activity} = req.body.submission;
  155. console.log("Submission data: ");
  156. console.log("spent_on: ", spent_on, " comments: ", comments, " spent_hours: ", spent_hours);
  157. console.log(" billable_hours: ", billable_hours, " activity: ", activity);
  158. if (this.util.checkDate(this.moment, spent_on)) {
  159. if (this.util.checkHours(spent_hours, parseFloat(billable_hours))) {
  160. let axiosData = {
  161. "_links": {
  162. "project": {
  163. "href": "/api/v3/projects/" + this.projectId
  164. },
  165. "activity": {
  166. "href": "/api/v3/time_entries/activities/" + activity.slice(this.optLen)
  167. },
  168. "workPackage": {
  169. "href": "/api/v3/work_packages/" + this.wpId
  170. }
  171. },
  172. "hours": this.moment.duration(spent_hours*60, "m"),
  173. "comment": {
  174. "raw": comments
  175. },
  176. "spentOn": spent_on,
  177. [this.customFieldForBillableHours]: billable_hours
  178. }
  179. /*log time log data to open project*/
  180. axios({
  181. url: 'time_entries',
  182. method: 'post',
  183. baseURL: this.opURL,
  184. data: axiosData,
  185. auth: this.opAuth
  186. }).then((response) => {
  187. console.log("Time logged. Save response: %o", response.data);
  188. this.message.showMsg(req, res, axios, "Time entry ID - " + response.data.id + this.util.timeLogSuccessMsg);
  189. return true;
  190. }).catch((error) => {
  191. console.log("OP time entries create error: %o", error.response.data.message);
  192. if (error.response.status === 403) {
  193. this.message.showMsg(req, res, axios, this.util.timeLogForbiddenMsg);
  194. }
  195. else {
  196. this.message.showMsg(req, res, axios, this.util.timeLogFailMsg);
  197. }
  198. return false;
  199. });
  200. }
  201. else {
  202. console.log("Billable hours incorrect: ", billable_hours);
  203. this.message.showMsg(req, res, axios, this.util.billableHoursErrMsg);
  204. return false;
  205. }
  206. }
  207. else {
  208. console.log("Date incorrect: ", spent_on);
  209. this.message.showMsg(req, res, axios, this.util.dateErrMsg);
  210. return false;
  211. }
  212. }
  213. else {
  214. console.log("empty submission");
  215. this.message.showMsg(req, res, axios, this.util.wpDtlEmptyMsg);
  216. return false;
  217. }
  218. }
  219. getCustomFieldForBillableHours(schema, targetValue, parentKey = '') {
  220. for (let key in schema) {
  221. if (schema.hasOwnProperty(key)) {
  222. const value = schema[key];
  223. if (typeof value === 'string' && value.toLowerCase().includes(targetValue.toLowerCase())) {
  224. const words = value.toLowerCase().split(' ');
  225. if (words.includes(targetValue.toLowerCase())) {
  226. return parentKey;
  227. }
  228. }
  229. if (typeof value === 'object') {
  230. const nestedKey = this.getCustomFieldForBillableHours(value, targetValue, key);
  231. if (nestedKey !== null) {
  232. return nestedKey;
  233. }
  234. }
  235. }
  236. }
  237. return 'customField1';
  238. }
  239. getTimeLog(req, res, axios, mode = '') {
  240. axios({
  241. url: 'time_entries?sortBy=[["createdAt", "desc"]]',
  242. method: 'get',
  243. baseURL: this.opURL,
  244. auth: this.opAuth
  245. }).then((response) => {
  246. console.log("Time entries obtained from OP: %o", response.data);
  247. let timeLogArray = [];
  248. response.data._embedded.elements.forEach(element => {
  249. timeLogArray.push({
  250. "spentOn": element.spentOn,
  251. "project": element._links.project.title,
  252. "workPackage": element._links.workPackage.title,
  253. "activity": element._links.activity.title,
  254. "loggedHours": this.moment.utc(this.moment.duration(element.hours, "hours").asMilliseconds()).format("H [hours] m [minutes]"),
  255. "billableHours": this.moment.utc(this.moment.duration(element[this.customFieldForBillableHours], "hours").asMilliseconds()).format("H [hours] m [minutes]"),
  256. "comment": element.comment.raw
  257. });
  258. });
  259. res.set('Content-Type', 'application/json').send(this.util.getTimeLogJSON(timeLogArray, mode)).status(200);
  260. }).catch((error) => {
  261. console.log("Error in getting time logs: ", error.response.data.message);
  262. this.message.showMsg(req, res, axios, this.util.timeLogFetchErrMsg);
  263. });
  264. };
  265. showTimeLogSel(req, res, axios, mode = '') {
  266. axios({
  267. url: 'time_entries?sortBy=[["createdAt", "desc"]]',
  268. method: 'get',
  269. baseURL: this.opURL,
  270. auth: this.opAuth
  271. }).then((response) => {
  272. console.log("Time entries obtained from OP: %o", response.data);
  273. let timeLogArray = [];
  274. response.data._embedded.elements.forEach(element => {
  275. timeLogArray.push({
  276. "value": "opt" + element.id,
  277. "text": element.comment.raw + '-' + element.spentOn + '-' + this.moment.utc(this.moment.duration(element.hours, "hours").asMilliseconds()).format("H [hours] m [minutes]") + '-' + element._links.workPackage.title + '-' + element._links.activity.title + '-' + element._links.project.title
  278. });
  279. });
  280. res.set('Content-Type', 'application/json').send(this.util.getTimeLogOptJSON(this.intURL, timeLogArray, "cnfDelTimeLog", mode)).status(200);
  281. }).catch((error) => {
  282. console.log("Error in getting time logs: ", error.response.data.message);
  283. this.message.showMsg(req, res, axios, this.util.timeLogFetchErrMsg);
  284. });
  285. };
  286. cnfDelTimeLog(req, res) {
  287. this.timeLogId = req.body.context.selected_option.slice(this.optLen);
  288. res.set('Content-Type', 'application/json').send(JSON.stringify(this.util.getCnfDelBtnJSON(this.intURL + "delTimeLog", this.util.cnfDelTimeLogMsg, "delSelTimeLog"))).status(200);
  289. }
  290. delTimeLog(req, res, axios) {
  291. axios({
  292. url: 'time_entries/' + this.timeLogId,
  293. method: 'delete',
  294. headers: {'Content-Type': 'application/json'},
  295. baseURL: this.opURL,
  296. auth: this.opAuth
  297. }).then((response) => {
  298. console.log("Time entry deleted. Response %o", response.data);
  299. res.set('Content-Type', 'application/json').send(JSON.stringify(this.util.getTimeLogDelMsgJSON(this.util.timeLogDelMsg, this.intURL))).status(200);
  300. }).catch((error) => {
  301. console.log("Error in time entry deletion: ", error.response.data.message);
  302. this.message.showMsg(req, res, axios, this.util.timeLogDelErrMsg);
  303. return false;
  304. });
  305. };
  306. createWP(req, res, axios) {
  307. this.projectId = req.body.context.selected_option.slice(this.optLen);
  308. axios({
  309. url: 'types',
  310. method: 'get',
  311. baseURL: this.opURL,
  312. auth: this.opAuth
  313. }).then((response) => {
  314. console.log("Response from get types: ", response.data);
  315. let typeArray = [];
  316. response.data._embedded.elements.forEach(element => {
  317. typeArray.push({
  318. "text": element.name,
  319. "value": "opt" + element.id
  320. });
  321. });
  322. axios({
  323. url: 'projects/' + this.projectId + '/available_assignees',
  324. method: 'get',
  325. baseURL: this.opURL,
  326. auth: this.opAuth
  327. }).then((response) => {
  328. console.log("Response from get available assignees: ", response.data);
  329. let assigneeArray = [];
  330. response.data._embedded.elements.forEach(element => {
  331. assigneeArray.push({
  332. "text": element.name,
  333. "value": "opt" + element.id
  334. });
  335. });
  336. let wpCreateDlgJSON = this.util.getWpCreateJSON(req.body.trigger_id, this.intURL, typeArray, assigneeArray);
  337. console.log("WpCreateDlgJSON: ", wpCreateDlgJSON);
  338. axios.post(this.mmURL + 'actions/dialogs/open', wpCreateDlgJSON).then(response => {
  339. console.log("Response from wp create dialog: ", response.data);
  340. let updateMsg = JSON.stringify({
  341. "update": {
  342. "message": "Updated!",
  343. "props": {}
  344. },
  345. "ephemeral_text": "Opening work package create dialog..."
  346. });
  347. res.type('application/json').send(updateMsg).status(200);
  348. }).catch(error => {
  349. console.log("Error while creating work package dialog", error);
  350. this.message.showMsg(req, res, axios, this.util.dlgCreateErrMsg);
  351. });
  352. });
  353. }).catch((error) => {
  354. console.log("Error in fetching types: ", error.response.data.message);
  355. this.message.showMsg(req, res, axios, this.util.typeFetchErrMsg);
  356. return false;
  357. });
  358. };
  359. saveWP(req, res, axios) {
  360. if (req.body.submission) {
  361. const { subject, type, assignee, notify } = req.body.submission;
  362. console.log("Submission data: ");
  363. console.log("subject: ", subject, " type: ", type, " assignee: ", assignee, " notify: ", notify);
  364. let postWpData = {
  365. "subject": subject,
  366. "_links": {
  367. "project": {
  368. "href": "/api/v3/projects/" + this.projectId
  369. },
  370. "type": {
  371. "href": "/api/v3/types/" + type.slice(this.optLen)
  372. }
  373. }
  374. };
  375. if (assignee !== null) {
  376. postWpData.assignee = {
  377. "href": "/api/v3/users/" + assignee.slice(this.optLen)
  378. }
  379. }
  380. /*save work-package to OpenProject*/
  381. axios({
  382. url: 'work_packages?notify=' + notify,
  383. method: 'post',
  384. baseURL: this.opURL,
  385. data: postWpData,
  386. auth: this.opAuth
  387. }).then(response => {
  388. console.log("Work package saved. Save response: %o", response.data);
  389. this.message.showMsg(req, res, axios, "Work package ID - " + response.data.id + this.util.saveWPSuccessMsg);
  390. return true;
  391. }).catch((error) => {
  392. console.log("OP WP entries create error: %o", error.response.data.message);
  393. if (error.response.status === 403) {
  394. this.message.showMsg(req, res, axios, this.util.wpCreateForbiddenMsg);
  395. }
  396. else if (error.response.status === 422) {
  397. this.message.showMsg(req, res, axios, this.util.wpTypeErrMsg);
  398. }
  399. else {
  400. this.message.showMsg(req, res, axios, this.util.genericErrMsg);
  401. }
  402. return false;
  403. });
  404. }
  405. else if (req.body.cancelled) {
  406. console.log("Dialog cancelled.");
  407. this.message.showMsg(req, res, axios, this.util.dlgCancelMsg);
  408. }
  409. else {
  410. console.log("Empty request body.");
  411. this.message.showMsg(req, res, axios, this.util.genericErrMsg);
  412. }
  413. };
  414. showDelWPSel(req, res, axios, mode = '') {
  415. axios({
  416. url: '/work_packages?sortBy=[["created_at","desc"]]',
  417. method: 'get',
  418. baseURL: this.opURL,
  419. auth: this.opAuth
  420. }).then((response) => {
  421. console.log("WP obtained from OP: %o", response.data);
  422. let wpOptArray = [];
  423. response.data._embedded.elements.forEach(element => {
  424. wpOptArray.push({
  425. "text": element.subject,
  426. "value": "opt" + element.id
  427. });
  428. });
  429. let wpOptJSON = this.util.getWpOptJSON(this.intURL, wpOptArray, "cnfDelWP", mode);
  430. console.log("opt Array for WP: ", wpOptJSON);
  431. res.set('Content-Type', 'application/json').send(JSON.stringify(wpOptJSON)).status(200);
  432. }).catch((error) => {
  433. console.log("Error is show work package selection: ", error.response.data.message);
  434. this.message.showMsg(req, res, axios, this.util.wpFetchErrMsg);
  435. });
  436. }
  437. showCnfDelWP(req, res, axios) {
  438. this.wpId = req.body.context.selected_option.slice(this.optLen);
  439. res.set('Content-Type', 'application/json').send(JSON.stringify(this.util.getCnfDelBtnJSON(this.intURL+ "delWP", this.util.cnfDelWPMsg, "delWP"))).status(200);
  440. }
  441. delWP(req, res, axios) {
  442. axios({
  443. url: 'work_packages/' + this.wpId,
  444. method: 'delete',
  445. headers: {'Content-Type': 'application/json'},
  446. baseURL: this.opURL,
  447. auth: this.opAuth
  448. }).then((response) => {
  449. console.log("WP deleted. Response %o", response.data);
  450. res.set('Content-Type', 'application/json').send(JSON.stringify(this.util.getWPDelMsgJSON(this.util.wpDelMsg))).status(200);
  451. }).catch((error) => {
  452. console.log("Error in work package deletion: ", error.response.data.message);
  453. if(error.response.status === 403) {
  454. this.message.showMsg(req, res, axios, this.util.wpForbiddenMsg);
  455. }
  456. else {
  457. this.message.showMsg(req, res, axios, this.util.wpDelErrMsg);
  458. }
  459. return false;
  460. });
  461. }
  462. showMenuBtn(req, res, axios) {
  463. axios({
  464. url: '/users/me',
  465. method: 'get',
  466. baseURL: this.opURL,
  467. auth: this.opAuth
  468. }).then((response) => {
  469. this.currentUser = response.data.firstName;
  470. res.set('Content-Type', 'application/json').send(JSON.stringify(this.util.getMenuBtnJSON(this.intURL, this.currentUser))).status(200);
  471. });
  472. }
  473. notifyChannel(req, res, axios) {
  474. let action = req.body.action;
  475. let notificationType = action.split(':')[0];
  476. const {createdAt, updatedAt, _embedded, description, comment, fileName, identifier} = req.body[notificationType];
  477. let msg = "unknown notification";
  478. switch(notificationType) {
  479. case "project":
  480. msg = action + "-" + identifier + " at " + updatedAt;
  481. break;
  482. case "work_package":
  483. msg = action + "-" + description.raw + " for " + _embedded.project.name + " at " + updatedAt + " by " + _embedded.user.name;
  484. break;
  485. case "time_entry":
  486. msg = action + "-" + comment.raw + " for " + _embedded.project.name + " at " + updatedAt + " by " + _embedded.user.name;
  487. break;
  488. case "attachment":
  489. msg = action + "-" + fileName + "-" + description.raw + " for " + " at " + createdAt + " by " + _embedded.author.name;
  490. break;
  491. default:
  492. msg = "default notification";
  493. break;
  494. }
  495. console.log("Notification message: ", msg);
  496. this.message.showNotification(res, axios, msg);
  497. }
  498. showByeMsg(req, res, mode) {
  499. let byeMsg = {
  500. "message": "Logged out. :wave:",
  501. "props": {}
  502. };
  503. if(mode === 'update') {
  504. byeMsg = {
  505. "update": byeMsg
  506. };
  507. }
  508. else {
  509. byeMsg.text = byeMsg.message;
  510. }
  511. res.type('application/json').send(JSON.stringify(byeMsg)).status(200);
  512. }
  513. notificationSubscribe(req, res, axios) {
  514. this.message.showMsg(req, res, axios, this.util.subscribeMsg);
  515. }
  516. }
  517. module.exports = UIactions;