@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Make some Differential comment actions (like "Accept" and "Reject") conflict with one another

Summary:
Ref T11114. When a user selects "Accept", and then selects "Reject", remove the "Accept". It does not make sense to both accept and reject a revision.

For now, every one of the "actions" conflicts: accept, reject, resign, claim, close, commandeer, etc, etc. I couldn't come up with any combinations that it seems like users are reasonably likely to want to try, and we haven't received combo-action requests in the past that I can recall.

Test Plan:
- Selected "Accept", then selected "Reject". One replaced the other.
- Selected "Accept", then selected "Change Subscribers". Both co-existed happily.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T11114

Differential Revision: https://secure.phabricator.com/D17132

+147 -73
+18 -11
resources/celerity/map.php
··· 396 396 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '019f36c4', 397 397 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '453c5375', 398 398 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'd4eecc63', 399 + 'rsrc/js/application/diff/behavior-preview-link.js' => '051c7832', 399 400 'rsrc/js/application/differential/ChangesetViewManager.js' => 'a2828756', 400 401 'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => '2e3f9738', 401 402 'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => 'e10f8e18', ··· 453 454 'rsrc/js/application/search/behavior-reorder-profile-menu-items.js' => 'e2e0a072', 454 455 'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08', 455 456 'rsrc/js/application/slowvote/behavior-slowvote-embed.js' => '887ad43f', 456 - 'rsrc/js/application/transactions/behavior-comment-actions.js' => 'c23ecb0b', 457 + 'rsrc/js/application/transactions/behavior-comment-actions.js' => '8fd8b2b1', 457 458 'rsrc/js/application/transactions/behavior-reorder-configs.js' => 'd7a74243', 458 459 'rsrc/js/application/transactions/behavior-reorder-fields.js' => 'b59e1e96', 459 460 'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '94c65b72', ··· 609 610 'javelin-behavior-bulk-job-reload' => 'edf8a145', 610 611 'javelin-behavior-calendar-month-view' => 'fe33e256', 611 612 'javelin-behavior-choose-control' => '327a00d1', 612 - 'javelin-behavior-comment-actions' => 'c23ecb0b', 613 + 'javelin-behavior-comment-actions' => '8fd8b2b1', 613 614 'javelin-behavior-config-reorder-fields' => 'b6993408', 614 615 'javelin-behavior-conpherence-menu' => '7524fcfa', 615 616 'javelin-behavior-conpherence-participant-pane' => '8604caa8', ··· 625 626 'javelin-behavior-desktop-notifications-control' => 'edd1ba66', 626 627 'javelin-behavior-detect-timezone' => '4c193c96', 627 628 'javelin-behavior-device' => 'bb1dd507', 629 + 'javelin-behavior-diff-preview-link' => '051c7832', 628 630 'javelin-behavior-differential-add-reviewers-and-ccs' => 'e10f8e18', 629 631 'javelin-behavior-differential-comment-jump' => '4fdb476d', 630 632 'javelin-behavior-differential-diff-radios' => 'e1ff79b1', ··· 943 945 'javelin-json', 944 946 'javelin-dom', 945 947 'phabricator-keyboard-shortcut', 948 + ), 949 + '051c7832' => array( 950 + 'javelin-behavior', 951 + 'javelin-stratcom', 952 + 'javelin-dom', 946 953 ), 947 954 '05270951' => array( 948 955 'javelin-util', ··· 1640 1647 'javelin-stratcom', 1641 1648 'javelin-util', 1642 1649 ), 1650 + '8fd8b2b1' => array( 1651 + 'javelin-behavior', 1652 + 'javelin-stratcom', 1653 + 'javelin-workflow', 1654 + 'javelin-dom', 1655 + 'phuix-form-control-view', 1656 + 'phuix-icon-view', 1657 + 'javelin-behavior-phabricator-gesture', 1658 + ), 1643 1659 '8ff5e24c' => array( 1644 1660 'javelin-behavior', 1645 1661 'javelin-stratcom', ··· 1946 1962 'bff6884b' => array( 1947 1963 'javelin-install', 1948 1964 'javelin-dom', 1949 - ), 1950 - 'c23ecb0b' => array( 1951 - 'javelin-behavior', 1952 - 'javelin-stratcom', 1953 - 'javelin-workflow', 1954 - 'javelin-dom', 1955 - 'phuix-form-control-view', 1956 - 'phuix-icon-view', 1957 - 'javelin-behavior-phabricator-gesture', 1958 1965 ), 1959 1966 'c587b80f' => array( 1960 1967 'javelin-install',
+12
src/applications/differential/xaction/DifferentialRevisionActionTransaction.php
··· 73 73 74 74 $group_key = $this->getRevisionActionGroupKey(); 75 75 $field->setCommentActionGroupKey($group_key); 76 + 77 + // Currently, every revision action conflicts with every other 78 + // revision action: for example, you can not simultaneously Accept and 79 + // Reject a revision. 80 + 81 + // Under some configurations, some combinations of actions are sort of 82 + // technically permissible. For example, you could reasonably Reject 83 + // and Abandon a revision if "anyone can abandon anything" is enabled. 84 + 85 + // It's not clear that these combinations are actually useful, so just 86 + // keep things simple for now. 87 + $field->setActionConflictKey('revision.action'); 76 88 } 77 89 } 78 90
+10
src/applications/transactions/commentaction/PhabricatorEditEngineCommentAction.php
··· 8 8 private $initialValue; 9 9 private $order; 10 10 private $groupKey; 11 + private $conflictKey; 11 12 12 13 abstract public function getPHUIXControlType(); 13 14 abstract public function getPHUIXControlSpecification(); ··· 28 29 29 30 public function getGroupKey() { 30 31 return $this->groupKey; 32 + } 33 + 34 + public function setConflictKey($conflict_key) { 35 + $this->conflictKey = $conflict_key; 36 + return $this; 37 + } 38 + 39 + public function getConflictKey() { 40 + return $this->conflictKey; 31 41 } 32 42 33 43 public function setLabel($label) {
+12 -1
src/applications/transactions/editfield/PhabricatorApplyEditField.php
··· 4 4 extends PhabricatorEditField { 5 5 6 6 private $actionDescription; 7 + private $actionConflictKey; 7 8 8 9 protected function newControl() { 9 10 return null; ··· 18 19 return $this->actionDescription; 19 20 } 20 21 22 + public function setActionConflictKey($action_conflict_key) { 23 + $this->actionConflictKey = $action_conflict_key; 24 + return $this; 25 + } 26 + 27 + public function getActionConflictKey() { 28 + return $this->actionConflictKey; 29 + } 30 + 21 31 protected function newHTTPParameterType() { 22 32 return new AphrontBoolHTTPParameterType(); 23 33 } ··· 34 44 35 45 protected function newCommentAction() { 36 46 return id(new PhabricatorEditEngineStaticCommentAction()) 37 - ->setDescription($this->getActionDescription()); 47 + ->setDescription($this->getActionDescription()) 48 + ->setConflictKey($this->getActionConflictKey()); 38 49 } 39 50 40 51 }
+1
src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
··· 301 301 'spec' => $comment_action->getPHUIXControlSpecification(), 302 302 'initialValue' => $comment_action->getInitialValue(), 303 303 'groupKey' => $comment_action->getGroupKey(), 304 + 'conflictKey' => $comment_action->getConflictKey(), 304 305 ); 305 306 306 307 $type_map[$key] = $comment_action;
+94 -61
webroot/rsrc/js/application/transactions/behavior-comment-actions.js
··· 43 43 return null; 44 44 } 45 45 46 - function add_row(option) { 47 - var action = action_map[option.value]; 48 - if (!action) { 49 - return; 46 + function remove_action(key) { 47 + var row = rows[key]; 48 + if (row) { 49 + JX.DOM.remove(row.node); 50 + row.option.disabled = false; 51 + delete rows[key]; 50 52 } 51 - 52 - option.disabled = true; 53 - 54 - var icon = new JX.PHUIXIconView() 55 - .setIcon('fa-times-circle'); 56 - var remove = JX.$N('a', {href: '#'}, icon.getNode()); 57 - 58 - var control = new JX.PHUIXFormControl() 59 - .setLabel(action.label) 60 - .setError(remove) 61 - .setControl(action.type, action.spec) 62 - .setClass('phui-comment-action'); 63 - var node = control.getNode(); 64 - 65 - JX.Stratcom.addSigil(node, 'touchable'); 66 - 67 - var remove_action = function() { 68 - JX.DOM.remove(node); 69 - delete rows[action.key]; 70 - option.disabled = false; 71 - }; 72 - 73 - JX.DOM.listen(node, 'gesture.swipe.end', null, function(e) { 74 - var data = e.getData(); 75 - 76 - if (data.direction != 'left') { 77 - // Didn't swipe left. 78 - return; 79 - } 80 - 81 - if (data.length <= (JX.Vector.getDim(node).x / 2)) { 82 - // Didn't swipe far enough. 83 - return; 84 - } 85 - 86 - remove_action(); 87 - }); 88 - 89 - rows[action.key] = control; 90 - 91 - JX.DOM.listen(remove, 'click', null, function(e) { 92 - e.kill(); 93 - remove_action(); 94 - }); 95 - 96 - place_node.parentNode.insertBefore(node, place_node); 97 - 98 - return control; 99 53 } 100 54 101 55 function serialize_actions() { ··· 104 58 for (var k in rows) { 105 59 data.push({ 106 60 type: k, 107 - value: rows[k].getValue(), 61 + value: rows[k].control.getValue(), 108 62 initialValue: action_map[k].initialValue || null 109 63 }); 110 64 } ··· 171 125 } 172 126 } 173 127 128 + function force_preview() { 129 + if (!config.shouldPreview) { 130 + return; 131 + } 132 + 133 + new JX.Request(config.actionURI, onresponse) 134 + .setData(get_data()) 135 + .send(); 136 + } 137 + 138 + function add_row(option) { 139 + var action = action_map[option.value]; 140 + if (!action) { 141 + return; 142 + } 143 + 144 + // Remove any conflicting actions. For example, "Accept Revision" conflicts 145 + // with "Reject Revision". 146 + var conflict_key = action.conflictKey || null; 147 + if (conflict_key !== null) { 148 + for (var k in action_map) { 149 + if (k === action) { 150 + continue; 151 + } 152 + if (action_map[k].conflictKey !== conflict_key) { 153 + continue; 154 + } 155 + 156 + if (!(k in rows)) { 157 + continue; 158 + } 159 + 160 + remove_action(k); 161 + } 162 + } 163 + 164 + option.disabled = true; 165 + 166 + var icon = new JX.PHUIXIconView() 167 + .setIcon('fa-times-circle'); 168 + var remove = JX.$N('a', {href: '#'}, icon.getNode()); 169 + 170 + var control = new JX.PHUIXFormControl() 171 + .setLabel(action.label) 172 + .setError(remove) 173 + .setControl(action.type, action.spec) 174 + .setClass('phui-comment-action'); 175 + var node = control.getNode(); 176 + 177 + JX.Stratcom.addSigil(node, 'touchable'); 178 + 179 + JX.DOM.listen(node, 'gesture.swipe.end', null, function(e) { 180 + var data = e.getData(); 181 + 182 + if (data.direction != 'left') { 183 + // Didn't swipe left. 184 + return; 185 + } 186 + 187 + if (data.length <= (JX.Vector.getDim(node).x / 2)) { 188 + // Didn't swipe far enough. 189 + return; 190 + } 191 + 192 + remove_action(action); 193 + }); 194 + 195 + rows[action.key] = { 196 + control: control, 197 + node: node, 198 + option: option 199 + }; 200 + 201 + JX.DOM.listen(remove, 'click', null, function(e) { 202 + e.kill(); 203 + remove_action(action); 204 + }); 205 + 206 + place_node.parentNode.insertBefore(node, place_node); 207 + 208 + force_preview(); 209 + 210 + return control; 211 + } 212 + 174 213 JX.DOM.listen(form_node, ['submit', 'didSyntheticSubmit'], null, function() { 175 214 input_node.value = serialize_actions(); 176 215 }); ··· 185 224 186 225 JX.DOM.listen(form_node, 'keydown', null, trigger); 187 226 188 - var always_trigger = function() { 189 - new JX.Request(config.actionURI, onresponse) 190 - .setData(get_data()) 191 - .send(); 192 - }; 193 - 194 - JX.DOM.listen(form_node, 'shouldRefresh', null, always_trigger); 227 + JX.DOM.listen(form_node, 'shouldRefresh', null, force_preview); 195 228 request.start(); 196 229 197 230 var old_device = JX.Device.getDevice(); ··· 206 239 // Force an immediate refresh if we switched from another device type 207 240 // to desktop. 208 241 if (old_device != new_device) { 209 - always_trigger(); 242 + force_preview(); 210 243 } 211 244 } else { 212 245 // On mobile, don't show live previews and only save drafts every