diff --git a/src/applications/home/controller/PhabricatorHomeMainController.php b/src/applications/home/controller/PhabricatorHomeMainController.php index 63ce763b69..9d604cf0d6 100644 --- a/src/applications/home/controller/PhabricatorHomeMainController.php +++ b/src/applications/home/controller/PhabricatorHomeMainController.php @@ -1,422 +1,422 @@ getUser(); $dashboard = PhabricatorDashboardInstall::getDashboard( $user, $user->getPHID(), get_class($this->getCurrentApplication())); if (!$dashboard) { $dashboard = PhabricatorDashboardInstall::getDashboard( $user, PhabricatorHomeApplication::DASHBOARD_DEFAULT, get_class($this->getCurrentApplication())); } if ($dashboard) { $content = id(new PhabricatorDashboardRenderingEngine()) ->setViewer($user) ->setDashboard($dashboard) ->renderDashboard(); } else { $project_query = new PhabricatorProjectQuery(); $project_query->setViewer($user); $project_query->withMemberPHIDs(array($user->getPHID())); $projects = $project_query->execute(); $content = $this->buildMainResponse($projects); } if (!$request->getURIData('only')) { $nav = $this->buildNav(); $nav->appendChild( array( $content, id(new PhabricatorGlobalUploadTargetView())->setUser($user), )); $content = $nav; } return $this->buildApplicationPage( $content, array( 'title' => 'Phabricator', )); } private function buildMainResponse(array $projects) { assert_instances_of($projects, 'PhabricatorProject'); $viewer = $this->getRequest()->getUser(); $has_maniphest = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorManiphestApplication', $viewer); $has_audit = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorAuditApplication', $viewer); $has_differential = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorDifferentialApplication', $viewer); if ($has_maniphest) { $unbreak_panel = $this->buildUnbreakNowPanel(); $triage_panel = $this->buildNeedsTriagePanel($projects); $tasks_panel = $this->buildTasksPanel(); } else { $unbreak_panel = null; $triage_panel = null; $tasks_panel = null; } if ($has_audit) { $audit_panel = $this->buildAuditPanel(); $commit_panel = $this->buildCommitPanel(); } else { $audit_panel = null; $commit_panel = null; } if (PhabricatorEnv::getEnvConfig('welcome.html') !== null) { $welcome_panel = $this->buildWelcomePanel(); } else { $welcome_panel = null; } if ($has_differential) { $revision_panel = $this->buildRevisionPanel(); } else { $revision_panel = null; } $home = phutil_tag( 'div', array( 'class' => 'homepage-panel', ), array( $welcome_panel, $unbreak_panel, $triage_panel, $revision_panel, $tasks_panel, $audit_panel, $commit_panel, $this->minipanels, )); return $home; } private function buildUnbreakNowPanel() { $unbreak_now = PhabricatorEnv::getEnvConfig( 'maniphest.priorities.unbreak-now'); if (!$unbreak_now) { return null; } $user = $this->getRequest()->getUser(); $task_query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->withPriorities(array($unbreak_now)) ->needProjectPHIDs(true) ->setLimit(10); $tasks = $task_query->execute(); if (!$tasks) { return $this->renderMiniPanel( pht('No "Unbreak Now!" Tasks'), pht('Nothing appears to be critically broken right now.')); } $href = urisprintf( '/maniphest/?statuses=open()&priorities=%s#R', $unbreak_now); $title = pht('Unbreak Now!'); $panel = new PHUIObjectBoxView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->setObjectList($this->buildTaskListView($tasks)); return $panel; } private function buildNeedsTriagePanel(array $projects) { assert_instances_of($projects, 'PhabricatorProject'); $needs_triage = PhabricatorEnv::getEnvConfig( 'maniphest.priorities.needs-triage'); if (!$needs_triage) { return null; } $user = $this->getRequest()->getUser(); if (!$user->isLoggedIn()) { return null; } if ($projects) { $task_query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->withPriorities(array($needs_triage)) ->withEdgeLogicPHIDs( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, PhabricatorQueryConstraint::OPERATOR_OR, mpull($projects, 'getPHID')) ->needProjectPHIDs(true) ->setLimit(10); $tasks = $task_query->execute(); } else { $tasks = array(); } if (!$tasks) { return $this->renderMiniPanel( pht('No "Needs Triage" Tasks'), - pht('No tasks in projects you are a member of need triage.')); + pht('No tasks tagged with projects you are a member of need triage.')); } $title = pht('Needs Triage'); $href = urisprintf( '/maniphest/?statuses=open()&priorities=%s&projects=projects(%s)#R', $needs_triage, $user->getPHID()); $panel = new PHUIObjectBoxView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->setObjectList($this->buildTaskListView($tasks)); return $panel; } private function buildRevisionPanel() { $user = $this->getRequest()->getUser(); $user_phid = $user->getPHID(); $revision_query = id(new DifferentialRevisionQuery()) ->setViewer($user) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->withResponsibleUsers(array($user_phid)) ->needRelationships(true) ->needFlags(true) ->needDrafts(true); $revisions = $revision_query->execute(); list($blocking, $active) = DifferentialRevisionQuery::splitResponsible( $revisions, array($user_phid)); if (!$blocking && !$active) { return $this->renderMiniPanel( pht('No Waiting Revisions'), pht('No revisions are waiting on you.')); } $title = pht('Revisions Waiting on You'); $href = '/differential'; $panel = new PHUIObjectBoxView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $revision_view = id(new DifferentialRevisionListView()) ->setHighlightAge(true) ->setRevisions(array_merge($blocking, $active)) ->setUser($user); $phids = array_merge( array($user_phid), $revision_view->getRequiredHandlePHIDs()); $handles = $this->loadViewerHandles($phids); $revision_view->setHandles($handles); $list_view = $revision_view->render(); $panel->setObjectList($list_view); return $panel; } private function buildWelcomePanel() { $panel = new PHUIObjectBoxView(); $panel->setHeaderText(pht('Welcome')); $panel->appendChild( phutil_safe_html( PhabricatorEnv::getEnvConfig('welcome.html'))); return $panel; } private function buildTasksPanel() { $user = $this->getRequest()->getUser(); $user_phid = $user->getPHID(); $task_query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->setGroupBy(ManiphestTaskQuery::GROUP_PRIORITY) ->withOwners(array($user_phid)) ->needProjectPHIDs(true) ->setLimit(10); $tasks = $task_query->execute(); if (!$tasks) { return $this->renderMiniPanel( pht('No Assigned Tasks'), pht('You have no assigned tasks.')); } $title = pht('Assigned Tasks'); $href = '/maniphest/query/assigned/'; $panel = new PHUIObjectBoxView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->setObjectList($this->buildTaskListView($tasks)); return $panel; } private function buildTaskListView(array $tasks) { assert_instances_of($tasks, 'ManiphestTask'); $user = $this->getRequest()->getUser(); $phids = array_merge( array_filter(mpull($tasks, 'getOwnerPHID')), array_mergev(mpull($tasks, 'getProjectPHIDs'))); $handles = $this->loadViewerHandles($phids); $view = new ManiphestTaskListView(); $view->setTasks($tasks); $view->setUser($user); $view->setHandles($handles); return $view; } private function renderSectionHeader($title, $href) { $title = phutil_tag( 'a', array( 'href' => $href, ), $title); $icon = id(new PHUIIconView()) ->setIconFont('fa-search') ->setHref($href); $header = id(new PHUIHeaderView()) ->setHeader($title) ->addActionIcon($icon); return $header; } private function renderMiniPanel($title, $body) { $panel = new PHUIInfoView(); $panel->setSeverity(PHUIInfoView::SEVERITY_NODATA); $panel->appendChild( phutil_tag( 'p', array( ), array( phutil_tag('strong', array(), $title.': '), $body, ))); $this->minipanels[] = $panel; } public function buildAuditPanel() { $request = $this->getRequest(); $user = $request->getUser(); $phids = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user); $query = id(new DiffusionCommitQuery()) ->setViewer($user) ->withNeedsAuditByPHIDs($phids) ->withAuditStatus(DiffusionCommitQuery::AUDIT_STATUS_OPEN) ->needAuditRequests(true) ->needCommitData(true) ->setLimit(10); $commits = $query->execute(); if (!$commits) { return $this->renderMinipanel( pht('No Audits'), pht('No commits are waiting for you to audit them.')); } $view = id(new PhabricatorAuditListView()) ->setCommits($commits) ->setUser($user); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); $title = pht('Audits'); $href = '/audit/'; $panel = new PHUIObjectBoxView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->setObjectList($view); return $panel; } public function buildCommitPanel() { $request = $this->getRequest(); $user = $request->getUser(); $phids = array($user->getPHID()); $query = id(new DiffusionCommitQuery()) ->setViewer($user) ->withAuthorPHIDs($phids) ->withAuditStatus(DiffusionCommitQuery::AUDIT_STATUS_CONCERN) ->needCommitData(true) ->needAuditRequests(true) ->setLimit(10); $commits = $query->execute(); if (!$commits) { return $this->renderMinipanel( pht('No Problem Commits'), pht('No one has raised concerns with your commits.')); } $view = id(new PhabricatorAuditListView()) ->setCommits($commits) ->setUser($user); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); $title = pht('Problem Commits'); $href = '/audit/'; $panel = new PHUIObjectBoxView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->setObjectList($view); return $panel; } } diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index 5f86c7a68b..4b29ba49f7 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -1,838 +1,839 @@ authorPHIDs = $authors; return $this; } public function withIDs(array $ids) { $this->taskIDs = $ids; return $this; } public function withPHIDs(array $phids) { $this->taskPHIDs = $phids; return $this; } public function withOwners(array $owners) { $no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN; $any_owner = PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN; foreach ($owners as $k => $phid) { if ($phid === $no_owner || $phid === null) { $this->noOwner = true; unset($owners[$k]); break; } if ($phid === $any_owner) { $this->anyOwner = true; unset($owners[$k]); break; } } $this->ownerPHIDs = $owners; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withPriorities(array $priorities) { $this->priorities = $priorities; return $this; } public function withSubpriorities(array $subpriorities) { $this->subpriorities = $subpriorities; return $this; } public function withSubpriorityBetween($min, $max) { $this->subpriorityMin = $min; $this->subpriorityMax = $max; return $this; } public function withSubscribers(array $subscribers) { $this->subscriberPHIDs = $subscribers; return $this; } public function withFullTextSearch($fulltext_search) { $this->fullTextSearch = $fulltext_search; return $this; } public function setGroupBy($group) { $this->groupBy = $group; switch ($this->groupBy) { case self::GROUP_NONE: $vector = array(); break; case self::GROUP_PRIORITY: $vector = array('priority'); break; case self::GROUP_OWNER: $vector = array('owner'); break; case self::GROUP_STATUS: $vector = array('status'); break; case self::GROUP_PROJECT: $vector = array('project'); break; } $this->setGroupVector($vector); return $this; } /** * True returns tasks that are blocking other tasks only. * False returns tasks that are not blocking other tasks only. * Null returns tasks regardless of blocking status. */ public function withBlockingTasks($mode) { $this->blockingTasks = $mode; return $this; } public function shouldJoinBlockingTasks() { return $this->blockingTasks !== null; } /** * True returns tasks that are blocked by other tasks only. * False returns tasks that are not blocked by other tasks only. * Null returns tasks regardless of blocked by status. */ public function withBlockedTasks($mode) { $this->blockedTasks = $mode; return $this; } public function shouldJoinBlockedTasks() { return $this->blockedTasks !== null; } public function withDateCreatedBefore($date_created_before) { $this->dateCreatedBefore = $date_created_before; return $this; } public function withDateCreatedAfter($date_created_after) { $this->dateCreatedAfter = $date_created_after; return $this; } public function withDateModifiedBefore($date_modified_before) { $this->dateModifiedBefore = $date_modified_before; return $this; } public function withDateModifiedAfter($date_modified_after) { $this->dateModifiedAfter = $date_modified_after; return $this; } public function needSubscriberPHIDs($bool) { $this->needSubscriberPHIDs = $bool; return $this; } public function needProjectPHIDs($bool) { $this->needProjectPHIDs = $bool; return $this; } public function newResultObject() { return new ManiphestTask(); } protected function loadPage() { $task_dao = new ManiphestTask(); $conn = $task_dao->establishConnection('r'); $where = $this->buildWhereClause($conn); $group_column = ''; switch ($this->groupBy) { case self::GROUP_PROJECT: $group_column = qsprintf( $conn, ', projectGroupName.indexedObjectPHID projectGroupPHID'); break; } $rows = queryfx_all( $conn, '%Q %Q FROM %T task %Q %Q %Q %Q %Q %Q', $this->buildSelectClause($conn), $group_column, $task_dao->getTableName(), $this->buildJoinClause($conn), $where, $this->buildGroupClause($conn), $this->buildHavingClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); switch ($this->groupBy) { case self::GROUP_PROJECT: $data = ipull($rows, null, 'id'); break; default: $data = $rows; break; } $tasks = $task_dao->loadAllFromArray($data); switch ($this->groupBy) { case self::GROUP_PROJECT: $results = array(); foreach ($rows as $row) { $task = clone $tasks[$row['id']]; $task->attachGroupByProjectPHID($row['projectGroupPHID']); $results[] = $task; } $tasks = $results; break; } return $tasks; } protected function willFilterPage(array $tasks) { if ($this->groupBy == self::GROUP_PROJECT) { // We should only return project groups which the user can actually see. $project_phids = mpull($tasks, 'getGroupByProjectPHID'); $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($project_phids) ->execute(); $projects = mpull($projects, null, 'getPHID'); foreach ($tasks as $key => $task) { if (!$task->getGroupByProjectPHID()) { - // This task is either not in any projects, or only in projects - // which we're ignoring because they're being queried for explicitly. + // This task is either not tagged with any projects, or only tagged + // with projects which we're ignoring because they're being queried + // for explicitly. continue; } if (empty($projects[$task->getGroupByProjectPHID()])) { unset($tasks[$key]); } } } return $tasks; } protected function didFilterPage(array $tasks) { $phids = mpull($tasks, 'getPHID'); if ($this->needProjectPHIDs) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($phids) ->withEdgeTypes( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, )); $edge_query->execute(); foreach ($tasks as $task) { $project_phids = $edge_query->getDestinationPHIDs( array($task->getPHID())); $task->attachProjectPHIDs($project_phids); } } if ($this->needSubscriberPHIDs) { $subscriber_sets = id(new PhabricatorSubscribersQuery()) ->withObjectPHIDs($phids) ->execute(); foreach ($tasks as $task) { $subscribers = idx($subscriber_sets, $task->getPHID(), array()); $task->attachSubscriberPHIDs($subscribers); } } return $tasks; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); $where[] = $this->buildStatusWhereClause($conn); $where[] = $this->buildDependenciesWhereClause($conn); $where[] = $this->buildOwnerWhereClause($conn); $where[] = $this->buildFullTextWhereClause($conn); if ($this->taskIDs !== null) { $where[] = qsprintf( $conn, 'task.id in (%Ld)', $this->taskIDs); } if ($this->taskPHIDs !== null) { $where[] = qsprintf( $conn, 'task.phid in (%Ls)', $this->taskPHIDs); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'task.status IN (%Ls)', $this->statuses); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( $conn, 'task.authorPHID in (%Ls)', $this->authorPHIDs); } if ($this->dateCreatedAfter) { $where[] = qsprintf( $conn, 'task.dateCreated >= %d', $this->dateCreatedAfter); } if ($this->dateCreatedBefore) { $where[] = qsprintf( $conn, 'task.dateCreated <= %d', $this->dateCreatedBefore); } if ($this->dateModifiedAfter) { $where[] = qsprintf( $conn, 'task.dateModified >= %d', $this->dateModifiedAfter); } if ($this->dateModifiedBefore) { $where[] = qsprintf( $conn, 'task.dateModified <= %d', $this->dateModifiedBefore); } if ($this->priorities !== null) { $where[] = qsprintf( $conn, 'task.priority IN (%Ld)', $this->priorities); } if ($this->subpriorities !== null) { $where[] = qsprintf( $conn, 'task.subpriority IN (%Lf)', $this->subpriorities); } if ($this->subpriorityMin !== null) { $where[] = qsprintf( $conn, 'task.subpriority >= %f', $this->subpriorityMin); } if ($this->subpriorityMax !== null) { $where[] = qsprintf( $conn, 'task.subpriority <= %f', $this->subpriorityMax); } return $where; } private function buildStatusWhereClause(AphrontDatabaseConnection $conn) { static $map = array( self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID, self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE, self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE, ); switch ($this->status) { case self::STATUS_ANY: return null; case self::STATUS_OPEN: return qsprintf( $conn, 'task.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); case self::STATUS_CLOSED: return qsprintf( $conn, 'task.status IN (%Ls)', ManiphestTaskStatus::getClosedStatusConstants()); default: $constant = idx($map, $this->status); if (!$constant) { throw new Exception(pht("Unknown status query '%s'!", $this->status)); } return qsprintf( $conn, 'task.status = %s', $constant); } } private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) { $subclause = array(); if ($this->noOwner) { $subclause[] = qsprintf( $conn, 'task.ownerPHID IS NULL'); } if ($this->anyOwner) { $subclause[] = qsprintf( $conn, 'task.ownerPHID IS NOT NULL'); } if ($this->ownerPHIDs) { $subclause[] = qsprintf( $conn, 'task.ownerPHID IN (%Ls)', $this->ownerPHIDs); } if (!$subclause) { return ''; } return '('.implode(') OR (', $subclause).')'; } private function buildFullTextWhereClause(AphrontDatabaseConnection $conn) { if (!strlen($this->fullTextSearch)) { return null; } // In doing a fulltext search, we first find all the PHIDs that match the // fulltext search, and then use that to limit the rest of the search $fulltext_query = id(new PhabricatorSavedQuery()) ->setEngineClassName('PhabricatorSearchApplicationSearchEngine') ->setParameter('query', $this->fullTextSearch); // NOTE: Setting this to something larger than 2^53 will raise errors in // ElasticSearch, and billions of results won't fit in memory anyway. $fulltext_query->setParameter('limit', 100000); $fulltext_query->setParameter('types', array(ManiphestTaskPHIDType::TYPECONST)); $engine = PhabricatorFulltextStorageEngine::loadEngine(); $fulltext_results = $engine->executeSearch($fulltext_query); if (empty($fulltext_results)) { $fulltext_results = array(null); } return qsprintf( $conn, 'task.phid IN (%Ls)', $fulltext_results); } private function buildDependenciesWhereClause( AphrontDatabaseConnection $conn) { if (!$this->shouldJoinBlockedTasks() && !$this->shouldJoinBlockingTasks()) { return null; } $parts = array(); if ($this->blockingTasks === true) { $parts[] = qsprintf( $conn, 'blocking.dst IS NOT NULL AND blockingtask.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } else if ($this->blockingTasks === false) { $parts[] = qsprintf( $conn, 'blocking.dst IS NULL OR blockingtask.status NOT IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } if ($this->blockedTasks === true) { $parts[] = qsprintf( $conn, 'blocked.dst IS NOT NULL AND blockedtask.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } else if ($this->blockedTasks === false) { $parts[] = qsprintf( $conn, 'blocked.dst IS NULL OR blockedtask.status NOT IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } return '('.implode(') OR (', $parts).')'; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) { $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE; $joins = array(); if ($this->shouldJoinBlockingTasks()) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T blocking ON blocking.src = task.phid '. 'AND blocking.type = %d '. 'LEFT JOIN %T blockingtask ON blocking.dst = blockingtask.phid', $edge_table, ManiphestTaskDependedOnByTaskEdgeType::EDGECONST, id(new ManiphestTask())->getTableName()); } if ($this->shouldJoinBlockedTasks()) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T blocked ON blocked.src = task.phid '. 'AND blocked.type = %d '. 'LEFT JOIN %T blockedtask ON blocked.dst = blockedtask.phid', $edge_table, ManiphestTaskDependsOnTaskEdgeType::EDGECONST, id(new ManiphestTask())->getTableName()); } if ($this->subscriberPHIDs !== null) { $joins[] = qsprintf( $conn_r, 'JOIN %T e_ccs ON e_ccs.src = task.phid '. 'AND e_ccs.type = %s '. 'AND e_ccs.dst in (%Ls)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorObjectHasSubscriberEdgeType::EDGECONST, $this->subscriberPHIDs); } switch ($this->groupBy) { case self::GROUP_PROJECT: $ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs(); if ($ignore_group_phids) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src AND projectGroup.type = %d AND projectGroup.dst NOT IN (%Ls)', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $ignore_group_phids); } else { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src AND projectGroup.type = %d', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); } $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroupName ON projectGroup.dst = projectGroupName.indexedObjectPHID', id(new ManiphestNameIndex())->getTableName()); break; } $joins[] = parent::buildJoinClauseParts($conn_r); return $joins; } protected function buildGroupClause(AphrontDatabaseConnection $conn_r) { $joined_multiple_rows = $this->shouldJoinBlockingTasks() || $this->shouldJoinBlockedTasks() || ($this->shouldGroupQueryResultRows()); $joined_project_name = ($this->groupBy == self::GROUP_PROJECT); // If we're joining multiple rows, we need to group the results by the // task IDs. if ($joined_multiple_rows) { if ($joined_project_name) { return 'GROUP BY task.phid, projectGroup.dst'; } else { return 'GROUP BY task.phid'; } } else { return ''; } } /** * Return project PHIDs which we should ignore when grouping tasks by * project. For example, if a user issues a query like: * - * Tasks in all projects: Frontend, Bugs + * Tasks tagged with all projects: Frontend, Bugs * * ...then we don't show "Frontend" or "Bugs" groups in the result set, since * they're meaningless as all results are in both groups. * * Similarly, for queries like: * - * Tasks in any projects: Public Relations + * Tasks tagged with any projects: Public Relations * * ...we ignore the single project, as every result is in that project. (In * the case that there are several "any" projects, we do not ignore them.) * * @return list Project PHIDs which should be ignored in query * construction. */ private function getIgnoreGroupedProjectPHIDs() { // Maybe we should also exclude the "OPERATOR_NOT" PHIDs? It won't // impact the results, but we might end up with a better query plan. // Investigate this on real data? This is likely very rare. $edge_types = array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, ); $phids = array(); $phids[] = $this->getEdgeLogicValues( $edge_types, array( PhabricatorQueryConstraint::OPERATOR_AND, )); $any = $this->getEdgeLogicValues( $edge_types, array( PhabricatorQueryConstraint::OPERATOR_OR, )); if (count($any) == 1) { $phids[] = $any; } return array_mergev($phids); } protected function getResultCursor($result) { $id = $result->getID(); if ($this->groupBy == self::GROUP_PROJECT) { return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.'); } return $id; } public function getBuiltinOrders() { $orders = array( 'priority' => array( 'vector' => array('priority', 'subpriority', 'id'), 'name' => pht('Priority'), 'aliases' => array(self::ORDER_PRIORITY), ), 'updated' => array( 'vector' => array('updated', 'id'), 'name' => pht('Date Updated (Latest First)'), 'aliases' => array(self::ORDER_MODIFIED), ), 'outdated' => array( 'vector' => array('-updated', '-id'), 'name' => pht('Date Updated (Oldest First)'), ), 'title' => array( 'vector' => array('title', 'id'), 'name' => pht('Title'), 'aliases' => array(self::ORDER_TITLE), ), ) + parent::getBuiltinOrders(); // Alias the "newest" builtin to the historical key for it. $orders['newest']['aliases'][] = self::ORDER_CREATED; $orders = array_select_keys( $orders, array( 'priority', 'updated', 'outdated', 'newest', 'oldest', 'title', )) + $orders; return $orders; } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'priority' => array( 'table' => 'task', 'column' => 'priority', 'type' => 'int', ), 'owner' => array( 'table' => 'task', 'column' => 'ownerOrdering', 'null' => 'head', 'reverse' => true, 'type' => 'string', ), 'status' => array( 'table' => 'task', 'column' => 'status', 'type' => 'string', 'reverse' => true, ), 'project' => array( 'table' => 'projectGroupName', 'column' => 'indexedObjectName', 'type' => 'string', 'null' => 'head', 'reverse' => true, ), 'title' => array( 'table' => 'task', 'column' => 'title', 'type' => 'string', 'reverse' => true, ), 'subpriority' => array( 'table' => 'task', 'column' => 'subpriority', 'type' => 'float', ), 'updated' => array( 'table' => 'task', 'column' => 'dateModified', 'type' => 'int', ), ); } protected function getPagingValueMap($cursor, array $keys) { $cursor_parts = explode('.', $cursor, 2); $task_id = $cursor_parts[0]; $group_id = idx($cursor_parts, 1); $task = $this->loadCursorObject($task_id); $map = array( 'id' => $task->getID(), 'priority' => $task->getPriority(), 'subpriority' => $task->getSubpriority(), 'owner' => $task->getOwnerOrdering(), 'status' => $task->getStatus(), 'title' => $task->getTitle(), 'updated' => $task->getDateModified(), ); foreach ($keys as $key) { switch ($key) { case 'project': $value = null; if ($group_id) { $paging_projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($group_id)) ->execute(); if ($paging_projects) { $value = head($paging_projects)->getName(); } } $map[$key] = $value; break; } } foreach ($keys as $key) { if ($this->isCustomFieldOrderKey($key)) { $map += $this->getPagingValueMapForCustomFields($task); break; } } return $map; } protected function getPrimaryTableAlias() { return 'task'; } public function getQueryApplicationClass() { return 'PhabricatorManiphestApplication'; } } diff --git a/src/applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php b/src/applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php index ccb29d86ef..cd481946b2 100644 --- a/src/applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php +++ b/src/applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php @@ -1,75 +1,75 @@ getPHID(); if ($object_phid) { $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object_phid, $project_edge_type); $project_phids = array_reverse($project_phids); } else { $project_phids = array(); } $projects_field = id(new PhabricatorProjectsEditField()) ->setKey('projectPHIDs') - ->setLabel(pht('Projects')) + ->setLabel(pht('Tags')) ->setEditTypeKey('projects') - ->setAliases(array('project', 'projects')) + ->setAliases(array('project', 'projects', 'tag', 'tags')) ->setIsCopyable(true) ->setUseEdgeTransactions(true) - ->setCommentActionLabel(pht('Change Projects')) - ->setDescription(pht('Select projects for the object.')) + ->setCommentActionLabel(pht('Change Project Tags')) + ->setDescription(pht('Select project tags for the object.')) ->setTransactionType($edge_type) ->setMetadataValue('edge:type', $project_edge_type) ->setValue($project_phids); $projects_field->setViewer($engine->getViewer()); $edit_add = $projects_field->getConduitEditType('projects.add') - ->setConduitDescription(pht('Add projects.')); + ->setConduitDescription(pht('Add project tags.')); $edit_set = $projects_field->getConduitEditType('projects.set') ->setConduitDescription( - pht('Set projects, overwriting current value.')); + pht('Set project tags, overwriting current value.')); $edit_rem = $projects_field->getConduitEditType('projects.remove') - ->setConduitDescription(pht('Remove projects.')); + ->setConduitDescription(pht('Remove project tags.')); return array( $projects_field, ); } } diff --git a/src/applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php b/src/applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php index 37d00e9391..031dad7577 100644 --- a/src/applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php +++ b/src/applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php @@ -1,60 +1,60 @@ withEdgeLogicConstraints( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $map['projectPHIDs']); } } public function getSearchFields($object) { $fields = array(); $fields[] = id(new PhabricatorProjectSearchField()) ->setKey('projectPHIDs') ->setConduitKey('projects') - ->setAliases(array('project', 'projects')) - ->setLabel(pht('Projects')) + ->setAliases(array('project', 'projects', 'tag', 'tags')) + ->setLabel(pht('Tags')) ->setDescription( - pht('Search for objects associated with given projects.')); + pht('Search for objects tagged with given projects.')); return $fields; } public function getSearchAttachments($object) { return array( id(new PhabricatorProjectsSearchEngineAttachment()) ->setAttachmentKey('projects'), ); } } diff --git a/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php b/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php index 156f04605f..d7d22f2417 100644 --- a/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php @@ -1,73 +1,74 @@ array( - 'name' => pht('Not In Any Projects'), - 'summary' => pht('Find results which are not in any projects.'), + 'name' => pht('Not Tagged With Any Projects'), + 'summary' => pht( + 'Find results which are not tagged with any projects.'), 'description' => pht( - "This function matches results which are not associated with any ". + "This function matches results which are not tagged with any ". "projects. It is usually most often used to find objects which ". "might have slipped through the cracks and not been organized ". "properly.\n\n%s", '> null()'), ), ); } public function loadResults() { $results = array( $this->buildNullResult(), ); return $this->filterResultsAgainstTokens($results); } protected function evaluateFunction($function, array $argv_list) { $results = array(); foreach ($argv_list as $argv) { $results[] = new PhabricatorQueryConstraint( PhabricatorQueryConstraint::OPERATOR_NULL, 'empty'); } return $results; } public function renderFunctionTokens($function, array $argv_list) { $results = array(); foreach ($argv_list as $argv) { $results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( $this->buildNullResult()); } return $results; } private function buildNullResult() { - $name = pht('Not In Any Projects'); + $name = pht('Not Tagged With Any Projects'); return $this->newFunctionResult() ->setUnique(true) ->setPHID('null()') ->setIcon('fa-ban') ->setName('null '.$name) ->setDisplayName($name); } } diff --git a/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php b/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php index e1502294d3..835e483cff 100644 --- a/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php +++ b/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php @@ -1,283 +1,283 @@ setParameter('query', $request->getStr('query')); $saved->setParameter( 'statuses', $this->readListFromRequest($request, 'statuses')); $saved->setParameter( 'types', $this->readListFromRequest($request, 'types')); $saved->setParameter( 'authorPHIDs', $this->readUsersFromRequest($request, 'authorPHIDs')); $saved->setParameter( 'ownerPHIDs', $this->readUsersFromRequest($request, 'ownerPHIDs')); $saved->setParameter( 'subscriberPHIDs', $this->readSubscribersFromRequest($request, 'subscriberPHIDs')); $saved->setParameter( 'projectPHIDs', $this->readPHIDsFromRequest($request, 'projectPHIDs')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = new PhabricatorSearchDocumentQuery(); // Convert the saved query into a resolved form (without typeahead // functions) which the fulltext search engines can execute. $config = clone $saved; $viewer = $this->requireViewer(); $datasource = id(new PhabricatorPeopleOwnerDatasource()) ->setViewer($viewer); $owner_phids = $this->readOwnerPHIDs($config); $owner_phids = $datasource->evaluateTokens($owner_phids); foreach ($owner_phids as $key => $phid) { if ($phid == PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN) { $config->setParameter('withUnowned', true); unset($owner_phids[$key]); } if ($phid == PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN) { $config->setParameter('withAnyOwner', true); unset($owner_phids[$key]); } } $config->setParameter('ownerPHIDs', $owner_phids); $datasource = id(new PhabricatorPeopleUserFunctionDatasource()) ->setViewer($viewer); $author_phids = $config->getParameter('authorPHIDs', array()); $author_phids = $datasource->evaluateTokens($author_phids); $config->setParameter('authorPHIDs', $author_phids); $datasource = id(new PhabricatorMetaMTAMailableFunctionDatasource()) ->setViewer($viewer); $subscriber_phids = $config->getParameter('subscriberPHIDs', array()); $subscriber_phids = $datasource->evaluateTokens($subscriber_phids); $config->setParameter('subscriberPHIDs', $subscriber_phids); $query->withSavedQuery($config); return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $options = array(); $author_value = null; $owner_value = null; $subscribers_value = null; $project_value = null; $author_phids = $saved->getParameter('authorPHIDs', array()); $owner_phids = $this->readOwnerPHIDs($saved); $subscriber_phids = $saved->getParameter('subscriberPHIDs', array()); $project_phids = $saved->getParameter('projectPHIDs', array()); $status_values = $saved->getParameter('statuses', array()); $status_values = array_fuse($status_values); $statuses = array( PhabricatorSearchRelationship::RELATIONSHIP_OPEN => pht('Open'), PhabricatorSearchRelationship::RELATIONSHIP_CLOSED => pht('Closed'), ); $status_control = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Document Status')); foreach ($statuses as $status => $name) { $status_control->addCheckbox( 'statuses[]', $status, $name, isset($status_values[$status])); } $type_values = $saved->getParameter('types', array()); $type_values = array_fuse($type_values); $types_control = id(new AphrontFormTokenizerControl()) ->setLabel(pht('Document Types')) ->setName('types') ->setDatasource(new PhabricatorSearchDocumentTypeDatasource()) ->setValue($type_values); $form ->appendChild( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'jump', 'value' => 'no', ))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Query')) ->setName('query') ->setValue($saved->getParameter('query'))) ->appendChild($status_control) ->appendControl($types_control) ->appendControl( id(new AphrontFormTokenizerControl()) ->setName('authorPHIDs') ->setLabel(pht('Authors')) ->setDatasource(new PhabricatorPeopleUserFunctionDatasource()) ->setValue($author_phids)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setName('ownerPHIDs') ->setLabel(pht('Owners')) ->setDatasource(new PhabricatorPeopleOwnerDatasource()) ->setValue($owner_phids)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setName('subscriberPHIDs') ->setLabel(pht('Subscribers')) ->setDatasource(new PhabricatorMetaMTAMailableFunctionDatasource()) ->setValue($subscriber_phids)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setName('projectPHIDs') - ->setLabel(pht('In Any Project')) + ->setLabel(pht('Tags')) ->setDatasource(new PhabricatorProjectDatasource()) ->setValue($project_phids)); } protected function getURI($path) { return '/search/'.$path; } protected function getBuiltinQueryNames() { return array( 'all' => pht('All Documents'), 'open' => pht('Open Documents'), 'open-tasks' => pht('Open Tasks'), ); } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; case 'open': return $query->setParameter('statuses', array('open')); case 'open-tasks': return $query ->setParameter('statuses', array('open')) ->setParameter('types', array(ManiphestTaskPHIDType::TYPECONST)); } return parent::buildSavedQueryFromBuiltin($query_key); } public static function getIndexableDocumentTypes( PhabricatorUser $viewer = null) { // TODO: This is inelegant and not very efficient, but gets us reasonable // results. It would be nice to do this more elegantly. $objects = id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorFulltextInterface') ->execute(); $type_map = array(); foreach ($objects as $object) { $phid_type = phid_get_type($object->generatePHID()); $type_map[$phid_type] = $object; } if ($viewer) { $types = PhabricatorPHIDType::getAllInstalledTypes($viewer); } else { $types = PhabricatorPHIDType::getAllTypes(); } $results = array(); foreach ($types as $type) { $typeconst = $type->getTypeConstant(); $object = idx($type_map, $typeconst); if ($object) { $results[$typeconst] = $type->getTypeName(); } } asort($results); return $results; } public function shouldUseOffsetPaging() { return true; } protected function renderResultList( array $results, PhabricatorSavedQuery $query, array $handles) { $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); $list->setNoDataString(pht('No results found.')); if ($results) { $objects = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(mpull($results, 'getPHID')) ->execute(); foreach ($results as $phid => $handle) { $view = id(new PhabricatorSearchResultView()) ->setHandle($handle) ->setQuery($query) ->setObject(idx($objects, $phid)) ->render(); $list->addItem($view); } } $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($list); return $result; } private function readOwnerPHIDs(PhabricatorSavedQuery $saved) { $owner_phids = $saved->getParameter('ownerPHIDs', array()); // This was an old checkbox from before typeahead functions. if ($saved->getParameter('withUnowned')) { $owner_phids[] = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN; } return $owner_phids; } }