1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-18 12:52:42 +01:00

Basic stacked action support for EditEngine

Summary: Ref T9132. This still has a lot of rough edges but the basics seem to work OK.

Test Plan: {F1012627}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T9132

Differential Revision: https://secure.phabricator.com/D14653
This commit is contained in:
epriestley 2015-12-02 14:38:11 -08:00
parent b82863d972
commit dc0d914134
14 changed files with 532 additions and 8 deletions

View file

@ -420,6 +420,7 @@ return array(
'rsrc/js/application/repository/repository-crossreference.js' => 'e5339c43',
'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08',
'rsrc/js/application/slowvote/behavior-slowvote-embed.js' => '887ad43f',
'rsrc/js/application/transactions/behavior-comment-actions.js' => 'f2c64202',
'rsrc/js/application/transactions/behavior-reorder-fields.js' => 'b59e1e96',
'rsrc/js/application/transactions/behavior-show-older-transactions.js' => 'dbbf48b6',
'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => 'b23b49e6',
@ -498,6 +499,8 @@ return array(
'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8',
'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262',
'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca',
'rsrc/js/phuix/PHUIXFormControl.js' => 'f9fba5ee',
'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b',
),
'symbols' => array(
'almanac-css' => 'dbb9b3af',
@ -561,6 +564,7 @@ return array(
'javelin-behavior-audit-preview' => 'd835b03a',
'javelin-behavior-bulk-job-reload' => 'edf8a145',
'javelin-behavior-choose-control' => '6153c708',
'javelin-behavior-comment-actions' => 'f2c64202',
'javelin-behavior-config-reorder-fields' => 'b6993408',
'javelin-behavior-conpherence-drag-and-drop-photo' => 'cf86d16a',
'javelin-behavior-conpherence-menu' => '1d45c74d',
@ -823,6 +827,8 @@ return array(
'phuix-action-list-view' => 'b5c256b8',
'phuix-action-view' => '8cf6d262',
'phuix-dropdown-menu' => 'bd4c8dca',
'phuix-form-control-view' => 'f9fba5ee',
'phuix-icon-view' => 'bff6884b',
'policy-css' => '957ea14c',
'policy-edit-css' => '815c66f7',
'policy-transaction-detail-css' => '82100a43',
@ -1767,6 +1773,10 @@ return array(
'javelin-util',
'javelin-request',
),
'bff6884b' => array(
'javelin-install',
'javelin-dom',
),
'c1700f6f' => array(
'javelin-install',
'javelin-util',
@ -1973,6 +1983,14 @@ return array(
'javelin-workflow',
'javelin-json',
),
'f2c64202' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phuix-form-control-view',
'phuix-icon-view',
),
'f36e01af' => array(
'javelin-behavior',
'javelin-behavior-device',
@ -2029,6 +2047,10 @@ return array(
'javelin-util',
'phabricator-busy',
),
'f9fba5ee' => array(
'javelin-install',
'javelin-dom',
),
'fa0f4fc2' => array(
'javelin-behavior',
'javelin-dom',

View file

@ -34,6 +34,7 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController {
->setViewer($viewer)
->withIDs(array($id))
->needContent(true)
->needRawContent(true)
->executeOne();
if (!$paste) {
return new Aphront404Response();

View file

@ -53,6 +53,7 @@ final class PhabricatorProjectsEditEngineExtension
pht('Add projects.'),
pht('Remove projects.'),
pht('Set associated projects, overwriting current value.'))
->setCommentActionLabel(pht('Add Projects'))
->setTransactionType($edge_type)
->setMetadataValue('edge:type', $project_edge_type)
->setValue($project_phids);

View file

@ -50,6 +50,7 @@ final class PhabricatorSubscriptionsEditEngineExtension
pht('Add subscribers.'),
pht('Remove subscribers.'),
pht('Set subscribers, overwriting current value.'))
->setCommentActionLabel(pht('Add Subscribers'))
->setTransactionType($subscribers_type)
->setValue($sub_phids);

View file

@ -873,6 +873,8 @@ abstract class PhabricatorEditEngine
}
final public function buildEditEngineCommentView($object) {
$config = $this->loadEditEngineConfiguration(null);
$viewer = $this->getViewer();
$object_phid = $object->getPHID();
@ -897,6 +899,19 @@ abstract class PhabricatorEditEngine
$view->setCurrentVersion($this->loadDraftVersion($object));
$fields = $this->buildEditFields($object);
$all_types = array();
foreach ($fields as $field) {
// TODO: Load draft stuff.
$types = $field->getCommentEditTypes();
foreach ($types as $type) {
$all_types[] = $type;
}
}
$view->setEditTypes($all_types);
return $view;
}
@ -999,6 +1014,9 @@ abstract class PhabricatorEditEngine
return new Aphront400Response();
}
$config = $this->loadEditEngineConfiguration(null);
$fields = $this->buildEditFields($object);
$is_preview = $request->isPreviewRequest();
$view_uri = $this->getObjectViewURI($object);
@ -1025,11 +1043,46 @@ abstract class PhabricatorEditEngine
$xactions = array();
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(clone $comment_template)
->setContent($comment_text));
$actions = $request->getStr('editengine.actions');
if ($actions) {
$type_map = array();
foreach ($fields as $field) {
$types = $field->getCommentEditTypes();
foreach ($types as $type) {
$type_map[$type->getEditType()] = $type;
}
}
$actions = phutil_json_decode($actions);
foreach ($actions as $action) {
$type = idx($action, 'type');
if (!$type) {
continue;
}
$edit_type = idx($type_map, $type);
if (!$edit_type) {
continue;
}
$type_xactions = $edit_type->generateTransactions(
$template,
array(
'value' => idx($action, 'value'),
));
foreach ($type_xactions as $type_xaction) {
$xactions[] = $type_xaction;
}
}
}
if (strlen($comment_text) || !$xactions) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(clone $comment_template)
->setContent($comment_text));
}
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)

