taskkanban.ui.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  1. $('#kanbanList').on('enterFullscreen', () => {$('#kanbanList > div').css('height', '100%'); window.hideAllAction();});
  2. $('#kanbanList').on('exitFullscreen', () => {$('#kanbanList > div').css('height', 'calc(100vh - 120px)')});
  3. searchValue = '';
  4. const kanbanDropRules =
  5. {
  6. story:
  7. {
  8. backlog: ['ready', 'backlog'],
  9. ready: ['backlog', 'ready'],
  10. tested: ['verified', 'rejected'],
  11. verified: ['tested', 'pending', 'released'],
  12. pending: ['verified', 'released'],
  13. released: ['verified', 'closed'],
  14. closed: ['released']
  15. },
  16. bug:
  17. {
  18. 'unconfirmed': ['confirmed', 'fixing', 'fixed'],
  19. 'confirmed': ['fixing', 'fixed'],
  20. 'fixing': ['fixed'],
  21. 'fixed': ['testing', 'tested', 'fixing'],
  22. 'testing': ['tested', 'closed', 'fixing'],
  23. 'tested': ['closed', 'fixing'],
  24. 'closed': ['fixing'],
  25. },
  26. task:
  27. {
  28. 'wait': ['developing', 'developed', 'canceled'],
  29. 'developing': ['developed', 'pause', 'canceled'],
  30. 'developed': ['developing', 'closed'],
  31. 'pause': ['developing'],
  32. 'canceled': ['developing', 'closed'],
  33. 'closed': ['developing'],
  34. },
  35. risk:
  36. {
  37. 'active': ['track', 'hangup', 'canceled', 'closed'],
  38. 'track': ['active', 'hangup', 'canceled', 'closed'],
  39. 'hangup': ['active', 'track', 'canceled', 'closed'],
  40. 'canceled': ['active', 'track', 'closed'],
  41. 'closed': ['active', 'track'],
  42. }
  43. };
  44. window.changeBrowseType = function()
  45. {
  46. const type = $('.c-type [name=type]').val();
  47. loadPage($.createLink('execution', 'taskKanban', "executionID=" + executionID + '&type=' + type));
  48. };
  49. window.changeGroupBy = function()
  50. {
  51. const group = $('.c-group [name=group]').val();
  52. const type = $('.c-type [name=type]').val();
  53. loadPage($.createLink('execution', 'taskKanban', 'executionID=' + executionID + '&type=' + type + '&orderBy=order_asc' + '&groupBy=' + group));
  54. };
  55. window.changeBugProduct = function()
  56. {
  57. const productID = $('[name=productName]').val();
  58. if(productID) $('#batchCreateBugButton').attr('href', $.createLink('bug', 'batchCreate', 'productID=' + productID + '&branch=&executionID=' + executionID));
  59. };
  60. window.changeShowParent = function()
  61. {
  62. const showParent = $('[name=showParent]').prop('checked') ? 1 : 0;
  63. $.cookie.set('showParent', showParent, {expires:config.cookieLife, path:config.webRoot});
  64. const type = $('.c-type [name=type]').val();
  65. const group = $('.c-group [name=group]').val();
  66. let link = $.createLink('execution', 'taskKanban', 'executionID=' + executionID + '&type=' + type);
  67. if(typeof group != 'undefined') link = $.createLink('execution', 'taskKanban', 'executionID=' + executionID + '&type=' + type + '&orderBy=order_asc' + '&groupBy=' + group);
  68. loadPage(link);
  69. };
  70. window.linkPlanStory = function()
  71. {
  72. const planID = $('[name=plan]').val();
  73. if(planID)
  74. {
  75. var param = "&param=executionID=" + executionID + ",browseType=" + browseType + ",orderBy=id_asc,groupBy=" + groupBy;
  76. $.ajaxSubmit({url: $.createLink('execution', 'importPlanStories', 'executionID=' + executionID + '&planID=' + planID + '&productID=0&fromMethod=taskKanban&extra=' + param)});
  77. }
  78. };
  79. window.toggleSearchBox = function()
  80. {
  81. $('#taskKanbanSearch').toggle();
  82. if($('#taskKanbanSearch').css('display') == 'block')
  83. {
  84. $(".querybox-toggle").css("color", "#0c64eb");
  85. }
  86. else
  87. {
  88. $(".querybox-toggle").css("color", "#3c495c");
  89. $('#taskKanbanSearchInput').attr('value', '');
  90. searchCards('');
  91. }
  92. };
  93. window.getLane = function(lane)
  94. {
  95. /* 看板只有一个泳道时,铺满全屏。 */
  96. if(laneCount < 2) lane.minHeight = window.innerHeight - 235;
  97. }
  98. /*
  99. * 构造看板泳道上的操作按钮。
  100. * Build action buttons on the kanban lane.
  101. */
  102. window.getLaneActions = function(lane)
  103. {
  104. return [{
  105. type: 'dropdown',
  106. icon: 'ellipsis-v',
  107. caret: false,
  108. }];
  109. }
  110. window.getCol = function(col)
  111. {
  112. /* 计算WIP。*/
  113. if(['story', 'epic', 'requirement', 'parentStory'].includes(col.group) && ERURColumn.includes(col.type)) return;
  114. const limit = col.limit == -1 ? "<i class='icon icon-md icon-infinite'></i>" : col.limit;
  115. const cards = col.cards;
  116. col.subtitleClass = 'ml-1';
  117. let wip = `(${cards}/${limit})`;
  118. if(col.limit != -1 && cards > col.limit)
  119. {
  120. col.subtitleClass += ' text-danger';
  121. wip += ' <i class="icon icon-exclamation-sign" data-toggle="tooltip" data-title="' + kanbanLang.limitExceeded + '"></i>';
  122. }
  123. col.subtitle = {html: wip};
  124. }
  125. window.getColActions = function(col)
  126. {
  127. let actionList = [];
  128. /* 父列不需要创建卡片相关的操作按钮。 */
  129. if(col.parent != '-1' && (['backlog', 'unconfirmed', 'wait', 'active'].includes(col.type)))
  130. {
  131. let cardActions = buildColCardActions(col);
  132. if(cardActions.length > 0) actionList.push({type:'dropdown', icon:'expand-alt text-primary', caret:false, items:cardActions});
  133. if(cardActions.length == 0 && ((col.type == 'backlog' && !priv.canCreateStory && productID == '0') || (col.type == 'unconfirmed' && !priv.canCreateBug && productID == '0'))) actionList.push({icon:'expand-alt text-primary', 'data-on': 'click', 'data-do': "zui.Modal.alert('" + needLinkProducts + "')"});
  134. }
  135. actionList.push({type:'dropdown', icon:'ellipsis-v', caret:false, items:buildColActions(col)});
  136. return actionList;
  137. }
  138. /*
  139. * 构造看板上的创建卡片相关操作按钮。
  140. * Build create card related action buttons on the kanban.
  141. */
  142. window.buildColCardActions = function(col)
  143. {
  144. let actions = [];
  145. if(col.group == 'parentStory') return actions;
  146. if(col.type == 'backlog')
  147. {
  148. if(priv.canCreateStory) actions.push({text: storyLang.create, url:$.createLink('story', 'create', 'productID=' + productID + '&branch=0&moduleID=0&storyID=0&objectID=' + executionID), 'data-toggle': 'modal', 'data-size': 'lg'});
  149. if(priv.canBatchCreateStory) actions.push({text: executionLang.batchCreateStory, url: $.createLink('story', 'batchcreate', 'productID=' + productID + '&branch=0&moduleID=0&storyID=0&executionID=' + executionID), 'data-toggle': 'modal', 'data-size': 'lg'});
  150. if(priv.canLinkStory) actions.push({text: executionLang.linkStory, url: $.createLink('execution', 'linkStory', 'executionID=' + executionID), 'data-toggle': 'modal', 'data-size': 'lg'});
  151. if(priv.canLinkStoryByPlan) actions.push({text: executionLang.linkStoryByPlan, url: '#linkStoryByPlan', 'data-toggle': 'modal', 'data-size': 'sm'});
  152. }
  153. else if(col.type == 'unconfirmed')
  154. {
  155. if(priv.canCreateBug) actions.push({text: bugLang.create, url: $.createLink('bug', 'create', 'productID=' + productID + '&moduleID=0&extra=executionID=' + executionID), 'data-toggle': 'modal', 'data-size': 'lg'});
  156. if(priv.canBatchCreateBug)
  157. {
  158. if(productNum > 1) actions.push({text: bugLang.batchCreate, url: '#batchCreateBug', 'data-toggle': 'modal', 'data-size': 'lg'});
  159. else actions.push({text: bugLang.batchCreate, url: $.createLink('bug', 'batchcreate', 'productID=' + productID + '&moduleID=0&executionID=' + executionID), 'data-toggle': 'modal', 'data-size': 'lg'});
  160. }
  161. }
  162. else if(col.type == 'wait')
  163. {
  164. if(priv.canCreateTask) actions.push({text: taskLang.create, url: $.createLink('task', 'create', 'executionID=' + executionID), 'data-toggle': 'modal', 'data-size': 'lg'});
  165. if(priv.canBatchCreateTask) actions.push({text: taskLang.batchCreate, url: $.createLink('task', 'batchcreate', 'executionID=' + executionID), 'data-toggle': 'modal', 'data-size': 'lg'});
  166. if(priv.canImportBug && canImportBug) actions.push({text: executionLang.importBug, url: $.createLink('execution', 'importBug', 'executionID=' + executionID), 'data-toggle': 'modal', 'data-size': 'lg'});
  167. }
  168. else if(col.type == 'active')
  169. {
  170. if(priv.canCreateRisk) actions.push({text: riskLang.create, url: $.createLink('risk', 'create', 'executionID=' + executionID + '&from=execution'), 'data-toggle': 'modal', 'data-size': 'lg'});
  171. if(priv.canBatchCreateRisk) actions.push({text: riskLang.batchCreateRisk, url: $.createLink('risk', 'batchcreate', 'executionID=' + executionID + '&from=execution'), 'data-toggle': 'modal', 'data-size': 'lg'});
  172. }
  173. return actions;
  174. }
  175. /*
  176. * 构造看板上的创建列、拆分列等操作按钮。
  177. * Build create column, split column and other action buttons on the kanban.
  178. */
  179. window.buildColActions = function(col)
  180. {
  181. let actions = [];
  182. if(priv.canEditName && col.actionList && col.actionList.includes('setColumn')) actions.push({text: kanbanLang.setColumn, url: $.createLink('kanban', 'setColumn', `columnID=${col.id}&executionID=${executionID}&from=RDKanban`), 'data-toggle': 'modal', 'icon': 'edit'});
  183. if(priv.canSetWIP && col.actionList && col.actionList.includes('setWIP') && !(['story', 'epic', 'requirement', 'parentStory'].includes(col.group) && ERURColumn.includes(col.type))) actions.push({text: kanbanLang.setWIP, url: $.createLink('kanban', 'setWIP', `columnID=${col.id}&executionID=${executionID}&from=RDKanban`), 'data-toggle': 'modal', 'icon': 'alert'});
  184. return actions;
  185. }
  186. /**
  187. * 渲染卡片内容。
  188. * Render card content.
  189. */
  190. window.getItem = function(info)
  191. {
  192. let begin = info.item.begin;
  193. let end = info.item.end;
  194. let beginAndEnd = '';
  195. let assignLink = '';
  196. let assignedTo = '';
  197. let avatar = "<span class='avatar rounded-full size-xs ml-1 bg-gray-100 text-canvas' title='" + noAssigned + "'><i class='icon icon-person'></i></span>";
  198. info.item.originalTitle = info.item.title;
  199. if(begin < '1970-01-01' && end > '1970-01-01')
  200. {
  201. beginAndEnd = end + ' ' + cardLang.deadlineAB;
  202. }
  203. else if(end < '1970-01-01' && begin > '1970-01-01')
  204. {
  205. beginAndEnd = begin + ' ' + cardLang.beginAB;
  206. }
  207. else if(begin > '1970-01-01' && end > '1970-01-01')
  208. {
  209. beginAndEnd = formatDate(begin) + ' ~ ' + formatDate(end);
  210. }
  211. if(info.item.assignedTo && typeof userList[info.item.assignedTo] != 'undefined')
  212. {
  213. user = userList[info.item.assignedTo];
  214. assignedTo = user.realname;
  215. userAvatar = user.avatar ? "<img src='" + user.avatar + "' />" : assignedTo.slice(-2).toUpperCase();
  216. avatar = "<span class='avatar rounded-full size-xs ml-1 primary nowrap' title=" + assignedTo + '>' + userAvatar + '</span>';
  217. }
  218. if(info.laneInfo.type == 'story' && priv.canAssignStory) assignLink = $.createLink('story', 'assignto', 'id=' + info.item.id + '&kanbanGroup=default&from=taskkanban');
  219. if(info.laneInfo.type == 'bug' && priv.canAssignBug) assignLink = $.createLink('bug', 'assignto', 'id=' + info.item.id + '&kanbanGroup=default&from=taskkanban');
  220. if(info.laneInfo.type == 'risk' && priv.canAssignRisk) assignLink = $.createLink('risk', 'assignto', 'id=' + info.item.id + '&kanbanGroup=default&from=taskkanban');
  221. if(info.laneInfo.type == 'task' && priv.canAssignTask && info.item.canAssignTo) assignLink = $.createLink('task', 'assignto', 'executionID=' + executionID + '&id=' + info.item.id + '&kanbanGroup=default&from=taskkanban');
  222. if(assignLink)
  223. {
  224. avatar = "<a href='" + assignLink + "' data-toggle='modal'>" + avatar + "</a>";
  225. }
  226. else if(['parentStory', 'requirement', 'epic'].includes(info.laneInfo.type))
  227. {
  228. avatar = '<div class="avatar child-item rounded-full size-xs ml-1" title="' + storyLang.children + '" style="background: #ccc; color: #fff; cursor: pointer;"><i class="icon icon-split"></i></div>';
  229. }
  230. if(info.laneInfo.type == 'risk')
  231. {
  232. var content = `
  233. <div class='flex items-center'>
  234. <span class='text-gray mr-1'>#${info.item.id}</span>
  235. <span class='pri-${info.item.priLevel}'>${info.item.pri}</span>
  236. <span class='ml-1'>${info.item.strategy}</span>
  237. <span class='date ml-1'>${beginAndEnd}</span>
  238. <div class='flex-1 flex justify-end'>${avatar}</div>
  239. </div>
  240. `;
  241. }
  242. else
  243. {
  244. var content = `
  245. <div class='flex items-center'>
  246. <span class='text-gray mr-1'>#${info.item.id}</span>
  247. <span class='pri-${info.item.pri}'>${info.item.pri}</span>
  248. <span class='date ml-1'>${beginAndEnd}</span>
  249. <div class='flex-1 flex justify-end'>${avatar}</div>
  250. </div>
  251. `;
  252. }
  253. if(searchValue != '')
  254. {
  255. info.item.title = info.item.title.replaceAll(searchValue, "<span class='text-danger'>" + searchValue + "</span>");
  256. info.item.title = {html: info.item.title};
  257. }
  258. if(info.laneInfo.type == 'task')
  259. {
  260. let label = '';
  261. if(info.item.isParent == '1')
  262. {
  263. label = "<span class='label gray-pale rounded p-0 size-sm whitespace-nowrap'>" + parentAB + "</span> ";
  264. }
  265. else if(info.item.parent > 0)
  266. {
  267. label = "<span class='label gray-pale rounded p-0 size-sm whitespace-nowrap'>" + childrenAB + "</span> ";
  268. }
  269. if(typeof info.item.title == 'string') info.item.title = {html: info.item.title};
  270. if(label && typeof info.item.title == 'object') info.item.title.html = label + info.item.title.html;
  271. }
  272. if(['story', 'epic', 'requirement', 'parentStory'].includes(info.laneInfo.type))
  273. {
  274. info.item.titleUrl = $.createLink('execution', 'storyView', `id=${info.item.id}&executionID=${executionID}`);
  275. }
  276. else if(info.laneInfo.type == 'task' || info.laneInfo.type == 'bug')
  277. {
  278. info.item.titleUrl = $.createLink(info.laneInfo.type, 'view', `id=${info.item.id}`);
  279. }
  280. else if(info.laneInfo.type == 'risk')
  281. {
  282. info.item.titleUrl = $.createLink(info.laneInfo.type, 'view', `id=${info.item.id}&from=execution`);
  283. }
  284. info.item.titleAttrs = {'data-toggle': 'modal', 'data-size': 'lg', 'title': info.item.originalTitle, 'class': 'card-title clip'};
  285. info.item.content = {html: content};
  286. if(info.item.color && info.item.color != '#fff') info.item.className = 'color-' + info.item.color.replace('#', '');
  287. }
  288. window.getItemActions = function(item)
  289. {
  290. const actionList = typeof item.actionList == 'object' ? Object.values(item.actionList) : item.actionList;
  291. if(actionList.length)
  292. {
  293. return [{
  294. type: 'dropdown',
  295. icon: 'ellipsis-v',
  296. caret: false,
  297. items: buildCardActions(item),
  298. }];
  299. }
  300. }
  301. window.buildCardActions = function(item)
  302. {
  303. let actions = [];
  304. const actionList = typeof item.actionList == 'object' ? Object.values(item.actionList) : item.actionList;
  305. actionList.forEach(action =>
  306. {
  307. actionMap = {'text': action.label, 'icon': action.icon, 'url': action.url};
  308. if(typeof(action.confirm) != 'undefined') actionMap['data-confirm'] = action.confirm;
  309. if(typeof(action.modal) != 'undefined') actionMap['data-toggle'] = 'modal';
  310. if(typeof(action.size) != 'undefined') actionMap['data-size'] = action.size;
  311. if(typeof(action.modal) == 'undefined') actionMap['innerClass'] = 'ajax-submit';
  312. actions.push(actionMap);
  313. });
  314. return actions;
  315. }
  316. window.canDrop = function(dragInfo, dropInfo)
  317. {
  318. if(!dragInfo || $.getFullscreenElement()) return false;
  319. const fromColumn = this.getCol(dragInfo.col);
  320. const toColumn = this.getCol(dropInfo.col);
  321. const lane = this.getLane(dropInfo.lane);
  322. const laneType = lanePairs[lane.id];
  323. const fromColType = colPairs[fromColumn.id];
  324. const toColType = colPairs[toColumn.id];
  325. if(!fromColumn || !lane) return false;
  326. if(dragInfo.type == 'item' && dragInfo.item.group == 'task' && dragInfo.item.isParent == '1') return false; // 父任务不可拖动。
  327. if(priv.canSortCards && dropInfo.type == 'item' && (dropInfo.col != dragInfo.item.col || dropInfo.lane != dragInfo.item.lane)) return false;
  328. if(!priv.canSortCards && dropInfo.type == 'item') return false;
  329. /* 卡片可在同组内拖动。 */
  330. if(dragInfo.lane != dropInfo.lane) return false;
  331. if(dragInfo.item.group != toColumn.group) return false;
  332. if(dropInfo.type != 'item')
  333. {
  334. if(!canChangeObject) return false;
  335. let kanbanRules = kanbanDropRules[laneType];
  336. let colRules = typeof kanbanRules[fromColType] == 'undefined' ? null : kanbanRules[fromColType];
  337. if(!colRules) return false;
  338. if(!colRules.includes(toColType)) return false;
  339. }
  340. }
  341. window.onDrop = function(changes, dropInfo)
  342. {
  343. if(!dropInfo) return false;
  344. const toLaneID = dropInfo.drop.lane;
  345. const toColumn = this.getCol(dropInfo.drop.col);
  346. const laneType = lanePairs[dropInfo.drag.lane];
  347. const fromColID = dropInfo.drag.col;
  348. const toColID = dropInfo.drop.col;
  349. const fromColType = colPairs[fromColID];
  350. const toColType = colPairs[toColID];
  351. const item = dropInfo.drag.item;
  352. const objectID = item.id;
  353. let link = '';
  354. let moveCard = false;
  355. let laneID = toLaneID;
  356. if(typeof toColumn.laneName != 'undefined') laneID = toColumn.laneName;
  357. if(item.col == toColID && item.lane == toLaneID && priv.canSortCards)
  358. {
  359. link = $.createLink('kanban', 'sortCard', 'kanbanID=' + executionID + '&laneID=' + laneID + '&columnID=' + toColID + '&cards=' + dropInfo['data']['list'].join(','));
  360. $.get(link, function(){refreshKanban()})
  361. return true;
  362. }
  363. /* Task lane. */
  364. if(laneType == 'task')
  365. {
  366. if(toColType == 'developed' && (fromColType == 'developing' || fromColType == 'wait') && priv.canFinishTask && typeof item.actionList.finish != 'undefined') link = $.createLink('task', 'finish', 'taskID=' + objectID + '&extra=from=taskkanban');
  367. if(toColType == 'pause' && fromColType == 'developing' && priv.canPauseTask && typeof item.actionList.pause != 'undefined') link = $.createLink('task', 'pause', 'taskID=' + objectID + '&extra=from=taskkanban');
  368. if(toColType == 'canceled' && (fromColType == 'developing' || fromColType == 'wait' || fromColType == 'pause') && priv.canCancelTask && typeof item.actionList.cancel != 'undefined') link = $.createLink('task', 'cancel', 'taskID=' + objectID + '&cardPosition=&from=taskkanban');
  369. if(toColType == 'closed' && (fromColType == 'developed' || fromColType == 'canceled') && priv.canCloseTask) link = $.createLink('task', 'close', 'taskID=' + objectID + '&extra=from=taskkanban');
  370. if(toColType == 'developing')
  371. {
  372. if((fromColType == 'canceled' || fromColType == 'closed' || fromColType == 'developed') && priv.canActivateTask && typeof item.actionList.activate != 'undefined') link = $.createLink('task', 'activate', 'taskID=' + objectID + '&extra=&from=taskkanban');
  373. if(fromColType == 'pause' && priv.canActivateTask && typeof item.actionList.restart != 'undefined') link = $.createLink('task', 'restart', 'taskID=' + objectID + '&from=taskkanban');
  374. if(fromColType == 'wait' && priv.canStartTask && typeof item.actionList.start != 'undefined') link = $.createLink('task', 'start', 'taskID=' + objectID + '&extra=from=taskkanban');
  375. }
  376. }
  377. if(laneType == 'bug')
  378. {
  379. if(toColType == 'confirmed' && fromColType == 'unconfirmed' && priv.canConfirmBug) link = $.createLink('bug', 'confirm', 'bugID=' + objectID + '&extra=&from=taskkanban');
  380. if(toColType == 'fixed' && (fromColType == 'fixing' || fromColType == 'confirmed' || fromColType == 'unconfirmed') && priv.canResolveBug) link = $.createLink('bug', 'resolve', 'bugID=' + objectID + '&extra=&from=taskkanban');
  381. if(toColType == 'closed' && (fromColType == 'testing' || fromColType == 'tested') && priv.canCloseBug) link = $.createLink('bug', 'close', 'bugID=' + objectID + '&extra=&from=taskkanban');
  382. if(toColType == 'testing' && fromColType == 'fixed') moveCard = true;
  383. if(toColType == 'tested' && (fromColType == 'fixed' || fromColType == 'testing')) moveCard = true;
  384. if(toColType == 'fixing')
  385. {
  386. if(fromColType == 'confirmed' || fromColType == 'unconfirmed') moveCard = true;
  387. if((fromColType == 'closed' || fromColType == 'fixed' || fromColType == 'testing' || fromColType == 'tested') && priv.canActivateBug) link = $.createLink('bug', 'activate', 'bugID=' + objectID);
  388. }
  389. if(moveCard)
  390. {
  391. link = $.createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + laneID + '&toLaneID=' + laneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy);
  392. refreshKanban(link);
  393. return true;
  394. }
  395. }
  396. if(laneType == 'story')
  397. {
  398. if(toColType == 'closed' && priv.canCloseStory) link = $.createLink('story', 'close', 'storyID=' + objectID + '&from=taskkanban');
  399. if(toColType == 'ready')
  400. {
  401. if(item.status == 'draft' || item.status == 'changing' || item.status == 'reviewing')
  402. {
  403. zui.Modal.alert(executionLang.storyDragError);
  404. return false;
  405. }
  406. }
  407. if(!link)
  408. {
  409. ajaxMoveCard(objectID, fromColID, toColID, laneID, laneID);
  410. return true;
  411. }
  412. }
  413. if(laneType == 'risk')
  414. {
  415. let params = window.btoa('module=kanban&method=moveRiskCard&objectID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&laneID=' + laneID + '&execution=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy);
  416. if(toColType == 'active' && fromColType == 'track')
  417. {
  418. ajaxMoveCard(objectID, fromColID, toColID, laneID, laneID);
  419. return true;
  420. }
  421. if(toColType == 'active' && (fromColType == 'hangup' || fromColType == 'canceled' || fromColType == 'closed') && priv.canActivateRisk) link = $.createLink('risk', 'activate', 'riskID=' + objectID);
  422. if(toColType == 'track' && fromColType == 'active' && priv.canTrackRisk) link = $.createLink('risk', 'track', 'riskID=' + objectID + '&params=' + params);
  423. if(toColType == 'track' && (fromColType == 'hangup' || fromColType == 'canceled' || fromColType == 'closed') && priv.canTrackRisk) link = $.createLink('risk', 'activate', 'riskID=' + objectID + '&params=' + params);
  424. if(toColType == 'hangup' && (fromColType == 'active' || fromColType == 'track') && priv.canHangupRisk) link = $.createLink('risk', 'hangup', 'riskID=' + objectID);
  425. if(toColType == 'canceled' && (fromColType == 'active' || fromColType == 'track' || fromColType == 'hangup') && priv.canCancelRisk) link = $.createLink('risk', 'cancel', 'riskID=' + objectID);
  426. if(toColType == 'closed' && (fromColType == 'active' || fromColType == 'track' || fromColType == 'hangup' || fromColType == 'canceled') && priv.canCancelRisk) link = $.createLink('risk', 'close', 'riskID=' + objectID);
  427. }
  428. if(link) zui.Modal.open({url: link});
  429. return false;
  430. }
  431. function formatDate(inputDate)
  432. {
  433. const date = new Date(inputDate);
  434. const formattedDate = `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}`;
  435. return formattedDate;
  436. }
  437. window.fullScreen = function()
  438. {
  439. var element = document.getElementById('kanbanList');
  440. var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullscreen;
  441. if(requestMethod)
  442. {
  443. var afterEnterFullscreen = function()
  444. {
  445. $('#kanbanList').addClass('fullscreen').css('background', '#fff');
  446. window.hideAllAction();
  447. $.cookie.set('isFullScreen', 1, {expires:config.cookieLife, path:config.webRoot});
  448. };
  449. var whenFailEnterFullscreen = function()
  450. {
  451. exitFullScreen();
  452. };
  453. try
  454. {
  455. var result = requestMethod.call(element);
  456. if(result && (typeof result.then === 'function' || result instanceof window.Promise))
  457. {
  458. result.then(afterEnterFullscreen).catch(whenFailEnterFullscreen);
  459. }
  460. else
  461. {
  462. afterEnterFullscreen();
  463. }
  464. }
  465. catch (error)
  466. {
  467. whenFailEnterFullscreen(error);
  468. }
  469. }
  470. }
  471. /**
  472. * Exit full screen.
  473. *
  474. * @access public
  475. * @return void
  476. */
  477. function exitFullScreen()
  478. {
  479. $('.btn').show();
  480. $.cookie.set('isFullScreen', 0, {expires:config.cookieLife, path:config.webRoot});
  481. $('#kanbanList .card-heading a').each(function(){restoreDiv($(this));});
  482. $('#kanbanList .avatar').closest('a').each(function(){restoreDiv($(this));});
  483. }
  484. function restoreDiv(obj)
  485. {
  486. const $this = $(obj);
  487. const relatedDiv = $this.data('relatedDiv');
  488. if(relatedDiv)
  489. {
  490. relatedDiv.remove();
  491. $this.show().removeData('relatedDiv');
  492. }
  493. }
  494. document.addEventListener('fullscreenchange', function (e)
  495. {
  496. if(!document.fullscreenElement) exitFullScreen();
  497. });
  498. document.addEventListener('webkitfullscreenchange', function (e)
  499. {
  500. if(!document.webkitFullscreenElement) exitFullScreen();
  501. });
  502. document.addEventListener('mozfullscreenchange', function (e)
  503. {
  504. if(!document.mozFullScreenElement) exitFullScreen();
  505. });
  506. document.addEventListener('msfullscreenChange', function (e)
  507. {
  508. if(!document.msfullscreenElement) exitFullScreen();
  509. });
  510. window.hideAllAction = function()
  511. {
  512. $('.btn').hide();
  513. $('#kanbanList .card-heading a').each(function(){replaceDiv($(this));});
  514. $('#kanbanList .avatar').closest('a').each(function(){replaceDiv($(this));});
  515. }
  516. function replaceDiv(obj)
  517. {
  518. var $this = $(obj);
  519. var newDiv = $('<div></div>').addClass($this.attr('class')).html($this.html());
  520. $this.data('relatedDiv', newDiv).before(newDiv).hide();
  521. }
  522. /**
  523. * AJAX: move card.
  524. *
  525. * @param int $objectID
  526. * @param int $fromColID
  527. * @param int $toColID
  528. * @param int $fromLaneID
  529. * @param int $toLaneID
  530. * @access public
  531. * @return void
  532. */
  533. window.ajaxMoveCard = function(objectID, fromColID, toColID, fromLaneID, toLaneID)
  534. {
  535. var link = $.createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy);
  536. refreshKanban(link);
  537. };
  538. window.debounce = function (callback, delay)
  539. {
  540. let timer;
  541. return function() {
  542. const context = this;
  543. const args = arguments;
  544. clearTimeout(timer);
  545. timer = setTimeout(function () {
  546. callback.apply(context, args);
  547. }, delay);
  548. };
  549. };
  550. $('#taskKanbanSearchInput').on('input', debounce(function(){
  551. searchCards($(this).val());
  552. }, 500));
  553. window.searchCards = function(value, order)
  554. {
  555. searchValue = value;
  556. if(typeof order == 'undefined') order = orderBy;
  557. refreshKanban($.createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=0&browseType=" + browseType + "&groupBy=" + groupBy + '&from=taskkanban&searchValue=' + value + '&orderBy=' + order));
  558. };
  559. window.refreshKanban = function(url)
  560. {
  561. if(typeof url == 'undefined') url = $.createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=0&browseType=" + browseType + "&groupBy=" + groupBy + '&from=taskkanban&searchValue=' + searchValue + '&orderBy=' + orderBy);
  562. const $kanbanList = $('[data-zui-kanbanlist]').zui('kanbanList');
  563. let options = $kanbanList.options;
  564. $.getJSON(url, function(data)
  565. {
  566. for(const group of data)
  567. {
  568. group.getLane = window.getLane;
  569. group.getCol = window.getCol;
  570. group.getItem = window.getItem;
  571. group.canDrop = window.canDrop;
  572. group.onDrop = window.onDrop;
  573. group.minColWidth = minColWidth;
  574. group.maxColWidth = maxColWidth;
  575. group.colProps = {'actions': window.getColActions};
  576. group.itemProps = {'actions': window.getItemActions};
  577. }
  578. options.items = data;
  579. $kanbanList.render(options);
  580. });
  581. };
  582. waitDom('.c-group .picker-box .picker-single-selection', function(){this.html(kanbanLang.laneGroup + ': ' + this.attr('title'));});
  583. $(document).off('click', '#linkStoryByPlan button[type="submit"]').on('click', '#linkStoryByPlan button[type="submit"]', function()
  584. {
  585. const planID = $('[name=plan]').val();
  586. if(planID)
  587. {
  588. var param = "&param=executionID=" + executionID + ",browseType=" + browseType + ",orderBy=id_asc,groupBy=" + groupBy;
  589. $.ajaxSubmit({url: $.createLink('execution', 'importPlanStories', 'executionID=' + executionID + '&planID=' + planID + '&productID=0&fromMethod=taskKanban&extra=' + param)});
  590. }
  591. return false;
  592. })