@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at recaptime-dev/main 1089 lines 32 kB view raw
1<?php 2 3final class PhabricatorProjectBoardViewController 4 extends PhabricatorProjectBoardController { 5 6 public function shouldAllowPublic() { 7 return true; 8 } 9 10 /** 11 * @return AphrontResponse|PhabricatorStandardPageView 12 */ 13 public function handleRequest(AphrontRequest $request) { 14 $viewer = $request->getUser(); 15 16 $response = $this->loadProject(); 17 if ($response) { 18 return $response; 19 } 20 21 $project = $this->getProject(); 22 $state = $this->getViewState(); 23 $board_uri = $project->getWorkboardURI(); 24 25 $search_engine = $state->getSearchEngine(); 26 $query_key = $state->getQueryKey(); 27 $saved = $state->getSavedQuery(); 28 if (!$saved) { 29 return new Aphront404Response(); 30 } 31 32 if ($saved->getID()) { 33 $custom_query = $saved; 34 } else { 35 $custom_query = null; 36 } 37 38 $columns = array(); 39 $board_phid = $project->getPHID(); 40 41 if ($project->getHasWorkboard()) { 42 $layout_engine = $state->getLayoutEngine(); 43 $columns = $layout_engine->getColumns($board_phid); 44 } else { 45 // Avoid setting up from scratch if board existed but got disabled 46 $columns = id(new PhabricatorProjectColumnQuery()) 47 ->setViewer($viewer) 48 ->withProjectPHIDs(array($project->getPHID())) 49 ->withIsProxyColumn(false) 50 ->execute(); 51 } 52 53 if (!$columns || !$project->getHasWorkboard()) { 54 $has_normal_columns = false; 55 56 foreach ($columns as $column) { 57 if (!$column->getProxyPHID()) { 58 $has_normal_columns = true; 59 break; 60 } 61 } 62 63 $can_edit = PhabricatorPolicyFilter::hasCapability( 64 $viewer, 65 $project, 66 PhabricatorPolicyCapability::CAN_EDIT); 67 68 if (!$has_normal_columns) { 69 if (!$can_edit) { 70 $content = $this->buildNoAccessContent($project); 71 } else { 72 $content = $this->buildInitializeContent($project); 73 } 74 } else { 75 if (!$can_edit) { 76 $content = $this->buildDisabledContent($project); 77 } else { 78 $content = $this->buildEnableContent($project); 79 } 80 } 81 82 if ($content instanceof AphrontResponse) { 83 return $content; 84 } 85 86 $nav = $this->newNavigation( 87 $project, 88 PhabricatorProject::ITEM_WORKBOARD); 89 90 $crumbs = $this->buildApplicationCrumbs(); 91 $crumbs->addTextCrumb(pht('Workboard')); 92 93 return $this->newPage() 94 ->setTitle( 95 array( 96 $project->getDisplayName(), 97 pht('Workboard'), 98 )) 99 ->setNavigation($nav) 100 ->setCrumbs($crumbs) 101 ->appendChild($content); 102 } 103 104 $tasks = $state->getObjects(); 105 106 $task_can_edit_map = id(new PhabricatorPolicyFilter()) 107 ->setViewer($viewer) 108 ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) 109 ->apply($tasks); 110 111 $board_id = celerity_generate_unique_node_id(); 112 113 $board = id(new PHUIWorkboardView()) 114 ->setViewer($viewer) 115 ->setID($board_id) 116 ->addSigil('jx-workboard') 117 ->setMetadata( 118 array( 119 'boardPHID' => $project->getPHID(), 120 )); 121 122 $visible_columns = array(); 123 $column_phids = array(); 124 $visible_phids = array(); 125 foreach ($columns as $column) { 126 if (!$state->getShowHidden()) { 127 if ($column->isHidden()) { 128 continue; 129 } 130 } 131 132 $proxy = $column->getProxy(); 133 if ($proxy && !$proxy->isMilestone()) { 134 // TODO: For now, don't show subproject columns because we can't 135 // handle tasks with multiple positions yet. 136 continue; 137 } 138 139 $task_phids = $layout_engine->getColumnObjectPHIDs( 140 $board_phid, 141 $column->getPHID()); 142 143 $column_tasks = array_select_keys($tasks, $task_phids); 144 $column_phid = $column->getPHID(); 145 146 $visible_columns[$column_phid] = $column; 147 $column_phids[$column_phid] = $column_tasks; 148 149 foreach ($column_tasks as $phid => $task) { 150 $visible_phids[$phid] = $phid; 151 } 152 } 153 154 $container_phids = $state->getBoardContainerPHIDs(); 155 156 $rendering_engine = id(new PhabricatorBoardRenderingEngine()) 157 ->setViewer($viewer) 158 ->setObjects(array_select_keys($tasks, $visible_phids)) 159 ->setEditMap($task_can_edit_map) 160 ->setExcludedProjectPHIDs($container_phids); 161 162 $templates = array(); 163 $all_tasks = array(); 164 $column_templates = array(); 165 $sounds = array(); 166 foreach ($visible_columns as $column_phid => $column) { 167 $column_tasks = $column_phids[$column_phid]; 168 169 $panel = id(new PHUIWorkpanelView()) 170 ->setHeader($column->getDisplayName()) 171 ->setSubHeader($column->getDisplayType()) 172 ->addSigil('workpanel'); 173 174 $proxy = $column->getProxy(); 175 if ($proxy) { 176 $proxy_id = $proxy->getID(); 177 $href = $this->getApplicationURI("view/{$proxy_id}/"); 178 $panel->setHref($href); 179 } 180 181 $header_icon = $column->getHeaderIcon(); 182 if ($header_icon) { 183 $panel->setHeaderIcon($header_icon); 184 } 185 186 $display_class = $column->getDisplayClass(); 187 if ($display_class) { 188 $panel->addClass($display_class); 189 } 190 191 if ($column->isHidden()) { 192 $panel->addClass('project-panel-hidden'); 193 } 194 195 $column_menu = $this->buildColumnMenu($project, $column); 196 $panel->addHeaderAction($column_menu); 197 198 if ($column->canHaveTrigger()) { 199 $trigger = $column->getTrigger(); 200 if ($trigger) { 201 $trigger->setViewer($viewer); 202 } 203 204 $trigger_menu = $this->buildTriggerMenu($column); 205 $panel->addHeaderAction($trigger_menu); 206 } 207 208 $column_points_tip = pht('Tasks'); 209 $point_limit = $column->getPointLimit(); 210 if (ManiphestTaskPoints::getIsEnabled()) { 211 $column_points_tip .= ' | '.pht('Points'); 212 if ($point_limit !== null) { 213 $column_points_tip .= ' / '.pht('Point Limit'); 214 } 215 } else if ($point_limit !== null) { 216 $column_points_tip .= ' / '.pht('Count Limit'); 217 } 218 219 $count_tag = id(new PHUITagView()) 220 ->setType(PHUITagView::TYPE_SHADE) 221 ->setColor(PHUITagView::COLOR_BLUE) 222 ->addSigil('column-points') 223 ->addSigil('has-tooltip') 224 ->setName( 225 javelin_tag( 226 'span', 227 array( 228 'sigil' => 'column-points-content', 229 ), 230 pht('-'))) 231 ->setMetadata( 232 array( 233 'tip' => $column_points_tip, 234 'align' => 'S', 235 )) 236 ->setStyle('display: none'); 237 238 $panel->setHeaderTag($count_tag); 239 240 $cards = id(new PHUIObjectItemListView()) 241 ->setViewer($viewer) 242 ->setFlush(true) 243 ->setAllowEmptyList(true) 244 ->addSigil('project-column') 245 ->setItemClass('phui-workcard') 246 ->setMetadata( 247 array( 248 'columnPHID' => $column->getPHID(), 249 'pointLimit' => $column->getPointLimit(), 250 )); 251 252 $card_phids = array(); 253 foreach ($column_tasks as $task) { 254 $object_phid = $task->getPHID(); 255 256 $card = $rendering_engine->renderCard($object_phid); 257 $templates[$object_phid] = hsprintf('%s', $card->getItem()); 258 $card_phids[] = $object_phid; 259 260 $all_tasks[$object_phid] = $task; 261 } 262 263 $panel->setCards($cards); 264 $board->addPanel($panel); 265 266 $drop_effects = $column->getDropEffects(); 267 $drop_effects = mpull($drop_effects, 'toDictionary'); 268 269 $preview_effect = null; 270 if ($column->canHaveTrigger()) { 271 $trigger = $column->getTrigger(); 272 if ($trigger) { 273 $preview_effect = $trigger->getPreviewEffect() 274 ->toDictionary(); 275 276 foreach ($trigger->getSoundEffects() as $sound) { 277 $sounds[] = $sound; 278 } 279 } 280 } 281 282 $column_templates[] = array( 283 'columnPHID' => $column_phid, 284 'effects' => $drop_effects, 285 'cardPHIDs' => $card_phids, 286 'triggerPreviewEffect' => $preview_effect, 287 ); 288 } 289 290 $order_key = $state->getOrder(); 291 292 $ordering_map = PhabricatorProjectColumnOrder::getEnabledOrders(); 293 $ordering = id(clone $ordering_map[$order_key]) 294 ->setViewer($viewer); 295 296 $headers = $ordering->getHeadersForObjects($all_tasks); 297 $headers = mpull($headers, 'toDictionary'); 298 299 $vectors = $ordering->getSortVectorsForObjects($all_tasks); 300 $vector_map = array(); 301 foreach ($vectors as $task_phid => $vector) { 302 $vector_map[$task_phid][$order_key] = $vector; 303 } 304 305 $header_keys = $ordering->getHeaderKeysForObjects($all_tasks); 306 307 $order_maps = array(); 308 $order_maps[] = $ordering->toDictionary(); 309 310 $properties = array(); 311 foreach ($all_tasks as $task) { 312 $properties[$task->getPHID()] = 313 PhabricatorBoardResponseEngine::newTaskProperties($task); 314 } 315 316 $behavior_config = array( 317 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 318 'uploadURI' => '/file/dropupload/', 319 'coverURI' => $this->getApplicationURI('cover/'), 320 'reloadURI' => phutil_string_cast($state->newWorkboardURI('reload/')), 321 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), 322 'pointsEnabled' => ManiphestTaskPoints::getIsEnabled(), 323 324 'boardPHID' => $project->getPHID(), 325 'order' => $state->getOrder(), 326 'orders' => $order_maps, 327 'headers' => $headers, 328 'headerKeys' => $header_keys, 329 'templateMap' => $templates, 330 'orderMaps' => $vector_map, 331 'propertyMaps' => $properties, 332 'columnTemplates' => $column_templates, 333 334 'boardID' => $board_id, 335 'projectPHID' => $project->getPHID(), 336 'preloadSounds' => $sounds, 337 ); 338 $this->initBehavior('project-boards', $behavior_config); 339 340 $sort_menu = $this->buildSortMenu( 341 $viewer, 342 $project, 343 $state->getOrder(), 344 $ordering_map); 345 346 $filter_menu = $this->buildFilterMenu( 347 $viewer, 348 $project, 349 $custom_query, 350 $search_engine, 351 $query_key); 352 353 $manage_menu = $this->buildManageMenu($project, $state->getShowHidden()); 354 355 $header_link = phutil_tag( 356 'a', 357 array( 358 'href' => $this->getApplicationURI('profile/'.$project->getID().'/'), 359 ), 360 $project->getName()); 361 362 $board_box = id(new PHUIBoxView()) 363 ->appendChild($board) 364 ->addClass('project-board-wrapper'); 365 366 $nav = $this->newNavigation( 367 $project, 368 PhabricatorProject::ITEM_WORKBOARD); 369 370 $divider = id(new PHUIListItemView()) 371 ->setType(PHUIListItemView::TYPE_DIVIDER); 372 $fullscreen = $this->buildFullscreenMenu(); 373 374 $crumbs = $this->newWorkboardCrumbs(); 375 $crumbs->addTextCrumb(pht('Workboard')); 376 $crumbs->setBorder(true); 377 378 $crumbs->addAction($sort_menu); 379 $crumbs->addAction($filter_menu); 380 $crumbs->addAction($divider); 381 $crumbs->addAction($manage_menu); 382 $crumbs->addAction($fullscreen); 383 384 $page = $this->newPage() 385 ->setTitle( 386 array( 387 $project->getDisplayName(), 388 pht('Workboard'), 389 )) 390 ->setPageObjectPHIDs(array($project->getPHID())) 391 ->setShowFooter(false) 392 ->setNavigation($nav) 393 ->setCrumbs($crumbs) 394 ->addQuicksandConfig( 395 array( 396 'boardConfig' => $behavior_config, 397 )) 398 ->appendChild( 399 array( 400 $board_box, 401 )); 402 403 $background = $project->getDisplayWorkboardBackgroundColor(); 404 require_celerity_resource('phui-workboard-color-css'); 405 if ($background !== null) { 406 $background_color_class = "phui-workboard-{$background}"; 407 408 $page->addClass('phui-workboard-color'); 409 $page->addClass($background_color_class); 410 } else { 411 $page->addClass('phui-workboard-no-color'); 412 } 413 414 return $page; 415 } 416 417 /** 418 * @return PHUIListItemView 419 */ 420 private function buildSortMenu( 421 PhabricatorUser $viewer, 422 PhabricatorProject $project, 423 $sort_key, 424 array $ordering_map) { 425 426 $state = $this->getViewState(); 427 $base_uri = $state->newWorkboardURI(); 428 429 $items = array(); 430 foreach ($ordering_map as $key => $ordering) { 431 // TODO: It would be desirable to build a real "PHUIIconView" here, but 432 // the pathway for threading that through all the view classes ends up 433 // being fairly complex, since some callers read the icon out of other 434 // views. For now, just stick with a string. 435 $ordering_icon = $ordering->getMenuIconIcon(); 436 $ordering_name = $ordering->getDisplayName(); 437 438 $is_selected = ($key === $sort_key); 439 if ($is_selected) { 440 $active_name = $ordering_name; 441 $active_icon = $ordering_icon; 442 } 443 444 $item = id(new PhabricatorActionView()) 445 ->setIcon($ordering_icon) 446 ->setSelected($is_selected) 447 ->setName($ordering_name); 448 449 $uri = $base_uri->alter('order', $key); 450 $item->setHref($uri); 451 452 $items[] = $item; 453 } 454 455 $id = $project->getID(); 456 457 $save_uri = $state->newWorkboardURI('default/sort/'); 458 459 $can_edit = PhabricatorPolicyFilter::hasCapability( 460 $viewer, 461 $project, 462 PhabricatorPolicyCapability::CAN_EDIT); 463 464 $items[] = id(new PhabricatorActionView()) 465 ->setType(PhabricatorActionView::TYPE_DIVIDER); 466 467 $items[] = id(new PhabricatorActionView()) 468 ->setIcon('fa-floppy-o') 469 ->setName(pht('Save as Default')) 470 ->setHref($save_uri) 471 ->setWorkflow(true) 472 ->setDisabled(!$can_edit); 473 474 $sort_menu = id(new PhabricatorActionListView()) 475 ->setViewer($viewer); 476 foreach ($items as $item) { 477 $sort_menu->addAction($item); 478 } 479 480 $sort_button = id(new PHUIListItemView()) 481 ->setName($active_name) 482 ->setIcon($active_icon) 483 ->setHref('#') 484 ->addSigil('boards-dropdown-menu') 485 ->setMetadata( 486 array( 487 'items' => hsprintf('%s', $sort_menu), 488 )); 489 490 return $sort_button; 491 } 492 493 /** 494 * @return PHUIListItemView 495 */ 496 private function buildFilterMenu( 497 PhabricatorUser $viewer, 498 PhabricatorProject $project, 499 $custom_query, 500 PhabricatorApplicationSearchEngine $engine, 501 $query_key) { 502 503 $state = $this->getViewState(); 504 505 $named = array( 506 'open' => pht('Open Tasks'), 507 'all' => pht('All Tasks'), 508 ); 509 510 if ($viewer->isLoggedIn()) { 511 $named['assigned'] = pht('Assigned to Me'); 512 } 513 514 if ($custom_query) { 515 $named[$custom_query->getQueryKey()] = pht('Custom Filter'); 516 } 517 518 $items = array(); 519 foreach ($named as $key => $name) { 520 $is_selected = ($key == $query_key); 521 if ($is_selected) { 522 $active_filter = $name; 523 } 524 525 $is_custom = false; 526 if ($custom_query) { 527 $is_custom = ($key == $custom_query->getQueryKey()); 528 } 529 530 $item = id(new PhabricatorActionView()) 531 ->setIcon('fa-search') 532 ->setSelected($is_selected) 533 ->setName($name); 534 535 if ($is_custom) { 536 // When you're using a custom filter already and you select "Custom 537 // Filter", you get a dialog back to let you edit the filter. This is 538 // equivalent to selecting "Advanced Filter..." to configure a new 539 // filter. 540 $filter_uri = $state->newWorkboardURI('filter/'); 541 $item->setWorkflow(true); 542 } else { 543 $filter_uri = urisprintf('query/%s/', $key); 544 $filter_uri = $state->newWorkboardURI($filter_uri); 545 $filter_uri->removeQueryParam('filter'); 546 } 547 548 $item->setHref($filter_uri); 549 550 $items[] = $item; 551 } 552 553 $id = $project->getID(); 554 555 $filter_uri = $state->newWorkboardURI('filter/'); 556 557 $items[] = id(new PhabricatorActionView()) 558 ->setIcon('fa-cog') 559 ->setHref($filter_uri) 560 ->setWorkflow(true) 561 ->setName(pht('Advanced Filter...')); 562 563 $save_uri = $state->newWorkboardURI('default/filter/'); 564 565 $can_edit = PhabricatorPolicyFilter::hasCapability( 566 $viewer, 567 $project, 568 PhabricatorPolicyCapability::CAN_EDIT); 569 570 $items[] = id(new PhabricatorActionView()) 571 ->setType(PhabricatorActionView::TYPE_DIVIDER); 572 573 $items[] = id(new PhabricatorActionView()) 574 ->setIcon('fa-floppy-o') 575 ->setName(pht('Save as Default')) 576 ->setHref($save_uri) 577 ->setWorkflow(true) 578 ->setDisabled(!$can_edit); 579 580 $filter_menu = id(new PhabricatorActionListView()) 581 ->setViewer($viewer); 582 foreach ($items as $item) { 583 $filter_menu->addAction($item); 584 } 585 586 $filter_button = id(new PHUIListItemView()) 587 ->setName($active_filter) 588 ->setIcon('fa-search') 589 ->setHref('#') 590 ->addSigil('boards-dropdown-menu') 591 ->setMetadata( 592 array( 593 'items' => hsprintf('%s', $filter_menu), 594 )); 595 596 return $filter_button; 597 } 598 599 private function buildManageMenu( 600 PhabricatorProject $project, 601 $show_hidden) { 602 603 $request = $this->getRequest(); 604 $viewer = $request->getUser(); 605 $state = $this->getViewState(); 606 607 $id = $project->getID(); 608 609 $manage_uri = $this->getApplicationURI("board/{$id}/manage/"); 610 $add_uri = $this->getApplicationURI("board/{$id}/edit/"); 611 612 $can_edit = PhabricatorPolicyFilter::hasCapability( 613 $viewer, 614 $project, 615 PhabricatorPolicyCapability::CAN_EDIT); 616 617 $manage_items = array(); 618 619 $manage_items[] = id(new PhabricatorActionView()) 620 ->setIcon('fa-plus') 621 ->setName(pht('Add Column')) 622 ->setHref($add_uri) 623 ->setDisabled(!$can_edit) 624 ->setWorkflow(true); 625 626 $reorder_uri = $this->getApplicationURI("board/{$id}/reorder/"); 627 $manage_items[] = id(new PhabricatorActionView()) 628 ->setIcon('fa-exchange') 629 ->setName(pht('Reorder Columns')) 630 ->setHref($reorder_uri) 631 ->setDisabled(!$can_edit) 632 ->setWorkflow(true); 633 634 if ($show_hidden) { 635 $hidden_uri = $state->newWorkboardURI() 636 ->removeQueryParam('hidden'); 637 $hidden_icon = 'fa-eye-slash'; 638 $hidden_text = pht('Hide Hidden Columns'); 639 } else { 640 $hidden_uri = $state->newWorkboardURI() 641 ->replaceQueryParam('hidden', 'true'); 642 $hidden_icon = 'fa-eye'; 643 $hidden_text = pht('Show Hidden Columns'); 644 } 645 646 $manage_items[] = id(new PhabricatorActionView()) 647 ->setIcon($hidden_icon) 648 ->setName($hidden_text) 649 ->setHref($hidden_uri); 650 651 $manage_items[] = id(new PhabricatorActionView()) 652 ->setType(PhabricatorActionView::TYPE_DIVIDER); 653 654 $background_uri = $this->getApplicationURI("board/{$id}/background/"); 655 $manage_items[] = id(new PhabricatorActionView()) 656 ->setIcon('fa-paint-brush') 657 ->setName(pht('Change Background Color')) 658 ->setHref($background_uri) 659 ->setDisabled(!$can_edit) 660 ->setWorkflow(false); 661 662 $manage_uri = $this->getApplicationURI("board/{$id}/manage/"); 663 $manage_items[] = id(new PhabricatorActionView()) 664 ->setIcon('fa-gear') 665 ->setName(pht('Manage Workboard')) 666 ->setHref($manage_uri); 667 668 $manage_menu = id(new PhabricatorActionListView()) 669 ->setViewer($viewer); 670 foreach ($manage_items as $item) { 671 $manage_menu->addAction($item); 672 } 673 674 $manage_button = id(new PHUIListItemView()) 675 ->setIcon('fa-cog') 676 ->setHref('#') 677 ->addSigil('boards-dropdown-menu') 678 ->addSigil('has-tooltip') 679 ->setMetadata( 680 array( 681 'tip' => pht('Manage'), 682 'align' => 'S', 683 'items' => hsprintf('%s', $manage_menu), 684 )); 685 686 return $manage_button; 687 } 688 689 private function buildFullscreenMenu() { 690 691 $up = id(new PHUIListItemView()) 692 ->setIcon('fa-arrows-alt') 693 ->setHref('#') 694 ->addClass('phui-workboard-expand-icon') 695 ->addSigil('jx-toggle-class') 696 ->addSigil('has-tooltip') 697 ->setMetaData(array( 698 'tip' => pht('Fullscreen'), 699 'map' => array( 700 'phabricator-standard-page' => 'phui-workboard-fullscreen', 701 ), 702 )); 703 704 return $up; 705 } 706 707 private function buildColumnMenu( 708 PhabricatorProject $project, 709 PhabricatorProjectColumn $column) { 710 711 $request = $this->getRequest(); 712 $viewer = $request->getUser(); 713 $state = $this->getViewState(); 714 715 $can_edit = PhabricatorPolicyFilter::hasCapability( 716 $viewer, 717 $project, 718 PhabricatorPolicyCapability::CAN_EDIT); 719 720 $column_items = array(); 721 722 if ($column->getProxyPHID()) { 723 $default_phid = $column->getProxyPHID(); 724 } else { 725 $default_phid = $column->getProjectPHID(); 726 } 727 728 $specs = id(new ManiphestEditEngine()) 729 ->setViewer($viewer) 730 ->newCreateActionSpecifications(array()); 731 732 foreach ($specs as $spec) { 733 734 // Prefill tags= when you open the Column menu 735 // https://we.phorge.it/T15147 736 $spec_href = new PhutilURI($spec['uri']); 737 $spec_slug = $project->getPrimarySlug(); 738 if ($spec_slug !== null) { 739 $spec_href->replaceQueryParam('tags', $spec_slug); 740 } 741 742 $column_items[] = id(new PhabricatorActionView()) 743 ->setIcon($spec['icon']) 744 ->setName($spec['name']) 745 ->setHref($spec_href) 746 ->setDisabled($spec['disabled']) 747 ->addSigil('column-add-task') 748 ->setMetadata( 749 array( 750 'createURI' => $spec['uri'], 751 'columnPHID' => $column->getPHID(), 752 'boardPHID' => $project->getPHID(), 753 'projectPHID' => $default_phid, 754 )); 755 } 756 757 $column_items[] = id(new PhabricatorActionView()) 758 ->setType(PhabricatorActionView::TYPE_DIVIDER); 759 760 $query_uri = urisprintf('viewquery/%d/', $column->getID()); 761 $query_uri = $state->newWorkboardURI($query_uri); 762 763 $column_items[] = id(new PhabricatorActionView()) 764 ->setName(pht('View Tasks as Query')) 765 ->setIcon('fa-search') 766 ->setHref($query_uri); 767 768 $can_bulk_edit = PhabricatorPolicyFilter::hasCapability( 769 $viewer, 770 PhabricatorApplication::getByClass( 771 PhabricatorManiphestApplication::class), 772 ManiphestBulkEditCapability::CAPABILITY); 773 774 $column_move_uri = urisprintf('bulkmove/%d/column/', $column->getID()); 775 $column_move_uri = $state->newWorkboardURI($column_move_uri); 776 777 $column_items[] = id(new PhabricatorActionView()) 778 ->setIcon('fa-arrows-h') 779 ->setName(pht('Move Tasks to Column...')) 780 ->setHref($column_move_uri) 781 ->setDisabled(!$can_bulk_edit) 782 ->setWorkflow(true); 783 784 $project_move_uri = urisprintf('bulkmove/%d/project/', $column->getID()); 785 $project_move_uri = $state->newWorkboardURI($project_move_uri); 786 787 $column_items[] = id(new PhabricatorActionView()) 788 ->setIcon('fa-arrows') 789 ->setName(pht('Move Tasks to Project...')) 790 ->setHref($project_move_uri) 791 ->setDisabled(!$can_bulk_edit) 792 ->setWorkflow(true); 793 794 $bulk_edit_uri = urisprintf('bulk/%d/', $column->getID()); 795 $bulk_edit_uri = $state->newWorkboardURI($bulk_edit_uri); 796 797 $column_items[] = id(new PhabricatorActionView()) 798 ->setIcon('fa-pencil-square-o') 799 ->setName(pht('Bulk Edit Tasks...')) 800 ->setHref($bulk_edit_uri) 801 ->setDisabled(!$can_bulk_edit); 802 803 $column_items[] = id(new PhabricatorActionView()) 804 ->setType(PhabricatorActionView::TYPE_DIVIDER); 805 806 807 $edit_uri = 'board/'.$project->getID().'/edit/'.$column->getID().'/'; 808 $column_items[] = id(new PhabricatorActionView()) 809 ->setName(pht('Edit Column')) 810 ->setIcon('fa-pencil') 811 ->setHref($this->getApplicationURI($edit_uri)) 812 ->setDisabled(!$can_edit) 813 ->setWorkflow(true); 814 815 $can_hide = ($can_edit && !$column->isDefaultColumn()); 816 817 $hide_uri = urisprintf('hide/%d/', $column->getID()); 818 $hide_uri = $state->newWorkboardURI($hide_uri); 819 820 if (!$column->isHidden()) { 821 $column_items[] = id(new PhabricatorActionView()) 822 ->setName(pht('Hide Column')) 823 ->setIcon('fa-eye-slash') 824 ->setHref($hide_uri) 825 ->setDisabled(!$can_hide) 826 ->setWorkflow(true); 827 } else { 828 $column_items[] = id(new PhabricatorActionView()) 829 ->setName(pht('Show Column')) 830 ->setIcon('fa-eye') 831 ->setHref($hide_uri) 832 ->setDisabled(!$can_hide) 833 ->setWorkflow(true); 834 } 835 836 $column_menu = id(new PhabricatorActionListView()) 837 ->setViewer($viewer); 838 foreach ($column_items as $item) { 839 $column_menu->addAction($item); 840 } 841 842 $column_button = id(new PHUIIconView()) 843 ->setIcon('fa-pencil') 844 ->setHref('#') 845 ->addSigil('boards-dropdown-menu') 846 ->setMetadata( 847 array( 848 'items' => hsprintf('%s', $column_menu), 849 )); 850 851 return $column_button; 852 } 853 854 private function buildTriggerMenu(PhabricatorProjectColumn $column) { 855 $viewer = $this->getViewer(); 856 $trigger = $column->getTrigger(); 857 858 $can_edit = PhabricatorPolicyFilter::hasCapability( 859 $viewer, 860 $column, 861 PhabricatorPolicyCapability::CAN_EDIT); 862 863 $trigger_items = array(); 864 if (!$trigger) { 865 $set_uri = $this->getApplicationURI( 866 new PhutilURI( 867 'trigger/edit/', 868 array( 869 'columnPHID' => $column->getPHID(), 870 ))); 871 872 $trigger_items[] = id(new PhabricatorActionView()) 873 ->setIcon('fa-cogs') 874 ->setName(pht('New Trigger...')) 875 ->setHref($set_uri) 876 ->setDisabled(!$can_edit); 877 } else { 878 $trigger_items[] = id(new PhabricatorActionView()) 879 ->setIcon('fa-cogs') 880 ->setName(pht('View Trigger')) 881 ->setHref($trigger->getURI()) 882 ->setDisabled(!$can_edit); 883 } 884 885 $remove_uri = $this->getApplicationURI( 886 new PhutilURI( 887 urisprintf( 888 'column/remove/%d/', 889 $column->getID()))); 890 891 $trigger_items[] = id(new PhabricatorActionView()) 892 ->setIcon('fa-times') 893 ->setName(pht('Remove Trigger')) 894 ->setHref($remove_uri) 895 ->setWorkflow(true) 896 ->setDisabled(!$can_edit || !$trigger); 897 898 $trigger_menu = id(new PhabricatorActionListView()) 899 ->setViewer($viewer); 900 foreach ($trigger_items as $item) { 901 $trigger_menu->addAction($item); 902 } 903 904 if ($trigger) { 905 $trigger_icon = 'fa-cogs'; 906 } else { 907 $trigger_icon = 'fa-cogs grey'; 908 } 909 910 $trigger_button = id(new PHUIIconView()) 911 ->setIcon($trigger_icon) 912 ->setHref('#') 913 ->addSigil('boards-dropdown-menu') 914 ->addSigil('trigger-preview') 915 ->setMetadata( 916 array( 917 'items' => hsprintf('%s', $trigger_menu), 918 'columnPHID' => $column->getPHID(), 919 )); 920 921 return $trigger_button; 922 } 923 924 private function buildInitializeContent(PhabricatorProject $project) { 925 $request = $this->getRequest(); 926 $viewer = $this->getViewer(); 927 928 $type = $request->getStr('initialize-type'); 929 930 $id = $project->getID(); 931 932 $profile_uri = $this->getApplicationURI("profile/{$id}/"); 933 $board_uri = $this->getApplicationURI("board/{$id}/"); 934 $import_uri = $this->getApplicationURI("board/{$id}/import/"); 935 936 $set_default = $request->getBool('default'); 937 if ($set_default) { 938 $this 939 ->getProfileMenuEngine() 940 ->adjustDefault(PhabricatorProject::ITEM_WORKBOARD); 941 } 942 943 if ($request->isFormPost()) { 944 if ($type == 'backlog-only') { 945 $column = PhabricatorProjectColumn::initializeNewColumn($viewer) 946 ->setSequence(0) 947 ->setProperty('isDefault', true) 948 ->setProjectPHID($project->getPHID()) 949 ->save(); 950 951 $xactions = array(); 952 $xactions[] = id(new PhabricatorProjectTransaction()) 953 ->setTransactionType( 954 PhabricatorProjectWorkboardTransaction::TRANSACTIONTYPE) 955 ->setNewValue(1); 956 957 id(new PhabricatorProjectTransactionEditor()) 958 ->setActor($viewer) 959 ->setContentSourceFromRequest($request) 960 ->setContinueOnNoEffect(true) 961 ->setContinueOnMissingFields(true) 962 ->applyTransactions($project, $xactions); 963 964 return id(new AphrontRedirectResponse()) 965 ->setURI($board_uri); 966 } else { 967 return id(new AphrontRedirectResponse()) 968 ->setURI($import_uri); 969 } 970 } 971 972 // TODO: Tailor this UI if the project is already a parent project. We 973 // should not offer options for creating a parent project workboard, since 974 // they can't have their own columns. 975 976 $new_selector = id(new AphrontFormRadioButtonControl()) 977 ->setLabel(pht('Columns')) 978 ->setName('initialize-type') 979 ->setValue('backlog-only') 980 ->addButton( 981 'backlog-only', 982 pht('New Empty Board'), 983 pht('Create a new board with just a backlog column.')) 984 ->addButton( 985 'import', 986 pht('Import Columns'), 987 pht('Import board columns from another project.')); 988 989 $default_checkbox = id(new AphrontFormCheckboxControl()) 990 ->setLabel(pht('Make Default')) 991 ->addCheckbox( 992 'default', 993 1, 994 pht('Make the workboard the default view for this project.'), 995 true); 996 997 $form = id(new AphrontFormView()) 998 ->setUser($viewer) 999 ->addHiddenInput('initialize', 1) 1000 ->appendRemarkupInstructions( 1001 pht('The workboard for this project has not been created yet.')) 1002 ->appendControl($new_selector) 1003 ->appendControl($default_checkbox) 1004 ->appendControl( 1005 id(new AphrontFormSubmitControl()) 1006 ->addCancelButton($profile_uri) 1007 ->setValue(pht('Create Workboard'))); 1008 1009 $box = id(new PHUIObjectBoxView()) 1010 ->setHeaderText(pht('Create Workboard')) 1011 ->setForm($form); 1012 1013 return $box; 1014 } 1015 1016 private function buildNoAccessContent(PhabricatorProject $project) { 1017 $viewer = $this->getViewer(); 1018 1019 $id = $project->getID(); 1020 1021 $profile_uri = $this->getApplicationURI("profile/{$id}/"); 1022 1023 return $this->newDialog() 1024 ->setTitle(pht('Unable to Create Workboard')) 1025 ->appendParagraph( 1026 pht( 1027 'The workboard for this project has not been created yet, '. 1028 'but you do not have permission to create it. Only users '. 1029 'who can edit this project can create a workboard for it.')) 1030 ->addCancelButton($profile_uri); 1031 } 1032 1033 1034 private function buildEnableContent(PhabricatorProject $project) { 1035 $request = $this->getRequest(); 1036 $viewer = $this->getViewer(); 1037 1038 $id = $project->getID(); 1039 $profile_uri = $this->getApplicationURI("profile/{$id}/"); 1040 $board_uri = $this->getApplicationURI("board/{$id}/"); 1041 1042 if ($request->isFormPost()) { 1043 $xactions = array(); 1044 1045 $xactions[] = id(new PhabricatorProjectTransaction()) 1046 ->setTransactionType( 1047 PhabricatorProjectWorkboardTransaction::TRANSACTIONTYPE) 1048 ->setNewValue(1); 1049 1050 id(new PhabricatorProjectTransactionEditor()) 1051 ->setActor($viewer) 1052 ->setContentSourceFromRequest($request) 1053 ->setContinueOnNoEffect(true) 1054 ->setContinueOnMissingFields(true) 1055 ->applyTransactions($project, $xactions); 1056 1057 return id(new AphrontRedirectResponse()) 1058 ->setURI($board_uri); 1059 } 1060 1061 return $this->newDialog() 1062 ->setTitle(pht('Workboard Disabled')) 1063 ->addHiddenInput('initialize', 1) 1064 ->appendParagraph( 1065 pht( 1066 'This workboard has been disabled, but can be restored to its '. 1067 'former glory.')) 1068 ->addCancelButton($profile_uri) 1069 ->addSubmitButton(pht('Enable Workboard')); 1070 } 1071 1072 private function buildDisabledContent(PhabricatorProject $project) { 1073 $viewer = $this->getViewer(); 1074 1075 $id = $project->getID(); 1076 1077 $profile_uri = $this->getApplicationURI("profile/{$id}/"); 1078 1079 return $this->newDialog() 1080 ->setTitle(pht('Workboard Disabled')) 1081 ->appendParagraph( 1082 pht( 1083 'This workboard has been disabled, and you do not have permission '. 1084 'to enable it. Only users who can edit this project can restore '. 1085 'the workboard.')) 1086 ->addCancelButton($profile_uri); 1087 } 1088 1089}