diff --git a/scripts/celerity_mapper.php b/scripts/celerity_mapper.php index f766a669fb..2c6c26b496 100755 --- a/scripts/celerity_mapper.php +++ b/scripts/celerity_mapper.php @@ -101,6 +101,8 @@ foreach ($file_map as $path => $info) { $provides = array_filter($provides); $requires = array_filter($requires); + var_dump($requires); + if (count($provides) !== 1) { throw new Exception( "File {$path} must @provide exactly one Celerity target."); diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index c8bd5852be..193ab071fc 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -81,7 +81,7 @@ celerity_register_resource_map(array( ), 'aphront-side-nav-view-css' => array( - 'uri' => '/res/09b7eb85/rsrc/css/aphront/side-nav-view.css', + 'uri' => '/res/4f4c5ca8/rsrc/css/aphront/side-nav-view.css', 'type' => 'css', 'requires' => array( @@ -199,7 +199,7 @@ celerity_register_resource_map(array( ), 'diffusion-commit-view-css' => array( - 'uri' => '/res/4593ecc8/rsrc/css/application/diffusion/commit-view.css', + 'uri' => '/res/8c139192/rsrc/css/application/diffusion/commit-view.css', 'type' => 'css', 'requires' => array( @@ -336,6 +336,16 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/core/behavior-workflow.js', ), + 'multirow-row-manager' => + array( + 'uri' => '/res/330d076b/rsrc/js/application/core/MultirowRowManager.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-lib-dev', + ), + 'disk' => '/rsrc/js/application/core/MultirowRowManager.js', + ), 'javelin-behavior-differential-add-reviewers' => array( 'uri' => '/res/330154e4/rsrc/js/application/differential/behavior-add-reviewers.js', @@ -406,6 +416,26 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/differential/behavior-show-more.js', ), + 'javelin-behavior-herald-rule-editor' => + array( + 'uri' => '/res/f18bcd5e/rsrc/js/application/herald/herald-rule-editor.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'herald-rule-editor', + ), + 'disk' => '/rsrc/js/application/herald/herald-rule-editor.js', + ), + 'herald-rule-editor' => + array( + 'uri' => '/res/e71d1d0e/rsrc/js/application/herald/HeraldRuleEditor.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'multirow-row-manager', + ), + 'disk' => '/rsrc/js/application/herald/HeraldRuleEditor.js', + ), 'javelin-behavior-maniphest-transaction-controls' => array( 'uri' => '/res/fc6a8722/rsrc/js/application/maniphest/behavior-transaction-controls.js', @@ -491,7 +521,7 @@ celerity_register_resource_map(array( ), array ( 'packages' => array ( - 73063447 => + '848f4c9f' => array ( 'name' => 'core.pkg.css', 'symbols' => @@ -511,7 +541,7 @@ celerity_register_resource_map(array( 12 => 'phabricator-remarkup-css', 13 => 'syntax-highlighting-css', ), - 'uri' => '/res/pkg/73063447/core.pkg.css', + 'uri' => '/res/pkg/848f4c9f/core.pkg.css', 'type' => 'css', ), '76f3c1f8' => @@ -545,33 +575,33 @@ celerity_register_resource_map(array( 'uri' => '/res/pkg/30d594cf/differential.pkg.js', 'type' => 'js', ), - '2393c3a4' => + 'eadf6ec3' => array ( 'name' => 'diffusion.pkg.css', 'symbols' => array ( 0 => 'diffusion-commit-view-css', ), - 'uri' => '/res/pkg/2393c3a4/diffusion.pkg.css', + 'uri' => '/res/pkg/eadf6ec3/diffusion.pkg.css', 'type' => 'css', ), ), 'reverse' => array ( - 'phabricator-core-css' => '73063447', - 'phabricator-core-buttons-css' => '73063447', - 'phabricator-standard-page-view' => '73063447', - 'aphront-dialog-view-css' => '73063447', - 'aphront-form-view-css' => '73063447', - 'aphront-panel-view-css' => '73063447', - 'aphront-side-nav-view-css' => '73063447', - 'aphront-table-view-css' => '73063447', - 'aphront-crumbs-view-css' => '73063447', - 'aphront-tokenizer-control-css' => '73063447', - 'aphront-typeahead-control-css' => '73063447', - 'phabricator-directory-css' => '73063447', - 'phabricator-remarkup-css' => '73063447', - 'syntax-highlighting-css' => '73063447', + 'phabricator-core-css' => '848f4c9f', + 'phabricator-core-buttons-css' => '848f4c9f', + 'phabricator-standard-page-view' => '848f4c9f', + 'aphront-dialog-view-css' => '848f4c9f', + 'aphront-form-view-css' => '848f4c9f', + 'aphront-panel-view-css' => '848f4c9f', + 'aphront-side-nav-view-css' => '848f4c9f', + 'aphront-table-view-css' => '848f4c9f', + 'aphront-crumbs-view-css' => '848f4c9f', + 'aphront-tokenizer-control-css' => '848f4c9f', + 'aphront-typeahead-control-css' => '848f4c9f', + 'phabricator-directory-css' => '848f4c9f', + 'phabricator-remarkup-css' => '848f4c9f', + 'syntax-highlighting-css' => '848f4c9f', 'differential-core-view-css' => '76f3c1f8', 'differential-changeset-view-css' => '76f3c1f8', 'differential-revision-detail-css' => '76f3c1f8', @@ -585,6 +615,6 @@ celerity_register_resource_map(array( 'javelin-behavior-differential-populate' => '30d594cf', 'javelin-behavior-differential-show-more' => '30d594cf', 'javelin-behavior-differential-diff-radios' => '30d594cf', - 'diffusion-commit-view-css' => '2393c3a4', + 'diffusion-commit-view-css' => 'eadf6ec3', ), )); diff --git a/src/applications/herald/controller/rule/HeraldRuleController.php b/src/applications/herald/controller/rule/HeraldRuleController.php index 84c0e2b20b..0bf62be8d3 100644 --- a/src/applications/herald/controller/rule/HeraldRuleController.php +++ b/src/applications/herald/controller/rule/HeraldRuleController.php @@ -251,6 +251,7 @@ class HeraldRuleController extends HeraldController { $form = id(new AphrontFormView()) ->setUser($user) + ->setID('herald-rule-edit-form') ->addHiddenInput('type', $rule->getContentType()) ->addHiddenInput('save', true) ->addHiddenInput('rule', '') @@ -270,16 +271,33 @@ class HeraldRuleController extends HeraldController { ->appendChild( '

Conditions

'. '
'. - 'Create New Condition'. + javelin_render_tag( + 'a', + array( + 'href' => '#', + 'class' => 'button green', + 'sigil' => 'create-action', + ), + 'Create New Condition'). '

When '.$must_match.' these conditions are met:

'. - '
'. + javelin_render_tag( + 'table', + array( + 'sigil' => 'rule-conditions', + ), + ''). '
') ->appendChild( '

Action

'. '
'. 'Create New Action'. '

Take these actions:

'. - '
'. + javelin_render_tag( + 'table', + array( + 'sigil' => 'rule-actions', + ), + ''). '
') ->appendChild( id(new AphrontFormSubmitControl()) @@ -439,11 +457,10 @@ class HeraldRuleController extends HeraldController { HeraldValueTypeConfig::getValueTypeForAction($action); } -/* Javelin::initBehavior( 'herald-rule-editor', array( - 'root' => 'qq',//$form->requireUniqueId(), + 'root' => 'herald-rule-edit-form', 'conditions' => (object) $serial_conditions, 'actions' => (object) $serial_actions, 'template' => $this->buildTokenizerTemplates() + array( @@ -452,8 +469,6 @@ class HeraldRuleController extends HeraldController { 'info' => $config_info, )); -*/ - $panel = new AphrontPanelView(); $panel->setHeader('Edit Herald Rule'); $panel->setWidth(AphrontPanelView::WIDTH_WIDE); diff --git a/src/view/form/base/AphrontFormView.php b/src/view/form/base/AphrontFormView.php index 4722a32f7d..710b223638 100755 --- a/src/view/form/base/AphrontFormView.php +++ b/src/view/form/base/AphrontFormView.php @@ -25,6 +25,12 @@ final class AphrontFormView extends AphrontView { private $encType; private $user; private $workflow; + private $id; + + public function setID($id) { + $this->id = $id; + return $this; + } public function setUser(PhabricatorUser $user) { $this->user = $user; @@ -66,6 +72,7 @@ final class AphrontFormView extends AphrontView { 'class' => 'aphront-form-view', 'enctype' => $this->encType, 'sigil' => $this->workflow ? 'workflow' : null, + 'id' => $this->id, ), $this->renderDataInputs(). $this->renderChildren()); diff --git a/webroot/rsrc/js/application/core/MultirowRowManager.js b/webroot/rsrc/js/application/core/MultirowRowManager.js new file mode 100644 index 0000000000..45d1bf368a --- /dev/null +++ b/webroot/rsrc/js/application/core/MultirowRowManager.js @@ -0,0 +1,143 @@ +/** + * @requires javelin-lib-dev + * @provides multirow-row-manager + * @javelin + */ + + +/** + * Give a MultirowRowManager a table DOM elem to manage. + * You can add rows, and provide a given ID if you like. + * You can update rows by ID. + * Rows are automatically equipped with a removal button. + * You can listen to the 'row-removed' event on the Manager to get + * notifications of these row removals, with the DOM id of the removed + * row as event data. + */ +JX.install('MultirowRowManager', { + /** + * @param DOM element root Container for rows + */ + construct : function(root, minRows) { + this._root = root; + this._rows = []; + + if (typeof minRows !== "undefined") { + this._minRows = minRows; + } else { + this._minRows = 1; + } + + JX.DOM.listen( + this._root, + 'click', + JX.MultirowRowManager._removeSigil, + JX.bind(this, this._onrowremoved)); + }, + + members : { + _count : 0, + _nextID : 0, + _root : null, + _rows : null, + + _generateRowID : function() { + return "" + this._nextID++; + }, + + _wrapRowContents : function(row_id, row_contents) { + var row = JX.$N('tr', + { sigil : JX.MultirowRowManager.getRowSigil(), + meta : { multirow_row_manager_row_id : row_id } + }, + row_contents); + + var removeButton = JX.$N( + 'td', + {}, + JX.$N( + 'a', + { className: "button", + sigil: JX.MultirowRowManager._removeSigil + }, + '-')); + + JX.DOM.appendContent(row, removeButton); + return row; + }, + + getRowID : function(row) { + return JX.Stratcom.getData(row).multirow_row_manager_row_id; + }, + /** + * @param row_contents [DOM elements] New contents of row + * @param row_id row ID to update, will throw if this row has been removed + */ + updateRow : function(row_id, row_contents) { + if (__DEV__) { + if (typeof this._rows[row_id] === "undefined") { + throw new Error("JX.MultirowRowManager.updateRow(row_id, " + + "row_contents): provided row id does not exist." + + " Use addRow to create a new row and make sure " + + "not to update rows that have been deleted."); + } + } + var old_row = this._rows[row_id]; + var new_row = this._wrapRowContents(row_id, row_contents); + JX.copy(JX.Stratcom.getData(new_row), JX.Stratcom.getData(old_row)); + + JX.DOM.replace(old_row, new_row); + this._rows[row_id] = new_row; + + this._oncountchanged(); // Fix the new button. + return new_row; + }, + + addRow : function(row_contents) { + var row_id = this._generateRowID(); + var row = this._wrapRowContents(row_id, row_contents); + JX.DOM.appendContent(this._root, row); + + this._count++; + this._oncountchanged(); + + this._rows[row_id] = row; + return row; + }, + _onrowremoved : function(e) { + if (!JX.Stratcom.getData(e.getTarget()).enabled) { + return; + } + var row = e.getNode(JX.MultirowRowManager.getRowSigil()); + var row_id = this.getRowID(row); + delete this._rows[row_id]; + JX.DOM.remove(row); + + this._count--; + this._oncountchanged(); + this.invoke('row-removed', row_id); + }, + + _oncountchanged : function(e) { + var buttons = JX.DOM.scry( + this._root, + 'a', + JX.MultirowRowManager._removeSigil); + + var disable = (this._minRows >= 0 && this._count <= this._minRows); + for (var i = 0; i < buttons.length; i++) { + var button = buttons[i]; + JX.DOM.alterClass(button, 'disabled', disable); + JX.Stratcom.getData(button).enabled = !disable; + } + } + }, + events : ['row-removed'], + statics : { + getRowSigil : function() { + return "tools-multirow-row-manager-row"; + }, + _removeSigil : "tools-multirow-row-manager-row-remove" + } +}); + diff --git a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js new file mode 100644 index 0000000000..a6bce77dda --- /dev/null +++ b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js @@ -0,0 +1,363 @@ +/** + * @requires multirow-row-manager + * javelin-lib-dev + * javelin-typeahead-dev + * @provides herald-rule-editor + * @javelin + */ + +JX.install('HeraldRuleEditor', { + construct : function(config) { + var root = JX.$(config.root); + this._root = root; + + JX.DOM.listen( + root, + 'click', + 'create-condition', + JX.bind(this, this._onnewcondition)); + + JX.DOM.listen( + root, + 'click', + 'create-action', + JX.bind(this, this._onnewaction)); + + JX.DOM.listen(root, 'change', null, JX.bind(this, this._onchange)); + JX.DOM.listen(root, 'submit', null, JX.bind(this, this._onsubmit)); + + var conditionsTable = JX.DOM.find(root, 'table', 'rule-conditions'); + var actionsTable = JX.DOM.find(root, 'table', 'rule-actions'); + + this._conditionsRowManager = new JX.MultirowRowManager(conditionsTable); + this._conditionsRowManager.listen( + 'row-removed', + JX.bind(this, function(row_id) { + delete this._config.conditions[row_id]; + })); + + this._actionsRowManager = new JX.MultirowRowManager(actionsTable); + this._actionsRowManager.listen( + 'row-removed', + JX.bind(this, function(row_id) { + delete this._config.actions[row_id]; + })); + + this._conditionGetters = {}; + this._conditionTypes = {}; + this._actionGetters = {}; + this._actionTypes = {}; + + this._config = config; + + var conditions = this._config.conditions; + this._config.conditions = []; + + var actions = this._config.actions; + this._config.actions = []; + + this._renderConditions(conditions); + this._renderActions(actions); + }, + + members : { + _config : null, + _root : null, + _conditionGetters : null, + _conditionTypes : null, + _actionGetters : null, + _actionTypes : null, + _conditionsRowManager : null, + _actionsRowManager : null, + + _onnewcondition : function(e) { + this._newCondition(); + e.kill(); + }, + _onnewaction : function(e) { + this._newAction(); + e.kill(); + }, + _onchange : function(e) { + var target = e.getTarget(); + + var row = e.getNode(JX.MultirowRowManager.getRowSigil()); + if (!row) { + // Changing the "when all of / any of these..." dropdown. + return; + } + + if (JX.Stratcom.hasSigil(target, 'field-select')) { + this._onfieldchange(row); + } else if (JX.Stratcom.hasSigil(target, 'condition-select')) { + this._onconditionchange(row); + } else if (JX.Stratcom.hasSigil(target, 'action-select')) { + this._onactionchange(row); + } + }, + _onsubmit : function(e) { + var rule = JX.DOM.find(this._root, 'input', 'rule'); + + var k; + + for (k in this._config.conditions) { + this._config.conditions[k][2] = this._getConditionValue(k); + } + + var acts = this._config.actions; + for (k in this._config.actions) { + this._config.actions[k][1] = this._getActionTarget(k); + } + + rule.value = JX.JSON.serialize({ + conditions: this._config.conditions, + actions: this._config.actions + }); + }, + + _getConditionValue : function(id) { + if (this._conditionGetters[id]) { + return this._conditionGetters[id](); + } + return this._config.conditions[id][2]; + }, + + _getActionTarget : function(id) { + if (this._actionGetters[id]) { + return this._actionGetters[id](); + } + return this._config.actions[id][1]; + }, + + _onactionchange : function(r) { + var target = JX.DOM.find(r, 'select', 'action-select'); + var row_id = this._actionsRowManager.getRowID(r); + + this._config.actions[row_id][0] = target.value; + + var target_cell = JX.DOM.find(r, 'td', 'target-cell'); + var target_input = this._renderTargetInputForRow(row_id); + + JX.DOM.setContent(target_cell, target_input); + }, + _onfieldchange : function(r) { + var target = JX.DOM.find(r, 'select', 'field-select'); + var row_id = this._actionsRowManager.getRowID(r); + + this._config.conditions[row_id][0] = target.value; + + var condition_cell = JX.DOM.find(r, 'td', 'condition-cell'); + var condition_select = this._renderSelect( + this._selectKeys( + this._config.info.conditions, + this._config.info.conditionMap[target.value]), + this._config.conditions[row_id][1], + 'condition-select'); + + JX.DOM.setContent(condition_cell, condition_select); + + this._onconditionchange(r); + }, + _onconditionchange : function(r) { + var target = JX.DOM.find(r, 'select', 'condition-select'); + var row_id = this._conditionsRowManager.getRowID(r); + + this._config.conditions[row_id][1] = target.value; + + var value_cell = JX.DOM.find(r, 'td', 'value-cell'); + var value_input = this._renderValueInputForRow(row_id); + JX.DOM.setContent(value_cell, value_input); + }, + + _renderTargetInputForRow : function(row_id) { + var action = this._config.actions[row_id]; + var type = this._config.info.targets[action[0]]; + + var input = this._buildInput(type); + var node = input[0]; + var get_fn = input[1]; + var set_fn = input[2]; + + if (node) { + JX.Stratcom.addSigil(node, 'action-target'); + } + + + var old_type = this._actionTypes[row_id]; + if (old_type == type || !old_type) { + set_fn(this._getActionTarget(row_id)); + } + + this._actionTypes[row_id] = type; + this._actionGetters[row_id] = get_fn; + + return node; + }, + + _buildInput : function(type) { + var input; + var get_fn; + var set_fn; + switch (type) { + case 'rule': + input = this._renderSelect(this._config.template.rules); + get_fn = function() { return input.value; }; + set_fn = function(v) { input.value = v; }; + break; + case 'email': + case 'employee': + case 'repository': + case 'tag': + case 'package': + var tokenizer = this._newTokenizer(type); + input = tokenizer[0]; + get_fn = tokenizer[1]; + set_fn = tokenizer[2]; + break; + case 'none': + input = ''; + get_fn = JX.bag; + set_fn = JX.bag; + break; + default: + input = JX.$N('input'); + get_fn = function() { return input.value; }; + set_fn = function(v) { input.value = v; }; + break; + } + + return [input, get_fn, set_fn]; + }, + + _renderValueInputForRow : function(row_id) { + var cond = this._config.conditions[row_id]; + var type = this._config.info.values[cond[0]][cond[1]]; + + var input = this._buildInput(type); + var node = input[0]; + var get_fn = input[1]; + var set_fn = input[2]; + + if (node) { + JX.Stratcom.addSigil(node, 'condition-value'); + } + + var old_type = this._conditionTypes[row_id]; + if (old_type == type || !old_type) { + set_fn(this._getConditionValue(row_id)); + } + + this._conditionTypes[row_id] = type; + this._conditionGetters[row_id] = get_fn; + + return node; + }, + + _newTokenizer : function(type) { + var template = JX.$N( + 'div', + new JX.HTML(this._config.template.markup)); + template = template.firstChild; + template.id = ''; + + var datasource = new JX.TypeaheadPreloadedSource( + this._config.template.source[type]); + + var typeahead = new JX.Typeahead(template); + typeahead.setDatasource(datasource); + + var tokenizer = new JX.Tokenizer(template); + tokenizer.setTypeahead(typeahead); + tokenizer.start(); + + return [ + template, + function() { + return tokenizer.getTokens(); + }, + function(map) { + for (var k in map) { + tokenizer.addToken(k, map[k]); + } + }]; + }, + _selectKeys : function(map, keys) { + var r = {}; + for (var ii = 0; ii < keys.length; ii++) { + r[keys[ii]] = map[keys[ii]]; + } + return r; + }, + _renderConditions : function(conditions) { + for (var k in conditions) { + this._newCondition(conditions[k]); + } + }, + _newCondition : function(data) { + var row = this._conditionsRowManager.addRow([]); + var row_id = this._conditionsRowManager.getRowID(row); + this._config.conditions[row_id] = data || [null, null, '']; + var r = this._conditionsRowManager.updateRow( + row_id, + this._renderCondition(row_id)); + + this._onfieldchange(r); + }, + _renderCondition : function(row_id) { + var field_select = this._renderSelect( + this._config.info.fields, + this._config.conditions[row_id][0], + 'field-select'); + var field_cell = JX.$N('td', {sigil: 'field-cell'}, field_select); + + var condition_cell = JX.$N('td', {sigil: 'condition-cell'}); + var value_cell = JX.$N('td', {className : 'value', sigil: 'value-cell'}); + + return [field_cell, condition_cell, value_cell]; + }, + _renderActions : function(actions) { + for (var k in actions) { + this._newAction(actions[k]); + delete actions[k]; + } + }, + _newAction : function(data) { + data = data || []; + var temprow = this._actionsRowManager.addRow([]); + var row_id = this._actionsRowManager.getRowID(temprow); + this._config.actions[row_id] = data; + var r = this._actionsRowManager.updateRow(row_id, + this._renderAction(data)); + this._onactionchange(r); + }, + _renderAction : function(action) { + var action_select = this._renderSelect( + this._config.info.actions, + action[0], + 'action-select'); + var action_cell = JX.$N('td', {sigil: 'action-cell'}, action_select); + + var target_cell = JX.$N( + 'td', + {className : 'target', sigil : 'target-cell'}); + + return [action_cell, target_cell]; + }, + _renderSelect : function(map, selected, sigil) { + var select = JX.$N( + 'select', + { + style : {width: '250px', margin: '0 .5em 0 0'}, + sigil : sigil + }); + for (var k in map) { + select.options[select.options.length] = new Option(map[k], k); + if (k == selected) { + select.value = k; + } + } + select.value = select.value || JX.keys(map)[0]; + return select; + } + } +}); \ No newline at end of file diff --git a/webroot/rsrc/js/application/herald/herald-rule-editor.js b/webroot/rsrc/js/application/herald/herald-rule-editor.js new file mode 100644 index 0000000000..ff4f17ecc0 --- /dev/null +++ b/webroot/rsrc/js/application/herald/herald-rule-editor.js @@ -0,0 +1,10 @@ +/** + * @requires herald-rule-editor + * javelin-behavior + * @provides javelin-behavior-herald-rule-editor + * @javelin + */ + +JX.behavior('herald-rule-editor', function(config) { + new JX.HeraldRuleEditor(config); +});