diff --git a/src/applications/audit/storage/PhabricatorAuditInlineComment.php b/src/applications/audit/storage/PhabricatorAuditInlineComment.php index a5417376fa..c3f2263de6 100644 --- a/src/applications/audit/storage/PhabricatorAuditInlineComment.php +++ b/src/applications/audit/storage/PhabricatorAuditInlineComment.php @@ -1,284 +1,312 @@ proxy = new PhabricatorAuditTransactionComment(); } public function __clone() { $this->proxy = clone $this->proxy; } public function getTransactionPHID() { return $this->proxy->getTransactionPHID(); } public function getTransactionComment() { return $this->proxy; } public function getTransactionCommentForSave() { $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_LEGACY, array()); $this->proxy ->setViewPolicy('public') ->setEditPolicy($this->getAuthorPHID()) ->setContentSource($content_source) ->setCommentVersion(1); return $this->proxy; } public static function loadID($id) { $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere( 'id = %d', $id); if (!$inlines) { return null; } return head(self::buildProxies($inlines)); } + public static function loadPHID($phid) { + $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere( + 'phid = %s', + $phid); + if (!$inlines) { + return null; + } + return head(self::buildProxies($inlines)); + } + public static function loadDraftComments( PhabricatorUser $viewer, $commit_phid) { $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere( 'authorPHID = %s AND commitPHID = %s AND transactionPHID IS NULL AND pathID IS NOT NULL', $viewer->getPHID(), $commit_phid); return self::buildProxies($inlines); } public static function loadPublishedComments( PhabricatorUser $viewer, $commit_phid) { $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere( 'commitPHID = %s AND transactionPHID IS NOT NULL AND pathID IS NOT NULL', $commit_phid); return self::buildProxies($inlines); } public static function loadDraftAndPublishedComments( PhabricatorUser $viewer, $commit_phid, $path_id = null) { if ($path_id === null) { $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere( 'commitPHID = %s AND (transactionPHID IS NOT NULL OR authorPHID = %s) AND pathID IS NOT NULL', $commit_phid, $viewer->getPHID()); } else { $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere( 'commitPHID = %s AND pathID = %d AND (authorPHID = %s OR transactionPHID IS NOT NULL)', $commit_phid, $path_id, $viewer->getPHID()); } return self::buildProxies($inlines); } private static function buildProxies(array $inlines) { $results = array(); foreach ($inlines as $key => $inline) { $results[$key] = PhabricatorAuditInlineComment::newFromModernComment( $inline); } return $results; } public function setSyntheticAuthor($synthetic_author) { $this->syntheticAuthor = $synthetic_author; return $this; } public function getSyntheticAuthor() { return $this->syntheticAuthor; } public function openTransaction() { $this->proxy->openTransaction(); } public function saveTransaction() { $this->proxy->saveTransaction(); } public function save() { $this->getTransactionCommentForSave()->save(); return $this; } public function delete() { $this->proxy->delete(); return $this; } public function getID() { return $this->proxy->getID(); } public function getPHID() { return $this->proxy->getPHID(); } public static function newFromModernComment( PhabricatorAuditTransactionComment $comment) { $obj = new PhabricatorAuditInlineComment(); $obj->proxy = $comment; return $obj; } public function isCompatible(PhabricatorInlineCommentInterface $comment) { return ($this->getAuthorPHID() === $comment->getAuthorPHID()) && ($this->getSyntheticAuthor() === $comment->getSyntheticAuthor()) && ($this->getContent() === $comment->getContent()); } public function setContent($content) { $this->proxy->setContent($content); return $this; } public function getContent() { return $this->proxy->getContent(); } public function isDraft() { return !$this->proxy->getTransactionPHID(); } public function setPathID($id) { $this->proxy->setPathID($id); return $this; } public function getPathID() { return $this->proxy->getPathID(); } public function setIsNewFile($is_new) { $this->proxy->setIsNewFile($is_new); return $this; } public function getIsNewFile() { return $this->proxy->getIsNewFile(); } public function setLineNumber($number) { $this->proxy->setLineNumber($number); return $this; } public function getLineNumber() { return $this->proxy->getLineNumber(); } public function setLineLength($length) { $this->proxy->setLineLength($length); return $this; } public function getLineLength() { return $this->proxy->getLineLength(); } public function setCache($cache) { return $this; } public function getCache() { return null; } public function setAuthorPHID($phid) { $this->proxy->setAuthorPHID($phid); return $this; } public function getAuthorPHID() { return $this->proxy->getAuthorPHID(); } public function setCommitPHID($commit_phid) { $this->proxy->setCommitPHID($commit_phid); return $this; } public function getCommitPHID() { return $this->proxy->getCommitPHID(); } // When setting a comment ID, we also generate a phantom transaction PHID for // the future transaction. public function setAuditCommentID($id) { $this->proxy->setLegacyCommentID($id); $this->proxy->setTransactionPHID( PhabricatorPHID::generateNewPHID( PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST, PhabricatorRepositoryCommitPHIDType::TYPECONST)); return $this; } public function getAuditCommentID() { return $this->proxy->getLegacyCommentID(); } public function setChangesetID($id) { return $this->setPathID($id); } public function getChangesetID() { return $this->getPathID(); } + public function setReplyToCommentPHID($phid) { + $this->proxy->setReplyToCommentPHID($phid); + return $this; + } + + public function getReplyToCommentPHID() { + return $this->proxy->getReplyToCommentPHID(); + } + + public function setHasReplies($has_replies) { + $this->proxy->setHasReplies($has_replies); + return $this; + } + + public function getHasReplies() { + return $this->proxy->getHasReplies(); + } + /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ public function getMarkupFieldKey($field) { return 'AI:'.$this->getID(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newDifferentialMarkupEngine(); } public function getMarkupText($field) { return $this->getContent(); } public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { // Only cache submitted comments. return ($this->getID() && $this->getAuditCommentID()); } } diff --git a/src/applications/differential/controller/DifferentialInlineCommentEditController.php b/src/applications/differential/controller/DifferentialInlineCommentEditController.php index 08366bbdd6..89d5b5e428 100644 --- a/src/applications/differential/controller/DifferentialInlineCommentEditController.php +++ b/src/applications/differential/controller/DifferentialInlineCommentEditController.php @@ -1,96 +1,102 @@ revisionID = $data['id']; } protected function createComment() { // Verify revision and changeset correspond to actual objects. $revision_id = $this->revisionID; $changeset_id = $this->getChangesetID(); $viewer = $this->getRequest()->getUser(); $revision = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs(array($revision_id)) ->executeOne(); if (!$revision) { throw new Exception('Invalid revision ID!'); } if (!id(new DifferentialChangeset())->load($changeset_id)) { throw new Exception('Invalid changeset ID!'); } return id(new DifferentialInlineComment()) ->setRevision($revision) ->setChangesetID($changeset_id); } protected function loadComment($id) { return id(new DifferentialInlineCommentQuery()) ->withIDs(array($id)) ->executeOne(); } + protected function loadCommentByPHID($phid) { + return id(new DifferentialInlineCommentQuery()) + ->withPHIDs(array($phid)) + ->executeOne(); + } + protected function loadCommentForEdit($id) { $request = $this->getRequest(); $user = $request->getUser(); $inline = $this->loadComment($id); if (!$this->canEditInlineComment($user, $inline)) { throw new Exception('That comment is not editable!'); } return $inline; } private function canEditInlineComment( PhabricatorUser $user, DifferentialInlineComment $inline) { // Only the author may edit a comment. if ($inline->getAuthorPHID() != $user->getPHID()) { return false; } // Saved comments may not be edited, for now, although the schema now // supports it. if (!$inline->isDraft()) { return false; } // Inline must be attached to the active revision. if ($inline->getRevisionID() != $this->revisionID) { return false; } return true; } protected function deleteComment(PhabricatorInlineCommentInterface $inline) { $inline->openTransaction(); DifferentialDraft::deleteHasDraft( $inline->getAuthorPHID(), $inline->getRevisionPHID(), $inline->getPHID()); $inline->delete(); $inline->saveTransaction(); } protected function saveComment(PhabricatorInlineCommentInterface $inline) { $inline->openTransaction(); $inline->save(); DifferentialDraft::markHasDraft( $inline->getAuthorPHID(), $inline->getRevisionPHID(), $inline->getPHID()); $inline->saveTransaction(); } } diff --git a/src/applications/differential/query/DifferentialInlineCommentQuery.php b/src/applications/differential/query/DifferentialInlineCommentQuery.php index cd338b74c1..c60316336e 100644 --- a/src/applications/differential/query/DifferentialInlineCommentQuery.php +++ b/src/applications/differential/query/DifferentialInlineCommentQuery.php @@ -1,156 +1,169 @@ revisionIDs = $ids; return $this; } public function withNotDraft($not_draft) { $this->notDraft = $not_draft; return $this; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + public function withViewerAndChangesetIDs($author_phid, array $ids) { $this->viewerAndChangesetIDs = array($author_phid, $ids); return $this; } public function withDraftComments($author_phid, $revision_id) { $this->draftComments = array($author_phid, $revision_id); return $this; } public function withDraftsByAuthors(array $author_phids) { $this->draftsByAuthors = $author_phids; return $this; } public function execute() { $table = new DifferentialTransactionComment(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildLimitClause($conn_r)); $comments = $table->loadAllFromArray($data); foreach ($comments as $key => $value) { $comments[$key] = DifferentialInlineComment::newFromModernComment( $value); } return $comments; } public function executeOne() { return head($this->execute()); } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); // Only find inline comments. $where[] = qsprintf( $conn_r, 'changesetID IS NOT NULL'); if ($this->revisionIDs) { // Look up revision PHIDs. $revision_phids = queryfx_all( $conn_r, 'SELECT phid FROM %T WHERE id IN (%Ld)', id(new DifferentialRevision())->getTableName(), $this->revisionIDs); if (!$revision_phids) { throw new PhabricatorEmptyQueryException(); } $revision_phids = ipull($revision_phids, 'phid'); $where[] = qsprintf( $conn_r, 'revisionPHID IN (%Ls)', $revision_phids); } if ($this->notDraft) { $where[] = qsprintf( $conn_r, 'transactionPHID IS NOT NULL'); } if ($this->ids) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } + if ($this->phids !== null) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + if ($this->viewerAndChangesetIDs) { list($phid, $ids) = $this->viewerAndChangesetIDs; $where[] = qsprintf( $conn_r, 'changesetID IN (%Ld) AND (authorPHID = %s OR transactionPHID IS NOT NULL)', $ids, $phid); } if ($this->draftComments) { list($phid, $rev_id) = $this->draftComments; $rev_phid = queryfx_one( $conn_r, 'SELECT phid FROM %T WHERE id = %d', id(new DifferentialRevision())->getTableName(), $rev_id); if (!$rev_phid) { throw new PhabricatorEmptyQueryException(); } $rev_phid = $rev_phid['phid']; $where[] = qsprintf( $conn_r, 'authorPHID = %s AND revisionPHID = %s AND transactionPHID IS NULL', $phid, $rev_phid); } if ($this->draftsByAuthors) { $where[] = qsprintf( $conn_r, 'authorPHID IN (%Ls) AND transactionPHID IS NULL', $this->draftsByAuthors); } return $this->formatWhereClause($where); } } diff --git a/src/applications/differential/storage/DifferentialInlineComment.php b/src/applications/differential/storage/DifferentialInlineComment.php index 59e555c0fb..8701363708 100644 --- a/src/applications/differential/storage/DifferentialInlineComment.php +++ b/src/applications/differential/storage/DifferentialInlineComment.php @@ -1,217 +1,236 @@ proxy = new DifferentialTransactionComment(); } public function __clone() { $this->proxy = clone $this->proxy; } public function getTransactionCommentForSave() { $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_LEGACY, array()); $this->proxy ->setViewPolicy('public') ->setEditPolicy($this->getAuthorPHID()) ->setContentSource($content_source) ->setCommentVersion(1); return $this->proxy; } public function openTransaction() { $this->proxy->openTransaction(); } public function saveTransaction() { $this->proxy->saveTransaction(); } public function save() { $this->getTransactionCommentForSave()->save(); return $this; } public function delete() { $this->proxy->delete(); return $this; } public function getID() { return $this->proxy->getID(); } public function getPHID() { return $this->proxy->getPHID(); } public static function newFromModernComment( DifferentialTransactionComment $comment) { $obj = new DifferentialInlineComment(); $obj->proxy = $comment; return $obj; } public function setSyntheticAuthor($synthetic_author) { $this->syntheticAuthor = $synthetic_author; return $this; } public function getSyntheticAuthor() { return $this->syntheticAuthor; } public function isCompatible(PhabricatorInlineCommentInterface $comment) { return ($this->getAuthorPHID() === $comment->getAuthorPHID()) && ($this->getSyntheticAuthor() === $comment->getSyntheticAuthor()) && ($this->getContent() === $comment->getContent()); } public function setContent($content) { $this->proxy->setContent($content); return $this; } public function getContent() { return $this->proxy->getContent(); } public function isDraft() { return !$this->proxy->getTransactionPHID(); } public function setChangesetID($id) { $this->proxy->setChangesetID($id); return $this; } public function getChangesetID() { return $this->proxy->getChangesetID(); } public function setIsNewFile($is_new) { $this->proxy->setIsNewFile($is_new); return $this; } public function getIsNewFile() { return $this->proxy->getIsNewFile(); } public function setLineNumber($number) { $this->proxy->setLineNumber($number); return $this; } public function getLineNumber() { return $this->proxy->getLineNumber(); } public function setLineLength($length) { $this->proxy->setLineLength($length); return $this; } public function getLineLength() { return $this->proxy->getLineLength(); } public function setCache($cache) { return $this; } public function getCache() { return null; } public function setAuthorPHID($phid) { $this->proxy->setAuthorPHID($phid); return $this; } public function getAuthorPHID() { return $this->proxy->getAuthorPHID(); } public function setRevision(DifferentialRevision $revision) { $this->proxy->setRevisionPHID($revision->getPHID()); return $this; } public function getRevisionPHID() { return $this->proxy->getRevisionPHID(); } // Although these are purely transitional, they're also *extra* dumb. public function setRevisionID($revision_id) { $revision = id(new DifferentialRevision())->load($revision_id); return $this->setRevision($revision); } public function getRevisionID() { $phid = $this->proxy->getRevisionPHID(); if (!$phid) { return null; } $revision = id(new DifferentialRevision())->loadOneWhere( 'phid = %s', $phid); if (!$revision) { return null; } return $revision->getID(); } // When setting a comment ID, we also generate a phantom transaction PHID for // the future transaction. public function setCommentID($id) { $this->proxy->setTransactionPHID( PhabricatorPHID::generateNewPHID( PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST, DifferentialRevisionPHIDType::TYPECONST)); return $this; } + public function setReplyToCommentPHID($phid) { + $this->proxy->setReplyToCommentPHID($phid); + return $this; + } + + public function getReplyToCommentPHID() { + return $this->proxy->getReplyToCommentPHID(); + } + + public function setHasReplies($has_replies) { + $this->proxy->setHasReplies($has_replies); + return $this; + } + + public function getHasReplies() { + return $this->proxy->getHasReplies(); + } + + /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ public function getMarkupFieldKey($field) { // We can't use ID because synthetic comments don't have it. return 'DI:'.PhabricatorHash::digest($this->getContent()); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newDifferentialMarkupEngine(); } public function getMarkupText($field) { return $this->getContent(); } public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { // Only cache submitted comments. return ($this->getID() && !$this->isDraft()); } } diff --git a/src/applications/diffusion/controller/DiffusionInlineCommentController.php b/src/applications/diffusion/controller/DiffusionInlineCommentController.php index c7d45c903d..62e70dde6c 100644 --- a/src/applications/diffusion/controller/DiffusionInlineCommentController.php +++ b/src/applications/diffusion/controller/DiffusionInlineCommentController.php @@ -1,87 +1,91 @@ commitPHID = $data['phid']; } protected function createComment() { // Verify commit and path correspond to actual objects. $commit_phid = $this->commitPHID; $path_id = $this->getChangesetID(); $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( 'phid = %s', $commit_phid); if (!$commit) { throw new Exception('Invalid commit ID!'); } // TODO: Write a real PathQuery object? $path = queryfx_one( id(new PhabricatorRepository())->establishConnection('r'), 'SELECT path FROM %T WHERE id = %d', PhabricatorRepository::TABLE_PATH, $path_id); if (!$path) { throw new Exception('Invalid path ID!'); } return id(new PhabricatorAuditInlineComment()) ->setCommitPHID($commit_phid) ->setPathID($path_id); } protected function loadComment($id) { return PhabricatorAuditInlineComment::loadID($id); } + protected function loadCommentByPHID($phid) { + return PhabricatorAuditInlineComment::loadPHID($phid); + } + protected function loadCommentForEdit($id) { $request = $this->getRequest(); $user = $request->getUser(); $inline = $this->loadComment($id); if (!$this->canEditInlineComment($user, $inline)) { throw new Exception('That comment is not editable!'); } return $inline; } private function canEditInlineComment( PhabricatorUser $user, PhabricatorAuditInlineComment $inline) { // Only the author may edit a comment. if ($inline->getAuthorPHID() != $user->getPHID()) { return false; } // Saved comments may not be edited. if ($inline->getAuditCommentID()) { return false; } // Inline must be attached to the active revision. if ($inline->getCommitPHID() != $this->commitPHID) { return false; } return true; } protected function deleteComment(PhabricatorInlineCommentInterface $inline) { return $inline->delete(); } protected function saveComment(PhabricatorInlineCommentInterface $inline) { return $inline->save(); } } diff --git a/src/infrastructure/diff/PhabricatorInlineCommentController.php b/src/infrastructure/diff/PhabricatorInlineCommentController.php index f11fb4a572..4aa0a7d6ee 100644 --- a/src/infrastructure/diff/PhabricatorInlineCommentController.php +++ b/src/infrastructure/diff/PhabricatorInlineCommentController.php @@ -1,269 +1,303 @@ commentID; } public function getOperation() { return $this->operation; } public function getCommentText() { return $this->commentText; } public function getLineLength() { return $this->lineLength; } public function getLineNumber() { return $this->lineNumber; } public function getIsOnRight() { return $this->isOnRight; } public function getChangesetID() { return $this->changesetID; } public function getIsNewFile() { return $this->isNewFile; } public function setRenderer($renderer) { $this->renderer = $renderer; return $this; } public function getRenderer() { return $this->renderer; } + public function setReplyToCommentPHID($phid) { + $this->replyToCommentPHID = $phid; + return $this; + } + + public function getReplyToCommentPHID() { + return $this->replyToCommentPHID; + } + public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $this->readRequestParameters(); switch ($this->getOperation()) { case 'delete': $inline = $this->loadCommentForEdit($this->getCommentID()); if ($request->isFormPost()) { $this->deleteComment($inline); return $this->buildEmptyResponse(); } $dialog = new AphrontDialogView(); $dialog->setUser($user); $dialog->setSubmitURI($request->getRequestURI()); $dialog->setTitle(pht('Really delete this comment?')); $dialog->addHiddenInput('id', $this->getCommentID()); $dialog->addHiddenInput('op', 'delete'); $dialog->appendChild( phutil_tag('p', array(), pht('Delete this inline comment?'))); $dialog->addCancelButton('#'); $dialog->addSubmitButton(pht('Delete')); return id(new AphrontDialogResponse())->setDialog($dialog); case 'edit': $inline = $this->loadCommentForEdit($this->getCommentID()); $text = $this->getCommentText(); if ($request->isFormPost()) { if (strlen($text)) { $inline->setContent($text); $this->saveComment($inline); return $this->buildRenderedCommentResponse( $inline, $this->getIsOnRight()); } else { $this->deleteComment($inline); return $this->buildEmptyResponse(); } } $edit_dialog = $this->buildEditDialog(); $edit_dialog->setTitle(pht('Edit Inline Comment')); $edit_dialog->addHiddenInput('id', $this->getCommentID()); $edit_dialog->addHiddenInput('op', 'edit'); - $edit_dialog->addHiddenInput('renderer', $this->getRenderer()); $edit_dialog->appendChild( $this->renderTextArea( nonempty($text, $inline->getContent()))); return id(new AphrontAjaxResponse()) ->setContent($edit_dialog->render()); case 'create': $text = $this->getCommentText(); if (!$request->isFormPost() || !strlen($text)) { return $this->buildEmptyResponse(); } $inline = $this->createComment() ->setChangesetID($this->getChangesetID()) ->setAuthorPHID($user->getPHID()) ->setLineNumber($this->getLineNumber()) ->setLineLength($this->getLineLength()) ->setIsNewFile($this->getIsNewFile()) ->setContent($text); + + if ($this->getReplyToCommentPHID()) { + $inline->setReplyToCommentPHID($this->getReplyToCommentPHID()); + } + $this->saveComment($inline); return $this->buildRenderedCommentResponse( $inline, $this->getIsOnRight()); case 'reply': default: $edit_dialog = $this->buildEditDialog(); if ($this->getOperation() == 'reply') { $inline = $this->loadComment($this->getCommentID()); $edit_dialog->setTitle(pht('Reply to Inline Comment')); $changeset = $inline->getChangesetID(); $is_new = $inline->getIsNewFile(); $number = $inline->getLineNumber(); $length = $inline->getLineLength(); } else { $edit_dialog->setTitle(pht('New Inline Comment')); $changeset = $this->getChangesetID(); $is_new = $this->getIsNewFile(); $number = $this->getLineNumber(); $length = $this->getLineLength(); } $edit_dialog->addHiddenInput('op', 'create'); - $edit_dialog->addHiddenInput('changeset', $changeset); $edit_dialog->addHiddenInput('is_new', $is_new); $edit_dialog->addHiddenInput('number', $number); $edit_dialog->addHiddenInput('length', $length); - $edit_dialog->addHiddenInput('renderer', $this->getRenderer()); $text_area = $this->renderTextArea($this->getCommentText()); $edit_dialog->appendChild($text_area); return id(new AphrontAjaxResponse()) ->setContent($edit_dialog->render()); } } private function readRequestParameters() { $request = $this->getRequest(); // NOTE: This isn't necessarily a DifferentialChangeset ID, just an // application identifier for the changeset. In Diffusion, it's a Path ID. - $this->changesetID = $request->getInt('changeset'); + $this->changesetID = $request->getInt('changesetID'); $this->isNewFile = (int)$request->getBool('is_new'); $this->isOnRight = $request->getBool('on_right'); $this->lineNumber = $request->getInt('number'); $this->lineLength = $request->getInt('length'); $this->commentText = $request->getStr('text'); $this->commentID = $request->getInt('id'); $this->operation = $request->getStr('op'); $this->renderer = $request->getStr('renderer'); + $this->replyToCommentPHID = $request->getStr('replyToCommentPHID'); + + if ($this->getReplyToCommentPHID()) { + $reply_phid = $this->getReplyToCommentPHID(); + $reply_comment = $this->loadCommentByPHID($reply_phid); + if (!$reply_comment) { + throw new Exception( + pht('Failed to load comment "%s".', $reply_phid)); + } + + if ($reply_comment->getChangesetID() != $this->getChangesetID()) { + throw new Exception( + pht( + 'Comment "%s" belongs to wrong changeset (%s vs %s).', + $reply_phid, + $reply_comment->getChangesetID(), + $this->getChangesetID())); + } + } } private function buildEditDialog() { $request = $this->getRequest(); $user = $request->getUser(); $edit_dialog = id(new PHUIDiffInlineCommentEditView()) ->setUser($user) ->setSubmitURI($request->getRequestURI()) ->setOnRight($this->getIsOnRight()) ->setIsNewFile($this->getIsNewFile()) ->setNumber($this->getLineNumber()) ->setLength($this->getLineLength()) - ->setRenderer($this->getRenderer()); + ->setRenderer($this->getRenderer()) + ->setReplyToCommentPHID($this->getReplyToCommentPHID()) + ->setChangesetID($this->getChangesetID()); return $edit_dialog; } private function buildEmptyResponse() { return id(new AphrontAjaxResponse()) ->setContent( array( 'markup' => '', )); } private function buildRenderedCommentResponse( PhabricatorInlineCommentInterface $inline, $on_right) { $request = $this->getRequest(); $user = $request->getUser(); $engine = new PhabricatorMarkupEngine(); $engine->setViewer($user); $engine->addObject( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); $engine->process(); $phids = array($user->getPHID()); $handles = $this->loadViewerHandles($phids); $view = id(new PHUIDiffInlineCommentDetailView()) ->setInlineComment($inline) ->setOnRight($on_right) ->setMarkupEngine($engine) ->setHandles($handles) ->setEditable(true); $renderer = DifferentialChangesetHTMLRenderer::getHTMLRendererByKey( $this->getRenderer()); $view = $renderer->getRowScaffoldForInline($view); $view = id(new PHUIDiffInlineCommentTableScaffold()) ->addRowScaffold($view); return id(new AphrontAjaxResponse()) ->setContent( array( 'inlineCommentID' => $inline->getID(), 'markup' => $view->render(), )); } private function renderTextArea($text) { return id(new PhabricatorRemarkupControl()) ->setUser($this->getRequest()->getUser()) ->setSigil('differential-inline-comment-edit-textarea') ->setName('text') ->setValue($text) ->setDisableFullScreen(true); } } diff --git a/src/infrastructure/diff/interface/PhabricatorInlineCommentInterface.php b/src/infrastructure/diff/interface/PhabricatorInlineCommentInterface.php index 55295fd2e3..01914e03db 100644 --- a/src/infrastructure/diff/interface/PhabricatorInlineCommentInterface.php +++ b/src/infrastructure/diff/interface/PhabricatorInlineCommentInterface.php @@ -1,40 +1,46 @@ onRight; } public function setInlineComment(PhabricatorInlineCommentInterface $comment) { $this->inlineComment = $comment; return $this; } public function setOnRight($on_right) { $this->onRight = $on_right; return $this; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function setMarkupEngine(PhabricatorMarkupEngine $engine) { $this->markupEngine = $engine; return $this; } public function setEditable($editable) { $this->editable = $editable; return $this; } public function setPreview($preview) { $this->preview = $preview; return $this; } public function setAllowReply($allow_reply) { $this->allowReply = $allow_reply; return $this; } public function setRenderer($renderer) { $this->renderer = $renderer; return $this; } public function getRenderer() { return $this->renderer; } public function render() { $inline = $this->inlineComment; $start = $inline->getLineNumber(); $length = $inline->getLineLength(); if ($length) { $end = $start + $length; $line = 'Lines '.number_format($start).'-'.number_format($end); } else { $line = 'Line '.number_format($start); } $metadata = array( 'id' => $inline->getID(), + 'phid' => $inline->getPHID(), + 'changesetID' => $inline->getChangesetID(), 'number' => $inline->getLineNumber(), 'length' => $inline->getLineLength(), 'isNewFile' => (bool)$inline->getIsNewFile(), 'on_right' => $this->onRight, 'original' => $inline->getContent(), + 'replyToCommentPHID' => $inline->getReplyToCommentPHID(), ); $sigil = 'differential-inline-comment'; if ($this->preview) { $sigil = $sigil.' differential-inline-comment-preview'; } $content = $inline->getContent(); $handles = $this->handles; $links = array(); $is_synthetic = false; if ($inline->getSyntheticAuthor()) { $is_synthetic = true; } $is_draft = false; if ($inline->isDraft() && !$is_synthetic) { $links[] = pht('Not Submitted Yet'); $is_draft = true; } + + // TODO: This stuff is nonfinal, just making it do something. + if ($inline->getHasReplies()) { + $links[] = pht('Has Reply'); + } + if ($inline->getReplyToCommentPHID()) { + $links[] = pht('Is Reply'); + } + if (!$this->preview) { $links[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'differential-inline-prev', ), pht('Previous')); $links[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'differential-inline-next', ), pht('Next')); if ($this->allowReply) { if (!$is_synthetic) { // NOTE: No product reason why you can't reply to these, but the reply // mechanism currently sends the inline comment ID to the server, not // file/line information, and synthetic comments don't have an inline // comment ID. $links[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'differential-inline-reply', ), pht('Reply')); } } } $anchor_name = 'inline-'.$inline->getID(); if ($this->editable && !$this->preview) { $links[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'differential-inline-edit', ), pht('Edit')); $links[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'differential-inline-delete', ), pht('Delete')); } else if ($this->preview) { $links[] = javelin_tag( 'a', array( 'meta' => array( 'anchor' => $anchor_name, ), 'sigil' => 'differential-inline-preview-jump', ), pht('Not Visible')); $links[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'differential-inline-delete', ), pht('Delete')); } if ($links) { $links = phutil_tag( 'span', array('class' => 'differential-inline-comment-links'), phutil_implode_html(" \xC2\xB7 ", $links)); } else { $links = null; } $content = $this->markupEngine->getOutput( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); if ($this->preview) { $anchor = null; } else { $anchor = phutil_tag( 'a', array( 'name' => $anchor_name, 'id' => $anchor_name, 'class' => 'differential-inline-comment-anchor', ), ''); } $classes = array( 'differential-inline-comment', ); if ($is_draft) { $classes[] = 'differential-inline-comment-unsaved-draft'; } if ($is_synthetic) { $classes[] = 'differential-inline-comment-synthetic'; } $classes = implode(' ', $classes); if ($is_synthetic) { $author = $inline->getSyntheticAuthor(); } else { $author = $handles[$inline->getAuthorPHID()]->getName(); } $line = phutil_tag( 'span', array('class' => 'differential-inline-comment-line'), $line); $markup = javelin_tag( 'div', array( 'class' => $classes, 'sigil' => $sigil, 'meta' => $metadata, ), array( phutil_tag_div('differential-inline-comment-head', array( $anchor, $links, ' ', $line, ' ', $author, )), phutil_tag_div( 'differential-inline-comment-content', phutil_tag_div('phabricator-remarkup', $content)), )); return $markup; } } diff --git a/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php b/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php index 47569dab3a..47710a85e0 100644 --- a/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php +++ b/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php @@ -1,186 +1,214 @@ isNewFile = $is_new_file; return $this; } public function getIsNewFile() { return $this->isNewFile; } public function getIsOnRight() { return $this->onRight; } public function setRenderer($renderer) { $this->renderer = $renderer; return $this; } public function getRenderer() { return $this->renderer; } public function addHiddenInput($key, $value) { $this->inputs[] = array($key, $value); return $this; } public function setSubmitURI($uri) { $this->uri = $uri; return $this; } public function setTitle($title) { $this->title = $title; return $this; } + public function setReplyToCommentPHID($reply_to_phid) { + $this->replyToCommentPHID = $reply_to_phid; + return $this; + } + + public function getReplyToCommentPHID() { + return $this->replyToCommentPHID; + } + + public function setChangesetID($changeset_id) { + $this->changesetID = $changeset_id; + return $this; + } + + public function getChangesetID() { + return $this->changesetID; + } + public function setOnRight($on_right) { $this->onRight = $on_right; - $this->addHiddenInput('on_right', $on_right); return $this; } public function setNumber($number) { $this->number = $number; return $this; } public function setLength($length) { $this->length = $length; return $this; } public function render() { if (!$this->uri) { throw new Exception('Call setSubmitURI() before render()!'); } if (!$this->user) { throw new Exception('Call setUser() before render()!'); } $content = phabricator_form( $this->user, array( 'action' => $this->uri, 'method' => 'POST', 'sigil' => 'inline-edit-form', ), array( $this->renderInputs(), $this->renderBody(), )); if ($this->renderer == '1up') { $cells = array( phutil_tag('th', array()), phutil_tag('th', array()), phutil_tag( 'td', array('colspan' => 3, 'class' => 'right3'), $content), ); } else { $cells = array( phutil_tag('th', array()), phutil_tag( 'td', array('class' => 'left'), $this->onRight ? null : $content), phutil_tag('th', array()), phutil_tag( 'td', array('colspan' => 3, 'class' => 'right3'), $this->onRight ? $content : null), ); } $row = phutil_tag('tr', array('class' => 'inline-comment-splint'), $cells); return phutil_tag('table', array(), $row); } private function renderInputs() { + $inputs = $this->inputs; $out = array(); - foreach ($this->inputs as $input) { + + $inputs[] = array('on_right', (bool)$this->getIsOnRight()); + $inputs[] = array('replyToCommentPHID', $this->getReplyToCommentPHID()); + $inputs[] = array('renderer', $this->getRenderer()); + $inputs[] = array('changesetID', $this->getChangesetID()); + + foreach ($inputs as $input) { list($name, $value) = $input; $out[] = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => $name, 'value' => $value, )); } return $out; } private function renderBody() { $buttons = array(); $buttons[] = phutil_tag('button', array(), pht('Ready')); $buttons[] = javelin_tag( 'button', array( 'sigil' => 'inline-edit-cancel', 'class' => 'grey', ), pht('Cancel')); $title = phutil_tag( 'div', array( 'class' => 'differential-inline-comment-edit-title', ), $this->title); $body = phutil_tag( 'div', array( 'class' => 'differential-inline-comment-edit-body', ), $this->renderChildren()); $edit = phutil_tag( 'div', array( 'class' => 'differential-inline-comment-edit-buttons', ), array( $buttons, phutil_tag('div', array('style' => 'clear: both'), ''), )); return javelin_tag( 'div', array( 'class' => 'differential-inline-comment-edit', 'sigil' => 'differential-inline-comment', 'meta' => array( + 'changesetID' => $this->getChangesetID(), 'on_right' => $this->getIsOnRight(), 'isNewFile' => (bool)$this->getIsNewFile(), 'number' => $this->number, 'length' => $this->length, + 'replyToCommentPHID' => $this->getReplyToCommentPHID(), ), ), array( $title, $body, $edit, )); } } diff --git a/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js b/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js index c3e0dd9bd4..98b2745633 100644 --- a/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js +++ b/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js @@ -1,296 +1,298 @@ /** * @provides differential-inline-comment-editor * @requires javelin-dom * javelin-util * javelin-stratcom * javelin-install * javelin-request * javelin-workflow */ JX.install('DifferentialInlineCommentEditor', { construct : function(uri) { this._uri = uri; }, events : ['done'], members : { _uri : null, _undoText : null, _skipOverInlineCommentRows : function(node) { // TODO: Move this semantic information out of class names. while (node && node.className.indexOf('inline') !== -1) { node = node.nextSibling; } return node; }, _buildRequestData : function() { return { op : this.getOperation(), on_right : this.getOnRight(), id : this.getID(), number : this.getLineNumber(), is_new : this.getIsNew(), length : this.getLength(), - changeset : this.getChangeset(), + changesetID : this.getChangesetID(), text : this.getText() || '', - renderer: this.getRenderer() + renderer: this.getRenderer(), + replyToCommentPHID: this.getReplyToCommentPHID() || '', }; }, _draw : function(content, exact_row) { var row = this.getRow(); var table = this.getTable(); var target = exact_row ? row : this._skipOverInlineCommentRows(row); function copyRows(dst, src, before) { var rows = JX.DOM.scry(src, 'tr'); for (var ii = 0; ii < rows.length; ii++) { // Find the table this