diff --git a/db/update21.7.2.sql b/db/update21.7.2.sql new file mode 100644 index 0000000000..1cb9c17e91 --- /dev/null +++ b/db/update21.7.2.sql @@ -0,0 +1,2 @@ +ALTER TABLE `zt_project` ADD `tplAcl` char(30) NOT NULL DEFAULT 'open' AFTER `whitelist`; +ALTER TABLE `zt_project` ADD `tplWhiteList` text NULL AFTER `tplAcl`; diff --git a/db/zentao.sql b/db/zentao.sql index 1aec9cf67f..56c17bc1f5 100755 --- a/db/zentao.sql +++ b/db/zentao.sql @@ -1615,6 +1615,8 @@ CREATE TABLE IF NOT EXISTS `zt_project` ( `team` varchar(90) NOT NULL DEFAULT '', `acl` char(30) NOT NULL DEFAULT 'open', `whitelist` text NULL, + `tplAcl` char(30) NOT NULL DEFAULT 'open', + `tplWhiteList` text NULL, `order` mediumint(8) unsigned NOT NULL DEFAULT '0', `vision` varchar(10) NOT NULL DEFAULT 'rnd', `stageBy` enum('project','product') NOT NULL DEFAULT 'product', diff --git a/framework/control.class.php b/framework/control.class.php index 8bf985df95..648b4f68e5 100644 --- a/framework/control.class.php +++ b/framework/control.class.php @@ -534,6 +534,16 @@ class control extends baseControl $uiID = $this->loadModel('workflowlayout')->getUIByData($flow->module, $action->action, $object); $fieldList = $this->workflowaction->getPageFields($flow->module, $action->action, true, null, $uiID, $groupID); + + /* 复制项目时显示被复制项目的工作流字段值。*/ + if($moduleName == 'project' && $methodName == 'create') + { + foreach($fieldList as $field) + { + $objectValue = zget($object, $field->field, ''); + if($objectValue) $field->default = $objectValue; + } + } return $this->loadModel('flow')->buildFormFields($fields, $fieldList, array(), $object); } diff --git a/lib/dao/dao.class.php b/lib/dao/dao.class.php index 35f277dc9f..47435fbc0e 100644 --- a/lib/dao/dao.class.php +++ b/lib/dao/dao.class.php @@ -225,6 +225,7 @@ class dao extends baseDAO $method = $this->app->getMethodName(); if($module == 'story' && $this->app->rawModule == 'requirement') $module = 'requirement'; if($module == 'story' && $this->app->rawModule == 'epic') $module = 'epic'; + if($module == 'project' && $method == 'createtemplate') $method = 'create'; $flowAction = $this->dbh->query("SELECT * FROM " . TABLE_WORKFLOWACTION . " WHERE `module` = '{$module}' AND `action` = '{$method}' AND `buildin` = '1' AND `extensionType` = 'extend' AND `vision` = '{$this->config->vision}'")->fetch(PDO::FETCH_OBJ); if(!$flowAction) return $this; diff --git a/lib/form/form.class.php b/lib/form/form.class.php index 029c01253e..921d431336 100644 --- a/lib/form/form.class.php +++ b/lib/form/form.class.php @@ -119,6 +119,10 @@ class form extends fixer if($moduleName == 'projectrelease') $moduleName = 'release'; if($moduleName == 'projectbuild') $moduleName = 'build'; + /* 项目复制用项目创建的工作流。 */ + if($moduleName == 'project' && $methodName == 'copyconfirm') $methodName = 'create'; + if($moduleName == 'project' && $methodName == 'edittemplate') $methodName = 'edit'; + /* 用户需求和业务需求用自己的工作流。*/ if($moduleName == 'story' && $app->rawModule == 'requirement') $moduleName = 'requirement'; if($moduleName == 'story' && $app->rawModule == 'epic') $moduleName = 'epic'; diff --git a/lib/zin/wg/dropmenu/css/v1.css b/lib/zin/wg/dropmenu/css/v1.css index 9904ddbb1d..bd76831963 100644 --- a/lib/zin/wg/dropmenu/css/v1.css +++ b/lib/zin/wg/dropmenu/css/v1.css @@ -3,3 +3,4 @@ #pick-pop-admin-menu .dropmenu-item + .dropmenu-item {margin-top: 4px} #pick-pop-admin-menu .dropmenu-item.active {background-color: unset;} #pick-pop-admin-menu .dropmenu-item:hover {color: rgba(var(--color-primary-500-rgb),var(--tw-text-opacity)); background-color: rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity));} +.dropmenu-item .item-content .label {flex-shrink: 0;} \ No newline at end of file diff --git a/lib/zin/wg/formpanel/v1.php b/lib/zin/wg/formpanel/v1.php index 5baaba7ff0..4be8c68c2b 100644 --- a/lib/zin/wg/formpanel/v1.php +++ b/lib/zin/wg/formpanel/v1.php @@ -77,12 +77,13 @@ class formPanel extends panel { global $app; $moduleName = $app->getModuleName(); + $methodName = $app->getMethodName(); if($moduleName == 'caselib') return data('lib'); if($moduleName == 'flow') return data('data'); if($moduleName == 'productplan') return data('plan'); if($moduleName == 'projectrelease') return data('release'); if($moduleName == 'projectbuild') return data('build'); - if($moduleName == 'project' && $app->getMethodName() == 'create') return data('copyProject'); + if($moduleName == 'project' && ($methodName == 'create' or $methodName == 'createtemplate')) return data('copyProject') ? data('copyProject') : null; return data($moduleName); } @@ -105,6 +106,9 @@ class formPanel extends panel } } + if($moduleName == 'project' && $methodName == 'createtemplate') $methodName = 'create'; + if($moduleName == 'project' && $methodName == 'edittemplate') $methodName = 'edit'; + /* 反馈转化。 */ if($moduleName == 'feedback') { diff --git a/module/datatable/control.php b/module/datatable/control.php index a0d30c76c1..b28de93029 100644 --- a/module/datatable/control.php +++ b/module/datatable/control.php @@ -211,6 +211,12 @@ class datatable extends control if(!$execution->hasProduct) unset($cols['branch']); } + if($module == 'project' && $method == 'execution') + { + $project = $this->datatable->fetchByID($this->session->project, 'project'); + if(!empty($project->isTpl)) unset($cols['deliverable']); + } + if($extra == 'unsetStory' && isset($cols['story'])) unset($cols['story']); if($this->config->edition == 'ipd' && $module == 'product' && $method == 'browse' && $extra == 'story') unset($cols['roadmap']); diff --git a/module/doc/control.php b/module/doc/control.php index f8b04dedd0..a7b3d8447e 100755 --- a/module/doc/control.php +++ b/module/doc/control.php @@ -367,6 +367,7 @@ class doc extends control public function createLib(string $type = '', int $objectID = 0, int $libID = 0) { $this->app->loadLang('api'); + $this->doc->setMenuByType($type, (int)$objectID, (int)$libID); if(!empty($_POST)) { @@ -386,10 +387,10 @@ class doc extends control } $objects = array(); - if($type == 'project' && $this->app->tab == 'doc') + if($type == 'project') { - $this->view->executionPairs = $this->execution->getPairs($objectID, 'all', 'multiple,leaf,noprefix'); - $this->view->project = $this->project->getById($objectID); + $this->view->project = $this->project->getById($objectID); + if($this->app->tab == 'doc') $this->view->executionPairs = $this->execution->getPairs($objectID, 'all', 'multiple,leaf,noprefix'); } if($type == 'execution') @@ -397,6 +398,7 @@ class doc extends control $objects = $this->execution->getPairs(0, 'all', 'multiple,leaf,noprefix,withobject'); $execution = $this->execution->getByID($objectID); if($execution->type == 'stage') $this->lang->doc->execution = str_replace($this->lang->executionCommon, $this->lang->project->stage, $this->lang->doc->execution); + $this->view->execution = $this->doc->fetchByID($objectID, 'execution'); } if($type == 'custom' || $type == 'mine' || $type == 'doctemplate') @@ -456,6 +458,11 @@ class doc extends control { if(!common::hasPriv('doc', 'editSpace') && !common::hasPriv('doc', 'editLib')) return; $lib = $this->doc->getLibByID($libID); + + if($lib->type != 'project') $this->docZen->setAclForEditLib($lib); + $this->doc->setMenuByType($lib->type, (int)zget($lib, $lib->type, 0), (int)$libID); + if($lib->type == 'project') $this->docZen->setAclForEditLib($lib); + if(!empty($_POST)) { $this->lang->doc->name = $this->lang->nameAB; @@ -499,8 +506,6 @@ class doc extends control $this->view->object = $execution; } - $this->docZen->setAclForEditLib($lib); - $this->view->lib = $lib; $this->view->groups = $this->loadModel('group')->getPairs(); $this->view->users = $this->user->getPairs('noletter|noclosed', $lib->users); @@ -2208,6 +2213,7 @@ class doc extends control $this->doc->setMenuByType($type, $spaceID, $libID); $objectKey = $type . 'ID'; $this->view->$objectKey = $spaceID; + $this->view->$type = $this->doc->fetchByID($spaceID, $type); } if($type == 'mine') $menuType = 'my'; diff --git a/module/doc/ui/app.html.php b/module/doc/ui/app.html.php index f1468b4d91..4f7f0639d4 100644 --- a/module/doc/ui/app.html.php +++ b/module/doc/ui/app.html.php @@ -25,26 +25,27 @@ if($type === 'project') * 定义文档界面上的权限。 * Define the privs of doc app. */ -$hasCustomSpace = $type == 'mine' || $type == 'custom'; +$hasCustomSpace = $type == 'mine' || $type == 'custom'; +$projectTemplate = ($type === 'project' && !empty($project->isTpl)) || ($type == 'execution' && !empty($execution->isTpl)); $privs = array(); -$privs['create'] = hasPriv('doc', 'create'); -$privs['edit'] = hasPriv('doc', 'edit'); -$privs['delete'] = hasPriv('doc', 'delete'); +$privs['create'] = hasPriv('doc', 'create') && !$projectTemplate; +$privs['edit'] = hasPriv('doc', 'edit') && !$projectTemplate; +$privs['delete'] = hasPriv('doc', 'delete') && !$projectTemplate; $privs['view'] = hasPriv('doc', 'view'); $privs['effort'] = $this->config->edition != 'open' && hasPriv('effort', 'createForObject'); -$privs['exportDoc'] = $this->config->edition != 'open' && hasPriv('doc', $type . '2export'); +$privs['exportDoc'] = $this->config->edition != 'open' && hasPriv('doc', $type . '2export') && !$projectTemplate; $privs['exportApi'] = $this->config->edition != 'open' && hasPriv('api', 'export'); -$privs['moveDoc'] = hasPriv('doc', 'moveDoc'); +$privs['moveDoc'] = hasPriv('doc', 'moveDoc') && !$projectTemplate; $privs['collect'] = hasPriv('doc', 'collect'); $privs['createLib'] = hasPriv('doc', 'createLib'); $privs['editLib'] = hasPriv('doc', 'editLib'); -$privs['moveLib'] = hasPriv('doc', 'moveLib'); +$privs['moveLib'] = hasPriv('doc', 'moveLib') && !$projectTemplate; $privs['deleteLib'] = hasPriv('doc', 'deleteLib'); $privs['sortDocLib'] = hasPriv('doc', 'sortDocLib'); -$privs['uploadFile'] = hasPriv('doc', 'create'); +$privs['uploadFile'] = hasPriv('doc', 'create') && !$projectTemplate; $privs['editFile'] = hasPriv('file', 'edit'); $privs['deleteFile'] = hasPriv('doc', 'deleteFile'); -$privs['exportFiles'] = hasPriv('doc', 'exportFiles'); +$privs['exportFiles'] = hasPriv('doc', 'exportFiles') && !$projectTemplate; $privs['createSpace'] = $hasCustomSpace && hasPriv('doc', 'createSpace'); $privs['deleteSpace'] = $hasCustomSpace && hasPriv('doc', 'deleteSpace'); $privs['editSpace'] = $hasCustomSpace && hasPriv('doc', 'editSpace'); diff --git a/module/doc/ui/createlib.html.php b/module/doc/ui/createlib.html.php index dcadcd160f..121cb7b93c 100644 --- a/module/doc/ui/createlib.html.php +++ b/module/doc/ui/createlib.html.php @@ -26,6 +26,7 @@ $renderObjectBox = function(string $type) use ($lang, $objects, $objectID) return formRow( setClass('objectBox'), formGroup( + set::id('object'), set::width('5/6'), set::label($lang->doc->{$type}), set::name($type), diff --git a/module/execution/control.php b/module/execution/control.php index 94741ab8f1..605e0003de 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -2738,7 +2738,7 @@ class execution extends control $executionGroups = $this->dao->select('*')->from(TABLE_EXECUTION) /* 按照项目分组,获取有权限访问的执行列表。*/ ->where('deleted')->eq('0') ->andWhere('multiple')->eq('1') - ->andWhere('type')->in('sprint,stage,kanban') + ->andWhere('type')->in(!empty($execution->isTpl) ? 'sprint,stage' : 'sprint,stage,kanban') ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->sprints)->fi() ->beginIF(!empty($execution->isTpl))->andWhere('isTpl')->eq('1')->fi() ->andWhere('project')->in(array_keys($projects)) diff --git a/module/execution/model.php b/module/execution/model.php index a619021167..3224289d71 100755 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -153,7 +153,6 @@ class executionModel extends model unset($this->lang->execution->menu->burn); unset($this->lang->execution->menu->kanban); - unset($this->lang->execution->menu->view); unset($this->lang->execution->menu->story); unset($this->lang->execution->menu->qa); unset($this->lang->execution->menu->devops); @@ -162,6 +161,22 @@ class executionModel extends model unset($this->lang->execution->menu->effort); unset($this->lang->execution->menu->more); + if(!empty($this->lang->execution->menu->view['subMenu']->gantt)) + { + $this->lang->execution->menu->gantt = $this->lang->execution->menu->view['subMenu']->gantt; + + $taskOrder = 0; + foreach($this->lang->execution->menuOrder as $order => $menu) if($menu == 'task') $taskOrder = $order; + $this->lang->execution->menuOrder[$taskOrder + 1] = 'gantt'; + + if(!empty($this->lang->project->menuOrder)) + { + $taskOrder = 0; + foreach($this->lang->project->menuOrder as $order => $menu) if($menu == 'task') $taskOrder = $order; + $this->lang->project->menuOrder[$taskOrder + 1] = 'gantt'; + } + } + if(!empty($this->lang->execution->menu->other['dropMenu']->pssp)) { $this->lang->execution->menu->pssp = $this->lang->execution->menu->other['dropMenu']->pssp; @@ -181,6 +196,7 @@ class executionModel extends model } } + unset($this->lang->execution->menu->view); unset($this->lang->execution->menu->other); if(isset($this->lang->execution->menu->settings['subMenu']->products)) unset($this->lang->execution->menu->settings['subMenu']->products); // 模板下隐藏产品 if(isset($this->lang->execution->menu->settings['subMenu']->whitelist)) unset($this->lang->execution->menu->settings['subMenu']->whitelist); // 模板下隐藏白名单 diff --git a/module/group/packagemanager.php b/module/group/packagemanager.php index 6bd26a987d..5ee6a98e0d 100644 --- a/module/group/packagemanager.php +++ b/module/group/packagemanager.php @@ -1074,9 +1074,13 @@ $config->group->package->projectTemplate = new stdclass(); $config->group->package->projectTemplate->order = 5; $config->group->package->projectTemplate->subset = 'projectTemplate'; $config->group->package->projectTemplate->privs = array(); -$config->group->package->projectTemplate->privs['project-template'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 5, 'depend' => array('project-index')); -$config->group->package->projectTemplate->privs['project-createTemplate'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 10, 'depend' => array('project-index', 'project-template')); -$config->group->package->projectTemplate->privs['project-deleteTemplate'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 15, 'depend' => array('project-index', 'project-template')); +$config->group->package->projectTemplate->privs['project-template'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 5, 'depend' => array('project-index')); +$config->group->package->projectTemplate->privs['project-createTemplate'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 10, 'depend' => array('project-index', 'project-template')); +$config->group->package->projectTemplate->privs['project-editTemplate'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 15, 'depend' => array('project-index', 'project-template')); +$config->group->package->projectTemplate->privs['project-publishTemplate'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 20, 'depend' => array('project-index', 'project-template')); +$config->group->package->projectTemplate->privs['project-disableTemplate'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 25, 'depend' => array('project-index', 'project-template')); +$config->group->package->projectTemplate->privs['project-templatePriv'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 30, 'depend' => array('project-index', 'project-template')); +$config->group->package->projectTemplate->privs['project-deleteTemplate'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 35, 'depend' => array('project-index', 'project-template')); $config->group->package->browseExecution = new stdclass(); $config->group->package->browseExecution->order = 5; diff --git a/module/project/model.php b/module/project/model.php index aba9b20e91..e7e8dfc0de 100755 --- a/module/project/model.php +++ b/module/project/model.php @@ -189,6 +189,9 @@ class projectModel extends model $action = strtolower($action); + if($action == 'publishtemplate') return $project->status == 'wait' || $project->status == 'closed'; + if($action == 'disabletemplate') return $project->status == 'doing'; + if($action == 'close') return $project->status != 'closed'; if($action == 'group') return $project->model != 'kanban'; if($action == 'start') return $project->status == 'wait' || $project->status == 'suspended'; diff --git a/module/project/ui/common.field.php b/module/project/ui/common.field.php index 0850f765ae..69ebd09d5e 100644 --- a/module/project/ui/common.field.php +++ b/module/project/ui/common.field.php @@ -9,7 +9,7 @@ $fields = defineFieldList('project'); $model = data('model'); $hasCode = !empty($config->setCode); $currency = data('parentProgram') ? data('parentProgram.budgetUnit') : $config->project->defaultCurrency; -$disableStageBy = !empty(data('executions')) || data('app.rawMethod') == 'edit' ? true : false; +$disableStageBy = !empty(data('executions')) || data('app.rawMethod') == 'edit' || !empty(data('copyProject')); $fields->field('parent') ->control('picker', array('required' => true)) @@ -46,6 +46,18 @@ $fields->field('PM')->control('picker')->items(data('PMUsers')); unset($lang->project->endList[999]); $isLongTime = data('project.end') == LONG_TIME; +$copyProject = data('copyProject'); +if($copyProject) +{ + $beginValue = $copyProject->begin; + $endValue = $copyProject->end == LONG_TIME ? '' : $copyProject->end; +} +else +{ + $beginValue = data('project.begin') ? data('project.begin') : date('Y-m-d'); + $endValue = data('project.end') == LONG_TIME ? '' : data('project.end'); +} + $fields->field('begin') ->label($lang->project->planDate) ->required() @@ -54,10 +66,10 @@ $fields->field('begin') ->control('dateRangePicker') ->beginName('begin') ->beginPlaceholder($lang->project->begin) - ->beginValue(data('project.begin') ? data('project.begin') : date('Y-m-d')) + ->beginValue($beginValue) ->endName('end') ->endPlaceholder($lang->project->end) - ->endValue(data('project.end') == LONG_TIME ? '' : data('project.end')) + ->endValue($endValue) ->endDisabled($isLongTime) ->endList($lang->project->endList) ->itemEnd() diff --git a/module/project/ui/create.field.php b/module/project/ui/create.field.php index 96f4811844..0c01915beb 100644 --- a/module/project/ui/create.field.php +++ b/module/project/ui/create.field.php @@ -47,10 +47,13 @@ $fields->field('days') ->control('input', array('className' => $copyProject ? 'has-warning' : '')) ->className($copyProject ? 'has-warning' : '') ->tip($copyProject ? $lang->project->copyProject->daysTips : null) + ->value($copyProject ? data('copyProject.days') : '') ->tipClass($copyProject ? 'text-warning' : null); $fields->field('productsBox')->hidden(data('copyProject') && data('copyProject.hasProduct') == 0); +$fields->field('PM')->value(data('copyProject.PM')); +$fields->field('budget')->value(data('copyProject.budget')); $fields->field('budget')->foldable(); diff --git a/module/project/ui/execution.html.php b/module/project/ui/execution.html.php index 931b2f54e0..16cb76f598 100644 --- a/module/project/ui/execution.html.php +++ b/module/project/ui/execution.html.php @@ -60,6 +60,8 @@ if($canBatchAction) $fieldList = $config->project->execution->dtable->fieldList; $fieldList['status']['statusMap']['changed'] = $lang->task->storyChange; +if(!empty($project->isTpl)) unset($fieldList['deliverable']); + /* waterfall & waterfallplus & ipd model with different edit link. */ if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd'))) { diff --git a/module/project/view/ajaxgetolddropmenu.html.php b/module/project/view/ajaxgetolddropmenu.html.php index 3d81502e2a..f8a7430b64 100644 --- a/module/project/view/ajaxgetolddropmenu.html.php +++ b/module/project/view/ajaxgetolddropmenu.html.php @@ -110,6 +110,10 @@ foreach($projects as $programID => $programProjects) { $link = $indexLink; } + elseif(!empty($project->isTpl)) + { + $link = helper::createLink('project', 'execution', "status=undone&projectID={$project->id}"); + } else { $link = $defaultLink;