Merge branch 'release20.1' into story

This commit is contained in:
sunguangming
2024-05-30 09:12:45 +08:00
54 changed files with 590 additions and 144 deletions
@@ -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, ''))
)
)
)
+89
View File
@@ -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.
*
+124 -16
View File
@@ -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.
*
+1 -2
View File
@@ -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();
+2 -2
View File
@@ -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);
+1
View File
@@ -97,6 +97,7 @@ class productRoadmapBox extends wg
set::name("branch[$index]"),
set::items($branches),
set::value($defaultBranch),
set::emptyValue('')
)
)
)
+4
View File
@@ -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);
+1 -1
View File
@@ -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')
+4 -4
View File
@@ -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 跳转实现。
+1
View File
@@ -8,3 +8,4 @@
.actions-menu {bottom: 12px;}
#mainContent {margin-bottom: 60px;}
#mainContent .desc-box {max-height: 6rem;}
+2 -1
View File
@@ -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)
)
)
+7
View File
@@ -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()
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
-5
View File
@@ -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;}
+61
View File
@@ -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('');
}
});
}
+56
View File
@@ -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();
+1 -1
View File
@@ -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);
+2
View File
@@ -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;}
+2 -1
View File
@@ -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)
)
)
+1 -3
View File
@@ -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);
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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);
+2
View File
@@ -13,3 +13,5 @@
.member-list > .center-y {width: 12.5%}
.maxh-80{max-height: 20rem;}
#mainContent .desc-box {max-height: 6rem;}
+1 -1
View File
@@ -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)
{
+6 -1
View File
@@ -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
+1 -1
View File
@@ -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 -5
View File
@@ -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;}
+1 -1
View File
@@ -280,7 +280,7 @@ window.afterPageUpdate = function()
title: copied,
tipClass: 'success',
});
}, 200);
}, 1);
};
$(function()
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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
(
-7
View File
@@ -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;
+4 -5
View File
@@ -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';
+2 -2
View File
@@ -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)
+4 -4
View File
@@ -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/>");
}
+1 -1
View File
@@ -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/>");
}
+4 -4
View File
@@ -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/>");
}
+1 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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. */
+2 -2
View File
@@ -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';
+2 -2
View File
@@ -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)
{
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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}"));
+6 -6
View File
@@ -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
View File
@@ -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);
}
+2 -2
View File
@@ -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))
{
+7 -3
View File
@@ -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 -27
View File
@@ -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;
+42
View File
@@ -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
View File
@@ -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