Merge branch 'sprint/168_sgm_kanban' into 'sprint/168'

* Add kanban view.

See merge request easycorp/zentaopms!154
This commit is contained in:
李玉春
2021-10-26 07:16:12 +00:00
4 changed files with 560 additions and 443 deletions
-12
View File
@@ -1849,20 +1849,13 @@ class execution extends control
$this->execution->setMenu($executionID);
$execution = $this->loadModel('execution')->getById($executionID);
$tasks = $this->execution->getKanbanTasks($executionID, "id");
$bugs = $this->loadModel('bug')->getExecutionBugs($executionID);
$stories = $this->loadModel('story')->getExecutionStories($executionID);
/* Determines whether an object is editable. */
$canBeChanged = common::canModify('execution', $execution);
$kanbanGroup = $this->execution->getKanbanGroupData($stories, $tasks, $bugs, $type);
$kanbanSetting = $this->execution->getKanbanSetting();
$this->view->title = $this->lang->execution->kanban;
$this->view->position[] = html::a($this->createLink('execution', 'browse', "executionID=$executionID"), $execution->name);
$this->view->position[] = $this->lang->execution->kanban;
$this->view->stories = $stories;
$this->view->realnames = $this->loadModel('user')->getPairs('noletter');
$this->view->storyOrder = $orderBy;
$this->view->orderBy = 'id_asc';
@@ -1870,11 +1863,6 @@ class execution extends control
$this->view->browseType = '';
$this->view->execution = $execution;
$this->view->type = $type;
$this->view->kanbanGroup = $kanbanGroup;
$this->view->kanbanColumns = $this->execution->getKanbanColumns($kanbanSetting);
$this->view->statusMap = $canBeChanged ? $this->execution->getKanbanStatusMap($kanbanSetting) : array();
$this->view->statusList = $this->execution->getKanbanStatusList($kanbanSetting);
$this->view->colorList = $this->execution->getKanbanColorList($kanbanSetting);
$this->view->canBeChanged = $canBeChanged;
$this->display();
+541 -245
View File
@@ -1,253 +1,549 @@
/**
* Get story demo kanban data
* 获取“软件需求”看板演示数据
* @returns {Object} Kanban data
* @todo 该方法作为演示,应该在最终版本中移除
*/
function getStoryKanbanDemoData()
{
/* Define kanban columns 定义看板列 */
var columns =
[
/* 看板列定义数据结构:
id: 列 ID(确保在页面上唯一),
type: 类型,
name: 显示的名称,
color: 列名称颜色,
parentType: 所属的父级列类型
asParent: 是否作为父级列
itemType: 看板条目类型,例如:'story'(需求)
maxCount: 最大数目,该列上允许展示的条目最大数目,如果为 0 表示无限制 */
{id: 'story-blacklog', type: 'blacklog', name: 'Blacklog', color: '', maxCount: 0},
{id: 'story-ready', type: 'ready', name: '准备好', color: '', maxCount: 4},
{id: 'story-dev', type: 'dev', name: '开发', color: '', maxCount: 4, asParent: true},
{id: 'story-dev-doing', type: 'dev-doing', name: '进行中', color: '#126eed', maxCount: 2, parentType: 'dev'},
{id: 'story-dev-done', type: 'dev-done', name: '完成', color: '#2fab8e', maxCount: 2, parentType: 'dev'},
{id: 'story-test', type: 'test', name: '测试', color: '', maxCount: 4, asParent: true},
{id: 'story-test-doing', type: 'test-doing', name: '进行中', color: '#126eed', maxCount: 2, parentType: 'test'},
{id: 'story-test-done', type: 'test-done', name: '完成', color: '#2fab8e', maxCount: 2, parentType: 'test'},
{id: 'story-accepted', type: 'accepted', name: '已验收', color: '', maxCount: 0},
{id: 'story-published', type: 'published', name: '已发布', color: '', maxCount: 0},
];
/* Define items in blacklog column 定义 Blacklog 列上的条目 */
var blacklogColItems =
[
/* 需求数据结构:
id: ID,通常为需求 ID,
title: 名称,
order: 显示顺序,如果指定为 0,则按 ID 倒序排序,
pri: 优先级,
estimate: 预估时间,
assignedTo: 指派给,
deadline: 截止日期 */
{id: 10010, title: '第一条 Blacklog', order: 1, pri: 1, estimate: 2, assignedTo: 'admin', deadline: '2021-10-30'},
{id: 10011, title: '文档模块的实现', order: 2, pri: 2, estimate: 0, assignedTo: 'sunhao'},
{id: 10012, title: '实现软件需求看板视图中不同分组条件的看板展示方式', order: 3, pri: 3, estimate: 10, assignedTo: 'admin'},
{id: 10013, title: 'Blacklog 3', order: 4, pri: 0, estimate: 0, assignedTo: 'sunhao'},
{id: 10014, title: '实现泳道的更多操作菜单', order: 5, pri: 0, estimate: 0, assignedTo: 'sunhao'},
];
/* Define items in ready column 定义 准备好 列上的条目 */
var readyColItems =
[
/* 需求数据结构参见 blacklogColItems 定义 */
{id: 10020, title: '在看板列中实现创建任务功能', order: 10, pri: 1, estimate: 2, assignedTo: 'admin', deadline: '2021-10-30'},
{id: 10021, title: '实现泳道的上下移动', order: 11, pri: 2, estimate: 0, assignedTo: 'sunhao'},
{id: 10024, title: '实现泳道的更多操作菜单', order: 15, pri: 0, estimate: 0, assignedTo: 'sunhao'},
];
var devDoingColItems = /* Define items in dev/doing column 定义 开发/进行中 列上的条目 */
[
/* 需求数据结构参见 blacklogColItems 定义 */
{id: 10030, title: '实现看板的更多操作菜单', order: 10, pri: 3, estimate: 0, assignedTo: 'admin', deadline: '2021-10-30'},
{id: 10031, title: '实现卡片拖动效果功能', order: 11, pri: 2, estimate: 2, assignedTo: 'sunhao'},
];
var devDoneColItems = /* Define items in dev/done column 定义 开发/完成 列上的条目 */
[
/* 需求数据结构参见 blacklogColItems 定义 */
{id: 10040, title: '实现看板列卡片排序功能', order: 10, pri: 1, estimate: 4, assignedTo: 'admin', deadline: '2021-10-30'},
];
var testDoingColItems = /* Define items in test/doing column 定义 测试/进行中 列上的条目 */
[
/* 需求数据结构参见 blacklogColItems 定义 */
{id: 10050, title: '实现看板列的更多操作菜单', order: 10, pri: 3, estimate: 0, assignedTo: 'admin', deadline: '2021-10-30'},
{id: 10051, title: '实现Bug卡片的更多操作功能', order: 11, pri: 2, estimate: 2, assignedTo: 'sunhao'},
];
var testDoneColItems = /* Define items in test/done column 定义 测试/完成 列上的条目 */
[
/* 需求数据结构参见 blacklogColItems 定义 */
{id: 10060, title: '实现任务卡片的更多操作功能', order: 10, pri: 1, estimate: 4, assignedTo: 'admin', deadline: '2021-10-30'},
];
var acceptedColItems =
[
/* 需求数据结构参见 blacklogColItems 定义 */
{id: 10070, title: '实现执行看板的新建按钮组功能', order: 10, pri: 3, estimate: 12, assignedTo: 'admin', deadline: '2021-10-30'},
{id: 10071, title: '在看板列中实现提交Bug功能', order: 11, pri: 2, estimate: 2, assignedTo: 'sunhao'},
{id: 10072, title: '在看板列中实现需求的添加和关联功能', order: 10, pri: 3, estimate: 10, assignedTo: 'admin', deadline: '2021-10-30'},
{id: 10073, title: '实现任务看板视图中不同分组条件的看板展示方式', order: 11, pri: 2, estimate: 2, assignedTo: 'sunhao'},
];
var publishedColItems =
[
/* 需求数据结构参见 blacklogColItems 定义 */
{id: 10080, title: '实现任务看板视图中泳道分组下拉菜单功能', order: 10, pri: 3, estimate: 2, assignedTo: 'admin'},
];
/* Define kanban items in lane 定义看板泳道内每一列的条目,属性名为看板列类型,属性值为条目列表 */
var items =
{
/* 看板列类型: 条目列表 */
blacklog: blacklogColItems,
ready: readyColItems,
'dev-doing': devDoingColItems,
'dev-done': devDoneColItems,
'test-doing': testDoingColItems,
'test-done': testDoneColItems,
accepted: acceptedColItems,
published: publishedColItems,
};
/* Define kanban lanes 定义看板泳道 */
var lanes =
[
/* 泳道数据结构:
id: 泳道 ID,确保页面上唯一,可以为数字或字符串,
name: 名称,
items: 定义每列上的条目
color: 泳道名称背景色
defaultItemType: 看板泳道上的条目默认类型,例如:'story'(需求) */
{id: 'story', name: '软件需求', items: items, color: '#3dc6fc', defaultItemType: 'story'},
]
/* Return kanban data 返回看板数据 */
/* 看板数据结构:
id: ID,确保页面上唯一,可以为数字或字符串,
columns: 定义看板上的所有列,
lanes: 定义看板上的所有泳道
defaultItemType: 看板上的条目默认类型,例如:'story'(需求) */
return {id: 'story', columns: columns, lanes: lanes, defaultItemType: 'story'};
}
/**
* Get bug demo kanban data
* 获取“Bug”看板演示数据
* @returns {Object} Kanban data
* @todo 该方法作为演示,应该在最终版本中移除
*/
function getBugKanbanDemoData()
{
/* Define kanban columns 定义看板列 */
var columns =
[
/* 看板列定义数据结构:
id: 列 ID(确保在页面上唯一),
type: 类型,
name: 显示的名称,
color: 列名称颜色,
parentType: 所属的父级列类型
asParent: 是否作为父级列
itemType: 看板条目类型,例如:'story'(需求)
maxCount: 最大数目,该列上允许展示的条目最大数目,如果为 0 表示无限制 */
{id: 'bug-wait', type: 'wait', name: '待确认', color: '', maxCount: 0},
{id: 'bug-confirmed', type: 'confirmed', name: '已确认', color: '', maxCount: 4},
{id: 'bug-resolving', type: 'resolving', name: '解决中', color: '', maxCount: 4, asParent: true},
{id: 'bug-resolving-doing', type: 'resolving-doing',name: '进行中', color: '#126eed', maxCount: 2, parentType: 'resolving'},
{id: 'bug-resolving-done', type: 'resolving-done', name: '完成', color: '#2fab8e', maxCount: 2, parentType: 'resolving'},
{id: 'bug-test', type: 'test', name: '测试', color: '', maxCount: 4, asParent: true},
{id: 'bug-test-doing', type: 'test-doing', name: '测试中', color: '#126eed', maxCount: 2, parentType: 'test'},
{id: 'bug-test-done', type: 'test-done', name: '测试完毕', color: '#2fab8e', maxCount: 2, parentType: 'test'},
{id: 'bug-closed', type: 'closed', name: '已关闭', color: '', maxCount: 0},
];
/* Define items in wait column 定义 待确认 列上的条目 */
var waitColItems =
[
/* bug 数据结构:
id: ID,通常为 Bug ID,
title: 名称,
order: 显示顺序,如果指定为 0,则按 ID 倒序排序,
pri: 优先级,
severity: 紧急程度,
assignedTo: 指派给 */
{id: 10010, title: '在看板列中实现创建任务功能', order: 10, pri: 1, severity: 2, assignedTo: 'admin'},
{id: 10011, title: '实现泳道的上下移动', order: 11, pri: 2, severity: 1, assignedTo: 'sunhao'},
{id: 10014, title: '实现泳道的更多操作菜单', order: 15, pri: 0, severity: 1, assignedTo: 'sunhao'},
];
/* Define items in confirmed column 定义 已确认 列上的条目 */
var confirmedColItems =
[
/* Bug 数据结构参见 waitColItems 定义 */
{id: 10020, title: '在看板列中实现创建任务功能', order: 10, pri: 1, severity: 2, assignedTo: 'admin'},
{id: 10021, title: '实现泳道的上下移动', order: 11, pri: 2, severity: 1, assignedTo: 'sunhao'},
{id: 10024, title: '实现泳道的更多操作菜单', order: 15, pri: 0, severity: 1, assignedTo: 'sunhao'},
];
var resolvingDoingColItems = /* Define items in resolving/doing column 定义 开发/进行中 列上的条目 */
[
/* Bug 数据结构参见 waitColItems 定义 */
{id: 10030, title: '实现看板的更多操作菜单', order: 10, pri: 3, severity: 1, assignedTo: 'admin'},
{id: 10031, title: '实现卡片拖动效果功能', order: 11, pri: 2, severity: 2, assignedTo: 'sunhao'},
];
var resolvingDoneColItems = /* Define items in resolving/done column 定义 开发/完成 列上的条目 */
[
/* Bug 数据结构参见 waitColItems 定义 */
{id: 10040, title: '实现看板列卡片排序功能', order: 10, pri: 1, severity: 4, assignedTo: 'admin'},
];
var testDoingColItems = /* Define items in test/doing column 定义 测试/进行中 列上的条目 */
[
/* Bug 数据结构参见 waitColItems 定义 */
{id: 10050, title: '实现看板列的更多操作菜单', order: 10, pri: 3, severity: 3, assignedTo: 'admin'},
{id: 10051, title: '实现Bug卡片的更多操作功能', order: 11, pri: 2, severity: 2, assignedTo: 'sunhao'},
];
var testDoneColItems = /* Define items in test/done column 定义 测试/完成 列上的条目 */
[
/* Bug 数据结构参见 waitColItems 定义 */
{id: 10060, title: '实现任务卡片的更多操作功能', order: 10, pri: 1, severity: 4, assignedTo: 'admin'},
];
var closedColItems =
[
/* Bug 数据结构参见 waitColItems 定义 */
{id: 10070, title: '实现执行看板的新建按钮组功能', order: 10, pri: 3, severity: 3, assignedTo: 'admin'},
{id: 10071, title: '在看板列中实现提交Bug功能', order: 11, pri: 2, severity: 2, assignedTo: 'sunhao'},
{id: 10072, title: '在看板列中实现需求的添加和关联功能', order: 10, pri: 3, severity: 3, assignedTo: 'admin'},
{id: 10073, title: '实现任务看板视图中不同分组条件的看板展示方式', order: 11, pri: 2, severity: 2, assignedTo: 'sunhao'},
];
/* Define kanban items in lane 定义看板泳道内每一列的条目,属性名为看板列类型,属性值为条目列表 */
var items =
{
/* 看板列类型: 条目列表 */
wait: waitColItems,
confirmed: confirmedColItems,
'resolving-doing': resolvingDoingColItems,
'resolving-done': resolvingDoneColItems,
'test-doing': testDoingColItems,
'test-done': testDoneColItems,
closed: closedColItems,
};
/* Define kanban lanes 定义看板泳道 */
var lanes =
[
/* 泳道数据结构:
id: 泳道 ID,确保页面上唯一,可以为数字或字符串,
name: 名称,
items: 定义每列上的条目
color: 泳道名称背景色
defaultItemType: 看板泳道上的条目默认类型,例如:'bug'(Bug) */
{id: 'bug', name: 'Bug', items: items, color: '#9c30b0', defaultItemType: 'bug'},
]
/* Return kanban data 返回看板数据 */
/* 看板数据结构:
id: ID,确保页面上唯一,可以为数字或字符串,
columns: 定义看板上的所有列,
lanes: 定义看板上的所有泳道
defaultItemType: 看板上的条目默认类型,例如:'bug'(Bug) */
return {id: 'bug', columns: columns, lanes: lanes, defaultItemType: 'bug'};
}
/**
* Get task demo kanban data
* 获取“任务”看板演示数据
* @returns {Object} Kanban data
* @todo 该方法作为演示,应该在最终版本中移除
*/
function getTaskKanbanDemoData()
{
/* Define kanban columns 定义看板列 */
var columns =
[
/* 看板列定义数据结构:
id: 列 ID(确保在页面上唯一),
type: 类型,
name: 显示的名称,
color: 列名称颜色,
parentType: 所属的父级列类型
asParent: 是否作为父级列
itemType: 看板条目类型,例如:'task'(任务)
maxCount: 最大数目,该列上允许展示的条目最大数目,如果为 0 表示无限制 */
{id: 'task-wait', type: 'wait', name: '未开始', color: '', maxCount: 0},
{id: 'task-dev', type: 'dev', name: '开发', color: '', maxCount: 4, asParent: true},
{id: 'task-dev-doing', type: 'dev-doing', name: '研发中', color: '#126eed', maxCount: 2, parentType: 'dev'},
{id: 'task-dev-done', type: 'dev-done', name: '研发完成', color: '#2fab8e', maxCount: 2, parentType: 'dev'},
{id: 'task-pause', type: 'pause', name: '已暂停', color: '', maxCount: 0},
{id: 'task-cancel', type: 'cancel', name: '已取消', color: '', maxCount: 0},
{id: 'task-closed', type: 'closed', name: '已关闭', color: '', maxCount: 0},
];
/* Define items in wait column 定义 未开始 列上的条目 */
var waitColItems =
[
/* 任务数据结构:
id: ID,通常为任务 ID,
name: 名称,
order: 显示顺序,如果指定为 0,则按 ID 倒序排序,
pri: 优先级,
estimate: 预估时间,
assignedTo: 指派给,
deadline: 截止日期 */
{id: 10020, name: '在看板列中实现创建任务功能', order: 10, pri: 1, estimate: 2, assignedTo: 'admin', deadline: '2021-10-30'},
{id: 10021, name: '实现泳道的上下移动', order: 11, pri: 2, estimate: 0, assignedTo: 'sunhao', deadline: '2021-10-22'},
{id: 10024, name: '实现泳道的更多操作菜单', order: 15, pri: 0, estimate: 0, assignedTo: 'sunhao'},
];
var devDoingColItems = /* Define items in dev/doing column 定义 开发/进行中 列上的条目 */
[
/* 任务数据结构参见 waitColItems 定义 */
{id: 10030, name: '实现看板的更多操作菜单', order: 10, pri: 3, estimate: 0, assignedTo: 'admin', deadline: '2021-10-30'},
{id: 10031, name: '实现卡片拖动效果功能', order: 11, pri: 2, estimate: 2, assignedTo: 'sunhao'},
];
var devDoneColItems = /* Define items in dev/done column 定义 开发/完成 列上的条目 */
[
/* 任务数据结构参见 waitColItems 定义 */
{id: 10040, name: '实现看板列卡片排序功能', order: 10, pri: 1, estimate: 4, assignedTo: 'admin', deadline: '2021-10-30'},
];
var pauseColItems = /* Define items in pause column 定义 已暂停 列上的条目 */
[
/* 任务数据结构参见 waitColItems 定义 */
{id: 10050, name: '实现看板列的更多操作菜单', order: 10, pri: 3, estimate: 0, assignedTo: 'admin', deadline: '2021-10-30'},
{id: 10051, name: '实现Bug卡片的更多操作功能', order: 11, pri: 2, estimate: 2, assignedTo: 'sunhao'},
];
var cancelColItems = /* Define items in cancel column 定义 已取消 列上的条目 */
[
/* 任务数据结构参见 waitColItems 定义 */
{id: 10060, name: '实现任务卡片的更多操作功能', order: 10, pri: 1, estimate: 4, assignedTo: 'admin', deadline: '2021-10-30'},
];
var closedColItems =
[
/* 任务数据结构参见 waitColItems 定义 */
{id: 10070, name: '实现执行看板的新建按钮组功能', order: 10, pri: 3, estimate: 12, assignedTo: 'admin', deadline: '2021-10-30'},
{id: 10071, name: '在看板列中实现提交Bug功能', order: 11, pri: 2, estimate: 2, assignedTo: 'sunhao'},
{id: 10072, name: '在看板列中实现任务的添加和关联功能', order: 10, pri: 3, estimate: 10, assignedTo: 'admin', deadline: '2021-10-30'},
{id: 10073, name: '实现任务看板视图中不同分组条件的看板展示方式', order: 11, pri: 2, estimate: 2, assignedTo: 'sunhao'},
];
/* Define kanban items in lane 定义看板泳道内每一列的条目,属性名为看板列类型,属性值为条目列表 */
var items =
{
/* 看板列类型: 条目列表 */
wait: waitColItems,
'dev-doing': devDoingColItems,
'dev-done': devDoneColItems,
pause: pauseColItems,
cancel: cancelColItems,
closed: closedColItems,
};
/* Define kanban lanes 定义看板泳道 */
var lanes =
[
/* 泳道数据结构:
id: 泳道 ID,确保页面上唯一,可以为数字或字符串,
name: 名称,
items: 定义每列上的条目
color: 泳道名称背景色
defaultItemType: 看板泳道上的条目默认类型,例如:'task'(任务) */
{id: 'task', name: '任务', items: items, color: '#126eed', defaultItemType: 'task'},
]
/* Return kanban data 返回看板数据 */
/* 看板数据结构:
id: ID,确保页面上唯一,可以为数字或字符串,
columns: 定义看板上的所有列,
lanes: 定义看板上的所有泳道
defaultItemType: 看板上的条目默认类型,例如:'task'(任务) */
return {id: 'task', columns: columns, lanes: lanes, defaultItemType: 'task'};
}
/**
* Render user account
* @param {String} userAccount User account
* @returns {string}
*/
function renderUserAvatar(userAccount)
{
var hue = $.zui.strCode(userAccount) * 43 / 360;
return ('<div class="avatar has-text avatar-sm avatar-circle" style="background: hsl(' + hue + ', 40%, 60%);"><span>'
+ userAccount[0].toUpperCase()
+ '</span></div>');
}
/**
* 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 $title = $item.find('.title');
if(!$title.length)
{
$title = $('<a class="title iframe"><i class="icon icon-lightbulb text-muted"></i> <span class="text"></span></a>')
.attr('href', $.createLink('story', 'view', 'storyID=' + item.id));
$title.appendTo($item);
}
$title.attr('title', item.title).find('.text').text(item.title);
var $infos = $item.find('.infos');
if(!$infos.length)
{
$infos = $('<div class="infos"></div>').appendTo($item);
}
$infos.html(
[
'<span class="info info-id text-muted">#' + item.id + '</span>',
'<span class="info info-pri label-pri label-pri-' + item.pri + '" title="' + item.pri + '">' + item.pri + '</span>',
item.estimate ? '<span class="info info-estimate text-muted">' + item.estimate + 'h</span>' : '',
item.assignedTo ? renderUserAvatar(item.assignedTo) : '',
].join(''));
$item.attr('data-type', 'story').addClass('kanban-item-story');
return $item;
}
/**
* Render bug item 提供方法渲染看板中的 Bug 条目
* @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 $title = $item.find('.title');
if(!$title.length)
{
$title = $('<a class="title iframe"><i class="icon icon-bug text-muted"></i> <span class="text"></span></a>')
.attr('href', $.createLink('bug', 'view', 'bugID=' + item.id));
$title.appendTo($item);
}
$title.attr('title', item.title).find('.text').text(item.title);
var $infos = $item.find('.infos');
if(!$infos.length)
{
$infos = $('<div class="infos"></div>').appendTo($item);
}
$infos.html(
[
'<span class="info info-id text-muted">#' + item.id + '</span>',
'<span class="info info-severity label-severity" data-severity="' + item.severity + '" title="' + item.severity + '"></span>',
'<span class="info info-pri label-pri label-pri-' + item.pri + '" title="' + item.pri + '">' + item.pri + '</span>',
item.assignedTo ? renderUserAvatar(item.assignedTo) : '',
].join(''));
$item.attr('data-type', 'bug').addClass('kanban-item-bug');
return $item;
}
/**
* 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 $title = $item.find('.title');
if(!$title.length)
{
$title = $('<a class="title iframe"><i class="icon icon-lightbulb text-muted"></i> <span class="text"></span></a>')
.attr('href', $.createLink('task', 'view', 'taskID=' + item.id));
$title.appendTo($item);
}
$title.attr('title', item.name).find('.text').text(item.name);
var $infos = $item.find('.infos');
if(!$infos.length)
{
$infos = $('<div class="infos"></div>').appendTo($item);
}
$infos.html(
[
'<span class="info info-id text-muted">#' + item.id + '</span>',
'<span class="info info-pri label-pri label-pri-' + item.pri + '" title="' + item.pri + '">' + item.pri + '</span>',
item.estimate ? '<span class="info info-estimate text-muted">' + item.estimate + 'h</span>' : '',
item.assignedTo ? renderUserAvatar(item.assignedTo) : '',
].join(''));
$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 items count
* @param {number} col Column object
* @param {Object} kanban Kanban intance
*/
function renderColumnCount($count, count, col)
{
var text = count + '/' + (!col.maxCount ? '<i class="icon icon-infinite"></i>' : '');
$count.html(text + '<i class="icon icon-arrow-up"></i>');
}
/**
* Updata kanban data
* 更新看板上的数据
* @param {string} kanbanID Kanban id 看板 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 看板 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 = $('<div id="kanban-' + kanbanID + '"></div>').appendTo('#kanbans');
$kanban.kanban($.extend({data: data}, options));
}
/* Example code: */
$(function()
{
var isFirefox = $.zui.browser.firefox;
var adjustBoardsHeight = function()
/* Common options 用于初始化看板的通用选项 */ 
var commonOptions =
{
var $cBoards = $('.c-boards');
var viewHeight = $(window).height() - $('#header').height() - $('#footer').height() - 111;
if ($cBoards.length === 1)
{
var $boardsWrapper = $cBoards.find('.boards-wrapper');
$boardsWrapper.css('min-height', viewHeight);
if($boardsWrapper.height() > $boardsWrapper.find('.boards').height())
{
$boardsWrapper.find('.boards').css(isFirefox ? 'height' : 'min-height', $boardsWrapper.height() - 1);
}
return
}
$cBoards.each(function()
{
var $theBoards = $(this);
var $boardsWrapper = $theBoards.find('.boards-wrapper');
var minHeight = Math.min($theBoards.prev().find('.board-story').outerHeight() + 4, viewHeight);
$boardsWrapper.css({maxHeight: viewHeight, minHeight: minHeight});
if($boardsWrapper.height() > $boardsWrapper.find('.boards').height())
{
var $boards = $boardsWrapper.find('.boards');
$boards.css({maxHeight: $theBoards.height(), minHeight: minHeight});
if ($boards.outerHeight() < minHeight) $boards.css('height', minHeight);
}
});
};
adjustBoardsHeight();
var boardID = '';
var onlybody = config.requestType == 'GET' ? "&onlybody=yes" : "?onlybody=yes";
$.cookie('selfClose', 0, {expires:config.cookieLife, path:config.webRoot});
var $kanban = $('#kanban');
// Get scrollbar width
var getScrollbarWidth = function ()
{
var outer = document.createElement("div");
outer.style.visibility = "hidden";
outer.style.width = "100px";
outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
document.body.appendChild(outer);
var widthNoScroll = outer.offsetWidth;
// force scrollbars
outer.style.overflow = "scroll";
// add innerdiv
var inner = document.createElement("div");
inner.style.width = "100%";
outer.appendChild(inner);
var widthWithScroll = inner.offsetWidth;
// remove divs
outer.parentNode.removeChild(outer);
return widthNoScroll - widthWithScroll;
maxColHeight: 'auto',
minColWidth: 240,
dropable: true,
showCount: true,
showZeroCount: true,
countRender: renderColumnCount
};
var scrollbarWidth = getScrollbarWidth();
var fixBoardWidth = function()
{
var $table = $kanban.children('.table:first');
var kanbanWidth = $table.width();
var $cBoards = $table.find('thead>tr>th.c-board:not(.c-side)');
var boardCount = $cBoards.length;
var $cSide = $table.find('thead>tr>th.c-board.c-side');
var totalWidth = kanbanWidth - scrollbarWidth - 1;
if ($cSide.length) totalWidth = totalWidth - ($cSide.outerWidth() + 5);
var cBoardWidth = Math.floor(totalWidth/boardCount);
$cBoards.not(':last').width(cBoardWidth);
if ($cSide.length) $cBoards.first().width(cBoardWidth + (isFirefox ? 0 : 5));
$kanban.find('.boards > .board').width(cBoardWidth - (isFirefox ? 21 : 22));
};
fixBoardWidth();
/* Create story kanban 创建需求看板 */
createKanban('story', getStoryKanbanDemoData(), commonOptions);
var updateUI = function()
{
fixBoardWidth();
adjustBoardsHeight();
$kanban.data('zui.table').updateFixUI();
};
/* Create bug kanban 创建 Bug 看板 */
createKanban('bug', getBugKanbanDemoData(), commonOptions);
$(window).on('resize', updateUI);
var refresh = function(force)
{
var selfClose = $.cookie('selfClose');
$.cookie('selfClose', 0, {expires:config.cookieLife, path:config.webRoot});
if(selfClose == 1 || force)
{
$kanban.load(location.href + ' #kanban>*', updateUI);
}
};
window.refreshKanban = refresh;
var kanbanModalTrigger = new $.zui.ModalTrigger({type: 'iframe', width: 800});
var dropTo = function(id, from, to, type)
{
if(statusMap[type][from] && statusMap[type][from][to])
{
var method = statusMap[type][from][to];
var link = $.createLink(type, method, 'id=' + id + '&subStatus=' + to);
if(method == 'ajaxChangeSubStatus')
{
$.getJSON(link, function(response)
{
if(response.result == 'fail' && response.message)
{
bootAlert(response.message);
setTimeout(function(){location.reload();}, 1000);
}
});
}
else
{
kanbanModalTrigger.show(
{
url: link + onlybody,
shown: function(){$('.modal-iframe').addClass('with-titlebar').data('cancel-reload', true)},
width: 900,
hidden: refresh
});
}
}
/* Keep the draged element stay in the new place. */
return true;
};
$kanban.droppable(
{
selector: '.board-item:not(.disabled)',
target: function($ele)
{
var itemType = $ele.data('type');
var $board = $ele.closest('.board');
var type = $board.data('type');
return $board.siblings('.board').filter(function()
{
var typeMap = statusMap[itemType];
var actionMap = typeMap && typeMap[type];
return !!actionMap && actionMap[$(this).data('type')];
});
},
start: function(e)
{
$kanban.addClass('dragging');
e.targets.addClass('can-drop-in');
var $item = $(e.element).addClass('dragging');
$item.closest('.boards').addClass('dragging');
},
drag: function(e)
{
var $item = $(e.element);
var $target = $(e.target);
var $holder = $target.find('.board-drag-holder');
if (!$holder.length) $holder = $('<div class="board-drag-holder"></div>').appendTo($target);
$kanban.find('.c-board.dragging').removeClass('dragging');
$kanban.find('.c-board.s-' + $target.data('type')).addClass('dragging');
$holder.height($item.outerHeight());
},
drop: function(e)
{
var result = dropTo(e.element.data('id'), e.element.closest('.board').data('type'), e.target.data('type'), e.element.data('type'));
if(result !== false)
{
e.element.insertBefore(e.target.find('.board-drag-holder'));
}
},
finish: function()
{
$kanban.removeClass('dragging').find('.can-drop-in').removeClass('can-drop-in');
$kanban.find('.dragging').removeClass('dragging');
}
});
$kanban.on('click', '.kanbaniframe', function(e)
{
var $link = $(this);
kanbanModalTrigger.show(
{
url: $link.attr('href'),
shown: function(){$('.modal-iframe').addClass('with-titlebar').data('cancel-reload', true)},
hidden: refresh,
width: $(this).is('.task-assignedTo,.bug-assignedTo') ? 800 : 1100
});
return false;
});
fixKanbanSide($kanban);
/* Create task kanban 创建 任务 看板 */
createKanban('task', getTaskKanbanDemoData(), commonOptions);
});
function fixKanbanSide($kanban)
{
if($kanban.length == 0) return false;
fixSideInit();
$kanban.scroll(fixSide);//Fix kanban side when scrolling.
var tableWidth, kanbanOffset, fixedSide, $fixedSide;
function fixSide()
{
kanbanOffset = $kanban.offset().left;
$fixedSide = $kanban.parent().find('.fixedSide');
if($fixedSide.length <= 0 && kanbanOffset < $kanban.scrollLeft())
{
var $th = $kanban.find('table thead tr th:first');
tableWidth = $th.width();
fixedSide = "<table class='table table-bordered fixedSide'><thead><tr><th class='c-board c-side has-btn'>" + $th.html()+ '</th></tr></thead><tbody>';
$kanban.find('table tbody tr').each(function()
{
var $td = $(this).find('td:first');
fixedSide = fixedSide + "<tr><td class='c-side text-left'>" + $td.html() + '</td></tr>';
});
fixedSide = fixedSide + '</tbody></table>';
$kanban.before(fixedSide);
$('.fixedSide').width(tableWidth);
$('.fixedSide').css('top', $kanban.offset());
/* Reset height. */
var index = 1;
$('.fixedSide tbody tr').each(function()
{
var $td = $kanban.find('table tbody tr:nth-child(' + index + ') td:first');
if($(this).find('td:first div:first').length == 0) $(this).find('td:first').html('<div></div>');
$(this).find('td:first div:first').height($td.height());
index++;
})
}
if($fixedSide.length > 0 && kanbanOffset >= $kanban.scrollLeft()) $fixedSide.remove();
}
function fixSideInit()
{
$fixedSide = $kanban.parent().find('.fixedSide');
if($fixedSide.length > 0) $fixedSide.remove();
fixSide();
}
}
+11 -184
View File
@@ -9,7 +9,8 @@
*/
?>
<?php include '../../common/view/header.html.php';?>
<?php js::set('statusMap', $statusMap);?>
<?php include '../../common/view/kanban.html.php';?>
<div id='mainMenu' class='clearfix'>
<div class='btn-toolbar pull-right'>
<?php echo html::a($this->createLink('execution', 'ajaxKanbanSetting', "executionID=$executionID"), "<i class='icon-cog muted'></i> " . $lang->execution->kanbanSetting, '', "class='iframe btn btn-link'");?>
@@ -46,189 +47,15 @@
<?php endif;?>
</div>
</div>
<style>
<?php foreach($colorList as $status => $color):?>
<?php echo "#kanban .c-board.s-$status{border-color: " . ($color ? $color : '#000') . ";}\n"?>
<?php endforeach?>
</style>
<div id="kanban" class="main-table fade auto-fade-in" data-ride="table" data-checkable="false" data-group="true">
<?php
$hasTask = false;
foreach($kanbanGroup as $group)
{
if(count(get_object_vars($group)) > 0)
{
$hasTask = true;
break;
}
}
?>
<?php if(!$hasTask):?>
<div class="table-empty-tip">
<p>
<span class="text-muted"><?php echo $lang->task->noTask;?></span>
<?php if($canBeChanged and common::hasPriv('task', 'create', $checkObject)):?>
<?php echo html::a($this->createLink('task', 'create', "execution=$executionID" . (isset($moduleID) ? "&storyID=&moduleID=$moduleID" : '')), "<i class='icon icon-plus'></i> " . $lang->task->create, '', "class='btn btn-info'");?>
<?php endif;?>
</p>
<div class='panel'>
<div class='panel-heading'>
<strong>Section</strong>
</div>
<div class='panel-body'>
<div id='kanbans'></div>
</div>
<?php else:?>
<table class="table no-margin table-grouped text-center">
<thead>
<tr>
<th class="c-board c-side has-btn">
<div class="dropdown">
<?php $dropTitle = $type == 'story' ? $lang->execution->orderList[$storyOrder] : $lang->task->$type;?>
<button type="button" data-toggle="dropdown" class="btn btn-block btn-link"><?php echo $dropTitle;?> <span class="caret"></span></button>
<ul class='dropdown-menu text-left'>
<?php foreach($lang->execution->orderList as $key => $value):?>
<li <?php echo ($type == 'story' and $storyOrder == $key) ? " class='active'" : '' ?>>
<?php echo html::a($this->createLink('execution', 'kanban', "executionID=$executionID&type=story&orderBy=$key"), $value);?>
</li>
<?php endforeach;?>
<?php echo "<li" . ($type == 'assignedTo' ? " class='active'" : '') . ">" . html::a(inlink('kanban', "execution=$executionID&type=assignedTo"), $lang->execution->groups['assignedTo']) . "</li>";?>
<?php echo "<li" . ($type == 'finishedBy' ? " class='active'" : '') . ">" . html::a(inlink('kanban', "execution=$executionID&type=finishedBy"), $lang->execution->groups['finishedBy']) . "</li>";?>
</ul>
</div>
</th>
<?php foreach($kanbanColumns as $col):?>
<th class='c-board s-<?php echo $col?>'><?php echo zget($statusList, $col);?></th>
<?php endforeach;?>
</tr>
</thead>
<tbody>
<?php $rowIndex = 0; ?>
<?php foreach($kanbanGroup as $groupKey => $group):?>
<?php if(count(get_object_vars($group)) == 0) continue;?>
<tr data-id='<?php echo $rowIndex++?>'>
<td class='c-side text-left'>
<?php if($groupKey != 'nokey'):?>
<?php if($type == 'story'):?>
<?php $story = $group;?>
<div class='board-story' data-id='<?php echo $story->id;?>'>
<div class='board-title'>
<?php
if(common::hasPriv('story', 'view'))
{
echo html::a($this->createLink('story', 'view', "storyID=$story->id", '', true), $story->title, '', 'class="kanbaniframe group-title" title="' . $story->title . '"');
}
else
{
echo "<span class='group-title' title='{$story->title}'>{$story->title}</span>";
}
?>
<?php if($canBeChanged):?>
<nav class='board-actions nav nav-default'>
<li class='dropdown'>
<a href='javascript:;' data-toggle='dropdown' class='panel-action'><i class='icon icon-ellipsis-v'></i></a>
<ul class='dropdown-menu pull-right'>
<?php
$misc = "data-toggle='modal' data-type='iframe' data-width='95%'";
echo (common::hasPriv('task', 'create')) ? '<li>' . html::a($this->createLink('task', 'create', "executionID=$executionID&storyID=$story->id&moduleID=$story->module", '', true), $lang->execution->wbs, '', $misc) : '' . '</li>';
echo (common::hasPriv('task', 'batchCreate')) ? '<li>' . html::a($this->createLink('task', 'batchCreate', "executionID=$executionID&storyID=$story->id&moduleID=0&taskID=0&iframe=true", '', true), $lang->execution->batchWBS, '', $misc) : '' . '</li>';
echo (common::hasPriv('execution', 'unlinkStory')) ? '<li>' . html::a($this->createLink('execution', 'unlinkStory', "executionID=$executionID&storyID=$story->story&confirm=no", '', true), $lang->execution->unlinkStory, 'hiddenwin') : '' . '</li>';
$misc = "data-toggle='modal' data-type='iframe'";
echo (common::hasPriv('story', 'close')) ? '<li>' . html::a($this->createLink('story', 'close', "storyID=$story->id", '', true), $lang->story->close, '', $misc) : '' . '</li>';
?>
</ul>
</li>
</nav>
<?php endif;?>
</div>
<div class="small group-info">
<span class='story-id board-id' title='<?php echo $lang->story->id?>'>#<?php echo $story->id?></span>
<span class='label-pri label-pri-<?php echo $story->pri?>' title='<?php echo $lang->story->pri?>'><?php echo zget($lang->story->priList, $story->pri);?></span>
<span class='story-stage' title='<?php echo $lang->story->stage?>'><span class="label label-dot"></span> <?php echo $lang->story->stageList[$story->stage];?></span>
<div class='pull-right text-muted story-estimate' title='<?php echo $lang->story->estimate?>'><?php echo $story->estimate . 'h ';?></div>
</div>
</div>
<?php else:?>
<div class='board-story' data-id='<?php echo $groupKey?>'><?php echo zget($realnames, $groupKey);?></div>
<?php endif;?>
<?php endif;?>
</td>
<td class="c-boards no-padding text-left" colspan="<?php echo count($kanbanColumns);?>">
<div class="boards-wrapper">
<div class="boards">
<?php foreach($kanbanColumns as $col):?>
<div class="board" data-type="<?php echo $col;?>">
<?php if(!empty($group->tasks[$col])):?>
<?php foreach($group->tasks[$col] as $task):?>
<?php $disabled = common::hasDBPriv($task, 'task') ? '' : 'disabled';?>
<div class='board-item <?php echo $disabled;?>' data-id='<?php echo $task->id?>' id='task-<?php echo $task->id?>' data-type='task'>
<?php
$childrenAB = $task->parent > 0 ? "<span class='label label-light label-badge'>" . $lang->task->childrenAB . '</span> ' : '';
if(common::hasPriv('task', 'view'))
{
echo html::a($this->createLink('task', 'view', "taskID=$task->id", '', true), "{$childrenAB}{$task->name}", '', 'class="title kanbaniframe" title="' . $task->name . '"');
}
else
{
echo "<span class='title' title='{$task->name}'>{$childrenAB}{$task->name}</span>";
}
?>
<div class='info'>
<?php
$assignedToRealName = "<span>" . zget($realnames, $task->assignedTo) . "</span>";
if(empty($task->assignedTo)) $assignedToRealName = "<span class='text-primary'>{$lang->task->noAssigned}</span>";
if(common::hasPriv('task', 'assignTo', $task))
{
echo html::a($this->createLink('task', 'assignTo', "executionID={$task->execution}&taskID={$task->id}", '', true), '<i class="icon icon-hand-right"></i> ' . $assignedToRealName, '', 'class="btn btn-icon-left kanbaniframe task-assignedTo"');
}
else
{
echo "<span class='btn btn-icon-left task-assignedTo disabled'><i class='icon icon-hand-right'></i> {$assignedToRealName}</span>";
}
?>
<?php if(isset($task->delay)):?>
<span class="status-task status-delayed"> <?php echo $lang->task->delayed;?></span>
<?php endif;?>
<small class="task-left" title='<?php echo $lang->task->left?>'><?php echo $task->left;?>h</small>
</div>
</div>
<?php endforeach?>
<?php endif?>
<?php if(!empty($group->bugs[$col])):?>
<?php foreach($group->bugs[$col] as $bug):?>
<div class='board-item' data-id='<?php echo $bug->id?>' id='bug-<?php echo $bug->id?>' data-type='bug'>
<?php
if(common::hasPriv('bug', 'view'))
{
echo html::a($this->createLink('bug', 'view', "bugID=$bug->id", '', true), "<i class='icon icon-sm icon-bug text-red'></i> #{$bug->id}{$bug->title}", '', 'class="title kanbaniframe" title="' . $bug->title . '"');
}
else
{
echo "<span class='title' title='{$bug->title}'><i class='icon icon-sm icon-bug text-red'></i> #{$bug->id}{$bug->title}</span>";
}
?>
<div class='info'>
<?php
$assignedToRealName = "<span>" . zget($realnames, $bug->assignedTo) . "</span>";
if(empty($bug->assignedTo)) $assignedToRealName = "<span class='text-primary'>{$lang->task->noAssigned}</span>";
if(common::hasPriv('bug', 'assignTo', $bug))
{
echo html::a($this->createLink('bug', 'assignTo', "bugID={$bug->id}", '', true), '<i class="icon icon-hand-right"></i> ' . $assignedToRealName, '', 'class="btn btn-icon-left kanbaniframe bug-assignedTo"');
}
else
{
echo "<span class='btn btn-icon-left bug-assignedTo disabled'><i class='icon icon-hand-right'></i> {$assignedToRealName}</span>";
}
?>
<span class='status-bug status-<?php echo $bug->status;?>' title='<?php echo $lang->bug->status?>'><span class="label label-dot"></span> <?php echo zget($lang->bug->statusList, $bug->status);?></span>
</div>
</div>
<?php endforeach?>
<?php endif?>
</div>
<?php endforeach;?>
</div>
</div>
</td>
</tr>
<?php endforeach;?>
</tbody>
</table>
<?php endif;?>
</div>
<?php echo js::set('executionID', $executionID);?>
<?php js::set('executionID', $executionID);?>
<?php //js::set('statusMap', $statusMap);?>
<?php include '../../common/view/footer.html.php';?>
+8 -2
View File
@@ -59,8 +59,14 @@ class kanbanModel extends model
}
}
foreach($lanes as $laneID => $lane) $lane->columns = $columns[$laneID];
return $lanes;
$kanban = array();
foreach($lanes as $laneID => $lane)
{
$lane->columns = $columns[$laneID];
$kanban[$lane->type] = $lane;
}
return $kanban;
}
/**