View file

@ -494,4 +494,8 @@ abstract class PhabricatorEditField extends Phobject {
return array($edit_type);
}
public function getCommentEditTypes() {
return array();
}
}

View file

@ -3,8 +3,19 @@
abstract class PhabricatorTokenizerEditField
extends PhabricatorPHIDListEditField {
private $commentActionLabel;
abstract protected function newDatasource();
public function setCommentActionLabel($label) {
$this->commentActionLabel = $label;
return $this;
}
public function getCommentActionLabel() {
return $this->commentActionLabel;
}
protected function newControl() {
$control = id(new AphrontFormTokenizerControl())
->setDatasource($this->newDatasource());
@ -21,4 +32,42 @@ abstract class PhabricatorTokenizerEditField
return $request->getArr($key.'.original');
}
protected function newEditType() {
$type = parent::newEditType();
if ($this->getUseEdgeTransactions()) {
$datasource = $this->newDatasource()
->setViewer($this->getViewer());
$type->setDatasource($datasource);
}
return $type;
}
public function getCommentEditTypes() {
if (!$this->getUseEdgeTransactions()) {
return parent::getCommentEditTypes();
}
$transaction_type = $this->getTransactionType();
if ($transaction_type === null) {
return array();
}
$label = $this->getCommentActionLabel();
if ($label === null) {
return array();
}
$type_key = $this->getEditTypeKey();
$base = $this->getEditType();
$add = id(clone $base)
->setEditType($type_key.'.add')
->setEdgeOperation('+')
->setLabel($label);
return array($add);
}
}

View file

@ -811,7 +811,17 @@ abstract class PhabricatorApplicationTransactionEditor
$this->adjustTransactionValues($object, $xaction);
}
$xactions = $this->filterTransactions($object, $xactions);
try {
$xactions = $this->filterTransactions($object, $xactions);
} catch (Exception $ex) {
if ($read_locking) {
$object->endReadLocking();
}
if ($transaction_open) {
$object->killTransaction();
}
throw $ex;
}
// Now that we've merged, filtered, and combined transactions, check for
// required capabilities.

View file

