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,26 +1,27 @@ 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(); $user = $request->getUser(); 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); } $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI('/maniphest/view/')); $nav->addLabel('User Tasks'); $nav->addFilter('action', 'Assigned'); $nav->addFilter('created', 'Created'); $nav->addFilter('subscribed', 'Subscribed'); $nav->addFilter('triage', 'Need Triage'); $nav->addSpacer(); $nav->addLabel('All Tasks'); $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'); $has_filter = array( 'action' => true, 'created' => true, 'subscribed' => true, 'triage' => true, ); list($status_map, $status_control) = $this->renderStatusLinks(); 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; $query = new PhabricatorSearchQuery(); $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(); $query->save(); unset($unguarded); list($tasks, $handles, $total_count) = self::loadTasks($query); $form = id(new AphrontFormView()) ->setUser($user) ->setAction($request->getRequestURI()); if (isset($has_filter[$this->view])) { $tokens = array(); foreach ($user_phids as $phid) { $tokens[$phid] = $handles[$phid]->getFullName(); } $form->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/searchowner/') ->setName('set_users') ->setLabel('Users') ->setValue($tokens)); } if ($this->view == 'custom') { $form->appendChild( id(new AphrontFormTextControl()) ->setName('set_tasks') ->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(); foreach ($project_phids as $phid) { $tokens[$phid] = $handles[$phid]->getFullName(); } $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) ->appendChild($order_control); $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Filter Tasks')); $create_uri = new PhutilURI('/maniphest/task/create/'); if ($project_phids) { // If we have project filters selected, use them as defaults for task // creation. $create_uri->setQueryParam('projects', implode(';', $project_phids)); } $filter = new AphrontListFilterView(); $filter->addButton( phutil_render_tag( 'a', array( 'href' => (string)$create_uri, 'class' => 'green button', ), 'Create New Task')); $filter->appendChild($form); $nav->appendChild($filter); $have_tasks = false; foreach ($tasks as $group => $list) { if (count($list)) { $have_tasks = true; break; } } require_celerity_resource('maniphest-task-summary-css'); if (!$have_tasks) { $nav->appendChild( '

'. 'No matching tasks.'. '

'); } else { $pager = new AphrontPagerView(); $pager->setURI($request->getRequestURI(), 'page'); $pager->setPageSize($page_size); $pager->setOffset($page); $pager->setCount($total_count); $cur = ($pager->getOffset() + 1); $max = min($pager->getOffset() + $page_size, $total_count); $tot = $total_count; $cur = number_format($cur); $max = number_format($max); $tot = number_format($tot); $nav->appendChild( '
'. "Displaying tasks {$cur} - {$max} of {$tot}.". '
'); $selector = new AphrontNullView(); foreach ($tasks as $group => $list) { $task_list = new ManiphestTaskListView(); $task_list->setShowBatchControls(true); $task_list->setUser($user); $task_list->setTasks($list); $task_list->setHandles($handles); $count = number_format(count($list)); $selector->appendChild( '

'. phutil_escape_html($group).' ('.$count.')'. '

'); $selector->appendChild($task_list); } $selector->appendChild($this->renderBatchEditor($query)); $selector = phabricator_render_form( $user, array( 'method' => 'POST', 'action' => '/maniphest/batch/', ), $selector->render()); $nav->appendChild($selector); $nav->appendChild($pager); } return $this->buildStandardPageResponse( $nav, array( 'title' => 'Task List', )); } public static function loadTasks(PhabricatorSearchQuery $search_query) { $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); } else if (!empty($status['open'])) { $query->withStatus(ManiphestTaskQuery::STATUS_OPEN); } else { $query->withStatus(ManiphestTaskQuery::STATUS_CLOSED); } switch ($search_query->getParameter('view')) { case 'action': $query->withOwners($user_phids); break; case 'created': $query->withAuthors($user_phids); break; case 'subscribed': $query->withSubscribers($user_phids); break; case 'triage': $query->withOwners($user_phids); $query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE); break; case 'alltriage': $query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE); break; case 'all': break; } $order_map = array( 'priority' => ManiphestTaskQuery::ORDER_PRIORITY, 'created' => ManiphestTaskQuery::ORDER_CREATED, ); $query->setOrderBy( idx( $order_map, $search_query->getParameter('order'), ManiphestTaskQuery::ORDER_MODIFIED)); $group_map = array( 'priority' => ManiphestTaskQuery::GROUP_PRIORITY, 'owner' => ManiphestTaskQuery::GROUP_OWNER, 'status' => ManiphestTaskQuery::GROUP_STATUS, ); $query->setGroupBy( idx( $group_map, $search_query->getParameter('group'), ManiphestTaskQuery::GROUP_NONE)); $query->setCalculateRows(true); $query->setLimit($search_query->getParameter('limit')); $query->setOffset($search_query->getParameter('offset')); $data = $query->execute(); $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(); switch ($search_query->getParameter('group')) { case 'priority': $data = mgroup($data, 'getPriority'); krsort($data); // If we have invalid priorities, they'll all map to "???". Merge // arrays to prevent them from overwriting each other. $out = array(); foreach ($data as $pri => $tasks) { $out[ManiphestTaskPriority::getTaskPriorityName($pri)][] = $tasks; } foreach ($out as $pri => $tasks) { $out[$pri] = array_mergev($tasks); } $data = $out; break; case 'status': $data = mgroup($data, 'getStatus'); ksort($data); $out = array(); foreach ($data as $status => $tasks) { $out[ManiphestTaskStatus::getTaskStatusFullName($status)] = $tasks; } $data = $out; break; case 'owner': $data = mgroup($data, 'getOwnerPHID'); $out = array(); foreach ($data as $phid => $tasks) { if ($phid) { $out[$handles[$phid]->getFullName()] = $tasks; } else { $out['Unassigned'] = $tasks; } } if (isset($out['Unassigned'])) { // If any tasks are unassigned, move them to the front of the list. $data = array('Unassigned' => $out['Unassigned']) + $out; } else { $data = $out; } ksort($data); break; default: $data = array( 'Tasks' => $data, ); break; } return array($data, $handles, $total_row_count); } public function renderStatusLinks() { $request = $this->getRequest(); $statuses = array( 'o' => array('open' => true), 'c' => array('closed' => true), 'oc' => array('open' => true, 'closed' => true), ); $status = $request->getStr('s'); if (empty($statuses[$status])) { $status = 'o'; } $status_control = id(new AphrontFormToggleButtonsControl()) ->setLabel('Status') ->setValue($status) ->setBaseURI($request->getRequestURI(), 's') ->setButtons( array( 'o' => 'Open', 'c' => 'Closed', 'oc' => 'All', )); return array($statuses[$status], $status_control); } public function renderOrderLinks() { $request = $this->getRequest(); $order = $request->getStr('o'); $orders = array( 'u' => 'updated', 'c' => 'created', 'p' => 'priority', ); if (empty($orders[$order])) { $order = 'p'; } $order_by = $orders[$order]; $order_control = id(new AphrontFormToggleButtonsControl()) ->setLabel('Order') ->setValue($order) ->setBaseURI($request->getRequestURI(), 'o') ->setButtons( array( 'p' => 'Priority', 'u' => 'Updated', 'c' => 'Created', )); return array($order_by, $order_control); } public function renderGroupLinks() { $request = $this->getRequest(); $group = $request->getStr('g'); $groups = array( 'n' => 'none', 'p' => 'priority', 's' => 'status', 'o' => 'owner', ); if (empty($groups[$group])) { $group = 'p'; } $group_by = $groups[$group]; $group_control = id(new AphrontFormToggleButtonsControl()) ->setLabel('Group') ->setValue($group) ->setBaseURI($request->getRequestURI(), 'g') ->setButtons( array( 'p' => 'Priority', 'o' => 'Owner', 's' => 'Status', 'n' => 'None', )); return array($group_by, $group_control); } private function renderBatchEditor(PhabricatorSearchQuery $search_query) { Javelin::initBehavior( 'maniphest-batch-selector', array( 'selectAll' => 'batch-select-all', 'selectNone' => 'batch-select-none', 'submit' => 'batch-select-submit', 'status' => 'batch-select-status-cell', )); $select_all = javelin_render_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'class' => 'grey button', 'id' => 'batch-select-all', ), 'Select All'); $select_none = javelin_render_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'class' => 'grey button', 'id' => 'batch-select-none', ), 'Clear Selection'); $submit = phutil_render_tag( 'button', array( 'id' => 'batch-select-submit', 'disabled' => 'disabled', 'class' => 'disabled', ), 'Batch Edit Selected Tasks »'); $export = javelin_render_tag( 'a', array( 'href' => '/maniphest/export/'.$search_query->getQueryKey().'/', 'class' => 'grey button', ), 'Export Tasks to Excel...'); return '
'. '
Batch Task Editor
'. ''. ''. ''. ''. ''. ''. ''. '
'. $select_all. $select_none. ''. $export. ''. '0 Selected Tasks'. ''.$submit.'
'. ''; } } 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,412 +1,463 @@ authorPHIDs = $authors; return $this; } public function withTaskIDs(array $ids) { $this->taskIDs = $ids; return $this; } public function withOwners(array $owners) { $this->includeUnowned = false; foreach ($owners as $k => $phid) { if ($phid == ManiphestTaskOwner::OWNER_UP_FOR_GRABS) { $this->includeUnowned = true; unset($owners[$k]); break; } } $this->ownerPHIDs = $owners; return $this; } public function withProjects(array $projects) { + $this->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; } public function withPriority($priority) { $this->priority = $priority; return $this; } public function withSubscribers(array $subscribers) { $this->subscriberPHIDs = $subscribers; return $this; } public function setGroupBy($group) { $this->groupBy = $group; return $this; } public function setOrderBy($order) { $this->orderBy = $order; return $this; } public function setLimit($limit) { $this->limit = $limit; return $this; } public function setOffset($offset) { $this->offset = $offset; return $this; } public function setCalculateRows($calculate_rows) { $this->calculateRows = $calculate_rows; return $this; } public function getRowCount() { if ($this->rowCount === null) { throw new Exception( "You must execute a query with setCalculateRows() before you can ". "retrieve a row count."); } return $this->rowCount; } public function withAnyProject($any_project) { $this->anyProject = $any_project; return $this; } public function execute() { $task_dao = new ManiphestTask(); $conn = $task_dao->establishConnection('r'); if ($this->calculateRows) { $calc = 'SQL_CALC_FOUND_ROWS'; } else { $calc = ''; } $where = array(); $where[] = $this->buildTaskIDsWhereClause($conn); $where[] = $this->buildStatusWhereClause($conn); $where[] = $this->buildPriorityWhereClause($conn); $where[] = $this->buildAuthorWhereClause($conn); $where[] = $this->buildOwnerWhereClause($conn); $where[] = $this->buildSubscriberWhereClause($conn); $where[] = $this->buildProjectWhereClause($conn); + $where[] = $this->buildXProjectWhereClause($conn); $where = array_filter($where); if ($where) { $where = 'WHERE ('.implode(') AND (', $where).')'; } else { $where = ''; } $join = array(); $join[] = $this->buildProjectJoinClause($conn); + $join[] = $this->buildXProjectJoinClause($conn); $join[] = $this->buildSubscriberJoinClause($conn); $join = array_filter($join); if ($join) { $join = implode(' ', $join); } else { $join = ''; } $having = ''; $count = ''; $group = ''; if (count($this->projectPHIDs) > 1) { // If we're searching for more than one project: // - We'll get multiple rows for tasks when they join the project table // multiple times. We use GROUP BY to make them distinct again. // - We want to treat the query as an intersection query, not a union // query. We sum the project count and require it be the same as the // number of projects we're searching for. (If 'anyProject' is set, // we do union instead.) $group = 'GROUP BY task.id'; if (!$this->anyProject) { - $count = ', COUNT(1) projectCount'; + $count = ', COUNT(project.projectPHID) projectCount'; $having = qsprintf( $conn, 'HAVING projectCount = %d', count($this->projectPHIDs)); } } $order = $this->buildOrderClause($conn); $offset = (int)nonempty($this->offset, 0); $limit = (int)nonempty($this->limit, self::DEFAULT_PAGE_SIZE); $data = queryfx_all( $conn, 'SELECT %Q * %Q FROM %T task %Q %Q %Q %Q %Q LIMIT %d, %d', $calc, $count, $task_dao->getTableName(), $join, $where, $group, $having, $order, $offset, $limit); if ($this->calculateRows) { $count = queryfx_one( $conn, 'SELECT FOUND_ROWS() N'); $this->rowCount = $count['N']; } else { $this->rowCount = null; } return $task_dao->loadAllFromArray($data); } private function buildTaskIDsWhereClause($conn) { if (!$this->taskIDs) { return null; } return qsprintf( $conn, 'id in (%Ld)', $this->taskIDs); } private function buildStatusWhereClause($conn) { switch ($this->status) { case self::STATUS_ANY: return null; case self::STATUS_OPEN: return 'status = 0'; case self::STATUS_CLOSED: return 'status > 0'; default: throw new Exception("Unknown status query '{$this->status}'!"); } } private function buildPriorityWhereClause($conn) { if ($this->priority === null) { return null; } return qsprintf( $conn, 'priority = %d', $this->priority); } private function buildAuthorWhereClause($conn) { if (!$this->authorPHIDs) { return null; } return qsprintf( $conn, 'authorPHID in (%Ls)', $this->authorPHIDs); } private function buildOwnerWhereClause($conn) { if (!$this->ownerPHIDs) { if ($this->includeUnowned === null) { return null; } else if ($this->includeUnowned) { return qsprintf( $conn, 'ownerPHID IS NULL'); } else { return qsprintf( $conn, 'ownerPHID IS NOT NULL'); } } if ($this->includeUnowned) { return qsprintf( $conn, 'ownerPHID IN (%Ls) OR ownerPHID IS NULL', $this->ownerPHIDs); } else { return qsprintf( $conn, 'ownerPHID IN (%Ls)', $this->ownerPHIDs); } } private function buildSubscriberWhereClause($conn) { if (!$this->subscriberPHIDs) { return null; } return qsprintf( $conn, 'subscriber.subscriberPHID IN (%Ls)', $this->subscriberPHIDs); } 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; } $subscriber_dao = new ManiphestTaskSubscriber(); return qsprintf( $conn, 'JOIN %T subscriber ON subscriber.taskPHID = task.phid', $subscriber_dao->getTableName()); } private function buildOrderClause($conn) { $order = array(); switch ($this->groupBy) { case self::GROUP_NONE: break; case self::GROUP_PRIORITY: $order[] = 'priority'; break; case self::GROUP_OWNER: $order[] = 'ownerOrdering'; break; case self::GROUP_STATUS: $order[] = 'status'; break; default: throw new Exception("Unknown group query '{$this->groupBy}'!"); } switch ($this->orderBy) { case self::ORDER_PRIORITY: $order[] = 'priority'; $order[] = 'dateModified'; break; case self::ORDER_CREATED: $order[] = 'id'; break; case self::ORDER_MODIFIED: $order[] = 'dateModified'; break; default: throw new Exception("Unknown order query '{$this->orderBy}'!"); } $order = array_unique($order); if (empty($order)) { return null; } foreach ($order as $k => $column) { switch ($column) { case 'ownerOrdering': $order[$k] = "task.{$column} ASC"; break; default: $order[$k] = "task.{$column} DESC"; break; } } return 'ORDER BY '.implode(', ', $order); } } 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 @@ -1,493 +1,498 @@ phids = array_unique($phids); } public function loadObjects() { $types = array(); foreach ($this->phids as $phid) { $type = $this->lookupType($phid); $types[$type][] = $phid; } $objects = array_fill_keys($this->phids, null); foreach ($types as $type => $phids) { switch ($type) { case PhabricatorPHIDConstants::PHID_TYPE_USER: $user_dao = newv('PhabricatorUser', array()); $users = $user_dao->loadAllWhere( 'phid in (%Ls)', $phids); foreach ($users as $user) { $objects[$user->getPHID()] = $user; } break; case PhabricatorPHIDConstants::PHID_TYPE_CMIT: $commit_dao = newv('PhabricatorRepositoryCommit', array()); $commits = $commit_dao->loadAllWhere( 'phid IN (%Ls)', $phids); $commit_data = array(); if ($commits) { $data_dao = newv('PhabricatorRepositoryCommitData', array()); $commit_data = $data_dao->loadAllWhere( 'commitID IN (%Ld)', mpull($commits, 'getID')); $commit_data = mpull($commit_data, null, 'getCommitID'); } foreach ($commits as $commit) { $data = idx($commit_data, $commit->getID()); if ($data) { $commit->attachCommitData($data); $objects[$commit->getPHID()] = $commit; } else { // If we couldn't load the commit data, just act as though we // couldn't load the object at all so we don't load half an object. } } break; case PhabricatorPHIDConstants::PHID_TYPE_TASK: $task_dao = newv('ManiphestTask', array()); $tasks = $task_dao->loadAllWhere( 'phid IN (%Ls)', $phids); foreach ($tasks as $task) { $objects[$task->getPHID()] = $task; } break; case PhabricatorPHIDConstants::PHID_TYPE_DREV: $revision_dao = newv('DifferentialRevision', array()); $revisions = $revision_dao->loadAllWhere( 'phid IN (%Ls)', $phids); foreach ($revisions as $revision) { $objects[$revision->getPHID()] = $revision; } break; } } return $objects; } public function loadHandles() { $types = array(); foreach ($this->phids as $phid) { $type = $this->lookupType($phid); $types[$type][] = $phid; } $handles = array(); $external_loaders = PhabricatorEnv::getEnvConfig('phid.external-loaders'); foreach ($types as $type => $phids) { switch ($type) { case PhabricatorPHIDConstants::PHID_TYPE_MAGIC: // Black magic! foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); switch ($phid) { case ManiphestTaskOwner::OWNER_UP_FOR_GRABS: $handle->setName('Up For Grabs'); $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; } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_USER: $class = 'PhabricatorUser'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $users = $object->loadAllWhere('phid IN (%Ls)', $phids); $users = mpull($users, null, 'getPHID'); $image_phids = mpull($users, 'getProfileImagePHID'); $image_phids = array_unique(array_filter($image_phids)); $images = array(); if ($image_phids) { $images = id(new PhabricatorFile())->loadAllWhere( 'phid IN (%Ls)', $image_phids); $images = mpull($images, 'getBestURI', 'getPHID'); } foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($users[$phid])) { $handle->setName('Unknown User'); } else { $user = $users[$phid]; $handle->setName($user->getUsername()); $handle->setURI('/p/'.$user->getUsername().'/'); $handle->setEmail($user->getEmail()); $handle->setFullName( $user->getUsername().' ('.$user->getRealName().')'); $handle->setAlternateID($user->getID()); $handle->setComplete(true); $handle->setDisabled($user->getIsDisabled()); $img_uri = idx($images, $user->getProfileImagePHID()); if ($img_uri) { $handle->setImageURI($img_uri); } } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_MLST: $class = 'PhabricatorMetaMTAMailingList'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $lists = $object->loadAllWhere('phid IN (%Ls)', $phids); $lists = mpull($lists, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($lists[$phid])) { $handle->setName('Unknown Mailing List'); } else { $list = $lists[$phid]; $handle->setEmail($list->getEmail()); $handle->setName($list->getName()); $handle->setURI($list->getURI()); $handle->setFullName($list->getName()); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_DREV: $class = 'DifferentialRevision'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $revs = $object->loadAllWhere('phid in (%Ls)', $phids); $revs = mpull($revs, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($revs[$phid])) { $handle->setName('Unknown Revision'); } else { $rev = $revs[$phid]; $handle->setName($rev->getTitle()); $handle->setURI('/D'.$rev->getID()); $handle->setFullName('D'.$rev->getID().': '.$rev->getTitle()); $handle->setComplete(true); $status = $rev->getStatus(); if (($status == ArcanistDifferentialRevisionStatus::COMMITTED) || ($status == ArcanistDifferentialRevisionStatus::ABANDONED)) { $closed = PhabricatorObjectHandleStatus::STATUS_CLOSED; $handle->setStatus($closed); } } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_CMIT: $class = 'PhabricatorRepositoryCommit'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $commits = $object->loadAllWhere('phid in (%Ls)', $phids); $commits = mpull($commits, null, 'getPHID'); $repository_ids = array(); $callsigns = array(); if ($commits) { $repository_ids = mpull($commits, 'getRepositoryID'); $repositories = id(new PhabricatorRepository())->loadAllWhere( 'id in (%Ld)', array_unique($repository_ids)); $callsigns = mpull($repositories, 'getCallsign'); } foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($commits[$phid]) || !isset($callsigns[$repository_ids[$phid]])) { $handle->setName('Unknown Commit'); } else { $commit = $commits[$phid]; $callsign = $callsigns[$repository_ids[$phid]]; $repository = $repositories[$repository_ids[$phid]]; $commit_identifier = $commit->getCommitIdentifier(); // In case where the repository for the commit was deleted, // we don't have have info about the repository anymore. if ($repository) { $vcs = $repository->getVersionControlSystem(); if ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) { $short_identifier = substr($commit_identifier, 0, 16); } else { $short_identifier = $commit_identifier; } $handle->setName('r'.$callsign.$short_identifier); } else { $handle->setName('Commit '.'r'.$callsign.$commit_identifier); } $handle->setURI('/r'.$callsign.$commit_identifier); $handle->setFullName('r'.$callsign.$commit_identifier); $handle->setTimestamp($commit->getEpoch()); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_TASK: $class = 'ManiphestTask'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $tasks = $object->loadAllWhere('phid in (%Ls)', $phids); $tasks = mpull($tasks, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($tasks[$phid])) { $handle->setName('Unknown Revision'); } else { $task = $tasks[$phid]; $handle->setName($task->getTitle()); $handle->setURI('/T'.$task->getID()); $handle->setFullName('T'.$task->getID().': '.$task->getTitle()); $handle->setComplete(true); $handle->setAlternateID($task->getID()); if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) { $closed = PhabricatorObjectHandleStatus::STATUS_CLOSED; $handle->setStatus($closed); } } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_FILE: $class = 'PhabricatorFile'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $files = $object->loadAllWhere('phid IN (%Ls)', $phids); $files = mpull($files, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($files[$phid])) { $handle->setName('Unknown File'); } else { $file = $files[$phid]; $handle->setName($file->getName()); $handle->setURI($file->getBestURI()); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_PROJ: $class = 'PhabricatorProject'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $projects = $object->loadAllWhere('phid IN (%Ls)', $phids); $projects = mpull($projects, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($projects[$phid])) { $handle->setName('Unknown Project'); } else { $project = $projects[$phid]; $handle->setName($project->getName()); $handle->setURI('/project/view/'.$project->getID().'/'); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_REPO: $class = 'PhabricatorRepository'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $repositories = $object->loadAllWhere('phid in (%Ls)', $phids); $repositories = mpull($repositories, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($repositories[$phid])) { $handle->setName('Unknown Repository'); } else { $repository = $repositories[$phid]; $handle->setName($repository->getCallsign()); $handle->setURI('/diffusion/'.$repository->getCallsign().'/'); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_OPKG: $class = 'PhabricatorOwnersPackage'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $packages = $object->loadAllWhere('phid in (%Ls)', $phids); $packages = mpull($packages, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($packages[$phid])) { $handle->setName('Unknown Package'); } else { $package = $packages[$phid]; $handle->setName($package->getName()); $handle->setURI('/owners/package/'.$package->getID().'/'); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_APRJ: $project_dao = newv('PhabricatorRepositoryArcanistProject', array()); $projects = $project_dao->loadAllWhere( 'phid IN (%Ls)', $phids); $projects = mpull($projects, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($projects[$phid])) { $handle->setName('Unknown Arcanist Project'); } else { $project = $projects[$phid]; $handle->setName($project->getName()); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_WIKI: $document_dao = newv('PhrictionDocument', array()); $content_dao = newv('PhrictionContent', array()); $conn = $document_dao->establishConnection('r'); $documents = queryfx_all( $conn, 'SELECT * FROM %T document JOIN %T content ON document.contentID = content.id WHERE document.phid IN (%Ls)', $document_dao->getTableName(), $content_dao->getTableName(), $phids); $documents = ipull($documents, null, 'phid'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($documents[$phid])) { $handle->setName('Unknown Document'); } else { $info = $documents[$phid]; $handle->setName($info['title']); $handle->setURI(PhrictionDocument::getSlugURI($info['slug'])); $handle->setComplete(true); } $handles[$phid] = $handle; } break; default: $loader = null; if (isset($external_loaders[$type])) { $loader = $external_loaders[$type]; } else if (isset($external_loaders['*'])) { $loader = $external_loaders['*']; } if ($loader) { PhutilSymbolLoader::loadClass($loader); $object = newv($loader, array()); $handles += $object->loadHandles($phids); break; } foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setType($type); $handle->setPHID($phid); $handle->setName('Unknown Object'); $handle->setFullName('An Unknown Object'); $handles[$phid] = $handle; } break; } } return $handles; } private function lookupType($phid) { $matches = null; if (preg_match('/^PHID-([^-]{4})-/', $phid, $matches)) { return $matches[1]; } return PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN; } } 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,182 +1,195 @@ type = $data['type']; } public function processRequest() { $request = $this->getRequest(); $query = $request->getStr('q'); $need_users = false; $need_all_users = false; $need_lists = false; $need_projs = false; $need_repos = false; $need_packages = false; $need_upforgrabs = false; $need_arcanist_projects = false; + $need_noproject = false; switch ($this->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; case 'mailable': $need_users = true; $need_lists = true; break; case 'projects': $need_projs = true; break; case 'repositories': $need_repos = true; break; case 'packages': $need_packages = true; break; case 'accounts': $need_users = true; $need_all_users = true; break; case 'arcanistprojects': $need_arcanist_projects = true; break; } $data = array(); if ($need_upforgrabs) { $data[] = array( 'upforgrabs (Up For Grabs)', null, ManiphestTaskOwner::OWNER_UP_FOR_GRABS, ); } + if ($need_noproject) { + $data[] = array( + 'noproject (No Project)', + null, + ManiphestTaskOwner::PROJECT_NO_PROJECT, + ); + } + if ($need_users) { $columns = array( 'isSystemAgent', 'isDisabled', 'userName', 'realName', 'phid'); if ($query) { $conn_r = id(new PhabricatorUser())->establishConnection('r'); $ids = queryfx_all( $conn_r, 'SELECT DISTINCT userID FROM %T WHERE token LIKE %>', PhabricatorUser::NAMETOKEN_TABLE, $query); $ids = ipull($ids, 'userID'); if ($ids) { $users = id(new PhabricatorUser())->loadColumnsWhere( $columns, 'id IN (%Ld)', $ids); } else { $users = array(); } } else { $users = id(new PhabricatorUser())->loadColumns($columns); } foreach ($users as $user) { if (!$need_all_users) { if ($user->getIsSystemAgent()) { continue; } if ($user->getIsDisabled()) { continue; } } $data[] = array( $user->getUsername().' ('.$user->getRealName().')', '/p/'.$user->getUsername(), $user->getPHID(), ); } } if ($need_lists) { $lists = id(new PhabricatorMetaMTAMailingList())->loadAll(); foreach ($lists as $list) { $data[] = array( $list->getName(), $list->getURI(), $list->getPHID(), ); } } if ($need_projs) { $projs = id(new PhabricatorProject())->loadAll(); foreach ($projs as $proj) { $data[] = array( $proj->getName(), '/project/view/'.$proj->getID().'/', $proj->getPHID(), ); } } if ($need_repos) { $repos = id(new PhabricatorRepository())->loadAll(); foreach ($repos as $repo) { $data[] = array( 'r'.$repo->getCallsign().' ('.$repo->getName().')', '/diffusion/'.$repo->getCallsign().'/', $repo->getPHID(), ); } } if ($need_packages) { $packages = id(new PhabricatorOwnersPackage())->loadAll(); foreach ($packages as $package) { $data[] = array( $package->getName(), '/owners/package/'.$package->getID().'/', $package->getPHID(), ); } } if ($need_arcanist_projects) { $arcprojs = id(new PhabricatorRepositoryArcanistProject())->loadAll(); foreach ($arcprojs as $proj) { $data[] = array( $proj->getName(), null, $proj->getPHID(), ); } } return id(new AphrontAjaxResponse()) ->setContent($data); } }