Differential D10610 Diff 30841 src/applications/project/controller/PhabricatorProjectColumnContentController.php
Changeset View
Changeset View
Standalone View
Standalone View
src/applications/project/controller/PhabricatorProjectColumnContentController.php
- This file was added.
| <?php | |||||
| final class PhabricatorProjectColumnContentController | |||||
| extends PhabricatorProjectBoardController { | |||||
| private $id; | |||||
| private $projectID; | |||||
| private $queryKey; | |||||
| private $filter; | |||||
| private $sortKey; | |||||
| public function willProcessRequest(array $data) { | |||||
| $this->projectID = $data['projectID']; | |||||
| $this->id = idx($data, 'id'); | |||||
| $this->queryKey = idx($data, 'queryKey'); | |||||
| $this->filter = (bool)idx($data, 'filter'); | |||||
| } | |||||
| public function processRequest() { | |||||
| $request = $this->getRequest(); | |||||
| $viewer = $request->getUser(); | |||||
| $project = id(new PhabricatorProjectQuery()) | |||||
| ->setViewer($viewer) | |||||
| ->requireCapabilities( | |||||
| array( | |||||
| PhabricatorPolicyCapability::CAN_VIEW, | |||||
| )) | |||||
| ->withIDs(array($this->projectID)) | |||||
| ->executeOne(); | |||||
| if (!$project) { | |||||
| return new Aphront404Response(); | |||||
| } | |||||
| $this->setProject($project); | |||||
| $column = id(new PhabricatorProjectColumnQuery()) | |||||
| ->setViewer($viewer) | |||||
| ->withIDs(array($this->id)) | |||||
| ->requireCapabilities( | |||||
| array( | |||||
| PhabricatorPolicyCapability::CAN_VIEW, | |||||
| )) | |||||
| ->executeOne(); | |||||
| if (!$column) { | |||||
| return new Aphront404Response(); | |||||
| } | |||||
| $sort_key = $request->getStr('order'); | |||||
| switch ($sort_key) { | |||||
| case PhabricatorProjectColumn::ORDER_NATURAL: | |||||
| case PhabricatorProjectColumn::ORDER_PRIORITY: | |||||
| break; | |||||
| default: | |||||
| $sort_key = PhabricatorProjectColumn::DEFAULT_ORDER; | |||||
| break; | |||||
| } | |||||
| $this->sortKey = $sort_key; | |||||
| // We already know who we are, this is for the side nav later | |||||
| $column_query = id(new PhabricatorProjectColumnQuery()) | |||||
| ->setViewer($viewer) | |||||
| ->withStatuses( | |||||
| array(PhabricatorProjectColumn::STATUS_ACTIVE)) | |||||
| ->withProjectPHIDs(array($project->getPHID())); | |||||
| $columns = $column_query->execute(); | |||||
| $columns = array_reverse(mpull($columns, null, 'getSequence')); | |||||
| $title = pht('%s', $column->getDisplayName()); | |||||
| $crumbs = $this->buildApplicationCrumbs(); | |||||
| $crumbs->addTextCrumb( | |||||
| pht('Board'), | |||||
| $this->getApplicationURI('board/'.$project->getID().'/')); | |||||
| $crumbs->addTextCrumb($title); | |||||
| $column_uri = $this->getApplicationURI('board/'.$project->getID(). | |||||
| '/column/'.$this->id.'/'); | |||||
| // copy pasted from view starts | |||||
| $engine = id(new ManiphestTaskSearchEngine()) | |||||
| ->setViewer($viewer) | |||||
| ->setBaseURI($column_uri) | |||||
| ->setIsBoardView(true); | |||||
| if ($request->isFormPost()) { | |||||
| $saved = $engine->buildSavedQueryFromRequest($request); | |||||
| $engine->saveQuery($saved); | |||||
| return id(new AphrontRedirectResponse())->setURI( | |||||
| $this->getURIWithState( | |||||
| $engine->getQueryResultsPageURI($saved->getQueryKey()))); | |||||
| } | |||||
| $query_key = $this->queryKey; | |||||
| if (!$query_key) { | |||||
| $query_key = 'open'; | |||||
| } | |||||
| $this->queryKey = $query_key; | |||||
| $custom_query = null; | |||||
| if ($engine->isBuiltinQuery($query_key)) { | |||||
| $saved = $engine->buildSavedQueryFromBuiltin($query_key); | |||||
| $saved->setParameter('group', 'none'); | |||||
| } else { | |||||
| $saved = id(new PhabricatorSavedQueryQuery()) | |||||
| ->setViewer($viewer) | |||||
| ->withQueryKeys(array($query_key)) | |||||
| ->executeOne(); | |||||
| if (!$saved) { | |||||
| return new Aphront404Response(); | |||||
| } | |||||
| $custom_query = $saved; | |||||
| } | |||||
| if ($this->filter) { | |||||
| $filter_form = id(new AphrontFormView()) | |||||
| ->setUser($viewer); | |||||
| $engine->buildSearchForm($filter_form, $saved); | |||||
| return $this->newDialog() | |||||
| ->setWidth(AphrontDialogView::WIDTH_FULL) | |||||
| ->setTitle(pht('Advanced Filter')) | |||||
| ->appendChild($filter_form->buildLayoutView()) | |||||
| ->setSubmitURI($column_uri) | |||||
| ->addSubmitButton(pht('Apply Filter')) | |||||
| ->addCancelButton($column_uri); | |||||
| } | |||||
| $task_query = $engine->buildQueryFromSavedQuery($saved); | |||||
| $tasks = $task_query | |||||
| ->withEdgeLogicPHIDs( | |||||
| PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, | |||||
| PhabricatorQueryConstraint::OPERATOR_AND, | |||||
| array($project->getPHID())) | |||||
| ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) | |||||
| ->setGroupBy(ManiphestTaskQuery::GROUP_NONE) | |||||
| ->setViewer($viewer) | |||||
| ->execute(); | |||||
| $tasks = mpull($tasks, null, 'getPHID'); | |||||
| if ($tasks) { | |||||
| $positions = id(new PhabricatorProjectColumnPositionQuery()) | |||||
| ->setViewer($viewer) | |||||
| ->withObjectPHIDs(mpull($tasks, 'getPHID')) | |||||
| ->withColumns(array($column)) | |||||
| ->execute(); | |||||
| $positions = mpull($positions, null, 'getObjectPHID'); | |||||
| } else { | |||||
| $positions = array(); | |||||
| } | |||||
| $task_map = array(); | |||||
| foreach ($tasks as $task) { | |||||
| $task_phid = $task->getPHID(); | |||||
| if (empty($positions[$task_phid])) { | |||||
| // This shouldn't normally be possible because we create positions on | |||||
| // demand, but we might have raced as an object was removed from the | |||||
| // board. Just drop the task if we don't have a position for it. | |||||
| continue; | |||||
| } | |||||
| $position = $positions[$task_phid]; | |||||
| $task_map[$position->getColumnPHID()][] = $task_phid; | |||||
| } | |||||
| // We get all open tasks for the project (or whatever the query | |||||
| // is) and then filter down to tasks for a specific column. This is | |||||
| // potentially inefficient for large projects, but we aleady need to | |||||
| // display everything on a the main workboard anyway so we should be okay | |||||
| // here. | |||||
| $tasks = array_select_keys($tasks, array_keys($positions)); | |||||
| // If we're showing the board in "natural" order, sort columns by their | |||||
| // column positions. | |||||
| if ($this->sortKey == PhabricatorProjectColumn::ORDER_NATURAL) { | |||||
| foreach ($task_map as $column_phid => $task_phids) { | |||||
| $order = array(); | |||||
| foreach ($task_phids as $task_phid) { | |||||
| if (isset($positions[$task_phid])) { | |||||
| $order[$task_phid] = $positions[$task_phid]->getOrderingKey(); | |||||
| } else { | |||||
| $order[$task_phid] = 0; | |||||
| } | |||||
| } | |||||
| asort($order); | |||||
| $task_map[$column_phid] = array_keys($order); | |||||
| } | |||||
| } | |||||
| if (!empty($tasks)) { | |||||
| $tasks = array_select_keys(mpull($tasks, null, 'getPHID'), | |||||
| $task_map[$column->getPHID()]); | |||||
| } | |||||
| $task_can_edit_map = id(new PhabricatorPolicyFilter()) | |||||
| ->setViewer($viewer) | |||||
| ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) | |||||
| ->apply($tasks); | |||||
| $results_view = id(new ManiphestTaskResultListView()) | |||||
| ->setUser($viewer) | |||||
| ->setTasks($tasks) | |||||
| ->setSavedQuery($saved) | |||||
| ->setCanBatchEdit(true) | |||||
| ->setCanEditPriority(true) | |||||
| ->setShowBatchControls(true); | |||||
| $sort_menu = $this->buildSortMenu( | |||||
| $viewer, | |||||
| $sort_key); | |||||
| $filter_menu = $this->buildFilterMenu( | |||||
| $viewer, | |||||
| $custom_query, | |||||
| $engine, | |||||
| $query_key); | |||||
| $header = id(new PHUIHeaderView()) | |||||
| // FIXME: PHUIHeaderView with only ActionLinks cuts off menus | |||||
| ->setHeader('dummy') | |||||
| ->setNoBackground(true) | |||||
| ->addActionLink($sort_menu) | |||||
| ->addActionLink($filter_menu); | |||||
| $nav = $this->buildSideNav($this->getApplicationURI('board/'. | |||||
| $project->getID(). | |||||
| '/column'), | |||||
| $columns); | |||||
| $nav->appendChild( | |||||
| array( | |||||
| $crumbs, | |||||
| $header, | |||||
| $results_view->render(), | |||||
| )); | |||||
| return $this->buildApplicationPage( | |||||
| $nav, | |||||
| array( | |||||
| 'title' => $title, | |||||
| )); | |||||
| } | |||||
| private function buildSideNav($base_uri, array $columns) { | |||||
| $nav = new AphrontSideNavFilterView(); | |||||
| $nav->setBaseURI(new PhutilURI($base_uri.'/')) | |||||
| ->addLabel('Columns'); | |||||
| foreach ($columns as $column) { | |||||
| $nav->addFilter($column->getID(), $column->getDisplayName()); | |||||
| } | |||||
| $valid_filter = $nav->selectFilter($this->id); | |||||
| return $nav; | |||||
| } | |||||
| // FIXME: pure copy-paste from PhabricatorProjectBoardViewController | |||||
| private function buildSortMenu( | |||||
| PhabricatorUser $viewer, | |||||
| $sort_key) { | |||||
| $sort_icon = id(new PHUIIconView()) | |||||
| ->setIconFont('fa-sort-amount-asc bluegrey'); | |||||
| $named = array( | |||||
| PhabricatorProjectColumn::ORDER_NATURAL => pht('Natural'), | |||||
| PhabricatorProjectColumn::ORDER_PRIORITY => pht('Sort by Priority'), | |||||
| ); | |||||
| $base_uri = $this->getURIWithState(); | |||||
| $items = array(); | |||||
| foreach ($named as $key => $name) { | |||||
| $is_selected = ($key == $sort_key); | |||||
| if ($is_selected) { | |||||
| $active_order = $name; | |||||
| } | |||||
| $item = id(new PhabricatorActionView()) | |||||
| ->setIcon('fa-sort-amount-asc') | |||||
| ->setSelected($is_selected) | |||||
| ->setName($name); | |||||
| $uri = $base_uri->alter('order', $key); | |||||
| $item->setHref($uri); | |||||
| $items[] = $item; | |||||
| } | |||||
| $sort_menu = id(new PhabricatorActionListView()) | |||||
| ->setUser($viewer); | |||||
| foreach ($items as $item) { | |||||
| $sort_menu->addAction($item); | |||||
| } | |||||
| $sort_button = id(new PHUIButtonView()) | |||||
| ->setText(pht('Sort: %s', $active_order)) | |||||
| ->setIcon($sort_icon) | |||||
| ->setTag('a') | |||||
| ->setHref('#') | |||||
| ->addSigil('boards-dropdown-menu') | |||||
| ->setMetadata( | |||||
| array( | |||||
| 'items' => hsprintf('%s', $sort_menu), | |||||
| )); | |||||
| return $sort_button; | |||||
| } | |||||
| private function buildFilterMenu( | |||||
| PhabricatorUser $viewer, | |||||
| $custom_query, | |||||
| PhabricatorApplicationSearchEngine $engine, | |||||
| $query_key) { | |||||
| $filter_icon = id(new PHUIIconView()) | |||||
| ->setIconFont('fa-search-plus bluegrey'); | |||||
| $named = array( | |||||
| 'open' => pht('Open Tasks'), | |||||
| 'all' => pht('All Tasks'), | |||||
| ); | |||||
| if ($viewer->isLoggedIn()) { | |||||
| $named['assigned'] = pht('Assigned to Me'); | |||||
| } | |||||
| if ($custom_query) { | |||||
| $named[$custom_query->getQueryKey()] = pht('Custom Filter'); | |||||
| } | |||||
| $items = array(); | |||||
| foreach ($named as $key => $name) { | |||||
| $is_selected = ($key == $query_key); | |||||
| if ($is_selected) { | |||||
| $active_filter = $name; | |||||
| } | |||||
| $is_custom = false; | |||||
| if ($custom_query) { | |||||
| $is_custom = ($key == $custom_query->getQueryKey()); | |||||
| } | |||||
| $item = id(new PhabricatorActionView()) | |||||
| ->setIcon('fa-search') | |||||
| ->setSelected($is_selected) | |||||
| ->setName($name); | |||||
| if ($is_custom) { | |||||
| $uri = $this->getApplicationURI('board/'.$this->projectID. | |||||
| '/column/'.$this->id.'/filter/query/'.$key.'/'); | |||||
| $item->setWorkflow(true); | |||||
| } else { | |||||
| $uri = $engine->getQueryResultsPageURI($key); | |||||
| } | |||||
| $uri = $this->getURIWithState($uri); | |||||
| $item->setHref($uri); | |||||
| $items[] = $item; | |||||
| } | |||||
| $items[] = id(new PhabricatorActionView()) | |||||
| ->setIcon('fa-cog') | |||||
| ->setHref($this->getApplicationURI('board/'.$this->projectID. | |||||
| '/column/'.$this->id.'/filter/')) // mod | |||||
| ->setWorkflow(true) | |||||
| ->setName(pht('Advanced Filter...')); | |||||
| $filter_menu = id(new PhabricatorActionListView()) | |||||
| ->setUser($viewer); | |||||
| foreach ($items as $item) { | |||||
| $filter_menu->addAction($item); | |||||
| } | |||||
| $filter_button = id(new PHUIButtonView()) | |||||
| ->setText(pht('Filter: %s', $active_filter)) | |||||
| ->setIcon($filter_icon) | |||||
| ->setTag('a') | |||||
| ->setHref('#') | |||||
| ->addSigil('boards-dropdown-menu') | |||||
| ->setMetadata( | |||||
| array( | |||||
| 'items' => hsprintf('%s', $filter_menu), | |||||
| )); | |||||
| return $filter_button; | |||||
| } | |||||
| /** | |||||
| * Add current state parameters (like order and the visibility of hidden | |||||
| * columns) to a URI. | |||||
| * | |||||
| * This allows actions which toggle or adjust one piece of state to keep | |||||
| * the rest of the board state persistent. If no URI is provided, this method | |||||
| * starts with the request URI. | |||||
| * | |||||
| * @param string|null URI to add state parameters to. | |||||
| * @return PhutilURI URI with state parameters. | |||||
| */ | |||||
| private function getURIWithState($base = null) { | |||||
| if ($base === null) { | |||||
| $base = $this->getRequest()->getRequestURI(); | |||||
| } | |||||
| $base = new PhutilURI($base); | |||||
| if ($this->sortKey != PhabricatorProjectColumn::DEFAULT_ORDER) { | |||||
| $base->setQueryParam('order', $this->sortKey); | |||||
| } else { | |||||
| $base->setQueryParam('order', null); | |||||
| } | |||||
| // $base->setQueryParam('hidden', $this->showHidden ? 'true' : null); | |||||
| return $base; | |||||
| } | |||||
| } | |||||