@ -4,6 +4,7 @@ final class PhabricatorEdgeEditType extends PhabricatorEditType {
private $edgeOperation;
private $valueDescription;
private $datasource;
public function setEdgeOperation($edge_operation) {
$this->edgeOperation = $edge_operation;
@ -14,6 +15,15 @@ final class PhabricatorEdgeEditType extends PhabricatorEditType {
return $this->edgeOperation;
}
public function setDatasource($datasource) {
$this->datasource = $datasource;
return $this;
}
public function getDatasource() {
return $this->datasource;
}
public function getValueType() {
return 'list<phid>';
}
@ -46,4 +56,33 @@ final class PhabricatorEdgeEditType extends PhabricatorEditType {
return $this->valueDescription;
}
public function getPHUIXControlType() {
$datasource = $this->getDatasource();
if (!$datasource) {
return null;
}
return 'tokenizer';
}
public function getPHUIXControlSpecification() {
$datasource = $this->getDatasource();
if (!$datasource) {
return null;
}
$template = new AphrontTokenizerTemplateView();
return array(
'markup' => $template->render(),
'config' => array(
'src' => $datasource->getDatasourceURI(),
'browseURI' => $datasource->getBrowseURI(),
'placeholder' => $datasource->getPlaceholderText(),
),
);
}
}

View file

@ -4,6 +4,7 @@ abstract class PhabricatorEditType extends Phobject {
private $editType;
private $transactionType;
private $label;
private $field;
private $description;
private $summary;
@ -30,6 +31,15 @@ abstract class PhabricatorEditType extends Phobject {
return $this->summary;
}
public function setLabel($label) {
$this->label = $label;
return $this;
}
public function getLabel() {
return $this->label;
}
public function setField(PhabricatorEditField $field) {
$this->field = $field;
return $this;
@ -86,4 +96,12 @@ abstract class PhabricatorEditType extends Phobject {
return $xaction;
}
public function getPHUIXControlType() {
return null;
}
public function getPHUIXControlSpecification() {
return null;
}
}

View file

@ -22,6 +22,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
private $currentVersion;
private $versionedDraft;
private $editTypes;
public function setObjectPHID($object_phid) {
$this->objectPHID = $object_phid;
@ -100,6 +101,15 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
return $this;
}
public function setEditTypes($edit_types) {
$this->editTypes = $edit_types;
return $this;
}
public function getEditTypes() {
return $this->editTypes;
}
public function render() {
$user = $this->getUser();
@ -182,7 +192,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
$version_key = PhabricatorVersionedDraft::KEY_VERSION;
$version_value = $this->getCurrentVersion();
return id(new AphrontFormView())
$form = id(new AphrontFormView())
->setUser($this->getUser())
->addSigil('transaction-append')
->setWorkflow(true)
@ -193,7 +203,57 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
->setAction($this->getAction())
->setID($this->getFormID())
->addHiddenInput('__draft__', $draft_key)
->addHiddenInput($version_key, $version_value)
->addHiddenInput($version_key, $version_value);
$edit_types = $this->getEditTypes();
if ($edit_types) {
$action_map = array();
foreach ($edit_types as $edit_type) {
$key = $edit_type->getEditType();
$action_map[$key] = array(
'key' => $key,
'label' => $edit_type->getLabel(),
'type' => $edit_type->getPHUIXControlType(),
'spec' => $edit_type->getPHUIXControlSpecification(),
);
}
$options = array();
$options['+'] = pht('Add Action...');
foreach ($action_map as $key => $item) {
$options[$key] = $item['label'];
}
$action_id = celerity_generate_unique_node_id();
$input_id = celerity_generate_unique_node_id();
$form->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'editengine.actions',
'id' => $input_id,
)));
$form->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Actions'))
->setID($action_id)
->setOptions($options));
Javelin::initBehavior(
'comment-actions',
array(
'actionID' => $action_id,
'inputID' => $input_id,
'formID' => $this->getFormID(),
'actions' => $action_map,
));
}
$form
->appendChild(
id(new PhabricatorRemarkupControl())
->setID($this->getCommentID())
@ -207,6 +267,8 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
->appendChild(
id(new AphrontFormMarkupControl())
->setValue($status));
return $form;
}
private function renderPreviewPanel() {

View file

@ -0,0 +1,84 @@
/**
* @provides javelin-behavior-comment-actions
* @requires javelin-behavior
* javelin-stratcom
* javelin-workflow
* javelin-dom
* phuix-form-control-view
* phuix-icon-view
*/
JX.behavior('comment-actions', function(config) {
var action_map = config.actions;
var action_node = JX.$(config.actionID);
var form_node = JX.$(config.formID);
var input_node = JX.$(config.inputID);
var rows = {};
JX.DOM.listen(action_node, 'change', null, function() {
var options = action_node.options;
var option;
var selected = action_node.value;
action_node.value = '+';
for (var ii = 0; ii < options.length; ii++) {
option = options[ii];
if (option.value == selected) {
add_row(option);
break;
}
}
});
JX.DOM.listen(form_node, 'submit', null, function() {
var data = [];
for (var k in rows) {
data.push({
type: k,
value: rows[k].getValue()
});
}
input_node.value = JX.JSON.stringify(data);
});
function add_row(option) {
var action = action_map[option.value];
if (!action) {
return;
}
option.disabled = true;
var icon = new JX.PHUIXIconView()
.setIcon('fa-times-circle');
var remove = JX.$N('a', {href: '#'}, icon.getNode());
var control = new JX.PHUIXFormControl()
.setLabel(action.label)
.setError(remove)
.setControl('tokenizer', action.spec);
var node = control.getNode();
rows[action.key] = control;
JX.DOM.listen(remove, 'click', null, function(e) {
e.kill();
JX.DOM.remove(node);
delete rows[action.key];
option.disabled = false;
});
// TODO: Grotesque.
action_node
.parentNode
.parentNode
.parentNode
.insertBefore(node, action_node.parentNode.parentNode.nextSibling);
}
});

View file

@ -0,0 +1,133 @@
/**
* @provides phuix-form-control-view
* @requires javelin-install
* javelin-dom
*/
JX.install('PHUIXFormControl', {
members: {
_node: null,
_labelNode: null,
_errorNode: null,
_inputNode: null,
_valueSetCallback: null,
_valueGetCallback: null,
setLabel: function(label) {
JX.DOM.setContent(this._getLabelNode(), label);
return this;
},
setError: function(error) {
JX.DOM.setContent(this._getErrorNode(), error);
return this;
},
setControl: function(type, spec) {
var node = this._getInputNode();
var input;
switch (type) {
case 'tokenizer':
input = this._newTokenizer(spec);
break;
default:
// TODO: Default or better error?
JX.$E('Bad Input Type');
return;
}
JX.DOM.setContent(node, input.node);
this._valueGetCallback = input.get;
this._valueSetCallback = input.set;
return this;
},
setValue: function(value) {
this._valueSetCallback(value);
return this;
},
getValue: function() {
return this._valueGetCallback();
},
getNode: function() {
if (!this._node) {
var attrs = {
className: 'aphront-form-control grouped'
};
var content = [
this._getLabelNode(),
this._getErrorNode(),
this._getInputNode()
];
this._node = JX.$N('div', attrs, content);
}
return this._node;
},
_getLabelNode: function() {
if (!this._labelNode) {
var attrs = {
className: 'aphront-form-label'
};
this._labelNode = JX.$N('label', attrs);
}
return this._labelNode;
},
_getErrorNode: function() {
if (!this._errorNode) {
var attrs = {
className: 'aphront-form-error'
};
this._errorNode = JX.$N('span', attrs);
}
return this._errorNode;
},
_getInputNode: function() {
if (!this._inputNode) {
var attrs = {
className: 'aphront-form-input'
};
this._inputNode = JX.$N('div', attrs);
}
return this._inputNode;
},
_newTokenizer: function(spec) {
var build = JX.Prefab.newTokenizerFromTemplate(
spec.markup,
spec.config);
build.tokenizer.start();
return {
node: build.node,
get: function() {
return JX.keys(build.tokenizer.getTokens());
},
set: function(map) {
for (var k in map) {
build.tokenizer.addToken(k, map[k]);
}
}
};
}
}
});

View file

@ -0,0 +1,47 @@
/**
* @provides phuix-icon-view
* @requires javelin-install
* javelin-dom
*/
JX.install('PHUIXIconView', {
members: {
_node: null,
_icon: null,
_color: null,
setIcon: function(icon) {
var node = this.getNode();
if (this._icon) {
JX.DOM.alterClass(node, this._icon, false);
}
this._icon = icon;
JX.DOM.alterClass(node, this._icon, true);
return this;
},
setColor: function(color) {
var node = this.getNode();
if (this._color) {
JX.DOM.alterClass(node, this._color, false);
}
this._color = color;
JX.DOM.alterClass(node, this._color, true);
return this;
},
getNode: function() {
if (!this._node) {
var attrs = {
className: 'phui-icon-view phui-font-fa'
};
this._node = JX.$N('span', attrs);
}
return this._node;
}
}
});