diff --git a/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php b/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php index b9889fcbd2..1cd0ccd3a3 100644 --- a/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php +++ b/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php @@ -1,620 +1,651 @@ 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); $uri = $request->getRequestURI() ->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 = $this->buildBaseSideNav(); $this->view = $nav->selectFilter($this->view, 'action'); $has_filter = array( 'action' => true, 'created' => true, 'subscribed' => true, 'triage' => true, 'projecttriage' => true, 'projectall' => true, ); list($status_map, $status_control) = $this->renderStatusLinks(); list($grouping, $group_control) = $this->renderGroupLinks(); list($order, $order_control) = $this->renderOrderLinks(); $user_phids = $request->getStrList( 'users', array($user->getPHID())); if ($this->view == 'projecttriage' || $this->view == 'projectall') { $project_query = new PhabricatorProjectQuery(); $project_query->setMembers($user_phids); $projects = $project_query->execute(); $project_phids = mpull($projects, 'getPHID'); } else { $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, '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(); } if ($this->view != 'projectall' && $this->view != 'projecttriage') { $form->appendChild( id(new AphrontFormTokenizerControl()) ->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'); $list_container = new AphrontNullView(); $list_container->appendChild('
'); if (!$have_tasks) { $list_container->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); $list_container->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()); $list_container->appendChild($selector); $list_container->appendChild($pager); } $list_container->appendChild('
'); $nav->appendChild($list_container); 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; case 'projecttriage': $query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE); $query->withAnyProject(true); break; case 'projectall': $query->withAnyProject(true); 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, + '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); + $author_phids, + $project_group_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; + case 'project': + $grouped = array(); + foreach ($data as $task) { + $phids = $task->getProjectPHIDs(); + if ($project_phids) { + // If the user is filtering on "Bugs", don't show a "Bugs" group + // with every result since that's silly (the query also does this + // on the backend). + $phids = array_diff($phids, $project_phids); + } + if ($phids) { + foreach ($phids as $phid) { + $grouped[$handles[$phid]->getName()][$task->getID()] = $task; + } + } else { + $grouped['No Project'][$task->getID()] = $task; + } + } + $data = $grouped; + 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', + 'j' => 'project', ); 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', + 'j' => 'Project', '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 861a84b194..c0edae6c0f 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -1,463 +1,544 @@ 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(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); + if ($this->groupBy == self::GROUP_PROJECT) { + $limit = PHP_INT_MAX; + $offset = 0; + } + $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); + $tasks = $task_dao->loadAllFromArray($data); + + if ($this->groupBy == self::GROUP_PROJECT) { + $tasks = $this->applyGroupByProject($tasks); + } + + return $tasks; } 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 && !$this->includeNoProject) { return null; } $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 && !$this->includeNoProject) { return null; } $project_dao = new ManiphestTaskProject(); return qsprintf( $conn, '%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; + case self::GROUP_PROJECT: + // NOTE: We have to load the entire result set and apply this grouping + // in the PHP process for now. + 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); } + /** + * To get paging to work for "group by project", we need to do a bunch of + * server-side magic since there's currently no way to sort by project name on + * the database. + * + * TODO: Move this all to the database. + */ + private function applyGroupByProject(array $tasks) { + + $project_phids = array(); + foreach ($tasks as $task) { + foreach ($task->getProjectPHIDs() as $phid) { + $project_phids[$phid] = true; + } + } + + $handles = id(new PhabricatorObjectHandleData(array_keys($project_phids))) + ->loadHandles(); + + $max = 1; + foreach ($handles as $handle) { + $max = max($max, strlen($handle->getName())); + } + + $items = array(); + $ii = 0; + foreach ($tasks as $key => $task) { + $phids = $task->getProjectPHIDs(); + if ($this->projectPHIDs) { + $phids = array_diff($phids, $this->projectPHIDs); + } + if ($phids) { + foreach ($phids as $phid) { + $items[] = array( + 'key' => $key, + 'seq' => sprintf( + '%'.$max.'s%d', + $handles[$phid]->getName(), + $ii), + ); + } + } else { + // Sort "no project" tasks first. + $items[] = array( + 'key' => $key, + 'seq' => '', + ); + } + ++$ii; + } + + $items = isort($items, 'seq'); + $items = array_slice( + $items, + nonempty($this->offset), + nonempty($this->limit, self::DEFAULT_PAGE_SIZE)); + + $result = array(); + foreach ($items as $item) { + $result[] = $tasks[$item['key']]; + } + + return $result; + } + } diff --git a/src/applications/maniphest/query/__init__.php b/src/applications/maniphest/query/__init__.php index 86a637a392..bbd8f47297 100644 --- a/src/applications/maniphest/query/__init__.php +++ b/src/applications/maniphest/query/__init__.php @@ -1,19 +1,20 @@