Merge branch 'master' of https://gitlab.zcorp.cc/easycorp/zentaopms into sprint/272

This commit is contained in:
田淑杰
2022-12-30 08:46:01 +08:00
53 changed files with 7295 additions and 243 deletions
+82
View File
@@ -0,0 +1,82 @@
<?php
/**
* The ticket entry point of ZenTaoPMS.
*
* @copyright Copyright 2009-2021 青岛易软天创网络科技有限公司(QingDao Nature Easy Soft Network Technology Co,LTD, www.cnezsoft.com)
* @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Chunsheng Wang <chunsheng@cnezsoft.com>
* @package entries
* @version 1
* @link http://www.zentao.net
*/
class ticketEntry extends entry
{
/**
* GET method.
*
* @param int $ticketID
* @access public
* @return string
*/
public function get($ticketID)
{
$control = $this->loadController('ticket', 'view');
$control->view($ticketID);
$data = $this->getData();
$ticket = $data->data->ticket;
$ticket->productName = $data->data->product;
$ticket->moduleName = isset($data->data->modulePath[0]->name) ? $data->data->modulePath[0]->name : '/';
if(!$data or !isset($data->status)) return $this->send400('error');
if(isset($data->status) and $data->status == 'fail') return $this->sendError(zget($data, 'code', 400), $data->message);
$ticket->actions = $this->loadModel('action')->processActionForAPI($data->data->actions, $data->data->users, $this->lang->ticket);
return $this->send(200, $this->format($ticket, 'activatedDate:time,openedBy:user,openedDate:time,assignedTo:user,assignedDate:time,mailto:userList,resolvedBy:user,resolvedDate:time,closedBy:user,closedDate:time,lastEditedBy:user,lastEditedDate:time,deadline:date,deleted:bool'));
}
/**
* PUT method.
*
* @param int $ticketID
* @access public
* @return string
*/
public function put($ticketID)
{
$oldTicket = $this->loadModel('ticket')->getById($ticketID);
$fields = 'module,product,type,openedBuild,assignedTo,deadline,title,desc,status,notify,uid';
$this->batchSetPost($fields, $oldTicket);
$control = $this->loadController('ticket', 'edit');
$control->edit($ticketID);
$data = $this->getData();
if(isset($data->result) and $data->result == 'fail') return $this->sendError(400, $data->message);
if(!isset($data->result)) return $this->sendError(400, 'error');
$ticket = $this->ticket->getByID($ticketID);
return $this->send(200, $this->format($ticket, 'openedBy:user,openedDate:time,activatedBy:user,activatedDate:time,finishedBy:user,finishedDate:time,closedBy:user,closedDate:time,editedBy:user,editedDate:time,deadline:time,assignedTo:user,mailto:userList,deleted:bool'));
}
/**
* DELETE method.
*
* @param int $ticketID
* @access public
* @return string
*/
public function delete($ticketID)
{
$control = $this->loadController('ticket', 'delete');
$control->delete($ticketID, 'yes');
$this->getData();
return $this->sendSuccess(200, 'success');
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
/**
* The ticketsEntry entry point of ZenTaoPMS.
*
* @copyright Copyright 2009-2021 青岛易软天创网络科技有限公司(QingDao Nature Easy Soft Network Technology Co,LTD, www.cnezsoft.com)
* @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Chunsheng Wang <chunsheng@cnezsoft.com>
* @package entries
* @version 1
* @link http://www.zentao.net
*/
class ticketsEntry extends entry
{
/**
* GET method.
*
* @access public
* @return string
*/
public function get()
{
if(strpos(strtolower($this->param('fields')), 'moduleandproduct') !== false) return $this->getModuleAndProduct();
$control = $this->loadController('ticket', 'browse');
$control->browse($this->param('status', 'wait'), 0, $this->param('orderBy', 'id_desc'), 0, $this->param('limit', 20), $this->param('page', 1));
$data = $this->getData();
if(!$data or !isset($data->status)) return $this->sendError(400, 'error');
if(isset($data->status) and $data->status == 'fail') return $this->sendError(400, $data->message);
$tickets = $data->data->tickets;
$pager = $data->data->pager;
$result = array();
foreach($tickets as $ticket)
{
$result[] = $this->format($ticket, 'openedBy:user,openedDate:time,activatedBy:user,activatedDate:time,finishedBy:user,finishedDate:time,closedBy:user,closedDate:time,editedBy:user,editedDate:time,deadline:time,assignedTo:user,mailto:userList,deleted:bool');
}
$data = array();
$data['page'] = $pager->pageID;
$data['total'] = $pager->recTotal;
$data['limit'] = $pager->recPerPage;
$data['tickets'] = $result;
return $this->send(200, $data);
}
/**
* POST method.
*
* @access public
* @return string
*/
public function post()
{
$fields = 'module,product,type,openedBuild,assignedTo,deadline,title,desc,status,notify,uid';
$this->batchSetPost($fields);
$control = $this->loadController('ticket', 'create');
$this->requireFields('title,product,module');
$control->create();
$data = $this->getData();
if(isset($data->result) and $data->result == 'fail') return $this->sendError(400, $data->message);
$ticket = $this->loadModel('ticket')->getByID($data->id);
return $this->send(201, $this->format($ticket, 'openedBy:user,openedDate:time,activatedBy:user,activatedDate:time,finishedBy:user,finishedDate:time,closedBy:user,closedDate:time,editedBy:user,editedDate:time,deadline:time,assignedTo:user,mailto:userList,deleted:bool'));
}
/**
* GET method.
*
* @access public
* @return string
*/
public function getModuleAndProduct()
{
$control = $this->loadController('ticket', 'create');
$control->create();
$data = $this->getData();
$modules = $data->data->modules;
$products = $data->data->products;
$data = array();
$data['modules'] = $modules;
$data['products'] = $products;
return $this->send(200, $data);
}
}
+5
View File
@@ -21,6 +21,11 @@ $routes['/feedbacks/:id'] = 'feedback';
$routes['/feedbacks/:id/assign'] = 'feedbackAssignto';
$routes['/feedbacks/:id/close'] = 'feedbackClose';
$routes['/tickets'] = 'tickets';
$routes['/tickets/:id'] = 'ticket';
$routes['/tickets/:id/assign'] = 'ticketAssignto';
$routes['/tickets/:id/close'] = 'ticketClose';
$routes['/options/:type'] = 'options';
$routes['/configurations'] = 'configs';
+304 -77
View File
File diff suppressed because one or more lines are too long
+1068 -10
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
a:1:{i:0;O:8:"stdClass":12:{s:2:"id";s:2:"14";s:3:"lib";s:2:"24";s:4:"name";s:4:"user";s:4:"type";s:4:"json";s:4:"desc";s:0:"";s:7:"version";s:1:"2";s:9:"attribute";s:537:"[{"field":"id","paramsType":"int","required":"","desc":"","structType":"json","sub":1,"key":"kwoq04srulxevt93a3h","children":[]},{"field":"account","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0a387on1wpuwntd","children":[]},{"field":"realname","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0i4g80qyu8lqp28","children":[]},{"field":"avatar","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0le57o2p6vxc8ov","children":[]}]";s:7:"addedBy";s:5:"admin";s:9:"addedDate";s:19:"2022-06-28 09:51:30";s:8:"editedBy";s:5:"admin";s:10:"editedDate";s:19:"2022-06-28 09:51:30";s:7:"deleted";s:1:"0";}}
a:1:{i:0;O:8:"stdClass":12:{s:2:"id";s:1:"1";s:3:"lib";s:3:"833";s:4:"name";s:4:"user";s:4:"type";s:4:"json";s:4:"desc";s:0:"";s:7:"version";s:1:"2";s:9:"attribute";s:537:"[{"field":"id","paramsType":"int","required":"","desc":"","structType":"json","sub":1,"key":"kwoq04srulxevt93a3h","children":[]},{"field":"account","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0a387on1wpuwntd","children":[]},{"field":"realname","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0i4g80qyu8lqp28","children":[]},{"field":"avatar","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0le57o2p6vxc8ov","children":[]}]";s:7:"addedBy";s:6:"wwccss";s:9:"addedDate";s:19:"2022-12-22 10:35:42";s:8:"editedBy";s:6:"wwccss";s:10:"editedDate";s:19:"2022-12-22 10:35:42";s:7:"deleted";s:1:"0";}}
+1 -1
View File
@@ -1 +1 @@
a:3:{i:0;O:8:"stdClass":8:{s:2:"id";s:2:"21";s:4:"name";s:4:"user";s:4:"type";s:4:"json";s:4:"desc";s:0:"";s:9:"attribute";s:537:"[{"field":"id","paramsType":"int","required":"","desc":"","structType":"json","sub":1,"key":"kwoq04srulxevt93a3h","children":[]},{"field":"account","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0a387on1wpuwntd","children":[]},{"field":"realname","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0i4g80qyu8lqp28","children":[]},{"field":"avatar","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0le57o2p6vxc8ov","children":[]}]";s:7:"version";s:1:"1";s:7:"addedBy";s:5:"admin";s:9:"addedDate";s:19:"2022-06-28 09:51:30";}i:1;O:8:"stdClass":8:{s:2:"id";s:2:"20";s:4:"name";s:3:"aaa";s:4:"type";s:8:"formData";s:4:"desc";s:0:"";s:9:"attribute";s:137:"[{"field":"aaa","paramsType":"object","required":"","desc":"","structType":"formData","sub":1,"key":"l11gznwwu2chih9mc28","children":[]}]";s:7:"version";s:1:"1";s:7:"addedBy";s:5:"admin";s:9:"addedDate";s:19:"2022-06-28 09:51:30";}i:2;O:8:"stdClass":8:{s:2:"id";s:2:"19";s:4:"name";s:4:"user";s:4:"type";s:4:"json";s:4:"desc";s:0:"";s:9:"attribute";s:537:"[{"field":"id","paramsType":"int","required":"","desc":"","structType":"json","sub":1,"key":"kwoq04srulxevt93a3h","children":[]},{"field":"account","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0a387on1wpuwntd","children":[]},{"field":"realname","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0i4g80qyu8lqp28","children":[]},{"field":"avatar","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0le57o2p6vxc8ov","children":[]}]";s:7:"version";s:1:"2";s:7:"addedBy";s:5:"admin";s:9:"addedDate";s:19:"2022-06-28 09:51:30";}}
a:3:{i:0;O:8:"stdClass":8:{s:2:"id";s:1:"1";s:4:"name";s:4:"user";s:4:"type";s:4:"json";s:4:"desc";s:0:"";s:9:"attribute";s:537:"[{"field":"id","paramsType":"int","required":"","desc":"","structType":"json","sub":1,"key":"kwoq04srulxevt93a3h","children":[]},{"field":"account","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0a387on1wpuwntd","children":[]},{"field":"realname","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0i4g80qyu8lqp28","children":[]},{"field":"avatar","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0le57o2p6vxc8ov","children":[]}]";s:7:"version";s:1:"1";s:7:"addedBy";s:6:"wwccss";s:9:"addedDate";s:19:"2022-12-22 10:35:42";}i:1;O:8:"stdClass":8:{s:2:"id";s:1:"2";s:4:"name";s:3:"aaa";s:4:"type";s:8:"formData";s:4:"desc";s:0:"";s:9:"attribute";s:137:"[{"field":"aaa","paramsType":"object","required":"","desc":"","structType":"formData","sub":1,"key":"l11gznwwu2chih9mc28","children":[]}]";s:7:"version";s:1:"1";s:7:"addedBy";s:6:"wwccss";s:9:"addedDate";s:19:"2022-12-22 10:35:42";}i:2;O:8:"stdClass":8:{s:2:"id";s:1:"3";s:4:"name";s:4:"user";s:4:"type";s:4:"json";s:4:"desc";s:0:"";s:9:"attribute";s:537:"[{"field":"id","paramsType":"int","required":"","desc":"","structType":"json","sub":1,"key":"kwoq04srulxevt93a3h","children":[]},{"field":"account","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0a387on1wpuwntd","children":[]},{"field":"realname","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0i4g80qyu8lqp28","children":[]},{"field":"avatar","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0le57o2p6vxc8ov","children":[]}]";s:7:"version";s:1:"2";s:7:"addedBy";s:6:"wwccss";s:9:"addedDate";s:19:"2022-12-22 10:35:42";}}
+1 -1
View File
File diff suppressed because one or more lines are too long
+3758
View File
File diff suppressed because one or more lines are too long
+1476
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
a:1:{i:0;O:8:"stdClass":12:{s:2:"id";s:1:"1";s:3:"lib";s:3:"833";s:4:"name";s:4:"user";s:4:"type";s:4:"json";s:4:"desc";s:0:"";s:7:"version";s:1:"2";s:9:"attribute";s:537:"[{"field":"id","paramsType":"int","required":"","desc":"","structType":"json","sub":1,"key":"kwoq04srulxevt93a3h","children":[]},{"field":"account","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0a387on1wpuwntd","children":[]},{"field":"realname","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0i4g80qyu8lqp28","children":[]},{"field":"avatar","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0le57o2p6vxc8ov","children":[]}]";s:7:"addedBy";s:6:"wwccss";s:9:"addedDate";s:19:"2022-12-22 10:35:42";s:8:"editedBy";s:6:"wwccss";s:10:"editedDate";s:19:"2022-12-22 10:35:42";s:7:"deleted";s:1:"0";}}
+1
View File
@@ -0,0 +1 @@
a:3:{i:0;O:8:"stdClass":8:{s:2:"id";s:1:"1";s:4:"name";s:4:"user";s:4:"type";s:4:"json";s:4:"desc";s:0:"";s:9:"attribute";s:537:"[{"field":"id","paramsType":"int","required":"","desc":"","structType":"json","sub":1,"key":"kwoq04srulxevt93a3h","children":[]},{"field":"account","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0a387on1wpuwntd","children":[]},{"field":"realname","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0i4g80qyu8lqp28","children":[]},{"field":"avatar","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0le57o2p6vxc8ov","children":[]}]";s:7:"version";s:1:"1";s:7:"addedBy";s:6:"wwccss";s:9:"addedDate";s:19:"2022-12-22 10:35:42";}i:1;O:8:"stdClass":8:{s:2:"id";s:1:"2";s:4:"name";s:3:"aaa";s:4:"type";s:8:"formData";s:4:"desc";s:0:"";s:9:"attribute";s:137:"[{"field":"aaa","paramsType":"object","required":"","desc":"","structType":"formData","sub":1,"key":"l11gznwwu2chih9mc28","children":[]}]";s:7:"version";s:1:"1";s:7:"addedBy";s:6:"wwccss";s:9:"addedDate";s:19:"2022-12-22 10:35:42";}i:2;O:8:"stdClass":8:{s:2:"id";s:1:"3";s:4:"name";s:4:"user";s:4:"type";s:4:"json";s:4:"desc";s:0:"";s:9:"attribute";s:537:"[{"field":"id","paramsType":"int","required":"","desc":"","structType":"json","sub":1,"key":"kwoq04srulxevt93a3h","children":[]},{"field":"account","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0a387on1wpuwntd","children":[]},{"field":"realname","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0i4g80qyu8lqp28","children":[]},{"field":"avatar","paramsType":"string","required":"","desc":"","structType":"json","sub":1,"key":"kwoq0le57o2p6vxc8ov","children":[]}]";s:7:"version";s:1:"2";s:7:"addedBy";s:6:"wwccss";s:9:"addedDate";s:19:"2022-12-22 10:35:42";}}
+1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -53,6 +53,9 @@ else
$lang->task->common = 'Task';
$lang->dashboard = isset($lang->dashboard->common) ? $lang->dashboard->common : $lang->dashboard;
/* Ticket. */
if($config->edition != 'open') $lang->feedback->menu->ticket = array('link' => 'Ticket|ticket|browse|browseType=unclosed');
/* My menu. */
$lang->my->menu = new stdclass();
$lang->my->menu->index = array('link' => "$lang->dashboard|my|index");
@@ -53,6 +53,9 @@ else
$lang->task->common = 'Task';
$lang->dashboard = isset($lang->dashboard->common) ? $lang->dashboard->common : $lang->dashboard;
/* Ticket. */
if($config->edition != 'open') $lang->feedback->menu->ticket = array('link' => 'Ticket|ticket|browse|browseType=unclosed');
/* My menu. */
$lang->my->menu = new stdclass();
$lang->my->menu->index = array('link' => "$lang->dashboard|my|index");
@@ -53,6 +53,9 @@ else
$lang->task->common = 'Task';
$lang->dashboard = isset($lang->dashboard->common) ? $lang->dashboard->common : $lang->dashboard;
/* Ticket. */
if($config->edition != 'open') $lang->feedback->menu->ticket = array('link' => 'Ticket|ticket|browse|browseType=unclosed');
/* My menu. */
$lang->my->menu = new stdclass();
$lang->my->menu->index = array('link' => "$lang->dashboard|my|index");
@@ -53,6 +53,10 @@ else
$lang->task->common = '任务';
$lang->dashboard = isset($lang->dashboard->common) ? $lang->dashboard->common : $lang->dashboard;
/* Ticket. */
if($config->edition != 'open') $lang->feedback->menu->ticket = array('link' => '工单|ticket|browse|browseType=unclosed');
/* My menu. */
$lang->my->menu = new stdclass();
$lang->my->menu->index = array('link' => "$lang->dashboard|my|index");
@@ -984,6 +984,21 @@ if($config->edition == 'biz' or $config->edition == 'max')
$lang->resource->faq = new stdclass();
$lang->resource->faq->browse = 'browse';
/* Ticket */
$lang->resource->ticket = new stdclass();
$lang->resource->ticket->index = 'index';
$lang->resource->ticket->create = 'create';
$lang->resource->ticket->edit = 'edit';
$lang->resource->ticket->view = 'view';
$lang->resource->ticket->browse = 'browse';
$lang->resource->ticket->assignTo = 'assign';
$lang->resource->ticket->close = 'close';
$lang->resource->ticket->activate = 'activate';
$lang->resource->ticket->delete = 'delete';
$lang->resource->ticket->exportTemplate = 'exportTemplate';
$lang->resource->ticket->import = 'import';
$lang->resource->ticket->export = 'exportAction';
/* Attend */
$lang->resource->attend = new stdclass();
$lang->resource->attend->department = 'department';
+61
View File
@@ -0,0 +1,61 @@
<?php
/**
* Build ticket browse menu.
*
* @param object $ticketID
* @access public
* @return string
*/
public function buildOperateBrowseMenu($ticketID)
{
$ticket = $this->getByID($ticketID);
$menu = '';
$params = "ticket=$ticketID";
$disabled = '';
if($ticket->status == 'done')
{
$menu .= $this->buildMenu('ticket', 'close', $params, $ticket, 'browse', 'off', 'hiddenwin', '', '', '', $this->lang->ticket->close);
}
else
{
$menu .= $this->buildMenu('ticket', 'close', $params, $ticket, 'browse', 'off', '', 'iframe', true, '', $this->lang->ticket->close);
}
$menu .= $this->buildMenu('ticket', 'edit', $params, $ticket, 'browse', 'edit', '', '', '', '', $this->lang->ticket->edit);
return $menu;
}
/**
* Build ticket view menu.
*
* @param int $ticketID
* @access public
* @return string
*/
public function buildOperateViewMenu($ticketID)
{
$ticket = $this->getByID($ticketID);
if($ticket->deleted) return '';
$menu = '';
$params = "ticket=$ticket->id";
if($ticket->status != 'closed') $menu .= $this->loadModel('effort')->createAppendLink('ticket', $ticketID);
if($ticket->status == 'done')
{
$menu .= $this->buildMenu('ticket', 'close', $params, $ticket, 'view', 'off', 'hiddenwin', '', '', '', $this->lang->ticket->close);
}
else
{
$menu .= $this->buildMenu('ticket', 'close', $params, $ticket, 'view', 'off', '', 'iframe', true, '', $this->lang->ticket->close);
}
$menu .= $this->buildMenu('ticket', 'activate', $params, $ticket, 'view', 'magic', '', "iframe", true, '', $this->lang->ticket->activate);
$menu .= $this->buildMenu('ticket', 'edit', $params, $ticket, 'view', 'edit', '', '', '', '', $this->lang->ticket->edit);
$menu .= $this->buildMenu('ticket', 'delete', $params, $ticket, 'view', 'trash', 'hiddenwin');
return $menu;
}
@@ -0,0 +1,3 @@
<script>
$('#sidebar .cell > div').addClass('hidden')
</script>
@@ -0,0 +1,20 @@
<script>
/**
* Load modules by product.
*
* @param int productID
*
* @access public
* @return void
*/
function loadModules(productID)
{
link = createLink('tree', 'ajaxGetOptionMenu', 'productID=' + productID + '&viewtype=ticket&branch=all&rootModuleID=0&returnType=html&fieldID=&needManage=false');
$('#moduleBox').load(link, function(data)
{
var $inputGroup = $(this);
$inputGroup.find('select').chosen()
$inputGroup.prepend("<span class='input-group-addon'>" + moduleLang + "</span>");
});
}
</script>
+1
View File
@@ -49,6 +49,7 @@ html[lang='de'] #keywordsAddonLabel, .task {width: 73px;}
#osBox {width: 190px;}
#projectBox .required:after {right: 1px;}
#branch_chosen {min-width: 90px;}
#branch_chosen.chosen-container-active .chosen-single{border-left-color: #2e7fff !important;}
#notifyEmailTd .input-group-addon, #deadlineTd .input-group-addon {border-radius: 2px 0px 2px 0px !important;}
#feedback.input-group + #notifyEmailLabel {position: relative; width: 50%; left: 48%;}
+6 -1
View File
@@ -3713,8 +3713,13 @@ class bugModel extends model
/* Get related objects title or names. */
$table = $this->config->objectTables[$object];
if($table) $relatedObjects = $this->dao->select($pairs)->from($table)->where('id')->in($relatedObjectIdList)->fetchPairs();
if($object == 'branch' and $this->session->currentProductType != 'normal')
{
$productID = current($bugs)->product;
$relatedObjects = $this->dao->select($pairs)->from($table)->where('product')->eq($productID)->fetchPairs();
}
if(in_array($object, array('build','resolvedBuild'))) $relatedObjects= array('trunk' => $this->lang->trunk) + $relatedObjects;
if(in_array($object, array('build','resolvedBuild','branch'))) $relatedObjects = array('trunk' => $this->lang->trunk) + $relatedObjects;
return array('' => '', 0 => '') + $relatedObjects;
}
+4 -4
View File
@@ -781,11 +781,11 @@ class build extends control
*/
protected function unlinkOldBranch($changes)
{
foreach($changes as $buildID => $changes)
foreach($changes as $buildID => $changeList)
{
$oldBranch = '';
$newBranch = '';
foreach($changes as $changeId => $change)
foreach($changeList as $changeId => $change)
{
if($change['field'] == 'branch')
{
@@ -795,8 +795,8 @@ class build extends control
}
}
$build = $this->build->getByID($buildID);
$planStories = $this->loadModel('story')->getByList($build->allStories);
$planBugs = $this->loadModel('bug')->getByList($build->allBugs);
$planStories = $build->allStories ? $this->loadModel('story')->getByList($build->allStories) : '';
$planBugs = $build->allBugs ? $this->loadModel('bug')->getByList($build->allBugs) : '';
if($oldBranch)
{
foreach($planStories as $storyID => $story)
+2 -2
View File
@@ -2029,7 +2029,7 @@ class execution extends control
$productPlans = array(0 => '');
$linkedBranches = array();
$linkedBranchList = array();
$linkedProducts = $this->product->getProducts($executionID, 'all', '', true, $linkedProductIdList);
$linkedProducts = $this->product->getProducts($executionID, 'all', '', true, $linkedProductIdList, false);
$plans = $this->productplan->getGroupByProduct(array_keys($linkedProducts), 'skipParent|unexpired');
$executionStories = $this->project->getStoriesByProject($executionID);
@@ -2039,7 +2039,7 @@ class execution extends control
$linkedStoryIDList = array();
foreach($linkedProducts as $productID => $linkedProduct)
{
if(!isset($allProducts[$productID])) $allProducts[$productID] = $linkedProduct->name;
if(!isset($allProducts[$productID])) $allProducts[$productID] = $linkedProduct->deleted ? $linkedProduct->name . "({$this->lang->product->deleted})" : $linkedProduct->name;
$productPlans[$productID] = array();
foreach($branches[$productID] as $branchID => $branch)
+1 -1
View File
@@ -810,7 +810,7 @@ class kanbanModel extends model
}
elseif($branchID == 'all')
{
$branches = $this->branch->getPairs($product->id);
$branches = $this->branch->getPairs($product->id, 'active');
}
elseif($branchID == BRANCH_MAIN)
{
+8 -5
View File
@@ -382,16 +382,17 @@ class productModel extends model
* @param int $projectID
* @param string $status all|noclosed
* @param string|array $append
* @param bool $noDeleted
* @access public
* @return array
*/
public function getProductPairsByProject($projectID = 0, $status = 'all', $append = '')
public function getProductPairsByProject($projectID = 0, $status = 'all', $append = '', $noDeleted = true)
{
$products = empty($projectID) ? $this->getList(0, 'all', 0, 0, 'all') : $this->getProducts($projectID, $status, '', true, $append);
$products = empty($projectID) ? $this->getList(0, 'all', 0, 0, 'all') : $this->getProducts($projectID, $status, '', true, $append, $noDeleted);
$pairs = array();
if(!empty($products))
{
foreach($products as $product) $pairs[$product->id] = $product->name;
foreach($products as $product) $pairs[$product->id] = $product->deleted ? $product->name . "({$this->lang->product->deleted})" : $product->name;
}
return $pairs;
@@ -424,10 +425,11 @@ class productModel extends model
* @param string $orderBy
* @param bool $withBranch
* @param string|array $append
* @param bool $noDeleted
* @access public
* @return array
*/
public function getProducts($projectID = 0, $status = 'all', $orderBy = '', $withBranch = true, $append = '')
public function getProducts($projectID = 0, $status = 'all', $orderBy = '', $withBranch = true, $append = '', $noDeleted = true)
{
if(defined('TUTORIAL'))
{
@@ -441,7 +443,8 @@ class productModel extends model
$projectProducts = $this->dao->select("t1.branch, t1.plan, t2.*")
->from(TABLE_PROJECTPRODUCT)->alias('t1')
->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id')
->where('t2.deleted')->eq(0)
->where('1=1')
->beginIF($noDeleted)->andWhere('t2.deleted')->eq(0)->fi()
->beginIF(!empty($projectID))->andWhere('t1.project')->in($projectID)->fi()
->beginIF(!$this->app->user->admin and $this->config->vision == 'rnd')->andWhere('t2.id')->in($views)->fi()
->andWhere('t2.vision')->eq($this->config->vision)
+2 -1
View File
@@ -358,7 +358,7 @@ class productplan extends control
else
{
$planGroup = $this->productplan->getGroupByProduct($product->id, 'skipParent', '', $orderBy);
$branches = $this->branch->getPairs($product->id);
$branches = $this->branch->getPairs($product->id, 'active');
foreach($branches as $id => $name)
{
@@ -383,6 +383,7 @@ class productplan extends control
$this->view->browseType = $browseType;
$this->view->viewType = $viewType;
$this->view->orderBy = $orderBy;
$this->view->users = $this->loadModel('user')->getPairs('noletter|nodeleted');
$this->view->plans = $this->productplan->getList($productID, $branch, $browseType, $pager, $sort, "", $queryID);
$this->view->pager = $pager;
$this->view->projects = $this->product->getProjectPairsByProduct($productID, $branch, '', $status = 'closed', 'multiple');
+16
View File
@@ -36,8 +36,24 @@ td.c-branch {overflow: hidden; text-align: left !important; white-space: nowrap;
.switchButton {background: #fff !important;}
.panel-actions {position: relative; padding: 0 0; padding-top: 1px;}
#kanbanContainer {background: #efefef; box-shadow: none;}
#kanbanContainer {padding-bottom: 0; margin-bottom: 0; border: unset;}
#kanbanContainer.fullscreen {overflow: auto;}
.icon-list {padding-left: 7px;}
.titleBox .icon-delay {padding-right: 8px;}
.kanban-card .productplanInfo .user {float: right;display: block;}
.kanban-card a[disabled] {color: #313c52;}
.kanban-card .header .actions {position: absolute;top: 4px;right: 4px;opacity: 0;}
.kanban-card .productplanDesc {min-width: 100%;color: #838a9d;overflow: hidden;white-space: nowrap;text-overflow: clip;}
.kanban-card {height: auto !important;padding: 8px 14px !important;min-height: 60px;}
.icon-kanban {padding-left: 7px; font-size: 16px;}
.label-wait {background: #EFEFEF !important;color: #838A9D;}
.label-future {background: #EFEFEF !important;color: #838A9D;border-radius: 9px;}
.label-doing {background: #f8d2d2 !important;color: #e64b4c;}
.label-done {background: #dfe9d8 !important;color: #429b16;}
.label-closed, .label-terminate {background: #e5e5e5 !important;color: #b8b8b8;}
#productplanList thead th.c-title {width: 100%;}
#productplanList .c-actions {width: 265px;}
#productplanList .c-actions .btn+.btn {margin-left: -1px;}
+86 -38
View File
@@ -365,8 +365,39 @@ function renderKanbanItem(item, $item)
if(privs.includes('view')) $title = $('<a class="title"></a>').appendTo($titleBox).attr('href', createLink(rawModule, 'view', 'cardID=' + item.id));
if(!privs.includes('view')) $title = $('<a class="title"></a>').appendTo($titleBox);
}
if(!$title.children('i').length)
{
$(function() {$title.prepend('<i class="icon icon-delay"></i>');})
}
$title.text(item.title).attr('title', item.title);
var $info = $item.children('.productplanInfo');
if(!$info.length) $info = $(
[
'<div class="productplanInfo">',
'</div>'
].join('')).appendTo($item);
/* Output plan desc information. */
var $descBox = $item.children('.productplanDesc');
if(!$descBox.length)
{
$descBox = $('<div class="productplanDesc c-name cardDesc" title="' + item.desc + '">' + item.desc + '</div>').appendTo($info);
}
var $statusBox = $item.children('.productplanStatus');
if(!$statusBox.length)
{
if(item.deleted == '0')
{
$statusBox = $('<span class="productplanStatus label label-' + item.status + '">' + productplanLang.statusList[item.status] + '</span>').appendTo($info);
}
else
{
$statusBox = $('<span class="productplanStatus label label-deleted">' + productplanLang.deleted + '</span>').appendTo($info);
}
}
/* Determine whether to print an expired label. */
var today = new Date();
var begin = $.zui.createDate(item.begin);
@@ -392,54 +423,71 @@ function renderKanbanItem(item, $item)
].join('')).appendTo($header);
}
/* Output plan date information. */
var $dateBox = $item.children('.dateBox');
if(!$dateBox.length) $dateBox = $(
[
'<div class="dateBox">',
'<span class="time label label-outline"></span>',
'</div>'
].join('')).appendTo($item);
/* Display date of product plan. */
var $date = $info.children('.date');
var begin = $.zui.createDate(item.begin);
var end = $.zui.createDate(item.end);
var today = new Date();
var labelType = (item.begin <= $.zui.formatDate(today, 'yyyy-MM-dd') && item.end >= $.zui.formatDate(today, 'yyyy-MM-dd')) ? 'danger' : 'wait';
var labelTitle = $.zui.formatDate(begin, 'MM-dd') + ' ' + productplanLang.to + ' ' + $.zui.formatDate(end, 'MM-dd');
var $time = $dateBox.children('.time');
var beginTimeShort = $.zui.formatDate(begin, 'MM-dd');
var beginTimeLong = $.zui.formatDate(begin, 'yyyy-MM-dd');
var endTimeShort = $.zui.formatDate(end, 'MM-dd');
var endTimeLong = $.zui.formatDate(end, 'yyyy-MM-dd')
var to = productplanLang.to;
var undetermined = productplanLang.future;
var undetermindeDate = '2030-01-01';
if(item.begin != undetermindeDate && item.end != undetermindeDate)
if((item.begin == '2030-01-01' || item.end == '2030-01-01'))
{
$time.text(beginTimeShort + ' ' + to + ' ' + endTimeShort).attr('title', beginTimeLong + to + endTimeLong).show();
labelType = 'future';
labelTitle = productplanLang.future;
}
else if(item.begin != undetermindeDate && item.end == undetermindeDate)
if(!$date.length) $date = $('<span class="date label label-' + labelType + '"></span>').appendTo($info);
$date.text(labelTitle).attr('title', labelTitle).show();
/* Display avatars of creator. */
var $user = $info.children('.user');
var user = [item.createdBy];
if(users[item.createdBy])
{
$time.text(beginTimeShort + ' ' + to + ' ' + undetermined).attr('title', beginTimeLong + to).show();
if(!$user.length) $user = $('<div class="user"></div>').appendTo($info);
$user.html(renderUsersAvatar(user, item.id)).attr('title', users[item.createdBy]);
}
else if(item.begin == undetermindeDate && item.end != undetermindeDate)
}
/**
* Render avatars of user.
* @param {String|{account: string, avatar: string}} user User account or user object
* @returns {string}
*/
function renderUsersAvatar(users, itemID, size)
{
var avatarSizeClass = 'avatar-' + (size || 'md');
if(users.length == 0 || (users.length == 1 && users[0] == ''))
{
$time.text(undetermined + ' ' + to + ' ' + endTimeShort).attr('title', to + endTimeLong).show();
}
else if(item.begin != '2030-01-01' && item.end == '2030-01-01')
{
$time.text($.zui.formatDate(begin, 'MM-dd') + ' ' + productplanLang.to + ' ' + productplanLang.future).attr('title', $.zui.formatDate(begin, 'yyyy-MM-dd') + productplanLang.to).show();
}
else if(item.begin == '2030-01-01' && item.end != '2030-01-01')
{
$time.text(productplanLang.future + ' ' + productplanLang.to + ' ' + $.zui.formatDate(end, 'MM-dd')).attr('title', $.zui.formatDate(end, 'yyyy-MM-dd') + productplanLang.to).show();
}
else
{
$time.text(undetermined).attr('title', undetermined).show();
return $('<div class="avatar has-text ' + avatarSizeClass + ' avatar-circle iframe" title="' + noAssigned + '" style="background: #ccc"><i class="icon icon-person"></i></div>');
}
/* Output plan desc information. */
var $desc = $item.children('.desc');
if(!$desc.length)
var assignees = [];
for(var user of users)
{
$("<div class='desc c-name'"+ " title='" + item.desc + "'>" + item.desc + '</div>').appendTo($item);
var $noPrivAndNoAssigned = $('<div class="avatar has-text ' + avatarSizeClass + ' avatar-circle" title="' + noAssigned + '" style="background: #ccc"><i class="icon icon-person"></i></div>');
if(!priv.canAssignCard && !user)
{
assignees.push($noPrivAndNoAssigned);
continue;
}
if(!user)
{
assignees.push($('<div class="avatar has-text ' + avatarSizeClass + ' avatar-circle iframe" title="' + noAssigned + '" style="background: #ccc"><i class="icon icon-person"></i></div>'));
continue;
}
if(typeof user === 'string') user = {account: user};
if(!user.avatar && window.userList && window.userList[user.account]) user = window.userList[user.account];
if(!user.name && window.users && window.users[user.account]) user.name = window.users[user.account];
assignees.push($('<div class="avatar has-text ' + avatarSizeClass + ' avatar-circle iframe"></div>').avatar({user: user}));
}
if(assignees.length > 3) assignees.splice(3, assignees.length - 3, '<span>...</span>');
return assignees;
}
/**
+1
View File
@@ -56,6 +56,7 @@ $lang->productplan->batchUnlinkBug = "Mehrere Verknpfungen aufheben";
$lang->productplan->linkedBugs = 'Verknüpfte Bugs';
$lang->productplan->unlinkedBugs = 'Unverknüpfte Bugs';
$lang->productplan->unexpired = 'Unexpired Plans';
$lang->productplan->noAssigned = 'No Assigned';
$lang->productplan->all = 'All Plans';
$lang->productplan->setDate = "Set Start and end Date";
$lang->productplan->expired = "Expired";
+1
View File
@@ -56,6 +56,7 @@ $lang->productplan->batchUnlinkBug = "Batch Unlink Bugs";
$lang->productplan->linkedBugs = 'Linked Bugs';
$lang->productplan->unlinkedBugs = 'Unlinked Bugs';
$lang->productplan->unexpired = 'Unexpired';
$lang->productplan->noAssigned = 'No Assigned';
$lang->productplan->all = 'All Plans';
$lang->productplan->setDate = "Set Start and end Date";
$lang->productplan->expired = "Expired";
+1
View File
@@ -56,6 +56,7 @@ $lang->productplan->batchUnlinkBug = "Retirer Bugs par Lot";
$lang->productplan->linkedBugs = 'Bugs Planifiés';
$lang->productplan->unlinkedBugs = 'Bugs non Planifiés';
$lang->productplan->unexpired = 'Plans non échus';
$lang->productplan->noAssigned = 'No Assigned';
$lang->productplan->all = 'Tous les Plans';
$lang->productplan->setDate = "Set Start and end Date";
$lang->productplan->expired = "Expired";
+1
View File
@@ -56,6 +56,7 @@ $lang->productplan->batchUnlinkBug = "批量移除Bug";
$lang->productplan->linkedBugs = 'Bug';
$lang->productplan->unlinkedBugs = '未关联Bug';
$lang->productplan->unexpired = "未过期";
$lang->productplan->noAssigned = '未指派';
$lang->productplan->all = "所有计划";
$lang->productplan->setDate = "设置计划起止时间";
$lang->productplan->expired = "已过期";
@@ -26,14 +26,16 @@
.kanban-card:hover > .header > .actions {opacity: 1;}
.kanban-card .title {white-space: nowrap; overflow: hidden;}
.kanban-card .expired {margin-left: 2px;}
.kanban-card .dateBox {padding-top: 22px;}
.kanban-card .desc {overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #838a9d; padding-top: 5px;}
.kanban-card .actions > a {display: block; float: left; width: 20px; height: 20px; line-height: 20px; text-align: center; border-radius: 4px; opacity: .7;}
.dropdown-menu > li > .disabled {pointer-events: none; color: #838a9d;}
</style>
<?php js::set('kanbanData', $kanbanData);?>
<?php js::set('rawModule', $app->rawModule);?>
<?php js::set('users', $users);?>
<?php js::set('noAssigned', $lang->productplan->noAssigned);?>
<?php js::set('productplanLang', $lang->productplan);?>
<?php js::set('priv', array('canAssignCard' => common::hasPriv('kanban', 'assigncard')));?>
<div id="mainMenu" class="clearfix">
<div class="btn-toolbar pull-left">
<?php if($product->type == 'normal'):?>
+7 -6
View File
@@ -64,7 +64,7 @@ class programplan extends control
if(!defined('RUN_MODE') || RUN_MODE != 'api') $projectID = $this->project->saveState((int)$projectID, $this->project->getPairsByProgram());
$products = $this->loadModel('product')->getProducts($projectID);
if($this->session->hasProduct) $this->lang->modulePageNav = $this->product->select($products, $this->productID, 'programplan', 'browse', $type, 0, 0, '', false);
if($this->session->hasProduct) $this->lang->modulePageNav = $this->product->select($products, $productID, 'programplan', 'browse', $type, 0, 0, '', false);
$selectCustom = 0; // Display date and task settings.
$dateDetails = 1; // Gantt chart detail date display.
@@ -81,7 +81,7 @@ class programplan extends control
if(strpos($selectCustom, 'date') !== false) $dateDetails = 0;
$plans = $this->programplan->getDataForGantt($projectID, $this->productID, $baselineID, $selectCustom, false);
$plans = $this->programplan->getDataForGantt($projectID, $productID, $baselineID, $selectCustom, false);
/* Set Custom. */
foreach(explode(',', $this->config->programplan->custom->customGanttFields) as $field) $customFields[$field] = $this->lang->programplan->ganttCustom[$field];
@@ -98,7 +98,7 @@ class programplan extends control
$selectCustom = $this->loadModel('setting')->getItem("owner={$owner}&module={$module}&section={$section}&key={$object}");
if(strpos($selectCustom, 'date') !== false) $dateDetails = 0;
$plans = $this->programplan->getDataForGanttGroupByAssignedTo($projectID, $this->productID, $baselineID, $selectCustom, false);
$plans = $this->programplan->getDataForGanttGroupByAssignedTo($projectID, $productID, $baselineID, $selectCustom, false);
/* Set Custom. */
foreach(explode(',', $this->config->programplan->custom->customGanttFields) as $field) $customFields[$field] = $this->lang->programplan->ganttCustom[$field];
@@ -110,7 +110,7 @@ class programplan extends control
{
$sort = common::appendOrder($orderBy);
$this->loadModel('datatable');
$plans = $this->programplan->getPlans($projectID, $this->productID, $sort);
$plans = $this->programplan->getPlans($projectID, $productID, $sort);
}
$zooming = !empty($this->config->programplan->ganttCustom->zooming) ? $this->config->programplan->ganttCustom->zooming : 'day';
@@ -118,8 +118,9 @@ class programplan extends control
$this->view->position[] = $this->lang->programplan->browse;
$this->view->projectID = $projectID;
$this->view->project = $this->project->getByID($projectID);
$this->view->productID = $this->productID;
$this->view->productList = $this->loadModel('product')->getProductPairsByProject($projectID);
$this->view->productID = $productID;
$this->view->product = $this->product->getByID($productID);
$this->view->productList = $this->product->getProductPairsByProject($projectID, 'all', '', false);
$this->view->type = $type;
$this->view->plans = $plans;
$this->view->orderBy = $orderBy;
+2 -2
View File
@@ -52,12 +52,12 @@ class programplanModel extends model
*/
public function getStage($executionID = 0, $productID = 0, $browseType = 'all', $orderBy = 'id_asc')
{
if(empty($executionID) || empty($productID)) return array();
if(empty($executionID)) return array();
$plans = $this->dao->select('t2.*')->from(TABLE_PROJECTPRODUCT)->alias('t1')
->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id')
->where('t2.type')->eq('stage')
->andWhere('t1.product')->eq($productID)
->beginIF($productID)->andWhere('t1.product')->eq($productID)->fi()
->beginIF($browseType == 'all')->andWhere('t2.project')->eq($executionID)->fi()
->beginIF($browseType == 'parent')->andWhere('t2.parent')->eq($executionID)->fi()
->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->sprints)->fi()
+5 -3
View File
@@ -71,6 +71,8 @@ form {display: block; margin-top: 0em; margin-block-end: 1em;}
.switchBtn > i {padding-left: 7px;}
#mainContent > .pull-left > .btn-group > .text{display: block;margin-top: 7px;}
#mainContent > .pull-left > .btn-group > a > .text{overflow: hidden;display: block;}
#mainContent > .pull-right > .button-group .text{margin-left: 0px;}
.pull-right .icon-plus.icon-sm:before{vertical-align: 4%;}
</style>
<?php js::set('customUrl', $this->createLink('programplan', 'ajaxCustom'));?>
<?php js::set('dateDetails', $dateDetails);?>
@@ -91,10 +93,10 @@ form {display: block; margin-top: 0em; margin-block-end: 1em;}
<?php
$class = '';
if($productID == 0) $class = 'class="active"';
foreach($productList as $key => $product)
foreach($productList as $key => $productName)
{
$class = $productID == $key ? 'class="active"' : '';
echo "<li $class>" . html::a($this->createLink('programplan', 'browse', "projectID=$projectID&productID=$key&type=gantt"), $product) . "</li>";
echo "<li $class>" . html::a($this->createLink('programplan', 'browse', "projectID=$projectID&productID=$key&type=gantt"), $productName) . "</li>";
}
?>
</ul>
@@ -118,7 +120,7 @@ form {display: block; margin-top: 0em; margin-block-end: 1em;}
</div>
<?php
echo html::a(helper::createLink('programplan', 'ajaxcustom', '', '', true), '<i class="icon icon-cog-outline"></i> ' . $lang->settings, '', "class='iframe btn btn-link' data-width='45%'");
if(common::hasPriv('programplan', 'create')) echo html::a($this->createLink('programplan', 'create', "projectID=$projectID"), "<i class='icon icon-sm icon-plus'> </i> " . $this->lang->programplan->create, '', "class='btn btn-primary'");
if(common::hasPriv('programplan', 'create') and empty($product->deleted)) echo html::a($this->createLink('programplan', 'create', "projectID=$projectID"), "<i class='icon icon-sm icon-plus'> </i> " . $this->lang->programplan->create, '', "class='btn btn-primary'");
?>
</div>
<?php endif;?>
+2 -1
View File
@@ -1116,8 +1116,9 @@ class project extends control
$this->view->executionStats = $executionStats;
$this->view->showToggleIcon = $showToggleIcon;
$this->view->productList = $this->loadModel('product')->getProductPairsByProject($projectID);
$this->view->productList = $this->loadModel('product')->getProductPairsByProject($projectID, 'all', '', false);
$this->view->productID = $productID;
$this->view->product = $this->product->getByID($productID);
$this->view->projectID = $projectID;
$this->view->project = $project;
$this->view->projects = $projects;
+1
View File
@@ -18,3 +18,4 @@ td.c-name > .project-type-label {flex: 0 0 36px; padding-right: 2px;}
span.table-nest-icon.table-nest-toggle {min-width: 22px; max-width: 22px;}
#projectForm .c-name, #executionTableList .c-name, #programTableList .c-name {padding-right:47px;}
#mainMenu .pull-left > .btn-group > a > .text{overflow: hidden;display: block;}
#mainMenu .pull-right .icon-list{padding-left: 7px;}
+6 -6
View File
@@ -17,17 +17,17 @@
<div class='btn-toolbar pull-left'>
<?php if($project->division and $project->hasProduct):?>
<div class='btn-group'>
<?php $viewName = $productID != 0 ? zget($productList,$productID) : $lang->product->allProduct;?>
<?php $viewName = $productID != 0 ? zget($productList, $productID) : $lang->product->allProduct;?>
<a href='javascript:;' class='btn btn-link btn-limit' data-toggle='dropdown'><span class='text' title='<?php echo $viewName;?>'><?php echo $viewName;?></span> <span class='caret'></span></a>
<ul class='dropdown-menu' style='max-height:240px; max-width: 300px; overflow-y:auto'>
<?php
$class = '';
if($productID == 0) $class = 'class="active"';
echo "<li $class>" . html::a($this->createLink('project', 'execution', "status=$status&projectID=$projectID&orderby=$orderBy"), $lang->product->allProduct) . "</li>";
foreach($productList as $key => $product)
foreach($productList as $key => $productName)
{
$class = $productID == $key ? 'class="active"' : '';
echo "<li $class>" . html::a($this->createLink('project', 'execution', "status=$status&projectID=$projectID&orderby=$orderBy&productID=$key"), $product) . "</li>";
echo "<li $class>" . html::a($this->createLink('project', 'execution', "status=$status&projectID=$projectID&orderby=$orderBy&productID=$key"), $productName) . "</li>";
}
?>
</ul>
@@ -50,10 +50,10 @@
</div>
<?php endif;?>
<?php common::printLink('execution', 'export', "status=$status&productID=$productID&orderBy=$orderBy&from=project", "<i class='icon-export muted'> </i> " . $lang->export, '', "class='btn btn-link export'")?>
<?php if(common::hasPriv('programplan', 'create') and $isStage):?>
<?php echo html::a($this->createLink('programplan', 'create', "projectID=$projectID&productID=$productID"), "<i class='icon icon-plus'></i> " . $lang->programplan->create, '', "class='btn btn-primary'");?>
<?php if(common::hasPriv('programplan', 'create') and $isStage and empty($product->deleted)):?>
<?php echo html::a($this->createLink('programplan', 'create', "projectID=$projectID&productID=$productID"), "<i class='icon icon-plus'></i> " . $lang->programplan->create, '', "class='btn btn-primary'");?>
<?php else: ?>
<?php if(common::hasPriv('execution', 'create')) echo html::a($this->createLink('execution', 'create', "projectID=$projectID"), "<i class='icon icon-sm icon-plus'></i> " . $lang->execution->create, '', "class='btn btn-primary create-execution-btn' data-app='execution' onclick='$(this).removeAttr(\"data-toggle\")'");?>
<?php if(common::hasPriv('execution', 'create') and !$isStage) echo html::a($this->createLink('execution', 'create', "projectID=$projectID"), "<i class='icon icon-sm icon-plus'></i> " . $lang->execution->create, '', "class='btn btn-primary create-execution-btn' data-app='execution' onclick='$(this).removeAttr(\"data-toggle\")'");?>
<?php endif;?>
</div>
</div>
+52 -6
View File
@@ -662,10 +662,26 @@ class searchModel extends model
{
list($words, $againstCond, $likeCondition, $allowedObject) = $this->getSqlParams($keywords, $type);
$filterObject = array();
foreach($allowedObject as $index => $object)
{
if(strpos(',feedback,ticket,', ",$object,") !== false)
{
unset($allowedObject[$index]);
$filterObject[] = $object;
}
}
$typeCount = $this->dao->select("objectType, count(*) as objectCount")
->from(TABLE_SEARCHINDEX)
->where('vision')->eq($this->config->vision)
->andWhere('objectType')->in($allowedObject)
->where('((vision')->eq($this->config->vision)
->beginIF(!empty($allowedObject))->andWhere('objectType')->in($allowedObject)->fi()
->markRight(1)
->beginIF(!empty($filterObject))
->orWhere('(objectType')->in($filterObject)
->markRight(1)
->fi()
->markRight(1)
->andWhere('addedDate')->le(helper::now())
->groupBy('objectType')
->fetchPairs('objectType', 'objectCount');
@@ -686,12 +702,28 @@ class searchModel extends model
{
list($words, $againstCond, $likeCondition, $allowedObject) = $this->getSqlParams($keywords, $type);
$filterObject = array();
foreach($allowedObject as $index => $object)
{
if(strpos(',feedback,ticket,', ",$object,") !== false)
{
unset($allowedObject[$index]);
$filterObject[] = $object;
}
}
$scoreColumn = "(MATCH(title, content) AGAINST('{$againstCond}' IN BOOLEAN MODE))";
$stmt = $this->dao->select("*, {$scoreColumn} as score")
->from(TABLE_SEARCHINDEX)
->where("(MATCH(title,content) AGAINST('{$againstCond}' IN BOOLEAN MODE) >= 1 {$likeCondition})")
->andWhere('vision')->eq($this->config->vision)
->andWhere('objectType')->in($allowedObject)
->andWhere('((vision')->eq($this->config->vision)
->beginIF(!empty($allowedObject))->andWhere('objectType')->in($allowedObject)->fi()
->markRight(1)
->beginIF(!empty($filterObject))
->orWhere('(objectType')->in($filterObject)
->markRight(1)
->fi()
->markRight(1)
->andWhere('addedDate')->le(helper::now())
->orderBy('score_desc, editedDate_desc')
->query();
@@ -978,6 +1010,7 @@ class searchModel extends model
$programs = $this->app->user->view->programs;
$projects = $this->app->user->view->projects;
$executions = $this->app->user->view->sprints;
$grantProducts = $this->loadModel('feedback')->getGrantProducts();
$objectPairs = array();
$total = count($results);
@@ -992,7 +1025,7 @@ class searchModel extends model
$objectExecutions = array();
if(!isset($this->config->objectTables[$objectType])) continue;
$table = $this->config->objectTables[$objectType];
if(strpos(',bug,case,testcase,productplan,release,story,testtask,', ",$objectType,") !== false)
if(strpos(',bug,case,testcase,productplan,release,story,testtask,ticket,', ",$objectType,") !== false)
{
$objectProducts = $this->dao->select('id,product')->from($table)->where('id')->in(array_keys($objectIdList))->fetchGroup('product', 'id');
}
@@ -1099,11 +1132,24 @@ class searchModel extends model
}
}
}
elseif($objectType == 'feedback')
{
$objectFeedbacks = $this->dao->select('*')->from($table)->where('id')->in(array_keys($objectIdList))->fetchAll('id');
foreach($objectFeedbacks as $feedbackID => $feedback)
{
if($feedback->openedBy == $this->app->user->account or isset($grantProducts[$feedback->product])) continue;
if(isset($objectIdList[$feedbackID]))
{
$recordID = $objectIdList[$feedbackID];
unset($results[$recordID]);
}
}
}
foreach($objectProducts as $productID => $idList)
{
if(empty($productID)) continue;
if(strpos(",$products,", ",$productID,") === false)
if(strpos(",$products,", ",$productID,") === false or ($objectType == 'ticket' and !isset($grantProducts[$productID])))
{
foreach($idList as $object)
{
+16 -3
View File
@@ -1910,7 +1910,6 @@ class story extends control
/* Get edited stories. */
$stories = $this->story->getByList($storyIdList);
$productStoryList = array();
$productList = array();
$ignoreTwins = array();
$twinsCount = array();
@@ -1935,7 +1934,6 @@ class story extends control
$storyProduct = isset($productList[$story->product]) ? $productList[$story->product] : $this->product->getByID($story->product);
$branch = $storyProduct->type == 'branch' ? ($story->branch > 0 ? $story->branch : '0') : 'all';
if(!isset($productStoryList[$story->product][$story->branch])) $productStoryList[$story->product][$story->branch] = $this->story->getProductStoryPairs($story->product, $branch, 0, 'all', 'id_desc', 0, '', $story->type);
if(!empty($story->twins))
{
@@ -2031,7 +2029,6 @@ class story extends control
$this->view->storyIdList = $storyIdList;
$this->view->storyType = $storyType;
$this->view->reasonList = $this->lang->story->reasonList;
$this->view->productStoryList = $productStoryList;
$this->view->twinsCount = $twinsCount;
$this->display();
@@ -3066,4 +3063,20 @@ class story extends control
if(!dao::isError()) $this->loadModel('action')->create('story', $twinID, 'relieved');
return $this->send(array('result' => 'success', 'silbingsCount' => count($twins)-1));
}
/**
* Ajax get story pairs.
*
* @param int $storyID
* @access public
* @return void
*/
public function ajaxGetStoryPairs($storyID)
{
$this->app->loadLang('bug');
$story = $this->story->getByID($storyID);
$stories = $this->story->getProductStoryPairs($story->product, $story->branch, 0, 'all', 'id_desc', 0, '', $story->type);
return print html::select("duplicateStoryIDList[$storyID]", $stories, '', "class='form-control' placeholder='{$this->lang->bug->duplicateTip}'");
}
}
+3 -1
View File
@@ -19,4 +19,6 @@
.btn.btn-action, .c-actions .btn {color: grey}
#storyNoticeBranch {font-size: 13px; color: #5e626d;}
#storyNoticeBranch i {font-size: 14px; color: #0075ff; margin-right: 3px;}
#storyNoticeBranch i {font-size: 14px; color: #0075ff; margin-right: 3px;}
.moduleBox .required:after {right: 25px;}
+23 -7
View File
@@ -27,14 +27,30 @@ function setDuplicateAndChild(resolution, storyID)
$(function()
{
$('select[id^="duplicateStoryIDList"]').picker(
$('td[id^="duplicateStoryBox"]').on('mouseenter', 'select[id^="duplicateStoryIDList"]', function()
{
disableEmptySearch : true,
dropWidth : 'auto',
onReady: function(event)
var options = $(this).find('option').length;
if(options <= 1)
{
$(event.picker.$container).addClass('required');
}
var id = $(this).attr('id');
var storyID = id.replace('duplicateStoryIDList', '');
var link = createLink('story', 'ajaxGetStoryPairs', 'storyID=' + storyID);
var that = $(this);
});
$.get(link, function(data)
{
that.replaceWith(data);
$("#duplicateStoryIDList" + storyID).picker(
{
disableEmptySearch : true,
dropWidth : 'auto',
onReady: function(event)
{
$(event.picker.$container).addClass('required');
}
});
})
}
})
});
+5 -1
View File
@@ -176,7 +176,9 @@ $(window).unload(function(){
$('.table-col .icon-plus').parent().addClass('disabled')
}
itemIndex ++;
if(requiredFields.indexOf('module') > 0) $('#modules' + itemIndex + '_chosen').addClass('required')
itemIndex ++;
}
/**
@@ -305,6 +307,8 @@ function loadModuleForTwins(productID, branch, branchIndex)
$moduleIDBox.fixInputGroup();
});
if(requiredFields.indexOf('module') > 0) $('#moduleIdBox #modules' + branchIndex + '_chosen').addClass('required')
}
/**
+12 -48
View File
@@ -941,7 +941,7 @@ class storyModel extends model
->join('linkStories', ',')
->join('linkRequirements', ',')
->join('childStories', ',')
->remove('files,labels,comment,contactListMenu,stages,reviewer,needNotReview')
->remove('files,labels,comment,contactListMenu,reviewer,needNotReview')
->get();
/* Relieve twins when change product. */
@@ -958,35 +958,6 @@ class storyModel extends model
if(isset($story->plan) and is_array($story->plan)) $story->plan = trim(join(',', $story->plan), ',');
if(isset($_POST['branch']) and $_POST['branch'] == 0) $story->branch = 0;
if(!empty($_POST['stages']))
{
$oldStages = $this->dao->select('*')->from(TABLE_STORYSTAGE)->where('story')->eq($storyID)->fetchAll('branch');
$this->dao->delete()->from(TABLE_STORYSTAGE)->where('story')->eq($storyID)->exec();
$stageList = join(',', array_keys($this->lang->story->stageList));
$minStagePos = strlen($stageList);
$minStage = '';
foreach($this->post->stages as $branch => $stage)
{
$newStage = new stdclass();
$newStage->story = $storyID;
$newStage->branch = $branch;
$newStage->stage = $stage;
if(isset($oldStages[$branch]))
{
$oldStage = $oldStages[$branch];
$newStage->stagedBy = $oldStage->stagedBy;
if($stage != $oldStage->stage) $newStage->stagedBy = (strpos('tested|verified|released|closed', $stage) !== false) ? $this->app->user->account : '';
}
if($story->branch == 0) $this->dao->insert(TABLE_STORYSTAGE)->data($newStage)->exec();
if(strpos($stageList, $stage) !== false and strpos($stageList, $stage) < $minStagePos)
{
$minStage = $stage;
$minStagePos = strpos($stageList, $stage);
}
}
$story->stage = $minStage;
}
if(isset($story->stage) and $oldStory->stage != $story->stage) $story->stagedBy = (strpos('tested|verified|released|closed', $story->stage) !== false) ? $this->app->user->account : '';
$story = $this->loadModel('file')->processImgURL($story, $this->config->story->editor->edit['id'], $this->post->uid);
@@ -5109,13 +5080,22 @@ class storyModel extends model
elseif($id == 'stage')
{
$style .= 'overflow: visible;';
$maxStage = $story->stage;
$stageList = join(',', array_keys($this->lang->story->stageList));
$maxStagePos = strpos($stageList, $maxStage);
if(isset($storyStages[$story->id]))
{
foreach($storyStages[$story->id] as $storyBranch => $storyStage)
{
if(isset($branches[$storyBranch])) $title .= $branches[$storyBranch] . ": " . $this->lang->story->stageList[$storyStage->stage] . "\n";
if(strpos($stageList, $storyStage->stage) !== false and strpos($stageList, $storyStage->stage) > $maxStagePos)
{
$maxStage = $storyStage->stage;
$maxStagePos = strpos($stageList, $storyStage->stage);
}
}
}
$title .= $this->lang->story->stageList[$maxStage];
}
elseif($id == 'feedbackBy')
{
@@ -5210,23 +5190,7 @@ class storyModel extends model
echo $story->estimate . $this->config->hourUnit;
break;
case 'stage':
if(isset($storyStages[$story->id]) and !empty($branches))
{
echo "<div class='dropdown dropdown-hover'>";
echo $this->lang->story->stageList[$story->stage];
echo "<span class='caret'></span>";
echo "<ul class='dropdown-menu pull-right'>";
foreach($storyStages[$story->id] as $storyBranch => $storyStage)
{
if(isset($branches[$storyBranch])) echo '<li class="text-ellipsis">' . $branches[$storyBranch] . ": " . $this->lang->story->stageList[$storyStage->stage] . '</li>';
}
echo "</ul>";
echo '</div>';
}
else
{
echo $this->lang->story->stageList[$story->stage];
}
echo $this->lang->story->stageList[$maxStage];
break;
case 'taskCount':
$tasksLink = helper::createLink('story', 'tasks', "storyID=$story->id", '', 'class="iframe"');
+1 -1
View File
@@ -49,7 +49,7 @@
<?php echo html::select("closedReasons[$storyID]", $reasonList, 'done', "class=form-control onchange=setDuplicateAndChild(this.value,$storyID) style='min-width: 80px'");?>
</td>
<td class='pd-0 w-p60 text-left' id='<?php echo 'duplicateStoryBox' . $storyID;?>' <?php if($story->closedReason != 'duplicate') echo "style='display:none'";?>>
<?php echo html::select("duplicateStoryIDList[$storyID]", array('' => '') + $productStoryList[$story->product][$story->branch], $story->duplicateStory ? $story->duplicateStory : '', "class='form-control' placeholder='{$lang->bug->duplicateTip}'");?>
<?php echo html::select("duplicateStoryIDList[$storyID]", '', '', "class='form-control' placeholder='{$lang->bug->duplicateTip}'");?>
</td>
<td class='pd-0' id='<?php echo 'childStoryBox' . $storyID;?>' <?php if($story->closedReason != 'subdivided') echo "style='display:none'";?>>
<?php echo html::input("childStoriesIDList[$storyID]", '', "class='form-control' placeholder='{$lang->idAB}'");?>
+4 -3
View File
@@ -97,10 +97,11 @@ foreach(explode(',', $config->story->create->requiredFields) as $field)
<?php echo html::select('branches[0]', $branches, $branch, "onchange='loadBranchRelation(this.value, 0);' class='form-control chosen control-branch'");?>
</div>
</div>
<div class='table-col' id='moduleIdBox'>
<div class='table-col moduleBox' id='moduleIdBox'>
<div class='input-group'>
<span class='input-group-addon fix-border'><?php echo $lang->story->module?></span>
<?php echo html::select('modules[0]', $moduleOptionMenu, $moduleID, "class='form-control chosen'");?>
<?php $required = strpos($config->story->create->requiredFields, 'module') === false ? '' : 'required';?>
<?php echo html::select('modules[0]', $moduleOptionMenu, $moduleID, "class='form-control chosen' $required");?>
</div>
</div>
<div class='table-col' id='planIdBox'>
@@ -379,7 +380,7 @@ foreach(explode(',', $config->story->create->requiredFields) as $field)
<?php echo html::select("branches[$i]", $branches, $branch, "onchange='loadBranchRelation(this.value, $i);' class='form-control chosen control-branch'");?>
</div>
</div>
<div class='table-col' id='moduleIdBox' style='flex: 1 0 160px'>
<div class='table-col moduleBox' id='moduleIdBox' style='flex: 1 0 160px'>
<div class='input-group'>
<span class='input-group-addon fix-border'><?php echo $lang->story->module?></span>
<?php echo html::select("modules[$i]", $moduleOptionMenu, $moduleID, "class='form-control chosen'");?>
+9 -5
View File
@@ -227,17 +227,21 @@
<th><?php echo $lang->story->stage;?></th>
<td>
<?php
$maxStage = $story->stage;
$stageList = join(',', array_keys($this->lang->story->stageList));
$maxStagePos = strpos($stageList, $maxStage);
if($story->stages and $branchTagOption)
{
foreach($story->stages as $branch => $stage)
{
if(isset($branchTagOption[$branch])) echo '<p>' . $branchTagOption[$branch] . html::select("stages[$branch]", $lang->story->stageList, $stage, "class='form-control chosen'") . '</p>';
if(strpos($stageList, $stage) !== false and strpos($stageList, $stage) > $maxStagePos)
{
$maxStage = $stage;
$maxStagePos = strpos($stageList, $stage);
}
}
}
else
{
echo html::select('stage', $lang->story->stageList, $story->stage, "class='form-control chosen'");
}
echo html::select('stage', $lang->story->stageList, $maxStage, "class='form-control chosen'");
?>
</td>
</tr>
+12 -5
View File
@@ -302,14 +302,21 @@
<th><?php echo $lang->story->stage;?></th>
<td>
<?php
$maxStage = $story->stage;
$stageList = join(',', array_keys($this->lang->story->stageList));
$maxStagePos = strpos($stageList, $maxStage);
if($story->stages and $branches)
{
foreach($story->stages as $branch => $stage) if(isset($branches[$branch])) echo $branches[$branch] . ' : ' . $lang->story->stageList[$stage] . '<br />';
}
else
{
echo $lang->story->stageList[$story->stage];
foreach($story->stages as $branch => $stage)
{
if(strpos($stageList, $stage) !== false and strpos($stageList, $stage) > $maxStagePos)
{
$maxStage = $stage;
$maxStagePos = strpos($stageList, $stage);
}
}
}
echo $lang->story->stageList[$maxStage];
?>
</td>
</tr>
+9 -2
View File
@@ -74,7 +74,14 @@ foreach(explode(',', $config->task->create->requiredFields) as $field)
<td>
<div class="input-group" id="dataPlanGroup">
<?php echo html::select('assignedTo[]', $members, $task->assignedTo, "class='form-control chosen'");?>
<?php echo html::input('teamMember', '', "class='form-control team-group fix-border hidden' readonly='readonly'");?>
<?php
$teamMember = '';
if(!empty($task->team))
{
foreach($task->team as $team) $teamMember .= ' ' . zget($members, $team->account);
}
?>
<?php echo html::input('teamMember', $teamMember, "class='form-control team-group fix-border hidden' readonly='readonly'");?>
<span class="input-group-btn team-group hidden"><a class="btn br-0" href="#modalTeam" data-toggle="modal"><?php echo $lang->task->team;?></a></span>
</div>
</td>
@@ -115,7 +122,7 @@ foreach(explode(',', $config->task->create->requiredFields) as $field)
<div class='input-group <?php if(empty($stories)) echo "hidden";?>'>
<?php echo html::select('story', $stories, $task->story, "class='form-control chosen' onchange='setStoryRelated();'");?>
<?php if(common::hasPriv('execution', 'storyView')):?>
<span class='input-group-btn' id='preview'><a href='#' class='btn iframe'><?php echo $lang->preview;?></a></span>
<span class='input-group-btn' id='preview'><a href='#' class='btn iframe' data-width="85%" data-height="300px"><?php echo $lang->preview;?></a></span>
<?php endif;?>
</div>
</td>