diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php index 0dd5a95ed..b67c833b4 100644 --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php @@ -1,82 +1,100 @@ phid = $data['phid']; - $this->anchor = idx($data, 'anchor'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $xactions = id(new PhabricatorObjectHandleData(array($this->phid))) ->setViewer($user) ->loadObjects(); $xaction = idx($xactions, $this->phid); if (!$xaction) { // TODO: This may also mean you don't have permission to edit the object, // but we can't make that distinction via PhabricatorObjectHandleData // at the moment. return new Aphront404Response(); } if (!$xaction->getComment()) { // You can't currently edit a transaction which doesn't have a comment. // Some day you may be able to edit the visibility. return new Aphront404Response(); } $obj_phid = $xaction->getObjectPHID(); $obj_handle = PhabricatorObjectHandleData::loadOneHandle($obj_phid, $user); if (!$obj_handle) { // Require the corresponding object exist and be visible to the user. return new Aphront404Response(); } if ($request->isDialogFormPost()) { $text = $request->getStr('text'); $comment = $xaction->getApplicationTransactionCommentObject(); $comment->setContent($text); if (!strlen($text)) { $comment->setIsDeleted(true); } $editor = id(new PhabricatorApplicationTransactionCommentEditor()) ->setActor($user) ->setContentSource( $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_WEB, array( 'ip' => $request->getRemoteAddr(), ))) ->applyEdit($xaction, $comment); - return id(new AphrontReloadResponse())->setURI($obj_handle->getURI()); + if ($request->isAjax()) { + $view = id(new PhabricatorApplicationTransactionView()) + ->setViewer($user) + ->setTransactions(array($xaction)); + + $anchor = $request->getStr('anchor'); + if ($anchor) { + $view->setAnchorOffset($anchor); + } + + return id(new AphrontAjaxResponse())->setContent( + array( + 'xactions' => mpull( + $view->buildEvents(), + 'render', + 'getTransactionPHID'), + )); + } else { + return id(new AphrontReloadResponse())->setURI($obj_handle->getURI()); + } } $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('Edit Comment')); $dialog + ->addHiddenInput('anchor', $request->getStr('anchor')) ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('text') ->setValue($xaction->getComment()->getContent())); $dialog ->addSubmitButton(pht('Edit Comment')) ->addCancelButton($obj_handle->getURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php index 8ffe23868..b58aad2b6 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php @@ -1,117 +1,149 @@ showEditActions = $show_edit_actions; return $this; } public function getShowEditActions() { return $this->showEditActions; } public function setAnchorOffset($anchor_offset) { $this->anchorOffset = $anchor_offset; return $this; } public function setMarkupEngine(PhabricatorMarkupEngine $engine) { $this->engine = $engine; return $this; } public function setTransactions(array $transactions) { assert_instances_of($transactions, 'PhabricatorApplicationTransaction'); $this->transactions = $transactions; return $this; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } - public function render() { + public function buildEvents() { $field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT; + $engine = $this->getOrBuildEngine(); - if (!$this->engine) { - $engine = id(new PhabricatorMarkupEngine()) - ->setViewer($this->viewer); - foreach ($this->transactions as $xaction) { - if (!$xaction->hasComment()) { - continue; - } - $engine->addObject($xaction->getComment(), $field); - } - $engine->process(); - - $this->engine = $engine; - } - - $view = new PhabricatorTimelineView(); $viewer = $this->viewer; $anchor = $this->anchorOffset; + $events = array(); foreach ($this->transactions as $xaction) { if ($xaction->shouldHide()) { continue; } - $anchor++; $event = id(new PhabricatorTimelineEventView()) ->setViewer($viewer) ->setTransactionPHID($xaction->getPHID()) ->setUserHandle($xaction->getHandle($xaction->getAuthorPHID())) ->setIcon($xaction->getIcon()) ->setColor($xaction->getColor()) ->setTitle($xaction->getTitle()) ->setDateCreated($xaction->getDateCreated()) ->setContentSource($xaction->getContentSource()) ->setAnchor($anchor); + $anchor++; + + $has_deleted_comment = $xaction->getComment() && $xaction->getComment()->getIsDeleted(); if ($this->getShowEditActions()) { if ($xaction->getCommentVersion() > 1) { $event->setIsEdited(true); } $can_edit = PhabricatorPolicyCapability::CAN_EDIT; if ($xaction->hasComment() || $has_deleted_comment) { $has_edit_capability = PhabricatorPolicyFilter::hasCapability( $viewer, $xaction, $can_edit); if ($has_edit_capability) { $event->setIsEditable(true); } } } if ($xaction->hasComment()) { $event->appendChild( - $this->engine->getOutput($xaction->getComment(), $field)); + $engine->getOutput($xaction->getComment(), $field)); } else if ($has_deleted_comment) { $event->appendChild( ''.pht('This comment has been deleted.').''); } + $events[] = $event; + } + + return $events; + } + + public function render() { + $view = new PhabricatorTimelineView(); + foreach ($this->buildEvents() as $event) { $view->addEvent($event); } + if ($this->getShowEditActions()) { + $list_id = celerity_generate_unique_node_id(); + + $view->setID($list_id); + + Javelin::initBehavior( + 'phabricator-transaction-list', + array( + 'listID' => $list_id, + )); + } + return $view->render(); } + + + private function getOrBuildEngine() { + if ($this->engine) { + return $this->engine; + } + + $field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT; + + $engine = id(new PhabricatorMarkupEngine()) + ->setViewer($this->viewer); + foreach ($this->transactions as $xaction) { + if (!$xaction->hasComment()) { + continue; + } + $engine->addObject($xaction->getComment(), $field); + } + $engine->process(); + + return $engine; + } + } diff --git a/src/view/layout/PhabricatorTimelineEventView.php b/src/view/layout/PhabricatorTimelineEventView.php index 6a5db0ff7..cfd4b4af9 100644 --- a/src/view/layout/PhabricatorTimelineEventView.php +++ b/src/view/layout/PhabricatorTimelineEventView.php @@ -1,275 +1,287 @@ transactionPHID = $transaction_phid; return $this; } public function getTransactionPHID() { return $this->transactionPHID; } public function setIsEdited($is_edited) { $this->isEdited = $is_edited; return $this; } public function getIsEdited() { return $this->isEdited; } public function setIsEditable($is_editable) { $this->isEditable = $is_editable; return $this; } public function getIsEditable() { return $this->isEditable; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setDateCreated($date_created) { $this->dateCreated = $date_created; return $this; } public function getDateCreated() { return $this->dateCreated; } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } public function getContentSource() { return $this->contentSource; } public function setUserHandle(PhabricatorObjectHandle $handle) { $this->userHandle = $handle; return $this; } public function setAnchor($anchor) { $this->anchor = $anchor; return $this; } public function setTitle($title) { $this->title = $title; return $this; } public function addClass($class) { $this->classes[] = $class; return $this; } public function setIcon($icon) { $this->icon = $icon; return $this; } public function setColor($color) { $this->color = $color; return $this; } public function render() { $content = $this->renderChildren(); $title = $this->title; if (($title === null) && !strlen($content)) { $title = ''; } $extra = array(); $xaction_phid = $this->getTransactionPHID(); if ($this->getIsEdited()) { $extra[] = javelin_render_tag( 'a', array( 'href' => '/transactions/history/'.$xaction_phid.'/', 'sigil' => 'workflow', ), pht('Edited')); } if ($this->getIsEditable()) { $extra[] = javelin_render_tag( 'a', array( 'href' => '/transactions/edit/'.$xaction_phid.'/', - 'sigil' => 'workflow', + 'sigil' => 'workflow transaction-edit', ), pht('Edit')); } $source = $this->getContentSource(); if ($source) { $extra[] = id(new PhabricatorContentSourceView()) ->setContentSource($source) ->setUser($this->getViewer()) ->render(); } if ($this->getDateCreated()) { $date = phabricator_datetime( $this->getDateCreated(), $this->getViewer()); if ($this->anchor) { Javelin::initBehavior('phabricator-watch-anchor'); $anchor = id(new PhabricatorAnchorView()) ->setAnchorName($this->anchor) ->render(); $date = $anchor.phutil_render_tag( 'a', array( 'href' => '#'.$this->anchor, ), $date); } $extra[] = $date; } $extra = implode(' · ', $extra); if ($extra) { $extra = phutil_render_tag( 'span', array( 'class' => 'phabricator-timeline-extra', ), $extra); } if ($title !== null || $extra !== null) { $title_classes = array(); $title_classes[] = 'phabricator-timeline-title'; $icon = null; if ($this->icon) { $title_classes[] = 'phabricator-timeline-title-with-icon'; $icon = phutil_render_tag( 'span', array( 'class' => 'phabricator-timeline-icon-fill', ), phutil_render_tag( 'span', array( 'class' => 'phabricator-timeline-icon sprite-icon '. 'action-'.$this->icon.'-white', ), '')); } $title = phutil_render_tag( 'div', array( 'class' => implode(' ', $title_classes), ), $icon.$title.$extra); } $wedge = phutil_render_tag( 'div', array( 'class' => 'phabricator-timeline-wedge phabricator-timeline-border', ), ''); $image_uri = $this->userHandle->getImageURI(); $image = phutil_render_tag( 'div', array( 'style' => 'background-image: url('.$image_uri.')', 'class' => 'phabricator-timeline-image', ), ''); $content_classes = array(); $content_classes[] = 'phabricator-timeline-content'; $classes = array(); $classes[] = 'phabricator-timeline-event-view'; $classes[] = 'phabricator-timeline-border'; if ($content) { $classes[] = 'phabricator-timeline-major-event'; $content = phutil_render_tag( 'div', array( 'class' => implode(' ', $content_classes), ), phutil_render_tag( 'div', array( 'class' => 'phabricator-timeline-inner-content', ), $title. phutil_render_tag( 'div', array( 'class' => 'phabricator-timeline-core-content', ), $content))); $content = $image.$wedge.$content; } else { $classes[] = 'phabricator-timeline-minor-event'; $content = phutil_render_tag( 'div', array( 'class' => implode(' ', $content_classes), ), $image.$wedge.$title); } $outer_classes = $this->classes; $outer_classes[] = 'phabricator-timeline-shell'; if ($this->color) { $outer_classes[] = 'phabricator-timeline-'.$this->color; } - return phutil_render_tag( + $sigil = null; + $meta = null; + if ($this->getTransactionPHID()) { + $sigil = 'transaction'; + $meta = array( + 'phid' => $this->getTransactionPHID(), + 'anchor' => $this->anchor, + ); + } + + return javelin_render_tag( 'div', array( 'class' => implode(' ', $outer_classes), 'id' => $this->anchor ? 'anchor-'.$this->anchor : null, + 'sigil' => $sigil, + 'meta' => $meta, ), phutil_render_tag( 'div', array( 'class' => implode(' ', $classes), ), $content)); } } diff --git a/src/view/layout/PhabricatorTimelineView.php b/src/view/layout/PhabricatorTimelineView.php index f48b7144b..eae7f527d 100644 --- a/src/view/layout/PhabricatorTimelineView.php +++ b/src/view/layout/PhabricatorTimelineView.php @@ -1,38 +1,45 @@ id = $id; + return $this; + } public function addEvent(PhabricatorTimelineEventView $event) { $this->events[] = $event; return $this; } public function render() { require_celerity_resource('phabricator-timeline-view-css'); $spacer = phutil_render_tag( 'div', array( 'class' => 'phabricator-timeline-event-view '. 'phabricator-timeline-spacer', ), ''); $events = array(); foreach ($this->events as $event) { $events[] = $spacer; $events[] = $this->renderSingleView($event); } $events[] = $spacer; return phutil_render_tag( 'div', array( 'class' => 'phabricator-timeline-view', + 'id' => $this->id, ), implode('', $events)); } } diff --git a/webroot/rsrc/js/application/transactions/behavior-transaction-list.js b/webroot/rsrc/js/application/transactions/behavior-transaction-list.js new file mode 100644 index 000000000..b683a5213 --- /dev/null +++ b/webroot/rsrc/js/application/transactions/behavior-transaction-list.js @@ -0,0 +1,51 @@ +/** + * @provides javelin-behavior-phabricator-transaction-list + * @requires javelin-behavior + * javelin-stratcom + * javelin-workflow + * javelin-dom + */ + +JX.behavior('phabricator-transaction-list', function(config) { + + var list = JX.$(config.listID); + var xaction_nodes = null; + + function get_xaction_nodes() { + if (xaction_nodes === null) { + xaction_nodes = {}; + var xactions = JX.DOM.scry(list, 'div', 'transaction'); + for (var ii = 0; ii < xactions.length; ii++) { + xaction_nodes[JX.Stratcom.getData(xactions[ii]).phid] = xactions[ii]; + } + } + return xaction_nodes; + } + + function ontransactions(response) { + var nodes = get_xaction_nodes(); + for (var phid in response.xactions) { + var new_node = JX.$H(response.xactions[phid]).getFragment().firstChild; + if (nodes[phid]) { + JX.DOM.replace(nodes[phid], new_node); + } else { + list.appendChild(new_node); + } + nodes[phid] = new_node; + } + } + + JX.DOM.listen(list, 'click', 'transaction-edit', function(e) { + if (!e.isNormalClick()) { + return; + } + + JX.Workflow.newFromLink(e.getTarget()) + .setData({anchor: e.getNodeData('transaction').anchor}) + .setHandler(ontransactions) + .start(); + + e.kill(); + }); + +});