diff --git a/src/applications/maniphest/constants/owner/ManiphestTaskOwner.php b/src/applications/maniphest/constants/owner/ManiphestTaskOwner.php index 7b820a8f74..db8ee391a8 100644 --- a/src/applications/maniphest/constants/owner/ManiphestTaskOwner.php +++ b/src/applications/maniphest/constants/owner/ManiphestTaskOwner.php @@ -1,7 +1,7 @@ view = idx($data, 'view'); } + private function getArrToStrList($key) { + $arr = $this->getRequest()->getArr($key); + $arr = implode(',', $arr); + return nonempty($arr, null); + } + public function processRequest() { $request = $this->getRequest(); @@ -37,18 +43,15 @@ class ManiphestTaskListController extends ManiphestController { if ($request->isFormPost()) { // Redirect to GET so URIs can be copy/pasted. - $user_phids = $request->getArr('set_users'); - $proj_phids = $request->getArr('set_projects'); $task_ids = $request->getStr('set_tasks'); - $user_phids = implode(',', $user_phids); - $proj_phids = implode(',', $proj_phids); - $user_phids = nonempty($user_phids, null); - $proj_phids = nonempty($proj_phids, null); $task_ids = nonempty($task_ids, null); $uri = $request->getRequestURI() - ->alter('users', $user_phids) - ->alter('projects', $proj_phids) + ->alter('users', $this->getArrToStrList('set_users')) + ->alter('projects', $this->getArrToStrList('set_projects')) + ->alter('xprojects', $this->getArrToStrList('set_xprojects')) + ->alter('owners', $this->getArrToStrList('set_owners')) + ->alter('authors', $this->getArrToStrList('set_authors')) ->alter('tasks', $task_ids); return id(new AphrontRedirectResponse())->setURI($uri); @@ -66,7 +69,8 @@ class ManiphestTaskListController extends ManiphestController { $nav->addFilter('alltriage', 'Need Triage'); $nav->addFilter('all', 'All Tasks'); $nav->addSpacer(); - $nav->addFilter('custom', 'Custom'); + $nav->addLabel('Custom'); + $nav->addFilter('custom', 'Custom Query'); $this->view = $nav->selectFilter($this->view, 'action'); @@ -81,21 +85,12 @@ class ManiphestTaskListController extends ManiphestController { list($grouping, $group_control) = $this->renderGroupLinks(); list($order, $order_control) = $this->renderOrderLinks(); - $user_phids = $request->getStr('users'); - if (strlen($user_phids)) { - $user_phids = explode(',', $user_phids); - } else { - $user_phids = array($user->getPHID()); - } - - $project_phids = $request->getStr('projects'); - if (strlen($project_phids)) { - $project_phids = explode(',', $project_phids); - } else { - $project_phids = array(); - } - + $user_phids = $request->getStrList('users'); + $project_phids = $request->getStrList('projects'); + $exclude_project_phids = $request->getStrList('xprojects'); $task_ids = $request->getStrList('tasks'); + $owner_phids = $request->getStrList('owners'); + $author_phids = $request->getStrList('authors'); $page = $request->getInt('page'); $page_size = self::DEFAULT_PAGE_SIZE; @@ -104,15 +99,18 @@ class ManiphestTaskListController extends ManiphestController { $query->setQuery('<>'); $query->setParameters( array( - 'view' => $this->view, - 'userPHIDs' => $user_phids, - 'projectPHIDs' => $project_phids, - 'taskIDs' => $task_ids, - 'group' => $grouping, - 'order' => $order, - 'offset' => $page, - 'limit' => $page_size, - 'status' => $status_map, + 'view' => $this->view, + 'userPHIDs' => $user_phids, + 'projectPHIDs' => $project_phids, + 'excludeProjectPHIDs' => $exclude_project_phids, + 'ownerPHIDs' => $owner_phids, + 'authorPHIDs' => $author_phids, + 'taskIDs' => $task_ids, + 'group' => $grouping, + 'order' => $order, + 'offset' => $page, + 'limit' => $page_size, + 'status' => $status_map, )); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); @@ -145,6 +143,28 @@ class ManiphestTaskListController extends ManiphestController { ->setLabel('Task IDs') ->setValue(join(',', $task_ids)) ); + + $tokens = array(); + foreach ($owner_phids as $phid) { + $tokens[$phid] = $handles[$phid]->getFullName(); + } + $form->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/searchowner/') + ->setName('set_owners') + ->setLabel('Owners') + ->setValue($tokens)); + + $tokens = array(); + foreach ($author_phids as $phid) { + $tokens[$phid] = $handles[$phid]->getFullName(); + } + $form->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/users/') + ->setName('set_authors') + ->setLabel('Authors') + ->setValue($tokens)); } $tokens = array(); @@ -153,11 +173,24 @@ class ManiphestTaskListController extends ManiphestController { } $form->appendChild( id(new AphrontFormTokenizerControl()) - ->setDatasource('/typeahead/common/projects/') + ->setDatasource('/typeahead/common/searchproject/') ->setName('set_projects') ->setLabel('Projects') ->setValue($tokens)); + if ($this->view == 'custom') { + $tokens = array(); + foreach ($exclude_project_phids as $phid) { + $tokens[$phid] = $handles[$phid]->getFullName(); + } + $form->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/projects/') + ->setName('set_xprojects') + ->setLabel('Exclude Projects') + ->setValue($tokens)); + } + $form ->appendChild($status_control) ->appendChild($group_control) @@ -266,11 +299,28 @@ class ManiphestTaskListController extends ManiphestController { $user_phids = $search_query->getParameter('userPHIDs', array()); $project_phids = $search_query->getParameter('projectPHIDs', array()); $task_ids = $search_query->getParameter('taskIDs', array()); + $xproject_phids = $search_query->getParameter( + 'excludeProjectPHIDs', + array()); + $owner_phids = $search_query->getParameter('ownerPHIDs', array()); + $author_phids = $search_query->getParameter('authorPHIDs', array()); $query = new ManiphestTaskQuery(); $query->withProjects($project_phids); $query->withTaskIDs($task_ids); + if ($xproject_phids) { + $query->withoutProjects($xproject_phids); + } + + if ($owner_phids) { + $query->withOwners($owner_phids); + } + + if ($author_phids) { + $query->withAuthors($author_phids); + } + $status = $search_query->getParameter('status', 'all'); if (!empty($status['open']) && !empty($status['closed'])) { $query->withStatus(ManiphestTaskQuery::STATUS_ANY); @@ -330,7 +380,13 @@ class ManiphestTaskListController extends ManiphestController { $total_row_count = $query->getRowCount(); $handle_phids = mpull($data, 'getOwnerPHID'); - $handle_phids = array_merge($handle_phids, $project_phids, $user_phids); + $handle_phids = array_merge( + $handle_phids, + $project_phids, + $user_phids, + $xproject_phids, + $owner_phids, + $author_phids); $handles = id(new PhabricatorObjectHandleData($handle_phids)) ->loadHandles(); diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index 4e4463eb0b..861a84b194 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -1,7 +1,7 @@ includeNoProject = false; + foreach ($projects as $k => $phid) { + if ($phid == ManiphestTaskOwner::PROJECT_NO_PROJECT) { + $this->includeNoProject = true; + unset($projects[$k]); + } + } $this->projectPHIDs = $projects; return $this; } + public function withoutProjects(array $projects) { + $this->xprojectPHIDs = $projects; + return $this; + } + public function withStatus($status) { $this->status = $status; return $this; @@ -160,6 +174,7 @@ final class ManiphestTaskQuery { $where[] = $this->buildOwnerWhereClause($conn); $where[] = $this->buildSubscriberWhereClause($conn); $where[] = $this->buildProjectWhereClause($conn); + $where[] = $this->buildXProjectWhereClause($conn); $where = array_filter($where); if ($where) { @@ -170,6 +185,7 @@ final class ManiphestTaskQuery { $join = array(); $join[] = $this->buildProjectJoinClause($conn); + $join[] = $this->buildXProjectJoinClause($conn); $join[] = $this->buildSubscriberJoinClause($conn); $join = array_filter($join); @@ -195,7 +211,7 @@ final class ManiphestTaskQuery { $group = 'GROUP BY task.id'; if (!$this->anyProject) { - $count = ', COUNT(1) projectCount'; + $count = ', COUNT(project.projectPHID) projectCount'; $having = qsprintf( $conn, 'HAVING projectCount = %d', @@ -320,28 +336,63 @@ final class ManiphestTaskQuery { } private function buildProjectWhereClause($conn) { - if (!$this->projectPHIDs) { + if (!$this->projectPHIDs && !$this->includeNoProject) { return null; } - return qsprintf( - $conn, - 'project.projectPHID IN (%Ls)', - $this->projectPHIDs); + $parts = array(); + if ($this->projectPHIDs) { + $parts[] = qsprintf( + $conn, + 'project.projectPHID in (%Ls)', + $this->projectPHIDs); + } + if ($this->includeNoProject) { + $parts[] = qsprintf( + $conn, + 'project.projectPHID IS NULL'); + } + + return '('.implode(') OR (', $parts).')'; } private function buildProjectJoinClause($conn) { - if (!$this->projectPHIDs) { + if (!$this->projectPHIDs && !$this->includeNoProject) { return null; } $project_dao = new ManiphestTaskProject(); return qsprintf( $conn, - 'JOIN %T project ON project.taskPHID = task.phid', + '%Q JOIN %T project ON project.taskPHID = task.phid', + ($this->includeNoProject ? 'LEFT' : ''), $project_dao->getTableName()); } + private function buildXProjectWhereClause($conn) { + if (!$this->xprojectPHIDs) { + return null; + } + + return qsprintf( + $conn, + 'xproject.projectPHID IS NULL'); + } + + private function buildXProjectJoinClause($conn) { + if (!$this->xprojectPHIDs) { + return null; + } + + $project_dao = new ManiphestTaskProject(); + return qsprintf( + $conn, + 'LEFT JOIN %T xproject ON xproject.taskPHID = task.phid + AND xproject.projectPHID IN (%Ls)', + $project_dao->getTableName(), + $this->xprojectPHIDs); + } + private function buildSubscriberJoinClause($conn) { if (!$this->subscriberPHIDs) { return null; diff --git a/src/applications/phid/handle/data/PhabricatorObjectHandleData.php b/src/applications/phid/handle/data/PhabricatorObjectHandleData.php index a807ffa24f..2e757ff4d5 100644 --- a/src/applications/phid/handle/data/PhabricatorObjectHandleData.php +++ b/src/applications/phid/handle/data/PhabricatorObjectHandleData.php @@ -117,6 +117,11 @@ class PhabricatorObjectHandleData { $handle->setFullName('upforgrabs (Up For Grabs)'); $handle->setComplete(true); break; + case ManiphestTaskOwner::PROJECT_NO_PROJECT: + $handle->setName('No Project'); + $handle->setFullName('noproject (No Project)'); + $handle->setComplete(true); + break; default: $handle->setName('Foul Magicks'); break; diff --git a/src/applications/typeahead/controller/common/PhabricatorTypeaheadCommonDatasourceController.php b/src/applications/typeahead/controller/common/PhabricatorTypeaheadCommonDatasourceController.php index dcc03a10ad..86c67d5299 100644 --- a/src/applications/typeahead/controller/common/PhabricatorTypeaheadCommonDatasourceController.php +++ b/src/applications/typeahead/controller/common/PhabricatorTypeaheadCommonDatasourceController.php @@ -1,7 +1,7 @@ type) { case 'searchowner': $need_users = true; $need_upforgrabs = true; break; + case 'searchproject': + $need_projs = true; + $need_noproject = true; + break; case 'users': $need_users = true; break; @@ -77,6 +82,14 @@ class PhabricatorTypeaheadCommonDatasourceController ); } + if ($need_noproject) { + $data[] = array( + 'noproject (No Project)', + null, + ManiphestTaskOwner::PROJECT_NO_PROJECT, + ); + } + if ($need_users) { $columns = array( 'isSystemAgent',