story.html.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. <?php
  2. /**
  3. * The story view file of execution module of ZenTaoPMS.
  4. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
  5. * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  6. * @author dingguodong <dingguodong@easycorp.ltd>
  7. * @package execution
  8. * @link https://www.zentao.net
  9. */
  10. namespace zin;
  11. include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php');
  12. data('activeMenuID', $storyType);
  13. jsVar('orderBy', $orderBy);
  14. jsVar('storyPageID', $pager->pageID);
  15. jsVar('storyRecPerPage', $pager->recPerPage);
  16. jsVar('executionID', $execution->id);
  17. jsVar('childrenAB', $lang->story->childrenAB);
  18. jsVar('modulePairs', $modulePairs);
  19. jsVar('oldShowGrades', $showGrades);
  20. jsVar('gradeGroup', $gradeGroup);
  21. jsVar('hasProduct', $execution->hasProduct);
  22. jsVar('linkedTaskStories', $linkedTaskStories);
  23. jsVar('URChanged', $lang->story->URChanged);
  24. jsVar('confirmStoryToTask', $lang->execution->confirmStoryToTask);
  25. jsVar('typeNotEmpty', sprintf($lang->error->notempty, $lang->task->type));
  26. jsVar('hourPointNotEmpty', sprintf($lang->error->notempty, $lang->story->convertRelations));
  27. jsVar('hourPointNotError', sprintf($lang->story->float, $lang->story->convertRelations));
  28. $isFromDoc = $from === 'doc';
  29. $isFromAI = $from === 'ai';
  30. if($isFromDoc || $isFromAI)
  31. {
  32. $this->app->loadLang('doc');
  33. $products = $this->loadModel('product')->getPairs();
  34. $executionChangeLink = createLink('execution', 'story', "executionID={executionID}&storyType=$storyType&orderBy=$orderBy&type=$type&param=$param&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}&from=$from&blockID=$blockID");
  35. $insertListLink = createLink('execution', 'story', "executionID=$executionID&storyType=$storyType&orderBy=$orderBy&type=$type&param=$param&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}&from=$from&blockID={blockID}");
  36. formPanel
  37. (
  38. setID('zentaolist'),
  39. setClass('mb-4-important'),
  40. set::title(sprintf($this->lang->doc->insertTitle, $this->lang->doc->zentaoList['executionStory'])),
  41. set::actions(array()),
  42. to::titleSuffix
  43. (
  44. span
  45. (
  46. setClass('text-muted text-sm text-gray-600 font-light'),
  47. span(setClass('text-warning mr-1'), icon('help')),
  48. $lang->doc->previewTip
  49. )
  50. ),
  51. formRow
  52. (
  53. formGroup
  54. (
  55. set::width('1/2'),
  56. set::name('execution'),
  57. set::label($lang->doc->execution),
  58. set::control(array('required' => false)),
  59. set::items($executions),
  60. set::value($execution->id),
  61. set::required(),
  62. span
  63. (
  64. setClass('error-tip text-danger hidden'),
  65. $lang->doc->emptyError
  66. ),
  67. on::change('[name="execution"]')->do("loadModal('$executionChangeLink'.replace('{executionID}', $(this).val()))")
  68. )
  69. )
  70. );
  71. }
  72. /* Show feature bar. */
  73. $queryMenuLink = createLink($app->rawModule, $app->rawMethod, "&executionID=$execution->id&storyType=$storyType&orderBy=$orderBy&type=bySearch&param={queryID}&recTotal={$pager->recTotal}&recPerPae={$pager->recPerPage}&pageID={$pager->pageID}&from=$from&blockID=$blockID");
  74. if(empty($param) && $this->cookie->storyModuleParam) $param = $this->cookie->storyModuleParam;
  75. featureBar
  76. (
  77. ($isFromDoc || $isFromAI) ? null : to::leading
  78. (
  79. picker
  80. (
  81. set::boxClass('whitespace-nowrap'),
  82. set::tree(),
  83. set::name('showGrades'),
  84. set::items($gradeMenu),
  85. set::search(false),
  86. set::multiple(true),
  87. set::width('150px'),
  88. setStyle('justify-content', 'center'),
  89. set::display($lang->story->viewAllGrades),
  90. set::menu(array('checkbox' => true, 'itemProps' => array('innerComponent' => 'a'))),
  91. set::popMinWidth(150),
  92. set::value($showGrades),
  93. set::toolbar(array('text' => $lang->confirm, 'onClick' => jsRaw('(e,info) => {setShowGrades();info.relativeTarget.close();}')), array('text' => $lang->cancel, 'onClick' => jsRaw('(e,info) => info.relativeTarget.close()')))
  94. )
  95. ),
  96. set::param($param),
  97. set::searchModule('executionStory'),
  98. set::current($this->session->storyBrowseType),
  99. set::link(createLink($app->rawModule, $app->rawMethod, "&executionID=$execution->id&storyType=$storyType&orderBy=$orderBy&type={key}&param=$param&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}&from={$from}&blockID={$blockID}")),
  100. set::queryMenuLinkCallback(array(function ($key) use ($queryMenuLink) {
  101. return str_replace('{queryID}', (string)$key, $queryMenuLink);
  102. })),
  103. set::isModal($isFromDoc || $isFromAI),
  104. set::modalTarget('#stories_table'),
  105. li(searchToggle
  106. (
  107. set::simple($isFromDoc || $isFromAI),
  108. set::module('executionStory'),
  109. set::open($type == 'bysearch'),
  110. ($isFromDoc || $isFromAI) ? set::target('#docSearchForm') : null,
  111. ($isFromDoc || $isFromAI) ? set::onSearch(jsRaw('function(){$(this.element).closest(".modal").find("#featureBar .nav-item>.active").removeClass("active").find(".label").hide()}')) : null
  112. ))
  113. );
  114. if($isFromDoc || $isFromAI) div(setID('docSearchForm'));
  115. $linkStoryByPlanTips = $multiBranch ? sprintf($lang->execution->linkBranchStoryByPlanTips, $lang->project->branch) : $lang->execution->linkNormalStoryByPlanTips;
  116. $linkStoryByPlanTips = $execution->multiple ? $linkStoryByPlanTips : str_replace($lang->execution->common, $lang->projectCommon, $linkStoryByPlanTips);
  117. modal
  118. (
  119. setID('linkStoryByPlan'),
  120. set::modalProps(array('title' => $lang->execution->linkStoryByPlan)),
  121. div
  122. (
  123. setClass('flex-auto'),
  124. icon('info-sign', setClass('warning-pale rounded-full mr-1')),
  125. $linkStoryByPlanTips
  126. ),
  127. form
  128. (
  129. setClass('text-center', 'py-4'),
  130. set::actions(array('submit')),
  131. set::submitBtnText($lang->execution->linkStory),
  132. formGroup
  133. (
  134. set::label($lang->execution->selectStoryPlan),
  135. set::required(true),
  136. setClass('text-left'),
  137. picker
  138. (
  139. set::name('plan'),
  140. set::required(true),
  141. set::items($allPlans)
  142. )
  143. )
  144. )
  145. );
  146. /* Show tool bar. */
  147. if(!$product)
  148. {
  149. $product = new stdclass();
  150. $product->id = 0;
  151. }
  152. $canModifyProduct = common::canModify('product', $product);
  153. $canModifyExecution = common::canModify('execution', $execution);
  154. $canOpreate['create'] = $canModifyProduct && $canModifyExecution && hasPriv('story', 'create');
  155. $canOpreate['batchCreate'] = $canModifyProduct && $canModifyExecution && hasPriv('story', 'batchCreate');
  156. $canOpreate['createEpic'] = $canModifyProduct && $canModifyExecution && hasPriv('epic', 'create') && strpos($project->storyType, 'epic') !== false && $this->config->enableER;
  157. $canOpreate['batchCreateEpic'] = $canModifyProduct && $canModifyExecution && hasPriv('epic', 'batchCreate') && strpos($project->storyType, 'epic') !== false && $this->config->enableER;
  158. $canOpreate['createRequirement'] = $canModifyProduct && $canModifyExecution && hasPriv('requirement', 'create') && strpos($project->storyType, 'requirement') !== false && $this->config->URAndSR;
  159. $canOpreate['batchCreateRequirement'] = $canModifyProduct && $canModifyExecution && hasPriv('requirement', 'batchCreate') && strpos($project->storyType, 'requirement') !== false && $this->config->URAndSR;
  160. $createLink = createLink('story', 'create', "product={$product->id}&branch=0&moduleID=0&storyID=0&objectID={$execution->id}&bugID=0&planID=0&todoID=0&extra=&storyType={$storyType}") . "#app={$app->tab}";
  161. $batchCreateLink = createLink('story', 'batchCreate', "productID={$product->id}&branch=0&moduleID=0&storyID=0&executionID={$execution->id}&plan=0&storyType={$storyType}") . "#app={$app->tab}";
  162. $createEpicLink = createLink('epic', 'create', "product={$product->id}&branch=0&moduleID=0&storyID=0&objectID={$execution->id}") . "#app={$app->tab}";
  163. $batchCreateEpicLink = createLink('epic', 'batchCreate', "productID={$product->id}&branch=0&moduleID=0&storyID=0&executionID={$execution->id}") . "#app={$app->tab}";
  164. $createRequirementLink = createLink('requirement', 'create', "product={$product->id}&branch=0&moduleID=0&storyID=0&objectID={$execution->id}") . "#app={$app->tab}";
  165. $batchCreateRequirementLink = createLink('requirement', 'batchCreate', "productID={$product->id}&branch=0&moduleID=0&storyID=0&executionID={$execution->id}") . "#app={$app->tab}";
  166. /* Tutorial create link. */
  167. if(commonModel::isTutorialMode())
  168. {
  169. $wizardParams = helper::safe64Encode("productID={$product->id}&branch=0&moduleID=0");
  170. $createLink = $this->createLink('tutorial', 'wizard', "module=story&method=create&params={$wizardParams}");
  171. $canBatchCreate = false;
  172. }
  173. $createItems = array();
  174. $batchItems = array();
  175. if($canOpreate['batchCreate']) $batchItems[] = array('text' => $lang->SRCommon, 'url' => $batchCreateLink);
  176. if(in_array($execution->attribute, array('mix', 'request', 'design')) || !$execution->multiple)
  177. {
  178. if($canOpreate['createRequirement']) $createItems[] = array('text' => $lang->requirement->create, 'url' => $createRequirementLink, 'hint' => $hasFrozenStories ? sprintf($lang->story->frozenTip, $lang->requirement->create) : '');
  179. if($canOpreate['createEpic']) $createItems[] = array('text' => $lang->epic->create, 'url' => $createEpicLink, 'hint' => $hasFrozenStories ? sprintf($lang->story->frozenTip, $lang->epic->create) : '');
  180. if($canOpreate['batchCreateRequirement']) $batchItems[] = array('text' => $lang->URCommon, 'url' => $batchCreateRequirementLink);
  181. if($canOpreate['batchCreateEpic']) $batchItems[] = array('text' => $lang->ERCommon, 'url' => $batchCreateEpicLink);
  182. }
  183. if(!empty($product->id))
  184. {
  185. if(count($batchItems) > 1)
  186. {
  187. $createItems[] = array('text' => $lang->story->batchCreate, 'items' => $batchItems, 'hint' => $hasFrozenStories ? sprintf($lang->story->frozenTip, $lang->story->batchCreate) : '');
  188. }
  189. elseif($batchItems)
  190. {
  191. $batchItems[0]['text'] = $lang->story->batchCreate;
  192. $createItems = array_merge($createItems, $batchItems);
  193. }
  194. }
  195. $canLinkStory = ($execution->hasProduct || $app->tab == 'execution') && $canModifyProduct && $canModifyExecution && hasPriv('execution', 'linkStory');
  196. $canlinkPlanStory = ($execution->hasProduct || $app->tab == 'execution') && $canModifyProduct && $canModifyExecution && hasPriv('execution', 'importPlanStories') && $storyType == 'story';
  197. $linkStoryUrl = createLink('execution', 'linkStory', "project={$execution->id}&browseType=&param=0&orderBy=id_desc&recPerPage=50&pageID=1&extra=&storyType=$storyType");
  198. if(commonModel::isTutorialMode())
  199. {
  200. $wizardParams = helper::safe64Encode("project={$execution->id}");
  201. $linkStoryUrl = createLink('tutorial', 'wizard', "module=execution&method=linkStory&params=$wizardParams");
  202. $canlinkPlanStory = false;
  203. }
  204. $linkItem = array('text' => $lang->story->linkStory, 'url' => $linkStoryUrl, 'data-app' => $app->tab, 'hint' => $hasFrozenStories ? sprintf($lang->story->frozenTip, $lang->story->linkStory) : '');
  205. $linkPlanItem = array('text' => $lang->execution->linkStoryByPlan, 'url' => '#linkStoryByPlan', 'data-toggle' => 'modal', 'data-size' => 'sm', 'hint' => $hasFrozenStories ? sprintf($lang->story->frozenTip, $lang->execution->linkStoryByPlan) : '');
  206. $createBtnGroup = null;
  207. if(!$isFromDoc && !$isFromAI)
  208. {
  209. if($canOpreate['create'])
  210. {
  211. $createBtnGroup = btngroup
  212. (
  213. btn
  214. (
  215. setClass('btn secondary'),
  216. set::icon('plus'),
  217. set::url($createLink),
  218. set::disabled(!empty($hasFrozenStories)),
  219. $hasFrozenStories ? set::hint(sprintf($lang->story->frozenTip, $lang->story->create)) : null,
  220. $lang->story->create
  221. ),
  222. empty($createItems) || $hasFrozenStories ? null : dropdown
  223. (
  224. btn(setClass('btn secondary dropdown-toggle'),
  225. setStyle(array('padding' => '6px', 'border-radius' => '0 2px 2px 0'))),
  226. set::items($createItems),
  227. set::placement('bottom-end')
  228. )
  229. );
  230. }
  231. elseif(count($createItems) == 1)
  232. {
  233. $createBtnGroup = item(set($createItems[0] + array('class' => 'btn secondary' . ($hasFrozenStories ? ' disabled' : ''), 'icon' => 'plus')));
  234. }
  235. }
  236. $reportText = $config->edition != 'open' ? 'hint' : 'text';
  237. if($product && !$isFromDoc && !$isFromAI) toolbar
  238. (
  239. common::hasPriv('execution', 'storykanban') && $storyType == 'story' ? btnGroup(btn
  240. (
  241. setClass('text-primary font-bold shadow-inner bg-canvas'),
  242. set::icon('format-list-bulleted'),
  243. set::hint($lang->execution->list),
  244. set::url(inlink('story', "executionID={$execution->id}&storyType={$storyType}&orderBy={$orderBy}&type=all")),
  245. setData('app', $app->tab)
  246. ), btn
  247. (
  248. set::icon('kanban'),
  249. set::hint($lang->execution->kanban),
  250. set::url($this->createLink('execution', 'storykanban', "executionID={$execution->id}")),
  251. setData('app', $app->tab)
  252. )) : null,
  253. hasPriv('story', 'report') ? item(set(array
  254. (
  255. $reportText => $lang->story->report->common,
  256. 'icon' => 'bar-chart',
  257. 'class' => 'ghost',
  258. 'url' => createLink('story', 'report', "productID={$product->id}&branchID=&storyType={$storyType}&browseType={$type}&moduleID={$param}&chartType=pie&projectID={$execution->id}") . "#app={$app->tab}"
  259. ))) : null,
  260. hasPriv('story', 'export') && ($linkedProductCount < 2 || $type == 'byproduct' || $type == 'bymodule') ? item(set(array
  261. (
  262. 'text' => $lang->export,
  263. 'icon' => 'export',
  264. 'class' => 'ghost',
  265. 'url' => createLink('story', 'export', "productID={$product->id}&orderBy=$orderBy&executionID=$execution->id&browseType=$type&storyType=$storyType"),
  266. 'data-toggle' => 'modal'
  267. ))) : null,
  268. $createBtnGroup,
  269. $canLinkStory && $canlinkPlanStory ? btngroup
  270. (
  271. btn(
  272. setClass('btn primary'),
  273. set::icon('link'),
  274. set::url($linkStoryUrl),
  275. set::disabled(!empty($hasFrozenStories)),
  276. $hasFrozenStories ? set::hint(sprintf($lang->story->frozenTip, $lang->story->linkStory)) : null,
  277. setData('app', $app->tab),
  278. $lang->story->linkStory
  279. ),
  280. $hasFrozenStories ? null : dropdown
  281. (
  282. btn(setClass('btn primary dropdown-toggle'),
  283. setStyle(array('padding' => '6px', 'border-radius' => '0 2px 2px 0'))),
  284. set::items(array_filter(array($linkItem, $linkPlanItem))),
  285. set::placement('bottom-end')
  286. )
  287. ) : null,
  288. $canLinkStory && !$canlinkPlanStory ? item(set($linkItem + array('class' => 'btn primary link-story-btn' . ($hasFrozenStories ? ' disabled' : ''), 'icon' => 'link'))) : null,
  289. $canlinkPlanStory && !$canLinkStory ? item(set($linkPlanItem + array('class' => 'btn primary' . ($hasFrozenStories ? ' disabled' : ''), 'icon' => 'link'))) : null
  290. );
  291. if(!$isFromDoc && !$isFromAI) sidebar
  292. (
  293. moduleMenu(set(array(
  294. 'modules' => $moduleTree,
  295. 'activeKey' => $type == 'byproduct' ? "p_$param" : $param,
  296. 'settingLink' => !$execution->hasProduct && !$execution->multiple ? createLink('tree', 'browse', "rootID={$product->id}&viewType=story") : null,
  297. 'closeLink' => $this->createLink('execution', 'story', "executionID={$execution->id}&storyType={$storyType}&orderBy={$orderBy}&type=byModule&param=0"),
  298. 'app' => !$execution->multiple ? 'project' : '',
  299. 'settingApp' => !$execution->multiple ? 'project' : ''
  300. )))
  301. );
  302. modal
  303. (
  304. setID('taskModal'),
  305. set::modalProps(array('title' => $lang->story->batchToTask, 'titleClass' => 'flex-initial')),
  306. to::header
  307. (
  308. div
  309. (
  310. setClass('flex-auto'),
  311. icon('info-sign', setClass('warning-pale rounded-full mr-1')),
  312. $lang->story->batchToTaskTips
  313. )
  314. ),
  315. form
  316. (
  317. setClass('text-center', 'py-4'),
  318. setID('toTaskForm'),
  319. set::actions(array()),
  320. set::url(createLink('story', 'batchToTask', "executionID={$execution->id}&projectID={$execution->project}")),
  321. formGroup
  322. (
  323. setClass('text-left'),
  324. set::label($lang->task->type),
  325. set::required(true),
  326. set::width('1/2'),
  327. picker
  328. (
  329. set::required(true),
  330. set::name('type'),
  331. set::items($lang->task->typeList)
  332. )
  333. ),
  334. $lang->hourCommon !== $lang->workingHour ? formGroup
  335. (
  336. set::label($lang->story->one . $lang->hourCommon),
  337. set::required(true),
  338. set::width('1/2'),
  339. inputGroup
  340. (
  341. span
  342. (
  343. setClass('input-group-addon'),
  344. "≈ "
  345. ),
  346. input(set::name('hourPointValue')),
  347. span
  348. (
  349. setClass('input-group-addon'),
  350. $lang->workingHour
  351. )
  352. )
  353. ) : null,
  354. formGroup
  355. (
  356. set::label($lang->story->field),
  357. checkList
  358. (
  359. set::name('fields[]'),
  360. set::inline(true),
  361. set::value(array_keys($lang->story->convertToTask->fieldList)),
  362. set::items($lang->story->convertToTask->fieldList)
  363. ),
  364. input
  365. (
  366. set::type('hidden'),
  367. set::name('storyIdList')
  368. )
  369. ),
  370. formGroup
  371. (
  372. setClass('justify-center'),
  373. btn
  374. (
  375. set::text($lang->execution->next),
  376. set::btnType('submit'),
  377. set::type('primary')
  378. )
  379. )
  380. )
  381. );
  382. $checkObject = new stdclass();
  383. $checkObject->execution = $execution->id;
  384. $canBatchEdit = common::hasPriv('story', 'batchEdit');
  385. $canBatchClose = common::hasPriv('story', 'batchClose') && $storyType != 'requirement';
  386. $canBatchChangeStage = common::hasPriv('story', 'batchChangeStage') && $storyType != 'requirement';
  387. $canBatchUnlink = empty($hasFrozenStories) && ($execution->hasProduct || $app->tab == 'execution') && common::hasPriv('execution', 'batchUnlinkStory');
  388. $canBatchToTask = common::hasPriv('story', 'batchToTask', $checkObject) && $storyType != 'requirement';
  389. $canBatchAssignTo = common::hasPriv($storyType, 'batchAssignTo');
  390. $canBatchAction = $canBeChanged && in_array(true, array($canBatchEdit, $canBatchClose, $canBatchChangeStage, $canBatchUnlink, $canBatchToTask, $canBatchAssignTo));
  391. $footToolbar = array();
  392. if($canBatchAction && !$isFromDoc && !$isFromAI)
  393. {
  394. if($canBatchToTask)
  395. {
  396. menu(setID('batchToTask'), setClass('dropdown-menu'), $canBatchToTask ? item(set(array(
  397. 'text' => $lang->story->batchToTask,
  398. 'url' => '#taskModal',
  399. 'data-toggle' => 'modal'
  400. ))) : null);
  401. }
  402. if($canBatchToTask || $canBatchEdit)
  403. {
  404. $items = array(array('text' => $lang->edit, 'className' => "btn secondary size-sm batch-btn", 'disabled' => !$canBatchEdit, 'btnType' => 'primary', 'data-url' => $canBatchEdit ? createLink('story', 'batchEdit', "productID=0&executionID={$execution->id}&branch=0&storyType={$storyType}") : null));
  405. if($canBatchToTask) $items[] = array('caret' => 'up', 'className' => 'btn btn-caret size-sm secondary', 'url' => '#batchToTask', 'data-toggle' => 'dropdown', 'data-placement' => 'top-start');
  406. $footToolbar['items'][] = array(
  407. 'type' => 'btn-group',
  408. 'items' => $items
  409. );
  410. }
  411. if($canBatchAssignTo)
  412. {
  413. $pinyinItems = common::convert2Pinyin($users);
  414. $assignedToItems = array();
  415. foreach($users as $account => $name)
  416. {
  417. if($account == 'closed' || !$name) continue;
  418. $assignedToItems[] = array(
  419. 'text' => $name,
  420. 'keys' => zget($pinyinItems, $name, ''),
  421. 'innerClass' => 'batch-btn ajax-btn',
  422. 'data-url' => createLink('story', 'batchAssignTo', "toryType={$storyType}&assignedTo={$account}")
  423. );
  424. }
  425. }
  426. if($canBatchAssignTo)
  427. {
  428. $footToolbar['items'][] = array(
  429. 'caret' => 'up',
  430. 'text' => $lang->story->assignedTo,
  431. 'className' => 'btn btn-caret size-sm secondary',
  432. 'type' => 'dropdown',
  433. 'items' => $assignedToItems,
  434. 'menu' => array('searchBox' => true)
  435. );
  436. }
  437. if($canBatchClose)
  438. {
  439. $footToolbar['items'][] = array(
  440. 'text' => $lang->close,
  441. 'className' => 'btn batch-btn size-sm secondary',
  442. 'data-url' => $this->createLink('story', 'batchClose', "productID=0&executionID={$execution->id}")
  443. );
  444. }
  445. if($canBatchChangeStage)
  446. {
  447. $stageItems = array();
  448. foreach($lang->story->stageList as $stageID => $stage)
  449. {
  450. if($stageID == 'delivered' || $stageID == 'delivering') continue;
  451. $stageItems[] = array(
  452. 'text' => $stage,
  453. 'innerClass' => 'batch-btn ajax-btn',
  454. 'data-url' => createLink('story', 'batchChangeStage', "stageID=$stageID")
  455. );
  456. }
  457. }
  458. if($canBatchChangeStage)
  459. {
  460. $footToolbar['items'][] = array(
  461. 'caret' => 'up',
  462. 'text' => $lang->story->stageAB,
  463. 'className' => 'btn btn-caret size-sm secondary',
  464. 'type' => 'dropdown',
  465. 'items' => $stageItems,
  466. 'data-placement' => 'top-start'
  467. );
  468. }
  469. if($canBatchUnlink)
  470. {
  471. $footToolbar['items'][] = array(
  472. 'text' => $lang->execution->unlinkStoryAB,
  473. 'className' => 'btn batch-btn ajax-btn size-sm secondary',
  474. 'data-url' => $this->createLink('execution', 'batchUnlinkStory', "executionID={$execution->id}")
  475. );
  476. }
  477. }
  478. /* DataTable columns. */
  479. $config->story->dtable->fieldList['title']['title'] = $lang->story->title;
  480. $cols = array();
  481. $setting = $this->loadModel('datatable')->getSetting('execution', 'story', false, $storyType);
  482. if(!$canModifyExecution) $setting['actions']['actionsMap'] = array();
  483. if(($isFromDoc || $isFromAI) && isset($setting['actions'])) unset($setting['actions']);
  484. if($storyType == 'requirement') unset($setting['plan'], $setting['stage'], $setting['taskCount'], $setting['bugCount'], $setting['caseCount']);
  485. foreach($setting as $key => $col)
  486. {
  487. if(!$execution->hasProduct && $col['name'] == 'branch') continue;
  488. if(!$execution->hasProduct && !$execution->multiple && $col['name'] == 'plan') continue;
  489. if($col['name'] == 'title')
  490. {
  491. $tab = $execution->multiple ? 'execution' : 'project';
  492. $col['link'] = createLink('execution', 'storyView', array('storyID' => '{id}', 'execution' => $execution->id)) . "#app={$tab}";
  493. $col['title'] = $this->lang->story->name;
  494. }
  495. if($isFromDoc || $isFromAI)
  496. {
  497. $col['sortType'] = false;
  498. if(isset($col['link'])) unset($col['link']);
  499. if($key == 'assignedTo') $col['type'] = 'user';
  500. if($key == 'title') $col['link'] = array('url' => createLink('{type}', 'view', "storyID={id}&version={version}"), 'data-toggle' => 'modal', 'data-size' => 'lg');
  501. }
  502. $cols[] = $col;
  503. }
  504. /* DataTable data. */
  505. $data = array();
  506. $actionMenus = array('submitreview', 'recall', 'recalledchange', 'review', 'dropdown', 'createTask', 'batchCreateTask', 'divider', 'storyEstimate', 'testcase', 'batchCreate', 'unlink', 'processStoryChange');
  507. if(empty($execution->hasProduct) && empty($execution->multiple))
  508. {
  509. $actionMenus = array('submitreview', 'recall', 'recalledchange', 'review', 'dropdown', 'createTask', 'batchCreateTask', 'edit', 'divider', 'storyEstimate', 'testcase', 'batchCreate', 'close', 'processStoryChange');
  510. if($storyType == 'requirement') $actionMenus = array('change', 'submitreview', 'recall', 'recalledchange', 'review', 'dropdown', 'edit', 'divider', 'batchCreate', 'close');
  511. }
  512. if(!$canModifyExecution) $actionMenus = array();
  513. if($config->edition == 'ipd')
  514. {
  515. $actionMenus[] = 'confirmDemandRetract';
  516. $actionMenus[] = 'confirmDemandUnlink';
  517. }
  518. $options = array('storyTasks' => $storyTasks, 'storyBugs' => $storyBugs, 'storyCases' => $storyCases, 'modules' => $modules ?? array(), 'plans' => (isset($plans) ? $plans : array()), 'users' => $users, 'execution' => $execution, 'actionMenus' => $actionMenus, 'branches' => $branchPairs);
  519. foreach($stories as $story)
  520. {
  521. $story->moduleID = $story->module;
  522. $story->from = 'execution';
  523. $data[] = $this->story->formatStoryForList($story, $options, $storyType, $maxGradeGroup);
  524. if(!isset($story->children)) continue;
  525. }
  526. if($isFromDoc) $footToolbar = array(array('text' => $lang->doc->insertText, 'data-on' => 'click', 'data-call' => "insertListToDoc('#table-execution-story', 'executionStory', $blockID, '$insertListLink')"));
  527. if($isFromAI) $footToolbar = array(array('text' => $lang->doc->insertText, 'data-on' => 'click', 'data-call' => "insertListToAI('#table-execution-story', 'story')"));
  528. $createStoryLink = $canOpreate['create'] ? $createLink : '';
  529. jsVar('cases', $storyCases);
  530. jsVar('summary', $summary);
  531. jsVar('checkedSummary', $lang->product->checkedAllSummary);
  532. jsVar('storyType', $storyType);
  533. dtable
  534. (
  535. setClass('shadow rounded'),
  536. set::userMap($users),
  537. set::groupDivider(true),
  538. set::cols($cols),
  539. set::data($data),
  540. set::plugins(array('sortable')),
  541. set::sortHandler('.move-plan'),
  542. set::sortable($orderBy == 'order_desc'),
  543. set::onSortEnd($orderBy == 'order_desc' ? jsRaw('window.onSortEnd') : null),
  544. set::noNestedCheck(),
  545. set::footToolbar($footToolbar),
  546. set::onRenderCell(jsRaw('window.renderStoryCell')),
  547. set::orderBy($orderBy),
  548. set::footPager(usePager(array
  549. (
  550. 'recPerPage' => $pager->recPerPage,
  551. 'recTotal' => $pager->recTotal,
  552. 'linkCreator' => helper::createLink('execution', 'story', "executionID={$execution->id}&storyType={$storyType}&orderBy=$orderBy&type={$type}&param={$param}&recTotal={recTotal}&recPerPage={recPerPage}&page={page}&from={$from}&blockID={$blockID}") . "#app={$app->tab}"
  553. ))),
  554. set::emptyTip($lang->execution->noStory),
  555. !$isFromDoc ? null : set::afterRender(jsCallback()->call('toggleCheckRows', $idList)),
  556. (!$isFromDoc && !$isFromAI) ? null : set::onCheckChange(jsRaw('window.checkedChange')),
  557. (!$isFromDoc && !$isFromAI) ? null : set::height(400),
  558. ($isFromDoc || $isFromAI) ? null : set::customCols(array('url' => createLink('datatable', 'ajaxcustom', "module={$app->moduleName}&method={$app->methodName}&extra={$storyType}"), 'globalUrl' => createLink('datatable', 'ajaxsaveglobal', "module={$app->moduleName}&method={$app->methodName}&extra={$storyType}"))),
  559. ($isFromDoc || $isFromAI) ? null : set::sortLink(createLink('execution', 'story', "executionID={$execution->id}&storyType={$storyType}&orderBy={name}_{sortType}&type={$type}&param={$param}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&page={$pager->pageID}")),
  560. ($isFromDoc || $isFromAI) ? null : set::checkInfo(jsRaw('function(checkedIDList){return window.setStatistics(this, checkedIDList);}')),
  561. ($isFromDoc || $isFromAI) ? null : set::createTip($lang->story->create),
  562. ($isFromDoc || $isFromAI || $hasFrozenStories) ? null : set::createLink($createStoryLink)
  563. );
  564. render();