diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 27cf1bf344..9ca30c22ed 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -1,285 +1,285 @@ loadProject(); if ($response) { return $response; } $viewer = $request->getUser(); $project = $this->getProject(); $id = $project->getID(); $picture = $project->getProfileImageURI(); $icon = $project->getDisplayIconIcon(); $icon_name = $project->getDisplayIconName(); $tag = id(new PHUITagView()) ->setIcon($icon) ->setName($icon_name) ->addClass('project-view-header-tag') ->setType(PHUITagView::TYPE_SHADE); $header = id(new PHUIHeaderView()) ->setHeader(array($project->getDisplayName(), $tag)) ->setUser($viewer) ->setPolicyObject($project) ->setImage($picture) ->setProfileHeader(true); if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ACTIVE) { $header->setStatus('fa-check', 'bluegrey', pht('Active')); } else { $header->setStatus('fa-ban', 'red', pht('Archived')); } $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); if ($can_edit) { $header->setImageEditURL($this->getApplicationURI("picture/{$id}/")); } $properties = $this->buildPropertyListView($project); $watch_action = $this->renderWatchAction($project); $header->addActionLink($watch_action); $milestone_list = $this->buildMilestoneList($project); $subproject_list = $this->buildSubprojectList($project); $member_list = id(new PhabricatorProjectMemberListView()) ->setUser($viewer) ->setProject($project) ->setLimit(5) ->setBackground(PHUIBoxView::GREY) ->setUserPHIDs($project->getMemberPHIDs()); $watcher_list = id(new PhabricatorProjectWatcherListView()) ->setUser($viewer) ->setProject($project) ->setLimit(5) ->setBackground(PHUIBoxView::GREY) ->setUserPHIDs($project->getWatcherPHIDs()); $nav = $this->getProfileMenu(); $nav->selectFilter(PhabricatorProject::PANEL_PROFILE); $stories = id(new PhabricatorFeedQuery()) ->setViewer($viewer) ->setFilterPHIDs( array( $project->getPHID(), )) ->setLimit(50) ->execute(); $feed = $this->renderStories($stories); $feed = phutil_tag_div('project-view-feed', $feed); $columns = id(new PHUITwoColumnView()) ->setMainColumn( array( $properties, $feed, )) ->setSideColumn( array( $milestone_list, $subproject_list, $member_list, $watcher_list, )); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setBorder(true); require_celerity_resource('project-view-css'); $home = phutil_tag( 'div', array( 'class' => 'project-view-home', ), array( $header, $columns, )); return $this->newPage() ->setNavigation($nav) ->setCrumbs($crumbs) ->setTitle($project->getDisplayName()) ->setPageObjectPHIDs(array($project->getPHID())) ->appendChild( array( $home, )); } private function buildPropertyListView( PhabricatorProject $project) { $request = $this->getRequest(); $viewer = $request->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($project); $field_list = PhabricatorCustomField::getObjectFields( $project, PhabricatorCustomField::ROLE_VIEW); $field_list->appendFieldsToPropertyList($project, $viewer, $view); if (!$view->hasAnyProperties()) { return null; } $view = id(new PHUIBoxView()) ->setColor(PHUIBoxView::GREY) ->appendChild($view) ->addClass('project-view-properties'); return $view; } private function renderStories(array $stories) { assert_instances_of($stories, 'PhabricatorFeedStory'); $builder = new PhabricatorFeedBuilder($stories); $builder->setUser($this->getRequest()->getUser()); $builder->setShowHovercards(true); $view = $builder->buildView(); return $view; } private function renderWatchAction(PhabricatorProject $project) { $viewer = $this->getViewer(); $viewer_phid = $viewer->getPHID(); $id = $project->getID(); $is_watcher = ($viewer_phid && $project->isUserWatcher($viewer_phid)); if (!$is_watcher) { $watch_icon = 'fa-eye'; $watch_text = pht('Watch Project'); $watch_href = "/project/watch/{$id}/?via=profile"; } else { $watch_icon = 'fa-eye-slash'; $watch_text = pht('Unwatch Project'); $watch_href = "/project/unwatch/{$id}/?via=profile"; } $watch_icon = id(new PHUIIconView()) ->setIcon($watch_icon); return id(new PHUIButtonView()) ->setTag('a') ->setWorkflow(true) ->setIcon($watch_icon) ->setText($watch_text) ->setHref($watch_href); } private function buildMilestoneList(PhabricatorProject $project) { if (!$project->getHasMilestones()) { return null; } $viewer = $this->getViewer(); $id = $project->getID(); $milestones = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withParentProjectPHIDs(array($project->getPHID())) ->needImages(true) ->withIsMilestone(true) ->withStatuses( array( PhabricatorProjectStatus::STATUS_ACTIVE, )) - ->setOrder('newest') + ->setOrderVector(array('milestoneNumber', 'id')) ->execute(); if (!$milestones) { return null; } $milestone_list = id(new PhabricatorProjectListView()) ->setUser($viewer) ->setProjects($milestones) ->renderList(); $view_all = id(new PHUIButtonView()) ->setTag('a') ->setIcon( id(new PHUIIconView()) ->setIcon('fa-list-ul')) ->setText(pht('View All')) ->setHref("/project/subprojects/{$id}/"); $header = id(new PHUIHeaderView()) ->setHeader(pht('Milestones')) ->addActionLink($view_all); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIBoxView::GREY) ->setObjectList($milestone_list); } private function buildSubprojectList(PhabricatorProject $project) { if (!$project->getHasSubprojects()) { return null; } $viewer = $this->getViewer(); $id = $project->getID(); $limit = 25; $subprojects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withParentProjectPHIDs(array($project->getPHID())) ->needImages(true) ->withStatuses( array( PhabricatorProjectStatus::STATUS_ACTIVE, )) ->withIsMilestone(false) ->setLimit($limit) ->execute(); if (!$subprojects) { return null; } $subproject_list = id(new PhabricatorProjectListView()) ->setUser($viewer) ->setProjects($subprojects) ->renderList(); $view_all = id(new PHUIButtonView()) ->setTag('a') ->setIcon( id(new PHUIIconView()) ->setIcon('fa-list-ul')) ->setText(pht('View All')) ->setHref("/project/subprojects/{$id}/"); $header = id(new PHUIHeaderView()) ->setHeader(pht('Subprojects')) ->addActionLink($view_all); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIBoxView::GREY) ->setObjectList($subproject_list); } } diff --git a/src/applications/project/controller/PhabricatorProjectSubprojectsController.php b/src/applications/project/controller/PhabricatorProjectSubprojectsController.php index afafed77b1..36f9d641a8 100644 --- a/src/applications/project/controller/PhabricatorProjectSubprojectsController.php +++ b/src/applications/project/controller/PhabricatorProjectSubprojectsController.php @@ -1,249 +1,249 @@ getViewer(); $response = $this->loadProject(); if ($response) { return $response; } $project = $this->getProject(); $id = $project->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $allows_subprojects = $project->supportsSubprojects(); $allows_milestones = $project->supportsMilestones(); if ($allows_subprojects) { $subprojects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withParentProjectPHIDs(array($project->getPHID())) ->needImages(true) ->withIsMilestone(false) ->execute(); } else { $subprojects = array(); } if ($allows_milestones) { $milestones = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withParentProjectPHIDs(array($project->getPHID())) ->needImages(true) ->withIsMilestone(true) - ->setOrder('newest') + ->setOrderVector(array('milestoneNumber', 'id')) ->execute(); } else { $milestones = array(); } if ($milestones) { $milestone_list = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Milestones')) ->setObjectList( id(new PhabricatorProjectListView()) ->setUser($viewer) ->setProjects($milestones) ->renderList()); } else { $milestone_list = null; } if ($subprojects) { $subproject_list = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Subprojects')) ->setObjectList( id(new PhabricatorProjectListView()) ->setUser($viewer) ->setProjects($subprojects) ->renderList()); } else { $subproject_list = null; } $property_list = $this->buildPropertyList( $project, $milestones, $subprojects); $action_list = $this->buildActionList( $project, $milestones, $subprojects); $property_list->setActionList($action_list); $header_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Subprojects and Milestones')) ->addPropertyList($property_list); $nav = $this->getProfileMenu(); $nav->selectFilter(PhabricatorProject::PANEL_SUBPROJECTS); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Subprojects')); return $this->newPage() ->setNavigation($nav) ->setCrumbs($crumbs) ->setTitle(array($project->getName(), pht('Subprojects'))) ->appendChild( array( $header_box, $milestone_list, $subproject_list, )); } private function buildPropertyList( PhabricatorProject $project, array $milestones, array $subprojects) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); $view->addProperty( pht('Prototype'), $this->renderStatus( 'fa-exclamation-triangle red', pht('Warning'), pht('Subprojects and milestones are only partially implemented.'))); if (!$project->supportsMilestones()) { $milestone_status = $this->renderStatus( 'fa-times grey', pht('Already Milestone'), pht( 'This project is already a milestone, and milestones may not '. 'have their own milestones.')); } else { if (!$milestones) { $milestone_status = $this->renderStatus( 'fa-check grey', pht('None Created'), pht( 'You can create milestones for this project.')); } else { $milestone_status = $this->renderStatus( 'fa-check green', pht('Has Milestones'), pht('This project has milestones.')); } } $view->addProperty(pht('Milestones'), $milestone_status); if (!$project->supportsSubprojects()) { $subproject_status = $this->renderStatus( 'fa-times grey', pht('Milestone'), pht( 'This project is a milestone, and milestones may not have '. 'subprojects.')); } else { if (!$subprojects) { $subproject_status = $this->renderStatus( 'fa-check grey', pht('None Created'), pht('You can create subprojects for this project.')); } else { $subproject_status = $this->renderStatus( 'fa-check green', pht('Has Subprojects'), pht( 'This project has subprojects.')); } } $view->addProperty(pht('Subprojects'), $subproject_status); return $view; } private function buildActionList( PhabricatorProject $project, array $milestones, array $subprojects) { $viewer = $this->getViewer(); $id = $project->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $allows_milestones = $project->supportsMilestones(); $allows_subprojects = $project->supportsSubprojects(); $view = id(new PhabricatorActionListView()) ->setUser($viewer); if ($allows_milestones && $milestones) { $milestone_text = pht('Create Next Milestone'); } else { $milestone_text = pht('Create Milestone'); } $can_milestone = ($can_edit && $allows_milestones); $milestone_href = "/project/edit/?milestone={$id}"; $view->addAction( id(new PhabricatorActionView()) ->setName($milestone_text) ->setIcon('fa-plus') ->setHref($milestone_href) ->setDisabled(!$can_milestone) ->setWorkflow(!$can_milestone)); $can_subproject = ($can_edit && $allows_subprojects); // If we're offering to create the first subproject, we're going to warn // the user about the effects before moving forward. if ($can_subproject && !$subprojects) { $subproject_href = "/project/warning/{$id}/"; $subproject_disabled = false; $subproject_workflow = true; } else { $subproject_href = "/project/edit/?parent={$id}"; $subproject_disabled = !$can_subproject; $subproject_workflow = !$can_subproject; } $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Create Subproject')) ->setIcon('fa-plus') ->setHref($subproject_href) ->setDisabled($subproject_disabled) ->setWorkflow($subproject_workflow)); return $view; } private function renderStatus($icon, $target, $note) { $item = id(new PHUIStatusItemView()) ->setIcon($icon) ->setTarget(phutil_tag('strong', array(), $target)) ->setNote($note); return id(new PHUIStatusListView()) ->addItem($item); } } diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index d12e66e392..b72fa1cec2 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -1,758 +1,763 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withMemberPHIDs(array $member_phids) { $this->memberPHIDs = $member_phids; return $this; } public function withSlugs(array $slugs) { $this->slugs = $slugs; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withNameTokens(array $tokens) { $this->nameTokens = array_values($tokens); return $this; } public function withIcons(array $icons) { $this->icons = $icons; return $this; } public function withColors(array $colors) { $this->colors = $colors; return $this; } public function withParentProjectPHIDs($parent_phids) { $this->parentPHIDs = $parent_phids; return $this; } public function withAncestorProjectPHIDs($ancestor_phids) { $this->ancestorPHIDs = $ancestor_phids; return $this; } public function withIsMilestone($is_milestone) { $this->isMilestone = $is_milestone; return $this; } public function withHasSubprojects($has_subprojects) { $this->hasSubprojects = $has_subprojects; return $this; } public function withDepthBetween($min, $max) { $this->minDepth = $min; $this->maxDepth = $max; return $this; } public function withMilestoneNumberBetween($min, $max) { $this->minMilestoneNumber = $min; $this->maxMilestoneNumber = $max; return $this; } public function needMembers($need_members) { $this->needMembers = $need_members; return $this; } public function needAncestorMembers($need_ancestor_members) { $this->needAncestorMembers = $need_ancestor_members; return $this; } public function needWatchers($need_watchers) { $this->needWatchers = $need_watchers; return $this; } public function needImages($need_images) { $this->needImages = $need_images; return $this; } public function needSlugs($need_slugs) { $this->needSlugs = $need_slugs; return $this; } public function newResultObject() { return new PhabricatorProject(); } protected function getDefaultOrderVector() { return array('name'); } public function getBuiltinOrders() { return array( 'name' => array( 'vector' => array('name'), 'name' => pht('Name'), ), ) + parent::getBuiltinOrders(); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'name' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'name', 'reverse' => true, 'type' => 'string', 'unique' => true, ), + 'milestoneNumber' => array( + 'table' => $this->getPrimaryTableAlias(), + 'column' => 'milestoneNumber', + 'type' => 'int', + ), ); } protected function getPagingValueMap($cursor, array $keys) { $project = $this->loadCursorObject($cursor); return array( 'name' => $project->getName(), ); } public function getSlugMap() { if ($this->slugMap === null) { throw new PhutilInvalidStateException('execute'); } return $this->slugMap; } protected function willExecute() { $this->slugMap = array(); $this->slugNormals = array(); $this->allSlugs = array(); if ($this->slugs) { foreach ($this->slugs as $slug) { if (PhabricatorSlug::isValidProjectSlug($slug)) { $normal = PhabricatorSlug::normalizeProjectSlug($slug); $this->slugNormals[$slug] = $normal; $this->allSlugs[$normal] = $normal; } // NOTE: At least for now, we query for the normalized slugs but also // for the slugs exactly as entered. This allows older projects with // slugs that are no longer valid to continue to work. $this->allSlugs[$slug] = $slug; } } } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $projects) { $ancestor_paths = array(); foreach ($projects as $project) { foreach ($project->getAncestorProjectPaths() as $path) { $ancestor_paths[$path] = $path; } } if ($ancestor_paths) { $ancestors = id(new PhabricatorProject())->loadAllWhere( 'projectPath IN (%Ls)', $ancestor_paths); } else { $ancestors = array(); } $projects = $this->linkProjectGraph($projects, $ancestors); $viewer_phid = $this->getViewer()->getPHID(); $material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST; $watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST; $types = array(); $types[] = $material_type; if ($this->needWatchers) { $types[] = $watcher_type; } $all_graph = $this->getAllReachableAncestors($projects); if ($this->needAncestorMembers) { $src_projects = $all_graph; } else { $src_projects = $projects; } $all_sources = array(); foreach ($src_projects as $project) { if ($project->isMilestone()) { $phid = $project->getParentProjectPHID(); } else { $phid = $project->getPHID(); } $all_sources[$phid] = $phid; } $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($all_sources) ->withEdgeTypes($types); $need_all_edges = $this->needMembers || $this->needWatchers || $this->needAncestorMembers; // If we only need to know if the viewer is a member, we can restrict // the query to just their PHID. $any_edges = true; if (!$need_all_edges) { if ($viewer_phid) { $edge_query->withDestinationPHIDs(array($viewer_phid)); } else { // If we don't need members or watchers and don't have a viewer PHID // (viewer is logged-out or omnipotent), they'll never be a member // so we don't need to issue this query at all. $any_edges = false; } } if ($any_edges) { $edge_query->execute(); } $membership_projects = array(); foreach ($src_projects as $project) { $project_phid = $project->getPHID(); if ($project->isMilestone()) { $source_phids = array($project->getParentProjectPHID()); } else { $source_phids = array($project_phid); } if ($any_edges) { $member_phids = $edge_query->getDestinationPHIDs( $source_phids, array($material_type)); } else { $member_phids = array(); } if (in_array($viewer_phid, $member_phids)) { $membership_projects[$project_phid] = $project; } if ($this->needMembers || $this->needAncestorMembers) { $project->attachMemberPHIDs($member_phids); } if ($this->needWatchers) { $watcher_phids = $edge_query->getDestinationPHIDs( $source_phids, array($watcher_type)); $project->attachWatcherPHIDs($watcher_phids); $project->setIsUserWatcher( $viewer_phid, in_array($viewer_phid, $watcher_phids)); } } // If we loaded ancestor members, we've already populated membership // lists above, so we can skip this step. if (!$this->needAncestorMembers) { $member_graph = $this->getAllReachableAncestors($membership_projects); foreach ($all_graph as $phid => $project) { $is_member = isset($member_graph[$phid]); $project->setIsUserMember($viewer_phid, $is_member); } } return $projects; } protected function didFilterPage(array $projects) { if ($this->needImages) { $default = null; $file_phids = mpull($projects, 'getProfileImagePHID'); $file_phids = array_filter($file_phids); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); } else { $files = array(); } foreach ($projects as $project) { $file = idx($files, $project->getProfileImagePHID()); if (!$file) { if (!$default) { $default = PhabricatorFile::loadBuiltin( $this->getViewer(), 'project.png'); } $file = $default; } $project->attachProfileImageFile($file); } } $this->loadSlugs($projects); return $projects; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->status != self::STATUS_ANY) { switch ($this->status) { case self::STATUS_OPEN: case self::STATUS_ACTIVE: $filter = array( PhabricatorProjectStatus::STATUS_ACTIVE, ); break; case self::STATUS_CLOSED: case self::STATUS_ARCHIVED: $filter = array( PhabricatorProjectStatus::STATUS_ARCHIVED, ); break; default: throw new Exception( pht( "Unknown project status '%s'!", $this->status)); } $where[] = qsprintf( $conn, 'status IN (%Ld)', $filter); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'status IN (%Ls)', $this->statuses); } if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } if ($this->memberPHIDs !== null) { $where[] = qsprintf( $conn, 'e.dst IN (%Ls)', $this->memberPHIDs); } if ($this->slugs !== null) { $where[] = qsprintf( $conn, 'slug.slug IN (%Ls)', $this->allSlugs); } if ($this->names !== null) { $where[] = qsprintf( $conn, 'name IN (%Ls)', $this->names); } if ($this->icons !== null) { $where[] = qsprintf( $conn, 'icon IN (%Ls)', $this->icons); } if ($this->colors !== null) { $where[] = qsprintf( $conn, 'color IN (%Ls)', $this->colors); } if ($this->parentPHIDs !== null) { $where[] = qsprintf( $conn, 'parentProjectPHID IN (%Ls)', $this->parentPHIDs); } if ($this->ancestorPHIDs !== null) { $ancestor_paths = queryfx_all( $conn, 'SELECT projectPath, projectDepth FROM %T WHERE phid IN (%Ls)', id(new PhabricatorProject())->getTableName(), $this->ancestorPHIDs); if (!$ancestor_paths) { throw new PhabricatorEmptyQueryException(); } $sql = array(); foreach ($ancestor_paths as $ancestor_path) { $sql[] = qsprintf( $conn, '(projectPath LIKE %> AND projectDepth > %d)', $ancestor_path['projectPath'], $ancestor_path['projectDepth']); } $where[] = '('.implode(' OR ', $sql).')'; $where[] = qsprintf( $conn, 'parentProjectPHID IS NOT NULL'); } if ($this->isMilestone !== null) { if ($this->isMilestone) { $where[] = qsprintf( $conn, 'milestoneNumber IS NOT NULL'); } else { $where[] = qsprintf( $conn, 'milestoneNumber IS NULL'); } } if ($this->hasSubprojects !== null) { $where[] = qsprintf( $conn, 'hasSubprojects = %d', (int)$this->hasSubprojects); } if ($this->minDepth !== null) { $where[] = qsprintf( $conn, 'projectDepth >= %d', $this->minDepth); } if ($this->maxDepth !== null) { $where[] = qsprintf( $conn, 'projectDepth <= %d', $this->maxDepth); } if ($this->minMilestoneNumber !== null) { $where[] = qsprintf( $conn, 'milestoneNumber >= %d', $this->minMilestoneNumber); } if ($this->maxMilestoneNumber !== null) { $where[] = qsprintf( $conn, 'milestoneNumber <= %d', $this->maxMilestoneNumber); } return $where; } protected function shouldGroupQueryResultRows() { if ($this->memberPHIDs || $this->nameTokens) { return true; } return parent::shouldGroupQueryResultRows(); } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn, 'JOIN %T e ON e.src = p.phid AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorProjectMaterializedMemberEdgeType::EDGECONST); } if ($this->slugs !== null) { $joins[] = qsprintf( $conn, 'JOIN %T slug on slug.projectPHID = p.phid', id(new PhabricatorProjectSlug())->getTableName()); } if ($this->nameTokens !== null) { foreach ($this->nameTokens as $key => $token) { $token_table = 'token_'.$key; $joins[] = qsprintf( $conn, 'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>', PhabricatorProject::TABLE_DATASOURCE_TOKEN, $token_table, $token_table, $token_table, $token); } } return $joins; } public function getQueryApplicationClass() { return 'PhabricatorProjectApplication'; } protected function getPrimaryTableAlias() { return 'p'; } private function linkProjectGraph(array $projects, array $ancestors) { $ancestor_map = mpull($ancestors, null, 'getPHID'); $projects_map = mpull($projects, null, 'getPHID'); $all_map = $projects_map + $ancestor_map; $done = array(); foreach ($projects as $key => $project) { $seen = array($project->getPHID() => true); if (!$this->linkProject($project, $all_map, $done, $seen)) { $this->didRejectResult($project); unset($projects[$key]); continue; } foreach ($project->getAncestorProjects() as $ancestor) { $seen[$ancestor->getPHID()] = true; } } return $projects; } private function linkProject($project, array $all, array $done, array $seen) { $parent_phid = $project->getParentProjectPHID(); // This project has no parent, so just attach `null` and return. if (!$parent_phid) { $project->attachParentProject(null); return true; } // This project has a parent, but it failed to load. if (empty($all[$parent_phid])) { return false; } // Test for graph cycles. If we encounter one, we're going to hide the // entire cycle since we can't meaningfully resolve it. if (isset($seen[$parent_phid])) { return false; } $seen[$parent_phid] = true; $parent = $all[$parent_phid]; $project->attachParentProject($parent); if (!empty($done[$parent_phid])) { return true; } return $this->linkProject($parent, $all, $done, $seen); } private function getAllReachableAncestors(array $projects) { $ancestors = array(); $seen = mpull($projects, null, 'getPHID'); $stack = $projects; while ($stack) { $project = array_pop($stack); $phid = $project->getPHID(); $ancestors[$phid] = $project; $parent_phid = $project->getParentProjectPHID(); if (!$parent_phid) { continue; } if (isset($seen[$parent_phid])) { continue; } $seen[$parent_phid] = true; $stack[] = $project->getParentProject(); } return $ancestors; } private function loadSlugs(array $projects) { // Build a map from primary slugs to projects. $primary_map = array(); foreach ($projects as $project) { $primary_slug = $project->getPrimarySlug(); if ($primary_slug === null) { continue; } $primary_map[$primary_slug] = $project; } // Link up all of the queried slugs which correspond to primary // slugs. If we can link up everything from this (no slugs were queried, // or only primary slugs were queried) we don't need to load anything // else. $unknown = $this->slugNormals; foreach ($unknown as $input => $normal) { if (isset($primary_map[$input])) { $match = $input; } else if (isset($primary_map[$normal])) { $match = $normal; } else { continue; } $this->slugMap[$input] = array( 'slug' => $match, 'projectPHID' => $primary_map[$match]->getPHID(), ); unset($unknown[$input]); } // If we need slugs, we have to load everything. // If we still have some queried slugs which we haven't mapped, we only // need to look for them. // If we've mapped everything, we don't have to do any work. $project_phids = mpull($projects, 'getPHID'); if ($this->needSlugs) { $slugs = id(new PhabricatorProjectSlug())->loadAllWhere( 'projectPHID IN (%Ls)', $project_phids); } else if ($unknown) { $slugs = id(new PhabricatorProjectSlug())->loadAllWhere( 'projectPHID IN (%Ls) AND slug IN (%Ls)', $project_phids, $unknown); } else { $slugs = array(); } // Link up any slugs we were not able to link up earlier. $extra_map = mpull($slugs, 'getProjectPHID', 'getSlug'); foreach ($unknown as $input => $normal) { if (isset($extra_map[$input])) { $match = $input; } else if (isset($extra_map[$normal])) { $match = $normal; } else { continue; } $this->slugMap[$input] = array( 'slug' => $match, 'projectPHID' => $extra_map[$match], ); unset($unknown[$input]); } if ($this->needSlugs) { $slug_groups = mgroup($slugs, 'getProjectPHID'); foreach ($projects as $project) { $project_slugs = idx($slug_groups, $project->getPHID(), array()); $project->attachSlugs($project_slugs); } } } }