diff --git a/src/applications/maniphest/controller/ManiphestTaskListController.php b/src/applications/maniphest/controller/ManiphestTaskListController.php index 67a4a7e91f..c7f2e8dff6 100644 --- a/src/applications/maniphest/controller/ManiphestTaskListController.php +++ b/src/applications/maniphest/controller/ManiphestTaskListController.php @@ -1,978 +1,978 @@ <?php /** * @group maniphest */ final class ManiphestTaskListController extends ManiphestController { const DEFAULT_PAGE_SIZE = 1000; private $view; public function willProcessRequest(array $data) { $this->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. $task_ids = $request->getStr('set_tasks'); $task_ids = nonempty($task_ids, null); $search_text = $request->getStr('set_search'); $min_priority = $request->getInt('set_lpriority'); $max_priority = $request->getInt('set_hpriority'); $uri = $request->getRequestURI() ->alter('users', $this->getArrToStrList('set_users')) ->alter('projects', $this->getArrToStrList('set_projects')) ->alter('aprojects', $this->getArrToStrList('set_aprojects')) ->alter('useraprojects', $this->getArrToStrList('set_useraprojects')) ->alter('xprojects', $this->getArrToStrList('set_xprojects')) ->alter('owners', $this->getArrToStrList('set_owners')) ->alter('authors', $this->getArrToStrList('set_authors')) ->alter('lpriority', $min_priority) ->alter('hpriority', $max_priority) ->alter('tasks', $task_ids) ->alter('search', $search_text); return id(new AphrontRedirectResponse())->setURI($uri); } $nav = $this->buildBaseSideNav(); $has_filter = array( 'action' => true, 'created' => true, 'subscribed' => true, 'triage' => true, 'projecttriage' => true, 'projectall' => true, ); $query = null; $key = $request->getStr('key'); if (!$key && !$this->view) { if ($this->getDefaultQuery()) { $key = $this->getDefaultQuery()->getQueryKey(); } } if ($key) { $query = id(new PhabricatorSearchQuery())->loadOneWhere( 'queryKey = %s', $key); } // If the user is running a saved query, load query parameters from that // query. Otherwise, build a new query object from the HTTP request. if ($query) { $nav->selectFilter('Q:'.$query->getQueryKey(), 'custom'); $this->view = 'custom'; } else { $this->view = $nav->selectFilter($this->view, 'action'); $query = $this->buildQueryFromRequest(); } // Execute the query. list($tasks, $handles, $total_count) = self::loadTasks( $query, $user); // Extract information we need to render the filters from the query. $search_text = $query->getParameter('fullTextSearch'); $user_phids = $query->getParameter('userPHIDs', array()); $task_ids = $query->getParameter('taskIDs', array()); $owner_phids = $query->getParameter('ownerPHIDs', array()); $author_phids = $query->getParameter('authorPHIDs', array()); $project_phids = $query->getParameter('projectPHIDs', array()); $any_project_phids = $query->getParameter( 'anyProjectPHIDs', array()); $any_user_project_phids = $query->getParameter( 'anyUserProjectPHIDs', array()); $exclude_project_phids = $query->getParameter( 'excludeProjectPHIDs', array()); $low_priority = $query->getParameter('lowPriority'); $high_priority = $query->getParameter('highPriority'); $page_size = $query->getParameter('limit'); $page = $query->getParameter('offset'); $q_status = $query->getParameter('status'); $q_group = $query->getParameter('group'); $q_order = $query->getParameter('order'); $form = id(new AphrontFormView()) ->setUser($user) ->setNoShading(true) ->setAction( $request->getRequestURI() ->alter('key', null) ->alter( $this->getStatusRequestKey(), $this->getStatusRequestValue($q_status)) ->alter( $this->getOrderRequestKey(), $this->getOrderRequestValue($q_order)) ->alter( $this->getGroupRequestKey(), $this->getGroupRequestValue($q_group))); 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(pht('Users')) ->setValue($tokens)); } if ($this->view == 'custom') { $form->appendChild( id(new AphrontFormTextControl()) ->setName('set_search') ->setLabel(pht('Search')) ->setValue($search_text)); $form->appendChild( id(new AphrontFormTextControl()) ->setName('set_tasks') ->setLabel(pht('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(pht('Owners')) ->setValue($tokens)); $tokens = array(); foreach ($author_phids as $phid) { $tokens[$phid] = $handles[$phid]->getFullName(); } $form->appendChild( id(new AphrontFormTokenizerControl()) - ->setDatasource('/typeahead/common/users/') + ->setDatasource('/typeahead/common/authors/') ->setName('set_authors') ->setLabel(pht('Authors')) ->setValue($tokens)); } $tokens = array(); foreach ($project_phids as $phid) { $tokens[$phid] = $handles[$phid]->getFullName(); } if ($this->view != 'projectall' && $this->view != 'projecttriage') { $caption = null; if ($this->view == 'custom') { $caption = pht('Find tasks in ALL of these projects ("AND" query).'); } $form->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/searchproject/') ->setName('set_projects') ->setLabel(pht('Projects')) ->setCaption($caption) ->setValue($tokens)); } if ($this->view == 'custom') { $atokens = array(); foreach ($any_project_phids as $phid) { $atokens[$phid] = $handles[$phid]->getFullName(); } $form->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/projects/') ->setName('set_aprojects') ->setLabel(pht('Any Projects')) ->setCaption(pht('Find tasks in ANY of these projects ("OR" query).')) ->setValue($atokens)); $tokens = array(); foreach ($any_user_project_phids as $phid) { $tokens[$phid] = $handles[$phid]->getFullName(); } $form->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/users/') ->setName('set_useraprojects') ->setLabel(pht('Any User Projects')) ->setCaption( pht('Find tasks in ANY of these users\' projects ("OR" query).')) ->setValue($tokens)); $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(pht('Exclude Projects')) ->setCaption(pht('Find tasks NOT in any of these projects.')) ->setValue($tokens)); $priority = ManiphestTaskPriority::getLowestPriority(); if ($low_priority !== null) { $priority = $low_priority; } $form->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Min Priority')) ->setName('set_lpriority') ->setValue($priority) ->setOptions(array_reverse( ManiphestTaskPriority::getTaskPriorityMap(), true))); $priority = ManiphestTaskPriority::getHighestPriority(); if ($high_priority !== null) { $priority = $high_priority; } $form->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Max Priority')) ->setName('set_hpriority') ->setValue($priority) ->setOptions(ManiphestTaskPriority::getTaskPriorityMap())); } $form ->appendChild($this->renderStatusControl($q_status)) ->appendChild($this->renderGroupControl($q_group)) ->appendChild($this->renderOrderControl($q_order)); $submit = id(new AphrontFormSubmitControl()) ->setValue(pht('Filter Tasks')); // Only show "Save..." for novel queries which have some kind of query // parameters set. if ($this->view === 'custom' && empty($key) && $request->getRequestURI()->getQueryParams()) { $submit->addCancelButton( '/maniphest/custom/edit/?key='.$query->getQueryKey(), pht('Save Custom Query...')); } $form->appendChild($submit); $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(); if (empty($key)) { $filter->appendChild($form); } $have_tasks = false; foreach ($tasks as $group => $list) { if (count($list)) { $have_tasks = true; break; } } require_celerity_resource('maniphest-task-summary-css'); $list_container = new AphrontNullView(); $list_container->appendChild(hsprintf( '<div class="maniphest-list-container">')); if (!$have_tasks) { $no_tasks = pht('No matching tasks.'); $list_container->appendChild(hsprintf( '<h1 class="maniphest-task-group-header">'. '%s'. '</h1>', $no_tasks)); $result_count = null; } else { $pager = new AphrontPagerView(); $pager->setURI($request->getRequestURI(), 'offset'); $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; $results = pht('Displaying tasks %s - %s of %s.', number_format($cur), number_format($max), number_format($tot)); $result_count = phutil_tag( 'div', array( 'class' => 'maniphest-total-result-count' ), $results); $selector = new AphrontNullView(); $group = $query->getParameter('group'); $order = $query->getParameter('order'); $is_draggable = ($order == 'priority') && ($group == 'none' || $group == 'priority'); $lists = array(); foreach ($tasks as $group => $list) { $task_list = new ManiphestTaskListView(); $task_list->setShowBatchControls(true); if ($is_draggable) { $task_list->setShowSubpriorityControls(true); } $task_list->setUser($user); $task_list->setTasks($list); $task_list->setHandles($handles); $count = number_format(count($list)); $header = javelin_tag( 'h1', array( 'class' => 'maniphest-task-group-header', 'sigil' => 'task-group', 'meta' => array( 'priority' => head($list)->getPriority(), ), ), $group.' ('.$count.')'); $lists[] = phutil_tag( 'div', array( 'class' => 'maniphest-task-group' ), array( $header, $task_list, )); } $selector->appendChild($lists); $selector->appendChild($this->renderBatchEditor($query)); $list_container->appendChild($selector); $list_container->appendChild($pager); Javelin::initBehavior( 'maniphest-subpriority-editor', array( 'uri' => '/maniphest/subpriority/', )); } $nav->appendChild($filter); $nav->appendChild($result_count); $nav->appendChild($list_container); $title = pht('Task List'); $crumbs = $this->buildApplicationCrumbs() ->addCrumb( id(new PhabricatorCrumbView()) ->setName($title)) ->addAction( id(new PHUIListItemView()) ->setHref($this->getApplicationURI('/task/create/')) ->setName(pht('Create Task')) ->setIcon('create')); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, 'dust' => true, )); } public static function loadTasks( PhabricatorSearchQuery $search_query, PhabricatorUser $viewer) { $any_project = false; $search_text = $search_query->getParameter('fullTextSearch'); $user_phids = $search_query->getParameter('userPHIDs', array()); $task_ids = $search_query->getParameter('taskIDs', array()); $project_phids = $search_query->getParameter('projectPHIDs', array()); $any_project_phids = $search_query->getParameter( 'anyProjectPHIDs', array()); $any_user_project_phids = $search_query->getParameter( 'anyUserProjectPHIDs', array()); $xproject_phids = $search_query->getParameter( 'excludeProjectPHIDs', array()); $owner_phids = $search_query->getParameter('ownerPHIDs', array()); $author_phids = $search_query->getParameter('authorPHIDs', array()); $low_priority = $search_query->getParameter('lowPriority'); $low_priority = coalesce($low_priority, ManiphestTaskPriority::getLowestPriority()); $high_priority = $search_query->getParameter('highPriority'); $high_priority = coalesce($high_priority, ManiphestTaskPriority::getHighestPriority()); $query = new ManiphestTaskQuery(); $query->withTaskIDs($task_ids); if ($project_phids) { $query->withAllProjects($project_phids); } if ($xproject_phids) { $query->withoutProjects($xproject_phids); } if ($any_project_phids) { $query->withAnyProjects($any_project_phids); } if ($owner_phids) { $query->withOwners($owner_phids); } if ($author_phids) { $query->withAuthors($author_phids); } if ($any_user_project_phids) { $query->setViewer($viewer); $query->withAnyUserProjects($any_user_project_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; case 'projecttriage': $query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE); break; case 'projectall': break; case 'custom': $query->withPrioritiesBetween($low_priority, $high_priority); break; } $query->withFullTextSearch($search_text); $order_map = array( 'priority' => ManiphestTaskQuery::ORDER_PRIORITY, 'created' => ManiphestTaskQuery::ORDER_CREATED, 'title' => ManiphestTaskQuery::ORDER_TITLE, ); $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, 'project' => ManiphestTaskQuery::GROUP_PROJECT, ); $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(); $project_group_phids = array(); if ($search_query->getParameter('group') == 'project') { foreach ($data as $task) { foreach ($task->getProjectPHIDs() as $phid) { $project_group_phids[] = $phid; } } } $handle_phids = mpull($data, 'getOwnerPHID'); $handle_phids = array_merge( $handle_phids, $project_phids, $user_phids, $xproject_phids, $owner_phids, $author_phids, $project_group_phids, $any_project_phids, $any_user_project_phids, array_mergev(mpull($data, 'getProjectPHIDs'))); $handles = id(new PhabricatorObjectHandleData($handle_phids)) ->setViewer($viewer) ->loadHandles(); switch ($search_query->getParameter('group')) { case 'priority': $data = mgroup($data, 'getPriority'); // 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'); $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; } } $data = $out; ksort($data); // Move "Unassigned" to the top of the list. if (isset($data['Unassigned'])) { $data = array('Unassigned' => $out['Unassigned']) + $out; } break; case 'project': $grouped = array(); foreach ($query->getGroupByProjectResults() as $project => $tasks) { foreach ($tasks as $task) { $group = $project ? $handles[$project]->getName() : 'No Project'; $grouped[$group][$task->getID()] = $task; } } $data = $grouped; ksort($data); // Move "No Project" to the end of the list. if (isset($data['No Project'])) { $noproject = $data['No Project']; unset($data['No Project']); $data += array('No Project' => $noproject); } break; default: $data = array( 'Tasks' => $data, ); break; } return array($data, $handles, $total_row_count); } private function renderBatchEditor(PhabricatorSearchQuery $search_query) { $user = $this->getRequest()->getUser(); Javelin::initBehavior( 'maniphest-batch-selector', array( 'selectAll' => 'batch-select-all', 'selectNone' => 'batch-select-none', 'submit' => 'batch-select-submit', 'status' => 'batch-select-status-cell', 'idContainer' => 'batch-select-id-container', 'formID' => 'batch-select-form', )); $select_all = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'class' => 'grey button', 'id' => 'batch-select-all', ), pht('Select All')); $select_none = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'class' => 'grey button', 'id' => 'batch-select-none', ), pht('Clear Selection')); $submit = phutil_tag( 'button', array( 'id' => 'batch-select-submit', 'disabled' => 'disabled', 'class' => 'disabled', ), pht("Batch Edit Selected \xC2\xBB")); $export = javelin_tag( 'a', array( 'href' => '/maniphest/export/'.$search_query->getQueryKey().'/', 'class' => 'grey button', ), pht('Export to Excel')); $hidden = phutil_tag( 'div', array( 'id' => 'batch-select-id-container', ), ''); $editor = hsprintf( '<div class="maniphest-batch-editor">'. '<div class="batch-editor-header">%s</div>'. '<table class="maniphest-batch-editor-layout">'. '<tr>'. '<td>%s%s</td>'. '<td>%s</td>'. '<td id="batch-select-status-cell">%s</td>'. '<td class="batch-select-submit-cell">%s%s</td>'. '</tr>'. '</table>'. '</div>', pht('Batch Task Editor'), $select_all, $select_none, $export, '', $submit, $hidden); $editor = phabricator_form( $user, array( 'method' => 'POST', 'action' => '/maniphest/batch/', 'id' => 'batch-select-form', ), $editor); return $editor; } private function buildQueryFromRequest() { $request = $this->getRequest(); $user = $request->getUser(); $status = $this->getStatusValueFromRequest(); $group = $this->getGroupValueFromRequest(); $order = $this->getOrderValueFromRequest(); $user_phids = $request->getStrList( 'users', array($user->getPHID())); if ($this->view == 'projecttriage' || $this->view == 'projectall') { $projects = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withMemberPHIDs($user_phids) ->execute(); $any_project_phids = mpull($projects, 'getPHID'); $any_user_project_phids = array(); } else { $any_project_phids = $request->getStrList('aprojects'); $any_user_project_phids = $request->getStrList('useraprojects'); } $project_phids = $request->getStrList('projects'); $exclude_project_phids = $request->getStrList('xprojects'); $task_ids = $request->getStrList('tasks'); if ($task_ids) { // We only need the integer portion of each task ID, so get rid of any // non-numeric elements $numeric_task_ids = array(); foreach ($task_ids as $task_id) { $task_id = preg_replace('/\D+/', '', $task_id); if (!empty($task_id)) { $numeric_task_ids[] = $task_id; } } if (empty($numeric_task_ids)) { $numeric_task_ids = array(null); } $task_ids = $numeric_task_ids; } $owner_phids = $request->getStrList('owners'); $author_phids = $request->getStrList('authors'); $search_string = $request->getStr('search'); $low_priority = $request->getInt('lpriority'); $high_priority = $request->getInt('hpriority'); $page = $request->getInt('offset'); $page_size = self::DEFAULT_PAGE_SIZE; $query = new PhabricatorSearchQuery(); $query->setQuery('<<maniphest>>'); $query->setParameters( array( 'fullTextSearch' => $search_string, 'view' => $this->view, 'userPHIDs' => $user_phids, 'projectPHIDs' => $project_phids, 'anyProjectPHIDs' => $any_project_phids, 'anyUserProjectPHIDs' => $any_user_project_phids, 'excludeProjectPHIDs' => $exclude_project_phids, 'ownerPHIDs' => $owner_phids, 'authorPHIDs' => $author_phids, 'taskIDs' => $task_ids, 'lowPriority' => $low_priority, 'highPriority' => $high_priority, 'group' => $group, 'order' => $order, 'offset' => $page, 'limit' => $page_size, 'status' => $status, )); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $query->save(); unset($unguarded); return $query; } /* -( Toggle Button Controls )--------------------------------------------- These are a giant mess since we have several different values: the request key (GET param used in requests), the request value (short names used in requests to keep URIs readable), and the query value (complex value stored in the query). */ private function getStatusValueFromRequest() { $map = $this->getStatusMap(); $val = $this->getRequest()->getStr($this->getStatusRequestKey()); return idx($map, $val, head($map)); } private function getGroupValueFromRequest() { $map = $this->getGroupMap(); $val = $this->getRequest()->getStr($this->getGroupRequestKey()); return idx($map, $val, head($map)); } private function getOrderValueFromRequest() { $map = $this->getOrderMap(); $val = $this->getRequest()->getStr($this->getOrderRequestKey()); return idx($map, $val, head($map)); } private function getStatusRequestKey() { return 's'; } private function getGroupRequestKey() { return 'g'; } private function getOrderRequestKey() { return 'o'; } private function getStatusRequestValue($value) { return array_search($value, $this->getStatusMap()); } private function getGroupRequestValue($value) { return array_search($value, $this->getGroupMap()); } private function getOrderRequestValue($value) { return array_search($value, $this->getOrderMap()); } private function getStatusMap() { return array( 'o' => array( 'open' => true, ), 'c' => array( 'closed' => true, ), 'oc' => array( 'open' => true, 'closed' => true, ), ); } private function getGroupMap() { return array( 'p' => 'priority', 'o' => 'owner', 's' => 'status', 'j' => 'project', 'n' => 'none', ); } private function getOrderMap() { return array( 'p' => 'priority', 'u' => 'updated', 'c' => 'created', 't' => 'title', ); } private function getStatusButtonMap() { return array( 'o' => pht('Open'), 'c' => pht('Closed'), 'oc' => pht('All'), ); } private function getGroupButtonMap() { return array( 'p' => pht('Priority'), 'o' => pht('Owner'), 's' => pht('Status'), 'j' => pht('Project'), 'n' => pht('None'), ); } private function getOrderButtonMap() { return array( 'p' => pht('Priority'), 'u' => pht('Updated'), 'c' => pht('Created'), 't' => pht('Title'), ); } public function renderStatusControl($value) { $request = $this->getRequest(); return id(new AphrontFormToggleButtonsControl()) ->setLabel(pht('Status')) ->setValue($this->getStatusRequestValue($value)) ->setBaseURI($request->getRequestURI(), $this->getStatusRequestKey()) ->setButtons($this->getStatusButtonMap()); } public function renderOrderControl($value) { $request = $this->getRequest(); return id(new AphrontFormToggleButtonsControl()) ->setLabel(pht('Order')) ->setValue($this->getOrderRequestValue($value)) ->setBaseURI($request->getRequestURI(), $this->getOrderRequestKey()) ->setButtons($this->getOrderButtonMap()); } public function renderGroupControl($value) { $request = $this->getRequest(); return id(new AphrontFormToggleButtonsControl()) ->setLabel(pht('Group')) ->setValue($this->getGroupRequestValue($value)) ->setBaseURI($request->getRequestURI(), $this->getGroupRequestKey()) ->setButtons($this->getGroupButtonMap()); } } diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php index 3346b72423..5c02ac1d8a 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php @@ -1,336 +1,341 @@ <?php final class PhabricatorTypeaheadCommonDatasourceController extends PhabricatorTypeaheadDatasourceController { private $type; public function willProcessRequest(array $data) { $this->type = $data['type']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $query = $request->getStr('q'); $need_rich_data = false; $need_users = false; + $need_agents = false; $need_applications = 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; $need_symbols = false; switch ($this->type) { case 'mainsearch': $need_users = true; $need_applications = true; $need_rich_data = true; $need_symbols = true; $need_projs = true; break; 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 'authors': + $need_users = true; + $need_agents = true; + break; case 'mailable': $need_users = true; $need_lists = true; break; case 'allmailable': $need_users = true; $need_all_users = true; $need_lists = true; break; case 'projects': $need_projs = true; break; case 'usersorprojects': $need_users = true; $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; } $results = array(); if ($need_upforgrabs) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName('upforgrabs (Up For Grabs)') ->setPHID(ManiphestTaskOwner::OWNER_UP_FOR_GRABS); } if ($need_noproject) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName('noproject (No Project)') ->setPHID(ManiphestTaskOwner::PROJECT_NO_PROJECT); } if ($need_users) { $columns = array( 'isSystemAgent', 'isAdmin', 'isDisabled', 'userName', 'realName', 'phid'); if ($query) { // This is an arbitrary limit which is just larger than any limit we // actually use in the application. // TODO: The datasource should pass this in the query. $limit = 15; $user_table = new PhabricatorUser(); $conn_r = $user_table->establishConnection('r'); $ids = queryfx_all( $conn_r, 'SELECT id FROM %T WHERE username LIKE %> ORDER BY username ASC LIMIT %d', $user_table->getTableName(), $query, $limit); $ids = ipull($ids, 'id'); if (count($ids) < $limit) { // If we didn't find enough username hits, look for real name hits. // We need to pull the entire pagesize so that we end up with the // right number of items if this query returns many duplicate IDs // that we've already selected. $realname_ids = queryfx_all( $conn_r, 'SELECT DISTINCT userID FROM %T WHERE token LIKE %> ORDER BY token ASC LIMIT %d', PhabricatorUser::NAMETOKEN_TABLE, $query, $limit); $realname_ids = ipull($realname_ids, 'userID'); $ids = array_merge($ids, $realname_ids); $ids = array_unique($ids); $ids = array_slice($ids, 0, $limit); } // Always add the logged-in user because some tokenizers autosort them // first. They'll be filtered out on the client side if they don't // match the query. $ids[] = $request->getUser()->getID(); if ($ids) { $users = id(new PhabricatorUser())->loadColumnsWhere( $columns, 'id IN (%Ld)', $ids); } else { $users = array(); } } else { $users = id(new PhabricatorUser())->loadColumns($columns); } if ($need_rich_data) { $phids = mpull($users, 'getPHID'); $handles = $this->loadViewerHandles($phids); } foreach ($users as $user) { if (!$need_all_users) { - if ($user->getIsSystemAgent()) { + if (!$need_agents && $user->getIsSystemAgent()) { continue; } if ($user->getIsDisabled()) { continue; } } $result = id(new PhabricatorTypeaheadResult()) ->setName($user->getFullName()) ->setURI('/p/'.$user->getUsername()) ->setPHID($user->getPHID()) ->setPriorityString($user->getUsername()); if ($need_rich_data) { $display_type = 'User'; if ($user->getIsAdmin()) { $display_type = 'Administrator'; } $result->setDisplayType($display_type); $result->setImageURI($handles[$user->getPHID()]->getImageURI()); $result->setPriorityType('user'); } $results[] = $result; } } if ($need_lists) { $lists = id(new PhabricatorMetaMTAMailingList())->loadAll(); foreach ($lists as $list) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($list->getName()) ->setURI($list->getURI()) ->setPHID($list->getPHID()); } } if ($need_projs) { $projs = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withStatus(PhabricatorProjectQuery::STATUS_OPEN) ->execute(); foreach ($projs as $proj) { $proj_result = id(new PhabricatorTypeaheadResult()) ->setName($proj->getName()) ->setDisplayType("Project") ->setURI('/project/view/'.$proj->getID().'/') ->setPHID($proj->getPHID()); $prof = $proj->loadProfile(); if ($prof) { $proj_result->setImageURI($prof->loadProfileImageURI()); } $results[] = $proj_result; } } if ($need_repos) { $repos = id(new PhabricatorRepository())->loadAll(); foreach ($repos as $repo) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName('r'.$repo->getCallsign().' ('.$repo->getName().')') ->setURI('/diffusion/'.$repo->getCallsign().'/') ->setPHID($repo->getPHID()) ->setPriorityString('r'.$repo->getCallsign()); } } if ($need_packages) { $packages = id(new PhabricatorOwnersPackage())->loadAll(); foreach ($packages as $package) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($package->getName()) ->setURI('/owners/package/'.$package->getID().'/') ->setPHID($package->getPHID()); } } if ($need_arcanist_projects) { $arcprojs = id(new PhabricatorRepositoryArcanistProject())->loadAll(); foreach ($arcprojs as $proj) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($proj->getName()) ->setPHID($proj->getPHID()); } } if ($need_applications) { $applications = PhabricatorApplication::getAllInstalledApplications(); foreach ($applications as $application) { $uri = $application->getTypeaheadURI(); if (!$uri) { continue; } $name = $application->getName().' '.$application->getShortDescription(); $results[] = id(new PhabricatorTypeaheadResult()) ->setName($name) ->setURI($uri) ->setPHID($application->getPHID()) ->setPriorityString($application->getName()) ->setDisplayName($application->getName()) ->setDisplayType($application->getShortDescription()) ->setImageuRI($application->getIconURI()) ->setPriorityType('apps'); } } if ($need_symbols) { $symbols = id(new DiffusionSymbolQuery()) ->setNamePrefix($query) ->setLimit(15) ->needArcanistProjects(true) ->needRepositories(true) ->needPaths(true) ->execute(); foreach ($symbols as $symbol) { $lang = $symbol->getSymbolLanguage(); $name = $symbol->getSymbolName(); $type = $symbol->getSymbolType(); $proj = $symbol->getArcanistProject()->getName(); $results[] = id(new PhabricatorTypeaheadResult()) ->setName($name) ->setURI($symbol->getURI()) ->setPHID(md5($symbol->getURI())) // Just needs to be unique. ->setDisplayName($name) ->setDisplayType(strtoupper($lang).' '.ucwords($type).' ('.$proj.')') ->setPriorityType('symb'); } } $content = mpull($results, 'getWireFormat'); if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent($content); } // If there's a non-Ajax request to this endpoint, show results in a tabular // format to make it easier to debug typeahead output. $rows = array(); foreach ($results as $result) { $wire = $result->getWireFormat(); $rows[] = $wire; } $table = new AphrontTableView($rows); $table->setHeaders( array( 'Name', 'URI', 'PHID', 'Priority', 'Display Name', 'Display Type', 'Image URI', 'Priority Type', )); $panel = new AphrontPanelView(); $panel->setHeader('Typeahead Results'); $panel->appendChild($table); return $this->buildStandardPageResponse( $panel, array( 'title' => 'Typeahead Results', )); } } diff --git a/src/view/form/control/AphrontFormTokenizerControl.php b/src/view/form/control/AphrontFormTokenizerControl.php index 35ef006475..d8d7ecdc18 100644 --- a/src/view/form/control/AphrontFormTokenizerControl.php +++ b/src/view/form/control/AphrontFormTokenizerControl.php @@ -1,101 +1,102 @@ <?php final class AphrontFormTokenizerControl extends AphrontFormControl { private $datasource; private $disableBehavior; private $limit; private $placeholder; public function setDatasource($datasource) { $this->datasource = $datasource; return $this; } public function setDisableBehavior($disable) { $this->disableBehavior = $disable; return $this; } protected function getCustomControlClass() { return 'aphront-form-control-tokenizer'; } public function setLimit($limit) { $this->limit = $limit; return $this; } public function setPlaceholder($placeholder) { $this->placeholder = $placeholder; return $this; } protected function renderInput() { $name = $this->getName(); $values = nonempty($this->getValue(), array()); if ($this->getID()) { $id = $this->getID(); } else { $id = celerity_generate_unique_node_id(); } if (!$this->placeholder) { $this->placeholder = $this->getDefaultPlaceholder(); } $template = new AphrontTokenizerTemplateView(); $template->setName($name); $template->setID($id); $template->setValue($values); $username = null; if ($this->user) { $username = $this->user->getUsername(); } if (!$this->disableBehavior) { Javelin::initBehavior('aphront-basic-tokenizer', array( 'id' => $id, 'src' => $this->datasource, 'value' => $values, 'limit' => $this->limit, 'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'), 'username' => $username, 'placeholder' => $this->placeholder, )); } return $template->render(); } private function getDefaultPlaceholder() { $datasource = $this->datasource; $matches = null; if (!preg_match('@^/typeahead/common/(.*)/$@', $datasource, $matches)) { return null; } $request = $matches[1]; $map = array( 'users' => pht('Type a user name...'), + 'authors' => pht('Type a user name...'), 'usersorprojects' => pht('Type a user or project name...'), 'searchowner' => pht('Type a user name...'), 'accounts' => pht('Type a user name...'), 'mailable' => pht('Type a user or mailing list...'), 'allmailable' => pht('Type a user or mailing list...'), 'searchproject' => pht('Type a project name...'), 'projects' => pht('Type a project name...'), 'repositories' => pht('Type a repository name...'), 'packages' => pht('Type a package name...'), 'arcanistproject' => pht('Type an arc project name...'), ); return idx($map, $request); } }