diff --git a/module/execution/control.php b/module/execution/control.php index 1d33689ac7..3cb03c47ec 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -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(); diff --git a/module/execution/js/kanban.js b/module/execution/js/kanban.js index 001ec4c5df..d5c2ba7f21 100644 --- a/module/execution/js/kanban.js +++ b/module/execution/js/kanban.js @@ -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 ('
' + + userAccount[0].toUpperCase() + + '
'); +} + +/** + * 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 = $(' ') + .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 = $('
').appendTo($item); + } + $infos.html( + [ + '#' + item.id + '', + '' + item.pri + '', + item.estimate ? '' + item.estimate + 'h' : '', + 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 = $(' ') + .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 = $('
').appendTo($item); + } + $infos.html( + [ + '#' + item.id + '', + '', + '' + item.pri + '', + 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 = $(' ') + .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 = $('
').appendTo($item); + } + $infos.html( + [ + '#' + item.id + '', + '' + item.pri + '', + item.estimate ? '' + item.estimate + 'h' : '', + 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 ? '' : ''); + $count.html(text + ''); +} + +/** + * 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 = $('
').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 = $('
').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 = "'; - $kanban.find('table tbody tr').each(function() - { - var $td = $(this).find('td:first'); - fixedSide = fixedSide + "'; - }); - fixedSide = fixedSide + '
" + $th.html()+ '
" + $td.html() + '
'; - - $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('
'); - $(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(); - } -} diff --git a/module/execution/view/kanban.html.php b/module/execution/view/kanban.html.php index 5db8444efb..1b12366872 100644 --- a/module/execution/view/kanban.html.php +++ b/module/execution/view/kanban.html.php @@ -9,7 +9,8 @@ */ ?> - + + - -
- 0) - { - $hasTask = true; - break; - } - } - ?> - -
-

- task->noTask;?> - - createLink('task', 'create', "execution=$executionID" . (isset($moduleID) ? "&storyID=&moduleID=$moduleID" : '')), " " . $lang->task->create, '', "class='btn btn-info'");?> - -

+ +
+
+ Section +
+
+
- - - - - - - - - - - - - $group):?> - - - - - - - -
- -
- - - -
-
- createLink('story', 'view', "storyID=$story->id", '', true), $story->title, '', 'class="kanbaniframe group-title" title="' . $story->title . '"'); - } - else - { - echo "{$story->title}"; - } - ?> - - - -
-
- #id?> - story->priList, $story->pri);?> - story->stageList[$story->stage];?> -
estimate . 'h ';?>
-
-
- -
- - -
-
-
- -
- tasks[$col])):?> - tasks[$col] as $task):?> - -
- parent > 0 ? "" . $lang->task->childrenAB . ' ' : ''; - 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 "{$childrenAB}{$task->name}"; - } - ?> -
- " . zget($realnames, $task->assignedTo) . ""; - if(empty($task->assignedTo)) $assignedToRealName = "{$lang->task->noAssigned}"; - if(common::hasPriv('task', 'assignTo', $task)) - { - echo html::a($this->createLink('task', 'assignTo', "executionID={$task->execution}&taskID={$task->id}", '', true), ' ' . $assignedToRealName, '', 'class="btn btn-icon-left kanbaniframe task-assignedTo"'); - } - else - { - echo " {$assignedToRealName}"; - } - ?> - delay)):?> - task->delayed;?> - - left;?>h -
-
- - - bugs[$col])):?> - bugs[$col] as $bug):?> -
- createLink('bug', 'view', "bugID=$bug->id", '', true), " #{$bug->id}{$bug->title}", '', 'class="title kanbaniframe" title="' . $bug->title . '"'); - } - else - { - echo " #{$bug->id}{$bug->title}"; - } - ?> -
- " . zget($realnames, $bug->assignedTo) . ""; - if(empty($bug->assignedTo)) $assignedToRealName = "{$lang->task->noAssigned}"; - if(common::hasPriv('bug', 'assignTo', $bug)) - { - echo html::a($this->createLink('bug', 'assignTo', "bugID={$bug->id}", '', true), ' ' . $assignedToRealName, '', 'class="btn btn-icon-left kanbaniframe bug-assignedTo"'); - } - else - { - echo " {$assignedToRealName}"; - } - ?> - bug->statusList, $bug->status);?> -
-
- - -
- -
-
-
-
- + + diff --git a/module/kanban/model.php b/module/kanban/model.php index 97844906f5..9d41ba899b 100644 --- a/module/kanban/model.php +++ b/module/kanban/model.php @@ -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; } /**