taskkanban.js 54 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528
  1. /**
  2. * Update column name.
  3. *
  4. * @param int $columnID
  5. * @param string $name
  6. * @param string $color
  7. * @access public
  8. * @return void
  9. */
  10. function updateColumnName(columnID, name, color)
  11. {
  12. $('.kanban-col[data-id="' + columnID + '"] > div.title > span:first').text(name).attr('title', name).css('color', color);
  13. }
  14. /**
  15. * Change view.
  16. *
  17. * @param string $view
  18. * @access public
  19. * @return void
  20. */
  21. function changeView(view)
  22. {
  23. var link = createLink('execution', 'taskKanban', "executionID=" + executionID + '&type=' + view);
  24. location.href = link;
  25. }
  26. /**
  27. * Render user avatar
  28. *
  29. * @param array $user
  30. * @param string $objectType
  31. * @param int $objectID
  32. * @param int $size
  33. * @param string $objectStatus
  34. * @access public
  35. * @return void
  36. */
  37. function renderUserAvatar(user, objectType, objectID, size, objectStatus)
  38. {
  39. var avatarSizeClass = 'avatar-' + (size || 'sm');
  40. var $noPrivAndNoAssigned = $('<div class="avatar has-text ' + avatarSizeClass + ' avatar-circle" title="' + noAssigned + '" style="background: #ccc"><i class="icon icon-person"></i></div>');
  41. if(objectType == 'task')
  42. {
  43. if(!priv.canAssignTask && !user) return $noPrivAndNoAssigned;
  44. var link = createLink('task', 'assignto', 'executionID=' + executionID + '&id=' + objectID + '&kanbanGroup=default&from=taskkanban', '', true);
  45. }
  46. if(objectType == 'story')
  47. {
  48. if(!priv.canAssignStory && !user) return $noPrivAndNoAssigned;
  49. var link = createLink('story', 'assignto', 'id=' + objectID + '&kanbanGroup=default&from=taskkanban', '', true);
  50. }
  51. if(objectType == 'bug')
  52. {
  53. if(!priv.canAssignBug && !user) return $noPrivAndNoAssigned;
  54. var link = createLink('bug', 'assignto', 'id=' + objectID + '&kanbanGroup=default&from=taskkanban', '', true);
  55. }
  56. if(!user) return objectStatus == 'closed' ? '' : $('<a class="avatar has-text ' + avatarSizeClass + ' avatar-circle iframe" title="' + noAssigned + '" style="background: #ccc" href="' + link + '"><i class="icon icon-person"></i></a>');
  57. if(typeof user === 'string') user = {account: user};
  58. if(!user.avatar && window.userList && window.userList[user.account]) user = window.userList[user.account];
  59. var $noPrivAvatar = $('<div class="avatar has-text ' + avatarSizeClass + ' avatar-circle" />').avatar({user: user});
  60. if(objectType == 'task' && !priv.canAssignTask) return $noPrivAvatar;
  61. if(objectType == 'story' && !priv.canAssignStory) return $noPrivAvatar;
  62. if(objectType == 'bug' && !priv.canAssignBug) return $noPrivAvatar;
  63. var realname = user.realname ? user.realname : user.account;
  64. var title = user.title ? user.title : realname;
  65. return objectStatus == 'closed' ? '' : $('<a class="avatar has-text ' + avatarSizeClass + ' avatar-circle iframe" title="' + title + '" href="' + link + '"/>').avatar({user: user});
  66. }
  67. /**
  68. * Render deadline
  69. * @param {String|Date} deadline Deadline
  70. @param {string} status
  71. * @returns {JQuery}
  72. */
  73. function renderDeadline(deadline, status)
  74. {
  75. if(deadline == '0000-00-00') return;
  76. var date = $.zui.createDate(deadline);
  77. var now = new Date();
  78. now.setHours(0);
  79. now.setMinutes(0);
  80. now.setSeconds(0);
  81. now.setMilliseconds(0);
  82. var isEarlyThanToday = date.getTime() < now.getTime();
  83. var deadlineDate = $.zui.formatDate(date, 'MM-dd');
  84. var statusList = ['doing','pause'];
  85. var textColor = isEarlyThanToday && typeof(status) != 'undefined' && statusList.indexOf(status) != -1 ? 'text-red' : 'text-muted';
  86. return $('<span class="info info-deadline"/>').text(deadlineLang + ' ' + deadlineDate).addClass(textColor);
  87. }
  88. /**
  89. * Render estStarted
  90. *
  91. * @param {String|Date} estStarted EstStarted
  92. * @param {string} status
  93. * @access public
  94. * @return void
  95. */
  96. function renderEstStarted(estStarted, status)
  97. {
  98. if(estStarted == '0000-00-00') return;
  99. var date = $.zui.createDate(estStarted);
  100. var now = new Date();
  101. now.setHours(0);
  102. now.setMinutes(0);
  103. now.setSeconds(0);
  104. now.setMilliseconds(0);
  105. var isEarlyThanToday = date.getTime() < now.getTime();
  106. var estStartedDate = $.zui.formatDate(date, 'MM-dd');
  107. var textColor = isEarlyThanToday && typeof(status) != 'undefined' && status == 'wait' ? 'text-red' : 'text-muted';
  108. return $('<span class="info info-deadline"/>').text(estStartedLang + ' ' + estStartedDate).addClass(textColor);
  109. }
  110. /**
  111. * Render story item
  112. * @param {Object} item Story item object
  113. * @param {JQuery} $item Kanban item element
  114. * @param {Object} col Column object
  115. * @returns {JQuery} $item Kanban item element
  116. */
  117. function renderStoryItem(item, $item, col)
  118. {
  119. if(groupBy == 'story' && item.id == '0')
  120. {
  121. $('.storyCell').css('width', '100%');
  122. $parentItem = $item[0] == undefined ? $('.storyCell') : $item.parent();
  123. $parentItem.addClass('text-center storyCell');
  124. $parentItem.css('line-height', ($parentItem.parent().height() - 20) + 'px');
  125. $item.replaceWith('<span class="text-muted">' + item.title + '</span>');
  126. return;
  127. }
  128. var scaleSize = window.kanbanScaleSize;
  129. if($item.attr('data-scale-size') !== scaleSize) $item.empty().attr('data-scale-size', scaleSize);
  130. if(scaleSize <= 3)
  131. {
  132. var $title = $item.find('.title');
  133. if(!$title.length)
  134. {
  135. $title = $('<a class="title iframe" data-width="95%">' + (scaleSize <= 1 ? '<i class="icon icon-lightbulb text-muted"></i> ' : '') + '<span class="text"></span></a>')
  136. .attr('href', $.createLink('execution', 'storyView', 'storyID=' + item.id, '', true));
  137. $title.appendTo($item);
  138. }
  139. var title = searchValue != '' ? "<span class='text'>" + item.title.replaceAll(searchValue, "<span class='text-danger'>" + searchValue + "</span>") + "</span>": "<span class='text'>" + item.title + "</span>";
  140. $title.attr('title', item.title).find('.text').replaceWith(title);
  141. }
  142. if(scaleSize <= 2)
  143. {
  144. var idHtml = scaleSize <= 1 ? ('<span class="info info-id text-muted">#' + item.id + '</span>') : '';
  145. var priHtml = '<span class="info info-pri' + (item.pri ? ' label-pri label-pri-' + item.pri : '') + '" title="' + item.pri + '">' + item.pri + '</span>';
  146. var hoursHtml = (item.estimate && scaleSize <= 1) ? ('<span class="info info-estimate text-muted">' + item.estimate + hourUnit +'</span>') : '';
  147. var avatarHtml = renderUserAvatar(item.assignedTo, 'story', item.id, '', col.type);
  148. var $infos = $item.find('.infos');
  149. if(!$infos.length) $infos = $('<div class="infos"></div>');
  150. $infos.html([idHtml, priHtml, hoursHtml].join(''));
  151. $infos[scaleSize <= 1 ? 'append' : 'prepend'](avatarHtml);
  152. if(scaleSize <= 1) $infos.appendTo($item);
  153. else if(scaleSize === 2) $infos.prependTo($item);
  154. else $infos.prependTo($item.find('.title'));
  155. }
  156. else if(scaleSize === 4)
  157. {
  158. $item.html(renderUserAvatar(item.assignedTo, 'story', item.id, 'md'));
  159. }
  160. if(scaleSize <= 1)
  161. {
  162. var $actions = $item.find('.actions');
  163. if(!$actions.length && item.menus && item.menus.length)
  164. {
  165. $actions = $([
  166. '<div class="actions">',
  167. '<a data-contextmenu="story" data-col="' + col.type + '">',
  168. '<i class="icon icon-ellipsis-v"></i>',
  169. '</a>',
  170. '</div>'
  171. ].join('')).appendTo($item);
  172. }
  173. }
  174. if($.cookie('isFullScreen') == 1) hideAllAction();
  175. return $item.attr('data-type', 'story').addClass('kanban-item-story');
  176. }
  177. /**
  178. * Render bug item
  179. * @param {Object} item Bug item object
  180. * @param {JQuery} $item Kanban item element
  181. * @param {Object} col Column object
  182. * @returns {JQuery} $item Kanban item element
  183. */
  184. function renderBugItem(item, $item, col)
  185. {
  186. var scaleSize = window.kanbanScaleSize;
  187. if($item.attr('data-scale-size') !== scaleSize) $item.empty().attr('data-scale-size', scaleSize);
  188. if(scaleSize <= 3)
  189. {
  190. var $title = $item.find('.title');
  191. if(!$title.length)
  192. {
  193. $title = $('<a class="title iframe" data-width="95%">' + (scaleSize <= 1 ? '<i class="icon icon-bug text-muted"></i> ' : '') + '<span class="text"></span></a>')
  194. .attr('href', $.createLink('bug', 'view', 'bugID=' + item.id, '', true));
  195. $title.appendTo($item);
  196. }
  197. var title = searchValue != '' ? "<span class='text'>" + item.title.replaceAll(searchValue, "<span class='text-danger'>" + searchValue + "</span>") + "</span>": "<span class='text'>" + item.title + "</span>";
  198. $title.attr('title', item.title).find('.text').replaceWith(title);
  199. }
  200. if(scaleSize <= 2)
  201. {
  202. var idHtml = scaleSize <= 1 ? ('<span class="info info-id text-muted">#' + item.id + '</span>') : '';
  203. var severityHtml = scaleSize <= 1 ? ('<span class="info info-severity label-severity" data-severity="' + item.severity + '" title="' + item.severity + '"></span>') : '';
  204. var priHtml = '<span class="info info-pri' + (item.pri ? ' label-pri label-pri-' + item.pri : '') + '" title="' + item.pri + '">' + item.pri + '</span>';
  205. var avatarHtml = renderUserAvatar(item.assignedTo, 'bug', item.id, '', col.type);
  206. var $infos = $item.find('.infos');
  207. if(!$infos.length) $infos = $('<div class="infos"></div>');
  208. $infos.html([idHtml, severityHtml, priHtml].join(''));
  209. if(item.deadline && scaleSize <= 1) $infos.append(renderDeadline(item.deadline));
  210. $infos[scaleSize <= 1 ? 'append' : 'prepend'](avatarHtml);
  211. if(scaleSize <= 1) $infos.appendTo($item);
  212. else if(scaleSize === 2) $infos.prependTo($item);
  213. else $infos.prependTo($item.find('.title'));
  214. }
  215. else if(scaleSize === 4)
  216. {
  217. $item.html(renderUserAvatar(item.assignedTo, 'bug', item.id, 'md'));
  218. }
  219. if(scaleSize <= 1)
  220. {
  221. var $actions = $item.find('.actions');
  222. if(!$actions.length && item.menus && item.menus.length)
  223. {
  224. $actions = $([
  225. '<div class="actions">',
  226. '<a data-contextmenu="bug" data-col="' + col.type + '">',
  227. '<i class="icon icon-ellipsis-v"></i>',
  228. '</a>',
  229. '</div>'
  230. ].join('')).appendTo($item);
  231. }
  232. }
  233. if($.cookie('isFullScreen') == 1) hideAllAction();
  234. return $item.attr('data-type', 'bug').addClass('kanban-item-bug');
  235. }
  236. /**
  237. * Render task item
  238. * @param {Object} item Task item object
  239. * @param {JQuery} $item Kanban item element
  240. * @param {Object} col Column object
  241. * @returns {JQuery} $item Kanban item element
  242. */
  243. function renderTaskItem(item, $item, col)
  244. {
  245. var scaleSize = window.kanbanScaleSize;
  246. if($item.attr('data-scale-size') !== scaleSize) $item.empty().attr('data-scale-size', scaleSize);
  247. if(scaleSize <= 3)
  248. {
  249. var $title = $item.find('.title');
  250. if(!$title.length)
  251. {
  252. $title = $('<a class="title iframe" data-width="95%">' + (scaleSize <= 1 ? '<i class="icon icon-checked text-muted"></i> ' : '') + '<span class="text"></span></a>').attr('href', $.createLink('task', 'view', 'taskID=' + item.id, '', true));
  253. $title.appendTo($item);
  254. }
  255. var name = searchValue != '' ? "<span class='text'>" + item.name.replaceAll(searchValue, "<span class='text-danger'>" + searchValue + "</span>") + "</span>": "<span class='text'>" + item.name + "</span>";
  256. $title.attr('title', item.name).find('.text').replaceWith(name);
  257. }
  258. if(scaleSize <= 2)
  259. {
  260. var priHtml = '<span class="info info-pri' + (item.pri ? ' label-pri label-pri-' + item.pri : '') + '" title="' + item.pri + '">' + item.pri + '</span>';
  261. var hoursHtml = scaleSize <= 1 && item.status != 'wait' ? ('<span class="info info-estimate text-muted">' + taskLang.leftAB + ' ' + item.left + 'h</span>') : ('<span class="info info-estimate text-muted">' + taskLang.estimateAB + ' ' + item.estimate + 'h</span>');
  262. var avatarHtml = '';
  263. if(item.assignedTo == '' && item.mode == 'multi') avatarHtml = renderUserAvatar({title: item.teamMembers, realname: teamWords}, 'task', item.id, '', col.type);
  264. else avatarHtml = renderUserAvatar(item.assignedTo, 'task', item.id, '', col.type);
  265. var $infos = $item.find('.infos');
  266. if(!$infos.length) $infos = $('<div class="infos"></div>');
  267. $infos.html([priHtml, hoursHtml].join(''));
  268. if(item.deadline && scaleSize <= 1 && (item.status == 'doing' || item.status == 'pause')) $infos.append(renderDeadline(item.deadline, item.status));
  269. if(item.estStarted && scaleSize <= 1 && item.status == 'wait') $infos.append(renderEstStarted(item.estStarted, item.status));
  270. $infos[scaleSize <= 1 ? 'append' : 'prepend'](avatarHtml);
  271. if(scaleSize <= 1) $infos.appendTo($item);
  272. else if(scaleSize === 2) $infos.prependTo($item);
  273. else $infos.prependTo($item.find('.title'));
  274. }
  275. else if(scaleSize === 4)
  276. {
  277. $item.html(renderUserAvatar(item.assignedTo, 'task', item.id, 'md'));
  278. }
  279. if(scaleSize <= 1)
  280. {
  281. var $actions = $item.find('.actions');
  282. if(!$actions.length && item.menus && item.menus.length)
  283. {
  284. $actions = $([
  285. '<div class="actions">',
  286. '<a data-contextmenu="task" data-col="' + col.type + '">',
  287. '<i class="icon icon-ellipsis-v"></i>',
  288. '</a>',
  289. '</div>'
  290. ].join('')).appendTo($item);
  291. }
  292. }
  293. $item.attr('data-type', 'task').addClass('kanban-item-task');
  294. if($.cookie('isFullScreen') == 1) hideAllAction();
  295. return $item;
  296. }
  297. /* Add column renderer */
  298. addColumnRenderer('story', renderStoryItem);
  299. addColumnRenderer('bug', renderBugItem);
  300. addColumnRenderer('task', renderTaskItem);
  301. /**
  302. * Render column count
  303. * @param {JQuery} $count Kanban count element
  304. * @param {number} count Column cards count
  305. * @param {number} col Column object
  306. * @param {Object} kanban Kanban intance
  307. */
  308. function renderColumnCount($count, count, col)
  309. {
  310. if(groupBy == 'story' && col.type == 'story')
  311. {
  312. var orderButton = '<a class="btn btn-link action storyColumn ' + (changeOrder ? 'text-primary' : '') + '" type="button" data-toggle="dropdown">'
  313. + "<i class='icon icon-swap'></i>"
  314. + '</a>'
  315. + '<ul class="dropdown-menu">';
  316. for(var order in kanbanLang.orderList) orderButton += '<li class="' + (order == orderBy ? 'active' : '') + '"><a href="###" onclick="searchCards(searchValue, \'' + order + '\')">' + kanbanLang.orderList[order] + '</a></li>';
  317. orderButton += '</ul>';
  318. $count.parent().next().html(orderButton);
  319. $count.parent().next().addClass('createButton');
  320. $count.hide();
  321. return;
  322. }
  323. var text = count + '/' + (col.limit < 0 ? '<i class="icon icon-infinite"></i>' : col.limit);
  324. $count.html(text + '<i class="icon icon-arrow-up" data-toggle="tooltip" data-original-title="' + kanbanLang.limitExceeded + '"></i>');
  325. if(col.limit != -1 && col.limit < count)
  326. {
  327. $count.parents('.title').parent('.kanban-header-col').css('background-color', '#F6A1A1');
  328. $count.parents('.title').find('.text').css('max-width', $count.parents('.title').width() - 200);
  329. $count.css('color', '#E33030');
  330. if(!$count.parent().find('.error').length) $count.parent().find('.include-last').after("<span class='error text-grey'><icon class='icon icon-help' title='" + kanbanLang.limitExceeded + "'></icon></span>");
  331. }
  332. else
  333. {
  334. $count.parents('.title').parent('.kanban-header-col').css('background-color', 'transparent');
  335. $count.parents('.title').find('.text').css('max-width', $count.parents('.title').width() - 120);
  336. $count.css('color', '#8B91A2');
  337. $count.parent().find('.error').remove();
  338. }
  339. }
  340. /**
  341. * Alert to link product.
  342. *
  343. * @access public
  344. * @return void
  345. */
  346. function tips()
  347. {
  348. bootbox.alert(needLinkProducts);
  349. }
  350. /**
  351. * Render header column
  352. * @param {JQuery} $col Header column element
  353. * @param {Object} col Header column object
  354. * @param {JQuery} $header Header element
  355. * @param {Object} kanban Kanban object
  356. */
  357. function renderHeaderCol($col, col, $header, kanban)
  358. {
  359. if(col.asParent) $col = $col.children('.kanban-header-col');
  360. if($col.children('.actions').context != undefined || (groupBy == 'story' && col.type == 'story')) return;
  361. var $actions = $('<div class="actions createButton" />');
  362. var printStoryButton = printTaskButton = printBugButton = false;
  363. if(priv.canCreateStory || priv.canBatchCreateStory || priv.canLinkStory || priv.canLinkStoryByPlan) printStoryButton = true;
  364. if(priv.canCreateTask || priv.canBatchCreateTask) printTaskButton = true;
  365. if(priv.canCreateBug || priv.canBatchCreateBug) printBugButton = true;
  366. if(col.type === 'backlog' || col.type === 'wait' || col.type == 'unconfirmed')
  367. {
  368. var tips = productID ? '' : 'onclick="tips()"';
  369. $actions.append([
  370. '<a data-contextmenu="columnCreate" data-type="' + col.type + '" data-kanban="' + kanban.id + '" data-parent="' + (col.parentType || '') + '" class="text-primary"' + ((col.laneType !== 'task') ? tips : '') + '>',
  371. '<i class="icon icon-expand-alt"></i>',
  372. '</a>'
  373. ].join(''));
  374. }
  375. if(priv.canSetWIP || priv.canEditName)
  376. {
  377. $actions.append([
  378. '<a data-contextmenu="column" title="' + kanbanLang.moreAction + '" data-type="' + col.type + '" data-kanban="' + kanban.id + '" data-parent="' + (col.parentType || '') + '">',
  379. '<i class="icon icon-ellipsis-v"></i>',
  380. '</a>'
  381. ].join(''));
  382. }
  383. $actions.appendTo($col);
  384. }
  385. /**
  386. * Render lane name
  387. * @param {JQuery} $name Name element
  388. * @param {Object} lane Lane object
  389. * @param {JQuery} $kanban $kanban element
  390. * @param {Object} columns Kanban columns
  391. * @param {Object} kanban Kanban object
  392. */
  393. function renderLaneName($name, lane, $kanban, columns, kanban)
  394. {
  395. if(groupBy == 'story')
  396. {
  397. $name.hide();
  398. return;
  399. }
  400. if(lane.id != 'story' && lane.id != 'task' && lane.id != 'bug') return false;
  401. if(!$name.children('.actions').length && (priv.canSetLane || priv.canMoveLane))
  402. {
  403. $([
  404. '<div class="actions" title="' + kanbanLang.moreAction + '">',
  405. '<a data-contextmenu="lane" data-lane="' + lane.id + '" data-kanban="' + kanban.id + '">',
  406. '<i class="icon icon-ellipsis-v"></i>',
  407. '</a>',
  408. '</div>'
  409. ].join('')).appendTo($name);
  410. }
  411. }
  412. /**
  413. * Updata kanban data
  414. * @param {string} kanbanID Kanban id
  415. * @param {Object} data Kanban data
  416. */
  417. function updateKanban(kanbanID, data)
  418. {
  419. var $kanban = $('#kanban-' + kanbanID);
  420. if(!$kanban.length) return;
  421. if(data == null)
  422. {
  423. $kanban.hide();
  424. return false;
  425. }
  426. $kanban.show();
  427. $kanban.data('zui.kanban').render(data);
  428. resetKanbanHeight();
  429. return true;
  430. }
  431. /**
  432. * Create kanban in page
  433. * @param {string} kanbanID Kanban id
  434. * @param {Object} data Kanban data
  435. * @param {Object} options Kanban options
  436. */
  437. function createKanban(kanbanID, data, options)
  438. {
  439. var $kanban = $('#kanban-' + kanbanID);
  440. if($kanban.length) return updateKanban(kanbanID, data);
  441. $kanban = $('<div id="kanban-' + kanbanID + '" data-id="' + kanbanID + '"></div>').appendTo('#kanbans');
  442. $kanban.kanban($.extend({data: data, calcColHeight: calcColHeight, displayCards: typeof window.displayCards === 'number' ? window.displayCards : 2}, options));
  443. }
  444. /**
  445. * Hide all actions.
  446. *
  447. * @access public
  448. * @return void
  449. */
  450. function hideAllAction()
  451. {
  452. $('.actions').hide();
  453. $(".title, .avatar.iframe").attr("disabled", true).css("pointer-events", "none");
  454. }
  455. /**
  456. * Display the kanban in full screen.
  457. *
  458. * @access public
  459. * @return void
  460. */
  461. function fullScreen()
  462. {
  463. $('#kanbans .kanban-header').addClass('headerTop');
  464. var element = document.getElementById('kanbanContainer');
  465. var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen;
  466. if(requestMethod)
  467. {
  468. var afterEnterFullscreen = function()
  469. {
  470. $('#kanbanContainer').addClass('scrollbar-hover');
  471. hideAllAction();
  472. $.cookie('isFullScreen', 1);
  473. }
  474. var whenFailEnterFullscreen = function()
  475. {
  476. exitFullScreen();
  477. }
  478. try
  479. {
  480. var result = requestMethod.call(element);
  481. if(result && (typeof result.then === 'function' || result instanceof window.Promise))
  482. {
  483. result.then(afterEnterFullscreen).catch(whenFailEnterFullscreen);
  484. }
  485. else
  486. {
  487. afterEnterFullscreen();
  488. }
  489. }
  490. catch (error)
  491. {
  492. whenFailEnterFullscreen(error);
  493. }
  494. }
  495. }
  496. /**
  497. * Exit full screen.
  498. *
  499. * @access public
  500. * @return void
  501. */
  502. function exitFullScreen()
  503. {
  504. $('#kanbans .kanban-header').removeClass('headerTop');
  505. $('#kanbanContainer').removeClass('scrollbar-hover');
  506. $('.actions').show();
  507. $(".title, .avatar.iframe").attr("disabled", false).css("pointer-events", "auto");
  508. $.cookie('isFullScreen', 0);
  509. }
  510. document.addEventListener('fullscreenchange', function (e)
  511. {
  512. if(!document.fullscreenElement) exitFullScreen();
  513. });
  514. document.addEventListener('webkitfullscreenchange', function (e)
  515. {
  516. if(!document.webkitFullscreenElement) exitFullScreen();
  517. });
  518. document.addEventListener('mozfullscreenchange', function (e)
  519. {
  520. if(!document.mozFullScreenElement) exitFullScreen();
  521. });
  522. document.addEventListener('msfullscreenChange', function (e)
  523. {
  524. if(!document.msfullscreenElement) exitFullScreen();
  525. });
  526. /* Define drag and drop rules */
  527. if(!window.kanbanDropRules)
  528. {
  529. window.kanbanDropRules =
  530. {
  531. story:
  532. {
  533. backlog: ['ready'],
  534. ready: ['backlog'],
  535. tested: ['verified'],
  536. verified: ['tested', 'released'],
  537. released: ['verified', 'closed'],
  538. closed: ['released'],
  539. },
  540. bug:
  541. {
  542. 'unconfirmed': ['confirmed', 'fixing', 'fixed'],
  543. 'confirmed': ['fixing', 'fixed'],
  544. 'fixing': ['fixed'],
  545. 'fixed': ['testing', 'tested', 'fixing'],
  546. 'testing': ['tested', 'closed', 'fixing'],
  547. 'tested': ['closed', 'fixing'],
  548. 'closed': ['fixing'],
  549. },
  550. task:
  551. {
  552. 'wait': ['developing', 'developed', 'canceled'],
  553. 'developing': ['developed', 'pause', 'canceled'],
  554. 'developed': ['developing', 'closed'],
  555. 'pause': ['developing'],
  556. 'canceled': ['developing', 'closed'],
  557. 'closed': ['developing'],
  558. }
  559. }
  560. }
  561. /*
  562. * Find drop columns
  563. * @param {JQuery} $element Drag element
  564. * @param {JQuery} $root Dnd root element
  565. */
  566. function findDropColumns($element, $root)
  567. {
  568. var $col = $element.closest('.kanban-col');
  569. var col = $col.data();
  570. var kanbanID = $root.data('id');
  571. var kanbanRules = window.kanbanDropRules ? window.kanbanDropRules[kanbanID] : null;
  572. $.zui.ContextMenu.hide();
  573. if(!kanbanRules) return $root.find('.kanban-lane-col:not([data-type="' + col.type + '"])');
  574. var colRules = kanbanRules[col.type];
  575. var lane = $col.closest('.kanban-lane').data('lane');
  576. return $root.find('.kanban-lane-col').filter(function()
  577. {
  578. if(!colRules) return false;
  579. if(colRules === true) return true;
  580. if($.cookie('isFullScreen') == 1) return false;
  581. var $newCol = $(this);
  582. var newCol = $newCol.data();
  583. if(newCol.id === col.id) return false;
  584. var $newLane = $newCol.closest('.kanban-lane');
  585. var newLane = $newLane.data('lane');
  586. var canDropHere = colRules.indexOf(newCol.type) > -1 && newLane.id === lane.id;
  587. if(canDropHere) $newCol.addClass('can-drop-here');
  588. return canDropHere;
  589. });
  590. }
  591. /**
  592. * Change card's type by changing column.
  593. *
  594. * @param int $cardID
  595. * @param int $fromColID
  596. * @param int $toColID
  597. * @param int $fromLaneID
  598. * @param int $toLaneID
  599. * @param string $cardType
  600. * @param string $fromColType
  601. * @param string $toColType
  602. * @access public
  603. * @return void
  604. */
  605. function changeCardColType(cardID, fromColID, toColID, fromLaneID, toLaneID, cardType, fromColType, toColType)
  606. {
  607. var objectID = cardID;
  608. var showIframe = false;
  609. var moveCard = false;
  610. /* Task lane. */
  611. if(cardType == 'task')
  612. {
  613. if(toColType == 'developed')
  614. {
  615. if((fromColType == 'developing' || fromColType == 'wait') && priv.canFinishTask)
  616. {
  617. var link = createLink('task', 'finish', 'taskID=' + objectID + '&extra=from=' + 'taskkanban', '', true);
  618. showIframe = true;
  619. }
  620. }
  621. else if(toColType == 'pause')
  622. {
  623. if(fromColType == 'developing' && priv.canPauseTask)
  624. {
  625. var link = createLink('task', 'pause', 'taskID=' + objectID + '&extra=from=' + 'taskkanban', '', true);
  626. showIframe = true;
  627. }
  628. }
  629. else if(toColType == 'developing')
  630. {
  631. if((fromColType == 'canceled' || fromColType == 'closed' || fromColType == 'developed') && priv.canActivateTask)
  632. {
  633. var link = createLink('task', 'activate', 'taskID=' + objectID + '&extra=from=' + 'taskkanban', '', true);
  634. showIframe = true;
  635. }
  636. if(fromColType == 'pause' && priv.canActivateTask)
  637. {
  638. var link = createLink('task', 'restart', 'taskID=' + objectID + '&from=' + 'taskkanban', '', true);
  639. showIframe = true;
  640. }
  641. if(fromColType == 'wait' && priv.canStartTask)
  642. {
  643. var link = createLink('task', 'start', 'taskID=' + objectID + '&extra=from=' + 'taskkanban', '', true);
  644. showIframe = true;
  645. }
  646. }
  647. else if(toColType == 'canceled')
  648. {
  649. if((fromColType == 'developing' || fromColType == 'wait' || fromColType == 'pause') && priv.canCancelTask)
  650. {
  651. var link = createLink('task', 'cancel', 'taskID=' + objectID + '&extra=from=' + 'taskkanban', '', true);
  652. showIframe = true;
  653. }
  654. }
  655. else if(toColType == 'closed')
  656. {
  657. if((fromColType == 'developed' || fromColType == 'canceled') && priv.canCloseTask)
  658. {
  659. var link = createLink('task', 'close', 'taskID=' + objectID + '&extra=from=' + 'taskkanban', '', true);
  660. showIframe = true;
  661. }
  662. }
  663. }
  664. /* Bug lane. */
  665. if(cardType == 'bug')
  666. {
  667. if(toColType == 'confirmed')
  668. {
  669. if(fromColType == 'unconfirmed' && priv.canConfirmBug)
  670. {
  671. var link = createLink('bug', 'confirm', 'bugID=' + objectID + '&extra=&from=taskkanban', '', true);
  672. showIframe = true;
  673. }
  674. }
  675. else if(toColType == 'fixing')
  676. {
  677. if(fromColType == 'confirmed' || fromColType == 'unconfirmed') moveCard = true;
  678. if((fromColType == 'closed' || fromColType == 'fixed' || fromColType == 'testing' || fromColType == 'tested') && priv.canActivateBug)
  679. {
  680. var link = createLink('bug', 'activate', 'bugID=' + objectID, '', true);
  681. showIframe = true;
  682. }
  683. }
  684. else if(toColType == 'fixed')
  685. {
  686. if(fromColType == 'fixing' || fromColType == 'confirmed' || fromColType == 'unconfirmed')
  687. {
  688. var link = createLink('bug', 'resolve', 'bugID=' + objectID + '&extra=&from=taskkanban', '', true);
  689. showIframe = true;
  690. }
  691. }
  692. else if(toColType == 'testing')
  693. {
  694. if(fromColType == 'fixed') moveCard = true;
  695. }
  696. else if(toColType == 'tested')
  697. {
  698. if(fromColType == 'fixed' || fromColType == 'testing') moveCard = true;
  699. }
  700. else if(toColType == 'closed')
  701. {
  702. if(fromColType == 'testing' || fromColType == 'tested')
  703. {
  704. var link = createLink('bug', 'close', 'bugID=' + objectID + '&extra=&from=taskkanban', '', true);
  705. showIframe = true;
  706. }
  707. }
  708. if(moveCard)
  709. {
  710. var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy);
  711. $.get(link, function(data)
  712. {
  713. if(data)
  714. {
  715. kanbanGroup = $.parseJSON(data);
  716. if(groupBy == 'default')
  717. {
  718. updateKanban('bug', kanbanGroup.bug);
  719. }
  720. else
  721. {
  722. updateKanban(browseType, kanbanGroup[groupBy]);
  723. }
  724. }
  725. })
  726. }
  727. }
  728. /* Story lane. */
  729. if(cardType == 'story')
  730. {
  731. if(toColType == 'closed' && priv.canCloseStory)
  732. {
  733. var link = createLink('story', 'close', 'storyID=' + objectID + '&from=taskkanban', '', true);
  734. showIframe = true;
  735. }
  736. else
  737. {
  738. if(toColType == 'ready')
  739. {
  740. $.get(createLink('story', 'ajaxGetInfo', "storyID=" + cardID), function(data)
  741. {
  742. if(data)
  743. {
  744. data = $.parseJSON(data);
  745. if(data.status == 'draft' || data.status == 'changing' || data.status == 'reviewing')
  746. {
  747. bootbox.alert(executionLang.storyDragError);
  748. }
  749. else
  750. {
  751. ajaxMoveCard(objectID, fromColID, toColID, fromLaneID, toLaneID);
  752. }
  753. }
  754. });
  755. }
  756. else
  757. {
  758. ajaxMoveCard(objectID, fromColID, toColID, fromLaneID, toLaneID);
  759. }
  760. }
  761. }
  762. if(showIframe)
  763. {
  764. var modalTrigger = new $.zui.ModalTrigger({type: 'iframe', width: '80%', url: link});
  765. modalTrigger.show();
  766. }
  767. }
  768. /**
  769. * AJAX: move card.
  770. *
  771. * @param int $objectID
  772. * @param int $fromColID
  773. * @param int $toColID
  774. * @param int $fromLaneID
  775. * @param int $toLaneID
  776. * @access public
  777. * @return void
  778. */
  779. function ajaxMoveCard(objectID, fromColID, toColID, fromLaneID, toLaneID)
  780. {
  781. var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy);
  782. $.get(link, function(data)
  783. {
  784. if(data)
  785. {
  786. kanbanGroup = $.parseJSON(data);
  787. if(groupBy == 'default')
  788. {
  789. updateKanban('story', kanbanGroup.story);
  790. }
  791. else
  792. {
  793. updateKanban(browseType, kanbanGroup[groupBy]);
  794. }
  795. }
  796. });
  797. }
  798. /**
  799. * Handle drop task.
  800. *
  801. * @param object $element
  802. * @param object $event
  803. * @param object $kanban
  804. * @access public
  805. * @return void
  806. */
  807. function handleDropTask($element, event, kanban)
  808. {
  809. if(!event.target) return;
  810. var $card = $element;
  811. var $oldCol = $card.closest('.kanban-col');
  812. var $newCol = $(event.target).closest('.kanban-col');
  813. var oldCol = $oldCol.data();
  814. var newCol = $newCol.data();
  815. var oldLane = $oldCol.closest('.kanban-lane').data('lane');
  816. var newLane = $newCol.closest('.kanban-lane').data('lane');
  817. var cardType = $card.find('.kanban-card').data('type');
  818. if(!oldCol || !newCol || !newLane || !oldLane) return false;
  819. if(oldCol.id === newCol.id && newLane.id === oldLane.id) return false;
  820. var cardID = $card.data().id;
  821. var fromColType = $oldCol.data('type');
  822. var toColType = $newCol.data('type');
  823. changeCardColType(cardID, oldCol.id, newCol.id, oldLane.id, newLane.id, cardType, fromColType, toColType);
  824. }
  825. var kanbanActionHandlers =
  826. {
  827. dropItem: handleDropTask
  828. };
  829. /**
  830. * Handle kanban action.
  831. *
  832. * @param string $action
  833. * @param object $element
  834. * @param object $event
  835. * @param object $kanban
  836. * @access public
  837. * @return void
  838. */
  839. function handleKanbanAction(action, $element, event, kanban)
  840. {
  841. $('.kanban').attr('data-action-enabled', action);
  842. var handler = kanbanActionHandlers[action];
  843. if(handler) handler($element, event, kanban);
  844. }
  845. /**
  846. * Handle finish drop task
  847. * @param {Object} event Event object
  848. * @returns {void}
  849. */
  850. function handleFinishDrop(event)
  851. {
  852. $('#kanbans').find('.can-drop-here').removeClass('can-drop-here');
  853. }
  854. /**
  855. * Create column menu
  856. * @returns {Object[]}
  857. */
  858. function createColumnMenu(options)
  859. {
  860. var $col = options.$trigger.closest('.kanban-col');
  861. var col = $col.data('col');
  862. var kanbanID = options.kanban;
  863. var items = [];
  864. if(priv.canEditName) items.push({label: executionLang.editName, url: $.createLink('kanban', 'setColumn', 'col=' + col.id + '&executionID=' + executionID + '&from=execution'), className: 'iframe', attrs: {'data-width': '500px'}})
  865. if(priv.canSetWIP) items.push({label: executionLang.setWIP, url: $.createLink('kanban', 'setWIP', 'col=' + col.id + '&executionID=' + executionID + '&from=execution'), className: 'iframe', attrs: {'data-width': '500px'}})
  866. //if(priv.canSortCards) items.push({label: executionLang.sortColumn, items: ['按ID倒序', '按ID顺序'], className: 'iframe', onClick: handleSortColCards})
  867. return items;
  868. }
  869. /**
  870. * Create column create button menu
  871. * @returns {Object[]}
  872. */
  873. function createColumnCreateMenu(options)
  874. {
  875. var $col = options.$trigger.closest('.kanban-col');
  876. var col = $col.data('col');
  877. var items = [];
  878. if(col.laneType == 'story')
  879. {
  880. if(priv.canCreateStory) items.push({label: storyLang.create, url: $.createLink('story', 'create', 'productID=' + productID + '&branch=0&moduleID=0&storyID=0&objectID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '80%'}});
  881. if(priv.canBatchCreateStory) items.push({label: executionLang.batchCreateStory, url: $.createLink('story', 'batchcreate', 'productID=' + productID + '&branch=0&moduleID=0&storyID=0&executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}});
  882. if(priv.canLinkStory) items.push({label: executionLang.linkStory, url: $.createLink('execution', 'linkStory', 'executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}});
  883. if(priv.canLinkStoryByPlan) items.push({label: executionLang.linkStoryByPlan, url: '#linkStoryByPlan', 'attrs' : {'data-toggle': 'modal'}});
  884. }
  885. else if(col.laneType == 'bug')
  886. {
  887. if(priv.canCreateBug) items.push({label: bugLang.create, url: $.createLink('bug', 'create', 'productID=0&moduleID=0&extra=executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '80%'}});
  888. if(priv.canBatchCreateBug)
  889. {
  890. if(productNum > 1) items.push({label: bugLang.batchCreate, url: '#batchCreateBug', 'attrs' : {'data-toggle': 'modal'}});
  891. else items.push({label: bugLang.batchCreate, url: $.createLink('bug', 'batchcreate', 'productID=' + productID + '&moduleID=0&executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}});
  892. }
  893. }
  894. else
  895. {
  896. if(priv.canCreateTask) items.push({label: taskLang.create, url: $.createLink('task', 'create', 'executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '80%'}});
  897. if(priv.canBatchCreateTask) items.push({label: taskLang.batchCreate, url: $.createLink('task', 'batchcreate', 'executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}});
  898. if(priv.canImportBug && canImportBug) items.push({label: executionLang.importBug, url: $.createLink('execution', 'importBug', 'executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}});
  899. }
  900. return items;
  901. }
  902. /**
  903. * Create lane menu
  904. * @returns {Object[]}
  905. */
  906. function createLaneMenu(options)
  907. {
  908. var $lane = options.$trigger.closest('.kanban-lane');
  909. var $kanban = $lane.closest('.kanban');
  910. var lane = $lane.data('lane');
  911. var kanbanID = options.kanban;
  912. var upTargetKanban = $kanban.prev('.kanban').length ? $kanban.prev('.kanban').data('id') : '';
  913. var downTargetKanban = $kanban.next('.kanban').length ? $kanban.next('.kanban').data('id') : '';
  914. var items = [];
  915. if(priv.canSetLane) items.push({label: kanbanLang.setLane, icon: 'edit', url: $.createLink('kanban', 'setLane', 'lane=' + lane.laneID + '&executionID=' + executionID + '&from=execution'), className: 'iframe'});
  916. var bounds = options.$trigger[0].getBoundingClientRect();
  917. items.$options = {x: bounds.right, y: bounds.top};
  918. return items;
  919. }
  920. /**
  921. * Create story menu
  922. * @returns {Object[]}
  923. */
  924. function createStoryMenu(options)
  925. {
  926. var $card = options.$trigger.closest('.kanban-item');
  927. var story = $card.data('item');
  928. var items = [];
  929. $.each(story.menus, function()
  930. {
  931. var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}};
  932. if(this.size) item.attrs['data-width'] = this.size;
  933. if(this.icon == 'unlink' || this.icon == 'trash') item = {label: this.label, icon: this.icon, url: this.url, attrs: {'target': 'hiddenwin'}};
  934. items.push(item);
  935. });
  936. return items;
  937. }
  938. /**
  939. * Create bug menu
  940. * @returns {Object[]}
  941. */
  942. function createBugMenu(options)
  943. {
  944. var $card = options.$trigger.closest('.kanban-item');
  945. var bug = $card.data('item');
  946. var items = [];
  947. $.each(bug.menus, function()
  948. {
  949. var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}};
  950. if(this.size) item.attrs['data-width'] = this.size;
  951. if(this.icon == 'trash') item = {label: this.label, icon: this.icon, url: this.url, attrs: {'target': 'hiddenwin'}};
  952. items.push(item);
  953. });
  954. return items;
  955. }
  956. /**
  957. * Create task menu
  958. * @returns {Object[]}
  959. */
  960. function createTaskMenu(options)
  961. {
  962. var $card = options.$trigger.closest('.kanban-item');
  963. var task = $card.data('item');
  964. var items = [];
  965. $.each(task.menus, function()
  966. {
  967. var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}};
  968. if(this.size) item.attrs['data-width'] = this.size;
  969. if(this.icon == 'trash') item = {label: this.label, icon: this.icon, url: this.url, attrs: {'target': 'hiddenwin'}};
  970. items.push(item);
  971. });
  972. return items;
  973. }
  974. /** Resize kanban container size */
  975. function resizeKanbanContainer()
  976. {
  977. var $container = $('#kanbanContainer');
  978. var maxHeight = window.innerHeight - 98 - 15;
  979. if($.cookie('isFullScreen') == 1) maxHeight = window.innerHeight - 15;
  980. $container.children('.panel-body').css('max-height', maxHeight);
  981. }
  982. /* Define menu creators */
  983. window.menuCreators =
  984. {
  985. column: createColumnMenu,
  986. columnCreate: createColumnCreateMenu,
  987. lane: createLaneMenu,
  988. story: createStoryMenu,
  989. bug: createBugMenu,
  990. task: createTaskMenu,
  991. };
  992. /* Set kanban affix container */
  993. window.kanbanAffixContainer = '#kanbanContainer>.panel-body';
  994. /* Overload kanban default options */
  995. $.extend($.fn.kanban.Constructor.DEFAULTS,
  996. {
  997. onRender: function()
  998. {
  999. var maxWidth = 0;
  1000. $('#kanbans .kanban-board').each(function()
  1001. {
  1002. maxWidth = Math.max(maxWidth, $(this).outerWidth());
  1003. });
  1004. $('#kanbans').css('min-width', maxWidth);
  1005. }
  1006. });
  1007. /** Get card height */
  1008. function getCardHeight()
  1009. {
  1010. return [59, 59, 62, 62, 47][window.kanbanScaleSize];
  1011. }
  1012. /** Change kanban scale size */
  1013. function changeKanbanScaleSize(newScaleSize)
  1014. {
  1015. var newScaleSize = Math.max(1, Math.min(4, newScaleSize));
  1016. if(newScaleSize === window.kanbanScaleSize) return;
  1017. window.kanbanScaleSize = newScaleSize;
  1018. $.zui.store.set('executionKanbanScaleSize', newScaleSize);
  1019. $('#kanbanScaleSize').text(newScaleSize);
  1020. $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', newScaleSize >= 4 ? 'disabled' : null);
  1021. $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', newScaleSize <= 1 ? 'disabled' : null);
  1022. $('#kanbans').children('.kanban').each(function()
  1023. {
  1024. var kanban = $(this).data('zui.kanban');
  1025. if(!kanban) return;
  1026. kanban.setOptions({cardsPerRow: newScaleSize, cardHeight: getCardHeight()});
  1027. });
  1028. resetKanbanHeight();
  1029. return newScaleSize;
  1030. }
  1031. /** Affix kanban board header */
  1032. window.affixKanbanHeader = function($kanbanBoard, affixed)
  1033. {
  1034. var $header = $kanbanBoard.children('.kanban-header');
  1035. var $headerCols = $header.children('.kanban-header-cols');
  1036. var headerStyle = {width: '', left: 0};
  1037. var headerColsStyle = {width: '', marginLeft: ''};
  1038. if(affixed)
  1039. {
  1040. var $kanban = $('#kanbanContainer');
  1041. var kanbanBounding = $kanban[0].getBoundingClientRect();
  1042. var kanbanBoardBounding = $kanbanBoard[0].getBoundingClientRect();
  1043. var laneNameWidth = +$headerCols.css('left').replace('px', '');
  1044. headerStyle.width = kanbanBounding.width;
  1045. headerStyle.left = kanbanBounding.left;
  1046. headerColsStyle.width = kanbanBoardBounding.width - laneNameWidth;
  1047. headerColsStyle.marginLeft = kanbanBoardBounding.left - kanbanBounding.left;
  1048. }
  1049. $header.css(headerStyle);
  1050. $headerCols.css(headerColsStyle);
  1051. $kanbanBoard.toggleClass('kanban-affixed', !!affixed);
  1052. $kanbanBoard.css('padding-top', affixed ? $header.outerHeight() : '');
  1053. }
  1054. /**
  1055. * Handle sort cards.
  1056. *
  1057. * @param object event
  1058. * @access public
  1059. * @return void
  1060. */
  1061. function handleSortCards(event)
  1062. {
  1063. if(groupBy != 'default' || searchValue != '') return;
  1064. var newLaneID = event.element.closest('.kanban-lane').data('id');
  1065. var newColID = event.element.closest('.kanban-col').data('id');
  1066. var cards = event.element.closest('.kanban-lane-items').data('cards');
  1067. var orders = cards.map(function(card){return card.id});
  1068. var fromID = String(event.element.data('id'));
  1069. var toID = String(event.target.data('id'));
  1070. orders.splice(orders.indexOf(fromID), 1);
  1071. orders.splice(orders.indexOf(toID) + (event.insert === 'before' ? 0 : 1), 0, fromID);
  1072. var url = createLink('kanban', 'sortCard', 'kanbanID=' + executionID + '&laneID=' + newLaneID + '&columnID=' + newColID + '&cards=' + orders.join(','));
  1073. $.getJSON(url, function(response)
  1074. {
  1075. if(response.result === 'fail')
  1076. {
  1077. if(typeof response.message === 'string' && response.message.length)
  1078. {
  1079. bootbox.alert(response.message);
  1080. }
  1081. setTimeout(function(){return location.reload()}, 3000);
  1082. }
  1083. else
  1084. {
  1085. $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=0&browseType=" + browseType + "&groupBy=" + groupBy + '&from=execution' + '&searchValue=' + searchValue + '&orderBy=' + orderBy), function(data)
  1086. {
  1087. if(data && lastUpdateData !== data)
  1088. {
  1089. lastUpdateData = data;
  1090. kanbanGroup = $.parseJSON(data);
  1091. var kanbanLane = '';
  1092. for(var i in kanbanList)
  1093. {
  1094. if(kanbanList[i] == 'story') kanbanLane = kanbanGroup.story;
  1095. if(kanbanList[i] == 'bug') kanbanLane = kanbanGroup.bug;
  1096. if(kanbanList[i] == 'task') kanbanLane = kanbanGroup.task;
  1097. if(browseType == kanbanList[i] || browseType == 'all') updateKanban(kanbanList[i], kanbanLane);
  1098. }
  1099. }
  1100. });
  1101. }
  1102. });
  1103. }
  1104. /* Example code: */
  1105. $(function()
  1106. {
  1107. $.cookie('isFullScreen', 0);
  1108. window.kanbanScaleSize = +$.zui.store.get('executionKanbanScaleSize', 1);
  1109. $('#kanbanScaleSize').text(window.kanbanScaleSize);
  1110. $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', window.kanbanScaleSize >= 4 ? 'disabled' : null);
  1111. $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', window.kanbanScaleSize <= 1 ? 'disabled' : null);
  1112. changeOrder = false;
  1113. /* Common options */ 
  1114. var commonOptions =
  1115. {
  1116. maxColHeight: 'auto',
  1117. minColWidth: typeof window.minColWidth === 'number' ? window.minColWidth : defaultMinColWidth,
  1118. maxColWidth: typeof window.maxColWidth === 'number' ? window.maxColWidth : defaultMaxColWidth,
  1119. cardHeight: getCardHeight(),
  1120. showCount: true,
  1121. showZeroCount: true,
  1122. fluidBoardWidth: fluidBoard,
  1123. cardsPerRow: window.kanbanScaleSize,
  1124. virtualize: true,
  1125. onAction: handleKanbanAction,
  1126. virtualRenderOptions: {container: '#kanbanContainer>.panel-body,#kanbanContainer'},
  1127. virtualCardList: true,
  1128. droppable:
  1129. {
  1130. target: findDropColumns,
  1131. finish: handleFinishDrop
  1132. },
  1133. onRenderHeaderCol: renderHeaderCol,
  1134. onRenderLaneName: renderLaneName,
  1135. onRenderCount: renderColumnCount,
  1136. sortable: handleSortCards,
  1137. };
  1138. /* Create kanban */
  1139. if(groupBy == 'default')
  1140. {
  1141. var kanbanLane = '';
  1142. for(var i in kanbanList)
  1143. {
  1144. if(kanbanList[i] == 'story') kanbanLane = kanbanGroup.story;
  1145. if(kanbanList[i] == 'bug') kanbanLane = kanbanGroup.bug;
  1146. if(kanbanList[i] == 'task') kanbanLane = kanbanGroup.task;
  1147. if(browseType == kanbanList[i] || browseType == 'all') createKanban(kanbanList[i], kanbanLane, commonOptions);
  1148. }
  1149. }
  1150. else
  1151. {
  1152. /* Create kanban by group. */
  1153. createKanban(browseType, kanbanGroup[groupBy], commonOptions);
  1154. }
  1155. /* Init iframe modals */
  1156. $(document).on('click', '#kanbans .iframe,.contextmenu-menu .iframe', function(event)
  1157. {
  1158. var $link = $(this);
  1159. if($link.data('zui.modaltrigger')) return;
  1160. $link.modalTrigger({show: true});
  1161. event.preventDefault();
  1162. });
  1163. /* Init contextmenu */
  1164. $('#kanbans').on('click', '[data-contextmenu]', function(event)
  1165. {
  1166. var $trigger = $(this);
  1167. var menuType = $trigger.data('contextmenu');
  1168. var menuCreator = window.menuCreators[menuType];
  1169. if(!menuCreator) return;
  1170. var options = $.extend({event: event, $trigger: $trigger}, $trigger.data());
  1171. var items = menuCreator(options);
  1172. if(!items || !items.length) return;
  1173. $.zui.ContextMenu.show(items, items.$options || {event: event});
  1174. });
  1175. /* Make kanbanScaleControl works */
  1176. $('#kanbanScaleControl').on('click', '.btn', function()
  1177. {
  1178. changeKanbanScaleSize(window.kanbanScaleSize + ($(this).data('type') === '+' ? 1 : -1));
  1179. });
  1180. /* Resize kanban container on window resize */
  1181. resizeKanbanContainer();
  1182. $(window).on('resize', resizeKanbanContainer);
  1183. /* Hide contextmenu when page scroll */
  1184. $(window).on('scroll', function()
  1185. {
  1186. $.zui.ContextMenu.hide();
  1187. });
  1188. $('#toStoryButton').on('click', function()
  1189. {
  1190. var planID = $('#plan').val();
  1191. if(planID)
  1192. {
  1193. var param = "&param=executionID=" + executionID + ",browseType=" + browseType + ",orderBy=id_asc,groupBy=" + groupBy;
  1194. location.href = createLink('execution', 'importPlanStories', 'executionID=' + executionID + '&planID=' + planID + '&productID=0&extra=' + param);
  1195. }
  1196. });
  1197. $('#product').change(function()
  1198. {
  1199. var product = $('#product').val();
  1200. if(product)
  1201. {
  1202. var link = createLink('bug', 'batchCreate', 'productID=' + product + '&branch=&executionID=' + executionID, '', true);
  1203. $('#batchCreateBugButton').attr('href', link);
  1204. }
  1205. });
  1206. document.addEventListener('scroll', function()
  1207. {
  1208. $('.storyColumn').parent().removeClass('open');
  1209. }, true);
  1210. $('#type_chosen .chosen-single span').prepend('<i class="icon-kanban"></i>');
  1211. $('#group_chosen .chosen-single span').prepend(kanbanLang.laneGroup + ': ');
  1212. /* Ajax update kanban. */
  1213. lastUpdateData = '';
  1214. setInterval(function()
  1215. {
  1216. $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=" + entertime + "&browseType=" + browseType + "&groupBy=" + groupBy + '&from=execution&searchValue=' + searchValue + '&orderBy=' + orderBy), function(data)
  1217. {
  1218. if(lastUpdateData == '') lastUpdateData = data;
  1219. if(data && lastUpdateData !== data)
  1220. {
  1221. lastUpdateData = data;
  1222. kanbanGroup = $.parseJSON(data);
  1223. if(groupBy == 'default')
  1224. {
  1225. var kanbanLane = '';
  1226. for(var i in kanbanList)
  1227. {
  1228. if(kanbanList[i] == 'story') kanbanLane = kanbanGroup.story;
  1229. if(kanbanList[i] == 'bug') kanbanLane = kanbanGroup.bug;
  1230. if(kanbanList[i] == 'task') kanbanLane = kanbanGroup.task;
  1231. if(browseType == kanbanList[i] || browseType == 'all') updateKanban(kanbanList[i], kanbanLane);
  1232. }
  1233. }
  1234. else
  1235. {
  1236. updateKanban(browseType, kanbanGroup[groupBy]);
  1237. }
  1238. }
  1239. });
  1240. }, 10000);
  1241. resetKanbanHeight();
  1242. var kanbanMinColWidth = typeof window.minColWidth === 'number' ? window.minColWidth : defaultMinColWidth;
  1243. if(kanbanMinColWidth < 190)
  1244. {
  1245. var miniColWidth = kanbanMinColWidth * 0.2;
  1246. $('.kanban-header-col>.title>span:not(.text)').hide();
  1247. $('.kanban-header-col>.title > span.text').css('max-width', miniColWidth + 'px');
  1248. }
  1249. $('[data-toggle="tooltip"]').tooltip();
  1250. });
  1251. $('#type').change(function()
  1252. {
  1253. var type = $('#type').val();
  1254. if(type != 'all')
  1255. {
  1256. $('.c-group').show();
  1257. $.get(createLink('execution', 'ajaxGetGroup', 'type=' + type), function(data)
  1258. {
  1259. $('#group_chosen').remove();
  1260. $('#group').replaceWith(data);
  1261. $('#group').chosen();
  1262. })
  1263. }
  1264. var link = createLink('execution', 'taskKanban', "executionID=" + executionID + '&type=' + type);
  1265. location.href = link;
  1266. });
  1267. $('.c-group').change(function()
  1268. {
  1269. $('.c-group').show();
  1270. var type = $('#type').val();
  1271. var group = $('#group').val();
  1272. var link = createLink('execution', 'taskKanban', 'executionID=' + executionID + '&type=' + type + '&orderBy=order_asc' + '&groupBy=' + group);
  1273. location.href = link;
  1274. });
  1275. /** Calculate column height */
  1276. function calcColHeight(col, lane, colCards, colHeight, kanban)
  1277. {
  1278. var options = kanban.options;
  1279. if(!options.displayCards) return colHeight;
  1280. var displayCards = +(options.displayCards || 2);
  1281. if (typeof displayCards !== 'number' || displayCards < 2) displayCards = 2;
  1282. return (displayCards * (options.cardHeight + options.cardSpace) + options.cardSpace);
  1283. }
  1284. /* Hide contextmenu when page scroll */
  1285. $('.panel-body').scroll(function()
  1286. {
  1287. $.zui.ContextMenu.hide();
  1288. });
  1289. /**
  1290. * Reset kanban height according to window height.
  1291. *
  1292. * @access public
  1293. * @return void
  1294. */
  1295. function resetKanbanHeight()
  1296. {
  1297. var laneCount = $('.kanban-lane').length;
  1298. if(laneCount > 1) return;
  1299. var windowHeight = $(window).height();
  1300. var headerHeight = $('#mainHeader').outerHeight();
  1301. var mainPadding = $('#main').css('padding-top');
  1302. var menuHeight = $('#mainMenu').height();
  1303. var panelBorder = $('.panel').css('border-top-width');
  1304. var bodyPadding = $('.panel-body').css('padding-top');
  1305. var columnHeight = $('.kanban-header').outerHeight();
  1306. var height = windowHeight - headerHeight - (parseInt(mainPadding) * 2) - menuHeight - (parseInt(panelBorder) * 2) - (parseInt(bodyPadding) * 2) - columnHeight;
  1307. $('.kanban-lane').css('height', height -2);
  1308. }
  1309. $(document).on('click', '.dropdown-menu', function()
  1310. {
  1311. $.zui.ContextMenu.hide();
  1312. });
  1313. /**
  1314. * Toggle kanban search box.
  1315. *
  1316. * @access public
  1317. * @return void
  1318. */
  1319. function toggleSearchBox()
  1320. {
  1321. $('#searchBox').toggle();
  1322. if($('#searchBox').css('display') == 'block')
  1323. {
  1324. $(".querybox-toggle").css("color", "#0c64eb");
  1325. }
  1326. else
  1327. {
  1328. $(".querybox-toggle").css("color", "#3c495c");
  1329. $('#taskKanbanSearchInput').attr('value', '');
  1330. searchCards('');
  1331. }
  1332. }
  1333. /**
  1334. * Search kanban cards.
  1335. *
  1336. * @param string value
  1337. * @param string order
  1338. *
  1339. * @access public
  1340. * @return void
  1341. */
  1342. function searchCards(value, order = '')
  1343. {
  1344. searchValue = value;
  1345. orderBy = order == '' ? orderBy : order;
  1346. if(order != '') changeOrder = true;
  1347. $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=0&browseType=" + browseType + "&groupBy=" + groupBy + '&from=execution&searchValue=' + value + '&orderBy=' + orderBy), function(data)
  1348. {
  1349. lastUpdateData = data;
  1350. var kanbanData = $.parseJSON(data);
  1351. var hideAll = true;
  1352. if(groupBy == 'default')
  1353. {
  1354. var kanbanLane = '';
  1355. for(var i in kanbanList)
  1356. {
  1357. if(kanbanList[i] == 'story') kanbanLane = kanbanData.story;
  1358. if(kanbanList[i] == 'bug') kanbanLane = kanbanData.bug;
  1359. if(kanbanList[i] == 'task') kanbanLane = kanbanData.task;
  1360. if(browseType == kanbanList[i] || browseType == 'all') hideAll = !updateKanban(kanbanList[i], kanbanLane) && hideAll;
  1361. }
  1362. }
  1363. else
  1364. {
  1365. hideAll = !updateKanban(browseType, kanbanData[groupBy]) && hideAll;
  1366. }
  1367. if(hideAll)
  1368. {
  1369. $("#emptyBox").removeClass('hidden');
  1370. $("#kanbanContainer .panel-body").addClass('hidden');
  1371. }
  1372. else
  1373. {
  1374. $("#emptyBox").addClass('hidden');
  1375. $("#kanbanContainer .panel-body").removeClass('hidden');
  1376. }
  1377. });
  1378. }