[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 final class PhabricatorProjectBoardViewController 4 extends PhabricatorProjectBoardController { 5 6 private $id; 7 private $slug; 8 private $handles; 9 private $queryKey; 10 private $filter; 11 private $sortKey; 12 private $showHidden; 13 14 public function shouldAllowPublic() { 15 return true; 16 } 17 18 public function willProcessRequest(array $data) { 19 $this->id = idx($data, 'id'); 20 $this->slug = idx($data, 'slug'); 21 $this->queryKey = idx($data, 'queryKey'); 22 $this->filter = (bool)idx($data, 'filter'); 23 } 24 25 public function processRequest() { 26 $request = $this->getRequest(); 27 $viewer = $request->getUser(); 28 29 $show_hidden = $request->getBool('hidden'); 30 $this->showHidden = $show_hidden; 31 32 $project = id(new PhabricatorProjectQuery()) 33 ->setViewer($viewer) 34 ->needImages(true); 35 if ($this->slug) { 36 $project->withSlugs(array($this->slug)); 37 } else { 38 $project->withIDs(array($this->id)); 39 } 40 $project = $project->executeOne(); 41 if (!$project) { 42 return new Aphront404Response(); 43 } 44 45 $this->setProject($project); 46 $this->id = $project->getID(); 47 48 $sort_key = $request->getStr('order'); 49 switch ($sort_key) { 50 case PhabricatorProjectColumn::ORDER_NATURAL: 51 case PhabricatorProjectColumn::ORDER_PRIORITY: 52 break; 53 default: 54 $sort_key = PhabricatorProjectColumn::DEFAULT_ORDER; 55 break; 56 } 57 $this->sortKey = $sort_key; 58 59 $column_query = id(new PhabricatorProjectColumnQuery()) 60 ->setViewer($viewer) 61 ->withProjectPHIDs(array($project->getPHID())); 62 63 if (!$show_hidden) { 64 $column_query->withStatuses( 65 array(PhabricatorProjectColumn::STATUS_ACTIVE)); 66 } 67 68 $columns = $column_query->execute(); 69 $columns = mpull($columns, null, 'getSequence'); 70 71 // TODO: Expand the checks here if we add the ability 72 // to hide the Backlog column 73 if (!$columns) { 74 switch ($request->getStr('initialize-type')) { 75 case 'backlog-only': 76 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 77 $column = PhabricatorProjectColumn::initializeNewColumn($viewer) 78 ->setSequence(0) 79 ->setProperty('isDefault', true) 80 ->setProjectPHID($project->getPHID()) 81 ->save(); 82 $column->attachProject($project); 83 $columns[0] = $column; 84 unset($unguarded); 85 break; 86 case 'import': 87 return id(new AphrontRedirectResponse()) 88 ->setURI( 89 $this->getApplicationURI('board/'.$project->getID().'/import/')); 90 break; 91 default: 92 return $this->initializeWorkboardDialog($project); 93 break; 94 } 95 } 96 97 ksort($columns); 98 99 $board_uri = $this->getApplicationURI('board/'.$project->getID().'/'); 100 101 $engine = id(new ManiphestTaskSearchEngine()) 102 ->setViewer($viewer) 103 ->setBaseURI($board_uri) 104 ->setIsBoardView(true); 105 106 if ($request->isFormPost()) { 107 $saved = $engine->buildSavedQueryFromRequest($request); 108 $engine->saveQuery($saved); 109 return id(new AphrontRedirectResponse())->setURI( 110 $this->getURIWithState( 111 $engine->getQueryResultsPageURI($saved->getQueryKey()))); 112 } 113 114 $query_key = $this->queryKey; 115 if (!$query_key) { 116 $query_key = 'open'; 117 } 118 $this->queryKey = $query_key; 119 120 $custom_query = null; 121 if ($engine->isBuiltinQuery($query_key)) { 122 $saved = $engine->buildSavedQueryFromBuiltin($query_key); 123 } else { 124 $saved = id(new PhabricatorSavedQueryQuery()) 125 ->setViewer($viewer) 126 ->withQueryKeys(array($query_key)) 127 ->executeOne(); 128 129 if (!$saved) { 130 return new Aphront404Response(); 131 } 132 133 $custom_query = $saved; 134 } 135 136 if ($this->filter) { 137 $filter_form = id(new AphrontFormView()) 138 ->setUser($viewer); 139 $engine->buildSearchForm($filter_form, $saved); 140 141 return $this->newDialog() 142 ->setWidth(AphrontDialogView::WIDTH_FULL) 143 ->setTitle(pht('Advanced Filter')) 144 ->appendChild($filter_form->buildLayoutView()) 145 ->setSubmitURI($board_uri) 146 ->addSubmitButton(pht('Apply Filter')) 147 ->addCancelButton($board_uri); 148 } 149 150 $task_query = $engine->buildQueryFromSavedQuery($saved); 151 152 $tasks = $task_query 153 ->addWithAllProjects(array($project->getPHID())) 154 ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) 155 ->setViewer($viewer) 156 ->execute(); 157 $tasks = mpull($tasks, null, 'getPHID'); 158 159 if ($tasks) { 160 $positions = id(new PhabricatorProjectColumnPositionQuery()) 161 ->setViewer($viewer) 162 ->withObjectPHIDs(mpull($tasks, 'getPHID')) 163 ->withColumns($columns) 164 ->execute(); 165 $positions = mpull($positions, null, 'getObjectPHID'); 166 } else { 167 $positions = array(); 168 } 169 170 $task_map = array(); 171 foreach ($tasks as $task) { 172 $task_phid = $task->getPHID(); 173 if (empty($positions[$task_phid])) { 174 // This shouldn't normally be possible because we create positions on 175 // demand, but we might have raced as an object was removed from the 176 // board. Just drop the task if we don't have a position for it. 177 continue; 178 } 179 180 $position = $positions[$task_phid]; 181 $task_map[$position->getColumnPHID()][] = $task_phid; 182 } 183 184 // If we're showing the board in "natural" order, sort columns by their 185 // column positions. 186 if ($this->sortKey == PhabricatorProjectColumn::ORDER_NATURAL) { 187 foreach ($task_map as $column_phid => $task_phids) { 188 $order = array(); 189 foreach ($task_phids as $task_phid) { 190 if (isset($positions[$task_phid])) { 191 $order[$task_phid] = $positions[$task_phid]->getOrderingKey(); 192 } else { 193 $order[$task_phid] = 0; 194 } 195 } 196 asort($order); 197 $task_map[$column_phid] = array_keys($order); 198 } 199 } 200 201 $task_can_edit_map = id(new PhabricatorPolicyFilter()) 202 ->setViewer($viewer) 203 ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) 204 ->apply($tasks); 205 206 $board_id = celerity_generate_unique_node_id(); 207 208 $board = id(new PHUIWorkboardView()) 209 ->setUser($viewer) 210 ->setID($board_id); 211 212 $this->initBehavior( 213 'project-boards', 214 array( 215 'boardID' => $board_id, 216 'projectPHID' => $project->getPHID(), 217 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 218 'createURI' => '/maniphest/task/create/', 219 'order' => $this->sortKey, 220 )); 221 222 $this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); 223 224 foreach ($columns as $column) { 225 $task_phids = idx($task_map, $column->getPHID(), array()); 226 $column_tasks = array_select_keys($tasks, $task_phids); 227 228 $panel = id(new PHUIWorkpanelView()) 229 ->setHeader($column->getDisplayName()) 230 ->setSubHeader($column->getDisplayType()) 231 ->addSigil('workpanel'); 232 233 $header_icon = $column->getHeaderIcon(); 234 if ($header_icon) { 235 $panel->setHeaderIcon($header_icon); 236 } 237 238 if ($column->isHidden()) { 239 $panel->addClass('project-panel-hidden'); 240 } 241 242 $column_menu = $this->buildColumnMenu($project, $column); 243 $panel->addHeaderAction($column_menu); 244 245 $tag_id = celerity_generate_unique_node_id(); 246 $tag_content_id = celerity_generate_unique_node_id(); 247 248 $count_tag = id(new PHUITagView()) 249 ->setType(PHUITagView::TYPE_SHADE) 250 ->setShade(PHUITagView::COLOR_BLUE) 251 ->setID($tag_id) 252 ->setName(phutil_tag('span', array('id' => $tag_content_id), '-')) 253 ->setStyle('display: none'); 254 255 $panel->setHeaderTag($count_tag); 256 257 $cards = id(new PHUIObjectItemListView()) 258 ->setUser($viewer) 259 ->setFlush(true) 260 ->setAllowEmptyList(true) 261 ->addSigil('project-column') 262 ->setMetadata( 263 array( 264 'columnPHID' => $column->getPHID(), 265 'countTagID' => $tag_id, 266 'countTagContentID' => $tag_content_id, 267 'pointLimit' => $column->getPointLimit(), 268 )); 269 270 foreach ($column_tasks as $task) { 271 $owner = null; 272 if ($task->getOwnerPHID()) { 273 $owner = $this->handles[$task->getOwnerPHID()]; 274 } 275 $can_edit = idx($task_can_edit_map, $task->getPHID(), false); 276 $cards->addItem(id(new ProjectBoardTaskCard()) 277 ->setViewer($viewer) 278 ->setTask($task) 279 ->setOwner($owner) 280 ->setCanEdit($can_edit) 281 ->getItem()); 282 } 283 $panel->setCards($cards); 284 $board->addPanel($panel); 285 } 286 287 Javelin::initBehavior( 288 'boards-dropdown', 289 array()); 290 291 $sort_menu = $this->buildSortMenu( 292 $viewer, 293 $sort_key); 294 295 $filter_menu = $this->buildFilterMenu( 296 $viewer, 297 $custom_query, 298 $engine, 299 $query_key); 300 301 $manage_menu = $this->buildManageMenu($project, $show_hidden); 302 303 $header_link = phutil_tag( 304 'a', 305 array( 306 'href' => $this->getApplicationURI('view/'.$project->getID().'/'), 307 ), 308 $project->getName()); 309 310 $header = id(new PHUIHeaderView()) 311 ->setHeader($header_link) 312 ->setUser($viewer) 313 ->setNoBackground(true) 314 ->setImage($project->getProfileImageURI()) 315 ->setImageURL($this->getApplicationURI('view/'.$project->getID().'/')) 316 ->addActionLink($sort_menu) 317 ->addActionLink($filter_menu) 318 ->addActionLink($manage_menu) 319 ->setPolicyObject($project); 320 321 $board_box = id(new PHUIBoxView()) 322 ->appendChild($board) 323 ->addClass('project-board-wrapper'); 324 325 return $this->buildApplicationPage( 326 array( 327 $header, 328 $board_box, 329 ), 330 array( 331 'title' => pht('%s Board', $project->getName()), 332 'showFooter' => false, 333 )); 334 } 335 336 private function buildSortMenu( 337 PhabricatorUser $viewer, 338 $sort_key) { 339 340 $sort_icon = id(new PHUIIconView()) 341 ->setIconFont('fa-sort-amount-asc bluegrey'); 342 343 $named = array( 344 PhabricatorProjectColumn::ORDER_NATURAL => pht('Natural'), 345 PhabricatorProjectColumn::ORDER_PRIORITY => pht('Sort by Priority'), 346 ); 347 348 $base_uri = $this->getURIWithState(); 349 350 $items = array(); 351 foreach ($named as $key => $name) { 352 $is_selected = ($key == $sort_key); 353 if ($is_selected) { 354 $active_order = $name; 355 } 356 357 $item = id(new PhabricatorActionView()) 358 ->setIcon('fa-sort-amount-asc') 359 ->setSelected($is_selected) 360 ->setName($name); 361 362 $uri = $base_uri->alter('order', $key); 363 $item->setHref($uri); 364 365 $items[] = $item; 366 } 367 368 $sort_menu = id(new PhabricatorActionListView()) 369 ->setUser($viewer); 370 foreach ($items as $item) { 371 $sort_menu->addAction($item); 372 } 373 374 $sort_button = id(new PHUIButtonView()) 375 ->setText(pht('Sort: %s', $active_order)) 376 ->setIcon($sort_icon) 377 ->setTag('a') 378 ->setHref('#') 379 ->addSigil('boards-dropdown-menu') 380 ->setMetadata( 381 array( 382 'items' => hsprintf('%s', $sort_menu), 383 )); 384 385 return $sort_button; 386 } 387 private function buildFilterMenu( 388 PhabricatorUser $viewer, 389 $custom_query, 390 PhabricatorApplicationSearchEngine $engine, 391 $query_key) { 392 393 $filter_icon = id(new PHUIIconView()) 394 ->setIconFont('fa-search-plus bluegrey'); 395 396 $named = array( 397 'open' => pht('Open Tasks'), 398 'all' => pht('All Tasks'), 399 ); 400 401 if ($viewer->isLoggedIn()) { 402 $named['assigned'] = pht('Assigned to Me'); 403 } 404 405 if ($custom_query) { 406 $named[$custom_query->getQueryKey()] = pht('Custom Filter'); 407 } 408 409 $items = array(); 410 foreach ($named as $key => $name) { 411 $is_selected = ($key == $query_key); 412 if ($is_selected) { 413 $active_filter = $name; 414 } 415 416 $is_custom = false; 417 if ($custom_query) { 418 $is_custom = ($key == $custom_query->getQueryKey()); 419 } 420 421 $item = id(new PhabricatorActionView()) 422 ->setIcon('fa-search') 423 ->setSelected($is_selected) 424 ->setName($name); 425 426 if ($is_custom) { 427 $uri = $this->getApplicationURI( 428 'board/'.$this->id.'/filter/query/'.$key.'/'); 429 $item->setWorkflow(true); 430 } else { 431 $uri = $engine->getQueryResultsPageURI($key); 432 } 433 434 $uri = $this->getURIWithState($uri); 435 $item->setHref($uri); 436 437 $items[] = $item; 438 } 439 440 $items[] = id(new PhabricatorActionView()) 441 ->setIcon('fa-cog') 442 ->setHref($this->getApplicationURI('board/'.$this->id.'/filter/')) 443 ->setWorkflow(true) 444 ->setName(pht('Advanced Filter...')); 445 446 $filter_menu = id(new PhabricatorActionListView()) 447 ->setUser($viewer); 448 foreach ($items as $item) { 449 $filter_menu->addAction($item); 450 } 451 452 $filter_button = id(new PHUIButtonView()) 453 ->setText(pht('Filter: %s', $active_filter)) 454 ->setIcon($filter_icon) 455 ->setTag('a') 456 ->setHref('#') 457 ->addSigil('boards-dropdown-menu') 458 ->setMetadata( 459 array( 460 'items' => hsprintf('%s', $filter_menu), 461 )); 462 463 return $filter_button; 464 } 465 466 private function buildManageMenu( 467 PhabricatorProject $project, 468 $show_hidden) { 469 470 $request = $this->getRequest(); 471 $viewer = $request->getUser(); 472 473 $can_edit = PhabricatorPolicyFilter::hasCapability( 474 $viewer, 475 $project, 476 PhabricatorPolicyCapability::CAN_EDIT); 477 478 $manage_icon = id(new PHUIIconView()) 479 ->setIconFont('fa-cog bluegrey'); 480 481 $manage_items = array(); 482 483 $manage_items[] = id(new PhabricatorActionView()) 484 ->setIcon('fa-plus') 485 ->setName(pht('Add Column')) 486 ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/')) 487 ->setDisabled(!$can_edit) 488 ->setWorkflow(!$can_edit); 489 490 $manage_items[] = id(new PhabricatorActionView()) 491 ->setIcon('fa-exchange') 492 ->setName(pht('Reorder Columns')) 493 ->setHref($this->getApplicationURI('board/'.$this->id.'/reorder/')) 494 ->setDisabled(!$can_edit) 495 ->setWorkflow(true); 496 497 if ($show_hidden) { 498 $hidden_uri = $this->getURIWithState() 499 ->setQueryParam('hidden', null); 500 $hidden_icon = 'fa-eye-slash'; 501 $hidden_text = pht('Hide Hidden Columns'); 502 } else { 503 $hidden_uri = $this->getURIWithState() 504 ->setQueryParam('hidden', 'true'); 505 $hidden_icon = 'fa-eye'; 506 $hidden_text = pht('Show Hidden Columns'); 507 } 508 509 $manage_items[] = id(new PhabricatorActionView()) 510 ->setIcon($hidden_icon) 511 ->setName($hidden_text) 512 ->setHref($hidden_uri); 513 514 $manage_menu = id(new PhabricatorActionListView()) 515 ->setUser($viewer); 516 foreach ($manage_items as $item) { 517 $manage_menu->addAction($item); 518 } 519 520 $manage_button = id(new PHUIButtonView()) 521 ->setText(pht('Manage Board')) 522 ->setIcon($manage_icon) 523 ->setTag('a') 524 ->setHref('#') 525 ->addSigil('boards-dropdown-menu') 526 ->setMetadata( 527 array( 528 'items' => hsprintf('%s', $manage_menu), 529 )); 530 531 return $manage_button; 532 } 533 534 private function buildColumnMenu( 535 PhabricatorProject $project, 536 PhabricatorProjectColumn $column) { 537 538 $request = $this->getRequest(); 539 $viewer = $request->getUser(); 540 541 $can_edit = PhabricatorPolicyFilter::hasCapability( 542 $viewer, 543 $project, 544 PhabricatorPolicyCapability::CAN_EDIT); 545 546 $column_items = array(); 547 548 $column_items[] = id(new PhabricatorActionView()) 549 ->setIcon('fa-plus') 550 ->setName(pht('Create Task...')) 551 ->setHref('/maniphest/task/create/') 552 ->addSigil('column-add-task') 553 ->setMetadata( 554 array( 555 'columnPHID' => $column->getPHID(), 556 )) 557 ->setDisabled(!$can_edit); 558 559 $edit_uri = $this->getApplicationURI( 560 'board/'.$this->id.'/column/'.$column->getID().'/'); 561 562 $column_items[] = id(new PhabricatorActionView()) 563 ->setIcon('fa-pencil') 564 ->setName(pht('Edit Column')) 565 ->setHref($edit_uri) 566 ->setDisabled(!$can_edit) 567 ->setWorkflow(!$can_edit); 568 569 $can_hide = ($can_edit && !$column->isDefaultColumn()); 570 $hide_uri = 'board/'.$this->id.'/hide/'.$column->getID().'/'; 571 $hide_uri = $this->getApplicationURI($hide_uri); 572 $hide_uri = $this->getURIWithState($hide_uri); 573 574 if (!$column->isHidden()) { 575 $column_items[] = id(new PhabricatorActionView()) 576 ->setName(pht('Hide Column')) 577 ->setIcon('fa-eye-slash') 578 ->setHref($hide_uri) 579 ->setDisabled(!$can_hide) 580 ->setWorkflow(true); 581 } else { 582 $column_items[] = id(new PhabricatorActionView()) 583 ->setName(pht('Show Column')) 584 ->setIcon('fa-eye') 585 ->setHref($hide_uri) 586 ->setDisabled(!$can_hide) 587 ->setWorkflow(true); 588 } 589 590 $column_menu = id(new PhabricatorActionListView()) 591 ->setUser($viewer); 592 foreach ($column_items as $item) { 593 $column_menu->addAction($item); 594 } 595 596 $column_button = id(new PHUIIconView()) 597 ->setIconFont('fa-caret-down') 598 ->setHref('#') 599 ->addSigil('boards-dropdown-menu') 600 ->setMetadata( 601 array( 602 'items' => hsprintf('%s', $column_menu), 603 )); 604 605 return $column_button; 606 } 607 608 private function initializeWorkboardDialog(PhabricatorProject $project) { 609 610 $instructions = pht('This workboard has not been setup yet.'); 611 $new_selector = id(new AphrontFormRadioButtonControl()) 612 ->setName('initialize-type') 613 ->setValue('backlog-only') 614 ->addButton( 615 'backlog-only', 616 pht('New Empty Board'), 617 pht('Create a new board with just a backlog column.')) 618 ->addButton( 619 'import', 620 pht('Import Columns'), 621 pht('Import board columns from another project.')); 622 623 $dialog = id(new AphrontDialogView()) 624 ->setUser($this->getRequest()->getUser()) 625 ->setTitle(pht('New Workboard')) 626 ->addSubmitButton('Continue') 627 ->addCancelButton($this->getApplicationURI('view/'.$project->getID().'/')) 628 ->appendParagraph($instructions) 629 ->appendChild($new_selector); 630 631 return id(new AphrontDialogResponse()) 632 ->setDialog($dialog); 633 } 634 635 636 /** 637 * Add current state parameters (like order and the visibility of hidden 638 * columns) to a URI. 639 * 640 * This allows actions which toggle or adjust one piece of state to keep 641 * the rest of the board state persistent. If no URI is provided, this method 642 * starts with the request URI. 643 * 644 * @param string|null URI to add state parameters to. 645 * @return PhutilURI URI with state parameters. 646 */ 647 private function getURIWithState($base = null) { 648 if ($base === null) { 649 $base = $this->getRequest()->getRequestURI(); 650 } 651 652 $base = new PhutilURI($base); 653 654 if ($this->sortKey != PhabricatorProjectColumn::DEFAULT_ORDER) { 655 $base->setQueryParam('order', $this->sortKey); 656 } else { 657 $base->setQueryParam('order', null); 658 } 659 660 $base->setQueryParam('hidden', $this->showHidden ? 'true' : null); 661 662 return $base; 663 } 664 665 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |