diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -26,6 +26,7 @@ private $closedEpochMin; private $closedEpochMax; private $closerPHIDs; + private $columnPHIDs; private $status = 'status-any'; const STATUS_ANY = 'status-any'; @@ -213,6 +214,11 @@ return $this; } + public function withColumnPHIDs(array $column_phids) { + $this->columnPHIDs = $column_phids; + return $this; + } + public function newResultObject() { return new ManiphestTask(); } @@ -442,6 +448,91 @@ $this->subtypes); } + + if ($this->columnPHIDs !== null) { + $viewer = $this->getViewer(); + + $columns = id(new PhabricatorProjectColumnQuery()) + ->setParentQuery($this) + ->setViewer($viewer) + ->withPHIDs($this->columnPHIDs) + ->execute(); + if (!$columns) { + throw new PhabricatorEmptyQueryException(); + } + + // We must do board layout before we move forward because the column + // positions may not yet exist otherwise. An example is that newly + // created tasks may not yet be positioned in the backlog column. + + $projects = mpull($columns, 'getProject'); + $projects = mpull($projects, null, 'getPHID'); + + // The board layout engine needs to know about every object that it's + // going to be asked to do layout for. For now, we're just doing layout + // on every object on the boards. In the future, we could do layout on a + // smaller set of objects by using the constraints on this Query. For + // example, if the caller is only asking for open tasks, we only need + // to do layout on open tasks. + + // This fetches too many objects (every type of object tagged with the + // project, not just tasks). We could narrow it by querying the edge + // table on the Maniphest side, but there's currently no way to build + // that query with EdgeQuery. + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array_keys($projects)) + ->withEdgeTypes( + array( + PhabricatorProjectProjectHasObjectEdgeType::EDGECONST, + )); + + $edge_query->execute(); + $all_phids = $edge_query->getDestinationPHIDs(); + + // Since we overfetched PHIDs, filter out any non-tasks we got back. + foreach ($all_phids as $key => $phid) { + if (phid_get_type($phid) !== ManiphestTaskPHIDType::TYPECONST) { + unset($all_phids[$key]); + } + } + + // If there are no tasks on the relevant boards, this query can't + // possibly hit anything so we're all done. + $task_phids = array_fuse($all_phids); + if (!$task_phids) { + throw new PhabricatorEmptyQueryException(); + } + + // We know everything we need to know, so perform board layout. + $engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setFetchAllBoards(true) + ->setBoardPHIDs(array_keys($projects)) + ->setObjectPHIDs($task_phids) + ->executeLayout(); + + // Find the tasks that are in the constraint columns after board layout + // completes. + $select_phids = array(); + foreach ($columns as $column) { + $in_column = $engine->getColumnObjectPHIDs( + $column->getProjectPHID(), + $column->getPHID()); + foreach ($in_column as $phid) { + $select_phids[$phid] = $phid; + } + } + + if (!$select_phids) { + throw new PhabricatorEmptyQueryException(); + } + + $where[] = qsprintf( + $conn, + 'task.phid IN (%Ls)', + $select_phids); + } + return $where; } diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php --- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php +++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php @@ -86,6 +86,10 @@ pht('Search for tasks with given subtypes.')) ->setDatasource(new ManiphestTaskSubtypeDatasource()) ->setIsHidden($hide_subtypes), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Columns')) + ->setKey('columnPHIDs') + ->setAliases(array('column', 'columnPHID', 'columns')), id(new PhabricatorSearchThreeStateField()) ->setLabel(pht('Open Parents')) ->setKey('hasParents') @@ -246,6 +250,10 @@ $query->withSubtaskIDs($map['subtaskIDs']); } + if ($map['columnPHIDs']) { + $query->withColumnPHIDs($map['columnPHIDs']); + } + $group = idx($map, 'group'); $group = idx($this->getGroupValues(), $group); if ($group) { diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -36,7 +36,8 @@ if ($request->isFormPost() && !$request->getBool('initialize') - && !$request->getStr('move')) { + && !$request->getStr('move') + && !$request->getStr('queryColumnID')) { $saved = $search_engine->buildSavedQueryFromRequest($request); $search_engine->saveQuery($saved); $filter_form = id(new AphrontFormView()) @@ -188,6 +189,46 @@ ->appendChild($content); } + // If the user wants to turn a particular column into a query, build an + // apropriate filter and redirect them to the query results page. + $query_column_id = $request->getInt('queryColumnID'); + if ($query_column_id) { + $column_id_map = mpull($columns, null, 'getID'); + $query_column = idx($column_id_map, $query_column_id); + if (!$query_column) { + return new Aphront404Response(); + } + + // Create a saved query to combine the active filter on the workboard + // with the column filter. If the user currently has constraints on the + // board, we want to add a new column or project constraint, not + // completely replace the constraints. + $saved_query = clone $saved; + + if ($query_column->getProxyPHID()) { + $project_phids = $saved_query->getParameter('projectPHIDs'); + if (!$project_phids) { + $project_phids = array(); + } + $project_phids[] = $query_column->getProxyPHID(); + $saved_query->setParameter('projectPHIDs', $project_phids); + } else { + $saved_query->setParameter( + 'columnPHIDs', + array($query_column->getPHID())); + } + + $search_engine = id(new ManiphestTaskSearchEngine()) + ->setViewer($viewer); + $search_engine->saveQuery($saved_query); + + $query_key = $saved_query->getQueryKey(); + $query_uri = new PhutilURI("/maniphest/query/{$query_key}/#R"); + + return id(new AphrontRedirectResponse()) + ->setURI($query_uri); + } + $task_can_edit_map = id(new PhabricatorPolicyFilter()) ->setViewer($viewer) ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) @@ -1069,8 +1110,14 @@ ->setHref($batch_move_uri) ->setWorkflow(true); - // Column Related Actions Below - // + $query_uri = $request->getRequestURI(); + $query_uri->setQueryParam('queryColumnID', $column->getID()); + + $column_items[] = id(new PhabricatorActionView()) + ->setName(pht('View as Query')) + ->setIcon('fa-search') + ->setHref($query_uri); + $edit_uri = 'board/'.$this->id.'/edit/'.$column->getID().'/'; $column_items[] = id(new PhabricatorActionView()) ->setName(pht('Edit Column'))