1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-09-20 09:18:48 +02: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',
'PhabricatorApplicationTransactionFeedStory' => 'applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php',
'PhabricatorApplicationTransactionQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionQuery.php',
'PhabricatorApplicationTransactionResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionResponse.php',
'PhabricatorApplicationTransactionView' => 'applications/transactions/view/PhabricatorApplicationTransactionView.php',
'PhabricatorApplicationTransactions' => 'applications/transactions/application/PhabricatorApplicationTransactions.php',
'PhabricatorApplicationUIExamples' => 'applications/uiexample/application/PhabricatorApplicationUIExamples.php',
@ -1884,6 +1885,7 @@ phutil_register_library_map(array(
'PhabricatorApplicationTransactionEditor' => 'PhabricatorEditor',
'PhabricatorApplicationTransactionFeedStory' => 'PhabricatorFeedStory',
'PhabricatorApplicationTransactionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorApplicationTransactionResponse' => 'AphrontProxyResponse',
'PhabricatorApplicationTransactionView' => 'AphrontView',
'PhabricatorApplicationTransactions' => 'PhabricatorApplication',
'PhabricatorApplicationUIExamples' => 'PhabricatorApplication',

View file

@ -42,8 +42,15 @@ final class PhabricatorMacroCommentController
)))
->applyTransactions($macro, $xactions);
return id(new AphrontRedirectResponse())
->setURI($view_uri);
if ($request->isAjax()) {
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())
->setWorkflow(true)
->setFlexible(true)
->addSigil('transaction-append')
->setAction($this->getApplicationURI('/comment/'.$macro->getID().'/'))
->setUser($user)
->appendChild(

View file

@ -43,7 +43,8 @@ final class PholioMockCommentController extends PholioController {
'ip' => $request->getRemoteAddr(),
));
$xaction = id(new PholioTransaction())
$xactions = array();
$xactions[] = id(new PholioTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new PholioTransactionComment())
@ -52,9 +53,16 @@ final class PholioMockCommentController extends PholioController {
id(new PholioMockEditor())
->setActor($user)
->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()) {
$engine->addObject(
$xaction->getComment(),
PholioTransaction::MARKUP_FIELD_COMMENT);
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
}
}
$engine->process();
@ -64,7 +64,10 @@ final class PholioMockViewController extends PholioController {
'<h1 style="margin: 2em; padding: 1em; border: 1px dashed grey;">'.
'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);
@ -171,6 +174,7 @@ final class PholioMockViewController extends PholioController {
$form = id(new AphrontFormView())
->setUser($user)
->addSigil('transaction-append')
->setAction($this->getApplicationURI('/comment/'.$mock->getID().'/'))
->setWorkflow(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);
if ($request->isAjax()) {
$view = id(new PhabricatorApplicationTransactionView())
return id(new PhabricatorApplicationTransactionResponse())
->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'),
));
->setTransactions(array($xaction))
->setAnchorOffset($request->getStr('anchor'));
} else {
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++;
$has_deleted_comment = $xaction->getComment() &&
$xaction->getComment()->getIsDeleted();
@ -105,7 +104,8 @@ class PhabricatorApplicationTransactionView extends AphrontView {
public function render() {
$view = new PhabricatorTimelineView();
foreach ($this->buildEvents() as $event) {
$events = $this->buildEvents();
foreach ($events as $event) {
$view->addEvent($event);
}
@ -117,7 +117,8 @@ class PhabricatorApplicationTransactionView extends AphrontView {
Javelin::initBehavior(
'phabricator-transaction-list',
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 $id;
private $flexible;
private $sigils = array();
public function setFlexible($flexible) {
$this->flexible = $flexible;
@ -52,6 +53,11 @@ final class AphrontFormView extends AphrontView {
return $this;
}
public function addSigil($sigil) {
$this->sigils[] = $sigil;
return $this;
}
public function render() {
if ($this->flexible) {
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.');
}
$sigils = $this->sigils;
if ($this->workflow) {
$sigils[] = 'workflow';
}
return phabricator_render_form(
$this->user,
array(
@ -83,7 +94,7 @@ final class AphrontFormView extends AphrontView {
'action' => $this->action,
'method' => $this->method,
'enctype' => $this->encType,
'sigil' => $this->workflow ? 'workflow' : null,
'sigil' => $sigils ? implode(' ', $sigils) : null,
'id' => $this->id,
),
$layout->render());

View file

@ -18,13 +18,7 @@ final class PhabricatorTimelineView extends AphrontView {
public function render() {
require_celerity_resource('phabricator-timeline-view-css');
$spacer = phutil_render_tag(
'div',
array(
'class' => 'phabricator-timeline-event-view '.
'phabricator-timeline-spacer',
),
'');
$spacer = self::renderSpacer();
$events = array();
foreach ($this->events as $event) {
@ -42,4 +36,13 @@ final class PhabricatorTimelineView extends AphrontView {
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-workflow
* javelin-dom
* javelin-fx
*/
JX.behavior('phabricator-transaction-list', function(config) {
var list = JX.$(config.listID);
var xaction_nodes = null;
var next_anchor = config.nextAnchor;
function get_xaction_nodes() {
if (xaction_nodes === null) {
@ -23,16 +25,41 @@ JX.behavior('phabricator-transaction-list', function(config) {
}
function ontransactions(response) {
var fade_in = [];
var first_new = null;
var nodes = get_xaction_nodes();
for (var phid in response.xactions) {
var new_node = JX.$H(response.xactions[phid]).getFragment().firstChild;
fade_in.push(new_node);
if (nodes[phid]) {
JX.DOM.replace(nodes[phid], new_node);
} else {
if (first_new === null) {
first_new = 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;
}
// 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) {
@ -48,4 +75,17 @@ JX.behavior('phabricator-transaction-list', function(config) {
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();
});
});