1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-18 18:51:12 +01:00

Render application transactions via Ajax

Summary:
When possible, render application transactions via Ajax. Instead of reloading the page when the response returns, append new transactions to the transaction view.

Scroll the window to the new transactions, animate them in, and clear the form to make this interaction feel reasonable.

When editing transactions, fade them in but do not scroll to them (i.e., don't disrupt the user's position).

Test Plan: Edited and appended transactions via Ajax. Observed fade in animations and scroll behavior. Clicked anchors to verify proper anchor accounting.

Reviewers: vrana, btrahan, chad

Reviewed By: vrana

CC: aran

Maniphest Tasks: T1960

Differential Revision: https://secure.phabricator.com/D4151
This commit is contained in:
epriestley 2012-12-11 14:02:29 -08:00
parent 025411990b
commit d2a5ee4fa4
11 changed files with 163 additions and 71 deletions

View file

@ -607,6 +607,7 @@ phutil_register_library_map(array(
'PhabricatorApplicationTransactionEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionEditor.php', 'PhabricatorApplicationTransactionEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionEditor.php',
'PhabricatorApplicationTransactionFeedStory' => 'applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php', 'PhabricatorApplicationTransactionFeedStory' => 'applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php',
'PhabricatorApplicationTransactionQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionQuery.php', 'PhabricatorApplicationTransactionQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionQuery.php',
'PhabricatorApplicationTransactionResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionResponse.php',
'PhabricatorApplicationTransactionView' => 'applications/transactions/view/PhabricatorApplicationTransactionView.php', 'PhabricatorApplicationTransactionView' => 'applications/transactions/view/PhabricatorApplicationTransactionView.php',
'PhabricatorApplicationTransactions' => 'applications/transactions/application/PhabricatorApplicationTransactions.php', 'PhabricatorApplicationTransactions' => 'applications/transactions/application/PhabricatorApplicationTransactions.php',
'PhabricatorApplicationUIExamples' => 'applications/uiexample/application/PhabricatorApplicationUIExamples.php', 'PhabricatorApplicationUIExamples' => 'applications/uiexample/application/PhabricatorApplicationUIExamples.php',
@ -1884,6 +1885,7 @@ phutil_register_library_map(array(
'PhabricatorApplicationTransactionEditor' => 'PhabricatorEditor', 'PhabricatorApplicationTransactionEditor' => 'PhabricatorEditor',
'PhabricatorApplicationTransactionFeedStory' => 'PhabricatorFeedStory', 'PhabricatorApplicationTransactionFeedStory' => 'PhabricatorFeedStory',
'PhabricatorApplicationTransactionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorApplicationTransactionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorApplicationTransactionResponse' => 'AphrontProxyResponse',
'PhabricatorApplicationTransactionView' => 'AphrontView', 'PhabricatorApplicationTransactionView' => 'AphrontView',
'PhabricatorApplicationTransactions' => 'PhabricatorApplication', 'PhabricatorApplicationTransactions' => 'PhabricatorApplication',
'PhabricatorApplicationUIExamples' => 'PhabricatorApplication', 'PhabricatorApplicationUIExamples' => 'PhabricatorApplication',

View file

@ -42,8 +42,15 @@ final class PhabricatorMacroCommentController
))) )))
->applyTransactions($macro, $xactions); ->applyTransactions($macro, $xactions);
return id(new AphrontRedirectResponse()) if ($request->isAjax()) {
->setURI($view_uri); return id(new PhabricatorApplicationTransactionResponse())
->setViewer($user)
->setTransactions($xactions)
->setAnchorOffset($request->getStr('anchor'));
} else {
return id(new AphrontRedirectResponse())
->setURI($view_uri);
}
} }
} }

View file

