Merge branch 'release20.1' into story
This commit is contained in:
@@ -87,7 +87,7 @@ class product extends control
|
||||
$this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story', ($browseType != 'bysearch' and $browseType != 'reviewbyme' and $this->app->rawModule != 'projectstory'));
|
||||
|
||||
/* Save session. */
|
||||
$this->productZen->saveSession4Browse($product, $storyType, $browseType, $isProjectStory);
|
||||
$this->productZen->saveSession4Browse($product, $browseType);
|
||||
|
||||
/* Build search form. */
|
||||
$this->productZen->buildSearchFormForBrowse($project, $projectID, $productID, $branch, $param, $storyType, $browseType, $isProjectStory);
|
||||
|
||||
@@ -116,7 +116,7 @@ $fnBuildSingleCard = function($kanban) use ($executionActions, $lang, $kanbanvie
|
||||
cell
|
||||
(
|
||||
setClass('kanban-acl'),
|
||||
span(icon($kanban->acl == 'private' ? 'lock' : 'unlock-alt'), zget($lang->project->acls, $kanban->acl, ''))
|
||||
span(icon($kanban->acl == 'private' ? 'lock' : 'inherit-space', setClass('mr-1')), zget($lang->execution->kanbanAclList, $kanban->acl, ''))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -193,6 +193,95 @@ class dingapi
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据open_id列表,获取用户。
|
||||
* Batch get users by open_id list.
|
||||
*
|
||||
* @param array $userIdList
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function batchGetUsers($userIdList)
|
||||
{
|
||||
$useridPairs = array();
|
||||
$userGroup = array_chunk($userIdList, 49);
|
||||
foreach($userGroup as $userIdList)
|
||||
{
|
||||
$urls = array();
|
||||
foreach($userIdList as $userID) $urls[] = $this->apiUrl . "topapi/v2/user/get?access_token={$this->token}&userid={$userID}";
|
||||
$datas = $this->multiRequest($urls);
|
||||
foreach($datas as $response)
|
||||
{
|
||||
$response = json_decode($response);
|
||||
if(empty($response->result)) continue;
|
||||
|
||||
$user = $response->result;
|
||||
$useridPairs[$user->userid] = $user->name;
|
||||
}
|
||||
}
|
||||
|
||||
return $useridPairs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the concurrency of requests.
|
||||
*
|
||||
* @param array $urls
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function multiRequest($urls)
|
||||
{
|
||||
$curl = curl_multi_init();
|
||||
$urlHandlers = array();
|
||||
$urlData = array();
|
||||
|
||||
/* Set request header information. */
|
||||
/* Initialize multiple request handles to one. */
|
||||
foreach($urls as $url)
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
if(!isset($_SERVER['HTTPS']) or $_SERVER['HTTPS'] != 'on')
|
||||
{
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
}
|
||||
|
||||
$urlHandlers[] = $ch;
|
||||
curl_multi_add_handle($curl, $ch);
|
||||
}
|
||||
|
||||
$active = null;
|
||||
do
|
||||
{
|
||||
$mrc = curl_multi_exec($curl, $active);
|
||||
}
|
||||
while($mrc == CURLM_CALL_MULTI_PERFORM);
|
||||
|
||||
while($active and $mrc == CURLM_OK)
|
||||
{
|
||||
usleep(50000);
|
||||
if(curl_multi_select($curl) != -1)
|
||||
{
|
||||
do
|
||||
{
|
||||
$mrc = curl_multi_exec($curl, $active);
|
||||
}
|
||||
while($mrc == CURLM_CALL_MULTI_PERFORM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach($urlHandlers as $index => $ch)
|
||||
{
|
||||
$urlData[$index] = curl_multi_getcontent($ch);
|
||||
curl_multi_remove_handle($curl, $ch);
|
||||
}
|
||||
curl_multi_close($curl);
|
||||
return $urlData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for errors.
|
||||
*
|
||||
|
||||
@@ -155,7 +155,6 @@ class feishuapi
|
||||
/* Get depts by parent dept. */
|
||||
$depts = array();
|
||||
$pageToken = '';
|
||||
$index = 0;
|
||||
while(true)
|
||||
{
|
||||
$response = $this->queryAPI($this->apiUrl . "contact/v3/departments?parent_department_id={$departmentID}" . ($pageToken ? "&page_token={$pageToken}" : '') . "&fetch_child=false&page_size=50", '', array(CURLOPT_CUSTOMREQUEST => "GET"));
|
||||
@@ -163,11 +162,13 @@ class feishuapi
|
||||
{
|
||||
foreach($response->data->items as $key => $dept)
|
||||
{
|
||||
$depts[$index]['id'] = $dept->open_department_id;
|
||||
$depts[$index]['pId'] = empty($dept->parent_department_id) ? 1 : $dept->parent_department_id;
|
||||
$depts[$index]['name'] = $dept->name;
|
||||
$depts[$index]['open'] = 1;
|
||||
$index++;
|
||||
$data = array();
|
||||
$data['id'] = $dept->open_department_id;
|
||||
$data['pId'] = empty($dept->parent_department_id) ? 1 : $dept->parent_department_id;
|
||||
$data['name'] = $dept->name;
|
||||
$data['open'] = 1;
|
||||
|
||||
$depts[] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,19 +200,18 @@ class feishuapi
|
||||
foreach($departmentIdList as $departmentID) $urls[] = $this->apiUrl . "contact/v3/departments/{$departmentID}";
|
||||
$datas = $this->multiRequest($urls);
|
||||
|
||||
foreach($datas as $index => $dept)
|
||||
foreach($datas as $dept)
|
||||
{
|
||||
$index += 1;
|
||||
$dept = json_decode($dept);
|
||||
$dept = json_decode($dept);
|
||||
$data = array();
|
||||
$data['id'] = $dept->data->department->open_department_id;
|
||||
$data['pId'] = empty($dept->data->department->parent_department_id) ? 1 : $dept->data->department->parent_department_id;
|
||||
$data['name'] = $dept->data->department->name;
|
||||
$data['open'] = 1;
|
||||
|
||||
$memberCount = $dept->data->department->member_count;
|
||||
$status = $dept->data->department->status->is_deleted;
|
||||
|
||||
$depts[$index]['id'] = $dept->data->department->open_department_id;
|
||||
$depts[$index]['pId'] = empty($dept->data->department->parent_department_id) ? 1 : $dept->data->department->parent_department_id;
|
||||
$depts[$index]['name'] = $dept->data->department->name;
|
||||
$depts[$index]['open'] = 1;
|
||||
$depts[] = $data;
|
||||
}
|
||||
$depts = array_merge($depts, $this->getNextStepDeptTree($departmentIdList));
|
||||
|
||||
return $depts;
|
||||
}
|
||||
@@ -363,6 +363,114 @@ class feishuapi
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取传入部门列表的下一级部门树数据
|
||||
* Get next step dept tree by dept id list.
|
||||
*
|
||||
* @param array $deptIdList
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getNextStepDeptTree($deptIdList)
|
||||
{
|
||||
if(empty($deptIdList)) return array();
|
||||
|
||||
$nextStepUrls = array();
|
||||
foreach($deptIdList as $deptID) $nextStepUrls[] = $this->apiUrl . "contact/v3/departments?parent_department_id={$deptID}&fetch_child=false&page_size=50";
|
||||
|
||||
return $this->multiGetChildrenData($nextStepUrls);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取传入部门列表的下一页部门树数据。
|
||||
* Get next page dept tree by dept and page token pairs.
|
||||
*
|
||||
* @param array $deptPageTokenPairs
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getNextPageDeptTree($deptPageTokenPairs)
|
||||
{
|
||||
if(empty($deptPageTokenPairs)) return array();
|
||||
|
||||
$nextPageUrls = array();
|
||||
foreach($deptPageTokenPairs as $deptID => $pageToken) $nextPageUrls[] = $this->apiUrl . "contact/v3/departments?parent_department_id={$deptID}&page_token={$pageToken}&fetch_child=false&page_size=50";
|
||||
|
||||
return $this->multiGetChildrenData($nextPageUrls);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据API链接列表,多路获取子部门数据。
|
||||
* Multi get children data by api url list.
|
||||
*
|
||||
* @param array $urlList
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function multiGetChildrenData($urlList)
|
||||
{
|
||||
if(empty($urlList)) return array();
|
||||
|
||||
$nextStepParentIdList = array();
|
||||
$nextPageTokenPairs = array();
|
||||
|
||||
$depts = array();
|
||||
$datas = $this->multiRequest($urlList);
|
||||
foreach($datas as $response)
|
||||
{
|
||||
$response = json_decode($response);
|
||||
if(!isset($response->data->items)) continue;
|
||||
|
||||
foreach($response->data->items as $key => $dept)
|
||||
{
|
||||
$data = array();
|
||||
$data['id'] = $dept->open_department_id;
|
||||
$data['pId'] = empty($dept->parent_department_id) ? 1 : $dept->parent_department_id;
|
||||
$data['name'] = $dept->name;
|
||||
$data['open'] = 1;
|
||||
|
||||
$depts[] = $data;
|
||||
$nextStepParentIdList[] = $data['id'];
|
||||
}
|
||||
|
||||
if(!empty($response->data->page_token)) $nextPageTokenPairs[$data['pId']] = $response->data->page_token;
|
||||
}
|
||||
|
||||
$depts = array_merge($depts, $this->getNextStepDeptTree($nextStepParentIdList));
|
||||
$depts = array_merge($depts, $this->getNextPageDeptTree($nextPageTokenPairs));
|
||||
return $depts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据open_id列表,获取用户。
|
||||
* Batch get users by open_id list.
|
||||
*
|
||||
* @param array $userIdList
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function batchGetUsers($userIdList)
|
||||
{
|
||||
$openidPairs = array();
|
||||
$userGroup = array_chunk($userIdList, 49);
|
||||
foreach($userGroup as $userIdList)
|
||||
{
|
||||
$urls = array();
|
||||
foreach($userIdList as $userID) $urls[] = $this->apiUrl . "contact/v3/users/{$userID}";
|
||||
$datas = $this->multiRequest($urls);
|
||||
foreach($datas as $response)
|
||||
{
|
||||
$response = json_decode($response);
|
||||
if(empty($response->data->user)) continue;
|
||||
|
||||
$user = $response->data->user;
|
||||
$openidPairs[$user->open_id] = $user->name;
|
||||
}
|
||||
}
|
||||
|
||||
return $openidPairs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for errors.
|
||||
*
|
||||
|
||||
@@ -59,8 +59,7 @@ class node implements \JsonSerializable
|
||||
|
||||
public function __construct(mixed ...$args)
|
||||
{
|
||||
global $config;
|
||||
$this->gid = (isset($config->clientCache) && $config->clientCache) ? static::nextGid() : 'zin_' . uniqid();
|
||||
$this->gid = 'zin_' . uniqid();
|
||||
$this->props = new props();
|
||||
|
||||
disableGlobalRender();
|
||||
|
||||
@@ -49,7 +49,7 @@ window.addNewLine = function(e)
|
||||
options.disabled = false;
|
||||
options.placeholder = '';
|
||||
new zui.Picker(`#product${index}`, options);
|
||||
new zui.Picker(`#branch${index} `, {name: `branch[${index}]`, items: []});
|
||||
new zui.Picker(`#branch${index} `, {name: `branch[${index}]`, items: [], emptyValue: ''});
|
||||
new zui.Picker(`#roadmap${index}`, {name: `roadmap[${index}]`, items: []});
|
||||
|
||||
refreshPicker();
|
||||
@@ -119,7 +119,7 @@ window.loadProductBranches = function(obj)
|
||||
$.getJSON($.createLink('demand', 'ajaxGetBranches', "productID=" + productID), function(data)
|
||||
{
|
||||
$product.closest('.input-group').find('.linkBranch').toggleClass('hidden', !data.length);
|
||||
branchPicker.render({items: data});
|
||||
branchPicker.render({items: data, emptyValue: ''});
|
||||
if(data.length) branchPicker.$.setValue(data[0].value);
|
||||
|
||||
loadRoadmap($product);
|
||||
|
||||
@@ -97,6 +97,7 @@ class productRoadmapBox extends wg
|
||||
set::name("branch[$index]"),
|
||||
set::items($branches),
|
||||
set::value($defaultBranch),
|
||||
set::emptyValue('')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -308,6 +308,10 @@ $config->block->size['scrumproject']['projectdynamic'] = array(1 => 8, 2 => 8);
|
||||
$config->block->size['waterfallproject']['waterfallgantt'] = array(2 => 8, 1 => 8);
|
||||
$config->block->size['waterfallproject']['projectdynamic'] = array(1 => 8, 2 => 8);
|
||||
|
||||
$config->block->size['agileplusproject'] = $config->block->size['scrumproject'];
|
||||
$config->block->size['waterfallplusproject'] = $config->block->size['waterfallproject'];
|
||||
$config->block->size['ipdproject'] = $config->block->size['waterfallproject'];
|
||||
|
||||
$config->block->size['execution']['overview'] = array(1 => 3);
|
||||
$config->block->size['execution']['statistic'] = array(2 => 5, 1 => 8);
|
||||
$config->block->size['execution']['list'] = array(2 => 6, 1 => 6);
|
||||
|
||||
@@ -615,7 +615,7 @@ class blockZen extends block
|
||||
if(preg_match('/[^a-zA-Z0-9_]/', $block->params->type)) return;
|
||||
|
||||
$this->view->projects = $this->loadModel('project')->getPairsByProgram();
|
||||
$this->view->testtasks = $this->dao->select("t1.*,t2.name as productName,t2.shadow,t3.name as buildName,t4.name as projectName, CONCAT(t4.name, '/', t3.name) as executionBuild")->from(TABLE_TESTTASK)->alias('t1')
|
||||
$this->view->testtasks = $this->dao->select("DISTINCT t1.*,t2.name as productName,t2.shadow,t3.name as buildName,t4.name as projectName, CONCAT(t4.name, '/', t3.name) as executionBuild")->from(TABLE_TESTTASK)->alias('t1')
|
||||
->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product=t2.id')
|
||||
->leftJoin(TABLE_BUILD)->alias('t3')->on('t1.build=t3.id')
|
||||
->leftJoin(TABLE_PROJECT)->alias('t4')->on('t1.execution=t4.id')
|
||||
|
||||
@@ -1217,11 +1217,11 @@ class commonModel extends model
|
||||
* 以下页面可以允许在非 iframe 中打开,所以要忽略这些页面。
|
||||
* The following pages can be allowed to open in non-iframe, so ignore these pages.
|
||||
*/
|
||||
$module = $this->app->getModuleName();
|
||||
$whitelist = is_string($whitelist) ? $whitelist : '|index|tutorial|install|upgrade|sso|cron|misc|user-login|user-deny|user-logout|user-reset|user-forgetpassword|user-resetpassword|my-changepassword|my-preference|file-read|file-download|file-preview|file-uploadimages|file-ajaxwopifiles|report-annualdata|misc-captcha|execution-printkanban|traincourse-ajaxuploadlargefile|traincourse-playvideo|screen-view|zanode-create|screen-ajaxgetchart|ai-chat|';
|
||||
$skiplist = '|cron-index|';
|
||||
$module = $this->app->getModuleName();
|
||||
$whitelist = is_string($whitelist) ? $whitelist : '|index|tutorial|install|upgrade|sso|cron|misc|user-login|user-deny|user-logout|user-reset|user-forgetpassword|user-resetpassword|my-changepassword|my-preference|file-read|file-download|file-preview|file-uploadimages|file-ajaxwopifiles|report-annualdata|misc-captcha|execution-printkanban|traincourse-ajaxuploadlargefile|traincourse-playvideo|screen-view|zanode-create|screen-ajaxgetchart|ai-chat|';
|
||||
$iframeList = '|cron-index|';
|
||||
|
||||
if((strpos($whitelist, "|{$module}|") !== false && strpos($skiplist, "|{$module}-{$method}|") === false) || strpos($whitelist, "|{$module}-{$method}|") !== false) return true;
|
||||
if(strpos($iframeList, "|{$module}-{$method}|") === false && (strpos($whitelist, "|{$module}|") !== false || strpos($whitelist, "|{$module}-{$method}|") !== false)) return true;
|
||||
|
||||
/**
|
||||
* 如果以上条件都不满足,则视为当前页面必须在 iframe 中打开,使用 302 跳转实现。
|
||||
|
||||
@@ -8,3 +8,4 @@
|
||||
|
||||
.actions-menu {bottom: 12px;}
|
||||
#mainContent {margin-bottom: 60px;}
|
||||
#mainContent .desc-box {max-height: 6rem;}
|
||||
|
||||
@@ -298,7 +298,8 @@ div
|
||||
),
|
||||
div
|
||||
(
|
||||
set::className('detail-content mt-4'),
|
||||
set::className('detail-content mt-4 overflow-hidden desc-box'),
|
||||
set::title(strip_tags($execution->desc)),
|
||||
html($execution->desc)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ const apps =
|
||||
defaultCode: '',
|
||||
zIndex: 10,
|
||||
frameContent: null,
|
||||
theme: null,
|
||||
oldPages: new Set(oldPages)
|
||||
};
|
||||
|
||||
@@ -742,6 +743,12 @@ function changeAppsTheme(theme)
|
||||
app.iframe.contentWindow.changeAppTheme(theme);
|
||||
}
|
||||
});
|
||||
apps.theme = theme;
|
||||
$.get($.createLink('index', 'app'), html =>
|
||||
{
|
||||
apps.frameContent = html;
|
||||
apps.theme = null;
|
||||
});
|
||||
}
|
||||
|
||||
function updateUserToolbar()
|
||||
|
||||
@@ -273,7 +273,7 @@ window.renderBuildItem = function(info)
|
||||
{
|
||||
info.item.icon = 'ver';
|
||||
info.item.titleUrl = canViewBuild ? $.createLink('build', 'view', `id=${info.item.fromID}`) : '';
|
||||
info.item.titleAttrs = {'class': 'card-title clip', 'title' : info.item.title};
|
||||
info.item.titleAttrs = {'class': 'card-title clip', 'title': info.item.title, 'data-app': 'project'};
|
||||
|
||||
const date = '<span class="label gray-pale">' + info.item.date + '</span>';
|
||||
info.item.content = {html: date}
|
||||
@@ -282,7 +282,7 @@ window.renderProductplanItem = function(info)
|
||||
{
|
||||
info.item.icon = 'delay';
|
||||
info.item.titleUrl = canViewPlan ? $.createLink('productplan', 'view', `id=${info.item.fromID}`) : '';
|
||||
info.item.titleAttrs = {'class': 'card-title clip', 'title' : info.item.title};
|
||||
info.item.titleAttrs = {'class': 'card-title clip', 'title': info.item.title};
|
||||
|
||||
if(info.item.deleted == '0')
|
||||
{
|
||||
|
||||
+19
-1
@@ -244,7 +244,9 @@ class mailModel extends model
|
||||
|
||||
ob_start();
|
||||
|
||||
$body = $this->mailTao->replaceImageURL($body);
|
||||
$images = $this->mailTao->getImages($body);
|
||||
if($images) $body = $this->mailTao->replaceImageURL($body, $images);
|
||||
|
||||
list($toList, $ccList) = $this->mailTao->processToAndCC($toList, $ccList, $includeMe);
|
||||
/* Get realname and email of users. */
|
||||
if(empty($emails)) $emails = $this->loadModel('user')->getRealNameAndEmails($toList . ',' . $ccList);
|
||||
@@ -261,6 +263,7 @@ class mailModel extends model
|
||||
$this->setTO(explode(',', $toList), $emails);
|
||||
$this->setCC(explode(',', $ccList), $emails);
|
||||
$this->setBody($this->convertCharset($body));
|
||||
if($images) $this->setImages($images);
|
||||
$this->setErrorLang();
|
||||
$this->mta->send();
|
||||
}
|
||||
@@ -352,6 +355,21 @@ class mailModel extends model
|
||||
$this->mta->msgHtml($body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置嵌入邮件的图片。
|
||||
* Set embedded images in the email.
|
||||
*
|
||||
* @param array $images
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
public function setImages(array $images): void
|
||||
{
|
||||
$wwwRoot = $this->app->getWwwRoot();
|
||||
$images = array_filter(array_unique($images));
|
||||
foreach($images as $image) $this->mta->AddEmbeddedImage($wwwRoot . $image, basename($image));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert charset.
|
||||
*
|
||||
|
||||
+86
-9
@@ -62,21 +62,98 @@ class mailTao extends mailModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace image URL for mail content.
|
||||
* 获取邮件内容中的图片 url 和物理文件的键值对。
|
||||
* Get key-value pairs of image URL and physical file in mail content.
|
||||
*
|
||||
* @param string $body
|
||||
* @access protected
|
||||
* @return array
|
||||
*/
|
||||
public function getImages(string $body): array
|
||||
{
|
||||
$images = array();
|
||||
|
||||
/* 匹配形如 src="/file-read-1.jpg" 或 scr="/index.php?m=file&f=read&fileID=1" 的图片。 Match images like src="/file-read-1.jpg" or scr="/index.php?m=file&f=read&fileID=1". */
|
||||
$readLinkReg = str_replace(array('%fileID%', '/', '.', '?'), array('[0-9]+', '\/', '\.', '\?'), helper::createLink('file', 'read', 'fileID=(%fileID%)', '\w+'));
|
||||
preg_match_all('/ src="(' . $readLinkReg . ')" /', $body, $matches);
|
||||
$images += $this->getImagesByFileID($matches);
|
||||
|
||||
/* 匹配形如 src="{1.jpg}" 的图片。 Match images like src="{1.jpg}". */
|
||||
preg_match_all('/ src="({([0-9]+)\.\w+?})" /', $body, $matches);
|
||||
$images += $this->getImagesByFileID($matches);
|
||||
|
||||
/* 匹配形如 src="/data/upload/1.jpg" 的图片。 Match images like src="/data/upload/1.jpg". */
|
||||
preg_match_all('/ src="(\/?data\/upload\/[\/\w+]*)"/', $body, $matches);
|
||||
$images += $this->getImagesByPath($matches);
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件 ID 获取图片 url 和物理文件的键值对。
|
||||
* Get key-value pairs of image URL and physical file by file ID.
|
||||
*
|
||||
* @param array $matches
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getImagesByFileID(array $matches): array
|
||||
{
|
||||
if(!isset($matches[2])) return array();
|
||||
|
||||
$this->loadModel('file');
|
||||
|
||||
$images = array();
|
||||
foreach($matches[2] as $key => $fileID)
|
||||
{
|
||||
if(!$fileID) continue;
|
||||
|
||||
$file = $this->file->getByID((int)$fileID);
|
||||
if(!$file) continue;
|
||||
if(!in_array($file->extension, $this->config->file->imageExtensions)) continue;
|
||||
|
||||
$images[$matches[1][$key]] = $file->webPath;
|
||||
}
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径获取图片 url 和物理文件的键值对。
|
||||
* Get key-value pairs of image URL and physical file by path.
|
||||
*
|
||||
* @param array $matches
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getImagesByPath(array $matches): array
|
||||
{
|
||||
if(!isset($matches[1])) return array();
|
||||
|
||||
$images = array();
|
||||
foreach($matches[1] as $key => $path)
|
||||
{
|
||||
if(!$path) continue;
|
||||
|
||||
$images[$path] = $path;
|
||||
}
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace image URL for mail content.
|
||||
*
|
||||
* @param string $body
|
||||
* @param array $images
|
||||
* @access protected
|
||||
* @return string
|
||||
*/
|
||||
protected function replaceImageURL(string $body): string
|
||||
protected function replaceImageURL(string $body, array $images): string
|
||||
{
|
||||
/* Replace full webPath image for mail. */
|
||||
$sysURL = zget($this->config->mail, 'domain', common::getSysURL());
|
||||
$readLinkReg = str_replace(array('%fileID%', '/', '.', '?'), array('[0-9]+', '\/', '\.', '\?'), helper::createLink('file', 'read', 'fileID=(%fileID%)', '\w+'));
|
||||
|
||||
$body = preg_replace('/ src="(' . $readLinkReg . ')" /', ' src="' . $sysURL . '$1" ', $body);
|
||||
$body = preg_replace('/ src="{([0-9]+)(\.(\w+))?}" /', ' src="' . $sysURL . helper::createLink('file', 'read', "fileID=$1", "$3") . '" ', $body);
|
||||
$body = preg_replace('/<img (.*)src="\/?data\/upload/', '<img $1 src="' . $sysURL . $this->config->webRoot . 'data/upload', $body);
|
||||
foreach($images as $url => $file)
|
||||
{
|
||||
if(!$file) continue;
|
||||
$body = str_replace($url, 'cid:' . basename($file), $body);
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
@@ -47,12 +47,7 @@
|
||||
.diff-back-btn {border: none; --tw-ring-shadow: none}
|
||||
.diff-back-btn::after {border: none;}
|
||||
.diff-back-btn::before {border: none;}
|
||||
.label-info {background-color: #37b2fe}
|
||||
.label-info[href]:focus,.label-info[href]:hover {color: #fff; background-color: #049efe}
|
||||
.label-info.label-outline {color: #37b2fe; background: 0 0; border: 1px solid #37b2fe}
|
||||
.label {padding: .2em .6em .2em; color: #fff;text-align: center; white-space: nowrap; vertical-align: middle; border-radius: .25em;}
|
||||
.diff-label {border: none; --tw-ring-shadow: none; margin-left: 5px; margin-right: 10px}
|
||||
.label-exchange {background-color: #566F7C; cursor: pointer;}
|
||||
|
||||
#fileTabs .tab-pane {display: none;}
|
||||
#fileTabs .tab-pane.active {display: block;}
|
||||
|
||||
@@ -17,3 +17,64 @@ $(document).off('click', '.batch-btn').on('click', '.batch-btn', function()
|
||||
postAndLoadPage(url, form);
|
||||
}
|
||||
});
|
||||
|
||||
window.clickTotask = function(event)
|
||||
{
|
||||
const params = $(event.target).closest('a').attr('href').split('&');
|
||||
$('#feedbackID').val(params[0]);
|
||||
getProjects(params[1]);
|
||||
changeTaskProjects();
|
||||
};
|
||||
|
||||
window.toTask = function()
|
||||
{
|
||||
const projectID = $('[name="taskProjects"]').val();
|
||||
const executionID = $('[name="executions"]').val() ? $('[name="executions"]').val() : 0;
|
||||
const feedbackID = $('#feedbackID').val();
|
||||
changeTaskProjects();
|
||||
|
||||
if(projectID && executionID != 0)
|
||||
{
|
||||
zui.Modal.hide('#toTask');
|
||||
|
||||
const url = $.createLink('task', 'create', 'executionID=' + executionID + '&storyID=0&moduleID=0&taskID=0&todoID=0&extra=projectID=' + projectID + ',feedbackID=' + feedbackID);
|
||||
openPage(url, 'execution');
|
||||
}
|
||||
else if(projectID == 0)
|
||||
{
|
||||
zui.Modal.alert(errorNoProject);
|
||||
}
|
||||
else
|
||||
{
|
||||
zui.Modal.alert(errorNoExecution);
|
||||
}
|
||||
};
|
||||
|
||||
function getProjects(productID)
|
||||
{
|
||||
const link = $.createLink('feedback', 'ajaxGetProjects', 'productID=' + productID + '&field=taskProjects');
|
||||
$.getJSON(link, function(data)
|
||||
{
|
||||
if(data)
|
||||
{
|
||||
let $projectPicker = $('[name=taskProjects]').zui('picker');
|
||||
$projectPicker.render(data);
|
||||
$projectPicker.$.setValue('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function changeTaskProjects(event)
|
||||
{
|
||||
const projectID = event != undefined ? $(event.target).val() : $('[name="taskProjects"]').val();
|
||||
const link = $.createLink('feedback', 'ajaxGetExecutions', 'projectID=' + projectID);
|
||||
$.getJSON(link, function(data)
|
||||
{
|
||||
if(data)
|
||||
{
|
||||
let $executionPicker = $('[name=executions]').zui('picker');
|
||||
$executionPicker.render(data);
|
||||
$executionPicker.$.setValue('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace zin;
|
||||
|
||||
include 'header.html.php';
|
||||
|
||||
jsVar('errorNoProject', $lang->feedback->noProject);
|
||||
jsVar('errorNoExecution', $lang->feedback->noExecution);
|
||||
|
||||
featureBar
|
||||
(
|
||||
set::current($type),
|
||||
@@ -76,4 +79,57 @@ dtable
|
||||
set::footToolbar($footToolbar)
|
||||
);
|
||||
|
||||
modal
|
||||
(
|
||||
setID('toTask'),
|
||||
set::modalProps(array('title' => $lang->feedback->selectProjects)),
|
||||
to::footer
|
||||
(
|
||||
div
|
||||
(
|
||||
setClass('toolbar gap-4 w-full justify-center'),
|
||||
btn($lang->feedback->nextStep, setID('toTaskButton'), setClass('primary'), set('data-on', 'click'), set('data-call', 'toTask')),
|
||||
btn($lang->cancel, setID('cancelButton'), setData(array('dismiss' => 'modal')))
|
||||
)
|
||||
),
|
||||
formPanel
|
||||
(
|
||||
on::change('#taskProjects', 'changeTaskProjects'),
|
||||
set::actions(''),
|
||||
formRow
|
||||
(
|
||||
formGroup
|
||||
(
|
||||
set::label($lang->feedback->project),
|
||||
set::required(true),
|
||||
set::control('picker'),
|
||||
set::name('taskProjects'),
|
||||
set::items($projects),
|
||||
)
|
||||
),
|
||||
formRow
|
||||
(
|
||||
formGroup
|
||||
(
|
||||
set::label($lang->feedback->execution),
|
||||
set::required(true),
|
||||
inputGroup
|
||||
(
|
||||
setID('executionBox'),
|
||||
picker
|
||||
(
|
||||
set::name('executions'),
|
||||
set::items(array())
|
||||
),
|
||||
input
|
||||
(
|
||||
setClass('hidden'),
|
||||
set::name('feedbackID')
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
render();
|
||||
|
||||
@@ -169,7 +169,7 @@ class product extends control
|
||||
if($storyType != 'story') $stories = $this->loadModel('story')->appendChildren($productID, $stories, $storyType);
|
||||
|
||||
/* Save session. */
|
||||
$this->productZen->saveSession4Browse($product, $storyType, $browseType, $isProjectStory);
|
||||
$this->productZen->saveSession4Browse($product, $browseType);
|
||||
|
||||
/* Build search form. */
|
||||
$this->productZen->buildSearchFormForBrowse($project, $projectID, $productID, $branch, $param, $storyType, $browseType, $isProjectStory);
|
||||
|
||||
@@ -10,3 +10,5 @@
|
||||
.member-list > .center-y {width: 12.5%}
|
||||
.memberBox .panel-heading, .otherInfoBox .panel-heading{background-color: var(--color-gray-100); color: var(--color-gray-900)}
|
||||
#mainContainer{margin-bottom: 60px;}
|
||||
|
||||
#mainContent .desc-box {max-height: 6rem;}
|
||||
|
||||
@@ -195,7 +195,8 @@ div
|
||||
),
|
||||
div
|
||||
(
|
||||
set::className('detail-content mt-4'),
|
||||
set::className('detail-content mt-4 overflow-hidden desc-box'),
|
||||
set::title(strip_tags($product->desc)),
|
||||
html($product->desc)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1067,13 +1067,11 @@ class productZen extends product
|
||||
* Save session variables for browse page.
|
||||
*
|
||||
* @param object|null $product
|
||||
* @param string $storyType
|
||||
* @param string $browseType
|
||||
* @param bool $isProjectStory
|
||||
* @access protected
|
||||
* @return void
|
||||
*/
|
||||
protected function saveSession4Browse(object|null $product, string $storyType, string $browseType, bool $isProjectStory): void
|
||||
protected function saveSession4Browse(object|null $product, string $browseType): void
|
||||
{
|
||||
$uri = $this->app->getURI(true);
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ $config->programplan->custom->createFields = 'PM,attribute,mileston
|
||||
$config->programplan->custom->createIpdFields = 'PM,attribute,milestone';
|
||||
$config->programplan->custom->createWaterfallFields = 'PM,attribute,milestone';
|
||||
$config->programplan->custom->createWaterfallplusFields = 'PM,attribute,milestone';
|
||||
if(!empty((bool)$config->setPercent)) $config->programplan->list->customCreateFields .= ',percent';
|
||||
if(isset($config->setPercent) && !empty((bool)$config->setPercent)) $config->programplan->list->customCreateFields .= ',percent';
|
||||
if(!empty($config->setCode))
|
||||
{
|
||||
$config->programplan->custom->createFields .= ',code';
|
||||
|
||||
@@ -12,7 +12,7 @@ $config->programplan->form->create['type'] = array('label' => $lang->execu
|
||||
$config->programplan->form->create['name'] = array('label' => $lang->nameAB, 'type' => 'string', 'control' => 'text', 'required' => true, 'default' => '', 'base' => true, 'filter' => 'trim');
|
||||
$config->programplan->form->create['code'] = array('label' => $lang->code, 'type' => 'string', 'control' => 'text', 'required' => false, 'default' => '', 'filter' => 'trim');
|
||||
$config->programplan->form->create['PM'] = array('label' => $lang->programplan->PMAB, 'type' => 'string', 'control' => 'picker', 'required' => false, 'default' => '', 'options' => '');
|
||||
if(!empty((bool)$config->setPercent)) $config->programplan->form->create['percent'] = array('label' => $lang->programplan->percent, 'type' => 'float', 'control' => 'text', 'required' => false, 'default' => 0);
|
||||
if(isset($config->setPercent) && !empty((bool)$config->setPercent)) $config->programplan->form->create['percent'] = array('label' => $lang->programplan->percent, 'type' => 'float', 'control' => 'text', 'required' => false, 'default' => 0);
|
||||
$config->programplan->form->create['attribute'] = array('label' => $lang->programplan->attribute, 'type' => 'string', 'control' => 'picker', 'required' => false, 'default' => 0, 'options' => $lang->stage->typeList);
|
||||
$config->programplan->form->create['point'] = array('label' => $lang->programplan->point, 'type' => 'array', 'control' => 'picker', 'required' => false, 'default' => '', 'options' => array(''), 'multiple' => true);
|
||||
$config->programplan->form->create['parallel'] = array('label' => '', 'type' => 'int', 'control' => 'hidden', 'required' => false, 'default' => 0);
|
||||
|
||||
@@ -13,3 +13,5 @@
|
||||
|
||||
.member-list > .center-y {width: 12.5%}
|
||||
.maxh-80{max-height: 20rem;}
|
||||
|
||||
#mainContent .desc-box {max-height: 6rem;}
|
||||
|
||||
@@ -25,7 +25,7 @@ $fields->field('name')
|
||||
->tip($copyProject ? $lang->project->copyProject->nameTips : null)
|
||||
->tipClass($copyProject ? 'text-warning' : null)
|
||||
->value($copyProject ? data('copyProject.name') : '');
|
||||
if($copyProject) $fields->field('multiple')->hidden(true)->value(data('copyProject.multiple'));
|
||||
if($copyProject) $fields->field('multiple')->hidden(true)->value(in_array($model, array('scrum', 'kanban')) ? data('copyProject.multiple') : 'on');
|
||||
|
||||
if($hasCode)
|
||||
{
|
||||
|
||||
@@ -274,7 +274,12 @@ row
|
||||
)
|
||||
),
|
||||
div(setClass('flex mt-4 program'), div(setClass('clip programBox'), $programDom)),
|
||||
div(set::className('detail-content mt-4'), html($project->desc))
|
||||
div
|
||||
(
|
||||
set::className('detail-content mt-4 overflow-hidden desc-box'),
|
||||
set::title(strip_tags($project->desc)),
|
||||
html($project->desc)
|
||||
)
|
||||
)
|
||||
),
|
||||
div
|
||||
|
||||
@@ -46,7 +46,7 @@ class projectZen extends project
|
||||
$copyProject = $this->project->getByID($copyProjectID);
|
||||
if($copyProject)
|
||||
{
|
||||
$project->multiple = $copyProject->multiple;
|
||||
if(in_array($this->post->model, array('scrum', 'kanban'))) $project->multiple = $copyProject->multiple;
|
||||
$project->hasProduct = $copyProject->hasProduct;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
.diff-back-btn {border: none; --tw-ring-shadow: none}
|
||||
.diff-back-btn::after {border: none;}
|
||||
.diff-back-btn::before {border: none;}
|
||||
.label-info {background-color: #37b2fe}
|
||||
.label-info[href]:focus,.label-info[href]:hover {color: #fff; background-color: #049efe}
|
||||
.label-info.label-outline {color: #37b2fe; background: 0 0; border: 1px solid #37b2fe}
|
||||
.label {padding: .2em .6em .2em; color: #fff;text-align: center; white-space: nowrap; vertical-align: middle; border-radius: .25em;}
|
||||
.diff-label {border: none; --tw-ring-shadow: none; margin-left: 5px; margin-right: 10px}
|
||||
.label-exchange {background-color: #566F7C; cursor: pointer;}
|
||||
.label-exchange {background-color: #565F7C; cursor: pointer;}
|
||||
|
||||
#fileTabs .tab-pane {display: none;}
|
||||
#fileTabs .tab-pane.active {display: block;}
|
||||
|
||||
@@ -280,7 +280,7 @@ window.afterPageUpdate = function()
|
||||
title: copied,
|
||||
tipClass: 'success',
|
||||
});
|
||||
}, 200);
|
||||
}, 1);
|
||||
};
|
||||
|
||||
$(function()
|
||||
|
||||
@@ -622,7 +622,7 @@ class repoModel extends model
|
||||
{
|
||||
$productItem = array();
|
||||
$productItem['pid'] = $productID;
|
||||
$productItem['type'] = 'product';
|
||||
$productItem['type'] = $product->shadow ? $this->lang->project->common : 'product';
|
||||
$productItem['text'] = $product->name;
|
||||
$productItem['items'] = array();
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ if($repo->SCM != 'Subversion')
|
||||
set::text(substr($oldRevision, 0, 10)),
|
||||
set::data(array('data' => $menuData, 'tabs' => $tabs))
|
||||
);
|
||||
$breadcrumbItems[] = span(setClass('label label-exchange mr-2'), icon('exchange'));
|
||||
$breadcrumbItems[] = span(setClass('label label-exchange mr-2 text-white'), icon('exchange'));
|
||||
$breadcrumbItems[] = span($lang->repo->target . ':');
|
||||
$breadcrumbItems[] = dropmenu
|
||||
(
|
||||
|
||||
@@ -75,13 +75,6 @@ $config->story->actionList['recall']['url'] = array('module' => 'story', '
|
||||
$config->story->actionList['recall']['data-app'] = $app->tab;
|
||||
$config->story->actionList['recall']['className'] = 'ajax-submit';
|
||||
|
||||
$config->story->actionList['recallChange']['icon'] = 'undo';
|
||||
$config->story->actionList['recallChange']['text'] = $lang->story->recallChange;
|
||||
$config->story->actionList['recallChange']['hint'] = $lang->story->recallChange;
|
||||
$config->story->actionList['recallChange']['url'] = array('module' => 'story', 'method' => 'recall', 'params' => 'storyID={id}&from=view&confirm=no&storyType={type}');
|
||||
$config->story->actionList['recallChange']['data-app'] = $app->tab;
|
||||
$config->story->actionList['recallChange']['className'] = 'ajax-submit';
|
||||
|
||||
$config->story->actionList['review']['icon'] = 'search';
|
||||
$config->story->actionList['review']['text'] = $lang->story->review;
|
||||
$config->story->actionList['review']['hint'] = $lang->story->review;
|
||||
|
||||
@@ -3643,11 +3643,10 @@ class storyModel extends model
|
||||
$action = 'batchcreate';
|
||||
}
|
||||
|
||||
if($action == 'recallchange') return $story->status == 'changing';
|
||||
if($action == 'recall') return $story->status == 'reviewing' || $story->status == 'changing';
|
||||
if($action == 'close') return $story->status != 'closed';
|
||||
if($action == 'activate') return $story->status == 'closed';
|
||||
if($action == 'assignto') return $story->status != 'closed';
|
||||
if($action == 'recall') return $story->status == 'reviewing' || $story->status == 'changing';
|
||||
if($action == 'close') return $story->status != 'closed';
|
||||
if($action == 'activate') return $story->status == 'closed';
|
||||
if($action == 'assignto') return $story->status != 'closed';
|
||||
if($action == 'submitreview' && strpos('draft,changing', $story->status) === false) return false;
|
||||
if($action == 'createtestcase' || $action == 'batchcreatetestcase') return $config->vision != 'lite' && $story->parent >= 0 && $story->type != 'requirement';
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ for($i = $story->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'));
|
||||
}
|
||||
|
||||
$versionItem->selected($version == $i);
|
||||
@@ -220,7 +220,7 @@ foreach($actions as $key => $action)
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($action['url'])) $actions[$key]['url'] = str_replace(array('{id}', '{type}', '{product}', '{branch}', '{module}', '{execution}'), array($story->id, $story->type, $story->product, $story->branch, $story->module, isset($projectID) ? $projectID : 0), $action['url']);
|
||||
if(isset($action['url'])) $actions[$key]['url'] = str_replace(array('{id}', '{type}', '{product}', '{branch}', '{module}', '{execution}'), array($story->id, $story->type, $story->product, $story->branch, $story->module, $app->tab == 'project' ? $projectID : $executionID), $action['url']);
|
||||
if(isset($action['items']))
|
||||
{
|
||||
foreach($action['items'] as $itemKey => $itemAction)
|
||||
|
||||
@@ -215,9 +215,9 @@ window.clickSubmit = function()
|
||||
window.renderRowData = function($row, index, row)
|
||||
{
|
||||
$row.addClass('member member-' + (row ? row.status : 'wait'));
|
||||
$row.data('estimate', row ? row.teamEstimate : 0);
|
||||
$row.data('consumed', row ? row.teamConsumed : 0);
|
||||
$row.data('left', row ? row.teamLeft : 0);
|
||||
$row.attr('data-estimate', row ? row.teamEstimate : 0);
|
||||
$row.attr('data-consumed', row ? row.teamConsumed : 0);
|
||||
$row.attr('data-left', row ? row.teamLeft : 0);
|
||||
|
||||
if(row && row.memberDisabled)
|
||||
{
|
||||
@@ -237,5 +237,5 @@ window.renderRowData = function($row, index, row)
|
||||
$row.find('[name^=teamConsumed]').attr('readonly', 'readonly');
|
||||
}
|
||||
|
||||
if(taskMode == 'linear') $row.find('[data-name=id]').addClass('center').html("<span class='team-number'>" + $row.find('[data-name=id]').text() + "</span><i class='icon-angle-down'><i/>");
|
||||
$row.find('[data-name=id]').addClass('center').html("<span class='team-number'>" + $row.find('[data-name=id]').text() + "</span><i class='icon-angle-down " + (taskMode == 'linear' ? '' : 'hidden') + "'><i/>");
|
||||
}
|
||||
|
||||
@@ -611,5 +611,5 @@ window.changeTeamMember = function(e)
|
||||
window.renderRowData = function($row, index, row)
|
||||
{
|
||||
const mode = $('[name=mode]').val();
|
||||
if(mode == 'linear') $row.find('[data-name=id]').addClass('center').html("<span class='team-number'>" + $row.find('[data-name=id]').text() + "</span><i class='icon-angle-down'><i/>");
|
||||
$row.find('[data-name=id]').addClass('center').html("<span class='team-number'>" + $row.find('[data-name=id]').text() + "</span><i class='icon-angle-down " + (mode == 'linear' ? '' : 'hidden') + "'><i/>");
|
||||
}
|
||||
|
||||
@@ -268,9 +268,9 @@ function updateAssignedTo()
|
||||
window.renderRowData = function($row, index, row)
|
||||
{
|
||||
$row.addClass('member member-' + (row ? row.memberStatus : 'wait'));
|
||||
$row.data('estimate', row ? row.teamEstimate : 0);
|
||||
$row.data('consumed', row ? row.teamConsumed : 0);
|
||||
$row.data('left', row ? row.teamLeft : 0);
|
||||
$row.attr('data-estimate', row ? row.teamEstimate : 0);
|
||||
$row.attr('data-consumed', row ? row.teamConsumed : 0);
|
||||
$row.attr('data-left', row ? row.teamLeft : 0);
|
||||
|
||||
if(row && row.memberDisabled)
|
||||
{
|
||||
@@ -291,5 +291,5 @@ window.renderRowData = function($row, index, row)
|
||||
}
|
||||
|
||||
const mode = $('[name=mode]').val();
|
||||
if(mode == 'linear') $row.find('[data-name=id]').addClass('center').html("<span class='team-number'>" + $row.find('[data-name=id]').text() + "</span><i class='icon-angle-down'><i/>");
|
||||
$row.find('[data-name=id]').addClass('center').html("<span class='team-number'>" + $row.find('[data-name=id]').text() + "</span><i class='icon-angle-down " + (mode == 'linear' ? '' : 'hidden') + "'><i/>");
|
||||
}
|
||||
|
||||
@@ -2775,7 +2775,7 @@ class taskModel extends model
|
||||
$now = helper::now();
|
||||
if($team->status == 'done')
|
||||
{
|
||||
$task->assignedTo = $this->getAssignedTo4Multi($oldTask->team, $oldTask, 'next');
|
||||
$task->assignedTo = $this->getAssignedTo4Multi($oldTask->team, $oldTask, 'current');
|
||||
$task->assignedDate = $now;
|
||||
}
|
||||
|
||||
|
||||
@@ -2293,7 +2293,7 @@ class testcaseZen extends testcase
|
||||
|
||||
if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $message, 'id' => $caseID));
|
||||
/* If link from no head then reload. */
|
||||
if(isonlybody() || helper::isAjaxRequest('modal')) return $this->send(array('result' => 'success', 'message' => $message, 'load' => true, 'closeModal' => true));
|
||||
if(isInModal() || helper::isAjaxRequest('modal')) return $this->send(array('result' => 'success', 'message' => $message, 'load' => true, 'closeModal' => true));
|
||||
|
||||
/* 判断是否当前一级菜单不是 QA,并且 caseList session 存在,并且 caseList 不是动态页面。 */
|
||||
/* Use this session link, when the tab is not QA, a session of the case list exists, and the session is not from the Dynamic page. */
|
||||
|
||||
@@ -19,7 +19,7 @@ $config->testreport->actionList['delete']['data-confirm'] = array('message' => $
|
||||
$config->testreport->actionList['create']['icon'] = 'refresh';
|
||||
$config->testreport->actionList['create']['hint'] = $lang->testreport->recreate;
|
||||
$config->testreport->actionList['create']['text'] = $lang->testreport->recreate;
|
||||
$config->testreport->actionList['create']['url'] = array('module' => 'testreport', 'method' => 'create', 'params' => 'reportID={objectID}&objectType={objectType}');
|
||||
$config->testreport->actionList['create']['url'] = array('module' => 'testreport', 'method' => 'create', 'params' => 'reportID={objectID}&objectType={objectType}&extra={tasks}');
|
||||
$config->testreport->actionList['create']['data-app'] = $app->tab;
|
||||
|
||||
$config->testreport->dtable->fieldList['id']['name'] = 'id';
|
||||
@@ -43,7 +43,7 @@ $config->testreport->dtable->fieldList['execution']['title'] = $lang->testrep
|
||||
$config->testreport->dtable->fieldList['execution']['type'] = 'text';
|
||||
$config->testreport->dtable->fieldList['execution']['sortType'] = true;
|
||||
|
||||
$config->testreport->dtable->fieldList['tasks']['name'] = 'tasks';
|
||||
$config->testreport->dtable->fieldList['tasks']['name'] = 'taskName';
|
||||
$config->testreport->dtable->fieldList['tasks']['title'] = $lang->testreport->testtask;
|
||||
$config->testreport->dtable->fieldList['tasks']['type'] = 'text';
|
||||
|
||||
|
||||
@@ -135,13 +135,13 @@ class testreport extends control
|
||||
*
|
||||
* @param int $objectID
|
||||
* @param string $objectType
|
||||
* @param int $extra
|
||||
* @param string $extra
|
||||
* @param string $begin
|
||||
* @param string $end
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
public function create(int $objectID = 0, string $objectType = 'testtask', int $extra = 0, string $begin = '', string $end = '')
|
||||
public function create(int $objectID = 0, string $objectType = 'testtask', string $extra = '', string $begin = '', string $end = '')
|
||||
{
|
||||
if($_POST)
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@ foreach($reports as $report)
|
||||
{
|
||||
$taskName = '';
|
||||
foreach(explode(',', $report->tasks) as $taskID) $taskName .= '#' . $taskID . $tasks[$taskID] . ' ';
|
||||
$report->tasks = $taskName;
|
||||
$report->taskName = $taskName;
|
||||
}
|
||||
|
||||
$config->testreport->dtable->fieldList['execution']['map'] = $executions;
|
||||
|
||||
@@ -339,7 +339,7 @@ foreach($bugInfo as $infoKey => $infoValue)
|
||||
|
||||
$mainActions = array();
|
||||
$canBeChanged = common::canBeChanged('testreport', $report);
|
||||
if($canBeChanged && hasPriv('testreport', 'create')) $mainActions[] = array('icon' => 'refresh', 'hint' => $lang->testreport->recreate, 'url' => inlink('create', "objectID={$report->objectID}&objectType={$report->objectType}" . ($report->objectType == 'execution' ? "&extra=$report->tasks" : '')));
|
||||
if($canBeChanged && hasPriv('testreport', 'create')) $mainActions[] = array('icon' => 'refresh', 'hint' => $lang->testreport->recreate, 'url' => inlink('create', "objectID={$report->objectID}&objectType={$report->objectType}" . ($report->objectType == 'execution' || $report->objectType == 'project' ? "&extra=$report->tasks" : '')));
|
||||
if($canBeChanged && hasPriv('testreport', 'edit')) $mainActions[] = array('icon' => 'edit', 'hint' => $lang->testreport->edit, 'url' => inlink('edit', "objectID={$report->id}"));
|
||||
if($canBeChanged && hasPriv('testreport', 'delete')) $mainActions[] = array('icon' => 'trash', 'hint' => $lang->testreport->delete, 'url' => inlink('delete', "objectID={$report->id}"), 'className' => 'ajax-submit', 'data-confirm' => $lang->testreport->confirmDelete, 'url' => inlink('delete', "objectID={$report->id}"));
|
||||
|
||||
|
||||
@@ -92,13 +92,13 @@ class testreportZen extends testreport
|
||||
* Get task pairs for creation.
|
||||
*
|
||||
* @param int $objectID
|
||||
* @param int $extra
|
||||
* @param string $extra
|
||||
* @access protected
|
||||
* @return array
|
||||
*/
|
||||
protected function assignTaskParisForCreate(int $objectID = 0, int $extra = 0): array
|
||||
protected function assignTaskParisForCreate(int $objectID = 0, string $extra = ''): array
|
||||
{
|
||||
if(!$objectID && $extra) $productID = $extra;
|
||||
if(!$objectID && $extra) $productID = (int)$extra;
|
||||
if($objectID)
|
||||
{
|
||||
$task = $this->testtask->getByID($objectID);
|
||||
@@ -181,14 +181,14 @@ class testreportZen extends testreport
|
||||
*
|
||||
* @param int $objectID
|
||||
* @param string $objectType
|
||||
* @param int $extra
|
||||
* @param string $extra
|
||||
* @param string $begin
|
||||
* @param string $end
|
||||
* @param int $executionID
|
||||
* @access protected
|
||||
* @return array
|
||||
*/
|
||||
protected function assignProjectReportDataForCreate(int $objectID, string $objectType, int $extra, string $begin = '', string $end = '', int $executionID = 0): array
|
||||
protected function assignProjectReportDataForCreate(int $objectID, string $objectType, string $extra, string $begin = '', string $end = '', int $executionID = 0): array
|
||||
{
|
||||
$owners = array();
|
||||
$buildIdList = array();
|
||||
@@ -208,7 +208,7 @@ class testreportZen extends testreport
|
||||
if($task->build != 'trunk') $buildIdList[$task->build] = $task->build;
|
||||
}
|
||||
|
||||
$task = $objectID ? $this->testtask->getByID($extra) : key($tasks);
|
||||
$task = $objectID ? $this->testtask->getByID((int)$extra) : key($tasks);
|
||||
$begin = !empty($begin) ? date("Y-m-d", strtotime($begin)) : (string)$task->begin;
|
||||
$end = !empty($end) ? date("Y-m-d", strtotime($end)) : (string)$task->end;
|
||||
$builds = $this->build->getByList($buildIdList);
|
||||
|
||||
+9
-1
@@ -109,7 +109,7 @@ class todoZen extends todo
|
||||
$hasObject = in_array($objectType, $this->config->todo->moduleList);
|
||||
|
||||
$objectID = 0;
|
||||
if($hasObject && $objectType) $objectID = !empty($_POST[$objectType]) ? $_POST[$objectType] : $rawData->objectID;
|
||||
if($hasObject && $objectType) $objectID = zget($form->rawdata, $objectType, $rawData->objectID);
|
||||
$rawData->date = !empty($rawData->config['date']) ? $rawData->config['date'] : $rawData->date;
|
||||
|
||||
return $form->add('account', $this->app->user->account)
|
||||
@@ -255,6 +255,14 @@ class todoZen extends todo
|
||||
}
|
||||
if($todo->type != 'custom') $todo->objectID = (int)$todo->name;
|
||||
|
||||
if($todo->type != 'custom' && !empty($todo->objectID))
|
||||
{
|
||||
$type = $todo->type;
|
||||
$object = $this->loadModel($type)->getByID($todo->objectID);
|
||||
if(isset($object->name)) $todo->name = $object->name;
|
||||
if(isset($object->title)) $todo->name = $object->title;
|
||||
}
|
||||
|
||||
unset($todo->switchTime);
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,8 @@ $config->upgrade->execFlow['18_10_1'] = array('functions' => 'migrateAIModel
|
||||
$config->upgrade->execFlow['20_0_alpha1'] = array('functions' => 'revertStoryCustomFields');
|
||||
$config->upgrade->execFlow['20_0_beta1'] = array('functions' => 'hideOA,updateMetricDateType,update18101,migrateAIModelConfig');
|
||||
$config->upgrade->execFlow['20_0_beta2'] = array('functions' => 'updateWorkflowFieldDefaultValue,update1811,updateZeroDateToNull,updateProgramplanCustom');
|
||||
$config->upgrade->execFlow['20_0'] = array('functions' => 'changeCustomStoryStage,processStoryRelation,processLinkStories,addERName');
|
||||
$config->upgrade->execFlow['20_0_beta2'] = array('functions' => 'updateWorkflowFieldDefaultValue,update1811,updateZeroDateToNull,updateProgramplanCustom,importBuildinModules', 'params' => array('importBuildinModules' => array('or')));
|
||||
$config->upgrade->execFlow['20_1_0'] = array('functions' => 'changeCustomStoryStage,processStoryRelation,processLinkStories,addERName');
|
||||
|
||||
if(!empty($config->isINT))
|
||||
{
|
||||
@@ -155,7 +156,6 @@ $config->upgrade->execFlow['biz5_0_1'] = array('functions' => 'updateWorkflo
|
||||
$config->upgrade->execFlow['biz5_2'] = array('functions' => 'addDefaultKanbanPri');
|
||||
$config->upgrade->execFlow['biz5_3_1'] = array('functions' => 'processFeedbackField,addFileFields,addReportActions');
|
||||
$config->upgrade->execFlow['biz6_4'] = array('functions' => 'importLiteModules');
|
||||
$config->upgrade->execFlow['biz9_0'] = array('functions' => 'importBuildinModules', 'params' => array('vision' => 'or'));
|
||||
|
||||
if(!empty($config->isINT))
|
||||
{
|
||||
|
||||
@@ -194,13 +194,17 @@ class webhook extends control
|
||||
$this->app->loadClass('pager', true);
|
||||
$pager = new pager($recTotal, $recPerPage, $pageID);
|
||||
|
||||
$users = $this->loadModel('user')->getByQuery('inside', '', $pager);
|
||||
$bindedUsers = $this->webhook->getBoundUsers($id);
|
||||
$useridPairs = $this->webhookZen->getUseridPairs($webhook, $users, $bindedUsers, $oauthUsers);
|
||||
|
||||
$this->view->title = $this->lang->webhook->bind;
|
||||
$this->view->webhook = $webhook;
|
||||
$this->view->oauthUsers = $oauthUsers;
|
||||
$this->view->useridPairs = array_flip($oauthUsers);
|
||||
$this->view->users = $this->loadModel('user')->getByQuery('inside', '', $pager);
|
||||
$this->view->useridPairs = $useridPairs;
|
||||
$this->view->users = $users;
|
||||
$this->view->pager = $pager;
|
||||
$this->view->bindedUsers = $this->webhook->getBoundUsers($id);
|
||||
$this->view->bindedUsers = $bindedUsers;
|
||||
$this->display();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,10 @@
|
||||
loadedDept = [];
|
||||
window.loadChildDept = function(event, node)
|
||||
{
|
||||
if(typeof(node.parentKey) == 'undefined') return;
|
||||
|
||||
var tree = $('#deptList').zui('tree');
|
||||
var options = tree.options;
|
||||
var departmentID = node.key;
|
||||
if(loadedDept.includes(departmentID)) return;
|
||||
|
||||
$.ajax(
|
||||
{
|
||||
type: "post",
|
||||
url: feishuUrl,
|
||||
data: {departmentID: departmentID},
|
||||
dataType: "json",
|
||||
async: true,
|
||||
success: function(jsonData)
|
||||
{
|
||||
options.items = buildTreeItems(jsonData, options.items);
|
||||
tree.render(options);
|
||||
}
|
||||
});
|
||||
loadedDept.push(departmentID);
|
||||
};
|
||||
|
||||
window.buildTreeItems = function(deptTree, treeItems)
|
||||
{
|
||||
if(typeof(treeItems) == 'undefined') treeItems = [];
|
||||
for(i in deptTree)
|
||||
{
|
||||
let dept = deptTree[i];
|
||||
let treeItem = {key: dept.id, text: dept.name, onClick: loadChildDept};
|
||||
let treeItem = {key: dept.id, text: dept.name};
|
||||
treeItems = appendItems(treeItems, treeItem, dept.pId);
|
||||
}
|
||||
return treeItems;
|
||||
|
||||
@@ -45,4 +45,46 @@ class webhookZen extends webhook
|
||||
$this->view->selectedDepts = $selectedDepts;
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取open_id键值对,并追加未查询出的open_id。
|
||||
* Get open_id and name pairs, and append no fetch oauth users.
|
||||
*
|
||||
* @param object $webhook
|
||||
* @param array $users
|
||||
* @param array $bindedUsers
|
||||
* @param array $oauthUsers
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
public function getUseridPairs(object $webhook, array $users, array $bindedUsers, array $oauthUsers): array
|
||||
{
|
||||
$useridPairs = array_flip($oauthUsers);
|
||||
$noFetchOauth = array();
|
||||
foreach($users as $user)
|
||||
{
|
||||
if(isset($bindedUsers[$user->account])) $userid = $bindedUsers[$user->account];
|
||||
if(isset($oauthUsers[$user->realname])) $userid = $oauthUsers[$user->realname];
|
||||
if(!isset($userid)) continue;
|
||||
if(!isset($useridPairs[$userid])) $noFetchOauth[$userid] = $userid;
|
||||
}
|
||||
|
||||
if($noFetchOauth)
|
||||
{
|
||||
if($webhook->type == 'dinguser')
|
||||
{
|
||||
$this->app->loadClass('dingapi', true);
|
||||
$dingapi = new dingapi($webhook->secret->appKey, $webhook->secret->appSecret, $webhook->secret->agentId);
|
||||
foreach($dingapi->batchGetUsers($noFetchOauth) as $userid => $name) $useridPairs[$userid] = $name;
|
||||
}
|
||||
elseif($webhook->type == 'feishuuser')
|
||||
{
|
||||
$this->app->loadClass('feishuapi', true);
|
||||
$feishuApi = new feishuapi($webhook->secret->appId, $webhook->secret->appSecret);
|
||||
foreach($feishuApi->batchGetUsers($noFetchOauth) as $openid => $name) $useridPairs[$openid] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
return $useridPairs;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-5
@@ -6,15 +6,18 @@
|
||||
const currentModule = config.currentModule;
|
||||
const currentMethod = config.currentMethod;
|
||||
const isIndexPage = currentModule === 'index' && currentMethod === 'index';
|
||||
const moduleMethod = `${currentModule}-${currentMethod}`;
|
||||
|
||||
const selfOpenList = new Set('index|tutorial|install|upgrade|sso|cron|misc|user-login|user-deny|user-logout|user-reset|user-forgetpassword|user-resetpassword|my-changepassword|my-preference|file-read|file-download|file-uploadimages|report-annualdata|misc-captcha|execution-printkanban|traincourse-playvideo'.split('|'));
|
||||
const isAllowSelfOpen = isIndexPage
|
||||
const selfOpenList = new Set('index|tutorial|install|upgrade|sso|cron|misc|user-login|user-deny|user-logout|user-reset|user-forgetpassword|user-resetpassword|my-changepassword|my-preference|file-read|file-download|file-preview|file-uploadimages|file-ajaxwopifiles|report-annualdata|misc-captcha|execution-printkanban|traincourse-ajaxuploadlargefile|traincourse-playvideo|screen-view|zanode-create|screen-ajaxgetchart|ai-chat'.split('|'));
|
||||
const iframeList = new Set(['cron-index']);
|
||||
const isAllowSelfOpen = !iframeList.has(moduleMethod) &&
|
||||
(isIndexPage
|
||||
|| location.hash === '#_single'
|
||||
|| /(\?|\&)_single/.test(location.search)
|
||||
|| currentMethod.startsWith('ajax')
|
||||
|| selfOpenList.has(`${currentModule}-${currentMethod}`)
|
||||
|| selfOpenList.has(moduleMethod)
|
||||
|| selfOpenList.has(currentModule)
|
||||
|| $('body').hasClass('allow-self-open');
|
||||
|| $('body').hasClass('allow-self-open'));
|
||||
|
||||
if(parent === window && !isAllowSelfOpen)
|
||||
{
|
||||
@@ -475,7 +478,7 @@
|
||||
if(options.modal) headers['X-Zui-Modal'] = 'true';
|
||||
const requestMethod = (options.method || 'GET').toUpperCase();
|
||||
if(!options.cache && options.cache !== false) options.cache = requestMethod === 'GET' ? (url + (url.includes('?') ? '&zin=' : '?zin=') + encodeURIComponent(selectors.join(','))) : false;
|
||||
if(!window.config || !window.config.clientCache) options.cache = false;
|
||||
options.cache = false; // Disable local cache for 20.1.
|
||||
const cacheKey = options.cache;
|
||||
let cache;
|
||||
const renderPageData = (data, onlyZinDebug) =>
|
||||
@@ -1626,6 +1629,8 @@
|
||||
|
||||
$(() =>
|
||||
{
|
||||
if($.apps.theme) changeAppTheme($.apps.theme);
|
||||
|
||||
if(isIndexPage) return;
|
||||
|
||||
initZinbar();
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user