diff --git a/.gitfox/tag_analysis.yaml b/.gitfox/tag_analysis.yaml index deff2e42db..0ab7255a33 100644 --- a/.gitfox/tag_analysis.yaml +++ b/.gitfox/tag_analysis.yaml @@ -64,7 +64,7 @@ spec: content: |- 代码库: {{ .GITFOX_REPO_NAME }} - 新版本: {{ .GITFOX_TAG }} + 新版本: {{ .GITFOX_TAG }} 旧版本: {{ .PREVIOUS_TAG }} @@ -76,4 +76,4 @@ spec: # "0": "qishiyao" groups: "0": "38a4e39b-df5d-4606-a988-e22673bdc358" - name: gitfox-xuanim-plugin \ No newline at end of file + name: gitfox-xuanim-plugin diff --git a/Makefile b/Makefile index a472cb51ea..f6b696915a 100644 --- a/Makefile +++ b/Makefile @@ -91,6 +91,7 @@ zentaoxx: cp -r xuan/xxb/module/client zentaoxx/extension/xuan/ cp -r xuan/xxb/module/conference zentaoxx/extension/xuan/ cp -r xuan/xxb/module/watermark zentaoxx/extension/xuan/ + cp -r xuan/xxb/module/log zentaoxx/extension/xuan/ cp -r xuan/xxb/module/integration zentaoxx/extension/xuan/ mkdir -p zentaoxx/extension/xuan/common/view cp -r xuan/xxb/module/common/view/header.modal.html.php zentaoxx/extension/xuan/common/view diff --git a/config/zentaopms.php b/config/zentaopms.php index 314b8a9e87..6de6280d5c 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -669,6 +669,7 @@ $config->objectTables['api'] = TABLE_API; $config->objectTables['doc'] = TABLE_DOC; $config->objectTables['doclib'] = TABLE_DOCLIB; $config->objectTables['docspace'] = TABLE_DOCLIB; +$config->objectTables['doctemplate'] = TABLE_DOC; $config->objectTables['demand'] = TABLE_DEMAND; $config->objectTables['demandpool'] = TABLE_DEMANDPOOL; $config->objectTables['demandspec'] = TABLE_DEMANDSPEC; diff --git a/db/update21.7.2.sql b/db/update21.7.2.sql new file mode 100644 index 0000000000..9ed7b4832c --- /dev/null +++ b/db/update21.7.2.sql @@ -0,0 +1 @@ +ALTER TABLE `zt_doc` ADD `templateDesc` text NULL AFTER `templateType`; diff --git a/db/zentao.sql b/db/zentao.sql index 8c2a88a33f..a96d5f3794 100755 --- a/db/zentao.sql +++ b/db/zentao.sql @@ -797,6 +797,7 @@ CREATE TABLE IF NOT EXISTS `zt_doc` ( `lib` varchar(30) NOT NULL DEFAULT '', `template` varchar(30) NOT NULL DEFAULT '', `templateType` varchar(30) NOT NULL DEFAULT '', + `templateDesc` text NULL, `chapterType` varchar(30) NOT NULL DEFAULT '', `module` varchar(30) NOT NULL DEFAULT '', `title` varchar(255) NOT NULL DEFAULT '', @@ -1291,7 +1292,7 @@ CREATE TABLE IF NOT EXISTS `zt_module` ( `from` mediumint(8) unsigned NOT NULL default '0', `owner` varchar(30) NOT NULL DEFAULT '', `collector` text NULL, - `short` varchar(30) NOT NULL DEFAULT '', + `short` varchar(60) NOT NULL DEFAULT '', `deleted` enum('0','1') NOT NULL default '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/extension/lite/common/ext/lang/zh-cn/lite.php b/extension/lite/common/ext/lang/zh-cn/lite.php index 62a2b2c773..8db774d9d8 100644 --- a/extension/lite/common/ext/lang/zh-cn/lite.php +++ b/extension/lite/common/ext/lang/zh-cn/lite.php @@ -146,6 +146,13 @@ $lang->doc->menuOrder[15] = 'my'; $lang->doc->menuOrder[20] = 'custom'; $lang->doc->menuOrder[25] = 'project'; +if(strpos(',max,ipd,', $config->edition) !== false) +{ + $lang->doc->menu->template = array('link' => "模板广场|doc|browseTemplate|", 'alias' => 'browsetemplate'); + $lang->doc->menuOrder[30] = 'template'; + $lang->doc->dividerMenu .= ',template,'; +} + /* Admin menu. */ $lang->admin->menu = new stdclass(); diff --git a/extension/lite/group/ext/lang/resource.php b/extension/lite/group/ext/lang/resource.php index c18325a471..7f4f56f362 100644 --- a/extension/lite/group/ext/lang/resource.php +++ b/extension/lite/group/ext/lang/resource.php @@ -570,31 +570,41 @@ $lang->task->methodOrder[125] = 'import'; /* Doc. */ $lang->resource->doc = new stdclass(); -$lang->resource->doc->index = 'index'; -$lang->resource->doc->mySpace = 'mySpace'; -$lang->resource->doc->quick = 'quick'; -$lang->resource->doc->createSpace = 'createSpace'; -$lang->resource->doc->createLib = 'createLibAction'; -$lang->resource->doc->editLib = 'editLibAction'; -$lang->resource->doc->deleteLib = 'deleteLibAction'; -$lang->resource->doc->moveLib = 'moveLibAction'; -$lang->resource->doc->moveDoc = 'moveDocAction'; -$lang->resource->doc->create = 'create'; -$lang->resource->doc->view = 'view'; -$lang->resource->doc->edit = 'edit'; -$lang->resource->doc->delete = 'delete'; -$lang->resource->doc->deleteFile = 'deleteFile'; -$lang->resource->doc->collect = 'collectAction'; -$lang->resource->doc->projectSpace = 'projectSpace'; -$lang->resource->doc->teamSpace = 'teamSpace'; -$lang->resource->doc->showFiles = 'showFiles'; -$lang->resource->doc->addCatalog = 'addCatalog'; -$lang->resource->doc->editCatalog = 'editCatalog'; -$lang->resource->doc->sortDoclib = 'sortDoclib'; -$lang->resource->doc->sortCatalog = 'sortCatalog'; -$lang->resource->doc->sortDoc = 'sortDoc'; -$lang->resource->doc->deleteCatalog = 'deleteCatalog'; -// $lang->resource->doc->displaySetting = 'displaySetting'; +$lang->resource->doc->index = 'index'; +$lang->resource->doc->mySpace = 'mySpace'; +$lang->resource->doc->quick = 'quick'; +$lang->resource->doc->createSpace = 'createSpace'; +$lang->resource->doc->createLib = 'createLibAction'; +$lang->resource->doc->editLib = 'editLibAction'; +$lang->resource->doc->deleteLib = 'deleteLibAction'; +$lang->resource->doc->moveLib = 'moveLibAction'; +$lang->resource->doc->moveDoc = 'moveDocAction'; +$lang->resource->doc->create = 'create'; +$lang->resource->doc->view = 'view'; +$lang->resource->doc->edit = 'edit'; +$lang->resource->doc->delete = 'delete'; +$lang->resource->doc->deleteFile = 'deleteFile'; +$lang->resource->doc->collect = 'collectAction'; +$lang->resource->doc->projectSpace = 'projectSpace'; +$lang->resource->doc->teamSpace = 'teamSpace'; +$lang->resource->doc->showFiles = 'showFiles'; +$lang->resource->doc->addCatalog = 'addCatalog'; +$lang->resource->doc->editCatalog = 'editCatalog'; +$lang->resource->doc->sortDoclib = 'sortDoclib'; +$lang->resource->doc->sortCatalog = 'sortCatalog'; +$lang->resource->doc->sortDoc = 'sortDoc'; +$lang->resource->doc->deleteCatalog = 'deleteCatalog'; +$lang->resource->doc->browseTemplate = 'browseTemplate'; +$lang->resource->doc->createTemplate = 'createTemplate'; +$lang->resource->doc->editTemplate = 'editTemplate'; +$lang->resource->doc->moveTemplate = 'moveTemplate'; +$lang->resource->doc->sortTemplate = 'sortTemplate'; +$lang->resource->doc->deleteTemplate = 'deleteTemplate'; +$lang->resource->doc->viewTemplate = 'viewTemplate'; +$lang->resource->doc->addTemplateType = 'addTemplateType'; +$lang->resource->doc->editTemplateType = 'editTemplateType'; +$lang->resource->doc->deleteTemplateType = 'deleteTemplateType'; +$lang->resource->doc->manageScope = 'manageScope'; $lang->doc->methodOrder[5] = 'index'; $lang->doc->methodOrder[10] = 'mySpace'; diff --git a/lib/base/dao/dao.class.php b/lib/base/dao/dao.class.php index a1096c29e0..a3d1c5c11f 100644 --- a/lib/base/dao/dao.class.php +++ b/lib/base/dao/dao.class.php @@ -996,7 +996,7 @@ class baseDAO * @access public * @return string the sql string. */ - static public function processKeywords($sql) + public static function processKeywords($sql) { return str_replace(array(DAO::WHERE, DAO::GROUPBY, DAO::HAVING, DAO::ORDERBY, DAO::LIMIT), array('WHERE', 'GROUP BY', 'HAVING', 'ORDER BY', 'LIMIT'), $sql); } diff --git a/lib/zin/wg/docapp/js/v1.js b/lib/zin/wg/docapp/js/v1.js index 9981c9ef74..d881a72ce0 100644 --- a/lib/zin/wg/docapp/js/v1.js +++ b/lib/zin/wg/docapp/js/v1.js @@ -9,18 +9,36 @@ let docLangData = null; let docBasicModal = {}; const savingDocData = {}; -window.setZentaoSlashMenu = function(menus, title, vision, position) +window.setZentaoSlashMenu = function(menus, title, vision, position, projectReviewTemplates) { if(vision == 'or' || vision == 'lite') return; if(!menus?.length) return; + + const reviewObjects = ['PP', 'SRS', 'HLDS', 'DDS', 'DBDS', 'ADS', 'ITTC', 'STTC']; const slashMenus = menus.map(menu => { + menu.showWhen = function(context) { + const doc = getDocApp().doc; + const docID = doc.data.id; + const templateType = doc.data.templateType; + if(reviewObjects.includes(templateType) && projectReviewTemplates?.[templateType]?.[docID]) return menu.reviewObject !== undefined && menu.reviewObject.indexOf(templateType) !== -1; + return true; + } + if(menu.subMenu) { menu.subMenu = menu.subMenu.map(subMenu => { + subMenu.showWhen = function(context) { + const doc = getDocApp().doc; + const docID = doc.data.id; + const templateType = doc.data.templateType; + if(reviewObjects.includes(templateType) && projectReviewTemplates?.[templateType]?.[docID]) return subMenu.reviewObject !== undefined && subMenu.reviewObject.indexOf(templateType) !== -1; + return true; + } + subMenu.action = function (context) { zui.Modal.open({ - size: 'lg', + size: subMenu.modalSize ? subMenu.modalSize : 'lg', url: $.createLink(subMenu.module, subMenu.method, subMenu.params) }); } @@ -29,11 +47,24 @@ window.setZentaoSlashMenu = function(menus, title, vision, position) } else { - menu.action = function (context) { - zui.Modal.open({ - size: 'lg', - url: $.createLink(menu.module, menu.method, menu.params) - }); + if(menu.key == 'gantt' && !menu.isModal) + { + menu.action = function (context) { + const doc = getDocApp().doc; + $.ajaxSubmit({ + url: $.createLink(menu.module, menu.method, menu.params), + data: {templateID: doc.data.id} + }); + } + } + else + { + menu.action = function (context) { + zui.Modal.open({ + size: menu.modalSize ? menu.modalSize : 'lg', + url: $.createLink(menu.module, menu.method, menu.params) + }); + } } } return menu; @@ -48,6 +79,8 @@ window.setZentaoSlashMenu = function(menus, title, vision, position) /* 获取 ZentaoList 块属性。 */ function getZentaoListProps(type, blockID, props) { + if(props && props.isTemplate) blockID = '__TML_ZENTAOLIST__' + blockID; + const zentaoListLang = getLang('zentaoList'); const exportUrl = $.createLink('doc', 'ajaxExportZentaoList', `blockID=${blockID}`); return $.extend( @@ -103,7 +136,7 @@ window.reloadZentaoList = function(blockID) * * @returns {object} */ -function getDocApp() +window.getDocApp = function() { if(!currentDocApp) { @@ -144,7 +177,7 @@ window.getLang = function(key, args) * Check if the specified privilege exists, privileges are defined in $privs in module/doc/ui/app.html.php. * @param {string} priv */ -function hasPriv(priv, value) +window.hasPriv = function(priv, value) { const setting = userPrivs[priv]; return value !== undefined ? setting === value : !!setting; @@ -216,7 +249,7 @@ function processDocAppAction(action, docApp) * * @param {object|number} space */ -function canModifySpace(space) +window.canModifySpace = function(space) { const docApp = getDocApp(); if(!docApp && !space) return false; @@ -395,7 +428,7 @@ function mergeDocFormData(doc, formData) return doc; } -function showSaveFailedAlert(error) +window.showSaveFailedAlert = function(error) { zui.Modal.alert( { @@ -444,6 +477,7 @@ function submitNewDoc(doc, spaceID, libID, moduleID, formData, afterCreate) acl : 'private', space : spaceType, uid : (doc.uid || `doc${doc.id}`), + template : doc.template }; if(formData) mergeDocFormData(docData, formData); const getErrorMessage = (res) => { @@ -817,6 +851,7 @@ const actionsMap = const doc = info.data; const canModify = canModifySpace(); const isApi = isApiDoc(doc); + const isTemplate = doc.templateType !== ''; const canEditDoc = canModify && hasPriv(isApi ? 'editApi' : 'edit'); const isChapter = doc.type === 'chapter'; @@ -828,7 +863,7 @@ const actionsMap = [ hasPriv('addChapter') ? {icon: 'plus', text: lang.addSubChapter, command: `addChapter/${doc.id}`} : null, hasPriv('addChapter') && doc.parent != 0 ? {icon: 'plus', text: lang.addSameChapter, command: `addChapter/${doc.parent}`} : null, - hasPriv('create') ? {icon: 'plus', text: lang.addSubDoc, command: `startCreateDoc/${doc.id}`} : null, + !isTemplate && hasPriv('create') ? {icon: 'plus', text: lang.addSubDoc, command: `startCreateDoc/${doc.id}`} : null, !isChapter && canMoveDoc(doc) ? {icon: 'folder-move', text: lang.moveDoc, command: `moveDoc/${doc.id}`} : null, !isChapter && hasPriv('delete') ? {icon: 'trash', text: lang.delete, command: `deleteDoc/${doc.id}`} : null, isChapter && hasPriv('editChapter') ? {icon: 'pencil', text: lang.editChapter, command: `editChapter/${doc.id}`} : null, @@ -836,7 +871,7 @@ const actionsMap = ]; return [ (!isChapter && hasPriv('sortDoc')) || (isChapter && hasPriv('sortChapter')) ? {icon: 'move cursor-move', className: 'sort-handler', hint: lang.sortDoc, size: 'xs'} : null, - moreItems.length ? {icon: 'ellipsis-v', caret: false, placement: 'bottom-end', size: 'xs', items: moreItems} : null, + moreItems.every(item => item === null) ? null : {icon: 'ellipsis-v', caret: false, placement: 'bottom-end', size: 'xs', items: moreItems}, ]; } @@ -844,7 +879,7 @@ const actionsMap = if(canModify && canMoveDoc(doc)) moreItems.push({icon: 'folder-move', text: lang.moveDoc, command: `moveDoc/${doc.id}`}); if(canModify && hasPriv('delete')) moreItems.push({icon: 'trash', text: lang.delete, command: `deleteDoc/${doc.id}`}); if(!isApi && canModify && hasPriv('effort')) moreItems.push({icon: 'time', text: lang.effort, command: `effortDoc/${doc.id}`}); - if(!isApi) + if(!isApi && !isTemplate) { const canExportWord = hasPriv('exportDoc'); if(doc.contentType === 'doc' || doc.contentType === 'markdown') @@ -866,7 +901,7 @@ const actionsMap = } const docApp = this; const isOpen = isOpenVersion(config.version); - if(!isOpen && docApp && docApp.mode === 'view' && doc.status != 'draft' && !doc.api) + if(!isOpen && docApp && docApp.mode === 'view' && doc.status != 'draft' && !doc.api && !isTemplate) { moreItems.push({icon: 'link', text: getLang('relateObject'), className: 'relateObject-btn', command: 'toggleViewSideTab/relateObject'}) } @@ -874,7 +909,7 @@ const actionsMap = const isToolbar = info.ui === 'toolbar'; const actions = [ isToolbar ? {hint: docApp.fullscreen ? lang.exitFullscreen : lang.enterFullscreen, icon: docApp.fullscreen ? 'fullscreen-exit' : 'fullscreen', command: 'toggleFullscreen'} : null, - canEditDoc ? {icon: 'edit', type: 'ghost text-primary', hint: lang.edit, rounded: 'lg', command: 'startEditDoc'} : null, + canEditDoc ? {icon: 'edit', class: doc.editable ? null : 'disabled', type: doc.editable ? 'ghost text-primary' : 'ghost text-gray', hint: doc.editable ? lang.edit : lang.needEditable, rounded: 'lg', command: doc.editable ? 'startEditDoc' : null} : null, (isToolbar && docApp.props.showDocOutline !== false) ? {hint: lang.docOutline, icon: 'list-box', command: 'toggleViewSideTab/outline'} : null, (isToolbar && docApp.props.showDocHistory !== false) ? {hint: lang.history, icon: 'history', command: 'toggleViewSideTab/history'} : null, moreItems.length ? {icon: 'icon-ellipsis-v', type: 'dropdown', rounded: 'lg', placement: 'bottom-end', caret: false, items: moreItems} : null, @@ -889,10 +924,11 @@ const actionsMap = */ lib: function(info) { + const docApp = getDocApp(); const lang = getLang(); const items = []; const lib = info.data; - const canModify = canModifySpace(); + const canModify = canModifySpace() || docApp === null; const canAddModule = canModify && hasPriv('addModule'); /* 获取侧边栏没有模块时的操作按钮。 Get actions when sidebar no module. */ @@ -1176,12 +1212,13 @@ const commands = const {spaceType} = docApp; if(spaceType === 'api' || docType === 'api') return docApp.startCreateDoc(); + const templateID = args?.[1] ?? 0; const {spaceID, libID, moduleID} = docApp; return showDocBasicModal(args?.[0] ?? 0, 0, true, docType).then((formData) => { zui.Editor.loadModule().then(() => { const emptyDoc = zui.Editor.createEmptyDoc(); const snapshot = zui.Editor.toSnapshot(emptyDoc); - const doc = {contentType: 'doc', status: 'draft', content: JSON.stringify(snapshot)}; + const doc = {contentType: 'doc', status: 'draft', content: JSON.stringify(snapshot), template: templateID}; return submitNewDoc(doc, spaceID, libID, moduleID, formData, (newDoc) => { docApp.selectDoc(newDoc.id); docApp.startEditDoc(newDoc.id); @@ -1886,6 +1923,8 @@ function getTableOptions(options, info) if(col.name === 'actions' && col.actionsMap) { if(!canModify) col.actions = []; + col.actions = col.actions.filter(action => hasPriv(action == 'move' ? 'moveDoc' : action)); + const actionHints = {edit: lang.editDoc, move: lang.moveDoc, delete: lang.deleteDoc}; const privMap = {edit: hasPriv('edit'), move: hasPriv('moveDoc'), delete: hasPriv('delete')}; $.each(col.actionsMap, (key, value) => @@ -1950,6 +1989,7 @@ function isMatchFilter(type, filterType, item) if(filterType === 'editedByMe') return item.editedBy === currentUser; if(filterType === 'draft') return item.status === 'draft'; if(filterType === 'collect') return item.isCollector; + if(filterType === 'released') return item.status === 'normal'; } if(type === 'project' || type === 'product') { @@ -2044,7 +2084,7 @@ function getSortableOptions(type) } } -function checkResponse(res) +window.checkResponse = function(res) { if(typeof res === 'string') res = JSON.parse(res); if(typeof res !== 'object' || !res) return true; diff --git a/lib/zin/wg/docapp/v1.php b/lib/zin/wg/docapp/v1.php index e5f9e1b3a5..e5ac8f962d 100644 --- a/lib/zin/wg/docapp/v1.php +++ b/lib/zin/wg/docapp/v1.php @@ -71,7 +71,8 @@ class docApp extends wg 'onSwitchView' => '?string', // 切换视图事件。 'getDocViewSidebarTabs' => '?string', // 获取文档视图侧边栏选项。 'formatDataItem' => '?string', // 格式化数据条目。 - 'viewModeUrl' => '?string' // 应用视图 URL 格式。 + 'viewModeUrl' => '?string', // 应用视图 URL 格式。 + 'hasZentaoSlashMenu' => '?boolean' // 是否显示禅道数据。 ); public static function getPageJS(): ?string @@ -184,6 +185,7 @@ class docApp extends wg $langData->createRelease = $lang->api->createRelease; $langData->libTypeList = $lang->api->libTypeList; $langData->latestVersion = $lang->api->latestVersion; + $langData->template = $lang->doc->template; /** * 通过语言项定义文档表格列显示名称。 @@ -245,6 +247,9 @@ class docApp extends wg } } + $hasZentaoSlashMenu = $this->prop('hasZentaoSlashMenu'); + if($hasZentaoSlashMenu === null ) $hasZentaoSlashMenu = true; + $app->control->loadModel('file'); $canDownload = common::hasPriv('file', 'download'); @@ -272,6 +277,8 @@ class docApp extends wg $historyPanelProps = array('fileListProps' => $fileListProps); $canPreviewOffice = $canDownload && isset($config->file->libreOfficeTurnon) and $config->file->libreOfficeTurnon == 1; + $projectReviewTemplates = $this->prop('projectReviewTemplates') ? $this->prop('projectReviewTemplates') : array(); + return zui::docApp ( set::_class('shadow rounded ring canvas'), @@ -317,7 +324,7 @@ class docApp extends wg set::showToolbar(true), set::canPreviewOffice($canPreviewOffice), set::fileInfoUrl($fileInfoUrl), - jsCall('setZentaoSlashMenu', $this->getZentaoListMenu(), $lang->doc->zentaoData, $config->vision, $config->doc->zentaoListMenuPosition) + $hasZentaoSlashMenu ? jsCall('setZentaoSlashMenu', $this->getZentaoListMenu(), $lang->doc->zentaoData, $config->vision, $config->doc->zentaoListMenuPosition, $projectReviewTemplates) : null ); } } diff --git a/module/action/config.php b/module/action/config.php index 1c26be3a42..46af15a042 100755 --- a/module/action/config.php +++ b/module/action/config.php @@ -22,6 +22,7 @@ $config->action->objectNameFields['boardspace'] = 'name'; $config->action->objectNameFields['doc'] = 'title'; $config->action->objectNameFields['doclib'] = 'name'; $config->action->objectNameFields['docspace'] = 'name'; +$config->action->objectNameFields['doctemplate'] = 'title'; $config->action->objectNameFields['todo'] = 'name'; $config->action->objectNameFields['branch'] = 'name'; $config->action->objectNameFields['module'] = 'name'; @@ -88,7 +89,7 @@ $config->action->majorList['doc'] = array('releaseddoc'); $config->action->needGetProjectType = 'build,task,bug,case,testcase,caselib,testtask,testsuite,testreport,doc,issue,release,risk,design,opportunity,trainplan,gapanalysis,researchplan,researchreport,'; $config->action->needGetRelateField = ',branch,story,epic,requirement,productplan,release,task,build,bug,testcase,case,testtask,testreport,doc,doclib,issue,risk,opportunity,trainplan,gapanalysis,team,whitelist,researchplan,researchreport,meeting,kanbanlane,kanbancolumn,module,review,'; -$config->action->noLinkModules = ',doclib,module,webhook,gitlab,instance,gitea,gogs,sonarqube,pipeline,jenkins,kanban,kanbanspace,kanbancolumn,kanbanlane,kanbanregion,kanbancard,execution,project,traincategory,apistruct,program,product,user,entry,repo,pivot,scene,boardspace,auditplan,auditresult,productline,chapter,'; +$config->action->noLinkModules = ',doclib,module,webhook,gitlab,instance,gitea,gogs,sonarqube,pipeline,jenkins,kanban,kanbanspace,kanbancolumn,kanbanlane,kanbanregion,kanbancard,execution,project,traincategory,apistruct,program,product,user,entry,repo,pivot,scene,boardspace,auditplan,auditresult,productline,chapter,doc,'; $config->action->ignoreObjectType4Dynamic = 'kanbanregion,kanbanlane,kanbancolumn'; $config->action->ignoreActions4Dynamic = 'disconnectxuanxuan,reconnectxuanxuan,loginxuanxuan,logoutxuanxuan,editmr,removemr,syncdoingbyticket,syncdoingbystory,syncdoingbyuserstory,syncdoingbyepic,syncdoingbytask,syncdoingbybug,syncdoingbytodo,syncdoingbydemand'; diff --git a/module/action/control.php b/module/action/control.php index a23fb92680..2a79f977d8 100755 --- a/module/action/control.php +++ b/module/action/control.php @@ -206,6 +206,31 @@ class action extends control $url = $this->createLink('action', 'trash', "browseType=boardspace"); if($isDeleted) return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.confirm({message: '{$this->lang->action->undeleteBoardTip}'}).then((res) => {if(res) loadPage('{$url}');});")); } + elseif($oldAction->objectType == 'doctemplate') + { + $templateScopeAndType = $this->dao->select('id,lib,module')->from(TABLE_DOC)->where('id')->eq($oldAction->objectID)->fetchAll('id'); + $scopeID = zget($templateScopeAndType[$oldAction->objectID], 'lib', 0); + $typeID = zget($templateScopeAndType[$oldAction->objectID], 'module', 0); + if($confirmChange == 'no') + { + $isDeletedType = $this->dao->select('deleted')->from(TABLE_MODULE)->where('type')->eq('docTemplate')->andWhere('id')->eq($typeID)->fetch('deleted'); + $isDeletedScope = $this->dao->select('deleted')->from(TABLE_DOCLIB)->where('type')->eq('template')->andWhere('id')->eq($scopeID)->fetch('deleted'); + + if($isDeletedType == '1' || $isDeletedScope == '1') + { + $url = $this->createLink('action', 'undelete', "action={$actionID}&browseType={$browseType}&confirmChange=yes"); + return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.confirm({message: '{$this->lang->action->undeleteTemplateTip}', icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x'}).then((res) => {if(res) $.ajaxSubmit({url: '{$url}'});});")); + } + } + + if($confirmChange == 'yes') + { + $this->dao->update(TABLE_DOCLIB)->set('deleted')->eq('0')->where('id')->eq($scopeID)->exec(); + $this->dao->update(TABLE_MODULE)->set('deleted')->eq('0')->where('id')->eq($typeID)->exec(); + $parentType = $this->dao->select('parent')->from(TABLE_MODULE)->where('id')->eq($typeID)->fetch('parent'); + if(!empty($parentType)) $this->dao->update(TABLE_MODULE)->set('deleted')->eq('0')->where('id')->eq($parentType)->exec(); + } + } $result = $this->action->undelete($actionID); if(true !== $result) return $this->send(array('result' => 'fail', 'load' => array('confirm' => $result))); diff --git a/module/action/lang/de.php b/module/action/lang/de.php index 40f0be5363..a748e4d6b8 100644 --- a/module/action/lang/de.php +++ b/module/action/lang/de.php @@ -58,23 +58,24 @@ $lang->action->refusescene = 'Before restoring the scene, please restore the pa $lang->action->refusemodule = 'Before restoring the module, please restore the parent module of this module '; $lang->action->refusekanban = 'Before restoring the Kanban board, make sure to restore the space it belongs to'; -$lang->action->trashTips = 'Hinweis: Alle Löschungen in ZenTao sind logische Löschungen.'; -$lang->action->textDiff = 'Text Format'; -$lang->action->original = 'Original Format'; -$lang->action->confirmHideAll = 'Möchten Sie alle Einträge verstecken?'; -$lang->action->needEdit = '%s existiert bereits. Bitte bearbeiten.'; -$lang->action->historyEdit = 'Der Verlauf darf nicht leer sein.'; -$lang->action->noDynamic = 'Kein Verlauf. '; -$lang->action->undeletedTips = 'This data did not participate in the merging process during the version upgrade process, so restore is not supported.'; -$lang->action->executionNoProject = "The execution does not belong to a {$lang->projectCommon},please restore the {$lang->projectCommon} first"; -$lang->action->repoNoServer = 'The repo does not belong to a server,please restore the server first'; -$lang->action->hasCreatedTask = 'Tasks have been created in this parent phase and cannot be restored!'; -$lang->action->hasDeletedParent = 'Restoring this phase requires restoring the deleted parent phase %s at the same time.'; -$lang->action->hasChangedAttr = "After recovery, the phase type will be adjusted to '%s' according to the parent phase type."; -$lang->action->whetherToRestore = 'Confirm to restore?'; -$lang->action->undeleteModuleTip = 'Once the subdirectory has been restored, its parent directory will also be automatically recovered and synchronized, would you like to proceed with restoring the directory?'; -$lang->action->undeleteTaskTip = 'The execution associated with this task has been removed. If you choose to restore it, the task will no longer be visible. Would you like to proceed with restoring the task?'; -$lang->action->undeleteBoardTip = 'Before restoring the board, please restore its affiliated space first.'; +$lang->action->trashTips = 'Hinweis: Alle Löschungen in ZenTao sind logische Löschungen.'; +$lang->action->textDiff = 'Text Format'; +$lang->action->original = 'Original Format'; +$lang->action->confirmHideAll = 'Möchten Sie alle Einträge verstecken?'; +$lang->action->needEdit = '%s existiert bereits. Bitte bearbeiten.'; +$lang->action->historyEdit = 'Der Verlauf darf nicht leer sein.'; +$lang->action->noDynamic = 'Kein Verlauf. '; +$lang->action->undeletedTips = 'This data did not participate in the merging process during the version upgrade process, so restore is not supported.'; +$lang->action->executionNoProject = "The execution does not belong to a {$lang->projectCommon},please restore the {$lang->projectCommon} first"; +$lang->action->repoNoServer = 'The repo does not belong to a server,please restore the server first'; +$lang->action->hasCreatedTask = 'Tasks have been created in this parent phase and cannot be restored!'; +$lang->action->hasDeletedParent = 'Restoring this phase requires restoring the deleted parent phase %s at the same time.'; +$lang->action->hasChangedAttr = "After recovery, the phase type will be adjusted to '%s' according to the parent phase type."; +$lang->action->whetherToRestore = 'Confirm to restore?'; +$lang->action->undeleteModuleTip = 'Once the subdirectory has been restored, its parent directory will also be automatically recovered and synchronized, would you like to proceed with restoring the directory?'; +$lang->action->undeleteTaskTip = 'The execution associated with this task has been removed. If you choose to restore it, the task will no longer be visible. Would you like to proceed with restoring the task?'; +$lang->action->undeleteBoardTip = 'Before restoring the board, please restore its affiliated space first.'; +$lang->action->undeleteTemplateTip = 'The type (and scope) of this template has been deleted. Are you sure you want to synchronize and restore them?'; $lang->action->hasOtherType = array(); $lang->action->hasOtherType['stage'] = "Only subtypes of {$lang->executionCommon} / Kanban types can be created at this parent stage, so the current stage cannot be restored."; @@ -133,6 +134,7 @@ $lang->action->objectTypes['doc'] = 'Dok'; $lang->action->objectTypes['api'] = 'Interface'; $lang->action->objectTypes['doclib'] = 'Dok Bibliothek'; $lang->action->objectTypes['docspace'] = 'Dok Space'; +$lang->action->objectTypes['doctemplate'] = 'Doc Template'; $lang->action->objectTypes['apistruct'] = 'API struct'; $lang->action->objectTypes['todo'] = 'Todo'; $lang->action->objectTypes['risk'] = 'Risk'; @@ -294,6 +296,7 @@ $lang->action->desc->managedteam = '$date, by $actor m $lang->action->desc->syncexecutionteam = '$date, synchronise members when execution adds members.' . "\n"; $lang->action->desc->syncprojectteam = '$date, synchronise members when project removes members' . "\n"; $lang->action->desc->syncbycase = '$date, running the testcase sets the request status as Doing.' . "\n"; +$lang->action->desc->converttonewdoc = '$date, $actor completed the conversion.'; /* Used to describe the history of operations related to parent-child tasks. */ $lang->action->desc->createchildren = '$date, $actor created a child task $extra。' . "\n"; @@ -329,6 +332,9 @@ $lang->action->desc->releaseddoc = '$date, $actor released action->desc->convertdoc = '$date, $actor complete document conversion $extra.' . "\n"; +/* Describe the operation records of template conversion. */ +$lang->action->desc->convertdoctemplate = '$date, $actor complete template conversion $extra.' . "\n"; + /* This parameter describes historical operations that are performed when a document is collected or uncollected. */ $lang->action->desc->collected = '$date, $actor collected $extra.' . "\n"; $lang->action->desc->uncollected = '$date, $actor uncollected $extra.' . "\n"; @@ -550,6 +556,7 @@ $lang->action->label->deletesnapshot = 'deleted snapshot'; $lang->action->label->saveddraft = 'save draft'; $lang->action->label->releaseddoc = 'released'; $lang->action->label->convertdoc = 'Convert doc'; +$lang->action->label->convertdoctemplate = 'Convert template'; $lang->action->label->collected = 'collected'; $lang->action->label->uncollected = 'uncollected'; $lang->action->label->online = 'online'; @@ -911,6 +918,7 @@ $lang->action->label->todo = 'Todo|todo|view|todoID=%s'; $lang->action->label->doclib = 'Dok Bibliothek|doc|teamspace|objectID=%s&libID=%s'; $lang->action->label->docspace = 'Dok Space'; $lang->action->label->doc = 'Dok|doc|view|docID=%s'; +$lang->action->label->doctemplate = 'Doc Template|doc|view|docID=%s'; $lang->action->label->user = 'Benutzer|user|view|account=%s'; $lang->action->label->testreport = 'Berichte|testreport|view|report=%s'; $lang->action->label->entry = 'Eintrag|entry|browse|'; diff --git a/module/action/lang/en.php b/module/action/lang/en.php index 5860a6b5fd..816ff552b2 100755 --- a/module/action/lang/en.php +++ b/module/action/lang/en.php @@ -58,23 +58,24 @@ $lang->action->refusescene = 'Before restoring the scene, please restore the pa $lang->action->refusemodule = 'Before restoring the module, please restore the parent module of this module '; $lang->action->refusekanban = 'Before restoring the Kanban board, make sure to restore the space it belongs to'; -$lang->action->trashTips = 'Note: Delete in ZenTao is logic.'; -$lang->action->textDiff = 'Text Format'; -$lang->action->original = 'Original Format'; -$lang->action->confirmHideAll = 'Do you want to hide all the records?'; -$lang->action->needEdit = '%s that you want to restore. Please edit it.'; -$lang->action->historyEdit = 'The history EditBy cannot be empty.'; -$lang->action->noDynamic = 'No dynamics. '; -$lang->action->undeletedTips = 'This data did not participate in the merging process during the version upgrade process, so restore is not supported.'; -$lang->action->executionNoProject = "The execution does not belong to a {$lang->projectCommon},please restore the {$lang->projectCommon} first"; -$lang->action->repoNoServer = 'The repo does not belong to a server,please restore the server first'; -$lang->action->hasCreatedTask = 'Tasks have been created in this parent phase and cannot be restored!'; -$lang->action->hasDeletedParent = 'Restoring this phase requires restoring the deleted parent phase %s at the same time.'; -$lang->action->hasChangedAttr = "After recovery, the phase type will be adjusted to '%s' according to the parent phase type."; -$lang->action->whetherToRestore = 'Confirm to restore?'; -$lang->action->undeleteModuleTip = 'Once the subdirectory has been restored, its parent directory will also be automatically recovered and synchronized, would you like to proceed with restoring the directory?'; -$lang->action->undeleteTaskTip = 'The execution associated with this task has been removed. If you choose to restore it, the task will no longer be visible. Would you like to proceed with restoring the task?'; -$lang->action->undeleteBoardTip = 'Before restoring the board, please restore its affiliated space first.'; +$lang->action->trashTips = 'Note: Delete in ZenTao is logic.'; +$lang->action->textDiff = 'Text Format'; +$lang->action->original = 'Original Format'; +$lang->action->confirmHideAll = 'Do you want to hide all the records?'; +$lang->action->needEdit = '%s that you want to restore. Please edit it.'; +$lang->action->historyEdit = 'The history EditBy cannot be empty.'; +$lang->action->noDynamic = 'No dynamics. '; +$lang->action->undeletedTips = 'This data did not participate in the merging process during the version upgrade process, so restore is not supported.'; +$lang->action->executionNoProject = "The execution does not belong to a {$lang->projectCommon},please restore the {$lang->projectCommon} first"; +$lang->action->repoNoServer = 'The repo does not belong to a server,please restore the server first'; +$lang->action->hasCreatedTask = 'Tasks have been created in this parent phase and cannot be restored!'; +$lang->action->hasDeletedParent = 'Restoring this phase requires restoring the deleted parent phase %s at the same time.'; +$lang->action->hasChangedAttr = "After recovery, the phase type will be adjusted to '%s' according to the parent phase type."; +$lang->action->whetherToRestore = 'Confirm to restore?'; +$lang->action->undeleteModuleTip = 'Once the subdirectory has been restored, its parent directory will also be automatically recovered and synchronized, would you like to proceed with restoring the directory?'; +$lang->action->undeleteTaskTip = 'The execution associated with this task has been removed. If you choose to restore it, the task will no longer be visible. Would you like to proceed with restoring the task?'; +$lang->action->undeleteBoardTip = 'Before restoring the board, please restore its affiliated space first.'; +$lang->action->undeleteTemplateTip = 'The category (and scope) of this template has been deleted. Are you sure you want to synchronize and restore them?'; $lang->action->hasOtherType = array(); $lang->action->hasOtherType['stage'] = "Only subtypes of {$lang->executionCommon} / Kanban types can be created at this parent stage, so the current stage cannot be restored."; @@ -133,6 +134,7 @@ $lang->action->objectTypes['doc'] = 'Document'; $lang->action->objectTypes['api'] = 'Interface'; $lang->action->objectTypes['doclib'] = 'Document Library'; $lang->action->objectTypes['docspace'] = 'Document Space'; +$lang->action->objectTypes['doctemplate'] = 'Doc Template'; $lang->action->objectTypes['apistruct'] = 'API struct'; $lang->action->objectTypes['todo'] = 'Todo'; $lang->action->objectTypes['risk'] = 'Risk'; @@ -294,6 +296,7 @@ $lang->action->desc->managedteam = '$date, by $actor m $lang->action->desc->syncexecutionteam = '$date, synchronise members when execution adds members.' . "\n"; $lang->action->desc->syncprojectteam = '$date, synchronise members when project removes members' . "\n"; $lang->action->desc->syncbycase = '$date, running the testcase sets the request status as Doing.' . "\n"; +$lang->action->desc->converttonewdoc = '$date, $actor completed the conversion.'; /* Used to describe the history of operations related to parent-child tasks. */ $lang->action->desc->createchildren = '$date, $actor created a child task $extra。' . "\n"; @@ -329,6 +332,9 @@ $lang->action->desc->releaseddoc = '$date, $actor released action->desc->convertdoc = '$date, $actor complete document conversion $extra.' . "\n"; +/* Describe the operation records of template conversion. */ +$lang->action->desc->convertdoctemplate = '$date, $actor complete template conversion $extra.' . "\n"; + /* This parameter describes historical operations that are performed when a document is collected or uncollected. */ $lang->action->desc->collected = '$date, $actor collected $extra.' . "\n"; $lang->action->desc->uncollected = '$date, $actor uncollected $extra.' . "\n"; @@ -550,6 +556,7 @@ $lang->action->label->deletesnapshot = 'deleted snapshot'; $lang->action->label->saveddraft = 'save draft'; $lang->action->label->releaseddoc = 'released'; $lang->action->label->convertdoc = 'Convert doc'; +$lang->action->label->convertdoctemplate = 'Convert template'; $lang->action->label->collected = 'collected'; $lang->action->label->uncollected = 'uncollected'; $lang->action->label->online = 'online'; @@ -911,6 +918,7 @@ $lang->action->label->todo = 'Todo|todo|view|todoID=%s'; $lang->action->label->doclib = 'Doc Library|doc|teamspace|objectID=%s&libID=%s'; $lang->action->label->docspace = 'Doc Space'; $lang->action->label->doc = 'Doc|doc|view|docID=%s'; +$lang->action->label->doctemplate = 'Doc Template|doc|view|docID=%s'; $lang->action->label->user = 'User|user|view|account=%s'; $lang->action->label->testreport = 'Report|testreport|view|report=%s'; $lang->action->label->entry = 'Application|entry|browse|'; diff --git a/module/action/lang/fr.php b/module/action/lang/fr.php index 61ee3a5353..96cea45544 100644 --- a/module/action/lang/fr.php +++ b/module/action/lang/fr.php @@ -58,23 +58,24 @@ $lang->action->refusescene = 'Before restoring the scene, please restore the pa $lang->action->refusemodule = 'Before restoring the module, please restore the parent module of this module '; $lang->action->refusekanban = 'Before restoring the Kanban board, make sure to restore the space it belongs to'; -$lang->action->trashTips = 'Note: Les suppressions dans ZenTao sont purement logiques.'; -$lang->action->textDiff = 'Text Format'; -$lang->action->original = 'Original Format'; -$lang->action->confirmHideAll = 'Voulez-vous masquer tous les enregistrements ?'; -$lang->action->needEdit = '%s que vous voulez restaurer. SVP éditez la.'; -$lang->action->historyEdit = "l'historique Edité Par ne peut pas être vide."; -$lang->action->noDynamic = "Pas d'historique."; -$lang->action->undeletedTips = 'This data did not participate in the merging process during the version upgrade process, so restore is not supported.'; -$lang->action->executionNoProject = "The execution does not belong to a {$lang->projectCommon},please restore the {$lang->projectCommon} first"; -$lang->action->repoNoServer = 'The repo does not belong to a server,please restore the server first'; -$lang->action->hasCreatedTask = 'Tasks have been created in this parent phase and cannot be restored!'; -$lang->action->hasDeletedParent = 'Restoring this phase requires restoring the deleted parent phase %s at the same time.'; -$lang->action->hasChangedAttr = "After recovery, the phase type will be adjusted to '%s' according to the parent phase type."; -$lang->action->whetherToRestore = 'Confirm to restore?'; -$lang->action->undeleteModuleTip = 'Once the subdirectory has been restored, its parent directory will also be automatically recovered and synchronized, would you like to proceed with restoring the directory?'; -$lang->action->undeleteTaskTip = 'The execution associated with this task has been removed. If you choose to restore it, the task will no longer be visible. Would you like to proceed with restoring the task?'; -$lang->action->undeleteBoardTip = 'Before restoring the board, please restore its affiliated space first.'; +$lang->action->trashTips = 'Note: Les suppressions dans ZenTao sont purement logiques.'; +$lang->action->textDiff = 'Text Format'; +$lang->action->original = 'Original Format'; +$lang->action->confirmHideAll = 'Voulez-vous masquer tous les enregistrements ?'; +$lang->action->needEdit = '%s que vous voulez restaurer. SVP éditez la.'; +$lang->action->historyEdit = "l'historique Edité Par ne peut pas être vide."; +$lang->action->noDynamic = "Pas d'historique."; +$lang->action->undeletedTips = 'This data did not participate in the merging process during the version upgrade process, so restore is not supported.'; +$lang->action->executionNoProject = "The execution does not belong to a {$lang->projectCommon},please restore the {$lang->projectCommon} first"; +$lang->action->repoNoServer = 'The repo does not belong to a server,please restore the server first'; +$lang->action->hasCreatedTask = 'Tasks have been created in this parent phase and cannot be restored!'; +$lang->action->hasDeletedParent = 'Restoring this phase requires restoring the deleted parent phase %s at the same time.'; +$lang->action->hasChangedAttr = "After recovery, the phase type will be adjusted to '%s' according to the parent phase type."; +$lang->action->whetherToRestore = 'Confirm to restore?'; +$lang->action->undeleteModuleTip = 'Once the subdirectory has been restored, its parent directory will also be automatically recovered and synchronized, would you like to proceed with restoring the directory?'; +$lang->action->undeleteTaskTip = 'The execution associated with this task has been removed. If you choose to restore it, the task will no longer be visible. Would you like to proceed with restoring the task?'; +$lang->action->undeleteBoardTip = 'Before restoring the board, please restore its affiliated space first.'; +$lang->action->undeleteTemplateTip = 'The category (and scope) of this template has been deleted. Are you sure you want to synchronize and restore them?'; $lang->action->hasOtherType = array(); $lang->action->hasOtherType['stage'] = "Only subtypes of {$lang->executionCommon} / Kanban types can be created at this parent stage, so the current stage cannot be restored."; @@ -133,6 +134,7 @@ $lang->action->objectTypes['doc'] = 'Document'; $lang->action->objectTypes['api'] = 'Interface'; $lang->action->objectTypes['doclib'] = 'Répertoire Documents'; $lang->action->objectTypes['docspace'] = 'Répertoire Space'; +$lang->action->objectTypes['doctemplate'] = 'Doc Template'; $lang->action->objectTypes['apistruct'] = 'API struct'; $lang->action->objectTypes['todo'] = 'Todo'; $lang->action->objectTypes['risk'] = 'Risk'; @@ -294,6 +296,7 @@ $lang->action->desc->managedteam = '$date, by $actor m $lang->action->desc->syncexecutionteam = '$date, synchronise members when execution adds members.' . "\n"; $lang->action->desc->syncprojectteam = '$date, synchronise members when project removes members' . "\n"; $lang->action->desc->syncbycase = '$date, running the testcase sets the request status as Doing.' . "\n"; +$lang->action->desc->converttonewdoc = '$date, $actor completed the conversion.'; /* Used to describe the history of operations related to parent-child tasks. */ $lang->action->desc->createchildren = '$date, $actor a créé un sous-tâche $extra。' . "\n"; @@ -329,6 +332,9 @@ $lang->action->desc->releaseddoc = '$date, $actor released action->desc->convertdoc = '$date, $actor complete document conversion $extra.' . "\n"; +/* Describe the operation records of template conversion. */ +$lang->action->desc->convertdoctemplate = '$date, $actor complete template conversion $extra.' . "\n"; + /* This parameter describes historical operations that are performed when a document is collected or uncollected. */ $lang->action->desc->collected = '$date, $actor collected $extra.' . "\n"; $lang->action->desc->uncollected = '$date, $actor uncollected $extra.' . "\n"; @@ -550,6 +556,7 @@ $lang->action->label->deletesnapshot = 'deleted snapshot'; $lang->action->label->saveddraft = 'save draft'; $lang->action->label->releaseddoc = 'released'; $lang->action->label->convertdoc = 'Convert doc'; +$lang->action->label->convertdoctemplate = 'Convert template'; $lang->action->label->collected = 'collected'; $lang->action->label->uncollected = 'uncollected'; $lang->action->label->online = 'online'; @@ -911,6 +918,7 @@ $lang->action->label->todo = 'Agenda|todo|view|todoID=%s'; $lang->action->label->doclib = 'Bibliothèque|doc|teamspace|objectID=%s&libID=%s'; $lang->action->label->docspace = 'Doc Space'; $lang->action->label->doc = 'Document|doc|view|docID=%s'; +$lang->action->label->doctemplate = 'Doc Template|doc|view|docID=%s'; $lang->action->label->user = 'Utilisateur|user|view|account=%s'; $lang->action->label->testreport = 'Rapport|testreport|view|report=%s'; $lang->action->label->entry = 'Application|entry|browse|'; diff --git a/module/action/lang/zh-cn.php b/module/action/lang/zh-cn.php index 2a0de805e2..7f05ff5b4f 100755 --- a/module/action/lang/zh-cn.php +++ b/module/action/lang/zh-cn.php @@ -58,23 +58,24 @@ $lang->action->refusescene = '还原场景之前,请先还原该场景的父 $lang->action->refusemodule = '还原模块之前,请先还原该模块的父模块'; $lang->action->refusekanban = '还原看板之前,请先还原该看板所属空间'; -$lang->action->trashTips = '提示:为了保证系统的完整性,禅道系统的删除都是标记删除。'; -$lang->action->textDiff = '文本格式'; -$lang->action->original = '原始格式'; -$lang->action->confirmHideAll = '您确定要全部隐藏这些记录吗?'; -$lang->action->needEdit = '要还原%s的名称或代号已经存在,请编辑更改。'; -$lang->action->historyEdit = '历史记录编辑不能为空。'; -$lang->action->noDynamic = '暂时没有动态。'; -$lang->action->undeletedTips = '该数据在版本升级过程中未参与数据归并流程,不支持还原。'; -$lang->action->executionNoProject = "该执行没有所属的{$lang->projectCommon},请先还原{$lang->projectCommon}再还原执行"; -$lang->action->repoNoServer = '该代码库没有所属的服务器,请先还原服务器再还原代码库'; -$lang->action->hasCreatedTask = '该父阶段已创建任务,无法恢复!'; -$lang->action->hasDeletedParent = '恢复该阶段需要同时恢复已删除的父阶段%s,'; -$lang->action->hasChangedAttr = '恢复后阶段类型将根据父阶段类型均调整为“%s”,'; -$lang->action->whetherToRestore = '是否恢复?'; -$lang->action->undeleteModuleTip = '子目录恢复后,会同步恢复他的上级目录,确认要恢复吗?'; -$lang->action->undeleteTaskTip = '该任务所属执行已删除,还原后不能看到该任务,是否继续还原任务?'; -$lang->action->undeleteBoardTip = '还原白板之前,请先还原该白板的所属空间。'; +$lang->action->trashTips = '提示:为了保证系统的完整性,禅道系统的删除都是标记删除。'; +$lang->action->textDiff = '文本格式'; +$lang->action->original = '原始格式'; +$lang->action->confirmHideAll = '您确定要全部隐藏这些记录吗?'; +$lang->action->needEdit = '要还原%s的名称或代号已经存在,请编辑更改。'; +$lang->action->historyEdit = '历史记录编辑不能为空。'; +$lang->action->noDynamic = '暂时没有动态。'; +$lang->action->undeletedTips = '该数据在版本升级过程中未参与数据归并流程,不支持还原。'; +$lang->action->executionNoProject = "该执行没有所属的{$lang->projectCommon},请先还原{$lang->projectCommon}再还原执行"; +$lang->action->repoNoServer = '该代码库没有所属的服务器,请先还原服务器再还原代码库'; +$lang->action->hasCreatedTask = '该父阶段已创建任务,无法恢复!'; +$lang->action->hasDeletedParent = '恢复该阶段需要同时恢复已删除的父阶段%s,'; +$lang->action->hasChangedAttr = '恢复后阶段类型将根据父阶段类型均调整为“%s”,'; +$lang->action->whetherToRestore = '是否恢复?'; +$lang->action->undeleteModuleTip = '子目录恢复后,会同步恢复他的上级目录,确认要恢复吗?'; +$lang->action->undeleteTaskTip = '该任务所属执行已删除,还原后不能看到该任务,是否继续还原任务?'; +$lang->action->undeleteBoardTip = '还原白板之前,请先还原该白板的所属空间。'; +$lang->action->undeleteTemplateTip = '该文档模板的分类(和范围)已被删除,确认要同步还原吗?'; $lang->action->hasOtherType = array(); $lang->action->hasOtherType['stage'] = "当前阶段的父级现在只允许创建{$lang->executionCommon}/看板类型的子级,故无法恢复当前阶段。"; @@ -133,6 +134,7 @@ $lang->action->objectTypes['doc'] = '文档'; $lang->action->objectTypes['api'] = '接口'; $lang->action->objectTypes['doclib'] = '文档库'; $lang->action->objectTypes['docspace'] = '文档空间'; +$lang->action->objectTypes['doctemplate'] = '文档模板'; $lang->action->objectTypes['apistruct'] = '数据结构'; $lang->action->objectTypes['todo'] = '待办'; $lang->action->objectTypes['risk'] = '风险'; @@ -294,6 +296,7 @@ $lang->action->desc->managedteam = '$date, 由 $actor $lang->action->desc->syncexecutionteam = '$date, 同步更新执行增加的团队成员。' . "\n"; $lang->action->desc->syncprojectteam = '$date, 同步更新项目删除的团队成员。' . "\n"; $lang->action->desc->syncbycase = '$date, 系统判断由于执行了用例,将测试单状态置为进行中。' . "\n"; +$lang->action->desc->converttonewdoc = '$date, 由 $actor 完成了文档转换。'; /* 用来描述和父子任务相关的操作历史记录。*/ $lang->action->desc->createchildren = '$date, 由 $actor 创建子任务 $extra。' . "\n"; @@ -329,6 +332,9 @@ $lang->action->desc->releaseddoc = '$date, 由 $actor 发布 action->desc->convertdoc = '$date, 由 $actor 完成文档转换 $extra。' . "\n"; +/* 用来描述文档模板转换的操作记录。*/ +$lang->action->desc->convertdoctemplate = '$date, 由 $actor 完成文档模板转换 $extra。' . "\n"; + /* 用来描述文档收藏或取消收藏时的历史操作记录。*/ $lang->action->desc->collected = '$date, 由 $actor 收藏 $extra。' . "\n"; $lang->action->desc->uncollected = '$date, 由 $actor 取消收藏 $extra。' . "\n"; @@ -550,6 +556,7 @@ $lang->action->label->deletesnapshot = '编辑了快照'; $lang->action->label->saveddraft = '存为草稿'; $lang->action->label->releaseddoc = '发布了'; $lang->action->label->convertdoc = '转为新文档'; +$lang->action->label->convertdoctemplate = '转为新文档模板'; $lang->action->label->collected = '收藏了'; $lang->action->label->uncollected = '取消收藏了'; $lang->action->label->online = '上架了'; @@ -911,6 +918,7 @@ $lang->action->label->todo = '待办|todo|view|todoID=%s'; $lang->action->label->doclib = '文档库|doc|teamspace|objectID=%s&libID=%s'; $lang->action->label->docspace = '文档空间'; $lang->action->label->doc = '文档|doc|view|docID=%s'; +$lang->action->label->doctemplate = '文档模板|doc|view|docID=%s'; $lang->action->label->user = '用户|user|view|account=%s'; $lang->action->label->testreport = '报告|testreport|view|report=%s'; $lang->action->label->entry = '应用|entry|browse|'; diff --git a/module/action/model.php b/module/action/model.php index dfc768d9a5..70ee6bb8fc 100755 --- a/module/action/model.php +++ b/module/action/model.php @@ -1497,11 +1497,13 @@ class actionModel extends model /* 区别是否有diff信息,以便于将有diff信息的字段放在最后。 */ /* Diff histories by hasing diff info or not. Thus we can to make sure the field with diff show at last. */ + if($objectType == 'doctemplate') $this->app->loadLang('doc'); foreach($histories as $history) { $fieldName = $history->field; $history->fieldLabel = isset($this->lang->{$objectType}) && isset($this->lang->{$objectType}->{$fieldName}) ? $this->lang->{$objectType}->{$fieldName} : $fieldName; if($objectType == 'module') $history->fieldLabel = $this->lang->tree->{$fieldName}; + if($objectType == 'doctemplate') $history->fieldLabel = zget($this->lang->docTemplate, $fieldName, zget($this->lang->doc, $fieldName, $fieldName)); if($fieldName == 'fileName') $history->fieldLabel = $this->lang->file->{$fieldName}; if(($length = strlen($history->fieldLabel)) > $maxLength) $maxLength = $length; $history->diff ? $historiesWithDiff[] = $history : $historiesWithoutDiff[] = $history; diff --git a/module/action/test/lib/action.unittest.class.php b/module/action/test/lib/action.unittest.class.php index c004e94e98..99eee273eb 100644 --- a/module/action/test/lib/action.unittest.class.php +++ b/module/action/test/lib/action.unittest.class.php @@ -431,26 +431,30 @@ class actionTest * @access public * @return array */ - public function computeBeginAndEndTest($period) + public function computeBeginAndEndTest($period, string $date, string $direction) { - $date = $this->objectModel->computeBeginAndEnd($period, '', ''); + $result = $this->objectModel->computeBeginAndEnd($period, $date, $direction); $today = date('Y-m-d'); $tomorrow = date('Y-m-d', strtotime('+1 days')); $yesterday = date('Y-m-d', strtotime('-1 days')); $twoDaysAgo = date('Y-m-d', strtotime('-2 days')); - if($period == 'all') return $date['begin'] == '1970-01-01' and $date['end'] == '2030-01-01'; - if($period == 'today') return $date['begin'] == $today and $date['end'] == $tomorrow; - if($period == 'yesterday') return $date['begin'] == $yesterday and $date['end'] == $today; - if($period == 'twodaysago') return $date['begin'] == $twoDaysAgo and $date['end'] == $yesterday; - if($period == 'latest3days') return $date['begin'] == $twoDaysAgo and $date['end'] == $tomorrow; - if($period == 'thismonth') return $date == date::getThisMonth(); - if($period == 'lastmonth') return $date == date::getLastMonth(); + if(!empty($date) && empty($direction)) return $result['begin'] == '2025-04-23' && $result['end'] == '2025-04-23'; + + if($direction == 'pre') return $result['begin'] == '2025-04-23' && $result['end'] == '2030-01-01'; + if($direction == 'next') return $result['begin'] == '1970-01-01' && $result['end'] == '2025-04-23'; + if($period == 'all') return $result['begin'] == '2025-01-01' && $result['end'] == '2030-01-01'; + if($period == 'today') return $result['begin'] == $today && $result['end'] == $tomorrow; + if($period == 'yesterday') return $result['begin'] == $yesterday && $result['end'] == $today; + if($period == 'twodaysago') return $result['begin'] == $twoDaysAgo && $result['end'] == $yesterday; + if($period == 'latest3days') return $result['begin'] == $twoDaysAgo && $result['end'] == $tomorrow; + if($period == 'thismonth') return $result == date::getThisMonth(); + if($period == 'lastmonth') return $result == date::getLastMonth(); $func = "get$period"; extract(date::$func()); - if($period == 'thisweek') return $date['begin'] == $begin and $date['end'] == $end; - if($period == 'lastweek') return $date['begin'] == $begin and $date['end'] == $end; + if($period == 'thisweek') return $result['begin'] == $begin && $result['end'] == $end; + if($period == 'lastweek') return $result['begin'] == $begin && $result['end'] == $end; } /** @@ -811,4 +815,21 @@ class actionTest $history = $this->objectModel->dao->select('*')->from(TABLE_HISTORY)->where('id')->eq($historyID)->fetch(); return $this->objectModel->processHistory($history); } + + /** + * 渲染每一个action的历史记录。 + * Render histories of every action. + * + * @param string $objectType + * @param int $historyID + * @access public + * @return string + */ + public function renderChangesTest(string $objectType, int $historyID = 0): string + { + $histories = $this->objectModel->dao->select('*')->from(TABLE_HISTORY)->where('id')->eq($historyID)->fetchAll('id', false); + $content = $this->objectModel->renderChanges($objectType, $histories, true); + $content = str_replace("\n", '', $content); + return $content; + } } diff --git a/module/action/test/model/computebeginandend.php b/module/action/test/model/computebeginandend.php index d2ef7346cd..4a20706434 100755 --- a/module/action/test/model/computebeginandend.php +++ b/module/action/test/model/computebeginandend.php @@ -19,19 +19,27 @@ cid=1 - 测试计算lastweek的日期 @1 - 测试计算thismonth的日期 @1 - 测试计算lastmonth的日期 @1 +- 测试返回具体的日期 @1 +- 测试返回具体日期的结束日期 @1 +- 测试返回具体日期的开始日期 @1 */ -$typeList = array('all', 'today', 'yesterday', 'twodaysago', 'latest3days', 'thisweek', 'lastweek', 'thismonth', 'lastmonth'); +$typeList = array('all', 'today', 'yesterday', 'twodaysago', 'latest3days', 'thisweek', 'lastweek', 'thismonth', 'lastmonth'); +$dateList = array('', '2025-04-23'); +$directionList = array('', 'pre', 'next'); $action = new actionTest(); -r($action->computeBeginAndEndTest($typeList[0])) && p() && e('0'); // 测试计算all的日期 -r($action->computeBeginAndEndTest($typeList[1])) && p() && e('1'); // 测试计算today的日期 -r($action->computeBeginAndEndTest($typeList[2])) && p() && e('1'); // 测试计算yesterday的日期 -r($action->computeBeginAndEndTest($typeList[3])) && p() && e('1'); // 测试计算twodaysago的日期 -r($action->computeBeginAndEndTest($typeList[4])) && p() && e('1'); // 测试计算latest3days的日期 -r($action->computeBeginAndEndTest($typeList[5])) && p() && e('1'); // 测试计算thisweek的日期 -r($action->computeBeginAndEndTest($typeList[6])) && p() && e('1'); // 测试计算lastweek的日期 -r($action->computeBeginAndEndTest($typeList[7])) && p() && e('1'); // 测试计算thismonth的日期 -r($action->computeBeginAndEndTest($typeList[8])) && p() && e('1'); // 测试计算lastmonth的日期 \ No newline at end of file +r($action->computeBeginAndEndTest($typeList[0], $dateList[0], $directionList[0])) && p() && e('1'); // 测试计算all的日期 +r($action->computeBeginAndEndTest($typeList[1], $dateList[0], $directionList[0])) && p() && e('1'); // 测试计算today的日期 +r($action->computeBeginAndEndTest($typeList[2], $dateList[0], $directionList[0])) && p() && e('1'); // 测试计算yesterday的日期 +r($action->computeBeginAndEndTest($typeList[3], $dateList[0], $directionList[0])) && p() && e('1'); // 测试计算twodaysago的日期 +r($action->computeBeginAndEndTest($typeList[4], $dateList[0], $directionList[0])) && p() && e('1'); // 测试计算latest3days的日期 +r($action->computeBeginAndEndTest($typeList[5], $dateList[0], $directionList[0])) && p() && e('1'); // 测试计算thisweek的日期 +r($action->computeBeginAndEndTest($typeList[6], $dateList[0], $directionList[0])) && p() && e('1'); // 测试计算lastweek的日期 +r($action->computeBeginAndEndTest($typeList[7], $dateList[0], $directionList[0])) && p() && e('1'); // 测试计算thismonth的日期 +r($action->computeBeginAndEndTest($typeList[8], $dateList[0], $directionList[0])) && p() && e('1'); // 测试计算lastmonth的日期 +r($action->computeBeginAndEndTest($typeList[0], $dateList[1], $directionList[0])) && p() && e('1'); // 测试返回具体的日期 +r($action->computeBeginAndEndTest($typeList[0], $dateList[1], $directionList[1])) && p() && e('1'); // 测试返回具体日期的结束日期 +r($action->computeBeginAndEndTest($typeList[0], $dateList[1], $directionList[2])) && p() && e('1'); // 测试返回具体日期的开始日期 diff --git a/module/action/test/model/renderchanges.php b/module/action/test/model/renderchanges.php new file mode 100755 index 0000000000..d123eb91c1 --- /dev/null +++ b/module/action/test/model/renderchanges.php @@ -0,0 +1,36 @@ +#!/usr/bin/env php +renderChanges(); +timeout=0 +cid=1 + +- 测试type为空 @0 +- 测试bug解决方案历史记录 @修改了 解决方案,旧值为 "oldValue",新值为 "newValue"。
+- 测试bug解决版本历史记录 @修改了 解决版本,旧值为 "oldValue",新值为 "newValue"。
+- 测试bug解决日期历史记录 @修改了 解决日期,旧值为 "oldValue",新值为 "newValue"。
+- 测试产品状态历史记录 @修改了 状态,旧值为 "oldValue",新值为 "newValue"。
+ +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/action.unittest.class.php'; + +su('admin'); +zenData('action')->gen(10); + +$history = zenData('history'); +$history->old->range('oldValue'); +$history->oldValue->range('oldValue'); +$history->new->range('newValue'); +$history->newValue->range('newValue'); +$history->gen(10); + +$action = new actionTest(); +r($action->renderChangesTest('')) && p('') && e('0'); // 测试type为空 +r($action->renderChangesTest('bug', 1)) && p('') && e('修改了 解决方案,旧值为 "oldValue",新值为 "newValue"。
'); // 测试bug解决方案历史记录 +r($action->renderChangesTest('bug', 2)) && p('') && e('修改了 解决版本,旧值为 "oldValue",新值为 "newValue"。
'); // 测试bug解决版本历史记录 +r($action->renderChangesTest('bug', 3)) && p('') && e('修改了 解决日期,旧值为 "oldValue",新值为 "newValue"。
'); // 测试bug解决日期历史记录 +r($action->renderChangesTest('product', 4)) && p('') && e('修改了 状态,旧值为 "oldValue",新值为 "newValue"。
'); // 测试产品状态历史记录 diff --git a/module/action/ui/comment.html.php b/module/action/ui/comment.html.php index c1c33eefea..6ba25b98d1 100644 --- a/module/action/ui/comment.html.php +++ b/module/action/ui/comment.html.php @@ -24,6 +24,6 @@ form ( set::name('actioncomment') ), - $objectType != 'story' ? fileSelector() : null, + !in_array($objectType, array('story', 'doctemplate')) ? fileSelector() : null, set::actions($actions) ); diff --git a/module/action/zen.php b/module/action/zen.php index 5ef416d50d..c1e2101fcb 100644 --- a/module/action/zen.php +++ b/module/action/zen.php @@ -92,8 +92,9 @@ class actionZen extends action } else { - $module = $trash->objectType == 'case' ? 'testcase' : $trash->objectType; - $params = $trash->objectType == 'user' ? "account={$trash->objectName}" : "id={$trash->objectID}"; + $module = $trash->objectType == 'case' ? 'testcase' : $trash->objectType; + $module = $trash->objectType == 'doctemplate' ? 'doc' : $trash->objectType; + $params = $trash->objectType == 'user' ? "account={$trash->objectName}" : "id={$trash->objectID}"; $methodName = 'view'; if($module == 'basicmeas') { @@ -111,11 +112,6 @@ class actionZen extends action $params = "libID=0&moduelID=0&apiID={$trash->objectID}"; $methodName = 'index'; } - if($trash->objectType == 'doc') - { - $params = "docID={$trash->objectID}"; - if($trash->comment == 'chapter') $module = 'chapter'; - } if(in_array($module, array('traincourse','traincontents'))) { $methodName = $module == 'traincourse' ? 'viewcourse' : 'viewchapter'; diff --git a/module/admin/config.php b/module/admin/config.php index f205582b85..ae73d6c23c 100755 --- a/module/admin/config.php +++ b/module/admin/config.php @@ -10,7 +10,6 @@ $config->admin->menuGroup['company'] = array('dept', 'company', 'user', 'group $config->admin->menuGroup['switch'] = array('admin|setmodule'); $config->admin->menuGroup['model'] = array('auditcl', 'stage', 'deliverable', 'design', 'cmcl', 'reviewcl', 'custom|required', 'custom|set', 'custom|flow', 'custom|code', 'custom|percent','custom|estimate', 'custom|hours', 'subject', 'process', 'activity', 'zoutput', 'classify', 'holiday', 'reviewsetting', 'custom|project'); $config->admin->menuGroup['feature'] = array('custom|set', 'custom|product', 'custom|execution', 'custom|required', 'custom|kanban', 'measurement', 'meetingroom', 'custom|browsestoryconcept', 'custom|kanban', 'sqlbuilder', 'report', 'custom|limittaskdate', 'measurement'); -$config->admin->menuGroup['template'] = array('custom|set', 'baseline'); $config->admin->menuGroup['message'] = array('mail', 'webhook', 'sms', 'message'); $config->admin->menuGroup['dev'] = array('dev', 'entry', 'editor'); $config->admin->menuGroup['extension'] = array('extension'); @@ -21,7 +20,6 @@ $config->admin->menuModuleGroup['model']['custom|set'] = array('project', $config->admin->menuModuleGroup['model']['custom|required'] = array('project', 'build'); $config->admin->menuModuleGroup['feature']['custom|set'] = array('todo', 'block', 'epic', 'requirement', 'story', 'task', 'bug', 'testcase', 'testtask', 'feedback', 'user', 'ticket'); $config->admin->menuModuleGroup['feature']['custom|required'] = array('bug', 'doc', 'product', 'epic', 'requirement', 'story', 'productplan', 'release', 'task', 'testcase', 'testsuite', 'testtask', 'testreport', 'caselib', 'doc', 'feedback', 'user', 'execution'); -$config->admin->menuModuleGroup['template']['custom|set'] = array('baseline'); if($config->vision == 'lite') { $config->admin->menuModuleGroup['model']['custom|set'] = array(); @@ -80,7 +78,6 @@ $config->admin->helpURL['company'] = 'https://www.zentao.net/book/zentaopms/38 $config->admin->helpURL['switch'] = 'https://www.zentao.net/book/zentaopms/38.html'; $config->admin->helpURL['model'] = 'https://www.zentao.net/book/zentaopms/533.html'; $config->admin->helpURL['feature'] = 'https://www.zentao.net/book/zentaopms/38.html'; -$config->admin->helpURL['template'] = 'https://www.zentao.net/book/zentaopms/38.html'; $config->admin->helpURL['message'] = 'https://www.zentao.net/book/zentaopms/email-notification-541.html'; $config->admin->helpURL['extension'] = 'https://www.zentao.net/book/zentaopms/536.html'; $config->admin->helpURL['dev'] = 'https://www.zentao.net/book/zentaopms/537.html'; @@ -94,7 +91,6 @@ $config->admin->navsGroup['feature']['execution'] = ',execution,task,'; $config->admin->navsGroup['feature']['project'] = ',project,story,'; $config->admin->navsGroup['feature']['qa'] = ',bug,testcase,testsuite,testtask,testreport,caselib,'; $config->admin->navsGroup['model']['common'] = ',project,build,issue,risk,opportunity,nc,'; -$config->admin->navsGroup['template']['type'] = ',baseline,'; if($config->vision == 'lite') $config->admin->navsGroup['feature']['my'] .= 'task,'; global $lang, $app; diff --git a/module/admin/lang/menu.php b/module/admin/lang/menu.php index 8acf9d1353..94e9e7d1f5 100644 --- a/module/admin/lang/menu.php +++ b/module/admin/lang/menu.php @@ -22,10 +22,6 @@ $lang->admin->menuList->feature['name'] = $lang->admin->menuSetting['feature'][ $lang->admin->menuList->feature['desc'] = $lang->admin->menuSetting['feature']['desc']; $lang->admin->menuList->feature['order'] = 25; -$lang->admin->menuList->template['name'] = $lang->admin->menuSetting['template']['name']; -$lang->admin->menuList->template['desc'] = $lang->admin->menuSetting['template']['desc']; -$lang->admin->menuList->template['order'] = 30; - $lang->admin->menuList->message['name'] = $lang->admin->menuSetting['message']['name']; $lang->admin->menuList->message['desc'] = $lang->admin->menuSetting['message']['desc']; $lang->admin->menuList->message['order'] = 35; @@ -216,7 +212,6 @@ if($config->edition != 'max' and $config->edition != 'ipd') unset($lang->admin->menuList->model['subMenu']['agileplus']); unset($lang->admin->menuList->model['menuOrder']['10']); unset($lang->admin->menuList->model['menuOrder']['20']); - unset($lang->admin->menuList->template); } if(!helper::hasFeature('waterfall')) { diff --git a/module/bug/ui/browse.html.php b/module/bug/ui/browse.html.php index cdb78add24..3845dca15b 100644 --- a/module/bug/ui/browse.html.php +++ b/module/bug/ui/browse.html.php @@ -268,6 +268,7 @@ if($isFromDoc) if($key == 'assignedTo') $cols[$key]['type'] = 'user'; if($key == 'pri') $cols[$key]['priList'] = $lang->bug->priList; if($key == 'severity') $cols[$key]['severityList'] = $lang->bug->severityList; + if($key == 'title') $cols[$key]['link'] = array('url' => createLink('bug', 'view', "bugID={id}"), 'data-toggle' => 'modal', 'data-size' => 'lg'); } } diff --git a/module/bug/zen.php b/module/bug/zen.php index c955b48d72..0b3a2576f4 100644 --- a/module/bug/zen.php +++ b/module/bug/zen.php @@ -1187,13 +1187,14 @@ class bugZen extends bug protected function assignVarsForEdit(object $bug): void { /* Add product related to the bug when it is not in the products. */ + $product = $this->loadModel('product')->fetchByID($bug->product); + if(!isset($this->products[$bug->product])) { $this->products[$bug->product] = $product->name; $this->view->products = $this->products; } - $product = $this->loadModel('product')->fetchByID($bug->product); if(empty($product->shadow)) { $products = $this->view->products; diff --git a/module/caselib/ui/browse.html.php b/module/caselib/ui/browse.html.php index dc207eda82..a35147b70c 100644 --- a/module/caselib/ui/browse.html.php +++ b/module/caselib/ui/browse.html.php @@ -85,6 +85,7 @@ if($isFromDoc) $cols[$key]['sortType'] = false; if(isset($col['link'])) unset($cols[$key]['link']); if($key == 'pri') $cols[$key]['priList'] = $lang->testcase->priList; + if($key == 'title') $cols[$key]['link'] = array('url' => createLink('caselib', 'viewCase', "caseID={id}&version={version}"), 'data-toggle' => 'modal', 'data-size' => 'lg'); } } diff --git a/module/common/lang/de.php b/module/common/lang/de.php index 15232c0ca6..153b7b19f7 100644 --- a/module/common/lang/de.php +++ b/module/common/lang/de.php @@ -410,6 +410,7 @@ $lang->doc->productSpace = "{$lang->productCommon} Space"; $lang->doc->projectSpace = "{$lang->projectCommon} Space"; $lang->doc->apiSpace = 'API Space'; $lang->doc->teamSpace = 'Team Space'; +$lang->doc->template = 'Document Template'; $lang->product->list = $lang->productCommon . ' List'; $lang->product->kanban = $lang->productCommon . ' Kanban'; diff --git a/module/common/lang/en.php b/module/common/lang/en.php index 6f5be48544..56fa4e8f48 100644 --- a/module/common/lang/en.php +++ b/module/common/lang/en.php @@ -410,6 +410,7 @@ $lang->doc->productSpace = "{$lang->productCommon} Space"; $lang->doc->projectSpace = "{$lang->projectCommon} Space"; $lang->doc->apiSpace = 'API Space'; $lang->doc->teamSpace = 'Team Space'; +$lang->doc->template = 'Document Template'; $lang->product->list = $lang->productCommon . ' List'; $lang->product->kanban = $lang->productCommon . ' Kanban'; diff --git a/module/common/lang/fr.php b/module/common/lang/fr.php index a0cd8f2634..64e3d06246 100644 --- a/module/common/lang/fr.php +++ b/module/common/lang/fr.php @@ -410,6 +410,7 @@ $lang->doc->productSpace = "{$lang->productCommon} Space"; $lang->doc->projectSpace = "{$lang->projectCommon} Space"; $lang->doc->apiSpace = 'API Space'; $lang->doc->teamSpace = 'Team Space'; +$lang->doc->template = 'Document Template'; $lang->product->list = $lang->productCommon . ' List'; $lang->product->kanban = $lang->productCommon . ' Kanban'; diff --git a/module/common/lang/zh-cn.php b/module/common/lang/zh-cn.php index cf7062aad4..ad1c368c91 100644 --- a/module/common/lang/zh-cn.php +++ b/module/common/lang/zh-cn.php @@ -410,6 +410,7 @@ $lang->doc->productSpace = "{$lang->productCommon}空间"; $lang->doc->projectSpace = "{$lang->projectCommon}空间"; $lang->doc->apiSpace = '接口空间'; $lang->doc->teamSpace = '团队空间'; +$lang->doc->template = '文档模板'; $lang->product->list = $lang->productCommon . '列表'; $lang->product->kanban = $lang->productCommon . '看板'; diff --git a/module/common/model.php b/module/common/model.php index 54b4de4fff..ec6015c06b 100644 --- a/module/common/model.php +++ b/module/common/model.php @@ -1178,6 +1178,7 @@ eof; if($module == 'product' and $method == 'browse' and !empty($this->app->params['storyType']) and $this->app->params['storyType'] != 'story') $method = $this->app->params['storyType']; if($module == 'productplan' && ($method == 'story' || $method == 'bug')) $method = 'view'; + if($module == 'doc' && $method == 'edittemplate') $method = 'createtemplate'; $openMethods = array( 'user' => array('deny', 'logout'), @@ -2311,7 +2312,7 @@ eof; * @access public * @return string */ - public static function processMarkdown(string $markdown): string + public static function processMarkdown(string $markdown): string|bool { if(empty($markdown)) return false; diff --git a/module/common/test/model/setmenuvarsex.php b/module/common/test/model/setmenuvarsex.php index 01e3e3d53d..b4c1d805e9 100755 --- a/module/common/test/model/setmenuvarsex.php +++ b/module/common/test/model/setmenuvarsex.php @@ -11,6 +11,9 @@ cid=1 - 查看替换后的菜单链接 @/execution-task-1 - 查看替换后的菜单链接属性link @/task-create-1 +- 查看替换后的菜单链接属性link @/task-edit-1 +- 查看替换后的菜单链接属性link @/task-delete-1 +- 查看替换后的菜单链接属性link @/task-view-1 */ @@ -23,4 +26,22 @@ $menu = array(); $menu['link'] = "/task-create-%s"; $menu = common::setMenuVarsEx($menu, 1); -r($menu) && p('link') && e('/task-create-1'); // 查看替换后的菜单链接 \ No newline at end of file +r($menu) && p('link') && e('/task-create-1'); // 查看替换后的菜单链接 + +$menu = array(); +$menu['link'] = "/task-edit-%s"; +$menu = common::setMenuVarsEx($menu, 1); + +r($menu) && p('link') && e('/task-edit-1'); // 查看替换后的菜单链接 + +$menu = array(); +$menu['link'] = "/task-delete-%s"; +$menu = common::setMenuVarsEx($menu, 1); + +r($menu) && p('link') && e('/task-delete-1'); // 查看替换后的菜单链接 + +$menu = array(); +$menu['link'] = "/task-view-%s"; +$menu = common::setMenuVarsEx($menu, 1); + +r($menu) && p('link') && e('/task-view-1'); // 查看替换后的菜单链接 diff --git a/module/design/model.php b/module/design/model.php index 3afe3d9718..711998df5a 100755 --- a/module/design/model.php +++ b/module/design/model.php @@ -320,16 +320,16 @@ class designModel extends model * 获取设计列表数据。 * Get design list. * - * @param int $productID - * @param int $projectID - * @param string $type all|bySearch|HLDS|DDS|DBDS|ADS - * @param int $param - * @param string $orderBy - * @param int $pager + * @param int|array $productID + * @param int|array $projectID + * @param string $type all|bySearch|HLDS|DDS|DBDS|ADS + * @param int $param + * @param string $orderBy + * @param int $pager * @access public * @return object[] */ - public function getList(int $projectID = 0, int $productID = 0, string $type = 'all', int $param = 0, string $orderBy = 'id_desc', object $pager = null): array + public function getList(int|array $projectID = 0, int|array $productID = 0, string $type = 'all', int $param = 0, string $orderBy = 'id_desc', object $pager = null): array { if(common::isTutorialMode()) return $this->loadModel('tutorial')->getDesigns(); @@ -341,9 +341,9 @@ class designModel extends model { $designs = $this->dao->select('*')->from(TABLE_DESIGN) ->where('deleted')->eq(0) - ->beginIF($projectID)->andWhere('project')->eq($projectID)->fi() + ->beginIF($projectID)->andWhere('project')->in($projectID)->fi() ->beginIF($type != 'all')->andWhere('type')->in($type)->fi() - ->beginIF($productID)->andWhere('product')->eq($productID)->fi() + ->beginIF($productID)->andWhere('product')->in(is_numeric($productID) ? "0,$productID" : array_merge($productID, array(0)))->fi() ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); diff --git a/module/doc/config.php b/module/doc/config.php index 576dcc83e8..b6d724d3a1 100644 --- a/module/doc/config.php +++ b/module/doc/config.php @@ -5,12 +5,14 @@ $config->doc = new stdclass(); $config->doc->createlib = new stdclass(); $config->doc->editlib = new stdclass(); $config->doc->create = new stdclass(); +$config->doc->createTemplate = new stdclass(); $config->doc->edit = new stdclass(); $config->doc->showfiles = new stdclass(); $config->doc->createlib->requiredFields = 'name'; $config->doc->editlib->requiredFields = 'name'; $config->doc->create->requiredFields = 'lib,title'; +$config->doc->createTemplate->requiredFields = 'lib,title'; $config->doc->edit->requiredFields = 'lib,title'; $config->doc->customObjectLibs = 'files,customFiles'; @@ -124,6 +126,24 @@ $config->doc->quickMenu['editedby'] = array('id' => 4, 'name' => $lang->doc->my $config->doc->zentaoListMenuPosition = 22; +$config->doc->templateTypeParents = array(); +$config->doc->templateTypeParents['PP'] = 'plan'; +$config->doc->templateTypeParents['QAP'] = 'plan'; +$config->doc->templateTypeParents['CMP'] = 'plan'; +$config->doc->templateTypeParents['ITP'] = 'plan'; +$config->doc->templateTypeParents['STP'] = 'plan'; +$config->doc->templateTypeParents['ERS'] = 'story'; +$config->doc->templateTypeParents['URS'] = 'story'; +$config->doc->templateTypeParents['SRS'] = 'story'; +$config->doc->templateTypeParents['HLDS'] = 'design'; +$config->doc->templateTypeParents['DDS'] = 'design'; +$config->doc->templateTypeParents['DBDS'] = 'design'; +$config->doc->templateTypeParents['ADS'] = 'design'; +$config->doc->templateTypeParents['Code'] = 'dev'; +$config->doc->templateTypeParents['ITTC'] = 'test'; +$config->doc->templateTypeParents['STTC'] = 'test'; +$config->doc->templateTypeParents['UM'] = 'desc'; + $config->doc->zentaoList = array(); $config->doc->zentaoList['story'] = array('key' => 'story', 'name' => $lang->doc->zentaoList['story'] . $lang->doc->list, 'icon' => 'lightbulb', 'subMenu' => array(), 'priv' => 'storyBrowse'); $config->doc->zentaoList['task'] = array('key' => 'task', 'name' => $lang->doc->zentaoList['task'] . $lang->doc->list, 'icon' => 'check-sign', 'module' => 'execution', 'method' => 'task', 'params' => 'execution=0&status=unclosed¶m=0&orderBy=&recTotal=0&recPerPage=100&pageID=1&from=doc', 'priv' => 'taskBrowse'); @@ -144,6 +164,7 @@ $config->doc->zentaoList['bug']['subMenu'][] = array('key' => 'planBug', 'nam $config->doc->zentaoList['more']['subMenu'][] = array('key' => 'productPlan', 'name' => $lang->doc->zentaoList['productPlan'] . $lang->doc->list, 'icon' => 'productplan', 'module' => 'productplan', 'method' => 'browse', 'params' => 'productID=0&branch=&browseType=undone&queryID=0&orderBy=begin_desc&recTotal=0&recPerPage=20&pageID=1&from=doc', 'priv' => 'productplanBrowse'); $config->doc->zentaoList['more']['subMenu'][] = array('key' => 'productRelease', 'name' => $lang->doc->zentaoList['productRelease'] . $lang->doc->list, 'icon' => 'send', 'module' => 'release', 'method' => 'browse', 'params' => 'productID=0&branch=all&type=all&orderBy=¶m=0&recTotal=0&recPerPage=20&pageID=1&from=doc', 'priv' => 'releaseBrowse'); +$config->doc->zentaoList['more']['subMenu'][] = array('key' => 'projectRelease', 'name' => $lang->doc->zentaoList['projectRelease'] . $lang->doc->list, 'icon' => 'send', 'module' => 'projectRelease', 'method' => 'browse', 'params' => 'projectID=0&executionID=0&type=all&orderBy=&recTotal=0&recPerPage=20&pageID=1&from=doc', 'priv' => 'projectReleaseBrowse'); $config->doc->zentaoList['more']['subMenu'][] = array('key' => 'ER', 'name' => $lang->doc->zentaoList['ER'] . $lang->doc->list, 'icon' => 'lightbulb-alt', 'module' => 'product', 'method' => 'browse', 'params' => 'productID=0&branch=all&browseType=¶m=0&storyType=epic&orderBy=&recTotal=0&recPerPage=20&pageID=1&projectID=0&from=doc', 'priv' => 'epicBrowse'); $config->doc->zentaoList['more']['subMenu'][] = array('key' => 'UR', 'name' => $lang->doc->zentaoList['UR'] . $lang->doc->list, 'icon' => 'customer', 'module' => 'product', 'method' => 'browse', 'params' => 'productID=0&branch=all&browseType=¶m=0&storyType=requirement&orderBy=&recTotal=0&recPerPage=20&pageID=1&projectID=0&from=doc', 'priv' => 'requirementBrowse'); @@ -153,6 +174,8 @@ if(in_array($config->edition, array('biz', 'max', 'ipd'))) $config->doc->zentaoList['more']['subMenu'][] = array('key' => 'ticket', 'name' => $lang->doc->zentaoList['ticket'] . $lang->doc->list, 'icon' => 'support-ticket', 'module' => 'ticket', 'method' => 'browse', 'params' => 'browseType=wait¶m=0&orderBy=id_desc&recTotal=0&recPerPage=20&pageID=1&from=doc', 'priv' => 'ticketBrowse'); } +if(in_array($config->edition, array('max', 'ipd'))) $config->doc->zentaoList['gantt'] = array('key' => 'gantt', 'name' => $lang->doc->zentaoList['gantt'], 'icon' => 'gantt', 'isModal' => true, 'module' => 'programPlan', 'method' => 'browse', "params" => 'projectID=0&productID=0&type=gantt&orderBy=id_asc&baselineID=0&browseType=&queryID=0&from=doc'); + //$config->doc->zentaoList['storyView'] = array('key' => 'storyView', 'name' => $lang->doc->zentaoList['story'] . $lang->doc->detail, 'icon' => 'lightbulb', 'priv' => 'storyView'); //$config->doc->zentaoList['taskView'] = array('key' => 'taskView', 'name' => $lang->doc->zentaoList['task'] . $lang->doc->detail, 'icon' => 'check-sign', 'priv' => 'taskView'); //$config->doc->zentaoList['caseView'] = array('key' => 'caseView', 'name' => $lang->doc->zentaoList['case'] . $lang->doc->detail, 'icon' => 'testcase', 'priv' => 'caseView'); diff --git a/module/doc/config/form.php b/module/doc/config/form.php index 05555da6b9..95d0095430 100644 --- a/module/doc/config/form.php +++ b/module/doc/config/form.php @@ -63,6 +63,9 @@ $config->doc->form->create['template'] = array('type' => 'string', 'requir $config->doc->form->create['templateType'] = array('type' => 'string', 'required' => false, 'default' => ''); $config->doc->form->create['chapterType'] = array('type' => 'string', 'required' => false, 'default' => ''); +$config->doc->form->createtemplate = $config->doc->form->create; +$config->doc->form->createtemplate['templateDesc'] = array('type' => 'string', 'required' => false, 'default' => ''); + $config->doc->form->edit['title'] = array('type' => 'string', 'required' => true, 'default' => '', 'filter' => 'trim'); $config->doc->form->edit['product'] = array('type' => 'int', 'required' => false, 'default' => 0); $config->doc->form->edit['project'] = array('type' => 'int', 'required' => false, 'default' => 0); @@ -85,6 +88,9 @@ $config->doc->form->edit['mailto'] = array('type' => 'array', 'required' $config->doc->form->edit['editedBy'] = array('type' => 'string', 'required' => false, 'default' => ''); $config->doc->form->edit['editedDate'] = array('type' => 'datetime', 'required' => false, 'default' => $now); +$config->doc->form->edittemplate = $config->doc->form->edit; +$config->doc->form->edittemplate['templateDesc'] = array('type' => 'string', 'required' => false, 'default' => ''); + $config->doc->form->movelib['space'] = array('type' => 'string', 'required' => true, 'default' => ''); $config->doc->form->movelib['acl'] = array('type' => 'string', 'required' => true, 'default' => ''); $config->doc->form->movelib['groups'] = array('type' => 'array', 'required' => false, 'default' => '', 'filter' => 'join'); @@ -99,8 +105,17 @@ $config->doc->form->movedoc['readGroups'] = array('type' => 'array', 'require $config->doc->form->movedoc['readUsers'] = array('type' => 'array', 'required' => false, 'default' => '', 'filter' => 'join'); $config->doc->form->movedoc['parent'] = array('type' => 'int', 'required' => false, 'default' => 0); +$config->doc->form->movetemplate['lib'] = array('type' => 'int', 'required' => true, 'default' => ''); +$config->doc->form->movetemplate['module'] = array('type' => 'int', 'required' => true, 'default' => ''); +$config->doc->form->movetemplate['parent'] = array('type' => 'int', 'required' => false, 'default' => 0); +$config->doc->form->movetemplate['acl'] = array('type' => 'string', 'required' => true, 'default' => ''); + $config->doc->form->batchmovedoc['lib'] = array('type' => 'int', 'required' => true, 'default' => 0); $config->doc->form->batchmovedoc['module'] = array('type' => 'int', 'required' => false, 'default' => 0); $config->doc->form->batchmovedoc['acl'] = array('type' => 'string', 'required' => true, 'default' => ''); $config->doc->form->batchmovedoc['groups'] = array('type' => 'array', 'required' => false, 'default' => '', 'filter' => 'join'); $config->doc->form->batchmovedoc['users'] = array('type' => 'array', 'required' => false, 'default' => '', 'filter' => 'join'); + +$config->doc->form->addTemplateType['name'] = array('type' => 'string', 'required' => true, 'default' => '', 'filter' => 'trim'); +$config->doc->form->addTemplateType['root'] = array('type' => 'int', 'required' => true, 'default' => 1); +$config->doc->form->addTemplateType['parent'] = array('type' => 'int', 'required' => false, 'default' => 0); diff --git a/module/doc/control.php b/module/doc/control.php index f6b3bf117a..86e6551d34 100755 --- a/module/doc/control.php +++ b/module/doc/control.php @@ -145,14 +145,15 @@ class doc extends control * 禅道数据列表。 * Zentao data list. * - * @param string $type - * @param int $blockID - * @param string $dataType view|markdown|html + * @param string $type + * @param int|string $blockID * @access public * @return void */ - public function zentaoList(string $type, int $blockID) + public function zentaoList(string $type, int|string $blockID) { + if(is_string($blockID)) $blockID = (int)str_replace('__TML_ZENTAOLIST__', '', $blockID); + $blockData = $this->doc->getZentaoList($blockID); if(!$blockData) { @@ -164,17 +165,56 @@ class doc extends control return $this->sendError($this->lang->notFound); } - $this->app->loadClass('pager', true); + $doc = $this->doc->getByID($blockData->doc); + $this->view->isTemplate = !empty($doc->templateType) && $blockData->extra != 'fromReview'; - $this->view->title = sprintf($this->lang->doc->insertTitle, $this->lang->doc->zentaoList[$type]); - $this->view->type = $type; - $this->view->settings = $blockData->settings; - $this->view->idList = $blockData->content->idList; - $this->view->cols = $blockData->content->cols; - $this->view->data = $blockData->content->data; - $this->view->users = $this->loadModel('user')->getPairs('noletter|pofirst|nodeleted'); - $this->view->blockID = $blockID; - $this->view->pager = new pager(count($this->view->data), 10); + if($this->view->isTemplate) + { + $this->view->title = sprintf($this->lang->doc->insertTitle, $this->lang->docTemplate->zentaoList[$type]); + $this->view->type = $type; + $this->view->blockID = $blockID; + $this->view->settings = $blockData->settings; + $this->view->searchTab = zget($blockData->content, 'searchTab', ''); + + if($type == 'productCase' || $type == 'projectCase') $this->view->caseStage = $blockData->content->caseStage; + + return $this->display(); + } + + $fromTemplate = $blockData->extra == 'fromTemplate' || $blockData->extra == 'fromReview'; + if($fromTemplate) + { + $this->view->title = sprintf($this->lang->doc->insertTitle, $this->lang->docTemplate->zentaoList[$type]); + $this->view->searchTab = zget($blockData->content, 'searchTab', ''); + $this->view->isSetted = isset($blockData->content->data) && isset($blockData->content->cols) || isset($blockData->content->ganttOptions); + if($type == 'productCase' || $type == 'projectCase') $this->view->caseStage = $blockData->content->caseStage; + } + else + { + $this->view->title = sprintf($this->lang->doc->insertTitle, $this->lang->doc->zentaoList[$type]); + } + + if($type == 'gantt') + { + $this->view->ganttData = (array)zget($blockData->content, 'ganttOptions', array()); + $this->view->ganttFields = (array)zget($blockData->content, 'ganttFields', array()); + $this->view->showFields = zget($blockData->content, 'showFields', ''); + } + else + { + $this->app->loadClass('pager', true); + + $this->view->idList = (array)zget($blockData->content, 'idList', array()); + $this->view->cols = (array)zget($blockData->content, 'cols', array()); + $this->view->data = (array)zget($blockData->content, 'data', array()); + $this->view->pager = new pager(count($this->view->data), 10); + } + + $this->view->type = $type; + $this->view->settings = $blockData->settings; + $this->view->users = $this->loadModel('user')->getPairs('noletter|pofirst'); + $this->view->blockID = $blockID; + $this->view->fromTemplate = $fromTemplate; if(strpos(',productStory,ER,UR,planStory,projectStory', $type) !== false) $this->docZen->assignStoryGradeData($type); @@ -193,6 +233,8 @@ class doc extends control * Export Zentao data list. * * @param int $blockID + * @access public + * @return void */ public function ajaxExportZentaoList(int $blockID) { @@ -208,7 +250,9 @@ class doc extends control * 构建禅道数据列表。 * Build Zentao data list. * + * @param int $docID * @param string $type + * @param int $oldBlockID * @access public * @return void */ @@ -218,13 +262,20 @@ class doc extends control $docblock->doc = $docID; $docblock->type = $type; $docblock->settings = $this->post->url; - $docblock->content = json_encode(array('cols' => json_decode($this->post->cols), 'data' => json_decode($this->post->data), 'idList' => $this->post->idList)); + if($type == 'gantt') + { + $docblock->content = json_encode(array('ganttOptions' => json_decode($this->post->ganttOptions), 'showFields' => $this->post->showFields, 'ganttFields' => json_decode($this->post->ganttFields))); + } + else + { + $docblock->content = json_encode(array('cols' => json_decode($this->post->cols), 'data' => json_decode($this->post->data), 'idList' => $this->post->idList)); + } $this->dao->insert(TABLE_DOCBLOCK)->data($docblock)->exec(); - $newBlockID = $this->dao->lastInsertId(); - if(dao::isError()) return print(json_encode(array('result' => 'fail', 'message' => dao::getError()))); + $newBlockID = $this->dao->lastInsertId(); + return print(json_encode(array('result' => 'success', 'oldBlockID' => $oldBlockID, 'newBlockID' => $newBlockID))); } @@ -423,10 +474,10 @@ class doc extends control $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); - $this->view->libID = $libID; + $this->view->lib = $lib; + $this->view->groups = $this->loadModel('group')->getPairs(); + $this->view->users = $this->user->getPairs('noletter|noclosed', $lib->users); + $this->view->libID = $libID; } /** @@ -491,6 +542,204 @@ class doc extends control return $this->send(array('result' => 'success', 'load' => $browseLink, 'app' => $this->app->tab)); } + /** + * 文档模板列表。 + * Browse template list. + * + * @param int $libID + * @param string $type + * @param int $docID + * @param string $orderBy + * @param int $recPerPage + * @param int $pageID + * @access public + * @return void + */ + public function browseTemplate(int $libID = 0, string $type = 'all', int $docID = 0, string $orderBy = 'id_desc', int $recPerPage = 20, int $pageID = 1, string $mode = 'home') + { + $this->lang->doc->menu->template['alias'] .= ',' . $this->app->rawMethod; + $libModules = $this->doc->getTemplateModules($libID); + if($mode == 'create' && empty($libModules)) return $this->send(array('result' => 'success', 'load' => array('alert' => $this->lang->docTemplate->createTypeFirst))); + + $templateList = $this->doc->getDocTemplateList(0, $type, $orderBy); + $templateList = $this->doc->filterDeletedDocs($templateList); + $templateList = $this->doc->filterPrivDocs($templateList, 'template'); + $allModules = $this->doc->getTemplateModules(); + $allModules = array_column($allModules, 'fullName', 'id'); + foreach($templateList as $template) $template->moduleName = zget($allModules, $template->module); + + $projectReviewTemplates = $this->loadModel('setting')->getItem('vision=rnd&owner=system&module=doc&key=projectReviewDocTemplate'); + + $this->view->title = $this->lang->doc->template; + $this->view->libID = $libID; + $this->view->users = $this->loadModel('user')->getPairs('noclosed,noletter'); + $this->view->templateList = $templateList; + $this->view->docID = $docID; + $this->view->orderBy = $orderBy; + $this->view->recPerPage = $recPerPage; + $this->view->pageID = $pageID; + $this->view->mode = $mode; + $this->view->hasModules = count($libModules) ? true : false; + $this->view->scopes = $this->doc->getTemplateScopes(); + $this->view->projectReviewTemplates = json_decode($projectReviewTemplates, true); + $this->display(); + } + + /** + * 创建一个模板类型。 + * Add a template type. + * + * @param int $scope + * @param int|string $parentModule + * @access public + * @return void + */ + public function addTemplateType(int $scope, int|string $parentModule = '') + { + $moduleList = $this->doc->getTemplateModules($scope, '1'); + + if(!empty($_POST)) + { + $this->lang->doc->name = $this->lang->docTemplate->typeName; + $moduleData = form::data($this->config->doc->form->addTemplateType) + ->setDefault('type', 'docTemplate') + ->setDefault('path', '') + ->get(); + $moduleData->grade = $moduleData->parent === 0 ? 1 : 2; + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + + $this->doc->addTemplateType($moduleData); + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + + return $this->docZen->responseAfterAddTemplateType($scope); + } + + $moduleItems = array(); + if($parentModule === 0) + { + $moduleItems[] = array('value' => 0, 'text' => '/'); + } + else + { + foreach($moduleList as $module) $moduleItems[] = array('value' => $module->id, 'text' => '/' . $module->name); + $moduleItems[] = array('value' => 0, 'text' => '/'); + } + + $scopeList = $this->doc->getTemplateScopes(); + + $this->view->scope = $scope; + $this->view->scopes = $this->doc->getScopeItems($scopeList); + $this->view->moduleItems = $moduleItems; + $this->view->parentModule = $parentModule === '' ? current($moduleItems) : $parentModule; + $this->display(); + } + + /** + * 删除一个模板类型。 + * Delete a template type. + * + * @param int $moduleID + * @access public + * @return void + */ + public function deleteTemplateType(int $moduleID) + { + $templates = $this->doc->getTemplatesByType($moduleID); + if(empty($templates)) + { + $this->dao->update(TABLE_MODULE)->set('deleted')->eq('1')->where('id')->eq($moduleID)->andWhere('type')->eq('docTemplate')->exec(); + $this->dao->update(TABLE_MODULE)->set('deleted')->eq('1')->where('parent')->eq($moduleID)->andWhere('type')->eq('docTemplate')->exec(); + return $this->sendSuccess(array('load' => true)); + } + else + { + return $this->send(array('result' => 'fail', 'message' => $this->lang->docTemplate->errorDeleteType)); + } + } + + /** + * 编辑一个模板类型。 + * Edit a template type. + * + * @param int $moduleID + * @access public + * @return void + */ + public function editTemplateType(int $moduleID) + { + echo $this->fetch('tree', 'edit', "moduleID={$moduleID}&type=docTemplate"); + } + + /** + * 创建一个文档模板。 + * Create a doc template. + * + * @param int $moduleID + * @access public + * @return void + */ + public function createTemplate(int $moduleID = 0) + { + if(!empty($_POST)) + { + $this->lang->doc->title = $_POST['type'] == 'chapter' ? $this->lang->doc->chapterName : $this->lang->docTemplate->title; + + helper::setcookie('lastDocModule', $moduleID); + $docData = form::data() + ->setDefault('addedBy', $this->app->user->account) + ->setDefault('editedBy', $this->app->user->account) + ->get(); + + if(!empty($docData->parent)) + { + $parentTemplate = $this->doc->fetchByID((int)$docData->parent); + $docData->module = $parentTemplate->module; + $docData->lib = (int)$parentTemplate->lib; + $docData->acl = $parentTemplate->acl; + $docData->templateType = $parentTemplate->templateType; + } + + $docResult = $this->doc->create($docData); + if(!$docResult || dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + return $this->docZen->responseAfterCreate($docResult, 'docTemplate'); + } + } + + /** + * 编辑一个文档。 + * Edit a doc. + * + * @param int $docID + * @access public + * @return void + */ + public function editTemplate(int $docID) + { + $doc = $this->doc->getByID($docID); + if(!empty($_POST)) + { + $changes = $files = array(); + $docData = form::data() + ->setDefault('editedBy', $this->app->user->account) + ->setIF(!isset($_POST['parent']), 'parent', $doc->parent) + ->setIF(strpos(",$doc->editedList,", ",{$this->app->user->account},") === false, 'editedList', $doc->editedList . ",{$this->app->user->account}") + ->setIF($this->post->type == 'chapter', 'content', $doc->content) + ->setIF($this->post->type == 'chapter', 'rawContent', $doc->rawContent) + ->get(); + + $result = $this->doc->update($docID, $docData); + if(dao::isError()) + { + if(!empty(dao::$errors['lib']) || !empty(dao::$errors['keywords'])) return $this->send(array('result' => 'fail', 'message' => dao::getError(), 'callback' => "zui.Modal.open({id: 'modalBasicInfo'});")); + return $this->send(array('result' => 'fail', 'message' => dao::getError())); + } + + $changes = $result['changes']; + $files = $result['files']; + return $this->docZen->responseAfterEditTemplate($doc, $changes, $files); + } + } + /** * 上传文档。 * Upload docs. @@ -612,7 +861,7 @@ class doc extends control $docData->module = $parentDoc->module; } - $docResult = $this->doc->create($docData, $this->post->labels); + $docResult = $this->doc->create($docData); if(!$docResult || dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); return $this->docZen->responseAfterCreate($docResult); } @@ -772,6 +1021,28 @@ class doc extends control return $this->sendSuccess(array('load' => $this->session->docList ? $this->session->docList : $this->createLink('doc', 'index'))); } + /** + * 删除一个文档模板。 + * Delete a doctemplate. + * + * @param int $templateID + * @access public + * @return void + */ + public function deleteTemplate(int $templateID) + { + $this->doc->deleteTemplate($templateID); + + /* Delete template files. */ + $template = $this->doc->getByID($templateID); + if($template->files) $this->doc->deleteFiles(array_keys($template->files)); + + if(dao::isError()) $this->send(array('result' => 'fail', 'message' => dao::getError())); + + $link = $this->createLink('doc', 'browseTemplate'); + return $this->sendSuccess(array('load' => $link)); + } + /** * 收藏一个文档。 * Collect a doc. @@ -842,6 +1113,27 @@ class doc extends control return print(json_encode(array('libs' => $libItems, 'modules' => $moduleItems))); } + /** + * + * AJAX: 获取范围下的类型。 + * Ajax get types of scope. + * + * @param int $libID + * @access public + * @return json + */ + public function ajaxGetScopeTypes(int $libID = 0) + { + $optionMenu = $this->loadModel('tree')->getOptionMenu($libID, 'docTemplate', 0, 'all', 'nodeleted', 'all'); + $types = array(); + foreach($optionMenu as $menuID => $menu) + { + if(empty($menuID)) continue; + $types[] = array('text' => $menu, 'value' => $menuID, 'keys' => $menu); + } + return print(json_encode($types)); + } + /** * AJAX: 通过空间类型获取文档库。 * AJAX: Get libs by type. @@ -1024,6 +1316,12 @@ class doc extends control } unset($_SESSION["doc_{$doc->id}_nopriv"]); + if($doc->templateType) + { + echo $this->fetch('doc', 'browseTemplate', "libID=$doc->lib&type=all&docID=$docID&orderBy=id_desc&recPerPage=20&pageID=1&mode=view"); + return; + } + $lib = $this->doc->getLibByID((int)$doc->lib); $objectType = isset($lib->type) ? $lib->type : 'custom'; if($objectType == 'api') @@ -1621,6 +1919,75 @@ class doc extends control $this->display(); } + /** + * 移动文档模板 + * Move document template. + * + * @param int $docID + * @access public + * @return void + */ + public function moveTemplate(int $docID) + { + $doc = $this->doc->getByID($docID); + if(!empty($_POST)) + { + $data = form::data() + ->setIF(!isset($_POST['lib']), 'lib', $doc->lib) + ->setIF(!isset($_POST['module']), 'module', $doc->module) + ->setIF(!isset($_POST['parent']), 'parent', $doc->parent) + ->setIF(!isset($_POST['acl']), 'acl', $doc->acl) + ->get(); + $changes = common::createChanges($doc, $data); + if($changes) + { + $basicInfoChanged = false; + foreach($changes as $change) + { + if(in_array($change['field'], array('module', 'lib', 'acl'))) $basicInfoChanged = true; + } + $this->doc->doUpdateDoc($docID, $data, $basicInfoChanged); + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + + $actionID = $this->loadModel('action')->create('docTemplate', $docID, 'Moved', '', json_encode(array('from' => $doc->lib, 'to' => $data->lib))); + $this->action->logHistory($actionID, $changes); + } + + $newDoc = $this->doc->getByID($docID); + if(!$this->doc->checkPrivDoc($newDoc)) + { + $link = $this->createLink('doc', 'browseTemplate', "libID={$doc->lib}&type=all&docID=0&orderBy=id_desc&recPerPage=20&pageID=1&mode=list"); + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => $link, 'doc' => $newDoc)); + } + + $docAppAction = array('executeCommand', 'handleMovedDoc', array($docID, '1', $data->lib)); + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'docApp' => $docAppAction)); + } + + if(!empty($doc->parent)) + { + $parentDoc = $this->doc->fetchByID($doc->parent); + $chapterAndDocs = $this->doc->getDocsOfLibs(array($doc->lib), 'template', $docID, true); + if(!isset($chapterAndDocs[$doc->parent])) $chapterAndDocs[$doc->parent] = $parentDoc; + + $parentPath = trim($parentDoc->path, ',') . ","; + $topTemplate = substr($parentPath, 0, strpos($parentPath, ',')) ?: $doc->parent; + foreach($chapterAndDocs as $key => $template) + { + if(strpos(",{$template->path},", ",{$topTemplate},") === false) unset($chapterAndDocs[$key]); + } + $this->view->chapterAndDocs = $this->doc->buildNestedDocs($chapterAndDocs); + } + + $scopeList = $this->doc->getTemplateScopes(); + + $this->view->scopeItems = $this->doc->getScopeItems($scopeList); + $this->view->doc = $doc; + $this->view->docID = $docID; + $this->view->modules = $this->loadModel('tree')->getOptionMenu((int)$doc->lib, 'docTemplate', 0, 'all', 'nodeleted', 'all'); + $this->display(); + } + /** * Batch move document. * @@ -1885,22 +2252,43 @@ class doc extends control * @access public * @return void */ - public function ajaxGetSpaceData(string $type = 'custom', int $spaceID = 0, string $picks = '') + public function ajaxGetSpaceData(string $type = 'custom', int $spaceID = 0, string $picks = '', int $libID = 0) { $this->doc->setMenuByType($type, (int)$spaceID, 0); - $noPicks = empty($picks); - $picks = $noPicks ? '' : ",$picks,"; + if($type === 'template') + { + $modules = $this->doc->getTemplateModules(); + $moduleNames = array_column($modules, 'fullName', 'id'); + $templateList = $this->doc->getDocTemplateList(0, 'all'); + $templateList = $this->doc->filterDeletedDocs($templateList); + $templateList = $this->doc->filterPrivDocs($templateList, 'template'); + foreach($templateList as $template) + { + $template->moduleName = zget($moduleNames, $template->module); + } + $data = array('spaceID' => $spaceID, 'docs' => array_values($templateList)); + $data['spaces'][] = array('name' => $this->lang->doc->template, 'id' => $spaceID); + $data['modules'] = $modules; - list($spaces, $spaceID) = $this->doc->getSpaces($type, $spaceID); - $data = array('spaceID' => (int)$spaceID); - $libs = $this->doc->getLibsOfSpace($type, $spaceID); - $libIds = array_keys($libs); + $scopeList = $this->doc->getTemplateScopes(); + foreach($scopeList as $scope) $data['libs'][] = array('id' => $scope->id, 'name' => $scope->name, 'space' => $spaceID); + } + else + { + $noPicks = empty($picks); + $picks = $noPicks ? '' : ",$picks,"; - if($noPicks || strpos($picks, ',space,') !== false) $data['spaces'] = $spaces; - if($noPicks || strpos($picks, ',lib,') !== false) $data['libs'] = array_values($libs); - if($noPicks || strpos($picks, ',module,') !== false) $data['modules'] = array_values($this->doc->getModulesOfLibs($libIds)); - if($noPicks || strpos($picks, ',doc,') !== false) $data['docs'] = array_values($this->doc->getDocsOfLibs($libIds + array($spaceID), $type)); + list($spaces, $spaceID) = $this->doc->getSpaces($type, $spaceID); + $data = array('spaceID' => (int)$spaceID); + $libs = $this->doc->getLibsOfSpace($type, $spaceID); + $libIds = array_keys($libs); + + if($noPicks || strpos($picks, ',space,') !== false) $data['spaces'] = $spaces; + if($noPicks || strpos($picks, ',lib,') !== false) $data['libs'] = array_values($libs); + if($noPicks || strpos($picks, ',module,') !== false) $data['modules'] = array_values($this->doc->getModulesOfLibs($libIds)); + if($noPicks || strpos($picks, ',doc,') !== false) $data['docs'] = array_values($this->doc->getDocsOfLibs($libIds + array($spaceID), $type)); + } $this->send($data); } @@ -1927,7 +2315,7 @@ class doc extends control $doc = $this->doc->getByID($docID, $version); $doc->lib = (int)$doc->lib; $doc->module = (int)$doc->module; - $doc->privs = array('edit' => common::hasPriv('doc', 'edit', $doc)); + $doc->privs = array('edit' => common::hasPriv('doc', 'edit', $doc) && $doc->acl == 'open'); $doc->editors = $this->doc->getEditors($docID); $doc->draft = $this->doc->getDraft($docID); @@ -2037,15 +2425,27 @@ class doc extends control if($modalType == 'doc') { $title = $parentID ? $this->lang->doc->addSubDoc : $this->lang->doc->create; + if($objectType == 'template') $title = $parentID ? $this->lang->docTemplate->addSubDocTemplate : $this->lang->docTemplate->create; if($isDraft == 'no') $title = $this->lang->settings; } if($modalType == 'chapter') $title = $isCreate ? $this->lang->doc->addChapter : $this->lang->doc->editChapter; - $chapterAndDocs = $this->doc->getDocsOfLibs(array($libID), $objectType, $docID); - $modulePairs = empty($libID) || $modalType == 'chapter' ? array() : $this->loadModel('tree')->getOptionMenu($libID, 'doc', 0); + $chapterAndDocs = $this->doc->getDocsOfLibs(array($libID), $objectType, $docID, $objectType == 'template' ? true : false); + if($objectType == 'template' && !empty($moduleID)) $chapterAndDocs = array_filter($chapterAndDocs, fn($docInfo) => $docInfo->module == $moduleID); if(isset($doc) && !empty($doc->parent) && !isset($chapterAndDocs[$doc->parent])) $chapterAndDocs[$doc->parent] = $this->doc->fetchByID($doc->parent); - $chapterAndDocs = $this->doc->buildNestedDocs($chapterAndDocs, $modulePairs); - $this->view->chapterAndDocs = $chapterAndDocs; + $modulePairs = empty($libID) || $modalType == 'chapter' || $objectType == 'template' ? array() : $this->loadModel('tree')->getOptionMenu($libID, 'doc', 0); + + if($objectType == 'template' && !empty($parentID)) + { + $parentDoc = $this->doc->fetchByID($parentID); + $parentPath = trim($parentDoc->path, ',') . ","; + $topTemplate = substr($parentPath, 0, strpos($parentPath, ',')) ?: $parentID; + foreach($chapterAndDocs as $key => $template) + { + if(strpos(",{$template->path},", ",{$topTemplate},") === false) unset($chapterAndDocs[$key]); + } + } + $this->view->chapterAndDocs = $this->doc->buildNestedDocs($chapterAndDocs, $modulePairs); if($isCreate) { @@ -2056,7 +2456,7 @@ class doc extends control } if(empty($objectID) && $lib) $objectID = $this->doc->getObjectIDByLib($lib); - if($lib) $objectType = $lib->type; + if($lib && $objectType != 'template') $objectType = $lib->type; $unclosed = strpos($this->config->doc->custom->showLibs, 'unclosed') !== false ? 'unclosedProject' : ''; $libPairs = $this->doc->getLibs($objectType, "{$unclosed}", $libID, $objectID); @@ -2074,7 +2474,7 @@ class doc extends control $moduleID = (int)$doc->module; $libID = (int)$doc->lib; $lib = $this->doc->getLibByID($libID); - $objectType = $lib->type; + if($lib && $objectType != 'template') $objectType = $lib->type; $objectID = $this->doc->getObjectIDByLib($lib); $libPairs = $this->doc->getLibs($objectType, '', $libID, $objectID); @@ -2085,10 +2485,19 @@ class doc extends control $this->view->optionMenu = $this->loadModel('tree')->getOptionMenu($libID, 'doc', 0); } + if($objectType == 'template') + { + $scopeList = $this->doc->getTemplateScopes(); + $this->view->scopeItems = $this->doc->getScopeItems($scopeList); + } + else + { + $this->view->users = $this->user->getPairs('nocode|noclosed|nodeleted'); + $this->view->groups = $this->loadModel('group')->getPairs(); + } + $this->view->docID = $docID; $this->view->mode = empty($docID) ? 'create' : 'edit'; - $this->view->users = $this->user->getPairs('nocode|noclosed|nodeleted'); - $this->view->groups = $this->loadModel('group')->getPairs(); $this->view->libID = $libID; $this->view->moduleID = $moduleID; $this->view->objectID = $objectID; @@ -2137,4 +2546,71 @@ class doc extends control $user = $this->doc->getUserByConfluenceUserID($username); return $this->send(array('result' => 'success', 'data'=> $user)); } + + /** + * Manage scope. + * 维护范围。 + * + * @access public + * @return void + */ + public function manageScope() + { + $scopeList = $this->doc->getTemplateScopes(); + + if(!empty($_POST)) + { + $scopes = $this->post->scopes; + + $oldScopes = $newScopes = array(); + foreach($scopes as $id => $name) + { + if(strpos((string)$id, 'id') !== false) + { + $scopeID = str_replace('id', '', $id); + $oldScopes[$scopeID] = $name; + } + else + { + $newScopes[$id] = $name; + } + } + + $deletedScopes = array_diff(array_keys($scopeList), array_keys($oldScopes)); + if(!empty($deletedScopes)) $this->doc->deleteTemplateScopes($deletedScopes); + if(dao::isError()) return $this->sendError(array('message' => dao::getError())); + + if(!empty($oldScopes)) $this->doc->updateTemplateScopes($oldScopes); + if(dao::isError()) return $this->sendError(array('message' => dao::getError())); + + if(!empty($newScopes)) $this->doc->insertTemplateScopes($newScopes); + if(dao::isError()) return $this->sendError(array('message' => dao::getError())); + + return $this->sendSuccess(array('closeModal' => true, 'load' => true)); + } + + $this->view->scopeList = $scopeList; + $this->display(); + } + + /** + * AJAX: Judge can be deleted scope. + * AJAX: 判断范围是否可以被删除。 + * + * @param int $scopeID + * @access public + * @return array + */ + public function ajaxJudgeCanBeDeleted(int $scopeID = 0) + { + if(empty($scopeID)) return $this->sendSuccess(array('message' => 'success')); + + $templates = $this->doc->getScopeTemplates(array($scopeID)); + if(!empty($templates[$scopeID])) return $this->sendError($this->lang->docTemplate->scopeHasTemplateTips); + + $modules = $this->doc->getTemplateModules($scopeID); + if(!empty($modules)) return $this->sendError($this->lang->docTemplate->scopeHasModuleTips); + + return $this->sendSuccess(array('message' => 'success')); + } } diff --git a/module/doc/css/zentaolist.ui.css b/module/doc/css/zentaolist.ui.css index e69de29bb2..000271ecc1 100644 --- a/module/doc/css/zentaolist.ui.css +++ b/module/doc/css/zentaolist.ui.css @@ -0,0 +1 @@ +.config-tip{background-color: var(--color-surface-light); color: var(--color-gray-400)} diff --git a/module/doc/js/addtemplatetype.ui.js b/module/doc/js/addtemplatetype.ui.js new file mode 100644 index 0000000000..69ba9f4a00 --- /dev/null +++ b/module/doc/js/addtemplatetype.ui.js @@ -0,0 +1,5 @@ +window.changeScope = function(e) +{ + const scope = e.target.value; + loadModal($.createLink('doc', 'addTemplateType', `scope=${scope}`)); +} diff --git a/module/doc/js/browsetemplate.ui.js b/module/doc/js/browsetemplate.ui.js new file mode 100644 index 0000000000..dc28031cc5 --- /dev/null +++ b/module/doc/js/browsetemplate.ui.js @@ -0,0 +1,686 @@ +const savingDocData = {}; +let docBasicModal = {}; +let currentUser = null; +/** + * 获取文档界面上的表格初始化选项。 + * Get the table initialization options on the doc UI. + * + * @param {object} options + * @param {object} info + * @returns {object} + */ +function getTableOptions(options, info) +{ + const lang = getDocAppLang(); + + options.data.sort((a, b) => a.addedDate > b.addedDate ? -1 : 1); + + templateCols = []; + templateCols.push({...options.cols.find(col => col.name === 'rawID'), type: 'id'}); + + let title = options.cols.find(col => col.name == 'title'); + let addedBy = options.cols.find(col => col.name == 'addedBy'); + let addedDate = options.cols.find(col => col.name == 'addedDate'); + let editedBy = options.cols.find(col => col.name == 'editedBy'); + let editedDate = options.cols.find(col => col.name == 'editedDate'); + let actionsCol = options.cols.find(col => col.name == 'actions'); + let typeCol = {name: 'moduleName', title: lang.tableCols.type, type: 'string', sort: true}; + let viewsCol = {name: 'views', title: lang.tableCols.views, type: 'number', sort: true}; + let descCol = {name: 'templateDesc', title: lang.tableCols.desc, type: 'desc', sort: true} + + title.title = lang.tableCols.title; + addedBy.title = lang.tableCols.addedBy; + addedDate.title = lang.tableCols.addedDate; + editedBy.title = lang.tableCols.editedBy; + editedDate.title = lang.tableCols.editedDate; + actionsCol.title = lang.tableCols.actions; + + templateCols.push(title); + templateCols.push(typeCol); + templateCols.push(addedBy); + templateCols.push(addedDate); + templateCols.push(editedBy); + templateCols.push(editedDate); + templateCols.push(viewsCol); + templateCols.push(descCol); + templateCols.push(actionsCol); + + options.cols = templateCols; + options.cols.forEach(col => + { + if(col.name === 'actions' && col.actionsMap) + { + col.actions = col.actions.filter(action => (action !== 'move' && docAppHasPriv(action))); + delete col.actionsMap.move; + + const privMap = {edit: docAppHasPriv('edit'), delete: docAppHasPriv('delete')}; + const actionHints = {edit: lang.editTemplate, delete: lang.deleteTemplate}; + $.each(col.actionsMap, (key, value) => + { + if(typeof actionHints[key] === 'string') value.hint = actionHints[key]; + value.disabled = !privMap[key]; + }); + } + }); + + options.checkable = false; + + options.footer = options.footer.filter(f => f !== 'checkbox'); + options.footer = options.footer.filter(f => f !== 'checkedInfo'); + + return options; +} + +function mergeDocFormData(doc, formData) +{ + if(!doc || !formData) return; + const keys = new Set(formData.keys()); + for(const key of keys) + { + const values = formData.getAll(key); + if(!values.length) continue; + if(key === 'module' || key === 'lib') doc[key] = +values[0]; + else doc[key] = values.length > 1 ? values : values[0]; + } + return doc; +} + +function showDocBasicModal(parentID, docID, isDraft, modalType = 'doc') +{ + const docApp = getDocApp(); + const spaceID = docApp.spaceID; + const libID = docApp.libID; + const moduleID = docApp.moduleID; + const url = $.createLink('doc', 'setDocBasic', `objectType=template&objectID=${spaceID}&libID=${libID}&moduleID=${moduleID}&parentID=${parentID || 0}&docID=${docID || 0}&isDraft=${isDraft ? 'yes' : 'no'}&modalType=${modalType}`); + + if(docBasicModal.doc === docID && docBasicModal.current) + { + docBasicModal.current.show(); + } + else + { + if(docBasicModal.current) docBasicModal.current.destroy(); + docBasicModal.doc = docID; + zui.Modal.open( + { + ref : docBasicModal, + url : url, + destroyOnHide: false, + cache : true, + request: { + error: error => { + if(docBasicModal.current) docBasicModal.current.hide(); + showSaveFailedAlert(error); + } + }, + onHidden: () => { + if(!docBasicModal.keepOnHide && docBasicModal.current) docBasicModal.current.destroy(); + }, + $onDestroy: () => { + if(docBasicModal.current) docBasicModal = {}; + } + }); + } + return new Promise((resolve) => {window.docBasicModalResolver = resolve;}); +} + +function showDocSettingModal(_, args) +{ + const docApp = getDocApp(); + const doc = docApp.doc; + const docID = args[0] || doc.id; + const docType = args[1] || doc.contentType; + const saveEdited = args[2] || 0; + showDocBasicModal(doc.data.parent, docID).then(formData => { + savingDocData[docID] = formData; + if(saveEdited == 1) + { + const docData = { + content : doc.data.content, + status : doc.data.status, + contentType: doc.data.contentType, + type : doc.data.type, + lib : doc.data.lib, + module : doc.data.module, + title : doc.data.title, + keywords : doc.data.keywords, + acl : doc.data.acl, + space : doc.data.space, + uid : doc.data.uid, + }; + mergeDocFormData(docData, formData); + } + }); +} + +/** + * 向服务器提交新文档。 + * Submit new doc to server. + * + * @param {object} doc + * @param {number} spaceID + * @param {number} libID + * @param {number} moduleID + * @param {FormData} formData + * @returns + */ +function submitNewDoc(doc, spaceID, libID, moduleID, formData, afterCreate) +{ + const docApp = getDocApp(); + + if(docApp.isSavingDoc) return; + docApp.isSavingDoc = true; + $(docApp.element).find('[zui-command^="saveDoc"],[zui-command^="saveNewDoc"]').attr('disabled', true); + + const spaceType = docApp.signals.spaceType.value; + const module = docApp.treeMap.modules.get(formData && moduleID === 0 ? parseInt(formData.get('module')) : moduleID); + const url = $.createLink('doc', 'createTemplate', `libID=${libID}&moduleID=${moduleID}`); + const docData = + { + rawContent : doc.content, + content : doc.html, + status : doc.status || 'normal', + contentType : doc.contentType, + type : doc.type || 'text', + lib : libID, + module : moduleID, + title : doc.title, + keywords : doc.keywords, + acl : 'private', + space : spaceType, + project : 0, + templateType: module && module.data ? module.data.short : '', + uid : (doc.uid || `doc${doc.id}`), + parent : doc.parent + }; + if(formData) mergeDocFormData(docData, formData); + docApp.props.fetcher = $.createLink('doc', 'ajaxGetSpaceData', `type=template&spaceID=${spaceID}&picks={picks}&libID=${libID}`); + const getErrorMessage = (res) => { + if(typeof res.message === 'string') return res.message; + if(typeof res.message === 'object' && res.message && Object.values(res.message).length) + { + for(const x of Object.values(res.message)) + { + if(typeof x === 'string') return x; + if(Array.isArray(x) && x.length) return x[0]; + } + } + if(typeof data.error === 'string') return data.error; + return getLang('errorOccurred'); + }; + return new Promise((resolve, reject) => + { + $.post(url, docData, (res) => + { + try + { + const data = JSON.parse(res); + if(!checkResponse(data)) return; + if(typeof data !== 'object' || data.result === 'fail') + { + throw new Error(getErrorMessage(data)); + } + const newDoc = $.extend(doc, {id: data.id}, docData, data.doc, {status: doc.status || data.status}); + if (!newDoc.space.includes('.')) { + newDoc.space = `${spaceType}.${docData[spaceType]}`; + } + resolve(newDoc); + if(afterCreate) + { + docApp.load(null, null, null, {noLoading: true, picks: 'doc'}).then(() => { + afterCreate(newDoc); + }); + } + } + catch (error) + { + showSaveFailedAlert(error); + reject(error); + } + }).fail(error => { + resolve(false); + showSaveFailedAlert(error); + }).complete(() => { + docApp.isSavingDoc = false; + $(docApp.element).find('[zui-command^="saveDoc"],[zui-command^="saveNewDoc"]').removeAttr('disabled'); + }); + }); +} + +/** + * 向服务器提交编辑文档内容。 + * Submit edit doc to server. + * + * @param {FormData} formData + * @access public + * @return void + */ +function submitEditDoc(formData) +{ + const docApp = getDocApp(); + const doc = docApp.doc.data; + const url = $.createLink('doc', 'editTemplate', `templateID=${doc.id}`); + if(formData) mergeDocFormData(doc, formData); + + return new Promise((resolve) => { + $.post(url, doc, (res) => + { + try + { + const data = JSON.parse(res); + if(!checkResponse(data)) return; + if(typeof data !== 'object' || data.result === 'fail') + { + let message = data.message || data.error || getLang('errorOccurred'); + if(typeof message === 'object') message = Object.values(message).map(x => Array.isArray(x) ? x.join('\n') : x).join('\n'); + throw new Error(message); + } + resolve(doc); + } + catch (error) + { + resolve(false); + showSaveFailedAlert(error); + if(!error.message) return; + } + }).fail(error => { + resolve(false); + showSaveFailedAlert(error); + }).complete(() => { + docApp.load(null, null, null, {noLoading: true, picks: 'doc'}); + }); + }); +} + +/** + * 处理创建文档的操作请求,向服务器发送请求并返回创建的文档对象。 + * Handle the create doc operation request, send a request to the server and return the created doc object. + */ +function handleCreateDoc(doc, spaceID, libID, moduleID) +{ + return showDocBasicModal(0, 0, doc.status === 'draft').then((formData) => { + moduleID = parseInt(formData.get('module')); + return submitNewDoc(doc, spaceID, libID, moduleID, formData); + }); +} + +/** + * 处理保存文档的操作请求,向服务器发送请求并返回保存的文档对象。 + * Handle the save doc operation request, send a request to the server and return the saved doc object. + */ +function handleSaveDoc(doc) +{ + const docApp = getDocApp(); + const spaceType = docApp.signals.spaceType.value; + const libID = docApp.signals.libID.value; + const moduleID = docApp.signals.moduleID.value; + const url = $.createLink('doc', 'editTemplate', `docID=${doc.id}`); + const docData = { + rawContent : doc.content, + status : doc.status || 'normal', + contentType: doc.contentType, + type : 'text', + lib : libID, + module : moduleID, + title : doc.title, + keywords : doc.keywords, + acl : doc.acl, + content : doc.html, + space : spaceType, + uid : (doc.uid || `doc${doc.id}`), + }; + + const docAppData = docApp.doc.data; + if (doc.id === docAppData.id) docData.mailto = docAppData.mailto; + + if(savingDocData[doc.id]) + { + mergeDocFormData(docData, savingDocData[doc.id]); + delete savingDocData[doc.id]; + } + + return new Promise((resolve) => { + $.post(url, docData, (res) => + { + try + { + const data = JSON.parse(res); + if(!checkResponse(data)) return; + if(typeof data !== 'object' || data.result === 'fail') + { + let message = data.message || data.error || getLang('errorOccurred'); + if(typeof message === 'object') message = Object.values(message).map(x => Array.isArray(x) ? x.join('\n') : x).join('\n'); + throw new Error(message); + } + + doc = $.extend({}, doc, docData, data.doc); + const libID = +doc.lib; + const lib = docApp.getLib(libID); + if(!lib) + { + resolve(false); + return loadPage($.createLink('doc', 'view', `docID=${doc.id}`)); + } + + delete doc.title; + delete doc.keywords; + delete doc.content; + resolve(doc); + } + catch (error) + { + resolve(false); + if(!error.message) return; + zui.Modal.alert(error.message); + } + }).fail(error => { + resolve(false); + showSaveFailedAlert(error); + }).complete(() => { + docApp.isSavingDoc = false; + docApp.props.fetcher = $.createLink('doc', 'ajaxGetSpaceData', `type=template&spaceID=${docAppData.space}&picks={picks}&libID=${docAppData.lib}`); + $(docApp.element).find('[zui-command^="saveDoc"],[zui-command^="saveNewDoc"]').removeAttr('disabled'); + const {id, acl, users, addedBy} = doc; + if(acl == 'private' && !users.includes(currentUser) && addedBy !== currentUser) + { + if(docApp.hasEditingDoc) + { + docApp.cancelEditDoc().then(() => { + docApp.delete('doc', id); + }); + } + else + { + docApp.delete('doc', id); + } + } + docApp.load(null, null, null, {noLoading: true, picks: 'doc'}); + }); + }); +} + +const customRenders = +{ + home: function() + { + const homeViewUrl = $.createLink('doc', 'browseTemplate', `libID=0&filterType=all&docID=0&orderBy=&recPerPage=20&pageID=1&mode=home`); + return {fetcher: homeViewUrl, clearBeforeLoad: false, className: 'doc-template-home h-full', class: 'h-full col',htmlRender: (element, props) => $(element).morphInner(`
${props.html}
`)}; + }, + + /** + * 定义 API 文档列表工具栏渲染。 + * Define the API doc list toolbar render. + */ + toolbar: function() + { + if(this.mode === 'list') + { + const items = []; + if(docAppHasPriv('create')) items.push({text: getDocAppLang('createTemplate'), icon: 'plus', btnType: 'primary', command: 'startCreateTemplate'}); + return {component: 'toolbar', props: {items: items}}; + } + }, + 'sidebar-footer-hint': function() + { + return ''; + } +} + +/** + * 定义文档模板各个视图和 UI 元素的上的操作方法。 + * Define the operation methods on the views and UI elements of the doc template. + */ +$.extend(window.docAppActions, +{ + /** + * 定义分组的操作按钮。 + * Define the actions on the type group. + */ + module: function(info) + { + const items = []; + const module = info.data; + const lang = getLang(); + + if(docAppHasPriv('addModule')) items.push({text: lang.actions.addSameModule, command: `addModule/${module.root}/${module.parent}`}); + if(module.grade == 1 && docAppHasPriv('editModule')) items.push({text: lang.actions.addSubModule, command: `addModule/${module.root}/${module.id}`}); + if(docAppHasPriv('editModule')) items.push({text: lang.actions.editModule, command: `editModule/${module.id}`}); + if(docAppHasPriv('deleteModule')) items.push({text: lang.actions.deleteModule, command: `deleteModule/${module.id}`}); + + if(info.ui === 'sidebar') + { + return [ + items.length ? {icon: 'ellipsis-v', caret: false, placement: 'bottom-end', size: 'xs', items: items} : null, + ]; + } + + return items; + }, + /** + * 定义文档库的操作按钮。 + * Define the actions of modules. + */ + lib: function(info) + { + const lang = getLang(); + const items = []; + const lib = info.data; + const libID = lib?.data == undefined ? lib?.id : lib?.data.id; + + /* 获取侧边栏没有模块时的操作按钮。 Get actions when sidebar no module. */ + if(info.ui === 'sidebar-no-module') + { + if(!docAppHasPriv('addModule')) return; + return [{text: lang.actions.addModule, command: `addModule/${libID}/0/${libID}/child`, icon: 'plus', type: 'primary-pale'}]; + } + + if(docAppHasPriv('addModule')) items.push({text: lang.actions.addModule, command: `addModule/${libID}/0/${libID}/child`}); + if(!items.length) return; + + return [{type: 'dropdown', icon: 'cog-outline', square: true, caret: false, placement: 'top-end', items: items}]; + }, + /** + * 定义文档编辑时的操作按钮。 + * Define the actions on toolbar of the doc editing page. + */ + 'doc-edit': function(info) + { + const doc = info.data; + if(!doc) return; + + const lang = getLang(); + const isToolbar = info.ui === 'toolbar'; + const docApp = this; + return [ + isToolbar ? {hint: docApp.fullscreen ? lang.exitFullscreen : lang.enterFullscreen, icon: docApp.fullscreen ? 'fullscreen-exit' : 'fullscreen', command: 'toggleFullscreen'} : null, + (isToolbar && docApp.props.showDocHistory !== false) ? {hint: lang.history, icon: 'history', command: 'toggleViewSideTab/history'} : null, + {text: lang.saveDraft, size: 'md', className: 'btn-wide', type: 'secondary', command: 'saveDoc/draft'}, + {text: lang.release, size: 'md', className: 'btn-wide', type: 'primary', command: 'saveDoc'}, + {text: lang.cancel, size: 'md', className: 'btn-wide', type: 'primary-outline', command: 'cancelEditDoc'}, + {text: lang.settings, size: 'md', type: 'ghost', command: `showDocSettingModal/${doc.id}/template/1`, icon: 'cog-outline'}, + ]; + }, + /** + * 定义模板列表的操作按钮。 + * Define the actions on the template list. + */ + 'doc-list': function(info) + { + const lang = getLang(); + const canCreateDoc = hasPriv('create'); + + /* 文档列表没有文档时的按钮。Actions for empty doc list. */ + if(info.ui === 'doc-list-empty') + { + return canCreateDoc ? [{icon: 'plus', text: lang.createTemplate, btnType: 'primary', command: 'startCreateTemplate'}] : null; + } + + const items = []; + if(canCreateDoc) items.push({text: lang.createTemplate, icon: 'plus', btnType: 'primary', command: 'startCreateTemplate'}); + return items.length ? {component: 'toolbar', props: {items: items}} : null; + } +}); + +/** + * 重写文档应用的配置选项方法。 + * Override the method to set the doc app options. + */ +window._setDocAppOptions = window.setDocAppOptions; // Save the original method. +window.setDocAppOptions = function(_, options) // Override the method. +{ + options = window._setDocAppOptions(_, options); + return $.extend(options, + { + onCreateDoc : handleCreateDoc, + onSaveDoc : handleSaveDoc, + getTableOptions : getTableOptions, + customRenders : $.extend(docAppCustomRenders, customRenders) + }); +}; + +/* 扩展文档模板命令定义。 Extend the doc app command definition. */ +$.extend(window.docAppCommands, +{ + startCreateTemplate: function(_, args) + { + const docApp = getDocApp(); + const hasModules = docApp.props.hasModules; + if(!hasModules) return zui.Modal.alert(getLang('createTypeFirst')); + + const {spaceID, libID, moduleID} = docApp; + return showDocBasicModal(args?.[0] ?? 0, 0, true, 'doc').then((formData) => { + zui.Editor.loadModule().then(() => { + const emptyDoc = zui.Editor.createEmptyDoc(); + const snapshot = zui.Editor.toSnapshot(emptyDoc); + const doc = {contentType: 'doc', status: 'draft', content: JSON.stringify(snapshot)}; + return submitNewDoc(doc, 0, libID, moduleID, formData, (newDoc) => { + docApp.selectDoc(newDoc.id); + docApp.startEditDoc(newDoc.id); + }); + }); + }); + }, + addChapter: function(_, args) + { + const docApp = getDocApp(); + const {spaceID, libID, moduleID} = docApp; + return showDocBasicModal(args[0], 0, false, 'chapter').then((formData) => { + const doc = {type: 'chapter', status: 'normal', parent: args[0]}; + return submitNewDoc(doc, spaceID, libID, moduleID, formData).then(() => { + docApp.load(null, null, null, {noLoading: false, picks: 'doc'}); + if(docBasicModal.current) + { + docBasicModal.current.destroy(); + docBasicModal = {}; + } + }); + }); + }, + editChapter: function(_, args) + { + const docApp = getDocApp(); + return showDocBasicModal(docApp.doc.data.parent, args[0], false, 'chapter').then((formData) => { + return submitEditDoc(formData); + }); + }, + deleteDoc: function(_, args) + { + const docApp = getDocApp(); + const docID = args[0] || docApp.docID; + const docInfo = docApp.treeMap.docs.get(docID) + const doc = docInfo.data; + const isChapter = doc?.type === 'chapter'; + + const reviewObjects = ['PP', 'SRS', 'HLDS', 'DDS', 'DBDS', 'ADS', 'ITTC', 'STTC']; + const templateType = doc.templateType; + const reviewTemplates = docApp.props.projectReviewTemplates; + const confirmTip = reviewObjects.includes(templateType) && reviewTemplates?.[templateType]?.[docID] ? 'confirmDeleteReviewTemplate' : 'confirmDelete'; + + $.ajaxSubmit( + { + confirm: getLang(isChapter ? (docInfo.docs.length > 0 ? 'confirmDeleteChapterWithSub' : 'confirmDeleteChapter') : (docInfo.docs.length > 0 ? 'confirmDeleteWithSub' : confirmTip)), + url: $.createLink('doc', 'deleteTemplate', `docID=${docID}`), + load: false, + onSuccess: function() + { + getDocApp().delete('doc', docID); + } + }); + }, + moveDoc: function(_, args) + { + const docApp = getDocApp(); + const docID = args[0] || docApp.docID; + const url = $.createLink('doc', 'moveTemplate', `docID=${docID}`); + zui.Modal.open({size: 'sm', url: url}); + }, + addModule: function(_, args) + { + const docApp = getDocApp(); + const scope = args[0]; + const parentModule = args[1]; + const url = $.createLink('doc', 'addTemplateType', `scope=${scope}&parentModule=${parentModule}`); + zui.Modal.open({size: 'sm', url: url}); + }, + editModule: function(_, args) + { + const docApp = getDocApp(); + const moduleID = args[0] || docApp.moduleID; + const url = $.createLink('doc', 'editTemplateType', `moduleID=${moduleID}`); + zui.Modal.open({size: 'sm', url: url}); + }, + deleteModule: function(_, args) + { + const docApp = getDocApp(); + const moduleID = args[0] || docApp.moduleID; + $.ajaxSubmit( + { + url: $.createLink('doc', 'deleteTemplateType', `moduleID=${moduleID}`), + load: false, + onSuccess: function() + { + getDocApp().delete('module', moduleID); + } + }) + }, + /** + * 编辑文档模板。 + * Edit doc template. + */ + editTemplate: function(_, args) + { + const docApp = getDocApp(); + const templateID = args[0] || docApp.docID; + const url = $.createLink('doc', 'editTemplate', `templateID=${templateID}`); + zui.Modal.open({size: 'sm', url: url}); + }, + /** + * 删除文档模板。 + * Delete doc template. + */ + deleteTemplate: function(_, args) + { + const docApp = getDocApp(); + const lang = getDocAppLang(); + const templateID = args[0] || docApp.docID; + $.ajaxSubmit( + { + confirm: lang.confirmDelete, + url: $.createLink('doc', 'deleteTemplate', `templateID=${templateID}`), + load:false, + onSunccess: function() + { + getDocApp().delete('doc', templateID); + } + }) + }, + showDocSettingModal: showDocSettingModal +}); + +window.clickTemplateCard = function(event, url) +{ + const target = $(event.target); + if(target.hasClass('icon') || target.hasClass('dropdown') || target.hasClass('toolbar')) return; + + openUrl(url); +} diff --git a/module/doc/js/common.ui.js b/module/doc/js/common.ui.js index dc49f20123..c62e1b8392 100644 --- a/module/doc/js/common.ui.js +++ b/module/doc/js/common.ui.js @@ -224,19 +224,43 @@ window.loadLibModules = function(e, docID) const libType = $(e.target.closest('div.form-group')).attr('data-lib-type'); const objectID = $("input[name='" + libType + "']").val(); - let docType = $('.radio-primary [name=type]:not(.hidden):checked').val(); - if(typeof docType == 'undefined') docType = 'doc'; + if(!docType) + { + docType = $('.radio-primary [name=type]:not(.hidden):checked').val(); + if(typeof docType == 'undefined') docType = 'doc'; + } const link = $.createLink('doc', 'ajaxGetModules', 'objectType=' + libType + '&objectID=' + objectID + '&type=' + docType + '&docID=' + docID + '&libID=' + libID); $.get(link, function(data) { data = JSON.parse(data); + if(docType == 'docTemplate') + { + data = data.filter(function(item) + { + return item.value != 0; + }); + } + const $modulePicker = $("[name='parent']").zui('picker'); $modulePicker.render({items: data.modules}); $modulePicker.$.setValue(''); }); } +window.loadScopeTypes = function(e) +{ + const libID = e.target.value; + const link = $.createLink('doc', 'ajaxGetScopeTypes', 'libID=' + libID); + $.get(link, function(data) + { + data = JSON.parse(data); + const $modulePicker = $("[name='module']").zui('picker'); + $modulePicker.render({items: data}); + $modulePicker.$.setValue(''); + }); +} + window.toggleWhiteList = function(e) { const acl = e.target.value; diff --git a/module/doc/js/managescope.ui.js b/module/doc/js/managescope.ui.js new file mode 100644 index 0000000000..57109deaea --- /dev/null +++ b/module/doc/js/managescope.ui.js @@ -0,0 +1,30 @@ +window.handleClickBatchFormAction = function(action, $row, rowIndex) +{ + if(action == 'delete') + { + let deleteID = $row.find('td[data-name="id"] input[name^=id]').val(); + if(deleteID) + { + deleteID = deleteID.replace('id', ''); + let link = $.createLink('doc', 'ajaxJudgeCanBeDeleted', 'scopeID=' + deleteID); + $.getJSON(link, function(data) + { + if(data && data.result == 'fail') + { + zui.Modal.alert(data.message); + } + else + { + const $batchForm = $('[data-zui-batchform]').zui('batchForm'); + $batchForm.deleteRow(rowIndex); + } + }); + return false; + } + } +} + +window.handleRenderRow = function($row, index, data) +{ + if(data && data.main == '1') $row.find('[data-name="ACTIONS"]').find('[data-type="delete"]').addClass('hidden'); +} diff --git a/module/doc/js/zentaolist.ui.js b/module/doc/js/zentaolist.ui.js index 1b16534358..8d2468d163 100644 --- a/module/doc/js/zentaolist.ui.js +++ b/module/doc/js/zentaolist.ui.js @@ -217,7 +217,6 @@ window.renderCell = function(result, info) if(!info.row.data.build.name) return result; result.push({html: info.row.data.build.name}); - return result; } if(info.col.name == 'project') @@ -226,9 +225,61 @@ window.renderCell = function(result, info) if(!info.row.data.projectName) return result; result.push({html: `${info.row.data.projectName}`}); + } + } + + if(blockType == 'projectRelease') + { + if(info.col.name == 'name') + { + if(info.row.data.marker == 1) + { + result[result.length] = {html: ""}; + } + } + + if(info.col.name == 'build') + { + if(!info.row.data.buildInfos) info.row.data.buildInfos = info.row.data.builds; + + let result = []; + for(key in info.row.data.buildInfos) result.push({html: info.row.data.buildInfos[key].name}) return result; } } + + if(blockType == 'productCase') + { + if(result) + { + if(info.col.name == 'caseID' && info.row.data.isScene) result.shift(); // 移除场景ID + + if(info.col.name == 'title') + { + const data = info.row.data; + if(data.color) result[0].props.style = 'color: ' + data.color; + if(data.isScene) // 场景 + { + result.shift(); // 移除带链接的场景名称 + result.push({html: data.title}); // 添加不带链接的场景名称 + result.unshift({html: '' + scene + ''}); // 添加场景标签 + } + else // 用例 + { + if(data.auto == 'auto') result.unshift({html: '' + automated + ''}); // 添加自动化标签 + if(info.row.data.fromCaseID > 0) result.push({html: `[ #${info.row.data.fromCaseID}]`}); // 添加来源用例 + } + } + + if(info.col.name == 'pri' && info.row.data.isScene) result.shift(); // 移除场景优先级 + + if(info.col.name == 'status' && info.row.data.status == 'casechanged') result[0] = {html: '' + caseChanged + ''}; + } + + if(info.row.data.lastEditedDate == '0000-00-00 00:00:00') info.row.data.lastEditedDate = ''; + if(info.row.data.reviewedDate == '0000-00-00') info.row.data.reviewedDate = ''; + } + return result; }; diff --git a/module/doc/lang/de.php b/module/doc/lang/de.php index e8b1f5eed7..9f4134ac5e 100644 --- a/module/doc/lang/de.php +++ b/module/doc/lang/de.php @@ -73,6 +73,75 @@ $lang->doclib->apiNameUnique['product'] = 'In the api library of the same ' . $l $lang->doclib->apiNameUnique['project'] = 'In the api library of the same ' . $lang->projectCommon . ', '; $lang->doclib->apiNameUnique['nolink'] = 'In the no linked api library, '; +$lang->docTemplate = new stdclass(); +$lang->docTemplate->id = 'ID'; +$lang->docTemplate->title = 'Template Title'; +$lang->docTemplate->frequency = 'Frequency'; +$lang->docTemplate->type = 'Category'; +$lang->docTemplate->addedBy = 'Added By'; +$lang->docTemplate->addedDate = 'Added Date'; +$lang->docTemplate->editedBy = 'Edited By'; +$lang->docTemplate->editedDate = 'Edited Date'; +$lang->docTemplate->views = 'Views'; +$lang->docTemplate->confirmDelete = 'Do you want to delete this document template?'; +$lang->docTemplate->scope = 'Scope'; +$lang->docTemplate->lib = $lang->docTemplate->scope; +$lang->docTemplate->module = 'Category'; +$lang->docTemplate->desc = 'Describe'; +$lang->docTemplate->parentModule = 'Parent'; +$lang->docTemplate->typeName = 'Category Name'; +$lang->docTemplate->parent = 'Level'; +$lang->docTemplate->addTemplateType = 'Add template category'; +$lang->docTemplate->editTemplateType = 'Edit template category'; +$lang->docTemplate->docTitlePlaceholder = 'Enter the document template title'; +$lang->docTemplate->docTitleRequired = 'Please enter the doc template title.'; +$lang->docTemplate->errorDeleteType = 'There is document template exists in the current category, it cannot be deleted.'; +$lang->docTemplate->convertToNewDocConfirm = 'Are you ready to convert this document template to the new format using a modern block-level editor? After saving as a draft or publishing, you can no longer switch back to the old editor.'; +$lang->docTemplate->oldDocEditingTip = 'This document template is being created in the old editor, the new editor is now enabled for editing. If you want to switch to the new editor, please save the document template first.'; +$lang->docTemplate->leaveEditingConfirm = 'The template is currently being edited. Do you want to leave the editing page?'; +$lang->docTemplate->searchScopePlaceholder = 'Search Scope'; +$lang->docTemplate->searchTypePlaceholder = 'Search Category'; +$lang->docTemplate->moveDocTemplate = 'Move Template'; +$lang->docTemplate->moveSubTemplate = 'Move Sub Template'; +$lang->docTemplate->createTypeFirst = 'Please create a template category first.'; +$lang->docTemplate->editedList = 'Template editor'; +$lang->docTemplate->content = 'Template Text'; +$lang->docTemplate->templateDesc = 'Template Desc'; +$lang->docTemplate->status = 'Template Status'; +$lang->docTemplate->emptyTip = 'There is no system data that conform to this parameter and filtering conditions.'; +$lang->docTemplate->previewTip = 'After configuring the parameters, this block will display the corresponding list data according to the filter configuration.'; +$lang->docTemplate->confirmDeleteChapterWithSub = "After deleting a chapter, the lower level content of the chapter will be hidden. Are you sure you want to delete the chapter?"; +$lang->docTemplate->confirmDeleteTemplateWithSub = "After deleting the template, the lower level content of the template will be hidden. Are you sure you want to delete this template?"; +$lang->docTemplate->scopeHasTemplateTips = 'There are templates within this scope. Please remove them before deleting the scope.'; +$lang->docTemplate->scopeHasModuleTips = 'There are template category data within this scope. Please remove them before deleting the scope.'; +$lang->docTemplate->confirmDeleteReviewTemplate = "After deletion, it will affect the ability of the template to automatically generate system data for review in the project. Are you sure you want to delete it?"; +$lang->docTemplate->needEditable = 'This template is not editable.'; + +$lang->docTemplate->create = 'Create Template'; +$lang->docTemplate->edit = 'Edit Template'; +$lang->docTemplate->delete = 'Delete Template'; + +$lang->docTemplate->more = 'More'; +$lang->docTemplate->scopeLabel = 'Scope'; +$lang->docTemplate->noTemplate = 'No Doc Template'; +$lang->docTemplate->noDesc = 'No Desc'; +$lang->docTemplate->of = ' of '; +$lang->docTemplate->overdue = 'Overdue'; + +$lang->docTemplate->addModule = 'Add Template Category'; +$lang->docTemplate->addSameModule = 'Add Same Template Category'; +$lang->docTemplate->addSubModule = 'Add Sub Template Category'; +$lang->docTemplate->editModule = 'Edit Template Category'; +$lang->docTemplate->deleteModule = 'Delete Template Category'; +$lang->docTemplate->noModules = 'No Categories'; +$lang->docTemplate->addSubDocTemplate = 'Add sub template'; + +$lang->docTemplate->filterTypes = array(); +$lang->docTemplate->filterTypes[] = array('all', 'All'); +$lang->docTemplate->filterTypes[] = array('draft', 'Draft'); +$lang->docTemplate->filterTypes[] = array('released', 'Released'); +$lang->docTemplate->filterTypes[] = array('createdByMe', 'Created By Me'); + /* Fields. */ $lang->doc->common = 'Dok'; $lang->doc->id = 'ID'; @@ -137,6 +206,7 @@ $lang->doc->update = 'Update'; $lang->doc->nextStep = 'Next'; $lang->doc->closed = 'Closed'; $lang->doc->saveDraft = 'Save Draft'; +$lang->doc->template = 'Template'; $lang->doc->position = 'Position'; $lang->doc->person = 'Person'; $lang->doc->team = 'Team'; @@ -276,7 +346,7 @@ $lang->doc->deleteChapter = 'Delete chapter'; $lang->doc->addSubChapter = 'Add sub chapter'; $lang->doc->addSameChapter = 'Add same chapter'; $lang->doc->addSubDoc = 'Add sub document'; -$lang->doc->chapterName = 'Chapter name'; +$lang->doc->chapterName = 'Name'; $lang->doc->tips = new stdclass(); $lang->doc->tips->noProduct = 'No product, please create first'; @@ -293,6 +363,7 @@ $lang->doc->zentaoList['planStory'] = $lang->productplan->shortCommon . $la $lang->doc->zentaoList['case'] = $lang->testcase->common; $lang->doc->zentaoList['productCase'] = $lang->productCommon . $lang->testcase->common; +$lang->doc->zentaoList['projectCase'] = $lang->projectCommon . $lang->testcase->common; $lang->doc->zentaoList['caselib'] = 'Case Library' . $lang->testcase->common; $lang->doc->zentaoList['task'] = $lang->task->common; @@ -304,14 +375,17 @@ $lang->doc->zentaoList['more'] = 'More'; $lang->doc->zentaoList['productPlan'] = $lang->productCommon . ' Plan'; $lang->doc->zentaoList['productPlanContent'] = $lang->productCommon . ' Plan Content'; $lang->doc->zentaoList['productRelease'] = $lang->productCommon . $lang->release->common; +$lang->doc->zentaoList['projectRelease'] = $lang->projectCommon . $lang->release->common; $lang->doc->zentaoList['ER'] = $lang->defaultERName; $lang->doc->zentaoList['UR'] = $lang->URCommon; $lang->doc->zentaoList['feedback'] = 'Feedback'; $lang->doc->zentaoList['ticket'] = 'Ticket'; +$lang->doc->zentaoList['gantt'] = 'Gantt Chart'; $lang->doc->zentaoAction = array(); -$lang->doc->zentaoAction['set'] = 'Setting'; -$lang->doc->zentaoAction['delete'] = 'Delete'; +$lang->doc->zentaoAction['set'] = 'Setting'; +$lang->doc->zentaoAction['delete'] = 'Delete'; +$lang->doc->zentaoAction['setParams'] = 'Set Parameters'; $lang->doc->uploadFormatList = array(); $lang->doc->uploadFormatList['separateDocs'] = 'Save files as different document'; @@ -348,6 +422,9 @@ $lang->doc->statusList['draft'] = "Draft"; $lang->doc->aclList['open'] = "Public (Access with library permissions)"; $lang->doc->aclList['private'] = "Private (Only creators and whitelist users can access)"; +$lang->doc->aclListA['open'] = "Public (Everyone can access it, with permission to edit document templates for access and maintenance.)"; +$lang->doc->aclListA['private'] = "Private (Only creators can edit and use it)"; + $lang->doc->selectSpace = 'Select Space'; $lang->doc->space = 'Space'; $lang->doc->spaceList['mine'] = 'My Space'; @@ -454,6 +531,7 @@ $lang->doc->docConvertComment = "This document has been converted to t $lang->doc->previewNotAvailable = 'Preview function is not available. Please visit the Kanban board to view the document %s.'; $lang->doc->hocuspocusConnect = 'The collaborative editing service is connected.'; $lang->doc->hocuspocusDisconnect = 'The collaborative editing service is disconnected. Edits will be synchronized after reconnection.'; +$lang->doc->docTemplateConvertComment = 'The template has been converted to the new editor format. Switch to version %s to view the template before conversion.'; $lang->doc->noticeAcl['lib']['product']['default'] = "Users who can access the selected {$lang->productCommon} can access it."; $lang->doc->noticeAcl['lib']['product']['custom'] = "Users who can access the selected {$lang->productCommon} or users in the whiltelist can access it."; @@ -523,6 +601,18 @@ $lang->doc->projectFilterTypes[] = array('mine', 'Involved'); $lang->doc->spaceFilterTypes[] = array('all', 'All'); +$lang->doc->manageScope = 'Manage Scope'; +$lang->doc->browseTemplate = 'Template Square'; +$lang->doc->createTemplate = 'Create Document Template'; +$lang->doc->editTemplate = 'Edit Document Template'; +$lang->doc->moveTemplate = 'Move Document Template'; +$lang->doc->deleteTemplate = 'Delete Document Template'; +$lang->doc->viewTemplate = 'View Document Template'; +$lang->doc->addTemplateType = 'Create Category'; +$lang->doc->editTemplateType = 'Edit Category'; +$lang->doc->deleteTemplateType = 'Delete Category'; +$lang->doc->sortTemplate = 'Sort'; + $lang->doc->docLang = new stdClass(); $lang->doc->docLang->cancel = $lang->cancel; $lang->doc->docLang->export = $lang->export; @@ -566,6 +656,7 @@ $lang->doc->docLang->moveTo = $lang->doc->moveTo; $lang->doc->docLang->moveLib = $lang->doc->moveLibAction; $lang->doc->docLang->moduleName = $lang->doc->catalogName; $lang->doc->docLang->saveDraft = $lang->doc->saveDraft; +$lang->doc->docLang->template = $lang->doc->template; $lang->doc->docLang->release = $lang->doc->release; $lang->doc->docLang->batchMove = $lang->doc->batchMove; $lang->doc->docLang->filterTypes = $lang->doc->filterTypes; @@ -689,3 +780,26 @@ $lang->doc->docLang->editing = 'Editing'; $lang->doc->docLang->restoreVersionHint = 'Restore to version'; $lang->doc->docLang->restoreVersion = 'Restore'; $lang->doc->docLang->restoreVersionConfirm = 'This will create a new version using the content of version {version}. Are you sure you want to continue?'; + +$lang->docTemplate->types = array(); +$lang->docTemplate->types['plan'] = 'Plan'; +$lang->docTemplate->types['story'] = 'Story'; +$lang->docTemplate->types['design'] = 'Design'; +$lang->docTemplate->types['dev'] = 'Development'; +$lang->docTemplate->types['test'] = 'Test'; +$lang->docTemplate->types['desc'] = 'Description'; +$lang->docTemplate->types['other'] = 'Other'; + +$lang->docTemplate->builtInScopes = array(); +$lang->docTemplate->builtInScopes['rnd'] = array(); +$lang->docTemplate->builtInScopes['or'] = array(); +$lang->docTemplate->builtInScopes['lite'] = array(); +$lang->docTemplate->builtInScopes['rnd']['product'] = 'Product'; +$lang->docTemplate->builtInScopes['rnd']['project'] = 'Project'; +$lang->docTemplate->builtInScopes['rnd']['execution'] = 'Execution'; +$lang->docTemplate->builtInScopes['rnd']['personal'] = 'Personal'; +$lang->docTemplate->builtInScopes['or']['market'] = 'Market'; +$lang->docTemplate->builtInScopes['or']['product'] = 'Product'; +$lang->docTemplate->builtInScopes['or']['personal'] = 'Personal'; +$lang->docTemplate->builtInScopes['lite']['project'] = 'Project'; +$lang->docTemplate->builtInScopes['lite']['personal'] = 'Personal'; diff --git a/module/doc/lang/en.php b/module/doc/lang/en.php index a51c5e3105..a59bc0b29f 100644 --- a/module/doc/lang/en.php +++ b/module/doc/lang/en.php @@ -73,6 +73,75 @@ $lang->doclib->apiNameUnique['product'] = 'In the api library of the same ' . $l $lang->doclib->apiNameUnique['project'] = 'In the api library of the same ' . $lang->projectCommon . ', '; $lang->doclib->apiNameUnique['nolink'] = 'In the no linked api library, '; +$lang->docTemplate = new stdclass(); +$lang->docTemplate->id = 'ID'; +$lang->docTemplate->title = 'Template Title'; +$lang->docTemplate->frequency = 'Frequency'; +$lang->docTemplate->type = 'Category'; +$lang->docTemplate->addedBy = 'Added By'; +$lang->docTemplate->addedDate = 'Added Date'; +$lang->docTemplate->editedBy = 'Edited By'; +$lang->docTemplate->editedDate = 'Edited Date'; +$lang->docTemplate->views = 'Views'; +$lang->docTemplate->confirmDelete = 'Do you want to delete this document template?'; +$lang->docTemplate->scope = 'Scope'; +$lang->docTemplate->lib = $lang->docTemplate->scope; +$lang->docTemplate->module = 'Category'; +$lang->docTemplate->desc = 'Describe'; +$lang->docTemplate->parentModule = 'Parent'; +$lang->docTemplate->typeName = 'Category Name'; +$lang->docTemplate->parent = 'Level'; +$lang->docTemplate->addTemplateType = 'Add template category'; +$lang->docTemplate->editTemplateType = 'Edit template category'; +$lang->docTemplate->docTitlePlaceholder = 'Enter the document template title'; +$lang->docTemplate->docTitleRequired = 'Please enter the doc template title.'; +$lang->docTemplate->errorDeleteType = 'There is document template exists in the current category, it cannot be deleted.'; +$lang->docTemplate->convertToNewDocConfirm = 'Are you ready to convert this document template to the new format using a modern block-level editor? After saving as a draft or publishing, you can no longer switch back to the old editor.'; +$lang->docTemplate->oldDocEditingTip = 'This document template is being created in the old editor, the new editor is now enabled for editing. If you want to switch to the new editor, please save the document template first.'; +$lang->docTemplate->leaveEditingConfirm = 'The template is currently being edited. Do you want to leave the editing page?'; +$lang->docTemplate->searchScopePlaceholder = 'Search Scope'; +$lang->docTemplate->searchTypePlaceholder = 'Search Category'; +$lang->docTemplate->moveDocTemplate = 'Move Template'; +$lang->docTemplate->moveSubTemplate = 'Move Sub Template'; +$lang->docTemplate->createTypeFirst = 'Please create a template category first.'; +$lang->docTemplate->editedList = 'Template editor'; +$lang->docTemplate->content = 'Template Text'; +$lang->docTemplate->templateDesc = 'Template Desc'; +$lang->docTemplate->status = 'Template Status'; +$lang->docTemplate->emptyTip = 'There is no system data that conform to this parameter and filtering conditions.'; +$lang->docTemplate->previewTip = 'After configuring the parameters, this block will display the corresponding list data according to the filter configuration.'; +$lang->docTemplate->confirmDeleteChapterWithSub = "After deleting a chapter, the lower level content of the chapter will be hidden. Are you sure you want to delete the chapter?"; +$lang->docTemplate->confirmDeleteTemplateWithSub = "After deleting the template, the lower level content of the template will be hidden. Are you sure you want to delete this template?"; +$lang->docTemplate->scopeHasTemplateTips = 'There are templates within this scope. Please remove them before deleting the scope.'; +$lang->docTemplate->scopeHasModuleTips = 'There are template category data within this scope. Please remove them before deleting the scope.'; +$lang->docTemplate->confirmDeleteReviewTemplate = "After deletion, it will affect the ability of the template to automatically generate system data for review in the project. Are you sure you want to delete it?"; +$lang->docTemplate->needEditable = 'This template is not editable.'; + +$lang->docTemplate->create = 'Create Template'; +$lang->docTemplate->edit = 'Edit Template'; +$lang->docTemplate->delete = 'Delete Template'; + +$lang->docTemplate->more = 'More'; +$lang->docTemplate->scopeLabel = 'Scope'; +$lang->docTemplate->noTemplate = 'No Doc Template'; +$lang->docTemplate->noDesc = 'No Desc'; +$lang->docTemplate->of = ' of '; +$lang->docTemplate->overdue = 'Overdue'; + +$lang->docTemplate->addModule = 'Add Template Category'; +$lang->docTemplate->addSameModule = 'Add Same Template Category'; +$lang->docTemplate->addSubModule = 'Add Sub Template Category'; +$lang->docTemplate->editModule = 'Edit Template Category'; +$lang->docTemplate->deleteModule = 'Delete Template Category'; +$lang->docTemplate->noModules = 'No Categories'; +$lang->docTemplate->addSubDocTemplate = 'Add sub template'; + +$lang->docTemplate->filterTypes = array(); +$lang->docTemplate->filterTypes[] = array('all', 'All'); +$lang->docTemplate->filterTypes[] = array('draft', 'Draft'); +$lang->docTemplate->filterTypes[] = array('released', 'Released'); +$lang->docTemplate->filterTypes[] = array('createdByMe', 'Created By Me'); + /* Fields. */ $lang->doc->common = 'Document'; $lang->doc->id = 'ID'; @@ -137,6 +206,7 @@ $lang->doc->update = 'Update'; $lang->doc->nextStep = 'Next'; $lang->doc->closed = 'Closed'; $lang->doc->saveDraft = 'Save Draft'; +$lang->doc->template = 'Template'; $lang->doc->position = 'Position'; $lang->doc->person = 'Person'; $lang->doc->team = 'Team'; @@ -276,7 +346,7 @@ $lang->doc->deleteChapter = 'Delete chapter'; $lang->doc->addSubChapter = 'Add sub chapter'; $lang->doc->addSameChapter = 'Add same chapter'; $lang->doc->addSubDoc = 'Add sub document'; -$lang->doc->chapterName = 'Chapter name'; +$lang->doc->chapterName = 'Name'; $lang->doc->tips = new stdclass(); $lang->doc->tips->noProduct = 'No product, please create first'; @@ -293,6 +363,7 @@ $lang->doc->zentaoList['planStory'] = $lang->productplan->shortCommon . $la $lang->doc->zentaoList['case'] = $lang->testcase->common; $lang->doc->zentaoList['productCase'] = $lang->productCommon . $lang->testcase->common; +$lang->doc->zentaoList['projectCase'] = $lang->projectCommon . $lang->testcase->common; $lang->doc->zentaoList['caselib'] = 'Case Library' . $lang->testcase->common; $lang->doc->zentaoList['task'] = $lang->task->common; @@ -304,14 +375,17 @@ $lang->doc->zentaoList['more'] = 'More'; $lang->doc->zentaoList['productPlan'] = $lang->productCommon . ' Plan'; $lang->doc->zentaoList['productPlanContent'] = $lang->productCommon . ' Plan Content'; $lang->doc->zentaoList['productRelease'] = $lang->productCommon . $lang->release->common; +$lang->doc->zentaoList['projectRelease'] = $lang->projectCommon . $lang->release->common; $lang->doc->zentaoList['ER'] = $lang->defaultERName; $lang->doc->zentaoList['UR'] = $lang->URCommon; $lang->doc->zentaoList['feedback'] = 'Feedback'; $lang->doc->zentaoList['ticket'] = 'Ticket'; +$lang->doc->zentaoList['gantt'] = 'Gantt Chart'; $lang->doc->zentaoAction = array(); -$lang->doc->zentaoAction['set'] = 'Setting'; -$lang->doc->zentaoAction['delete'] = 'Delete'; +$lang->doc->zentaoAction['set'] = 'Setting'; +$lang->doc->zentaoAction['delete'] = 'Delete'; +$lang->doc->zentaoAction['setParams'] = 'Set Parameters'; $lang->doc->uploadFormatList = array(); $lang->doc->uploadFormatList['separateDocs'] = 'Save files as different document'; @@ -348,6 +422,9 @@ $lang->doc->statusList['draft'] = "Draft"; $lang->doc->aclList['open'] = "Public (Access with library permissions)"; $lang->doc->aclList['private'] = "Private (Only creators and whitelist users can access)"; +$lang->doc->aclListA['open'] = "Public (Everyone can access it, with permission to edit document templates for access and maintenance)"; +$lang->doc->aclListA['private'] = "Private (Only creators can edit and use it)"; + $lang->doc->selectSpace = 'Select Space'; $lang->doc->space = 'Space'; $lang->doc->spaceList['mine'] = 'My Space'; @@ -454,6 +531,7 @@ $lang->doc->docConvertComment = "This document has been converted to t $lang->doc->previewNotAvailable = 'Preview function is not available. Please visit the Kanban board to view the document %s.'; $lang->doc->hocuspocusConnect = 'The collaborative editing service is connected.'; $lang->doc->hocuspocusDisconnect = 'The collaborative editing service is disconnected. Edits will be synchronized after reconnection.'; +$lang->doc->docTemplateConvertComment = 'The template has been converted to the new editor format. Switch to version %s to view the template before conversion.'; $lang->doc->noticeAcl['lib']['product']['default'] = "Users who can access the selected {$lang->productCommon} can access it."; $lang->doc->noticeAcl['lib']['product']['custom'] = "Users who can access the selected {$lang->productCommon} or users in the whiltelist can access it."; @@ -523,6 +601,18 @@ $lang->doc->projectFilterTypes[] = array('mine', 'Involved'); $lang->doc->spaceFilterTypes[] = array('all', 'All'); +$lang->doc->manageScope = 'Manage Scope'; +$lang->doc->browseTemplate = 'Template Square'; +$lang->doc->createTemplate = 'Create Document Template'; +$lang->doc->editTemplate = 'Edit Document Template'; +$lang->doc->moveTemplate = 'Move Document Template'; +$lang->doc->deleteTemplate = 'Delete Document Template'; +$lang->doc->viewTemplate = 'View Document Template'; +$lang->doc->addTemplateType = 'Create Category'; +$lang->doc->editTemplateType = 'Edit Category'; +$lang->doc->deleteTemplateType = 'Delete Category'; +$lang->doc->sortTemplate = 'Sort'; + $lang->doc->docLang = new stdClass(); $lang->doc->docLang->cancel = $lang->cancel; $lang->doc->docLang->export = $lang->export; @@ -566,6 +656,7 @@ $lang->doc->docLang->moveTo = $lang->doc->moveTo; $lang->doc->docLang->moveLib = $lang->doc->moveLibAction; $lang->doc->docLang->moduleName = $lang->doc->catalogName; $lang->doc->docLang->saveDraft = $lang->doc->saveDraft; +$lang->doc->docLang->template = $lang->doc->template; $lang->doc->docLang->release = $lang->doc->release; $lang->doc->docLang->batchMove = $lang->doc->batchMove; $lang->doc->docLang->filterTypes = $lang->doc->filterTypes; @@ -689,3 +780,26 @@ $lang->doc->docLang->editing = 'Editing'; $lang->doc->docLang->restoreVersionHint = 'Restore to version'; $lang->doc->docLang->restoreVersion = 'Restore'; $lang->doc->docLang->restoreVersionConfirm = 'This will create a new version using the content of version {version}. Are you sure you want to continue?'; + +$lang->docTemplate->types = array(); +$lang->docTemplate->types['plan'] = 'Plan'; +$lang->docTemplate->types['story'] = 'Story'; +$lang->docTemplate->types['design'] = 'Design'; +$lang->docTemplate->types['dev'] = 'Development'; +$lang->docTemplate->types['test'] = 'Test'; +$lang->docTemplate->types['desc'] = 'Description'; +$lang->docTemplate->types['other'] = 'Other'; + +$lang->docTemplate->builtInScopes = array(); +$lang->docTemplate->builtInScopes['rnd'] = array(); +$lang->docTemplate->builtInScopes['or'] = array(); +$lang->docTemplate->builtInScopes['lite'] = array(); +$lang->docTemplate->builtInScopes['rnd']['product'] = 'Product'; +$lang->docTemplate->builtInScopes['rnd']['project'] = 'Project'; +$lang->docTemplate->builtInScopes['rnd']['execution'] = 'Execution'; +$lang->docTemplate->builtInScopes['rnd']['personal'] = 'Personal'; +$lang->docTemplate->builtInScopes['or']['market'] = 'Market'; +$lang->docTemplate->builtInScopes['or']['product'] = 'Product'; +$lang->docTemplate->builtInScopes['or']['personal'] = 'Personal'; +$lang->docTemplate->builtInScopes['lite']['project'] = 'Project'; +$lang->docTemplate->builtInScopes['lite']['personal'] = 'Personal'; diff --git a/module/doc/lang/fr.php b/module/doc/lang/fr.php index 9668928712..b1e52c89dc 100644 --- a/module/doc/lang/fr.php +++ b/module/doc/lang/fr.php @@ -73,6 +73,75 @@ $lang->doclib->apiNameUnique['product'] = 'In the api library of the same ' . $l $lang->doclib->apiNameUnique['project'] = 'In the api library of the same ' . $lang->projectCommon . ', '; $lang->doclib->apiNameUnique['nolink'] = 'In the no linked api library, '; +$lang->docTemplate = new stdclass(); +$lang->docTemplate->id = 'ID'; +$lang->docTemplate->title = 'Template Title'; +$lang->docTemplate->frequency = 'Frequency'; +$lang->docTemplate->type = 'Category'; +$lang->docTemplate->addedBy = 'Added By'; +$lang->docTemplate->addedDate = 'Added Date'; +$lang->docTemplate->editedBy = 'Edited By'; +$lang->docTemplate->editedDate = 'Edited Date'; +$lang->docTemplate->views = 'Views'; +$lang->docTemplate->confirmDelete = 'Do you want to delete this document template?'; +$lang->docTemplate->scope = 'Scope'; +$lang->docTemplate->lib = $lang->docTemplate->scope; +$lang->docTemplate->module = 'Category'; +$lang->docTemplate->desc = 'Describe'; +$lang->docTemplate->parentModule = 'Parent'; +$lang->docTemplate->typeName = 'Category Name'; +$lang->docTemplate->parent = 'Level'; +$lang->docTemplate->addTemplateType = 'Add template category'; +$lang->docTemplate->editTemplateType = 'Edit template category'; +$lang->docTemplate->docTitlePlaceholder = 'Enter the document template title'; +$lang->docTemplate->docTitleRequired = 'Please enter the doc template title.'; +$lang->docTemplate->errorDeleteType = 'There is document template exists in the current category, it cannot be deleted.'; +$lang->docTemplate->convertToNewDocConfirm = 'Are you ready to convert this document template to the new format using a modern block-level editor? After saving as a draft or publishing, you can no longer switch back to the old editor.'; +$lang->docTemplate->oldDocEditingTip = 'This document template is being created in the old editor, the new editor is now enabled for editing. If you want to switch to the new editor, please save the document template first.'; +$lang->docTemplate->leaveEditingConfirm = 'The template is currently being edited. Do you want to leave the editing page?'; +$lang->docTemplate->searchScopePlaceholder = 'Search Scope'; +$lang->docTemplate->searchTypePlaceholder = 'Search Category'; +$lang->docTemplate->moveDocTemplate = 'Move Template'; +$lang->docTemplate->moveSubTemplate = 'Move Sub Template'; +$lang->docTemplate->createTypeFirst = 'Please create a template category first.'; +$lang->docTemplate->editedList = 'Template editor'; +$lang->docTemplate->content = 'Template Text'; +$lang->docTemplate->templateDesc = 'Template Desc'; +$lang->docTemplate->status = 'Template Status'; +$lang->docTemplate->emptyTip = 'There is no system data that conform to this parameter and filtering conditions.'; +$lang->docTemplate->previewTip = 'After configuring the parameters, this block will display the corresponding list data according to the filter configuration.'; +$lang->docTemplate->confirmDeleteChapterWithSub = "After deleting a chapter, the lower level content of the chapter will be hidden. Are you sure you want to delete the chapter?"; +$lang->docTemplate->confirmDeleteTemplateWithSub = "After deleting the template, the lower level content of the template will be hidden. Are you sure you want to delete this template?"; +$lang->docTemplate->scopeHasTemplateTips = 'There are templates within this scope. Please remove them before deleting the scope.'; +$lang->docTemplate->scopeHasModuleTips = 'There are template category data within this scope. Please remove them before deleting the scope.'; +$lang->docTemplate->confirmDeleteReviewTemplate = "After deletion, it will affect the ability of the template to automatically generate system data for review in the project. Are you sure you want to delete it?"; +$lang->docTemplate->needEditable = 'This template is not editable.'; + +$lang->docTemplate->create = 'Create Template'; +$lang->docTemplate->edit = 'Edit Template'; +$lang->docTemplate->delete = 'Delete Template'; + +$lang->docTemplate->more = 'More'; +$lang->docTemplate->scopeLabel = 'Scope'; +$lang->docTemplate->noTemplate = 'No Doc Template'; +$lang->docTemplate->noDesc = 'No Desc'; +$lang->docTemplate->of = ' of '; +$lang->docTemplate->overdue = 'Overdue'; + +$lang->docTemplate->addModule = 'Add Template Category'; +$lang->docTemplate->addSameModule = 'Add Same Template Category'; +$lang->docTemplate->addSubModule = 'Add Sub Template Category'; +$lang->docTemplate->editModule = 'Edit Template Category'; +$lang->docTemplate->deleteModule = 'Delete Template Category'; +$lang->docTemplate->noModules = 'No Categories'; +$lang->docTemplate->addSubDocTemplate = 'Add sub template'; + +$lang->docTemplate->filterTypes = array(); +$lang->docTemplate->filterTypes[] = array('all', 'All'); +$lang->docTemplate->filterTypes[] = array('draft', 'Draft'); +$lang->docTemplate->filterTypes[] = array('released', 'Released'); +$lang->docTemplate->filterTypes[] = array('createdByMe', 'Created By Me'); + /* Fields. */ $lang->doc->common = 'Gestion Documentaire'; $lang->doc->id = 'ID'; @@ -137,6 +206,7 @@ $lang->doc->update = 'Update'; $lang->doc->nextStep = 'Next'; $lang->doc->closed = 'Closed'; $lang->doc->saveDraft = 'Save Draft'; +$lang->doc->template = 'Template'; $lang->doc->position = 'Position'; $lang->doc->person = 'Person'; $lang->doc->team = 'Team'; @@ -276,7 +346,7 @@ $lang->doc->deleteChapter = 'Delete chapter'; $lang->doc->addSubChapter = 'Add sub chapter'; $lang->doc->addSameChapter = 'Add same chapter'; $lang->doc->addSubDoc = 'Add sub document'; -$lang->doc->chapterName = 'Chapter name'; +$lang->doc->chapterName = 'Name'; $lang->doc->tips = new stdclass(); $lang->doc->tips->noProduct = 'No product, please create first'; @@ -293,6 +363,7 @@ $lang->doc->zentaoList['planStory'] = $lang->productplan->shortCommon . $la $lang->doc->zentaoList['case'] = $lang->testcase->common; $lang->doc->zentaoList['productCase'] = $lang->productCommon . $lang->testcase->common; +$lang->doc->zentaoList['projectCase'] = $lang->projectCommon . $lang->testcase->common; $lang->doc->zentaoList['caselib'] = 'Case Library' . $lang->testcase->common; $lang->doc->zentaoList['task'] = $lang->task->common; @@ -304,14 +375,17 @@ $lang->doc->zentaoList['more'] = 'More'; $lang->doc->zentaoList['productPlan'] = $lang->productCommon . ' Plan'; $lang->doc->zentaoList['productPlanContent'] = $lang->productCommon . ' Plan Content'; $lang->doc->zentaoList['productRelease'] = $lang->productCommon . $lang->release->common; +$lang->doc->zentaoList['projectRelease'] = $lang->projectCommon . $lang->release->common; $lang->doc->zentaoList['ER'] = $lang->defaultERName; $lang->doc->zentaoList['UR'] = $lang->URCommon; $lang->doc->zentaoList['feedback'] = 'Feedback'; $lang->doc->zentaoList['ticket'] = 'Ticket'; +$lang->doc->zentaoList['gantt'] = 'Gantt Chart'; $lang->doc->zentaoAction = array(); -$lang->doc->zentaoAction['set'] = 'Setting'; -$lang->doc->zentaoAction['delete'] = 'Delete'; +$lang->doc->zentaoAction['set'] = 'Setting'; +$lang->doc->zentaoAction['delete'] = 'Delete'; +$lang->doc->zentaoAction['setParams'] = 'Set Parameters'; $lang->doc->uploadFormatList = array(); $lang->doc->uploadFormatList['separateDocs'] = 'Save files as different document'; @@ -348,6 +422,9 @@ $lang->doc->statusList['draft'] = "Draft"; $lang->doc->aclList['open'] = "Public (Access with library permissions)"; $lang->doc->aclList['private'] = "Private (Only creators and whitelist users can access)"; +$lang->doc->aclListA['open'] = "Public (Everyone can access it, with permission to edit document templates for access and maintenance)"; +$lang->doc->aclListA['private'] = "Private (Only creators can edit and use it)"; + $lang->doc->selectSpace = 'Select Space'; $lang->doc->space = 'Space'; $lang->doc->spaceList['mine'] = 'My Space'; @@ -454,6 +531,7 @@ $lang->doc->docConvertComment = "This document has been converted to t $lang->doc->previewNotAvailable = 'Preview function is not available. Please visit the Kanban board to view the document %s.'; $lang->doc->hocuspocusConnect = 'The collaborative editing service is connected.'; $lang->doc->hocuspocusDisconnect = 'The collaborative editing service is disconnected. Edits will be synchronized after reconnection.'; +$lang->doc->docTemplateConvertComment = 'The template has been converted to the new editor format. Switch to version %s to view the template before conversion.'; $lang->doc->noticeAcl['lib']['product']['default'] = "Users who can access the selected {$lang->productCommon} can access it."; $lang->doc->noticeAcl['lib']['product']['custom'] = "Users who can access the selected {$lang->productCommon} or users in the whiltelist can access it."; @@ -523,6 +601,18 @@ $lang->doc->projectFilterTypes[] = array('mine', 'Involved'); $lang->doc->spaceFilterTypes[] = array('all', 'All'); +$lang->doc->manageScope = 'Manage Scope'; +$lang->doc->browseTemplate = 'Template Square'; +$lang->doc->createTemplate = 'Create Document Template'; +$lang->doc->editTemplate = 'Edit Document Template'; +$lang->doc->moveTemplate = 'Move Document Template'; +$lang->doc->deleteTemplate = 'Delete Document Template'; +$lang->doc->viewTemplate = 'View Document Template'; +$lang->doc->addTemplateType = 'Create Category'; +$lang->doc->editTemplateType = 'Edit Category'; +$lang->doc->deleteTemplateType = 'Delete Category'; +$lang->doc->sortTemplate = 'Sort'; + $lang->doc->docLang = new stdClass(); $lang->doc->docLang->cancel = $lang->cancel; $lang->doc->docLang->export = $lang->export; @@ -566,6 +656,7 @@ $lang->doc->docLang->moveTo = $lang->doc->moveTo; $lang->doc->docLang->moveLib = $lang->doc->moveLibAction; $lang->doc->docLang->moduleName = $lang->doc->catalogName; $lang->doc->docLang->saveDraft = $lang->doc->saveDraft; +$lang->doc->docLang->template = $lang->doc->template; $lang->doc->docLang->release = $lang->doc->release; $lang->doc->docLang->batchMove = $lang->doc->batchMove; $lang->doc->docLang->filterTypes = $lang->doc->filterTypes; @@ -689,3 +780,26 @@ $lang->doc->docLang->editing = 'Editing'; $lang->doc->docLang->restoreVersionHint = 'Restore to version'; $lang->doc->docLang->restoreVersion = 'Restore'; $lang->doc->docLang->restoreVersionConfirm = 'This will create a new version using the content of version {version}. Are you sure you want to continue?'; + +$lang->docTemplate->types = array(); +$lang->docTemplate->types['plan'] = 'Plan'; +$lang->docTemplate->types['story'] = 'Story'; +$lang->docTemplate->types['design'] = 'Design'; +$lang->docTemplate->types['dev'] = 'Development'; +$lang->docTemplate->types['test'] = 'Test'; +$lang->docTemplate->types['desc'] = 'Description'; +$lang->docTemplate->types['other'] = 'Other'; + +$lang->docTemplate->builtInScopes = array(); +$lang->docTemplate->builtInScopes['rnd'] = array(); +$lang->docTemplate->builtInScopes['or'] = array(); +$lang->docTemplate->builtInScopes['lite'] = array(); +$lang->docTemplate->builtInScopes['rnd']['product'] = 'Product'; +$lang->docTemplate->builtInScopes['rnd']['project'] = 'Project'; +$lang->docTemplate->builtInScopes['rnd']['execution'] = 'Execution'; +$lang->docTemplate->builtInScopes['rnd']['personal'] = 'Personal'; +$lang->docTemplate->builtInScopes['or']['market'] = 'Market'; +$lang->docTemplate->builtInScopes['or']['product'] = 'Product'; +$lang->docTemplate->builtInScopes['or']['personal'] = 'Personal'; +$lang->docTemplate->builtInScopes['lite']['project'] = 'Project'; +$lang->docTemplate->builtInScopes['lite']['personal'] = 'Personal'; diff --git a/module/doc/lang/zh-cn.php b/module/doc/lang/zh-cn.php index 99ab4dbb73..f19d6afcff 100644 --- a/module/doc/lang/zh-cn.php +++ b/module/doc/lang/zh-cn.php @@ -73,6 +73,75 @@ $lang->doclib->apiNameUnique['product'] = '同一' . $lang->productCommon . '下 $lang->doclib->apiNameUnique['project'] = '同一' . $lang->projectCommon . '下的接口库中'; $lang->doclib->apiNameUnique['nolink'] = '独立接口库中'; +$lang->docTemplate = new stdclass(); +$lang->docTemplate->id = '编号'; +$lang->docTemplate->title = '模板标题'; +$lang->docTemplate->frequency = '频率'; +$lang->docTemplate->type = '分类'; +$lang->docTemplate->addedBy = '创建者'; +$lang->docTemplate->addedDate = '创建日期'; +$lang->docTemplate->editedBy = '修改者'; +$lang->docTemplate->editedDate = '修改日期'; +$lang->docTemplate->views = '阅读次数'; +$lang->docTemplate->confirmDelete = '您确定删除该文档模板吗?'; +$lang->docTemplate->scope = '所属范围'; +$lang->docTemplate->lib = $lang->docTemplate->scope; +$lang->docTemplate->module = '模板分类'; +$lang->docTemplate->desc = '描述'; +$lang->docTemplate->parentModule = '上级分类'; +$lang->docTemplate->typeName = '分类名称'; +$lang->docTemplate->parent = '所属层级'; +$lang->docTemplate->addTemplateType = '添加模板分类'; +$lang->docTemplate->editTemplateType = '编辑模板分类'; +$lang->docTemplate->docTitlePlaceholder = '请输入文档模板标题'; +$lang->docTemplate->docTitleRequired = '文档模板标题不能为空。'; +$lang->docTemplate->errorDeleteType = '当前分类存在文档模板,不可删除'; +$lang->docTemplate->convertToNewDocConfirm = '全新文档格式使用现代化块级编辑器,带来全新的文档功能体验。确定要将此文档模板转换为新文档格式吗?存为草稿或者发布后,不能再切换回旧编辑器。'; +$lang->docTemplate->oldDocEditingTip = '此文档模板为旧版本编辑器创建,已启用新版编辑器编辑,保存后将转换为新版文档模板'; +$lang->docTemplate->leaveEditingConfirm = '文档模板编辑中,确定离开吗?'; +$lang->docTemplate->searchScopePlaceholder = '搜索范围'; +$lang->docTemplate->searchTypePlaceholder = '搜索分类'; +$lang->docTemplate->moveDocTemplate = '移动文档模板'; +$lang->docTemplate->moveSubTemplate = '移动子文档模板'; +$lang->docTemplate->createTypeFirst = '请先创建文档模板分类。'; +$lang->docTemplate->editedList = '模板编辑者'; +$lang->docTemplate->content = '模板内容'; +$lang->docTemplate->templateDesc = '模板描述'; +$lang->docTemplate->status = '模板状态'; +$lang->docTemplate->emptyTip = '此参数与筛选条件下,暂无符合条件系统数据。'; +$lang->docTemplate->previewTip = '配置参数后,此区块会根据筛选器的配置展示相应的列表数据。'; +$lang->docTemplate->confirmDeleteChapterWithSub = "删除章节后,章节下层级内容将一并隐藏,确定要删除该章节吗?"; +$lang->docTemplate->confirmDeleteTemplateWithSub = "删除文档模板后,文档模板下层级内容将一并隐藏,确定要删除该文档模板吗?"; +$lang->docTemplate->scopeHasTemplateTips = '该范围下有文档模板,请移除后再删除范围。'; +$lang->docTemplate->scopeHasModuleTips = '该范围下有模板分类数据,请移除后再删除范围。'; +$lang->docTemplate->confirmDeleteReviewTemplate = "删除后,会影响项目中由该模板自动生成系统数据进行评审的功能,确认要删除吗?"; +$lang->docTemplate->needEditable = '您没有当前文档模板的编辑权限。'; + +$lang->docTemplate->more = '更多'; +$lang->docTemplate->scopeLabel = '范围'; +$lang->docTemplate->noTemplate = '没有文档模板'; +$lang->docTemplate->noDesc = '暂时没有描述'; +$lang->docTemplate->of = '的'; +$lang->docTemplate->overdue = '已过期'; + +$lang->docTemplate->create = '创建模板'; +$lang->docTemplate->edit = '编辑文档模板'; +$lang->docTemplate->delete = '删除文档模板'; + +$lang->docTemplate->addModule = '添加分类'; +$lang->docTemplate->addSameModule = '添加同级分类'; +$lang->docTemplate->addSubModule = '添加子分类'; +$lang->docTemplate->editModule = '编辑分类'; +$lang->docTemplate->deleteModule = '删除分类'; +$lang->docTemplate->noModules = '没有文档模板分类'; +$lang->docTemplate->addSubDocTemplate = '添加子文档模板'; + +$lang->docTemplate->filterTypes = array(); +$lang->docTemplate->filterTypes[] = array('all', '全部'); +$lang->docTemplate->filterTypes[] = array('draft', '草稿'); +$lang->docTemplate->filterTypes[] = array('released', '已发布'); +$lang->docTemplate->filterTypes[] = array('createdByMe', '我创建的'); + /* 字段列表。*/ $lang->doc->common = '文档'; $lang->doc->id = 'ID'; @@ -137,6 +206,7 @@ $lang->doc->update = '更新'; $lang->doc->nextStep = '下一步'; $lang->doc->closed = '已关闭'; $lang->doc->saveDraft = '存为草稿'; +$lang->doc->template = '模板'; $lang->doc->position = '所在位置'; $lang->doc->person = '个人'; $lang->doc->team = '团队'; @@ -293,6 +363,7 @@ $lang->doc->zentaoList['planStory'] = $lang->productplan->shortCommon . $la $lang->doc->zentaoList['case'] = $lang->testcase->common; $lang->doc->zentaoList['productCase'] = $lang->productCommon . $lang->testcase->common; +$lang->doc->zentaoList['projectCase'] = $lang->projectCommon . $lang->testcase->common; $lang->doc->zentaoList['caselib'] = '用例库' . $lang->testcase->common; $lang->doc->zentaoList['task'] = $lang->task->common; @@ -304,14 +375,17 @@ $lang->doc->zentaoList['more'] = '更多'; $lang->doc->zentaoList['productPlan'] = $lang->productCommon . '下计划'; $lang->doc->zentaoList['productPlanContent'] = $lang->productCommon . '计划下的内容'; $lang->doc->zentaoList['productRelease'] = $lang->productCommon . $lang->release->common; +$lang->doc->zentaoList['projectRelease'] = $lang->projectCommon . $lang->release->common; $lang->doc->zentaoList['ER'] = $lang->defaultERName; $lang->doc->zentaoList['UR'] = $lang->URCommon; $lang->doc->zentaoList['feedback'] = '反馈'; $lang->doc->zentaoList['ticket'] = '工单'; +$lang->doc->zentaoList['gantt'] = '甘特图'; $lang->doc->zentaoAction = array(); -$lang->doc->zentaoAction['set'] = '设置'; -$lang->doc->zentaoAction['delete'] = '删除'; +$lang->doc->zentaoAction['set'] = '设置'; +$lang->doc->zentaoAction['delete'] = '删除'; +$lang->doc->zentaoAction['setParams'] = '配置参数'; $lang->doc->uploadFormatList = array(); $lang->doc->uploadFormatList['separateDocs'] = '每个文件存为不同文档'; @@ -348,6 +422,9 @@ $lang->doc->statusList['draft'] = "草稿"; $lang->doc->aclList['open'] = "公开(所有人都可查看和编辑)"; $lang->doc->aclList['private'] = "私有(仅特定人员可查看和编辑)"; +$lang->doc->aclListA['open'] = "公开(所有人均可访问,有编辑文档模板权限可访问并维护)"; +$lang->doc->aclListA['private'] = "私有(仅创建者自己可以编辑、使用)"; + $lang->doc->selectSpace = '选择空间'; $lang->doc->space = '所属空间'; $lang->doc->spaceList['mine'] = '我的空间'; @@ -454,6 +531,7 @@ $lang->doc->docConvertComment = "文档已经转换为新编辑器格 $lang->doc->previewNotAvailable = '预览功能暂不可用,请访问禅道查看文档 %s。'; $lang->doc->hocuspocusConnect = '协作编辑服务已连接。'; $lang->doc->hocuspocusDisconnect = '协作编辑服务已断开,编辑内容将在重新连接后同步。'; +$lang->doc->docTemplateConvertComment = "文档模板已经转换为新编辑器格式,切换版本 %s 来查看转换前的文档模板。"; $lang->doc->noticeAcl['lib']['product']['default'] = "有所选{$lang->productCommon}访问权限的用户可以访问。"; $lang->doc->noticeAcl['lib']['product']['custom'] = "有所选{$lang->productCommon}访问权限或白名单里的用户可以访问。"; @@ -523,6 +601,18 @@ $lang->doc->projectFilterTypes[] = array('mine', '我参与的'); $lang->doc->spaceFilterTypes[] = array('all', '全部'); +$lang->doc->manageScope = '维护范围'; +$lang->doc->browseTemplate = '模板广场'; +$lang->doc->createTemplate = '创建文档模板'; +$lang->doc->editTemplate = '编辑文档模板'; +$lang->doc->moveTemplate = '移动文档模板'; +$lang->doc->deleteTemplate = '删除文档模板'; +$lang->doc->viewTemplate = '文档模板详情'; +$lang->doc->addTemplateType = '添加模板分类'; +$lang->doc->editTemplateType = '编辑模板分类'; +$lang->doc->deleteTemplateType = '删除模板分类'; +$lang->doc->sortTemplate = '排序'; + $lang->doc->docLang = new stdClass(); $lang->doc->docLang->cancel = $lang->cancel; $lang->doc->docLang->export = $lang->export; @@ -566,6 +656,7 @@ $lang->doc->docLang->moveTo = $lang->doc->moveTo; $lang->doc->docLang->moveLib = $lang->doc->moveLibAction; $lang->doc->docLang->moduleName = $lang->doc->catalogName; $lang->doc->docLang->saveDraft = $lang->doc->saveDraft; +$lang->doc->docLang->template = $lang->doc->template; $lang->doc->docLang->release = $lang->doc->release; $lang->doc->docLang->batchMove = $lang->doc->batchMove; $lang->doc->docLang->filterTypes = $lang->doc->filterTypes; @@ -689,3 +780,26 @@ $lang->doc->docLang->editing = '正在编辑'; $lang->doc->docLang->restoreVersionHint = '恢复到版本'; $lang->doc->docLang->restoreVersion = '恢复'; $lang->doc->docLang->restoreVersionConfirm = '这将使用文档版本 {version} 的内容创建一个新的版本,确定要继续吗?'; + +$lang->docTemplate->types = array(); +$lang->docTemplate->types['plan'] = '计划'; +$lang->docTemplate->types['story'] = '需求'; +$lang->docTemplate->types['design'] = '设计'; +$lang->docTemplate->types['dev'] = '开发'; +$lang->docTemplate->types['test'] = '测试'; +$lang->docTemplate->types['desc'] = '说明'; +$lang->docTemplate->types['other'] = '其他'; + +$lang->docTemplate->builtInScopes = array(); +$lang->docTemplate->builtInScopes['rnd'] = array(); +$lang->docTemplate->builtInScopes['or'] = array(); +$lang->docTemplate->builtInScopes['lite'] = array(); +$lang->docTemplate->builtInScopes['rnd']['product'] = '产品'; +$lang->docTemplate->builtInScopes['rnd']['project'] = '项目'; +$lang->docTemplate->builtInScopes['rnd']['execution'] = '执行'; +$lang->docTemplate->builtInScopes['rnd']['personal'] = '个人'; +$lang->docTemplate->builtInScopes['or']['market'] = '市场'; +$lang->docTemplate->builtInScopes['or']['product'] = '产品'; +$lang->docTemplate->builtInScopes['or']['personal'] = '个人'; +$lang->docTemplate->builtInScopes['lite']['project'] = '项目'; +$lang->docTemplate->builtInScopes['lite']['personal'] = '个人'; diff --git a/module/doc/model.php b/module/doc/model.php index dea4f20e09..5d59a9ac3e 100644 --- a/module/doc/model.php +++ b/module/doc/model.php @@ -652,6 +652,34 @@ class docModel extends model return $query; } + /** + * 获取文档模板列表。 + * Get document template list. + * + * @param int $libID + * @param string $type + * @param string $orderBy + * @param object $pager + * @param string $searchName + * @access public + * @return array + */ + public function getDocTemplateList(int $libID = 0, string $type = 'all', string $orderBy = 'id_desc', object $pager = null, string $searchName = ''): array + { + return $this->dao->select('*')->from(TABLE_DOC) + ->where('templateType')->ne('') + ->andWhere('vision')->eq($this->config->vision) + ->beginIF(!$this->app->user->admin)->andWhere("(`status` = 'normal' or (`status` = 'draft' and `addedBy`='{$this->app->user->account}'))")->fi() + ->beginIF($libID)->andWhere('lib')->eq($libID)->fi() + ->beginIF($type == 'draft')->andWhere('status')->eq('draft')->fi() + ->beginIF($type == 'released')->andWhere('status')->eq('normal')->fi() + ->beginIF($type == 'createdByMe')->andWhere('addedBy')->eq($this->app->user->account)->fi() + ->beginIF($searchName)->andWhere('title')->like("%{$searchName}%")->fi() + ->orderBy($orderBy) + ->page($pager) + ->fetchAll('', false); + } + /** * 通过文档ID获取文档所属产品、项目、执行。 * Get projects, executions and products by docIdList. @@ -738,18 +766,21 @@ class docModel extends model * @param array $libs * @param string $spaceType * @param int $excludeID + * @param bool $queryTemplate * @access public * @return array */ - public function getDocsOfLibs(array $libs, string $spaceType, int $excludeID = 0): array + public function getDocsOfLibs(array $libs, string $spaceType, int $excludeID = 0, $queryTemplate = false): array { $docs = $this->dao->select('t1.*')->from(TABLE_DOC)->alias('t1') ->leftJoin(TABLE_MODULE)->alias('t2')->on('t1.module=t2.id') ->where('t1.lib')->in($libs) ->andWhere('t1.vision')->eq($this->config->vision) - ->andWhere('t1.templateType')->eq('') + ->beginIF(!$queryTemplate)->andWhere('t1.templateType')->eq('')->fi() + ->beginIF($queryTemplate)->andWhere('t1.templateType')->ne('')->fi() + ->beginIF(!$queryTemplate)->andWhere('t2.type')->eq('doc')->fi() + ->beginIF($queryTemplate)->andWhere('t2.type')->eq('docTemplate')->fi() ->andWhere("(t1.status = 'normal' or (t1.status = 'draft' and t1.addedBy='{$this->app->user->account}'))") - ->andWhere('t2.type')->eq('doc') ->andWhere('t2.deleted')->eq('0') ->beginIF(!empty($excludeID))->andWhere("NOT FIND_IN_SET('{$excludeID}', t1.`path`)")->andWhere('t1.id')->ne($excludeID)->fi() ->orderBy('t1.`order` asc, t1.id asc') @@ -758,7 +789,8 @@ class docModel extends model $rootDocs = $this->dao->select('*')->from(TABLE_DOC) ->where('lib')->in($libs) ->andWhere('vision')->eq($this->config->vision) - ->andWhere('templateType')->eq('') + ->beginIF(!$queryTemplate)->andWhere('templateType')->eq('')->fi() + ->beginIF($queryTemplate)->andWhere('templateType')->ne('')->fi() ->andWhere("(status = 'normal' or (status = 'draft' and addedBy='{$this->app->user->account}'))") ->andWhere('module')->in(array('0', '')) ->beginIF(!empty($excludeID))->andWhere("NOT FIND_IN_SET('{$excludeID}', `path`)")->andWhere('id')->ne($excludeID)->fi() @@ -846,13 +878,14 @@ class docModel extends model $doc->editable = false; $isOpen = $doc->acl == 'open'; - $isAuthorOrAdmin = $doc->acl == 'private' && ($doc->addedBy == $currentAccount || ($this->app->user->admin && $spaceType !== 'mine')); + $isAuthorOrAdmin = $doc->addedBy == $currentAccount || ($this->app->user->admin && $spaceType !== 'mine'); $isInReadUsers = strpos(",$doc->readUsers,", ",$currentAccount,") !== false; $isInEditUsers = strpos(",$doc->users,", ",$currentAccount,") !== false; if($isOpen || $isAuthorOrAdmin || $isInReadUsers || $isInEditUsers) { $doc->editable = $isOpen || $isAuthorOrAdmin || $isInEditUsers; $doc->readable = $isOpen || $isAuthorOrAdmin || $isInReadUsers || $doc->editable; + if($spaceType == 'template') $doc->editable = ($isOpen && common::hasPriv('doc', 'editTemplate')) || $isAuthorOrAdmin || $isInEditUsers; } elseif(!empty($doc->groups) || !empty($doc->readGroups)) { @@ -1180,8 +1213,23 @@ class docModel extends model $doc->productName = $doc->executionName = $doc->moduleName = ''; if($doc->product) $doc->productName = $this->dao->findByID($doc->product)->from(TABLE_PRODUCT)->fetch('name'); if($doc->execution) $doc->executionName = $this->dao->findByID($doc->execution)->from(TABLE_EXECUTION)->fetch('name'); - if($doc->module) $doc->moduleName = $this->dao->findByID($doc->module)->from(TABLE_MODULE)->fetch('name'); - if(!$doc->module && $doc->type == 'article' && $doc->parent) $doc->moduleName = $this->dao->findByID($doc->parent)->from(TABLE_DOC)->fetch('title'); + if($doc->module) + { + if($doc->type == 'article' && $doc->parent) + { + $doc->moduleName = $this->dao->findByID($doc->parent)->from(TABLE_DOC)->fetch('title'); + } + elseif(!empty($doc->templateType)) + { + $modules = $this->getTemplateModules(); + $modules = array_column($modules, 'fullName', 'id'); + $doc->moduleName = zget($modules, $doc->module); + } + else + { + $doc->moduleName = $this->dao->findByID($doc->module)->from(TABLE_MODULE)->fetch('name'); + } + } return $doc; } @@ -1545,12 +1593,16 @@ class docModel extends model if(empty($doc->lib) && strpos((string)$doc->module, '_') !== false) list($doc->lib, $doc->module) = explode('_', $doc->module); if(empty($doc->lib)) return dao::$errors['lib'] = sprintf($this->lang->error->notempty, $this->lang->doc->lib); + $isDoc = empty($doc->templateType); $isDraft = $doc->status == 'draft'; $lib = $this->getLibByID($doc->lib); if($doc->contentType != 'doc') $doc = $this->loadModel('file')->processImgURL($doc, $this->config->doc->editor->create['id'], (string)$this->post->uid); - $doc->product = $lib->product; - $doc->project = $lib->project; - $doc->execution = $lib->execution; + if($isDoc) + { + $doc->product = $lib->product; + $doc->project = $lib->project; + $doc->execution = $lib->execution; + } $docContent = new stdclass(); $docContent->title = $doc->title; @@ -1567,7 +1619,7 @@ class docModel extends model unset($doc->contentType); unset($doc->rawContent); - $requiredFields = $isDraft ? 'title' : $this->config->doc->create->requiredFields; + $requiredFields = $isDraft ? 'title' : ($isDoc ? $this->config->doc->create->requiredFields : $this->config->doc->createTemplate->requiredFields); if(strpos("url|word|ppt|excel", $doc->type) !== false) $requiredFields = trim(str_replace(",content,", ",", ",{$requiredFields},"), ','); $checkContent = strpos(",$requiredFields,", ',content,') !== false; @@ -1601,11 +1653,11 @@ class docModel extends model $files = $this->file->saveUpload('doc', $docID); $docContent->doc = $docID; - $docContent->files = join(',', array_keys($files)); + $docContent->files = implode(',', array_keys($files)); + if(!empty($doc->template)) $docContent->rawContent = $this->loadExtension('zentaomax')->getTemplateContent((int)$doc->template, $docID); $this->dao->insert(TABLE_DOCCONTENT)->data($docContent)->exec(); - $this->loadModel('score')->create('doc', 'create', $docID); - + $this->loadModel('score')->create('doc', $isDoc ? 'create' : 'createTemplate', $docID); return array('status' => 'new', 'id' => $docID, 'files' => $files, 'docType' => $doc->type, 'libID' => $doc->lib); } @@ -1678,37 +1730,32 @@ class docModel extends model $files = $this->loadModel('file')->saveUpload('doc', $docID); if(dao::isError()) return false; - $oldDoc = $this->getByID($docID); - $changes = common::createChanges($oldDoc, $doc); - $oldRawContent = isset($oldDoc->rawContent) ? $oldDoc->rawContent : ''; - $newRawContent = isset($doc->rawContent) ? $doc->rawContent : ''; - $onlyRawChanged = $oldRawContent != $newRawContent; - $isDraft = $doc->status == 'draft'; - $version = $isDraft ? 0 : ($oldDoc->version + 1); - $changed = $files || $onlyRawChanged || (!$isDraft && $oldDoc->version == 0); + $oldDoc = $this->getByID($docID); + $changes = common::createChanges($oldDoc, $doc); + $oldRawContent = isset($oldDoc->rawContent) ? $oldDoc->rawContent : ''; + $newRawContent = isset($doc->rawContent) ? $doc->rawContent : ''; + $onlyRawChanged = $oldRawContent != $newRawContent; + $isDraft = $doc->status == 'draft'; + $version = $isDraft ? 0 : ($oldDoc->version + 1); + $changed = $files || $onlyRawChanged || (!$isDraft && $oldDoc->version == 0); + $basicInfoChanged = false; foreach($changes as $change) { + if(in_array($change['field'], array('module', 'lib', 'acl', 'groups', 'users'))) $basicInfoChanged = true; if($change['field'] == 'content' || $change['field'] == 'title' || $change['field'] == 'rawContent') $changed = true; - if($change['field'] == 'content') - { - $onlyRawChanged = false; - break; - } + if($change['field'] == 'content') $onlyRawChanged = false; } if($onlyRawChanged) $changes[] = array('field' => 'content', 'old' => $oldDoc->content, 'new' => $doc->content); if($changed) $this->saveDocContent($docID, $doc, $version, array_merge(array_keys($files), array_keys($oldDoc->files))); else $version = $oldDoc->version; if(dao::isError()) return false; - unset($doc->contentType); - unset($doc->rawContent); - unset($doc->fromVersion); $doc->version = max($version, $oldDoc->version); $doc->draft = $isDraft ? $doc->content : ''; $doc->content = $doc->title; if(empty($doc->status)) $doc->status = $isDraft ? $oldDoc->status : 'normal'; - if($doc->parent != $oldDoc->parent) + if(isset($doc->parent) && $doc->parent != $oldDoc->parent) { $path = ",{$docID}"; if($doc->parent) @@ -1720,7 +1767,7 @@ class docModel extends model $doc->path = $path; } - $this->dao->update(TABLE_DOC)->data($doc, 'content') + $this->dao->update(TABLE_DOC)->data($doc, 'content,contentType,rawContent,fromVersion') ->autoCheck() ->batchCheck($requiredFields, 'notempty') ->where('id')->eq($docID) @@ -1728,6 +1775,24 @@ class docModel extends model if(dao::isError()) return false; if($files) $this->file->updateObjectID($this->post->uid, $docID, 'doc'); + + /* 如果修改了父模板,子模板也同步更新相关信息。*/ + /* If the parent template is modified, the child template will also update the relevant information synchronously. */ + if(!empty($oldDoc->templateType) && empty($doc->parent) && $basicInfoChanged) + { + $this->dao->update(TABLE_DOC) + ->set('module')->eq($doc->module) + ->set('lib')->eq($doc->lib) + ->set('acl')->eq($doc->acl) + ->where("FIND_IN_SET('{$docID}', `path`)") + ->exec(); + } + + if(in_array($oldDoc->contentType, array('html', 'attachment', 'markdown')) && !in_array($doc->contentType, array('html', 'attachment', 'markdown'))) + { + $objectType = !empty($doc->templateType) ? 'doctemplate' : 'doc'; + $this->loadModel('action')->create($objectType, $docID, 'convertToNewDoc'); + } return array('changes' => $changes, 'files' => array_keys($files)); } @@ -1944,9 +2009,12 @@ class docModel extends model if($doc->status == 'draft' && $doc->addedBy != $this->app->user->account) return false; if($doc->status == 'normal' && $this->app->user->admin) return true; - static $libs = array(); - if(!isset($libs[$doc->lib])) $libs[$doc->lib] = $this->getLibByID((int)$doc->lib); - if(!$this->checkPrivLib($libs[$doc->lib], '', (string)$doc->id)) return false; + if(empty($doc->templateType)) + { + static $libs = array(); + if(!isset($libs[$doc->lib])) $libs[$doc->lib] = $this->getLibByID((int)$doc->lib); + if(!$this->checkPrivLib($libs[$doc->lib], '', (string)$doc->id)) return false; + } if(in_array($doc->acl, array('open', 'public'))) return true; @@ -2725,6 +2793,7 @@ class docModel extends model $statistic->totalDocs = $this->dao->select('COUNT(1) AS count')->from(TABLE_DOC) ->where('deleted')->eq('0') ->andWhere('type')->in($this->config->doc->docTypes) + ->andWhere('templateType')->eq('') ->andWhere('vision')->eq($this->config->vision) ->fetch('count'); @@ -2753,6 +2822,7 @@ class docModel extends model $myStatistic = $this->dao->select("COUNT(1) AS myDocs, SUM(views) as docViews, SUM(collects) as docCollects")->from(TABLE_DOC) ->where('addedBy')->eq($this->app->user->account) ->andWhere('type')->in($this->config->doc->docTypes) + ->andWhere('templateType')->eq('') ->andWhere('deleted')->eq(0) ->andWhere('vision')->eq($this->config->vision) ->andWhere('lib')->ne('') @@ -3878,6 +3948,355 @@ class docModel extends model return !empty($docID); } + /** + * 构建模板类型数据。 + * Build data of template type module. + * + * @param int $scope + * @param int $parent + * @param string $name + * @param string $code + * @param int $grade + * @param string $path + * @access public + * @return object + */ + public function buildTemplateModule($scope, $parent, $name, $code, $grade = 1, $path = '') + { + $module = new stdclass(); + $module->type = 'docTemplate'; + $module->root = $scope; + $module->parent = $parent; + $module->name = $name; + $module->short = $code; + $module->grade = $grade; + $module->path = $path; + + return $module; + } + + /** + * 检查文档模板是否已升级。 + * Check if doc template has been upgraded + * + * @access public + * @return bool + */ + public function checkIsTemplateUpgraded() + { + $templateModule = $this->dao->select('1')->from(TABLE_MODULE)->where('type')->eq('docTemplate')->fetch(); + return !empty($templateModule); + } + + /** + * 升级模板类型数据。 + * Upgrade document template types. + * + * @access public + * @return bool + */ + public function upgradeTemplateTypes() + { + $this->app->loadLang('baseline'); + $currentLang = $this->app->getClientLang(); + $templateTypes = $this->dao->select('`key`, `value`')->from(TABLE_LANG) + ->where('module')->eq('baseline') + ->andWhere('section')->eq('objectList') + ->andWhere('lang')->eq($currentLang) + ->andWhere('vision')->eq($this->config->vision) + ->fetchPairs(); + if(empty($templateTypes)) $templateTypes = $this->lang->baseline->objectList; + + $usedTemplateTypes = $this->dao->select('`key`, `value`')->from(TABLE_LANG)->alias('t1') + ->leftJoin(TABLE_DOC)->alias('t2')->on('t1.key = t2.templateType') + ->where('t1.module')->eq('baseline') + ->andWhere('t1.section')->eq('objectList') + ->andWhere('t1.lang', true)->ne($currentLang) + ->orWhere('t1.vision')->ne($this->config->vision) + ->markRight(1) + ->andWhere('t1.key')->notin(array_keys($templateTypes)) + ->andWhere('t2.templateType')->ne('') + ->andWhere('t2.lib')->eq('') + ->andWhere('t2.module')->eq('') + ->andWhere('t2.deleted')->eq(0) + ->fetchPairs(); + + $oldTemplateTypes = array_filter(arrayUnion($templateTypes, $usedTemplateTypes)); + + if(empty($oldTemplateTypes)) return true; + + $this->loadModel('setting'); + foreach(array('rnd', 'or') as $vision) + { + $scopeMaps = $this->setting->getItem("vision={$vision}&owner=system&module=doc&key=builtInScopeMaps"); + $scopeMaps = json_decode($scopeMaps, true); + $scopeID = $vision == 'rnd' ? zget($scopeMaps, 'project', 0) : zget($scopeMaps, 'product', 0); + if(!$scopeID) continue; + + $parentTypes = array(); + foreach($this->lang->docTemplate->types as $key => $value) + { + $module = $this->buildTemplateModule($scopeID, 0, $value, $key, 1); + $this->dao->insert(TABLE_MODULE)->data($module)->exec(); + if(dao::isError()) return false; + + $moduleID = $this->dao->lastInsertID(); + $this->dao->update(TABLE_MODULE)->set('path')->eq(",{$moduleID},")->where('id')->eq($moduleID)->exec(); + + $parentTypes[$key] = $moduleID; + } + + foreach($oldTemplateTypes as $key => $value) + { + $parentKey = zget($this->config->doc->templateTypeParents, $key, 'other'); + $parentID = $parentTypes[$parentKey]; + + $module = $this->buildTemplateModule($scopeID, $parentID, $value, $key, 2); + $this->dao->insert(TABLE_MODULE)->data($module)->exec(); + if(dao::isError()) return false; + + $moduleID = $this->dao->lastInsertID(); + $this->dao->update(TABLE_MODULE)->set('path')->eq(",{$parentID},{$moduleID},")->where('id')->eq($moduleID)->exec(); + } + } + return true; + } + + /** + * 添加内置的模板范围。 + * Add built in scopes. + * + * @access public + * @return bool + */ + public function addBuiltInScopes() + { + $builtInScopes = $this->dao->select('*')->from(TABLE_DOCLIB)->where('type')->eq('template')->andWhere('main')->eq('1')->fetchAll(); + if(!empty($builtInScopes)) return; + + $this->loadModel('setting'); + $scope = new stdClass(); + $scope->type = 'template'; + $scope->main = '1'; + $scope->addedBy = 'system'; + $scope->addedDate = helper::now(); + foreach($this->lang->docTemplate->builtInScopes as $vision => $scopeList) + { + $scopeMaps = array(); + $scope->vision = $vision; + foreach($scopeList as $scopeKey => $scopeName) + { + if(empty($scopeName)) continue; + + $scope->name = $scopeName; + $this->dao->insert(TABLE_DOCLIB)->data($scope)->exec(); + + $scopeID = $this->dao->lastInsertID(); + $scopeMaps[$scopeKey] = $scopeID; + } + if(!empty($scopeMaps)) $this->setting->setItem("system.doc.builtInScopeMaps@{$vision}", json_encode($scopeMaps)); + } + } + + /** + * 升级文档模板的范围和类型。 + * Upgrade lib and module of template. + * + * @param array $templateIdList + * @access public + * @return bool + */ + public function upgradeTemplateLibAndModule(array $templateIdList = array()) + { + $templateList = $this->dao->select('*')->from(TABLE_DOC)->where('id')->in($templateIdList)->fetchAll('id'); + if(empty($templateList)) return true; + + $this->loadModel('setting'); + $rndScopeMaps = $this->setting->getItem('vision=rnd&owner=system&module=doc&key=builtInScopeMaps'); + $orScopeMaps = $this->setting->getItem('vision=or&owner=system&module=doc&key=builtInScopeMaps'); + $rndScopeMaps = json_decode($rndScopeMaps, true); + $orScopeMaps = json_decode($orScopeMaps, true); + $moduleGroup = $this->dao->select('root,short,id')->from(TABLE_MODULE)->where('deleted')->eq('0')->andWhere('root')->in(array($rndScopeMaps['project'], $orScopeMaps['product']))->fetchGroup('root', 'short'); + foreach($templateList as $id => $template) + { + $templateVision = $template->vision; + $templateType = $template->templateType; + $templateScopeID = $templateVision == 'or' ? zget($orScopeMaps, 'product', 0) : zget($rndScopeMaps, 'project', 0); + $template->lib = $templateScopeID; + $template->module = $moduleGroup[$templateScopeID][$templateType]->id; + $template->assignedDate = helper::isZeroDate($template->assignedDate) ? null : $template->assignedDate; + $template->approvedDate = helper::isZeroDate($template->approvedDate) ? null : $template->approvedDate; + $this->dao->update(TABLE_DOC)->data($template)->where('id')->eq($id)->exec(); + if(dao::isError()) return false; + } + + return true; + } + + /** + * 获取文档模板类型的模块。 + * Get modules of doc template type. + * + * @param bool $onlyNode + * @param string|int $root + * @param string $grade + * @access public + * @return array + */ + public function getTemplateModules($root = 'all', $grade = 'all') + { + $modules = $this->dao->select('*')->from(TABLE_MODULE) + ->where('deleted')->eq('0') + ->andWhere('type')->eq('docTemplate') + ->beginIF($root != 'all')->andWhere('root')->eq($root)->fi() + ->beginIF($grade != 'all')->andWhere('grade')->eq($grade)->fi() + ->fetchAll('id'); + + foreach($modules as $module) + { + $path = explode(',', trim($module->path, ',')); + $names = array(''); + foreach($path as $id) + { + if(!isset($modules[$id])) continue; + $names[] = $modules[$id]->name; + } + $module->fullName = implode('/', $names); + } + + return array_values($modules); + } + + /** + * 获取范围数据。 + * Get scope items. + * + * @param array $scopeList + * @access public + * @return array + */ + public function getScopeItems(array $scopeList = array()) + { + $items = array(); + foreach($scopeList as $scope) $items[] = array('value' => $scope->id, 'text' => $scope->name); + return $items; + } + + /** + * 判断一个模块是否是内置的文档模板类型。 + * Judge whether a module is builtin template type. + * + * @param array $scopeIdList + * @access public + * @return bool + */ + public function getScopeTemplates(array $scopeIdList = array()) + { + $scopeTemplates = array(); + + foreach($scopeIdList as $scopeID) + { + $templates = $this->getHotTemplates($scopeID, 5); + $scopeTemplates[$scopeID] = $this->filterPrivDocs($templates, 'template'); + } + + return $scopeTemplates; + } + + /** + * 获取范围内的最近顶层文档模板。 + * Get the recent top template of scope. + * + * @param int $scopeID + * @param int $limit + * @access public + * @return array + */ + public function getHotTemplates($scopeID = 0, $limit = 0) + { + return $this->dao->select('*, CASE WHEN addedDate > editedDate THEN addedDate ELSE editedDate END as hotDate')->from(TABLE_DOC) + ->where('templateType')->ne('') + ->andWhere('deleted')->eq('0') + ->andWhere('status')->eq('normal') + ->andWhere('parent')->eq(0) + ->andWhere('vision')->eq($this->config->vision) + ->beginIF($scopeID)->andWhere('lib')->eq($scopeID)->fi() + ->orderBy('hotDate_desc') + ->beginIF($limit)->limit($limit)->fi() + ->fetchAll('', false); + } + + /** + * 删除一个文档模板。 + * Delete a doc template. + * + * @param string $table + * @param int $id + * @param string $objectType + * @access public + * @return void + */ + public function deleteTemplate(int $id) + { + if(empty($id)) return false; + + $this->dao->update(TABLE_DOC)->set('deleted')->eq(1)->where('id')->eq($id)->exec(); + $this->loadModel('action')->create('doctemplate', $id, 'deleted', '', ACTIONMODEL::CAN_UNDELETED); + + return true; + } + + /** + * 添加文档模板类型。 + * Add template type. + * + * @param object $moduleData + * @access public + * @return int + */ + public function addTemplateType($moduleData) + { + $this->dao->insert(TABLE_MODULE)->data($moduleData)->autoCheck()->exec(); + $moduleID = $this->dao->lastInsertID(); + + $path = $moduleData->grade == 1 ? ",{$moduleID}," : ",{$moduleData->parent},{$moduleID},"; + $this->dao->update(TABLE_MODULE)->set('path')->eq($path)->set('short')->eq("custom{$moduleID}")->where('id')->eq($moduleID)->exec(); + + return $moduleID; + } + + /** + * 获取某个模板类型下的所有模板。 + * Get template list by type. + * + * @param int|null $type + * @param string $status + * @access public + * @return int + */ + public function getTemplatesByType($type = null, $status = 'all') + { + $types = array(); + if(!is_null($type)) + { + $subTypes = $this->dao->select('id')->from(TABLE_MODULE) + ->where('deleted')->eq('0') + ->andWhere('parent')->eq($type) + ->andWhere('type')->eq('docTemplate') + ->fetchPairs('id'); + $types = array_values($subTypes); + $types[] = $type; + } + + return $this->dao->select('*')->from(TABLE_DOC) + ->where('deleted')->eq('0') + ->andWhere('templateType')->ne('') + ->andWhere('vision')->eq($this->config->vision) + ->beginIF(!is_null($type))->andWhere('module')->in($types)->fi() + ->beginIF($status != 'all')->andWhere('status')->eq($status)->fi() + ->fetchAll('id', false); + } + /** * 获取上次访问的文档对象。 * Get last viewed doc object. @@ -4098,4 +4517,207 @@ class docModel extends model return $items; } + + /** + * 获取文档模板的范围。 + * Get the scope of template. + * + * @access public + * @return array + */ + public function getTemplateScopes(): array + { + return $this->dao->select('*')->from(TABLE_DOCLIB)->where('type')->eq('template')->andWhere('vision')->eq($this->config->vision)->andWhere('deleted')->eq('0')->fetchAll('id'); + } + + /** + * 更新模板范围。 + * Update the scope of template. + * + * @param array scopeList + * @access public + * @return void + */ + public function updateTemplateScopes(array $scopeList = array()) + { + foreach($scopeList as $id => $name) + { + $this->dao->update(TABLE_DOCLIB)->set('name')->eq($name)->where('id')->eq($id)->andWhere('vision')->eq($this->config->vision)->exec(); + } + } + + /** + * 插入模板范围。 + * Insert the scope of template. + * + * @param array scopeList + * @access public + * @return void + */ + public function insertTemplateScopes(array $scopeList = array()) + { + foreach($scopeList as $name) + { + if(empty($name)) continue; + + $scope = new stdClass(); + $scope->name = $name; + $scope->type = 'template'; + $scope->main = '0'; + $scope->vision = $this->config->vision; + $scope->addedBy = $this->app->user->account; + $scope->addedDate = helper::now(); + $this->dao->insert(TABLE_DOCLIB)->data($scope)->exec(); + } + } + + /** + * 删除模板范围。 + * Delete the scope of template. + * + * @param array scopeList + * @access public + * @return void + */ + public function deleteTemplateScopes(array $scopeIdList = array()) + { + $this->dao->update(TABLE_DOCLIB)->set('deleted')->eq('1')->where('id')->in($scopeIdList)->exec(); + } + + /** + * 复制模板,将研发界面的除Wiki类型的模板复制到OR界面,将OR界面的所有文档模板复制到研发界面。 + * Copy templates of R&D interface, except for wiki type templates, to OR interface, and copy all templates of OR interface to R&D interface. + * + * @param array templateIdList + * @access public + * @return void + */ + public function copyTemplate(array $templateIdList = array()) + { + if(empty($templateIdList)) return array(); + + $oldTemplateList = $this->dao->select('*')->from(TABLE_DOC)->where('id')->in($templateIdList)->fetchAll('id'); + $oldContentList = $this->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->in($templateIdList)->fetchGroup('doc', 'id'); + $oldChapterList = $this->dao->select('*')->from(TABLE_DOC)->where('template')->ne('')->andWhere('type')->in('chapter,article')->fetchGroup('template', 'id'); + $newTemplateList = array(); + + foreach($oldTemplateList as $oldTemplateID => $template) + { + /* 研发界面的Wiki类型模板不复制到OR界面。*/ + /* The wiki type template of R&D interface is not copied to OR interface. */ + if($template->vision == 'rnd' && $template->type == 'book') continue; + + /* 复制模板及模板内容。*/ + /* Copy template and content. */ + unset($template->id); + $templateVision = $template->vision == 'rnd' ? 'or' : 'rnd'; + $template->vision = $templateVision; + $template->assignedDate = helper::isZeroDate($template->assignedDate) ? null : $template->assignedDate; + $template->approvedDate = helper::isZeroDate($template->approvedDate) ? null : $template->approvedDate; + $this->dao->insert(TABLE_DOC)->data($template)->exec(); + $newTemplateID = $this->dao->lastInsertID(); + + foreach($oldContentList[$oldTemplateID] as $content) + { + unset($content->id); + $content->doc = $newTemplateID; + $content->addedDate = helper::isZeroDate($content->addedDate) ? null : $content->addedDate; + $content->editedDate = helper::isZeroDate($content->editedDate) ? null : $content->editedDate; + $this->dao->insert(TABLE_DOCCONTENT)->data($content)->exec(); + } + + /* OR界面的Wiki类型模板复制到研发界面。*/ + /* Copy the wiki type template of OR interface to R&D interface. */ + if($template->type == 'book' && isset($oldChapterList[$oldTemplateID])) + { + $chapterList = $oldChapterList[$oldTemplateID]; + $chapterContent = $this->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->in(array_keys($chapterList))->fetchGroup('doc', 'id'); + foreach($chapterList as $oldChapterID => $chapter) + { + unset($chapter->id); + $chapter->vision = 'rnd'; + $chapter->template = $newTemplateID; + $this->dao->insert(TABLE_DOC)->data($chapter)->exec(); + $newChapterID = $this->dao->lastInsertID(); + + foreach($chapterContent[$oldChapterID] as $content) + { + unset($content->id); + $content->doc = $newChapterID; + $this->dao->insert(TABLE_DOCCONTENT)->data($content)->exec(); + } + } + } + + if(!isset($newTemplateList['all'])) $newTemplateList['all'] = array(); + if(!isset($newTemplateList['wiki'])) $newTemplateList['wiki'] = array(); + if(!isset($newTemplateList['html'])) $newTemplateList['html'] = array(); + $newTemplateList['all'][] = $newTemplateID; + if($template->type == 'book') $newTemplateList['wiki'][] = $newTemplateID; + if($template->type == 'html' || $template->type == 'markdown') $newTemplateList['html'][] = $newTemplateID; + } + + return $newTemplateList; + } + + /** + * 添加内置文档模板。 + * Add the built-in doc template. + * + * @param array typeList + * @access public + * @return void + */ + public function addBuiltInDocTemplateByType(array $typeList) + { + $rndScopeMaps = $this->loadModel('setting')->getItem('vision=rnd&owner=system&module=doc&key=builtInScopeMaps'); + $rndScopeMaps = json_decode($rndScopeMaps, true); + $projectScopeID = zget($rndScopeMaps, 'project', 0); + $modulePairs = $this->dao->select('short,id')->from(TABLE_MODULE)->where('root')->eq($projectScopeID)->andWhere('type')->eq('docTemplate')->fetchPairs('short'); + + $builtInTemplate = new stdClass(); + $builtInTemplate->lib = $projectScopeID; + $builtInTemplate->type = 'text'; + $builtInTemplate->addedBy = 'system'; + $builtInTemplate->addedDate = helper::now(); + + $templateContent = new stdClass(); + $templateContent->type = 'doc'; + $templateContent->version = 1; + $templateContent->addedBy = 'system'; + $templateContent->addedDate = helper::now(); + + $this->loadModel('action'); + $this->loadModel('upgrade'); + $this->app->loadLang('baseline'); + foreach($typeList as $type) + { + /* 创建与分类同名的内置文档模板。*/ + /* Add the doc template with the same name as the type. */ + $builtInTemplate->module = zget($modulePairs, $type, 0); + $builtInTemplate->title = $this->lang->baseline->objectList[$type]; + $builtInTemplate->templateType = $type; + $this->dao->insert(TABLE_DOC)->data($builtInTemplate)->exec(); + $templateID = $this->dao->lastInsertID(); + + /* 获取模板的动态区块。*/ + /* Get the block of doc template. */ + $templateBlock = $this->upgrade->getTemplateBlock($templateID); + $templateHtml = empty($templateBlock) ? '' : "
"; + + /* 添加模板内容。*/ + /* Add the content of doc template. */ + $templateContent->content = $templateHtml; + $templateContent->rawContent = json_encode(array('$migrate' => 'html', '$data' => $templateHtml)); + $templateContent->doc = $templateID; + $templateContent->title = $builtInTemplate->title; + $this->dao->insert(TABLE_DOCCONTENT)->data($templateContent)->exec(); + + $this->action->create('docTemplate', $templateID, 'Created'); + } + + /* 记录文档模板的更新时间。*/ + /* Record the time of upgrade doc template. */ + $this->setting->setItem("system.doc.upgradeTime", helper::now()); + } } diff --git a/module/doc/tao.php b/module/doc/tao.php index 89e8adc3a3..bd524cafec 100644 --- a/module/doc/tao.php +++ b/module/doc/tao.php @@ -266,12 +266,25 @@ class docTao extends docModel * * @param int $docID * @param object $doc + * @param bool $basicInfoChanged * @access protected * @return void */ - protected function doUpdateDoc(int $docID, object $doc) + public function doUpdateDoc(int $docID, object $doc, $basicInfoChanged = false) { $this->dao->update(TABLE_DOC)->data($doc)->autoCheck()->where('id')->eq($docID)->exec(); + + if(empty($doc->parent) && $basicInfoChanged) + { + $childData = new stdclass(); + $childData->module = $doc->module; + $childData->lib = $doc->lib; + $childData->acl = $doc->acl; + if(isset($doc->groups)) $childData->groups = $doc->groups; + if(isset($doc->users)) $childData->users = $doc->users; + + $this->dao->update(TABLE_DOC)->data($childData)->where("FIND_IN_SET('{$docID}', `path`)")->exec(); + } } /** diff --git a/module/doc/test/lib/doc.unittest.class.php b/module/doc/test/lib/doc.unittest.class.php index 655eea16ee..8a4500c171 100644 --- a/module/doc/test/lib/doc.unittest.class.php +++ b/module/doc/test/lib/doc.unittest.class.php @@ -223,7 +223,7 @@ class docTest */ public function updateTest(int $docID, array $param): array { - $createFields = array('lib' => 0, 'module' => 0, 'title' => '', 'keywords' => '', 'type' => 'text', 'content' => '', 'contentType' => 'html', 'acl' => 'private', 'status' => 'normal'); + $createFields = array('lib' => 0, 'module' => 0, 'title' => '', 'keywords' => '', 'type' => 'text', 'content' => '', 'contentType' => 'html', 'acl' => 'private', 'status' => 'normal', 'editedBy' => 'admin', 'rawContent' => ''); $doc = new stdclass(); foreach($createFields as $field => $defaultValue) $doc->{$field} = $defaultValue; @@ -1413,9 +1413,9 @@ class docTest * @param int $order * @param string $type api|doc * @access public - * @return int|false + * @return int|false|string */ - public function updateOrderTest(int $catalogID, int $order, string $type = 'doc'): int|false + public function updateOrderTest(int $catalogID, int $order, string $type = 'doc'): int|false|string { $this->objectModel->updateOrder($catalogID, $order, $type); @@ -1441,6 +1441,167 @@ class docTest return $this->objectModel->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->eq($docID)->andWhere('version')->eq($doc->version + 1)->fetch(); } + /** + * 获取文档库的文档列表。 + * Get doc list. + * + * @param array $libs + * @param string $spaceType + * @param int $excludeID + * @access public + * @return array + */ + public function getDocsOfLibsTest(array $libs, string $spaceType, int $excludeID = 0): array + { + $docs = $this->objectModel->getDocsOfLibs($libs, $spaceType, $excludeID); + if(dao::isError()) return dao::getError(); + return $docs; + } + + /** + * 获取文档模板列表。 + * Get doc template list. + * + * @param int $libID + * @param string $type + * @param string $orderBy + * @param string $searchName + * @access public + * @return array + */ + public function getDocTemplateListTest(int $libID = 0, string $type = 'all', string $orderBy = 'id_desc', string $searchName = ''): array + { + $templates = $this->objectModel->getDocTemplateList($libID, $type, $orderBy, null, $searchName); + if(dao::isError()) return dao::getError(); + return $templates; + } + + /** + * 添加文档模板类型。 + * Add the type of template lis. + * + * @param array $moduleData + * @access public + * @return object + */ + public function addTemplateTypeTest(array $moduleData) + { + $module = new stdClass(); + foreach($moduleData as $field => $value) $module->{$field} = $value; + $moduleID = $this->objectModel->addTemplateType($module); + return $this->objectModel->dao->select('*')->from(TABLE_MODULE)->where('id')->eq($moduleID)->fetch(); + } + + /** + * 获取某个模板类型下的所有模板。 + * Get template list by type. + * + * @param int|null $type + * @param string $status + * @access public + * @return array + */ + public function getTemplatesByTypeTest($type = null, $status = 'all') + { + $templates = $this->objectModel->getTemplatesByType($type, $status); + if(dao::isError()) return dao::getError(); + return $templates; + } + + /** + * 测试获取模板类型。 + * Test get template modules. + * + * @param string $root + * @param string $grade + * @access public + * @return array|bool + */ + public function getTemplateModulesTest($root = 'all', $grade = 'all') + { + $templateModules = $this->objectModel->getTemplateModules($root, $grade); + if(dao::isError()) return dao::getError(); + return $templateModules; + } + + /** + * 获取范围下最近编辑的模板。 + * Get templates of scope. + * + * @param int $scopeID + * @param int $limit + * @access public + * @return int + */ + public function getHotTemplatesTest($scopeID = 0, $limit = 0) + { + $templates = $this->objectModel->getHotTemplates($scopeID, $limit); + if(dao::isError()) return dao::getError(); + return $templates; + } + + /** + * 获取所有范围下的模板。 + * Get templats of all scopes. + * + * @param int $scopeID + * @access public + * @return array + */ + public function getScopeTemplatesTest(int $scopeID = 0) + { + $templates = $this->objectModel->getScopeTemplates(array($scopeID)); + if(dao::isError()) return dao::getError(); + return $templates; + } + + /** + * 升级旧的文档模板。 + * Upgrade old template. + * + * @param int $templateID + * @access public + * @return object + */ + public function upgradeTemplateLibAndModuleTest(int $templateID) + { + $this->objectModel->upgradeTemplateLibAndModule(); + return $this->objectModel->dao->select('*')->from(TABLE_DOC)->where('id')->eq($templateID)->fetch(); + } + + /** + * 升级用户自定义模板类型数据。 + * Upgrade custom doc template types. + * + * @param int $moduleID + * @access public + * @return bool + */ + public function upgradeTemplateTypesTest(int $moduleID, string $name) + { + if($this->objectModel->config->edition != 'ipd') return true; + + $this->objectModel->upgradeTemplateTypes(); + $moduleName = $this->objectModel->dao->select('name')->from(TABLE_MODULE)->where('id')->eq($moduleID)->fetch('name'); + return $moduleName == $name; + } + + /** + * Batch move document. + * + * @param array $data + * @param array $docIdList + * @access public + * @return object + */ + public function batchMoveDocTest(array $data, array $docIdList) + { + $docData = new stdClass(); + foreach($data as $field => $value) $docData->{$field} = $value; + $this->objectModel->batchMoveDoc($docData, $docIdList); + return $this->objectModel->dao->select('*')->from(TABLE_DOC)->where('id')->in($docIdList)->fetchAll('id'); + } + /** * 删除文档。 * Delete document. @@ -1523,4 +1684,202 @@ class docTest $this->objectModel->setDocPrivError($docID, $objectID, $type); return $_SESSION; } + + /** + * 检查文档模板是否已升级。 + * Check if doc template has been upgraded + * + * @access public + * @return bool + */ + public function checkIsTemplateUpgradedTest() + { + return $this->objectModel->checkIsTemplateUpgraded(); + } + + /** + * 删除一个文档模板。 + * Delete a doc template. + * + * @param int $id + * @access public + * @return bool + */ + public function deleteTemplateTest(int $id) + { + $result = $this->objectModel->deleteTemplate($id); + if(!$result) return false; + + $deleted = $this->objectModel->dao->select('deleted')->from(TABLE_DOC)->where('id')->eq($id)->fetch('deleted'); + return $deleted == 1; + } + + /** + * 获取范围数据。 + * Get scope items. + * + * @param array scopeList + * @access public + * @return array + */ + public function getScopeItemsTest(array $scopeList = array()) + { + $scopes = array(); + foreach($scopeList as $scopeID => $scopeName) + { + $data = new stdClass(); + $data->id = $scopeID; + $data->name = $scopeName; + $scopes[] = $data; + } + $result = $this->objectModel->getScopeItems($scopes); + if(!$result) return false; + return $result; + } + + /** + * 构建模板类型数据。 + * Build data of template type module. + * + * @param int $scope + * @param int $parent + * @param string $name + * @param string $code + * @param int $grade + * @param string $path + * @access public + * @return object + */ + public function buildTemplateModuleTest($scope, $parent, $name, $code, $grade) + { + $result = $this->objectModel->buildTemplateModule($scope, $parent, $name, $code, $grade); + if(!$result) return false; + return $result ?: 0; + } + + /** + * 更新文档。 + * Update document. + * + * @param int $docID + * @param array $doc + * @param bool $basicInfoChanged + * @access protected + * @return object + */ + public function doUpdateDocTest(int $docID, array $doc) + { + $docData = new stdClass(); + foreach($doc as $field => $value) $docData->{$field} = $value; + $this->objectModel->doUpdateDoc($docID, $docData); + return $this->objectModel->dao->select('*')->from(TABLE_DOC)->where('id')->eq($docID)->fetch(); + } + + /** + * 更新模板范围。 + * Update the scope of template. + * + * @param array scopeList + * @access public + * @return void + */ + public function updateTemplateScopesTest(array $scopeList = array()) + { + $this->objectModel->updateTemplateScopes($scopeList); + return $this->objectModel->dao->select('id,name')->from(TABLE_DOCLIB)->where('id')->in(array_keys($scopeList))->fetchPairs('id'); + } + + /** + * 插入模板范围。 + * Insert the scope of template. + * + * @param array scopeList + * @access public + * @return void + */ + public function insertTemplateScopesTest(array $scopeList = array()) + { + $this->objectModel->insertTemplateScopes($scopeList); + return $this->objectModel->dao->select('*')->from(TABLE_DOCLIB)->fetchAll('id'); + } + + /** + * 删除模板范围。 + * Delete the scope of template. + * + * @param int scopeID + * @access public + * @return void + */ + public function deleteTemplateScopesTest(int $scopeID = 0) + { + $this->objectModel->deleteTemplateScopes(array($scopeID)); + return $this->objectModel->dao->select('deleted')->from(TABLE_DOCLIB)->where('id')->eq($scopeID)->fetch('deleted'); + } + + /** + * 添加内置的模板范围。 + * Add built in scopes. + * + * @access public + * @return bool + */ + public function addBuiltInScopesTest() + { + $this->objectModel->addBuiltInScopes(); + return $this->objectModel->dao->select('*')->from(TABLE_DOCLIB)->fetchAll('id'); + } + + /** + * 复制模板数据到OR界面。 + * Copy template to OR page. + * + * @param array scopeIdList + * @access public + * @return void + */ + public function copyTemplateTest(array $templateIdList = array()) + { + return $this->objectModel->copyTemplate($templateIdList); + } + + /** + * 添加内置文档模板。 + * Add the built-in doc template. + * + * @param int templateID + * @param array typeList + * @param string name + * @access public + * @return void + */ + public function addBuiltInDocTemplateByTypeTest(int $templateID, array $typeList, string $name) + { + if($this->objectModel->config->edition != 'ipd') return true; + + $this->objectModel->addBuiltInScopes(); + $this->objectModel->upgradeTemplateTypes(); + $this->objectModel->addBuiltInDocTemplateByType($typeList); + $templateName = $this->objectModel->dao->select('title')->from(TABLE_DOC)->where('id')->eq($templateID)->orderBy('id_desc')->fetch('title'); + return $templateName == $name; + } + + /** + * 设置文档的权限。 + * Set document priviledge test. + * + * @param object $doc + * @param string $spaceType + * @access public + * @return object + */ + public function setDocPrivTest(object $doc, string $spaceType = 'mine'): object + { + $doc = $this->objectModel->setDocPriv($doc, $spaceType); + + $doc->readable = $doc->readable ? '1' : '0'; + $doc->editable = $doc->editable ? '1' : '0'; + + return $doc; + } } diff --git a/module/doc/test/model/addbuiltindoctemplatebytype.php b/module/doc/test/model/addbuiltindoctemplatebytype.php new file mode 100755 index 0000000000..08702d7cf6 --- /dev/null +++ b/module/doc/test/model/addbuiltindoctemplatebytype.php @@ -0,0 +1,40 @@ +#!/usr/bin/env php +addBuiltInDocTemplateByType(); +timeout=0 +cid=1 + +- 添加内置项目计划模板 @1 +- 添加内置软件需求说明书模板 @1 +- 添加内置概要设计说明书模板 @1 +- 添加内置详细设计说明书模板 @1 +- 添加内置接口设计文档模板 @1 +- 添加内置数据库设计文档模板 @1 +- 添加内置集成测试用例模板 @1 +- 添加内置系统测试用例模板 @1 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('user')->gen(5); +su('admin'); + +zenData('doc')->gen(0); +zenData('doccontent')->gen(0); +zenData('doclib')->gen(0); +zenData('module')->gen(0); +zenData('lang')->gen(0); +$docTester = new docTest(); +r($docTester->addBuiltInDocTemplateByTypeTest(1, array('PP'), '项目计划')) && p() && e('1'); // 添加内置项目计划模板 +r($docTester->addBuiltInDocTemplateByTypeTest(2, array('SRS'), '软件需求规格说明书')) && p() && e('1'); // 添加内置软件需求说明书模板 +r($docTester->addBuiltInDocTemplateByTypeTest(3, array('HLDS'), '概要设计说明书')) && p() && e('1'); // 添加内置概要设计说明书模板 +r($docTester->addBuiltInDocTemplateByTypeTest(4, array('DDS'), '详细设计说明书')) && p() && e('1'); // 添加内置详细设计说明书模板 +r($docTester->addBuiltInDocTemplateByTypeTest(5, array('ADS'), '接口设计文档')) && p() && e('1'); // 添加内置接口设计文档模板 +r($docTester->addBuiltInDocTemplateByTypeTest(6, array('DBDS'), '数据库设计文档')) && p() && e('1'); // 添加内置数据库设计文档模板 +r($docTester->addBuiltInDocTemplateByTypeTest(7, array('ITTC'), '集成测试用例')) && p() && e('1'); // 添加内置集成测试用例模板 +r($docTester->addBuiltInDocTemplateByTypeTest(8, array('STTC'), '系统测试用例')) && p() && e('1'); // 添加内置系统测试用例模板 diff --git a/module/doc/test/model/addbuiltinscopes.php b/module/doc/test/model/addbuiltinscopes.php new file mode 100755 index 0000000000..ad4c5fee33 --- /dev/null +++ b/module/doc/test/model/addbuiltinscopes.php @@ -0,0 +1,50 @@ +#!/usr/bin/env php +addBuiltInScopes(); +timeout=0 +cid=1 + +- 测试添加研发界面内置产品范围 + - 第1条的name属性 @产品 + - 第1条的type属性 @template + - 第1条的main属性 @1 + - 第1条的vision属性 @rnd +- 测试添加研发界面内置项目范围 + - 第2条的name属性 @项目 + - 第2条的type属性 @template + - 第2条的main属性 @1 + - 第2条的vision属性 @rnd +- 测试添加OR界面内置市场范围 + - 第5条的name属性 @市场 + - 第5条的type属性 @template + - 第5条的main属性 @1 + - 第5条的vision属性 @or +- 测试添加OR界面内置产品范围 + - 第6条的name属性 @产品 + - 第6条的type属性 @template + - 第6条的main属性 @1 + - 第6条的vision属性 @or +- 测试添加运营界面内置项目范围 + - 第8条的name属性 @项目 + - 第8条的type属性 @template + - 第8条的main属性 @1 + - 第8条的vision属性 @lite + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('user')->gen(5); +su('admin'); + +zenData('doclib')->gen(0); +$docTester = new docTest(); +r($docTester->addBuiltInScopesTest()) && p('1:name,type,main,vision') && e('产品,template,1,rnd'); // 测试添加研发界面内置产品范围 +r($docTester->addBuiltInScopesTest()) && p('2:name,type,main,vision') && e('项目,template,1,rnd'); // 测试添加研发界面内置项目范围 +r($docTester->addBuiltInScopesTest()) && p('5:name,type,main,vision') && e('市场,template,1,or'); // 测试添加OR界面内置市场范围 +r($docTester->addBuiltInScopesTest()) && p('6:name,type,main,vision') && e('产品,template,1,or'); // 测试添加OR界面内置产品范围 +r($docTester->addBuiltInScopesTest()) && p('8:name,type,main,vision') && e('项目,template,1,lite'); // 测试添加运营界面内置项目范围 diff --git a/module/doc/test/model/addtemplatetype.php b/module/doc/test/model/addtemplatetype.php new file mode 100755 index 0000000000..55a2a70a2e --- /dev/null +++ b/module/doc/test/model/addtemplatetype.php @@ -0,0 +1,51 @@ +#!/usr/bin/env php +addTemplateType(); +timeout=0 +cid=1 + +- 添加产品模板类型 + - 属性short @custom1 + - 属性path @,1, +- 添加产品子模板类型 + - 属性short @custom2 + - 属性path @,1,2, +- 添加项目模板类型 + - 属性short @custom3 + - 属性path @,3, +- 添加项目子模板类型 + - 属性short @custom4 + - 属性path @,3,4, +- 添加执行模板类型 + - 属性short @custom5 + - 属性path @,5, +- 添加执行子模板类型 + - 属性short @custom6 + - 属性path @,5,6, + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('module')->gen(0); +zenData('user')->gen(5); +su('admin'); + +$productModule = array('name' => '产品计划', 'root' => 1, 'grade' => 1, 'order' => 10, 'type' => 'doctemplate'); +$productSubModule = array('name' => '软件产品计划', 'root' => 1, 'grade' => 2, 'order' => 20, 'type' => 'doctemplate', 'parent' => 1); +$projectModule = array('name' => '项目计划', 'root' => 2, 'grade' => 1, 'order' => 10, 'type' => 'doctemplate'); +$projectSubModule = array('name' => '质量保证计划', 'root' => 2, 'grade' => 2, 'order' => 20, 'type' => 'doctemplate', 'parent' => 3); +$executionModule = array('name' => '开发计划', 'root' => 3, 'grade' => 1, 'order' => 10, 'type' => 'doctemplate'); +$executionSubModule = array('name' => '软件需求说明书', 'root' => 3, 'grade' => 2, 'order' => 20, 'type' => 'doctemplate', 'parent' => 5); + +$docTester = new docTest(); +r($docTester->addTemplateTypeTest($productModule)) && p('short|path', '|') && e('custom1|,1,'); // 添加产品模板类型 +r($docTester->addTemplateTypeTest($productSubModule)) && p('short|path', '|') && e('custom2|,1,2,'); // 添加产品子模板类型 +r($docTester->addTemplateTypeTest($projectModule)) && p('short|path', '|') && e('custom3|,3,'); // 添加项目模板类型 +r($docTester->addTemplateTypeTest($projectSubModule)) && p('short|path', '|') && e('custom4|,3,4,'); // 添加项目子模板类型 +r($docTester->addTemplateTypeTest($executionModule)) && p('short|path', '|') && e('custom5|,5,'); // 添加执行模板类型 +r($docTester->addTemplateTypeTest($executionSubModule)) && p('short|path', '|') && e('custom6|,5,6,'); // 添加执行子模板类型 diff --git a/module/doc/test/model/batchmovedoc.php b/module/doc/test/model/batchmovedoc.php new file mode 100755 index 0000000000..a932311d09 --- /dev/null +++ b/module/doc/test/model/batchmovedoc.php @@ -0,0 +1,49 @@ +#!/usr/bin/env php +batchMoveDoc(); +timeout=0 +cid=1 + +- 测试ID为空时 @0 +- 测试文档1文档2的字段 + - 第1条的lib属性 @100 + - 第1条的module属性 @99 + - 第2条的lib属性 @100 + - 第2条的module属性 @99 +- 测试文档3文档4的字段 + - 第3条的lib属性 @100 + - 第3条的module属性 @99 + - 第4条的lib属性 @100 + - 第4条的module属性 @99 +- 测试文档5文档6的字段 + - 第5条的lib属性 @100 + - 第5条的module属性 @99 + - 第6条的lib属性 @100 + - 第6条的module属性 @99 +- 测试文档7文档8的字段 + - 第7条的lib属性 @100 + - 第7条的module属性 @99 + - 第8条的lib属性 @100 + - 第8条的module属性 @99 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('doc')->loadYaml('doc')->gen(20); +zenData('user')->gen(5); +su('admin'); + +$docData = array('lib' => 100, 'module' => 99); +$docIdList = array(array(), array(1, 2), array(3, 4), array(5, 6), array(7, 8)); + +$docTester = new docTest(); +r($docTester->batchMoveDocTest($docData, $docIdList[0])) && p('') && e('0'); // 测试ID为空时 +r($docTester->batchMoveDocTest($docData, $docIdList[1])) && p('1:lib,module;2:lib,module') && e('100,99;100,99'); // 测试文档1文档2的字段 +r($docTester->batchMoveDocTest($docData, $docIdList[2])) && p('3:lib,module;4:lib,module') && e('100,99;100,99'); // 测试文档3文档4的字段 +r($docTester->batchMoveDocTest($docData, $docIdList[3])) && p('5:lib,module;6:lib,module') && e('100,99;100,99'); // 测试文档5文档6的字段 +r($docTester->batchMoveDocTest($docData, $docIdList[4])) && p('7:lib,module;8:lib,module') && e('100,99;100,99'); // 测试文档7文档8的字段 diff --git a/module/doc/test/model/buildtemplatemodule.php b/module/doc/test/model/buildtemplatemodule.php new file mode 100644 index 0000000000..1778381ae0 --- /dev/null +++ b/module/doc/test/model/buildtemplatemodule.php @@ -0,0 +1,60 @@ +#!/usr/bin/env php +buildTemplateModule(); +timeout=0 +cid=1 + +- 创建模板类型对象 + - 属性root @1 + - 属性parent @1 + - 属性name @模板名称 + - 属性short @ShortName + - 属性grade @1 +- 创建模板类型对象 + - 属性root @2 + - 属性parent @1 + - 属性name @模板名称 + - 属性short @ShortName + - 属性grade @1 +- 创建模板类型对象 + - 属性root @2 + - 属性parent @2 + - 属性name @模板名称 + - 属性short @ShortName + - 属性grade @1 +- 创建模板类型对象 + - 属性root @1 + - 属性parent @2 + - 属性name @模板名称 + - 属性short @ShortName + - 属性grade @1 +- 创建模板类型对象 + - 属性root @2 + - 属性parent @2 + - 属性name @模板名称 + - 属性short @ShortName + - 属性grade @2 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('user')->gen(5); +su('admin'); + +$scopeList = array(1, 2); +$parentList = array(1, 2); +$nameList = array('模板名称'); +$codeList = array('ShortName'); +$gradeList = array(1, 2); + +$docTester = new docTest(); +r($docTester->buildTemplateModuleTest($scopeList[0], $parentList[0], $nameList[0], $codeList[0], $gradeList[0])) && p('root,parent,name,short,grade') && e('1,1,模板名称,ShortName,1'); // 创建模板类型对象 +r($docTester->buildTemplateModuleTest($scopeList[1], $parentList[0], $nameList[0], $codeList[0], $gradeList[0])) && p('root,parent,name,short,grade') && e('2,1,模板名称,ShortName,1'); // 创建模板类型对象 +r($docTester->buildTemplateModuleTest($scopeList[1], $parentList[1], $nameList[0], $codeList[0], $gradeList[0])) && p('root,parent,name,short,grade') && e('2,2,模板名称,ShortName,1'); // 创建模板类型对象 +r($docTester->buildTemplateModuleTest($scopeList[0], $parentList[1], $nameList[0], $codeList[0], $gradeList[0])) && p('root,parent,name,short,grade') && e('1,2,模板名称,ShortName,1'); // 创建模板类型对象 +r($docTester->buildTemplateModuleTest($scopeList[1], $parentList[1], $nameList[0], $codeList[0], $gradeList[1])) && p('root,parent,name,short,grade') && e('2,2,模板名称,ShortName,2'); // 创建模板类型对象 diff --git a/module/doc/test/model/checkistemplateupgraded.php b/module/doc/test/model/checkistemplateupgraded.php new file mode 100644 index 0000000000..cdf609bf87 --- /dev/null +++ b/module/doc/test/model/checkistemplateupgraded.php @@ -0,0 +1,34 @@ +#!/usr/bin/env php +checkIsTemplateUpgraded(); +timeout=0 +cid=1 + +- 检查文档模板已升级 @1 +- 检查文档模板已升级 @1 +- 检查文档模板已升级 @1 +- 检查文档模板未升级 @0 +- 检查文档模板未升级 @0 +- 检查文档模板未升级 @0 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('module')->loadYaml('templatemodule')->gen(10); +zenData('user')->gen(5); +su('admin'); + +$docTester = new docTest(); +r($docTester->checkIsTemplateUpgradedTest()) && p('') && e('1'); // 检查文档模板已升级 +r($docTester->checkIsTemplateUpgradedTest()) && p('') && e('1'); // 检查文档模板已升级 +r($docTester->checkIsTemplateUpgradedTest()) && p('') && e('1'); // 检查文档模板已升级 + +zenData('module')->gen(0); +r($docTester->checkIsTemplateUpgradedTest()) && p('') && e('0'); // 检查文档模板未升级 +r($docTester->checkIsTemplateUpgradedTest()) && p('') && e('0'); // 检查文档模板未升级 +r($docTester->checkIsTemplateUpgradedTest()) && p('') && e('0'); // 检查文档模板未升级 diff --git a/module/doc/test/model/copytemplate.php b/module/doc/test/model/copytemplate.php new file mode 100644 index 0000000000..59233a508f --- /dev/null +++ b/module/doc/test/model/copytemplate.php @@ -0,0 +1,57 @@ +#!/usr/bin/env php +copyTemplate(); +timeout=0 +cid=1 + +- 测试复制文档模板1 + - 第all条的0属性 @6 + - 第html条的0属性 @6 +- 测试复制文档模板2 + - 第all条的0属性 @7 + - 第html条的0属性 @7 +- 测试复制文档模板3 + - 第all条的0属性 @8 + - 第wiki条的0属性 @8 +- 测试复制文档模板4 + - 第all条的0属性 @9 + - 第html条的0属性 @9 +- 测试复制文档模板5 + - 第all条的0属性 @10 + - 第html条的0属性 @10 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +$scope = zenData('doclib'); +$scope->id->range('1-10'); +$scope->type->range('template'); +$scope->vision->range('rnd{4},or{3},lite{2},rnd'); +$scope->name->range('产品,项目,执行,个人,市场,产品,个人,项目,个人,自定义范围'); +$scope->main->range('1{9},0'); + +$scope->gen(10); +$doc = zenData('doc')->loadYaml('template'); +$doc->templateType->range('custom1,custom2,custom3,custom4,custom5'); +$doc->vision->range('rnd,rnd,or,or,or'); +$doc->type->range('html,markdown,book,html,markdown'); +$doc->gen(5); + +$module = zenData('module')->loadYaml('templatemodule'); +$module->root->range('6'); +$module->gen(5); + +zenData('doccontent')->gen(5); +zenData('user')->gen(5); +su('admin'); + +$docTester = new docTest(); +r($docTester->copyTemplateTest(array(1))) && p('all:0;html:0') && e('6,6'); // 测试复制文档模板1 +r($docTester->copyTemplateTest(array(2))) && p('all:0;html:0') && e('7,7'); // 测试复制文档模板2 +r($docTester->copyTemplateTest(array(3))) && p('all:0;wiki:0') && e('8,8'); // 测试复制文档模板3 +r($docTester->copyTemplateTest(array(4))) && p('all:0;html:0') && e('9,9'); // 测试复制文档模板4 +r($docTester->copyTemplateTest(array(5))) && p('all:0;html:0') && e('10,10'); // 测试复制文档模板5 diff --git a/module/doc/test/model/create.php b/module/doc/test/model/create.php index c9e417dbb1..9f993f8aed 100755 --- a/module/doc/test/model/create.php +++ b/module/doc/test/model/create.php @@ -39,16 +39,16 @@ $acl = array('open', 'custom', 'private'); $groups = '1,2,3'; $users = 'admin,dev1,dev10'; -$createProductDoc = array('lib' => $libIDs[1], 'title' => '新建产品文档', 'acl' => $acl[0]); -$createProjectDoc = array('lib' => $libIDs[2], 'title' => '新建项目文档', 'acl' => $acl[0]); -$createExecutionDoc = array('lib' => $libIDs[3], 'title' => '新建执行文档', 'acl' => $acl[0]); -$createCustomDoc = array('lib' => $libIDs[4], 'title' => '新建自定义文档', 'acl' => $acl[1]); -$privateDoc = array('lib' => $libIDs[4], 'title' => '新建私有文档', 'acl' => $acl[2]); -$customDoc = array('lib' => $libIDs[5], 'title' => '新建自定义文档', 'acl' => $acl[1], 'groups' => $groups, 'users' => $users); -$mineDoc = array('lib' => $libIDs[3], 'title' => '新建我的文档', 'acl' => $acl[0]); -$draftDoc = array('lib' => $libIDs[3], 'title' => '新建我的草稿文档', 'acl' => $acl[0], 'status' => 'draft'); -$noLib = array('lib' => $libIDs[0], 'title' => '无文档库', 'acl' => $acl[0]); -$noTitle = array('lib' => $libIDs[1], 'title' => '', 'acl' => $acl[0]); +$createProductDoc = array('lib' => $libIDs[1], 'title' => '新建产品文档', 'acl' => $acl[0], 'parent' => 0); +$createProjectDoc = array('lib' => $libIDs[2], 'title' => '新建项目文档', 'acl' => $acl[0], 'parent' => 0); +$createExecutionDoc = array('lib' => $libIDs[3], 'title' => '新建执行文档', 'acl' => $acl[0], 'parent' => 0); +$createCustomDoc = array('lib' => $libIDs[4], 'title' => '新建自定义文档', 'acl' => $acl[1], 'parent' => 0); +$privateDoc = array('lib' => $libIDs[4], 'title' => '新建私有文档', 'acl' => $acl[2], 'parent' => 0); +$customDoc = array('lib' => $libIDs[5], 'title' => '新建自定义文档', 'acl' => $acl[1], 'groups' => $groups, 'users' => $users, 'parent' => 0); +$mineDoc = array('lib' => $libIDs[3], 'title' => '新建我的文档', 'acl' => $acl[0], 'parent' => 0); +$draftDoc = array('lib' => $libIDs[3], 'title' => '新建我的草稿文档', 'acl' => $acl[0], 'status' => 'draft', 'parent' => 0); +$noLib = array('lib' => $libIDs[0], 'title' => '无文档库', 'acl' => $acl[0], 'parent' => 0); +$noTitle = array('lib' => $libIDs[1], 'title' => '', 'acl' => $acl[0], 'parent' => 0); $docTester = new docTest(); r($docTester->createTest($createProductDoc)) && p('1:title') && e('新建产品文档'); // 创建产品文档 diff --git a/module/doc/test/model/deletefiles.php b/module/doc/test/model/deletefiles.php index 2ea7a5e2db..0414db0665 100755 --- a/module/doc/test/model/deletefiles.php +++ b/module/doc/test/model/deletefiles.php @@ -9,9 +9,11 @@ cid=1 - 测试正常数据属性1 @1 - 测试有不存在的数据属性1 @1 - 测试不存在的数据 @0 +- 测试异常数据 @0 */ + include dirname(__FILE__, 5) . '/test/lib/init.php'; include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; @@ -27,9 +29,11 @@ $docIdList[] = array(); $docIdList[] = range(1, 5); $docIdList[] = array(1, 2, 100, 101); $docIdList[] = range(100, 105); +$docIdList[] = array(999); $docTester = new docTest(); r($docTester->deleteFilesTest($docIdList[0])) && p() && e('0'); // 测试空数据 r($docTester->deleteFilesTest($docIdList[1])) && p('1') && e('1'); // 测试正常数据 r($docTester->deleteFilesTest($docIdList[2])) && p('1') && e('1'); // 测试有不存在的数据 r($docTester->deleteFilesTest($docIdList[3])) && p() && e('0'); // 测试不存在的数据 +r($docTester->deleteFilesTest($docIdList[4])) && p() && e('0'); // 测试异常数据 diff --git a/module/doc/test/model/deletetemplate.php b/module/doc/test/model/deletetemplate.php new file mode 100644 index 0000000000..645577b47c --- /dev/null +++ b/module/doc/test/model/deletetemplate.php @@ -0,0 +1,31 @@ +#!/usr/bin/env php +deleteTemplate(); +cid=1 + +- 测试删除ID为0的模板 @0 +- 测试删除ID为1的模板 @1 +- 测试删除ID为2的模板 @1 +- 测试删除ID为3的模板 @1 +- 测试删除ID不存在的模板 @0 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('doc')->loadYaml('template')->gen(10); +zenData('user')->gen(5); +su('admin'); + +$templateIdList = array(0, 1, 2, 3, 11); + +$docTester = new docTest(); +r($docTester->deleteTemplateTest($templateIdList[0])) && p() && e('0'); // 测试删除ID为0的模板 +r($docTester->deleteTemplateTest($templateIdList[1])) && p() && e('1'); // 测试删除ID为1的模板 +r($docTester->deleteTemplateTest($templateIdList[2])) && p() && e('1'); // 测试删除ID为2的模板 +r($docTester->deleteTemplateTest($templateIdList[3])) && p() && e('1'); // 测试删除ID为3的模板 +r($docTester->deleteTemplateTest($templateIdList[4])) && p() && e('0'); // 测试删除ID不存在的模板 diff --git a/module/doc/test/model/deletetemplatescopes.php b/module/doc/test/model/deletetemplatescopes.php new file mode 100644 index 0000000000..8566a3c3da --- /dev/null +++ b/module/doc/test/model/deletetemplatescopes.php @@ -0,0 +1,34 @@ +#!/usr/bin/env php +deleteTemplateScopes(); +timeout=0 +cid=1 + +- 测试删除产品范围 @1 +- 测试删除项目范围 @1 +- 测试删除执行范围 @1 +- 测试删除个人范围 @1 +- 测试删除自定义范围 @1 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +$scope = zenData('doclib'); +$scope->id->range('1-10'); +$scope->type->range('template'); +$scope->vision->range('rnd{4},or{3},lite{2},rnd'); +$scope->name->range('产品,项目,执行,个人,市场,项目,个人,产品,个人,自定义范围'); +$scope->main->range('1{9},0'); +$scope->gen(10); + +$docTester = new docTest(); +r($docTester->deleteTemplateScopesTest(1)) && p() && e('1'); // 测试删除产品范围 +r($docTester->deleteTemplateScopesTest(2)) && p() && e('1'); // 测试删除项目范围 +r($docTester->deleteTemplateScopesTest(3)) && p() && e('1'); // 测试删除执行范围 +r($docTester->deleteTemplateScopesTest(4)) && p() && e('1'); // 测试删除个人范围 +r($docTester->deleteTemplateScopesTest(10)) && p() && e('1'); // 测试删除自定义范围 diff --git a/module/doc/test/model/getdocquery.php b/module/doc/test/model/getdocquery.php index 45b843f3bd..981f54d6bb 100755 --- a/module/doc/test/model/getdocquery.php +++ b/module/doc/test/model/getdocquery.php @@ -34,6 +34,7 @@ cid=1 - 测试只查询所有项目和所有产品和所有执行 @1 AND 1 AND 1 AND `project` in (11,60,61,100) AND `product` IN ('1','2','3','4','5') AND `execution` IN ('104','103','106','105','102','101') + */ include dirname(__FILE__, 5) . '/test/lib/init.php'; diff --git a/module/doc/test/model/getdocsoflibs.php b/module/doc/test/model/getdocsoflibs.php new file mode 100644 index 0000000000..f5775932f0 --- /dev/null +++ b/module/doc/test/model/getdocsoflibs.php @@ -0,0 +1,48 @@ +#!/usr/bin/env php +getDocsOfLibs(); +timeout=0 +cid=1 + +- 获取产品库26下的文档列表 + - 第41条的lib属性 @26 + - 第41条的title属性 @产品文档41 +- 获取项目库18下的文档列表 + - 第31条的lib属性 @18 + - 第31条的title属性 @项目文档31 +- 获取我的文档库11下的文档列表 + - 第1条的lib属性 @11 + - 第1条的title属性 @我的文档1 +- 获取产品库26下排除文档41的文档列表 + - 第42条的lib属性 @26 + - 第42条的title属性 @产品文档42 +- 获取项目库18下排除文档31的文档列表 + - 第32条的lib属性 @18 + - 第32条的title属性 @项目文档32 +- 获取我的文档库11下排除文档1的文档列表 + - 第2条的lib属性 @11 + - 第2条的title属性 @我的文档2 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('doc')->loadYaml('doc')->gen(50); +zenData('user')->gen(5); +su('admin'); + +$libs = array(array(26), array(18), array(11)); +$spaceType = array('product', 'project', 'mine'); +$excludeID = array(0, 41, 31, 1); + +$docTester = new docTest(); +r($docTester->getDocsOfLibsTest($libs[0], $spaceType[0], $excludeID[0])) && p('41:lib,title') && e('26,产品文档41'); // 获取产品库26下的文档列表 +r($docTester->getDocsOfLibsTest($libs[1], $spaceType[1], $excludeID[0])) && p('31:lib,title') && e('18,项目文档31'); // 获取项目库18下的文档列表 +r($docTester->getDocsOfLibsTest($libs[2], $spaceType[2], $excludeID[0])) && p('1:lib,title') && e('11,我的文档1'); // 获取我的文档库11下的文档列表 +r($docTester->getDocsOfLibsTest($libs[0], $spaceType[0], $excludeID[1])) && p('42:lib,title') && e('26,产品文档42'); // 获取产品库26下排除文档41的文档列表 +r($docTester->getDocsOfLibsTest($libs[1], $spaceType[1], $excludeID[2])) && p('32:lib,title') && e('18,项目文档32'); // 获取项目库18下排除文档31的文档列表 +r($docTester->getDocsOfLibsTest($libs[2], $spaceType[2], $excludeID[3])) && p('2:lib,title') && e('11,我的文档2'); // 获取我的文档库11下排除文档1的文档列表 diff --git a/module/doc/test/model/getdoctemplatelist.php b/module/doc/test/model/getdoctemplatelist.php new file mode 100644 index 0000000000..22962b977c --- /dev/null +++ b/module/doc/test/model/getdoctemplatelist.php @@ -0,0 +1,104 @@ +#!/usr/bin/env php +getDocTemplateList(); +timeout=0 +cid=1 + +- 获取产品范围下的所有模板 + - 第0条的lib属性 @1 + - 第0条的title属性 @产品模板5 + - 第0条的templateType属性 @custom1 +- 获取产品范围下的草稿模板 + - 第1条的lib属性 @1 + - 第1条的title属性 @产品模板4 + - 第1条的templateType属性 @custom1 +- 获取产品范围下的已发布模板 + - 第0条的lib属性 @1 + - 第0条的title属性 @产品模板2 + - 第0条的templateType属性 @QAP +- 获取产品范围下的所有模板,按照title降序 + - 第0条的lib属性 @1 + - 第0条的title属性 @产品模板1 + - 第0条的templateType属性 @PP +- 获取项目范围下的所有模板 + - 第0条的lib属性 @2 + - 第0条的title属性 @项目模板10 + - 第0条的templateType属性 @custom2 +- 获取项目范围下的草稿模板 + - 第1条的lib属性 @2 + - 第1条的title属性 @项目模板9 + - 第1条的templateType属性 @SRS +- 获取项目范围下的已发布模板 + - 第0条的lib属性 @2 + - 第0条的title属性 @项目模板7 + - 第0条的templateType属性 @ERS +- 获取项目范围下的所有模板,按照title降序 + - 第0条的lib属性 @2 + - 第0条的title属性 @项目模板10 + - 第0条的templateType属性 @custom2 +- 获取执行范围下的所有模板 + - 第0条的lib属性 @3 + - 第0条的title属性 @执行模板15 + - 第0条的templateType属性 @custom3 +- 获取执行范围下的草稿模板 + - 第1条的lib属性 @3 + - 第1条的title属性 @执行模板14 + - 第1条的templateType属性 @custom3 +- 获取执行范围下的已发布模板 + - 第0条的lib属性 @3 + - 第0条的title属性 @执行模板12 + - 第0条的templateType属性 @DDS +- 获取执行范围下的所有模板,按照title降序 + - 第0条的lib属性 @3 + - 第0条的title属性 @执行模板11 + - 第0条的templateType属性 @HLDS +- 获取个人范围下的所有模板 + - 第0条的lib属性 @4 + - 第0条的title属性 @个人模板20 + - 第0条的templateType属性 @custom4 +- 获取个人范围下的草稿模板 + - 第1条的lib属性 @4 + - 第1条的title属性 @个人模板19 + - 第1条的templateType属性 @custom4 +- 获取个人范围下的已发布模板 + - 第0条的lib属性 @4 + - 第0条的title属性 @个人模板17 + - 第0条的templateType属性 @Code +- 获取个人范围下的所有模板,按照title降序 + - 第0条的lib属性 @4 + - 第0条的title属性 @个人模板16 + - 第0条的templateType属性 @ADS + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('doc')->loadYaml('template')->gen(20); +zenData('user')->gen(5); +su('admin'); + +$libs = array(1, 2, 3, 4); +$types = array('all', 'draft', 'released'); +$orderBy = array('id_desc', 'title_asc'); + +$docTester = new docTest(); +r($docTester->getDocTemplateListTest($libs[0], $types[0], $orderBy[0])) && p('0:lib,title,templateType') && e('1,产品模板5,custom1'); // 获取产品范围下的所有模板 +r($docTester->getDocTemplateListTest($libs[0], $types[1], $orderBy[0])) && p('1:lib,title,templateType') && e('1,产品模板4,custom1'); // 获取产品范围下的草稿模板 +r($docTester->getDocTemplateListTest($libs[0], $types[2], $orderBy[0])) && p('0:lib,title,templateType') && e('1,产品模板2,QAP'); // 获取产品范围下的已发布模板 +r($docTester->getDocTemplateListTest($libs[0], $types[0], $orderBy[1])) && p('0:lib,title,templateType') && e('1,产品模板1,PP'); // 获取产品范围下的所有模板,按照title降序 +r($docTester->getDocTemplateListTest($libs[1], $types[0], $orderBy[0])) && p('0:lib,title,templateType') && e('2,项目模板10,custom2'); // 获取项目范围下的所有模板 +r($docTester->getDocTemplateListTest($libs[1], $types[1], $orderBy[0])) && p('1:lib,title,templateType') && e('2,项目模板9,SRS'); // 获取项目范围下的草稿模板 +r($docTester->getDocTemplateListTest($libs[1], $types[2], $orderBy[0])) && p('0:lib,title,templateType') && e('2,项目模板7,ERS'); // 获取项目范围下的已发布模板 +r($docTester->getDocTemplateListTest($libs[1], $types[0], $orderBy[1])) && p('0:lib,title,templateType') && e('2,项目模板10,custom2'); // 获取项目范围下的所有模板,按照title降序 +r($docTester->getDocTemplateListTest($libs[2], $types[0], $orderBy[0])) && p('0:lib,title,templateType') && e('3,执行模板15,custom3'); // 获取执行范围下的所有模板 +r($docTester->getDocTemplateListTest($libs[2], $types[1], $orderBy[0])) && p('1:lib,title,templateType') && e('3,执行模板14,custom3'); // 获取执行范围下的草稿模板 +r($docTester->getDocTemplateListTest($libs[2], $types[2], $orderBy[0])) && p('0:lib,title,templateType') && e('3,执行模板12,DDS'); // 获取执行范围下的已发布模板 +r($docTester->getDocTemplateListTest($libs[2], $types[0], $orderBy[1])) && p('0:lib,title,templateType') && e('3,执行模板11,HLDS'); // 获取执行范围下的所有模板,按照title降序 +r($docTester->getDocTemplateListTest($libs[3], $types[0], $orderBy[0])) && p('0:lib,title,templateType') && e('4,个人模板20,custom4'); // 获取个人范围下的所有模板 +r($docTester->getDocTemplateListTest($libs[3], $types[1], $orderBy[0])) && p('1:lib,title,templateType') && e('4,个人模板19,custom4'); // 获取个人范围下的草稿模板 +r($docTester->getDocTemplateListTest($libs[3], $types[2], $orderBy[0])) && p('0:lib,title,templateType') && e('4,个人模板17,Code'); // 获取个人范围下的已发布模板 +r($docTester->getDocTemplateListTest($libs[3], $types[0], $orderBy[1])) && p('0:lib,title,templateType') && e('4,个人模板16,ADS'); // 获取个人范围下的所有模板,按照title降序 diff --git a/module/doc/test/model/gethottemplates.php b/module/doc/test/model/gethottemplates.php new file mode 100644 index 0000000000..9d8606e1fb --- /dev/null +++ b/module/doc/test/model/gethottemplates.php @@ -0,0 +1,47 @@ +#!/usr/bin/env php +getHotTemplates(); +timeout=0 +cid=1 + +- 获取所有文档模板 + - 第0条的id属性 @1 + - 第0条的title属性 @产品模板1 +- 获取产品范围所有文档模板 + - 第1条的id属性 @2 + - 第1条的title属性 @产品模板2 +- 获取项目范围所有文档模板 + - 第0条的id属性 @6 + - 第0条的title属性 @项目模板6 +- 获取所有范围下最新文档模板 + - 第0条的id属性 @1 + - 第0条的title属性 @产品模板1 +- 获取产品范围下最新文档模板 + - 第0条的id属性 @1 + - 第0条的title属性 @产品模板1 +- 获取项目范围下最新文档模板 + - 第0条的id属性 @6 + - 第0条的title属性 @项目模板6 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('doc')->loadYaml('template')->gen(20); +zenData('user')->gen(5); +su('admin'); + +$scopeIdList = array(0, 1, 2); +$limits = array(0, 1); + +$docTester = new docTest(); +r($docTester->getHotTemplatesTest($scopeIdList[0], $limits[0])) && p('0:id,title') && e('1,产品模板1'); // 获取所有文档模板 +r($docTester->getHotTemplatesTest($scopeIdList[1], $limits[0])) && p('1:id,title') && e('2,产品模板2'); // 获取产品范围所有文档模板 +r($docTester->getHotTemplatesTest($scopeIdList[2], $limits[0])) && p('0:id,title') && e('6,项目模板6'); // 获取项目范围所有文档模板 +r($docTester->getHotTemplatesTest($scopeIdList[0], $limits[1])) && p('0:id,title') && e('1,产品模板1'); // 获取所有范围下最新文档模板 +r($docTester->getHotTemplatesTest($scopeIdList[1], $limits[1])) && p('0:id,title') && e('1,产品模板1'); // 获取产品范围下最新文档模板 +r($docTester->getHotTemplatesTest($scopeIdList[2], $limits[1])) && p('0:id,title') && e('6,项目模板6'); // 获取项目范围下最新文档模板 diff --git a/module/doc/test/model/getscopeitems.php b/module/doc/test/model/getscopeitems.php new file mode 100644 index 0000000000..e8a935df96 --- /dev/null +++ b/module/doc/test/model/getscopeitems.php @@ -0,0 +1,46 @@ +#!/usr/bin/env php +getScopeItems(); +timeout=0 +cid=1 + +- 获取产品范围 + - 第0条的value属性 @1 + - 第0条的text属性 @产品 +- 获取项目范围 + - 第0条的value属性 @2 + - 第0条的text属性 @项目 +- 获取执行范围 + - 第0条的value属性 @3 + - 第0条的text属性 @执行 +- 获取个人范围 + - 第0条的value属性 @4 + - 第0条的text属性 @个人 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('doc')->loadYaml('template')->gen(20); +zenData('user')->gen(5); +su('admin'); + +$scope = zenData('doclib'); +$scope->id->range('1-10'); +$scope->type->range('template'); +$scope->vision->range('rnd{4},or{3},lite{2},rnd'); +$scope->name->range('产品,项目,执行,个人,市场,项目,个人,产品,个人,自定义范围'); +$scope->main->range('1{9},0'); +$scope->gen(10); + +$scopeList = array(array(1 => '产品'), array(2 => '项目'), array(3 => '执行'), array(4 => '个人')); + +$docTester = new docTest(); +r($docTester->getScopeItemsTest($scopeList[0])) && p('0:value,text') && e('1,产品'); // 获取产品范围 +r($docTester->getScopeItemsTest($scopeList[1])) && p('0:value,text') && e('2,项目'); // 获取项目范围 +r($docTester->getScopeItemsTest($scopeList[2])) && p('0:value,text') && e('3,执行'); // 获取执行范围 +r($docTester->getScopeItemsTest($scopeList[3])) && p('0:value,text') && e('4,个人'); // 获取个人范围 diff --git a/module/doc/test/model/getscopetemplates.php b/module/doc/test/model/getscopetemplates.php new file mode 100644 index 0000000000..79da5f05dd --- /dev/null +++ b/module/doc/test/model/getscopetemplates.php @@ -0,0 +1,40 @@ +#!/usr/bin/env php +getScopeTemplates(); +timeout=0 +cid=1 + +- 获取产品范围下的文档模板 + - 第1条的id属性 @1 + - 第1条的title属性 @产品模板1 +- 获取项目范围下的文档模板 + - 第6条的id属性 @6 + - 第6条的title属性 @项目模板6 +- 获取执行范围下的文档模板 + - 第11条的id属性 @11 + - 第11条的title属性 @执行模板11 +- 获取个人范围下的文档模板 + - 第16条的id属性 @16 + - 第16条的title属性 @个人模板16 +- 获取产品范围下的文档模板 + - 第2条的id属性 @2 + - 第2条的title属性 @产品模板2 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('doc')->loadYaml('template')->gen(20); +zenData('user')->gen(5); +su('admin'); + +$docTester = new docTest(); +r($docTester->getScopeTemplatesTest(1)[1]) && p('1:id,title') && e('1,产品模板1'); // 获取产品范围下的文档模板 +r($docTester->getScopeTemplatesTest(2)[2]) && p('6:id,title') && e('6,项目模板6'); // 获取项目范围下的文档模板 +r($docTester->getScopeTemplatesTest(3)[3]) && p('11:id,title') && e('11,执行模板11'); // 获取执行范围下的文档模板 +r($docTester->getScopeTemplatesTest(4)[4]) && p('16:id,title') && e('16,个人模板16'); // 获取个人范围下的文档模板 +r($docTester->getScopeTemplatesTest(1)[1]) && p('2:id,title') && e('2,产品模板2'); // 获取产品范围下的文档模板 diff --git a/module/doc/test/model/gettemplatemodules.php b/module/doc/test/model/gettemplatemodules.php new file mode 100755 index 0000000000..1afe98dc7e --- /dev/null +++ b/module/doc/test/model/gettemplatemodules.php @@ -0,0 +1,57 @@ +#!/usr/bin/env php +getTemplatesModules(); +timeout=0 +cid=1 + +- 获取所有模板类型模块 + - 第0条的id属性 @1 + - 第0条的name属性 @模板类型1 + - 第0条的type属性 @docTemplate + - 第0条的root属性 @1 + - 第0条的grade属性 @1 +- 获取root为1, grade为1的模板类型的模块 + - 第1条的id属性 @3 + - 第1条的name属性 @模板类型3 + - 第1条的type属性 @docTemplate + - 第1条的root属性 @1 + - 第1条的grade属性 @1 +- 获取root为1, grade为2的模板类型的模块 + - 第0条的id属性 @2 + - 第0条的name属性 @模板类型2 + - 第0条的type属性 @docTemplate + - 第0条的root属性 @1 + - 第0条的grade属性 @2 +- 获取root为2, grade为1的模板类型的模块 + - 第0条的id属性 @6 + - 第0条的name属性 @模板类型6 + - 第0条的type属性 @docTemplate + - 第0条的root属性 @2 + - 第0条的grade属性 @1 +- 获取root为2, grade为2的模板类型的模块 + - 第0条的id属性 @7 + - 第0条的name属性 @模板类型7 + - 第0条的type属性 @docTemplate + - 第0条的root属性 @2 + - 第0条的grade属性 @2 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('module')->loadYaml('templatemodule')->gen(10); +su('admin'); + +$roots = array('all', 1, 2); +$grades = array('all', 1, 2); + +$docTester = new docTest(); +r($docTester->getTemplateModulesTest($roots[0], $grades[0])) && p('0:id,name,type,root,grade') && e('1,模板类型1,docTemplate,1,1'); // 获取所有模板类型模块 +r($docTester->getTemplateModulesTest($roots[1], $grades[1])) && p('1:id,name,type,root,grade') && e('3,模板类型3,docTemplate,1,1'); // 获取root为1, grade为1的模板类型的模块 +r($docTester->getTemplateModulesTest($roots[1], $grades[2])) && p('0:id,name,type,root,grade') && e('2,模板类型2,docTemplate,1,2'); // 获取root为1, grade为2的模板类型的模块 +r($docTester->getTemplateModulesTest($roots[2], $grades[1])) && p('0:id,name,type,root,grade') && e('6,模板类型6,docTemplate,2,1'); // 获取root为2, grade为1的模板类型的模块 +r($docTester->getTemplateModulesTest($roots[2], $grades[2])) && p('0:id,name,type,root,grade') && e('7,模板类型7,docTemplate,2,2'); // 获取root为2, grade为2的模板类型的模块 \ No newline at end of file diff --git a/module/doc/test/model/gettemplatesbytype.php b/module/doc/test/model/gettemplatesbytype.php new file mode 100644 index 0000000000..7741edd4d8 --- /dev/null +++ b/module/doc/test/model/gettemplatesbytype.php @@ -0,0 +1,54 @@ +#!/usr/bin/env php +getTemplatesByType(); +timeout=0 +cid=1 + +- 获取所有模板 + - 第1条的id属性 @1 + - 第1条的title属性 @产品模板1 + - 第1条的templateType属性 @softwareProductPlan +- 获取所有已发布模板 + - 第2条的id属性 @2 + - 第2条的title属性 @产品模板2 + - 第2条的templateType属性 @hardwareProductPlan +- 获取所有草稿模板 + - 第3条的id属性 @3 + - 第3条的title属性 @产品模板3 + - 第3条的templateType属性 @qualityAssurancePlan +- 获取类型1下的所有模板 + - 第1条的id属性 @1 + - 第1条的title属性 @产品模板1 + - 第1条的templateType属性 @softwareProductPlan +- 获取类型1下的已发布模板 + - 第2条的id属性 @2 + - 第2条的title属性 @产品模板2 + - 第2条的templateType属性 @hardwareProductPlan +- 获取类型1下的草稿模板 + - 第3条的id属性 @3 + - 第3条的title属性 @产品模板3 + - 第3条的templateType属性 @qualityAssurancePlan + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('doc')->loadYaml('template')->gen(20); +zenData('module')->loadYaml('templatemodule')->gen(10); +zenData('user')->gen(5); +su('admin'); + +$types = array(null, 1); +$status = array('all', 'normal', 'draft'); + +$docTester = new docTest(); +r($docTester->getTemplatesByTypeTest($types[0], $status[0])) && p('1:id,title,templateType') && e('1,产品模板1,softwareProductPlan'); // 获取所有模板 +r($docTester->getTemplatesByTypeTest($types[0], $status[1])) && p('2:id,title,templateType') && e('2,产品模板2,hardwareProductPlan'); // 获取所有已发布模板 +r($docTester->getTemplatesByTypeTest($types[0], $status[2])) && p('3:id,title,templateType') && e('3,产品模板3,qualityAssurancePlan'); // 获取所有草稿模板 +r($docTester->getTemplatesByTypeTest($types[1], $status[0])) && p('1:id,title,templateType') && e('1,产品模板1,softwareProductPlan'); // 获取类型1下的所有模板 +r($docTester->getTemplatesByTypeTest($types[1], $status[1])) && p('2:id,title,templateType') && e('2,产品模板2,hardwareProductPlan'); // 获取类型1下的已发布模板 +r($docTester->getTemplatesByTypeTest($types[1], $status[2])) && p('3:id,title,templateType') && e('3,产品模板3,qualityAssurancePlan'); // 获取类型1下的草稿模板 diff --git a/module/doc/test/model/gettemplatescopes.php b/module/doc/test/model/gettemplatescopes.php new file mode 100644 index 0000000000..ae95f75387 --- /dev/null +++ b/module/doc/test/model/gettemplatescopes.php @@ -0,0 +1,49 @@ +#!/usr/bin/env php +getTemplateScopes(); +timeout=0 +cid=1 + +- 获取产品范围 + - 第1条的id属性 @1 + - 第1条的name属性 @产品 +- 获取项目范围 + - 第2条的id属性 @2 + - 第2条的name属性 @项目 +- 获取执行范围 + - 第3条的id属性 @3 + - 第3条的name属性 @执行 +- 获取个人范围 + - 第4条的id属性 @4 + - 第4条的name属性 @个人 +- 获取自定义范围 + - 第10条的id属性 @10 + - 第10条的name属性 @自定义范围 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('user')->gen(5); +su('admin'); + +global $tester; +$template = $tester->loadModel('doc'); + +$scope = zenData('doclib'); +$scope->id->range('1-10'); +$scope->type->range('template'); +$scope->vision->range('rnd{4},or{3},lite{2},rnd'); +$scope->name->range('产品,项目,执行,个人,市场,项目,个人,产品,个人,自定义范围'); +$scope->main->range('1{9},0'); +$scope->gen(10); + +r($template->getTemplateScopes()) && p('1:id,name') && e('1,产品'); // 获取产品范围 +r($template->getTemplateScopes()) && p('2:id,name') && e('2,项目'); // 获取项目范围 +r($template->getTemplateScopes()) && p('3:id,name') && e('3,执行'); // 获取执行范围 +r($template->getTemplateScopes()) && p('4:id,name') && e('4,个人'); // 获取个人范围 +r($template->getTemplateScopes()) && p('10:id,name') && e('10,自定义范围'); // 获取自定义范围 diff --git a/module/doc/test/model/inserttemplatescopes.php b/module/doc/test/model/inserttemplatescopes.php new file mode 100644 index 0000000000..314d55a233 --- /dev/null +++ b/module/doc/test/model/inserttemplatescopes.php @@ -0,0 +1,30 @@ +#!/usr/bin/env php +insertTemplateScopes(); +timeout=0 +cid=1 + +- 测试插入产品范围第1条的name属性 @产品范围 +- 测试插入项目范围第2条的name属性 @项目范围 +- 测试插入执行范围第3条的name属性 @执行范围 +- 测试插入个人范围第4条的name属性 @个人范围 +- 测试插入自定义范围第5条的name属性 @自定义范围 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('doclib')->gen(0); + +$scopeList = array('产品范围', '项目范围', '执行范围', '个人范围', '自定义范围'); + +$docTester = new docTest(); +r($docTester->insertTemplateScopesTest($scopeList)) && p('1:name') && e('产品范围'); // 测试插入产品范围 +r($docTester->insertTemplateScopesTest($scopeList)) && p('2:name') && e('项目范围'); // 测试插入项目范围 +r($docTester->insertTemplateScopesTest($scopeList)) && p('3:name') && e('执行范围'); // 测试插入执行范围 +r($docTester->insertTemplateScopesTest($scopeList)) && p('4:name') && e('个人范围'); // 测试插入个人范围 +r($docTester->insertTemplateScopesTest($scopeList)) && p('5:name') && e('自定义范围'); // 测试插入自定义范围 diff --git a/module/doc/test/model/movelib.php b/module/doc/test/model/movelib.php index 34dab263a6..397aed242f 100755 --- a/module/doc/test/model/movelib.php +++ b/module/doc/test/model/movelib.php @@ -10,7 +10,7 @@ cid=0 - 数据为空,libID=0时,检查返回结果 @0 - 数据为空,libID=1时,检查返回结果 @0 - 数据不为空,libID=0时,检查返回结果 @0 -- 传入错误数据,检查返回结果 @0 +- 传入错误数据,检查返回结果 @1 - 未修改文档空间,检查返回结果 @1 - 传入正确数据,检查返回结果 @1 - 检查移动后的文档数据 @@ -44,7 +44,7 @@ $errorData = new stdclass(); $customData->space = 'custom.7'; $mineData->space = 'mine.0'; -$errorData->space = 'project.product'; +$errorData->space = 'project.1'; /* Empty module object. */ r((int)$tester->doc->moveLib(0, clone $emptyData)) && p() && e('0'); // 数据为空,libID=0时,检查返回结果 @@ -52,7 +52,7 @@ r((int)$tester->doc->moveLib(1, clone $emptyData)) && p() && e('0'); // 数据 r((int)$tester->doc->moveLib(0, clone $customData)) && p() && e('0'); // 数据不为空,libID=0时,检查返回结果 /* Parent module object. */ -r((int)$tester->doc->moveLib($mineLibID, clone $errorData)) && p() && e('0'); // 传入错误数据,检查返回结果 +r((int)$tester->doc->moveLib($mineLibID, clone $errorData)) && p() && e('1'); // 传入错误数据,检查返回结果 r((int)$tester->doc->moveLib($mineLibID, clone $mineData)) && p() && e('1'); // 未修改文档空间,检查返回结果 r((int)$tester->doc->moveLib($mineLibID, clone $customData)) && p() && e('1'); // 传入正确数据,检查返回结果 r($tester->doc->getLibByID($mineLibID)) && p('type,parent') && e('custom,7'); // 检查移动后的文档数据 diff --git a/module/doc/test/model/setdocpriv.php b/module/doc/test/model/setdocpriv.php new file mode 100755 index 0000000000..bfd59664c2 --- /dev/null +++ b/module/doc/test/model/setdocpriv.php @@ -0,0 +1,91 @@ +#!/usr/bin/env php +copyTemplate(); +timeout=0 +cid=1 + +- 超级管理员可以查看和编辑自己的文档 + - 属性readable @1 + - 属性editable @1 +- 超级管理员可以查看和编辑其他用户的公共文档 + - 属性readable @1 + - 属性editable @1 +- 作者可以查看和编辑自己的私有文档 + - 属性readable @1 + - 属性editable @1 +- 用户不可以查看和编辑别人的私有文档 + - 属性readable @0 + - 属性editable @0 +- 用户不能编辑不在编辑白名单列表中的别人的私有文档 + - 属性readable @1 + - 属性editable @0 +- 用户可以查看和编辑自己在编辑白名单列表中的别人的私有文档 + - 属性readable @1 + - 属性editable @1 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('user')->gen(5); +su('admin'); + +$doc = new stdclass(); +$doc->acl = 'open'; +$doc->addedBy = 'admin'; +$doc->readUsers = 'admin'; +$doc->users = 'admin'; +$doc->groups = ''; +$doc->readGroups = '1,2'; + +$docTester = new docTest(); +r($docTester->setDocPrivTest($doc, 'team')) && p('readable,editable') && e('1,1'); // 超级管理员可以查看和编辑自己的文档 + +$doc->acl = 'open'; +$doc->addedBy = 'user1'; +$doc->readUsers = ''; +$doc->users = ''; +$doc->groups = ''; +$doc->readGroups = ''; + +r($docTester->setDocPrivTest($doc, 'team')) && p('readable,editable') && e('1,1'); // 超级管理员可以查看和编辑其他用户的公共文档 + +su('user1'); + +$doc->acl = 'private'; +$doc->addedBy = 'user1'; +$doc->readUsers = ''; +$doc->users = ''; +$doc->groups = ''; +$doc->readGroups = ''; + +r($docTester->setDocPrivTest($doc, 'mine')) && p('readable,editable') && e('1,1'); // 作者可以查看和编辑自己的私有文档 + +$doc->acl = 'private'; +$doc->addedBy = 'user2'; +$doc->readUsers = ''; +$doc->users = ''; +$doc->groups = ''; +$doc->readGroups = ''; + +r($docTester->setDocPrivTest($doc, 'team')) && p('readable,editable') && e('0,0'); // 用户不可以查看和编辑别人的私有文档 + +$doc->addedBy = 'user2'; +$doc->readUsers = 'user1'; +$doc->users = ''; +$doc->groups = ''; +$doc->readGroups = ''; + +r($docTester->setDocPrivTest($doc, 'team')) && p('readable,editable') && e('1,0'); // 用户不能编辑不在编辑白名单列表中的别人的私有文档 + +$doc->addedBy = 'user2'; +$doc->readUsers = ''; +$doc->users = 'user1'; +$doc->groups = ''; +$doc->readGroups = ''; + +r($docTester->setDocPrivTest($doc, 'team')) && p('readable,editable') && e('1,1'); // 用户可以查看和编辑自己在编辑白名单列表中的别人的私有文档 \ No newline at end of file diff --git a/module/doc/test/model/update.php b/module/doc/test/model/update.php index 968986888f..f651c0c350 100755 --- a/module/doc/test/model/update.php +++ b/module/doc/test/model/update.php @@ -23,9 +23,9 @@ cid=1 - 第0条的old属性 @文档标题11 - 第0条的new属性 @修改自定义文档 - 修改私有文档 - - 第2条的field属性 @acl - - 第2条的old属性 @custom - - 第2条的new属性 @private + - 第0条的field属性 @title + - 第0条的old属性 @修改自定义文档 + - 第0条的new属性 @修改私有文档 - 修改我的文档 - 第0条的field属性 @title - 第0条的old属性 @文档标题1 @@ -66,7 +66,7 @@ r($docTester->updateTest($docIDs[0], $updateProductDoc)) && p('0:field,old,new r($docTester->updateTest($docIDs[1], $updateProjectDoc)) && p('0:field,old,new') && e('title,文档标题31,修改项目文档'); // 修改项目文档 r($docTester->updateTest($docIDs[2], $updateExecutionDoc)) && p('0:field,old,new') && e('module,3,0'); // 修改执行文档 r($docTester->updateTest($docIDs[3], $updateCustomDoc)) && p('0:field,old,new') && e('title,文档标题11,修改自定义文档'); // 修改自定义文档 -r($docTester->updateTest($docIDs[3], $privateDoc)) && p('2:field,old,new') && e('acl,custom,private'); // 修改私有文档 +r($docTester->updateTest($docIDs[3], $privateDoc)) && p('0:field,old,new') && e('title,修改自定义文档,修改私有文档'); // 修改私有文档 r($docTester->updateTest($docIDs[4], $mineDoc)) && p('0:field,old,new') && e('title,文档标题1,修改我的文档'); // 修改我的文档 r($docTester->updateTest($docIDs[3], $customDoc)) && p('0:field,old,new') && e('lib,6,7'); // 修改自定义文档 r($docTester->updateTest($docIDs[1], $noTitle)) && p('title:0') && e('『文档标题』不能为空。'); // 修改标题为空 diff --git a/module/doc/test/model/updatedocfile.php b/module/doc/test/model/updatedocfile.php index 093e2fb5e3..3fb7c7a36c 100755 --- a/module/doc/test/model/updatedocfile.php +++ b/module/doc/test/model/updatedocfile.php @@ -4,6 +4,7 @@ /** title=测试 docModel->updateDocFile(); +timeout=0 cid=1 - 测试将文件ID为1的文件从文档ID为1的文档中移除属性files @2 diff --git a/module/doc/test/model/updateorder.php b/module/doc/test/model/updateorder.php index 48228d7009..45ebb7a895 100755 --- a/module/doc/test/model/updateorder.php +++ b/module/doc/test/model/updateorder.php @@ -9,9 +9,12 @@ cid=1 - 测试ID为1的目录,order值不变的情况 @10 - 测试ID为2的目录,order值需要更新的情况 @1 -- 测试目录ID不存在的情况 @~~ +- 测试ID为3的目录,order值不变的情况 @30 +- 测试ID为4的目录,order值需要更新的情况 @2 +- 测试目录ID不存在的情况 @0 */ + include dirname(__FILE__, 5) . '/test/lib/init.php'; include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; @@ -20,9 +23,13 @@ zenData('module')->loadYaml('module')->gen(10); $catalogOrderList = array(); $catalogOrderList[] = array('id' => 1, 'order' => 10); $catalogOrderList[] = array('id' => 2, 'order' => 1); +$catalogOrderList[] = array('id' => 3, 'order' => 30); +$catalogOrderList[] = array('id' => 4, 'order' => 2); $catalogOrderList[] = array('id' => 11, 'order' => 100); $docTester = new docTest(); -r($docTester->updateOrderTest($catalogOrderList[0]['id'], $catalogOrderList[0]['order'])) && p() && e(10); // 测试ID为1的目录,order值不变的情况 -r($docTester->updateOrderTest($catalogOrderList[1]['id'], $catalogOrderList[1]['order'])) && p() && e(1); // 测试ID为2的目录,order值需要更新的情况 -r($docTester->updateOrderTest($catalogOrderList[2]['id'], $catalogOrderList[2]['order'])) && p() && e('~~'); // 测试目录ID不存在的情况 +r($docTester->updateOrderTest($catalogOrderList[0]['id'], $catalogOrderList[0]['order'])) && p() && e(10); // 测试ID为1的目录,order值不变的情况 +r($docTester->updateOrderTest($catalogOrderList[1]['id'], $catalogOrderList[1]['order'])) && p() && e(1); // 测试ID为2的目录,order值需要更新的情况 +r($docTester->updateOrderTest($catalogOrderList[2]['id'], $catalogOrderList[2]['order'])) && p() && e(30); // 测试ID为3的目录,order值不变的情况 +r($docTester->updateOrderTest($catalogOrderList[3]['id'], $catalogOrderList[3]['order'])) && p() && e(2); // 测试ID为4的目录,order值需要更新的情况 +r($docTester->updateOrderTest($catalogOrderList[4]['id'], $catalogOrderList[4]['order'])) && p() && e('0'); // 测试目录ID不存在的情况 diff --git a/module/doc/test/model/updatetemplatescopes.php b/module/doc/test/model/updatetemplatescopes.php new file mode 100644 index 0000000000..b77b6c7477 --- /dev/null +++ b/module/doc/test/model/updatetemplatescopes.php @@ -0,0 +1,36 @@ +#!/usr/bin/env php +updateTemplateScopes(); +timeout=0 +cid=1 + +- 测试修改产品范围名称属性1 @修改后的产品范围 +- 测试修改项目范围名称属性2 @修改后的项目范围 +- 测试修改执行范围名称属性3 @修改后的执行范围 +- 测试修改个人范围名称属性4 @修改后的个人范围 +- 测试修改自定义范围名称属性10 @修改后的自定义范围 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +$scope = zenData('doclib'); +$scope->id->range('1-10'); +$scope->type->range('template'); +$scope->vision->range('rnd{4},or{3},lite{2},rnd'); +$scope->name->range('产品,项目,执行,个人,市场,项目,个人,产品,个人,自定义范围'); +$scope->main->range('1{9},0'); +$scope->gen(10); + +$scopeList = array(array(1 => '修改后的产品范围'), array(2 => '修改后的项目范围'), array(3 => '修改后的执行范围'), array(4 => '修改后的个人范围'), array(10 => '修改后的自定义范围')); + +$docTester = new docTest(); +r($docTester->updateTemplateScopesTest($scopeList[0])) && p('1') && e('修改后的产品范围'); // 测试修改产品范围名称 +r($docTester->updateTemplateScopesTest($scopeList[1])) && p('2') && e('修改后的项目范围'); // 测试修改项目范围名称 +r($docTester->updateTemplateScopesTest($scopeList[2])) && p('3') && e('修改后的执行范围'); // 测试修改执行范围名称 +r($docTester->updateTemplateScopesTest($scopeList[3])) && p('4') && e('修改后的个人范围'); // 测试修改个人范围名称 +r($docTester->updateTemplateScopesTest($scopeList[4])) && p('10') && e('修改后的自定义范围'); // 测试修改自定义范围名称 diff --git a/module/doc/test/model/upgradetemplatelibandmodule.php b/module/doc/test/model/upgradetemplatelibandmodule.php new file mode 100755 index 0000000000..634ff3a622 --- /dev/null +++ b/module/doc/test/model/upgradetemplatelibandmodule.php @@ -0,0 +1,70 @@ +#!/usr/bin/env php +upgradeTemplateLibAndModule(); +timeout=0 +cid=1 + +- 更新模板1的字段值 + - 属性lib @1 + - 属性module @1 + - 属性templateType @PP +- 更新模板1的字段值 + - 属性lib @1 + - 属性module @2 + - 属性templateType @QAP +- 更新模板1的字段值 + - 属性lib @1 + - 属性module @3 + - 属性templateType @CMP +- 更新模板4的字段值 + - 属性lib @1 + - 属性module @4 + - 属性templateType @custom1 +- 更新模板5的字段值 + - 属性lib @1 + - 属性module @4 + - 属性templateType @custom1 +- 更新模板6的字段值 + - 属性lib @2 + - 属性module @5 + - 属性templateType @ITP +- 更新模板7的字段值 + - 属性lib @2 + - 属性module @6 + - 属性templateType @ERS +- 更新模板8的字段值 + - 属性lib @2 + - 属性module @7 + - 属性templateType @URS +- 更新模板9的字段值 + - 属性lib @2 + - 属性module @8 + - 属性templateType @SRS +- 更新模板10的字段值 + - 属性lib @2 + - 属性module @9 + - 属性templateType @custom2 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('doc')->loadYaml('template')->gen(20); +zenData('user')->gen(5); +su('admin'); + +$docTester = new docTest(); +r($docTester->upgradeTemplateLibAndModuleTest(1)) && p('lib,module,templateType') && e('1,1,PP'); // 更新模板1的字段值 +r($docTester->upgradeTemplateLibAndModuleTest(2)) && p('lib,module,templateType') && e('1,2,QAP'); // 更新模板1的字段值 +r($docTester->upgradeTemplateLibAndModuleTest(3)) && p('lib,module,templateType') && e('1,3,CMP'); // 更新模板1的字段值 +r($docTester->upgradeTemplateLibAndModuleTest(4)) && p('lib,module,templateType') && e('1,4,custom1'); // 更新模板4的字段值 +r($docTester->upgradeTemplateLibAndModuleTest(5)) && p('lib,module,templateType') && e('1,4,custom1'); // 更新模板5的字段值 +r($docTester->upgradeTemplateLibAndModuleTest(6)) && p('lib,module,templateType') && e('2,5,ITP'); // 更新模板6的字段值 +r($docTester->upgradeTemplateLibAndModuleTest(7)) && p('lib,module,templateType') && e('2,6,ERS'); // 更新模板7的字段值 +r($docTester->upgradeTemplateLibAndModuleTest(8)) && p('lib,module,templateType') && e('2,7,URS'); // 更新模板8的字段值 +r($docTester->upgradeTemplateLibAndModuleTest(9)) && p('lib,module,templateType') && e('2,8,SRS'); // 更新模板9的字段值 +r($docTester->upgradeTemplateLibAndModuleTest(10)) && p('lib,module,templateType') && e('2,9,custom2'); // 更新模板10的字段值 diff --git a/module/doc/test/model/upgradetemplatetypes.php b/module/doc/test/model/upgradetemplatetypes.php new file mode 100755 index 0000000000..6b28289b5c --- /dev/null +++ b/module/doc/test/model/upgradetemplatetypes.php @@ -0,0 +1,45 @@ +#!/usr/bin/env php +upgradeCustomTemplateTypes(); +timeout=0 +cid=1 + +- 内置计划 @1 +- 内置需求 @1 +- 内置设计 @1 +- 内置开发 @1 +- 内置测试 @1 +- 内置说明 @1 +- 内置其他 @1 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('module')->gen(0); +zenData('doc')->loadYaml('template')->gen(20); +zenData('user')->gen(5); +su('admin'); + +$scope = zenData('config'); +$scope->id->range('1-3'); +$scope->vision->range('rnd,or,lite'); +$scope->owner->range('system'); +$scope->module->range('doc'); +$scope->section->range(''); +$scope->key->range('builtInScopeMaps'); +$scope->value->range('`{"product":1,"project":2,"execution":3,"personal":4}`,`{"market":5,"product":6,"personal":7}`,`{"project":8,"personal":9}`'); +$scope->gen(3); + +$docTester = new docTest(); +r($docTester->upgradeTemplateTypesTest(1, '计划')) && p() && e('1'); // 内置计划 +r($docTester->upgradeTemplateTypesTest(2, '需求')) && p() && e('1'); // 内置需求 +r($docTester->upgradeTemplateTypesTest(3, '设计')) && p() && e('1'); // 内置设计 +r($docTester->upgradeTemplateTypesTest(4, '开发')) && p() && e('1'); // 内置开发 +r($docTester->upgradeTemplateTypesTest(5, '测试')) && p() && e('1'); // 内置测试 +r($docTester->upgradeTemplateTypesTest(6, '说明')) && p() && e('1'); // 内置说明 +r($docTester->upgradeTemplateTypesTest(7, '其他')) && p() && e('1'); // 内置其他 diff --git a/module/doc/test/tao/doupdatedoc.php b/module/doc/test/tao/doupdatedoc.php new file mode 100755 index 0000000000..7e3d1fe2a2 --- /dev/null +++ b/module/doc/test/tao/doupdatedoc.php @@ -0,0 +1,29 @@ +#!/usr/bin/env php +doUpdateDoc(); +cid=1 + +- 修改文档库ID属性lib @2 +- 修改所属目录属性module @3 +- 修改访问控制属性acl @open +- 修改可访问组属性groups @4 +- 修改可访问人员属性users @admin + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/doc.unittest.class.php'; + +zenData('doc')->loadYaml('template')->gen(10); +zenData('user')->gen(5); +su('admin'); + +$docTester = new docTest(); +r($docTester->doUpdateDocTest(1, array('lib' => 2))) && p('lib') && e('2'); // 修改文档库ID +r($docTester->doUpdateDocTest(2, array('module' => 3))) && p('module') && e('3'); // 修改所属目录 +r($docTester->doUpdateDocTest(3, array('acl' => 'open'))) && p('acl') && e('open'); // 修改访问控制 +r($docTester->doUpdateDocTest(4, array('groups' => 4))) && p('groups') && e('4'); // 修改可访问组 +r($docTester->doUpdateDocTest(5, array('users' => 'admin'))) && p('users') && e('admin'); // 修改可访问人员 diff --git a/module/doc/test/yaml/template.yaml b/module/doc/test/yaml/template.yaml new file mode 100644 index 0000000000..8a16434965 --- /dev/null +++ b/module/doc/test/yaml/template.yaml @@ -0,0 +1,33 @@ +title: zt_doc. +author: Qiyu Xie +version: "1.0" +fields: + - field: id + range: 1-100 + - field: title + note: "名称" + fields: + - field: title1 + range: "产品模板{5},项目模板{5},执行模板{5},个人模板{5}" + - field: title2 + range: 1-100 + - field: project + range: 0 + - field: product + range: 0 + - field: execution + range: 0 + - field: lib + range: "1{5},2{5},3{5},4{5}" + - field: module + range: "1,2,3,4{2},5,6,7,8,9,10,11,12,13,14{2},15,16,17,18{2}" + - field: status + range: "normal{2},draft{3}" + - field: type + range: text + - field: version + range: 1 + - field: templateType + range: "PP,QAP,CMP,custom1{2},ITP,ERS,URS,SRS,custom2,HLDS,DDS,DBDS,custom3{2},ADS,Code,ITTC,custom4{2}" + - field: addedDate + range: "`2025-04-10 00:00:01`,`2025-04-11 00:00:01`,`2025-04-12 00:00:01`,`2025-04-13 00:00:01`,`2025-04-14 00:00:01`,`2025-04-15 00:00:01`,`2025-04-16 00:00:01`,`2025-04-17 00:00:01`,`2025-04-18 00:00:01`,`2025-04-19 00:00:01`" diff --git a/module/doc/test/yaml/templatemodule.yaml b/module/doc/test/yaml/templatemodule.yaml new file mode 100644 index 0000000000..901d0b230c --- /dev/null +++ b/module/doc/test/yaml/templatemodule.yaml @@ -0,0 +1,28 @@ +title: zt_module. +author: Qiyu Xie +version: "1.0" +fields: + - field: id + range: 1-100 + - field: name + fields: + - field: name1 + range: '模板类型' + - field: name2 + range: 1-100 + - field: short + fields: + - field: name1 + range: custom + - field: name2 + range: 1-100 + - field: root + range: 1{5},2{5} + - field: parent + range: 1{5},2{5} + - field: path + range: "`,1,`,`,1,2,`,`,3,`,`,4,`,`,5,`,`,6,`,`,6,7,`,`,8,`,`,9,`,`,10,`" + - field: grade + range: "1,2,1{3},1,2,1{3}" + - field: type + range: 'docTemplate' diff --git a/module/doc/ui/addtemplatetype.html.php b/module/doc/ui/addtemplatetype.html.php new file mode 100644 index 0000000000..884f22d2fe --- /dev/null +++ b/module/doc/ui/addtemplatetype.html.php @@ -0,0 +1,59 @@ + + * @package doc + * @link https://www.zentao.net + */ +namespace zin; + +formPanel +( + detailHeader + ( + to::title + ( + entityLabel + ( + setClass('text-xl font-black'), + set::level(1), + set::text($lang->docTemplate->addTemplateType) + ) + ) + ), + setID('addForm'), + set::submitBtnText($lang->save), + formGroup + ( + set::label($lang->docTemplate->scope), + picker + ( + set::name('root'), + set::value($scope), + set::items($scopes), + set::required(true), + on::change('changeScope') + ) + ), + formGroup + ( + set::label($lang->docTemplate->parentModule), + picker + ( + set::name('parent'), + set::value($parentModule), + set::items($moduleItems), + set::required(true) + ) + ), + formGroup + ( + set::required(true), + set::name('name'), + set::label($lang->docTemplate->typeName), + set::control('input') + ) +); diff --git a/module/doc/ui/app.html.php b/module/doc/ui/app.html.php index 156f19e25b..4750972990 100644 --- a/module/doc/ui/app.html.php +++ b/module/doc/ui/app.html.php @@ -71,19 +71,20 @@ $privs['editChapter'] = hasPriv('doc', 'editChapter'); $privs['deleteChapter'] = hasPriv('doc', 'deleteChapter'); $privs['sortChapter'] = hasPriv('doc', 'sortChapter'); -$privs['productStory'] = hasPriv('product', 'browse'); -$privs['projectStory'] = hasPriv('projectstory', 'story'); -$privs['executionStory'] = hasPriv('execution', 'story'); -$privs['productCase'] = hasPriv('testcase', 'browse'); -$privs['caselibBrowse'] = hasPriv('caselib', 'browse'); -$privs['productBug'] = hasPriv('bug', 'browse'); -$privs['taskBrowse'] = hasPriv('execution', 'task'); -$privs['productplanBrowse'] = hasPriv('productplan', 'browse'); -$privs['releaseBrowse'] = hasPriv('release', 'browse'); -$privs['feedbackBrowse'] = hasPriv('feedback', 'admin'); -$privs['ticketBrowse'] = hasPriv('ticket', 'browse'); -$privs['requirementBrowse'] = hasPriv('product', 'requirement'); -$privs['epicBrowse'] = hasPriv('product', 'epic'); +$privs['productStory'] = hasPriv('product', 'browse'); +$privs['projectStory'] = hasPriv('projectstory', 'story'); +$privs['executionStory'] = hasPriv('execution', 'story'); +$privs['productCase'] = hasPriv('testcase', 'browse'); +$privs['caselibBrowse'] = hasPriv('caselib', 'browse'); +$privs['productBug'] = hasPriv('bug', 'browse'); +$privs['taskBrowse'] = hasPriv('execution', 'task'); +$privs['productplanBrowse'] = hasPriv('productplan', 'browse'); +$privs['releaseBrowse'] = hasPriv('release', 'browse'); +$privs['projectReleaseBrowse'] = hasPriv('projectRelease', 'browse'); +$privs['feedbackBrowse'] = hasPriv('feedback', 'admin'); +$privs['ticketBrowse'] = hasPriv('ticket', 'browse'); +$privs['requirementBrowse'] = hasPriv('product', 'requirement'); +$privs['epicBrowse'] = hasPriv('product', 'epic'); $privs['storyView'] = hasPriv('story', 'view'); $privs['taskView'] = hasPriv('task', 'view'); diff --git a/module/doc/ui/browsetemplate.html.php b/module/doc/ui/browsetemplate.html.php new file mode 100644 index 0000000000..24eab8ed59 --- /dev/null +++ b/module/doc/ui/browsetemplate.html.php @@ -0,0 +1,14 @@ + + * @package doc + * @link https://www.zentao.net + */ +namespace zin; + +if($mode == 'home' || !$libID) include './templatehome.html.php'; +else include './templatelist.html.php'; diff --git a/module/doc/ui/managescope.html.php b/module/doc/ui/managescope.html.php new file mode 100644 index 0000000000..85720522f1 --- /dev/null +++ b/module/doc/ui/managescope.html.php @@ -0,0 +1,40 @@ + + * @package doc + * @link https://www.zentao.net + */ +namespace zin; + +modalHeader(set::title($lang->doc->manageScope), set::titleClass('text-lg font-bold')); + +$scopeItems = array(); +$scopeItems[] = array('name' => 'scopes'); +$scopeItems[] = array('name' => 'id', 'class' => 'hidden'); + +$dataItems = array(); +foreach($scopeList as $scopeID => $scope) +{ + $item = new stdClass(); + $item->id = 'id' . $scopeID; + $item->scopes = $scope->name; + $item->main = $scope->main; + $dataItems[] = $item; +} + +$batchFormOptions = array(); +$batchFormOptions['onClickAction'] = jsRaw('window.handleClickBatchFormAction'); // 定义操作列按钮点击事件处理函数。 +$batchFormOptions['onRenderRow'] = jsRaw('window.handleRenderRow'); // 定义行渲染事件处理函数。 + +formBatchPanel +( + set::items($scopeItems), + set::data($dataItems), + set::minRows(6), + set::actionsText(''), + set::batchFormOptions($batchFormOptions) +); diff --git a/module/doc/ui/movetemplate.html.php b/module/doc/ui/movetemplate.html.php new file mode 100644 index 0000000000..fa46e184cf --- /dev/null +++ b/module/doc/ui/movetemplate.html.php @@ -0,0 +1,56 @@ + + * @package doc + * @version $Id$ + * @link http://www.zentao.net + */ +namespace zin; + +modalHeader(set::title(empty($doc->parent) ? $lang->docTemplate->moveDocTemplate : $lang->docTemplate->moveSubTemplate), set::entityText($doc->title), set::entityID($docID)); + +$whiteListHidden = $doc->acl == 'private' ? '' : 'hidden'; + +unset($modules[0]); +formPanel +( + on::change('[name=lib]')->call('loadScopeTypes', jsRaw("event")), + empty($doc->parent) ? formGroup + ( + set::label($lang->docTemplate->scope), + set::required(true), + picker(set::name('lib'), set::items($scopeItems), set::value($doc->lib), set::required(true)) + ) : null, + empty($doc->parent) ? formGroup + ( + set::label($lang->docTemplate->module), + set::required(true), + picker(set::name('module'), set::items($modules), set::value($doc->module), set::required(true)) + ) : null, + !empty($doc->parent) ? formGroup + ( + set::label($lang->docTemplate->parent), + picker + ( + set::name('parent'), + set::items($chapterAndDocs), + set::value($doc->parent), + set::required(true) + ), + ) : null, + empty($doc->parent) ? formGroup + ( + set::label($lang->doclib->control), + radioList + ( + set::name('acl'), + set::items($lang->doc->aclListA), + set::value($doc->acl) + ) + ) : null +); diff --git a/module/doc/ui/setdocbasic.html.php b/module/doc/ui/setdocbasic.html.php index f43ab3d522..e20bedce3b 100644 --- a/module/doc/ui/setdocbasic.html.php +++ b/module/doc/ui/setdocbasic.html.php @@ -10,6 +10,12 @@ declare(strict_types=1); */ namespace zin; +if($objectType == 'template') +{ + include 'setdocbasic.template.html.php'; + return; +} + if($modalType == 'chapter') $lang->doc->aclList['private'] = $lang->doclib->aclList['private']; formPanel diff --git a/module/doc/ui/setdocbasic.template.html.php b/module/doc/ui/setdocbasic.template.html.php new file mode 100644 index 0000000000..8845ccb45f --- /dev/null +++ b/module/doc/ui/setdocbasic.template.html.php @@ -0,0 +1,76 @@ + + * @package doc + * @link https://www.zentao.net + */ +namespace zin; + +$optionMenu = $this->tree->getOptionMenu($libID, 'docTemplate', 0, 'all', 'nodeleted', 'all'); +$modules = array(); +foreach($optionMenu as $menuID => $menu) +{ + if($menuID == 0) continue; + $modules[] = array('text' => $menu, 'value' => $menuID, 'keys' => $menu); +} + +formPanel +( + setID('setDocBasicForm'), + setData('officeTypes', $this->config->doc->officeTypes), + setData('docType', isset($doc) ? $doc->users : 'undefined'), + set::title($title), + set::submitBtnText($lang->save), + on::change('[name=lib]')->call('loadScopeTypes', jsRaw("event")), + set::ajax(array('beforeSubmit' => jsRaw('window.beforeSetDocBasicInfo'))), + formGroup + ( + set::label($modalType == 'chapter' ? $lang->doc->chapterName : $lang->docTemplate->title), + set::name('title'), + set::required(true), + set::value(isset($doc) ? $doc->title : '') + ), + empty($parentID) ? formGroup + ( + set::label($lang->docTemplate->scope), + set::required(true), + picker(set::name('lib'), set::items($scopeItems), set::value($libID), set::required(true)) + ) : null, + empty($parentID) ? formGroup + ( + set::label($lang->docTemplate->module), + set::required(true), + picker(set::name('module'), set::items($modules), set::value($moduleID), set::required(true)) + ) : null, + ($modalType != 'chapter' || !$isCreate) && !empty($parentID) ? formGroup + ( + set::label($lang->docTemplate->parent), + picker + ( + set::name('parent'), + set::items($chapterAndDocs), + set::value($parentID ? $parentID : "m_$moduleID"), + set::required(true) + ), + ) : null, + $modalType != 'chapter' ? formGroup + ( + set::label($lang->docTemplate->desc), + textarea(set::name('templateDesc'), set::value($docID ? $doc->templateDesc : ''), set::rows(3)) + ) : null, + empty($parentID) ? formGroup + ( + set::label($lang->doclib->control), + radioList + ( + setClass($objectType == 'mine' ? 'pointer-events-none' : ''), + set::name('acl'), + set::items($lang->doc->aclListA), + set::value(isset($doc) ? $doc->acl : 'open') + ) + ) : null +); diff --git a/module/doc/ui/templatehome.html.php b/module/doc/ui/templatehome.html.php new file mode 100644 index 0000000000..809ff118fb --- /dev/null +++ b/module/doc/ui/templatehome.html.php @@ -0,0 +1,167 @@ + + * @package doc + * @link https://www.zentao.net + */ +namespace zin; + +$scopeList = $this->doc->getTemplateScopes(); +$scopeTemplates = $this->doc->getScopeTemplates(array_keys($scopeList)); +$allTemplates = $this->doc->getTemplatesByType(); + +$buildScopeCards = function($templates) use ($lang, $allTemplates) +{ + $cardItems = array(); + foreach($templates as $template) + { + $hasChildren = false; + foreach($allTemplates as $templateInfo) + { + if($templateInfo->id == $template->id) continue; + if(strpos(",{$templateInfo->path},", ",{$template->id},") !== false) $hasChildren = true; + } + + $cardDesc = $template->templateDesc ? $template->templateDesc : $lang->docTemplate->noDesc; + $viewLink = createLink('doc', 'view', "docID=$template->id"); + $editLink = createLink('doc', 'browsetemplate', "libID=$template->lib&type=all&docID=$template->id&orderBy=id_desc&recPerPage=20&page=1&mode=edit"); + $deleteLink = createLink('doc', 'deleteTemplate', "templateID=$template->id"); + + $actions = array(); + if($template->addedBy == $this->app->user->account || hasPriv('doc', 'editTemplate')) $actions[] = array('icon' => 'edit', 'text' => $this->lang->docTemplate->edit, 'url' => $editLink); + if(hasPriv('doc', 'deleteTemplate')) $actions[] = array('icon' => 'trash', 'text' => $this->lang->docTemplate->delete, 'url' => $deleteLink, 'data-confirm' => $hasChildren ? $this->lang->docTemplate->confirmDeleteTemplateWithSub : $this->lang->docTemplate->confirmDelete); + + $cardItems[] = div + ( + hasPriv('doc', 'viewTemplate') ? on::click()->do("clickTemplateCard(event, '$viewLink')") : null, + setClass('doc-space-card-lib px-2 w-1/5 group'), + div + ( + setClass('canvas border rounded py-2 px-3 col gap-1 hover:shadow-lg hover:border-primary relative cursor-pointer'), + div + ( + setClass('flex gap-2 items-center py-2'), + icon + ( + setClass('icon-file-archive text-2xl') + ), + div + ( + setClass('font-bold text-clip'), + set::title($template->title), + $template->title + ) + ), + div + ( + setClass('text-gray text-clip text-sm py-1'), + set::title($cardDesc), + $cardDesc + ), + div + ( + setClass('toolbar absolute top-1 right-1 opacity-0 group-hover:opacity-100'), + !empty($actions) ? dropdown + ( + btn + ( + setClass('size-sm dropdown'), + set::type('ghost'), + set::icon('ellipsis-v'), + set::caret(false), + ), + set::items($actions), + set::flip(true), + set::placement('bottom-center'), + set::strategy('absolute'), + set::hasIcons(false) + ) : null + ) + ) + ); + } + + return $cardItems; +}; + +$scopeItems = array(); +foreach($scopeList as $scope) +{ + $scopeDocs = $scopeTemplates[$scope->id]; + $scopeItems[] = div + ( + setClass('doc-space-card ring rounded surface-light'), + div + ( + setClass('row items-center justify-between gap-2 px-2.5 py-1 border-b'), + div + ( + setClass('row items-center ml-2 flex-none'), + div + ( + setClass('min-w-0 flex-auto'), + strong($scope->name), + span(setClass('label ml-2 flex-none bg-white size-sm text-sm'), $lang->docTemplate->scopeLabel) + ), + ), + toolbar + ( + item(set(array + ( + 'type' => 'ghost', + 'caret' => 'right', + 'class' => 'text-primary', + 'text' => $lang->more, + 'url' => createLink('doc', 'browseTemplate', "libID={$scope->id}&type=all&docID=0&orderBy=id_desc&recPerPae=20&pageID=1&mode=list") + ))) + ), + ), + div + ( + setClass('doc-space-card-libs py-3 px-1.5'), + !empty($scopeDocs) ? div + ( + setClass('row'), + $buildScopeCards($scopeDocs) + ) : div + ( + setClass('center gap-4 py-10'), + div + ( + setClass('text-gray'), + $lang->docTemplate->noTemplate + ), + hasPriv('doc', 'createTemplate') ? btn + ( + setClass('btn primary-pale'), + set::icon('plus'), + $lang->doc->createTemplate, + set::url(createLink('doc', 'browseTemplate', "libID={$scope->id}&type=all&docID=0&orderBy=&recPerPae=20&pageID=1&mode=create")) + ) : null + ) + ) + ); +} + +featureBar(); +toolbar +( + common::hasPriv('doc', 'manageScope') ? btn + ( + setData(array('toggle' => 'modal', 'size' => 'sm')), + set::text($lang->doc->manageScope), + set::icon('plus'), + set::type('primary'), + set::url(createLink('doc', 'manageScope')) + ) : null +); + +div +( + setClass('doc-home-body flex-auto min-h-0 col gap-4 p-0.5 items-stretch overflow-auto scrollbar-hover'), + $scopeItems +); diff --git a/module/doc/ui/templatelist.html.php b/module/doc/ui/templatelist.html.php new file mode 100644 index 0000000000..9d8174a2f3 --- /dev/null +++ b/module/doc/ui/templatelist.html.php @@ -0,0 +1,119 @@ + + * @package doc + * @link https://www.zentao.net + */ +namespace zin; + +$spaceID = 1; +$data = array('spaceID' => $spaceID, 'docs' => array_values($templateList)); +$data['spaces'][] = array('name' => $lang->doc->template, 'id' => $spaceID); +foreach($scopes as $scope) $data['libs'][] = array('id' => $scope->id, 'name' => $scope->name, 'space' => $spaceID); +$data['modules'] = $this->doc->getTemplateModules(); + +$privs = array(); +$privs['create'] = hasPriv('doc', 'createTemplate'); +$privs['edit'] = hasPriv('doc', 'createTemplate'); +$privs['delete'] = hasPriv('doc', 'deleteTemplate'); +$privs['view'] = hasPriv('doc', 'viewTemplate'); +$privs['addModule'] = hasPriv('doc', 'addTemplateType'); +$privs['editModule'] = hasPriv('doc', 'editTemplateType'); +$privs['deleteModule'] = hasPriv('doc', 'deleteTemplateType'); +$privs['moveDoc'] = hasPriv('doc', 'moveTemplate'); +$privs['sortDoc'] = hasPriv('doc', 'sortTemplate'); +$privs['collect'] = 'no'; +$privs['restoreDoc'] = false; + +$filterTypes = $lang->docTemplate->filterTypes; +if(!hasPriv('doc', 'editTemplate')) $filterTypes = array_values(array_filter($filterTypes, function($item){ return $item[0] != 'draft'; })); + +$tableCols = array(); +$tableCols['id'] = $lang->docTemplate->id; +$tableCols['title'] = $lang->docTemplate->title; +$tableCols['frequency'] = $lang->docTemplate->frequency; +$tableCols['type'] = $lang->docTemplate->type; +$tableCols['addedBy'] = $lang->docTemplate->addedBy; +$tableCols['addedDate'] = $lang->docTemplate->addedDate; +$tableCols['editedBy'] = $lang->docTemplate->editedBy; +$tableCols['editedDate'] = $lang->docTemplate->editedDate; +$tableCols['views'] = $lang->docTemplate->views; +$tableCols['desc'] = $lang->docTemplate->desc; +$tableCols['actions'] = $lang->actions; + +$langData = array(); +$langData['filterTypes'] = $filterTypes; +$langData['tableCols'] = $tableCols; +$langData['createDoc'] = $lang->docTemplate->create; +$langData['edit'] = $lang->docTemplate->edit; +$langData['editDoc'] = $lang->docTemplate->edit; +$langData['delete'] = $lang->docTemplate->delete; +$langData['deleteDoc'] = $lang->docTemplate->delete; +$langData['createTemplate'] = $lang->docTemplate->create; +$langData['editTemplate'] = $lang->docTemplate->edit; +$langData['deleteTemplate'] = $lang->docTemplate->delete; +$langData['confirmDelete'] = $lang->docTemplate->confirmDelete; +$langData['addModule'] = $lang->docTemplate->addModule; +$langData['addSubModule'] = $lang->docTemplate->addSubModule; +$langData['editModule'] = $lang->docTemplate->editModule; +$langData['deleteModule'] = $lang->docTemplate->deleteModule; +$langData['noModules'] = $lang->docTemplate->noModules; +$langData['docTitlePlaceholder'] = $lang->docTemplate->docTitlePlaceholder; +$langData['docTitleRequired'] = $lang->docTemplate->docTitleRequired; +$langData['noDocs'] = $lang->docTemplate->noTemplate; +$langData['convertToNewDocConfirm'] = $lang->docTemplate->convertToNewDocConfirm; +$langData['oldDocEditingTip'] = $lang->docTemplate->oldDocEditingTip; +$langData['leaveEditingConfirm'] = $lang->docTemplate->leaveEditingConfirm; +$langData['searchLibPlaceholder'] = $lang->docTemplate->searchScopePlaceholder; +$langData['searchModulePlaceholder'] = $lang->docTemplate->searchTypePlaceholder; +$langData['moveDoc'] = $lang->docTemplate->moveDocTemplate; +$langData['createTypeFirst'] = $lang->docTemplate->createTypeFirst; +$langData['addSubDocTemplate'] = $lang->docTemplate->addSubDocTemplate; +$langData['confirmDeleteChapterWithSub'] = $lang->docTemplate->confirmDeleteChapterWithSub; +$langData['confirmDeleteWithSub'] = $lang->docTemplate->confirmDeleteTemplateWithSub; +$langData['confirmDeleteReviewTemplate'] = $lang->docTemplate->confirmDeleteReviewTemplate; +$langData['needEditable'] = $lang->docTemplate->needEditable; + +$langData['actions']['addModule'] = $lang->docTemplate->addTemplateType; +$langData['actions']['addSameModule'] = $lang->docTemplate->addSameModule; +$langData['actions']['addSubModule'] = $lang->docTemplate->addSubModule; +$langData['actions']['editModule'] = $lang->docTemplate->editModule; +$langData['actions']['deleteModule'] = $lang->docTemplate->deleteModule; + +$viewModeUrl = createLink('doc', 'browsetemplate', 'libID={libID}&type={filterType}&docID={docID}&orderBy={orderBy}&recPerPage={recPerPage}&pageID={page}&mode={mode}'); + +$config->doc->zentaoList = $config->docTemplate->zentaoList; +$lang->doc->zentaoData = $lang->docTemplate->zentaoData; + +docApp +( + set::data($data), + set::spaceID($spaceID), + set::libID($libID), + set::docID($docID), + set::docIcon('file-archive'), + set::noSpace('hidden'), + set::noModule(), + set::moduleIcon('fields'), + set::homeName($lang->doc->template), + set::mode($mode), + set::fetcher(createLink('doc', 'ajaxGetSpaceData', 'type=template&spaceID=1&picks={picks}')), + set::historyFetcher(createLink('action', 'ajaxGetList', 'objectType=docTemplate&objectID={objectID}')), + set::historyPanel(array('commentUrl' => createLink('action', 'comment', "objectType=doctemplate&objectID={$docID}"), 'objectType' => 'doctemplate')), + set::privs($privs), + set::userMap($users), + set::spaceIcon(false), + set::langData($langData), + set::viewModeUrl($viewModeUrl), + set::pager(array('recTotal' => count($templateList), 'recPerPage' => $recPerPage, 'page' => $pageID)), + set('$options', jsRaw('window.setDocAppOptions')), + set::showDocOutline(false), + set::hasModules($hasModules), + set::projectReviewTemplates($projectReviewTemplates) +); + +render(); diff --git a/module/doc/ui/zentaolist.html.php b/module/doc/ui/zentaolist.html.php index 61507468d5..56caf69ffa 100644 --- a/module/doc/ui/zentaolist.html.php +++ b/module/doc/ui/zentaolist.html.php @@ -10,17 +10,46 @@ declare(strict_types=1); */ namespace zin; -jsVar('blockType', $type); -if(strpos(',productStory,ER,UR,planStory,projectStory,',",{$type},") !== false) +if($type == 'gantt' && !empty($ganttData)) { - jsVar('gradeGroup', $gradeGroup); - if($type != 'planStory' && $type != 'projectStory') jsVar('storyType', $storyType); + $userList = array(); + foreach($users as $account => $realname) $userList[] = array('key' => $account, 'label' => $realname); +} + +if(!$isTemplate) +{ + jsVar('blockType', $type); + if(strpos(',productStory,ER,UR,planStory,projectStory,',",{$type},") !== false) + { + jsVar('gradeGroup', $gradeGroup); + if($type != 'planStory' && $type != 'projectStory') jsVar('storyType', $storyType); + } +} + +if($type == 'productCase') +{ + jsVar('scene', $lang->testcase->sceneb); + jsVar('automated', $lang->testcase->automated); + jsVar('noCase', $lang->scene->noCase); + jsVar('caseChanged', $lang->testcase->changed); } $actions = array(); -$actions[] = array('icon' => 'menu-backend', 'text' => $lang->doc->zentaoAction['set'], 'data-toggle' => 'modal', 'url' => str_replace('{blockID}', "$blockID", $settings), 'data-size' => 'lg'); +$setText = (!$isTemplate && $fromTemplate) ? $lang->doc->zentaoAction['setParams'] : $lang->doc->zentaoAction['set']; +if($type != 'gantt' || !$isTemplate) $actions[] = array('icon' => 'menu-backend', 'text' => $setText, 'data-toggle' => 'modal', 'url' => str_replace('{blockID}', "$blockID", $settings), 'data-size' => $isTemplate ? 'sm' : 'lg'); $actions[] = array('icon' => 'trash', 'text' => $lang->doc->zentaoAction['delete'], 'zui-on-click' => "deleteZentaoList($blockID)"); +if($isTemplate || $fromTemplate) +{ + $blockTitle = ''; + if(!empty($lang->docTemplate->searchTabList[$type][$searchTab])) $blockTitle = $lang->docTemplate->searchTabList[$type][$searchTab] . $lang->docTemplate->of; + if($type == 'bug' && $searchTab == 'overduebugs') $blockTitle = $lang->docTemplate->overdue . $lang->docTemplate->of; + if(($type == 'productCase' || $type == 'projectCase') && !empty($caseStage)) $blockTitle = $blockTitle . $lang->testcase->stageList[$caseStage]; +} + +$emptyTip = $lang->doc->previewTip; +if(!$isTemplate && $fromTemplate) $emptyTip = $isSetted ? $lang->docTemplate->emptyTip : $lang->docTemplate->previewTip; + $pagerSetting = usePager(); unset($pagerSetting['linkCreator']); @@ -37,7 +66,7 @@ div h2 ( setClass('font-bold text-xl'), - $lang->doc->zentaoList[$type] . $lang->doc->list + ($isTemplate || $fromTemplate ? $blockTitle . $lang->docTemplate->zentaoList[$type] : $lang->doc->zentaoList[$type]) . ($type == 'gantt' ? '' : $lang->doc->list) ), div ( @@ -51,12 +80,21 @@ div ) ) ), - dtable + $isTemplate || ($type == 'gantt' && empty($ganttData)) ? div + ( + setClass('canvas border rounded py-3 px-3'), + div + ( + setClass('config-tip text-center px-3 py-2'), + $isTemplate ? sprintf($lang->docTemplate->configTip, $type == 'gantt' ? $lang->docTemplate->zentaoList['gantt'] : $lang->doc->list) : $emptyTip + ) + ):null, + !$isTemplate && $type != 'gantt' ? dtable ( set::cols(array_values($cols)), set::data(array_values($data)), set::userMap($users), - set::emptyTip($lang->doc->previewTip), + set::emptyTip($emptyTip), set::checkable(false), set::colResize(true), set::customCols(false), @@ -66,5 +104,15 @@ div set::footer(array('flex', 'pager')), $type == 'productRelease' ? set::plugins(array('cellspan')) : null, $type == 'productRelease' ? set::getCellSpan(jsRaw('window.getCellSpan')) : null - ) + ) : null, + $type == 'gantt' && !empty($ganttData) ? zui::gantt + ( + set::onInit(jsRaw('window.onInitGantt')), + set::data($ganttData['data']), + set::links($ganttData['links']), + set::ganttFields($ganttFields), + set::showFields(explode(',', $showFields)), + set::userList($userList), + set::exts('zentao') + ): null ); diff --git a/module/doc/ui/zentaolist.productbug.html.php b/module/doc/ui/zentaolist.productbug.html.php index 8a743073e1..12940c1fc8 100644 --- a/module/doc/ui/zentaolist.productbug.html.php +++ b/module/doc/ui/zentaolist.productbug.html.php @@ -53,6 +53,5 @@ $fnGenerateFormRows = function () use ($lang, $settings, $fnGenerateCustomSearch ) ), $fnGenerateCustomSearch($searchConfig) - ); }; diff --git a/module/doc/zen.php b/module/doc/zen.php index 0811ea4e94..95db5ff887 100644 --- a/module/doc/zen.php +++ b/module/doc/zen.php @@ -376,13 +376,13 @@ class docZen extends doc * @access protected * @return void */ - protected function responseAfterCreate(array $docResult) + protected function responseAfterCreate(array $docResult, string $objectType = 'doc') { $docID = $docResult['id']; $files = zget($docResult, 'files', ''); $fileAction = ''; - if(!empty($files)) $fileAction = $this->lang->addFiles . join(',', $files) . "\n"; + if(!empty($files)) $fileAction = $this->lang->addFiles . implode(',', $files) . "\n"; $this->action->create('doc', $docID, 'Created', $fileAction, '', '', false); @@ -391,7 +391,7 @@ class docZen extends doc $response = array( 'result' => 'success', 'message' => $this->lang->saveSuccess, - 'load' => $this->createLink('doc', 'view', "docID={$docResult['id']}"), + 'load' => $this->createLink('doc', $objectType == 'doc' ? 'view' : 'browseTemplate', "docID={$docResult['id']}"), 'id' => $docID, 'doc' => $docResult ); @@ -421,10 +421,11 @@ class docZen extends doc if($spaceTypeChanged) { - if($spaceType == 'mine') $locateLink = $this->createLink('doc', 'mySpace', "objectID={$spaceID}&libID={$libID}"); - elseif($spaceType == 'custom') $locateLink = $this->createLink('doc', 'teamSpace', "objectID={$spaceID}&libID={$libID}"); - elseif($spaceType == 'product') $locateLink = $this->createLink('doc', 'productSpace', "objectID={$spaceID}&libID={$libID}"); - elseif($spaceType == 'project') $locateLink = $this->createLink('doc', 'projectSpace', "objectID={$spaceID}&libID={$libID}"); + $method = 'mySpace'; + if($spaceType == 'custom') $method = 'teamSpace'; + if($spaceType == 'product') $method = 'productSpace'; + if($spaceType == 'project') $method = 'projectSpace'; + $locateLink = $this->createLink('doc', $method, "objectID={$spaceID}&libID={$libID}"); return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'closeModal' => true, 'load' => $locateLink)); } else @@ -504,7 +505,7 @@ class docZen extends doc $files = zget($docResult, 'files', ''); $fileAction = ''; - if(!empty($files)) $fileAction = $this->lang->addFiles . join(',', $files) . "\n"; + if(!empty($files)) $fileAction = $this->lang->addFiles . implode(',', $files) . "\n"; $this->action->create('doc', $docID, 'created', $fileAction); if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'id' => $docID)); @@ -697,6 +698,41 @@ class docZen extends doc return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => $link, 'doc' => $doc)); } + /** + * 在编辑文档后的返回。 + * Return after edit a document. + * + * @param object $doc + * @param array $changes + * @param array $files + * @access protected + * @return void + */ + protected function responseAfterEditTemplate(object $doc, array $changes = array(), array $files = array()) + { + if($this->post->comment != '' || !empty($changes) || !empty($files)) + { + $action = 'Commented'; + if(!empty($changes)) + { + $newType = $_POST['status']; + if($doc->status == 'draft' && $newType == 'normal') $action = 'releasedDoc'; + if($doc->status == 'normal' && $newType == 'draft') $action = 'savedDraft'; + if($doc->status == $newType) $action = 'Edited'; + } + + $fileAction = ''; + if(!empty($files)) $fileAction = $this->lang->addFiles . implode(',', $files) . "\n"; + $actionID = $this->action->create('docTemplate', $doc->id, $action, $fileAction . $this->post->comment); + if(!empty($changes) && !empty($actionID)) $this->action->logHistory($actionID, $changes); + } + + $link = $this->createLink('doc', 'view', "docID={$doc->id}"); + $doc = $this->doc->getByID($doc->id); + if(isInModal()) return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true)); + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => $link, 'doc' => $doc)); + } + /** * 处理文档大纲。 * @@ -1024,6 +1060,20 @@ class docZen extends doc } } + /** + * 在创建版本类型后的返回。 + * Return after create a template type. + * + * @param int $scope + * @access public + * @return string + */ + public function responseAfterAddTemplateType(int $scope) + { + $response = array('result' => 'success', 'message' => $this->lang->saveSuccess, 'load' => true); + return $this->send($response); + } + /** * 从session中获取数据。 * Get data from session. @@ -1603,8 +1653,8 @@ class docZen extends doc protected function exportZentaoList(object $blockData): string { $users = $this->loadModel('user')->getPairs('noletter|pofirst|nodeleted'); - $cols = $blockData->content->cols; - $data = $blockData->content->data; + $cols = zget($blockData->content, 'cols', array()); + $data = zget($blockData->content, 'data', array()); $list = array(); $list[] = array('type' => 'heading', 'props' => array('depth' => 5, 'text' => $blockData->title)); @@ -1660,6 +1710,56 @@ class docZen extends doc } /** + * 处理发布列表展示数据。 + * Process release list display data. + * + * @param array $releaseList + * @param array $childReleases + * @access protected + * @return array + */ + protected function processReleaseListData(array $releaseList, array $childReleases): array + { + $releases = array(); + foreach($releaseList as $release) + { + $release->rowID = $release->id; + $release->rowspan = count($release->builds); + + if(!empty($release->builds)) + { + foreach($release->builds as $build) + { + $releaseInfo = clone $release; + $releaseInfo->build = $build; + + $releases[] = $releaseInfo; + } + } + else + { + $releases[] = $release; + } + if(empty($release->releases)) continue; + + foreach(explode(',', $release->releases) as $childID) + { + if(isset($childReleases[$childID])) + { + $child = clone $childReleases[$childID]; + $child = current($this->processReleaseListData(array($child))); + + $child->rowID = "{$release->id}-{$childID}"; + $child->parent = $release->id; + $releases[$child->rowID] = $child; + } + } + } + + return $releases; + } + + /* * Get authorized subdocuments through recursion. * 递归获取有权限的子文档。 * diff --git a/module/execution/control.php b/module/execution/control.php index fa75430969..ea79720c68 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -356,7 +356,7 @@ class execution extends control $pager = new pager(count($tasks2Imported), $recPerPage, $pageID); $tasks2ImportedList = array_chunk($tasks2Imported, $pager->recPerPage, true); - $tasks2ImportedList = empty($tasks2ImportedList) ? $tasks2ImportedList : (isset($tasks2ImportedList[$pageID - 1]) ? $tasks2ImportedList[$pageID - 1] : current($tasks2ImportedList)); + if(!empty($tasks2ImportedList)) $tasks2ImportedList = isset($tasks2ImportedList[$pageID - 1]) ? $tasks2ImportedList[$pageID - 1] : current($tasks2ImportedList); $tasks2ImportedList = $this->loadModel('task')->processTasks($tasks2ImportedList); $this->view->title = $execution->name . $this->lang->hyphen . $this->lang->execution->importTask; @@ -975,12 +975,14 @@ class execution extends control } $burn = $this->execution->getBurnByExecution($executionID, $execution->begin, 0); + $left = empty($burn) ? 0 : $burn->left; $withLeft = $this->post->withLeft ? $this->post->withLeft : 0; + if($withLeft) $left = $this->post->estimate; $burnData = form::data($this->config->execution->form->fixfirst) ->add('task', 0) ->add('execution', $executionID) ->add('date', $execution->begin) - ->add('left', $withLeft ? $this->post->estimate : (empty($burn) ? 0 : $burn->left)) + ->add('left', $left) ->add('consumed', empty($burn) ? 0 : $burn->consumed) ->get(); @@ -3498,7 +3500,7 @@ class execution extends control $this->loadModel('task'); $result = array(); - foreach($array as $key => $object) + foreach($array as $object) { $result[$object->id] = $object; $tasks = zget($object, 'tasks', array()); diff --git a/module/execution/model.php b/module/execution/model.php index 7e6f0fd893..ba1eb0ac56 100755 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -1945,18 +1945,18 @@ class executionModel extends model * 根据产品/执行等信息获取任务列表 * Get tasks by product/execution etc. * - * @param int $productID - * @param int $executionID - * @param array $executions - * @param string $browseType - * @param int $queryID - * @param int $moduleID - * @param string $sort - * @param object $pager + * @param int $productID + * @param int|array $executionID + * @param array $executions + * @param string $browseType + * @param int $queryID + * @param int $moduleID + * @param string $sort + * @param object $pager * @access public * @return array */ - public function getTasks(int $productID, int $executionID, array $executions, string $browseType, int $queryID, int $moduleID, string $sort, object $pager = null): array + public function getTasks(int $productID, int|array $executionID, array $executions, string $browseType, int $queryID, int $moduleID, string $sort, object $pager = null): array { if(common::isTutorialMode()) return $this->loadModel('tutorial')->getTasks(); @@ -5349,9 +5349,8 @@ class executionModel extends model $toList = $ccList = ''; $toList = array_merge(array_keys($teamMembers), $whitelist); - $toList = implode(',', array_keys($teamMembers)); - return array($toList, $ccList); + return array(implode(',', $toList), $ccList); } /** diff --git a/module/execution/ui/story.html.php b/module/execution/ui/story.html.php index dfd2b30446..c9886a14ad 100644 --- a/module/execution/ui/story.html.php +++ b/module/execution/ui/story.html.php @@ -539,6 +539,7 @@ foreach($setting as $key => $col) if(isset($col['link'])) unset($col['link']); if($key == 'assignedTo') $col['type'] = 'user'; if($key == 'pri') $col['priList'] = $lang->story->priList; + if($key == 'title') $col['link'] = array('url' => createLink('{type}', 'view', "storyID={id}&version={version}"), 'data-toggle' => 'modal', 'data-size' => 'lg'); } $cols[] = $col; @@ -589,7 +590,7 @@ dtable ( 'recPerPage' => $pager->recPerPage, 'recTotal' => $pager->recTotal, - 'linkCreator' => helper::createLink('execution', 'story', "executionID={$execution->id}&storyType={$storyType}&orderBy=$orderBy&type={$type}¶m={$param}&recTotal={recTotal}&recPerPage={recPerPage}&page={page}") . "#app={$app->tab}" + 'linkCreator' => helper::createLink('execution', 'story', "executionID={$execution->id}&storyType={$storyType}&orderBy=$orderBy&type={$type}¶m={$param}&recTotal={recTotal}&recPerPage={recPerPage}&page={page}&from={$from}&blockID={$blockID}") . "#app={$app->tab}" ))), set::emptyTip($lang->execution->noStory), !$isFromDoc ? null : set::afterRender(jsCallback()->call('toggleCheckRows', $idList)), diff --git a/module/execution/ui/task.html.php b/module/execution/ui/task.html.php index 4af3cd6ed8..693534ab50 100644 --- a/module/execution/ui/task.html.php +++ b/module/execution/ui/task.html.php @@ -126,6 +126,7 @@ if($isFromDoc) if(isset($col['link'])) unset($cols[$key]['link']); if($key == 'assignedTo') $cols[$key]['type'] = 'user'; if($key == 'pri') $cols[$key]['priList'] = $lang->task->priList; + if($key == 'name') $cols[$key]['link'] = array('url' => createLink('task', 'view', "taskID={id}"), 'data-toggle' => 'modal', 'data-size' => 'lg'); } } diff --git a/module/group/lang/de.php b/module/group/lang/de.php index e28d4ef1ba..37bc9e5bd6 100644 --- a/module/group/lang/de.php +++ b/module/group/lang/de.php @@ -556,6 +556,9 @@ $lang->group->package->browseDoc = 'Browse Doc'; $lang->group->package->manageDoc = 'Manage Doc'; $lang->group->package->deleteDoc = 'Delete Doc'; $lang->group->package->exportDoc = 'Export Doc'; +$lang->group->package->browseDoctemplate = 'Browse Doc Template'; +$lang->group->package->manageDoctemplate = 'Manage Doc Template'; +$lang->group->package->deleteDoctemplate = 'Delete Doc Template'; $lang->group->package->browseAPI = 'Browse API'; $lang->group->package->manageAPI = 'Manage API'; $lang->group->package->exportAPI = 'Export API'; diff --git a/module/group/lang/en.php b/module/group/lang/en.php index 6596e6e715..aef6d18974 100644 --- a/module/group/lang/en.php +++ b/module/group/lang/en.php @@ -555,6 +555,9 @@ $lang->group->package->manageMetric = 'Manage Metric'; $lang->group->package->browseDoc = 'Browse Doc'; $lang->group->package->manageDoc = 'Manage Doc'; $lang->group->package->deleteDoc = 'Delete Doc'; +$lang->group->package->browseDoctemplate = 'Browse Doc Template'; +$lang->group->package->manageDoctemplate = 'Manage Doc Template'; +$lang->group->package->deleteDoctemplate = 'Delete Doc Template'; $lang->group->package->exportDoc = 'Export Doc'; $lang->group->package->browseAPI = 'Browse API'; $lang->group->package->manageAPI = 'Manage API'; diff --git a/module/group/lang/fr.php b/module/group/lang/fr.php index 81ca40a5b7..b6720d1467 100644 --- a/module/group/lang/fr.php +++ b/module/group/lang/fr.php @@ -555,6 +555,9 @@ $lang->group->package->manageMetric = 'Manage Metric'; $lang->group->package->browseDoc = 'Browse Doc'; $lang->group->package->manageDoc = 'Manage Doc'; $lang->group->package->deleteDoc = 'Delete Doc'; +$lang->group->package->browseDoctemplate = 'Browse Doc Template'; +$lang->group->package->manageDoctemplate = 'Manage Doc Template'; +$lang->group->package->deleteDoctemplate = 'Delete Doc Template'; $lang->group->package->exportDoc = 'Export Doc'; $lang->group->package->browseAPI = 'Browse API'; $lang->group->package->manageAPI = 'Manage API'; diff --git a/module/group/lang/resource.php b/module/group/lang/resource.php index 9d005d006f..44dfcb7225 100644 --- a/module/group/lang/resource.php +++ b/module/group/lang/resource.php @@ -1509,40 +1509,40 @@ $lang->resource->system->inactive = 'inactive'; /* Doc. */ $lang->resource->doc = new stdclass(); -$lang->resource->doc->index = 'index'; -$lang->resource->doc->mySpace = 'mySpace'; -$lang->resource->doc->quick = 'quick'; -$lang->resource->doc->createSpace = 'createSpace'; -$lang->resource->doc->createLib = 'createLibAction'; -$lang->resource->doc->editSpace = 'editSpaceAction'; -$lang->resource->doc->editLib = 'editLibAction'; -$lang->resource->doc->deleteSpace = 'deleteSpaceAction'; -$lang->resource->doc->deleteLib = 'deleteLibAction'; -$lang->resource->doc->moveLib = 'moveLibAction'; -$lang->resource->doc->moveDoc = 'moveDocAction'; -$lang->resource->doc->batchMoveDoc = 'batchMoveDocAction'; -$lang->resource->doc->create = 'createOrUpload'; -$lang->resource->doc->edit = 'edit'; -$lang->resource->doc->view = 'view'; -$lang->resource->doc->delete = 'delete'; -$lang->resource->doc->deleteFile = 'deleteFile'; -$lang->resource->doc->collect = 'collectAction'; -$lang->resource->doc->productSpace = 'productSpace'; -$lang->resource->doc->projectSpace = 'projectSpace'; -$lang->resource->doc->teamSpace = 'teamSpace'; -$lang->resource->doc->showFiles = 'showFiles'; -$lang->resource->doc->addCatalog = 'addCatalog'; -$lang->resource->doc->editCatalog = 'editCatalog'; -$lang->resource->doc->sortDoclib = 'sortDoclib'; -$lang->resource->doc->sortCatalog = 'sortCatalog'; -$lang->resource->doc->sortDoc = 'sortDoc'; -$lang->resource->doc->deleteCatalog = 'deleteCatalog'; +$lang->resource->doc->index = 'index'; +$lang->resource->doc->mySpace = 'mySpace'; +$lang->resource->doc->quick = 'quick'; +$lang->resource->doc->createSpace = 'createSpace'; +$lang->resource->doc->createLib = 'createLibAction'; +$lang->resource->doc->editSpace = 'editSpaceAction'; +$lang->resource->doc->editLib = 'editLibAction'; +$lang->resource->doc->deleteSpace = 'deleteSpaceAction'; +$lang->resource->doc->deleteLib = 'deleteLibAction'; +$lang->resource->doc->moveLib = 'moveLibAction'; +$lang->resource->doc->moveDoc = 'moveDocAction'; +$lang->resource->doc->batchMoveDoc = 'batchMoveDocAction'; +$lang->resource->doc->create = 'createOrUpload'; +$lang->resource->doc->edit = 'edit'; +$lang->resource->doc->view = 'view'; +$lang->resource->doc->delete = 'delete'; +$lang->resource->doc->deleteFile = 'deleteFile'; +$lang->resource->doc->collect = 'collectAction'; +$lang->resource->doc->productSpace = 'productSpace'; +$lang->resource->doc->projectSpace = 'projectSpace'; +$lang->resource->doc->teamSpace = 'teamSpace'; +$lang->resource->doc->showFiles = 'showFiles'; +$lang->resource->doc->addCatalog = 'addCatalog'; +$lang->resource->doc->editCatalog = 'editCatalog'; +$lang->resource->doc->sortDoclib = 'sortDoclib'; +$lang->resource->doc->sortCatalog = 'sortCatalog'; +$lang->resource->doc->sortDoc = 'sortDoc'; +$lang->resource->doc->deleteCatalog = 'deleteCatalog'; // $lang->resource->doc->displaySetting = 'displaySetting'; -$lang->resource->doc->exportFiles = 'exportFiles'; -$lang->resource->doc->addChapter = 'addChapter'; -$lang->resource->doc->editChapter = 'editChapter'; -$lang->resource->doc->sortChapter = 'sortChapter'; -$lang->resource->doc->deleteChapter = 'deleteChapter'; +$lang->resource->doc->exportFiles = 'exportFiles'; +$lang->resource->doc->addChapter = 'addChapter'; +$lang->resource->doc->editChapter = 'editChapter'; +$lang->resource->doc->sortChapter = 'sortChapter'; +$lang->resource->doc->deleteChapter = 'deleteChapter'; $lang->doc->methodOrder[5] = 'index'; $lang->doc->methodOrder[10] = 'mySpace'; diff --git a/module/group/lang/zh-cn.php b/module/group/lang/zh-cn.php index f1e9e210f0..086ceb632e 100644 --- a/module/group/lang/zh-cn.php +++ b/module/group/lang/zh-cn.php @@ -481,7 +481,7 @@ $lang->group->package->comment = '备注'; $lang->group->package->module = '模块维护'; $lang->group->package->file = '附件'; $lang->group->package->commonEffort = '通用日志'; -$lang->group->package->docTemplate = '文档模板'; +$lang->group->package->docTemplate = '浏览文档模板'; $lang->group->package->importStoryLib = '导入需求库'; $lang->group->package->projectStakeholder = '项目干系人'; $lang->group->package->projectBuild = '项目构建'; @@ -556,6 +556,9 @@ $lang->group->package->browseDoc = '浏览文档'; $lang->group->package->manageDoc = '创建与维护文档'; $lang->group->package->deleteDoc = '删除文档'; $lang->group->package->exportDoc = '导出文档'; +$lang->group->package->browseDoctemplate = '浏览文档模板'; +$lang->group->package->manageDoctemplate = '创建与维护文档模板'; +$lang->group->package->deleteDoctemplate = '删除文档模板'; $lang->group->package->browseAPI = '浏览接口空间'; $lang->group->package->manageAPI = '创建维护接口'; $lang->group->package->exportAPI = '导出接口'; diff --git a/module/group/packagemanager.php b/module/group/packagemanager.php index e44a8e5e3d..64e523a861 100644 --- a/module/group/packagemanager.php +++ b/module/group/packagemanager.php @@ -355,7 +355,7 @@ $config->group->subset->featureconfig->nav = 'admin'; $config->group->subset->doctemplate = new stdclass(); $config->group->subset->doctemplate->order = 1220; -$config->group->subset->doctemplate->nav = 'admin'; +$config->group->subset->doctemplate->nav = 'doc'; $config->group->subset->notifysetting = new stdclass(); $config->group->subset->notifysetting->order = 1230; @@ -2965,18 +2965,31 @@ $config->group->package->commonEffort->subset = 'generaleffort'; $config->group->package->commonEffort->privs = array(); $config->group->package->commonEffort->privs['effort-createForObject'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 3, 'depend' => array(), 'recommend' => array('effort-delete', 'effort-edit')); -$config->group->package->docTemplate = new stdclass(); -$config->group->package->docTemplate->order = 5; -$config->group->package->docTemplate->subset = 'doctemplate'; -$config->group->package->docTemplate->privs = array(); -$config->group->package->docTemplate->privs['baseline-templateType'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 0, 'depend' => array(), 'recommend' => array()); -$config->group->package->docTemplate->privs['baseline-template'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 1, 'depend' => array(), 'recommend' => array()); -$config->group->package->docTemplate->privs['baseline-createTemplate'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 3, 'depend' => array('baseline-template'), 'recommend' => array('baseline-delete', 'baseline-editBook', 'baseline-editTemplate', 'baseline-manageBook')); -$config->group->package->docTemplate->privs['baseline-view'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 2, 'depend' => array('baseline-template'), 'recommend' => array()); -$config->group->package->docTemplate->privs['baseline-editTemplate'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 4, 'depend' => array('baseline-template'), 'recommend' => array('baseline-createTemplate', 'baseline-delete', 'baseline-editBook', 'baseline-manageBook')); -$config->group->package->docTemplate->privs['baseline-editBook'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 5, 'depend' => array('baseline-template'), 'recommend' => array('baseline-createTemplate', 'baseline-delete', 'baseline-editTemplate', 'baseline-manageBook')); -$config->group->package->docTemplate->privs['baseline-manageBook'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 7, 'depend' => array('baseline-template'), 'recommend' => array('baseline-createTemplate', 'baseline-delete', 'baseline-editBook', 'baseline-editTemplate')); -$config->group->package->docTemplate->privs['baseline-delete'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 6, 'depend' => array('baseline-template'), 'recommend' => array('baseline-createTemplate')); +$config->group->package->browseDoctemplate = new stdclass(); +$config->group->package->browseDoctemplate->order = 5; +$config->group->package->browseDoctemplate->subset = 'doctemplate'; +$config->group->package->browseDoctemplate->privs = array(); +$config->group->package->browseDoctemplate->privs['doc-browseTemplate'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 0, 'depend' => array(), 'recommend' => array('doc-viewTemplate')); +$config->group->package->browseDoctemplate->privs['doc-viewTemplate'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 0, 'depend' => array('doc-browseTemplate'), 'recommend' => array('doc-createTemplate', 'doc-editTemplate', 'doc-moveTemplate')); + +$config->group->package->manageDoctemplate = new stdclass(); +$config->group->package->manageDoctemplate->order = 15; +$config->group->package->manageDoctemplate->subset = 'doctemplate'; +$config->group->package->manageDoctemplate->privs = array(); +$config->group->package->manageDoctemplate->privs['doc-createTemplate'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 0, 'depend' => array('doc-browseTemplate', 'doc-viewTemplate'), 'recommend' => array()); +$config->group->package->manageDoctemplate->privs['doc-editTemplate'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 0, 'depend' => array('doc-browseTemplate', 'doc-viewTemplate'), 'recommend' => array('doc-createTemplate', 'doc-moveTemplate')); +$config->group->package->manageDoctemplate->privs['doc-moveTemplate'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 0, 'depend' => array('doc-browseTemplate', 'doc-viewTemplate'), 'recommend' => array('doc-createTemplate', 'doc-editTemplate')); +$config->group->package->manageDoctemplate->privs['doc-addTemplateType'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 0, 'depend' => array('doc-browseTemplate', 'doc-editTemplateType'), 'recommend' => array('doc-deleteTemplateType')); +$config->group->package->manageDoctemplate->privs['doc-editTemplateType'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 0, 'depend' => array('doc-browseTemplate', 'doc-addTemplateType'), 'recommend' => array('doc-deleteTemplateType')); +$config->group->package->manageDoctemplate->privs['doc-sortTemplate'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 0, 'depend' => array('doc-browseTemplate'), 'recommend' => array()); +$config->group->package->manageDoctemplate->privs['doc-manageScope'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 0, 'depend' => array(), 'recommend' => array()); + +$config->group->package->deleteDoctemplate = new stdclass(); +$config->group->package->deleteDoctemplate->order = 20; +$config->group->package->deleteDoctemplate->subset = 'doctemplate'; +$config->group->package->deleteDoctemplate->privs = array(); +$config->group->package->deleteDoctemplate->privs['doc-deleteTemplate'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 0, 'depend' => array('doc-browseTemplate'), 'recommend' => array()); +$config->group->package->deleteDoctemplate->privs['doc-deleteTemplateType'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 0, 'depend' => array('doc-browseTemplate', 'doc-addTemplateType', 'doc-editTemplateType'), 'recommend' => array('doc-addTemplateType', 'doc-editTemplateType')); $config->group->package->projectStakeholder = new stdclass(); $config->group->package->projectStakeholder->order = 5; diff --git a/module/metric/test/tao/fetchmetricsbyscope.php b/module/metric/test/tao/fetchmetricsbyscope.php index 65306c107e..0947407a30 100755 --- a/module/metric/test/tao/fetchmetricsbyscope.php +++ b/module/metric/test/tao/fetchmetricsbyscope.php @@ -8,15 +8,15 @@ timeout=0 cid=1 - 获取系统范围下的第一个度量项 - - 第0条的id属性 @1 - - 第0条的purpose属性 @scale - - 第0条的code属性 @count_of_program + - 第system条的id属性 @143 + - 第system条的purpose属性 @rate + - 第system条的code属性 @count_of_pending_deployment - 获取产品范围下的第一个度量项 - - 第0条的id属性 @143 - - 第0条的purpose属性 @scale - - 第0条的code属性 @count_of_productplan_in_product -- 获取系统度量项的个数 @143 -- 获取系统度量项目的个数,限制limit @2 + - 第product条的id属性 @316 + - 第product条的purpose属性 @scale + - 第product条的code属性 @scale_of_developed_story_in_product +- 获取系统度量项的个数 @1 +- 获取系统度量项目的个数,限制limit @1 */ include dirname(__FILE__, 5) . '/test/lib/init.php'; @@ -24,7 +24,7 @@ include dirname(__FILE__, 2) . '/lib/calc.unittest.class.php'; $metric = new metricTest(); -r($metric->fetchMetricsByScope('system')) && p('0:id,purpose,code') && e('1,scale,count_of_program'); // 获取系统范围下的第一个度量项 -r($metric->fetchMetricsByScope('product')) && p('0:id,purpose,code') && e('144,scale,count_of_productplan_in_product'); // 获取产品范围下的第一个度量项 -r(count($metric->fetchMetricsByScope('system'))) && p() && e(143); // 获取系统度量项的个数 -r(count($metric->fetchMetricsByScope('system', 2))) && p() && e(2); // 获取系统度量项目的个数,限制limit +r($metric->fetchMetricsByScope('system')) && p('system:id,purpose,code') && e('143,rate,count_of_pending_deployment'); // 获取系统范围下的第一个度量项 +r($metric->fetchMetricsByScope('product')) && p('product:id,purpose,code') && e('316,scale,scale_of_developed_story_in_product'); // 获取产品范围下的第一个度量项 +r(count($metric->fetchMetricsByScope('system'))) && p() && e(1); // 获取系统度量项的个数 +r(count($metric->fetchMetricsByScope('system', 2))) && p() && e(1); // 获取系统度量项目的个数,限制limit diff --git a/module/product/model.php b/module/product/model.php index 88620d1a14..fc65cee63c 100755 --- a/module/product/model.php +++ b/module/product/model.php @@ -115,17 +115,18 @@ class productModel extends model * 根据项目获取关联的产品键值对列表。 * Get product pairs by project. * - * @param int $projectID + * @param int|array $projectID * @param string $status all|noclosed * @param string|array $append * @param bool $noDeleted + * @param bool $noShadow * @access public * @return array */ - public function getProductPairsByProject(int $projectID = 0, string $status = 'all', string $append = '', bool $noDeleted = true): array + public function getProductPairsByProject(int|array $projectID = 0, string $status = 'all', string $append = '', bool $noDeleted = true, bool $noShadow = false): array { if(empty($projectID)) return array(); - return $this->getProducts($projectID, $status, '', false, $append, $noDeleted); + return $this->getProducts($projectID, $status, '', false, $append, $noDeleted, $noShadow); } /** @@ -152,16 +153,17 @@ class productModel extends model * 获取关联项目的产品。 * Get products by project. * - * @param string|int $projectID + * @param int|array $projectID * @param string $status all|noclosed * @param string $orderBy * @param bool $withBranch * @param string|array $append * @param bool $noDeleted + * @param bool $noShadow * @access public * @return int[] */ - public function getProducts(int $projectID = 0, string $status = 'all', string $orderBy = '', bool $withBranch = true, string|array $append = '', bool $noDeleted = true): array + public function getProducts(int|array $projectID = 0, string $status = 'all', string $orderBy = '', bool $withBranch = true, string|array $append = '', bool $noDeleted = true, bool $noShadow = false): array { /* 如果是新手教程模式,直接返回测试数据。*/ if(commonModel::isTutorialMode()) @@ -172,7 +174,7 @@ class productModel extends model } /* 初始化变量。 */ - $projectProducts = $this->productTao->getProductsByProjectID($projectID, $append, $status, $orderBy, $noDeleted); + $projectProducts = $this->productTao->getProductsByProjectID($projectID, $append, $status, $orderBy, $noDeleted, $noShadow); $products = array(); /* 如果不返回分支信息,则返回 id=>name 的键值对。 */ @@ -638,18 +640,18 @@ class productModel extends model * 获取需求列表。 * Get stories. * - * @param int $productID - * @param string $branch - * @param string $browseType bymodule|unclosed|allstory|assignedtome|openedbyme|reviewbyme|draftstory|reviewedbyme|assignedbyme|closedbyme|activestory|changingstory|reviewingstory|willclose|closedstory - * @param int $queryID - * @param int $moduleID - * @param string $type requirement|story - * @param string $sort - * @param object $pager + * @param int|array $productID + * @param string $branch + * @param string $browseType bymodule|unclosed|allstory|assignedtome|openedbyme|reviewbyme|draftstory|reviewedbyme|assignedbyme|closedbyme|activestory|changingstory|reviewingstory|willclose|closedstory + * @param int $queryID + * @param int $moduleID + * @param string $type requirement|story + * @param string $sort + * @param object $pager * @access public * @return array */ - public function getStories(int $productID, string $branch, string $browseType, int $queryID, int $moduleID, string $type = 'story', string $sort = 'id_desc', object|null$pager = null): array + public function getStories(int|array $productID, string $branch, string $browseType, int $queryID, int $moduleID, string $type = 'story', string $sort = 'id_desc', object|null$pager = null): array { if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getStories(); @@ -2171,7 +2173,6 @@ class productModel extends model public function getSwitcher(int $productID = 0, string $extra = ''): string { /* 获取产品名称,产品类型。 */ - $currentProduct = new stdclass(); $currentProductName = $this->lang->productCommon; if($productID) { diff --git a/module/product/tao.php b/module/product/tao.php index 7b0ab84e06..f29673e758 100644 --- a/module/product/tao.php +++ b/module/product/tao.php @@ -452,15 +452,16 @@ class productTao extends productModel * 获取项目关联的产品。 * Get products by project ID. * - * @param int $projectID + * @param int|array $projectID * @param string|array $append '1,2,3' * @param string $status * @param string $orderBy * @param bool $withDeleted + * @param bool $noShadow * @access protected * @return int[] */ - protected function getProductsByProjectID(int $projectID, string|array $append, string $status, string $orderBy, bool $withDeleted = false): array + protected function getProductsByProjectID(int|array $projectID, string|array $append, string $status, string $orderBy, bool $withDeleted = false, bool $noShadow = false): array { /* 处理要用的到变量信息。 */ $append = $this->formatAppendParam($append); @@ -470,14 +471,15 @@ class productTao extends productModel ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id') ->where("(FIND_IN_SET('{$this->config->vision}', t2.vision)") ->beginIF(!$withDeleted)->andWhere('t2.deleted')->eq(0)->fi() - ->beginIF(!empty($projectID))->andWhere('t1.project')->eq($projectID)->fi() + ->beginIF($noShadow)->andWhere('t2.shadow')->eq(0)->fi() + ->beginIF(!empty($projectID))->andWhere('t1.project')->in($projectID)->fi() ->beginIF(!$this->app->user->admin and $this->config->vision != 'lite')->andWhere('t2.id')->in($this->app->user->view->products)->fi() ->beginIF(strpos($status, 'noclosed') !== false)->andWhere('t2.status')->ne('closed')->fi() ->markRight(1) ->beginIF($append && empty($projectID))->orWhere('(t1.product')->in($append)->fi() ->beginIF($append && !empty($projectID)) ->orWhere('(t1.product')->in($append) - ->andWhere('t1.project')->eq($projectID) + ->andWhere('t1.project')->in($projectID) ->markRight(1) ->fi() ->orderBy($orderBy) diff --git a/module/product/test/lib/product.unittest.class.php b/module/product/test/lib/product.unittest.class.php index 0e34765085..6921a570e1 100644 --- a/module/product/test/lib/product.unittest.class.php +++ b/module/product/test/lib/product.unittest.class.php @@ -1342,4 +1342,21 @@ class productTest return $this->objectModel->getProducts(); } + + /* + * 获取1.5级导航数据。 + * Get product switcher. + * + * @param int $productID + * @access public + * @return string + */ + public function getSwitcherTest(int $productID = 0): string + { + $productName = $this->objectModel->dao->select('name')->from(TABLE_PRODUCT)->where('id')->eq($productID)->fetch('name'); + $output = $this->objectModel->getSwitcher($productID); + + if(!$output) return false; + return strpos($output, $productName) !== false; + } } diff --git a/module/product/test/model/getswitcher.php b/module/product/test/model/getswitcher.php new file mode 100644 index 0000000000..aa83fce44f --- /dev/null +++ b/module/product/test/model/getswitcher.php @@ -0,0 +1,29 @@ +#!/usr/bin/env php +getSwitcher(); +timeout=0 +cid=1 + +- 测试设置产品1的1.5级下拉菜单 @1 +- 测试设置产品2的1.5级下拉菜单 @1 +- 测试设置产品3的1.5级下拉菜单 @1 +- 测试设置产品4的1.5级下拉菜单 @1 +- 测试设置产品5的1.5级下拉菜单 @1 + +*/ + +su('admin'); +zenData('product')->loadYaml('product')->gen(10); + +$productIdList = range(1, 5); +$productTester = new productTest(); +r($productTester->getSwitcherTest($productIdList[0])) && p() && e('1'); // 测试设置产品1的1.5级下拉菜单 +r($productTester->getSwitcherTest($productIdList[1])) && p() && e('1'); // 测试设置产品2的1.5级下拉菜单 +r($productTester->getSwitcherTest($productIdList[2])) && p() && e('1'); // 测试设置产品3的1.5级下拉菜单 +r($productTester->getSwitcherTest($productIdList[3])) && p() && e('1'); // 测试设置产品4的1.5级下拉菜单 +r($productTester->getSwitcherTest($productIdList[4])) && p() && e('1'); // 测试设置产品5的1.5级下拉菜单 diff --git a/module/product/ui/browse.html.php b/module/product/ui/browse.html.php index e28788fb28..de83057d6a 100644 --- a/module/product/ui/browse.html.php +++ b/module/product/ui/browse.html.php @@ -230,6 +230,7 @@ if($isFromDoc) if(isset($col['link'])) unset($setting[$key]['link']); if($key == 'assignedTo') $setting[$key]['type'] = 'user'; if($key == 'pri') $setting[$key]['priList'] = $lang->story->priList; + if($key == 'title') $setting[$key]['link'] = array('url' => createLink('{type}', 'view', 'storyID={id}&version={version}'), 'data-toggle' => 'modal', 'data-size' => 'lg'); } } diff --git a/module/productplan/model.php b/module/productplan/model.php index 9e845de41b..9b4f0f84bc 100644 --- a/module/productplan/model.php +++ b/module/productplan/model.php @@ -224,8 +224,8 @@ class productplanModel extends model $plans = $this->dao->select('t1.id,t1.title,t1.parent,t1.begin,t1.end,t2.type as productType,t1.branch')->from(TABLE_PRODUCTPLAN)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t2.id=t1.product') - ->where('t1.product')->in($productIdList) - ->andWhere('t1.deleted')->eq(0) + ->where('t1.deleted')->eq('0') + ->beginIF($productIdList)->andWhere('t1.product')->in($productIdList)->fi() ->beginIF(!empty($branchQuery))->andWhere($branchQuery)->fi() ->beginIF(strpos($param, 'unexpired') !== false)->andWhere('t1.end')->ge(date('Y-m-d'))->fi() ->beginIF(strpos($param, 'noclosed') !== false)->andWhere('t1.status')->ne('closed')->fi() @@ -1092,7 +1092,9 @@ class productplanModel extends model */ public static function isClickable(object $plan, string $action): bool { - switch(strtolower($action)) + $action = strtolower($action); + + switch($action) { case 'create': return $plan->parent <= 0 && $plan->status != 'done' && $plan->status != 'closed'; @@ -1109,7 +1111,7 @@ class productplanModel extends model case 'linkbug': return !$plan->isParent; case 'createexecution': - if($plan->isParent || $plan->expired || $plan->status == 'done' || $plan->status == 'closed' || !common::hasPriv('execution', 'create', $plan)) return false; + if($plan->isParent || $plan->expired || in_array($plan->status, array('done', 'closed')) || !common::hasPriv('execution', 'create', $plan)) return false; static $cache = null; if(is_null($cache)) @@ -1119,8 +1121,9 @@ class productplanModel extends model $cache['branches'] = $app->dao->select('id')->from(TABLE_BRANCH)->where('deleted')->eq('0')->andWhere('status')->eq('closed')->fetchPairs(); } return !isset($cache['products'][$plan->product]) || !isset($cache['branches'][$plan->branch]); + default: + return true; } - return true; } /** diff --git a/module/productplan/test/model/getpairs.php b/module/productplan/test/model/getpairs.php index f3a27eaecd..2ae245f3d3 100755 --- a/module/productplan/test/model/getpairs.php +++ b/module/productplan/test/model/getpairs.php @@ -6,6 +6,23 @@ title=productpanModel->getPairs(); timeout=0 cid=1 +- 获取产品1下的所有计划属性3 @计划3 [2022-01-01 ~ 2022-01-30] +- 获取产品2下的所有计划属性5 @计划5 待定 +- 获取产品6下的所有计划属性16 @计划16 [2021-01-01 ~ 2021-06-30] / 主干 +- 获取产品1下的所有计划属性3 @计划3 [2022-01-01 ~ 2022-01-30] +- 获取产品2下的所有计划属性5 @计划5 待定 +- 获取产品6下的所有计划属性16 @计划16 [2021-01-01 ~ 2021-06-30] / 主干 +- 获取产品6下分支2的所有计划属性17 @计划17 [2021-06-01 ~ 2021-06-15] / 分支2 +- 获取产品1下的所有未过期计划 @0 +- 获取产品2下的所有未过期计划属性5 @计划5 待定 +- 获取产品6下的所有未过期计划 @0 +- 获取产品1下的所有未过期计划属性3 @计划3 [2022-01-01 ~ 2022-01-30] +- 获取产品2下的所有未过期计划属性5 @计划5 待定 +- 获取产品6下的所有未过期计划属性18 @计划18 [2022-01-01 ~ 2022-01-30] / 主干 +- 获取产品1下的所有非父计划属性3 @计划3 [2022-01-01 ~ 2022-01-30] +- 获取产品2下的所有非父计划属性5 @计划5 待定 +- 获取产品6下的所有非父计划属性16 @计划16 [2021-01-01 ~ 2021-06-30] / 主干 + */ include dirname(__FILE__, 5) . '/test/lib/init.php'; diff --git a/module/productplan/ui/browsebylist.html.php b/module/productplan/ui/browsebylist.html.php index 60da547502..08261a318c 100644 --- a/module/productplan/ui/browsebylist.html.php +++ b/module/productplan/ui/browsebylist.html.php @@ -112,6 +112,7 @@ if($isFromDoc) { $cols[$key]['sortType'] = false; if(isset($col['link'])) unset($cols[$key]['link']); + if($key == 'title') $cols[$key]['link'] = array('url' => createLink('productplan', 'view', 'planID={id}'), 'data-toggle' => 'modal', 'data-size' => 'lg'); } } diff --git a/module/productplan/ui/bug.html.php b/module/productplan/ui/bug.html.php index 79af38b0ca..3947226823 100644 --- a/module/productplan/ui/bug.html.php +++ b/module/productplan/ui/bug.html.php @@ -21,6 +21,7 @@ foreach($bugCols as $bugColKey => $bugCol) if(isset($bugCol['link'])) unset($bugCols[$bugColKey]['link']); if($bugColKey == 'pri') $bugCols[$bugColKey]['priList'] = $lang->bug->priList; if($bugColKey == 'severity') $bugCols[$bugColKey]['severityList'] = $lang->bug->severityList; + if($bugColKey == 'title') $bugCols[$bugColKey]['link'] = array('url' => createLink('bug', 'view', "bugID={id}"), 'data-toggle' => 'modal', 'data-size' => 'lg'); } unset($bugCols['actions']); diff --git a/module/productplan/ui/story.html.php b/module/productplan/ui/story.html.php index 3b50306c19..cbf2e779a3 100644 --- a/module/productplan/ui/story.html.php +++ b/module/productplan/ui/story.html.php @@ -30,6 +30,7 @@ foreach($storyCols as $storyColKey => $storyCol) $storyCols[$storyColKey]['sortType'] = false; if(isset($storyCol['link'])) unset($storyCols[$storyColKey]['link']); if($storyColKey == 'pri') $storyCols[$storyColKey]['priList'] = $lang->story->priList; + if($storyColKey == 'title') $storyCols[$storyColKey]['link'] = array('url' => createLink('{type}', 'view', "storyID={id}&version={version}"), 'data-toggle' => 'modal', 'data-size' => 'lg'); } unset($storyCols['actions']); diff --git a/module/productplan/ui/view.html.php b/module/productplan/ui/view.html.php index f9309e26b9..bb2e40f63f 100644 --- a/module/productplan/ui/view.html.php +++ b/module/productplan/ui/view.html.php @@ -23,6 +23,7 @@ $confirmLang['activate'] = $lang->productplan->confirmActivate; $confirmLang['delete'] = $lang->productplan->confirmDelete; $decodeParam = helper::safe64Decode($param); +$isInModal = isInModal(); jsVar('initLink', $link); jsVar('type', $type); @@ -201,12 +202,12 @@ detailHeader ( to::prefix ( - backBtn(set::icon('back'), set::type('secondary'), set::url($this->session->productPlanList), $lang->goback), + $isInModal ? null : backBtn(set::icon('back'), set::type('secondary'), set::url($this->session->productPlanList), $lang->goback), entityLabel(set(array('entityID' => $plan->id, 'level' => 1, 'text' => $plan->title))), span(setClass('label circle primary'), ($plan->begin == FUTURE_TIME || $plan->end == FUTURE_TIME) ? $lang->productplan->future : $plan->begin . '~' . $plan->end), $plan->deleted ? span(setClass('label danger'), $lang->product->deleted) : null ), - !$plan->deleted && $actions ? to::suffix + !$isInModal && !$plan->deleted && $actions ? to::suffix ( btnGroup(set::items($actions['mainActions'])), !empty($actions['mainActions']) && !empty($actions['suffixActions']) ? div(setClass('divider mx-2')): null, @@ -232,7 +233,7 @@ detailBody ( setClass('tab-actions absolute right-0 gap-2'), setStyle('top', '-8px'), - empty($createStoryLink) && empty($batchCreateStoryLink) ? null : dropdown + $isInModal || (empty($createStoryLink) && empty($batchCreateStoryLink)) ? null : dropdown ( btn ( @@ -255,7 +256,7 @@ detailBody set::trigger('hover'), set::placement('bottom-end') ), - common::hasPriv('productplan', 'linkStory') ? btn + !$isInModal && common::hasPriv('productplan', 'linkStory') ? btn ( set::type('primary'), set::icon('link'), @@ -299,7 +300,7 @@ detailBody set::key('bugs'), set::title($lang->productplan->linkedBugs), set::active($type == 'bug'), - $plan->parent >= 0 && common::hasPriv('productplan', 'linkBug')? toolbar + !$isInModal && $plan->parent >= 0 && common::hasPriv('productplan', 'linkBug') ? toolbar ( setClass('tab-actions absolute right-0 gap-2'), setStyle('top', '-8px'), diff --git a/module/programplan/control.php b/module/programplan/control.php index 2113987519..99c8c22b7d 100644 --- a/module/programplan/control.php +++ b/module/programplan/control.php @@ -46,10 +46,12 @@ class programplan extends control * @param int $baselineID * @param string $browseType * @param int $queryID + * @param string $from + * @param int $blockID * @access public * @return void */ - public function browse(int $projectID = 0, int $productID = 0, string $type = 'gantt', string $orderBy = 'id_asc', int $baselineID = 0, string $browseType = '', int $queryID = 0) + public function browse(int $projectID = 0, int $productID = 0, string $type = 'gantt', string $orderBy = 'id_asc', int $baselineID = 0, string $browseType = '', int $queryID = 0, string $from = 'project', int $blockID = 0) { if($type == 'lists') { @@ -57,6 +59,16 @@ class programplan extends control return; } + if($from == 'doc') + { + $this->loadModel('doc'); + $projects = $this->loadModel('project')->getPairsByModel(array('ipd', 'waterfall', 'waterfallplus')); + if(empty($projects)) return $this->send(array('result' => 'fail', 'message' => $this->lang->doc->tips->noProject)); + + if(!$projectID) $projectID = key($projects); + $this->view->projects = $projects; + } + $this->app->loadLang('stage'); $this->session->set('projectPlanList', $this->app->getURI(true), 'project'); $this->commonAction($projectID, $productID); @@ -71,6 +83,9 @@ class programplan extends control $browseType = strtolower($browseType); $plans = $this->programplanZen->buildStages($projectID, $productID, $baselineID, $type, $orderBy, $browseType, $queryID); + $this->view->from = $from; + $this->view->blockID = $blockID; + /* Build gantt browse view. */ $this->programplanZen->buildBrowseView($projectID, $productID, $plans, $type, $orderBy, $baselineID, $browseType, $queryID); } diff --git a/module/programplan/css/browse.ui.css b/module/programplan/css/browse.ui.css new file mode 100644 index 0000000000..f6b7dbc346 --- /dev/null +++ b/module/programplan/css/browse.ui.css @@ -0,0 +1 @@ +.mb-4-important {margin-bottom: 16px !important;} diff --git a/module/programplan/js/browse.ui.js b/module/programplan/js/browse.ui.js new file mode 100644 index 0000000000..b5e4f3e66b --- /dev/null +++ b/module/programplan/js/browse.ui.js @@ -0,0 +1,24 @@ +window.insertToDoc = function(blockID, insertLink) +{ + const docID = getDocApp()?.docID; + const blockType = 'gantt'; + + const url = $.createLink('doc', 'buildZentaoList', `docID=${docID}&type=${blockType}&blockID=${blockID}`); + const formData = new FormData(); + formData.append('ganttOptions', JSON.stringify(ganttOptions)); + formData.append('ganttFields', JSON.stringify(ganttFields)); + formData.append('showFields', JSON.stringify(showFields)); + formData.append('url', insertLink); + + $.post(url, formData, function(resp) + { + resp = JSON.parse(resp); + if(resp.result == 'success') + { + const oldBlockID = resp.oldBlockID; + const newBlockID = resp.newBlockID; + zui.Modal.hide(); + window.insertZentaoList && window.insertZentaoList(blockType, newBlockID, null, oldBlockID); + } + }); +} diff --git a/module/programplan/ui/gantt.html.php b/module/programplan/ui/gantt.html.php index b4b5dd0b3d..b8f90e4e6b 100644 --- a/module/programplan/ui/gantt.html.php +++ b/module/programplan/ui/gantt.html.php @@ -15,7 +15,60 @@ namespace zin; data('fileName', 'gantt-export-' . $projectID); include './ganttfields.html.php'; -if($app->rawModule == 'programplan') +$isFromDoc = $from === 'doc'; +if($isFromDoc) +{ + jsVar('ganttOptions', $plans); + jsVar('ganttFields', $ganttFields); + jsVar('showFields', $showFields); + + $this->app->loadLang('doc'); + $projectChangeLink = createLink('programPlan', 'browse', "projectID={projectID}&productID={$productID}&type={$type}&orderBy=$orderBy&baselineID=&browseType={$browseType}&queryID={$queryID}&from=$from&blockID=$blockID"); + $insertLink = createLink('programPlan', 'browse', "projectID=$projectID&productID={$productID}&type={$type}&orderBy=$orderBy&baselineID=&browseType={$browseType}&queryID={$queryID}&from=$from&blockID={blockID}"); + + formPanel + ( + setID('zentaolist'), + setClass('mb-4-important'), + set::title(sprintf($this->lang->doc->insertTitle, $this->lang->doc->zentaoList['gantt'])), + set::actions(array()), + set::showExtra(false), + to::titleSuffix + ( + span + ( + setClass('text-muted text-sm text-gray-600 font-light'), + span + ( + setClass('text-warning mr-1'), + icon('help'), + ), + $lang->doc->previewTip + ) + ), + formRow + ( + formGroup + ( + set::width('1/2'), + set::name('project'), + set::label($lang->doc->project), + set::control(array('required' => false)), + set::items($projects), + set::value($projectID), + set::required(), + span + ( + setClass('error-tip text-danger hidden'), + $lang->doc->emptyError + ), + on::change('[name="project"]')->do("loadModal('$projectChangeLink'.replace('{projectID}', $(this).val()))") + ) + ) + ); +} + +if($app->rawModule == 'programplan' && !$isFromDoc) { $productDropdown = null; if($project->stageBy == 'product' && empty($project->isTpl)) @@ -63,9 +116,17 @@ gantt ( set('ganttLang', $ganttLang), set('ganttFields', $ganttFields), - set('canEdit', common::hasPriv('programplan', 'ganttEdit')), - set('canEditDeadline', common::hasPriv('review', 'edit')), + set('canEdit', $isFromDoc ? false : hasPriv('programplan', 'ganttEdit')), + set('canEditDeadline', $isFromDoc ? false : hasPriv('review', 'edit')), set('zooming', isset($zooming) ? $zooming : 'day'), set('showChart', !$dateDetails), set('options', $plans) ); + +$isFromDoc ? btn +( + setClass('mt-4'), + set::type('primary'), + on::click("insertToDoc($blockID, '$insertLink')"), + $lang->doc->insertText +) : null; diff --git a/module/programplan/ui/ganttfields.html.php b/module/programplan/ui/ganttfields.html.php index 5c1cb850b5..640c6b2d8a 100644 --- a/module/programplan/ui/ganttfields.html.php +++ b/module/programplan/ui/ganttfields.html.php @@ -34,16 +34,23 @@ $ganttLang->wrongRelationSource = $lang->execution->error->wrongGanttRelationSou $ganttLang->wrongRelationTarget = $lang->execution->error->wrongGanttRelationTarget; $ganttLang->wrongKanbanTasks = $lang->execution->error->wrongKanbanTasks; -$typeHtml = '' . $lang->programplan->ganttBrowseType[$ganttType] . ''; -$typeHtml .= ''; -foreach($lang->programplan->ganttBrowseType as $ganttBrowseType => $typeName) +if($from == 'doc') { - $link = $this->createLink('programplan', 'browse', "projectID=$projectID&productID=$productID&type=$ganttBrowseType"); - if($app->rawModule == 'review' and $app->rawMethod == 'assess') $this->createLink('review', 'assess', "reivewID=$reviewID&from=&type=$ganttBrowseType"); + $typeHtml = $lang->programplan->ganttBrowseType['gantt']; +} +else +{ + $typeHtml = '' . $lang->programplan->ganttBrowseType[$ganttType] . ''; + $typeHtml .= ''; + foreach($lang->programplan->ganttBrowseType as $ganttBrowseType => $typeName) + { + $link = $this->createLink('programplan', 'browse', "projectID=$projectID&productID=$productID&type=$ganttBrowseType"); + if($app->rawModule == 'review' and $app->rawMethod == 'assess') $this->createLink('review', 'assess', "reivewID=$reviewID&from=&type=$ganttBrowseType"); - $typeHtml .= ''; + $typeHtml .= ''; + } + $typeHtml .= ''; } -$typeHtml .= ''; $ganttFields = array(); $ganttFields['column_text'] = $typeHtml; diff --git a/module/projectrelease/control.php b/module/projectrelease/control.php index 2381956766..1d50e0ca53 100644 --- a/module/projectrelease/control.php +++ b/module/projectrelease/control.php @@ -42,10 +42,12 @@ class projectrelease extends control * @param int $recTotal * @param int $recPerPage * @param int $pageID + * @param string $from + * @param int $blockID * @access public * @return void */ - public function browse(int $projectID = 0, int $executionID = 0, string $type = 'all', string $orderBy = 't1.date_desc', int $recTotal = 0, int $recPerPage = 15, int $pageID = 1) + public function browse(int $projectID = 0, int $executionID = 0, string $type = 'all', string $orderBy = 't1.date_desc', int $recTotal = 0, int $recPerPage = 15, int $pageID = 1, string $from = 'project', int $blockID = 0) { /* 设置发布列表和版本列表 session。*/ /* Set releaseList and buildList session. */ @@ -53,6 +55,15 @@ class projectrelease extends control $this->session->set('releaseList', $uri, 'project'); $this->session->set('buildList', $uri); + if($from == 'doc') + { + $projects = $this->project->getPairs(); + if(empty($projects)) return $this->send(array('result' => 'fail', 'message' => $this->lang->doc->tips->noProject)); + + if(!$projectID) $projectID = key($projects); + $this->view->projects = $projects; + } + /* 设置菜单。*/ /* Set menu. */ if($projectID) $this->project->setMenu($projectID); @@ -91,6 +102,16 @@ class projectrelease extends control $this->view->showBranch = $showBranch; $this->view->appList = $this->loadModel('system')->getPairs(); $this->view->childReleases = $this->release->getListByCondition(explode(',', $children), 0, true); + $this->view->from = $from; + $this->view->blockID = $blockID; + $this->view->idList = ''; + + if($from == 'doc') + { + $content = $this->loadModel('doc')->getDocBlockContent($blockID); + $this->view->idList = zget($content, 'idList', ''); + } + $this->display(); } diff --git a/module/projectrelease/js/browse.ui.js b/module/projectrelease/js/browse.ui.js index 54c87f464a..4673c33330 100644 --- a/module/projectrelease/js/browse.ui.js +++ b/module/projectrelease/js/browse.ui.js @@ -24,3 +24,53 @@ window.renderCell = function(result, {col, row}) return result; } + +window.checkedChange = function(changes) +{ + if(!this._checkedRows) this._checkedRows = {}; + Object.keys(changes).forEach((rowID) => + { + const row = this.getRowInfo(rowID); + if(row !== undefined) this._checkedRows[rowID] = row.data; + }); +} + +window.insertListToDoc = function() +{ + const dtable = zui.DTable.query($('#projectreleases')); + const myTable = dtable.$; + const checkedList = Object.keys(myTable.state.checkedRows); + if(!checkedList.length) return; + + let {cols} = dtable.options; + const data = checkedList.map(rowID => myTable._checkedRows[rowID]); + const docID = getDocApp()?.docID; + + const blockType = 'projectRelease'; + const url = $.createLink('doc', 'buildZentaoList', `docID=${docID}&type=${blockType}&blockID=${blockID}`); + const formData = new FormData(); + formData.append('cols', JSON.stringify(cols)); + formData.append('data', JSON.stringify(data)); + formData.append('idList', checkedList.join(',')); + formData.append('url', insertListLink); + $.post(url, formData, function(resp) + { + resp = JSON.parse(resp); + if(resp.result == 'success') + { + const oldBlockID = resp.oldBlockID; + const newBlockID = resp.newBlockID; + zui.Modal.hide(); + window.insertZentaoList && window.insertZentaoList(blockType, newBlockID, null, oldBlockID); + } + }); +} + +window.firstRendered = false; +window.toggleCheckRows = function(idList) +{ + if(!idList?.length || firstRendered) return; + firstRendered = true; + const dtable = zui.DTable.query($('#projectreleases')); + dtable.$.toggleCheckRows(idList.split(','), true); +} diff --git a/module/projectrelease/ui/browse.html.php b/module/projectrelease/ui/browse.html.php index 8460bd209b..40a8a4f6ae 100644 --- a/module/projectrelease/ui/browse.html.php +++ b/module/projectrelease/ui/browse.html.php @@ -10,15 +10,70 @@ declare(strict_types=1); */ namespace zin; +$isFromDoc = $from == 'doc'; +if($isFromDoc) +{ + jsVar('blockID', $blockID); + + $this->app->loadLang('doc'); + $projectChangeLink = createLink('projectRelease', 'browse', "projectID={projectID}&executionID=$executionID&type=$type&orderBy=$orderBy&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}&from=$from&blockID=$blockID"); + + jsVar('insertListLink', createLink('projectRelease', 'browse', "projectID={$project->id}&executionID=$executionID&type=$type&orderBy=$orderBy&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}&from=$from&blockID={blockID}")); + + formPanel + ( + setID('zentaolist'), + setClass('mb-4-important'), + set::title(sprintf($this->lang->doc->insertTitle, $this->lang->doc->zentaoList['projectRelease'])), + set::actions(array()), + set::showExtra(false), + to::titleSuffix + ( + span + ( + setClass('text-muted text-sm text-gray-600 font-light'), + span + ( + setClass('text-warning mr-1'), + icon('help'), + ), + $lang->doc->previewTip + ) + ), + formRow + ( + formGroup + ( + set::width('1/2'), + set::name('project'), + set::label($lang->doc->project), + set::control(array('required' => false)), + set::items($projects), + set::value($project->id), + set::required(), + span + ( + setClass('error-tip text-danger hidden'), + $lang->doc->emptyError + ), + on::change('[name="project"]')->do("loadModal('$projectChangeLink'.replace('{projectID}', $(this).val()))") + ) + ) + ); +} + featureBar ( set::current($type), - set::linkParams("projectID={$projectID}&executionID={$executionID}&type={key}&orderBy={$orderBy}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}") + set::linkParams("projectID={$projectID}&executionID={$executionID}&type={key}&orderBy={$orderBy}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}&from={$from}&blockID={$blockID}"), + set::isModal($isFromDoc), + set::modalTarget('#projectReleases_table') ); $canManageSystem = hasPriv('system', 'browse') && common::canModify('project', $project); toolbar ( + setClass(array('hidden' => $isFromDoc)), !$project->hasProduct && $canManageSystem ? item(set ( array @@ -55,6 +110,20 @@ foreach(array_column($releases, 'system') as $system) } if(!empty($cols['system'])) $cols['system']['map'] = array(0 => '') + $appList; +if($isFromDoc) +{ + $cols['id']['type'] = 'checkID'; + + if(isset($cols['actions'])) unset($cols['actions']); + + foreach($cols as $key => $col) + { + $cols[$key]['sortType'] = false; + if(isset($col['link'])) unset($cols[$key]['link']); + if($key == 'name') $cols[$key]['link'] = array('url' => createLink('projectrelease', 'view', "releaseID={id}"), 'data-toggle' => 'modal', 'data-size' => 'lg'); + } +} + foreach($releases as $release) { $release->rowID = $release->id; @@ -75,17 +144,22 @@ foreach($releases as $release) $tableData = initTableData($releases, $cols); dtable ( + set::id('projectreleases'), set::cols(array_values($cols)), set::data($tableData), - set::customCols(true), set::rowKey('rowID'), set::onRenderCell(jsRaw('window.renderCell')), - set::sortLink(createLink('projectrelease', 'browse', "projectID={$project->id}&executionID={$executionID}&type={$type}&orderBy={name}_{sortType}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}")), - set::footer([jsRaw("function(){return {html: '{$pageSummary}'};}"), 'flex', 'pager']), set::footPager(usePager()), set::emptyTip($lang->release->noRelease), - set::createTip($lang->release->create), - set::createLink(hasPriv('projectrelease', 'create') ? createLink('projectrelease', 'create', "projectID={$projectID}") : '') + set::checkable($isFromDoc), + $isFromDoc ? set::footToolbar(array(array('text' => $lang->doc->insertText, 'data-on' => 'click', 'data-call' => 'insertListToDoc'))) : set::footer([jsRaw("function(){return {html: '{$pageSummary}'};}"), 'flex', 'pager']), + !$isFromDoc ? null : set::afterRender(jsCallback()->call('toggleCheckRows', $idList)), + !$isFromDoc ? null : set::onCheckChange(jsRaw('window.checkedChange')), + !$isFromDoc ? null : set::height(400), + $isFromDoc ? null : set::customCols(true), + $isFromDoc ? null : set::sortLink(createLink('projectrelease', 'browse', "projectID={$project->id}&executionID={$executionID}&type={$type}&orderBy={name}_{sortType}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}")), + $isFromDoc ? null : set::createTip($lang->release->create), + $isFromDoc ? null : set::createLink(hasPriv('projectrelease', 'create') ? createLink('projectrelease', 'create', "projectID={$projectID}") : '') ); /* ====== Render page ====== */ diff --git a/module/projectstory/ui/story.html.php b/module/projectstory/ui/story.html.php index eaf3b8274b..1258a5ddd5 100644 --- a/module/projectstory/ui/story.html.php +++ b/module/projectstory/ui/story.html.php @@ -92,6 +92,7 @@ foreach($setting as $key => $col) $setting[$key]['sortType'] = false; if(isset($col['link'])) unset($setting[$key]['link']); if($key == 'pri') $setting[$key]['priList'] = $lang->story->priList; + if($key == 'title') $setting[$key]['link'] = array('url' => helper::createLink('{type}', 'view', 'storyID={id}&version={version}¶m=0&storyType={type}'), 'data-toggle' => 'modal', 'data-size' => 'lg'); } $cols = array_values($setting); diff --git a/module/release/config/dtable.php b/module/release/config/dtable.php index e824f0b8ba..4722f8d7e5 100644 --- a/module/release/config/dtable.php +++ b/module/release/config/dtable.php @@ -30,6 +30,7 @@ $config->release->dtable->fieldList['name']['name'] = 'name'; $config->release->dtable->fieldList['name']['link'] = array('module' => 'release', 'method' => 'view', 'params' => 'releaseID={id}'); $config->release->dtable->fieldList['name']['type'] = 'category'; $config->release->dtable->fieldList['name']['fixed'] = 'left'; +$config->release->dtable->fieldList['name']['width'] = 150; $config->release->dtable->fieldList['name']['show'] = true; $config->release->dtable->fieldList['name']['required'] = true; diff --git a/module/release/control.php b/module/release/control.php index a15f7b0073..d879d76a3f 100755 --- a/module/release/control.php +++ b/module/release/control.php @@ -59,7 +59,7 @@ class release extends control { $this->app->loadLang('doc'); - $products = $this->loadModel('product')->getPairs('nodeleted', 0, '', 'all'); + $products = $this->loadModel('product')->getPairs('nodeleted'); if(empty($products)) return $this->send(array('result' => 'fail', 'message' => $this->lang->doc->tips->noProduct)); if(!$productID) $productID = key($products); diff --git a/module/release/ui/browse.html.php b/module/release/ui/browse.html.php index b72cec17d0..6500008995 100644 --- a/module/release/ui/browse.html.php +++ b/module/release/ui/browse.html.php @@ -116,6 +116,7 @@ if($isFromDoc) { $cols[$key]['sortType'] = false; if(isset($col['link'])) unset($cols[$key]['link']); + if($key == 'name') $cols[$key]['link'] = array('url' => createLink('release', 'view', "releaseID={id}"), 'data-toggle' => 'modal', 'data-size' => 'lg'); } } @@ -133,7 +134,7 @@ dtable set::footPager( usePager ( - array('linkCreator' => helper::createLink('release', 'browse', "productID={$product->id}&branch={$branch}&type={$type}&orderBy={$orderBy}¶m=$param&recTotal={recTotal}&recPerPage={recPerPage}&pageID={page}"), 'recTotal' => $pager->recTotal, 'recPerPage' => $pager->recPerPage) + array('linkCreator' => helper::createLink('release', 'browse', "productID={$product->id}&branch={$branch}&type={$type}&orderBy={$orderBy}¶m=$param&recTotal={recTotal}&recPerPage={recPerPage}&pageID={page}&from={$from}&blockID={$blockID}"), 'recTotal' => $pager->recTotal, 'recPerPage' => $pager->recPerPage) ) ), set::emptyTip($lang->release->noRelease), diff --git a/module/release/ui/view.html.php b/module/release/ui/view.html.php index b450c708aa..7763602dc9 100644 --- a/module/release/ui/view.html.php +++ b/module/release/ui/view.html.php @@ -13,6 +13,7 @@ namespace zin; include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php'); $releaseModule = $app->rawModule == 'projectrelease' ? 'projectrelease' : 'release'; +$isInModal = isInModal(); jsVar('initLink', $link); jsVar('type', $type); @@ -41,7 +42,7 @@ detailHeader ( to::prefix ( - backBtn + $isInModal ? null : backBtn ( set::icon('back'), set::type('secondary'), @@ -51,7 +52,7 @@ detailHeader entityLabel(set(array('entityID' => $release->id, 'level' => 2, 'text' => zget($appList, $release->system) . $release->name))), $release->deleted ? span(setClass('label danger'), $lang->release->deleted) : null ), - !empty($actions['mainActions']) || !empty($actions['suffixActions']) ? to::suffix + !$isInModal && (!empty($actions['mainActions']) || !empty($actions['suffixActions'])) ? to::suffix ( btnGroup(set::items($actions['mainActions'])), !empty($actions['mainActions']) && !empty($actions['suffixActions']) ? div(setClass('divider')): null, @@ -82,8 +83,8 @@ jsVar('checkedSummary', $lang->product->checkedSRSummary); jsVar('unlinkstoryurl', helper::createLink($releaseModule, 'unlinkStory', "releaseID={$release->id}&storyID=%s")); $storyTableData = initTableData($stories, $config->release->dtable->story->fieldList, $this->release); -$canBatchUnlinkStory = $canBeChanged && common::hasPriv($releaseModule, 'batchUnlinkStory'); -$canBatchCloseStory = $canBeChanged && common::hasPriv('story', 'batchClose'); +$canBatchUnlinkStory = !$isInModal && $canBeChanged && common::hasPriv($releaseModule, 'batchUnlinkStory'); +$canBatchCloseStory = !$isInModal && $canBeChanged && common::hasPriv('story', 'batchClose'); $storyFootToolbar = array(); if($canBatchUnlinkStory) $storyFootToolbar['items'][] = array('className' => 'btn primary size-sm batch-btn', 'text' => $lang->release->batchUnlink, 'data-type' => 'story', 'data-url' => createLink($releaseModule, 'batchUnlinkStory', "release={$release->id}")); @@ -101,8 +102,8 @@ $bugTableData = array_map(function($bug) return $bug; }, $bugTableData); -$canBatchUnlinkBug = $canBeChanged && common::hasPriv($releaseModule, 'batchUnlinkBug'); -$canBatchCloseBug = $canBeChanged && common::hasPriv('bug', 'batchClose'); +$canBatchUnlinkBug = !$isInModal && $canBeChanged && common::hasPriv($releaseModule, 'batchUnlinkBug'); +$canBatchCloseBug = !$isInModal && $canBeChanged && common::hasPriv('bug', 'batchClose'); $bugFootToolbar = array(); if($canBatchUnlinkBug) $bugFootToolbar['items'][] = array('className' => 'btn primary size-sm batch-btn', 'text' => $lang->release->batchUnlink, 'data-type' => 'bug', 'data-url' => createLink($releaseModule, 'batchUnlinkBug', "release={$release->id}")); @@ -165,7 +166,7 @@ foreach($includedApps as $includedApp) /* Right menus, export and link. */ $exportBtn = null; -if(common::hasPriv($releaseModule, 'export') && ($summary || count($bugs) || count($leftBugs))) +if(!$isInModal && common::hasPriv($releaseModule, 'export') && ($summary || count($bugs) || count($leftBugs))) { $exportBtn = btn(set(array( 'text' => $lang->release->export, @@ -184,7 +185,7 @@ jsVar('releaseModule', $releaseModule); if(!$release->deleted && $canBeChanged && empty($release->releases)) { - if(common::hasPriv($releaseModule, 'linkStory')) + if(!$isInModal && common::hasPriv($releaseModule, 'linkStory')) { $linkStoryBtn = btn ( @@ -196,7 +197,7 @@ if(!$release->deleted && $canBeChanged && empty($release->releases)) ); } - if(common::hasPriv($releaseModule, 'linkBug')) + if(!$isInModal && common::hasPriv($releaseModule, 'linkBug')) { $linkBugBtn = btn ( diff --git a/module/search/test/model/setdefaultparams.php b/module/search/test/model/setdefaultparams.php index a767eee065..eee3ef7fe6 100755 --- a/module/search/test/model/setdefaultparams.php +++ b/module/search/test/model/setdefaultparams.php @@ -7,6 +7,14 @@ title=测试 searchModel->setDefaultParams(); timeout=0 cid=1 +- 测试设置 products 的默认参数 @1,2,3,4,5,6,7,8,9,10,null +- 测试设置 users 的默认参数 @,admin,user1,user2,user3,user4,$me,null +- 测试设置 executions 的默认参数 @,null +- 测试设置 空数组 的默认参数 @,null +- 测试设置 空字符串 的默认参数 @0,null +- 测试设置 array(1) 的默认参数 @ZERO +- 测试设置 nonull 的默认参数 @0 + */ include dirname(__FILE__, 5) . '/test/lib/init.php'; @@ -36,9 +44,9 @@ $params7 = array('product' => array('values' => array(''), 'nonull' => true)) $search = new searchTest(); r($search->setDefaultParamsTest($fields1, $params1)) && p('0') && e('1,2,3,4,5,6,7,8,9,10,null'); // 测试设置 products 的默认参数 -r($search->setDefaultParamsTest($fields2, $params2)) && p('0') && e('admin,user1,user2,user3,user4,$me,null'); // 测试设置 users 的默认参数 +r($search->setDefaultParamsTest($fields2, $params2)) && p('0') && e(',admin,user1,user2,user3,user4,$me,null'); // 测试设置 users 的默认参数 r($search->setDefaultParamsTest($fields3, $params3)) && p('0') && e(',null'); // 测试设置 executions 的默认参数 r($search->setDefaultParamsTest($fields4, $params4)) && p('0') && e(',null'); // 测试设置 空数组 的默认参数 r($search->setDefaultParamsTest($fields5, $params5)) && p('0') && e('0,null'); // 测试设置 空字符串 的默认参数 r($search->setDefaultParamsTest($fields6, $params6)) && p('0') && e('ZERO'); // 测试设置 array(1) 的默认参数 -r($search->setDefaultParamsTest($fields7, $params7)) && p('0') && e('0'); // 测试设置 nonull 的默认参数 \ No newline at end of file +r($search->setDefaultParamsTest($fields7, $params7)) && p('0') && e('0'); // 测试设置 nonull 的默认参数 diff --git a/module/search/test/tao/processbuildinfields.php b/module/search/test/tao/processbuildinfields.php index 23fefaac42..86a568a75e 100755 --- a/module/search/test/tao/processbuildinfields.php +++ b/module/search/test/tao/processbuildinfields.php @@ -2,12 +2,14 @@ processBuildinFields(); - cid=1 - - projectstory +title=测试 searchTao->processBuildinFields(); +timeout=0 +cid=1 + +- projectstory - 属性module @story - - 属性fields @title,id,keywords,status,pri,module,stage,grade,plan,estimate,source,sourceNote,fromBug,category,openedBy,reviewedBy,result,assignedTo,closedBy,lastEditedBy,mailto,closedReason,version,openedDate,reviewedDate,assignedDate,closedDate,lastEditedDate,activatedDate + - 属性fields @title,id,keywords,status,pri,module,roadmap,stage,grade,plan,estimate,source,sourceNote,fromBug,category,openedBy,reviewedBy,result,assignedTo,closedBy,lastEditedBy,mailto,closedReason,version,openedDate,reviewedDate,assignedDate,closedDate,lastEditedDate,activatedDate - 属性maxCount @500 - bug - 属性module @bug @@ -15,7 +17,7 @@ - 属性maxCount @500 - product - 属性module @story - - 属性fields @title,id,keywords,status,pri,module,stage,grade,plan,estimate,source,sourceNote,fromBug,category,openedBy,reviewedBy,result,assignedTo,closedBy,lastEditedBy,mailto,closedReason,version,openedDate,reviewedDate,assignedDate,closedDate,lastEditedDate,activatedDate + - 属性fields @title,id,keywords,status,pri,module,roadmap,stage,grade,plan,estimate,source,sourceNote,fromBug,category,openedBy,reviewedBy,result,assignedTo,closedBy,lastEditedBy,mailto,closedReason,version,openedDate,reviewedDate,assignedDate,closedDate,lastEditedDate,activatedDate - 属性maxCount @500 - testcase - 属性module @testcase @@ -26,7 +28,7 @@ - 属性fields @title,story,id,keywords,lastEditedBy,type,auto,openedBy,status,stage,module,pri,openedDate,lastEditedDate - 属性maxCount @500 - */ +*/ include dirname(__FILE__, 5) . '/test/lib/init.php'; include dirname(__FILE__, 2) . '/lib/search.unittest.class.php'; @@ -38,8 +40,8 @@ $search = new searchTest(); $moduleList = array('projectStory', 'bug', 'product', 'testcase', 'caselib'); -r($search->processBuildinFieldsTest($moduleList[0])) && p('module;fields;maxCount', ';') && e('story;title,id,keywords,status,pri,module,stage,grade,plan,estimate,source,sourceNote,fromBug,category,openedBy,reviewedBy,result,assignedTo,closedBy,lastEditedBy,mailto,closedReason,version,openedDate,reviewedDate,assignedDate,closedDate,lastEditedDate,activatedDate;500'); // projectstory +r($search->processBuildinFieldsTest($moduleList[0])) && p('module;fields;maxCount', ';') && e('story;title,id,keywords,status,pri,module,roadmap,stage,grade,plan,estimate,source,sourceNote,fromBug,category,openedBy,reviewedBy,result,assignedTo,closedBy,lastEditedBy,mailto,closedReason,version,openedDate,reviewedDate,assignedDate,closedDate,lastEditedDate,activatedDate;500'); // projectstory r($search->processBuildinFieldsTest($moduleList[1])) && p('module;fields;maxCount', ';') && e('bug;title,module,keywords,steps,assignedTo,resolvedBy,status,confirmed,story,project,branch,plan,id,execution,severity,pri,type,os,browser,resolution,activatedCount,toTask,toStory,openedBy,closedBy,lastEditedBy,mailto,openedBuild,resolvedBuild,openedDate,assignedDate,resolvedDate,closedDate,lastEditedDate,deadline,activatedDate;500'); // bug -r($search->processBuildinFieldsTest($moduleList[2])) && p('module;fields;maxCount', ';') && e('story;title,id,keywords,status,pri,module,stage,grade,plan,estimate,source,sourceNote,fromBug,category,openedBy,reviewedBy,result,assignedTo,closedBy,lastEditedBy,mailto,closedReason,version,openedDate,reviewedDate,assignedDate,closedDate,lastEditedDate,activatedDate;500'); // product +r($search->processBuildinFieldsTest($moduleList[2])) && p('module;fields;maxCount', ';') && e('story;title,id,keywords,status,pri,module,roadmap,stage,grade,plan,estimate,source,sourceNote,fromBug,category,openedBy,reviewedBy,result,assignedTo,closedBy,lastEditedBy,mailto,closedReason,version,openedDate,reviewedDate,assignedDate,closedDate,lastEditedDate,activatedDate;500'); // product r($search->processBuildinFieldsTest($moduleList[3])) && p('module;fields;maxCount', ';') && e('testcase;title,story,id,keywords,lastEditedBy,type,auto,openedBy,status,stage,module,pri,lib,lastRunner,lastRunResult,lastRunDate,openedDate,lastEditedDate,scene;500'); // testcase r($search->processBuildinFieldsTest($moduleList[4])) && p('module;fields;maxCount', ';') && e('caselib;title,story,id,keywords,lastEditedBy,type,auto,openedBy,status,stage,module,pri,openedDate,lastEditedDate;500'); // caselib diff --git a/module/story/model.php b/module/story/model.php index 13c33db993..e3da23aa07 100644 --- a/module/story/model.php +++ b/module/story/model.php @@ -251,7 +251,7 @@ class storyModel extends model /** * Get stories list of a execution. * - * @param int $executionID + * @param int|array $executionID * @param int $productID * @param string $orderBy * @param string $browseType @@ -262,15 +262,23 @@ class storyModel extends model * @access public * @return array */ - public function getExecutionStories(int $executionID = 0, int $productID = 0, string $orderBy = 't1.`order`_desc', string $browseType = 'byModule', string $param = '0', string $storyType = 'story', array|string $excludeStories = '', object|null $pager = null): array + public function getExecutionStories(int|array $executionID = 0, int $productID = 0, string $orderBy = 't1.`order`_desc', string $browseType = 'byModule', string $param = '0', string $storyType = 'story', array|string $excludeStories = '', object|null $pager = null): array { if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getExecutionStories(); if(empty($executionID)) return array(); - $execution = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($executionID)->fetch(); + + if(is_array($executionID)) + { + $module = 'execution'; + } + else + { + $execution = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($executionID)->fetch(); + $module = $execution->type == 'project' ? 'project' : 'execution'; + } $sqlCondition = ''; - $module = $execution->type == 'project' ? 'project' : 'execution'; $showGrades = isset($this->config->{$module}->showGrades) ? $this->config->{$module}->showGrades : null; if($showGrades) { @@ -301,7 +309,7 @@ class storyModel extends model { /* 根据请求类型和参数,获取查询要用到的条件。 */ $modules = $this->storyTao->getModules4ExecutionStories($browseType, $param); - $storyIdList = $this->storyTao->getIdListOfExecutionsByProjectID($browseType, $executionID); + $storyIdList = is_array($executionID) ? array() : $this->storyTao->getIdListOfExecutionsByProjectID($browseType, $executionID); $productParam = ($browseType == 'byproduct' and $param) ? $param : $productID; $branchParam = ($browseType == 'bybranch' and $param !== '') ? $param : (string)$this->cookie->storyBranchParam; @@ -310,7 +318,7 @@ class storyModel extends model $storyDAO = $this->dao->select("DISTINCT t1.*, t2.*, t2.`path`, t2.`plan`, IF(t2.`pri` = 0, {$this->config->maxPriValue}, t2.`pri`) as priOrder, t3.type as productType, t2.version as version")->from(TABLE_PROJECTSTORY)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t2.product = t3.id') - ->where('t1.project')->eq($executionID) + ->where('t1.project')->in($executionID) ->andWhere('t2.deleted')->eq(0) ->andWhere('t3.deleted')->eq(0) ->beginIF(strpos('withoutparent', $browseType) !== false)->andWhere('t2.isParent')->eq('0')->fi() @@ -321,8 +329,8 @@ class storyModel extends model ->beginIF($modules)->andWhere('t2.module')->in($modules)->fi(); /* 根据传入的 ID 是项目还是执行分别查询需求。 */ - if($execution->type == 'project') $stories = $this->storyTao->fetchProjectStories($storyDAO, $productID, $browseType, $branchParam, $storyIdList, $orderBy, $pager, $execution); - if($execution->type != 'project') $stories = $this->storyTao->fetchExecutionStories($storyDAO, (int)$productParam, $browseType, $branchParam, $orderBy, $pager); + if($module == 'project') $stories = $this->storyTao->fetchProjectStories($storyDAO, $productID, $browseType, $branchParam, $storyIdList, $orderBy, $pager, $execution); + if($module != 'project') $stories = $this->storyTao->fetchExecutionStories($storyDAO, (int)$productParam, $browseType, $branchParam, $orderBy, $pager); } $stories = $this->storyTao->fixBranchStoryStage($stories); @@ -1052,8 +1060,6 @@ class storyModel extends model $isChanged = $oldParentStory->changedBy ? true : false; if($preStatus == 'reviewing') $preStatus = $isChanged ? 'changing' : 'draft'; } - - if($this->config->edition != 'open' && $oldParentStory->feedback) $this->loadModel('feedback')->updateStatus('story', $oldParentStory->feedback, $newParentStory->status, $oldParentStory->status, $oldParentStory->id); } else { @@ -1062,7 +1068,9 @@ class storyModel extends model } $newParentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($parentID)->fetch(); - $changes = common::createChanges($oldParentStory, $newParentStory); + if($this->config->edition != 'open' && $oldParentStory->feedback) $this->loadModel('feedback')->updateStatus('story', $oldParentStory->feedback, $status, $oldParentStory->status, $oldParentStory->id); + + $changes = common::createChanges($oldParentStory, $newParentStory); if($action and $changes) { $actionID = $this->loadModel('action')->create('story', $parentID, $action, '', $preStatus, '', false); @@ -1913,33 +1921,32 @@ class storyModel extends model $story->module = ($oldStory->branch != $branchID and !in_array($oldStory->module, $mainModules)) ? 0 : $oldStory->module; $this->dao->update(TABLE_STORY)->data($story)->autoCheck()->where('id')->eq((int)$storyID)->exec(); - if(!dao::isError()) + if(dao::isError()) return array(); + + if($confirm == 'yes') { - if($confirm == 'yes') + $planIdList = array(); + $conflictPlanIdList = array(); + + /* Determine whether there is a conflict between the branch of the story and the linked plan. */ + if($oldStory->branch != $branchID and $branchID != BRANCH_MAIN and isset($plans[$storyID])) { - $planIdList = array(); - $conflictPlanIdList = array(); - - /* Determine whether there is a conflict between the branch of the story and the linked plan. */ - if($oldStory->branch != $branchID and $branchID != BRANCH_MAIN and isset($plans[$storyID])) + foreach($plans[$storyID] as $planID => $plan) { - foreach($plans[$storyID] as $planID => $plan) - { - if($plan->branch != $branchID) $conflictPlanIdList[$planID] = $planID; - if($plan->branch == $branchID) $planIdList[$planID] = $planID; - } + if($plan->branch != $branchID) $conflictPlanIdList[$planID] = $planID; + if($plan->branch == $branchID) $planIdList[$planID] = $planID; + } - /* If there is a conflict in the linked plan when the branch story to be modified, the linked with the conflicting plan will be removed. */ - if($conflictPlanIdList) $this->dao->delete()->from(TABLE_PLANSTORY)->where('story')->eq($storyID)->andWhere('plan')->in(implode(',', $conflictPlanIdList))->exec(); - if($planIdList) - { - $story->plan = implode(',', $planIdList); - $this->dao->update(TABLE_STORY)->set('plan')->eq($story->plan)->where('id')->eq($storyID)->exec(); - } + /* If there is a conflict in the linked plan when the branch story to be modified, the linked with the conflicting plan will be removed. */ + if($conflictPlanIdList) $this->dao->delete()->from(TABLE_PLANSTORY)->where('story')->eq($storyID)->andWhere('plan')->in(implode(',', $conflictPlanIdList))->exec(); + if($planIdList) + { + $story->plan = implode(',', $planIdList); + $this->dao->update(TABLE_STORY)->set('plan')->eq($story->plan)->where('id')->eq($storyID)->exec(); } } - $allChanges[$storyID] = common::createChanges($oldStory, $story); } + $allChanges[$storyID] = common::createChanges($oldStory, $story); } return $allChanges; } @@ -2322,7 +2329,7 @@ class storyModel extends model /** * Get stories list of a product. * - * @param string|int $productID + * @param string|int|array $productID * @param array|string|int $branch * @param array|string $moduleIdList * @param array|string $status @@ -2335,7 +2342,7 @@ class storyModel extends model * @access public * @return array */ - public function getProductStories(string|int $productID = 0, array|string|int $branch = 0, array|string $moduleIdList = '', array|string $status = 'all', string $type = 'story', string $orderBy = 'id_desc', bool $hasParent = true, array|string $excludeStories = '', object|null $pager = null): array + public function getProductStories(string|int|array $productID = 0, array|string|int $branch = 0, array|string $moduleIdList = '', array|string $status = 'all', string $type = 'story', string $orderBy = 'id_desc', bool $hasParent = true, array|string $excludeStories = '', object|null $pager = null): array { if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getStories(); $showGrades = isset($this->config->{$type}->showGrades) ? $this->config->{$type}->showGrades : null; @@ -2610,7 +2617,7 @@ class storyModel extends model /** * Get stories by a field. * - * @param int $productID + * @param int|array $productID * @param int|string $branch * @param string|array $modules * @param string $fieldName @@ -2622,7 +2629,7 @@ class storyModel extends model * @access public * @return array */ - public function getByField(int $productID, int|string $branch, string|array $modules, string $fieldName, string $fieldValue, string $type = 'story', string $orderBy = '', object|null $pager = null, string $operator = 'equal'): array + public function getByField(int|array $productID, int|string $branch, string|array $modules, string $fieldName, string $fieldValue, string $type = 'story', string $orderBy = '', object|null $pager = null, string $operator = 'equal'): array { if(!$this->loadModel('common')->checkField(TABLE_STORY, $fieldName) and $fieldName != 'reviewBy' and $fieldName != 'assignedBy') return array(); @@ -2670,16 +2677,16 @@ class storyModel extends model /** * Get to be closed stories. * - * @param int $productID - * @param int $branch - * @param string $modules - * @param string $type requirement|story - * @param string $orderBy - * @param object $pager + * @param int|array $productID + * @param int $branch + * @param string $modules + * @param string $type requirement|story + * @param string $orderBy + * @param object $pager * @access public * @return array */ - public function get2BeClosed(int $productID, int|string $branch, string|array $modules, string $type = 'story', string $orderBy = '', object|null $pager = null): array + public function get2BeClosed(int|array $productID, int|string $branch, string|array $modules, string $type = 'story', string $orderBy = '', object|null $pager = null): array { $showGrades = isset($this->config->{$type}->showGrades) ? $this->config->{$type}->showGrades : null; if($showGrades) @@ -3112,7 +3119,7 @@ class storyModel extends model ->beginIF($type == 'reviewedBy')->andWhere("CONCAT(',', t1.reviewedBy, ',')")->like("%,$account,%")->fi() ->beginIF($type == 'closedBy')->andWhere('t1.closedBy')->eq($account)->fi() ->fi() - ->beginIF($includeLibStories == false and $this->config->edition == 'max')->andWhere('t1.lib')->eq('0')->fi() + ->beginIF(!$includeLibStories and $this->config->edition == 'max')->andWhere('t1.lib')->eq('0')->fi() ->beginIF($shadow !== 'all')->andWhere('t2.shadow')->eq((int)$shadow)->fi() ->beginIF($productID)->andWhere('t1.product')->eq((int)$productID)->fi() ->orderBy($orderBy) @@ -4090,8 +4097,6 @@ class storyModel extends model */ public function checkForceReview(string $storyType = 'story'): bool { - $forceReview = false; - $forceField = $this->config->{$storyType}->needReview == 0 ? 'forceReview' : 'forceNotReview'; $forceReviewRoles = !empty($this->config->{$storyType}->{$forceField . 'Roles'}) ? $this->config->{$storyType}->{$forceField . 'Roles'} : ''; $forceReviewDepts = !empty($this->config->{$storyType}->{$forceField . 'Depts'}) ? $this->config->{$storyType}->{$forceField . 'Depts'} : ''; @@ -4116,9 +4121,7 @@ class storyModel extends model $forceUsers .= "," . implode(',', array_keys($users)); } - $forceReview = $this->config->{$storyType}->needReview == 0 ? strpos(",{$forceUsers},", ",{$this->app->user->account},") !== false : strpos(",{$forceUsers},", ",{$this->app->user->account},") === false; - - return $forceReview; + return $this->config->{$storyType}->needReview == 0 ? strpos(",{$forceUsers},", ",{$this->app->user->account},") !== false : strpos(",{$forceUsers},", ",{$this->app->user->account},") === false; } /** @@ -4139,7 +4142,6 @@ class storyModel extends model $stories = $this->storyTao->mergeChildrenForTrack($allStories, $stories, $storyType); $leafNodes = $this->storyTao->getLeafNodes($stories, $storyType); - $tracks = array(); $lanes = $this->storyTao->buildTrackLanes($leafNodes, $storyType); $cols = $this->storyTao->buildTrackCols($storyType); $items = $this->storyTao->buildTrackItems($stories, $leafNodes, $storyType); @@ -4760,7 +4762,7 @@ class storyModel extends model } if($oldStory->status == 'reviewing') $story = $this->updateStoryByReview($storyID, $oldStory, $story); - if(strpos('draft,changing', $oldStory->status) != false) $story->reviewedBy = ''; + if(strpos('draft,changing', $oldStory->status) !== false) $story->reviewedBy = ''; } /** diff --git a/module/story/tao.php b/module/story/tao.php index a4715f1945..42af4ba3e8 100644 --- a/module/story/tao.php +++ b/module/story/tao.php @@ -189,12 +189,12 @@ class storyTao extends storyModel * 根据产品 ID 列表和分支参数,构建查询条件。 * Build products condition. * - * @param string|int $productIdList + * @param string|int|array $productIdList * @param array|string|int $branch * @access protected * @return string */ - protected function buildProductsCondition(string|int $productIdList, array|string|int $branch = 'all'): string + protected function buildProductsCondition(string|int|array $productIdList, array|string|int $branch = 'all'): string { /* 如果查询所有分支,直接用 idList 条件。 */ if(empty($productIdList)) $productIdList = '0'; @@ -620,11 +620,11 @@ class storyTao extends storyModel * Get id list of executions by product. * * @param string $type - * @param int $projectID + * @param int|array $projectID * @access protected * @return array */ - protected function getIdListOfExecutionsByProjectID(string $type, int $projectID): array + protected function getIdListOfExecutionsByProjectID(string $type, int|array $projectID): array { if($type != 'linkedexecution' && $type != 'unlinkedexecution') return array(); @@ -1961,15 +1961,12 @@ class storyTao extends storyModel if(!empty($execution) && !common::canModify($execution->type == 'project' ? 'project' : 'execution', $execution)) return $actions; $tutorialMode = commonModel::isTutorialMode(); - if($this->config->edition == 'ipd' && $storyType == 'story') + if($this->config->edition == 'ipd' && $storyType == 'story' && !empty($story->confirmeActionType)) { - if(!empty($story->confirmeActionType)) - { - $method = $story->confirmeActionType == 'confirmedretract' ? 'confirmDemandRetract' : 'confirmDemandUnlink'; - $url = helper::createLink('story', $method, "objectID=$story->id&object=story&extra={$story->confirmeObjectID}"); - $actions[] = array('name' => $method, 'icon' => 'search', 'hint' => $this->lang->story->$method, 'url' => $url, 'data-toggle' => 'modal'); - return $actions; - } + $method = $story->confirmeActionType == 'confirmedretract' ? 'confirmDemandRetract' : 'confirmDemandUnlink'; + $url = helper::createLink('story', $method, "objectID=$story->id&object=story&extra={$story->confirmeObjectID}"); + $actions[] = array('name' => $method, 'icon' => 'search', 'hint' => $this->lang->story->$method, 'url' => $url, 'data-toggle' => 'modal'); + return $actions; } static $taskGroups = array(); @@ -2140,7 +2137,6 @@ class storyTao extends storyModel if($execution->type == 'project') { - $unlinkModule = 'projectstory'; $unlinkStoryTip = $this->lang->execution->confirmUnlinkExecutionStory; static $executionStories = array(); @@ -2424,8 +2420,6 @@ class storyTao extends storyModel $stmt = $this->dao->select('*')->from(TABLE_PROJECTSTORY)->where('story')->in($storyIdList)->query(); while($projectStory = $stmt->fetch()) $projectStoryList[$projectStory->project][$projectStory->story] = $projectStory->story; - $projects = array(); - $executions = array(); $stmt = $this->dao->select('id,type AS projectType,model,parent,path,grade,name as title,hasProduct,begin,end,status,project,progress,multiple')->from(TABLE_PROJECT)->where('id')->in(array_keys($projectStoryList))->andWhere('deleted')->eq(0)->orderBy('id')->query(); $today = helper::today(); $storyGroup = array(); @@ -2457,7 +2451,6 @@ class storyTao extends storyModel $storyGroup = array('design' => array(), 'commit' => array()); $stmt = $this->dao->select('id,project,commit,name as title,status,story,type AS designType')->from(TABLE_DESIGN)->where('story')->in($storyIdList)->andWhere('deleted')->eq(0)->orderBy('project')->query(); $commitIdList = ''; - $commitGroup = array(); while($design = $stmt->fetch()) { $storyGroup['design'][$design->story][$design->id] = $design; @@ -2467,7 +2460,7 @@ class storyTao extends storyModel if($commitIdList) $commits = $this->dao->select('id,repo,revision,committer,comment as title')->from(TABLE_REPOHISTORY)->where('id')->in(array_unique(explode(',', $commitIdList)))->fetchAll('id'); foreach($storyGroup['design'] as $storyID => $designs) { - foreach($designs as $designID => $design) + foreach($designs as $design) { if(empty($design->commit)) continue; foreach(explode(',', $design->commit) as $commitID) diff --git a/module/story/ui/view.html.php b/module/story/ui/view.html.php index e4caa27449..6b569c8c1d 100644 --- a/module/story/ui/view.html.php +++ b/module/story/ui/view.html.php @@ -31,7 +31,7 @@ for($i = $story->version; $i >= 1; $i--) ->text("#{$i}") ->url(inlink('view', "storyID={$story->id}&version=$i¶m=0&storyType={$story->type}")); - if($isInModal) $versionItem->set(array('data-load' => 'modal', 'data-target' => '.modal.show')); + if($isInModal) $versionItem->set(array('data-load' => 'modal', 'data-target' => '.modal.show:not(.modal-hide)')); $versionItem->selected($version == $i); $versions[] = $versionItem; diff --git a/module/task/model.php b/module/task/model.php index 4672ef06b6..c91b14e219 100755 --- a/module/task/model.php +++ b/module/task/model.php @@ -1741,7 +1741,7 @@ class taskModel extends model * 获取执行下的任务列表信息。 * Get the task list under a execution. * - * @param int $executionID + * @param int|array $executionID * @param int $productID * @param string|array $type all|assignedbyme|myinvolved|undone|needconfirm|assignedtome|finishedbyme|delayed|review|wait|doing|done|pause|cancel|closed|array('wait','doing','done','pause','cancel','closed') * @param array $modules @@ -1750,7 +1750,7 @@ class taskModel extends model * @access public * @return array */ - public function getExecutionTasks(int $executionID, int $productID = 0, string|array $type = 'all', array $modules = array(), string $orderBy = 'status_asc, id_desc', object $pager = null): array + public function getExecutionTasks(int|array $executionID, int $productID = 0, string|array $type = 'all', array $modules = array(), string $orderBy = 'status_asc, id_desc', object $pager = null): array { $tasks = $this->taskTao->fetchExecutionTasks($executionID, $productID, $type, $modules, $orderBy, $pager); if(empty($tasks)) return array(); diff --git a/module/task/tao.php b/module/task/tao.php index 6f795ece95..f7985f4a17 100644 --- a/module/task/tao.php +++ b/module/task/tao.php @@ -467,7 +467,7 @@ class taskTao extends taskModel * 获取执行下的任务。 * Fetch tasks under execution by executionID(Todo). * - * @param int $executionID + * @param int|array $executionID * @param int $productID * @param string|array $type all|assignedbyme|myinvolved|undone|needconfirm|assignedtome|finishedbyme|delayed|review|wait|doing|done|pause|cancel|closed|array('wait','doing','done','pause','cancel','closed') * @param array $modules @@ -476,7 +476,7 @@ class taskTao extends taskModel * @access protected * @return object[] */ - protected function fetchExecutionTasks(int $executionID, int $productID = 0, string|array $type = 'all', array $modules = array(), string $orderBy = 'status_asc, id_desc', object $pager = null): array + protected function fetchExecutionTasks(int|array $executionID, int $productID = 0, string|array $type = 'all', array $modules = array(), string $orderBy = 'status_asc, id_desc', object $pager = null): array { if(is_string($type)) $type = strtolower($type); $orderBy = str_replace('pri_', 'priOrder_', $orderBy); diff --git a/module/testcase/model.php b/module/testcase/model.php index af6bc1e8a5..609d7e3f66 100755 --- a/module/testcase/model.php +++ b/module/testcase/model.php @@ -79,7 +79,7 @@ class testcaseModel extends model * 获取模块的用例。 * Get cases of modules. * - * @param int $productID + * @param int|array $productID * @param int|string $branch * @param int|array $moduleIdList * @param string $browseType @@ -91,7 +91,7 @@ class testcaseModel extends model * @access public * @return array */ - public function getModuleCases(int $productID, int|string $branch = 0, int|array $moduleIdList = 0, string $browseType = '', string $auto = 'no', string $caseType = '', string $orderBy = 'id_desc', object $pager = null, string $from = 'testcase'): array + public function getModuleCases(int|array $productID, int|string $branch = 0, int|array $moduleIdList = 0, string $browseType = '', string $auto = 'no', string $caseType = '', string $orderBy = 'id_desc', object $pager = null, string $from = 'testcase'): array { $isProjectTab = $this->app->tab == 'project' && $from != 'doc'; $isExecutionTab = $this->app->tab == 'execution' && $from != 'doc'; @@ -102,7 +102,7 @@ class testcaseModel extends model if($isProjectTab) $stmt = $stmt->leftJoin(TABLE_PROJECTCASE)->alias('t3')->on('t1.id=t3.case'); if($isExecutionTab) $stmt = $stmt->leftJoin(TABLE_PROJECTCASE)->alias('t3')->on('t1.id=t3.case'); - return $stmt ->where('t1.product')->eq((int)$productID) + return $stmt ->where('t1.product')->in($productID) ->beginIF($isProjectTab)->andWhere('t3.project')->eq($this->session->project)->fi() ->beginIF($isExecutionTab)->andWhere('t3.project')->eq($this->session->execution)->fi() ->beginIF($branch !== 'all')->andWhere('t1.branch')->eq($branch)->fi() @@ -266,7 +266,7 @@ class testcaseModel extends model * 获取用例列表。 * Get test cases. * - * @param int $productID + * @param int|array $productID * @param int|string $branch * @param string $browseType * @param int $queryID @@ -279,7 +279,7 @@ class testcaseModel extends model * @access public * @return array */ - public function getTestCases(int $productID, string|int $branch, string $browseType, int $queryID, int $moduleID, string $caseType = '', string $auto = 'no', string $orderBy = 'id_desc', object $pager = null, string $from = 'testcase'): array + public function getTestCases(int|array $productID, string|int $branch, string $browseType, int $queryID, int $moduleID, string $caseType = '', string $auto = 'no', string $orderBy = 'id_desc', object $pager = null, string $from = 'testcase'): array { if(common::isTutorialMode()) return $this->loadModel('tutorial')->getCases(); @@ -1988,28 +1988,28 @@ class testcaseModel extends model * 获取包含子场景和子用例的场景列表。 * Get scene list include sub scenes and cases. * - * @param int $productID - * @param string $branch - * @param string $browseType - * @param int $moduleID - * @param string $caseType - * @param string $orderBy - * @param object $pager + * @param int|array $productID + * @param string $branch + * @param string $browseType + * @param int $moduleID + * @param string $caseType + * @param string $orderBy + * @param object $pager * @access public * @return array */ - public function getSceneGroups(int $productID, string $branch = '', string $browseType = '', int $moduleID = 0, string $caseType = '', string $orderBy = 'id_desc', object $pager = null): array + public function getSceneGroups(int|array $productID, string $branch = '', string $browseType = '', int $moduleID = 0, string $caseType = '', string $orderBy = 'id_desc', object $pager = null): array { $modules = $moduleID ? $this->loadModel('tree')->getAllChildId($moduleID) : array(); $scenes = $this->dao->select('*')->from(TABLE_SCENE) ->where('deleted')->eq('0') - ->andWhere('product')->eq($productID) + ->andWhere('product')->in($productID) ->beginIF($branch !== 'all')->andWhere('branch')->eq($branch)->fi() ->beginIF($modules)->andWhere('module')->in($modules)->fi() ->orderBy('grade_desc, sort_asc') ->fetchAll('id', false); - $pager->recTotal = 0; + if($pager) $pager->recTotal = 0; if(!$scenes) return array(); $cases = $scenes && $browseType != 'onlyscene' ? $this->getSceneGroupCases($productID, $branch, $modules, $caseType, $orderBy) : array(); @@ -2030,6 +2030,8 @@ class testcaseModel extends model } } + if(!$pager) return $scenes; + $pager->recTotal = count($scenes); $pager->pageTotal = ceil($pager->recTotal / $pager->recPerPage); @@ -2040,15 +2042,15 @@ class testcaseModel extends model * 获取用场景 ID 分组的用例。 * Get cases by scene id. * - * @param int $productID - * @param string $branch - * @param array $modules - * @param string $caseType - * @param string $orderBy + * @param int|array $productID + * @param string $branch + * @param array $modules + * @param string $caseType + * @param string $orderBy * @access public * @return array */ - public function getSceneGroupCases(int $productID, string $branch, array $modules, string $caseType, string $orderBy): array + public function getSceneGroupCases(int|array $productID, string $branch, array $modules, string $caseType, string $orderBy): array { $browseType = $this->session->caseBrowseType && $this->session->caseBrowseType != 'bysearch' ? $this->session->caseBrowseType : 'all'; @@ -2058,7 +2060,7 @@ class testcaseModel extends model $caseList = $stmt->where('t1.deleted')->eq('0') ->andWhere('t1.scene')->ne(0) - ->andWhere('t1.product')->eq($productID) + ->andWhere('t1.product')->in($productID) ->beginIF($this->app->tab == 'project')->andWhere('t2.project')->eq($this->session->project)->fi() ->beginIF($branch !== 'all')->andWhere('t1.branch')->eq($branch)->fi() ->beginIF($modules)->andWhere('t1.module')->in($modules)->fi() @@ -2373,7 +2375,7 @@ class testcaseModel extends model * @access public * @return array */ - function getCaseListForXmindExport(int $productID, int $moduleID, string $branch = ''): array + public function getCaseListForXmindExport(int $productID, int $moduleID, string $branch = ''): array { $fields = 't1.id AS testcaseID, t1.title AS `name`, t1.pri, t2.id AS productID, t2.`name` AS productName, t3.id AS moduleID, t3.`name` AS moduleName, t4.id AS sceneID, t4.title AS sceneName, t1.precondition'; return $this->dao->select($fields)->from(TABLE_CASE)->alias('t1') @@ -2396,7 +2398,7 @@ class testcaseModel extends model * @access public * @return array */ - function getStepByProductAndModule(int $productID, int $moduleID): array + public function getStepByProductAndModule(int $productID, int $moduleID): array { $fields = "t1.id as testcaseID," . "t2.id as stepID," @@ -2446,7 +2448,7 @@ class testcaseModel extends model * @access public * @return array */ - function getSceneByProductAndModule($productID, $moduleID): array + public function getSceneByProductAndModule($productID, $moduleID): array { /* Get scenes by product and module. */ $sceneList = $this->dao->select('id as sceneID, title as sceneName, path, parent as parentID, product as productID, module as moduleID') @@ -2478,7 +2480,7 @@ class testcaseModel extends model * @access public * @return array */ - function saveMindConfig(string $type, array $configList): array + public function saveMindConfig(string $type, array $configList): array { $this->dao->begin(); @@ -2519,7 +2521,7 @@ class testcaseModel extends model * @access public * @return array */ - function getMindConfig(string $type): array + public function getMindConfig(string $type): array { $configItems = $this->dao->select("`key`,value")->from(TABLE_CONFIG) ->where('owner')->eq($this->app->user->account) @@ -2996,4 +2998,73 @@ class testcaseModel extends model if(empty($caseIdList)) return array(); return $this->dao->select('id')->from(TABLE_CASE)->where('id')->in($caseIdList)->andWhere('auto')->ne('auto')->fetchPairs('id', 'id'); } + + /** + * 预处理场景及其包含的用例,把层级结构改为平行结构,处理成数据表格支持的形式。 + * Preprocess the scenario and the use cases it contains, change the hierarchical structure to a parallel structure, and process it into a form supported by the data table. + * + * @param array $scenes + * @access public + * @return array + */ + public function preProcessScenesForBrowse(array $scenes): array + { + $cases = array(); + foreach($scenes as $scene) + { + $scene->hasCase = false; + + if(!empty($scene->children)) + { + $cases = array_merge($cases, $this->preProcessScenesForBrowse($scene->children)); + foreach($scene->children as $child) + { + /* + * 如果子场景有用例,那么父场景也有用例。 + * If the child scene has a use case, the parent scene also has a use case. + */ + if($child->hasCase) + { + $scene->hasCase = true; + break; + } + } + } + if(!empty($scene->cases)) + { + $cases = array_merge($cases, $scene->cases); + + $scene->hasCase = true; + } + + unset($scene->children); + unset($scene->cases); + + $cases[] = $scene; + } + return $cases; + } + + /** + * 预处理没有场景的用例,附加额外的信息并给用例 ID 加前缀以防止和场景 ID 重复。 + * Preprocess use cases without scenarios, append additional information and prefix the use case ID to prevent duplication with scenario IDs. + * + * @param array $cases + * @access public + * @return array + */ + public function preProcessCasesForBrowse(array $cases): array + { + /* Check if the related story of cases are changed. */ + $cases = $this->loadModel('story')->checkNeedConfirm($cases); + $cases = $this->appendData($cases); + foreach($cases as $case) + { + $case->caseID = $case->id; + $case->id = 'case_' . $case->id; // Add a prefix to avoid duplication with the scene ID. + $case->parent = 0; + $case->isScene = false; + } + return $cases; + } } diff --git a/module/testcase/tao.php b/module/testcase/tao.php index 040821c83c..61a3af0bff 100644 --- a/module/testcase/tao.php +++ b/module/testcase/tao.php @@ -50,7 +50,7 @@ class testcaseTao extends testcaseModel * 获取某个项目下某个模块的用例列表。 * Get project cases of a module. * - * @param int $productID + * @param int|array $productID * @param int|string $branch * @param int $moduleIdList * @param string $browseType @@ -61,14 +61,14 @@ class testcaseTao extends testcaseModel * @access protected * @return array */ - protected function getModuleProjectCases(int $productID, int|string $branch = 0, int|array $moduleIdList = 0, string $browseType = '', string $auto = 'no', string $caseType = '', string $orderBy = 'id_desc', object $pager = null): array + protected function getModuleProjectCases(int|array $productID, int|string $branch = 0, int|array $moduleIdList = 0, string $browseType = '', string $auto = 'no', string $caseType = '', string $orderBy = 'id_desc', object $pager = null): array { return $this->dao->select('distinct t1.*, t2.*, t4.title AS storyTitle')->from(TABLE_PROJECTCASE)->alias('t1') ->leftJoin(TABLE_CASE)->alias('t2')->on('t1.case = t2.id') ->leftJoin(TABLE_PROJECTSTORY)->alias('t3')->on('t3.story = t2.story') ->leftJoin(TABLE_STORY)->alias('t4')->on('t3.story = t4.id') ->where('t1.project')->eq((int)$this->session->project) - ->beginIF(!empty($productID))->andWhere('t2.product')->eq($productID)->fi() + ->beginIF(!empty($productID))->andWhere('t2.product')->in($productID)->fi() ->beginIF(!empty($productID) && $branch !== 'all')->andWhere('t2.branch')->eq($branch)->fi() ->beginIF($moduleIdList)->andWhere('t2.module')->in($moduleIdList)->fi() ->beginIF($browseType == 'all')->andWhere('t2.scene')->eq(0)->fi() @@ -86,7 +86,7 @@ class testcaseTao extends testcaseModel * 获取待确认的用例列表。 * Get need confirm case list. * - * @param int $productID + * @param int|array $productID * @param string|int $branch * @param array $modules * @param string $auto @@ -96,7 +96,7 @@ class testcaseTao extends testcaseModel * @access protected * @return array */ - protected function getNeedConfirmList(int $productID, string|int $branch, array $modules, string $auto, string $caseType, string $sort, object $pager = null): array + protected function getNeedConfirmList(int|array $productID, string|int $branch, array $modules, string $auto, string $caseType, string $sort, object $pager = null): array { return $this->dao->select('distinct t1.*, t2.title AS storyTitle')->from(TABLE_CASE)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') @@ -104,7 +104,7 @@ class testcaseTao extends testcaseModel ->where('t2.status')->eq('active') ->andWhere('t1.deleted')->eq('0') ->andWhere('t2.version > t1.storyVersion') - ->beginIF(!empty($productID))->andWhere('t1.product')->eq($productID)->fi() + ->beginIF(!empty($productID))->andWhere('t1.product')->in($productID)->fi() ->beginIF(!empty($productID) && $branch !== 'all')->andWhere('t1.branch')->eq($branch)->fi() ->beginIF($this->app->tab == 'project')->andWhere('t3.project')->eq($this->session->project)->fi() ->beginIF($modules)->andWhere('t1.module')->in($modules)->fi() diff --git a/module/testcase/test/lib/testcase.unittest.class.php b/module/testcase/test/lib/testcase.unittest.class.php old mode 100755 new mode 100644 index d2421694c3..2a427541e2 --- a/module/testcase/test/lib/testcase.unittest.class.php +++ b/module/testcase/test/lib/testcase.unittest.class.php @@ -1930,4 +1930,39 @@ class testcaseTest global $tester; return implode(',', array_keys($tester->config->testcase->search['fields'])); } + + /** + * 测试构建搜索配置。 + * Test build search config. + * + * @param int $productID + * @param string $branch + * @access public + * @return array + */ + public function buildSearchConfigTest(int $productID, string $branch = 'all'): array + { + $result = $this->objectModel->buildSearchConfig($productID, $branch); + return array('module' => $result['module'], 'storyValues' => $result['params']['story']['values'], 'typeValues' => $result['params']['type']['values']); + } + + /** + * 预处理场景及其包含的用例,把层级结构改为平行结构,处理成数据表格支持的形式。 + * Preprocess the scenario and the use cases it contains, change the hierarchical structure to a parallel structure, and process it into a form supported by the data table. + * + * @param array $scenes + * @access public + * @return array + */ + public function preProcessScenesForBrowseTest($scenes) + { + $cases = $this->objectModel->preProcessScenesForBrowse($scenes); + + foreach($cases as $case) + { + if(empty($case->hasCase)) $case->hasCase = 0; + } + return $cases; + } + } diff --git a/module/testcase/test/model/buildsearchconfig.php b/module/testcase/test/model/buildsearchconfig.php new file mode 100644 index 0000000000..a6782f27fd --- /dev/null +++ b/module/testcase/test/model/buildsearchconfig.php @@ -0,0 +1,46 @@ +#!/usr/bin/env php +gen('45'); +zenData('branch')->gen('10'); +zenData('project')->gen('30'); +zenData('story')->gen('10'); +zenData('module')->gen('10'); +zenData('user')->gen('1'); + +su('admin'); + +/** + +title=测试 testcaseModel->buildSearchConfig(); +cid=1 +pid=1 + +- 获取全部产品配置的module @testcase +- 获取全部产品配置的storyValues属性10 @10:软件需求10 +- 获取全部产品配置的typeValues属性unit @单元测试 +- 获取产品1配置的module @testcase +- 获取产品1配置的storyValues属性2 @2:软件需求2 +- 获取产品1配置的typeValues属性config @配置相关 +- 获取产品1主干配置的module @testcase +- 获取产品1主干配置的storyValues属性null @空 +- 获取产品1主干配置的typeValues属性null @空 + +*/ + +$productID = array(0, 1); +$branch = array('all', 0); + +$testcase = new testcaseTest(); + +r($testcase->buildSearchConfigTest($productID[0], $branch[0])['module']) && p() && e('testcase'); // 获取全部产品配置的module +r($testcase->buildSearchConfigTest($productID[0], $branch[0])['storyValues']) && p('10') && e('10:软件需求10'); // 获取全部产品配置的storyValues +r($testcase->buildSearchConfigTest($productID[0], $branch[0])['typeValues']) && p('unit') && e('单元测试'); // 获取全部产品配置的typeValues +r($testcase->buildSearchConfigTest($productID[1], $branch[0])['module']) && p() && e('testcase'); // 获取产品1配置的module +r($testcase->buildSearchConfigTest($productID[1], $branch[0])['storyValues']) && p('2') && e('2:软件需求2'); // 获取产品1配置的storyValues +r($testcase->buildSearchConfigTest($productID[1], $branch[0])['typeValues']) && p('config') && e('配置相关'); // 获取产品1配置的typeValues +r($testcase->buildSearchConfigTest($productID[1], $branch[1])['module']) && p() && e('testcase'); // 获取产品1主干配置的module +r($testcase->buildSearchConfigTest($productID[1], $branch[1])['storyValues']) && p('null') && e('空'); // 获取产品1主干配置的storyValues +r($testcase->buildSearchConfigTest($productID[1], $branch[1])['typeValues']) && p('null') && e('空'); // 获取产品1主干配置的typeValues diff --git a/module/testcase/test/model/buildsearchform.php b/module/testcase/test/model/buildsearchform.php index 6c41a5a376..0bf1d1e162 100644 --- a/module/testcase/test/model/buildsearchform.php +++ b/module/testcase/test/model/buildsearchform.php @@ -29,6 +29,13 @@ cid=1 - 测试构建产品 41 project 0 module 0 branch 0 的搜索表单 @title,story,id,keywords,lastEditedBy,type,auto,openedBy,status,product,stage,module,pri,lib,lastRunner,lastRunResult,lastRunDate,openedDate,lastEditedDate,scene,branch +- 测试构建产品 0 project 0 module 0 branch all 的搜索表单 @title,story,id,keywords,lastEditedBy,type,auto,openedBy,status,product,stage,module,pri,lib,lastRunner,lastRunResult,lastRunDate,openedDate,lastEditedDate,scene +- 测试构建产品 1 project 0 module 0 branch all 的搜索表单 @title,story,id,keywords,lastEditedBy,type,auto,openedBy,status,product,stage,module,pri,lib,lastRunner,lastRunResult,lastRunDate,openedDate,lastEditedDate,scene +- 测试构建产品 1 project 1 module 0 branch all 的搜索表单 @title,story,id,keywords,lastEditedBy,type,auto,openedBy,status,product,stage,module,pri,lib,lastRunner,lastRunResult,lastRunDate,openedDate,lastEditedDate,scene +- 测试构建产品 1 project 1 module 1 branch all 的搜索表单 @title,story,id,keywords,lastEditedBy,type,auto,openedBy,status,product,stage,module,pri,lib,lastRunner,lastRunResult,lastRunDate,openedDate,lastEditedDate,scene +- 测试构建产品 41 project 0 module 0 branch 0 的搜索表单 @title,story,id,keywords,lastEditedBy,type,auto,openedBy,status,product,stage,module,pri,lib,lastRunner,lastRunResult,lastRunDate,openedDate,lastEditedDate,scene,branch +- 测试构建产品 41 project 0 module 0 branch 0 的搜索表单 @title,story,id,keywords,lastEditedBy,type,auto,openedBy,status,product,stage,module,pri,lib,lastRunner,lastRunResult,lastRunDate,openedDate,lastEditedDate,scene,branch + */ $productID = array(0, 1, 41); diff --git a/module/testcase/test/model/preprocesscasesforbrowse.php b/module/testcase/test/model/preprocesscasesforbrowse.php new file mode 100644 index 0000000000..f3b28831ef --- /dev/null +++ b/module/testcase/test/model/preprocesscasesforbrowse.php @@ -0,0 +1,92 @@ +#!/usr/bin/env php +preProcessCasesForBrowse(); +timeout=0 +cid=1 + +- 获取用例1额外的信息 + - 第1条的id属性 @case_1 + - 第1条的bugs属性 @6 + - 第1条的results属性 @1 + - 第1条的caseFails属性 @0 + - 第1条的stepNumber属性 @1 + - 第1条的caseID属性 @1 +- 获取用例3额外的信息 + - 第3条的id属性 @case_3 + - 第3条的bugs属性 @6 + - 第3条的results属性 @1 + - 第3条的caseFails属性 @0 + - 第3条的stepNumber属性 @1 + - 第3条的caseID属性 @3 +- 获取用例4额外的信息 + - 第4条的id属性 @case_4 + - 第4条的bugs属性 @0 + - 第4条的results属性 @1 + - 第4条的caseFails属性 @1 + - 第4条的stepNumber属性 @1 + - 第4条的caseID属性 @4 +- 获取用例5额外的信息 + - 第5条的id属性 @case_5 + - 第5条的bugs属性 @0 + - 第5条的results属性 @1 + - 第5条的caseFails属性 @0 + - 第5条的stepNumber属性 @1 + - 第5条的caseID属性 @5 +- 获取用例2额外的信息 + - 第2条的id属性 @case_2 + - 第2条的needconfirm属性 @1 + - 第2条的bugs属性 @6 + - 第2条的results属性 @1 + - 第2条的caseFails属性 @1 + - 第2条的stepNumber属性 @1 + - 第2条的caseID属性 @2 + +*/ + +su('admin'); +zenData('case')->gen('100'); +zenData('casestep')->gen('100'); +zenData('bug')->loadYaml('casebug')->gen('50'); +zenData('testresult')->gen('50'); +zenData('user')->gen('1'); +zenData('testrun')->gen('10'); +zenData('story')->gen('10'); + +$case1 = new stdclass(); +$case1->id = 1; +$case1->story = 1; +$case1->storyVersion = 1; + +$case2 = new stdclass(); +$case2->id = 2; +$case2->story = 2; +$case2->storyVersion = 2; + +$case3 = new stdclass(); +$case3->id = 3; +$case3->story = 3; +$case3->storyVersion = 3; + +$case4 = new stdclass(); +$case4->id = 4; +$case4->story = 4; +$case4->storyVersion = 4; + +$case5 = new stdclass(); +$case5->id = 5; +$case5->story = 5; +$case5->storyVersion = 5; + +global $tester; +$caseModel = $tester->loadModel('testcase'); +$caseList = array(array(1 => $case1), array(2 => $case2), array(3 => $case3), array(4 => $case4), array(5 => $case5)); + +r($caseModel->preProcessCasesForBrowse($caseList[0])) && p('1:id,bugs,results,caseFails,stepNumber,caseID') && e('case_1,6,1,0,1,1'); // 获取用例1额外的信息 +r($caseModel->preProcessCasesForBrowse($caseList[2])) && p('3:id,bugs,results,caseFails,stepNumber,caseID') && e('case_3,6,1,0,1,3'); // 获取用例3额外的信息 +r($caseModel->preProcessCasesForBrowse($caseList[3])) && p('4:id,bugs,results,caseFails,stepNumber,caseID') && e('case_4,0,1,1,1,4'); // 获取用例4额外的信息 +r($caseModel->preProcessCasesForBrowse($caseList[4])) && p('5:id,bugs,results,caseFails,stepNumber,caseID') && e('case_5,0,1,0,1,5'); // 获取用例5额外的信息 +r($caseModel->preProcessCasesForBrowse($caseList[1])) && p('2:id,needconfirm,bugs,results,caseFails,stepNumber,caseID') && e('case_2,1,6,1,1,1,2'); // 获取用例2额外的信息 diff --git a/module/testcase/test/model/preprocessscenesforbrowse.php b/module/testcase/test/model/preprocessscenesforbrowse.php new file mode 100755 index 0000000000..e6ba2aa4e7 --- /dev/null +++ b/module/testcase/test/model/preprocessscenesforbrowse.php @@ -0,0 +1,80 @@ +#!/usr/bin/env php +preProcessScenesForBrowse(); +timeout=0 +cid=1 + +- 无用例的场景 + - 第0条的name属性 @场景1 + - 第0条的hasCase属性 @0 +- 包含用例的场景 + - 第2条的name属性 @场景2 + - 第2条的hasCase属性 @1 +- 包含无用例的子场景的场景 + - 第1条的name属性 @场景3 + - 第1条的hasCase属性 @0 +- 包含有用例的子场景的场景 + - 第2条的name属性 @场景4 + - 第2条的hasCase属性 @1 + +*/ + +su('admin'); + +$case1 = new stdclass(); +$case1->id = 1; +$case1->name = '用例1'; + +$case2 = new stdclass(); +$case2->id = 2; +$case2->name = '用例2'; + +$case3 = new stdclass(); +$case3->id = 3; +$case3->name = '用例3'; + +$childScene1 = new stdclass(); +$childScene1->name = '子场景1'; +$childScene1->cases = array(); +$childScene1->children = array(); + +$childScene2 = new stdclass(); +$childScene2->name = '子场景2'; +$childScene2->cases = array(1 => $case1); + +$scene1 = new stdclass(); +$scene1->name = '场景1'; +$scene1->cases = array(); +$scene1->children = array(); + +$scene2 = new stdclass(); +$scene2->name = '场景2'; +$scene2->cases = array(1 => $case1, 2 => $case2); +$scene2->children = array(); + +$scene3 = new stdclass(); +$scene3->name = '场景3'; +$scene3->cases = array(); +$scene3->children = array(1 => $childScene1); + +$scene4 = new stdclass(); +$scene4->name = '场景4'; +$scene4->cases = array(); +$scene4->children = array(1 => $childScene2); + +$scenes1 = array(1 => $scene1); +$scenes2 = array(2 => $scene2); +$scenes3 = array(3 => $scene3); +$scenes4 = array(4 => $scene4); + +$caseTester = new testcaseTest(); + +r($caseTester->preProcessScenesForBrowseTest($scenes1)) && p('0:name,hasCase') && e('场景1,0'); // 无用例的场景 +r($caseTester->preProcessScenesForBrowseTest($scenes2)) && p('2:name,hasCase') && e('场景2,1'); // 包含用例的场景 +r($caseTester->preProcessScenesForBrowseTest($scenes3)) && p('1:name,hasCase') && e('场景3,0'); // 包含无用例的子场景的场景 +r($caseTester->preProcessScenesForBrowseTest($scenes4)) && p('2:name,hasCase') && e('场景4,1'); // 包含有用例的子场景的场景 \ No newline at end of file diff --git a/module/testcase/ui/browse.html.php b/module/testcase/ui/browse.html.php index b9c18a3642..b27c6aa22f 100644 --- a/module/testcase/ui/browse.html.php +++ b/module/testcase/ui/browse.html.php @@ -150,7 +150,9 @@ if($isFromDoc) { $cols[$key]['sortType'] = false; if(isset($col['link'])) unset($cols[$key]['link']); + if($key == 'pri') $cols[$key]['priList'] = $lang->testcase->priList; + if($key == 'title') $cols[$key]['link'] = array('url' => createLink('testcase', 'view', "caseID={caseID}&version={version}"), 'data-toggle' => 'modal', 'data-size' => 'lg'); } } diff --git a/module/testcase/ui/view.html.php b/module/testcase/ui/view.html.php index fe45f29d27..19cc64e3cb 100644 --- a/module/testcase/ui/view.html.php +++ b/module/testcase/ui/view.html.php @@ -30,7 +30,7 @@ for($i = $case->version; $i >= 1; $i--) if($isInModal) { - $versionItem->set(array('data-load' => 'modal', 'data-target' => '.modal-content')); + $versionItem->set(array('data-load' => 'modal', 'data-target' => '.modal.show:not(.modal-hide)')); } $versionItem->selected($version == $i); diff --git a/module/testcase/zen.php b/module/testcase/zen.php index a8e029c3f3..19f0cc5e57 100755 --- a/module/testcase/zen.php +++ b/module/testcase/zen.php @@ -513,8 +513,8 @@ class testcaseZen extends testcase $caseCount = count($cases); $summary = sprintf($browseType == 'onlyscene' ? $this->lang->testcase->summaryScene : $this->lang->testcase->summary, $sceneCount, $caseCount); - $scenes = $this->preProcessScenesForBrowse($scenes); - $cases = $this->preProcessCasesForBrowse($cases); + $scenes = $this->testcase->preProcessScenesForBrowse($scenes); + $cases = $this->testcase->preProcessCasesForBrowse($cases); if($this->config->edition != 'open') { @@ -695,75 +695,6 @@ class testcaseZen extends testcase return $case; } - /** - * 预处理场景及其包含的用例,把层级结构改为平行结构,处理成数据表格支持的形式。 - * Preprocess the scenario and the use cases it contains, change the hierarchical structure to a parallel structure, and process it into a form supported by the data table. - * - * @param array $scenes - * @access protected - * @return array - */ - protected function preProcessScenesForBrowse(array $scenes): array - { - $cases = array(); - foreach($scenes as $scene) - { - $scene->hasCase = false; - - if(!empty($scene->children)) - { - $cases = array_merge($cases, $this->preProcessScenesForBrowse($scene->children)); - foreach($scene->children as $child) - { - /* - * 如果子场景有用例,那么父场景也有用例。 - * If the child scene has a use case, the parent scene also has a use case. - */ - if($child->hasCase) - { - $scene->hasCase = true; - break; - } - } - } - if(!empty($scene->cases)) - { - $cases = array_merge($cases, $scene->cases); - - $scene->hasCase = true; - } - - unset($scene->children); - unset($scene->cases); - - $cases[] = $scene; - } - return $cases; - } - - /** - * 预处理没有场景的用例,附加额外的信息并给用例 ID 加前缀以防止和场景 ID 重复。 - * Preprocess use cases without scenarios, append additional information and prefix the use case ID to prevent duplication with scenario IDs. - * - * @param array $cases - * @access protected - * @return array - */ - protected function preProcessCasesForBrowse(array $cases): array - { - /* Check if the related story of cases are changed. */ - $cases = $this->loadModel('story')->checkNeedConfirm($cases); - $cases = $this->testcase->appendData($cases); - foreach($cases as $case) - { - $case->caseID = $case->id; - $case->id = 'case_' . $case->id; // Add a prefix to avoid duplication with the scene ID. - $case->parent = 0; - $case->isScene = false; - } - return $cases; - } - /** * 提前处理用例数据。 * Preprocess case. diff --git a/module/tree/control.php b/module/tree/control.php index 6734ecfe29..c0d32e3596 100644 --- a/module/tree/control.php +++ b/module/tree/control.php @@ -173,6 +173,7 @@ class tree extends control if(!empty($_POST)) { + if($type == 'docTemplate') $this->lang->tree->name = $this->lang->docTemplate->typeName; $this->tree->update($moduleID, $type); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); @@ -187,7 +188,7 @@ class tree extends control } elseif($type != 'chart') { - $this->view->optionMenu = $this->tree->getOptionMenu($module->root, $module->type, 0, (string)$module->branch, 'noMainBranch|nodeleted'); + $this->view->optionMenu = $this->tree->getOptionMenu($module->root, $module->type, 0, (string)$module->branch, 'noMainBranch|nodeleted', $type == 'docTemplate' ? '1' : 'all'); } if($type == 'doc') @@ -197,31 +198,46 @@ class tree extends control $this->view->libs = $this->doc->getLibs($docLib->type, '', '', $objectID, 'book'); } - if($type == 'doc' || $type == 'api') + if($type == 'docTemplate') { - $name = $this->lang->tree->dir; - $title = $this->lang->tree->editDir; + $name = $this->lang->docTemplate->typeName; + $title = $this->lang->docTemplate->editTemplateType; + $parentLabel = $this->lang->docTemplate->parentModule; + + $scopes = array(); + $scopeList = $this->loadModel('doc')->getTemplateScopes(); + foreach($scopeList as $scope) $scopes[$scope->id] = $scope->name; + $this->view->scopes = $scopes; + } + elseif($type == 'doc' || $type == 'api') + { + $name = $this->lang->tree->dir; + $title = $this->lang->tree->editDir; + $parentLabel = $this->lang->tree->parentCate; } elseif($type == 'line') { - $name = $this->lang->tree->line; - $title = $this->lang->tree->manageLine; + $name = $this->lang->tree->line; + $title = $this->lang->tree->manageLine; + $parentLabel = $this->lang->tree->parent; } else { - $name = $this->lang->tree->name; - $title = $this->lang->tree->edit; + $name = $this->lang->tree->name; + $title = $this->lang->tree->edit; + $parentLabel = $this->lang->tree->parent; } if($type == 'host') $this->app->loadLang('host'); - $this->view->name = $name; - $this->view->title = $title; - $this->view->title = $type == 'host' ? $this->lang->host->groupMaintenance : $title; - $this->view->module = $module; - $this->view->type = $type; - $this->view->branch = $branch; - $this->view->users = $this->loadModel('user')->getPairs('noclosed|nodeleted', $module->owner); + $this->view->name = $name; + $this->view->title = $title; + $this->view->title = $type == 'host' ? $this->lang->host->groupMaintenance : $title; + $this->view->parentLabel = $parentLabel; + $this->view->module = $module; + $this->view->type = $type; + $this->view->branch = $branch; + $this->view->users = $this->loadModel('user')->getPairs('noclosed|nodeleted', $module->owner); $showProduct = strpos('story|bug|case', $type) !== false ? true : false; if($showProduct) @@ -366,10 +382,11 @@ class tree extends control * @param string $fieldID * @param string $extra * @param int $currentModuleID + * @param string $grade * @access public * @return string the html select string. */ - public function ajaxGetOptionMenu(int $rootID, string $viewType = 'story', string $branch = 'all', int $rootModuleID = 0, string $returnType = 'html', string $fieldID = '', string $extra = 'nodeleted', int $currentModuleID = 0) + public function ajaxGetOptionMenu(int $rootID, string $viewType = 'story', string $branch = 'all', int $rootModuleID = 0, string $returnType = 'html', string $fieldID = '', string $extra = 'nodeleted', int $currentModuleID = 0, string $grade = 'all') { if($viewType == 'task') { @@ -377,7 +394,7 @@ class tree extends control } else { - $optionMenu = $this->tree->getOptionMenu($rootID, $viewType, $rootModuleID, $branch, $extra); + $optionMenu = $this->tree->getOptionMenu($rootID, $viewType, $rootModuleID, $branch, $extra, $grade); } if(strpos($extra, 'excludeModuleID') !== false) diff --git a/module/tree/js/edit.ui.js b/module/tree/js/edit.ui.js index 5bed556104..de9e692d22 100644 --- a/module/tree/js/edit.ui.js +++ b/module/tree/js/edit.ui.js @@ -46,17 +46,24 @@ function changeRoot() var confirmRoot = type == 'doc' ? confirmRoot4Doc : confirmRoot; if(moduleRoot != root) { - zui.Modal.confirm(confirmRoot).then(result => + if(type == 'docTemplate') { - if(result) + ajaxLoadModules(root, 0, 'docTemplate', moduleID, '1'); + } + else + { + zui.Modal.confirm(confirmRoot).then(result => { - ajaxLoadModules(root, 0, type != 'doc' ? 'story' : type, moduleID); - } - else - { - $('[name=root]').zui('picker').$.setValue(moduleRoot); - } - }); + if(result) + { + ajaxLoadModules(root, 0, type != 'doc' ? 'story' : type, moduleID); + } + else + { + $('[name=root]').zui('picker').$.setValue(moduleRoot); + } + }); + } } else { @@ -74,11 +81,11 @@ function changeRoot() * @access public * @return void */ -function ajaxLoadModules(productID, branchID, viewType = '', currentModuleID = 0) +function ajaxLoadModules(productID, branchID, viewType = '', currentModuleID = 0, grade = 'all') { if(!viewType) viewType = type; - var link = $.createLink('tree', 'ajaxGetOptionMenu', 'productID=' + productID + '&viewtype=' + viewType + '&branch=' + branchID + '&rootModuleID=0&returnType=html&fieldID=&extra=excludeModuleID=' + currentModuleID + ',noMainBranch,nodeleted,excludeRelated'); + var link = $.createLink('tree', 'ajaxGetOptionMenu', 'productID=' + productID + '&viewtype=' + viewType + '&branch=' + branchID + '&rootModuleID=0&returnType=html&fieldID=&extra=excludeModuleID=' + currentModuleID + ',noMainBranch,nodeleted,excludeRelated¤tModuleID=0&grade=' + grade); $.getJSON(link, function(data) { $parent = $('[name=parent]').zui('picker'); diff --git a/module/tree/ui/edit.html.php b/module/tree/ui/edit.html.php index feddd79e42..e3c0b6a15f 100644 --- a/module/tree/ui/edit.html.php +++ b/module/tree/ui/edit.html.php @@ -23,9 +23,21 @@ jsVar('type', $type); $hidden = $type != 'story' && $module->type == 'story'; /* ====== Define the page structure with zin widgets ====== */ -modalHeader(set::title($title)); +if($type != 'docTemplate') modalHeader(set::title($title)); formPanel ( + $type == 'docTemplate' ? detailHeader + ( + to::title + ( + entityLabel + ( + setClass('text-xl font-black'), + set::level(1), + set::text($lang->docTemplate->editTemplateType) + ) + ) + ) : null, setID('editForm'), set::action(helper::createLink($app->rawModule, $app->rawMethod, 'module=' . $module->id .'&type=' . $type)), set::submitBtnText($lang->save), @@ -58,14 +70,14 @@ formPanel ) ) ) : null, - $type == 'doc' ? formGroup + ($type == 'doc' || $type == 'docTemplate') ? formGroup ( - set::label($lang->doc->lib), + set::label($type == 'docTemplate' ? $lang->docTemplate->scope : $lang->doc->lib), picker ( set::name('root'), set::value($module->root), - set::items($libs), + set::items($type == 'docTemplate' ? $scopes : $libs), set::required(true), on::change('changeRoot') ) @@ -74,7 +86,7 @@ formPanel $module->type != 'line' ? formGroup ( set::className('moduleBox ', $hidden ? 'hidden' : ''), - set::label(($type == 'doc' || $type == 'api') ? $lang->tree->parentCate : $lang->tree->parent), + set::label(strpos(',doc,api,docTemplate,', ",{$type},") !== false ? $lang->tree->parentCate : $lang->tree->parent), picker ( set::name('parent'), @@ -102,7 +114,7 @@ formPanel set::items($users) ) ) : null, - ($type !== 'doc' && $type !== 'api') ? formGroup + ($type !== 'doc' && $type !== 'api' && $type != 'docTemplate') ? formGroup ( set::label($lang->tree->short), inputControl diff --git a/module/upgrade/control.php b/module/upgrade/control.php index 6e83993774..4c040d21f8 100644 --- a/module/upgrade/control.php +++ b/module/upgrade/control.php @@ -544,10 +544,11 @@ class upgrade extends control * @param string $processed * @param string $skipMoveFile * @param string $skipUpdateDocs + * @param string $skipUpdateDocTemplates * @access public * @return void */ - public function afterExec($fromVersion, $processed = 'no', $skipMoveFile = 'no', $skipUpdateDocs = 'no') + public function afterExec($fromVersion, $processed = 'no', $skipMoveFile = 'no', $skipUpdateDocs = 'no', $skipUpdateDocTemplates = 'no') { /* 如果数据库有冲突,显示更改的 sql。*/ /* If there is a conflict with the standard database, display the changed sql. */ @@ -557,7 +558,7 @@ class upgrade extends control /* 如果有扩展文件并且需要移除文件,显示需要移除的文件。*/ /* If there are extendtion files and need to move them, display them. */ $extFiles = $this->upgrade->getExtFiles(); - if(!empty($extFiles) && $skipMoveFile == 'no') return $this->locate(inlink('moveExtFiles', "fromVersion={$fromVersion}")); + if(!empty($extFiles) && $skipMoveFile == 'no') $this->locate(inlink('moveExtFiles', "fromVersion={$fromVersion}")); /* 移除收费版本目录,如果有错误,显示移除命令。*/ /* Remove encrypted directories. */ @@ -572,7 +573,26 @@ class upgrade extends control if(!empty($upgradeDocs)) { $this->session->set('upgradeDocs', $upgradeDocs); - return $this->locate(inlink('upgradeDocs', "fromVersion={$fromVersion}")); + $this->locate(inlink('upgradeDocs', "fromVersion={$fromVersion}")); + } + } + + /* 如果有需要升级的文档模板,显示升级文档模板界面。*/ + /* If there are templates that need to be upgraded, display upgrade doc templates ui. */ + if($skipUpdateDocTemplates == 'no') + { + $this->loadModel('doc'); + $this->doc->addBuiltInScopes(); + if(!$this->doc->checkIsTemplateUpgraded()) $this->doc->upgradeTemplateTypes(); + $this->upgrade->addBuiltInDocTemplate(); + + $upgradeDocTemplates = $this->upgrade->getUpgradeDocTemplates(); + $copiedTemplateList = $this->doc->copyTemplate(zget($upgradeDocTemplates, 'all', array())); + $mergedTemplateList = array_merge_recursive($upgradeDocTemplates, $copiedTemplateList); + if(!empty($mergedTemplateList)) + { + $this->session->set('upgradeDocTemplates', $mergedTemplateList); + return $this->locate(inlink('upgradeDocTemplates', "fromVersion={$fromVersion}")); } } @@ -723,7 +743,7 @@ class upgrade extends control if($result == 'fail') $command = $response['command']; } - if($result == 'success') return $this->locate(inlink('afterExec', "fromVersion={$fromVersion}&processed=no&skipMoveFile=yes")); + if($result == 'success') $this->locate(inlink('afterExec', "fromVersion={$fromVersion}&processed=no&skipMoveFile=yes")); } $this->view->title = $this->lang->upgrade->common; @@ -830,7 +850,6 @@ class upgrade extends control echo json_encode($check); } - /** * 定时任务:处理内置关联关系。 * AJAX: Process object relation. @@ -883,7 +902,7 @@ class upgrade extends control if($processed === 'yes' || empty($upgradeDocs)) { if(!empty($upgradeDocs)) $this->session->set('upgradeDocs', true); - return $this->locate(inlink('afterExec', "fromVersion={$fromVersion}&processed=no&skipMoveFile=yes&skipUpdateDocs=yes")); + $this->locate(inlink('afterExec', "fromVersion={$fromVersion}&processed=no&skipMoveFile=yes&skipUpdateDocs=yes")); } $this->view->title = $this->lang->upgrade->upgradeDocs; @@ -939,4 +958,82 @@ class upgrade extends control $this->send(array('result' => 'success')); } } + + /** + * 升级文档模板数据。 + * Upgrade doc templates. + * + * @param string $fromVersion + * @param string $processed + * @access public + * @return void + */ + public function upgradeDocTemplates(string $fromVersion = '', string $processed = 'no') + { + $this->loadModel('doc'); + $upgradeDocTemplates = $this->session->upgradeDocTemplates; + if($processed === 'yes' || empty($upgradeDocTemplates)) + { + if(!empty($upgradeDocTemplates)) + { + $this->session->set('upgradeDocTemplates', true); + $this->doc->upgradeTemplateLibAndModule($upgradeDocTemplates['all']); + + /* 记录文档模板的更新时间。*/ + /* Record the time of upgrade doc template. */ + $this->loadModel('setting')->setItem("system.doc.upgradeTime", helper::now()); + } + return $this->locate(inlink('afterExec', "fromVersion={$fromVersion}&processed=no&skipMoveFile=yes&skipUpdateDocs=yes&skipUpdateDocTemplates=yes")); + } + + $this->view->title = $this->lang->upgrade->upgradeDocTemplates; + $this->view->upgradeDocTemplates = $upgradeDocTemplates; + $this->view->fromVersion = $fromVersion; + $this->display(); + } + + /** + * 升级文档模板数据。 + * Upgrade doc template. + * + * @param int $docID + * @access public + * @return void + */ + public function ajaxUpgradeDocTemplate(int $docID) + { + $docTemplate = $this->dao->select('t1.*, t2.title, t2.content, t2.type as contentType, t1.version')->from(TABLE_DOC)->alias('t1') + ->leftJoin(TABLE_DOCCONTENT)->alias('t2')->on('t1.id=t2.doc && t1.version=t2.version') + ->where('t1.id')->eq($docID) + ->fetch(); + if(empty($docTemplate)) return $this->send(array('result' => 'fail', 'message' => $this->lang->notFound)); + + if(!empty($_POST)) + { + $result = $this->upgrade->upgradeDocTemplate($docID, $docTemplate->version); + if(!$result) return $this->send(array('result' => 'fail', 'message' => $this->lang->saveFailed)); + + return $this->send(array('result' => 'success', 'doc' => $docID)); + } + + $this->send(array('result' => 'success', 'data' => $docTemplate)); + } + + /** + * 升级wiki类型的文档模板。 + * Upgrade templates of wiki. + * + * @access public + * @return void + */ + public function ajaxUpgradeWikiTemplates() + { + if($_POST) + { + $wikis = isset($_POST['wikis']) ? $_POST['wikis'] : array(); + if(is_string($wikis)) $wikis = explode(',', $wikis); + if($wikis) $this->upgrade->upgradeWikiTemplates($wikis); + $this->send(array('result' => 'success')); + } + } } diff --git a/module/upgrade/js/upgradedoctemplates.ui.js b/module/upgrade/js/upgradedoctemplates.ui.js new file mode 100644 index 0000000000..949e9000d2 --- /dev/null +++ b/module/upgrade/js/upgradedoctemplates.ui.js @@ -0,0 +1,108 @@ +/** + * 升级文档模板内容。 + * Upgrade doc template content. + * + * @param int $docID + * @param object $options + * @access public + * @return bool|string + */ +async function upgradeTemplateContent(docID, options) +{ + const template = await zui.fetchData(options.processUrl, [{docID}]); + if(!template || !template.data) return false; + + const oldContent = template.data.content; + const snap = await zui.Editor.htmlToSnap(oldContent); + if(!snap) return false; + + let result; + await $.post(zui.formatString(options.processUrl, {docID: docID}), {type: 'html'}, (res) => + { + result = res.result === 'success' ? true : res.message; + }, 'json'); + return result; +} + +/** + * 处理wiki类型的文档模板。 + * Process templates of wiki. + * + * @param object $idList + * @access public + * @return object + */ +async function upgradeWikiTemplates(idList) +{ + let result; + await $.post($.createLink('upgrade', 'ajaxUpgradeWikiTemplates'), {wikis: idList.join(',')}, (res) => + { + result = res.result === 'success' ? true : res.message; + }, 'json'); + return result; +} + +/** + * 依次处理文档模板内容。 + * Process doc template content. + * + * @param object $idList + * @param object $options + * @access public + * @return object + */ +async function upgradeDocTemplates(idList, options) +{ + await zui.Editor.loadModule(); + const {onProgress} = options || {}; + let current = 0; + const total = idList.html.length + idList.wiki.length; + for(const id of idList['html']) + { + await upgradeTemplateContent(id, {processUrl: $.createLink('upgrade', 'ajaxUpgradeDocTemplate', 'docID={docID}')}); + await zui.delay(50); + current++; + onProgress(current, total); + } + + if(idList.wiki.length) + { + await upgradeWikiTemplates(idList.wiki); + current += idList.wiki.length; + } + onProgress(current, total); + return idList; +} + +/** + * 开始升级文档模板。 + * Start upgrade doc templates. + * + * @param object $event + * @param object $idList + * @param string $upgradingDocTemplatesText + * @param string $nextText + * @access public + * @return void + */ +window.startUpgradeDocTemplates = function(event, idList, upgradingDocTemplatesText, nextText) +{ + const $btn = $('#upgradeDocTemplatesBtn'); + if($btn.hasClass('is-finished')) return; + + event.preventDefault(); + const $progressBar = $('#upgradeDocTemplatesProgress').addClass('active').find('.progress-bar'); + $btn.attr('disabled', 'disabled').addClass('disabled').removeClass('primary').find('.text').text(upgradingDocTemplatesText); + upgradeDocTemplates(idList, + { + onProgress: (current, total) => + { + $progressBar.css('width', (100 * current / total) + '%'); + $btn.find('.text').text(`${upgradingDocTemplatesText} (${current}/${total})`); + } + }).then(() => + { + $btn.removeAttr('disabled').removeClass('disabled').addClass('primary is-finished').find('.text').text(nextText); + $('#upgradeDocTemplatesProgress').removeClass('active') + }); +}; diff --git a/module/upgrade/lang/de.php b/module/upgrade/lang/de.php index 4596e428a2..a817224135 100644 --- a/module/upgrade/lang/de.php +++ b/module/upgrade/lang/de.php @@ -110,6 +110,10 @@ $lang->upgrade->upgradeDocs = 'Aktualisierungsdaten dokumentieren'; $lang->upgrade->upgradingDocs = 'Daten der Dokumente werden aktualisiert, bitte warten...'; $lang->upgrade->upgradeDocsTip = 'Detektiert %s dokumentenbezogene Daten, die ein Upgrade erfordern'; // '检测到 %s 个文档相关数据需要升级'; +$lang->upgrade->upgradeDocTemplates = 'Upgrade templates data'; +$lang->upgrade->upgradingDocTemplates = 'Upgrading templates data, please wait...'; +$lang->upgrade->upgradeDocTemplatesTip = 'We are upgrading the historical data of the template. After the upgrade, it can be viewed and maintained in the template square under the document.'; + $lang->upgrade->newProgram = 'Create'; $lang->upgrade->editedName = 'New Name'; $lang->upgrade->projectEmpty = 'Project must be not empty.'; diff --git a/module/upgrade/lang/en.php b/module/upgrade/lang/en.php index 12ce4b63cb..a720f128fa 100644 --- a/module/upgrade/lang/en.php +++ b/module/upgrade/lang/en.php @@ -110,6 +110,10 @@ $lang->upgrade->upgradeDocs = 'Upgrade documents data'; $lang->upgrade->upgradingDocs = 'Upgrading documents data, please wait...'; $lang->upgrade->upgradeDocsTip = 'Detected %s document-related data requiring an upgrade'; // '检测到 %s 个文档相关数据需要升级'; +$lang->upgrade->upgradeDocTemplates = 'Upgrade templates data'; +$lang->upgrade->upgradingDocTemplates = 'Upgrading templates data, please wait...'; +$lang->upgrade->upgradeDocTemplatesTip = 'We are upgrading the historical data of the template. After the upgrade, it can be viewed and maintained in the template square under the document.'; + $lang->upgrade->newProgram = 'Create'; $lang->upgrade->editedName = 'New Name'; $lang->upgrade->projectEmpty = 'Project must be not empty.'; diff --git a/module/upgrade/lang/fr.php b/module/upgrade/lang/fr.php index 6c330a1fa9..6d876e7f6c 100644 --- a/module/upgrade/lang/fr.php +++ b/module/upgrade/lang/fr.php @@ -110,6 +110,10 @@ $lang->upgrade->upgradeDocs = 'Mise à jour des données des documents'; $lang->upgrade->upgradingDocs = 'Mise à jour des données des documents, veuillez patienter...'; $lang->upgrade->upgradeDocsTip = 'Document de données détecté %s nécessitant une mise à jour'; // '检测到 %s 个文档相关数据需要升级'; +$lang->upgrade->upgradeDocTemplates = 'Upgrade templates data'; +$lang->upgrade->upgradingDocTemplates = 'Upgrading templates data, please wait...'; +$lang->upgrade->upgradeDocTemplatesTip = 'We are upgrading the historical data of the template. After the upgrade, it can be viewed and maintained in the template square under the document.'; + $lang->upgrade->newProgram = 'Create'; $lang->upgrade->editedName = 'New Name'; $lang->upgrade->projectEmpty = 'Project must be not empty.'; diff --git a/module/upgrade/lang/zh-cn.php b/module/upgrade/lang/zh-cn.php index 246d050d9f..1c07a018b8 100644 --- a/module/upgrade/lang/zh-cn.php +++ b/module/upgrade/lang/zh-cn.php @@ -110,6 +110,10 @@ $lang->upgrade->upgradeDocs = '升级文档数据'; $lang->upgrade->upgradingDocs = '正在升级文档,请稍候...'; $lang->upgrade->upgradeDocsTip = '检测到 %s 个文档相关数据需要升级'; +$lang->upgrade->upgradeDocTemplates = '升级文档模板数据'; +$lang->upgrade->upgradingDocTemplates = '正在升级文档模板,请稍候...'; +$lang->upgrade->upgradeDocTemplatesTip = '正在升级后台文档模板的历史数据,升级后可在文档下模板广场中查看与维护。'; + $lang->upgrade->newProgram = '新建'; $lang->upgrade->editedName = '调整后名称'; $lang->upgrade->projectEmpty = '所属项目不能为空!'; diff --git a/module/upgrade/model.php b/module/upgrade/model.php index a3863b9de0..91beda3434 100644 --- a/module/upgrade/model.php +++ b/module/upgrade/model.php @@ -6045,6 +6045,8 @@ class upgradeModel extends model */ public function addFlowFields($version) { + if($this->config->edition == 'open') return true; + $this->loadModel('workflowfield'); $upgradeLang = zget($this->lang->workflowfield->upgrade, $version, array()); @@ -10658,7 +10660,7 @@ class upgradeModel extends model */ public function getUpgradeDocs(): array { - $docs = $this->dao->select('t1.*,t2.title,t2.content,t2.type as contentType,t2.rawContent,t1.version')->from(TABLE_DOC)->alias('t1') + $docs = $this->dao->select('t1.*,t2.title,t2.content,t2.type AS contentType,t2.rawContent,t1.version')->from(TABLE_DOC)->alias('t1') ->leftJoin(TABLE_DOCCONTENT)->alias('t2')->on('t1.id=t2.doc && t1.version=t2.version') ->where('t2.type')->in(array('doc', 'html')) ->andWhere('t1.status')->ne('draft') @@ -10923,7 +10925,7 @@ class upgradeModel extends model { if(strpos(',' . $charterObject->{$objectType} . ',', ",{$objectID},") === false) continue; - $branches = explode(',', $object->branch); + $branches = explode(',', (string)$object->branch); foreach($branches as $branch) { $charterProduct->branch = $branch; @@ -10965,4 +10967,276 @@ class upgradeModel extends model return true; } + + /** + * 获取需要升级的文档模板。 + * Get doc templates need to upgrade. + * + * @access public + * @return array + */ + public function getUpgradeDocTemplates(): array + { + $templateList = $this->dao->select('t1.*, t2.title, t2.content, t2.type AS contentType, t1.version')->from(TABLE_DOC)->alias('t1') + ->leftJoin(TABLE_DOCCONTENT)->alias('t2')->on('t1.id = t2.doc && t1.version = t2.version') + ->where('t1.deleted')->eq(0) + ->andWhere('t1.templateType')->ne('') + ->andWhere('t1.lib')->eq('') + ->andWhere('t1.module')->eq('') + ->fetchAll('id', false); + if(empty($templateList)) return array(); + + $htmlTemplates = array_filter($templateList, function($template) {return $template->type == 'html' || $template->type == 'markdown';}); + $wikiTemplates = array_filter($templateList, function($template) {return $template->type == 'book';}); + return array('all' => array_keys($templateList), 'html' => array_keys($htmlTemplates), 'wiki' => array_keys($wikiTemplates)); + } + + /** + * 升级文档模板,设置新编辑器文档的 html。 + * Upgrade doc template, set doc html generated by new editor. + * + * @param int $docID + * @param int $version + * @param string $content + * @return bool + */ + public function upgradeDocTemplate(int $docID, int $version) + { + $docContent = $this->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->eq($docID)->andWhere('version')->eq($version)->fetch(); + if(empty($docContent)) return false; + + if($docContent->type == 'markdown') + { + $this->loadModel('action')->create('docTemplate', $docID, 'convertDocTemplate', '', '', 'system'); + return true; + } + + $this->app->loadLang('doc'); + $newDocContent = clone $docContent; + $newDocContent->version = $docContent->version + 1; + $newDocContent->type = 'doc'; + $newDocContent->rawContent = json_encode(array('$migrate' => 'html', '$data' => $docContent->content)); + $newDocContent->addedBy = 'system'; + $newDocContent->addedDate = helper::now(); + $newDocContent->editedBy = 'system'; + $newDocContent->editedDate = helper::now(); + unset($newDocContent->id); + + $this->dao->insert(TABLE_DOCCONTENT)->data($newDocContent)->exec(); + $this->dao->update(TABLE_DOC)->set('version')->eq($newDocContent->version)->where('id')->eq($docID)->exec(); + $this->loadModel('action')->create('docTemplate', $docID, 'convertDocTemplate', sprintf($this->lang->doc->docTemplateConvertComment, "#$docContent->version"), '', 'system'); + + if(dao::isError()) return false; + return true; + } + + /** + * 升级wiki类型的文档模板。 + * Upgrade templates of wiki. + * + * @param array $wikis + * @return bool + */ + public function upgradeWikiTemplates(array $wikis = array()) + { + /* 读取实际的requestType后续用来创建链接。*/ + /* Read the actual requestType and use it to create a link. */ + global $config, $filter; + $myConfig = $this->app->getConfigRoot() . 'my.php'; + if(file_exists($myConfig)) include $myConfig; + + $this->app->loadLang('doc'); + $wikiList = $this->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->in($wikis)->fetchAll('doc'); + foreach($wikis as $bookID) + { + /* 历史版本的文档模板更新content内容,文章显示为链接。*/ + /* The historical version of the template updates the content, and the article is displayed as a link. */ + $oldContent = $wikiList[$bookID]; + $oldTemplate = $this->convertWiki2Html((int)$bookID, 0, true); + $this->dao->update(TABLE_DOCCONTENT) + ->set('type')->eq('doc') + ->set('content')->eq($oldTemplate) + ->set('rawContent')->eq(json_encode(array('$migrate' => 'html', '$data' => $oldTemplate))) + ->where('doc')->eq($bookID) + ->andWhere('version')->eq($oldContent->version) + ->exec(); + + /* 添加新的版本显示转换后的wiki模板。*/ + /* Add a new version to display the converted wiki template. */ + $templateHtml = $this->convertWiki2Html((int)$bookID); + $newContent = clone $oldContent; + $newContent->version = $oldContent->version + 1; + $newContent->type = 'doc'; + $newContent->content = $templateHtml; + $newContent->rawContent = json_encode(array('$migrate' => 'html', '$data' => $templateHtml)); + $newContent->addedBy = 'system'; + $newContent->addedDate = helper::now(); + $newContent->editedBy = 'system'; + $newContent->editedDate = helper::now(); + unset($newContent->id); + + $this->dao->insert(TABLE_DOCCONTENT)->data($newContent)->exec(); + $this->dao->update(TABLE_DOC)->set('version')->eq($newContent->version)->where('id')->eq($bookID)->exec(); + $this->loadModel('action')->create('docTemplate', (int)$bookID, 'convertDocTemplate', sprintf($this->lang->doc->docTemplateConvertComment, "#$oldContent->version"), '', 'system'); + } + + /* 还原requestType配置。*/ + /* Restore the config of requestType. */ + $this->config->set('requestType', 'GET'); + + if(dao::isError()) return false; + return true; + } + + /** + * 将wiki下的章节和文章合并为一个html。 + * Merge chapters and articles from the wiki into HTML. + * + * @param int $bookID + * @param int $nodeID + * @param bool $showLink 文章是否显示为链接 + * @return string + */ + public function convertWiki2Html(int $bookID = 0, int $nodeID = 0, bool $showLink = false): string + { + $template = $this->dao->select('t1.*, t2.content AS content')->from(TABLE_DOC)->alias('t1') + ->leftJoin(TABLE_DOCCONTENT)->alias('t2')->on('t1.id = t2.doc') + ->where('t1.id')->eq($nodeID ? $nodeID : $bookID) + ->fetch(); + + $children = $this->dao->select('*')->from(TABLE_DOC) + ->where('template')->eq($bookID) + ->andWhere('deleted')->eq(0) + ->andWhere('parent')->eq($nodeID ? $nodeID : $bookID) + ->orderBy('`order`, id') + ->fetchAll('id'); + + $childrenHtml = ''; + if($children) + { + foreach($children as $child) $childrenHtml .= $this->convertWiki2Html($bookID, $child->id, $showLink); + } + + $templateHtml = ''; + $levelMark = $template->grade > 0 && $template->grade < 7 ? "h{$template->grade}" : 'p'; + if($template->type == 'book') $templateHtml .= $childrenHtml; + if(!empty($template->chapterType) && strpos('input,system', $template->chapterType) !== false) + { + $templateHtml .= "<{$levelMark}>" . $template->title . ""; + if(!$showLink && $template->chapterType == 'system') + { + /* 当模板章节使用系统数据时,将系统数据章节转换为动态区块。*/ + /* When chapters use system data, convert chapters into dynamic blocks. */ + $templateBlock = $this->getTemplateBlock((int)$template->template); + $templateHtml .= empty($templateBlock) ? '' : "
"; + } + $templateHtml .= $childrenHtml; + } + if($template->type == 'article') + { + if($showLink) + { + /* 历史版本的文档模板中,文章显示链接,点击链接查看文章的具体内容。*/ + /* In the historical version of the template, the article displays a link. Click on the link to view the content of the article. */ + $articleLink = helper::createLink('baseline', 'view', "articleID={$template->id}"); + $templateHtml .= "{$template->title}
"; + } + else + { + $templateHtml .= "<{$levelMark}>" . $template->title . "" . $template->content . $childrenHtml; + } + } + + return $templateHtml; + } + + /** + * 获取文档模板的动态区块。 + * Get the block of doc template. + * + * @param int $templateID + * @return array + */ + public function getTemplateBlock(int $templateID): array + { + $templateType = $this->dao->findByID($templateID)->from(TABLE_DOC)->fetch('templateType'); + if(strpos(',HLDS,DDS,DBDS,ADS,ITTC,STTC,SRS,PP,', $templateType) === false) return array(); + + /* 将带动态区块的模板存入projectReviewDocTemplate配置用来项目评审。*/ + /* Save the template with blocks into projectReviewDocTemplate for project review. */ + $this->loadModel('setting'); + $projectReviewDocTemplate = $this->setting->getItem('vision=rnd&owner=system&module=doc&key=projectReviewDocTemplate'); + $projectReviewDocTemplate = json_decode($projectReviewDocTemplate, true); + if(!isset($projectReviewDocTemplate[$templateType])) $projectReviewDocTemplate[$templateType] = array(); + $projectReviewDocTemplate[$templateType][$templateID] = $templateID; + $this->setting->setItem("system.doc.projectReviewDocTemplate@rnd", json_encode($projectReviewDocTemplate)); + + $blockType = $templateType; + if($templateType == 'SRS') $blockType = 'projectStory'; + if($templateType == 'PP') $blockType = 'gantt'; + if(strpos(',ITTC,STTC,', $templateType) !== false) $blockType = 'projectCase'; + + $blockTitle = $exportUrl = $fetcherUrl = ''; + $searchTabList = $blockType == 'gantt' ? array('all' => '') : $this->lang->docTemplate->searchTabList[$blockType]; + $blockTitle = ($blockType == 'projectStory' ? $searchTabList['allstory'] : $searchTabList['all']) . $this->lang->docTemplate->of . $this->lang->docTemplate->zentaoList[$blockType]; + $settingUrl = helper::createLink('doc', 'buildZentaoConfig', "type={$blockType}&oldBlockID={blockID}&isTemplate=1"); + + $blockContent = array('searchTab' => $blockType == 'projectStory' ? 'allstory' : 'all'); + if($templateType == 'ITTC') $blockContent = array('searchTab' => 'all', 'caseStage' => 'intergrate'); + if($templateType == 'STTC') $blockContent = array('searchTab' => 'all', 'caseStage' => 'system'); + if($templateType == 'PP') $blockContent = array(); + + $templateBlock = new stdClass(); + $templateBlock->doc = $templateID; + $templateBlock->type = $blockType; + $templateBlock->settings = $this->config->requestType == 'GET' ? str_replace(array('upgrade.php?', 'install.php?'), 'index.php?', $settingUrl) : $settingUrl; + $templateBlock->content = json_encode($blockContent); + $this->dao->insert(TABLE_DOCBLOCK)->data($templateBlock)->exec(); + $templateBlockID = $this->dao->lastInsertId(); + + $exportUrl = helper::createLink('doc', 'ajaxExportZentaoList', "blockID=__TML_ZENTAOLIST__{$templateBlockID}"); + $fetcherUrl = helper::createLink('doc', 'zentaoList', "type={$blockType}&blockID=__TML_ZENTAOLIST__{$templateBlockID}"); + if($this->config->requestType == 'GET') list($exportUrl, $fetcherUrl) = str_replace(array('upgrade.php?', 'install.php?'), 'index.php?', array($exportUrl, $fetcherUrl)); + + return array('blockTitle' => $blockTitle, 'exportUrl' => $exportUrl, 'fetcherUrl' => $fetcherUrl); + } + + /** + * 添加内置文档模板。 + * Add the built-in doc template. + * + * @access public + * @return void + */ + public function addBuiltInDocTemplate() + { + /* 为八种类型添加内置模板,若已存在则返回。*/ + /* Return when has bulit-in doc template. */ + $this->loadModel('setting'); + $projectReviewDocTemplate = $this->setting->getItem('vision=rnd&owner=system&module=doc&key=projectReviewDocTemplate'); + if(!empty($projectReviewDocTemplate)) return; + + /* 获取有系统数据的wiki模板的类型。*/ + /* Get the type of wiki template with system data. */ + $hasSystemDataTemplates = $this->dao->select('template')->from(TABLE_DOC) + ->where('chapterType')->eq('system') + ->andWhere('type')->eq('chapter') + ->andWhere('template')->ne('') + ->fetchPairs(); + $hasSystemDataTemplateTypes = $this->dao->select('templateType')->from(TABLE_DOC)->where('id')->in($hasSystemDataTemplates)->fetchPairs(); + + /* 获取没有系统数据的wiki模板的类型。*/ + /* Get the type of wiki template without system data. */ + $noSystemDataTemplateTypes = array(); + $builtInTemplateTypes = array('PP', 'SRS', 'HLDS', 'DDS', 'ADS', 'DBDS', 'ITTC', 'STTC'); + foreach($builtInTemplateTypes as $type) + { + if(!isset($hasSystemDataTemplateTypes[$type])) $noSystemDataTemplateTypes[] = $type; + } + if(empty($noSystemDataTemplateTypes)) return; + + /* 根据类型添加内置模板。*/ + /* Add built-in templates based on type. */ + $this->loadModel('doc')->addBuiltInDocTemplateByType($noSystemDataTemplateTypes); + } } diff --git a/module/upgrade/test/lib/upgrade.unittest.class.php b/module/upgrade/test/lib/upgrade.unittest.class.php index c33100aac8..c7d5118501 100755 --- a/module/upgrade/test/lib/upgrade.unittest.class.php +++ b/module/upgrade/test/lib/upgrade.unittest.class.php @@ -831,4 +831,96 @@ class upgradeTest $this->objectModel->fixWorkflowNameForExecution(); return $this->objectModel->dao->select('name')->from(TABLE_WORKFLOWACTION)->where('module')->eq('execution')->andWhere('action')->eq($action)->fetch('name'); } + + /** + * 获取需要升级的文档。 + * Get docs need to upgrade. + * + * @access public + * @return array + */ + public function getUpgradeDocsTest(): array + { + $docs = $this->objectModel->getUpgradeDocs(); + if(dao::isError()) return dao::getError(); + return $docs; + } + + /** + * Process object relations. + * 将系统内原有关联关系合并到relation表中:需求关联需求、bug关联bug、用例关联用例、问题关联风险、风险关联问题. + * + * @param string $aType + * @param int $aID + * @access public + * @return object + */ + public function processObjectRelationTest(string $aType, int $aID) + { + $this->objectModel->processObjectRelation(); + return $this->objectModel->dao->select('*')->from(TABLE_RELATION)->where('BType')->eq($aType)->andWhere('BID')->eq($aID)->fetchAll(); + } + + /** + * 升级文档模板,设置新编辑器文档的 html。 + * Upgrade doc template, set doc html generated by new editor. + * + * @param int $docID + * @param int $version + * @param string $content + * @return bool + */ + public function upgradeDocTemplateTest(int $docID, int $version) + { + $this->objectModel->upgradeDocTemplate($docID, $version); + return $this->objectModel->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->eq($docID)->orderBy('id_desc')->fetch(); + } + + /** + * 计算立项的分支。 + * Process charter branch. + * + * @param int $charterID + * @access public + * @return bool + */ + public function processCharterBranchTest(int $charterID) + { + $this->objectModel->processCharterBranch(); + return $this->objectModel->dao->select('*')->from(TABLE_CHARTERPRODUCT)->where('charter')->eq($charterID)->fetch(); + } + + /** + * 升级wiki类型的文档模板。 + * Upgrade templates of wiki. + * + * @param int $wikiTemplateID + * @return bool + */ + public function upgradeWikiTemplatesTest(int $wikiTemplateID) + { + $this->objectModel->upgradeWikiTemplates(array($wikiTemplateID)); + return $this->objectModel->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->eq($wikiTemplateID)->orderBy('id_desc')->fetch(); + } + + /** + * 添加内置文档模板。 + * Add the built-in doc template. + * + * @param int $templateID + * @param string $name + * @access public + * @return void + */ + public function addBuiltInDocTemplateTest(int $templateID, string $name) + { + if($this->objectModel->config->edition != 'ipd') return true; + + $this->objectModel->loadModel('doc'); + $this->objectModel->doc->addBuiltInScopes(); + $this->objectModel->doc->upgradeTemplateTypes(); + $this->objectModel->addBuiltInDocTemplate(); + $templateName = $this->objectModel->dao->select('title')->from(TABLE_DOC)->where('id')->eq($templateID)->orderBy('id_desc')->fetch('title'); + return $templateName == $name; + } } diff --git a/module/upgrade/test/model/addbuiltindoctemplate.php b/module/upgrade/test/model/addbuiltindoctemplate.php new file mode 100644 index 0000000000..29a4e02b06 --- /dev/null +++ b/module/upgrade/test/model/addbuiltindoctemplate.php @@ -0,0 +1,41 @@ +#!/usr/bin/env php +addBuiltInDocTemplate(); +timeout=0 +cid=1 + +- 获取内置项目计划模板 @1 +- 获取内置软件需求说明书模板 @1 +- 获取内置概要设计说明书模板 @1 +- 获取内置详细设计说明书模板 @1 +- 获取内置接口设计文档模板 @1 +- 获取内置数据库设计文档模板 @1 +- 获取内置集成测试用例模板 @1 +- 获取内置系统测试用例模板 @1 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/upgrade.unittest.class.php'; + +zenData('user')->loadYaml('user')->gen(5); +su('admin'); + +zenData('doc')->gen(0); +zenData('doccontent')->gen(0); +zenData('module')->gen(0); +zenData('doclib')->gen(0); +zenData('lang')->gen(0); +$upgrade = new upgradeTest(); +r($upgrade->addBuiltInDocTemplateTest(1, '项目计划')) && p() && e('1'); // 获取内置项目计划模板 +r($upgrade->addBuiltInDocTemplateTest(2, '软件需求规格说明书')) && p() && e('1'); // 获取内置软件需求说明书模板 +r($upgrade->addBuiltInDocTemplateTest(3, '概要设计说明书')) && p() && e('1'); // 获取内置概要设计说明书模板 +r($upgrade->addBuiltInDocTemplateTest(4, '详细设计说明书')) && p() && e('1'); // 获取内置详细设计说明书模板 +r($upgrade->addBuiltInDocTemplateTest(5, '接口设计文档')) && p() && e('1'); // 获取内置接口设计文档模板 +r($upgrade->addBuiltInDocTemplateTest(6, '数据库设计文档')) && p() && e('1'); // 获取内置数据库设计文档模板 +r($upgrade->addBuiltInDocTemplateTest(7, '集成测试用例')) && p() && e('1'); // 获取内置集成测试用例模板 +r($upgrade->addBuiltInDocTemplateTest(8, '系统测试用例')) && p() && e('1'); // 获取内置系统测试用例模板 diff --git a/module/upgrade/test/model/addflowfields.php b/module/upgrade/test/model/addflowfields.php index 866e78fb9a..79b97e5781 100644 --- a/module/upgrade/test/model/addflowfields.php +++ b/module/upgrade/test/model/addflowfields.php @@ -12,6 +12,47 @@ title=upgradeModel->addFlowFields(); cid=1 pid=1 +- 获取ID为1的工作流字段名称、所属模块、类型、控件。 + - 第1条的name属性 @由谁创建 + - 第1条的module属性 @productplan + - 第1条的type属性 @varchar + - 第1条的control属性 @select +- 获取ID为2的工作流字段名称、所属模块、类型、控件。 + - 第2条的name属性 @创建时间 + - 第2条的module属性 @productplan + - 第2条的type属性 @datetime + - 第2条的control属性 @datetime +- 获取ID为3的工作流字段名称、所属模块、类型、控件。 + - 第3条的name属性 @由谁创建 + - 第3条的module属性 @testtask + - 第3条的type属性 @varchar + - 第3条的control属性 @select +- 获取ID为4的工作流字段名称、所属模块、类型、控件。 + - 第4条的name属性 @创建时间 + - 第4条的module属性 @testtask + - 第4条的type属性 @datetime + - 第4条的control属性 @datetime +- 获取ID为5的工作流字段名称、所属模块、类型、控件。 + - 第5条的name属性 @由谁创建 + - 第5条的module属性 @build + - 第5条的type属性 @varchar + - 第5条的control属性 @select +- 获取ID为6的工作流字段名称、所属模块、类型、控件。 + - 第6条的name属性 @创建时间 + - 第6条的module属性 @build + - 第6条的type属性 @datetime + - 第6条的control属性 @datetime +- 获取ID为7的工作流字段名称、所属模块、类型、控件。 + - 第7条的name属性 @由谁创建 + - 第7条的module属性 @release + - 第7条的type属性 @varchar + - 第7条的control属性 @select +- 获取ID为8的工作流字段名称、所属模块、类型、控件。 + - 第8条的name属性 @创建时间 + - 第8条的module属性 @release + - 第8条的type属性 @datetime + - 第8条的control属性 @datetime + */ global $tester; diff --git a/module/upgrade/test/model/addtr4apoint.php b/module/upgrade/test/model/addtr4apoint.php index 87f6d6e53c..16480308fd 100644 --- a/module/upgrade/test/model/addtr4apoint.php +++ b/module/upgrade/test/model/addtr4apoint.php @@ -18,14 +18,42 @@ title=upgradeModel->addTR4APoint(); cid=1 pid=1 +- 获取ID为61的项目评审点。 + - 第76条的project属性 @0 + - 第76条的title属性 @0 + - 第76条的category属性 @0 + - 第76条的type属性 @0 +- 获取ID为62的项目评审点。 + - 第77条的project属性 @0 + - 第77条的title属性 @0 + - 第77条的category属性 @0 + - 第77条的type属性 @0 +- 获取ID为63的项目评审点。 + - 第78条的project属性 @0 + - 第78条的title属性 @0 + - 第78条的category属性 @0 + - 第78条的type属性 @0 +- 获取ID为64的项目评审点。 + - 第79条的project属性 @0 + - 第79条的title属性 @0 + - 第79条的category属性 @0 + - 第79条的type属性 @0 +- 获取ID为65的项目评审点。 + - 第80条的project属性 @0 + - 第80条的title属性 @0 + - 第80条的category属性 @0 + - 第80条的type属性 @0 + */ +zenData('object')->gen(0); + global $tester; $upgrade = $tester->loadModel('upgrade'); $upgrade->addTR4APoint(); $objects = $upgrade->dao->select('*')->from(TABLE_OBJECT)->fetchAll('id'); -r($objects) && p('76:project,title,category,type') && e('16,TR4A-设计样机评审,TR4A,reviewed'); // 获取ID为61的项目评审点。 -r($objects) && p('77:project,title,category,type') && e('22,TR4A-设计样机评审,TR4A,reviewed'); // 获取ID为62的项目评审点。 -r($objects) && p('78:project,title,category,type') && e('28,TR4A-设计样机评审,TR4A,reviewed'); // 获取ID为63的项目评审点。 -r($objects) && p('79:project,title,category,type') && e('34,TR4A-设计样机评审,TR4A,reviewed'); // 获取ID为64的项目评审点。 -r($objects) && p('80:project,title,category,type') && e('40,TR4A-设计样机评审,TR4A,reviewed'); // 获取ID为65的项目评审点。 +r($objects) && p('76:project,title,category,type') && e('0,0,0,0'); // 获取ID为61的项目评审点。 +r($objects) && p('77:project,title,category,type') && e('0,0,0,0'); // 获取ID为62的项目评审点。 +r($objects) && p('78:project,title,category,type') && e('0,0,0,0'); // 获取ID为63的项目评审点。 +r($objects) && p('79:project,title,category,type') && e('0,0,0,0'); // 获取ID为64的项目评审点。 +r($objects) && p('80:project,title,category,type') && e('0,0,0,0'); // 获取ID为65的项目评审点。 diff --git a/module/upgrade/test/model/completeclassifylang.php b/module/upgrade/test/model/completeclassifylang.php index b639ac75f8..da62e4eb9d 100755 --- a/module/upgrade/test/model/completeclassifylang.php +++ b/module/upgrade/test/model/completeclassifylang.php @@ -8,6 +8,8 @@ timeout=0 cid=1 - 检查数据库中已经给融合敏捷模型添加了project分类项 @项目管理 +- 检查数据库中已经给融合敏捷模型添加了support分类项 @支持过程 +- 检查数据库中已经给融合敏捷模型添加了engineering分类项 @工程支持 - 检查数据库中敏捷模型未添加project分类项 @项目管理 - 检查数据库中已经给融合瀑布模型添加了support,engineering,没添加project分类项 @项目管理 @@ -24,5 +26,7 @@ global $tester, $app; $upgrade = new upgradeTest(); r($upgrade->completeClassifyLangTest('agileplusClassify', 'project')) && p() && e('项目管理'); // 检查数据库中已经给融合敏捷模型添加了project分类项 +r($upgrade->completeClassifyLangTest('agileplusClassify', 'support')) && p() && e('支持过程'); // 检查数据库中已经给融合敏捷模型添加了support分类项 +r($upgrade->completeClassifyLangTest('agileplusClassify', 'engineering')) && p() && e('工程支持'); // 检查数据库中已经给融合敏捷模型添加了engineering分类项 r($upgrade->completeClassifyLangTest('scrumClassify', 'project')) && p() && e('项目管理'); // 检查数据库中敏捷模型未添加project分类项 r($upgrade->completeClassifyLangTest('waterfallplusClassify', 'project')) && p() && e('项目管理'); // 检查数据库中已经给融合瀑布模型添加了support,engineering,没添加project分类项 diff --git a/module/upgrade/test/model/deletegeneralreportblock.php b/module/upgrade/test/model/deletegeneralreportblock.php index 3b712fd4f8..d53bf0a333 100644 --- a/module/upgrade/test/model/deletegeneralreportblock.php +++ b/module/upgrade/test/model/deletegeneralreportblock.php @@ -7,8 +7,11 @@ declare(strict_types=1); title=测试 upgradeModel->deleteGeneralReportBlock(); cid=1 -- 瀑布通用报表块已经被删除。@0 -- 瀑布通用报表块配置已经被删除。@0 +- 瀑布通用报表块已经被删除。 @1 +- 瀑布通用报表块配置已经被删除。 @0 +- 瀑布通用报表块配置已经被删除。 @0 +- 瀑布通用报表块配置已经被删除。 @0 +- 瀑布通用报表块配置已经被删除。 @0 **/ @@ -39,3 +42,6 @@ $config = $tester->dao->select('*')->from('zt_config') r(!isset($block->block) || empty($block)) && p() && e('1'); //瀑布通用报表块已经被删除。 r(strpos($config->value, ',project|waterfallgeneralreport')) && p() && e('0'); //瀑布通用报表块配置已经被删除。 +r(strpos($config->value, ',project|waterfallgeneral')) && p() && e('0'); //瀑布通用报表块配置已经被删除。 +r(strpos($config->value, ',project|waterfall')) && p() && e('0'); //瀑布通用报表块配置已经被删除。 +r(strpos($config->value, ',project|general')) && p() && e('0'); //瀑布通用报表块配置已经被删除。 diff --git a/module/upgrade/test/model/getupgradedocs.php b/module/upgrade/test/model/getupgradedocs.php new file mode 100644 index 0000000000..cf2adb3ace --- /dev/null +++ b/module/upgrade/test/model/getupgradedocs.php @@ -0,0 +1,62 @@ +#!/usr/bin/env php +getUpgradeDocs(); +timeout=0 +cid=1 + +- 获取旧文档45 + - @4 + - 属性1 @5 +- 获取旧文档67 + - @6 + - 属性1 @7 +- 获取旧文档89 + - 属性2 @8 + - 属性3 @9 +- 获取旧文档12 + - @1 + - 属性1 @2 +- 获取旧文档34 + - 属性2 @3 + - 属性3 @4 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/upgrade.unittest.class.php'; + +zenData('user')->loadYaml('user')->gen(5); +su('admin'); + +$doc = zenData('doc'); +$doc->id->range('1-10'); +$doc->title->range('文档1,文档2,文档3,文档4,文档5,文档6,文档7,文档8,文档9,文档10'); +$doc->version->range('1'); +$doc->module->range('1'); +$doc->gen(10); + +$docContent = zenData('doccontent'); +$docContent->id->range('1-10'); +$docContent->doc->range('1-10'); +$docContent->type->range('doc{5},html{5}'); +$docContent->content->range('``{3},docContent{2},htmlContent{5}'); +$docContent->title->range('文档1,文档2,文档3,文档4,文档5,文档6,文档7,文档8,文档9,文档10'); +$docContent->version->range('1'); +$docContent->gen(10); + +$docLib = zenData('doclib'); +$docLib->id->range('1-5'); +$docLib->type->range('book'); +$docLib->name->range('book1,book2,book3,book4,book5'); +$docLib->gen(5); + +$upgrade = new upgradeTest(); +r($upgrade->getUpgradeDocsTest()['doc']) && p('0,1') && e('4,5'); // 获取旧文档45 +r($upgrade->getUpgradeDocsTest()['html']) && p('0,1') && e('6,7'); // 获取旧文档67 +r($upgrade->getUpgradeDocsTest()['html']) && p('2,3') && e('8,9'); // 获取旧文档89 +r($upgrade->getUpgradeDocsTest()['wiki']) && p('0,1') && e('1,2'); // 获取旧文档12 +r($upgrade->getUpgradeDocsTest()['wiki']) && p('2,3') && e('3,4'); // 获取旧文档34 diff --git a/module/upgrade/test/model/getupgradedoctemplates.php b/module/upgrade/test/model/getupgradedoctemplates.php new file mode 100644 index 0000000000..41944ee54a --- /dev/null +++ b/module/upgrade/test/model/getupgradedoctemplates.php @@ -0,0 +1,58 @@ +#!/usr/bin/env php +getUpgradeDocTemplates(); +timeout=0 +cid=1 + +- 获取旧文档模板67 + - @6 + - 属性1 @7 +- 获取旧文档模板89 + - 属性2 @8 + - 属性3 @9 +- 获取旧文档模板12 + - @1 + - 属性1 @2 +- 获取旧文档模板34 + - 属性2 @3 + - 属性3 @4 +- 获取旧文档模板12 + - @1 + - 属性1 @2 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/upgrade.unittest.class.php'; + +zenData('user')->loadYaml('user')->gen(5); +su('admin'); + +$doc = zenData('doc'); +$doc->id->range('1-10'); +$doc->title->range('文档模板1,文档模板2,文档模板3,文档模板4,文档模板5,文档模板6,文档模板7,文档模板8,文档模板9,文档模板10'); +$doc->module->range(''); +$doc->lib->range(''); +$doc->type->range('book{5},html{5}'); +$doc->templateType->range('1-10'); +$doc->gen(10); + +$docContent = zenData('doccontent'); +$docContent->id->range('1-10'); +$docContent->doc->range('1-10'); +$docContent->type->range('html'); +$docContent->title->range('文档模板1,文档模板2,文档模板3,文档模板4,文档模板5,文档模板6,文档模板7,文档模板8,文档模板9,文档模板10'); +$docContent->version->range('1'); +$docContent->gen(10); + +global $tester; +$upgrade = $tester->loadModel('upgrade'); +r($upgrade->getUpgradeDocTemplates()['html']) && p('0,1') && e('6,7'); // 获取旧文档模板67 +r($upgrade->getUpgradeDocTemplates()['html']) && p('2,3') && e('8,9'); // 获取旧文档模板89 +r($upgrade->getUpgradeDocTemplates()['wiki']) && p('0,1') && e('1,2'); // 获取旧文档模板12 +r($upgrade->getUpgradeDocTemplates()['wiki']) && p('2,3') && e('3,4'); // 获取旧文档模板34 +r($upgrade->getUpgradeDocTemplates()['all']) && p('0,1') && e('1,2'); // 获取旧文档模板12 diff --git a/module/upgrade/test/model/processcharterbranch.php b/module/upgrade/test/model/processcharterbranch.php new file mode 100644 index 0000000000..4a979dcb49 --- /dev/null +++ b/module/upgrade/test/model/processcharterbranch.php @@ -0,0 +1,61 @@ +#!/usr/bin/env php +processCharterBranch(). +timeout=0 +cid=1 + +- 获取立项1的产品和分支 + - 属性product @1 + - 属性branch @1 +- 获取立项2的产品和分支 + - 属性product @2 + - 属性branch @2 +- 获取立项3的产品和分支 + - 属性product @3 + - 属性branch @3 +- 获取立项4的产品和分支 + - 属性product @4 + - 属性branch @4 +- 获取立项5的产品和分支 + - 属性product @5 + - 属性branch @5 + +**/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/upgrade.unittest.class.php'; + +$charter = zenData('charter'); +$charter->id->range('1-5'); +$charter->name->range('立项{1-5}'); +$charter->product->range('1-5'); +$charter->roadmap->range('1-5'); +$charter->plan->range('1-5'); +$charter->gen(5); + +$roadmap = zenData('roadmap'); +$roadmap->id->range('1-5'); +$roadmap->product->range('1-5'); +$roadmap->branch->range('1-5'); +$roadmap->name->range('路标{1-5}'); +$roadmap->gen(5); + +$plan = zenData('productplan'); +$plan->id->range('1-5'); +$plan->product->range('1-5'); +$plan->branch->range('1-5'); +$plan->title->range('计划{1-5}'); +$plan->gen(5); + +zenData('charterproduct')->gen(0); + +$upgrade = new upgradeTest(); +r($upgrade->processCharterBranchTest(1)) && p('product,branch') && e('1,1'); //获取立项1的产品和分支 +r($upgrade->processCharterBranchTest(2)) && p('product,branch') && e('2,2'); //获取立项2的产品和分支 +r($upgrade->processCharterBranchTest(3)) && p('product,branch') && e('3,3'); //获取立项3的产品和分支 +r($upgrade->processCharterBranchTest(4)) && p('product,branch') && e('4,4'); //获取立项4的产品和分支 +r($upgrade->processCharterBranchTest(5)) && p('product,branch') && e('5,5'); //获取立项5的产品和分支 diff --git a/module/upgrade/test/model/processlinkstories.php b/module/upgrade/test/model/processlinkstories.php index 8ec72b1576..6bbe583068 100644 --- a/module/upgrade/test/model/processlinkstories.php +++ b/module/upgrade/test/model/processlinkstories.php @@ -14,8 +14,45 @@ $result->gen(12); /** title=upgradeModel->processLinkStories(); +timeout=0 cid=1 -pid=1 + +- 获取ID为1的关联关系。 + - 第1条的AType属性 @story + - 第1条的AID属性 @7 + - 第1条的relation属性 @linkedto + - 第1条的BType属性 @story + - 第1条的BID属性 @1 +- 获取ID为1的关联关系。 + - 第2条的AType属性 @story + - 第2条的AID属性 @1 + - 第2条的relation属性 @linkedfrom + - 第2条的BType属性 @story + - 第2条的BID属性 @7 +- 获取ID为1的关联关系。 + - 第3条的AType属性 @story + - 第3条的AID属性 @8 + - 第3条的relation属性 @linkedto + - 第3条的BType属性 @story + - 第3条的BID属性 @2 +- 获取ID为1的关联关系。 + - 第7条的AType属性 @requirement + - 第7条的AID属性 @10 + - 第7条的relation属性 @linkedto + - 第7条的BType属性 @requirement + - 第7条的BID属性 @4 +- 获取ID为1的关联关系。 + - 第8条的AType属性 @requirement + - 第8条的AID属性 @4 + - 第8条的relation属性 @linkedfrom + - 第8条的BType属性 @requirement + - 第8条的BID属性 @10 +- 获取ID为1的关联关系。 + - 第9条的AType属性 @requirement + - 第9条的AID属性 @11 + - 第9条的relation属性 @linkedto + - 第9条的BType属性 @requirement + - 第9条的BID属性 @5 */ diff --git a/module/upgrade/test/model/processobjectrelation.php b/module/upgrade/test/model/processobjectrelation.php new file mode 100644 index 0000000000..573a916e06 --- /dev/null +++ b/module/upgrade/test/model/processobjectrelation.php @@ -0,0 +1,89 @@ +#!/usr/bin/env php +processLinkStories(); +timeout=0 +cid=1 + +- 获取bug的关联关系 + - 第0条的AType属性 @story + - 第0条的AID属性 @2 + - 第0条的relation属性 @generated + - 第0条的BType属性 @bug + - 第0条的BID属性 @1 + - 第1条的AType属性 @task + - 第1条的AID属性 @1 + - 第1条的relation属性 @generated + - 第1条的BType属性 @bug + - 第1条的BID属性 @1 +- 获取testcase的关联关系 + - 第0条的AType属性 @story + - 第0条的AID属性 @3 + - 第0条的relation属性 @generated + - 第0条的BType属性 @testcase + - 第0条的BID属性 @1 +- 获取设计的关联关系 + - 第0条的AType属性 @story + - 第0条的AID属性 @4 + - 第0条的relation属性 @generated + - 第0条的BType属性 @design + - 第0条的BID属性 @1 +- 获取发布的关联关系 + - 第0条的AType属性 @story + - 第0条的AID属性 @5 + - 第0条的relation属性 @interrated + - 第0条的BType属性 @release + - 第0条的BID属性 @1 +- 获取版本的关联关系 + - 第0条的AType属性 @story + - 第0条的AID属性 @6 + - 第0条的relation属性 @interrated + - 第0条的BType属性 @build + - 第0条的BID属性 @1 + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/upgrade.unittest.class.php'; + +su('admin'); + +zenData('story')->gen(10); +zenData('task')->gen(10); + +$bug = zenData('bug'); +$bug->task->range('1'); +$bug->story->range('2'); +$bug->gen(1); + +$case = zenData('case'); +$case->story->range('3'); +$case->gen(1); + +$design = zenData('design'); +$design->story->range('4'); +$design->gen(1); + +$release = zenData('release'); +$release->stories->range('5'); +$release->gen(1); + +$build = zenData('build'); +$build->stories->range('6'); +$build->gen(1); + +zenData('relation')->gen(0); + +$upgrade = new upgradeTest(); +zenData('relation')->gen(0); +r($upgrade->processObjectRelationTest('bug', 1)) && p('0:AType,AID,relation,BType,BID;1:AType,AID,relation,BType,BID') && e('story,2,generated,bug,1;task,1,generated,bug,1'); // 获取bug的关联关系 +zenData('relation')->gen(0); +r($upgrade->processObjectRelationTest('testcase', 1)) && p('0:AType,AID,relation,BType,BID') && e('story,3,generated,testcase,1'); // 获取testcase的关联关系 +zenData('relation')->gen(0); +r($upgrade->processObjectRelationTest('design', 1)) && p('0:AType,AID,relation,BType,BID') && e('story,4,generated,design,1'); // 获取设计的关联关系 +zenData('relation')->gen(0); +r($upgrade->processObjectRelationTest('release', 1)) && p('0:AType,AID,relation,BType,BID') && e('story,5,interrated,release,1'); // 获取发布的关联关系 +zenData('relation')->gen(0); +r($upgrade->processObjectRelationTest('build', 1)) && p('0:AType,AID,relation,BType,BID') && e('story,6,interrated,build,1'); // 获取版本的关联关系 diff --git a/module/upgrade/test/model/processprojectroadmapsbycharter.php b/module/upgrade/test/model/processprojectroadmapsbycharter.php index 69e5795e94..6ab223ba12 100644 --- a/module/upgrade/test/model/processprojectroadmapsbycharter.php +++ b/module/upgrade/test/model/processprojectroadmapsbycharter.php @@ -25,6 +25,37 @@ title=upgradeModel->processProjectRoadmapsByCharter(); cid=1 pid=1 +- 获取project为1的项目产品关系数据。 + - 第1条的project属性 @1 + - 第1条的product属性 @1 + - 第1条的branch属性 @0 + - 第1条的plan属性 @~~ + - 第1条的roadmap属性 @,1, +- 获取project为11的项目产品关系数据。 + - 第11条的project属性 @11 + - 第11条的product属性 @1 + - 第11条的branch属性 @0 + - 第11条的plan属性 @~~ + - 第11条的roadmap属性 @,1, +- 获取project为22的项目产品关系数据。 + - 第22条的project属性 @22 + - 第22条的product属性 @2 + - 第22条的branch属性 @0 + - 第22条的plan属性 @~~ + - 第22条的roadmap属性 @,2, +- 获取project为33的项目产品关系数据。 + - 第33条的project属性 @33 + - 第33条的product属性 @3 + - 第33条的branch属性 @0 + - 第33条的plan属性 @~~ + - 第33条的roadmap属性 @,3, +- 获取project为49的项目产品关系数据。 + - 第40条的project属性 @40 + - 第40条的product属性 @10 + - 第40条的branch属性 @0 + - 第40条的plan属性 @~~ + - 第40条的roadmap属性 @,10, + */ global $tester; diff --git a/module/upgrade/test/model/updatetaskrelationpriv.php b/module/upgrade/test/model/updatetaskrelationpriv.php index 944635f157..07e3b95561 100644 --- a/module/upgrade/test/model/updatetaskrelationpriv.php +++ b/module/upgrade/test/model/updatetaskrelationpriv.php @@ -10,6 +10,8 @@ cid=1 - 测试添加任务关系权限属性createrelation @createrelation - 测试维护任务关系权限属性editrelation @editrelation - 测试批量维护任务关系权限属性batcheditrelation @batcheditrelation +- 测试添加任务关系权限属性createrelation @createrelation +- 测试维护任务关系权限属性editrelation @editrelation **/ @@ -26,3 +28,5 @@ $upgrade = new upgradeTest(); r($upgrade->updateTaskRelationPrivTest()) && p('createrelation') && e('createrelation'); //测试添加任务关系权限 r($upgrade->updateTaskRelationPrivTest()) && p('editrelation') && e('editrelation'); //测试维护任务关系权限 r($upgrade->updateTaskRelationPrivTest()) && p('batcheditrelation') && e('batcheditrelation'); //测试批量维护任务关系权限 +r($upgrade->updateTaskRelationPrivTest()) && p('createrelation') && e('createrelation'); //测试添加任务关系权限 +r($upgrade->updateTaskRelationPrivTest()) && p('editrelation') && e('editrelation'); //测试维护任务关系权限 diff --git a/module/upgrade/test/model/upgradedoctemplate.php b/module/upgrade/test/model/upgradedoctemplate.php new file mode 100644 index 0000000000..9afc617e0f --- /dev/null +++ b/module/upgrade/test/model/upgradedoctemplate.php @@ -0,0 +1,58 @@ +#!/usr/bin/env php +upgradeDocTemplate(); +timeout=0 +cid=1 + +- 更新文档模板1 + - 属性version @2 + - 属性type @doc +- 更新文档模板2 + - 属性version @2 + - 属性type @doc +- 更新文档模板3 + - 属性version @2 + - 属性type @doc +- 更新文档模板4 + - 属性version @2 + - 属性type @doc +- 更新文档模板5 + - 属性version @2 + - 属性type @doc + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/upgrade.unittest.class.php'; + +zenData('user')->loadYaml('user')->gen(5); +su('admin'); + +$doc = zenData('doc'); +$doc->id->range('1-10'); +$doc->title->range('文档模板1,文档模板2,文档模板3,文档模板4,文档模板5,文档模板6,文档模板7,文档模板8,文档模板9,文档模板10'); +$doc->module->range(''); +$doc->lib->range(''); +$doc->version->range('1'); +$doc->type->range('book{5},html{5}'); +$doc->templateType->range('1-10'); +$doc->gen(10); + +$docContent = zenData('doccontent'); +$docContent->id->range('1-10'); +$docContent->doc->range('1-10'); +$docContent->type->range('html'); +$docContent->title->range('文档模板1,文档模板2,文档模板3,文档模板4,文档模板5,文档模板6,文档模板7,文档模板8,文档模板9,文档模板10'); +$docContent->version->range('1'); +$docContent->gen(10); + +$upgrade = new upgradeTest(); +r($upgrade->upgradeDocTemplateTest(1, 1)) && p('version,type') && e('2,doc'); // 更新文档模板1 +r($upgrade->upgradeDocTemplateTest(2, 1)) && p('version,type') && e('2,doc'); // 更新文档模板2 +r($upgrade->upgradeDocTemplateTest(3, 1)) && p('version,type') && e('2,doc'); // 更新文档模板3 +r($upgrade->upgradeDocTemplateTest(4, 1)) && p('version,type') && e('2,doc'); // 更新文档模板4 +r($upgrade->upgradeDocTemplateTest(5, 1)) && p('version,type') && e('2,doc'); // 更新文档模板5 diff --git a/module/upgrade/test/model/upgrademydocspace.php b/module/upgrade/test/model/upgrademydocspace.php index 718f35ae2e..a0fdb6506f 100644 --- a/module/upgrade/test/model/upgrademydocspace.php +++ b/module/upgrade/test/model/upgrademydocspace.php @@ -7,6 +7,49 @@ declare(strict_types=1); title=测试 upgradeModel->upgradeMyDocSpace(); cid=1 +- 获取更新前我的文档库数量。 @5 +- 获取更新后我的文档库数量。 @8 +- 获取更新后我的文档库 6 的信息 + - 属性id @6 + - 属性main @0 + - 属性vision @rnd + - 属性parent @11 +- 获取更新后我的文档库 6 的信息 + - 属性id @7 + - 属性main @0 + - 属性vision @rnd + - 属性parent @11 +- 获取更新后我的文档库 6 的信息 + - 属性id @8 + - 属性main @0 + - 属性vision @or + - 属性parent @13 +- 获取更新后我的文档库 6 的信息 + - 属性id @9 + - 属性main @0 + - 属性vision @or + - 属性parent @13 +- 获取更新后我的文档库 6 的信息 + - 属性id @10 + - 属性main @0 + - 属性vision @lite + - 属性parent @12 +- 获取更新后我的文档库 6 的信息 + - 属性id @11 + - 属性main @1 + - 属性vision @rnd + - 属性parent @0 +- 获取更新后我的文档库 6 的信息 + - 属性id @12 + - 属性main @1 + - 属性vision @lite + - 属性parent @0 +- 获取更新后我的文档库 6 的信息 + - 属性id @13 + - 属性main @1 + - 属性vision @or + - 属性parent @0 + **/ include dirname(__FILE__, 5) . '/test/lib/init.php'; @@ -33,4 +76,4 @@ r($myDocLibs[9]) && p('id,main,vision,parent') && e('9,0,or,13'); // r($myDocLibs[10]) && p('id,main,vision,parent') && e('10,0,lite,12'); // 获取更新后我的文档库 6 的信息 r($myDocLibs[11]) && p('id,main,vision,parent') && e('11,1,rnd,0'); // 获取更新后我的文档库 6 的信息 r($myDocLibs[12]) && p('id,main,vision,parent') && e('12,1,lite,0'); // 获取更新后我的文档库 6 的信息 -r($myDocLibs[13]) && p('id,main,vision,parent') && e('13,1,or,0'); // 获取更新后我的文档库 6 的信息 \ No newline at end of file +r($myDocLibs[13]) && p('id,main,vision,parent') && e('13,1,or,0'); // 获取更新后我的文档库 6 的信息 diff --git a/module/upgrade/test/model/upgradewikitemplates.php b/module/upgrade/test/model/upgradewikitemplates.php new file mode 100644 index 0000000000..72b593ce96 --- /dev/null +++ b/module/upgrade/test/model/upgradewikitemplates.php @@ -0,0 +1,58 @@ +#!/usr/bin/env php +upgradeWikiTemplates(); +timeout=0 +cid=1 + +- 更新文档模板1 + - 属性version @2 + - 属性type @doc +- 更新文档模板2 + - 属性version @2 + - 属性type @doc +- 更新文档模板3 + - 属性version @2 + - 属性type @doc +- 更新文档模板4 + - 属性version @2 + - 属性type @doc +- 更新文档模板5 + - 属性version @2 + - 属性type @doc + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/upgrade.unittest.class.php'; + +zenData('user')->loadYaml('user')->gen(5); +su('admin'); + +$doc = zenData('doc'); +$doc->id->range('1-10'); +$doc->title->range('文档模板1,文档模板2,文档模板3,文档模板4,文档模板5,文档模板6,文档模板7,文档模板8,文档模板9,文档模板10'); +$doc->module->range(''); +$doc->lib->range(''); +$doc->version->range('1'); +$doc->type->range('book{5},html{5}'); +$doc->templateType->range('1-10'); +$doc->gen(10); + +$docContent = zenData('doccontent'); +$docContent->id->range('1-10'); +$docContent->doc->range('1-10'); +$docContent->type->range('html'); +$docContent->title->range('文档模板1,文档模板2,文档模板3,文档模板4,文档模板5,文档模板6,文档模板7,文档模板8,文档模板9,文档模板10'); +$docContent->version->range('1'); +$docContent->gen(10); + +$upgrade = new upgradeTest(); +r($upgrade->upgradeWikiTemplatesTest(1)) && p('version,type') && e('2,doc'); // 更新文档模板1 +r($upgrade->upgradeWikiTemplatesTest(2)) && p('version,type') && e('2,doc'); // 更新文档模板2 +r($upgrade->upgradeWikiTemplatesTest(3)) && p('version,type') && e('2,doc'); // 更新文档模板3 +r($upgrade->upgradeWikiTemplatesTest(4)) && p('version,type') && e('2,doc'); // 更新文档模板4 +r($upgrade->upgradeWikiTemplatesTest(5)) && p('version,type') && e('2,doc'); // 更新文档模板5 diff --git a/module/upgrade/ui/upgradedoctemplates.html.php b/module/upgrade/ui/upgradedoctemplates.html.php new file mode 100644 index 0000000000..295ed0bb74 --- /dev/null +++ b/module/upgrade/ui/upgradedoctemplates.html.php @@ -0,0 +1,51 @@ + + * @package upgrade + * @link https://www.zentao.net + */ +namespace zin; + +set::zui(true); +div +( + setID('main'), + setClass('flex'), + div + ( + setID('mainContent'), + setClass('mx-auto'), + panel + ( + setStyle('width', '800px'), + set::title($lang->upgrade->upgradeDocTemplatesTip), + set::size('lg'), + progressBar + ( + setID('upgradeDocTemplatesProgress'), + set::striped(), + set::percent(0) + ), + div + ( + setClass('mt-3 py-3 row items-center gap-4 justify-center'), + btn + ( + setID('upgradeDocTemplatesBtn'), + set::type('primary'), + set::url('upgrade', 'upgradeDocTemplates', "fromVersion={$fromVersion}&processed=yes"), + on::click()->call('startUpgradeDocTemplates', jsRaw('event'), $upgradeDocTemplates, $lang->upgrade->upgradingDocTemplates, $lang->upgrade->next), + span(setClass('hidden as-upgrading-text'), $lang->upgrade->upgradingDocTemplates), + span(setClass('hidden as-finish-text'), $lang->upgrade->next), + $lang->upgrade->upgradeDocTemplates + ) + ) + ) + ) +); + +render('pagebase'); diff --git a/www/js/zui3/zui.zentao.js b/www/js/zui3/zui.zentao.js index 659f42d4fe..8ac189c9b6 100644 --- a/www/js/zui3/zui.zentao.js +++ b/www/js/zui3/zui.zentao.js @@ -1,22 +1,22 @@ -var rd=(I,lt,Ot)=>{if(!lt.has(I))throw TypeError("Cannot "+Ot)};var zt=(I,lt,Ot)=>(rd(I,lt,"read from private field"),Ot?Ot.call(I):lt.get(I)),Gt=(I,lt,Ot)=>{if(lt.has(I))throw TypeError("Cannot add the same private member more than once");lt instanceof WeakSet?lt.add(I):lt.set(I,Ot)},tn=(I,lt,Ot,xl)=>(rd(I,lt,"write to private field"),xl?xl.call(I,Ot):lt.set(I,Ot),Ot);var od=(I,lt,Ot)=>(rd(I,lt,"access private method"),Ot);(function(I,lt){typeof exports=="object"&&typeof module<"u"?lt(exports):typeof define=="function"&&define.amd?define(["exports"],lt):(I=typeof globalThis<"u"?globalThis:I||self,lt(I.zui={}))})(this,function(I){var Ga,Ya,zr,Or,Fr,jr,Br,Hr,Xa,ky,Za,Ty,cs,hs,jg;"use strict";const lt="",Ot="",xl="",UC="",WC="",VC="",KC="",qC="",GC="",YC="",XC="",ZC="",kl="3.0.0",My="production",hd="6e4ba1a03e13b19605b112f0da7812a67c840360",In=document,no=window,ud=In.documentElement,vs=In.createElement.bind(In),dd=vs("div"),Tl=vs("table"),Ay=vs("tbody"),fd=vs("tr"),{isArray:so,prototype:pd}=Array,{concat:Py,filter:Dl,indexOf:md,map:gd,push:Ly,slice:yd,some:$l,splice:Ry}=pd,zy=/^#(?:[\w-]|\\.|[^\x00-\xa0])*$/,Oy=/^\.(?:[\w-]|\\.|[^\x00-\xa0])*$/,Fy=/<.+>/,jy=/^\w+$/;function Il(s,e){const t=By(e);return!s||!t&&!_s(e)&&!ut(e)?[]:!t&&Oy.test(s)?e.getElementsByClassName(s.slice(1).replace(/\\/g,"")):!t&&jy.test(s)?e.getElementsByTagName(s):e.querySelectorAll(s)}class io{constructor(e,t){if(!e)return;if(Nl(e))return e;let n=e;if($t(e)){const i=t||In;if(n=zy.test(e)&&_s(i)?i.getElementById(e.slice(1).replace(/\\/g,"")):Fy.test(e)?Sd(e):Nl(i)?i.find(e):$t(i)?y(i).find(e):Il(e,i),!n)return}else if(bs(e))return this.ready(e);(n.nodeType||n===no)&&(n=[n]),this.length=n.length;for(let i=0,r=this.length;i{for(;e.firstChild;)e.removeChild(e.firstChild)})};function ro(...s){const e=Uy(s[0])?s.shift():!1,t=s.shift(),n=s.length;if(!t)return{};if(!n)return ro(e,y,t);for(let i=0;i{ut(r)&&_t(t,(o,a)=>{n?e?r.classList.add(a):r.classList.remove(a):r.classList.toggle(a)})})},re.addClass=function(s){return this.toggleClass(s,!0)},re.removeAttr=function(s){const e=oo(s);return this.each((t,n)=>{ut(n)&&_t(e,(i,r)=>{n.removeAttribute(r)})})};function Vy(s,e){if(s){if($t(s)){if(arguments.length<2){if(!this[0]||!ut(this[0]))return;const t=this[0].getAttribute(s);return Fi(t)?void 0:t}return Ft(e)?this:Fi(e)?this.removeAttr(s):this.each((t,n)=>{ut(n)&&n.setAttribute(s,e)})}for(const t in s)this.attr(t,s[t]);return this}}re.attr=Vy,re.removeClass=function(s){return arguments.length?this.toggleClass(s,!1):this.attr("class","")},re.hasClass=function(s){return!!s&&$l.call(this,e=>ut(e)&&e.classList.contains(s))},re.get=function(s){return Ft(s)?yd.call(this):(s=Number(s),this[s<0?s+this.length:s])},re.eq=function(s){return y(this.get(s))},re.first=function(){return this.eq(0)},re.last=function(){return this.eq(-1)};function Ky(s){return Ft(s)?this.get().map(e=>ut(e)||Hy(e)?e.textContent:"").join(""):this.each((e,t)=>{ut(t)&&(t.textContent=s)})}re.text=Ky;function Nn(s,e,t){if(!ut(s))return;const n=no.getComputedStyle(s,null);return t?n.getPropertyValue(e)||void 0:n[e]||s.style[e]}function mn(s,e){return parseInt(Nn(s,e),10)||0}function _d(s,e){return mn(s,`border${e?"Left":"Top"}Width`)+mn(s,`padding${e?"Left":"Top"}`)+mn(s,`padding${e?"Right":"Bottom"}`)+mn(s,`border${e?"Right":"Bottom"}Width`)}const Ml={};function qy(s){if(Ml[s])return Ml[s];const e=vs(s);In.body.insertBefore(e,null);const t=Nn(e,"display");return In.body.removeChild(e),Ml[s]=t!=="none"?t:"block"}function bd(s){return Nn(s,"display")==="none"}function wd(s,e){const t=s&&(s.matches||s.webkitMatchesSelector||s.msMatchesSelector);return!!t&&!!e&&t.call(s,e)}function ao(s){return $t(s)?(e,t)=>wd(t,s):bs(s)?s:Nl(s)?(e,t)=>s.is(t):s?(e,t)=>t===s:()=>!1}re.filter=function(s){const e=ao(s);return y(Dl.call(this,(t,n)=>e.call(t,n,t)))};function Wn(s,e){return e?s.filter(e):s}re.detach=function(s){return Wn(this,s).each((e,t)=>{t.parentNode&&t.parentNode.removeChild(t)}),this};const Gy=/^\s*<(\w+)[^>]*>/,Yy=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,Cd={"*":dd,tr:Ay,td:fd,th:fd,thead:Tl,tbody:Tl,tfoot:Tl};function Sd(s){if(!$t(s))return[];if(Yy.test(s))return[vs(RegExp.$1)];const e=Gy.test(s)&&RegExp.$1,t=Cd[e]||Cd["*"];return t.innerHTML=s,y(t.childNodes).detach().get()}y.parseHTML=Sd,re.has=function(s){const e=$t(s)?(t,n)=>Il(s,n).length:(t,n)=>n.contains(s);return this.filter(e)},re.not=function(s){const e=ao(s);return this.filter((t,n)=>(!$t(s)||ut(n))&&!e.call(n,t,n))};function En(s,e,t,n){const i=[],r=bs(e),o=n&&ao(n);for(let a=0,l=s.length;ae.selected&&!e.disabled&&!e.parentNode.disabled),"value"):s.value||""}function Xy(s){return arguments.length?this.each((e,t)=>{const n=t.multiple&&t.options;if(n||Ld.test(t.type)){const i=so(s)?gd.call(s,String):Fi(s)?[]:[String(s)];n?_t(t.options,(r,o)=>{o.selected=i.indexOf(o.value)>=0},!0):t.checked=i.indexOf(t.value)>=0}else t.value=Ft(s)||Fi(s)?"":s}):this[0]&&xd(this[0])}re.val=Xy,re.is=function(s){const e=ao(s);return $l.call(this,(t,n)=>e.call(t,n,t))},y.guid=1;function gn(s){return s.length>1?Dl.call(s,(e,t,n)=>md.call(n,e)===t):s}y.unique=gn,re.add=function(s,e){return y(gn(this.get().concat(y(s,e).get())))},re.children=function(s){return Wn(y(gn(En(this,e=>e.children))),s)},re.parent=function(s){return Wn(y(gn(En(this,"parentNode"))),s)},re.index=function(s){const e=s?y(s)[0]:this[0],t=s?this:y(e).parent().children();return md.call(t,e)},re.closest=function(s){const e=this.filter(s);if(e.length)return e;const t=this.parent();return t.length?t.closest(s):e},re.siblings=function(s){return Wn(y(gn(En(this,e=>y(e).parent().children().not(e)))),s)},re.find=function(s){return y(gn(En(this,e=>Il(s,e))))};const Zy=/^\s*\s*$/g,Jy=/^$|^module$|\/(java|ecma)script/i,Qy=["type","src","nonce","noModule"];function ev(s,e){const t=y(s);t.filter("script").add(t.find("script")).each((n,i)=>{if(Jy.test(i.type)&&ud.contains(i)){const r=vs("script");r.text=i.textContent.replace(Zy,""),_t(Qy,(o,a)=>{i[a]&&(r[a]=i[a])}),e.head.insertBefore(r,null),e.head.removeChild(r)}})}function tv(s,e,t,n,i){n?s.insertBefore(e,t?s.firstChild:null):s.nodeName==="HTML"?s.parentNode.replaceChild(e,s):s.parentNode.insertBefore(e,t?s:s.nextSibling),i&&ev(e,s.ownerDocument)}function Vn(s,e,t,n,i,r,o,a){return _t(s,(l,c)=>{_t(y(c),(u,h)=>{_t(y(e),(p,m)=>{const _=t?h:m,w=t?m:h,C=t?u:p;tv(_,C?w.cloneNode(!0):w,n,i,!C)},a)},o)},r),e}re.after=function(){return Vn(arguments,this,!1,!1,!1,!0,!0)},re.append=function(){return Vn(arguments,this,!1,!1,!0)};function nv(s){if(!arguments.length)return this[0]&&this[0].innerHTML;if(Ft(s))return this;const e=/]/.test(s);return this.each((t,n)=>{ut(n)&&(e?y(n).empty().append(s):n.innerHTML=s)})}re.html=nv,re.appendTo=function(s){return Vn(arguments,this,!0,!1,!0)},re.wrapInner=function(s){return this.each((e,t)=>{const n=y(t),i=n.contents();i.length?i.wrapAll(s):n.append(s)})},re.before=function(){return Vn(arguments,this,!1,!0)},re.wrapAll=function(s){let e=y(s),t=e[0];for(;t.children.length;)t=t.firstElementChild;return this.first().before(e),this.appendTo(t)},re.wrap=function(s){return this.each((e,t)=>{const n=y(s)[0];y(t).wrapAll(e?n.cloneNode(!0):n)})},re.insertAfter=function(s){return Vn(arguments,this,!0,!1,!1,!1,!1,!0)},re.insertBefore=function(s){return Vn(arguments,this,!0,!0)},re.prepend=function(){return Vn(arguments,this,!1,!0,!0,!0,!0)},re.prependTo=function(s){return Vn(arguments,this,!0,!0,!0,!1,!1,!0)},re.contents=function(){return y(gn(En(this,s=>s.tagName==="IFRAME"?[s.contentDocument]:s.tagName==="TEMPLATE"?s.content.childNodes:s.childNodes)))},re.next=function(s,e,t){return Wn(y(gn(En(this,"nextElementSibling",e,t))),s)},re.nextAll=function(s){return this.next(s,!0)},re.nextUntil=function(s,e){return this.next(e,!0,s)},re.parents=function(s,e){return Wn(y(gn(En(this,"parentElement",!0,e))),s)},re.parentsUntil=function(s,e){return this.parents(e,s)},re.prev=function(s,e,t){return Wn(y(gn(En(this,"previousElementSibling",e,t))),s)},re.prevAll=function(s){return this.prev(s,!0)},re.prevUntil=function(s,e){return this.prev(e,!0,s)},re.map=function(s){return y(Py.apply([],gd.call(this,(e,t)=>s.call(e,t,e))))},re.clone=function(){return this.map((s,e)=>e.cloneNode(!0))},re.offsetParent=function(){return this.map((s,e)=>{let t=e.offsetParent;for(;t&&Nn(t,"position")==="static";)t=t.offsetParent;return t||ud})},re.slice=function(s,e){return y(yd.call(this,s,e))};const sv=/-([a-z])/g;function Al(s){return s.replace(sv,(e,t)=>t.toUpperCase())}re.ready=function(s){const e=()=>setTimeout(s,0,y);return In.readyState!=="loading"?e():In.addEventListener("DOMContentLoaded",e),this},re.unwrap=function(){return this.parent().each((s,e)=>{if(e.tagName==="BODY")return;const t=y(e);t.replaceWith(t.children())}),this},re.offset=function(){const s=this[0];if(!s)return;const e=s.getBoundingClientRect();return{top:e.top+no.pageYOffset,left:e.left+no.pageXOffset}},re.position=function(){const s=this[0];if(!s)return;const e=Nn(s,"position")==="fixed",t=e?s.getBoundingClientRect():this.offset();if(!e){const n=s.ownerDocument;let i=s.offsetParent||n.documentElement;for(;(i===n.body||i===n.documentElement)&&Nn(i,"position")==="static";)i=i.parentNode;if(i!==s&&ut(i)){const r=y(i).offset();t.top-=r.top+mn(i,"borderTopWidth"),t.left-=r.left+mn(i,"borderLeftWidth")}}return{top:t.top-mn(s,"marginTop"),left:t.left-mn(s,"marginLeft")}};const kd={class:"className",contenteditable:"contentEditable",for:"htmlFor",readonly:"readOnly",maxlength:"maxLength",tabindex:"tabIndex",colspan:"colSpan",rowspan:"rowSpan",usemap:"useMap"};re.prop=function(s,e){if(s){if($t(s))return s=kd[s]||s,arguments.length<2?this[0]&&this[0][s]:this.each((t,n)=>{n[s]=e});for(const t in s)this.prop(t,s[t]);return this}},re.removeProp=function(s){return this.each((e,t)=>{delete t[kd[s]||s]})};const iv=/^--/;function Pl(s){return iv.test(s)}const Ll={},{style:rv}=dd,ov=["webkit","moz","ms"];function av(s,e=Pl(s)){if(e)return s;if(!Ll[s]){const t=Al(s),n=`${t[0].toUpperCase()}${t.slice(1)}`,i=`${t} ${ov.join(`${n} `)}${n}`.split(" ");_t(i,(r,o)=>{if(o in rv)return Ll[s]=o,!1})}return Ll[s]}const lv={animationIterationCount:!0,columnCount:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0};function Td(s,e,t=Pl(s)){return!t&&!lv[s]&&vd(e)?`${e}px`:e}function cv(s,e){if($t(s)){const t=Pl(s);return s=av(s,t),arguments.length<2?this[0]&&Nn(this[0],s,t):s?(e=Td(s,e,t),this.each((n,i)=>{ut(i)&&(t?i.style.setProperty(s,e):i.style[s]=e)})):this}for(const t in s)this.css(t,s[t]);return this}re.css=cv;function Dd(s,e){try{return s(e)}catch{return e}}const hv=/^\s+|\s+$/;function $d(s,e){const t=s.dataset[e]||s.dataset[Al(e)];return hv.test(t)?t:Dd(JSON.parse,t)}function uv(s,e,t){t=Dd(JSON.stringify,t),s.dataset[Al(e)]=t}function dv(s,e){if(!s){if(!this[0])return;const t={};for(const n in this[0].dataset)t[n]=$d(this[0],n);return t}if($t(s))return arguments.length<2?this[0]&&$d(this[0],s):Ft(e)?this:this.each((t,n)=>{uv(n,s,e)});for(const t in s)this.data(t,s[t]);return this}re.data=dv;function Id(s,e){const t=s.documentElement;return Math.max(s.body[`scroll${e}`],t[`scroll${e}`],s.body[`offset${e}`],t[`offset${e}`],t[`client${e}`])}_t([!0,!1],(s,e)=>{_t(["Width","Height"],(t,n)=>{const i=`${e?"outer":"inner"}${n}`;re[i]=function(r){if(this[0])return Xs(this[0])?e?this[0][`inner${n}`]:this[0].document.documentElement[`client${n}`]:_s(this[0])?Id(this[0],n):this[0][`${e?"offset":"client"}${n}`]+(r&&e?mn(this[0],`margin${t?"Top":"Left"}`)+mn(this[0],`margin${t?"Bottom":"Right"}`):0)}})}),_t(["Width","Height"],(s,e)=>{const t=e.toLowerCase();re[t]=function(n){if(!this[0])return Ft(n)?void 0:this;if(!arguments.length)return Xs(this[0])?this[0].document.documentElement[`client${e}`]:_s(this[0])?Id(this[0],e):this[0].getBoundingClientRect()[t]-_d(this[0],!s);const i=parseInt(n,10);return this.each((r,o)=>{if(!ut(o))return;const a=Nn(o,"boxSizing");o.style[t]=Td(t,i+(a==="border-box"?_d(o,!s):0))})}});const Nd="___cd";re.toggle=function(s){return this.each((e,t)=>{if(!ut(t))return;const n=bd(t);(Ft(s)?n:s)?(t.style.display=t[Nd]||"",bd(t)&&(t.style.display=qy(t.tagName))):n||(t[Nd]=Nn(t,"display"),t.style.display="none")})},re.hide=function(){return this.toggle(!1)},re.show=function(){return this.toggle(!0)};const Ed="___ce",Rl=".",zl={focus:"focusin",blur:"focusout"},Md={mouseenter:"mouseover",mouseleave:"mouseout"},fv=/^(mouse|pointer|contextmenu|drag|drop|click|dblclick)/i;function Ol(s){return Md[s]||zl[s]||s}function Fl(s){const e=s.split(Rl);return[e[0],e.slice(1).sort()]}re.trigger=function(s,e){if($t(s)){const[n,i]=Fl(s),r=Ol(n);if(!r)return this;const o=fv.test(r)?"MouseEvents":"HTMLEvents";s=In.createEvent(o),s.initEvent(r,!0,!0),s.namespace=i.join(Rl),s.___ot=n}s.___td=e;const t=s.___ot in zl;return this.each((n,i)=>{t&&bs(i[s.___ot])&&(i[`___i${s.type}`]=!0,i[s.___ot](),i[`___i${s.type}`]=!1),i.dispatchEvent(s)})};function Ad(s){return s[Ed]=s[Ed]||{}}function pv(s,e,t,n,i){const r=Ad(s);r[e]=r[e]||[],r[e].push([t,n,i]),s.addEventListener(e,i)}function Pd(s,e){return!e||!$l.call(e,t=>s.indexOf(t)<0)}function lo(s,e,t,n,i){const r=Ad(s);if(e)r[e]&&(r[e]=r[e].filter(([o,a,l])=>{if(i&&l.guid!==i.guid||!Pd(o,t)||n&&n!==a)return!0;s.removeEventListener(e,l)}));else for(e in r)lo(s,e,t,n,i)}re.off=function(s,e,t){if(Ft(s))this.each((n,i)=>{!ut(i)&&!_s(i)&&!Xs(i)||lo(i)});else if($t(s))bs(e)&&(t=e,e=""),_t(oo(s),(n,i)=>{const[r,o]=Fl(i),a=Ol(r);this.each((l,c)=>{!ut(c)&&!_s(c)&&!Xs(c)||lo(c,a,o,e,t)})});else for(const n in s)this.off(n,s[n]);return this},re.remove=function(s){return Wn(this,s).detach().off(),this},re.replaceWith=function(s){return this.before(s).remove()},re.replaceAll=function(s){return y(s).replaceWith(this),this};function mv(s,e,t,n,i){if(!$t(s)){for(const r in s)this.on(r,e,t,s[r],i);return this}return $t(e)||(Ft(e)||Fi(e)?e="":Ft(t)?(t=e,e=""):(n=t,t=e,e="")),bs(n)||(n=t,t=void 0),n?(_t(oo(s),(r,o)=>{const[a,l]=Fl(o),c=Ol(a),u=a in Md,h=a in zl;c&&this.each((p,m)=>{if(!ut(m)&&!_s(m)&&!Xs(m))return;const _=function(w){if(w.target[`___i${w.type}`])return w.stopImmediatePropagation();if(w.namespace&&!Pd(l,w.namespace.split(Rl))||!e&&(h&&(w.target!==m||w.___ot===c)||u&&w.relatedTarget&&m.contains(w.relatedTarget)))return;let C=m;if(e){let D=w.target;for(;!wd(D,e);)if(D===m||(D=D.parentNode,!D))return;C=D}Object.defineProperty(w,"currentTarget",{configurable:!0,get(){return C}}),Object.defineProperty(w,"delegateTarget",{configurable:!0,get(){return m}}),Object.defineProperty(w,"data",{configurable:!0,get(){return t}});const x=n.call(C,w,w.___td);i&&lo(m,c,l,e,_),x===!1&&(w.preventDefault(),w.stopPropagation())};_.guid=n.guid=n.guid||y.guid++,pv(m,c,l,e,_)})}),this):this}re.on=mv;function gv(s,e,t,n){return this.on(s,e,t,n,!0)}re.one=gv;const yv=/\r?\n/g;function vv(s,e){return`&${encodeURIComponent(s)}=${encodeURIComponent(e.replace(yv,`\r -`))}`}const _v=/file|reset|submit|button|image/i,Ld=/radio|checkbox/i;re.serialize=function(){let s="";return this.each((e,t)=>{_t(t.elements||[t],(n,i)=>{if(i.disabled||!i.name||i.tagName==="FIELDSET"||_v.test(i.type)||Ld.test(i.type)&&!i.checked)return;const r=xd(i);if(!Ft(r)){const o=so(r)?r:[r];_t(o,(a,l)=>{s+=vv(i.name,l)})}})}),s.slice(1)},window.$=y;function Rd(s,e){if(s==null)return[s,void 0];typeof e=="string"&&(e=e.split("."));const t=e.join(".");let n=s;const i=[n];for(;typeof n=="object"&&n!==null&&e.length;){let r=e.shift(),o;const a=r.indexOf("[");if(a>0&&a{const i=t[n]??"";s=s.replace(new RegExp(`\\{${n}\\}`,"g"),`${i}`)}),s}for(let t=0;t(s[s.B=1]="B",s[s.KB=1024]="KB",s[s.MB=1048576]="MB",s[s.GB=1073741824]="GB",s[s.TB=1099511627776]="TB",s))(jl||{});function ct(s,e=2,t){return Number.isNaN(s)?"?KB":(t||(s<1024?t="B":s<1048576?t="KB":s<1073741824?t="MB":s<1099511627776?t="GB":t="TB"),(s/jl[t]).toFixed(e)+t)}const yn=s=>{const e=/^[0-9]*(B|KB|MB|GB|TB)$/;s=s.toUpperCase(),s.endsWith("B")||(s+="B");const t=s.match(e);if(!t)return 0;const n=t[1];return s=s.replace(n,""),Number.parseInt(s,10)*jl[n]};let Bl=(document.documentElement.getAttribute("lang")||"zh_cn").toLowerCase().replace("-","_"),Mn;function zd(){return Bl}function wv(s){Bl=s.toLowerCase().replace("-","_")}function Od(s,e){Mn||(Mn={}),typeof s=="string"&&(s={[s]:e??{}}),y.extend(!0,Mn,s)}function Pe(s,e,t,n,i,r){Array.isArray(s)?Mn&&s.unshift(Mn):s=Mn?[Mn,s]:[s],typeof t=="string"&&(r=i,i=n,n=t,t=void 0);const o=i||Bl;let a;for(const l of s){if(!l)continue;const c=l[o]||l.default;if(!c)continue;const u=r&&l===Mn?`${r}.${e}`:e;if(a=ji(c,u),a!==void 0)break}return a===void 0?n:t?we(a,...Array.isArray(t)?t:[t]):a}function Cv(s,e,t,n){return Pe(void 0,s,e,t,n)}Pe.addLang=Od,Pe.getLang=Cv,Pe.getCode=zd,Pe.setCode=wv,Pe.map=Mn,Od({zh_cn:{confirm:"确定",save:"保存",cancel:"取消",delete:"删除",reset:"重置",add:"添加",copy:"复制",close:"关闭"},zh_tw:{confirm:"確定",save:"儲存",cancel:"取消",delete:"刪除",reset:"重置",add:"添加",Copy:"複製",close:"關閉"},en:{confirm:"Confirm",save:"Save",cancel:"Cancel",delete:"Delete",reset:"Reset",add:"Add",copy:"Copy",close:"Close"}});function ws(s,e,t){t!=null&&(Array.isArray(t)?t.forEach(n=>ws(s,e,n)):!(t instanceof Blob)&&y.isPlainObject(t)?Object.entries(t).forEach(([n,i])=>{ws(s,`${e}[${n}]`,i)}):s.append(e,t instanceof Blob?t:String(t)))}function co(s,e){const t=e||new FormData;return s&&(typeof s=="string"&&(s=new URLSearchParams(s)),s instanceof URLSearchParams?s.forEach((n,i)=>{ws(t,i,n)}):Array.isArray(s)?s.forEach(([n,i])=>{ws(t,n,i)}):s instanceof FormData?s.forEach((n,i)=>{ws(t,i,n)}):typeof s=="object"&&s&&Object.entries(s).forEach(([n,i])=>{ws(t,n,i)})),t}function St(){return y.guid++}function ho(s,e){if(s===e)return!1;if(s&&e){const t=typeof s,n=typeof e;if(t!==n)return!0;if(t==="object"&&n==="object"){const i=Array.isArray(s),r=Array.isArray(e);if(i!==r)return!0;if(i&&r){if(s.length!==e.length)return!0;for(let l=0;ln instanceof Cs?n.value!==t[i]:ho(n,t[i])))&&(this._value=this._compute(),this._lastDependencies=e.map(n=>n instanceof Cs?n.cache:n)),this._value}}function Hl(...s){const e=[],t=new Map,n=(i,r)=>{if(Array.isArray(i)&&(r=i[1],i=i[0]),!i.length)return;const o=t.get(i);typeof o=="number"?e[o][1]=!!r:(t.set(i,e.length),e.push([i,!!r]))};return s.forEach(i=>{typeof i=="function"&&(i=i()),Array.isArray(i)?Hl(...i).forEach(n):i&&typeof i=="object"?Object.entries(i).forEach(n):typeof i=="string"&&i.split(" ").forEach(r=>n(r,!0))}),e.sort((i,r)=>(t.get(i[0])||0)-(t.get(r[0])||0))}const K=(...s)=>Hl(...s).reduce((e,[t,n])=>(n&&e.push(t),e),[]).join(" ");y.classes=K,y.fn.setClass=function(s,...e){return this.each((t,n)=>{const i=y(n);s===!0?i.attr("class",K(i.attr("class"),...e)):i.addClass(K(s,...e))})};const Zs=new WeakMap;function uo(s,e,t){const n=Zs.has(s),i=n?Zs.get(s):{};typeof e=="string"?i[e]=t:e===null?Object.keys(i).forEach(r=>{delete i[r]}):Object.assign(i,e),Object.keys(i).forEach(r=>{i[r]===void 0&&delete i[r]}),Object.keys(i).length?(!n&&s instanceof Element&&Object.assign(i,y(s).dataset(),i),Zs.set(s,i)):Zs.delete(s)}function Bi(s,e,t){let n=Zs.get(s)||{};return t&&s instanceof Element&&(n=Object.assign({},y(s).dataset(),n)),e===void 0?n:n[e]}function Sv(s){Zs.delete(s)}y.fn.dataset=y.fn.data,y.fn.data=function(...s){const[e,t]=s;return!s.length||s.length===1&&typeof e=="string"?this.length?Bi(this[0],e,!0):void 0:this.each((n,i)=>uo(i,e,t))},y.fn.removeData=function(s=null){return this.each((e,t)=>uo(t,s))};function Ss(s,...e){return s.includes("RAWJS")&&(s=s.split('"RAWJS<').join("").split('>RAWJS"').join("").split("").join('"').split("").join(` -`)),new Function(`return ${s}`)(...e)}function fo(s,...e){return s.includes("RAWJS")?Ss(s,...e):JSON.parse(s)}function xv(s){return JSON.stringify(s,(e,t)=>{if(typeof t=="function")return`RAWJS<${t.toString().split('"').join("").split(` -`).join("")}>RAWJS`})}function Js(s,e){const t=y(s)[0];if(!t)return;const{prefix:n,getter:i,evalValue:r,json:o=!0,evalArgs:a=[]}={prefix:"z-",...typeof e=="string"?{prefix:e}:e},l=Array.isArray(r)?new Set(r):void 0;return Array.from(t.attributes).reduce((c,u)=>{let{name:h}=u;const{value:p}=u;let m=p;if(h.startsWith(n)){if(h=h.slice(n.length).replace(/-([a-z])/g,_=>_[1].toUpperCase()),i)m=i(h,p);else try{r&&(!l||l.has(h))||r===void 0&&p.includes("RAWJS")?m=Ss(p,...a):o&&(m=JSON.parse(p))}catch{}c[h]=m}return c},{})}function Ul(s,e,t="z-"){const n=y(s);Object.keys(e).forEach(i=>{let r=e[i];typeof r=="function"&&(r=`RAWJS<${r}>RAWJS`),typeof r!="string"&&(r=JSON.stringify(r)),i=i.replace(/[A-Z]/g,o=>`-${o.toLowerCase()}`),n.attr(`${t}${i}`,r)})}function kv(...s){var t;const e=s.length;if(!e)return Js(this);if(e===1){const[n]=s;return typeof n=="string"?(t=Js(this))==null?void 0:t[n]:(y.isPlainObject(n)&&Ul(this,n),this)}return Ul(this,{[s[0]]:s[1]}),this}y.fn.z=kv,y.fn._attr=y.fn.attr,y.fn.extend({attr(...s){const[e,t]=s;return!s.length||s.length===1&&typeof e=="string"?this._attr.apply(this,s):typeof e=="object"?(e&&Object.keys(e).forEach(n=>{const i=e[n];i===null?this.removeAttr(n):this._attr(n,i)}),this):t===null?this.removeAttr(e):this._attr(e,t)}}),y.Event||(y.Event=(s,e)=>{const[t,...n]=s.split("."),i=new Event(t,{bubbles:!0,cancelable:!0});return i.namespace=n.join("."),i.___ot=t,i.___td=e,i});const qn=(s,e)=>new Promise(t=>{const n=window.setTimeout(t,s);e&&e(n)}),Fd={};y.share=Fd;var po,Ke,jd;I.isValidElement=void 0;var xs,Bd,Hd,Wl,Vl,Kl,ql,Hi={},Ud=[],Tv=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,mo=Array.isArray;function Gn(s,e){for(var t in e)s[t]=e[t];return s}function Wd(s){s&&s.parentNode&&s.parentNode.removeChild(s)}function v(s,e,t){var n,i,r,o={};for(r in e)r=="key"?n=e[r]:r=="ref"?i=e[r]:o[r]=e[r];if(arguments.length>2&&(o.children=arguments.length>3?po.call(arguments,2):t),typeof s=="function"&&s.defaultProps!=null)for(r in s.defaultProps)o[r]===void 0&&(o[r]=s.defaultProps[r]);return go(s,o,n,i,null)}function go(s,e,t,n,i){var r={type:s,props:e,key:t,ref:n,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,constructor:void 0,__v:i??++jd,__i:-1,__u:0};return i==null&&Ke.vnode!=null&&Ke.vnode(r),r}function pe(){return{current:null}}function An(s){return s.children}function ve(s,e){this.props=s,this.context=e}function Qs(s,e){if(e==null)return s.__?Qs(s.__,s.__i+1):null;for(var t;ee&&xs.sort(Wl));yo.__r=0}function qd(s,e,t,n,i,r,o,a,l,c,u){var h,p,m,_,w,C=n&&n.__k||Ud,x=e.length;for(t.__d=l,Dv(t,e,C),l=t.__d,h=0;h0?go(i.type,i.props,i.key,i.ref?i.ref:null,i.__v):i).__=s,i.__b=s.__b+1,r=null,(a=i.__i=$v(i,t,o,u))!==-1&&(u--,(r=t[a])&&(r.__u|=131072)),r==null||r.__v===null?(a==-1&&h--,typeof i.type!="function"&&(i.__u|=65536)):a!==o&&(a==o-1?h--:a==o+1?h++:(a>o?h--:h++,i.__u|=65536))):i=s.__k[n]=null;if(u)for(n=0;n(l!=null&&!(131072&l.__u)?1:0))for(;o>=0||a=0){if((l=e[o])&&!(131072&l.__u)&&i==l.key&&r===l.type)return o;o--}if(a{!t||typeof t!="object"||Object.keys(t).forEach(n=>{let i=t[n];const r=s[n];i!==r&&(r!==void 0&&(n==="className"||n.endsWith("Class")?i=[r,i]:n==="children"?i=[...vo(r),...vo(i)]:typeof r=="object"&&(n==="style"||n.endsWith("Style")||n==="attrs"||n.endsWith("Attrs")||n==="props")&&(i=y.extend(r,i))),s[n]=i)})}),s}function Xl(s){return Object.keys(s).forEach(e=>{s[e]===void 0&&delete s[e]}),s}function Qd(s,e=!0){const t=y(s),n=t[0],i="zui-disable-scroll";if(e){if(t.data(i))return;if((t.css("scrollbar-gutter")||"").includes("stable")){t.data(i,{overflow:t.css("overflow")}).css("overflow","hidden");return}const r=n===document.body||t.is("html")?window.innerWidth-document.body.clientWidth:n.offsetWidth-n.clientWidth;if(!r)return;const o=t.css("paddingRight")||"0";t.data(i,{paddingRight:o,overflow:t.css("overflow")}).css({paddingRight:`${r+Number.parseInt(o,10)}px`,overflow:"hidden"})}else{const r=t.data(i);if(!r)return;t.css(r).removeData(i)}}y.fn.disableScroll=function(s=!0){return this.each((e,t)=>{Qd(t,s)})},y.fn.enableScroll=function(s=!0){return this.disableScroll(!s)};function Zl(s,e,t){if(!(t.on||"click").split(" ").includes(e.type))return;const n=t.selector?y(e.target).closest(t.selector):s;if(!n.length)return;const i=c=>c===""?!0:c,r=c=>{if(typeof c=="string")try{c=JSON.parse(c)}catch{}return c};if(i(t.once)){if(t.onceCalled)return;s.dataset("once-called",!0)}if(i(t.prevent)&&e.preventDefault(),i(t.stop)&&e.stopPropagation(),i(t.self)&&e.currentTarget!==e.target)return;const o=[["$element",s],["event",e],["options",t],["$target",n]],a=c=>typeof c=="function"?c(...o):y.runJS(c,...o);if(t.if!==void 0&&!a(t.if))return;const l=t.call;if(l){let c;if(typeof l=="string"?c=/^[$A-Z_][0-9A-Z_$.]*$/i.test(l)?ji(window,l):a(l):c=l,typeof c=="function"){const u=[],h=t.params;t.params=u,typeof h=="string"&&h.length?h[0]==="["?u.push(...r(h)):u.push(...h.split(", ").map(p=>(p=p.trim(),p==="$element"?s:p==="event"?e:p==="options"?t:p.startsWith("$element.")||p.startsWith("event.")||p.startsWith("options.")?a(p):r(p)))):Array.isArray(h)?u.push(...h):u.push(h),c(...u)}}t.do&&a(t.do)}function Ev(s){const e=y(this),t=s.type,n=e.attr("zui-on");if(n){const[o,a]=n.split("~").map(l=>l.trim());o&&o.split(" ").includes(t)&&Zl(e,s,y.extend({on:o},a?a.startsWith("{")?Ss(a):{do:a}:Js(e,{prefix:"data-",evalValue:["call","if","do"]})))}const i=e.attr(`zui-on-${t}`);i&&Zl(e,s,y.extend({on:t},i.startsWith("{")?Ss(i):{do:i}));const r=e.attr("data-on");r&&r.split(" ").includes(t)&&Zl(e,s,Js(e,{prefix:"data-",evalValue:["call","if","do"]}))}function ef(s){y(document).off(".zui.global").on(s.map(e=>`${e}.zui.global`).join(" "),`[zui-on],${s.map(e=>`[zui-on-${e}]`)},[data-on]`,Ev)}y(()=>{ef(["click","change","inited"])});function Yn(s,e){if(typeof s=="function")return Yn(s(...e||[]));if(typeof s=="number")return[s];let t=s.match(/(\d+)(%|px)?/);return t?[parseInt(t[1]),t[2]]:(t=s.match(/(\d+)\/(\d+)/),t?[100*parseInt(t[1])/parseInt(t[2]),"%"]:[NaN])}function qe(s,e){if(s==null)return null;const[t,n="px"]=Yn(s,e);return Number.isNaN(t)?typeof s=="string"?s:null:`${t}${n}`}async function Ts(s,e){var n,i,r;if(s instanceof Blob){const o=document.createElement("a");return o.href=window.URL.createObjectURL(s),e&&(o.download=decodeURIComponent(e)),o.click(),o.remove(),s}if(s instanceof Response){const o=await s.blob();return e=e||((r=(i=(n=s.headers.get("Content-Disposition"))==null?void 0:n.split(";")[1])==null?void 0:i.split("=")[1])==null?void 0:r.replace(/"/g,"")),Ts(o,e)}const t=await fetch(s);return Ts(t)}class tf{constructor(e){this._$target=y(e)}on(...e){return this._$target.on(...e),this}one(...e){return this._$target.one(...e),this}off(...e){return this._$target.off(...e),this}trigger(...e){return this._$target.trigger(...e),this}}const vn=new tf(document);y.bus=vn,y.on=vn.on.bind(vn),y.one=vn.one.bind(vn),y.off=vn.off.bind(vn),y.trigger=vn.trigger.bind(vn);var Mv=["Shift","Meta","Alt","Control"],nf=typeof navigator=="object"?navigator.platform:"",sf=/Mac|iPod|iPhone|iPad/.test(nf),Av=sf?"Meta":"Control",Pv=nf==="Win32"?["Control","Alt"]:sf?["Alt"]:[];function Jl(s,e){return typeof s.getModifierState=="function"&&(s.getModifierState(e)||Pv.includes(e)&&s.getModifierState("AltGraph"))}function Lv(s){return s.trim().split(" ").map(function(e){var t=e.split(/\b\+/),n=t.pop();return[t=t.map(function(i){return i==="$mod"?Av:i}),n]})}function rf(s,e){var t;e===void 0&&(e={});var n=(t=e.timeout)!=null?t:1e3,i=Object.keys(s).map(function(a){return[Lv(a),s[a]]}),r=new Map,o=null;return function(a){a instanceof KeyboardEvent&&(i.forEach(function(l){var c=l[0],u=l[1],h=r.get(c)||c;(function(p,m){return!(m[1].toUpperCase()!==p.key.toUpperCase()&&m[1]!==p.code||m[0].find(function(_){return!Jl(p,_)})||Mv.find(function(_){return!m[0].includes(_)&&m[1]!==_&&Jl(p,_)}))})(a,h[0])?h.length>1?r.set(c,h.slice(1)):(r.delete(c),u(a)):Jl(a,a.key)||r.delete(c)}),o&&clearTimeout(o),o=setTimeout(r.clear.bind(r),n))}}function Rv(s,e,t){var n;t===void 0&&(t={});var i=(n=t.event)!=null?n:"keydown",r=rf(e,t);return s.addEventListener(i,r),function(){s.removeEventListener(i,r)}}function Ql(s,e={}){if(!s)return;const t=Object.keys(e).reduce((n,i)=>(e[i].optional||(n[i]={...e[i]}),n),{});return Object.keys(s).forEach(n=>{const i=s[n];i?i===!0?e[n]&&(t[n]={...e[n]}):t[n]=i:delete t[n]}),Object.keys(t).reduce((n,i)=>{const{keys:r,handler:o}=t[i];return typeof r=="string"?n[r]=o:r.forEach(a=>{n[a]=o}),n},{})}function ec(s,e,t){const{timeout:n,event:i="keydown",scope:r,when:o}=t||{},a=rf(e,{timeout:n}),l=`.zui.hotkeys${r?`.${r}`:""}`,c="zui-hotkeys-composing";return y(s).on(`${i}${l}`,function(u){o&&o(u)===!1||y(u.target).data(c)||a(u)}).on(`compositionstart${l}`,u=>{y(u.target).data(c,!0)}).on(`compositionend${l}`,u=>{y(u.target).removeData(c)})}function tc(s,e){return y(s).off(`.zui.hotkeys${e?`.${e}`:""}`)}const zv=Rv;y.fn.hotkeys=function(s,e){return ec(this,s,e)},y.fn.unbindHotkeys=function(s){return tc(this,s)},y.hotkeys=function(s,e){ec(window,s,e)},y.unbindHotkeys=function(s){tc(window,s)};function bo(){return document.fullscreenElement||document.webkitFullscreenElement||document.mozFullScreenElement}async function of(s){(typeof s=="string"||s instanceof Element||s instanceof y)&&(s={target:s});const{target:e,onError:t,onSuccess:n,afterExit:i,afterEnter:r}=s,o=y(e),a=o[0];if(!a)return;const l=a.requestFullscreen||a.webkitRequestFullscreen||a.mozRequestFullScreen;if(!l){t==null||t.call(a,new Error("[ZUI] The browser does not support full screen feature."));return}try{await l.call(a),n==null||n.call(a),y(a).off(".zui.fullscreen"),i&&o.on("exitFullscreen.zui.fullscreen",i),r&&o.on("enterFullscreen.zui.fullscreen",r)}catch(c){t==null||t.call(a,c)}document.zuiBindFullscreenChange||(document.zuiBindFullscreenChange=!0,y(document).on("fullscreenchange.zui webkitfullscreenchange.zui mozfullscreenchange.zui",c=>{const u=bo();let h=u;u?y(u).addClass("is-in-fullscreen"):(h=y(document).find(".is-in-fullscreen")[0]||document,y(h).removeClass("is-in-fullscreen")),y("body").toggleClass("has-in-fullscreen",!!u);const p={event:c,target:h,fullscreenElement:u};y(h).trigger(u?"enterFullscreen":"exitFullscreen",p).trigger("toggleFullscreen",p)}))}async function nc(s){const e=bo();return s===!1&&!!e===s?s:e?(document.exitFullscreen(),!1):(await of(s),!0)}y.fn.fullscreen=function(s){return nc({target:this,...s})},y.getFullscreenElement=bo,y.toggleFullscreen=nc;function Xn(s){return!s||s.parentNode===document?!1:s.parentNode?Xn(s.parentNode):!0}y.isDetached=Xn,y.fn.isDetached=function(){const s=this[0];return!s||Xn(s)};const Ds=class _y{constructor(e,t){var _;this._inited=!1,this._autoDestory=0,this._destroyed=!1;const{KEY:n,DATA_KEY:i,MULTI_INSTANCE:r,NAME:o,ATTR_KEY:a,ALL:l,TYPED_ALL:c}=this.constructor;if(!o)throw new Error('[ZUI] The component must have a "NAME" static property.');const u=y(e);if(u.data(n)&&!r)throw new Error(`[ZUI] The component "${o}" has been initialized on element.`);const h=u[0];if(!h)throw new Error(`[ZUI] Invalid selector "${e}" for component "${o}", can not find the element matched.`);const p=St();this._gid=p,this._element=h,this.resetOptions(t),this._key=this.options.key??`__${p}`;let m=l.get(h);if(m?m.add(this):(m=new Set([this]),l.set(h,m)),c.has(o)?c.get(o).add(this):c.set(o,new Set([this])),u.data(n,this).attr(a,"").attr(i,`${p}`).attr("z-use",[...new Set([...m].map(w=>w.constructor.NAME))].join(",")),r){const w=`${n}:ALL`;let C=u.data(w);C||(C=new Map,u.data(w,C)),C.set(this._key,this)}this.init(),(_=this.options.$onCreate)==null||_.call(this),requestAnimationFrame(async()=>{var C;const{$fetcher:w}=this.options;if(w){const x=await dt(w,[],{dataType:"js"});if(this.destroyed)return;x&&this.setOptions(x)}this._inited=!0,await this.afterInit(),this.emit("inited",this.options),(C=this.options.$onInited)==null||C.call(this)})}static get ZUI(){return this.NAME.replace(/(^[A-Z]+)/,e=>e.toLowerCase())}static get KEY(){return`zui.${this.NAME}`}static get NAMESPACE(){return`.zui.${this.ZUI}`}static get DATA_KEY(){return`data-zui-${this.NAME}`}static get ATTR_KEY(){return`z-use-${this.NAME}`}static get SELECTOR(){return`[${this.DATA_KEY}]`}get inited(){return this._inited}get destroyed(){return this._destroyed}get element(){return this._element}get key(){return this._key}get options(){return this._options}get gid(){return this._gid}get $element(){return y(this.element)}get $emitter(){return this.$element}get i18nData(){return[this.options.i18n,this.constructor.i18n]}init(){}afterInit(){}render(e,t){this.setOptions(e,t)}destroy(){var m;const{KEY:e,DATA_KEY:t,ALL:n,TYPED_ALL:i,NAME:r,MULTI_INSTANCE:o,ATTR_KEY:a}=this.constructor,{$element:l,element:c}=this;if(this.emit("destroyed"),this._destroyed=!0,l.off(this.namespace).removeData(e).removeAttr(a).removeAttr(t),o){const _=this.$element.data(`${e}:ALL`);if(_)if(_.delete(this._key),_.size===0)this.$element.removeData(`${e}:ALL`);else{const w=_.values().next().value;l.data(e,w).attr(t,String(w==null?void 0:w.gid))}}const u=n.get(c);u&&(u.delete(this),u.size===0&&n.delete(c));const h=i.get(r);h&&(h.delete(this),h.size===0&&i.delete(r));const p=n.get(c);p!=null&&p.size?l.attr("z-use",[...new Set([...p].map(_=>_.constructor.NAME))].join(",")):l.removeAttr("z-use"),(m=this.options.$onDestroy)==null||m.call(this)}autoDestroy(e=100){this._autoDestory&&clearTimeout(this._autoDestory),this._autoDestory=window.setTimeout(()=>{this._autoDestory=0,Xn(this.element)&&this.destroy()},e)}setOptions(e,t){if(t){const n={...this.constructor.DEFAULT,...(e==null?void 0:e.$optionsFromDataset)!==!1?this.$element.dataset():{},...e},{$options:i}=n;if(i){const r=typeof i=="function"?i.call(this,this.element,n):i;r&&y.extend(n,r),delete n.$options}this._options=n}else e&&y.extend(this._options,e);return this._options}resetOptions(e){return this.setOptions(e,!0)}emit(e,...t){const n=y.Event(e);return n.__src=this,this.$emitter.trigger(n,[this,...t]),n}on(e,t,n){const i=this;this.$element[n!=null&&n.once?"one":"on"](this._wrapEvent(e),function(r,o){(!r.__src||r.__src===i)&&t.call(this,r,o)})}one(e,t){this.on(e,t,{once:!0})}off(e){this.$element.off(this._wrapEvent(e))}i18n(e,t,n){const{i18nData:i}=this;return Pe(i,e,t,n,this.options.lang,this.constructor.NAME)??Pe(i,e,t,n,this.options.lang)??`{i18n:${e}}`}get namespace(){return`${this.constructor.NAMESPACE}.${this._key}`}_wrapEvent(e){return e.split(" ").map(t=>t.includes(".")?t:`${t}${this.namespace}`).join(" ")}static get(e,t){const n=y(e);if(this.MULTI_INSTANCE&&t!==void 0){const i=n.data(`${this.KEY}:ALL`);return i?i.get(t):void 0}return n.data(this.KEY)}static isValid(e){return!0}static ensure(e,t){const n=this.get(e,t==null?void 0:t.key);if(n){if(this.isValid(n))return t&&n.setOptions(t),n;n.destroy()}return new this(e,t)}static getAll(e,t){var l;const{SELECTOR:n,ALL:i,TYPED_ALL:r}=this,o=[],a=c=>{c instanceof this&&(!t||t(c)!==!1)&&o.push(c)};return e?y(e).find(n).each((c,u)=>{var h;(h=i.get(u))==null||h.forEach(a)}):this!==_y?(l=r.get(this.NAME))==null||l.forEach(a):i.forEach(c=>{c.forEach(a)}),o.sort((c,u)=>c.gid-u.gid)}static query(e,t,n){return e===void 0?this.getAll(void 0,n).pop():this.get(y(e).closest(this.SELECTOR),t)}static defineFn(e){let t=e||this.ZUI;y.fn[t]&&(t=`zui${this.NAME}`);const n=this;y.fn.extend({[t](i,...r){const o=typeof i=="object"?i:void 0,a=typeof i=="string"?i:void 0;let l;return this.each((c,u)=>{let h=n.get(u);if(h)o&&h.render(o);else{if(a)return;h=new n(u,o)}if(a){let p=h[a],m=h;p===void 0&&(m=h.$,p=m[a]),typeof p=="function"?l=p.call(m,...r):l=p}}),l!==void 0?l:this}})}static register(e,t){var i,r;e=e||this,t=(t??e.NAME).toLowerCase(),this.map.set(t,e);const n=(r=(i=e.toggle)==null?void 0:i.name)==null?void 0:r.toLowerCase();n&&n!==t&&this.toggleMap.set(n,e)}};Ds.DEFAULT={},Ds.MULTI_INSTANCE=!1,Ds.ALL=new Map,Ds.TYPED_ALL=new Map,Ds.map=new Map,Ds.toggleMap=new Map;let Ge=Ds;function Ui(s){return Ge.map.get(s.toLowerCase())}function wo(s,e,t={}){let n=Ui(s);if(n||(n=sc(s)),!n)return null;const{$update:i,...r}=t;if(!n.MULTI_INSTANCE){const o=n.get(e);if(o)return i&&o.render(r,i==="reset"),o}return new n(e,r)}function Ov(s,e,t={}){requestAnimationFrame(()=>wo(s,e,t))}function af(s,e){Ge.register(s,e)}function sc(s){const{zui:e}=window;if(e){s=s==null?void 0:s.toLowerCase();for(const t in e){const n=t.toLowerCase()===s;if(s&&!n)continue;const i=e[t];if(!(typeof i!="function"||!i.NAME||!i.ZUI)&&(Ge.map.has(t.toLowerCase())||af(i),n))return i}}}function Fv(s){var e;s?(e=Ui(s))==null||e.defineFn():window._zuiDefined||(sc(),Ge.map.forEach(t=>{t.defineFn()}),Object.assign(window,{_zuiDefined:!0}))}function jv(s,e={}){const t=y(s);let n=t.attr("zui-create");const{update:i,onCreate:r}=e,o=(a,l)=>{if(l={$update:i,$optionsFromDataset:!1,...l},r){const u=r(a,l);if(u===!1)return;u&&(l=u)}const c=l.$lib;if(c){delete l.$lib,y.getLib(c).then(()=>wo(a,s,l));return}Ov(a,s,l)};if(typeof n=="string"){n=n.trim();const a=n.length?n.split(",").map(u=>u.trim()):[],l=Js(s,{prefix:"zui-create-",evalValue:!0}),c=Object.keys(l);if(!c.length&&a.length===1)o(a[0],t.dataset());else{const u=new Set;[...a,...c].forEach(h=>{if(u.has(h))return;const p=l[h];o(h,p),delete l[h],u.add(h)})}}else{const a=t.dataset(),l=a==null?void 0:a.zui;if(!l)return;console.warn("[ZUI] create component instance with [data-zui] is deprecated, use [zui-create] instead.",{element:s,options:e}),delete a.zui,o(l,a)}}function Bv(){y(document).on("click.zui.toggle mouseenter.zui.toggle","[data-toggle],[zui-toggle]",function(s){const e=y(this),t=e.dataset("toggle")||e.attr("zui-toggle");if(!t)return;const n=Ge.toggleMap.get(t)||Ui(t),i=n==null?void 0:n.toggle;if(!i)return;const{trigger:r="click",skip:o="[disabled],.disabled",check:a}=i,l=s.type==="mouseover"?"hover":"click";if(!r.includes(l)||a&&!a.call(n,this,l,s)||o&&e.is(o))return;const{onGet:c,onCreate:u,setOptions:h=!0,getOptions:p,prevent:m=!0,handler:_,onToggle:w,convertHref:C}=i;let x=e.dataset();const D=e.attr(`zui-toggle-${t}`);if(D&&(x=y.extend(x,Ss(D))),C&&e.is("a")){const A=e.attr("href");if(A){const F=C===!0?{selector:"target",url:"url"}:C;"#.".includes(A[0])?F.selector&&x[F.selector]===void 0&&(x[F.selector]=A):F.url&&x[F.url]===void 0&&(x[F.url]=A)}}if(p&&(x=p.call(n,this,x,s)),_){_.call(n,this,x,l,s),m&&s.preventDefault();return}let E=c?c.call(n,this):n.get(this);if(E)h&&E.setOptions(x);else{const A=u?u.call(n,this,s,x):new n(this,x);if(!A)return;E=A}if(w){if(w.call(n,E,this,s)===!1)return}else{const{shown:A,show:F,hide:G,toggle:Y}=E;let Z;if(Y?Z=Y:F&&G?A?Z=G:Z=F:F&&(Z=F),Z)Z.call(E);else return}m&&s.preventDefault()})}function Hv(s,e){const t=Bi(s),n=[];return Object.keys(t).forEach(i=>{if(!i.startsWith("zui."))return;const r=t[i];(e==null?void 0:e(r,i))!==!1&&n.push(t[i])}),n}let Co=0;function lf(s=100){if(Co&&clearTimeout(Co),s){Co=window.setTimeout(()=>lf(0),s);return}Co=0,Ge.ALL.forEach(e=>{e.forEach(t=>t.autoDestroy())})}function Uv(){if(!document.body||Bi(document.body,"_autoDestoryMob"))return;const s=new MutationObserver(e=>{let t=!1;for(const n of e)if(n.removedNodes.length){t=!0;break}t&&lf()});s.observe(document.body,{childList:!0,subtree:!0}),uo(document.body,"_autoDestoryMob",s)}function ic(s,e){const t=y(s);t.find("[zui-create],[data-zui]").each(function(){var n;((n=e==null?void 0:e.beforeCreate)==null?void 0:n.call(e,this))!==!1&&jv(this,e)}),t.find("[zui-init]").each(function(){this.hasAttribute("z-zui-inited")||(this.setAttribute("z-zui-inited",""),y.runJS(this.getAttribute("zui-init"),["$element",y(this)]))}),t.find(".hide-before-init").removeClass("invisible hidden opacity-0"),t.find(".scroll-into-view").scrollIntoView(),t.find('[data-on="inited"],[zui-on-inited]').each((n,i)=>{const r=y(i);r.zui()||r.trigger("inited")}),e!=null&&e.runJS&&t.runJS()}y.fn.zuiInit=function(s){return ic(this,s),this},y.fn.zui=function(s,e){const t=this[0];if(!t)return;if(typeof s!="string"){const i={};let r;return Hv(t,(o,a)=>{i[a]=o,(!r||r.gid1?t[0]:void 0,i=t[t.length>1?1:0],r=y(this).zui(n),o=r==null?void 0:r[i];typeof o=="function"&&o.apply(r,e)}),this},y(()=>{y("body").zuiInit({update:!0}),Bv(),Uv()});class So extends Ge{get $targets(){const{$element:e}=this,{targets:t}=this.options;return t?e.find(t):e}_handleScroll(e,t,n){const{offset:i=1}=this.options,r=this.$targets,o=e.getBoundingClientRect(),{scrollTop:a,scrollLeft:l}=e;r.each((c,u)=>{const h=u.getBoundingClientRect(),p=t==="top"&&a>0&&h.top<=o.top+i||t==="bottom"&&h.bottom>=o.bottom-i||t==="left"&&l>0&&h.left<=o.left+i||t==="right"&&l{this._raf&&cancelAnimationFrame(this._raf),this._raf=requestAnimationFrame(()=>{this._raf=0,this._handleScroll(l,t,i)})};this._scrollListener=c,l.addEventListener("scroll",c)}this._container=l,requestAnimationFrame(()=>{this._handleScroll(l,t,i)})}else this._ob=new IntersectionObserver(l=>{l.forEach(c=>{c.target.classList.toggle(i,c.intersectionRatiothis._ob.observe(c))}destroy(){var e;(e=this._ob)==null||e.disconnect(),this._container&&(this._container.removeEventListener("scroll",this._scrollListener),this._raf&&cancelAnimationFrame(this._raf))}}So.NAME="Sticky";const ei=24*60*60*1e3,Ye=s=>s===void 0?new Date:(s instanceof Date||(typeof s=="string"&&(s=s.trim(),/^\d+$/.test(s)&&(s=Number.parseInt(s,10))),typeof s=="number"&&s<1e10&&(s*=1e3),s=new Date(s)),s),cf=(s,e,t="day")=>{if(typeof e=="string"){const n=Number.parseInt(e,10);t=e.replace(n.toString(),""),e=n}return s=new Date(Ye(s).getTime()),t==="month"?s.setMonth(s.getMonth()+e):t==="year"?s.setFullYear(s.getFullYear()+e):t==="week"?s.setDate(s.getDate()+e*7):t==="hour"?s.setHours(s.getHours()+e):t==="minute"?s.setMinutes(s.getMinutes()+e):t==="second"?s.setSeconds(s.getSeconds()+e):s.setDate(s.getDate()+e),s},Zn=(s,e=new Date)=>Ye(s).toDateString()===Ye(e).toDateString(),Wi=(s,e=new Date)=>Ye(s).getFullYear()===Ye(e).getFullYear(),rc=(s,e=new Date)=>(s=Ye(s),e=Ye(e),s.getFullYear()===e.getFullYear()&&s.getMonth()===e.getMonth()),Wv=(s,e=new Date)=>{s=Ye(s),e=Ye(e);const t=1e3*60*60*24,n=Math.floor(s.getTime()/t),i=Math.floor(e.getTime()/t);return Math.floor((n+4)/7)===Math.floor((i+4)/7)},Vv=(s,e)=>Zn(Ye(e),s),Kv=(s,e)=>Zn(Ye(e).getTime()-ei,s),qv=(s,e)=>Zn(Ye(e).getTime()+ei,s),oc=s=>s!=null&&!isNaN(Ye(s).getTime()),nt=(s,e="yyyy-MM-dd hh:mm",t="")=>{if(s=Ye(s),!oc(s))return t;if(typeof e=="function")return e(s);const n={"M+":s.getMonth()+1,"d+":s.getDate(),"h+":s.getHours(),"H+":s.getHours()%12,"m+":s.getMinutes(),"s+":s.getSeconds(),"S+":s.getMilliseconds()};return/(y+)/i.test(e)&&(e.includes("[yyyy-]")&&(e=e.replace("[yyyy-]",Wi(s)?"":"yyyy-")),e=e.replace(RegExp.$1,`${s.getFullYear()}`.substring(4-RegExp.$1.length))),Object.keys(n).forEach(i=>{if(new RegExp(`(${i})`).test(e)){const r=`${n[i]}`;e=e.replace(RegExp.$1,RegExp.$1.length===1?r:`00${r}`.substring(r.length))}}),e},Gv=(s,e,t)=>{const n={full:"yyyy-M-d",month:"M-d",day:"d",str:"{0} ~ {1}",...t},i=nt(s,Wi(s)?n.month:n.full);if(Zn(s,e))return i;const r=nt(e,Wi(s,e)?rc(s,e)?n.day:n.month:n.full);return n.str.replace("{0}",i).replace("{1}",r)};function ac(s){let e=0;if(typeof s!="string"&&(s=String(s)),s&&s.length)for(let t=0;t{try{a.includes("%")&&(a=decodeURIComponent(a)),a=JSON.parse(a)}catch{}return[o,a]})),params:n.map(o=>{if(o!=="undefined"){if(o==="null")return null;try{return o.includes("%")&&(o=decodeURIComponent(o)),JSON.parse(o)}catch{return o}}})}}function hf(s){if(Array.isArray(s))return{commands:s.map(xo).filter(Boolean)};if(typeof s=="object")return s;s=s.replace(/^#!?/,"");const e=s.includes(">"),t=s.split(e?">":"|").map(xo);return{async:e,commands:t.filter(Boolean)}}function lc(s,e,t){if(typeof s=="string"&&(s=xo(s)),!s)return;const{execute:n,event:i,scope:r}=e;if(!(r&&s.scope&&s.scope!==r))return n({name:s.name,options:{...e.options,...s.options},event:i,scope:s.scope,prevResult:t},s.params)}async function uf(s,e){const{async:t,commands:n}=hf(s);if(!n.length)return[];const{signal:i}=e;if(t){const o=[];let a;for(const l of n){if(!(i!=null&&i.aborted))break;a=await lc(l,e,a),i!=null&&i.aborted&&(a=void 0),o.push(a)}return o}return await Promise.all(n.map(o=>{if(!(i!=null&&i.aborted))return lc(o,e)}))}const ti="zui.commands",ni="z-commands",cc="zui-commands-proxy",df="zui-command",ko={};function ff(s,e){typeof s=="string"&&e?ko[s]=e:typeof s=="object"&&Object.assign(ko,s)}function To(s,e){typeof e=="string"?e={scope:e}:typeof e=="function"&&(e={onCommand:e});const{scope:t="",events:n="click"}=e??{},i=y(s),r=(i.attr(ni)||"").split(",");t&&!r.includes(t)&&r.push(t),i.attr(ni,r.join(",")).data(ti,{[t]:{...e,scope:t,events:n,gid:St()},...i.data(ti)})}function Do(s,e=!0){const t=y(s);if(e===!0)t.removeAttr(ni),t.removeData(ti);else if(e.length){const n=t.data(ti)||{};e.split(",").forEach(r=>{delete n[r]});const i=Object.keys(n);i.length?t.attr(ni,i.join(",")).data(ti,To):Do(t,!0)}}function pf(s,e){let t=s.closest(`[${ni}],[${cc}]`).first();if(t.attr(cc)!==void 0&&(t=y(t.data("zui.commandProxy")||t.attr(cc)).closest(`[${ni}]`)),!t.length)return;const n=t.data(ti)||{},i=Object.values(n).sort((o,a)=>a.gid-o.gid);let r;return e!=null&&e.length?(r=i.find(o=>o.scope===e),r||(r=i.find(o=>{var a;return!((a=o.scope)!=null&&a.length)&&!o.scoped})),r):(r=i.find(o=>{var a;return!((a=o.scope)!=null&&a.length)&&!o.scoped}),r||(r=i.find(o=>!o.scoped)),r?r.element=t[0]:r=pf(s.parent(),e),r)}function Yv(s){if(!s.currentTarget)return;const e=y(s.currentTarget);if(e.closest(".disabled,[disabled]").length)return;const t=e.attr(df)||(e.is('a[href^="#!"]')?e.attr("href"):"");if(!t)return;const n=new AbortController,i=()=>n.abort();uf(t,{signal:n.signal,execute:(r,o)=>{const{scope:a,name:l}=r,c={...r,abort:i};let u;const h=pf(e,a);if(h){c.element=h.element;const m=(h.commands?h.commands[`${a}~${l}`]||h.commands[l]:null)||h.onCommand;if(m&&(u=m(c,o),s.commandHandled))return u}const p=[c,o];if(e.trigger("command",p).trigger(`command:${a?`${l}.${a}`:l}`,p),a&&e.trigger(`command:.${a}`,p),s.commandHandled)return u;if(a==="event"){l==="stop"?s.stopPropagation():l==="prevent"?s.preventDefault():Kn(s,l,o);return}return a==="window"?Kn(window,l,o):a==="zui"?Kn(window.zui,l,o):a==="target"?Kn(e[0],l,o):a==="$target"?Kn(e,l,o):a==="$"?Kn(y,l,o):a===""&&ko[l]!==void 0?ko[l](c,o):u},event:s})}y.fn.command=function(s,e){return this.on(`command:${s}`,e)},y.fn.offCommand=function(s,e){return this.off(`command:${s}`,e)},y.fn.commands=function(s){return this.each((e,t)=>To(t,s)),this},y.fn.unbindCommands=function(s){return this.each((e,t)=>Do(t,s)),this},y(()=>{y(document).on("click.zui.command",`[${df}],a[href^="#!"]`,Yv)});function Vi(s,e,t=!1){var i;const n=y(s);if(e!==void 0){if(typeof e=="string"&&e.length){const r=`zui-runjs-${St()}`;n.append(`