@ -82,6 +82,7 @@ final class PhabricatorMacroViewController
$add_comment_form = id(new AphrontFormView()) $add_comment_form = id(new AphrontFormView())
->setWorkflow(true) ->setWorkflow(true)
->setFlexible(true) ->setFlexible(true)
->addSigil('transaction-append')
->setAction($this->getApplicationURI('/comment/'.$macro->getID().'/')) ->setAction($this->getApplicationURI('/comment/'.$macro->getID().'/'))
->setUser($user) ->setUser($user)
->appendChild( ->appendChild(

View file

@ -43,7 +43,8 @@ final class PholioMockCommentController extends PholioController {
'ip' => $request->getRemoteAddr(), 'ip' => $request->getRemoteAddr(),
)); ));
$xaction = id(new PholioTransaction()) $xactions = array();
$xactions[] = id(new PholioTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment( ->attachComment(
id(new PholioTransactionComment()) id(new PholioTransactionComment())
@ -52,9 +53,16 @@ final class PholioMockCommentController extends PholioController {
id(new PholioMockEditor()) id(new PholioMockEditor())
->setActor($user) ->setActor($user)
->setContentSource($content_source) ->setContentSource($content_source)
->applyTransactions($mock, array($xaction)); ->applyTransactions($mock, $xactions);
return id(new AphrontRedirectResponse())->setURI($mock_uri); if ($request->isAjax()) {
return id(new PhabricatorApplicationTransactionResponse())
->setViewer($user)
->setTransactions($xactions)
->setAnchorOffset($request->getStr('anchor'));
} else {
return id(new AphrontRedirectResponse())->setURI($mock_uri);
}
} }
} }

View file

@ -47,7 +47,7 @@ final class PholioMockViewController extends PholioController {
if ($xaction->getComment()) { if ($xaction->getComment()) {
$engine->addObject( $engine->addObject(
$xaction->getComment(), $xaction->getComment(),
PholioTransaction::MARKUP_FIELD_COMMENT); PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
} }
} }
$engine->process(); $engine->process();
@ -64,7 +64,10 @@ final class PholioMockViewController extends PholioController {
'<h1 style="margin: 2em; padding: 1em; border: 1px dashed grey;">'. '<h1 style="margin: 2em; padding: 1em; border: 1px dashed grey;">'.
'Carousel Goes Here</h1>'; 'Carousel Goes Here</h1>';
$xaction_view = $this->buildTransactionView($xactions, $engine); $xaction_view = id(new PhabricatorApplicationTransactionView())
->setViewer($this->getRequest()->getUser())
->setTransactions($xactions)
->setMarkupEngine($engine);
$add_comment = $this->buildAddCommentView($mock); $add_comment = $this->buildAddCommentView($mock);
@ -171,6 +174,7 @@ final class PholioMockViewController extends PholioController {
$form = id(new AphrontFormView()) $form = id(new AphrontFormView())
->setUser($user) ->setUser($user)
->addSigil('transaction-append')
->setAction($this->getApplicationURI('/comment/'.$mock->getID().'/')) ->setAction($this->getApplicationURI('/comment/'.$mock->getID().'/'))
->setWorkflow(true) ->setWorkflow(true)
->setFlexible(true) ->setFlexible(true)
@ -189,42 +193,4 @@ final class PholioMockViewController extends PholioController {
); );
} }
private function buildTransactionView(
array $xactions,
PhabricatorMarkupEngine $engine) {
assert_instances_of($xactions, 'PholioTransaction');
$view = new PhabricatorTimelineView();
$anchor_name = 0;
foreach ($xactions as $xaction) {
if ($xaction->shouldHide()) {
continue;
}
$anchor_name++;
$event = id(new PhabricatorTimelineEventView())
->setViewer($this->getRequest()->getUser())
->setUserHandle($xaction->getHandle($xaction->getAuthorPHID()))
->setIcon($xaction->getIcon())
->setColor($xaction->getColor())
->setTitle($xaction->getTitle())
->setDateCreated($xaction->getDateCreated())
->setContentSource($xaction->getContentSource())
->setAnchor($anchor_name);
if ($xaction->getComment()) {
$event->appendChild(
$engine->getOutput(
$xaction->getComment(),
PholioTransaction::MARKUP_FIELD_COMMENT));
}
$view->addEvent($event);
}
return $view;
}
} }

View file

