[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/project/controller/ -> PhabricatorProjectBoardViewController.php (source)

   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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1