diff --git a/src/applications/maniphest/controller/ManiphestTaskListControllerPro.php b/src/applications/maniphest/controller/ManiphestTaskListControllerPro.php index 47650dd711..07337602a6 100644 --- a/src/applications/maniphest/controller/ManiphestTaskListControllerPro.php +++ b/src/applications/maniphest/controller/ManiphestTaskListControllerPro.php @@ -1,68 +1,293 @@ queryKey = idx($data, 'queryKey'); } public function processRequest() { $request = $this->getRequest(); $controller = id(new PhabricatorApplicationSearchController($request)) ->setQueryKey($this->queryKey) ->setSearchEngine(new ManiphestTaskSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } public function renderResultsList( array $tasks, PhabricatorSavedQuery $query) { assert_instances_of($tasks, 'ManiphestTask'); $viewer = $this->getRequest()->getUser(); - $list = new PHUIObjectItemListView(); - $list->setUser($viewer); + // If we didn't match anything, just pick up the default empty state. + if (!$tasks) { + return id(new PHUIObjectItemListView()) + ->setUser($viewer); + } + + $group_parameter = $query->getParameter('group', 'priority'); + $order_parameter = $query->getParameter('order', 'priority'); + + $handles = $this->loadTaskHandles($tasks); + $groups = $this->groupTasks( + $tasks, + $group_parameter, + $handles); + + $can_drag = ($order_parameter == 'priority') && + ($group_parameter == 'none' || $group_parameter == 'priority'); + + $result = array(); + + $lists = array(); + foreach ($groups as $group => $list) { + $task_list = new ManiphestTaskListView(); + $task_list->setShowBatchControls(true); + if ($can_drag) { + $task_list->setShowSubpriorityControls(true); + } + $task_list->setUser($viewer); + $task_list->setTasks($list); + $task_list->setHandles($handles); + + $header = javelin_tag( + 'h1', + array( + 'class' => 'maniphest-task-group-header', + 'sigil' => 'task-group', + 'meta' => array( + 'priority' => head($list)->getPriority(), + ), + ), + pht('%s (%s)', $group, new PhutilNumber(count($list)))); + + $lists[] = phutil_tag( + 'div', + array( + 'class' => 'maniphest-task-group' + ), + array( + $header, + $task_list, + )); + } + + Javelin::initBehavior( + 'maniphest-subpriority-editor', + array( + 'uri' => '/maniphest/subpriority/', + )); + + return phutil_tag( + 'div', + array( + 'class' => 'maniphest-list-container', + ), + array( + $lists, + $this->renderBatchEditor($query), + )); + } + + private function loadTaskHandles(array $tasks) { + assert_instances_of($tasks, 'ManiphestTask'); + + $phids = array(); foreach ($tasks as $task) { - $item = id(new PHUIObjectItemView()) - ->setObjectName('T'.$task->getID()) - ->setHeader($task->getTitle()) - ->setHref('/T'.$task->getID()) - ->setObject($task); + $assigned_phid = $task->getOwnerPHID(); + if ($assigned_phid) { + $phids[] = $assigned_phid; + } + foreach ($task->getProjectPHIDs() as $project_phid) { + $phids[] = $project_phid; + } + } + + if (!$phids) { + return array(); + } + + return id(new PhabricatorHandleQuery()) + ->setViewer($this->getRequest()->getUser()) + ->withPHIDs($phids) + ->execute(); + } + + private function groupTasks(array $tasks, $group, array $handles) { + assert_instances_of($tasks, 'ManiphestTask'); + assert_instances_of($handles, 'PhabricatorObjectHandle'); + + $groups = $this->getTaskGrouping($tasks, $group); - $list->addItem($item); + $results = array(); + foreach ($groups as $label_key => $tasks) { + $label = $this->getTaskLabelName($group, $label_key, $handles); + $results[$label][] = $tasks; + } + foreach ($results as $label => $task_groups) { + $results[$label] = array_mergev($task_groups); } - return $list; + return $results; + } + + private function getTaskGrouping(array $tasks, $group) { + switch ($group) { + case 'priority': + return mgroup($tasks, 'getPriority'); + case 'status': + return mgroup($tasks, 'getStatus'); + case 'assigned': + return mgroup($tasks, 'getOwnerPHID'); + case 'project': + return mgroup($tasks, 'getGroupByProjectPHID'); + default: + return array(pht('Tasks') => $tasks); + } + } + + private function getTaskLabelName($group, $label_key, array $handles) { + switch ($group) { + case 'priority': + return ManiphestTaskPriority::getTaskPriorityName($label_key); + case 'status': + return ManiphestTaskStatus::getTaskStatusFullName($label_key); + case 'assigned': + if ($label_key) { + return $handles[$label_key]->getFullName(); + } else { + return pht('(Not Assigned)'); + } + case 'project': + if ($label_key) { + return $handles[$label_key]->getFullName(); + } else { + return pht('(No Project)'); + } + default: + return pht('Tasks'); + } } public function buildSideNavView($for_app = false) { $user = $this->getRequest()->getUser(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); if ($for_app) { $nav->addFilter('create', pht('Create Task')); } id(new ManiphestTaskSearchEngine()) ->setViewer($user) ->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } + private function renderBatchEditor(PhabricatorSavedQuery $saved_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/'.$saved_query->getQueryKey().'/', + 'class' => 'grey button', + ), + pht('Export to Excel')); + + $hidden = phutil_tag( + 'div', + array( + 'id' => 'batch-select-id-container', + ), + ''); + + $editor = hsprintf( + '
'. + '
%s
'. + ''. + ''. + ''. + ''. + ''. + ''. + ''. + '
%s%s%s%s%s%s
'. + '
', + 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; + } + } diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php index fb90035d59..ed07eaaa35 100644 --- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php +++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php @@ -1,396 +1,398 @@ setParameter( 'assignedPHIDs', $this->readUsersFromRequest($request, 'assigned')); $saved->setParameter('withUnassigned', $request->getBool('withUnassigned')); $saved->setParameter( 'authorPHIDs', $this->readUsersFromRequest($request, 'authors')); $saved->setParameter('statuses', $request->getArr('statuses')); $saved->setParameter('priorities', $request->getArr('priorities')); $saved->setParameter('group', $request->getStr('group')); $saved->setParameter('order', $request->getStr('order')); $ids = $request->getStrList('ids'); foreach ($ids as $key => $id) { $id = trim($id, ' Tt'); if (!$id || !is_numeric($id)) { unset($ids[$key]); } else { $ids[$key] = $id; } } $saved->setParameter('ids', $ids); $saved->setParameter('fulltext', $request->getStr('fulltext')); $saved->setParameter( 'allProjectPHIDs', $request->getArr('allProjects')); $saved->setParameter( 'withNoProject', $request->getBool('withNoProject')); $saved->setParameter( 'anyProjectPHIDs', $request->getArr('anyProjects')); $saved->setParameter( 'excludeProjectPHIDs', $request->getArr('excludeProjects')); $saved->setParameter( 'userProjectPHIDs', $this->readUsersFromRequest($request, 'userProjects')); $saved->setParameter('createdStart', $request->getStr('createdStart')); $saved->setParameter('createdEnd', $request->getStr('createdEnd')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new ManiphestTaskQuery()); $author_phids = $saved->getParameter('authorPHIDs'); if ($author_phids) { $query->withAuthors($author_phids); } $with_unassigned = $saved->getParameter('withUnassigned'); if ($with_unassigned) { $query->withOwners(array(null)); } else { $assigned_phids = $saved->getParameter('assignedPHIDs', array()); if ($assigned_phids) { $query->withOwners($assigned_phids); } } $statuses = $saved->getParameter('statuses'); if ($statuses) { $query->withStatuses($statuses); } $priorities = $saved->getParameter('priorities'); if ($priorities) { $query->withPriorities($priorities); } $order = $saved->getParameter('order'); $order = idx($this->getOrderValues(), $order); if ($order) { $query->setOrderBy($order); } else { $query->setOrderBy(head($this->getOrderValues())); } $group = $saved->getParameter('group'); $group = idx($this->getGroupValues(), $group); if ($group) { $query->setGroupBy($group); } else { $query->setGroupBy(head($this->getGroupValues())); } $ids = $saved->getParameter('ids'); if ($ids) { $query->withIDs($ids); } $fulltext = $saved->getParameter('fulltext'); if (strlen($fulltext)) { $query->withFullTextSearch($fulltext); } $with_no_project = $saved->getParameter('withNoProject'); if ($with_no_project) { $query->withAllProjects(array(ManiphestTaskOwner::PROJECT_NO_PROJECT)); } else { $project_phids = $saved->getParameter('allProjectPHIDs'); if ($project_phids) { $query->withAllProjects($project_phids); } } $any_project_phids = $saved->getParameter('anyProjectPHIDs'); if ($any_project_phids) { $query->withAnyProjects($any_project_phids); } $exclude_project_phids = $saved->getParameter('excludeProjectPHIDs'); if ($exclude_project_phids) { $query->withoutProjects($exclude_project_phids); } $user_project_phids = $saved->getParameter('userProjectPHIDs'); if ($user_project_phids) { $query->withAnyUserProjects($user_project_phids); } $start = $this->parseDateTime($saved->getParameter('createdStart')); $end = $this->parseDateTime($saved->getParameter('createdEnd')); if ($start) { $query->withDateCreatedAfter($start); } if ($end) { $query->withDateCreatedBefore($end); } return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $assigned_phids = $saved->getParameter('assignedPHIDs', array()); $author_phids = $saved->getParameter('authorPHIDs', array()); $all_project_phids = $saved->getParameter( 'allProjectPHIDs', array()); $any_project_phids = $saved->getParameter( 'anyProjectPHIDs', array()); $exclude_project_phids = $saved->getParameter( 'excludeProjectPHIDs', array()); $user_project_phids = $saved->getParameter( 'userProjectPHIDs', array()); $all_phids = array_merge( $assigned_phids, $author_phids, $all_project_phids, $any_project_phids, $exclude_project_phids, $user_project_phids); if ($all_phids) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->withPHIDs($all_phids) ->execute(); } else { $handles = array(); } $assigned_handles = array_select_keys($handles, $assigned_phids); $author_handles = array_select_keys($handles, $author_phids); $all_project_handles = array_select_keys($handles, $all_project_phids); $any_project_handles = array_select_keys($handles, $any_project_phids); $exclude_project_handles = array_select_keys( $handles, $exclude_project_phids); $user_project_handles = array_select_keys($handles, $user_project_phids); $with_unassigned = $saved->getParameter('withUnassigned'); $with_no_projects = $saved->getParameter('withNoProject'); $statuses = $saved->getParameter('statuses', array()); $statuses = array_fuse($statuses); $status_control = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Status')); foreach (ManiphestTaskStatus::getTaskStatusMap() as $status => $name) { $status_control->addCheckbox( 'statuses[]', $status, $name, isset($statuses[$status])); } $priorities = $saved->getParameter('priorities', array()); $priorities = array_fuse($priorities); $priority_control = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Priority')); foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $name) { $priority_control->addCheckbox( 'priorities[]', $pri, $name, isset($priorities[$pri])); } $ids = $saved->getParameter('ids', array()); $form ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/accounts/') ->setName('assigned') ->setLabel(pht('Assigned To')) ->setValue($assigned_handles)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'withUnassigned', 1, pht('Show only unassigned tasks.'), $with_unassigned)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/projects/') ->setName('allProjects') ->setLabel(pht('In All Projects')) ->setValue($all_project_handles)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'withNoProject', 1, pht('Show only tasks with no projects.'), $with_no_projects)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/projects/') ->setName('anyProjects') ->setLabel(pht('In Any Project')) ->setValue($any_project_handles)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/projects/') ->setName('excludeProjects') ->setLabel(pht('Not In Projects')) ->setValue($exclude_project_handles)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/accounts/') ->setName('userProjects') ->setLabel(pht('In Users\' Projects')) ->setValue($user_project_handles)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/accounts/') ->setName('authors') ->setLabel(pht('Authors')) ->setValue($author_handles)) ->appendChild($status_control) ->appendChild($priority_control) ->appendChild( id(new AphrontFormSelectControl()) ->setName('group') ->setLabel(pht('Group By')) ->setValue($saved->getParameter('group')) ->setOptions($this->getGroupOptions())) ->appendChild( id(new AphrontFormSelectControl()) ->setName('order') ->setLabel(pht('Order By')) ->setValue($saved->getParameter('order')) ->setOptions($this->getOrderOptions())) ->appendChild( id(new AphrontFormTextControl()) ->setName('fulltext') ->setLabel(pht('Contains Text')) ->setValue($saved->getParameter('fulltext'))) ->appendChild( id(new AphrontFormTextControl()) ->setName('ids') ->setLabel(pht('Task IDs')) ->setValue(implode(', ', $ids))); $this->buildDateRange( $form, $saved, 'createdStart', pht('Created After'), 'createdEnd', pht('Created Before')); } protected function getURI($path) { return '/maniphest/'.$path; } public function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { $names['assigned'] = pht('Assigned'); $names['authored'] = pht('Authored'); } $names['open'] = pht('Open Tasks'); $names['all'] = pht('All Tasks'); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); $viewer_phid = $this->requireViewer()->getPHID(); switch ($query_key) { case 'all': return $query; case 'assigned': return $query ->setParameter('assignedPHIDs', array($viewer_phid)) ->setParameter('statuses', array(ManiphestTaskStatus::STATUS_OPEN)); case 'open': return $query ->setParameter('statuses', array(ManiphestTaskStatus::STATUS_OPEN)); case 'authored': return $query - ->setParameter('authorPHIDs', array($viewer_phid)); + ->setParameter('authorPHIDs', array($viewer_phid)) + ->setParameter('order', 'created') + ->setParameter('group', 'none'); } return parent::buildSavedQueryFromBuiltin($query_key); } private function getOrderOptions() { return array( 'priority' => pht('Priority'), 'updated' => pht('Date Updated'), 'created' => pht('Date Created'), 'title' => pht('Title'), ); } private function getOrderValues() { return array( 'priority' => ManiphestTaskQuery::ORDER_PRIORITY, 'updated' => ManiphestTaskQuery::ORDER_MODIFIED, 'created' => ManiphestTaskQuery::ORDER_CREATED, 'title' => ManiphestTaskQuery::ORDER_TITLE, ); } private function getGroupOptions() { return array( 'priority' => pht('Priority'), 'assigned' => pht('Assigned'), 'status' => pht('Status'), 'project' => pht('Project'), 'none' => pht('None'), ); } private function getGroupValues() { return array( 'priority' => ManiphestTaskQuery::GROUP_PRIORITY, 'assigned' => ManiphestTaskQuery::GROUP_OWNER, 'status' => ManiphestTaskQuery::GROUP_STATUS, 'project' => ManiphestTaskQuery::GROUP_PROJECT, 'none' => ManiphestTaskQuery::GROUP_NONE, ); } }