@ -58,22 +58,10 @@ final class PhabricatorApplicationTransactionCommentEditController
->applyEdit($xaction, $comment); ->applyEdit($xaction, $comment);
if ($request->isAjax()) { if ($request->isAjax()) {
$view = id(new PhabricatorApplicationTransactionView()) return id(new PhabricatorApplicationTransactionResponse())
->setViewer($user) ->setViewer($user)
->setTransactions(array($xaction)); ->setTransactions(array($xaction))
->setAnchorOffset($request->getStr('anchor'));
$anchor = $request->getStr('anchor');
if ($anchor) {
$view->setAnchorOffset($anchor);
}
return id(new AphrontAjaxResponse())->setContent(
array(
'xactions' => mpull(
$view->buildEvents(),
'render',
'getTransactionPHID'),
));
} else { } else {
return id(new AphrontReloadResponse())->setURI($obj_handle->getURI()); return id(new AphrontReloadResponse())->setURI($obj_handle->getURI());
} }

View file

@ -0,0 +1,65 @@
<?php
final class PhabricatorApplicationTransactionResponse
extends AphrontProxyResponse {
private $viewer;
private $transactions;
private $anchorOffset;
protected function buildProxy() {
return new AphrontAjaxResponse();
}
public function setAnchorOffset($anchor_offset) {
$this->anchorOffset = $anchor_offset;
return $this;
}
public function getAnchorOffset() {
return $this->anchorOffset;
}
public function setTransactions($transactions) {
assert_instances_of($transactions, 'PhabricatorApplicationTransaction');
$this->transactions = $transactions;
return $this;
}
public function getTransactions() {
return $this->transactions;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function buildResponseString() {
$view = id(new PhabricatorApplicationTransactionView())
->setViewer($this->getViewer())
->setTransactions($this->getTransactions());
if ($this->getAnchorOffset()) {
$view->setAnchorOffset($this->getAnchorOffset());
}
$xactions = mpull($view->buildEvents(), 'render', 'getTransactionPHID');
$content = array(
'xactions' => $xactions,
'spacer' => PhabricatorTimelineView::renderSpacer(),
);
return $this
->getProxy()
->setContent($content)
->buildResponseString();
}
}

View file

@ -67,7 +67,6 @@ class PhabricatorApplicationTransactionView extends AphrontView {
$anchor++; $anchor++;
$has_deleted_comment = $xaction->getComment() && $has_deleted_comment = $xaction->getComment() &&
$xaction->getComment()->getIsDeleted(); $xaction->getComment()->getIsDeleted();
@ -105,7 +104,8 @@ class PhabricatorApplicationTransactionView extends AphrontView {
public function render() { public function render() {
$view = new PhabricatorTimelineView(); $view = new PhabricatorTimelineView();
foreach ($this->buildEvents() as $event) { $events = $this->buildEvents();
foreach ($events as $event) {
$view->addEvent($event); $view->addEvent($event);
} }
@ -117,7 +117,8 @@ class PhabricatorApplicationTransactionView extends AphrontView {
Javelin::initBehavior( Javelin::initBehavior(
'phabricator-transaction-list', 'phabricator-transaction-list',
array( array(
'listID' => $list_id, 'listID' => $list_id,
'nextAnchor' => $this->anchorOffset + count($events),
)); ));
} }

View file

@ -11,6 +11,7 @@ final class AphrontFormView extends AphrontView {
private $workflow; private $workflow;
private $id; private $id;
private $flexible; private $flexible;
private $sigils = array();
public function setFlexible($flexible) { public function setFlexible($flexible) {
$this->flexible = $flexible; $this->flexible = $flexible;
@ -52,6 +53,11 @@ final class AphrontFormView extends AphrontView {
return $this; return $this;
} }
public function addSigil($sigil) {
$this->sigils[] = $sigil;
return $this;
}
public function render() { public function render() {
if ($this->flexible) { if ($this->flexible) {
require_celerity_resource('phabricator-form-view-css'); require_celerity_resource('phabricator-form-view-css');
@ -76,6 +82,11 @@ final class AphrontFormView extends AphrontView {
throw new Exception('You must pass the user to AphrontFormView.'); throw new Exception('You must pass the user to AphrontFormView.');
} }
$sigils = $this->sigils;
if ($this->workflow) {
$sigils[] = 'workflow';
}
return phabricator_render_form( return phabricator_render_form(
$this->user, $this->user,
array( array(
@ -83,7 +94,7 @@ final class AphrontFormView extends AphrontView {
'action' => $this->action, 'action' => $this->action,
'method' => $this->method, 'method' => $this->method,
'enctype' => $this->encType, 'enctype' => $this->encType,
'sigil' => $this->workflow ? 'workflow' : null, 'sigil' => $sigils ? implode(' ', $sigils) : null,
'id' => $this->id, 'id' => $this->id,
), ),
$layout->render()); $layout->render());

View file

@ -18,13 +18,7 @@ final class PhabricatorTimelineView extends AphrontView {
public function render() { public function render() {
require_celerity_resource('phabricator-timeline-view-css'); require_celerity_resource('phabricator-timeline-view-css');
$spacer = phutil_render_tag( $spacer = self::renderSpacer();
'div',
array(
'class' => 'phabricator-timeline-event-view '.
'phabricator-timeline-spacer',
),
'');
$events = array(); $events = array();
foreach ($this->events as $event) { foreach ($this->events as $event) {
@ -42,4 +36,13 @@ final class PhabricatorTimelineView extends AphrontView {
implode('', $events)); implode('', $events));
} }
public static function renderSpacer() {
return phutil_render_tag(
'div',
array(
'class' => 'phabricator-timeline-event-view '.
'phabricator-timeline-spacer',
),
'');
}
} }

View file

@ -4,12 +4,14 @@
* javelin-stratcom * javelin-stratcom
* javelin-workflow * javelin-workflow
* javelin-dom * javelin-dom
* javelin-fx
*/ */
JX.behavior('phabricator-transaction-list', function(config) { JX.behavior('phabricator-transaction-list', function(config) {
var list = JX.$(config.listID); var list = JX.$(config.listID);
var xaction_nodes = null; var xaction_nodes = null;
var next_anchor = config.nextAnchor;
function get_xaction_nodes() { function get_xaction_nodes() {
if (xaction_nodes === null) { if (xaction_nodes === null) {
@ -23,16 +25,41 @@ JX.behavior('phabricator-transaction-list', function(config) {
} }
function ontransactions(response) { function ontransactions(response) {
var fade_in = [];
var first_new = null;
var nodes = get_xaction_nodes(); var nodes = get_xaction_nodes();
for (var phid in response.xactions) { for (var phid in response.xactions) {
var new_node = JX.$H(response.xactions[phid]).getFragment().firstChild; var new_node = JX.$H(response.xactions[phid]).getFragment().firstChild;
fade_in.push(new_node);
if (nodes[phid]) { if (nodes[phid]) {
JX.DOM.replace(nodes[phid], new_node); JX.DOM.replace(nodes[phid], new_node);
} else { } else {
if (first_new === null) {
first_new = new_node;
}
list.appendChild(new_node); list.appendChild(new_node);
// Add a spacer after new transactions.
var spacer = JX.$H(response.spacer).getFragment().firstChild;
list.appendChild(spacer);
fade_in.push(spacer);
next_anchor++;
} }
nodes[phid] = new_node; nodes[phid] = new_node;
} }
// Scroll to the first new transaction, if transactions were added.
if (first_new) {
JX.DOM.scrollTo(first_new);
}
// Make any new or updated transactions fade in.
for (var ii = 0; ii < fade_in.length; ii++) {
new JX.FX(fade_in[ii]).setDuration(500).start({opacity: [0, 1]});
}
} }
JX.DOM.listen(list, 'click', 'transaction-edit', function(e) { JX.DOM.listen(list, 'click', 'transaction-edit', function(e) {
@ -48,4 +75,17 @@ JX.behavior('phabricator-transaction-list', function(config) {
e.kill(); e.kill();
}); });
JX.Stratcom.listen('submit', 'transaction-append', function(e) {
var form = e.getTarget();
JX.Workflow.newFromForm(form, {anchor: next_anchor})
.setHandler(function(response) {
ontransactions(response);
form.reset();
})
.start();
e.kill();
});
}); });