kanban.ui.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797
  1. $(function()
  2. {
  3. if(taskToOpen)
  4. {
  5. window.waitDom('.card-list-item[z-key="' + taskToOpen + '"] .icon-checked + a', function()
  6. {
  7. $('.card-list-item[z-key="' + taskToOpen + '"] .icon-checked + a').trigger('click');
  8. });
  9. }
  10. })
  11. searchValue = '';
  12. window.getLane = function(lane)
  13. {
  14. /* 看板只有一个泳道时,铺满全屏。 */
  15. if(laneCount < 2) lane.minHeight = window.innerHeight - 235;
  16. }
  17. window.getCol = function(col)
  18. {
  19. /* 计算WIP。*/
  20. if(['story', 'epic', 'requirement', 'parentStory'].includes(col.laneType) && ERURColumn.includes(col.type)) return;
  21. const limit = col.limit == -1 ? "<i class='icon icon-md icon-infinite'></i>" : col.limit;
  22. const cards = col.cards;
  23. col.subtitleClass = 'ml-1';
  24. let wip = `(${cards} / ${limit})`;
  25. if(col.limit != -1 && cards > col.limit)
  26. {
  27. col.subtitleClass += ' text-danger';
  28. wip += ' <i class="icon icon-exclamation-sign" data-toggle="tooltip" data-title="' + kanbanLang.limitExceeded + '"></i>';
  29. }
  30. col.subtitle = {html: wip};
  31. }
  32. /*
  33. * 构造看板泳道上的操作按钮。
  34. * Build action buttons on the kanban lane.
  35. */
  36. window.getLaneActions = function(lane)
  37. {
  38. if(!lane.hasOwnProperty('actionList')) return false;
  39. return [{
  40. type: 'dropdown',
  41. icon: 'ellipsis-v',
  42. caret: false,
  43. items: [
  44. lane.actionList.includes('editLaneName') ? {text: kanbanLang.editLaneName, icon: 'edit', url: $.createLink('kanban', 'editLaneName', 'id=' + lane.id), 'data-toggle': 'modal'} : null,
  45. lane.actionList.includes('editLaneColor') ? {text: kanbanLang.editLaneColor, icon: 'color', url: $.createLink('kanban', 'editLaneColor', 'id=' + lane.id), 'data-toggle': 'modal'} : null,
  46. lane.actionList.includes('deleteLane') ? {text: kanbanLang.deleteLane, icon: 'trash', url: $.createLink('kanban', 'deleteLane', 'regionID=' + lane.region + '&id=' + lane.id), 'data-confirm': laneLang.confirmDelete, 'innerClass': 'ajax-submit'} : null,
  47. ],
  48. }];
  49. }
  50. window.getColActions = function(col)
  51. {
  52. let actionList = [];
  53. const firstCol = ['backlog', 'unconfirmed', 'wait'];
  54. if(firstCol.includes(col.type))
  55. {
  56. if(productID || col.type == 'wait')
  57. {
  58. const colCardActions = buildColCardActions(col);
  59. if(colCardActions.length)
  60. {
  61. actionList.push({
  62. type: 'dropdown',
  63. icon: 'expand-alt text-primary',
  64. caret: false,
  65. items: colCardActions,
  66. });
  67. }
  68. }
  69. else
  70. {
  71. actionList.push({
  72. type: 'ghost',
  73. icon: 'expand-alt text-primary',
  74. onClick: checkProducts
  75. });
  76. }
  77. }
  78. const colActions = buildColActions(col);
  79. if(colActions.length)
  80. {
  81. actionList.push(
  82. {
  83. type: 'dropdown',
  84. icon: 'ellipsis-v',
  85. caret: false,
  86. items: colActions,
  87. class: 'actionDrop'
  88. }
  89. );
  90. }
  91. return actionList;
  92. }
  93. window.buildColActions = function(col)
  94. {
  95. let actions = [];
  96. if(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'});
  97. if(col.actionList && col.actionList.includes('setWIP') && !(['story', 'epic', 'requirement', 'parentStory'].includes(col.laneType) && 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', 'class': 'setWIP-btn'});
  98. return actions;
  99. }
  100. window.checkProducts = function()
  101. {
  102. if(!productID) zui.Modal.alert(executionLang.needLinkProducts);
  103. }
  104. window.buildColCardActions = function(col)
  105. {
  106. let actions = [];
  107. let $group = $(`[z-key=group${col.group}]`);
  108. let laneID = 0;
  109. if($group.length) laneID = $group.find('.kanban-lane.is-first').attr('z-lane');
  110. if(col.laneType == 'parentStory') return [];
  111. if(col.type == 'backlog')
  112. {
  113. if(priv.canCreateStory)
  114. {
  115. action = {text: storyLang.create, url: $.createLink('story', 'create', 'productID=' + productID + '&branch=0&moduleID=0&storyID=0&objectID=' + executionID + '&bugID=0&planID=0&todoID=0&extra=regionID=' + col.region + ',laneID=' + laneID + ',columnID=' + col.id), 'data-toggle': 'modal', 'data-size': 'lg'};
  116. actions.push(action);
  117. }
  118. if(priv.canBatchCreateStory) actions.push({text: storyLang.batchCreate, url: productCount > 1 ? '#batchCreateStory' : $.createLink('story', 'batchCreate', 'productID=' + productID + '&branch=0&moduleID=0&storyID=0&objectID=' + executionID + '&planID=0&storyType=story&extra=regionID=' + col.region + ',laneID=' + laneID + ',columnID=' + col.id), 'data-toggle': 'modal', 'data-size' : 'lg'});
  119. if(priv.canLinkStory) actions.push({text: executionLang.linkStory, url: $.createLink('execution', 'linkStory', 'executionID=' + executionID + `&browseType=&param=0&orderBy=id_desc&recPerPage=50&pageID=1&extra=laneID=${laneID},columnID=` + col.id), 'data-toggle': 'modal', 'data-size' : 'lg'});
  120. if(priv.canLinkStoryByPlan) actions.push({text: executionLang.linkStoryByPlan, url: '#linkStoryByPlan', 'data-toggle': 'modal', 'data-size': 'sm'});
  121. }
  122. else if(col.type == 'unconfirmed')
  123. {
  124. if(priv.canCreateBug) actions.push({text: bugLang.create, url: $.createLink('bug', 'create', 'productID=' + productID + '&moduleID=0&extra=regionID=' + col.region + ',groupID=' + col.group + ',laneID=' + laneID + ',columnID=' + col.id + ',executionID=' + executionID), 'data-toggle': 'modal', 'data-size' : 'lg'});
  125. if(priv.canBatchCreateBug) actions.push({text: bugLang.batchCreate, url: productCount > 1 ? '#batchCreateBug' : $.createLink('bug', 'batchcreate', 'productID=' + productID + '&branch=all&executionID=' + executionID + '&module=0&extra=regionID=' + col.region + ',laneID=' + laneID + ',columnID=' + col.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  126. }
  127. else if(col.type == 'wait')
  128. {
  129. if(priv.canCreateTask) actions.push({text: taskLang.create, url: $.createLink('task', 'create', 'executionID=' + executionID + '&storyID=0&moduleID=0&taskID=0&todoID=0&extra=regionID=' + col.region + ',laneID=' + laneID + ',columnID=' + col.id), 'data-toggle': 'modal', 'data-size' : 'lg'});
  130. if(priv.canBatchCreateTask) actions.push({text: taskLang.batchCreate, url: $.createLink('task', 'batchcreate', 'executionID=' + executionID + '&storyID=0&moduleID=0&taskID=0&extra=regionID=' + col.region + ',laneID=' + laneID + ',columnID=' + col.id), 'data-toggle': 'modal', 'data-size' : 'lg'});
  131. if(priv.canImportBug && vision == 'rnd') actions.push({text: executionLang.importBug, url: $.createLink('execution', 'importBug', 'executionID=' + executionID + '&storyID=0&moduleID=0&taskID=0&todoID=0&extra=laneID=' + laneID + ',columnID=' + col.id), 'data-toggle': 'modal', 'data-size' : 'lg'});
  132. }
  133. return actions;
  134. }
  135. window.getItem = function(info)
  136. {
  137. const avatar = renderAvatar(info.item);
  138. info.item.titleAttrs = {};
  139. if(['story', 'parentStory'].includes(info.item.cardType))
  140. {
  141. info.item.icon = 'product';
  142. if(priv.canViewStory)
  143. {
  144. info.item.titleUrl = $.createLink('execution', 'storyView', `id=${info.item.id}&executionID=${executionID}`);
  145. info.item.titleAttrs = {'data-toggle': 'modal', 'data-size' : 'lg', 'title' : info.item.title};
  146. }
  147. const content = `
  148. <div class='flex items-center'>
  149. <span class='mr-1'>#${info.item.id}</span>
  150. <span class='pri-${info.item.pri}'>${info.item.pri}</span>
  151. <div class='flex-1 flex justify-end'>${avatar}</div>
  152. </div>
  153. `;
  154. info.item.content = {html: content};
  155. }
  156. else if(info.item.cardType == 'bug')
  157. {
  158. info.item.icon = 'bug';
  159. if(priv.canViewBug)
  160. {
  161. info.item.titleUrl = $.createLink('bug', 'view', `id=${info.item.id}`);
  162. info.item.titleAttrs = {'data-toggle': 'modal', 'data-size' : 'lg', 'title' : info.item.title};
  163. }
  164. const content = `
  165. <div class='flex items-center'>
  166. <span class='mr-1'>#${info.item.id}</span>
  167. <span class='severity' data-severity='${info.item.severity}'></span>
  168. <div class='flex-1 flex justify-end'>${avatar}</div>
  169. </div>
  170. `;
  171. info.item.content = {html: content};
  172. }
  173. else if(info.item.cardType == 'task')
  174. {
  175. info.item.icon = 'checked';
  176. if(priv.canViewTask)
  177. {
  178. info.item.titleUrl = $.createLink('task', 'view', `id=${info.item.id}`);
  179. info.item.titleAttrs = {'data-toggle': 'modal', 'data-size' : 'lg', 'title' : info.item.title};
  180. }
  181. const content = `
  182. <div class='flex items-center'>
  183. <span class='pri-${info.item.pri}'>${info.item.pri}</span>
  184. <span class='text-sm ml-2 mr-1'>${taskLang.estimateAB}</span>
  185. <span class='text-sm'>${info.item.estimate}h</span>
  186. <div class='flex-1 flex justify-end'>${avatar}</div>
  187. </div>
  188. `;
  189. info.item.content = {html: content};
  190. }
  191. if(info.item.color && info.item.titleAttrs) info.item.titleAttrs.style = {'color': info.item.color};
  192. if(searchValue != '')
  193. {
  194. info.item.title = info.item.title.replaceAll(searchValue, "<span class='text-danger'>" + searchValue + "</span>");
  195. info.item.title = {html: info.item.title};
  196. }
  197. info.item.titleAttrs.class = 'card-title clip';
  198. if(info.laneInfo.type == 'task')
  199. {
  200. let label = '';
  201. if(info.item.isParent == '1')
  202. {
  203. label = "<span class='label gray-pale rounded p-0 size-sm whitespace-nowrap'>" + parentAB + "</span> ";
  204. }
  205. else if(info.item.parent > 0)
  206. {
  207. label = "<span class='label gray-pale rounded p-0 size-sm whitespace-nowrap'>" + childrenAB + "</span> ";
  208. }
  209. if(label && typeof info.item.title == 'string') info.item.title = {html: label + info.item.title};
  210. else if(label && typeof info.item.title == 'object') info.item.title.html = label + info.item.title.html;
  211. }
  212. }
  213. window.renderAvatar = function(item)
  214. {
  215. let assignLink = '';
  216. if(item.cardType == 'parentStory')
  217. {
  218. return '<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>';
  219. }
  220. if(item.cardType == 'story' && priv.canAssignStory) assignLink = $.createLink('story', 'assignTo', "id=" + item.id);
  221. if(item.cardType == 'bug' && priv.canAssignBug) assignLink = $.createLink('bug', 'assignTo', "id=" + item.id);
  222. if(item.cardType == 'task' && priv.canAssignTask) assignLink = $.createLink('task', 'assignTo', "executionID=" + executionID + "&id=" + item.id);
  223. if(item.assignedTo.length == 0)
  224. {
  225. if(assignLink != '') return '<a href="' + assignLink + '" class="avatar rounded-full size-xs ml-1" title="' + cardLang.noAssigned + '" data-toggle="modal" style="background: #ccc; color: #fff"><i class="icon icon-person"></i></a>';
  226. return '<div class="avatar rounded-full size-xs ml-1" title="' + cardLang.noAssigned + '" style="background: #ccc"><i class="icon icon-person"></i></div>';
  227. }
  228. else
  229. {
  230. if(assignLink != '') return '<a href="' + assignLink + '" class="avatar rounded-full size-xs ml-1 primary" title="' + item.realnames + '" data-toggle="modal">' + item.avatarList[0] + '</a>';
  231. return '<div class="avatar rounded-full size-xs ml-1 primary" title="' + item.realnames + '">' + item.avatarList[0] + '</div>';
  232. }
  233. }
  234. window.getItemActions = function(item)
  235. {
  236. let actions = [];
  237. if(item.cardType == 'story') actions = buildStoryActions(item);
  238. else if(item.cardType == 'parentStory') actions = buildParentStoryActions(item);
  239. else if(item.cardType == 'bug') actions = buildBugActions(item);
  240. else if(item.cardType == 'task') actions = buildTaskActions(item);
  241. if(actions.length)
  242. {
  243. return [{
  244. type: 'dropdown',
  245. icon: 'ellipsis-v',
  246. caret: false,
  247. items: actions
  248. }];
  249. }
  250. }
  251. window.buildParentStoryActions = function(item)
  252. {
  253. let actions = [];
  254. if(priv.canEditStory) actions.push({text: storyLang.edit, icon: 'edit', url: $.createLink('story', 'edit', 'storyID=' + item.id + '&kanbanGroup=' + groupBy), 'data-toggle': 'modal', 'data-size': 'lg'});
  255. if(priv.canChangeStory && item.status == 'active') actions.push({text: storyLang.change, icon: 'change', url: $.createLink('story', 'change', 'storyID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  256. if(priv.canActivateStory && item.status == 'closed') actions.push({text: executionLang.activate, icon: 'magic', url: $.createLink('story', 'activate', 'storyID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  257. if(priv.canUnlinkStory) actions.push({text: executionLang.unlinkStory, icon: 'unlink', url: $.createLink('execution', 'unlinkStory', 'executionID=' + executionID + '&storyID=' + item.id + '&confirm=no&from=' + '&laneID=' + item.lane + '&columnID=' + item.column), 'innerClass' : 'ajax-submit'});
  258. if(priv.canDeleteStory) actions.push({text: storyLang.delete, icon: 'trash', url: $.createLink('story', 'delete', 'storyID=' + item.id), 'innerClass': 'ajax-submit'});
  259. return actions;
  260. }
  261. window.buildStoryActions = function(item)
  262. {
  263. let actions = [];
  264. if(priv.canEditStory) actions.push({text: storyLang.edit, icon: 'edit', url: $.createLink('story', 'edit', 'storyID=' + item.id + '&kanbanGroup=' + groupBy), 'data-toggle': 'modal', 'data-size': 'lg'});
  265. if(priv.canChangeStory && item.status == 'active') actions.push({text: storyLang.change, icon: 'change', url: $.createLink('story', 'change', 'storyID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  266. if(priv.canCreateTask && item.status == 'active') actions.push({text: executionLang.wbs, icon: 'plus', url: $.createLink('task', 'create', 'executionID=' + executionID + '&storyID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg', 'class': 'task-create-btn'});
  267. if(priv.canBatchCreateTask && item.status == 'active') actions.push({text: executionLang.batchWBS, icon: 'pluses', url: $.createLink('task', 'batchCreate', 'executionID=' + executionID + '&storyID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  268. if(priv.canActivateStory && item.status == 'closed') actions.push({text: executionLang.activate, icon: 'magic', url: $.createLink('story', 'activate', 'storyID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  269. if(priv.canUnlinkStory) actions.push({text: executionLang.unlinkStory, icon: 'unlink', url: $.createLink('execution', 'unlinkStory', 'executionID=' + executionID + '&storyID=' + item.id + '&confirm=no&from=' + '&laneID=' + item.lane + '&columnID=' + item.column), 'innerClass' : 'ajax-submit'});
  270. if(priv.canDeleteStory) actions.push({text: storyLang.delete, icon: 'trash', url: $.createLink('story', 'delete', 'storyID=' + item.id), 'innerClass': 'ajax-submit'});
  271. return actions;
  272. }
  273. window.buildBugActions = function(item)
  274. {
  275. let actions = [];
  276. if(priv.canEditBug) actions.push({text: bugLang.edit, icon: 'edit', url: $.createLink('bug', 'edit', 'bugID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  277. if(priv.canResolveBug && (item.status == 'unconfirmed' || item.status == 'confirmed' || item.status == 'fixing')) actions.push({text: bugLang.resolve, icon: 'checked', url: $.createLink('bug', 'resolve', 'bugID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  278. if(priv.canConfirmBug && (item.status == 'fixed' || item.status == 'testing' || item.status == 'tested')) actions.push({text: bugLang.close, icon: 'off', url: $.createLink('bug', 'close', 'bugID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  279. if(priv.canConfirmBug && item.status == 'unconfirmed') actions.push({text: bugLang.confirm, icon: 'ok', url: $.createLink('bug', 'confirm', 'bugID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  280. if(priv.canCopyBug) actions.push({text: bugLang.copy, icon: 'copy', url: $.createLink('bug', 'create', 'productID=' + productID + '&branch=&extras=bugID=' + item.id + ',regionID=' + item.lane + ',laneID=' + item.lane + ',columnID=' + item.column + ',executionID=' + executionID), 'data-toggle': 'modal', 'data-size': 'lg'});
  281. if(priv.canToStoryBug && (item.status != 'closed')) actions.push({text: bugLang.toStory, icon: 'lightbulb', url: $.createLink('story', 'create', 'product=' + productID + '&branch=' + '0' + '&module=' + '0' + '&story=' + '0' + '&execution=' + '0' + '&bugID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  282. if(priv.canActivateBug && (item.status == 'fixed' || item.status == 'testing' || item.status == 'tested' || item.status == 'closed')) actions.push({text: bugLang.activate, icon: 'magic', url: $.createLink('bug', 'activate', 'bugID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  283. if(priv.canDeleteBug) actions.push({text: bugLang.delete, icon: 'trash', url: $.createLink('bug', 'delete', 'bugID=' + item.id + '&from=taskkanban'), 'data-confirm': bugLang.confirmDelete, 'innerClass': 'ajax-submit'});
  284. return actions;
  285. }
  286. window.buildTaskActions = function(item)
  287. {
  288. let actions = [];
  289. if(!item.dbPrivs) item.dbPrivs = {};
  290. if(priv.canEditTask && item.dbPrivs.edit) actions.push({text: taskLang.edit, icon: 'edit', url: $.createLink('task', 'edit', 'taskID=' + item.id + '&comment=&kanbanGroup=' + groupBy), 'data-toggle': 'modal', 'data-size': 'lg'});
  291. if(priv.canRestartTask && item.dbPrivs.restart && item.status == 'pause') actions.push({text: taskLang.restart, icon: 'play', url: $.createLink('task', 'restart', 'taskID=' + item.id + '&from=execution'), 'data-toggle': 'modal'});
  292. if(priv.canPauseTask && item.dbPrivs.pause && item.status == 'developing') actions.push({text: taskLang.pause, icon: 'pause', url: $.createLink('task', 'pause', 'taskID=' + item.id), 'data-toggle': 'modal'});
  293. if(priv.canBatchCreateTask && item.canSplit) actions.push({text: taskLang.children, icon: 'split', url: $.createLink('task', 'batchCreate', `executionID=${item.execution}&storyID=${item.story}&moduleID=${item.module}&taskID=${item.id}`), 'data-toggle': 'modal', 'data-size': 'lg'});
  294. if(item.isParent == 0 && priv.canRecordWorkhourTask && item.dbPrivs.recordworkhour) actions.push({text: executionLang.effort, icon: 'time', url: $.createLink('task', 'recordWorkhour', 'taskID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  295. if(priv.canActivateTask && item.dbPrivs.activate && (item.status == 'developed' || item.status == 'canceled' || item.status == 'closed')) actions.push({text: executionLang.activate, icon: 'magic', url: $.createLink('task', 'activate', 'taskID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  296. if(priv.canCreateTask && !isLimited) actions.push({text: taskLang.copy, icon: 'copy', url: $.createLink('task', 'create', 'executionID=' + executionID + '&storyID=' + '0' + '&moduleID=' + '0' + '&taskID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  297. if(priv.canCancelTask && item.dbPrivs.cancel && (item.status == 'wait' || item.status == 'developing' || item.status == 'pause')) actions.push({text: taskLang.cancel, icon: 'cancel', url: $.createLink('task', 'cancel', 'taskID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'});
  298. if(priv.canDeleteTask && item.dbPrivs.delete) actions.push({text: taskLang.delete, icon: 'trash', url: $.createLink('task', 'delete', 'taskID=' + item.id + '&from=taskkanban'), 'data-confirm': item.isParent ? taskLang.confirmDeleteParent : taskLang.confirmDelete, 'innerClass': 'ajax-submit'});
  299. return actions;
  300. }
  301. window.canDrop = function(dragInfo, dropInfo)
  302. {
  303. if(!dragInfo) return false;
  304. const fromCol = this.getCol(dragInfo.col);
  305. const fromLane = this.getLane(dragInfo.lane);
  306. const toCol = this.getCol(dropInfo.col);
  307. const toLane = this.getLane(dropInfo.lane);
  308. const item = dragInfo.item;
  309. if(!toCol || !toLane) return false;
  310. if(dragInfo.type == 'item' && dragInfo.item.cardType == 'task' && dragInfo.item.isParent == '1') return false; // 父任务不可拖动。
  311. /* 卡片的排序目前仅支持本单元格内排序 */
  312. if(dropInfo.type == 'item' && (fromCol != toCol || fromLane != toLane)) return false;
  313. const kanbanRules = window.kanbanDropRules[item.cardType];
  314. const colRules = kanbanRules[fromCol.type];
  315. if(!colRules || !colRules.includes(toCol.type)) return false;
  316. }
  317. window.onDrop = function(changes, dropInfo)
  318. {
  319. if(!dropInfo) return false;
  320. const item = dropInfo['drag']['item'];
  321. const fromCol = item.column;
  322. const fromLane = item.lane;
  323. const toCol = dropInfo['drop']['col']
  324. const toLane = dropInfo['drop']['lane']
  325. if(fromCol == toCol && fromLane == toLane)
  326. {
  327. let sortList = '';
  328. for(let i = 0; i < dropInfo['data']['list'].length; i++) sortList += dropInfo['data']['list'][i] + ',';
  329. url = $.createLink('kanban', 'sortCard', `kanbanID=${executionID}&laneID=${toLane}&columnID=${toCol}&cards=${sortList}`);
  330. $.getJSON(url, function(response)
  331. {
  332. if(response.result == 'success') refreshKanban(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=0&browseType=" + browseType + "&groupBy=" + groupBy + '&from=RD' + '&serachValue=' + rdSearchValue + '&orderBy=' + orderBy));
  333. });
  334. }
  335. else
  336. {
  337. const fromColType = this.getCol(fromCol).type;
  338. const toColType = this.getCol(toCol).type;
  339. const regionID = this.getLane(toLane).region;
  340. changeCardColType(item.id, fromCol, toCol, fromLane, toLane, item.cardType, fromColType, toColType, regionID);
  341. return false;
  342. }
  343. }
  344. /* Define drag and drop rules */
  345. if(!window.kanbanDropRules)
  346. {
  347. window.kanbanDropRules =
  348. {
  349. story:
  350. {
  351. backlog: ['ready', 'backlog'],
  352. ready: ['backlog', 'ready'],
  353. tested: ['verified', 'rejected'],
  354. verified: ['tested', 'pending', 'released'],
  355. pending: ['verified', 'released'],
  356. released: ['verified', 'closed'],
  357. closed: ['released']
  358. },
  359. bug:
  360. {
  361. 'unconfirmed': ['unconfirmed', 'confirmed', 'fixing', 'fixed'],
  362. 'confirmed': ['confirmed', 'fixing', 'fixed'],
  363. 'fixing': ['fixing', 'fixed'],
  364. 'fixed': ['fixed', 'testing', 'tested', 'fixing'],
  365. 'testing': ['testing', 'tested', 'closed', 'fixing'],
  366. 'tested': ['tested', 'closed', 'fixing'],
  367. 'closed': ['closed', 'fixing'],
  368. },
  369. task:
  370. {
  371. 'wait': ['wait', 'developing', 'developed', 'canceled'],
  372. 'developing': ['developing', 'developed', 'pause', 'canceled'],
  373. 'developed': ['developed', 'developing', 'closed'],
  374. 'pause': ['pause', 'developing', 'canceled'],
  375. 'canceled': ['canceled', 'developing', 'closed'],
  376. 'closed': ['closed', 'developing'],
  377. }
  378. }
  379. }
  380. function changeCardColType(cardID, fromColID, toColID, fromLaneID, toLaneID, cardType, fromColType, toColType, regionID = 0)
  381. {
  382. let objectID = cardID;
  383. let showIframe = false;
  384. let moveCard = false;
  385. regionID = regionID ? regionID : 0;
  386. /* Task lane. */
  387. if(cardType == 'task')
  388. {
  389. if(toColType == 'developed')
  390. {
  391. if((fromColType == 'developing' || fromColType == 'wait') && priv.canFinishTask)
  392. {
  393. var link = $.createLink('task', 'finish', 'taskID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true);
  394. showIframe = true;
  395. }
  396. }
  397. else if(toColType == 'pause')
  398. {
  399. if(fromColType == 'developing' && priv.canPauseTask)
  400. {
  401. var link = $.createLink('task', 'pause', 'taskID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true);
  402. showIframe = true;
  403. }
  404. }
  405. else if(toColType == 'developing')
  406. {
  407. if((fromColType == 'canceled' || fromColType == 'closed' || fromColType == 'developed') && priv.canActivateTask)
  408. {
  409. var link = $.createLink('task', 'activate', 'taskID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true);
  410. showIframe = true;
  411. }
  412. if(fromColType == 'pause' && priv.canActivateTask)
  413. {
  414. var link = $.createLink('task', 'restart', 'taskID=' + objectID + '&from=execution', '', true);
  415. showIframe = true;
  416. }
  417. if(fromColType == 'wait' && priv.canStartTask)
  418. {
  419. var link = $.createLink('task', 'start', 'taskID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true);
  420. showIframe = true;
  421. }
  422. }
  423. else if(toColType == 'canceled')
  424. {
  425. if((fromColType == 'developing' || fromColType == 'wait' || fromColType == 'pause') && priv.canCancelTask)
  426. {
  427. var link = $.createLink('task', 'cancel', 'taskID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true);
  428. showIframe = true;
  429. }
  430. }
  431. else if(toColType == 'closed')
  432. {
  433. if((fromColType == 'developed' || fromColType == 'canceled') && priv.canCloseTask)
  434. {
  435. var link = $.createLink('task', 'close', 'taskID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true);
  436. showIframe = true;
  437. }
  438. }
  439. if(fromLaneID != toLaneID && fromColID == toColID) ajaxMoveCard(objectID, fromColID, toColID, fromLaneID, toLaneID, regionID);
  440. }
  441. /* Bug lane. */
  442. if(cardType == 'bug')
  443. {
  444. if(toColType == 'confirmed')
  445. {
  446. if(fromColType == 'unconfirmed' && priv.canConfirmBug)
  447. {
  448. var link = $.createLink('bug', 'confirm', 'bugID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true);
  449. showIframe = true;
  450. }
  451. }
  452. else if(toColType == 'fixing')
  453. {
  454. if(fromColType == 'confirmed' || fromColType == 'unconfirmed') moveCard = true;
  455. if((fromColType == 'closed' || fromColType == 'fixed' || fromColType == 'testing' || fromColType == 'tested') && priv.canActivateBug)
  456. {
  457. var link = $.createLink('bug', 'activate', 'bugID=' + objectID + '&kanbanInfo=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true);
  458. showIframe = true;
  459. }
  460. }
  461. else if(toColType == 'fixed')
  462. {
  463. if(fromColType == 'fixing' || fromColType == 'confirmed' || fromColType == 'unconfirmed')
  464. {
  465. var link = $.createLink('bug', 'resolve', 'bugID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true);
  466. showIframe = true;
  467. }
  468. }
  469. else if(toColType == 'testing')
  470. {
  471. if(fromColType == 'fixed') moveCard = true;
  472. }
  473. else if(toColType == 'tested')
  474. {
  475. if(fromColType == 'fixed' || fromColType == 'testing') moveCard = true;
  476. }
  477. else if(toColType == 'closed')
  478. {
  479. if(fromColType == 'testing' || fromColType == 'tested')
  480. {
  481. var link = $.createLink('bug', 'close', 'bugID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true);
  482. showIframe = true;
  483. }
  484. }
  485. if(moveCard || (fromLaneID != toLaneID && fromColID == toColID)) ajaxMoveCard(objectID, fromColID, toColID, fromLaneID, toLaneID, regionID);
  486. }
  487. /* Story lane. */
  488. if(cardType == 'story')
  489. {
  490. if(toColType == 'closed' && priv.canCloseStory)
  491. {
  492. var link = $.createLink('story', 'close', 'storyID=' + objectID, '', true);
  493. showIframe = true;
  494. }
  495. else
  496. {
  497. if(toColType == 'ready')
  498. {
  499. $.get($.createLink('story', 'ajaxGetInfo', "storyID=" + cardID), function(data)
  500. {
  501. data = JSON.parse(data);
  502. if(data.status == 'draft' || data.status == 'changing' || data.status == 'reviewing')
  503. {
  504. zui.Modal.alert(executionLang.storyDragError);
  505. }
  506. else
  507. {
  508. ajaxMoveCard(objectID, fromColID, toColID, fromLaneID, toLaneID, regionID);
  509. }
  510. });
  511. }
  512. else
  513. {
  514. ajaxMoveCard(objectID, fromColID, toColID, fromLaneID, toLaneID, regionID);
  515. }
  516. }
  517. }
  518. if(showIframe)
  519. {
  520. zui.Modal.open({url: link});
  521. }
  522. }
  523. window.ajaxMoveCard = function(objectID, fromColID, toColID, fromLaneID, toLaneID, regionID)
  524. {
  525. const link = $.createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy + '&regionID=' + regionID+ '&orderBy=' + orderBy );
  526. refreshKanban(link);
  527. }
  528. window.refreshKanban = function(url)
  529. {
  530. if(typeof url == 'undefined') url = $.createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=0&browseType=" + browseType + "&groupBy=" + groupBy + '&from=execution&searchValue=&orderBy=' + orderBy);
  531. const $kanbanList = $('[data-zui-kanbanlist]').zui('kanbanList');
  532. let options = $kanbanList.options;
  533. $.getJSON(url, function(data)
  534. {
  535. for(const region of data)
  536. {
  537. for(const group of region.items)
  538. {
  539. group.getLane = window.getLane;
  540. group.getCol = window.getCol;
  541. group.getItem = window.getItem;
  542. group.canDrop = window.canDrop;
  543. group.onDrop = window.onDrop;
  544. group.minColWidth = minColWidth;
  545. group.maxColWidth = maxColWidth;
  546. group.colProps = {'actions': window.getColActions};
  547. group.laneProps = {'actions': window.getLaneActions};
  548. group.itemProps = {'actions': window.getItemActions};
  549. }
  550. }
  551. options.items = data;
  552. $kanbanList.render(options);
  553. if(data.length == 0)
  554. {
  555. if($('#kanbanList').find('.dtable-empty-tip').length == 0) $('#kanbanList').prepend('<div class="dtable-empty-tip" style="background:#fff"><div class="row gap-4 items-center"><span class="text-gray">' + cardLang.empty + '</span></div></div>');
  556. }
  557. else
  558. {
  559. $('#kanbanList .dtable-empty-tip').remove();
  560. }
  561. });
  562. }
  563. window.toggleFullScreen = function()
  564. {
  565. var element = document.getElementById('kanbanList');
  566. var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullscreen;
  567. if(requestMethod)
  568. {
  569. var afterEnterFullscreen = function()
  570. {
  571. $('#kanbanList').addClass('fullscreen').css('background', '#fff');
  572. $('#kanbanList .kanban-list').css('height', '100%');
  573. window.hideAllAction();
  574. $.cookie.set('isFullScreen', 1, {expires:config.cookieLife, path:config.webRoot});
  575. };
  576. var whenFailEnterFullscreen = function()
  577. {
  578. exitFullScreen();
  579. };
  580. try
  581. {
  582. var result = requestMethod.call(element);
  583. if(result && (typeof result.then === 'function' || result instanceof window.Promise))
  584. {
  585. result.then(afterEnterFullscreen).catch(whenFailEnterFullscreen);
  586. }
  587. else
  588. {
  589. afterEnterFullscreen();
  590. }
  591. }
  592. catch (error)
  593. {
  594. whenFailEnterFullscreen(error);
  595. }
  596. }
  597. }
  598. /**
  599. * Exit full screen.
  600. *
  601. * @access public
  602. * @return void
  603. */
  604. function exitFullScreen()
  605. {
  606. $('.btn').show();
  607. $('#kanbanList .kanban-list').css('height', 'calc(100vh - 120px)');
  608. $.cookie.set('isFullScreen', 0, {expires:config.cookieLife, path:config.webRoot});
  609. }
  610. document.addEventListener('fullscreenchange', function (e)
  611. {
  612. if(!document.fullscreenElement) exitFullScreen();
  613. });
  614. document.addEventListener('webkitfullscreenchange', function (e)
  615. {
  616. if(!document.webkitFullscreenElement) exitFullScreen();
  617. });
  618. document.addEventListener('mozfullscreenchange', function (e)
  619. {
  620. if(!document.mozFullScreenElement) exitFullScreen();
  621. });
  622. document.addEventListener('msfullscreenChange', function (e)
  623. {
  624. if(!document.msfullscreenElement) exitFullScreen();
  625. });
  626. window.hideAllAction = function()
  627. {
  628. $('.btn').hide();
  629. }
  630. window.changeBrowseType = function()
  631. {
  632. const type = $('.c-type [name=type]').val();
  633. loadPage($.createLink('execution', 'kanban', "executionID=" + executionID + '&type=' + type));
  634. };
  635. window.changeGroupBy = function()
  636. {
  637. const group = $('.c-group [name=group]').val();
  638. const type = $('.c-type [name=type]').val();
  639. loadPage($.createLink('execution', 'kanban', 'executionID=' + executionID + '&type=' + type + '&orderBy=order_asc' + '&groupBy=' + group));
  640. };
  641. window.changeShowParent = function()
  642. {
  643. const showParent = $('[name=showParent]').prop('checked') ? 1 : 0;
  644. $.cookie.set('showParent', showParent, {expires:config.cookieLife, path:config.webRoot});
  645. const type = $('.c-type [name=type]').val();
  646. const group = $('.c-group [name=group]').val();
  647. let link = $.createLink('execution', 'kanban', 'executionID=' + executionID + '&type=' + type);
  648. if(typeof group != 'undefined') link = $.createLink('execution', 'kanban', 'executionID=' + executionID + '&type=' + type + '&orderBy=order_asc' + '&groupBy=' + group);
  649. loadPage(link);
  650. };
  651. window.toggleSearchBox = function()
  652. {
  653. $('#kanbanSearch').toggle();
  654. if($('#kanbanSearch').css('display') != 'none')
  655. {
  656. $(".querybox-toggle").css("color", "#0c64eb");
  657. }
  658. else
  659. {
  660. $(".querybox-toggle").css("color", "#3c495c");
  661. $('#kanbanSearchInput').attr('value', '');
  662. searchCards('');
  663. }
  664. };
  665. window.debounce = function (callback, delay)
  666. {
  667. let timer;
  668. return function() {
  669. const context = this;
  670. const args = arguments;
  671. clearTimeout(timer);
  672. timer = setTimeout(function () {
  673. callback.apply(context, args);
  674. }, delay);
  675. };
  676. };
  677. $('#kanbanSearchInput').on('input', debounce(function(){
  678. searchCards($(this).val());
  679. }, 500));
  680. window.searchCards = function(value, order)
  681. {
  682. searchValue = value;
  683. if(typeof order == 'undefined') order = orderBy;
  684. refreshKanban($.createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=0&browseType=" + browseType + "&groupBy=" + groupBy + '&from=execution&searchValue=' + value + '&orderBy=' + order));
  685. };
  686. window.changeStoryProduct = function()
  687. {
  688. const productID = $('#batchCreateStory [name=productName]').val();
  689. if(productID) $('#batchCreateStoryButton').attr('href', $.createLink('story', 'batchCreate', 'productID=' + productID + '&branch=0&moduleID=0&storyID=0&executionID=' + executionID));
  690. };
  691. window.changeBugProduct = function()
  692. {
  693. const productID = $('#batchCreateBug [name=productName]').val();
  694. if(productID) $('#batchCreateBugButton').attr('href', $.createLink('bug', 'batchCreate', 'productID=' + productID + '&branch=&executionID=' + executionID));
  695. };
  696. $(document).off('click', '#linkStoryByPlan button[type="submit"]').on('click', '#linkStoryByPlan button[type="submit"]', function()
  697. {
  698. const planID = $('[name=plan]').val();
  699. if(planID)
  700. {
  701. var param = "&param=executionID=" + executionID + ",browseType=" + browseType + ",orderBy=id_asc,groupBy=" + groupBy;
  702. $.ajaxSubmit({url: $.createLink('execution', 'importPlanStories', 'executionID=' + executionID + '&planID=' + planID + '&productID=0&fromMethod=taskKanban&extra=' + param)});
  703. }
  704. return false;
  705. })