diff --git a/resources/sql/patches/20130913.maniphest.1.migratesearch.php b/resources/sql/patches/20130913.maniphest.1.migratesearch.php index 7cf51069cd..7f7be6d505 100644 --- a/resources/sql/patches/20130913.maniphest.1.migratesearch.php +++ b/resources/sql/patches/20130913.maniphest.1.migratesearch.php @@ -1,209 +1,212 @@ establishConnection('w'); $search_table = new PhabricatorSearchQuery(); $search_conn_w = $search_table->establishConnection('w'); +// See T1812. This is an old status constant from the time of this migration. +$old_open_status = 0; + echo "Updating saved Maniphest queries...\n"; $rows = new LiskRawMigrationIterator($conn_w, 'maniphest_savedquery'); foreach ($rows as $row) { $id = $row['id']; echo "Updating query {$id}...\n"; $data = queryfx_one( $search_conn_w, 'SELECT parameters FROM %T WHERE queryKey = %s', $search_table->getTableName(), $row['queryKey']); if (!$data) { echo "Unable to locate query data.\n"; continue; } $data = json_decode($data['parameters'], true); if (!is_array($data)) { echo "Unable to decode query data.\n"; continue; } if (idx($data, 'view') != 'custom') { echo "Query is not a custom query.\n"; continue; } $new_data = array( 'limit' => 1000, ); if (isset($data['lowPriority']) || isset($data['highPriority'])) { $lo = idx($data, 'lowPriority'); $hi = idx($data, 'highPriority'); $priorities = array(); $all = ManiphestTaskPriority::getTaskPriorityMap(); foreach ($all as $pri => $name) { if (($lo !== null) && ($pri < $lo)) { continue; } if (($hi !== null) && ($pri > $hi)) { continue; } $priorities[] = $pri; } if (count($priorities) != count($all)) { $new_data['priorities'] = $priorities; } } foreach ($data as $key => $value) { switch ($key) { case 'fullTextSearch': if (strlen($value)) { $new_data['fulltext'] = $value; } break; case 'userPHIDs': // This was (I think?) one-off data provied to specific hard-coded // queries. break; case 'projectPHIDs': foreach ($value as $k => $v) { if ($v === null || $v === ManiphestTaskOwner::PROJECT_NO_PROJECT) { $new_data['withNoProject'] = true; unset($value[$k]); break; } } if ($value) { $new_data['allProjectPHIDs'] = $value; } break; case 'anyProjectPHIDs': if ($value) { $new_data['anyProjectPHIDs'] = $value; } break; case 'anyUserProjectPHIDs': if ($value) { $new_data['userProjectPHIDs'] = $value; } break; case 'excludeProjectPHIDs': if ($value) { $new_data['excludeProjectPHIDs'] = $value; } break; case 'ownerPHIDs': foreach ($value as $k => $v) { if ($v === null || $v === ManiphestTaskOwner::OWNER_UP_FOR_GRABS) { $new_data['withUnassigned'] = true; unset($value[$k]); break; } } if ($value) { $new_data['assignedPHIDs'] = $value; } break; case 'authorPHIDs': if ($value) { $new_data['authorPHIDs'] = $value; } break; case 'taskIDs': if ($value) { $new_data['ids'] = $value; } break; case 'status': $include_open = !empty($value['open']); $include_closed = !empty($value['closed']); if ($include_open xor $include_closed) { if ($include_open) { $new_data['statuses'] = array( - ManiphestTaskStatus::STATUS_OPEN, + $old_open_status, ); } else { $statuses = array(); foreach (ManiphestTaskStatus::getTaskStatusMap() as $status => $n) { - if ($status != ManiphestTaskStatus::STATUS_OPEN) { + if ($status != $old_open_status) { $statuses[] = $status; } } $new_data['statuses'] = $statuses; } } break; case 'order': $map = array( 'priority' => 'priority', 'updated' => 'updated', 'created' => 'created', 'title' => 'title', ); if (isset($map[$value])) { $new_data['order'] = $map[$value]; } else { $new_data['order'] = 'priority'; } break; case 'group': $map = array( 'priority' => 'priority', 'owner' => 'assigned', 'status' => 'status', 'project' => 'project', 'none' => 'none', ); if (isset($map[$value])) { $new_data['group'] = $map[$value]; } else { $new_data['group'] = 'priority'; } break; } } $saved = id(new PhabricatorSavedQuery()) ->setEngineClassName('ManiphestTaskSearchEngine'); foreach ($new_data as $key => $value) { $saved->setParameter($key, $value); } try { $saved->save(); } catch (AphrontQueryDuplicateKeyException $ex) { // Ignore this, we just have duplicate saved queries. } $named = id(new PhabricatorNamedQuery()) ->setEngineClassName('ManiphestTaskSearchEngine') ->setQueryKey($saved->getQueryKey()) ->setQueryName($row['name']) ->setUserPHID($row['userPHID']); try { $named->save(); } catch (Exception $ex) { // The user already has this query under another name. This can occur if // the migration runs twice. echo "Failed to save named query.\n"; continue; } echo "OK.\n"; } echo "Done.\n"; diff --git a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php index 8e56774502..a00dc7b9fb 100644 --- a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php @@ -1,224 +1,192 @@ ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, - 'resolves' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, - 'resolved' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, - 'fix' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, - 'fixes' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, - 'fixed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, - 'wontfix' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, - 'wontfixes' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, - 'wontfixed' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, - 'spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, - 'spites' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, - 'spited' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, - 'invalidate' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, - 'invaldiates' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, - 'invalidated' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, - 'close' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, - 'closes' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, - 'closed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, - 'ref' => null, - 'refs' => null, - 'references' => null, - 'cf.' => null, - ); - - $suffixes = array( - 'as resolved' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, - 'as fixed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, - 'as wontfix' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, - 'as spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, - 'out of spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, - 'as invalid' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, - '' => null, - ); + $prefixes = ManiphestTaskStatus::getStatusPrefixMap(); + $suffixes = ManiphestTaskStatus::getStatusSuffixMap(); $matches = id(new ManiphestCustomFieldStatusParser()) ->parseCorpus($message); $task_statuses = array(); foreach ($matches as $match) { $prefix = phutil_utf8_strtolower($match['prefix']); $suffix = phutil_utf8_strtolower($match['suffix']); $status = idx($suffixes, $suffix); if (!$status) { $status = idx($prefixes, $prefix); } foreach ($match['monograms'] as $task_monogram) { $task_id = (int)trim($task_monogram, 'tT'); $task_statuses[$task_id] = $status; } } return $task_statuses; } private function findDependentRevisions($message) { $matches = id(new DifferentialCustomFieldDependsOnParser()) ->parseCorpus($message); $dependents = array(); foreach ($matches as $match) { foreach ($match['monograms'] as $monogram) { $id = (int)trim($monogram, 'dD'); $dependents[$id] = $id; } } return $dependents; } public static function findRevertedCommits($message) { $matches = id(new DifferentialCustomFieldRevertsParser()) ->parseCorpus($message); $result = array(); foreach ($matches as $match) { foreach ($match['monograms'] as $monogram) { $result[$monogram] = $monogram; } } return $result; } public function didWriteRevision(DifferentialRevisionEditor $editor) { $message = $this->renderValueForCommitMessage(false); $tasks = $this->findMentionedTasks($message); if ($tasks) { $tasks = id(new ManiphestTaskQuery()) ->setViewer($editor->getActor()) ->withIDs(array_keys($tasks)) ->execute(); $this->saveFieldEdges( $editor->getRevision(), PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK, mpull($tasks, 'getPHID')); } $dependents = $this->findDependentRevisions($message); if ($dependents) { $dependents = id(new DifferentialRevisionQuery()) ->setViewer($editor->getActor()) ->withIDs($dependents) ->execute(); $this->saveFieldEdges( $editor->getRevision(), PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV, mpull($dependents, 'getPHID')); } } private function saveFieldEdges( DifferentialRevision $revision, $edge_type, array $add_phids) { $revision_phid = $revision->getPHID(); $old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $revision_phid, $edge_type); $add_phids = array_diff($add_phids, $old_phids); if (!$add_phids) { return; } $edge_editor = id(new PhabricatorEdgeEditor())->setActor($this->getUser()); foreach ($add_phids as $phid) { $edge_editor->addEdge($revision_phid, $edge_type, $phid); } // NOTE: Deletes only through the fields. $edge_editor->save(); } public function didParseCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data) { $message = $this->renderValueForCommitMessage($is_edit = false); $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $data->getCommitDetail('authorPHID')); if (!$user) { // TODO: Maybe after grey users, we should find a way to proceed even // if we don't know who the author is. return; } $commit_names = self::findRevertedCommits($message); if ($commit_names) { $reverts = id(new DiffusionCommitQuery()) ->setViewer($user) ->withIdentifiers($commit_names) ->withDefaultRepository($repository) ->execute(); foreach ($reverts as $revert) { // TODO: Do interesting things here. } } $tasks_statuses = $this->findMentionedTasks($message); if (!$tasks_statuses) { return; } $tasks = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array_keys($tasks_statuses)) ->execute(); foreach ($tasks as $task_id => $task) { id(new PhabricatorEdgeEditor()) ->setActor($user) ->addEdge( $task->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT, $commit->getPHID()) ->save(); $status = $tasks_statuses[$task_id]; if (!$status) { // Text like "Ref T123", don't change the task status. continue; } - if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) { - // Task is already closed. + if ($task->getStatus() == $status) { + // Task is already in the specified status, so skip updating it. continue; } $commit_name = $repository->formatCommitName( $commit->getCommitIdentifier()); $call = new ConduitCall( 'maniphest.update', array( 'id' => $task->getID(), 'status' => $status, 'comments' => "Closed by commit {$commit_name}.", )); $call->setUser($user); $call->execute(); } } } diff --git a/src/applications/home/controller/PhabricatorHomeMainController.php b/src/applications/home/controller/PhabricatorHomeMainController.php index bbd2bbc4d8..c3a60dddde 100644 --- a/src/applications/home/controller/PhabricatorHomeMainController.php +++ b/src/applications/home/controller/PhabricatorHomeMainController.php @@ -1,465 +1,465 @@ filter = idx($data, 'filter'); } public function processRequest() { $user = $this->getRequest()->getUser(); if ($this->filter == 'jump') { return $this->buildJumpResponse(); } $nav = $this->buildNav(); $project_query = new PhabricatorProjectQuery(); $project_query->setViewer($user); $project_query->withMemberPHIDs(array($user->getPHID())); $projects = $project_query->execute(); return $this->buildMainResponse($nav, $projects); } private function buildMainResponse($nav, array $projects) { assert_instances_of($projects, 'PhabricatorProject'); $maniphest = 'PhabricatorApplicationManiphest'; if (PhabricatorApplication::isClassInstalled($maniphest)) { $unbreak_panel = $this->buildUnbreakNowPanel(); $triage_panel = $this->buildNeedsTriagePanel($projects); $tasks_panel = $this->buildTasksPanel(); } else { $unbreak_panel = null; $triage_panel = null; $tasks_panel = null; } $audit = 'PhabricatorApplicationAudit'; if (PhabricatorApplication::isClassInstalled($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; } $jump_panel = $this->buildJumpPanel(); $revision_panel = $this->buildRevisionPanel(); $content = array( $jump_panel, $welcome_panel, $unbreak_panel, $triage_panel, $revision_panel, $tasks_panel, $audit_panel, $commit_panel, $this->minipanels, ); $nav->appendChild($content); $nav->appendChild(new PhabricatorGlobalUploadTargetView()); return $this->buildApplicationPage( $nav, array( 'title' => 'Phabricator', 'device' => true, )); } private function buildJumpResponse() { $request = $this->getRequest(); $jump = $request->getStr('jump'); $response = PhabricatorJumpNavHandler::getJumpResponse( $request->getUser(), $jump); if ($response) { return $response; } else if ($request->isFormPost()) { $uri = new PhutilURI('/search/'); $uri->setQueryParam('query', $jump); $uri->setQueryParam('search:primary', 'true'); return id(new AphrontRedirectResponse())->setURI((string)$uri); } else { return id(new AphrontRedirectResponse())->setURI('/'); } } 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(array(ManiphestTaskStatus::STATUS_OPEN)) + ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->withPriorities(array($unbreak_now)) ->setLimit(10); $tasks = $task_query->execute(); if (!$tasks) { return $this->renderMiniPanel( 'No "Unbreak Now!" Tasks', 'Nothing appears to be critically broken right now.'); } $href = '/maniphest/?statuses[]=0&priorities[]='.$unbreak_now.'#R'; $title = pht('Unbreak Now!'); $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($this->buildTaskListView($tasks)); $panel->setNoBackground(); 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(array(ManiphestTaskStatus::STATUS_OPEN)) + ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->withPriorities(array($needs_triage)) ->withAnyProjects(mpull($projects, 'getPHID')) ->setLimit(10); $tasks = $task_query->execute(); } else { $tasks = array(); } if (!$tasks) { return $this->renderMiniPanel( 'No "Needs Triage" Tasks', hsprintf( 'No tasks in projects you are a member of '. 'need triage.')); } $title = pht('Needs Triage'); $href = '/maniphest/?statuses[]=0&priorities[]='.$needs_triage. '&userProjects[]='.$user->getPHID().'#R'; $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($this->buildTaskListView($tasks)); $panel->setNoBackground(); 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); $revisions = $revision_query->execute(); list($blocking, $active, ) = DifferentialRevisionQuery::splitResponsible( $revisions, array($user_phid)); if (!$blocking && !$active) { return $this->renderMiniPanel( 'No Waiting Revisions', 'No revisions are waiting on you.'); } $title = pht('Revisions Waiting on You'); $href = '/differential'; $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $revision_view = id(new DifferentialRevisionListView()) ->setHighlightAge(true) ->setRevisions(array_merge($blocking, $active)) ->setFields(DifferentialRevisionListView::getDefaultFields($user)) ->setUser($user) ->loadAssets(); $phids = array_merge( array($user_phid), $revision_view->getRequiredHandlePHIDs()); $handles = $this->loadViewerHandles($phids); $revision_view->setHandles($handles); $list_view = $revision_view->render(); $list_view->setFlush(true); $panel->appendChild($list_view); $panel->setNoBackground(); return $panel; } private function buildWelcomePanel() { $panel = new AphrontPanelView(); $panel->appendChild( phutil_safe_html( PhabricatorEnv::getEnvConfig('welcome.html'))); $panel->setNoBackground(); return $panel; } private function buildTasksPanel() { $user = $this->getRequest()->getUser(); $user_phid = $user->getPHID(); $task_query = id(new ManiphestTaskQuery()) ->setViewer($user) - ->withStatus(ManiphestTaskQuery::STATUS_OPEN) + ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->setGroupBy(ManiphestTaskQuery::GROUP_PRIORITY) ->withOwners(array($user_phid)) ->setLimit(10); $tasks = $task_query->execute(); if (!$tasks) { return $this->renderMiniPanel( 'No Assigned Tasks', 'You have no assigned tasks.'); } $title = pht('Assigned Tasks'); $href = '/maniphest'; $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($this->buildTaskListView($tasks)); $panel->setNoBackground(); 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 buildJumpPanel($query=null) { $request = $this->getRequest(); $user = $request->getUser(); $uniq_id = celerity_generate_unique_node_id(); Javelin::initBehavior( 'phabricator-autofocus', array( 'id' => $uniq_id, )); require_celerity_resource('phabricator-jump-nav'); $doc_href = PhabricatorEnv::getDocLink('article/Jump_Nav_User_Guide.html'); $doc_link = phutil_tag( 'a', array( 'href' => $doc_href, ), 'Jump Nav User Guide'); $jump_input = phutil_tag( 'input', array( 'type' => 'text', 'class' => 'phabricator-jump-nav', 'name' => 'jump', 'id' => $uniq_id, 'value' => $query, )); $jump_caption = phutil_tag( 'p', array( 'class' => 'phabricator-jump-nav-caption', ), hsprintf( 'Enter the name of an object like D123 to quickly jump to '. 'it. See %s or type help.', $doc_link)); $form = phabricator_form( $user, array( 'action' => '/jump/', 'method' => 'POST', 'class' => 'phabricator-jump-nav-form', ), array( $jump_input, $jump_caption, )); $panel = new AphrontPanelView(); $panel->setNoBackground(); // $panel->appendChild(); $list_filter = new AphrontListFilterView(); $list_filter->appendChild($form); $container = phutil_tag('div', array('class' => 'phabricator-jump-nav-container'), $list_filter); return $container; } private function renderSectionHeader($title, $href) { $header = phutil_tag( 'a', array( 'href' => $href, ), $title); return $header; } private function renderMiniPanel($title, $body) { $panel = new AphrontMiniPanelView(); $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 = new PhabricatorAuditQuery(); $query->withAuditorPHIDs($phids); $query->withStatus(PhabricatorAuditQuery::STATUS_OPEN); $query->withAwaitingUser($user); $query->needCommitData(true); $query->setLimit(10); $audits = $query->execute(); $commits = $query->getCommits(); if (!$audits) { return $this->renderMinipanel( 'No Audits', 'No commits are waiting for you to audit them.'); } $view = new PhabricatorAuditListView(); $view->setAudits($audits); $view->setCommits($commits); $view->setUser($user); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); $title = pht('Audits'); $href = '/audit/'; $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($view); $panel->setNoBackground(); return $panel; } public function buildCommitPanel() { $request = $this->getRequest(); $user = $request->getUser(); $phids = array($user->getPHID()); $query = new PhabricatorAuditCommitQuery(); $query->withAuthorPHIDs($phids); $query->withStatus(PhabricatorAuditCommitQuery::STATUS_CONCERN); $query->needCommitData(true); $query->setLimit(10); $commits = $query->execute(); if (!$commits) { return $this->renderMinipanel( 'No Problem Commits', 'No one has raised concerns with your commits.'); } $view = new PhabricatorAuditCommitListView(); $view->setCommits($commits); $view->setUser($user); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); $title = pht('Problem Commits'); $href = '/audit/'; $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($view); $panel->setNoBackground(); return $panel; } } diff --git a/src/applications/maniphest/application/PhabricatorApplicationManiphest.php b/src/applications/maniphest/application/PhabricatorApplicationManiphest.php index ff3746ebf0..c0b3eaad66 100644 --- a/src/applications/maniphest/application/PhabricatorApplicationManiphest.php +++ b/src/applications/maniphest/application/PhabricatorApplicationManiphest.php @@ -1,127 +1,127 @@ [1-9]\d*)' => 'ManiphestTaskDetailController', '/maniphest/' => array( '(?:query/(?P[^/]+)/)?' => 'ManiphestTaskListController', 'report/(?:(?P\w+)/)?' => 'ManiphestReportController', 'batch/' => 'ManiphestBatchEditController', 'task/' => array( 'create/' => 'ManiphestTaskEditController', 'edit/(?P[1-9]\d*)/' => 'ManiphestTaskEditController', 'descriptionpreview/' => 'PhabricatorMarkupPreviewController', ), 'transaction/' => array( 'save/' => 'ManiphestTransactionSaveController', 'preview/(?P[1-9]\d*)/' => 'ManiphestTransactionPreviewController', ), 'export/(?P[^/]+)/' => 'ManiphestExportController', 'subpriority/' => 'ManiphestSubpriorityController', 'subscribe/(?Padd|rem)/(?P[1-9]\d*)/' => 'ManiphestSubscribeController', ), ); } public function loadStatus(PhabricatorUser $user) { $status = array(); $query = id(new ManiphestTaskQuery()) ->setViewer($user) - ->withStatus(ManiphestTaskQuery::STATUS_OPEN) + ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->withOwners(array($user->getPHID())); $count = count($query->execute()); $type = PhabricatorApplicationStatusView::TYPE_WARNING; $status[] = id(new PhabricatorApplicationStatusView()) ->setType($type) ->setText(pht('%s Assigned Task(s)', new PhutilNumber($count))) ->setCount($count); return $status; } public function getQuickCreateItems(PhabricatorUser $viewer) { $items = array(); $item = id(new PHUIListItemView()) ->setName(pht('Maniphest Task')) ->setAppIcon('maniphest-dark') ->setHref($this->getBaseURI().'task/create/'); $items[] = $item; return $items; } protected function getCustomCapabilities() { return array( ManiphestCapabilityDefaultView::CAPABILITY => array( 'caption' => pht( 'Default view policy for newly created tasks.'), ), ManiphestCapabilityDefaultEdit::CAPABILITY => array( 'caption' => pht( 'Default edit policy for newly created tasks.'), ), ManiphestCapabilityEditStatus::CAPABILITY => array( ), ManiphestCapabilityEditAssign::CAPABILITY => array( ), ManiphestCapabilityEditPolicies::CAPABILITY => array( ), ManiphestCapabilityEditPriority::CAPABILITY => array( ), ManiphestCapabilityEditProjects::CAPABILITY => array( ), ManiphestCapabilityBulkEdit::CAPABILITY => array( ), ); } } diff --git a/src/applications/maniphest/conduit/ConduitAPI_maniphest_Method.php b/src/applications/maniphest/conduit/ConduitAPI_maniphest_Method.php index 2de1faa2db..977d108fbd 100644 --- a/src/applications/maniphest/conduit/ConduitAPI_maniphest_Method.php +++ b/src/applications/maniphest/conduit/ConduitAPI_maniphest_Method.php @@ -1,300 +1,300 @@ 'Missing or malformed parameter.' ); } protected function buildTaskInfoDictionary(ManiphestTask $task) { $results = $this->buildTaskInfoDictionaries(array($task)); return idx($results, $task->getPHID()); } protected function getTaskFields($is_new) { $fields = array(); if (!$is_new) { $fields += array( 'id' => 'optional int', 'phid' => 'optional int', ); } $fields += array( 'title' => $is_new ? 'required string' : 'optional string', 'description' => 'optional string', 'ownerPHID' => 'optional phid', 'ccPHIDs' => 'optional list', 'priority' => 'optional int', 'projectPHIDs' => 'optional list', 'filePHIDs' => 'optional list', 'auxiliary' => 'optional dict', ); if (!$is_new) { $fields += array( 'status' => 'optional int', 'comments' => 'optional string', ); } return $fields; } protected function applyRequest( ManiphestTask $task, ConduitAPIRequest $request, $is_new) { $changes = array(); if ($is_new) { $task->setTitle((string)$request->getValue('title')); $task->setDescription((string)$request->getValue('description')); $changes[ManiphestTransaction::TYPE_STATUS] = - ManiphestTaskStatus::STATUS_OPEN; + ManiphestTaskStatus::getDefaultStatus(); } else { $comments = $request->getValue('comments'); if (!$is_new && $comments !== null) { $changes[PhabricatorTransactions::TYPE_COMMENT] = null; } $title = $request->getValue('title'); if ($title !== null) { $changes[ManiphestTransaction::TYPE_TITLE] = $title; } $desc = $request->getValue('description'); if ($desc !== null) { $changes[ManiphestTransaction::TYPE_DESCRIPTION] = $desc; } $status = $request->getValue('status'); if ($status !== null) { $valid_statuses = ManiphestTaskStatus::getTaskStatusMap(); if (!isset($valid_statuses[$status])) { throw id(new ConduitException('ERR-INVALID-PARAMETER')) ->setErrorDescription('Status set to invalid value.'); } $changes[ManiphestTransaction::TYPE_STATUS] = $status; } } $priority = $request->getValue('priority'); if ($priority !== null) { $valid_priorities = ManiphestTaskPriority::getTaskPriorityMap(); if (!isset($valid_priorities[$priority])) { throw id(new ConduitException('ERR-INVALID-PARAMETER')) ->setErrorDescription('Priority set to invalid value.'); } $changes[ManiphestTransaction::TYPE_PRIORITY] = $priority; } $owner_phid = $request->getValue('ownerPHID'); if ($owner_phid !== null) { $this->validatePHIDList(array($owner_phid), PhabricatorPeoplePHIDTypeUser::TYPECONST, 'ownerPHID'); $changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid; } $ccs = $request->getValue('ccPHIDs'); if ($ccs !== null) { $this->validatePHIDList($ccs, PhabricatorPeoplePHIDTypeUser::TYPECONST, 'ccPHIDS'); $changes[ManiphestTransaction::TYPE_CCS] = $ccs; } $project_phids = $request->getValue('projectPHIDs'); if ($project_phids !== null) { $this->validatePHIDList($project_phids, PhabricatorProjectPHIDTypeProject::TYPECONST, 'projectPHIDS'); $changes[ManiphestTransaction::TYPE_PROJECTS] = $project_phids; } $file_phids = $request->getValue('filePHIDs'); if ($file_phids !== null) { $this->validatePHIDList($file_phids, PhabricatorFilePHIDTypeFile::TYPECONST, 'filePHIDS'); $file_map = array_fill_keys($file_phids, true); $attached = $task->getAttached(); $attached[PhabricatorFilePHIDTypeFile::TYPECONST] = $file_map; $changes[ManiphestTransaction::TYPE_ATTACH] = $attached; } $template = new ManiphestTransaction(); $transactions = array(); foreach ($changes as $type => $value) { $transaction = clone $template; $transaction->setTransactionType($type); if ($type == PhabricatorTransactions::TYPE_COMMENT) { $transaction->attachComment( id(new ManiphestTransactionComment()) ->setContent($comments)); } else { $transaction->setNewValue($value); } $transactions[] = $transaction; } $field_list = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_EDIT); $field_list->readFieldsFromStorage($task); $auxiliary = $request->getValue('auxiliary'); if ($auxiliary) { foreach ($field_list->getFields() as $key => $field) { if (!array_key_exists($key, $auxiliary)) { continue; } $transaction = clone $template; $transaction->setTransactionType( PhabricatorTransactions::TYPE_CUSTOMFIELD); $transaction->setMetadataValue('customfield:key', $key); $transaction->setOldValue( $field->getOldValueForApplicationTransactions()); $transaction->setNewValue($auxiliary[$key]); $transactions[] = $transaction; } } if (!$transactions) { return; } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK, array( 'task' => $task, 'new' => $is_new, 'transactions' => $transactions, )); $event->setUser($request->getUser()); $event->setConduitRequest($request); PhutilEventEngine::dispatchEvent($event); $task = $event->getValue('task'); $transactions = $event->getValue('transactions'); $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_CONDUIT, array()); $editor = id(new ManiphestTransactionEditor()) ->setActor($request->getUser()) ->setContentSource($content_source) ->setContinueOnNoEffect(true); if (!$is_new) { $editor->setContinueOnMissingFields(true); } $editor->applyTransactions($task, $transactions); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK, array( 'task' => $task, 'new' => $is_new, 'transactions' => $transactions, )); $event->setUser($request->getUser()); $event->setConduitRequest($request); PhutilEventEngine::dispatchEvent($event); } protected function buildTaskInfoDictionaries(array $tasks) { assert_instances_of($tasks, 'ManiphestTask'); if (!$tasks) { return array(); } $task_phids = mpull($tasks, 'getPHID'); $all_deps = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($task_phids) ->withEdgeTypes(array(PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK)); $all_deps->execute(); $result = array(); foreach ($tasks as $task) { // TODO: Batch this get as CustomField gets cleaned up. $field_list = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_EDIT); $field_list->readFieldsFromStorage($task); $auxiliary = mpull( $field_list->getFields(), 'getValueForStorage', 'getFieldKey'); $task_deps = $all_deps->getDestinationPHIDs( array($task->getPHID()), array(PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK)); $result[$task->getPHID()] = array( 'id' => $task->getID(), 'phid' => $task->getPHID(), 'authorPHID' => $task->getAuthorPHID(), 'ownerPHID' => $task->getOwnerPHID(), 'ccPHIDs' => $task->getCCPHIDs(), 'status' => $task->getStatus(), 'priority' => ManiphestTaskPriority::getTaskPriorityName( $task->getPriority()), 'title' => $task->getTitle(), 'description' => $task->getDescription(), 'projectPHIDs' => $task->getProjectPHIDs(), 'uri' => PhabricatorEnv::getProductionURI('/T'.$task->getID()), 'auxiliary' => $auxiliary, 'objectName' => 'T'.$task->getID(), 'dateCreated' => $task->getDateCreated(), 'dateModified' => $task->getDateModified(), 'dependsOnTaskPHIDs' => $task_deps, ); } return $result; } /** * Note this is a temporary stop gap since its easy to make malformed Tasks. * Long-term, the values set in @{method:defineParamTypes} will be used to * validate data implicitly within the larger Conduit application. * * TODO -- remove this in favor of generalized Conduit hotness */ private function validatePHIDList(array $phid_list, $phid_type, $field) { $phid_groups = phid_group_by_type($phid_list); unset($phid_groups[$phid_type]); if (!empty($phid_groups)) { throw id(new ConduitException('ERR-INVALID-PARAMETER')) ->setErrorDescription( 'One or more PHIDs were invalid for '.$field.'.'); } return true; } } diff --git a/src/applications/maniphest/constants/ManiphestTaskStatus.php b/src/applications/maniphest/constants/ManiphestTaskStatus.php index e24037c6c3..c6e053efc3 100644 --- a/src/applications/maniphest/constants/ManiphestTaskStatus.php +++ b/src/applications/maniphest/constants/ManiphestTaskStatus.php @@ -1,101 +1,160 @@ $open, self::STATUS_CLOSED_RESOLVED => $resolved, self::STATUS_CLOSED_WONTFIX => $wontfix, self::STATUS_CLOSED_INVALID => $invalid, self::STATUS_CLOSED_DUPLICATE => $duplicate, self::STATUS_CLOSED_SPITE => $spite, ); } public static function getTaskStatusFullName($status) { $open = pht('Open'); $resolved = pht('Closed, Resolved'); $wontfix = pht('Closed, Wontfix'); $invalid = pht('Closed, Invalid'); $duplicate = pht('Closed, Duplicate'); $spite = pht('Closed, Spite'); $map = array( self::STATUS_OPEN => $open, self::STATUS_CLOSED_RESOLVED => $resolved, self::STATUS_CLOSED_WONTFIX => $wontfix, self::STATUS_CLOSED_INVALID => $invalid, self::STATUS_CLOSED_DUPLICATE => $duplicate, self::STATUS_CLOSED_SPITE => $spite, ); return idx($map, $status, '???'); } public static function getTaskStatusColor($status) { $default = self::COLOR_STATUS_OPEN; $map = array( self::STATUS_OPEN => self::COLOR_STATUS_OPEN, self::STATUS_CLOSED_RESOLVED => self::COLOR_STATUS_CLOSED, self::STATUS_CLOSED_WONTFIX => self::COLOR_STATUS_CLOSED, self::STATUS_CLOSED_INVALID => self::COLOR_STATUS_CLOSED, self::STATUS_CLOSED_DUPLICATE => self::COLOR_STATUS_CLOSED, self::STATUS_CLOSED_SPITE => self::COLOR_STATUS_CLOSED, ); return idx($map, $status, $default); } public static function getIcon($status) { $default = 'oh-open'; $map = array( self::STATUS_OPEN => 'oh-open', self::STATUS_CLOSED_RESOLVED => 'oh-closed-dark', self::STATUS_CLOSED_WONTFIX => 'oh-closed-dark', self::STATUS_CLOSED_INVALID => 'oh-closed-dark', self::STATUS_CLOSED_DUPLICATE => 'oh-closed-dark', self::STATUS_CLOSED_SPITE => 'oh-closed-dark', ); return idx($map, $status, $default); } public static function renderFullDescription($status) { $color = self::getTaskStatusColor($status); $img = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_STATUS) ->setSpriteIcon(self::getIcon($status)); $tag = phutil_tag( 'span', array( 'class' => 'phui-header-'.$color.' plr', ), array( $img, self::getTaskStatusFullName($status), )); return $tag; } + + public static function getDefaultStatus() { + return self::STATUS_OPEN; + } + + public static function getOpenStatusConstants() { + return array( + self::STATUS_OPEN, + ); + } + + public static function isOpenStatus($status) { + foreach (self::getOpenStatusConstants() as $constant) { + if ($status == $constant) { + return true; + } + } + return false; + } + + public static function getStatusPrefixMap() { + return array( + 'resolve' => self::STATUS_CLOSED_RESOLVED, + 'resolves' => self::STATUS_CLOSED_RESOLVED, + 'resolved' => self::STATUS_CLOSED_RESOLVED, + 'fix' => self::STATUS_CLOSED_RESOLVED, + 'fixes' => self::STATUS_CLOSED_RESOLVED, + 'fixed' => self::STATUS_CLOSED_RESOLVED, + 'wontfix' => self::STATUS_CLOSED_WONTFIX, + 'wontfixes' => self::STATUS_CLOSED_WONTFIX, + 'wontfixed' => self::STATUS_CLOSED_WONTFIX, + 'spite' => self::STATUS_CLOSED_SPITE, + 'spites' => self::STATUS_CLOSED_SPITE, + 'spited' => self::STATUS_CLOSED_SPITE, + 'invalidate' => self::STATUS_CLOSED_INVALID, + 'invaldiates' => self::STATUS_CLOSED_INVALID, + 'invalidated' => self::STATUS_CLOSED_INVALID, + 'close' => self::STATUS_CLOSED_RESOLVED, + 'closes' => self::STATUS_CLOSED_RESOLVED, + 'closed' => self::STATUS_CLOSED_RESOLVED, + 'ref' => null, + 'refs' => null, + 'references' => null, + 'cf.' => null, + ); + } + + public static function getStatusSuffixMap() { + return array( + 'as resolved' => self::STATUS_CLOSED_RESOLVED, + 'as fixed' => self::STATUS_CLOSED_RESOLVED, + 'as wontfix' => self::STATUS_CLOSED_WONTFIX, + 'as spite' => self::STATUS_CLOSED_SPITE, + 'out of spite' => self::STATUS_CLOSED_SPITE, + 'as invalid' => self::STATUS_CLOSED_INVALID, + ); + } + + } diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php index 5b7acefd88..eb6fe8ee97 100644 --- a/src/applications/maniphest/controller/ManiphestReportController.php +++ b/src/applications/maniphest/controller/ManiphestReportController.php @@ -1,751 +1,751 @@ view = idx($data, 'view'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($request->isFormPost()) { $uri = $request->getRequestURI(); $project = head($request->getArr('set_project')); $project = nonempty($project, null); $uri = $uri->alter('project', $project); $window = $request->getStr('set_window'); $uri = $uri->alter('window', $window); return id(new AphrontRedirectResponse())->setURI($uri); } $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI('/maniphest/report/')); $nav->addLabel(pht('Open Tasks')); $nav->addFilter('user', pht('By User')); $nav->addFilter('project', pht('By Project')); $nav->addLabel(pht('Burnup')); $nav->addFilter('burn', pht('Burnup Rate')); $this->view = $nav->selectFilter($this->view, 'user'); require_celerity_resource('maniphest-report-css'); switch ($this->view) { case 'burn': $core = $this->renderBurn(); break; case 'user': case 'project': $core = $this->renderOpenTasks(); break; default: return new Aphront404Response(); } $nav->appendChild($core); $nav->setCrumbs( $this->buildApplicationCrumbs() ->addTextCrumb(pht('Reports'))); return $this->buildApplicationPage( $nav, array( 'title' => pht('Maniphest Reports'), )); } public function renderBurn() { $request = $this->getRequest(); $user = $request->getUser(); $handle = null; $project_phid = $request->getStr('project'); if ($project_phid) { $phids = array($project_phid); $handles = $this->loadViewerHandles($phids); $handle = $handles[$project_phid]; } $table = new ManiphestTransaction(); $conn = $table->establishConnection('r'); $joins = ''; if ($project_phid) { $joins = qsprintf( $conn, 'JOIN %T t ON x.objectPHID = t.phid JOIN %T p ON p.taskPHID = t.phid AND p.projectPHID = %s', id(new ManiphestTask())->getTableName(), id(new ManiphestTaskProject())->getTableName(), $project_phid); } $data = queryfx_all( $conn, 'SELECT x.oldValue, x.newValue, x.dateCreated FROM %T x %Q WHERE transactionType = %s ORDER BY x.dateCreated ASC', $table->getTableName(), $joins, ManiphestTransaction::TYPE_STATUS); $stats = array(); $day_buckets = array(); $open_tasks = array(); foreach ($data as $key => $row) { // NOTE: Hack to avoid json_decode(). $oldv = trim($row['oldValue'], '"'); $newv = trim($row['newValue'], '"'); $old_is_open = ($oldv === (string)ManiphestTaskStatus::STATUS_OPEN); $new_is_open = ($newv === (string)ManiphestTaskStatus::STATUS_OPEN); $is_open = ($new_is_open && !$old_is_open); $is_close = ($old_is_open && !$new_is_open); $data[$key]['_is_open'] = $is_open; $data[$key]['_is_close'] = $is_close; if (!$is_open && !$is_close) { // This is either some kind of bogus event, or a resolution change // (e.g., resolved -> invalid). Just skip it. continue; } $day_bucket = phabricator_format_local_time( $row['dateCreated'], $user, 'Yz'); $day_buckets[$day_bucket] = $row['dateCreated']; if (empty($stats[$day_bucket])) { $stats[$day_bucket] = array( 'open' => 0, 'close' => 0, ); } $stats[$day_bucket][$is_close ? 'close' : 'open']++; } $template = array( 'open' => 0, 'close' => 0, ); $rows = array(); $rowc = array(); $last_month = null; $last_month_epoch = null; $last_week = null; $last_week_epoch = null; $week = null; $month = null; $last = last_key($stats) - 1; $period = $template; foreach ($stats as $bucket => $info) { $epoch = $day_buckets[$bucket]; $week_bucket = phabricator_format_local_time( $epoch, $user, 'YW'); if ($week_bucket != $last_week) { if ($week) { $rows[] = $this->formatBurnRow( 'Week of '.phabricator_date($last_week_epoch, $user), $week); $rowc[] = 'week'; } $week = $template; $last_week = $week_bucket; $last_week_epoch = $epoch; } $month_bucket = phabricator_format_local_time( $epoch, $user, 'Ym'); if ($month_bucket != $last_month) { if ($month) { $rows[] = $this->formatBurnRow( phabricator_format_local_time($last_month_epoch, $user, 'F, Y'), $month); $rowc[] = 'month'; } $month = $template; $last_month = $month_bucket; $last_month_epoch = $epoch; } $rows[] = $this->formatBurnRow(phabricator_date($epoch, $user), $info); $rowc[] = null; $week['open'] += $info['open']; $week['close'] += $info['close']; $month['open'] += $info['open']; $month['close'] += $info['close']; $period['open'] += $info['open']; $period['close'] += $info['close']; } if ($week) { $rows[] = $this->formatBurnRow( pht('Week To Date'), $week); $rowc[] = 'week'; } if ($month) { $rows[] = $this->formatBurnRow( pht('Month To Date'), $month); $rowc[] = 'month'; } $rows[] = $this->formatBurnRow( pht('All Time'), $period); $rowc[] = 'aggregate'; $rows = array_reverse($rows); $rowc = array_reverse($rowc); $table = new AphrontTableView($rows); $table->setRowClasses($rowc); $table->setHeaders( array( pht('Period'), pht('Opened'), pht('Closed'), pht('Change'), )); $table->setColumnClasses( array( 'right wide', 'n', 'n', 'n', )); if ($handle) { $inst = pht( "NOTE: This table reflects tasks currently in ". "the project. If a task was opened in the past but added to ". "the project recently, it is counted on the day it was ". "opened, not the day it was categorized. If a task was part ". "of this project in the past but no longer is, it is not ". "counted at all."); $header = pht("Task Burn Rate for Project %s", $handle->renderLink()); $caption = phutil_tag('p', array(), $inst); } else { $header = pht("Task Burn Rate for All Tasks"); $caption = null; } $panel = new AphrontPanelView(); $panel->setHeader($header); $panel->setCaption($caption); $panel->appendChild($table); $tokens = array(); if ($handle) { $tokens = array($handle); } $filter = $this->renderReportFilters($tokens, $has_window = false); $id = celerity_generate_unique_node_id(); $chart = phutil_tag( 'div', array( 'id' => $id, 'style' => 'border: 1px solid #6f6f6f; '. 'margin: 1em 2em; '. 'height: 400px; ', ), ''); list($burn_x, $burn_y) = $this->buildSeries($data); require_celerity_resource('raphael-core'); require_celerity_resource('raphael-g'); require_celerity_resource('raphael-g-line'); Javelin::initBehavior('line-chart', array( 'hardpoint' => $id, 'x' => array( $burn_x, ), 'y' => array( $burn_y, ), 'xformat' => 'epoch', 'yformat' => 'int', )); return array($filter, $chart, $panel); } private function renderReportFilters(array $tokens, $has_window) { $request = $this->getRequest(); $user = $request->getUser(); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/searchproject/') ->setLabel(pht('Project')) ->setLimit(1) ->setName('set_project') ->setValue($tokens)); if ($has_window) { list($window_str, $ignored, $window_error) = $this->getWindow(); $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Recently Means')) ->setName('set_window') ->setCaption( pht('Configure the cutoff for the "Recently Closed" column.')) ->setValue($window_str) ->setError($window_error)); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Filter By Project'))); $filter = new AphrontListFilterView(); $filter->appendChild($form); return $filter; } private function buildSeries(array $data) { $out = array(); $counter = 0; foreach ($data as $row) { $t = (int)$row['dateCreated']; if ($row['_is_close']) { --$counter; $out[$t] = $counter; } else if ($row['_is_open']) { ++$counter; $out[$t] = $counter; } } return array(array_keys($out), array_values($out)); } private function formatBurnRow($label, $info) { $delta = $info['open'] - $info['close']; $fmt = number_format($delta); if ($delta > 0) { $fmt = '+'.$fmt; $fmt = phutil_tag('span', array('class' => 'red'), $fmt); } else { $fmt = phutil_tag('span', array('class' => 'green'), $fmt); } return array( $label, number_format($info['open']), number_format($info['close']), $fmt); } public function renderOpenTasks() { $request = $this->getRequest(); $user = $request->getUser(); $query = id(new ManiphestTaskQuery()) ->setViewer($user) - ->withStatus(ManiphestTaskQuery::STATUS_OPEN); + ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()); $project_phid = $request->getStr('project'); $project_handle = null; if ($project_phid) { $phids = array($project_phid); $handles = $this->loadViewerHandles($phids); $project_handle = $handles[$project_phid]; $query->withAnyProjects($phids); } $tasks = $query->execute(); $recently_closed = $this->loadRecentlyClosedTasks(); $date = phabricator_date(time(), $user); switch ($this->view) { case 'user': $result = mgroup($tasks, 'getOwnerPHID'); $leftover = idx($result, '', array()); unset($result['']); $result_closed = mgroup($recently_closed, 'getOwnerPHID'); $leftover_closed = idx($result_closed, '', array()); unset($result_closed['']); $base_link = '/maniphest/?assigned='; $leftover_name = phutil_tag('em', array(), pht('(Up For Grabs)')); $col_header = pht('User'); $header = pht('Open Tasks by User and Priority (%s)', $date); break; case 'project': $result = array(); $leftover = array(); foreach ($tasks as $task) { $phids = $task->getProjectPHIDs(); if ($phids) { foreach ($phids as $project_phid) { $result[$project_phid][] = $task; } } else { $leftover[] = $task; } } $result_closed = array(); $leftover_closed = array(); foreach ($recently_closed as $task) { $phids = $task->getProjectPHIDs(); if ($phids) { foreach ($phids as $project_phid) { $result_closed[$project_phid][] = $task; } } else { $leftover_closed[] = $task; } } $base_link = '/maniphest/?allProjects[]='; $leftover_name = phutil_tag('em', array(), pht('(No Project)')); $col_header = pht('Project'); $header = pht('Open Tasks by Project and Priority (%s)', $date); break; } $phids = array_keys($result); $handles = $this->loadViewerHandles($phids); $handles = msort($handles, 'getName'); $order = $request->getStr('order', 'name'); list($order, $reverse) = AphrontTableView::parseSort($order); require_celerity_resource('aphront-tooltip-css'); Javelin::initBehavior('phabricator-tooltips', array()); $rows = array(); $pri_total = array(); foreach (array_merge($handles, array(null)) as $handle) { if ($handle) { if (($project_handle) && ($project_handle->getPHID() == $handle->getPHID())) { // If filtering by, e.g., "bugs", don't show a "bugs" group. continue; } $tasks = idx($result, $handle->getPHID(), array()); $name = phutil_tag( 'a', array( 'href' => $base_link.$handle->getPHID(), ), $handle->getName()); $closed = idx($result_closed, $handle->getPHID(), array()); } else { $tasks = $leftover; $name = $leftover_name; $closed = $leftover_closed; } $taskv = $tasks; $tasks = mgroup($tasks, 'getPriority'); $row = array(); $row[] = $name; $total = 0; foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $label) { $n = count(idx($tasks, $pri, array())); if ($n == 0) { $row[] = '-'; } else { $row[] = number_format($n); } $total += $n; } $row[] = number_format($total); list($link, $oldest_all) = $this->renderOldest($taskv); $row[] = $link; $normal_or_better = array(); foreach ($taskv as $id => $task) { // TODO: This is sort of a hard-code for the default "normal" status. // When reports are more powerful, this should be made more general. if ($task->getPriority() < 50) { continue; } $normal_or_better[$id] = $task; } list($link, $oldest_pri) = $this->renderOldest($normal_or_better); $row[] = $link; if ($closed) { $task_ids = implode(',', mpull($closed, 'getID')); $row[] = phutil_tag( 'a', array( 'href' => '/maniphest/?ids='.$task_ids, 'target' => '_blank', ), number_format(count($closed))); } else { $row[] = '-'; } switch ($order) { case 'total': $row['sort'] = $total; break; case 'oldest-all': $row['sort'] = $oldest_all; break; case 'oldest-pri': $row['sort'] = $oldest_pri; break; case 'closed': $row['sort'] = count($closed); break; case 'name': default: $row['sort'] = $handle ? $handle->getName() : '~'; break; } $rows[] = $row; } $rows = isort($rows, 'sort'); foreach ($rows as $k => $row) { unset($rows[$k]['sort']); } if ($reverse) { $rows = array_reverse($rows); } $cname = array($col_header); $cclass = array('pri right wide'); $pri_map = ManiphestTaskPriority::getShortNameMap(); foreach ($pri_map as $pri => $label) { $cname[] = $label; $cclass[] = 'n'; } $cname[] = 'Total'; $cclass[] = 'n'; $cname[] = javelin_tag( 'span', array( 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Oldest open task.'), 'size' => 200, ), ), pht('Oldest (All)')); $cclass[] = 'n'; $cname[] = javelin_tag( 'span', array( 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Oldest open task, excluding those with Low or '. 'Wishlist priority.'), 'size' => 200, ), ), pht('Oldest (Pri)')); $cclass[] = 'n'; list($ignored, $window_epoch) = $this->getWindow(); $edate = phabricator_datetime($window_epoch, $user); $cname[] = javelin_tag( 'span', array( 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Closed after %s', $edate), 'size' => 260 ), ), pht('Recently Closed')); $cclass[] = 'n'; $table = new AphrontTableView($rows); $table->setHeaders($cname); $table->setColumnClasses($cclass); $table->makeSortable( $request->getRequestURI(), 'order', $order, $reverse, array( 'name', null, null, null, null, null, null, 'total', 'oldest-all', 'oldest-pri', 'closed', )); $panel = new AphrontPanelView(); $panel->setHeader($header); $panel->appendChild($table); $tokens = array(); if ($project_handle) { $tokens = array($project_handle); } $filter = $this->renderReportFilters($tokens, $has_window = true); return array($filter, $panel); } /** * Load all the tasks that have been recently closed. */ private function loadRecentlyClosedTasks() { list($ignored, $window_epoch) = $this->getWindow(); $table = new ManiphestTask(); $xtable = new ManiphestTransaction(); $conn_r = $table->establishConnection('r'); $tasks = queryfx_all( $conn_r, 'SELECT t.* FROM %T t JOIN %T x ON x.objectPHID = t.phid WHERE t.status != 0 AND x.oldValue IN (null, %s, %s) AND x.newValue NOT IN (%s, %s) AND t.dateModified >= %d AND x.dateCreated >= %d', $table->getTableName(), $xtable->getTableName(), // TODO: Gross. This table is not meant to be queried like this. Build // real stats tables. json_encode((int)ManiphestTaskStatus::STATUS_OPEN), json_encode((string)ManiphestTaskStatus::STATUS_OPEN), json_encode((int)ManiphestTaskStatus::STATUS_OPEN), json_encode((string)ManiphestTaskStatus::STATUS_OPEN), $window_epoch, $window_epoch); return id(new ManiphestTask())->loadAllFromArray($tasks); } /** * Parse the "Recently Means" filter into: * * - A string representation, like "12 AM 7 days ago" (default); * - a locale-aware epoch representation; and * - a possible error. */ private function getWindow() { $request = $this->getRequest(); $user = $request->getUser(); $window_str = $this->getRequest()->getStr('window', '12 AM 7 days ago'); $error = null; $window_epoch = null; // Do locale-aware parsing so that the user's timezone is assumed for // time windows like "3 PM", rather than assuming the server timezone. $window_epoch = PhabricatorTime::parseLocalTime($window_str, $user); if (!$window_epoch) { $error = 'Invalid'; $window_epoch = time() - (60 * 60 * 24 * 7); } // If the time ends up in the future, convert it to the corresponding time // and equal distance in the past. This is so users can type "6 days" (which // means "6 days from now") and get the behavior of "6 days ago", rather // than no results (because the window epoch is in the future). This might // be a little confusing because it casues "tomorrow" to mean "yesterday" // and "2022" (or whatever) to mean "ten years ago", but these inputs are // nonsense anyway. if ($window_epoch > time()) { $window_epoch = time() - ($window_epoch - time()); } return array($window_str, $window_epoch, $error); } private function renderOldest(array $tasks) { assert_instances_of($tasks, 'ManiphestTask'); $oldest = null; foreach ($tasks as $id => $task) { if (($oldest === null) || ($task->getDateCreated() < $tasks[$oldest]->getDateCreated())) { $oldest = $id; } } if ($oldest === null) { return array('-', 0); } $oldest = $tasks[$oldest]; $raw_age = (time() - $oldest->getDateCreated()); $age = number_format($raw_age / (24 * 60 * 60)).' d'; $link = javelin_tag( 'a', array( 'href' => '/T'.$oldest->getID(), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => 'T'.$oldest->getID().': '.$oldest->getTitle(), ), 'target' => '_blank', ), $age); return array($link, $raw_age); } } diff --git a/src/applications/maniphest/field/parser/ManiphestCustomFieldStatusParser.php b/src/applications/maniphest/field/parser/ManiphestCustomFieldStatusParser.php index d9973d6d75..3021433c9f 100644 --- a/src/applications/maniphest/field/parser/ManiphestCustomFieldStatusParser.php +++ b/src/applications/maniphest/field/parser/ManiphestCustomFieldStatusParser.php @@ -1,59 +1,29 @@ loadPhabrictorUserPHID(); $author = id(new PhabricatorUser()) ->loadOneWhere('phid = %s', $authorPHID); $task = ManiphestTask::initializeNewTask($author) ->setSubPriority($this->generateTaskSubPriority()) - ->setTitle($this->generateTitle()) - ->setStatus(ManiphestTaskStatus::STATUS_OPEN); + ->setTitle($this->generateTitle()); $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_UNKNOWN, array()); $template = new ManiphestTransaction(); // Accumulate Transactions $changes = array(); $changes[ManiphestTransaction::TYPE_TITLE] = $this->generateTitle(); $changes[ManiphestTransaction::TYPE_DESCRIPTION] = $this->generateDescription(); $changes[ManiphestTransaction::TYPE_OWNER] = $this->loadOwnerPHID(); $changes[ManiphestTransaction::TYPE_STATUS] = $this->generateTaskStatus(); $changes[ManiphestTransaction::TYPE_PRIORITY] = $this->generateTaskPriority(); $changes[ManiphestTransaction::TYPE_CCS] = $this->getCCPHIDs(); $changes[ManiphestTransaction::TYPE_PROJECTS] = $this->getProjectPHIDs(); $transactions = array(); foreach ($changes as $type => $value) { $transaction = clone $template; $transaction->setTransactionType($type); $transaction->setNewValue($value); $transactions[] = $transaction; } // Apply Transactions $editor = id(new ManiphestTransactionEditor()) ->setActor($author) ->setContentSource($content_source) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($task, $transactions); return $task; } public function getCCPHIDs() { $ccs = array(); for ($i = 0; $i < rand(1, 4);$i++) { $ccs[] = $this->loadPhabrictorUserPHID(); } return $ccs; } public function getProjectPHIDs() { $projects = array(); for ($i = 0; $i < rand(1, 4);$i++) { $project = $this->loadOneRandom("PhabricatorProject"); if ($project) { $projects[] = $project->getPHID(); } } return $projects; } public function loadOwnerPHID() { if (rand(0, 3) == 0) { return null; } else { return $this->loadPhabrictorUserPHID(); } } public function generateTitle() { return id(new PhutilLipsumContextFreeGrammar()) ->generate(); } public function generateDescription() { return id(new PhutilLipsumContextFreeGrammar()) ->generateSeveral(rand(30, 40)); } public function generateTaskPriority() { return array_rand(ManiphestTaskPriority::getTaskPriorityMap()); } public function generateTaskSubPriority() { return rand(2 << 16, 2 << 32); } public function generateTaskStatus() { $statuses = array_keys(ManiphestTaskStatus::getTaskStatusMap()); // Make sure 4/5th of all generated Tasks are open $random = rand(0, 4); if ($random != 0) { - return ManiphestTaskStatus::STATUS_OPEN; + return ManiphestTaskStatus::getDefaultStatus(); } else { return array_rand($statuses); } } } diff --git a/src/applications/maniphest/mail/ManiphestReplyHandler.php b/src/applications/maniphest/mail/ManiphestReplyHandler.php index 9f0504655b..e83698c976 100644 --- a/src/applications/maniphest/mail/ManiphestReplyHandler.php +++ b/src/applications/maniphest/mail/ManiphestReplyHandler.php @@ -1,189 +1,189 @@ getDefaultPrivateReplyHandlerEmailAddress($handle, 'T'); } public function getPublicReplyHandlerEmailAddress() { return $this->getDefaultPublicReplyHandlerEmailAddress('T'); } public function getReplyHandlerDomain() { return PhabricatorEnv::getEnvConfig( 'metamta.maniphest.reply-handler-domain'); } public function getReplyHandlerInstructions() { if ($this->supportsReplies()) { return "Reply to comment or attach files, or !close, !claim, ". "!unsubscribe or !assign ."; } else { return null; } } protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { // NOTE: We'll drop in here on both the "reply to a task" and "create a // new task" workflows! Make sure you test both if you make changes! $task = $this->getMailReceiver(); $is_new_task = !$task->getID(); $user = $this->getActor(); $body_data = $mail->parseBody(); $body = $body_data['body']; $body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments()); $xactions = array(); $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_EMAIL, array( 'id' => $mail->getID(), )); $template = new ManiphestTransaction(); $is_unsub = false; if ($is_new_task) { // If this is a new task, create a "User created this task." transaction // and then set the title and description. $xaction = clone $template; $xaction->setTransactionType(ManiphestTransaction::TYPE_STATUS); - $xaction->setNewValue(ManiphestTaskStatus::STATUS_OPEN); + $xaction->setNewValue(ManiphestTaskStatus::getDefaultStatus()); $xactions[] = $xaction; $task->setAuthorPHID($user->getPHID()); $task->setTitle(nonempty($mail->getSubject(), 'Untitled Task')); $task->setDescription($body); $task->setPriority(ManiphestTaskPriority::getDefaultPriority()); } else { $command = $body_data['command']; $command_value = $body_data['command_value']; $ttype = PhabricatorTransactions::TYPE_COMMENT; $new_value = null; switch ($command) { case 'close': $ttype = ManiphestTransaction::TYPE_STATUS; $new_value = ManiphestTaskStatus::STATUS_CLOSED_RESOLVED; break; case 'claim': $ttype = ManiphestTransaction::TYPE_OWNER; $new_value = $user->getPHID(); break; case 'assign': $ttype = ManiphestTransaction::TYPE_OWNER; if ($command_value) { $assign_users = id(new PhabricatorPeopleQuery()) ->setViewer($user) ->withUsernames(array($command_value)) ->execute(); if ($assign_users) { $assign_user = head($assign_users); $new_value = $assign_user->getPHID(); } } // assign to the user by default if (!$new_value) { $new_value = $user->getPHID(); } break; case 'unsubscribe': $is_unsub = true; $ttype = ManiphestTransaction::TYPE_CCS; $ccs = $task->getCCPHIDs(); foreach ($ccs as $k => $phid) { if ($phid == $user->getPHID()) { unset($ccs[$k]); } } $new_value = array_values($ccs); break; } if ($ttype != PhabricatorTransactions::TYPE_COMMENT) { $xaction = clone $template; $xaction->setTransactionType($ttype); $xaction->setNewValue($new_value); $xactions[] = $xaction; } if (strlen($body)) { $xaction = clone $template; $xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT); $xaction->attachComment( id(new ManiphestTransactionComment()) ->setContent($body)); $xactions[] = $xaction; } } $ccs = $mail->loadCCPHIDs(); $old_ccs = $task->getCCPHIDs(); $new_ccs = array_merge($old_ccs, $ccs); if (!$is_unsub) { $new_ccs[] = $user->getPHID(); } $new_ccs = array_unique($new_ccs); if (array_diff($new_ccs, $old_ccs)) { $cc_xaction = clone $template; $cc_xaction->setTransactionType(ManiphestTransaction::TYPE_CCS); $cc_xaction->setNewValue($new_ccs); $xactions[] = $cc_xaction; } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK, array( 'task' => $task, 'mail' => $mail, 'new' => $is_new_task, 'transactions' => $xactions, )); $event->setUser($user); PhutilEventEngine::dispatchEvent($event); $task = $event->getValue('task'); $xactions = $event->getValue('transactions'); $editor = id(new ManiphestTransactionEditor()) ->setActor($user) ->setParentMessageID($mail->getMessageID()) ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->setContentSource($content_source) ->applyTransactions($task, $xactions); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK, array( 'task' => $task, 'new' => $is_new_task, 'transactions' => $xactions, )); $event->setUser($user); PhutilEventEngine::dispatchEvent($event); } } diff --git a/src/applications/maniphest/phid/ManiphestPHIDTypeTask.php b/src/applications/maniphest/phid/ManiphestPHIDTypeTask.php index 2af8135438..99bcd1d8ac 100644 --- a/src/applications/maniphest/phid/ManiphestPHIDTypeTask.php +++ b/src/applications/maniphest/phid/ManiphestPHIDTypeTask.php @@ -1,76 +1,76 @@ withPHIDs($phids); } public function loadHandles( PhabricatorHandleQuery $query, array $handles, array $objects) { foreach ($handles as $phid => $handle) { $task = $objects[$phid]; $id = $task->getID(); $title = $task->getTitle(); $handle->setName("T{$id}"); $handle->setFullName("T{$id}: {$title}"); $handle->setURI("/T{$id}"); - if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) { + if (!ManiphestTaskStatus::isOpenStatus($task->getStatus())) { $handle->setStatus(PhabricatorObjectHandleStatus::STATUS_CLOSED); } } } public function canLoadNamedObject($name) { return preg_match('/^T\d*[1-9]\d*$/i', $name); } public function loadNamedObjects( PhabricatorObjectQuery $query, array $names) { $id_map = array(); foreach ($names as $name) { $id = (int)substr($name, 1); $id_map[$id][] = $name; } $objects = id(new ManiphestTaskQuery()) ->setViewer($query->getViewer()) ->withIDs(array_keys($id_map)) ->execute(); $results = array(); foreach ($objects as $id => $object) { foreach (idx($id_map, $id, array()) as $name) { $results[$name] = $object; } } return $results; } } diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php index 401bff04b1..df9deff3c0 100644 --- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php +++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php @@ -1,443 +1,449 @@ setParameter( 'assignedPHIDs', $this->readUsersFromRequest($request, 'assigned')); $saved->setParameter('withUnassigned', $request->getBool('withUnassigned')); $saved->setParameter( 'authorPHIDs', $this->readUsersFromRequest($request, 'authors')); $saved->setParameter( 'subscriberPHIDs', $this->readPHIDsFromRequest($request, 'subscribers')); $saved->setParameter('statuses', $request->getArr('statuses')); $saved->setParameter('priorities', $request->getArr('priorities')); $saved->setParameter('group', $request->getStr('group')); $saved->setParameter('order', $request->getStr('order')); $ids = $request->getStrList('ids'); foreach ($ids as $key => $id) { $id = trim($id, ' Tt'); if (!$id || !is_numeric($id)) { unset($ids[$key]); } else { $ids[$key] = $id; } } $saved->setParameter('ids', $ids); $saved->setParameter('fulltext', $request->getStr('fulltext')); $saved->setParameter( 'allProjectPHIDs', $request->getArr('allProjects')); $saved->setParameter( 'withNoProject', $request->getBool('withNoProject')); $saved->setParameter( 'anyProjectPHIDs', $request->getArr('anyProjects')); $saved->setParameter( 'excludeProjectPHIDs', $request->getArr('excludeProjects')); $saved->setParameter( 'userProjectPHIDs', $this->readUsersFromRequest($request, 'userProjects')); $saved->setParameter('createdStart', $request->getStr('createdStart')); $saved->setParameter('createdEnd', $request->getStr('createdEnd')); $limit = $request->getInt('limit'); if ($limit > 0) { $saved->setParameter('limit', $limit); } $this->readCustomFieldsFromRequest($request, $saved); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new ManiphestTaskQuery()); $author_phids = $saved->getParameter('authorPHIDs'); if ($author_phids) { $query->withAuthors($author_phids); } $subscriber_phids = $saved->getParameter('subscriberPHIDs'); if ($subscriber_phids) { $query->withSubscribers($subscriber_phids); } $with_unassigned = $saved->getParameter('withUnassigned'); if ($with_unassigned) { $query->withOwners(array(null)); } else { $assigned_phids = $saved->getParameter('assignedPHIDs', array()); if ($assigned_phids) { $query->withOwners($assigned_phids); } } $statuses = $saved->getParameter('statuses'); if ($statuses) { $query->withStatuses($statuses); } $priorities = $saved->getParameter('priorities'); if ($priorities) { $query->withPriorities($priorities); } $order = $saved->getParameter('order'); $order = idx($this->getOrderValues(), $order); if ($order) { $query->setOrderBy($order); } else { $query->setOrderBy(head($this->getOrderValues())); } $group = $saved->getParameter('group'); $group = idx($this->getGroupValues(), $group); if ($group) { $query->setGroupBy($group); } else { $query->setGroupBy(head($this->getGroupValues())); } $ids = $saved->getParameter('ids'); if ($ids) { $query->withIDs($ids); } $fulltext = $saved->getParameter('fulltext'); if (strlen($fulltext)) { $query->withFullTextSearch($fulltext); } $with_no_project = $saved->getParameter('withNoProject'); if ($with_no_project) { $query->withAllProjects(array(ManiphestTaskOwner::PROJECT_NO_PROJECT)); } else { $project_phids = $saved->getParameter('allProjectPHIDs'); if ($project_phids) { $query->withAllProjects($project_phids); } } $any_project_phids = $saved->getParameter('anyProjectPHIDs'); if ($any_project_phids) { $query->withAnyProjects($any_project_phids); } $exclude_project_phids = $saved->getParameter('excludeProjectPHIDs'); if ($exclude_project_phids) { $query->withoutProjects($exclude_project_phids); } $user_project_phids = $saved->getParameter('userProjectPHIDs'); if ($user_project_phids) { $query->withAnyUserProjects($user_project_phids); } $start = $this->parseDateTime($saved->getParameter('createdStart')); $end = $this->parseDateTime($saved->getParameter('createdEnd')); if ($start) { $query->withDateCreatedAfter($start); } if ($end) { $query->withDateCreatedBefore($end); } $this->applyCustomFieldsToQuery($query, $saved); return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $assigned_phids = $saved->getParameter('assignedPHIDs', array()); $author_phids = $saved->getParameter('authorPHIDs', array()); $all_project_phids = $saved->getParameter( 'allProjectPHIDs', array()); $any_project_phids = $saved->getParameter( 'anyProjectPHIDs', array()); $exclude_project_phids = $saved->getParameter( 'excludeProjectPHIDs', array()); $user_project_phids = $saved->getParameter( 'userProjectPHIDs', array()); $subscriber_phids = $saved->getParameter('subscriberPHIDs', array()); $all_phids = array_merge( $assigned_phids, $author_phids, $all_project_phids, $any_project_phids, $exclude_project_phids, $user_project_phids, $subscriber_phids); if ($all_phids) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->withPHIDs($all_phids) ->execute(); } else { $handles = array(); } $assigned_handles = array_select_keys($handles, $assigned_phids); $author_handles = array_select_keys($handles, $author_phids); $all_project_handles = array_select_keys($handles, $all_project_phids); $any_project_handles = array_select_keys($handles, $any_project_phids); $exclude_project_handles = array_select_keys( $handles, $exclude_project_phids); $user_project_handles = array_select_keys($handles, $user_project_phids); $subscriber_handles = array_select_keys($handles, $subscriber_phids); $with_unassigned = $saved->getParameter('withUnassigned'); $with_no_projects = $saved->getParameter('withNoProject'); $statuses = $saved->getParameter('statuses', array()); $statuses = array_fuse($statuses); $status_control = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Status')); foreach (ManiphestTaskStatus::getTaskStatusMap() as $status => $name) { $status_control->addCheckbox( 'statuses[]', $status, $name, isset($statuses[$status])); } $priorities = $saved->getParameter('priorities', array()); $priorities = array_fuse($priorities); $priority_control = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Priority')); foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $name) { $priority_control->addCheckbox( 'priorities[]', $pri, $name, isset($priorities[$pri])); } $ids = $saved->getParameter('ids', array()); $form ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/accounts/') ->setName('assigned') ->setLabel(pht('Assigned To')) ->setValue($assigned_handles)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'withUnassigned', 1, pht('Show only unassigned tasks.'), $with_unassigned)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/projects/') ->setName('allProjects') ->setLabel(pht('In All Projects')) ->setValue($all_project_handles)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'withNoProject', 1, pht('Show only tasks with no projects.'), $with_no_projects)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/projects/') ->setName('anyProjects') ->setLabel(pht('In Any Project')) ->setValue($any_project_handles)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/projects/') ->setName('excludeProjects') ->setLabel(pht('Not In Projects')) ->setValue($exclude_project_handles)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/accounts/') ->setName('userProjects') ->setLabel(pht('In Users\' Projects')) ->setValue($user_project_handles)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/accounts/') ->setName('authors') ->setLabel(pht('Authors')) ->setValue($author_handles)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/mailable/') ->setName('subscribers') ->setLabel(pht('Subscribers')) ->setValue($subscriber_handles)) ->appendChild($status_control) ->appendChild($priority_control) ->appendChild( id(new AphrontFormSelectControl()) ->setName('group') ->setLabel(pht('Group By')) ->setValue($saved->getParameter('group')) ->setOptions($this->getGroupOptions())) ->appendChild( id(new AphrontFormSelectControl()) ->setName('order') ->setLabel(pht('Order By')) ->setValue($saved->getParameter('order')) ->setOptions($this->getOrderOptions())) ->appendChild( id(new AphrontFormTextControl()) ->setName('fulltext') ->setLabel(pht('Contains Text')) ->setValue($saved->getParameter('fulltext'))) ->appendChild( id(new AphrontFormTextControl()) ->setName('ids') ->setLabel(pht('Task IDs')) ->setValue(implode(', ', $ids))); $this->appendCustomFieldsToForm($form, $saved); $this->buildDateRange( $form, $saved, 'createdStart', pht('Created After'), 'createdEnd', pht('Created Before')); $form ->appendChild( id(new AphrontFormTextControl()) ->setName('limit') ->setLabel(pht('Page Size')) ->setValue($saved->getParameter('limit', 100))); } protected function getURI($path) { return '/maniphest/'.$path; } public function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { $names['assigned'] = pht('Assigned'); $names['authored'] = pht('Authored'); $names['subscribed'] = pht('Subscribed'); } $names['open'] = pht('Open Tasks'); $names['all'] = pht('All Tasks'); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); $viewer_phid = $this->requireViewer()->getPHID(); switch ($query_key) { case 'all': return $query; case 'assigned': return $query ->setParameter('assignedPHIDs', array($viewer_phid)) - ->setParameter('statuses', array(ManiphestTaskStatus::STATUS_OPEN)); + ->setParameter( + 'statuses', + ManiphestTaskStatus::getOpenStatusConstants()); case 'subscribed': return $query ->setParameter('subscriberPHIDs', array($viewer_phid)) - ->setParameter('statuses', array(ManiphestTaskStatus::STATUS_OPEN)); + ->setParameter( + 'statuses', + ManiphestTaskStatus::getOpenStatusConstants()); case 'open': return $query - ->setParameter('statuses', array(ManiphestTaskStatus::STATUS_OPEN)); + ->setParameter( + 'statuses', + ManiphestTaskStatus::getOpenStatusConstants()); case 'authored': return $query ->setParameter('authorPHIDs', array($viewer_phid)) ->setParameter('order', 'created') ->setParameter('group', 'none'); } return parent::buildSavedQueryFromBuiltin($query_key); } private function getOrderOptions() { return array( 'priority' => pht('Priority'), 'updated' => pht('Date Updated'), 'created' => pht('Date Created'), 'title' => pht('Title'), ); } private function getOrderValues() { return array( 'priority' => ManiphestTaskQuery::ORDER_PRIORITY, 'updated' => ManiphestTaskQuery::ORDER_MODIFIED, 'created' => ManiphestTaskQuery::ORDER_CREATED, 'title' => ManiphestTaskQuery::ORDER_TITLE, ); } private function getGroupOptions() { return array( 'priority' => pht('Priority'), 'assigned' => pht('Assigned'), 'status' => pht('Status'), 'project' => pht('Project'), 'none' => pht('None'), ); } private function getGroupValues() { return array( 'priority' => ManiphestTaskQuery::GROUP_PRIORITY, 'assigned' => ManiphestTaskQuery::GROUP_OWNER, 'status' => ManiphestTaskQuery::GROUP_STATUS, 'project' => ManiphestTaskQuery::GROUP_PROJECT, 'none' => ManiphestTaskQuery::GROUP_NONE, ); } } diff --git a/src/applications/maniphest/search/ManiphestSearchIndexer.php b/src/applications/maniphest/search/ManiphestSearchIndexer.php index 9d67fcf212..971488f64c 100644 --- a/src/applications/maniphest/search/ManiphestSearchIndexer.php +++ b/src/applications/maniphest/search/ManiphestSearchIndexer.php @@ -1,88 +1,88 @@ loadDocumentByPHID($phid); $doc = new PhabricatorSearchAbstractDocument(); $doc->setPHID($task->getPHID()); $doc->setDocumentType(ManiphestPHIDTypeTask::TYPECONST); $doc->setDocumentTitle($task->getTitle()); $doc->setDocumentCreated($task->getDateCreated()); $doc->setDocumentModified($task->getDateModified()); $doc->addField( PhabricatorSearchField::FIELD_BODY, $task->getDescription()); $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, $task->getAuthorPHID(), PhabricatorPeoplePHIDTypeUser::TYPECONST, $task->getDateCreated()); $doc->addRelationship( - ($task->getStatus() == ManiphestTaskStatus::STATUS_OPEN) + (ManiphestTaskStatus::isOpenStatus($task->getStatus())) ? PhabricatorSearchRelationship::RELATIONSHIP_OPEN : PhabricatorSearchRelationship::RELATIONSHIP_CLOSED, $task->getPHID(), ManiphestPHIDTypeTask::TYPECONST, time()); $this->indexTransactions( $doc, new ManiphestTransactionQuery(), array($phid)); foreach ($task->getProjectPHIDs() as $phid) { $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_PROJECT, $phid, PhabricatorProjectPHIDTypeProject::TYPECONST, $task->getDateModified()); // Bogus. } $owner = $task->getOwnerPHID(); if ($owner) { $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_OWNER, $owner, PhabricatorPeoplePHIDTypeUser::TYPECONST, time()); } else { $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_UNOWNED, $task->getPHID(), PhabricatorPHIDConstants::PHID_TYPE_VOID, $task->getDateCreated()); } // We need to load handles here since non-users may subscribe (mailing // lists, e.g.) $ccs = $task->getCCPHIDs(); $handles = id(new PhabricatorHandleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($ccs) ->execute(); foreach ($ccs as $cc) { $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER, $handles[$cc]->getPHID(), $handles[$cc]->getType(), time()); } $this->indexCustomFields($doc, $task); return $doc; } } diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 20e9620137..055042f1fb 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -1,276 +1,277 @@ setViewer($actor) ->withClasses(array('PhabricatorApplicationManiphest')) ->executeOne(); $view_policy = $app->getPolicy(ManiphestCapabilityDefaultView::CAPABILITY); $edit_policy = $app->getPolicy(ManiphestCapabilityDefaultEdit::CAPABILITY); return id(new ManiphestTask()) + ->setStatus(ManiphestTaskStatus::getDefaultStatus()) ->setPriority(ManiphestTaskPriority::getDefaultPriority()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'ccPHIDs' => self::SERIALIZATION_JSON, 'attached' => self::SERIALIZATION_JSON, 'projectPHIDs' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function loadDependsOnTaskPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK); } public function loadDependedOnByTaskPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK); } public function getAttachedPHIDs($type) { return array_keys(idx($this->attached, $type, array())); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(ManiphestPHIDTypeTask::TYPECONST); } public function getCCPHIDs() { return array_values(nonempty($this->ccPHIDs, array())); } public function setProjectPHIDs(array $phids) { $this->projectPHIDs = array_values($phids); $this->projectsNeedUpdate = true; return $this; } public function getProjectPHIDs() { return array_values(nonempty($this->projectPHIDs, array())); } public function setCCPHIDs(array $phids) { $this->ccPHIDs = array_values($phids); $this->subscribersNeedUpdate = true; return $this; } public function setOwnerPHID($phid) { $this->ownerPHID = nonempty($phid, null); $this->subscribersNeedUpdate = true; return $this; } public function setTitle($title) { $this->title = $title; if (!$this->getID()) { $this->originalTitle = $title; } return $this; } public function attachGroupByProjectPHID($phid) { $this->groupByProjectPHID = $phid; return $this; } public function getGroupByProjectPHID() { return $this->assertAttached($this->groupByProjectPHID); } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } $result = parent::save(); if ($this->projectsNeedUpdate) { // If we've changed the project PHIDs for this task, update the link // table. ManiphestTaskProject::updateTaskProjects($this); $this->projectsNeedUpdate = false; } if ($this->subscribersNeedUpdate) { // If we've changed the subscriber PHIDs for this task, update the link // table. ManiphestTaskSubscriber::updateTaskSubscribers($this); $this->subscribersNeedUpdate = false; } return $result; } /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "maniphest:T{$id}:{$field}:{$hash}"; } /** * @task markup */ public function getMarkupText($field) { return $this->getDescription(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newManiphestMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( Policy Interface )--------------------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // The owner of a task can always view and edit it. $owner_phid = $this->getOwnerPHID(); if ($owner_phid) { $user_phid = $user->getPHID(); if ($user_phid == $owner_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { return pht( 'The owner of a task can always view and edit it.'); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { // Sort of ambiguous who this was intended for; just let them both know. return array_filter( array_unique( array( $this->getAuthorPHID(), $this->getOwnerPHID(), ))); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('maniphest.fields'); } public function getCustomFieldBaseClass() { return 'ManiphestCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } } diff --git a/src/applications/project/controller/PhabricatorProjectBoardController.php b/src/applications/project/controller/PhabricatorProjectBoardController.php index 4afd84eaf8..f7ad05708b 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardController.php @@ -1,224 +1,224 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->needImages(true) ->withIDs(array($this->id)) ->executeOne(); if (!$project) { return new Aphront404Response(); } $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array($project->getPHID())) ->execute(); $columns = mpull($columns, null, 'getSequence'); // If there's no default column, create one now. if (empty($columns[0])) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $column = PhabricatorProjectColumn::initializeNewColumn($viewer) ->setSequence(0) ->setProjectPHID($project->getPHID()) ->save(); $column->attachProject($project); $columns[0] = $column; unset($unguarded); } ksort($columns); $tasks = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withAllProjects(array($project->getPHID())) - ->withStatus(ManiphestTaskQuery::STATUS_OPEN) + ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) ->execute(); $tasks = mpull($tasks, null, 'getPHID'); if ($tasks) { $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_COLUMN; $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($tasks, 'getPHID')) ->withEdgeTypes(array($edge_type)) ->withDestinationPHIDs(mpull($columns, 'getPHID')); $edge_query->execute(); } $task_map = array(); $default_phid = $columns[0]->getPHID(); foreach ($tasks as $task) { $task_phid = $task->getPHID(); $column_phids = $edge_query->getDestinationPHIDs(array($task_phid)); $column_phid = head($column_phids); $column_phid = nonempty($column_phid, $default_phid); $task_map[$column_phid][] = $task_phid; } $board_id = celerity_generate_unique_node_id(); $board = id(new PHUIWorkboardView()) ->setUser($viewer) ->setFluidishLayout(true) ->setID($board_id); $this->initBehavior( 'project-boards', array( 'boardID' => $board_id, 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), )); $this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); foreach ($columns as $column) { $panel = id(new PHUIWorkpanelView()) ->setHeader($column->getDisplayName()) ->setHeaderColor($column->getHeaderColor()) ->setEditURI('edit/'.$column->getID().'/'); $cards = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setCards(true) ->setFlush(true) ->setAllowEmptyList(true) ->addSigil('project-column') ->setMetadata( array( 'columnPHID' => $column->getPHID(), )); $task_phids = idx($task_map, $column->getPHID(), array()); foreach (array_select_keys($tasks, $task_phids) as $task) { $cards->addItem($this->renderTaskCard($task)); } $panel->setCards($cards); if (!$task_phids) { $cards->addClass('project-column-empty'); } $board->addPanel($panel); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( $project->getName(), $this->getApplicationURI('view/'.$project->getID().'/')); $crumbs->addTextCrumb(pht('Board')); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $actions = id(new PhabricatorActionListView()) ->setUser($viewer) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Add Column')) ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/')) ->setIcon('create') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $plist = id(new PHUIPropertyListView()); // TODO: Need this to get actions to render. $plist->addProperty( pht('Project Boards'), phutil_tag( 'em', array(), pht( 'This feature is beta, but should mostly work.'))); $plist->setActionList($actions); $header = id(new PHUIHeaderView()) ->setHeader($project->getName()) ->setUser($viewer) ->setImage($project->getProfileImageURI()) ->setPolicyObject($project); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($plist); $board_box = id(new PHUIBoxView()) ->appendChild($board) ->addMargin(PHUI::MARGIN_LARGE); return $this->buildApplicationPage( array( $crumbs, $box, $board_box, ), array( 'title' => pht('%s Board', $project->getName()), 'device' => true, )); } private function renderTaskCard(ManiphestTask $task) { $request = $this->getRequest(); $viewer = $request->getUser(); $handles = $this->handles; $color_map = ManiphestTaskPriority::getColorMap(); $bar_color = idx($color_map, $task->getPriority(), 'grey'); // TODO: Batch this earlier on. $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $task, PhabricatorPolicyCapability::CAN_EDIT); $card = id(new PHUIObjectItemView()) ->setObjectName('T'.$task->getID()) ->setHeader($task->getTitle()) ->setGrippable($can_edit) ->setHref('/T'.$task->getID()) ->addSigil('project-card') ->setMetadata( array( 'objectPHID' => $task->getPHID(), )) ->addAction( id(new PHUIListItemView()) ->setName(pht('Edit')) ->setIcon('edit') ->setHref('/maniphest/task/edit/'.$task->getID().'/') ->setWorkflow(true)) ->setBarColor($bar_color); if ($task->getOwnerPHID()) { $owner = $handles[$task->getOwnerPHID()]; $card->addAttribute($owner->renderLink()); } return $card; } } diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 6143907f3d..f1d0716384 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -1,265 +1,265 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->needMembers(true) ->needImages(true) ->executeOne(); if (!$project) { return new Aphront404Response(); } $picture = $project->getProfileImageURI(); require_celerity_resource('phabricator-profile-css'); $tasks = $this->renderTasksPage($project); $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs( array( $project->getPHID(), )); $query->setLimit(50); $query->setViewer($this->getRequest()->getUser()); $stories = $query->execute(); $feed = $this->renderStories($stories); $content = phutil_tag_div( 'phabricator-project-layout', array($tasks, $feed)); $id = $this->id; $icon = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) ->setSpriteIcon('workboard'); $board_btn = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Workboards')) ->setHref($this->getApplicationURI("board/{$id}/")) ->setIcon($icon); $header = id(new PHUIHeaderView()) ->setHeader($project->getName()) ->setUser($user) ->setPolicyObject($project) ->setImage($picture) ->addActionLink($board_btn); if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ACTIVE) { $header->setStatus('oh-ok', '', pht('Active')); } else { $header->setStatus('policy-noone', '', pht('Archived')); } $actions = $this->buildActionListView($project); $properties = $this->buildPropertyListView($project, $actions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($project->getName()) ->setActionList($actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $content, ), array( 'title' => $project->getName(), 'device' => true, )); } private function renderFeedPage(PhabricatorProject $project) { $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs(array($project->getPHID())); $query->setViewer($this->getRequest()->getUser()); $query->setLimit(100); $stories = $query->execute(); if (!$stories) { return pht('There are no stories about this project.'); } return $this->renderStories($stories); } 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 phutil_tag_div( 'profile-feed', $view->render()); } private function renderTasksPage(PhabricatorProject $project) { $user = $this->getRequest()->getUser(); $query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withAnyProjects(array($project->getPHID())) - ->withStatus(ManiphestTaskQuery::STATUS_OPEN) + ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) ->setLimit(10); $tasks = $query->execute(); $phids = mpull($tasks, 'getOwnerPHID'); $phids = array_merge( $phids, array_mergev(mpull($tasks, 'getProjectPHIDs'))); $phids = array_filter($phids); $handles = $this->loadViewerHandles($phids); $task_list = new ManiphestTaskListView(); $task_list->setUser($user); $task_list->setTasks($tasks); $task_list->setHandles($handles); $phid = $project->getPHID(); $view_uri = '/maniphest/?statuses[]=0&allProjects[]='.$phid.'#R'; $create_uri = '/maniphest/task/create/?projects='.$phid; $icon = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) ->setSpriteIcon('action-menu'); $button_view = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('View All')) ->setHref($view_uri) ->setIcon($icon); $icon_new = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) ->setSpriteIcon('new'); $button_add = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('New Task')) ->setHref($create_uri) ->setIcon($icon_new); $header = id(new PHUIHeaderView()) ->setHeader(pht('Open Tasks')) ->addActionLink($button_add) ->addActionLink($button_view); $content = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($task_list); return $content; } private function buildActionListView(PhabricatorProject $project) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $project->getID(); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($project) ->setObjectURI($request->getRequestURI()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Project')) ->setIcon('edit') ->setHref($this->getApplicationURI("edit/{$id}/"))); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Members')) ->setIcon('user') ->setHref($this->getApplicationURI("members/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $action = null; if (!$project->isUserMember($viewer->getPHID())) { $can_join = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_JOIN); $action = id(new PhabricatorActionView()) ->setUser($viewer) ->setRenderAsForm(true) ->setHref('/project/update/'.$project->getID().'/join/') ->setIcon('new') ->setDisabled(!$can_join) ->setName(pht('Join Project')); } else { $action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setHref('/project/update/'.$project->getID().'/leave/') ->setIcon('delete') ->setName(pht('Leave Project...')); } $view->addAction($action); return $view; } private function buildPropertyListView( PhabricatorProject $project, PhabricatorActionListView $actions) { $request = $this->getRequest(); $viewer = $request->getUser(); $this->loadHandles($project->getMemberPHIDs()); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($project) ->setActionList($actions); $view->addProperty( pht('Members'), $project->getMemberPHIDs() ? $this->renderHandlesForPHIDs($project->getMemberPHIDs(), ',') : phutil_tag('em', array(), pht('None'))); $field_list = PhabricatorCustomField::getObjectFields( $project, PhabricatorCustomField::ROLE_VIEW); $field_list->appendFieldsToPropertyList($project, $viewer, $view); return $view; } }