',
@@ -207,7 +482,7 @@ function renderTaskItem(item, $item, col)
if(!$title.length)
{
$title = $('
')
- .attr('href', $.createLink('task', 'view', 'taskID=' + item.id, '', true));
+ .attr('href', $.createLink('task', 'view', 'taskID=' + item.id, '', true)).attr('data-toggle', 'modal').attr('data-width', '80%');
$title.appendTo($item);
}
$title.attr('title', item.name).find('.text').text(item.name);
@@ -238,7 +513,7 @@ function renderTaskItem(item, $item, col)
if(scaleSize <= 1)
{
var $actions = $item.find('.actions');
- if(!$actions.length && item.menus.length)
+ if(!$actions.length && item.menus && item.menus.length)
{
$actions = $([
'
',
@@ -261,272 +536,202 @@ addColumnRenderer('bug', renderBugItem);
addColumnRenderer('task', renderTaskItem);
/**
- * Render column count
- * @param {JQuery} $count Kanban count element
- * @param {number} count Column cards count
- * @param {number} col Column object
- * @param {Object} kanban Kanban intance
+ * Render items count of a column.
*/
-function renderColumnCount($count, count, col)
+function renderCount($count, count, column)
{
- var text = count + '/' + (col.limit < 0 ? '
' : col.limit);
- $count.html(text + '
');
+ /* Render WIP. */
+ var limit = !column.limit || column.limit == '-1' ? '
' : column.limit;
+ if($count.parent().find('.limit').length)
+ {
+ $count.parent().find('.limit').html(limit);
+ }
+ else
+ {
+ $count.parent().find('.count').before("
(");
+ $count.parent().find('.count').after("
/" + limit + ")");
+ }
+
+ if(column.limit != -1 && column.limit < count)
+ {
+ $count.parents('.title').parent('.kanban-header-col').css('background-color', '#F6A1A1');
+ $count.parents('.title').find('.text').css('max-width', $count.parents('.title').width() - 200);
+ $count.css('color', '#E33030');
+ if(!$count.parent().find('.error').length) $count.parent().find('.include-last').after("
");
+ }
+ else
+ {
+ $count.parents('.title').parent('.kanban-header-col').css('background-color', 'transparent');
+ $count.parents('.title').find('.text').css('max-width', $count.parents('.title').width() - 120);
+ $count.css('color', '#8B91A2');
+ $count.parent().find('.error').remove();
+ }
}
/**
- * Render header column
- * @param {JQuery} $col Header column element
- * @param {Object} col Header column object
- * @param {JQuery} $header Header element
- * @param {Object} kanban Kanban object
+ * Render header of a column.
*/
-function renderHeaderCol($col, col, $header, kanban)
+function renderHeaderCol($column, column, $header, kanbanData)
{
- if(col.asParent) $col = $col.children('.kanban-header-col');
- var $actions = $('
');
- var printStoryButton = printTaskButton = printBugButton = false;
- if(priv.canCreateStory || priv.canBatchCreateStory || priv.canLinkStory || priv.canLinkStoryByPlane) printStoryButton = true;
- if(priv.canCreateTask || priv.canBatchCreateTask) printTaskButton = true;
- if(priv.canCreateBug || priv.canBatchCreateBug) printBugButton = true;
+ /* Render group header. */
+ var privs = kanbanData.actions;
+ var columnPrivs = kanbanData.columns[0].actions;
+ var $actions = $column.children('.actions');
- if((col.type === 'backlog' && printStoryButton) || (col.type === 'wait' && printTaskButton) || (col.type == 'unconfirmed' && printBugButton))
+ if(privs.includes('sortGroup'))
+ {
+ var groups = regions[column.region].groups;
+ if($header.closest('.kanban').data('zui.kanban'))
+ {
+ groups = $header.closest('.kanban').data('zui.kanban').data;
+ }
+ if(groups.length > 1)
+ {
+ $column.closest('.kanban-board').addClass('sort');
+ $column.closest('.kanban-header').find('.kanban-group-header').remove();
+ $column.closest('.kanban-header').prepend('');
+ }
+ }
+
+ var printMoreBtn = (columnPrivs.includes('setColumn') || columnPrivs.includes('setWIP'));
+
+ /* Render more menu. */
+ if(((column.type == 'backlog' && hasStoryButton) || (column.type == 'wait' && hasTaskButton) || (column.type == 'unconfirmed' && hasBugButton)) && $actions.children('.text-primary').length == 0)
{
$actions.append([
- '
',
+ '',
'',
''
].join(''));
}
-
- $actions.append([
- '
',
- '',
- ''
- ].join(''));
- $actions.appendTo($col);
-}
-
-/**
- * Render lane name
- * @param {JQuery} $name Name element
- * @param {Object} lane Lane object
- * @param {JQuery} $kanban $kanban element
- * @param {Object} columns Kanban columns
- * @param {Object} kanban Kanban object
- */
-function renderLaneName($name, lane, $kanban, columns, kanban)
-{
- if(lane.id != 'story' && lane.id != 'task' && lane.id != 'bug') return false;
- if(!$name.children('.actions').length && (priv.canSetLane || priv.canMoveLane))
+ if(printMoreBtn && $actions.children('.btn').length == 0)
{
- $([
- '
'
- ].join('')).appendTo($name);
+ $actions.append('
');
}
}
/**
- * Updata kanban data
- * @param {string} kanbanID Kanban id
- * @param {Object} data Kanban data
- */
-function updateKanban(kanbanID, data)
-{
- var $kanban = $('#kanban-' + kanbanID);
- if(!$kanban.length) return;
-
- $kanban.data('zui.kanban').render(data);
-}
-
-/**
- * Create kanban in page
- * @param {string} kanbanID Kanban id
- * @param {Object} data Kanban data
- * @param {Object} options Kanban options
- */
-function createKanban(kanbanID, data, options)
-{
- var $kanban = $('#kanban-' + kanbanID);
- if($kanban.length) return updateKanban(kanbanID, data);
-
- $kanban = $('
').appendTo('#kanbans');
- $kanban.kanban($.extend({data: data}, options));
-}
-
-function fullScreen()
-{
- var element = document.getElementById('kanbanContainer');
- var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen;
- if(requestMethod)
- {
- var afterEnterFullscreen = function()
- {
- $('#kanbanContainer').addClass('scrollbar-hover');
- $('.actions').hide();
- $('#kanbanContainer a.iframe').each(function()
- {
- if($(this).hasClass('iframe'))
- {
- var href = $(this).attr('href');
- $(this).removeClass('iframe');
- $(this).attr('href', 'javascript:void(0)');
- $(this).attr('href-bak', href);
- }
- })
- $.cookie('isFullScreen', 1);
- }
-
- var whenFailEnterFullscreen = function()
- {
- exitFullScreen();
- }
-
- try
- {
- var result = requestMethod.call(element);
- if(result && (typeof result.then === 'function' || result instanceof window.Promise))
- {
- result.then(afterEnterFullscreen).catch(whenFailEnterFullscreen);
- }
- else
- {
- afterEnterFullscreen();
- }
- }
- catch (error)
- {
- whenFailEnterFullscreen(error);
- }
- }
-}
-
-/**
- * Exit full screen.
+ * Render lane name.
*
+ * @param object $lane
+ * @param int lane
+ * @param object $kanban
+ * @param array columns
+ * @param object $kanban
* @access public
* @return void
*/
-function exitFullScreen()
+function renderLaneName($lane, lane, $kanban, columns, kanban)
{
- $('#kanbanContainer').removeClass('scrollbar-hover');
- $('.actions').show();
- $('#kanbanContainer a').each(function()
+ var canSet = lane.actions.includes('setLane');
+ var canSort = lane.actions.includes('sortLane') && kanban.lanes.length > 1;
+ var canDelete = lane.actions.includes('deleteLane');
+
+ $lane.parent().toggleClass('sort', canSort);
+
+ if(!$lane.children('.actions').length && (canSet || canDelete))
{
- var hrefBak = $(this).attr('href-bak');
- if(hrefBak)
- {
- $(this).addClass('iframe');
- $(this).attr('href', hrefBak);
- }
- })
- $.cookie('isFullScreen', 0);
-}
-
-document.addEventListener('fullscreenchange', function (e)
-{
- if(!document.fullscreenElement) exitFullScreen();
-});
-
-document.addEventListener('webkitfullscreenchange', function (e)
-{
- if(!document.webkitFullscreenElement) exitFullScreen();
-});
-
-document.addEventListener('mozfullscreenchange', function (e)
-{
- if(!document.mozFullScreenElement) exitFullScreen();
-});
-
-document.addEventListener('msfullscreenChange', function (e)
-{
- if(!document.msfullscreenElement) exitFullScreen();
-});
-
-/* Define drag and drop rules */
-if(!window.kanbanDropRules)
-{
- window.kanbanDropRules =
- {
- story:
- {
- backlog: ['ready'],
- ready: ['backlog'],
- },
- bug:
- {
- 'unconfirmed': ['confirmed', 'fixing', 'fixed'],
- 'confirmed': ['fixing', 'fixed'],
- 'fixing': ['fixed'],
- 'fixed': ['testing', 'tested', 'fixing'],
- 'testing': ['tested', 'closed', 'fixing'],
- 'tested': ['closed', 'fixing'],
- 'closed': ['fixing'],
- },
- task:
- {
- 'wait': ['developing', 'developed', 'canceled', 'closed'],
- 'developing': ['developed', 'pause'],
- 'developed': ['canceled', 'closed'],
- 'pause': ['developing'],
- 'canceled': ['developing'],
- 'closed': ['developing'],
- }
+ $([
+ '
'
+ ].join('')).appendTo($lane);
}
}
-/*
- * Find drop columns
- * @param {JQuery} $element Drag element
- * @param {JQuery} $root Dnd root element
+/**
+ * Update a region.
+ *
+ * @param int regionID
+ * @param array regionData
+ * @access public
+ * @return boolean
*/
-function findDropColumns($element, $root)
+function updateRegion(regionID, regionData = [])
{
- var $col = $element.closest('.kanban-col');
- var col = $col.data();
- var kanbanID = $root.data('id');
- var kanbanRules = window.kanbanDropRules ? window.kanbanDropRules[kanbanID] : null;
+ if(!regionID) return false;
- if(!kanbanRules) return $root.find('.kanban-lane-col:not([data-type="' + col.type + '"])');
+ var $region = $('#kanban'+ regionID).kanban();
- var colRules = kanbanRules[col.type];
- var lane = $col.closest('.kanban-lane').data('lane');
- return $root.find('.kanban-lane-col').filter(function()
- {
- if(!colRules) return false;
- if(colRules === true) return true;
+ if(!$region.length) return false;
+ if(!regionData) regionData = regions[regionID];
- var $newCol = $(this);
- var newCol = $newCol.data();
- if(newCol.id === col.id) return false;
-
- var $newLane = $newCol.closest('.kanban-lane');
- var newLane = $newLane.data('lane');
- var canDropHere = colRules.indexOf(newCol.type) > -1 && newLane.id === lane.id;
- if(canDropHere) $newCol.addClass('can-drop-here');
- return canDropHere;
- });
+ $region.data('zui.kanban').render(regionData.groups);
+ return true;
}
/**
- * Change column type for a card
- * @param {Object} card Card object
- * @param {String} fromColType The column type before change
- * @param {String} toColType The column type after change
- * @param {String} kanbanID Kanban ID
+ * Handle drop task.
+ *
+ * @param object $element
+ * @param object $event
+ * @param object $kanban
+ * @access public
+ * @return void
*/
-function changeCardColType(card, fromColType, toColType, kanbanID)
+function handleDropTask($element, event, kanban)
{
- if(typeof card == 'undefined') return false;
- var objectID = card.id;
+ if(!event.target || !event.isNew) return;
+
+ var $card = $element;
+ var $oldCol = $card.closest('.kanban-col');
+ var $newCol = $(event.target).closest('.kanban-col');
+ var oldCol = $oldCol.data();
+ var newCol = $newCol.data();
+ var oldLane = $oldCol.closest('.kanban-lane').data('lane');
+ var newLane = $newCol.closest('.kanban-lane').data('lane');
+ var cardType = $card.find('.kanban-card').data('type');
+
+ if(oldCol.id === newCol.id && newLane.id === oldLane.id) return false;
+
+ var cardID = $card.data().id;
+ var fromColType = $oldCol.data('type');
+ var toColType = $newCol.data('type');
+ var regionID = $card.closest('.region').data().id;
+
+ changeCardColType(cardID, oldCol.id, newCol.id, oldLane.id, newLane.id, cardType, fromColType, toColType, regionID);
+}
+
+var kanbanActionHandlers =
+{
+ dropItem: handleDropTask
+};
+
+/**
+ * Handle kanban action
+ */
+function handleKanbanAction(action, $element, event, kanban)
+{
+ $('.kanban').attr('data-action-enabled', action);
+ var handler = kanbanActionHandlers[action];
+ if(handler) handler($element, event, kanban);
+}
+
+/**
+ * changeCardColType
+ *
+ * @param int cardID
+ * @param int fromColID
+ * @param int toColID
+ * @param int fromLaneID
+ * @param int toLaneID
+ * @param string cardType
+ * @param string fromColType
+ * @param string toColType
+ * @param int regionID
+ * @access public
+ * @return void
+ */
+function changeCardColType(cardID, fromColID, toColID, fromLaneID, toLaneID, cardType, fromColType, toColType, regionID = 0)
+{
+ var objectID = cardID;
var showIframe = false;
var moveCard = false;
/* Task lane. */
- if(kanbanID == 'task')
+ if(cardType == 'task')
{
if(toColType == 'developed')
{
@@ -576,7 +781,7 @@ function changeCardColType(card, fromColType, toColType, kanbanID)
}
/* Bug lane. */
- if(kanbanID == 'bug')
+ if(cardType == 'bug')
{
if(toColType == 'confirmed')
{
@@ -597,7 +802,7 @@ function changeCardColType(card, fromColType, toColType, kanbanID)
}
else if(toColType == 'fixed')
{
- if(fromColType == 'fixing' || fromColType == 'confirmed' || fromColType == 'unconfirmed')
+ if(fromColType == 'fixing' || fromColType == 'confirmed' || fromColType == 'unconfirmed')
{
var link = createLink('bug', 'resolve', 'bugID=' + objectID, '', true);
showIframe = true;
@@ -622,48 +827,44 @@ function changeCardColType(card, fromColType, toColType, kanbanID)
if(moveCard)
{
- var colID = card.$col.columnID;
- var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&colID=' + colID + '&toColType=' + toColType + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy);
- $.get(link, function(data)
+ var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy + '®ionID=' + regionID + '&orderBy=' + orderBy );
+ $.ajax(
{
- if(data)
+ method: 'post',
+ dataType: 'json',
+ url: link,
+ success: function(data)
{
- kanbanGroup = $.parseJSON(data);
- if(groupBy == 'default')
- {
- updateKanban('bug', kanbanGroup.bug);
- }
- else
- {
- updateKanban(browseType, kanbanGroup[groupBy]);
- }
+ updateRegion(regionID, data[regionID]);
+ },
+ error: function(xhr, status, error)
+ {
+ showErrorMessager(error || lang.timeout);
}
- })
+ });
}
}
/* Story lane. */
- if(kanbanID == 'story')
+ if(cardType == 'story')
{
if(toColType == 'ready' || toColType == 'backlog')
{
- var colID = card.$col.columnID;
- var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&colID=' + colID + '&toColType=' + toColType + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy);
- $.get(link, function(data)
+ var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy + '®ionID=' + regionID+ '&orderBy=' + orderBy );
+ $.ajax(
{
- if(data)
+ method: 'post',
+ dataType: 'json',
+ url: link,
+ success: function(data)
{
- kanbanGroup = $.parseJSON(data);
- if(groupBy == 'default')
- {
- updateKanban('story', kanbanGroup.story);
- }
- else
- {
- updateKanban(browseType, kanbanGroup[groupBy]);
- }
+ updateRegion(regionID, data[regionID]);
+ },
+ error: function(xhr, status, error)
+ {
+ showErrorMessager(error || lang.timeout);
}
- })
+ });
}
}
@@ -674,313 +875,90 @@ function changeCardColType(card, fromColType, toColType, kanbanID)
}
}
-/**
- * Handle finish drop task
- * @param {Object} event Event object
- * @returns {void}
- */
-function handleFinishDrop(event)
+function processMinusBtn()
{
- var $card = $(event.element); // The drag card
- var $dragCol = $card.closest('.kanban-lane-col');
- var $dropCol = $(event.target);
-
- /* Get d-n-d(drag and drop) infos */
- var card = $card.data('item');
- var fromColType = $dragCol.data('type');
- var toColType = $dropCol.data('type');
- var kanbanID = $card.closest('.kanban').data('id');
-
- changeCardColType(card, fromColType, toColType, kanbanID);
-
- $('#kanbans').find('.can-drop-here').removeClass('can-drop-here');
-}
-
-/** Handle sort cards in column */
-function handleSortColCards()
-{
- /* TODO: handle sort cards from column contextmenu */
- return false;
-}
-
-/**
- * Create column menu
- * @returns {Object[]}
- */
-function createColumnMenu(options)
-{
- var $col = options.$trigger.closest('.kanban-col');
- var col = $col.data('col');
- var kanbanID = options.kanban;
-
- var items = [];
- if(priv.canEditName) items.push({label: executionLang.editName, url: $.createLink('kanban', 'setColumn', 'col=' + col.columnID + '&executionID=' + executionID + '&from=execution'), className: 'iframe', attrs: {'data-width': '500px'}})
- if(priv.canSetWIP) items.push({label: executionLang.setWIP, url: $.createLink('kanban', 'setWIP', 'col=' + col.columnID + '&executionID=' + executionID + '&from=execution'), className: 'iframe', attrs: {'data-width': '500px'}})
- //if(priv.canSortCards) items.push({label: executionLang.sortColumn, items: ['按ID倒序', '按ID顺序'], className: 'iframe', onClick: handleSortColCards})
- return items;
-}
-
-/**
- * Create column create button menu
- * @returns {Object[]}
- */
-function createColumnCreateMenu(options)
-{
- var $col = options.$trigger.closest('.kanban-col');
- var col = $col.data('col');
- var items = [];
-
- if(col.laneType == 'story')
+ var columnCount = $('#splitTable .child-column').size();
+ if(columnCount > 2 && columnCount < 10)
{
- if(priv.canCreateStory) items.push({label: storyLang.create, url: $.createLink('story', 'create', 'productID=' + productID, '', true), className: 'iframe'});
- if(priv.canBatchCreateStory) items.push({label: executionLang.batchCreateStroy, url: $.createLink('story', 'batchcreate', 'productID=' + productID + '&branch=0&moduleID=0&storyID=0&executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}});
- if(priv.canLinkStory) items.push({label: executionLang.linkStory, url: $.createLink('execution', 'linkStory', 'executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}});
- if(priv.canLinkStoryByPlane) items.push({label: executionLang.linkStoryByPlan, url: '#linkStoryByPlan', 'attrs' : {'data-toggle': 'modal'}});
+ $('#splitTable .btn-plus').show();
+ $('#splitTable .btn-close').show();
}
- else if(col.laneType == 'bug')
+ else if(columnCount <= 2)
{
- if(priv.canCreateBug) items.push({label: bugLang.create, url: $.createLink('bug', 'create', 'productID=0&moduleID=0&extra=executionID=' + executionID, '', true), className: 'iframe'});
- if(priv.canBatchCreateBug) items.push({label: bugLang.batchCreate, url: $.createLink('bug', 'batchcreate', 'productID=' + productID + '&moduleID=0&executionID=' + executionID, '', true), className: 'iframe'});
+ $('#splitTable .btn-close').hide();
}
- else
+ else if(columnCount >= 10)
{
- if(priv.canCreateTask) items.push({label: taskLang.create, url: $.createLink('task', 'create', 'executionID=' + executionID, '', true), className: 'iframe'});
- if(priv.canBatchCreateTask) items.push({label: taskLang.batchCreate, url: $.createLink('task', 'batchcreate', 'executionID=' + executionID, '', true), className: 'iframe'});
+ $('#splitTable .btn-plus').hide();
}
- return items;
-}
-
-/**
- * Create lane menu
- * @returns {Object[]}
- */
-function createLaneMenu(options)
-{
- var $lane = options.$trigger.closest('.kanban-lane');
- var $kanban = $lane.closest('.kanban');
- var lane = $lane.data('lane');
- var kanbanID = options.kanban;
- var upTargetKanban = $kanban.prev('.kanban').length ? $kanban.prev('.kanban').data('id') : '';
- var downTargetKanban = $kanban.next('.kanban').length ? $kanban.next('.kanban').data('id') : '';
-
- var items = [];
- if(priv.canSetLane) items.push({label: kanbanLang.setLane, icon: 'edit', url: $.createLink('kanban', 'setLane', 'lane=' + lane.laneID + '&executionID=' + executionID + '&from=execution'), className: 'iframe'});
- if(priv.canMoveLane) items.push(
- {label: kanbanLang.moveUp, icon: 'arrow-up', url: $.createLink('kanban', 'laneMove', 'executionID=' + executionID + '¤tLane=' + lane.id + '&targetLane=' + upTargetKanban), className: 'iframe', disabled: !$kanban.prev('.kanban').length},
- {label: kanbanLang.moveDown, icon: 'arrow-down', url: $.createLink('kanban', 'laneMove', 'executionID=' + executionID + '¤tLane=' + lane.id + '&targetLane=' + downTargetKanban), className: 'iframe', disabled: !$kanban.next('.kanban').length}
- );
-
- var bounds = options.$trigger[0].getBoundingClientRect();
- items.$options = {x: bounds.right, y: bounds.top};
- return items;
-}
-
-/**
- * Create story menu
- * @returns {Object[]}
- */
-function createStoryMenu(options)
-{
- var $card = options.$trigger.closest('.kanban-item');
- var story = $card.data('item');
-
- var items = [];
- $.each(story.menus, function()
- {
- var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}};
- if(this.size) item.attrs['data-width'] = this.size;
-
- if(this.icon == 'unlink') item = {label: this.label, icon: this.icon, url: this.url, attrs: {'target': 'hiddenwin'}};
- items.push(item);
- });
-
- return items;
-}
-
-/**
- * Create bug menu
- * @returns {Object[]}
- */
-function createBugMenu(options)
-{
- var $card = options.$trigger.closest('.kanban-item');
- var bug = $card.data('item');
-
- var items = [];
- $.each(bug.menus, function()
- {
- var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}};
- if(this.size) item.attrs['data-width'] = this.size;
-
- items.push(item);
- });
-
- return items;
-}
-
- /**
- * Create task menu
- * @returns {Object[]}
- */
-function createTaskMenu(options)
-{
- var $card = options.$trigger.closest('.kanban-item');
- var task = $card.data('item');
-
- var items = [];
- $.each(task.menus, function()
- {
- var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}};
- if(this.size) item.attrs['data-width'] = this.size;
-
- items.push(item);
- });
-
- return items;
-}
-
-/** Resize kanban container size */
-function resizeKanbanContainer()
-{
- var $container = $('#kanbanContainer');
- var maxHeight = window.innerHeight - 98 - 15;
- if($.cookie('isFullScreen') == 1) maxHeight = window.innerHeight - 15;
- $container.children('.panel-body').css('max-height', maxHeight);
}
/* Define menu creators */
window.menuCreators =
{
- column: createColumnMenu,
- columnCreate: createColumnCreateMenu,
lane: createLaneMenu,
- story: createStoryMenu,
- bug: createBugMenu,
- task: createTaskMenu,
+ column: createColumnMenu,
+ columnCreate: createColumnCreateMenu
};
-/* Set kanban affix container */
-window.kanbanAffixContainer = '#kanbanContainer>.panel-body';
-
-/* Overload kanban default options */
-$.extend($.fn.kanban.Constructor.DEFAULTS,
+/**
+ * init Kanban
+ */
+/**
+ * Init kanban.
+ *
+ * @param object $kanban
+ * @access public
+ * @return void
+ */
+function initKanban($kanban)
{
- onRender: function()
+ var id = $kanban.data('id');
+ var region = regions[id];
+ var displayCards = window.displayCards == 'undefined' ? 2 : window.displayCards;
+
+ $kanban.kanban(
{
- var maxWidth = 0;
- $('#kanbans .kanban-board').each(function()
- {
- maxWidth = Math.max(maxWidth, $(this).outerWidth());
- });
- $('#kanbans').css('min-width', maxWidth);
- }
-});
-
-/** Get card height */
-function getCardHeight()
-{
- return [59, 59, 62, 62, 47][window.kanbanScaleSize];
-}
-
-/** Change kanban scale size */
-function changeKanbanScaleSize(newScaleSize)
-{
- var newScaleSize = Math.max(1, Math.min(4, newScaleSize));
- if(newScaleSize === window.kanbanScaleSize) return;
-
- window.kanbanScaleSize = newScaleSize;
- $.zui.store.set('executionKanbanScaleSize', newScaleSize);
- $('#kanbanScaleSize').text(newScaleSize);
- $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', newScaleSize >= 4 ? 'disabled' : null);
- $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', newScaleSize <= 1 ? 'disabled' : null);
-
- $('#kanbans').children('.kanban').each(function()
- {
- var kanban = $(this).data('zui.kanban');
- if(!kanban) return;
- kanban.setOptions({cardsPerRow: newScaleSize, cardHeight: getCardHeight()});
- });
-
- return newScaleSize;
-}
-
-/* Example code: */
-$(function()
-{
- $.cookie('isFullScreen', 0);
-
- window.kanbanScaleSize = +$.zui.store.get('executionKanbanScaleSize', 1);
- $('#kanbanScaleSize').text(window.kanbanScaleSize);
- $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', window.kanbanScaleSize >= 4 ? 'disabled' : null);
- $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', window.kanbanScaleSize <= 1 ? 'disabled' : null);
-
- /* Common options */
- var commonOptions =
- {
- maxColHeight: 'auto',
- minColWidth: 240,
- maxColWidth: 240,
- cardHeight: getCardHeight(),
- showCount: true,
- showZeroCount: true,
- fluidBoardWidth: false,
- cardsPerRow: window.kanbanScaleSize,
- virtualize: true,
- virtualRenderOptions: {container: '#kanbanContainer>.panel-body'},
+ data: region.groups,
+ maxColHeight: 510,
+ calcColHeight: calcColHeight,
+ fluidBoardWidth: false,
+ minColWidth: 300,
+ maxColWidth: 300,
+ cardHeight: 60,
+ displayCards: displayCards,
+ createColumnText: kanbanLang.createColumn,
+ addItemText: '',
+ cardHeight: getCardHeight(),
+ cardsPerRow: window.kanbanScaleSize,
+ onAction: handleKanbanAction,
+ onRenderLaneName: renderLaneName,
+ onRenderHeaderCol: renderHeaderCol,
+ onRenderCount: renderCount,
droppable:
{
target: findDropColumns,
finish: handleFinishDrop,
mouseButton: 'left'
- },
- onRenderHeaderCol: renderHeaderCol,
- onRenderLaneName: renderLaneName,
- onRenderCount: renderColumnCount
- };
-
- /* Create kanban */
- if(groupBy == 'default')
- {
- var kanbanLane = '';
- for(var i in kanbanList)
- {
- if(kanbanList[i] == 'story') kanbanLane = kanbanGroup.story;
- if(kanbanList[i] == 'bug') kanbanLane = kanbanGroup.bug;
- if(kanbanList[i] == 'task') kanbanLane = kanbanGroup.task;
-
- if(browseType == kanbanList[i] || browseType == 'all') createKanban(kanbanList[i], kanbanLane, commonOptions);
}
- }
- else
- {
- /* Create kanban by group. */
- createKanban(browseType, kanbanGroup[groupBy], commonOptions);
- }
-
- /* Init iframe modals */
- $(document).on('click', '#kanbans .iframe,.contextmenu-menu .iframe', function(event)
- {
- var $link = $(this);
- if($link.data('zui.modaltrigger')) return;
- $link.modalTrigger({show: true});
- event.preventDefault();
});
- /* Init contextmenu */
- $('#kanbans').on('click', '[data-contextmenu]', function(event)
+ $kanban.on('click', '.action-cancel', hideKanbanAction);
+ $kanban.on('scroll', function()
{
- var $trigger = $(this);
- var menuType = $trigger.data('contextmenu');
-
- var menuCreator = window.menuCreators[menuType];
- if(!menuCreator) return;
-
- var options = $.extend({event: event, $trigger: $trigger}, $trigger.data());
- var items = menuCreator(options);
- if(!items || !items.length) return;
-
- $.zui.ContextMenu.show(items, items.$options || {event: event});
+ $.zui.ContextMenu.hide();
});
+}
+
+/**
+ * Init when page ready
+ */
+$(function()
+{
+ window.kanbanScaleSize = +$.zui.store.get('executionKanbanScaleSize', 1);
+ $('#kanbanScaleSize').text(window.kanbanScaleSize);
+ $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', window.kanbanScaleSize >= 4 ? 'disabled' : null);
+ $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', window.kanbanScaleSize <= 1 ? 'disabled' : null);
/* Make kanbanScaleControl works */
$('#kanbanScaleControl').on('click', '.btn', function()
@@ -988,9 +966,48 @@ $(function()
changeKanbanScaleSize(window.kanbanScaleSize + ($(this).data('type') === '+' ? 1 : -1));
});
- /* Resize kanban container on window resize */
- resizeKanbanContainer();
- $(window).on('resize', resizeKanbanContainer);
+ /* Init first kanban */
+ $('.kanban').each(function()
+ {
+ initKanban($(this));
+ });
+
+ $('.icon-chevron-double-up,.icon-chevron-double-down').on('click', function()
+ {
+ $(this).toggleClass('icon-chevron-double-up icon-chevron-double-down');
+ $(this).parents('.region').find('.kanban').toggle();
+ hideKanbanAction();
+ });
+
+ $('.region-header').on('click', '.action', hideKanbanAction);
+ $('#TRAction').on('click', '.btn', hideKanbanAction);
+
+ /* Hide action box when user click document */
+ $(document).on('click', function(e)
+ {
+ $('.kanban').each(function()
+ {
+ var currentAction = $(this).kanban().attr('data-action-enabled');
+ var canHideAction = (currentAction === 'headerMore' || currentAction === 'editLaneName')
+ && !$(e.target).closest('.action,.action-box').length;
+ if(canHideAction) hideKanbanAction();
+ });
+ });
+
+ /* Init contextmenu */
+ $('#kanban').on('click', '[data-contextmenu]', function(event)
+ {
+ var $trigger = $(this);
+ var menuType = $trigger.data('contextmenu');
+ var menuCreator = window.menuCreators[menuType];
+ if(!menuCreator) return;
+
+ var options = $.extend({event: event, $trigger: $trigger}, $trigger.data());
+ var items = menuCreator(options);
+ if(!items || !items.length) return;
+
+ $.zui.ContextMenu.show(items, items.$options || {event: event});
+ });
/* Hide contextmenu when page scroll */
$(window).on('scroll', function()
@@ -1004,67 +1021,160 @@ $(function()
if(planID)
{
location.href = createLink('execution', 'importPlanStories', 'executionID=' + executionID + '&planID=' + planID + '&productID=0&fromMethod=kanban');
+ $.closeModal();
}
});
- $('#type_chosen .chosen-single span').prepend('
');
- $('#group_chosen .chosen-single span').prepend(kanbanLang.laneGroup + ': ');
-
- /* Ajax update kanban. */
- var lastUpdateData;
- setInterval(function()
+ $(document).on('click', '#splitTable .btn-plus', function()
{
- $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=" + entertime + "&browseType=" + browseType + "&groupBy=" + groupBy), function(data)
+ var tr = $(this).closest('tr');
+ tr.after($('#childTpl').html().replace(/key/g, key));
+ tr.next().find('input[name^=color]').colorPicker();
+ key++;
+ processMinusBtn();
+ return false;
+ });
+
+ /* Remove a trade detail item. */
+ $(document).on('click', '#splitTable .btn-close', function()
+ {
+ $(this).closest('tr').remove();
+ processMinusBtn();
+ return false;
+ });
+
+ /* Mofidy dafault color's border color. */
+ $(document).on('mouseout', '.color0', function()
+ {
+ $('.color0 .cardcolor').css('border', '1px solid #b0b0b0');
+ });
+
+ /* Mofidy dafault color's border color. */
+ $(document).on('mouseover', '.color0', function()
+ {
+ $('.color0 .cardcolor').css('border', '1px solid #fff');
+ });
+
+ /* Init sortable */
+ var sortType = '';
+ var $cards = null;
+ $('#kanban').sortable(
+ {
+ selector: '.region, .kanban-board, .kanban-lane',
+ trigger: '.region.sort > .region-header, .kanban-board.sort > .kanban-header > .kanban-group-header, .kanban-lane.sort > .kanban-lane-name',
+ container: function($ele)
{
- if(data && lastUpdateData !== data)
+ return $ele.parent();
+ },
+ targetSelector: function($ele)
+ {
+ /* Sort regions */
+ if($ele.hasClass('region'))
{
- lastUpdateData = data;
- kanbanGroup = $.parseJSON(data);
- if(groupBy == 'default')
- {
- var kanbanLane = '';
- for(var i in kanbanList)
- {
- if(kanbanList[i] == 'story') kanbanLane = kanbanGroup.story;
- if(kanbanList[i] == 'bug') kanbanLane = kanbanGroup.bug;
- if(kanbanList[i] == 'task') kanbanLane = kanbanGroup.task;
-
- if(browseType == kanbanList[i] || browseType == 'all') updateKanban(kanbanList[i], kanbanLane);
- }
- }
- else
- {
- updateKanban(browseType, kanbanGroup[groupBy]);
- }
+ sortType = 'region';
+ return $ele.parent().children('.region');
}
- });
- }, 10000);
-});
-$('#type').change(function()
-{
- var type = $('#type').val();
- if(type != 'all')
- {
- $('.c-group').show();
- $.get(createLink('execution', 'ajaxGetGroup', 'type=' + type), function(data)
+ /* Sort boards */
+ if($ele.hasClass('kanban-board'))
+ {
+ sortType = 'board';
+ return $ele.parent().children('.kanban-board');
+ }
+
+ /* Sort lanes */
+ if($ele.hasClass('kanban-lane'))
+ {
+ sortType = 'lane';
+ $cards = $ele.find('.kanban-item');
+
+ return $ele.parent().children('.kanban-lane');
+ }
+
+ /* Sort lanes */
+ if($ele.hasClass('kanban-item'))
+ {
+ sortType = 'item';
+ return $ele.parent().children('.kanban-item');
+ }
+ },
+ start: function(e)
{
- $('#group_chosen').remove();
- $('#group').replaceWith(data);
- $('#group').chosen();
- })
- }
+ if(sortType == 'region')
+ {
+ showRegionIdList = '';
+ $('.icon-chevron-double-up').each(function()
+ {
+ showRegionIdList += $(this).attr('data-id') + ',';
+ $(this).attr('class', 'icon-chevron-double-down');
+ });
- var link = createLink('execution', 'kanban', "executionID=" + executionID + '&type=' + type);
- location.href = link;
+ $('.region').find('.kanban').hide();
+ hideKanbanAction();
+ }
+ },
+ finish: function(e)
+ {
+ var url = '';
+ var orders = [];
+ e.list.each(function(index, data)
+ {
+ orders.push(data.item.data('id'));
+ });
+
+ if(sortType == 'region')
+ {
+ $('.region').each(function()
+ {
+ if(showRegionIdList.includes($(this).attr('data-id')))
+ {
+ $(this).find('.icon-chevron-double-down').attr('class', 'icon-chevron-double-up');
+ $(this).find('.kanban').show();
+ }
+ })
+
+ url = createLink('kanban', 'sortRegion', 'regions=' + orders.join(','));
+ }
+ if(sortType == 'board')
+ {
+ var region = e.element.parent().data('id');
+ url = createLink('kanban', 'sortGroup', 'region=' + region + '&groups=' + orders.join(','));
+ }
+ if(sortType == 'lane')
+ {
+ var region = e.element.parent().parent().data('id');
+ url = createLink('kanban', 'sortLane', 'region=' + region + '&lanes=' + orders.join(','));
+ }
+ if(sortType == 'item')
+ {
+ url = createLink('task', 'sort', 'kanbanID=' + kanbanID + '&tasks=' + orders.join(','));
+ }
+ if(!url) return true;
+
+ $.getJSON(url, function(response)
+ {
+ if(response.result == 'fail' && response.message.length)
+ {
+ bootbox.alert(response.message);
+ setTimeout(function(){return location.reload()}, 3000);
+ }
+ });
+ },
+ always: function(e)
+ {
+ if(sortType == 'lane') $cards.show();
+ }
+ });
});
-$('.c-group').change(function()
+/** Calculate column height */
+function calcColHeight(col, lane, colCards, colHeight, kanban)
{
- $('.c-group').show();
+ var options = kanban.options;
+ if(!options.displayCards) return 0;
- var type = $('#type').val();
- var group = $('#group').val();
- var link = createLink('execution', 'kanban', 'executionID=' + executionID + '&type=' + type + '&orderBy=order_asc' + '&groupBy=' + group);
- location.href = link;
-});
+ var displayCards = +(options.displayCards || 2);
+
+ if (typeof displayCards !== 'number' || displayCards < 2) displayCards = 2;
+ return (displayCards * (options.cardHeight + options.cardSpace) + options.cardSpace);
+}
diff --git a/module/execution/js/taskkanban.js b/module/execution/js/taskkanban.js
new file mode 100644
index 0000000000..6ae8207fd6
--- /dev/null
+++ b/module/execution/js/taskkanban.js
@@ -0,0 +1,1132 @@
+function changeView(view)
+{
+ var link = createLink('execution', 'taskKanban', "executionID=" + executionID + '&type=' + view);
+ location.href = link;
+}
+
+/**
+ * Render user avatar
+ * @param {String|{account: string, avatar: string}} user User account or user object
+ * @returns {string}
+ */
+function renderUserAvatar(user, objectType, objectID, size)
+{
+ var avatarSizeClass = 'avatar-' + (size || 'sm');
+ var $noPrivAndNoAssigned = $('
');
+ if(objectType == 'task')
+ {
+ if(!priv.canAssignTask && !user) return $noPrivAndNoAssigned;
+ var link = createLink('task', 'assignto', 'executionID=' + executionID + '&id=' + objectID, '', true);
+ }
+ if(objectType == 'story')
+ {
+ if(!priv.canAssignStory && !user) return $noPrivAndNoAssigned;
+ var link = createLink('story', 'assignto', 'id=' + objectID, '', true);
+ }
+ if(objectType == 'bug')
+ {
+ if(!priv.canAssignBug && !user) return $noPrivAndNoAssigned;
+ var link = createLink('bug', 'assignto', 'id=' + objectID, '', true);
+ }
+
+ if(!user) return $('
');
+
+ if(typeof user === 'string') user = {account: user};
+ if(!user.avatar && window.userList && window.userList[user.account]) user = window.userList[user.account];
+
+ var $noPrivAvatar = $('
').avatar({user: user});
+ if(objectType == 'task' && !priv.canAssignTask) return $noPrivAvatar;
+ if(objectType == 'story' && !priv.canAssignStory) return $noPrivAvatar;
+ if(objectType == 'bug' && !priv.canAssignBug) return $noPrivAvatar;
+
+ return $('
').avatar({user: user});
+}
+
+/**
+ * Render deadline
+ * @param {String|Date} deadline Deadline
+ * @returns {JQuery}
+ */
+function renderDeadline(deadline)
+{
+ if(deadline == '0000-00-00') return;
+
+ var date = $.zui.createDate(deadline);
+ var now = new Date();
+ now.setHours(0);
+ now.setMinutes(0);
+ now.setSeconds(0);
+ now.setMilliseconds(0);
+ var isEarlyThanToday = date.getTime() < now.getTime();
+ var deadlineDate = $.zui.formatDate(date, 'MM-dd');
+
+ return $('
').text(deadlineLang + ' ' + deadlineDate).addClass(isEarlyThanToday ? 'text-red' : 'text-muted');
+}
+
+/**
+ * Render story item
+ * @param {Object} item Story item object
+ * @param {JQuery} $item Kanban item element
+ * @param {Object} col Column object
+ * @returns {JQuery} $item Kanban item element
+ */
+function renderStoryItem(item, $item, col)
+{
+ var scaleSize = window.kanbanScaleSize;
+ if(+$item.attr('data-scale-size') !== scaleSize) $item.empty().attr('data-scale-size', scaleSize);
+
+ if(scaleSize <= 3)
+ {
+ var $title = $item.find('.title');
+ if(!$title.length)
+ {
+ $title = $('
' + (scaleSize <= 1 ? ' ' : '') + '')
+ .attr('href', $.createLink('story', 'view', 'storyID=' + item.id, '', true));
+ $title.appendTo($item);
+ }
+ $title.attr('title', item.title).find('.text').text(item.title);
+ }
+
+ if(scaleSize <= 2)
+ {
+ var idHtml = scaleSize <= 1 ? ('
#' + item.id + '') : '';
+ var priHtml = '
' + item.pri + '';
+ var hoursHtml = (item.estimate && scaleSize <= 1) ? ('
' + item.estimate + 'h') : '';
+ var avatarHtml = renderUserAvatar(item.assignedTo, 'story', item.id);
+ var $infos = $item.find('.infos');
+ if(!$infos.length) $infos = $('
');
+ $infos.html([idHtml, priHtml, hoursHtml].join(''));
+
+ $infos[scaleSize <= 1 ? 'append' : 'prepend'](avatarHtml);
+ if(scaleSize <= 1) $infos.appendTo($item);
+ else if(scaleSize === 2) $infos.prependTo($item);
+ else $infos.prependTo($item.find('.title'));
+ }
+ else if(scaleSize === 4)
+ {
+ $item.html(renderUserAvatar(item.assignedTo, 'story', item.id, 'md'));
+ }
+
+ if(scaleSize <= 1)
+ {
+ var $actions = $item.find('.actions');
+ if(!$actions.length && item.menus && item.menus.length)
+ {
+ $actions = $([
+ '
'
+ ].join('')).appendTo($item);
+ }
+ }
+
+ return $item.attr('data-type', 'story').addClass('kanban-item-story');
+}
+
+/**
+ * Render bug item
+ * @param {Object} item Bug item object
+ * @param {JQuery} $item Kanban item element
+ * @param {Object} col Column object
+ * @returns {JQuery} $item Kanban item element
+ */
+function renderBugItem(item, $item, col)
+{
+ var scaleSize = window.kanbanScaleSize;
+ if(+$item.attr('data-scale-size') !== scaleSize) $item.empty().attr('data-scale-size', scaleSize);
+
+ if(scaleSize <= 3)
+ {
+ var $title = $item.find('.title');
+ if(!$title.length)
+ {
+ $title = $('
' + (scaleSize <= 1 ? ' ' : '') + '')
+ .attr('href', $.createLink('bug', 'view', 'bugID=' + item.id, '', true));
+ $title.appendTo($item);
+ }
+ $title.attr('title', item.title).find('.text').text(item.title);
+ }
+
+ if(scaleSize <= 2)
+ {
+ var idHtml = scaleSize <= 1 ? ('
#' + item.id + '') : '';
+ var severityHtml = scaleSize <= 1 ? ('
') : '';
+ var priHtml = '
' + item.pri + '';
+ var avatarHtml = renderUserAvatar(item.assignedTo, 'bug', item.id);
+
+ var $infos = $item.find('.infos');
+ if(!$infos.length) $infos = $('
');
+ $infos.html([idHtml, severityHtml, priHtml].join(''));
+ if(item.deadline && scaleSize <= 1) $infos.append(renderDeadline(item.deadline));
+ $infos[scaleSize <= 1 ? 'append' : 'prepend'](avatarHtml);
+
+ if(scaleSize <= 1) $infos.appendTo($item);
+ else if(scaleSize === 2) $infos.prependTo($item);
+ else $infos.prependTo($item.find('.title'));
+ }
+ else if(scaleSize === 4)
+ {
+ $item.html(renderUserAvatar(item.assignedTo, 'bug', item.id, 'md'));
+ }
+
+ if(scaleSize <= 1)
+ {
+ var $actions = $item.find('.actions');
+ if(!$actions.length && item.menus && item.menus.length)
+ {
+ $actions = $([
+ '
'
+ ].join('')).appendTo($item);
+ }
+ }
+
+ return $item.attr('data-type', 'bug').addClass('kanban-item-bug');
+}
+
+/**
+ * Render task item
+ * @param {Object} item Task item object
+ * @param {JQuery} $item Kanban item element
+ * @param {Object} col Column object
+ * @returns {JQuery} $item Kanban item element
+ */
+function renderTaskItem(item, $item, col)
+{
+ var scaleSize = window.kanbanScaleSize;
+ if(+$item.attr('data-scale-size') !== scaleSize) $item.empty().attr('data-scale-size', scaleSize);
+
+ if(scaleSize <= 3)
+ {
+ var $title = $item.find('.title');
+ if(!$title.length)
+ {
+ $title = $('
' + (scaleSize <= 1 ? ' ' : '') + '')
+ .attr('href', $.createLink('task', 'view', 'taskID=' + item.id, '', true));
+ $title.appendTo($item);
+ }
+ $title.attr('title', item.name).find('.text').text(item.name);
+ }
+
+ if(scaleSize <= 2)
+ {
+ var idHtml = scaleSize <= 1 ? ('
#' + item.id + '') : '';
+ var priHtml = '
' + item.pri + '';
+ var hoursHtml = (item.estimate && scaleSize <= 1) ? ('
' + item.estimate + 'h') : '';
+ var avatarHtml = renderUserAvatar(item.assignedTo, 'task', item.id);
+
+ var $infos = $item.find('.infos');
+ if(!$infos.length) $infos = $('
');
+ $infos.html([idHtml, priHtml, hoursHtml].join(''));
+ if(item.deadline && scaleSize <= 1) $infos.append(renderDeadline(item.deadline));
+ $infos[scaleSize <= 1 ? 'append' : 'prepend'](avatarHtml);
+
+ if(scaleSize <= 1) $infos.appendTo($item);
+ else if(scaleSize === 2) $infos.prependTo($item);
+ else $infos.prependTo($item.find('.title'));
+ }
+ else if(scaleSize === 4)
+ {
+ $item.html(renderUserAvatar(item.assignedTo, 'task', item.id, 'md'));
+ }
+
+ if(scaleSize <= 1)
+ {
+ var $actions = $item.find('.actions');
+ if(!$actions.length && item.menus && item.menus.length)
+ {
+ $actions = $([
+ '
'
+ ].join('')).appendTo($item);
+ }
+ }
+
+ $item.attr('data-type', 'task').addClass('kanban-item-task');
+
+ return $item;
+}
+
+/* Add column renderer */
+addColumnRenderer('story', renderStoryItem);
+addColumnRenderer('bug', renderBugItem);
+addColumnRenderer('task', renderTaskItem);
+
+/**
+ * Render column count
+ * @param {JQuery} $count Kanban count element
+ * @param {number} count Column cards count
+ * @param {number} col Column object
+ * @param {Object} kanban Kanban intance
+ */
+function renderColumnCount($count, count, col)
+{
+ var text = count + '/' + (col.limit < 0 ? '
' : col.limit);
+ $count.html(text + '
');
+}
+
+/**
+ * Render header column
+ * @param {JQuery} $col Header column element
+ * @param {Object} col Header column object
+ * @param {JQuery} $header Header element
+ * @param {Object} kanban Kanban object
+ */
+function renderHeaderCol($col, col, $header, kanban)
+{
+ if(col.asParent) $col = $col.children('.kanban-header-col');
+ var $actions = $('
');
+ var printStoryButton = printTaskButton = printBugButton = false;
+ if(priv.canCreateStory || priv.canBatchCreateStory || priv.canLinkStory || priv.canLinkStoryByPlane) printStoryButton = true;
+ if(priv.canCreateTask || priv.canBatchCreateTask) printTaskButton = true;
+ if(priv.canCreateBug || priv.canBatchCreateBug) printBugButton = true;
+
+ if((col.type === 'backlog' && printStoryButton) || (col.type === 'wait' && printTaskButton) || (col.type == 'unconfirmed' && printBugButton))
+ {
+ $actions.append([
+ '
',
+ '',
+ ''
+ ].join(''));
+ }
+
+ $actions.append([
+ '
',
+ '',
+ ''
+ ].join(''));
+ $actions.appendTo($col);
+}
+
+/**
+ * Render lane name
+ * @param {JQuery} $name Name element
+ * @param {Object} lane Lane object
+ * @param {JQuery} $kanban $kanban element
+ * @param {Object} columns Kanban columns
+ * @param {Object} kanban Kanban object
+ */
+function renderLaneName($name, lane, $kanban, columns, kanban)
+{
+ if(lane.id != 'story' && lane.id != 'task' && lane.id != 'bug') return false;
+ if(!$name.children('.actions').length && (priv.canSetLane || priv.canMoveLane))
+ {
+ $([
+ '
'
+ ].join('')).appendTo($name);
+ }
+}
+
+/**
+ * Updata kanban data
+ * @param {string} kanbanID Kanban id
+ * @param {Object} data Kanban data
+ */
+function updateKanban(kanbanID, data)
+{
+ var $kanban = $('#kanban-' + kanbanID);
+ if(!$kanban.length) return;
+
+ $kanban.data('zui.kanban').render(data);
+}
+
+/**
+ * Create kanban in page
+ * @param {string} kanbanID Kanban id
+ * @param {Object} data Kanban data
+ * @param {Object} options Kanban options
+ */
+function createKanban(kanbanID, data, options)
+{
+ var $kanban = $('#kanban-' + kanbanID);
+ var displayCards = window.displayCards == 'undefined' ? 2 : window.displayCards;
+ if($kanban.length) return updateKanban(kanbanID, data);
+
+ $kanban = $('
').appendTo('#kanbans');
+ $kanban.kanban($.extend({data: data, calcColHeight: calcColHeight, displayCards: displayCards}, options));
+}
+
+function fullScreen()
+{
+ var element = document.getElementById('kanbanContainer');
+ var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen;
+ if(requestMethod)
+ {
+ var afterEnterFullscreen = function()
+ {
+ $('#kanbanContainer').addClass('scrollbar-hover');
+ $('.actions').hide();
+ $('#kanbanContainer a.iframe').each(function()
+ {
+ if($(this).hasClass('iframe'))
+ {
+ var href = $(this).attr('href');
+ $(this).removeClass('iframe');
+ $(this).attr('href', 'javascript:void(0)');
+ $(this).attr('href-bak', href);
+ }
+ })
+ $.cookie('isFullScreen', 1);
+ }
+
+ var whenFailEnterFullscreen = function()
+ {
+ exitFullScreen();
+ }
+
+ try
+ {
+ var result = requestMethod.call(element);
+ if(result && (typeof result.then === 'function' || result instanceof window.Promise))
+ {
+ result.then(afterEnterFullscreen).catch(whenFailEnterFullscreen);
+ }
+ else
+ {
+ afterEnterFullscreen();
+ }
+ }
+ catch (error)
+ {
+ whenFailEnterFullscreen(error);
+ }
+ }
+}
+
+/**
+ * Exit full screen.
+ *
+ * @access public
+ * @return void
+ */
+function exitFullScreen()
+{
+ $('#kanbanContainer').removeClass('scrollbar-hover');
+ $('.actions').show();
+ $('#kanbanContainer a').each(function()
+ {
+ var hrefBak = $(this).attr('href-bak');
+ if(hrefBak)
+ {
+ $(this).addClass('iframe');
+ $(this).attr('href', hrefBak);
+ }
+ })
+ $.cookie('isFullScreen', 0);
+}
+
+document.addEventListener('fullscreenchange', function (e)
+{
+ if(!document.fullscreenElement) exitFullScreen();
+});
+
+document.addEventListener('webkitfullscreenchange', function (e)
+{
+ if(!document.webkitFullscreenElement) exitFullScreen();
+});
+
+document.addEventListener('mozfullscreenchange', function (e)
+{
+ if(!document.mozFullScreenElement) exitFullScreen();
+});
+
+document.addEventListener('msfullscreenChange', function (e)
+{
+ if(!document.msfullscreenElement) exitFullScreen();
+});
+
+/* Define drag and drop rules */
+if(!window.kanbanDropRules)
+{
+ window.kanbanDropRules =
+ {
+ story:
+ {
+ backlog: ['ready'],
+ ready: ['backlog'],
+ },
+ bug:
+ {
+ 'unconfirmed': ['confirmed', 'fixing', 'fixed'],
+ 'confirmed': ['fixing', 'fixed'],
+ 'fixing': ['fixed'],
+ 'fixed': ['testing', 'tested', 'fixing'],
+ 'testing': ['tested', 'closed', 'fixing'],
+ 'tested': ['closed', 'fixing'],
+ 'closed': ['fixing'],
+ },
+ task:
+ {
+ 'wait': ['developing', 'developed', 'canceled', 'closed'],
+ 'developing': ['developed', 'pause'],
+ 'developed': ['canceled', 'closed'],
+ 'pause': ['developing'],
+ 'canceled': ['developing'],
+ 'closed': ['developing'],
+ }
+ }
+}
+
+/*
+ * Find drop columns
+ * @param {JQuery} $element Drag element
+ * @param {JQuery} $root Dnd root element
+ */
+function findDropColumns($element, $root)
+{
+ var $col = $element.closest('.kanban-col');
+ var col = $col.data();
+ var kanbanID = $root.data('id');
+ var kanbanRules = window.kanbanDropRules ? window.kanbanDropRules[kanbanID] : null;
+
+ if(!kanbanRules) return $root.find('.kanban-lane-col:not([data-type="' + col.type + '"])');
+
+ var colRules = kanbanRules[col.type];
+ var lane = $col.closest('.kanban-lane').data('lane');
+ return $root.find('.kanban-lane-col').filter(function()
+ {
+ if(!colRules) return false;
+ if(colRules === true) return true;
+
+ var $newCol = $(this);
+ var newCol = $newCol.data();
+ if(newCol.id === col.id) return false;
+
+ var $newLane = $newCol.closest('.kanban-lane');
+ var newLane = $newLane.data('lane');
+ var canDropHere = colRules.indexOf(newCol.type) > -1 && newLane.id === lane.id;
+ if(canDropHere) $newCol.addClass('can-drop-here');
+ return canDropHere;
+ });
+}
+
+/**
+ * changeCardColType
+ *
+ * @param int $cardID
+ * @param int $fromColID
+ * @param int $toColID
+ * @param int $fromLaneID
+ * @param int $toLaneID
+ * @param string $cardType
+ * @param string $fromColType
+ * @param string $toColType
+ * @access public
+ * @return void
+ */
+function changeCardColType(cardID, fromColID, toColID, fromLaneID, toLaneID, cardType, fromColType, toColType)
+{
+ var objectID = cardID;
+ var showIframe = false;
+ var moveCard = false;
+
+ /* Task lane. */
+ if(cardType == 'task')
+ {
+ if(toColType == 'developed')
+ {
+ if((fromColType == 'developing' || fromColType == 'wait') && priv.canFinishTask)
+ {
+ var link = createLink('task', 'finish', 'taskID=' + objectID, '', true);
+ showIframe = true;
+ }
+ }
+ else if(toColType == 'pause')
+ {
+ if(fromColType == 'developing' && priv.canPauseTask)
+ {
+ var link = createLink('task', 'pause', 'taskID=' + objectID, '', true);
+ showIframe = true;
+ }
+ }
+ else if(toColType == 'developing')
+ {
+ if((fromColType == 'pause' || fromColType == 'cancel' || fromColType == 'closed' || fromColType == 'developed') && priv.canActivateTask)
+ {
+ var link = createLink('task', 'activate', 'taskID=' + objectID, '', true);
+ showIframe = true;
+ }
+ if(fromColType == 'wait' && priv.canStartTask)
+ {
+ var link = createLink('task', 'start', 'taskID=' + objectID, '', true);
+ showIframe = true;
+ }
+ }
+ else if(toColType == 'canceled')
+ {
+ if((fromColType == 'developing' || fromColType == 'wait' || fromColType == 'pause') && priv.canCancelTask)
+ {
+ var link = createLink('task', 'cancel', 'taskID=' + objectID, '', true);
+ showIframe = true;
+ }
+ }
+ else if(toColType == 'closed')
+ {
+ if((fromColType == 'developed' || fromColType == 'canceled') && priv.canCloseTask)
+ {
+ var link = createLink('task', 'close', 'taskID=' + objectID, '', true);
+ showIframe = true;
+ }
+ }
+ }
+
+ /* Bug lane. */
+ if(cardType == 'bug')
+ {
+ if(toColType == 'confirmed')
+ {
+ if(fromColType == 'unconfirmed' && priv.canConfirmBug)
+ {
+ var link = createLink('bug', 'confirmBug', 'bugID=' + objectID, '', true);
+ showIframe = true;
+ }
+ }
+ else if(toColType == 'fixing')
+ {
+ if(fromColType == 'confirmed' || fromColType == 'unconfirmed') moveCard = true;
+ if((fromColType == 'closed' || fromColType == 'fixed' || fromColType == 'testing' || fromColType == 'tested') && priv.canActivateBug)
+ {
+ var link = createLink('bug', 'activate', 'bugID=' + objectID, '', true);
+ showIframe = true;
+ }
+ }
+ else if(toColType == 'fixed')
+ {
+ if(fromColType == 'fixing' || fromColType == 'confirmed' || fromColType == 'unconfirmed')
+ {
+ var link = createLink('bug', 'resolve', 'bugID=' + objectID, '', true);
+ showIframe = true;
+ }
+ }
+ else if(toColType == 'testing')
+ {
+ if(fromColType == 'fixed') moveCard = true;
+ }
+ else if(toColType == 'tested')
+ {
+ if(fromColType == 'fixed' || fromColType == 'testing') moveCard = true;
+ }
+ else if(toColType == 'closed')
+ {
+ if(fromColType == 'testing' || fromColType == 'tested')
+ {
+ var link = createLink('bug', 'close', 'bugID=' + objectID, '', true);
+ showIframe = true;
+ }
+ }
+
+ if(moveCard)
+ {
+ var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy);
+ $.get(link, function(data)
+ {
+ if(data)
+ {
+ kanbanGroup = $.parseJSON(data);
+ if(groupBy == 'default')
+ {
+ updateKanban('bug', kanbanGroup.bug);
+ }
+ else
+ {
+ updateKanban(browseType, kanbanGroup[groupBy]);
+ }
+ }
+ })
+ }
+ }
+
+ /* Story lane. */
+ if(cardType == 'story')
+ {
+ if(toColType == 'ready' || toColType == 'backlog')
+ {
+ var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy);
+ $.get(link, function(data)
+ {
+ if(data)
+ {
+ kanbanGroup = $.parseJSON(data);
+ if(groupBy == 'default')
+ {
+ updateKanban('story', kanbanGroup.story);
+ }
+ else
+ {
+ updateKanban(browseType, kanbanGroup[groupBy]);
+ }
+ }
+ })
+ }
+ }
+
+ if(showIframe)
+ {
+ var modalTrigger = new $.zui.ModalTrigger({type: 'iframe', width: '80%', url: link});
+ modalTrigger.show();
+ }
+}
+
+/**
+ * Handle drop task.
+ *
+ * @param object $element
+ * @param object $event
+ * @param object $kanban
+ * @access public
+ * @return void
+ */
+function handleDropTask($element, event, kanban)
+{
+ if(!event.target) return;
+
+ var $card = $element;
+ var $oldCol = $card.closest('.kanban-col');
+ var $newCol = $(event.target).closest('.kanban-col');
+ var oldCol = $oldCol.data();
+ var newCol = $newCol.data();
+ var oldLane = $oldCol.closest('.kanban-lane').data('lane');
+ var newLane = $newCol.closest('.kanban-lane').data('lane');
+ var cardType = $card.find('.kanban-card').data('type');
+
+ if(oldCol.id === newCol.id && newLane.id === oldLane.id) return false;
+
+ var cardID = $card.data().id;
+ var fromColType = $oldCol.data('type');
+ var toColType = $newCol.data('type');
+
+ changeCardColType(cardID, oldCol.id, newCol.id, oldLane.id, newLane.id, cardType, fromColType, toColType);
+}
+
+var kanbanActionHandlers =
+{
+ dropItem: handleDropTask
+};
+
+/**
+ * Handle kanban action.
+ *
+ * @param string $action
+ * @param object $element
+ * @param object $event
+ * @param object $kanban
+ * @access public
+ * @return void
+ */
+function handleKanbanAction(action, $element, event, kanban)
+{
+ if(groupBy && groupBy != 'default') return false;
+ $('.kanban').attr('data-action-enabled', action);
+ var handler = kanbanActionHandlers[action];
+ if(handler) handler($element, event, kanban);
+}
+
+/**
+ * Handle finish drop task
+ * @param {Object} event Event object
+ * @returns {void}
+ */
+function handleFinishDrop(event)
+{
+ $('#kanbans').find('.can-drop-here').removeClass('can-drop-here');
+}
+
+/** Handle sort cards in column */
+function handleSortColCards()
+{
+ /* TODO: handle sort cards from column contextmenu */
+ return false;
+}
+
+/**
+ * Create column menu
+ * @returns {Object[]}
+ */
+function createColumnMenu(options)
+{
+ var $col = options.$trigger.closest('.kanban-col');
+ var col = $col.data('col');
+ var kanbanID = options.kanban;
+
+ var items = [];
+ if(priv.canEditName) items.push({label: executionLang.editName, url: $.createLink('kanban', 'setColumn', 'col=' + col.columnID + '&executionID=' + executionID + '&from=execution'), className: 'iframe', attrs: {'data-width': '500px'}})
+ if(priv.canSetWIP) items.push({label: executionLang.setWIP, url: $.createLink('kanban', 'setWIP', 'col=' + col.columnID + '&executionID=' + executionID + '&from=execution'), className: 'iframe', attrs: {'data-width': '500px'}})
+ //if(priv.canSortCards) items.push({label: executionLang.sortColumn, items: ['按ID倒序', '按ID顺序'], className: 'iframe', onClick: handleSortColCards})
+ return items;
+}
+
+/**
+ * Create column create button menu
+ * @returns {Object[]}
+ */
+function createColumnCreateMenu(options)
+{
+ var $col = options.$trigger.closest('.kanban-col');
+ var col = $col.data('col');
+ var items = [];
+
+ if(col.laneType == 'story')
+ {
+ if(priv.canCreateStory) items.push({label: storyLang.create, url: $.createLink('story', 'create', 'productID=' + productID, '', true), className: 'iframe'});
+ if(priv.canBatchCreateStory) items.push({label: executionLang.batchCreateStroy, url: $.createLink('story', 'batchcreate', 'productID=' + productID + '&branch=0&moduleID=0&storyID=0&executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}});
+ if(priv.canLinkStory) items.push({label: executionLang.linkStory, url: $.createLink('execution', 'linkStory', 'executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}});
+ if(priv.canLinkStoryByPlane) items.push({label: executionLang.linkStoryByPlan, url: '#linkStoryByPlan', 'attrs' : {'data-toggle': 'modal'}});
+ }
+ else if(col.laneType == 'bug')
+ {
+ if(priv.canCreateBug) items.push({label: bugLang.create, url: $.createLink('bug', 'create', 'productID=0&moduleID=0&extra=executionID=' + executionID, '', true), className: 'iframe'});
+ if(priv.canBatchCreateBug) items.push({label: bugLang.batchCreate, url: $.createLink('bug', 'batchcreate', 'productID=' + productID + '&moduleID=0&executionID=' + executionID, '', true), className: 'iframe'});
+ }
+ else
+ {
+ if(priv.canCreateTask) items.push({label: taskLang.create, url: $.createLink('task', 'create', 'executionID=' + executionID, '', true), className: 'iframe'});
+ if(priv.canBatchCreateTask) items.push({label: taskLang.batchCreate, url: $.createLink('task', 'batchcreate', 'executionID=' + executionID, '', true), className: 'iframe'});
+ }
+ return items;
+}
+
+/**
+ * Create lane menu
+ * @returns {Object[]}
+ */
+function createLaneMenu(options)
+{
+ var $lane = options.$trigger.closest('.kanban-lane');
+ var $kanban = $lane.closest('.kanban');
+ var lane = $lane.data('lane');
+ var kanbanID = options.kanban;
+ var upTargetKanban = $kanban.prev('.kanban').length ? $kanban.prev('.kanban').data('id') : '';
+ var downTargetKanban = $kanban.next('.kanban').length ? $kanban.next('.kanban').data('id') : '';
+
+ var items = [];
+ if(priv.canSetLane) items.push({label: kanbanLang.setLane, icon: 'edit', url: $.createLink('kanban', 'setLane', 'lane=' + lane.laneID + '&executionID=' + executionID + '&from=execution'), className: 'iframe'});
+ if(priv.canMoveLane) items.push(
+ {label: kanbanLang.moveUp, icon: 'arrow-up', url: $.createLink('kanban', 'laneMove', 'executionID=' + executionID + '¤tLane=' + lane.id + '&targetLane=' + upTargetKanban), className: 'iframe', disabled: !$kanban.prev('.kanban').length},
+ {label: kanbanLang.moveDown, icon: 'arrow-down', url: $.createLink('kanban', 'laneMove', 'executionID=' + executionID + '¤tLane=' + lane.id + '&targetLane=' + downTargetKanban), className: 'iframe', disabled: !$kanban.next('.kanban').length}
+ );
+
+ var bounds = options.$trigger[0].getBoundingClientRect();
+ items.$options = {x: bounds.right, y: bounds.top};
+ return items;
+}
+
+/**
+ * Create story menu
+ * @returns {Object[]}
+ */
+function createStoryMenu(options)
+{
+ var $card = options.$trigger.closest('.kanban-item');
+ var story = $card.data('item');
+
+ var items = [];
+ $.each(story.menus, function()
+ {
+ var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}};
+ if(this.size) item.attrs['data-width'] = this.size;
+
+ if(this.icon == 'unlink') item = {label: this.label, icon: this.icon, url: this.url, attrs: {'target': 'hiddenwin'}};
+ items.push(item);
+ });
+
+ return items;
+}
+
+/**
+ * Create bug menu
+ * @returns {Object[]}
+ */
+function createBugMenu(options)
+{
+ var $card = options.$trigger.closest('.kanban-item');
+ var bug = $card.data('item');
+
+ var items = [];
+ $.each(bug.menus, function()
+ {
+ var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}};
+ if(this.size) item.attrs['data-width'] = this.size;
+
+ items.push(item);
+ });
+
+ return items;
+}
+
+ /**
+ * Create task menu
+ * @returns {Object[]}
+ */
+function createTaskMenu(options)
+{
+ var $card = options.$trigger.closest('.kanban-item');
+ var task = $card.data('item');
+
+ var items = [];
+ $.each(task.menus, function()
+ {
+ var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}};
+ if(this.size) item.attrs['data-width'] = this.size;
+
+ items.push(item);
+ });
+
+ return items;
+}
+
+/** Resize kanban container size */
+function resizeKanbanContainer()
+{
+ var $container = $('#kanbanContainer');
+ var maxHeight = window.innerHeight - 98 - 15;
+ if($.cookie('isFullScreen') == 1) maxHeight = window.innerHeight - 15;
+ $container.children('.panel-body').css('max-height', maxHeight);
+}
+
+/* Define menu creators */
+window.menuCreators =
+{
+ column: createColumnMenu,
+ columnCreate: createColumnCreateMenu,
+ lane: createLaneMenu,
+ story: createStoryMenu,
+ bug: createBugMenu,
+ task: createTaskMenu,
+};
+
+/* Set kanban affix container */
+window.kanbanAffixContainer = '#kanbanContainer>.panel-body';
+
+/* Overload kanban default options */
+$.extend($.fn.kanban.Constructor.DEFAULTS,
+{
+ onRender: function()
+ {
+ var maxWidth = 0;
+ $('#kanbans .kanban-board').each(function()
+ {
+ maxWidth = Math.max(maxWidth, $(this).outerWidth());
+ });
+ $('#kanbans').css('min-width', maxWidth);
+ }
+});
+
+/** Get card height */
+function getCardHeight()
+{
+ return [59, 59, 62, 62, 47][window.kanbanScaleSize];
+}
+
+/** Change kanban scale size */
+function changeKanbanScaleSize(newScaleSize)
+{
+ var newScaleSize = Math.max(1, Math.min(4, newScaleSize));
+ if(newScaleSize === window.kanbanScaleSize) return;
+
+ window.kanbanScaleSize = newScaleSize;
+ $.zui.store.set('executionKanbanScaleSize', newScaleSize);
+ $('#kanbanScaleSize').text(newScaleSize);
+ $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', newScaleSize >= 4 ? 'disabled' : null);
+ $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', newScaleSize <= 1 ? 'disabled' : null);
+
+ $('#kanbans').children('.kanban').each(function()
+ {
+ var kanban = $(this).data('zui.kanban');
+ if(!kanban) return;
+ kanban.setOptions({cardsPerRow: newScaleSize, cardHeight: getCardHeight()});
+ });
+
+ return newScaleSize;
+}
+
+/* Example code: */
+$(function()
+{
+ $.cookie('isFullScreen', 0);
+
+ window.kanbanScaleSize = +$.zui.store.get('executionKanbanScaleSize', 1);
+ $('#kanbanScaleSize').text(window.kanbanScaleSize);
+ $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', window.kanbanScaleSize >= 4 ? 'disabled' : null);
+ $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', window.kanbanScaleSize <= 1 ? 'disabled' : null);
+
+ /* Common options */
+ var commonOptions =
+ {
+ maxColHeight: 'auto',
+ minColWidth: 240,
+ maxColWidth: 240,
+ cardHeight: getCardHeight(),
+ showCount: true,
+ showZeroCount: true,
+ fluidBoardWidth: false,
+ cardsPerRow: window.kanbanScaleSize,
+ virtualize: true,
+ onAction: handleKanbanAction,
+ virtualRenderOptions: {container: '#kanbanContainer>.panel-body'},
+ droppable:
+ {
+ target: findDropColumns,
+ finish: handleFinishDrop,
+ mouseButton: 'left'
+ },
+ onRenderHeaderCol: renderHeaderCol,
+ onRenderLaneName: renderLaneName,
+ onRenderCount: renderColumnCount
+ };
+
+ if(groupBy != 'default') commonOptions.droppable = false;
+
+ /* Create kanban */
+ if(groupBy == 'default')
+ {
+ var kanbanLane = '';
+ for(var i in kanbanList)
+ {
+ if(kanbanList[i] == 'story') kanbanLane = kanbanGroup.story;
+ if(kanbanList[i] == 'bug') kanbanLane = kanbanGroup.bug;
+ if(kanbanList[i] == 'task') kanbanLane = kanbanGroup.task;
+
+ if(browseType == kanbanList[i] || browseType == 'all') createKanban(kanbanList[i], kanbanLane, commonOptions);
+ }
+ }
+ else
+ {
+ /* Create kanban by group. */
+ createKanban(browseType, kanbanGroup[groupBy], commonOptions);
+ }
+
+ /* Init iframe modals */
+ $(document).on('click', '#kanbans .iframe,.contextmenu-menu .iframe', function(event)
+ {
+ var $link = $(this);
+ if($link.data('zui.modaltrigger')) return;
+ $link.modalTrigger({show: true});
+ event.preventDefault();
+ });
+
+ /* Init contextmenu */
+ $('#kanbans').on('click', '[data-contextmenu]', function(event)
+ {
+ var $trigger = $(this);
+ var menuType = $trigger.data('contextmenu');
+
+ var menuCreator = window.menuCreators[menuType];
+ if(!menuCreator) return;
+
+ var options = $.extend({event: event, $trigger: $trigger}, $trigger.data());
+ var items = menuCreator(options);
+ if(!items || !items.length) return;
+
+ $.zui.ContextMenu.show(items, items.$options || {event: event});
+ });
+
+ /* Make kanbanScaleControl works */
+ $('#kanbanScaleControl').on('click', '.btn', function()
+ {
+ changeKanbanScaleSize(window.kanbanScaleSize + ($(this).data('type') === '+' ? 1 : -1));
+ });
+
+ /* Resize kanban container on window resize */
+ resizeKanbanContainer();
+ $(window).on('resize', resizeKanbanContainer);
+
+ /* Hide contextmenu when page scroll */
+ $(window).on('scroll', function()
+ {
+ $.zui.ContextMenu.hide();
+ });
+
+ $('#toStoryButton').on('click', function()
+ {
+ var planID = $('#plan').val();
+ if(planID)
+ {
+ location.href = createLink('execution', 'importPlanStories', 'executionID=' + executionID + '&planID=' + planID + '&productID=0&fromMethod=kanban');
+ }
+ });
+
+ $('#type_chosen .chosen-single span').prepend('
');
+ $('#group_chosen .chosen-single span').prepend(kanbanLang.laneGroup + ': ');
+
+ /* Ajax update kanban. */
+ var lastUpdateData;
+ setInterval(function()
+ {
+ $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=" + entertime + "&browseType=" + browseType + "&groupBy=" + groupBy), function(data)
+ {
+ if(data && lastUpdateData !== data)
+ {
+ lastUpdateData = data;
+ kanbanGroup = $.parseJSON(data);
+ if(groupBy == 'default')
+ {
+ var kanbanLane = '';
+ for(var i in kanbanList)
+ {
+ if(kanbanList[i] == 'story') kanbanLane = kanbanGroup.story;
+ if(kanbanList[i] == 'bug') kanbanLane = kanbanGroup.bug;
+ if(kanbanList[i] == 'task') kanbanLane = kanbanGroup.task;
+
+ if(browseType == kanbanList[i] || browseType == 'all') updateKanban(kanbanList[i], kanbanLane);
+ }
+ }
+ else
+ {
+ updateKanban(browseType, kanbanGroup[groupBy]);
+ }
+ }
+ });
+ }, 10000);
+});
+
+$('#type').change(function()
+{
+ var type = $('#type').val();
+ if(type != 'all')
+ {
+ $('.c-group').show();
+ $.get(createLink('execution', 'ajaxGetGroup', 'type=' + type), function(data)
+ {
+ $('#group_chosen').remove();
+ $('#group').replaceWith(data);
+ $('#group').chosen();
+ })
+ }
+
+ var link = createLink('execution', 'taskKanban', "executionID=" + executionID + '&type=' + type);
+ location.href = link;
+});
+
+$('.c-group').change(function()
+{
+ $('.c-group').show();
+
+ var type = $('#type').val();
+ var group = $('#group').val();
+ var link = createLink('execution', 'taskKanban', 'executionID=' + executionID + '&type=' + type + '&orderBy=order_asc' + '&groupBy=' + group);
+ location.href = link;
+});
+
+/** Calculate column height */
+function calcColHeight(col, lane, colCards, colHeight, kanban)
+{
+ var options = kanban.options;
+ if(!options.displayCards) return 0;
+
+ var displayCards = +(options.displayCards || 2);
+
+ if (typeof displayCards !== 'number' || displayCards < 2) displayCards = 2;
+ return (displayCards * (options.cardHeight + options.cardSpace) + options.cardSpace);
+}
diff --git a/module/execution/lang/en.php b/module/execution/lang/en.php
index 6006065bfc..3f3905ff27 100644
--- a/module/execution/lang/en.php
+++ b/module/execution/lang/en.php
@@ -10,105 +10,106 @@
* @link http://www.zentao.net
*/
/* Fields. */
-$lang->execution->allExecutions = 'All ' . $lang->execution->common . 's';
-$lang->execution->allExecutionAB = 'Execution List';
-$lang->execution->id = $lang->executionCommon . ' ID';
-$lang->execution->type = $lang->executionCommon . 'Type';
-$lang->execution->name = $lang->executionCommon . 'Name';
-$lang->execution->code = $lang->executionCommon . 'Code';
-$lang->execution->projectName = 'Project';
-$lang->execution->execName = 'Execution Name';
-$lang->execution->execCode = 'Execution Code';
-$lang->execution->execType = 'Execution Type';
-$lang->execution->stage = 'Stage';
-$lang->execution->pri = 'Priority';
-$lang->execution->openedBy = 'OpenedBy';
-$lang->execution->openedDate = 'OpenedDate';
-$lang->execution->closedBy = 'ClosedBy';
-$lang->execution->closedDate = 'ClosedDate';
-$lang->execution->canceledBy = 'CanceledBy';
-$lang->execution->canceledDate = 'CanceledDate';
-$lang->execution->begin = 'Planned Begin';
-$lang->execution->end = 'Planned End';
-$lang->execution->dateRange = 'Duration';
-$lang->execution->realBeganAB = 'Actual Begin';
-$lang->execution->realEndAB = 'Actual End';
-$lang->execution->realBegan = 'Actual Begin';
-$lang->execution->realEnd = 'Actual End';
-$lang->execution->to = 'To';
-$lang->execution->days = ' Days';
-$lang->execution->day = ' Days';
-$lang->execution->workHour = ' Hours';
-$lang->execution->workHourUnit = 'H';
-$lang->execution->totalHours = ' Hours';
-$lang->execution->totalDays = ' Days';
-$lang->execution->status = $lang->executionCommon . 'Status';
-$lang->execution->execStatus = 'Status';
-$lang->execution->subStatus = 'Sub Status';
-$lang->execution->desc = $lang->executionCommon . 'Description';
-$lang->execution->execDesc = 'Description';
-$lang->execution->owner = 'Owner';
-$lang->execution->PO = "{$lang->executionCommon} Owner";
-$lang->execution->PM = "{$lang->executionCommon} Manager";
-$lang->execution->execPM = "Execution Manager";
-$lang->execution->QD = 'Test Manager';
-$lang->execution->RD = 'Release Manager';
-$lang->execution->release = 'Release';
-$lang->execution->acl = 'Access Control';
-$lang->execution->teamname = 'Team Name';
-$lang->execution->updateOrder = 'Rank';
-$lang->execution->order = "Rank {$lang->executionCommon}";
-$lang->execution->orderAB = "Rank";
-$lang->execution->products = "Link {$lang->productCommon}";
-$lang->execution->whitelist = 'Whitelist';
-$lang->execution->addWhitelist = 'Add Whitelist';
-$lang->execution->unbindWhitelist = 'Remove Whitelist';
-$lang->execution->totalEstimate = 'Estimates';
-$lang->execution->totalConsumed = 'Cost';
-$lang->execution->totalLeft = 'Left';
-$lang->execution->progress = ' Progress';
-$lang->execution->hours = 'Estimates: %s, Cost: %s, Left: %s.';
-$lang->execution->viewBug = 'Bugs';
-$lang->execution->noProduct = "No {$lang->productCommon} yet.";
-$lang->execution->createStory = "Create Story";
-$lang->execution->storyTitle = "Story Name";
-$lang->execution->all = "All {$lang->executionCommon}s";
-$lang->execution->undone = 'Unfinished ';
-$lang->execution->unclosed = 'Unclosed';
-$lang->execution->typeDesc = "OPS {$lang->executionCommon} has no {$lang->SRCommon}, Bug, Build, or Test features.";
-$lang->execution->mine = 'Mine: ';
-$lang->execution->involved = 'Mine';
-$lang->execution->other = 'Others';
-$lang->execution->deleted = 'Deleted';
-$lang->execution->delayed = 'Delayed';
-$lang->execution->product = $lang->execution->products;
-$lang->execution->readjustTime = "Adjust {$lang->executionCommon} Begin and End";
-$lang->execution->readjustTask = 'Adjust Task Begin and End';
-$lang->execution->effort = 'Effort';
-$lang->execution->storyEstimate = 'Story Estimate';
-$lang->execution->newEstimate = 'New Estimate';
-$lang->execution->reestimate = 'Reestimate';
-$lang->execution->selectRound = 'Select Round';
-$lang->execution->average = 'Average';
-$lang->execution->relatedMember = 'Team';
-$lang->execution->watermark = 'Exported by ZenTao';
-$lang->execution->burnXUnit = '(Date)';
-$lang->execution->burnYUnit = '(Hours)';
-$lang->execution->waitTasks = 'Waiting Tasks';
-$lang->execution->viewByUser = 'By User';
-$lang->execution->oneProduct = "Only one stage can be linked {$lang->productCommon}";
-$lang->execution->noLinkProduct = "Stage not linked {$lang->productCommon}";
-$lang->execution->recent = 'Recent visits: ';
-$lang->execution->copyNoExecution = 'There are no ' . $lang->executionCommon . 'available to copy.';
-$lang->execution->noTeam = 'No team members at the moment';
-$lang->execution->or = ' or ';
-$lang->execution->selectProject = 'Please select project';
-$lang->execution->unfoldClosed = 'Unfold Closed';
-$lang->execution->editName = 'Edit Name';
-$lang->execution->setWIP = 'WIP Settings';
-$lang->execution->sortColumn = 'Kanban Card Sorting';
-$lang->execution->batchCreateStroy = "Batch create {$lang->SRCommon}";
-$lang->execution->batchCreateTask = 'Batch create task';
+$lang->execution->allExecutions = 'All ' . $lang->execution->common . 's';
+$lang->execution->allExecutionAB = 'Execution List';
+$lang->execution->id = $lang->executionCommon . ' ID';
+$lang->execution->type = $lang->executionCommon . 'Type';
+$lang->execution->name = $lang->executionCommon . 'Name';
+$lang->execution->code = $lang->executionCommon . 'Code';
+$lang->execution->projectName = 'Project';
+$lang->execution->execName = 'Execution Name';
+$lang->execution->execCode = 'Execution Code';
+$lang->execution->execType = 'Execution Type';
+$lang->execution->stage = 'Stage';
+$lang->execution->pri = 'Priority';
+$lang->execution->openedBy = 'OpenedBy';
+$lang->execution->openedDate = 'OpenedDate';
+$lang->execution->closedBy = 'ClosedBy';
+$lang->execution->closedDate = 'ClosedDate';
+$lang->execution->canceledBy = 'CanceledBy';
+$lang->execution->canceledDate = 'CanceledDate';
+$lang->execution->begin = 'Planned Begin';
+$lang->execution->end = 'Planned End';
+$lang->execution->dateRange = 'Duration';
+$lang->execution->realBeganAB = 'Actual Begin';
+$lang->execution->realEndAB = 'Actual End';
+$lang->execution->realBegan = 'Actual Begin';
+$lang->execution->realEnd = 'Actual End';
+$lang->execution->to = 'To';
+$lang->execution->days = ' Days';
+$lang->execution->day = ' Days';
+$lang->execution->workHour = ' Hours';
+$lang->execution->workHourUnit = 'H';
+$lang->execution->totalHours = ' Hours';
+$lang->execution->totalDays = ' Days';
+$lang->execution->status = $lang->executionCommon . 'Status';
+$lang->execution->execStatus = 'Status';
+$lang->execution->subStatus = 'Sub Status';
+$lang->execution->desc = $lang->executionCommon . 'Description';
+$lang->execution->execDesc = 'Description';
+$lang->execution->owner = 'Owner';
+$lang->execution->PO = "{$lang->executionCommon} Owner";
+$lang->execution->PM = "{$lang->executionCommon} Manager";
+$lang->execution->execPM = "Execution Manager";
+$lang->execution->QD = 'Test Manager';
+$lang->execution->RD = 'Release Manager';
+$lang->execution->release = 'Release';
+$lang->execution->acl = 'Access Control';
+$lang->execution->teamname = 'Team Name';
+$lang->execution->updateOrder = 'Rank';
+$lang->execution->order = "Rank {$lang->executionCommon}";
+$lang->execution->orderAB = "Rank";
+$lang->execution->products = "Link {$lang->productCommon}";
+$lang->execution->whitelist = 'Whitelist';
+$lang->execution->addWhitelist = 'Add Whitelist';
+$lang->execution->unbindWhitelist = 'Remove Whitelist';
+$lang->execution->totalEstimate = 'Estimates';
+$lang->execution->totalConsumed = 'Cost';
+$lang->execution->totalLeft = 'Left';
+$lang->execution->progress = ' Progress';
+$lang->execution->hours = 'Estimates: %s, Cost: %s, Left: %s.';
+$lang->execution->viewBug = 'Bugs';
+$lang->execution->noProduct = "No {$lang->productCommon} yet.";
+$lang->execution->createStory = "Create Story";
+$lang->execution->storyTitle = "Story Name";
+$lang->execution->all = "All {$lang->executionCommon}s";
+$lang->execution->undone = 'Unfinished ';
+$lang->execution->unclosed = 'Unclosed';
+$lang->execution->typeDesc = "OPS {$lang->executionCommon} has no {$lang->SRCommon}, Bug, Build, or Test features.";
+$lang->execution->mine = 'Mine: ';
+$lang->execution->involved = 'Mine';
+$lang->execution->other = 'Others';
+$lang->execution->deleted = 'Deleted';
+$lang->execution->delayed = 'Delayed';
+$lang->execution->product = $lang->execution->products;
+$lang->execution->readjustTime = "Adjust {$lang->executionCommon} Begin and End";
+$lang->execution->readjustTask = 'Adjust Task Begin and End';
+$lang->execution->effort = 'Effort';
+$lang->execution->storyEstimate = 'Story Estimate';
+$lang->execution->newEstimate = 'New Estimate';
+$lang->execution->reestimate = 'Reestimate';
+$lang->execution->selectRound = 'Select Round';
+$lang->execution->average = 'Average';
+$lang->execution->relatedMember = 'Team';
+$lang->execution->watermark = 'Exported by ZenTao';
+$lang->execution->burnXUnit = '(Date)';
+$lang->execution->burnYUnit = '(Hours)';
+$lang->execution->waitTasks = 'Waiting Tasks';
+$lang->execution->viewByUser = 'By User';
+$lang->execution->oneProduct = "Only one stage can be linked {$lang->productCommon}";
+$lang->execution->noLinkProduct = "Stage not linked {$lang->productCommon}";
+$lang->execution->recent = 'Recent visits: ';
+$lang->execution->copyNoExecution = 'There are no ' . $lang->executionCommon . 'available to copy.';
+$lang->execution->noTeam = 'No team members at the moment';
+$lang->execution->or = ' or ';
+$lang->execution->selectProject = 'Please select project';
+$lang->execution->unfoldClosed = 'Unfold Closed';
+$lang->execution->editName = 'Edit Name';
+$lang->execution->setWIP = 'WIP Settings';
+$lang->execution->sortColumn = 'Kanban Card Sorting';
+$lang->execution->batchCreateStroy = "Batch create {$lang->SRCommon}";
+$lang->execution->batchCreateTask = 'Batch create task';
+$lang->execution->kanbanNoLinkProduct = "Kanban not linked {$lang->productCommon}";
/* Fields of zt_team. */
$lang->execution->root = 'Root';
@@ -163,7 +164,7 @@ global $config;
if($config->systemMode == 'new')
{
$lang->execution->aclList['private'] = 'Private (for team members and execution stakeholders)';
- $lang->execution->aclList['open'] = 'Inherited Execution ACL (for who can access the current execution)';
+ $lang->execution->aclList['open'] = 'Inherited Project ACL (for who can access the current project)';
}
else
{
@@ -171,6 +172,9 @@ else
$lang->execution->aclList['open'] = "Public (Users who can visit {$lang->executionCommon} can access it.)";
}
+$lang->execution->kanbanAclList['private'] = 'Private';
+$lang->execution->kanbanAclList['open'] = 'Inherited Project';
+
$lang->execution->storyPoint = 'Story Point';
$lang->execution->burnByList['left'] = 'View by remaining hours';
@@ -247,6 +251,8 @@ $lang->execution->iteration = 'Iterations';
$lang->execution->iterationInfo = '%s Iterations';
$lang->execution->viewAll = 'View All';
$lang->execution->testreport = 'Test Report';
+$lang->execution->taskKanban = 'Task Kanban';
+$lang->execution->RDKanban = 'Research & Development Kanban';
/* Group browsing. */
$lang->execution->allTasks = 'All';
@@ -422,6 +428,7 @@ $lang->printKanban->typeList['increment'] = 'Increment';
$lang->execution->typeList[''] = '';
$lang->execution->typeList['stage'] = 'Stage';
$lang->execution->typeList['sprint'] = $lang->executionCommon;
+$lang->execution->typeList['kanban'] = 'Kanban';
$lang->execution->featureBar['task']['all'] = $lang->execution->allTasks;
$lang->execution->featureBar['task']['unclosed'] = $lang->execution->unclosed;
diff --git a/module/execution/lang/zh-cn.php b/module/execution/lang/zh-cn.php
index eed1188d4c..148047ed53 100644
--- a/module/execution/lang/zh-cn.php
+++ b/module/execution/lang/zh-cn.php
@@ -10,105 +10,106 @@
* @link http://www.zentao.net
*/
/* 字段列表。*/
-$lang->execution->allExecutions = '所有' . $lang->execution->common;
-$lang->execution->allExecutionAB = "{$lang->execution->common}列表";
-$lang->execution->id = $lang->executionCommon . '编号';
-$lang->execution->type = $lang->executionCommon . '类型';
-$lang->execution->name = $lang->executionCommon . '名称';
-$lang->execution->code = $lang->executionCommon . '代号';
-$lang->execution->projectName = '所属项目';
-$lang->execution->execName = "{$lang->execution->common}名称";
-$lang->execution->execCode = "{$lang->execution->common}代号";
-$lang->execution->execType = "{$lang->execution->common}类型";
-$lang->execution->stage = '阶段';
-$lang->execution->pri = '优先级';
-$lang->execution->openedBy = '由谁创建';
-$lang->execution->openedDate = '创建日期';
-$lang->execution->closedBy = '由谁关闭';
-$lang->execution->closedDate = '关闭日期';
-$lang->execution->canceledBy = '由谁取消';
-$lang->execution->canceledDate = '取消日期';
-$lang->execution->begin = '计划开始';
-$lang->execution->end = '计划完成';
-$lang->execution->dateRange = '起始日期';
-$lang->execution->realBeganAB = '实际开始';
-$lang->execution->realEndAB = '实际完成';
-$lang->execution->realBegan = '实际开始日期';
-$lang->execution->realEnd = '实际完成日期';
-$lang->execution->to = '至';
-$lang->execution->days = '可用工作日';
-$lang->execution->day = '天';
-$lang->execution->workHour = '工时';
-$lang->execution->workHourUnit = 'h';
-$lang->execution->totalHours = '可用工时';
-$lang->execution->totalDays = '可用工日';
-$lang->execution->status = $lang->executionCommon . '状态';
-$lang->execution->execStatus = "{$lang->execution->common}状态";
-$lang->execution->subStatus = '子状态';
-$lang->execution->desc = $lang->executionCommon . '描述';
-$lang->execution->execDesc = "{$lang->execution->common}描述";
-$lang->execution->owner = '负责人';
-$lang->execution->PO = $lang->productCommon . '负责人';
-$lang->execution->PM = $lang->executionCommon . '负责人';
-$lang->execution->execPM = "{$lang->execution->common}负责人";
-$lang->execution->QD = '测试负责人';
-$lang->execution->RD = '发布负责人';
-$lang->execution->release = '发布';
-$lang->execution->acl = '访问控制';
-$lang->execution->teamname = '团队名称';
-$lang->execution->updateOrder = '排序';
-$lang->execution->order = $lang->executionCommon . '排序';
-$lang->execution->orderAB = '排序';
-$lang->execution->products = '相关' . $lang->productCommon;
-$lang->execution->whitelist = '白名单';
-$lang->execution->addWhitelist = '添加白名单';
-$lang->execution->unbindWhitelist = '删除白名单';
-$lang->execution->totalEstimate = '预计';
-$lang->execution->totalConsumed = '消耗';
-$lang->execution->totalLeft = '剩余';
-$lang->execution->progress = '进度';
-$lang->execution->hours = '预计 %s 消耗 %s 剩余 %s';
-$lang->execution->viewBug = '查看bug';
-$lang->execution->noProduct = "无{$lang->executionCommon}";
-$lang->execution->createStory = "提{$lang->SRCommon}";
-$lang->execution->storyTitle = "{$lang->SRCommon}名称";
-$lang->execution->all = '所有';
-$lang->execution->undone = '未完成';
-$lang->execution->unclosed = '未关闭';
-$lang->execution->typeDesc = "运维{$lang->executionCommon}没有{$lang->SRCommon}、bug、版本、测试功能。";
-$lang->execution->mine = '我负责:';
-$lang->execution->involved = '我参与';
-$lang->execution->other = '其他';
-$lang->execution->deleted = '已删除';
-$lang->execution->delayed = '已延期';
-$lang->execution->product = $lang->execution->products;
-$lang->execution->readjustTime = "调整{$lang->executionCommon}起止时间";
-$lang->execution->readjustTask = '顺延任务的起止时间';
-$lang->execution->effort = '日志';
-$lang->execution->storyEstimate = '需求估算';
-$lang->execution->newEstimate = '新一轮估算';
-$lang->execution->reestimate = '重新估算';
-$lang->execution->selectRound = '选择轮次';
-$lang->execution->average = '平均值';
-$lang->execution->relatedMember = '相关成员';
-$lang->execution->watermark = '由禅道导出';
-$lang->execution->burnXUnit = '(日期)';
-$lang->execution->burnYUnit = '(工时)';
-$lang->execution->waitTasks = '待处理';
-$lang->execution->viewByUser = '按用户查看';
-$lang->execution->oneProduct = "阶段只能关联一个{$lang->productCommon}";
-$lang->execution->noLinkProduct = "阶段没有关联{$lang->productCommon}";
-$lang->execution->recent = '近期访问:';
-$lang->execution->copyNoExecution = '没有可用的' . $lang->executionCommon . '来复制';
-$lang->execution->noTeam = '暂时没有团队成员';
-$lang->execution->or = '或';
-$lang->execution->selectProject = '请选择项目';
-$lang->execution->unfoldClosed = '展开已结束';
-$lang->execution->editName = '编辑名称';
-$lang->execution->setWIP = '在制品数量设置(WIP)';
-$lang->execution->sortColumn = '看板列卡片排序';
-$lang->execution->batchCreateStroy = "批量新建{$lang->SRCommon}";
-$lang->execution->batchCreateTask = '批量建任务';
+$lang->execution->allExecutions = '所有' . $lang->execution->common;
+$lang->execution->allExecutionAB = "{$lang->execution->common}列表";
+$lang->execution->id = $lang->executionCommon . '编号';
+$lang->execution->type = $lang->executionCommon . '类型';
+$lang->execution->name = $lang->executionCommon . '名称';
+$lang->execution->code = $lang->executionCommon . '代号';
+$lang->execution->projectName = '所属项目';
+$lang->execution->execName = "{$lang->execution->common}名称";
+$lang->execution->execCode = "{$lang->execution->common}代号";
+$lang->execution->execType = "{$lang->execution->common}类型";
+$lang->execution->stage = '阶段';
+$lang->execution->pri = '优先级';
+$lang->execution->openedBy = '由谁创建';
+$lang->execution->openedDate = '创建日期';
+$lang->execution->closedBy = '由谁关闭';
+$lang->execution->closedDate = '关闭日期';
+$lang->execution->canceledBy = '由谁取消';
+$lang->execution->canceledDate = '取消日期';
+$lang->execution->begin = '计划开始';
+$lang->execution->end = '计划完成';
+$lang->execution->dateRange = '起始日期';
+$lang->execution->realBeganAB = '实际开始';
+$lang->execution->realEndAB = '实际完成';
+$lang->execution->realBegan = '实际开始日期';
+$lang->execution->realEnd = '实际完成日期';
+$lang->execution->to = '至';
+$lang->execution->days = '可用工作日';
+$lang->execution->day = '天';
+$lang->execution->workHour = '工时';
+$lang->execution->workHourUnit = 'h';
+$lang->execution->totalHours = '可用工时';
+$lang->execution->totalDays = '可用工日';
+$lang->execution->status = $lang->executionCommon . '状态';
+$lang->execution->execStatus = "{$lang->execution->common}状态";
+$lang->execution->subStatus = '子状态';
+$lang->execution->desc = $lang->executionCommon . '描述';
+$lang->execution->execDesc = "{$lang->execution->common}描述";
+$lang->execution->owner = '负责人';
+$lang->execution->PO = $lang->productCommon . '负责人';
+$lang->execution->PM = $lang->executionCommon . '负责人';
+$lang->execution->execPM = "{$lang->execution->common}负责人";
+$lang->execution->QD = '测试负责人';
+$lang->execution->RD = '发布负责人';
+$lang->execution->release = '发布';
+$lang->execution->acl = '访问控制';
+$lang->execution->teamname = '团队名称';
+$lang->execution->updateOrder = '排序';
+$lang->execution->order = $lang->executionCommon . '排序';
+$lang->execution->orderAB = '排序';
+$lang->execution->products = '相关' . $lang->productCommon;
+$lang->execution->whitelist = '白名单';
+$lang->execution->addWhitelist = '添加白名单';
+$lang->execution->unbindWhitelist = '删除白名单';
+$lang->execution->totalEstimate = '预计';
+$lang->execution->totalConsumed = '消耗';
+$lang->execution->totalLeft = '剩余';
+$lang->execution->progress = '进度';
+$lang->execution->hours = '预计 %s 消耗 %s 剩余 %s';
+$lang->execution->viewBug = '查看bug';
+$lang->execution->noProduct = "无{$lang->executionCommon}";
+$lang->execution->createStory = "提{$lang->SRCommon}";
+$lang->execution->storyTitle = "{$lang->SRCommon}名称";
+$lang->execution->all = '所有';
+$lang->execution->undone = '未完成';
+$lang->execution->unclosed = '未关闭';
+$lang->execution->typeDesc = "运维{$lang->executionCommon}没有{$lang->SRCommon}、bug、版本、测试功能。";
+$lang->execution->mine = '我负责:';
+$lang->execution->involved = '我参与';
+$lang->execution->other = '其他';
+$lang->execution->deleted = '已删除';
+$lang->execution->delayed = '已延期';
+$lang->execution->product = $lang->execution->products;
+$lang->execution->readjustTime = "调整{$lang->executionCommon}起止时间";
+$lang->execution->readjustTask = '顺延任务的起止时间';
+$lang->execution->effort = '日志';
+$lang->execution->storyEstimate = '需求估算';
+$lang->execution->newEstimate = '新一轮估算';
+$lang->execution->reestimate = '重新估算';
+$lang->execution->selectRound = '选择轮次';
+$lang->execution->average = '平均值';
+$lang->execution->relatedMember = '相关成员';
+$lang->execution->watermark = '由禅道导出';
+$lang->execution->burnXUnit = '(日期)';
+$lang->execution->burnYUnit = '(工时)';
+$lang->execution->waitTasks = '待处理';
+$lang->execution->viewByUser = '按用户查看';
+$lang->execution->oneProduct = "阶段只能关联一个{$lang->productCommon}";
+$lang->execution->noLinkProduct = "阶段没有关联{$lang->productCommon}";
+$lang->execution->recent = '近期访问:';
+$lang->execution->copyNoExecution = '没有可用的' . $lang->executionCommon . '来复制';
+$lang->execution->noTeam = '暂时没有团队成员';
+$lang->execution->or = '或';
+$lang->execution->selectProject = '请选择项目';
+$lang->execution->unfoldClosed = '展开已结束';
+$lang->execution->editName = '编辑名称';
+$lang->execution->setWIP = '在制品数量设置(WIP)';
+$lang->execution->sortColumn = '看板列卡片排序';
+$lang->execution->batchCreateStroy = "批量新建{$lang->SRCommon}";
+$lang->execution->batchCreateTask = '批量建任务';
+$lang->execution->kanbanNoLinkProduct = "看板没有关联{$lang->productCommon}";
/* Fields of zt_team. */
$lang->execution->root = '源ID';
@@ -171,6 +172,9 @@ else
$lang->execution->aclList['open'] = "公开(有{$lang->executionCommon}视图权限即可访问)";
}
+$lang->execution->kanbanAclList['private'] = '私有';
+$lang->execution->kanbanAclList['open'] = '继承项目';
+
$lang->execution->storyPoint = '故事点';
$lang->execution->burnByList['left'] = '按剩余工时查看';
@@ -247,6 +251,8 @@ $lang->execution->iteration = '版本迭代';
$lang->execution->iterationInfo = '迭代%s次';
$lang->execution->viewAll = '查看所有';
$lang->execution->testreport = '测试报告';
+$lang->execution->taskKanban = '任务看板';
+$lang->execution->RDKanban = '研发看板';
/* 分组浏览。*/
$lang->execution->allTasks = '所有';
@@ -422,6 +428,7 @@ $lang->printKanban->typeList['increment'] = '增量';
$lang->execution->typeList[''] = '';
$lang->execution->typeList['stage'] = '阶段';
$lang->execution->typeList['sprint'] = $lang->executionCommon;
+$lang->execution->typeList['kanban'] = '看板';
$lang->execution->featureBar['task']['all'] = $lang->execution->allTasks;
$lang->execution->featureBar['task']['unclosed'] = $lang->execution->unclosed;
diff --git a/module/execution/model.php b/module/execution/model.php
index 6a7626aada..937402ee6b 100644
--- a/module/execution/model.php
+++ b/module/execution/model.php
@@ -65,13 +65,14 @@ class executionModel extends model
{
if(!$this->app->user->admin and strpos(",{$this->app->user->view->sprints},", ",$executionID,") === false and !defined('TUTORIAL') and $executionID != 0) die(js::error($this->lang->execution->accessDenied) . js::locate('back'));
- $executions = $this->loadModel('execution')->getPairs(0, 'all', 'nocode');
+ $executions = $this->getPairs(0, 'all', 'nocode');
if(!$executionID and $this->session->execution) $executionID = $this->session->execution;
if(!$executionID or !in_array($executionID, array_keys($executions))) $executionID = key($executions);
$this->session->set('execution', $executionID);
/* Unset story, bug, build and testtask if type is ops. */
$execution = $this->getByID($executionID);
+ if($execution and $execution->type == 'kanban') $this->lang->execution->menu = new stdclass();
if($execution and $execution->type == 'stage' and $this->config->systemMode == 'new')
{
@@ -88,15 +89,6 @@ class executionModel extends model
unset($this->lang->execution->menu->build);
}
- /* Hide story and qa menu when execution is story or design type. */
- /*
- if($execution and ($execution->attribute == 'story' or $execution->attribute == 'design'))
- {
- unset($this->lang->execution->menu->story);
- unset($this->lang->execution->menu->qa);
- }
- */
-
if($executions and (!isset($executions[$executionID]) or !$this->checkPriv($executionID))) $this->accessDenied();
$moduleName = $this->app->getModuleName();
@@ -312,6 +304,12 @@ class executionModel extends model
return false;
}
+ if($type == 'kanban' and empty($this->post->products[0]))
+ {
+ dao::$errors['message'][] = $this->lang->execution->kanbanNoLinkProduct;
+ return false;
+ }
+
$this->config->execution->create->requiredFields .= ',project';
}
@@ -381,7 +379,7 @@ class executionModel extends model
$creatorExists = false;
$teamMembers = array();
- $this->loadModel('kanban')->createExecutionLane($executionID);
+ if((isset($project) and $project->model != 'kanban') or empty($project)) $this->loadModel('kanban')->createExecutionLane($executionID);
/* Save order. */
$this->dao->update(TABLE_EXECUTION)->set('`order`')->eq($executionID * 5)->where('id')->eq($executionID)->exec();
@@ -1054,7 +1052,7 @@ class executionModel extends model
/* Order by status's content whether or not done */
$executions = $this->dao->select('*, IF(INSTR("done,closed", status) < 2, 0, 1) AS isDone, INSTR("doing,wait,suspended,closed", status) AS sortStatus')->from(TABLE_EXECUTION)
->where('deleted')->eq(0)
- ->beginIF($type == 'all')->andWhere('type')->in('stage,sprint')->fi()
+ ->beginIF($type == 'all')->andWhere('type')->in('stage,sprint,kanban')->fi()
->beginIF($projectID and $this->config->systemMode == 'new')->andWhere('project')->eq($projectID)->fi()
->beginIF($type != 'all' and $this->config->systemMode == 'new')->andWhere('type')->eq($type)->fi()
->beginIF(strpos($mode, 'withdelete') === false)->andWhere('deleted')->eq(0)->fi()
@@ -1202,7 +1200,7 @@ class executionModel extends model
public function getIdList($projectID, $status = 'all')
{
return $this->dao->select('id')->from(TABLE_EXECUTION)
- ->where('type')->in('sprint,stage')
+ ->where('type')->in('sprint,stage,kanban')
->andWhere('deleted')->eq('0')
->beginIF($projectID)->andWhere('project')->eq($projectID)->fi()
->beginIF($status == 'undone')->andWhere('status')->notIN('done,closed')->fi()
@@ -1228,7 +1226,7 @@ class executionModel extends model
$orderBy = (isset($project->model) and $project->model == 'waterfall') ? 'begin_asc,id_asc' : 'begin_desc,id_desc';
$executions = $this->dao->select('*')->from(TABLE_EXECUTION)
- ->where('type')->in('stage,sprint')
+ ->where('type')->in('stage,sprint,kanban')
->andWhere('deleted')->eq('0')
->beginIF($projectID)->andWhere('project')->eq((int)$projectID)->fi()
->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi()
@@ -1319,7 +1317,7 @@ class executionModel extends model
$module = 'execution';
$method = 'task';
}
- if($module == 'testcase' and ($method == 'view' || $method == 'edit' || $method == 'batchedit'))
+ if($module == 'testcase' and ($method == 'view' or $method == 'edit' or $method == 'batchedit'))
{
$module = 'execution';
$method = 'testcase';
@@ -1329,7 +1327,7 @@ class executionModel extends model
$module = 'execution';
$method = 'testtask';
}
- if($module == 'build' and ($method == 'edit' || $method= 'view'))
+ if($module == 'build' and ($method == 'edit' or $method == 'view'))
{
$module = 'execution';
$method = 'build';
@@ -2479,6 +2477,23 @@ class executionModel extends model
->fetchAll('account');
}
+ /**
+ * Get members by execution id list.
+ *
+ * @param array $executionIdList
+ * @access public
+ * @return void
+ */
+ public function getMembersByIdList($executionIdList)
+ {
+ return $this->dao->select("t1.root, t1.account, t2.realname")->from(TABLE_TEAM)->alias('t1')
+ ->leftJoin(TABLE_USER)->alias('t2')->on('t1.account = t2.account')
+ ->where('t1.root')->in($executionIdList)
+ ->andWhere('t1.type')->eq('execution')
+ ->andWhere('t2.deleted')->eq('0')
+ ->fetchGroup('root');
+ }
+
/**
* Get the skip members of the team.
*
@@ -3022,13 +3037,16 @@ class executionModel extends model
*/
public static function isClickable($execution, $action)
{
- $action = strtolower($action);
+ $action = strtolower($action);
+ $clickable = commonModel::hasPriv('execution', $action);
+ if(!$clickable) return false;
if($action == 'start') return $execution->status == 'wait';
if($action == 'close') return $execution->status != 'closed';
if($action == 'suspend') return $execution->status == 'wait' or $execution->status == 'doing';
if($action == 'putoff') return $execution->status == 'wait' or $execution->status == 'doing';
if($action == 'activate') return $execution->status == 'suspended' or $execution->status == 'closed';
+ if($action == 'delete') return $execution->status == 'wait' or $execution->status == 'doing';
return true;
}
diff --git a/module/execution/view/ajaxgetdropmenu.html.php b/module/execution/view/ajaxgetdropmenu.html.php
index e5ac0900c0..529e93b029 100644
--- a/module/execution/view/ajaxgetdropmenu.html.php
+++ b/module/execution/view/ajaxgetdropmenu.html.php
@@ -72,6 +72,11 @@ foreach($executions as $projectID => $projectExecutions)
foreach($projectExecutions as $index => $execution)
{
+ $kanbanLink = $this->createLink('execution', 'kanban', "executionID=%s");
+ $taskLink = $this->createLink('execution', 'task', "executionID=%s");
+ if($execution->type != 'kanban' and $link == $kanbanLink) $link = $taskLink;
+ if($execution->type == 'kanban' and $link != $kanbanLink) $link = $kanbanLink;
+
$selected = $execution->id == $executionID ? 'selected' : '';
if($execution->status != 'done' and $execution->status != 'closed' and ($execution->PM == $this->app->user->account or isset($execution->teams[$this->app->user->account])))
{
diff --git a/module/execution/view/all.html.php b/module/execution/view/all.html.php
index 4c46d38b2f..38e8118dfb 100644
--- a/module/execution/view/all.html.php
+++ b/module/execution/view/all.html.php
@@ -118,11 +118,10 @@
id);?>
flex' title='name?>'>
- config->maxVersion)):?>
- '>execution->typeList[$execution->type]?>
-
+ '>execution->typeList[$execution->type]?>
children) ? $execution->name : html::a($this->createLink('execution', 'task', 'execution=' . $execution->id), $execution->name, '', "class='text-ellipsis'");
+ $executionLink = $execution->projectModel == 'kanban' ? html::a($this->createLink('execution', 'kanban', 'executionID=' . $execution->id), $execution->name, '', "class='text-ellipsis'") : html::a($this->createLink('execution', 'task', 'execution=' . $execution->id), $execution->name, '', "class='text-ellipsis'");
+ echo !empty($execution->children) ? $execution->name : $executionLink;
if(isset($execution->delay)) echo "{$lang->execution->delayed} ";
?>
children)):?>
diff --git a/module/execution/view/create.html.php b/module/execution/view/create.html.php
index 2fb95dbfc9..b4ea6625c5 100644
--- a/module/execution/view/create.html.php
+++ b/module/execution/view/create.html.php
@@ -42,6 +42,7 @@
project->common);?>
systemMode);?>
+
| | |
+ model != 'kanban'):?>
| systemMode == 'new')) ? $lang->execution->execType : $lang->execution->type;?> |
@@ -104,6 +106,7 @@
|
execution->typeDesc;?> |
+
| stage->percent;?> |
diff --git a/module/execution/view/edit.html.php b/module/execution/view/edit.html.php
index dfd74c8305..0316288f4f 100644
--- a/module/execution/view/edit.html.php
+++ b/module/execution/view/edit.html.php
@@ -66,6 +66,7 @@
+ type != 'kanban'):?>
| execution->type;?> |
@@ -81,6 +82,7 @@
?>
|
+
| execution->teamname;?> |
team, "class='form-control'");?> |
diff --git a/module/execution/view/kanban.html.php b/module/execution/view/kanban.html.php
index 28fb732228..41dad25b85 100644
--- a/module/execution/view/kanban.html.php
+++ b/module/execution/view/kanban.html.php
@@ -1,15 +1,90 @@
* @package execution
- * @version $Id: kanban.html.php $
+ * @version $Id: kanban.html.php 935 2022-01-11 16:49:24Z $
+ * @link https://www.zentao.net
*/
?>
+
+laneCount;
+
+js::set('regions', $regions);
+js::set('browseType', $browseType);
+js::set('orderBy', $orderBy);
+js::set('groupBy', $groupBy);
+js::set('execution', $execution);
+js::set('productID', $productID);
+js::set('kanbanLang', $lang->kanban);
+js::set('kanbanlaneLang', $lang->kanbanlane);
+js::set('storyLang', $lang->story);
+js::set('executionLang', $lang->execution);
+js::set('bugLang', $lang->bug);
+js::set('taskLang', $lang->task);
+js::set('deadlineLang', $lang->task->deadlineAB);
+js::set('kanbancolumnLang', $lang->kanbancolumn);
+js::set('kanbancardLang', $lang->kanbancard);
+js::set('executionID', $execution->id);
+js::set('laneCount', $laneCount);
+js::set('userList', $userList);
+js::set('noAssigned', $lang->kanbancard->noAssigned);
+js::set('users', $users);
+js::set('displayCards', $execution->displayCards);
+js::set('colorListLang', $lang->kanbancard->colorList);
+js::set('colorList', $this->config->kanban->cardColorList);
+
+$canSortRegion = commonModel::hasPriv('kanban', 'sortRegion') && count($regions) > 1;
+$canEditRegion = commonModel::hasPriv('kanban', 'editRegion');
+$canDeleteRegion = commonModel::hasPriv('kanban', 'deleteRegion');
+$canCreateLane = commonModel::hasPriv('kanban', 'createLane');
+$canCreateTask = common::hasPriv('task', 'create');
+$canBatchCreateTask = common::hasPriv('task', 'batchCreate');
+$canCreateBug = common::hasPriv('bug', 'create');
+$canBatchCreateBug = common::hasPriv('bug', 'batchCreate');
+$canCreateStory = ($productID and common::hasPriv('story', 'create'));
+$canBatchCreateStory = ($productID and common::hasPriv('story', 'batchCreate'));
+$canLinkStory = ($productID and common::hasPriv('execution', 'linkStory'));
+$canLinkStoryByPlane = ($productID and common::hasPriv('execution', 'importplanstories'));
+$hasStoryButton = ($canCreateStory or $canBatchCreateStory or $canLinkStory or $canLinkStoryByPlane);
+$hasTaskButton = ($canCreateTask or $canBatchCreateTask);
+$hasBugButton = ($canCreateBug or $canBatchCreateBug);
+
+js::set('priv',
+ array(
+ 'canCreateTask' => $canCreateTask,
+ 'canBatchCreateTask' => $canBatchCreateTask,
+ 'canCreateBug' => $canCreateBug,
+ 'canBatchCreateBug' => $canBatchCreateBug,
+ 'canCreateStory' => $canCreateStory,
+ 'canBatchCreateStory' => $canBatchCreateStory,
+ 'canLinkStory' => $canLinkStory,
+ 'canLinkStoryByPlane' => $canLinkStoryByPlane,
+ 'canAssignTask' => common::hasPriv('task', 'assignto'),
+ 'canAssignStory' => common::hasPriv('story', 'assignto'),
+ 'canFinishTask' => common::hasPriv('task', 'finish'),
+ 'canPauseTask' => common::hasPriv('task', 'pause'),
+ 'canCancelTask' => common::hasPriv('task', 'cancel'),
+ 'canCloseTask' => common::hasPriv('task', 'close'),
+ 'canActivateTask' => common::hasPriv('task', 'activate'),
+ 'canStartTask' => common::hasPriv('task', 'start'),
+ 'canAssignBug' => common::hasPriv('bug', 'assignto'),
+ 'canConfirmBug' => common::hasPriv('bug', 'confirmBug'),
+ 'canActivateBug' => common::hasPriv('bug', 'activate')
+ )
+);
+js::set('hasStoryButton', $hasStoryButton);
+js::set('hasBugButton', $hasBugButton);
+js::set('hasTaskButton', $hasTaskButton);
+?>
+
-
-
-
-
-
-
-
-
- common::hasPriv('kanban', 'setColumn'),
- 'canSetWIP' => common::hasPriv('kanban', 'setWIP'),
- 'canSetLane' => common::hasPriv('kanban', 'setLane'),
- 'canMoveLane' => common::hasPriv('kanban', 'laneMove'),
- 'canSortCards' => common::hasPriv('kanban', 'cardsSort'),
- 'canCreateTask' => $canCreateTask,
- 'canBatchCreateTask' => $canBatchCreateTask,
- 'canCreateBug' => $canCreateBug,
- 'canBatchCreateBug' => $canBatchCreateBug,
- 'canCreateStory' => $canCreateStory,
- 'canBatchCreateStory' => $canBatchCreateStory,
- 'canLinkStory' => $canLinkStory,
- 'canLinkStoryByPlane' => $canLinkStoryByPlane,
- 'canAssignTask' => common::hasPriv('task', 'assignto'),
- 'canAssignStory' => common::hasPriv('story', 'assignto'),
- 'canFinishTask' => common::hasPriv('task', 'finish'),
- 'canPauseTask' => common::hasPriv('task', 'pause'),
- 'canCancelTask' => common::hasPriv('task', 'cancel'),
- 'canCloseTask' => common::hasPriv('task', 'close'),
- 'canActivateTask' => common::hasPriv('task', 'activate'),
- 'canStartTask' => common::hasPriv('task', 'start'),
- 'canAssignBug' => common::hasPriv('bug', 'assignto'),
- 'canConfirmBug' => common::hasPriv('bug', 'confirmBug'),
- 'canActivateBug' => common::hasPriv('bug', 'activate')
- )
-);
-?>
-execution);?>
-story);?>
-task);?>
-bug);?>
-execution->editName);?>
-execution->setWIP);?>
-execution->sortColumn);?>
-kanban);?>
-task->deadlineAB);?>
-task->noAssigned);?>
-
-
diff --git a/module/execution/view/taskheader.html.php b/module/execution/view/taskheader.html.php
index c613a8bf5b..e7c2eea834 100644
--- a/module/execution/view/taskheader.html.php
+++ b/module/execution/view/taskheader.html.php
@@ -58,7 +58,7 @@
}
}
- echo ""; common::printLink('execution', 'kanban', "executionID=$executionID", $lang->execution->kanban); echo '';
+ echo ""; common::printLink('execution', 'taskKanban', "executionID=$executionID", $lang->execution->kanban); echo '';
if($execution->type == 'sprint' or $execution->type == 'waterfall')
{
echo "";
diff --git a/module/execution/view/taskkanban.html.php b/module/execution/view/taskkanban.html.php
new file mode 100644
index 0000000000..5205158b97
--- /dev/null
+++ b/module/execution/view/taskkanban.html.php
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+
+ 1execution->kanbanCardsUnit; ?>
+
+
+
+
+
+
';
+ ?>
+ execution = $executionID;
+ $canCreateTask = common::hasPriv('task', 'create', $checkObject);
+ $canBatchCreateTask = common::hasPriv('task', 'batchCreate', $checkObject);
+ $canCreateBug = common::hasPriv('bug', 'create');
+ $canBatchCreateBug = common::hasPriv('bug', 'batchCreate');
+ $canCreateStory = ($productID and common::hasPriv('story', 'create'));
+ $canBatchCreateStory = ($productID and common::hasPriv('story', 'batchCreate'));
+ $canLinkStory = ($productID and common::hasPriv('execution', 'linkStory'));
+ $canLinkStoryByPlane = ($productID and common::hasPriv('execution', 'importplanstories'));
+ $hasStoryButton = ($canCreateStory or $canBatchCreateStory or $canLinkStory or $canLinkStoryByPlane);
+ $hasTaskButton = ($canCreateTask or $canBatchCreateTask);
+ $hasBugButton = ($canCreateBug or $canBatchCreateBug);
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ execution->linkStory, "id='toStoryButton'", 'btn btn-primary');?>
+
+
+
+
+
+
+
+
+
+
+
+ common::hasPriv('kanban', 'setColumn'),
+ 'canSetWIP' => common::hasPriv('kanban', 'setWIP'),
+ 'canSetLane' => common::hasPriv('kanban', 'setLane'),
+ 'canMoveLane' => common::hasPriv('kanban', 'laneMove'),
+ 'canSortCards' => common::hasPriv('kanban', 'cardsSort'),
+ 'canCreateTask' => $canCreateTask,
+ 'canBatchCreateTask' => $canBatchCreateTask,
+ 'canCreateBug' => $canCreateBug,
+ 'canBatchCreateBug' => $canBatchCreateBug,
+ 'canCreateStory' => $canCreateStory,
+ 'canBatchCreateStory' => $canBatchCreateStory,
+ 'canLinkStory' => $canLinkStory,
+ 'canLinkStoryByPlane' => $canLinkStoryByPlane,
+ 'canAssignTask' => common::hasPriv('task', 'assignto'),
+ 'canAssignStory' => common::hasPriv('story', 'assignto'),
+ 'canFinishTask' => common::hasPriv('task', 'finish'),
+ 'canPauseTask' => common::hasPriv('task', 'pause'),
+ 'canCancelTask' => common::hasPriv('task', 'cancel'),
+ 'canCloseTask' => common::hasPriv('task', 'close'),
+ 'canActivateTask' => common::hasPriv('task', 'activate'),
+ 'canStartTask' => common::hasPriv('task', 'start'),
+ 'canAssignBug' => common::hasPriv('bug', 'assignto'),
+ 'canConfirmBug' => common::hasPriv('bug', 'confirmBug'),
+ 'canActivateBug' => common::hasPriv('bug', 'activate')
+ )
+);
+?>
+execution);?>
+story);?>
+task);?>
+bug);?>
+execution->editName);?>
+execution->setWIP);?>
+execution->sortColumn);?>
+kanban);?>
+task->deadlineAB);?>
+task->noAssigned);?>
+
+
+displayCards);?>
+
diff --git a/module/group/lang/resource.php b/module/group/lang/resource.php
index bd9da48739..e72e4e7126 100644
--- a/module/group/lang/resource.php
+++ b/module/group/lang/resource.php
@@ -674,6 +674,7 @@ $lang->resource->kanban->cardsSort = 'cardsSort';
$lang->resource->kanban->viewArchivedColumn = 'viewArchivedColumn';
$lang->resource->kanban->viewArchivedCard = 'viewArchivedCard';
$lang->resource->kanban->restoreCard = 'restoreCard';
+$lang->resource->kanban->setLaneHeight = 'setLaneHeight';
$lang->kanban->methodOrder[5] = 'space';
$lang->kanban->methodOrder[10] = 'createSpace';
@@ -718,6 +719,7 @@ $lang->kanban->methodOrder[195] = 'viewArchivedColumn';
$lang->kanban->methodorder[200] = 'viewArchivedCard';
$lang->kanban->methodorder[205] = 'archiveColumn';
$lang->kanban->methodorder[210] = 'restoreCard';
+$lang->kanban->methodorder[215] = 'setLaneHeight';
/* Execution. */
$lang->resource->execution = new stdclass();
@@ -759,7 +761,7 @@ $lang->resource->execution->linkStory = 'linkStory';
$lang->resource->execution->unlinkStory = 'unlinkStory';
$lang->resource->execution->batchUnlinkStory = 'batchUnlinkStory';
$lang->resource->execution->updateOrder = 'updateOrder';
-$lang->resource->execution->kanban = 'kanban';
+$lang->resource->execution->taskKanban = 'taskKanban';
$lang->resource->execution->printKanban = 'printKanbanAction';
$lang->resource->execution->tree = 'treeAction';
$lang->resource->execution->treeTask = 'treeOnlyTask';
@@ -775,6 +777,7 @@ $lang->resource->execution->addWhitelist = 'addWhitelist';
$lang->resource->execution->unbindWhitelist = 'unbindWhitelist';
$lang->resource->execution->storyEstimate = 'storyEstimate';
$lang->resource->execution->executionkanban = 'kanbanAction';
+$lang->resource->execution->kanban = 'RDKanban';
//if($config->systemMode == 'classic') $lang->resource->project->list = 'list';
//$lang->execution->methodOrder[0] = 'index';
@@ -816,7 +819,7 @@ $lang->execution->methodOrder[170] = 'linkStory';
$lang->execution->methodOrder[175] = 'unlinkStory';
$lang->execution->methodOrder[180] = 'batchUnlinkStory';
$lang->execution->methodOrder[185] = 'updateOrder';
-$lang->execution->methodOrder[190] = 'kanban';
+$lang->execution->methodOrder[190] = 'taskKanban';
$lang->execution->methodOrder[195] = 'printKanban';
$lang->execution->methodOrder[200] = 'kanbanHideCols';
$lang->execution->methodOrder[205] = 'kanbanColsColor';
@@ -832,6 +835,7 @@ $lang->execution->methodOrder[250] = 'addWhitelist';
$lang->execution->methodOrder[255] = 'unbindWhitelist';
$lang->execution->methodOrder[260] = 'storyEstimate';
$lang->execution->methodOrder[265] = 'executionkanban';
+$lang->execution->methodOrder[270] = 'kanban';
/* Task. */
$lang->resource->task = new stdclass();
diff --git a/module/kanban/control.php b/module/kanban/control.php
index a98155bc96..534586b70a 100644
--- a/module/kanban/control.php
+++ b/module/kanban/control.php
@@ -294,18 +294,19 @@ class kanban extends control
* Create a region.
*
* @param int $kanbanID
+ * @param string $from kanban|execution
* @access public
* @return void
*/
- public function createRegion($kanbanID)
+ public function createRegion($kanbanID, $from = 'kanban')
{
if(!empty($_POST))
{
- $kanban = $this->kanban->getByID($kanbanID);
+ $kanban = $from == 'execution' ? $this->loadModel('execution')->getByID($kanbanID) : $this->kanban->getByID($kanbanID);
$copyRegionID = (int)$_POST['region'];
unset($_POST['region']);
- $regionID = $this->kanban->createRegion($kanban, '', $copyRegionID);
+ $regionID = $this->kanban->createRegion($kanban, '', $copyRegionID, $from);
if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent'));
@@ -417,10 +418,11 @@ class kanban extends control
*
* @param int $kanbanID
* @param int $regionID
+ * @param string $from kanban|execution
* @access public
* @return void
*/
- public function createLane($kanbanID, $regionID)
+ public function createLane($kanbanID, $regionID, $from = 'kanban')
{
if(!empty($_POST))
{
@@ -431,8 +433,12 @@ class kanban extends control
return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent'));
}
- $this->view->lanes = $this->kanban->getLanePairsByRegion($regionID);
- $this->display();
+ $this->view->lanes = $this->kanban->getLanePairsByRegion($regionID, $from == 'kanban' ? 'all' : 'story');
+ $this->view->from = $from;
+ $this->view->regionID = $regionID;
+
+ if($from == 'kanban') $this->display();
+ if($from == 'execution') $this->display('kanban', 'createexeclane');
}
/**
@@ -458,6 +464,34 @@ class kanban extends control
return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess));
}
+ /**
+ * Set lane height.
+ *
+ * @param int $kanbanID
+ * @param string $from kanban|execution
+ * @access public
+ * @return void
+ */
+ public function setLaneHeight($kanbanID, $from = 'kanban')
+ {
+ if(!empty($_POST))
+ {
+ $this->kanban->setLaneHeight($kanbanID, $from);
+
+ if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
+
+ return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => 'parent'));
+
+ }
+
+ $kanban = $from == 'execution' ? $this->loadModel('execution')->getByID($kanbanID) : $this->kanban->getByID($kanbanID);
+
+ $this->view->heightType = $kanban->displayCards > 2 ? 'custom' : 'auto';
+ $this->view->displayCards = $kanban->displayCards ? $kanban->displayCards : '';
+
+ $this->display();
+ }
+
/**
* Delete a lane.
*
@@ -470,7 +504,10 @@ class kanban extends control
{
if($confirm == 'no')
{
- die(js::confirm($this->lang->kanbanlane->confirmDelete, $this->createLink('kanban', 'deleteLane', "laneID=$laneID&confirm=yes"), ''));
+ $laneType = $this->kanban->getLaneById($laneID)->type;
+ $confirmTip = in_array($laneType, array('story', 'task', 'bug')) ? sprintf($this->lang->kanbanlane->confirmDeleteTip, $this->lang->{$laneType}->common) : $this->lang->kanbanlane->confirmDelete;
+
+ die(js::confirm($confirmTip, $this->createLink('kanban', 'deleteLane', "laneID=$laneID&confirm=yes"), ''));
}
else
{
@@ -710,15 +747,17 @@ class kanban extends control
* Move a card.
*
* @param int $cardID
+ * @param int $fromColID
* @param int $toColID
+ * @param int $fromLaneID
* @param int $toLaneID
* @param int $kanbanID
* @access public
* @return void
*/
- public function moveCard($cardID, $toColID, $toLaneID, $kanbanID)
+ public function moveCard($cardID, $fromColID, $toColID, $fromLaneID, $toLaneID, $kanbanID)
{
- $this->kanban->moveCard($cardID, $toColID, $toLaneID);
+ $this->kanban->moveCard($cardID, $fromColID, $toColID, $fromLaneID, $toLaneID);
if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
$kanbanGroup = $this->kanban->getKanbanData($kanbanID);
die(json_encode($kanbanGroup));
@@ -992,26 +1031,48 @@ class kanban extends control
*
* @param int $cardID
* @param int $fromColID
- * @param string $toColType
+ * @param int $toColID
+ * @param int $fromLaneID
+ * @param int $toLaneID
* @param int $executionID
* @param string $browseType
- * @param string $browseType
+ * @param string $groupBy
+ * @param int $regionID
+ * @param string $orderBy
* @access public
- * @return json
+ * @return void
*/
- public function ajaxMoveCard($cardID = 0, $fromColID = 0, $toColType = 'ready', $executionID = 0, $browseType = 'all', $groupBy = '')
+ public function ajaxMoveCard($cardID = 0, $fromColID = 0, $toColID = 0, $fromLaneID = 0, $toLaneID = 0, $executionID = 0, $browseType = 'all', $groupBy = '', $regionID = 0, $orderBy = '')
{
- $fromColumn = $this->dao->select('*')->from(TABLE_KANBANCOLUMN)->where('id')->eq($fromColID)->fetch();
- $toColumn = $this->dao->select('*')->from(TABLE_KANBANCOLUMN)->where('type')->eq($toColType)->andWhere('lane')->eq($fromColumn->lane)->fetch();
+ $fromCell = $this->dao->select('id, cards')->from(TABLE_KANBANCELL)
+ ->where('kanban')->eq($executionID)
+ ->andWhere('lane')->eq($fromLaneID)
+ ->andWhere('`column`')->eq($fromColID)
+ ->fetch();
- $fromCards = str_replace(",$cardID,", ',', $fromColumn->cards);
+ $toCell = $this->dao->select('id, cards')->from(TABLE_KANBANCELL)
+ ->where('kanban')->eq($executionID)
+ ->andWhere('lane')->eq($toLaneID)
+ ->andWhere('`column`')->eq($toColID)
+ ->fetch();
+
+ $fromCards = str_replace(",$cardID,", ',', $fromCell->cards);
$fromCards = $fromCards == ',' ? '' : $fromCards;
- $toCards = ",$cardID," . ltrim($toColumn->cards, ',');
+ $toCards = ",$cardID," . ltrim($toCell->cards, ',');
- $this->dao->update(TABLE_KANBANCOLUMN)->set('cards')->eq($fromCards)->where('id')->eq($fromColumn->id)->exec();
- $this->dao->update(TABLE_KANBANCOLUMN)->set('cards')->eq($toCards)->where('id')->eq($toColumn->id)->exec();
+ $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($fromCards)
+ ->where('kanban')->eq($executionID)
+ ->andWhere('lane')->eq($fromLaneID)
+ ->andWhere('`column`')->eq($fromColID)
+ ->exec();
- $kanbanGroup = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy);
+ $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($toCards)
+ ->where('kanban')->eq($executionID)
+ ->andWhere('lane')->eq($toLaneID)
+ ->andWhere('`column`')->eq($toColID)
+ ->exec();
+
+ $kanbanGroup = $regionID == 0 ? $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy) : $this->kanban->getRDKanban($executionID, $browseType, $orderBy, $groupBy, $regionID);
die(json_encode($kanbanGroup));
}
@@ -1084,4 +1145,20 @@ class kanban extends control
$this->view->method = $methodName;
$this->display();
}
+
+ /**
+ * Ajax get lanes by region id.
+ *
+ * @param int $regionID
+ * @param string $type all|story|task|bug
+ * @access public
+ * @return string
+ */
+ public function ajaxGetLanes($regionID, $type = 'all')
+ {
+ $lanes = $this->kanban->getLanePairsByRegion($regionID, $type);
+
+ if(empty($lanes)) return;
+ return print(html::select('otherLane', $lanes, '', "class='form-control'"));
+ }
}
diff --git a/module/kanban/css/view.css b/module/kanban/css/view.css
index 3d3351000f..b2e9f50509 100644
--- a/module/kanban/css/view.css
+++ b/module/kanban/css/view.css
@@ -58,8 +58,9 @@
.region .kanban-lane-items {overflow: auto; padding-bottom: 10px;}
.region .kanban-item {position: relative}
+.region .kanban-item > .kanban-card > .title {line-height: 18px;}
.region .kanban-item > .kanban-card > .title:hover {color: #2272eb}
-.region .kanban-item > .kanban-card > .info {margin-top: 5px; position: relative;}
+.region .kanban-item > .kanban-card > .info {margin-top: 4px; position: relative;}
.region .kanban-item > .kanban-card > .info > .pri {height: 14px; border-width: 1px; font-size: 12px; line-height: 12px; min-width: 14px; padding: 0 1px;}
.region .kanban-item > .kanban-card > .info > .estimate{margin-left: 4px; color: #999; background-color: rgb(255, 255, 255); padding: 3px 0px}
.region .kanban-item > .kanban-card > .info > .time {margin-left: 4px; font-size: 12px; background: rgb(242, 242, 242)}
@@ -82,7 +83,7 @@
.region .kanban-item .has-color .info > .label-pri {border-color: #FFFFFF;}
.region .kanban-item .item-more {position: absolute; right: 6px; top: 0px;}
-.kanban-card {height: auto !important; padding: 8px 14px !important}
+.kanban-card {height: auto !important; padding: 8px 14px !important; min-height: 60px;}
.kanban-card > .title {word-break: break-all;}
.kanban-card > .info {position: relative; margin-top: 5px}
.kanban-card > .info > .info + .info {margin-left: 8px}
diff --git a/module/kanban/js/createlane.js b/module/kanban/js/createlane.js
index a2fe47e34c..c963df8dad 100644
--- a/module/kanban/js/createlane.js
+++ b/module/kanban/js/createlane.js
@@ -7,4 +7,3 @@ $(document).ready(function()
$('#otherLane').parents('tr').toggle($(this).val() == 'sameAsOther');
});
})
-
diff --git a/module/kanban/js/setlaneheight.js b/module/kanban/js/setlaneheight.js
new file mode 100644
index 0000000000..4f37eb4763
--- /dev/null
+++ b/module/kanban/js/setlaneheight.js
@@ -0,0 +1,17 @@
+/**
+ * Set card count.
+ *
+ * @param string $heightType
+ * @access public
+ * @return void
+ */
+function setCardCount(heightType)
+{
+ heightType != 'custom' ? $('#cardBox').addClass('hidden') : $('#cardBox').removeClass('hidden');
+}
+
+$(function()
+{
+ var heightType = $("[name='heightType']:checked").val();
+ setCardCount(heightType);
+})
diff --git a/module/kanban/js/view.js b/module/kanban/js/view.js
index 49ec25d957..7ec9e82ef2 100644
--- a/module/kanban/js/view.js
+++ b/module/kanban/js/view.js
@@ -374,16 +374,19 @@ function showErrorMessager(message)
* Move a card.
*
* @param int $cardID
+ * @param int $fromColID
* @param int $toColID
+ * @param int $fromLaneID
+ * @param int $toLaneID
* @param int $kanbanID
* @param int $regionID
* @access public
* @return string
*/
-function moveCard(cardID, toColID, toLaneID, kanbanID, regionID)
+function moveCard(cardID, fromColID, toColID, fromLaneID, toLaneID, kanbanID, regionID)
{
if(!cardID) return false;
- var url = createLink('kanban', 'moveCard', 'cardID=' + cardID + '&toColID=' + toColID + '&toLaneID=' + toLaneID + '&kanbanID=' + kanbanID);
+ var url = createLink('kanban', 'moveCard', 'cardID=' + cardID + '&fromColID='+ fromColID + '&toColID=' + toColID + '&fromLaneID='+ fromLaneID + '&toLaneID=' + toLaneID + '&kanbanID=' + kanbanID);
return $.ajax(
{
method: 'post',
@@ -460,6 +463,7 @@ function updateRegion(regionID, regionData = [])
if(!regionData) regionData = regions[regionID];
$region.data('zui.kanban').render(regionData.groups);
+ resetRegionHeight('open');
return true;
}
@@ -586,7 +590,7 @@ function handleDropTask($element, event, kanban)
if(oldCol.id === newCol.id && newLane.id === oldLane.id) return false;
var cardID = $card.data().id;
- moveCard(cardID, newCol.id, newLane.id, kanbanID, regionID);
+ moveCard(cardID, oldCol.id, newCol.id, oldLane.id, newLane.id, kanbanID, regionID);
}
/**
@@ -762,6 +766,20 @@ function createColumnMenu(options)
return items;
}
+/** Calculate column height */
+function calcColHeight(col, lane, colCards, colHeight, kanban)
+{
+ if(!isMultiLanes) return 0;
+
+ var options = kanban.options;
+
+ if(!options.displayCards) return 0;
+ var displayCards = +(options.displayCards || 2);
+
+ if (typeof displayCards !== 'number' || displayCards < 2) displayCards = 2;
+ return (displayCards * (options.cardHeight + options.cardSpace) + options.cardSpace);
+}
+
/* Define menu creators */
window.menuCreators =
{
@@ -775,16 +793,20 @@ window.menuCreators =
*/
function initKanban($kanban)
{
- var id = $kanban.data('id');
- var region = regions[id];
+ var id = $kanban.data('id');
+ var region = regions[id];
+ var displayCards = window.displayCards == 'undefined' ? 2 : window.displayCards;
$kanban.kanban(
{
data: region.groups,
maxColHeight: 510,
+ calcColHeight: calcColHeight,
fluidBoardWidth: false,
- minColWidth: 300,
- maxColWidth: 300,
+ minColWidth: 287,
+ maxColWidth: 287,
+ cardHeight: 60,
+ displayCards: displayCards,
createColumnText: kanbanLang.createColumn,
addItemText: '',
itemRender: renderKanbanItem,
@@ -813,7 +835,7 @@ function initKanban($kanban)
*/
$(function()
{
- if($.cookie('isFullScreen') == 1) fullScreen();
+ window.isMultiLanes = laneCount > 1;
/* Init first kanban */
$('.kanban').each(function()
@@ -826,6 +848,7 @@ $(function()
$(this).toggleClass('icon-chevron-double-up icon-chevron-double-down');
$(this).parents('.region').find('.kanban').toggle();
hideKanbanAction();
+ resetRegionHeight($(this).hasClass('icon-chevron-double-up') ? 'open' : 'close');
});
$('.region-header').on('click', '.action', hideKanbanAction);
@@ -1012,4 +1035,65 @@ $(function()
if(sortType == 'lane') $cards.show();
}
});
+
+ resetRegionHeight('open');
});
+
+/**
+ * Reset region height according to window height.
+ *
+ * @param string fold
+ * @access public
+ * @return void
+ */
+function resetRegionHeight(fold)
+{
+ var regionCount = 0;
+ if($.isEmptyObject(regions)) return false;
+ for(var i in regions)
+ {
+ regionCount += 1;
+ if(regionCount > 1) return false;
+ }
+
+ var regionID = Object.keys(regions)[0];
+ var region = regions[regionID].groups;
+ var groupCount = 0;
+
+ if($.isEmptyObject(region)) return false;
+ for(var j in region)
+ {
+ groupCount += 1;
+ if(groupCount > 1) return false;
+ }
+
+ var group = region[0];
+ var laneCount = 0;
+
+ if($.isEmptyObject(group.lanes)) return false;
+ for(var h in group.lanes)
+ {
+ laneCount += 1;
+ if(laneCount > 1) return false;
+ }
+
+ var regionHeaderHeight = $('.region-header').outerHeight();
+ if(fold == 'open')
+ {
+ var windowHeight = $(window).height();
+ var headerHeight = $('#mainHeader').outerHeight();
+ var mainPadding = $('#main').css('padding-top');
+ var panelBorder = $('.panel').css('border-top-width');
+ var bodyPadding = $('.panel-body').css('padding-top');
+ var height = windowHeight - (parseInt(mainPadding) * 2) - (parseInt(bodyPadding) * 2) - headerHeight - (parseInt(panelBorder) * 2);
+ var regionPadding = $('.kanban').css('padding-bottom');
+ var columnHeight = $('.kanban-header').outerHeight();
+
+ $('.region').css('height', height);
+ $('.kanban-lane').css('height', height - regionHeaderHeight - parseInt(regionPadding) - columnHeight);
+ }
+ else
+ {
+ $('.region').css('height', regionHeaderHeight);
+ }
+}
diff --git a/module/kanban/lang/en.php b/module/kanban/lang/en.php
index 7ba2b2d2c4..484c0196f3 100644
--- a/module/kanban/lang/en.php
+++ b/module/kanban/lang/en.php
@@ -18,6 +18,8 @@ $lang->kanban->deleteRegion = 'Delete Region';
$lang->kanban->createLane = 'Create Lane';
$lang->kanban->editLane = 'Edit Lane';
$lang->kanban->sortLane = 'Sort Lane';
+$lang->kanban->laneHeight = 'Lane Height';
+$lang->kanban->setLaneHeight = 'Set Lane Height';
$lang->kanban->deleteLane = 'Delete Lane';
$lang->kanban->createColumn = 'Create Column';
$lang->kanban->editColumn = 'Edit Column';
@@ -73,15 +75,18 @@ $lang->kanban->closedBy = 'Closed By';
$lang->kanban->closedDate = 'Closed Date';
$lang->kanban->empty = 'No Kanban';
$lang->kanban->teamSumCount = '%s people in total';
+$lang->kanban->cardCount = 'Card Count';
$lang->kanban->createColumnOnLeft = 'Create Column On Left';
$lang->kanban->createColumnOnRight = 'Create Column On Right';
$lang->kanban->accessDenied = "You have no access to the kanban.";
$lang->kanban->confirmDelete = 'Do you want to delete this?';
+$lang->kanban->cardCountTip = 'Please enter the number of cards';
$lang->kanban->aclGroup['open'] = 'Open';
$lang->kanban->aclGroup['private'] = 'Private';
+$lang->kanban->aclGroup['extend'] = 'Extend';
$lang->kanban->aclList['extend'] = 'Extend (Accessible with space view permissions)';
$lang->kanban->aclList['private'] = 'Private (For the kanban team, whitelist members and space owner only)';
@@ -244,11 +249,20 @@ $lang->kanbanlane->default = 'Default Lane';
$lang->kanbanlane->column = 'Lane Kanban Column';
$lang->kanbanlane->otherlane = 'Select Existed Lane';
$lang->kanbanlane->color = 'Lane Color';
+$lang->kanbanlane->WIPType = 'Lane WIP Type';
-$lang->kanbanlane->confirmDelete = 'Are you sure to delete this lane? After deleting the lane, all data (columns and cards) in the lane will also be deleted.';
+$lang->kanbanlane->confirmDelete = 'Are you sure to delete this lane? After deleting the lane, all data (columns and cards) in the lane will also be deleted.';
+$lang->kanbanlane->confirmDeleteTip = 'Are you sure to delete this lane? After deleting the lane, all %s in the lane will be hidden.';
+
+$lang->kanbanlane->modeList['sameAsOther'] = 'Use the same Kanban column';
+$lang->kanbanlane->modeList['independent'] = 'Independent Kanban column';
+
+$lang->kanbanlane->heightTypeList['auto'] = 'Adaptive (Adaptive according to the card height)';
+$lang->kanbanlane->heightTypeList['custom'] = 'Custom (Customize lane height based on number of cards)';
+
+$lang->kanbanlane->error = new stdclass();
+$lang->kanbanlane->error->mustBeInt = 'The number of cards must be a positive integer greater than 2.';
-$lang->kanbanlane->modeList['sameAsOther'] = 'Use the same Kanban column as other lanes';
-$lang->kanbanlane->modeList['independent'] = 'Independent Kanban column is adopted';
$lang->kanbanregion = new stdclass();
$lang->kanbanregion->name = 'Region Name';
diff --git a/module/kanban/lang/zh-cn.php b/module/kanban/lang/zh-cn.php
index 62a58dec97..c5ccff65c5 100644
--- a/module/kanban/lang/zh-cn.php
+++ b/module/kanban/lang/zh-cn.php
@@ -18,6 +18,8 @@ $lang->kanban->deleteRegion = '删除区域';
$lang->kanban->createLane = '创建泳道';
$lang->kanban->editLane = '泳道设置';
$lang->kanban->sortLane = '泳道排序';
+$lang->kanban->laneHeight = '泳道高度';
+$lang->kanban->setLaneHeight = '设置泳道高度';
$lang->kanban->deleteLane = '删除泳道';
$lang->kanban->createColumn = '创建看板列';
$lang->kanban->editColumn = '编辑看板列';
@@ -73,15 +75,18 @@ $lang->kanban->closedBy = '由谁关闭';
$lang->kanban->closedDate = '关闭日期';
$lang->kanban->empty = '暂时没有看板';
$lang->kanban->teamSumCount = '共%s人';
+$lang->kanban->cardCount = '卡片数量';
$lang->kanban->createColumnOnLeft = '在左侧添加看板列';
$lang->kanban->createColumnOnRight = '在右侧添加看板列';
$lang->kanban->accessDenied = '您无权访问该看板';
$lang->kanban->confirmDelete = '您确认删除吗?';
+$lang->kanban->cardCountTip = '请输入卡片数量';
$lang->kanban->aclGroup['open'] = '公开';
$lang->kanban->aclGroup['private'] = '私有';
+$lang->kanban->aclGroup['extend'] = '继承空间';
$lang->kanban->aclList['extend'] = '继承空间访问权限(能访问当前空间,即可访问)';
$lang->kanban->aclList['private'] = '私有(看板团队成员、白名单、空间负责人可访问)';
@@ -244,12 +249,20 @@ $lang->kanbanlane->default = '默认泳道';
$lang->kanbanlane->column = '泳道看板列';
$lang->kanbanlane->otherlane = '选择共享看板列的泳道';
$lang->kanbanlane->color = '泳道颜色';
+$lang->kanbanlane->WIPType = '泳道在制品类型';
-$lang->kanbanlane->confirmDelete = '您确认删除该泳道吗?删除泳道后,该泳道中所有数据(列、卡片)也会被删除。';
+$lang->kanbanlane->confirmDelete = '您确认删除该泳道吗?删除泳道后,该泳道中所有数据(列、卡片)也会被删除。';
+$lang->kanbanlane->confirmDeleteTip = '您确认删除该泳道吗?删除泳道后,该泳道中所有的%s将被隐藏。';
$lang->kanbanlane->modeList['sameAsOther'] = '与其他泳道使用相同看板列';
$lang->kanbanlane->modeList['independent'] = '采用独立的看板列';
+$lang->kanbanlane->heightTypeList['auto'] = '自适应(根据卡片高度自适应)';
+$lang->kanbanlane->heightTypeList['custom'] = '自定义(根据卡片数量自定义泳道高度)';
+
+$lang->kanbanlane->error = new stdclass();
+$lang->kanbanlane->error->mustBeInt = '卡片数量必须是大于2的正整数。';
+
$lang->kanbanregion = new stdclass();
$lang->kanbanregion->name = '区域名称';
$lang->kanbanregion->default = '默认区域';
diff --git a/module/kanban/model.php b/module/kanban/model.php
index afabc80307..f2cf659958 100644
--- a/module/kanban/model.php
+++ b/module/kanban/model.php
@@ -65,10 +65,11 @@ class kanbanModel extends model
* @param object $kanban
* @param object $region
* @param int $copyRegionID
+ * @param string $from kanban|execution
* @access public
* @return int
*/
- public function createRegion($kanban, $region = null, $copyRegionID = 0)
+ public function createRegion($kanban, $region = null, $copyRegionID = 0, $from = 'kanban')
{
$account = $this->app->user->account;
$order = 1;
@@ -82,11 +83,11 @@ class kanbanModel extends model
$order = $maxOrder ? $maxOrder + 1 : 1;
$region = fixer::input('post')
->add('kanban', $kanban->id)
- ->add('space', $kanban->space)
->add('createdBy', $account)
->add('createdDate', helper::now())
->trim('name')
->get();
+ if($from == 'kanban') $region->space = $kanban->space;
}
$region->order = $order;
@@ -144,14 +145,14 @@ class kanbanModel extends model
$copyColumn->parent = $parentColumns[$copyColumn->parent];
}
- $parentColumnID = $this->createColumn($regionID, $copyColumn);
+ $parentColumnID = $this->createColumn($regionID, $copyColumn, 0, 0, $from);
if($copyColumn->parent < 0) $parentColumns[$copyColumnID] = $parentColumnID;
if(dao::isError()) return false;
}
}
}
- else
+ elseif($from == 'kanban')
{
$groupID = $this->createGroup($kanban->id, $regionID);
if(dao::isError()) return false;
@@ -229,10 +230,11 @@ class kanbanModel extends model
* @param object $column
* @param int $order
* @param int $parent
+ * @param string $from kanban|execution
* @access public
* @return int
*/
- public function createColumn($regionID, $column = null, $order = 0, $parent = 0)
+ public function createColumn($regionID, $column = null, $order = 0, $parent = 0, $from = 'kanban')
{
if(empty($column))
{
@@ -311,7 +313,12 @@ class kanbanModel extends model
$columnID = $this->dao->lastInsertID();
- $this->dao->update(TABLE_KANBANCOLUMN)->set('type')->eq("column{$columnID}")->where('id')->eq($columnID)->exec();
+ if($from == 'kanban') $this->dao->update(TABLE_KANBANCOLUMN)->set('type')->eq("column{$columnID}")->where('id')->eq($columnID)->exec();
+
+ /* Add kanban cell. */
+ $lanes = $this->dao->select('id,type')->from(TABLE_KANBANLANE)->where('`group`')->eq($column->group)->fetchPairs();
+ $kanbanID = $this->dao->select('kanban')->from(TABLE_KANBANREGION)->where('id')->eq($regionID)->fetch('kanban');
+ foreach($lanes as $laneID => $laneType) $this->addKanbanCell($kanbanID, $laneID, $columnID, $laneType);
return $columnID;
}
@@ -418,8 +425,6 @@ class kanbanModel extends model
->add('kanban', $kanbanID)
->add('region', $regionID)
->add('group', $groupID)
- ->add('lane', $laneID)
- ->add('column', $columnID)
->add('createdBy', $this->app->user->account)
->add('createdDate', $now)
->add('assignedDate', $now)
@@ -444,6 +449,7 @@ class kanbanModel extends model
$cardID = $this->dao->lastInsertID();
$this->file->saveUpload('kanbancard', $cardID);
$this->file->updateObjectID($this->post->uid, $cardID, 'kanbancard');
+ $this->addKanbanCell($kanbanID, $laneID, $columnID, 'common', $cardID);
return $cardID;
}
@@ -614,6 +620,67 @@ class kanbanModel extends model
return $kanbanData;
}
+ /**
+ * Get a RD kanban data.
+ *
+ * @param int $executionID
+ * @param string $browseType all|story|task|bug
+ * @param string $orderBy
+ * @param int $regionID
+ *
+ * @access public
+ * @return array
+ */
+ public function getRDKanban($executionID, $browseType = 'all', $orderBy = 'id_desc', $regionID = 0)
+ {
+ $kanbanData = array();
+ $actions = array('sortGroup');
+ $regions = $this->getRegionPairs($executionID);
+ $regionIDList = $regionID == 0 ? array_keys($regions) : array(0 => $regionID);
+ $groupGroup = $this->getGroupGroupByRegions($regionIDList);
+ $laneGroup = $this->getLaneGroupByRegions($regionIDList, $browseType);
+ $columnGroup = $this->getRDColumnGroupByRegions($regionIDList, array_keys($laneGroup));
+ $cardGroup = $this->getCardGroupByExecution($executionID, $browseType, $orderBy);
+
+ foreach($regions as $regionID => $regionName)
+ {
+ $region = new stdclass();
+ $region->id = $regionID;
+ $region->name = $regionName;
+ $region->laneCount = 0;
+
+ $groups = zget($groupGroup, $regionID, array());
+ foreach($groups as $group)
+ {
+ $lanes = zget($laneGroup, $group->id, array());
+ if(!$lanes) continue;
+
+ foreach($lanes as $lane)
+ {
+ $this->refreshCards($lane);
+ $lane->items = isset($cardGroup[$lane->id]) ? $cardGroup[$lane->id] : array();
+ $lane->defaultCardType = $lane->type;
+ }
+
+ $group->columns = zget($columnGroup, $group->id, array());
+ $group->lanes = $lanes;
+ $group->actions = array();
+
+ foreach($actions as $action)
+ {
+ if(commonModel::hasPriv('kanban', $action)) $group->actions[] = $action;
+ }
+
+ $region->groups[] = $group;
+ $region->laneCount += count($lanes);
+ }
+
+ $kanbanData[$regionID] = $region;
+ }
+
+ return $kanbanData;
+ }
+
/**
* Get region by id.
*
@@ -660,15 +727,17 @@ class kanbanModel extends model
/**
* Get lane group by regions.
*
- * @param array $regions
+ * @param array $regions
+ * @param string $browseType
* @access public
* @return array
*/
- public function getLaneGroupByRegions($regions)
+ public function getLaneGroupByRegions($regions, $browseType = 'all')
{
$laneGroup = $this->dao->select('*')->from(TABLE_KANBANLANE)
->where('deleted')->eq('0')
->andWhere('region')->in($regions)
+ ->beginIf($browseType != 'all')->andWhere('type')->eq($browseType)->fi()
->orderBy('order')
->fetchGroup('group');
@@ -758,10 +827,12 @@ class kanbanModel extends model
*/
public function getCardGroupByKanban($kanbanID)
{
- $cards = $this->dao->select('*')->from(TABLE_KANBANCARD)
+ $cards = $this->dao->select('t1.*,t2.kanban,t2.lane,t2.column')->from(TABLE_KANBANCARD)->alias('t1')
+ ->leftJoin(TABLE_KANBANCELL)->alias('t2')->on('t1.kanban=t2.kanban')
->where('deleted')->eq(0)
+ ->andWhere("INSTR(t2.cards, CONCAT(',',t1.id,','))")->gt(0)
->andWhere('archived')->eq(0)
- ->andWhere('kanban')->eq($kanbanID)
+ ->andWhere('t2.kanban')->eq($kanbanID)
//->orderBy('`order` asc')
->fetchAll('id');
@@ -781,6 +852,136 @@ class kanbanModel extends model
return $cardGroup;
}
+ /**
+ * Get RD column group by regions.
+ *
+ * @param array $regions
+ * @param array $groupIDList
+ * @access public
+ * @return array
+ */
+ public function getRDColumnGroupByRegions($regions, $groupIDList = array())
+ {
+ $columnGroup = $this->dao->select("*")->from(TABLE_KANBANCOLUMN)
+ ->where('deleted')->eq('0')
+ ->andWhere('region')->in($regions)
+ ->beginIF(!empty($groupIDList))->andWhere('`group`')->in($groupIDList)->fi()
+ ->orderBy('id_asc')
+ ->fetchGroup('group');
+
+ $actions = array('setColumn', 'setWIP', 'deleteColumn');
+
+ /* Group by parent. */
+ $parentColumnGroup = array();
+ foreach($columnGroup as $group => $columns)
+ {
+ foreach($columns as $column)
+ {
+ $column->actions = array();
+ /* Judge column action priv. */
+ foreach($actions as $action)
+ {
+ if($this->isClickable($column, $action)) $column->actions[] = $action;
+ }
+
+ if($column->parent > 0) continue;
+
+ $parentColumnGroup[$group][] = $column;
+ }
+ }
+
+ $columnData = array();
+ foreach($parentColumnGroup as $group => $parentColumns)
+ {
+ foreach($parentColumns as $parentColumn)
+ {
+ $columnData[$group][] = $parentColumn;
+ foreach($columnGroup[$group] as $column)
+ {
+ if($column->parent == $parentColumn->id)
+ {
+ $parentColumn->asParent = true;
+
+ if(strpos(',developing,developed,', $column->type) !== false) $column->parentType = 'develop';
+ if(strpos(',testing,tested,', $column->type) !== false) $column->parentType = 'test';
+ if(strpos(',fixing,fixed,', $column->type) !== false) $column->parentType = 'resolving';
+
+ $columnData[$group][] = $column;
+ }
+ }
+ }
+ }
+
+ return $columnData;
+ }
+
+ /**
+ * Get card group by execution id.
+ *
+ * @param int $kanbanID
+ * @param string $browseType all|task|bug|story
+ * @param string $orderBy
+ * @access public
+ * @return array
+ */
+ public function getCardGroupByExecution($executionID, $browseType = 'all', $orderBy = 'id_asc')
+ {
+ $cards = $this->dao->select('t1.*, t2.type as columnType')
+ ->from(TABLE_KANBANCELL)->alias('t1')
+ ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.column=t2.id')
+ ->where('t1.kanban')->eq($executionID)
+ ->beginIF($browseType != 'all')->andWhere('t1.type')->eq($browseType)->fi()
+ ->orderby($orderBy)
+ ->fetchgroup('lane', 'column');
+
+ /* Get group objects. */
+ if($browseType == 'all' or $browseType == 'story') $objectGroup['story'] = $this->loadModel('story')->getExecutionStories($executionID);
+ if($browseType == 'all' or $browseType == 'bug') $objectGroup['bug'] = $this->loadModel('bug')->getExecutionBugs($executionID);
+ if($browseType == 'all' or $browseType == 'task') $objectGroup['task'] = $this->loadModel('execution')->getKanbanTasks($executionID, "id");
+
+ $cardGroup = array();
+
+ foreach($cards as $laneID => $cells)
+ {
+ foreach($cells as $columnID => $cell)
+ {
+ $cardIdList = array_filter(explode(',', $cell->cards));
+ $cardOrder = 1;
+ foreach($cardIdList as $cardID)
+ {
+ $cardData = array();
+ $objects = zget($objectGroup, $cell->type, array());
+ $object = zget($objects, $cardID, array());
+
+ if(empty($object)) continue;
+
+ $cardData['id'] = $object->id;
+ $cardData['order'] = $cardOrder++;
+ $cardData['pri'] = $object->pri ? $object->pri : '';
+ $cardData['estimate'] = $cell->type == 'bug' ? '' : $object->estimate;
+ $cardData['assignedTo'] = $object->assignedTo;
+ $cardData['deadline'] = $cell->type == 'story' ? '' : $object->deadline;
+ $cardData['severity'] = $cell->type == 'bug' ? $object->severity : '';
+ $cardData['acl'] = 'open';
+ $cardData['lane'] = $laneID;
+ $cardData['column'] = $cell->column;
+
+ if($cell->type == 'task')
+ {
+ $cardData['name'] = $object->name;
+ }
+ else
+ {
+ $cardData['title'] = $object->title;
+ }
+ $cardGroup[$laneID][$cell->columnType][] = $cardData;
+ }
+ }
+ }
+
+ return $cardGroup;
+ }
+
/**
* Get Kanban by execution id.
*
@@ -792,34 +993,26 @@ class kanbanModel extends model
*/
public function getExecutionKanban($executionID, $browseType = 'all', $groupBy = 'default')
{
- if($browseType != 'all' and $groupBy != 'default') $this->updateGroupLanes($executionID, $browseType, $groupBy);
+ if($groupBy != 'default') return $this->getKanban4Group($executionID, $browseType, $groupBy);
$lanes = $this->dao->select('*')->from(TABLE_KANBANLANE)
->where('execution')->eq($executionID)
->andWhere('deleted')->eq(0)
->beginIF($browseType != 'all')->andWhere('type')->eq($browseType)->fi()
- ->beginIF($groupBy == 'default')->andWhere('groupby')->eq('')->fi()
- ->beginIF($groupBy != 'default')->andWhere('groupby')->eq($groupBy)->fi()
->orderBy('order_asc')
->fetchAll('id');
if(empty($lanes)) return array();
- foreach($lanes as $lane) $this->updateCards($lane);
+ foreach($lanes as $lane) $this->refreshCards($lane);
- $columns = $this->dao->select('*')->from(TABLE_KANBANCOLUMN)
- ->where('deleted')->eq(0)
- ->andWhere('lane')->in(array_keys($lanes))
+ $columns = $this->dao->select('t1.cards, t1.lane, t2.id, t2.type, t2.name, t2.color, t2.limit, t2.parent')->from(TABLE_KANBANCELL)->alias('t1')
+ ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.column = t2.id')
+ ->where('t2.deleted')->eq(0)
+ ->andWhere('t1.lane')->in(array_keys($lanes))
->orderBy('id_asc')
->fetchGroup('lane', 'id');
- /* Get parent column type pairs. */
- $parentTypes = $this->dao->select('id, type')->from(TABLE_KANBANCOLUMN)
- ->where('deleted')->eq(0)
- ->andWhere('lane')->in(array_keys($lanes))
- ->andWhere('parent')->eq(-1)
- ->fetchPairs('id', 'type');
-
/* Get group objects. */
if($browseType == 'all' or $browseType == 'story') $objectGroup['story'] = $this->loadModel('story')->getExecutionStories($executionID);
if($browseType == 'all' or $browseType == 'bug') $objectGroup['bug'] = $this->loadModel('bug')->getExecutionBugs($executionID);
@@ -836,9 +1029,8 @@ class kanbanModel extends model
{
$laneData = array();
$columnData = array();
- $laneType = $groupBy == 'default' ? $lane->type : $lane->groupby;
- $laneData['id'] = $groupBy == 'default' ? $lane->type : $lane->groupby . '-' . $lane->extra;
+ $laneData['id'] = $laneID;
$laneData['laneID'] = $laneID;
$laneData['name'] = $lane->name;
$laneData['color'] = $lane->color;
@@ -847,18 +1039,20 @@ class kanbanModel extends model
foreach($columns[$laneID] as $columnID => $column)
{
- $columnData[$column->id]['id'] = $laneType . '-' . $column->type;
- $columnData[$column->id]['columnID'] = $columnID;
+ $columnData[$column->id]['id'] = $columnID;
$columnData[$column->id]['type'] = $column->type;
$columnData[$column->id]['name'] = $column->name;
$columnData[$column->id]['color'] = $column->color;
$columnData[$column->id]['limit'] = $column->limit;
$columnData[$column->id]['laneType'] = $lane->type;
$columnData[$column->id]['asParent'] = $column->parent == -1 ? true : false;
+ $columnData[$column->id]['parent'] = $column->parent;
if($column->parent > 0)
{
- $columnData[$column->id]['parentType'] = zget($parentTypes, $column->parent, '');
+ if($column->type == 'developing' or $column->type == 'developed') $columnData[$column->id]['parentType'] = 'develop';
+ if($column->type == 'testing' or $column->type == 'tested') $columnData[$column->id]['parentType'] = 'test';
+ if($column->type == 'fixing' or $column->type == 'fixed') $columnData[$column->id]['parentType'] = 'resolving';
}
$cardOrder = 1;
@@ -898,15 +1092,201 @@ class kanbanModel extends model
if(!isset($laneData['cards'][$column->type])) $laneData['cards'][$column->type] = array();
}
- $kanbanGroup[$laneType]['id'] = $laneType;
- $kanbanGroup[$laneType]['columns'] = array_values($columnData);
- $kanbanGroup[$laneType]['lanes'][] = $laneData;
- $kanbanGroup[$laneType]['defaultCardType'] = $lane->type;
+ $kanbanGroup[$lane->type]['id'] = $laneID;
+ $kanbanGroup[$lane->type]['columns'] = array_values($columnData);
+ $kanbanGroup[$lane->type]['lanes'][] = $laneData;
+ $kanbanGroup[$lane->type]['defaultCardType'] = $lane->type;
}
return $kanbanGroup;
}
+ /**
+ * Get kanban for group view.
+ *
+ * @param int $executionID
+ * @param string $browseType
+ * @param string $groupBy
+ * @access public
+ * @return array
+ */
+ public function getKanban4Group($executionID, $browseType, $groupBy)
+ {
+ /* Get card data. */
+ if($browseType == 'story') $cardList = $this->loadModel('story')->getExecutionStories($executionID);
+ if($browseType == 'bug') $cardList = $this->loadModel('bug')->getExecutionBugs($executionID);
+ if($browseType == 'task') $cardList = $this->loadModel('execution')->getKanbanTasks($executionID, "id");
+
+ $lanes = $this->getLanes4Group($executionID, $browseType, $groupBy, $cardList);
+ if(empty($lanes)) return array();
+
+ $columns = $this->dao->select('t1.*, t2.`type` as columnType')->from(TABLE_KANBANCELL)->alias('t1')
+ ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.`column` = t2.id')
+ ->where('t1.kanban')->eq($executionID)
+ ->andWhere('t1.`type`')->eq($browseType)
+ ->fetchAll();
+
+ $cardGroup = array();
+ foreach($columns as $column)
+ {
+ if(empty($column->cards)) continue;
+ foreach($cardList as $card)
+ {
+ if(strpos($column->cards, ",$card->id,") !== false) $cardGroup[$column->columnType][$card->id] = $card;
+ }
+ }
+
+ /* Build kanban group data. */
+ $kanbanGroup = array();
+ foreach($lanes as $laneID => $lane)
+ {
+ $laneData = array();
+ $columnData = array();
+ $columnList = $this->lang->kanban->{$browseType . 'Column'};
+
+ $laneData['id'] = $groupBy . $laneID;
+ $laneData['laneID'] = $groupBy . $laneID;
+ $laneData['name'] = (($groupBy == 'pri' or $groupBy == 'severity') and $laneID) ? $this->lang->$browseType->$groupBy . ':' . $lane->name : $lane->name;
+ $laneData['color'] = $lane->color;
+ $laneData['order'] = $lane->order;
+ $laneData['defaultCardType'] = $browseType;
+
+ /* Construct kanban column data. */
+ foreach($columnList as $columnID => $columnName)
+ {
+ $parentColumn = '';
+ if(in_array($columnID, array('developing', 'developed'))) $parentColumn = 'develop';
+ if(in_array($columnID, array('testing', 'tested'))) $parentColumn = 'test';
+ if(in_array($columnID, array('fixing', 'fixed'))) $parentColumn = 'resolving';
+
+ $columnData[$columnID]['id'] = $columnID;
+ $columnData[$columnID]['type'] = $columnID;
+ $columnData[$columnID]['name'] = $columnName;
+ $columnData[$columnID]['color'] = '#333';
+ $columnData[$columnID]['limit'] = -1;
+ $columnData[$columnID]['laneType'] = $browseType;
+ $columnData[$columnID]['asParent'] = in_array($columnID, array('develop', 'test', 'resolving')) ? true : false;
+ $columnData[$columnID]['parentType'] = $parentColumn;
+
+ $cardOrder = 1;
+ $objects = zget($cardGroup, $columnID, array());
+ foreach($objects as $object)
+ {
+ if(empty($object)) continue;
+
+ $cardData = array();
+ if(in_array($groupBy, array('module', 'story', 'pri', 'severity')) and (int)$object->$groupBy !== $laneID) continue;
+ if(in_array($groupBy, array('assignedTo', 'type', 'category', 'source')) and $object->$groupBy != $laneID) continue;
+
+ $cardData['id'] = $object->id;
+ $cardData['order'] = $cardOrder;
+ $cardData['pri'] = $object->pri ? $object->pri : '';
+ $cardData['estimate'] = $browseType == 'bug' ? '' : $object->estimate;
+ $cardData['assignedTo'] = $object->assignedTo;
+ $cardData['deadline'] = $browseType == 'story' ? '' : $object->deadline;
+ $cardData['severity'] = $browseType == 'bug' ? $object->severity : '';
+
+ if($browseType == 'task')
+ {
+ $cardData['name'] = $object->name;
+ }
+ else
+ {
+ $cardData['title'] = $object->title;
+ }
+
+ $laneData['cards'][$columnID][] = $cardData;
+ $cardOrder ++;
+ }
+ if(!isset($laneData['cards'][$columnID])) $laneData['cards'][$columnID] = array();
+ }
+
+ $kanbanGroup[$groupBy]['id'] = $groupBy . $laneID;
+ $kanbanGroup[$groupBy]['columns'] = array_values($columnData);
+ $kanbanGroup[$groupBy]['lanes'][] = $laneData;
+ $kanbanGroup[$groupBy]['defaultCardType'] = $browseType;
+ }
+
+ return $kanbanGroup;
+ }
+
+ /**
+ * Build lane data for group kanban.
+ *
+ * @access public
+ * @param int $executionID
+ * @param string $browseType
+ * @param string $groupBy
+ * @param array $cardList
+ * @return array
+ */
+ public function getLanes4Group($executionID, $browseType, $groupBy, $cardList)
+ {
+ $lanes = array();
+ $groupByList = array();
+ $objectPairs = array();
+ foreach($cardList as $item)
+ {
+ if(!isset($groupByList[$item->$groupBy])) $groupByList[$item->$groupBy] = $item->$groupBy;
+ }
+
+ if(in_array($groupBy, array('module', 'story', 'pri', 'severity'))) $objectPairs[0] = $this->lang->$browseType->$groupBy . ': ' . $this->lang->kanban->noGroup;
+ if(in_array($groupBy, array('assignedTo', 'type', 'category', 'source'))) $objectPairs[''] = $this->lang->$browseType->$groupBy . ': ' . $this->lang->kanban->noGroup;
+
+ if(in_array($groupBy, array('module', 'story', 'assignedTo')))
+ {
+ if($groupBy == 'module')
+ {
+ $objectPairs += $this->dao->select('id,name')->from(TABLE_MODULE)
+ ->where('type')->in('story,task,bug')
+ ->andWhere('deleted')->eq('0')
+ ->andWhere('id')->in($groupByList)
+ ->fetchPairs();
+ }
+ elseif($groupBy == 'story')
+ {
+ $objectPairs += $this->dao->select('id,title')->from(TABLE_STORY)
+ ->where('deleted')->eq(0)
+ ->andWhere('id')->in($groupByList)
+ ->fetchPairs();
+ }
+ else
+ {
+ $objectPairs += $this->dao->select('account,realname')->from(TABLE_USER)
+ ->where('account')->in($groupByList)
+ ->fetchPairs();
+
+ if(isset($groupByList['closed'])) $objectPairs['closed'] = 'Closed';
+ }
+ }
+ else
+ {
+ $objectPairs += $this->lang->$browseType->{$groupBy . 'List'};
+ }
+
+ $laneColor = 0;
+ $order = 1;
+ foreach($objectPairs as $objectID => $objectName)
+ {
+ if(!isset($groupByList[$objectID]) and $objectID) continue;
+
+ $lane = new stdclass();
+ $lane->id = $groupBy . $objectID;
+ $lane->type = $browseType;
+ $lane->execution = $executionID;
+ $lane->name = $objectName;
+ $lane->order = $order;
+ $lane->color = $this->config->kanban->laneColorList[$laneColor];
+
+ $order += 1;
+ $laneColor += 1;
+ if($laneColor == count($this->config->kanban->laneColorList)) $laneColor = 0;
+ $lanes[$objectID] = $lane;
+ }
+
+ return $lanes;
+ }
+
/**
* Get space list.
*
@@ -1118,14 +1498,16 @@ class kanbanModel extends model
* Get lane pairs by region id.
*
* @param array $regionID
+ * @param string $type all|story|task|bug|common
* @access public
* @return array
*/
- public function getLanePairsByRegion($regionID)
+ public function getLanePairsByRegion($regionID, $type = 'all')
{
return $this->dao->select('id, name')->from(TABLE_KANBANLANE)
->where('deleted')->eq('0')
->andWhere('region')->eq($regionID)
+ ->beginIF($type != 'all')->andWhere('type')->eq($type)->fi()
->fetchPairs();
}
@@ -1149,11 +1531,14 @@ class kanbanModel extends model
->add('region', $regionID)
->add('order', $maxOrder ? $maxOrder + 1 : 1)
->add('lastEditedTime', helper::now())
- ->add('type', 'common')
+ ->setIF(isset($_POST['laneType']), 'execution', $kanbanID)
->trim('name')
->setDefault('color', '#7ec5ff')
+ ->remove('laneType')
->get();
+ $lane->type = isset($_POST['laneType']) ? $_POST['laneType'] : 'common';
+
$mode = zget($lane, 'mode', '');
if($mode == 'sameAsOther')
{
@@ -1162,11 +1547,12 @@ class kanbanModel extends model
}
elseif($mode == 'independent')
{
- $groupID = $this->createGroup($kanbanID, $regionID);
- $kanban = $this->getByID($kanbanID);
- $this->createDefaultColumns($kanban, $regionID, $groupID);
-
- $lane->group = $groupID;
+ $lane->group = $this->createGroup($kanbanID, $regionID);
+ if($lane->type == 'common')
+ {
+ $kanban = $this->getByID($kanbanID);
+ $this->createDefaultColumns($kanban, $regionID, $lane->group);
+ }
}
}
@@ -1177,6 +1563,18 @@ class kanbanModel extends model
if(dao::isError()) return false;
$laneID = $this->dao->lastInsertID();
+ if($lane->type != 'common' and isset($mode) and $mode == 'independent') $this->createRDColumn($regionID, $lane->group, $laneID, $lane->type, $kanbanID);
+
+ if(isset($mode) and ($mode == 'sameAsOther' or ($lane->type == 'common' and $mode == 'independent')))
+ {
+ $columnIDList = $this->dao->select('id')->from(TABLE_KANBANCOLUMN)->where('deleted')->eq(0)->andWhere('archived')->eq(0)->andWhere('`group`')->eq($lane->group)->fetchPairs();
+ foreach($columnIDList as $columnID)
+ {
+ $this->addKanbanCell($kanbanID, $laneID, $columnID, $lane->type);
+
+ if(dao::isError()) return false;
+ }
+ }
return $laneID;
}
@@ -1385,61 +1783,46 @@ class kanbanModel extends model
}
/**
- * createColumn
+ * Create execution columns.
*
- * @param int $laneID
- * @param string $type story|bug|task
- * @param int $executionID
- * @param string $groupBy
- * @param string $groupValue
+ * @param int|array $laneID
+ * @param string $type story|bug|task
+ * @param int $executionID
+ * @param string $groupBy
+ * @param string $groupValue
* @access public
* @return void
*/
- public function createExecutionColumns($laneID, $type, $executionID, $groupBy = '', $groupValue = '')
+ public function createExecutionColumns($laneID, $type, $executionID)
{
- $objects = array();
-
- if($type == 'story') $objects = $this->loadModel('story')->getExecutionStories($executionID, 0, 0, 't2.id_desc');
- if($type == 'bug') $objects = $this->loadModel('bug')->getExecutionBugs($executionID);
- if($type == 'task') $objects = $this->loadModel('execution')->getKanbanTasks($executionID);
-
- if(!empty($groupBy))
- {
- foreach($objects as $objectID => $object)
- {
- if($object->$groupBy != $groupValue) unset($objects[$objectID]);
- }
- }
-
$devColumnID = $testColumnID = $resolvingColumnID = 0;
if($type == 'story')
{
foreach($this->lang->kanban->storyColumn as $colType => $name)
{
$data = new stdClass();
- $data->lane = $laneID;
$data->name = $name;
$data->color = '#333';
$data->type = $colType;
- $data->cards = '';
if(strpos(',developing,developed,', $colType) !== false) $data->parent = $devColumnID;
- if(strpos(',testing,tested,', $colType) !== false) $data->parent = $testColumnID;
- if(strpos(',develop,test,', $colType) !== false) $data->parent = -1;
- if(strpos(',ready,develop,test,', $colType) === false)
- {
- $storyStatus = $this->config->kanban->storyColumnStatusList[$colType];
- $storyStage = $this->config->kanban->storyColumnStageList[$colType];
- foreach($objects as $storyID => $story)
- {
- if($story->status == $storyStatus and $story->stage == $storyStage) $data->cards .= $storyID . ',';
- }
- if(!empty($data->cards)) $data->cards = ',' . $data->cards;
- }
+ if(strpos(',testing,tested,', $colType) !== false) $data->parent = $testColumnID;
+ if(strpos(',develop,test,', $colType) !== false) $data->parent = -1;
$this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec();
- if($colType == 'develop') $devColumnID = $this->dao->lastInsertId();
- if($colType == 'test') $testColumnID = $this->dao->lastInsertId();
+
+ $colID = $this->dao->lastInsertId();
+ if($colType == 'develop') $devColumnID = $colID;
+ if($colType == 'test') $testColumnID = $colID;
+
+ if(is_array($laneID))
+ {
+ foreach($laneID as $id) $this->addKanbanCell($executionID, $id, $colID, 'story');
+ }
+ else
+ {
+ $this->addKanbanCell($executionID, $laneID, $colID, 'story');
+ }
}
}
elseif($type == 'bug')
@@ -1447,37 +1830,27 @@ class kanbanModel extends model
foreach($this->lang->kanban->bugColumn as $colType => $name)
{
$data = new stdClass();
- $data->lane = $laneID;
$data->name = $name;
$data->color = '#333';
$data->type = $colType;
- $data->cards = '';
- if(strpos(',fixing,fixed,', $colType) !== false) $data->parent = $resolvingColumnID;
+ if(strpos(',fixing,fixed,', $colType) !== false) $data->parent = $resolvingColumnID;
if(strpos(',testing,tested,', $colType) !== false) $data->parent = $testColumnID;
if(strpos(',resolving,test,', $colType) !== false) $data->parent = -1;
- if(strpos(',resolving,fixing,test,testing,tested,', $colType) === false)
- {
- $bugStatus = $this->config->kanban->bugColumnStatusList[$colType];
- foreach($objects as $bugID => $bug)
- {
- if($colType == 'unconfirmed' and $bug->status == $bugStatus and $bug->confirmed == 0)
- {
- $data->cards .= $bugID . ',';
- }
- elseif($colType == 'confirmed' and $bug->status == $bugStatus and $bug->confirmed == 1)
- {
- $data->cards .= $bugID . ',';
- }
- elseif(strpos(',unconfirmed,confirmed,', $colType) === false and $bug->status == $bugStatus)
- {
- $data->cards .= $bugID . ',';
- }
- }
- if(!empty($data->cards)) $data->cards = ',' . $data->cards;
- }
+
$this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec();
- if($colType == 'resolving') $resolvingColumnID = $this->dao->lastInsertId();
- if($colType == 'test') $testColumnID = $this->dao->lastInsertId();
+
+ $colID = $this->dao->lastInsertId();
+ if($colType == 'resolving') $resolvingColumnID = $colID;
+ if($colType == 'test') $testColumnID = $colID;
+
+ if(is_array($laneID))
+ {
+ foreach($laneID as $id) $this->addKanbanCell($executionID, $id, $colID, 'bug');
+ }
+ else
+ {
+ $this->addKanbanCell($executionID, $laneID, $colID, 'bug');
+ }
}
}
elseif($type == 'task')
@@ -1485,29 +1858,186 @@ class kanbanModel extends model
foreach($this->lang->kanban->taskColumn as $colType => $name)
{
$data = new stdClass();
- $data->lane = $laneID;
$data->name = $name;
$data->color = '#333';
$data->type = $colType;
- $data->cards = '';
if(strpos(',developing,developed,', $colType) !== false) $data->parent = $devColumnID;
if($colType == 'develop') $data->parent = -1;
- if(strpos(',develop,', $colType) === false)
- {
- $taskStatus = $this->config->kanban->taskColumnStatusList[$colType];
- foreach($objects as $taskID => $task)
- {
- if($task->status == $taskStatus) $data->cards .= $taskID . ',';
- }
- if(!empty($data->cards)) $data->cards = ',' . $data->cards;
- }
$this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec();
- if($colType == 'develop') $devColumnID = $this->dao->lastInsertId();
+
+ $colID = $this->dao->lastInsertId();
+ if($colType == 'develop') $devColumnID = $colID;
+
+ if(is_array($laneID))
+ {
+ foreach($laneID as $id) $this->addKanbanCell($executionID, $id, $colID, 'task');
+ }
+ else
+ {
+ $this->addKanbanCell($executionID, $laneID, $colID, 'task');
+ }
}
}
}
+ /**
+ * Add kanban cell for new lane.
+ *
+ * @param int $kanbanID
+ * @param int $laneID
+ * @param int $colID
+ * @param string $type story|task|bug|card
+ * @access public
+ * @return void
+ */
+ public function addKanbanCell($kanbanID, $laneID, $colID, $type, $cardID = 0)
+ {
+ $cell = $this->dao->select('id, cards')->from(TABLE_KANBANCELL)
+ ->where('kanban')->eq($kanbanID)
+ ->andWhere('lane')->eq($laneID)
+ ->andWhere('`column`')->eq($colID)
+ ->andWhere('type')->eq($type)
+ ->fetch();
+
+ if(empty($cell))
+ {
+ $cell = new stdclass();
+ $cell->kanban = $kanbanID;
+ $cell->lane = $laneID;
+ $cell->column = $colID;
+ $cell->type = $type;
+
+ $this->dao->insert(TABLE_KANBANCELL)->data($cell)->exec();
+ }
+ else
+ {
+ $cell->cards = $cell->cards ? $cell->cards . "$cardID," : ",$cardID,";
+ $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($cell->cards)->where('id')->eq($cell->id)->exec();
+ }
+ }
+
+ /**
+ * Create a default RD kanban.
+ *
+ * @param object $execution
+ * @access public
+ * @return void
+ */
+ public function createRDKanban($execution)
+ {
+ $regionID = $this->createRDRegion($execution);
+ if(dao::isError()) return false;
+
+ $groupID = $this->createGroup($execution->id, $regionID);
+ if(dao::isError()) return false;
+
+ $this->createRDLane($execution->id, $regionID);
+ if(dao::isError()) return false;
+ }
+
+ /**
+ * Create a default RD region.
+ *
+ * @param object $execution
+ *
+ * @access public
+ * @return int|bool
+ */
+ public function createRDRegion($execution)
+ {
+ $region = new stdclass();
+ $region->name = $this->lang->kanbanregion->default;
+ $region->kanban = $execution->id;
+ $region->createdBy = $this->app->user->account;
+ $region->createdDate = helper::today();
+ $region->order = 1;
+
+ $this->dao->insert(TABLE_KANBANREGION)->data($region)
+ ->check('name', 'unique', "kanban={$execution->id} AND deleted='0'")
+ ->autoCheck()
+ ->exec();
+
+ if(dao::isError()) return false;
+ return $this->dao->lastInsertId();
+ }
+
+ /**
+ * Create default RD lanes.
+ *
+ * @param int $executionID
+ * @param int $regionID
+ *
+ * @access public
+ * @return bool
+ */
+ public function createRDLane($executionID, $regionID)
+ {
+ $laneIndex = 0;
+ foreach($this->lang->kanban->laneTypeList as $type => $name)
+ {
+ $groupID = $this->createGroup($executionID, $regionID);
+ if(dao::isError()) return false;
+
+ $lane = new stdclass();
+ $lane->execution = $executionID;
+ $lane->type = $type;
+ $lane->region = $regionID;
+ $lane->group = $groupID;
+ $lane->name = $name;
+ $lane->color = $this->config->kanban->laneColorList[$laneIndex];
+ $lane->order = ++ $laneIndex * 5;
+
+ $this->dao->insert(TABLE_KANBANLANE)->data($lane)->autoCheck()->exec();
+ if(dao::isError()) return false;
+
+ $this->createRDColumn($regionID, $groupID, $this->dao->lastInsertId(), $type, $executionID);
+ }
+ }
+
+ /**
+ * Create default RD columns.
+ *
+ * @param int $regionID
+ * @param int $groupID
+ * @param int $laneID
+ * @param string $laneType
+ *
+ * @access public
+ * @return bool
+ */
+ public function createRDColumn($regionID, $groupID, $laneID, $laneType, $executionID)
+ {
+ $devColumnID = $testColumnID = $resolvingColumnID = 0;
+ if($laneType == 'story') $columnList = $this->lang->kanban->storyColumn;
+ if($laneType == 'bug') $columnList = $this->lang->kanban->bugColumn;
+ if($laneType == 'task') $columnList = $this->lang->kanban->taskColumn;
+
+ foreach($columnList as $type => $name)
+ {
+ $data = new stdClass();
+ $data->name = $name;
+ $data->color = '#333';
+ $data->type = $type;
+ $data->group = $groupID;
+ $data->region = $regionID;
+
+ if(strpos(',developing,developed,', $type) !== false) $data->parent = $devColumnID;
+ if(strpos(',testing,tested,', $type) !== false) $data->parent = $testColumnID;
+ if(strpos(',fixing,fixed,', $type) !== false) $data->parent = $resolvingColumnID;
+ if(strpos(',develop,test,resolving,', $type) !== false) $data->parent = -1;
+
+ $this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec();
+ if(dao::isError()) return false;
+
+ if($type == 'develop') $devColumnID = $this->dao->lastInsertId();
+ if($type == 'test') $testColumnID = $this->dao->lastInsertId();
+ if($type == 'resolving') $resolvingColumnID = $this->dao->lastInsertId();
+
+ $this->addKanbanCell($executionID, $laneID, $this->dao->lastInsertId(), $laneType);
+ }
+ }
+
/**
* Update a region.
*
@@ -1551,24 +2081,27 @@ class kanbanModel extends model
->andWhere('type')->eq($laneType)
->fetchAll('id');
- foreach($lanes as $lane) $this->updateCards($lane);
+ foreach($lanes as $lane) $this->refreshCards($lane);
}
/**
- * Update column cards.
+ * Refresh column cards.
*
* @param object $lane
* @access public
* @return void
*/
- public function updateCards($lane)
+ public function refreshCards($lane)
{
$laneType = $lane->type;
$executionID = $lane->execution;
- $cardPairs = $this->dao->select('*')->from(TABLE_KANBANCOLUMN)
- ->where('deleted')->eq(0)
- ->andWhere('lane')->eq($lane->id)
- ->fetchPairs('type' ,'cards');
+ $cardPairs = $this->dao->select('t2.type, t1.cards')->from(TABLE_KANBANCELL)->alias('t1')
+ ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.`column` = t2.id')
+ ->where('t1.kanban')->eq($executionID)
+ ->andWhere('t1.lane')->eq($lane->id)
+ ->fetchPairs();
+
+ if(empty($cardPairs)) return;
if($laneType == 'story')
{
@@ -1582,21 +2115,19 @@ class kanbanModel extends model
$cardPairs[$colType] = str_replace(",$storyID,", ',', $cardPairs[$colType]);
}
- if(strpos(',ready,develop,test,', $colType) !== false) continue;
+ if(strpos(',ready,backlog,develop,test,', $colType) !== false) continue;
- if($lane->groupby and $story->{$lane->groupby} != $lane->extra)
- {
- $cardPairs[$colType] = str_replace(",$storyID,", ',', $cardPairs[$colType]);
- }
- elseif($colType == 'backlog' and $story->stage == $stage and strpos($cardPairs['ready'], ",$storyID,") === false and strpos($cardPairs['backlog'], ",$storyID,") === false)
- {
- $cardPairs['backlog'] = empty($cardPairs['backlog']) ? ",$storyID," : ",$storyID" . $cardPairs['backlog'];
- }
- elseif($story->stage == $stage and strpos($cardPairs[$colType], ",$storyID,") === false and $colType != 'backlog')
+ if($story->stage == $stage and strpos($cardPairs[$colType], ",$storyID,") === false)
{
$cardPairs[$colType] = empty($cardPairs[$colType]) ? ",$storyID," : ",$storyID" . $cardPairs[$colType];
}
}
+
+ if($story->stage == 'projected' and strpos($cardPairs['ready'], ",$storyID,") === false and strpos($cardPairs['backlog'], ",$storyID,") === false)
+ {
+ $cardPairs['backlog'] = empty($cardPairs['backlog']) ? ",$storyID," : ",$storyID" . $cardPairs['backlog'];
+ }
+
}
}
elseif($laneType == 'bug')
@@ -1613,11 +2144,7 @@ class kanbanModel extends model
if(strpos(',resolving,test,testing,tested,', $colType) !== false) continue;
- if($lane->groupby and $bug->{$lane->groupby} != $lane->extra)
- {
- $cardPairs[$colType] = str_replace(",$bugID,", ',', $cardPairs[$colType]);
- }
- elseif($colType == 'unconfirmed' and $bug->status == $status and $bug->confirmed == 0 and strpos($cardPairs['unconfirmed'], ",$bugID,") === false and strpos($cardPairs['fixing'], ",$bugID,") === false and $bug->activatedCount == 0)
+ if($colType == 'unconfirmed' and $bug->status == $status and $bug->confirmed == 0 and strpos($cardPairs['unconfirmed'], ",$bugID,") === false and strpos($cardPairs['fixing'], ",$bugID,") === false and $bug->activatedCount == 0)
{
$cardPairs['unconfirmed'] = empty($cardPairs['unconfirmed']) ? ",$bugID," : ",$bugID" . $cardPairs['unconfirmed'];
if(strpos($cardPairs['closed'], ",$bugID,") !== false) $cardPairs['closed'] = str_replace(",$bugID,", ',', $cardPairs['closed']);
@@ -1655,11 +2182,7 @@ class kanbanModel extends model
{
if($colType == 'develop') continue;
- if($lane->groupby and $task->{$lane->groupby} != $lane->extra)
- {
- $cardPairs[$colType] = str_replace(",$taskID,", ',', $cardPairs[$colType]);
- }
- elseif($task->status == $status and strpos($cardPairs[$colType], ",$taskID,") === false)
+ if($task->status == $status and strpos($cardPairs[$colType], ",$taskID,") === false)
{
$cardPairs[$colType] = empty($cardPairs[$colType]) ? ",$taskID," : ",$taskID". $cardPairs[$colType];
}
@@ -1671,107 +2194,21 @@ class kanbanModel extends model
}
}
+ $colPairs = $this->dao->select('t2.type, t2.id')->from(TABLE_KANBANCELL)->alias('t1')
+ ->leftJoin(TABLE_KANBANCOLUMN)->alias('t2')->on('t1.`column` = t2.id')
+ ->where('t1.kanban')->eq($executionID)
+ ->andWhere('t1.lane')->eq($lane->id)
+ ->fetchPairs();
+
foreach($cardPairs as $colType => $cards)
{
- $this->dao->update(TABLE_KANBANCOLUMN)->set('cards')->eq($cards)->where('lane')->eq($lane->id)->andWhere('type')->eq($colType)->exec();
+ if(!isset($colPairs[$colType])) continue;
+ $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($cards)->where('lane')->eq($lane->id)->andWhere('`column`')->eq($colPairs[$colType])->exec();
}
$this->dao->update(TABLE_KANBANLANE)->set('lastEditedTime')->eq(helper::now())->where('id')->eq($lane->id)->exec();
}
- /**
- * Update group lanes.
- *
- * @param int $executionID
- * @param string $type
- * @param string $groupBy
- * @access public
- * @return array
- */
- public function updateGroupLanes($executionID, $type, $groupBy)
- {
- $this->loadModel($type);
-
- $lanes = $this->dao->select('*')->from(TABLE_KANBANLANE)
- ->where('execution')->eq($executionID)
- ->andWhere('deleted')->eq(0)
- ->andWhere('type')->eq($type)->fi()
- ->andWhere('groupby')->eq($groupBy)->fi()
- ->orderBy('order_asc')
- ->fetchAll('id');
-
- /* Get old group list of kanban lane. */
- $oldGroupList = array();
- foreach($lanes as $lane) $oldGroupList[] = $lane->extra;
-
- /* Get new group list of kanban lane. */
- $groupList = $this->getObjectGroup($executionID, $type, $groupBy);
-
- $removeGroupList = array_diff($oldGroupList, $groupList);
- $addGroupList = array_diff($groupList, $oldGroupList);
-
- $objectPairs = array();
- if($groupBy == 'module') $objectPairs = $this->dao->select('id,name')->from(TABLE_MODULE)->where('type')->in('story,bug,task')->andWhere('deleted')->eq('0')->fetchPairs();
- if($groupBy == 'story') $objectPairs = $this->dao->select('id,title')->from(TABLE_STORY)->where('deleted')->eq(0)->fetchPairs();
- if($groupBy == 'assignedTo') $objectPairs = $this->loadModel('user')->getPairs('noletter');
-
- $colorIndex = 0;
-
- foreach($lanes as $laneID => $lane)
- {
- if(in_array($lane->extra, $removeGroupList))
- {
- /* Remove lane and cloumns by laneID. */
- $this->dao->delete()->from(TABLE_KANBANLANE)->where('id')->eq($laneID)->exec();
- $this->dao->delete()->from(TABLE_KANBANCOLUMN)->where('lane')->eq($laneID)->exec();
- }
- else
- {
- /* Update kanban lanes by group. */
- $laneName = $this->lang->$type->$groupBy . ': ' . $this->lang->kanban->noGroup;
- if($lane->extra)
- {
- $namePairs = strpos('module,story,assignedTo', $groupBy) !== false ? $objectPairs : $this->lang->$type->{$groupBy . 'List'};
- $laneName = $this->lang->$type->$groupBy . ': ' . zget($namePairs, $lane->extra);
- }
-
- $this->dao->update(TABLE_KANBANLANE)->set('name')->eq($laneName)->where('id')->eq($laneID)->exec();
- $this->updateCards($lane);
-
- $colorIndex += 1;
- if($colorIndex == count($this->config->kanban->laneColorList)) $colorIndex = 0;
- }
- }
-
- /* Add new lanes by group. */
- foreach($addGroupList as $groupKey)
- {
- $laneName = $this->lang->kanban->noGroup;
- if($groupKey)
- {
- $namePairs = strpos('module,story,assignedTo', $groupBy) !== false ? $objectPairs : $this->lang->$type->{$groupBy . 'List'};
- $laneName = zget($namePairs, $groupKey);
- }
-
- $lane = new stdClass();
- $lane->execution = $executionID;
- $lane->type = $type;
- $lane->groupby = $groupBy;
- $lane->extra = $groupKey;
- $lane->name = $this->lang->$type->$groupBy . ": " . $laneName;
- $lane->color = $this->config->kanban->laneColorList[$colorIndex];
-
- $colorIndex += 1;
- if($colorIndex == count($this->config->kanban->laneColorList)) $colorIndex = 0;
- $this->dao->insert(TABLE_KANBANLANE)->data($lane)->exec();
-
- $laneID = $this->dao->lastInsertId();
- $this->createExecutionColumns($laneID, $type, $executionID, $groupBy, $groupKey);
- }
-
- $this->resetLaneOrder($executionID, $type, $groupBy);
- }
-
/**
* Update lane column.
*
@@ -1972,6 +2409,36 @@ class kanbanModel extends model
return dao::isError();
}
+ /**
+ * Set lane height.
+ *
+ * @param int $kanbanID
+ * @param string $from kanban|execution
+ * @access public
+ * @return bool
+ */
+ public function setLaneHeight($kanbanID, $from = 'kanban')
+ {
+ $kanbanID = (int)$kanbanID;
+ $kanban = fixer::input('post')
+ ->setIF($this->post->heightType == 'auto', 'displayCards', 0)
+ ->get();
+
+ if($kanban->heightType == 'custom')
+ {
+ if(!preg_match("/^-?\d+$/", $kanban->displayCards) or $kanban->displayCards < 3)
+ {
+ dao::$errors['displayCards'] = $this->lang->kanbanlane->error->mustBeInt;
+ return false;
+ }
+ }
+
+ $table = $this->config->objectTables[$from];
+ $this->dao->update($table)->set('displayCards')->eq((int)$kanban->displayCards)->where('id')->eq($kanbanID)->exec();
+
+ if(dao::isError()) return false;
+ }
+
/**
* Set kanban headerActions.
*
@@ -1981,24 +2448,45 @@ class kanbanModel extends model
*/
public function setHeaderActions($kanban)
{
+ $printSetHeight = false;
+ if(common::hasPriv('kanban', 'setLaneHeight'))
+ {
+ $laneCount = $this->dao->select('COUNT(t2.id) as count')->from(TABLE_KANBANREGION)->alias('t1')
+ ->leftJoin(TABLE_KANBANLANE)->alias('t2')->on('t1.id=t2.region')
+ ->where('t1.kanban')->eq($kanban->id)
+ ->andWhere('t1.deleted')->eq(0)
+ ->andWhere('t2.deleted')->eq(0)
+ ->fetch('count');
+
+ if($laneCount > 1) $printSetHeight = true;
+ }
+
$actions = '';
$actions .= "";
$actions .= "
{$this->lang->kanban->fullScreen}";
- $printSettingBtn = (common::hasPriv('kanban', 'createRegion') or common::hasPriv('kanban', 'edit') or common::hasPriv('kanban', 'close') or common::hasPriv('kanban', 'delete'));
+
+ $printSettingBtn = (common::hasPriv('kanban', 'createRegion') or $printSetHeight or common::hasPriv('kanban', 'edit') or common::hasPriv('kanban', 'close') or common::hasPriv('kanban', 'delete'));
+
if($printSettingBtn)
{
$actions .= "
" . ' ' . $this->lang->kanban->setting . '';
$actions .= "";
}
@@ -2061,17 +2549,23 @@ class kanbanModel extends model
* Move a card.
*
* @param int $cardID
+ * @param int $fromColID
* @param int $toColID
+ * @param int $fromLaneID
* @param int $toLaneID
* @access public
* @return void
*/
- public function moveCard($cardID, $toColID, $toLaneID)
+ public function moveCard($cardID, $fromColID, $toColID, $fromLaneID, $toLaneID)
{
- $this->dao->update(TABLE_KANBANCARD)
- ->set('column')->eq($toColID)
- ->beginIF($toLaneID)->set('lane')->eq($toLaneID)->fi()
- ->where('id')->eq($cardID)->exec();
+ $fromCellCards = $this->dao->select('cards')->from(TABLE_KANBANCELL)->where('lane')->eq($fromLaneID)->andWhere('`column`')->eq($fromColID)->fetch('cards');
+ $toCellCards = $this->dao->select('cards')->from(TABLE_KANBANCELL)->where('lane')->eq($toLaneID)->andWhere('`column`')->eq($toColID)->fetch('cards');
+
+ $fromCardList = str_replace("$cardID,", '', $fromCellCards);
+ $toCardList = rtrim($toCellCards, ',') . ",$cardID,";
+
+ $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($fromCardList)->where('`column`')->eq($fromColID)->andWhere('lane')->eq($fromLaneID)->exec();
+ $this->dao->update(TABLE_KANBANCELL)->set('cards')->eq($toCardList)->where('`column`')->eq($toColID)->andWhere('lane')->eq($toLaneID)->exec();
}
/**
@@ -2255,7 +2749,7 @@ class kanbanModel extends model
public function getColumnByID($columnID)
{
$column = $this->dao->select('t1.*, t2.type as laneType')->from(TABLE_KANBANCOLUMN)->alias('t1')
- ->leftjoin(TABLE_KANBANLANE)->alias('t2')->on('t1.lane=t2.id')
+ ->leftjoin(TABLE_KANBANLANE)->alias('t2')->on('t1.group=t2.id')
->where('t1.id')->eq($columnID)
->fetch();
diff --git a/module/kanban/view/createexeclane.html.php b/module/kanban/view/createexeclane.html.php
new file mode 100644
index 0000000000..c3d2e687fa
--- /dev/null
+++ b/module/kanban/view/createexeclane.html.php
@@ -0,0 +1,111 @@
+
+ * @package kanban
+ * @version $Id: createexeclane.html.php 935 2022-01-12 17:22:24Z $
+ * @link https://www.zentao.net
+ */
+?>
+
+
+kanban->laneColorList);?>
+
+
+
+
+
+
+ kanban->createLane;?>
+
+
+
+
+
+
diff --git a/module/kanban/view/setlaneheight.html.php b/module/kanban/view/setlaneheight.html.php
new file mode 100644
index 0000000000..62125573fd
--- /dev/null
+++ b/module/kanban/view/setlaneheight.html.php
@@ -0,0 +1,38 @@
+
+ * @package kanban
+ * @version $Id: setlaneheight.html.php 935 2022-01-11 10:49:24Z $
+ * @link https://www.zentao.net
+ */
+?>
+
+
+
+
+
kanban->setLaneHeight;?>
+
+
+
+
+
diff --git a/module/kanban/view/space.html.php b/module/kanban/view/space.html.php
index 7f469f7036..1354a492e7 100644
--- a/module/kanban/view/space.html.php
+++ b/module/kanban/view/space.html.php
@@ -141,9 +141,11 @@
kanban->teamSumCount, $teamCount);?>
- acl == 'open' ? 'unlock' : 'lock';?>
+
+ acl == 'private') $icon = 'lock';?>
+ acl == 'extend') $icon = 'inherit-space';?>
- kanban->aclGroup, $kanban->acl == 'open' ? 'open' : 'private', '');?>
+ kanban->aclGroup, $kanban->acl, '');?>
diff --git a/module/kanban/view/view.html.php b/module/kanban/view/view.html.php
index 01a5ebb44b..2855b0104d 100644
--- a/module/kanban/view/view.html.php
+++ b/module/kanban/view/view.html.php
@@ -30,6 +30,7 @@ js::set('noAssigned', $lang->kanbancard->noAssigned);
js::set('users', $users);
js::set('colorListLang', $lang->kanbancard->colorList);
js::set('colorList', $this->config->kanban->cardColorList);
+js::set('displayCards', $kanban->displayCards);
js::set('priv',
array(
diff --git a/module/my/control.php b/module/my/control.php
index 7be5a039d0..61f141c693 100644
--- a/module/my/control.php
+++ b/module/my/control.php
@@ -443,6 +443,7 @@ EOF;
$this->view->tasks = $tasks;
$this->view->summary = $this->loadModel('execution')->summary($tasks);
$this->view->type = $type;
+ $this->view->kanbanList = $this->execution->getPairs(0, 'kanban');
$this->view->recTotal = $recTotal;
$this->view->recPerPage = $recPerPage;
$this->view->pageID = $pageID;
diff --git a/module/my/view/task.html.php b/module/my/view/task.html.php
index 42af633614..4432aea57a 100644
--- a/module/my/view/task.html.php
+++ b/module/my/view/task.html.php
@@ -131,14 +131,15 @@
}
else
{
- if($task->status != 'pause') common::printIcon('task', 'start', "taskID=$task->id", $task, 'list', '', '', 'iframe', true, '', '', $task->project);
- if($task->status == 'pause') common::printIcon('task', 'restart', "taskID=$task->id", $task, 'list', '', '', 'iframe', true, '', '', $task->project);
- common::printIcon('task', 'close', "taskID=$task->id", $task, 'list', '', '', 'iframe', true, '', '', $task->project);
- common::printIcon('task', 'finish', "taskID=$task->id", $task, 'list', '', '', 'iframe', true, '', '', $task->project);
+ $attr = isset($kanbanList[$task->execution]) ? "disabled" : '';
+ if($task->status != 'pause') common::printIcon('task', 'start', "taskID=$task->id", $task, 'list', '', '', 'iframe', true);
+ if($task->status == 'pause') common::printIcon('task', 'restart', "taskID=$task->id", $task, 'list', '', '', 'iframe', true);
+ common::printIcon('task', 'close', "taskID=$task->id", $task, 'list', '', '', 'iframe', true);
+ common::printIcon('task', 'finish', "taskID=$task->id", $task, 'list', '', '', 'iframe', true);
- common::printIcon('task', 'recordEstimate', "taskID=$task->id", $task, 'list', 'time', '', 'iframe', true, '', '', $task->project);
- common::printIcon('task', 'edit', "taskID=$task->id", $task, 'list', '', '', 'iframe', true, "data-width='95%'", '', $task->project);
- common::printIcon('task', 'batchCreate', "executionID=$task->execution&storyID=$task->story&moduleID=$task->module&taskID=$task->id&ifame=true", $task, 'list', 'split', '', 'iframe', true, "data-width='95%'", $this->lang->task->children, $task->project);
+ common::printIcon('task', 'recordEstimate', "taskID=$task->id", $task, 'list', 'time', '', 'iframe', true);
+ common::printIcon('task', 'edit', "taskID=$task->id", $task, 'list', '', '', 'iframe', true, "data-width='95%'");
+ common::printIcon('task', 'batchCreate', "executionID=$task->execution&storyID=$task->story&moduleID=$task->module&taskID=$task->id&ifame=true", $task, 'list', 'split', '', 'iframe', true, "data-width='95%' $attr", $this->lang->task->children);
}
}
?>
diff --git a/module/product/model.php b/module/product/model.php
index 8abcccf230..0a4ef341f6 100644
--- a/module/product/model.php
+++ b/module/product/model.php
@@ -1465,7 +1465,7 @@ class productModel extends model
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
->where('t2.deleted')->eq(0)
->andWhere('t1.product')->eq($productID)
- ->andWhere('t2.type')->in('sprint,stage')
+ ->andWhere('t2.type')->in('sprint,stage,kanban')
->fetch();
$product->stories = $stories;
diff --git a/module/product/view/ajaxgetdropmenu.html.php b/module/product/view/ajaxgetdropmenu.html.php
index 7dacf4a174..1809c88c97 100644
--- a/module/product/view/ajaxgetdropmenu.html.php
+++ b/module/product/view/ajaxgetdropmenu.html.php
@@ -161,7 +161,7 @@ $(function()
}
})
- $('#tabContent [data-ride="tree"]').tree('expand');
+ $('#swapper [data-ride="tree"]').tree('expand');
$('#swapper #dropMenu .search-box').on('onSearchChange', function(event, value)
{
diff --git a/module/productplan/model.php b/module/productplan/model.php
index 596c73f0cd..3a4202134b 100644
--- a/module/productplan/model.php
+++ b/module/productplan/model.php
@@ -99,7 +99,7 @@ class productplanModel extends model
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
->where('t1.product')->eq($product)
->andWhere('t1.plan')->in(array_keys($plans))
- ->andWhere('t2.type')->in('sprint,stage')
+ ->andWhere('t2.type')->in('sprint,stage,kanban')
->fetchPairs('plan', 'project');
$storyCountInTable = $this->dao->select('plan,count(story) as count')->from(TABLE_PLANSTORY)->where('plan')->in($planIdList)->groupBy('plan')->fetchPairs('plan', 'count');
diff --git a/module/program/css/project.css b/module/program/css/project.css
new file mode 100644
index 0000000000..499e53ecc1
--- /dev/null
+++ b/module/program/css/project.css
@@ -0,0 +1,3 @@
+.project-type-label.label-outline {width: 50px; min-width: 50px;}
+.project-type-label.label {overflow: unset !important; text-overflow: unset !important; white-space: unset !important;}
+[lang^='en'] .project-name > .label-warning {width: 55px !important;}
diff --git a/module/program/model.php b/module/program/model.php
index 02b3f2e1be..70faddc9c5 100644
--- a/module/program/model.php
+++ b/module/program/model.php
@@ -205,7 +205,7 @@ class programModel extends model
/* Get doing executions. */
$doingExecutions = $this->dao->select('id, project, name, end')->from(TABLE_EXECUTION)
- ->where('type')->in('sprint,stage')
+ ->where('type')->in('sprint,stage,kanban')
->andWhere('status')->eq('doing')
->andWhere('deleted')->eq(0)
->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi()
@@ -1254,7 +1254,7 @@ class programModel extends model
$executions = $this->dao->select('id')->from(TABLE_EXECUTION)
->where('deleted')->eq(0)
->andWhere('project')->in($projectKeys)
- ->andWhere('type')->in('sprint,stage')
+ ->andWhere('type')->in('sprint,stage,kanban')
->fetchAll('id');
/* Get all tasks and compute totalEstimate, totalConsumed, totalLeft, progress according to them. */
diff --git a/module/program/view/project.html.php b/module/program/view/project.html.php
index e532ca9ed0..f5f336d3d9 100644
--- a/module/program/view/project.html.php
+++ b/module/program/view/project.html.php
@@ -15,10 +15,6 @@
js::set('programID', $programID);
js::set('browseType', $browseType);
?>
-
program->featureBar as $key => $label):?>
diff --git a/module/project/control.php b/module/project/control.php
index fb5a0cfbce..0951995446 100644
--- a/module/project/control.php
+++ b/module/project/control.php
@@ -222,10 +222,11 @@ class project extends control
* Project index view.
*
* @param int $projectID
+ * @param string $browseType
* @access public
* @return void
*/
- public function index($projectID = 0)
+ public function index($projectID = 0, $browseType = 'all')
{
$projectID = $this->project->saveState($projectID, $this->project->getPairsByProgram());
@@ -240,6 +241,27 @@ class project extends control
if(!$projectID) $this->locate($this->createLink('project', 'browse'));
setCookie("lastProject", $projectID, $this->config->cookieLife, $this->config->webRoot, '', false, true);
+ if($project->model == 'kanban')
+ {
+ $kanbanList = $this->loadModel('execution')->getList($projectID, 'all', $browseType);
+
+ $executionActions = array();
+ foreach($kanbanList as $kanbanID => $kanban)
+ {
+ foreach($this->config->execution->statusActions as $action)
+ {
+ if($this->execution->isClickable($kanban, $action)) $executionActions[$kanbanID][] = $action;
+ }
+ if($this->execution->isClickable($kanban, 'delete')) $executionActions[$kanbanID][] = 'delete';
+ }
+
+ $this->view->kanbanList = $kanbanList;
+ $this->view->browseType = $browseType;
+ $this->view->memberGroup = $this->execution->getMembersByIdList(array_keys($kanbanList));
+ $this->view->usersAvatar = $this->loadModel('user')->getAvatarPairs();
+ $this->view->executionActions = $executionActions;
+ }
+
$this->view->title = $this->lang->project->common . $this->lang->colon . $this->lang->project->index;
$this->view->position[] = $this->lang->project->index;
$this->view->project = $project;
@@ -352,6 +374,8 @@ class project extends control
$this->loadModel('execution');
$this->loadModel('product');
+ if($model == 'kanban') unset($this->lang->project->authList['reset']);
+
if($_POST)
{
$projectID = $this->project->create();
@@ -576,8 +600,7 @@ class project extends control
}
}
- $canChangeModel = $this->project->checkCanChangeModel($projectID, $project->model);
- $disableModel = $canChangeModel == true ? '' : 'disabled';
+ if($project->model != 'kanban') $canChangeModel = $this->project->checkCanChangeModel($projectID, $project->model);
$this->view->title = $this->lang->project->edit;
$this->view->position[] = $this->lang->project->edit;
@@ -604,7 +627,7 @@ class project extends control
$this->view->availableBudget = $this->program->getBudgetLeft($parentProject) + (float)$project->budget;
$this->view->budgetUnitList = $this->project->getBudgetUnitList();
$this->view->model = $project->model;
- $this->view->disableModel = $disableModel;
+ $this->view->disableModel = (isset($canChangeModel) and $canChangeModel == true) ? '' : 'disabled';
$this->display();
}
diff --git a/module/project/css/browse.css b/module/project/css/browse.css
index fecfbfca43..5e32206b22 100644
--- a/module/project/css/browse.css
+++ b/module/project/css/browse.css
@@ -1,3 +1,4 @@
+body {margin-bottom: 25px;}
.side-col .detail-content {padding-left: 10px; margin-top: 0px;}
.side-col {padding-right: 20px; width: 18%;}
#sidebar>.cell {width: 100%;}
@@ -16,3 +17,4 @@ th.c-name {width: 360px !important;}
{
th.c-name {width: auto;}
}
+[lang^='en'] .project-name > .label-warning {width: 55px !important;}
diff --git a/module/project/css/index.css b/module/project/css/index.css
new file mode 100644
index 0000000000..151516a0ab
--- /dev/null
+++ b/module/project/css/index.css
@@ -0,0 +1,30 @@
+#cards > .col {width: 25% !important;}
+#cards .panel {margin: 0 0; border: 1px solid #DCDCDC; border-radius: 4px; box-shadow: none; cursor: pointer; height: 160px; margin-bottom: 5px;}
+#cards .panel:hover {border-color: #006AF1; box-shadow: 0 0 10px 0 rgba(0,0,100,.25);}
+#cards .panel-heading {padding: 4px 2px 10px 16px;}
+#cards .kanban-name {overflow: hidden; white-space:nowrap; width: 88%; float: left; padding-top: 8px;}
+#cards .panel .label-closed {background: #AAA !important; color: #FFF; margin-top:-1px !important;}
+#cards .kanban-actions {float: right; visibility: hidden;}
+#cards .kanban-actions .dropdown-menu.pull-right {top:31px; right: -84px; left: auto;}
+#cards .kanban-actions .dropdown-menu.pull-left {top:31px; right:-1px; left: auto;}
+#cards .panel .label-closed {background: #AAA !important; color: #FFF; margin-top:-6px;}
+#cards .panel-body {padding: 0 16px 16px;}
+#cards .kanban-desc {color: #838a9d; word-break:break-all; height: 70px; overflow: hidden; display: -webkit-box; float:left;}
+#cards .kanban-footer {position: absolute; bottom: 10px; right: 10px; left: 15px;}
+#cards .kanban-members {float: left; height: 24px; line-height: 24px;}
+#cards .kanban-members > div {display: inline-block; height: 24px;}
+#cards .kanban-members > div + div {margin-left: -5px;}
+#cards .kanban-members > div > .avatar {display: inline-block; width: 24px; height: 24px; line-height: 24px; margin-right: 1px;}
+#cards .kanban-members > span {display: inline-block; color: transparent; width: 2px; height: 2px; background-color: #8990a2; position: relative; border-radius: 50%; top: 3px; margin: 0 3px;}
+#cards .kanban-members > span:before,
+#cards .kanban-members > span:after {content: ''; display: block; position: absolute; width: 2px; height: 2px; background-color: #8990a2; top: 0; border-radius: 50%}
+#cards .kanban-members > span:before {left: -4px;}
+#cards .kanban-members > span:after {right: -4px;}
+#cards .kanban-members-total {display: inline-block; margin-left: 6px; position: relative; top: 3px}
+#cards .kanbanAcl {position: absolute; right: 0px; bottom: 0px; color: #838a9d;}
+
+.kanbans .kanban-actions {float: right; visibility: hidden;}
+.kanbans .kanban-actions .dropdown-menu.pull-right {top:31px; right: -84px; left: auto;}
+.kanbans .kanban-actions .dropdown-menu.pull-left {top:31px; right:-1px; left: auto;}
+
+.label-doing {background: #ff5d5d}
diff --git a/module/project/js/index.js b/module/project/js/index.js
new file mode 100644
index 0000000000..da84c9f94a
--- /dev/null
+++ b/module/project/js/index.js
@@ -0,0 +1,21 @@
+/* Make cards clickable. */
+$('#cards').on('click', '.panel', function(e)
+{
+ if(!$(e.target).closest('.kanban-actions').length)
+ {
+ $.apps.open($(this).data('url'));
+ }
+});
+
+/* Display drop-down menu.*/
+$('.panel').mouseenter(function(e)
+{
+ $('.kanban-actions' + e.currentTarget.parentElement.dataset.id).css('visibility','visible');
+});
+
+/* Hide drop-down menu. */
+$('.panel').mouseleave(function(e)
+{
+ $('.kanban-actions').css('visibility','hidden');
+ $('.dropdown').removeClass('open');
+});
diff --git a/module/project/lang/en.php b/module/project/lang/en.php
index 2df5ddb34c..bcc353b63e 100644
--- a/module/project/lang/en.php
+++ b/module/project/lang/en.php
@@ -155,6 +155,8 @@ $lang->project->etc = " , etc";
$lang->project->product = 'Product';
$lang->project->branch = 'Branch';
$lang->project->plan = 'Plan';
+$lang->project->kanban = 'Kanban';
+$lang->project->createKanban = 'Create Kanban';
/* Project Kanban. */
$lang->project->typeList = array();
@@ -220,6 +222,7 @@ $lang->project->currencySymbol['SGD'] = 'S$';
$lang->project->modelList['scrum'] = "Scrum";
$lang->project->modelList['waterfall'] = "CMMI";
+$lang->project->modelList['kanban'] = "Kanban";
$lang->project->featureBar['all'] = 'All';
$lang->project->featureBar['doing'] = 'Going';
@@ -252,6 +255,10 @@ $lang->project->endList[186] = 'Half yearly';
$lang->project->endList[365] = 'One year';
$lang->project->endList[999] = 'Longtime';
+$lang->project->scrumTitle = 'Agile Development Management';
+$lang->project->waterfallTitle = 'Waterfall Project Management';
+$lang->project->kanbanTitle = 'Kanban Project Management';
+
$lang->project->empty = 'No project.';
$lang->project->nextStep = 'Next step';
$lang->project->hoursUnit = '%s hours';
@@ -260,7 +267,6 @@ $lang->project->lastIteration = "Recent {$lang->executionCommon}";
$lang->project->ongoingStage = 'Ongoing stage';
$lang->project->scrum = 'Scrum';
$lang->project->waterfall = 'Waterfall';
-$lang->project->waterfallTitle = 'Waterfall Project Management';
$lang->project->cannotCreateChild = 'It is not empty, so you cannot add a child. You can add a parent for it, and then create a child.';
$lang->project->confirmDelete = "Do you want to delete [%s]?";
$lang->project->emptyPM = 'No manager';
@@ -278,7 +284,6 @@ $lang->project->programTitle['end'] = 'End-level project only';
$lang->project->accessDenied = 'Access denied to this project';
$lang->project->chooseProgramType = 'Select management type';
-$lang->project->scrumTitle = 'Agile Development Management';
$lang->project->cannotCreateChild = 'The project has contents, so you cannot add a child project. You can create a parent project for this one and then add a child project for the parent project.';
$lang->project->hasChildren = 'This project has a child project, so it cannot be deleted.';
$lang->project->confirmDelete = 'Do you want to delete this project?';
diff --git a/module/project/lang/zh-cn.php b/module/project/lang/zh-cn.php
index 643d7f02d8..132c11dc68 100644
--- a/module/project/lang/zh-cn.php
+++ b/module/project/lang/zh-cn.php
@@ -155,6 +155,8 @@ $lang->project->etc = "等";
$lang->project->product = '所属产品';
$lang->project->branch = '所属分支';
$lang->project->plan = '所属计划';
+$lang->project->createKanban = '添加看板';
+$lang->project->kanban = '看板';
/* Project Kanban. */
$lang->project->typeList = array();
@@ -220,6 +222,7 @@ $lang->project->currencySymbol['SGD'] = 'S$';
$lang->project->modelList['scrum'] = "Scrum";
$lang->project->modelList['waterfall'] = "瀑布";
+$lang->project->modelList['kanban'] = "看板";
$lang->project->featureBar['all'] = '所有';
$lang->project->featureBar['doing'] = '进行中';
@@ -252,6 +255,10 @@ $lang->project->endList[186] = '半年';
$lang->project->endList[365] = '一年';
$lang->project->endList[999] = '长期';
+$lang->project->scrumTitle = '敏捷开发全流程项目管理';
+$lang->project->waterfallTitle = '瀑布式项目管理';
+$lang->project->kanbanTitle = '专业研发看板项目管理';
+
$lang->project->empty = '暂时没有项目';
$lang->project->nextStep = '下一步';
$lang->project->hoursUnit = '%s 工时';
@@ -260,7 +267,6 @@ $lang->project->lastIteration = "近期{$lang->executionCommon}";
$lang->project->ongoingStage = '进行中的阶段';
$lang->project->scrum = 'Scrum';
$lang->project->waterfall = '瀑布';
-$lang->project->waterfallTitle = '瀑布式项目管理';
$lang->project->cannotCreateChild = '该项目已经有实际的内容,无法直接添加子项目。您可以为当前项目创建一个父项目,然后在新的父项目下面添加子项目。';
$lang->project->confirmDelete = "您确定要删除吗?";
$lang->project->emptyPM = '暂无';
@@ -278,7 +284,6 @@ $lang->project->programTitle['end'] = '只显示最后一级项目集';
$lang->project->accessDenied = '您无权访问该项目!';
$lang->project->chooseProgramType = '选择项目管理方式';
-$lang->project->scrumTitle = '敏捷开发全流程项目管理';
$lang->project->cannotCreateChild = '该项目已经有实际的内容,无法直接添加子项目。您可以为当前项目创建一个父项目,然后在新的父项目下面添加子项目。';
$lang->project->hasChildren = '该项目有子项目存在,不能删除。';
$lang->project->confirmDelete = "您确定删除项目[%s]吗?";
diff --git a/module/project/model.php b/module/project/model.php
index c01a4e8c2c..5d59ff7334 100644
--- a/module/project/model.php
+++ b/module/project/model.php
@@ -632,7 +632,7 @@ class projectModel extends model
/**
* Get project pairs by model and project.
*
- * @param string $model all|scrum|waterfall
+ * @param string $model all|scrum|waterfall|kanban
* @param int $programID
* @access public
* @return array
@@ -1569,7 +1569,8 @@ class projectModel extends model
$prefix = '';
$suffix = '';
if($project->model === 'waterfall') $prefix = "
{$this->lang->project->waterfall} ";
- if($project->model === 'scrum') $prefix = "
{$this->lang->project->scrum} ";
+ if($project->model === 'scrum') $prefix = "
{$this->lang->project->scrum} ";
+ if($project->model === 'kanban') $prefix = "
{$this->lang->project->kanban} ";
if(isset($project->delay)) $suffix = "
{$this->lang->project->statusList['delay']}";
if(!empty($suffix) || !empty($prefix)) echo '
';
if(!empty($prefix)) echo $prefix;
@@ -1633,20 +1634,21 @@ class projectModel extends model
}
$from = $project->from == 'project' ? 'project' : 'pgmproject';
- $iframe = $this->app->tab == 'program' ? 'iframe' : '';
- $onlyBody = $this->app->tab == 'program' ? true : '';
+ $iframe = ($this->app->tab == 'program' or $project->model == 'kanban') ? 'iframe' : '';
+ $onlyBody = ($this->app->tab == 'program' or $project->model == 'kanban') ? true : '';
$dataApp = $this->config->systemMode == 'classic' ? "data-app=execution" : "data-app=project";
- common::printIcon($moduleName, 'edit', "projectID=$project->id", $project, 'list', 'edit', '', $iframe, $onlyBody, $dataApp, '', $project->id);
- common::printIcon($moduleName, 'manageMembers', "projectID=$project->id", $project, 'list', 'group', '', '', '', $dataApp, $this->lang->execution->team, $project->id);
- if($this->config->systemMode == 'new') common::printIcon('project', 'group', "projectID=$project->id&programID=$programID", $project, 'list', 'lock', '', '', '', $dataApp, '', $project->id);
+ $attr = $project->model == 'kanban' ? " disabled='disabled'" : '';
+ common::printIcon($moduleName, 'edit', "projectID=$project->id", $project, 'list', 'edit', '', $iframe, $onlyBody, $dataApp);
+ common::printIcon($moduleName, 'manageMembers', "projectID=$project->id", $project, 'list', 'group', '', '', '', $dataApp . $attr, $this->lang->execution->team);
+ if($this->config->systemMode == 'new') common::printIcon('project', 'group', "projectID=$project->id&programID=$programID", $project, 'list', 'lock', '', '', '', $dataApp . $attr);
if(common::hasPriv($moduleName, 'manageProducts') || common::hasPriv($moduleName, 'whitelist') || common::hasPriv($moduleName, 'delete'))
{
echo "
";
echo "";
echo "";
echo "
";
@@ -1776,7 +1778,7 @@ class projectModel extends model
$type = 'project';
if($this->config->systemMode == 'new')
{
- if($project->type == 'sprint' or $project->type == 'stage') $type = 'execution';
+ if($project->type == 'sprint' or $project->type == 'stage' or $project->type == 'kanban') $type = 'execution';
}
$members = $this->dao->select("t1.account, if(t2.deleted='0', t2.realname, t1.account) as realname")->from(TABLE_TEAM)->alias('t1')
@@ -1834,9 +1836,9 @@ class projectModel extends model
->fetchPairs();
}
- $executions = $this->dao->select('t1.*,t2.name projectName')->from(TABLE_EXECUTION)->alias('t1')
+ $executions = $this->dao->select('t1.*,t2.name projectName, t2.model as projectModel')->from(TABLE_EXECUTION)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
- ->where('t1.type')->in('sprint,stage')
+ ->where('t1.type')->in('sprint,stage,kanban')
->beginIF($projectID != 0)->andWhere('t1.project')->eq($projectID)->fi()
->beginIF(!empty($myExecutionIDList))->andWhere('t1.id')->in(array_keys($myExecutionIDList))->fi()
->beginIF($status == 'undone')->andWhere('t1.status')->notIN('done,closed')->fi()
@@ -1849,7 +1851,7 @@ class projectModel extends model
}
else
{
- $executions = $this->dao->select('t2.*,t3.name projectName')->from(TABLE_PROJECTPRODUCT)->alias('t1')
+ $executions = $this->dao->select('t2.*,t3.name projectName, t3.model as projectModel')->from(TABLE_PROJECTPRODUCT)->alias('t1')
->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project=t2.id')
->leftJoin(TABLE_PROJECT)->alias('t3')->on('t2.project=t3.id')
->where('t1.product')->eq($productID)
diff --git a/module/project/view/ajaxgetdropmenu.html.php b/module/project/view/ajaxgetdropmenu.html.php
index 4aa753f687..648ed20b7f 100644
--- a/module/project/view/ajaxgetdropmenu.html.php
+++ b/module/project/view/ajaxgetdropmenu.html.php
@@ -72,9 +72,12 @@ foreach($projects as $programID => $programProjects)
foreach($programProjects as $index => $project)
{
- $selected = $project->id == $projectID ? 'selected' : '';
- $link = helper::createLink('project', 'index', "projectID=%s", '', '', $project->id);
- $projectName = $project->model == 'scrum' ? '
' . $project->name : '
' . $project->name;
+ $selected = $project->id == $projectID ? 'selected' : '';
+ $link = helper::createLink('project', 'index', "projectID=%s", '', '', $project->id);
+ $icon = '
';
+
+ if($project->model != 'scrum') $icon = "
";
+ $projectName = $icon . $project->name;
if($project->status != 'done' and $project->status != 'closed' and $project->PM == $this->app->user->account)
{
diff --git a/module/project/view/browsebycard.html.php b/module/project/view/browsebycard.html.php
index 38d25b05ad..04a708ee67 100644
--- a/module/project/view/browsebycard.html.php
+++ b/module/project/view/browsebycard.html.php
@@ -84,10 +84,8 @@
project->empty;?>
- config->maxVersion) and !defined('TUTORIAL')):?>
+
' . $lang->project->create, '', 'class="btn btn-info" data-toggle="modal" data-target="#guideDialog"');?>
- config->systemMode == 'new'):?>
- ' . $lang->project->create, '', 'class="btn btn-info"');?>
' . $lang->execution->create, '', 'class="btn btn-info"');?>
@@ -102,9 +100,11 @@
project->statusList[$status];?>
- maxVersion) and $project->model === 'waterfall'):?>
+ model === 'waterfall'):?>
- maxVersion)):?>
+ model === 'kanban'):?>
+
+
name);?>
@@ -174,7 +174,9 @@
", '', "data-toggle='dropdown' class='btn btn-link'");?>