Changeset View
Changeset View
Standalone View
Standalone View
src/applications/maniphest/query/ManiphestTaskQuery.php
| <?php | <?php | ||||
| /** | /** | ||||
| * Query tasks by specific criteria. This class uses the higher-performance | * Query tasks by specific criteria. This class uses the higher-performance | ||||
| * but less-general Maniphest indexes to satisfy queries. | * but less-general Maniphest indexes to satisfy queries. | ||||
| */ | */ | ||||
| final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { | final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { | ||||
| private $taskIDs = array(); | private $taskIDs = array(); | ||||
| private $taskPHIDs = array(); | private $taskPHIDs = array(); | ||||
| private $authorPHIDs = array(); | private $authorPHIDs = array(); | ||||
| private $ownerPHIDs = array(); | private $ownerPHIDs = array(); | ||||
| private $includeUnowned = null; | private $includeUnowned = null; | ||||
| private $projectPHIDs = array(); | |||||
| private $xprojectPHIDs = array(); | |||||
| private $subscriberPHIDs = array(); | private $subscriberPHIDs = array(); | ||||
| private $anyProjectPHIDs = array(); | |||||
| private $anyUserProjectPHIDs = array(); | |||||
| private $includeNoProject = null; | |||||
| private $dateCreatedAfter; | private $dateCreatedAfter; | ||||
| private $dateCreatedBefore; | private $dateCreatedBefore; | ||||
| private $dateModifiedAfter; | private $dateModifiedAfter; | ||||
| private $dateModifiedBefore; | private $dateModifiedBefore; | ||||
| private $subpriorityMin; | private $subpriorityMin; | ||||
| private $subpriorityMax; | private $subpriorityMax; | ||||
| private $fullTextSearch = ''; | private $fullTextSearch = ''; | ||||
| Show All 24 Lines | final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { | ||||
| const ORDER_CREATED = 'order-created'; | const ORDER_CREATED = 'order-created'; | ||||
| const ORDER_MODIFIED = 'order-modified'; | const ORDER_MODIFIED = 'order-modified'; | ||||
| const ORDER_TITLE = 'order-title'; | const ORDER_TITLE = 'order-title'; | ||||
| private $needSubscriberPHIDs; | private $needSubscriberPHIDs; | ||||
| private $needProjectPHIDs; | private $needProjectPHIDs; | ||||
| private $blockingTasks; | private $blockingTasks; | ||||
| private $blockedTasks; | private $blockedTasks; | ||||
| private $projectPolicyCheckFailed = false; | |||||
| public function withAuthors(array $authors) { | public function withAuthors(array $authors) { | ||||
| $this->authorPHIDs = $authors; | $this->authorPHIDs = $authors; | ||||
| return $this; | return $this; | ||||
| } | } | ||||
| public function withIDs(array $ids) { | public function withIDs(array $ids) { | ||||
| $this->taskIDs = $ids; | $this->taskIDs = $ids; | ||||
| Show All 15 Lines | foreach ($owners as $k => $phid) { | ||||
| unset($owners[$k]); | unset($owners[$k]); | ||||
| break; | break; | ||||
| } | } | ||||
| } | } | ||||
| $this->ownerPHIDs = $owners; | $this->ownerPHIDs = $owners; | ||||
| return $this; | return $this; | ||||
| } | } | ||||
| public function withAllProjects(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; | |||||
| } | |||||
| /** | |||||
| * Add an additional "all projects" constraint to existing filters. | |||||
| * | |||||
| * This is used by boards to supplement queries. | |||||
| * | |||||
| * @param list<phid> List of project PHIDs to add to any existing constraint. | |||||
| * @return this | |||||
| */ | |||||
| public function addWithAllProjects(array $projects) { | |||||
| if ($this->projectPHIDs === null) { | |||||
| $this->projectPHIDs = array(); | |||||
| } | |||||
| return $this->withAllProjects(array_merge($this->projectPHIDs, $projects)); | |||||
| } | |||||
| public function withoutProjects(array $projects) { | |||||
| $this->xprojectPHIDs = $projects; | |||||
| return $this; | |||||
| } | |||||
| public function withStatus($status) { | public function withStatus($status) { | ||||
| $this->status = $status; | $this->status = $status; | ||||
| return $this; | return $this; | ||||
| } | } | ||||
| public function withStatuses(array $statuses) { | public function withStatuses(array $statuses) { | ||||
| $this->statuses = $statuses; | $this->statuses = $statuses; | ||||
| return $this; | return $this; | ||||
| Show All 30 Lines | public function setGroupBy($group) { | ||||
| return $this; | return $this; | ||||
| } | } | ||||
| public function setOrderBy($order) { | public function setOrderBy($order) { | ||||
| $this->orderBy = $order; | $this->orderBy = $order; | ||||
| return $this; | return $this; | ||||
| } | } | ||||
| public function withAnyProjects(array $projects) { | |||||
| $this->anyProjectPHIDs = $projects; | |||||
| return $this; | |||||
| } | |||||
| public function withAnyUserProjects(array $users) { | |||||
| $this->anyUserProjectPHIDs = $users; | |||||
| return $this; | |||||
| } | |||||
| /** | /** | ||||
| * True returns tasks that are blocking other tasks only. | * True returns tasks that are blocking other tasks only. | ||||
| * False returns tasks that are not blocking other tasks only. | * False returns tasks that are not blocking other tasks only. | ||||
| * Null returns tasks regardless of blocking status. | * Null returns tasks regardless of blocking status. | ||||
| */ | */ | ||||
| public function withBlockingTasks($mode) { | public function withBlockingTasks($mode) { | ||||
| $this->blockingTasks = $mode; | $this->blockingTasks = $mode; | ||||
| return $this; | return $this; | ||||
| ▲ Show 20 Lines • Show All 47 Lines • ▼ Show 20 Lines | public function needProjectPHIDs($bool) { | ||||
| return $this; | return $this; | ||||
| } | } | ||||
| protected function newResultObject() { | protected function newResultObject() { | ||||
| return new ManiphestTask(); | return new ManiphestTask(); | ||||
| } | } | ||||
| protected function willExecute() { | protected function willExecute() { | ||||
| // Make sure the user can see any projects specified in this | |||||
| // query FIRST. | |||||
| if ($this->projectPHIDs) { | |||||
| $projects = id(new PhabricatorProjectQuery()) | |||||
| ->setViewer($this->getViewer()) | |||||
| ->withPHIDs($this->projectPHIDs) | |||||
| ->execute(); | |||||
| $projects = mpull($projects, null, 'getPHID'); | |||||
| foreach ($this->projectPHIDs as $index => $phid) { | |||||
| $project = idx($projects, $phid); | |||||
| if (!$project) { | |||||
| unset($this->projectPHIDs[$index]); | |||||
| continue; | |||||
| } | |||||
| } | |||||
| if (!$this->projectPHIDs) { | |||||
| $this->projectPolicyCheckFailed = true; | |||||
| } | |||||
| $this->projectPHIDs = array_values($this->projectPHIDs); | |||||
| } | |||||
| // If we already have an order vector, use it as provided. | // If we already have an order vector, use it as provided. | ||||
| // TODO: This is a messy hack to make setOrderVector() stronger than | // TODO: This is a messy hack to make setOrderVector() stronger than | ||||
| // setPriority(). | // setPriority(). | ||||
| $vector = $this->getOrderVector(); | $vector = $this->getOrderVector(); | ||||
| $keys = mpull(iterator_to_array($vector), 'getOrderKey'); | $keys = mpull(iterator_to_array($vector), 'getOrderKey'); | ||||
| if (array_values($keys) !== array('id')) { | if (array_values($keys) !== array('id')) { | ||||
| return; | return; | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 47 Lines • ▼ Show 20 Lines | protected function willExecute() { | ||||
| $parts = array_mergev($parts); | $parts = array_mergev($parts); | ||||
| // We may have a duplicate column if we are both ordering and grouping | // We may have a duplicate column if we are both ordering and grouping | ||||
| // by priority. | // by priority. | ||||
| $parts = array_unique($parts); | $parts = array_unique($parts); | ||||
| $this->setOrderVector($parts); | $this->setOrderVector($parts); | ||||
| } | } | ||||
| protected function loadPage() { | protected function loadPage() { | ||||
| if ($this->projectPolicyCheckFailed) { | |||||
| throw new PhabricatorEmptyQueryException(); | |||||
| } | |||||
| $task_dao = new ManiphestTask(); | $task_dao = new ManiphestTask(); | ||||
| $conn = $task_dao->establishConnection('r'); | $conn = $task_dao->establishConnection('r'); | ||||
| $where = array(); | $where = array(); | ||||
| $where[] = $this->buildTaskIDsWhereClause($conn); | $where[] = $this->buildTaskIDsWhereClause($conn); | ||||
| $where[] = $this->buildTaskPHIDsWhereClause($conn); | $where[] = $this->buildTaskPHIDsWhereClause($conn); | ||||
| $where[] = $this->buildStatusWhereClause($conn); | $where[] = $this->buildStatusWhereClause($conn); | ||||
| $where[] = $this->buildStatusesWhereClause($conn); | $where[] = $this->buildStatusesWhereClause($conn); | ||||
| $where[] = $this->buildDependenciesWhereClause($conn); | $where[] = $this->buildDependenciesWhereClause($conn); | ||||
| $where[] = $this->buildAuthorWhereClause($conn); | $where[] = $this->buildAuthorWhereClause($conn); | ||||
| $where[] = $this->buildOwnerWhereClause($conn); | $where[] = $this->buildOwnerWhereClause($conn); | ||||
| $where[] = $this->buildProjectWhereClause($conn); | |||||
| $where[] = $this->buildAnyProjectWhereClause($conn); | |||||
| $where[] = $this->buildAnyUserProjectWhereClause($conn); | |||||
| $where[] = $this->buildXProjectWhereClause($conn); | |||||
| $where[] = $this->buildFullTextWhereClause($conn); | $where[] = $this->buildFullTextWhereClause($conn); | ||||
| if ($this->dateCreatedAfter) { | if ($this->dateCreatedAfter) { | ||||
| $where[] = qsprintf( | $where[] = qsprintf( | ||||
| $conn, | $conn, | ||||
| 'task.dateCreated >= %d', | 'task.dateCreated >= %d', | ||||
| $this->dateCreatedAfter); | $this->dateCreatedAfter); | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 46 Lines • ▼ Show 20 Lines | if ($this->subpriorityMax) { | ||||
| 'task.subpriority <= %f', | 'task.subpriority <= %f', | ||||
| $this->subpriorityMax); | $this->subpriorityMax); | ||||
| } | } | ||||
| $where[] = $this->buildWhereClauseParts($conn); | $where[] = $this->buildWhereClauseParts($conn); | ||||
| $where = $this->formatWhereClause($where); | $where = $this->formatWhereClause($where); | ||||
| $count = ''; | |||||
| if (count($this->projectPHIDs) > 1) { | |||||
| $count = ', COUNT(project.dst) projectCount'; | |||||
| } | |||||
| $group_column = ''; | $group_column = ''; | ||||
| switch ($this->groupBy) { | switch ($this->groupBy) { | ||||
| case self::GROUP_PROJECT: | case self::GROUP_PROJECT: | ||||
| $group_column = qsprintf( | $group_column = qsprintf( | ||||
| $conn, | $conn, | ||||
| ', projectGroupName.indexedObjectPHID projectGroupPHID'); | ', projectGroupName.indexedObjectPHID projectGroupPHID'); | ||||
| break; | break; | ||||
| } | } | ||||
| $rows = queryfx_all( | $rows = queryfx_all( | ||||
| $conn, | $conn, | ||||
| '%Q %Q %Q FROM %T task %Q %Q %Q %Q %Q %Q', | '%Q %Q FROM %T task %Q %Q %Q %Q %Q %Q', | ||||
| $this->buildSelectClause($conn), | $this->buildSelectClause($conn), | ||||
| $count, | |||||
| $group_column, | $group_column, | ||||
| $task_dao->getTableName(), | $task_dao->getTableName(), | ||||
| $this->buildJoinClause($conn), | $this->buildJoinClause($conn), | ||||
| $where, | $where, | ||||
| $this->buildGroupClause($conn), | $this->buildGroupClause($conn), | ||||
| $this->buildHavingClause($conn), | $this->buildHavingClause($conn), | ||||
| $this->buildOrderClause($conn), | $this->buildOrderClause($conn), | ||||
| $this->buildLimitClause($conn)); | $this->buildLimitClause($conn)); | ||||
| ▲ Show 20 Lines • Show All 247 Lines • ▼ Show 20 Lines | if ($this->blockedTasks === true) { | ||||
| $conn, | $conn, | ||||
| 'blocked.dst IS NULL OR blockedtask.status NOT IN (%Ls)', | 'blocked.dst IS NULL OR blockedtask.status NOT IN (%Ls)', | ||||
| ManiphestTaskStatus::getOpenStatusConstants()); | ManiphestTaskStatus::getOpenStatusConstants()); | ||||
| } | } | ||||
| return '('.implode(') OR (', $parts).')'; | return '('.implode(') OR (', $parts).')'; | ||||
| } | } | ||||
| private function buildProjectWhereClause(AphrontDatabaseConnection $conn) { | |||||
| if (!$this->projectPHIDs && !$this->includeNoProject) { | |||||
| return null; | |||||
| } | |||||
| $parts = array(); | |||||
| if ($this->projectPHIDs) { | |||||
| $parts[] = qsprintf( | |||||
| $conn, | |||||
| 'project.dst in (%Ls)', | |||||
| $this->projectPHIDs); | |||||
| } | |||||
| if ($this->includeNoProject) { | |||||
| $parts[] = qsprintf( | |||||
| $conn, | |||||
| 'project.dst IS NULL'); | |||||
| } | |||||
| return '('.implode(') OR (', $parts).')'; | |||||
| } | |||||
| private function buildAnyProjectWhereClause(AphrontDatabaseConnection $conn) { | |||||
| if (!$this->anyProjectPHIDs) { | |||||
| return null; | |||||
| } | |||||
| return qsprintf( | |||||
| $conn, | |||||
| 'anyproject.dst IN (%Ls)', | |||||
| $this->anyProjectPHIDs); | |||||
| } | |||||
| private function buildAnyUserProjectWhereClause( | |||||
| AphrontDatabaseConnection $conn) { | |||||
| if (!$this->anyUserProjectPHIDs) { | |||||
| return null; | |||||
| } | |||||
| $projects = id(new PhabricatorProjectQuery()) | |||||
| ->setViewer($this->getViewer()) | |||||
| ->withMemberPHIDs($this->anyUserProjectPHIDs) | |||||
| ->execute(); | |||||
| $any_user_project_phids = mpull($projects, 'getPHID'); | |||||
| if (!$any_user_project_phids) { | |||||
| throw new PhabricatorEmptyQueryException(); | |||||
| } | |||||
| return qsprintf( | |||||
| $conn, | |||||
| 'anyproject.dst IN (%Ls)', | |||||
| $any_user_project_phids); | |||||
| } | |||||
| private function buildXProjectWhereClause(AphrontDatabaseConnection $conn) { | |||||
| if (!$this->xprojectPHIDs) { | |||||
| return null; | |||||
| } | |||||
| return qsprintf( | |||||
| $conn, | |||||
| 'xproject.dst IS NULL'); | |||||
| } | |||||
| protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) { | protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) { | ||||
| $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE; | $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE; | ||||
| $joins = array(); | $joins = array(); | ||||
| if ($this->projectPHIDs || $this->includeNoProject) { | |||||
| $joins[] = qsprintf( | |||||
| $conn_r, | |||||
| '%Q JOIN %T project ON project.src = task.phid | |||||
| AND project.type = %d', | |||||
| ($this->includeNoProject ? 'LEFT' : ''), | |||||
| $edge_table, | |||||
| PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); | |||||
| } | |||||
| if ($this->shouldJoinBlockingTasks()) { | if ($this->shouldJoinBlockingTasks()) { | ||||
| $joins[] = qsprintf( | $joins[] = qsprintf( | ||||
| $conn_r, | $conn_r, | ||||
| 'LEFT JOIN %T blocking ON blocking.src = task.phid '. | 'LEFT JOIN %T blocking ON blocking.src = task.phid '. | ||||
| 'AND blocking.type = %d '. | 'AND blocking.type = %d '. | ||||
| 'LEFT JOIN %T blockingtask ON blocking.dst = blockingtask.phid', | 'LEFT JOIN %T blockingtask ON blocking.dst = blockingtask.phid', | ||||
| $edge_table, | $edge_table, | ||||
| ManiphestTaskDependedOnByTaskEdgeType::EDGECONST, | ManiphestTaskDependedOnByTaskEdgeType::EDGECONST, | ||||
| id(new ManiphestTask())->getTableName()); | id(new ManiphestTask())->getTableName()); | ||||
| } | } | ||||
| if ($this->shouldJoinBlockedTasks()) { | if ($this->shouldJoinBlockedTasks()) { | ||||
| $joins[] = qsprintf( | $joins[] = qsprintf( | ||||
| $conn_r, | $conn_r, | ||||
| 'LEFT JOIN %T blocked ON blocked.src = task.phid '. | 'LEFT JOIN %T blocked ON blocked.src = task.phid '. | ||||
| 'AND blocked.type = %d '. | 'AND blocked.type = %d '. | ||||
| 'LEFT JOIN %T blockedtask ON blocked.dst = blockedtask.phid', | 'LEFT JOIN %T blockedtask ON blocked.dst = blockedtask.phid', | ||||
| $edge_table, | $edge_table, | ||||
| ManiphestTaskDependsOnTaskEdgeType::EDGECONST, | ManiphestTaskDependsOnTaskEdgeType::EDGECONST, | ||||
| id(new ManiphestTask())->getTableName()); | id(new ManiphestTask())->getTableName()); | ||||
| } | } | ||||
| if ($this->anyProjectPHIDs || $this->anyUserProjectPHIDs) { | |||||
| $joins[] = qsprintf( | |||||
| $conn_r, | |||||
| 'JOIN %T anyproject ON anyproject.src = task.phid | |||||
| AND anyproject.type = %d', | |||||
| $edge_table, | |||||
| PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); | |||||
| } | |||||
| if ($this->xprojectPHIDs) { | |||||
| $joins[] = qsprintf( | |||||
| $conn_r, | |||||
| 'LEFT JOIN %T xproject ON xproject.src = task.phid | |||||
| AND xproject.type = %d | |||||
| AND xproject.dst IN (%Ls)', | |||||
| $edge_table, | |||||
| PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, | |||||
| $this->xprojectPHIDs); | |||||
| } | |||||
| if ($this->subscriberPHIDs) { | if ($this->subscriberPHIDs) { | ||||
| $joins[] = qsprintf( | $joins[] = qsprintf( | ||||
| $conn_r, | $conn_r, | ||||
| 'JOIN %T e_ccs ON e_ccs.src = task.phid '. | 'JOIN %T e_ccs ON e_ccs.src = task.phid '. | ||||
| 'AND e_ccs.type = %s '. | 'AND e_ccs.type = %s '. | ||||
| 'AND e_ccs.dst in (%Ls)', | 'AND e_ccs.dst in (%Ls)', | ||||
| PhabricatorEdgeConfig::TABLE_NAME_EDGE, | PhabricatorEdgeConfig::TABLE_NAME_EDGE, | ||||
| PhabricatorObjectHasSubscriberEdgeType::EDGECONST, | PhabricatorObjectHasSubscriberEdgeType::EDGECONST, | ||||
| Show All 29 Lines | protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) { | ||||
| } | } | ||||
| $joins[] = parent::buildJoinClauseParts($conn_r); | $joins[] = parent::buildJoinClauseParts($conn_r); | ||||
| return $joins; | return $joins; | ||||
| } | } | ||||
| protected function buildGroupClause(AphrontDatabaseConnection $conn_r) { | protected function buildGroupClause(AphrontDatabaseConnection $conn_r) { | ||||
| $joined_multiple_rows = (count($this->projectPHIDs) > 1) || | $joined_multiple_rows = $this->shouldJoinBlockingTasks() || | ||||
| (count($this->anyProjectPHIDs) > 1) || | |||||
| $this->shouldJoinBlockingTasks() || | |||||
| $this->shouldJoinBlockedTasks() || | $this->shouldJoinBlockedTasks() || | ||||
| ($this->shouldGroupQueryResultRows()); | ($this->shouldGroupQueryResultRows()); | ||||
| $joined_project_name = ($this->groupBy == self::GROUP_PROJECT); | $joined_project_name = ($this->groupBy == self::GROUP_PROJECT); | ||||
| // If we're joining multiple rows, we need to group the results by the | // If we're joining multiple rows, we need to group the results by the | ||||
| // task IDs. | // task IDs. | ||||
| if ($joined_multiple_rows) { | if ($joined_multiple_rows) { | ||||
| Show All 22 Lines | final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { | ||||
| * | * | ||||
| * ...we ignore the single project, as every result is in that project. (In | * ...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.) | * the case that there are several "any" projects, we do not ignore them.) | ||||
| * | * | ||||
| * @return list<phid> Project PHIDs which should be ignored in query | * @return list<phid> Project PHIDs which should be ignored in query | ||||
| * construction. | * construction. | ||||
| */ | */ | ||||
| private function getIgnoreGroupedProjectPHIDs() { | private function getIgnoreGroupedProjectPHIDs() { | ||||
| $phids = array(); | // 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. | |||||
| if ($this->projectPHIDs) { | $edge_types = array( | ||||
| $phids[] = $this->projectPHIDs; | PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, | ||||
| } | ); | ||||
| if (count($this->anyProjectPHIDs) == 1) { | $phids = array(); | ||||
| $phids[] = $this->anyProjectPHIDs; | |||||
| } | |||||
| // Maybe we should also exclude the "excludeProjectPHIDs"? It won't | $phids[] = $this->getEdgeLogicValues( | ||||
| // impact the results, but we might end up with a better query plan. | $edge_types, | ||||
| // Investigate this on real data? This is likely very rare. | array( | ||||
| PhabricatorQueryConstraint::OPERATOR_AND, | |||||
| )); | |||||
| return array_mergev($phids); | $any = $this->getEdgeLogicValues( | ||||
| $edge_types, | |||||
| array( | |||||
| PhabricatorQueryConstraint::OPERATOR_OR, | |||||
| )); | |||||
| if (count($any) == 1) { | |||||
| $phids[] = $any; | |||||
| } | } | ||||
| // TODO: Remove this when moving fully to edge logic. | return array_mergev($phids); | ||||
| protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) { | |||||
| $having = parent::buildHavingClauseParts($conn); | |||||
| if (count($this->projectPHIDs) > 1) { | |||||
| $having[] = qsprintf( | |||||
| $conn, | |||||
| 'projectCount = %d', | |||||
| count($this->projectPHIDs)); | |||||
| } | |||||
| return $having; | |||||
| } | } | ||||
| protected function getResultCursor($result) { | protected function getResultCursor($result) { | ||||
| $id = $result->getID(); | $id = $result->getID(); | ||||
| if ($this->groupBy == self::GROUP_PROJECT) { | if ($this->groupBy == self::GROUP_PROJECT) { | ||||
| return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');; | return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');; | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 104 Lines • Show Last 20 Lines | |||||