diff --git a/src/applications/ponder/constants/PonderQuestionStatus.php b/src/applications/ponder/constants/PonderQuestionStatus.php index f37a208bcc..c401d52234 100644 --- a/src/applications/ponder/constants/PonderQuestionStatus.php +++ b/src/applications/ponder/constants/PonderQuestionStatus.php @@ -1,90 +1,90 @@ pht('Open'), self::STATUS_CLOSED_RESOLVED => pht('Closed, Resolved'), self::STATUS_CLOSED_OBSOLETE => pht('Closed, Obsolete'), self::STATUS_CLOSED_DUPLICATE => pht('Closed, Duplicate'), ); } public static function getQuestionStatusFullName($status) { $map = array( self::STATUS_OPEN => pht('Open'), self::STATUS_CLOSED_RESOLVED => pht('Closed, Resolved'), self::STATUS_CLOSED_OBSOLETE => pht('Closed, Obsolete'), self::STATUS_CLOSED_DUPLICATE => pht('Closed, Duplicate'), ); return idx($map, $status, pht('Unknown')); } public static function getQuestionStatusName($status) { $map = array( self::STATUS_OPEN => pht('Open'), self::STATUS_CLOSED_RESOLVED => pht('Resolved'), self::STATUS_CLOSED_OBSOLETE => pht('Obsolete'), self::STATUS_CLOSED_DUPLICATE => pht('Duplicate'), ); return idx($map, $status, pht('Unknown')); } public static function getQuestionStatusDescription($status) { $map = array( self::STATUS_OPEN => pht('This question is open for answers.'), self::STATUS_CLOSED_RESOLVED => - pht('This question has been resolved.'), + pht('This question has been answered or resolved.'), self::STATUS_CLOSED_OBSOLETE => pht('This question is no longer valid or out of date.'), self::STATUS_CLOSED_DUPLICATE => pht('This question is a duplicate of another question.'), ); return idx($map, $status, pht('Unknown')); } public static function getQuestionStatusTagColor($status) { $map = array( self::STATUS_OPEN => PHUITagView::COLOR_BLUE, self::STATUS_CLOSED_RESOLVED => PHUITagView::COLOR_BLACK, self::STATUS_CLOSED_OBSOLETE => PHUITagView::COLOR_BLACK, self::STATUS_CLOSED_DUPLICATE => PHUITagView::COLOR_BLACK, ); return idx($map, $status); } public static function getQuestionStatusIcon($status) { $map = array( self::STATUS_OPEN => 'fa-question-circle', self::STATUS_CLOSED_RESOLVED => 'fa-check', self::STATUS_CLOSED_OBSOLETE => 'fa-ban', self::STATUS_CLOSED_DUPLICATE => 'fa-clone', ); return idx($map, $status); } public static function getQuestionStatusOpenMap() { return array( self::STATUS_OPEN, ); } public static function getQuestionStatusClosedMap() { return array( self::STATUS_CLOSED_RESOLVED, self::STATUS_CLOSED_OBSOLETE, self::STATUS_CLOSED_DUPLICATE, ); } } diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php index a974716540..2f0ae5099f 100644 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -1,333 +1,279 @@ getViewer(); $id = $request->getURIData('id'); $question = id(new PonderQuestionQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needAnswers(true) ->needProjectPHIDs(true) ->executeOne(); if (!$question) { return new Aphront404Response(); } $answers = $this->buildAnswers($question->getAnswers()); - $authors = mpull($question->getAnswers(), null, 'getAuthorPHID'); - if (isset($authors[$viewer->getPHID()])) { - $answer_add_panel = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) - ->appendChild( - pht( - 'You have already answered this question. You can not answer '. - 'twice, but you can edit your existing answer.')); - } else { - $answer_add_panel = new PonderAddAnswerView(); - $answer_add_panel - ->setQuestion($question) - ->setUser($viewer) - ->setActionURI('/ponder/answer/add/'); - } + $answer_add_panel = id(new PonderAddAnswerView()) + ->setQuestion($question) + ->setUser($viewer) + ->setActionURI('/ponder/answer/add/'); $header = new PHUIHeaderView(); $header->setHeader($question->getTitle()); $header->setUser($viewer); $header->setPolicyObject($question); if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $header->setStatus('fa-square-o', 'bluegrey', pht('Open')); } else { $text = PonderQuestionStatus::getQuestionStatusFullName( $question->getStatus()); $icon = PonderQuestionStatus::getQuestionStatusIcon( $question->getStatus()); $header->setStatus($icon, 'dark', $text); } $actions = $this->buildActionListView($question); $properties = $this->buildPropertyListView($question, $actions); $sidebar = $this->buildSidebar($question); $content_id = celerity_generate_unique_node_id(); $timeline = $this->buildTransactionTimeline( $question, id(new PonderQuestionTransactionQuery()) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))); $xactions = $timeline->getTransactions(); $add_comment = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($question->getPHID()) ->setShowPreview(false) ->setHeaderText(pht('Question Comment')) ->setAction($this->getApplicationURI("/question/comment/{$id}/")) ->setSubmitButtonName(pht('Comment')); $comment_view = phutil_tag( 'div', array( 'id' => $content_id, 'style' => 'display: none;', ), array( $timeline, $add_comment, )); $footer = id(new PonderFooterView()) ->setContentID($content_id) ->setCount(count($xactions)); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties) ->appendChild($footer); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); $crumbs->addTextCrumb('Q'.$id, '/Q'.$id); $ponder_view = id(new PHUITwoColumnView()) ->setMainColumn(array( $object_box, $comment_view, $answers, $answer_add_panel, )) ->setSideColumn($sidebar) ->addClass('ponder-question-view'); return $this->buildApplicationPage( array( $crumbs, $ponder_view, ), array( 'title' => 'Q'.$question->getID().' '.$question->getTitle(), 'pageObjects' => array_merge( array($question->getPHID()), mpull($question->getAnswers(), 'getPHID')), )); } private function buildActionListView(PonderQuestion $question) { $viewer = $this->getViewer(); $request = $this->getRequest(); $id = $question->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $question, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($question) ->setObjectURI($request->getRequestURI()); $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Question')) ->setHref($this->getApplicationURI("/question/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $name = pht('Close Question'); $icon = 'fa-check-square-o'; } else { $name = pht('Reopen Question'); $icon = 'fa-square-o'; } $view->addAction( id(new PhabricatorActionView()) ->setName($name) ->setIcon($icon) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setHref($this->getApplicationURI("/question/status/{$id}/"))); $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-list') ->setName(pht('View History')) ->setHref($this->getApplicationURI("/question/history/{$id}/"))); return $view; } private function buildPropertyListView( PonderQuestion $question, PhabricatorActionListView $actions) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($question) ->setActionList($actions); $view->addProperty( pht('Author'), $viewer->renderHandle($question->getAuthorPHID())); $view->addProperty( pht('Created'), phabricator_datetime($question->getDateCreated(), $viewer)); $view->invokeWillRenderEvent(); $view->addSectionHeader(pht('Question')); $view->addTextContent( array( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), PhabricatorMarkupEngine::renderOneObject( $question, $question->getMarkupField(), $viewer)), )); return $view; } /** * This is fairly non-standard; building N timelines at once (N = number of * answers) is tricky business. * * TODO - re-factor this to ajax in one answer panel at a time in a more * standard fashion. This is necessary to scale this application. */ private function buildAnswers(array $answers) { $viewer = $this->getViewer(); $xactions = id(new PonderAnswerTransactionQuery()) ->setViewer($viewer) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT)) ->withObjectPHIDs(mpull($answers, 'getPHID')) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $xaction_groups = mgroup($xactions, 'getObjectPHID'); $view = array(); foreach ($answers as $answer) { $xactions = idx($xaction_groups, $answer->getPHID(), array()); $id = $answer->getID(); $view[] = id(new PonderAnswerView()) ->setUser($viewer) ->setAnswer($answer) ->setTransactions($xactions) ->setMarkupEngine($engine); } return $view; } - private function wrapComments($n, $stuff) { - if ($n == 0) { - $text = pht('Add a Comment'); - } else { - $text = pht('Show %s Comments', new PhutilNumber($n)); - } - - $show_id = celerity_generate_unique_node_id(); - $hide_id = celerity_generate_unique_node_id(); - - Javelin::initBehavior('phabricator-reveal-content'); - require_celerity_resource('ponder-view-css'); - - $show = phutil_tag( - 'div', - array( - 'id' => $show_id, - 'class' => 'ponder-show-comments', - ), - javelin_tag( - 'a', - array( - 'href' => '#', - 'sigil' => 'reveal-content', - 'meta' => array( - 'showIDs' => array($hide_id), - 'hideIDs' => array($show_id), - ), - ), - $text)); - - $hide = phutil_tag( - 'div', - array( - 'class' => 'ponder-comments-view', - 'id' => $hide_id, - 'style' => 'display: none', - ), - $stuff); - - return array($show, $hide); - } - private function buildSidebar(PonderQuestion $question) { $viewer = $this->getViewer(); $status = $question->getStatus(); $id = $question->getID(); $questions = id(new PonderQuestionQuery()) ->setViewer($viewer) ->withStatuses(array($status)) ->withEdgeLogicPHIDs( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, PhabricatorQueryConstraint::OPERATOR_OR, $question->getProjectPHIDs()) ->setLimit(10) ->execute(); $list = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setNoDataString(pht('No similar questions found.')); foreach ($questions as $question) { if ($id == $question->getID()) { continue; } $item = new PHUIObjectItemView(); $item->setObjectName('Q'.$question->getID()); $item->setHeader($question->getTitle()); $item->setHref('/Q'.$question->getID()); $item->setObject($question); $item->addAttribute( pht('%d Answer(s)', $question->getAnswerCount())); $list->addItem($item); } $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Similar Questions')) ->setObjectList($list); return $box; } } diff --git a/src/applications/ponder/view/PonderAddAnswerView.php b/src/applications/ponder/view/PonderAddAnswerView.php index 53e06074f7..5ffe99edc7 100644 --- a/src/applications/ponder/view/PonderAddAnswerView.php +++ b/src/applications/ponder/view/PonderAddAnswerView.php @@ -1,46 +1,74 @@ question = $question; return $this; } public function setActionURI($uri) { $this->actionURI = $uri; return $this; } public function render() { $question = $this->question; + $viewer = $this->user; + + $authors = mpull($question->getAnswers(), null, 'getAuthorPHID'); + if (isset($authors[$viewer->getPHID()])) { + return id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) + ->setTitle(pht('Already Answered')) + ->appendChild( + pht( + 'You have already answered this question. You can not answer '. + 'twice, but you can edit your existing answer.')); + } + + $info_panel = null; + if ($question->getStatus() != PonderQuestionStatus::STATUS_OPEN) { + $info_panel = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->appendChild( + pht( + 'This question has been marked as closed, + but you can still leave a new answer.')); + } $header = id(new PHUIHeaderView()) ->setHeader(pht('Add Answer')); $form = new AphrontFormView(); $form ->setUser($this->user) ->setAction($this->actionURI) ->setWorkflow(true) ->addHiddenInput('question_id', $question->getID()) ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('answer') ->setLabel(pht('Answer')) ->setError(true) ->setID('answer-content') ->setUser($this->user)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Add Answer'))); - return id(new PHUIObjectBoxView()) + $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($form); + + if ($info_panel) { + $box->setInfoView($info_panel); + } + + return $box; } } diff --git a/src/applications/ponder/view/PonderFooterView.php b/src/applications/ponder/view/PonderFooterView.php index 7e553b3f4b..8f55b86aeb 100644 --- a/src/applications/ponder/view/PonderFooterView.php +++ b/src/applications/ponder/view/PonderFooterView.php @@ -1,79 +1,80 @@ contentID = $content_id; return $this; } public function setCount($count) { $this->count = $count; return $this; } public function addAction($action) { $this->actions[] = $action; return $this; } protected function getTagAttributes() { return array( 'class' => 'ponder-footer-view', ); } protected function getTagContent() { + require_celerity_resource('ponder-view-css'); Javelin::initBehavior('phabricator-reveal-content'); $hide_action_id = celerity_generate_unique_node_id(); $show_action_id = celerity_generate_unique_node_id(); $content_id = $this->contentID; if ($this->count == 0) { $text = pht('Add a Comment'); } else { $text = pht('Show %s Comments', new PhutilNumber($this->count)); } $actions = array(); $hide_action = javelin_tag( 'a', array( 'sigil' => 'reveal-content', 'class' => 'ponder-footer-action', 'id' => $hide_action_id, 'href' => '#', 'meta' => array( 'showIDs' => array($content_id, $show_action_id), 'hideIDs' => array($hide_action_id), ), ), $text); $show_action = javelin_tag( 'a', array( 'sigil' => 'reveal-content', 'style' => 'display: none;', 'class' => 'ponder-footer-action', 'id' => $show_action_id, 'href' => '#', 'meta' => array( 'showIDs' => array($hide_action_id), 'hideIDs' => array($content_id, $show_action_id), ), ), pht('Hide Comments')); $actions[] = $hide_action; $actions[] = $show_action; return array($actions, $this->actions); } }