@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.

When deleting inline comments, offer "undo" instead of prompting

Summary:
Ref T2009. Ref T1460.

Fixes T2618. When users hit "Delete" on inline comments, delete immediately and offer them "Undo". If they delete indirectly (e.g., by clicking "Delete" from the preview at the bottom of the page), we still prompt them, because the "Undo" action either won't be available or may not be easy to find. This is a "refdelete".

Fixes T6464. This was just a mess. Make it not as much of a mess. It should work now. Pretty sure.

Fixes T4999. We did not refresh these links often enough to find targets for them, so they could race with content. Reevaluate them after loading new changes.

Test Plan:
- Deleted and undid deletion of inlines from main view and preview.
- Clicked "View" on inlines.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T6464, T4999, T2618, T1460, T2009

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

+219 -130
+43 -43
resources/celerity/map.php
··· 11 11 'core.pkg.js' => '5a1c336d', 12 12 'darkconsole.pkg.js' => '8ab24e01', 13 13 'differential.pkg.css' => '1940be3f', 14 - 'differential.pkg.js' => '2b14c4a1', 14 + 'differential.pkg.js' => 'e62fe1cf', 15 15 'diffusion.pkg.css' => '591664fa', 16 16 'diffusion.pkg.js' => 'bfc0737b', 17 17 'maniphest.pkg.css' => '68d4dd3d', ··· 360 360 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '82439934', 361 361 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '453c5375', 362 362 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'd4eecc63', 363 - 'rsrc/js/application/differential/ChangesetViewManager.js' => 'a9af1212', 364 - 'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'd3aa4b40', 363 + 'rsrc/js/application/differential/ChangesetViewManager.js' => '88be0133', 364 + 'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => '1b772f31', 365 365 'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => 'e10f8e18', 366 366 'rsrc/js/application/differential/behavior-comment-jump.js' => '4fdb476d', 367 - 'rsrc/js/application/differential/behavior-comment-preview.js' => '6932def3', 367 + 'rsrc/js/application/differential/behavior-comment-preview.js' => '8e1389b5', 368 368 'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1', 369 369 'rsrc/js/application/differential/behavior-dropdown-menus.js' => '2035b9cb', 370 - 'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '7378d48a', 370 + 'rsrc/js/application/differential/behavior-edit-inline-comments.js' => 'a48aa699', 371 371 'rsrc/js/application/differential/behavior-keyboard-nav.js' => '2c426492', 372 372 'rsrc/js/application/differential/behavior-populate.js' => '8694b1df', 373 373 'rsrc/js/application/differential/behavior-show-field-details.js' => 'bba9eedf', ··· 509 509 'aphront-two-column-view-css' => '16ab3ad2', 510 510 'aphront-typeahead-control-css' => '0e403212', 511 511 'auth-css' => '1e655982', 512 - 'changeset-view-manager' => 'a9af1212', 512 + 'changeset-view-manager' => '88be0133', 513 513 'config-options-css' => '7fedf08b', 514 514 'config-welcome-css' => '6abd79be', 515 515 'conpherence-durable-column-view' => '9207426d', ··· 520 520 'conpherence-widget-pane-css' => '3d575438', 521 521 'differential-changeset-view-css' => '6a8b172a', 522 522 'differential-core-view-css' => '7ac3cabc', 523 - 'differential-inline-comment-editor' => 'd3aa4b40', 523 + 'differential-inline-comment-editor' => '1b772f31', 524 524 'differential-results-table-css' => '181aa9d9', 525 525 'differential-revision-add-comment-css' => 'c478bcaa', 526 526 'differential-revision-comment-css' => '48186045', ··· 569 569 'javelin-behavior-differential-comment-jump' => '4fdb476d', 570 570 'javelin-behavior-differential-diff-radios' => 'e1ff79b1', 571 571 'javelin-behavior-differential-dropdown-menus' => '2035b9cb', 572 - 'javelin-behavior-differential-edit-inline-comments' => '7378d48a', 573 - 'javelin-behavior-differential-feedback-preview' => '6932def3', 572 + 'javelin-behavior-differential-edit-inline-comments' => 'a48aa699', 573 + 'javelin-behavior-differential-feedback-preview' => '8e1389b5', 574 574 'javelin-behavior-differential-keyboard-navigation' => '2c426492', 575 575 'javelin-behavior-differential-populate' => '8694b1df', 576 576 'javelin-behavior-differential-show-field-details' => 'bba9eedf', ··· 931 931 'javelin-util', 932 932 'phabricator-keyboard-shortcut-manager', 933 933 ), 934 + '1b772f31' => array( 935 + 'javelin-dom', 936 + 'javelin-util', 937 + 'javelin-stratcom', 938 + 'javelin-install', 939 + 'javelin-request', 940 + 'javelin-workflow', 941 + ), 934 942 '1d298e3a' => array( 935 943 'javelin-install', 936 944 'javelin-util', ··· 1227 1235 '6882e80a' => array( 1228 1236 'javelin-dom', 1229 1237 ), 1230 - '6932def3' => array( 1231 - 'javelin-behavior', 1232 - 'javelin-stratcom', 1233 - 'javelin-dom', 1234 - 'javelin-request', 1235 - 'javelin-util', 1236 - 'phabricator-shaped-request', 1237 - ), 1238 1238 '69adf288' => array( 1239 1239 'javelin-install', 1240 1240 ), ··· 1315 1315 '7319e029' => array( 1316 1316 'javelin-behavior', 1317 1317 'javelin-dom', 1318 - ), 1319 - '7378d48a' => array( 1320 - 'javelin-behavior', 1321 - 'javelin-stratcom', 1322 - 'javelin-dom', 1323 - 'javelin-util', 1324 - 'javelin-vector', 1325 - 'differential-inline-comment-editor', 1326 1318 ), 1327 1319 '73d09eef' => array( 1328 1320 'javelin-behavior', ··· 1500 1492 'javelin-stratcom', 1501 1493 'javelin-dom', 1502 1494 ), 1495 + '88be0133' => array( 1496 + 'javelin-dom', 1497 + 'javelin-util', 1498 + 'javelin-stratcom', 1499 + 'javelin-install', 1500 + 'javelin-workflow', 1501 + 'javelin-router', 1502 + 'javelin-behavior-device', 1503 + 'javelin-vector', 1504 + ), 1503 1505 '88f0c5b3' => array( 1504 1506 'javelin-behavior', 1505 1507 'javelin-dom', ··· 1527 1529 'phabricator-notification', 1528 1530 'javelin-stratcom', 1529 1531 'javelin-behavior', 1532 + ), 1533 + '8e1389b5' => array( 1534 + 'javelin-behavior', 1535 + 'javelin-stratcom', 1536 + 'javelin-dom', 1537 + 'javelin-request', 1538 + 'javelin-util', 1539 + 'phabricator-shaped-request', 1530 1540 ), 1531 1541 '8ef9ab58' => array( 1532 1542 'javelin-behavior', ··· 1609 1619 'javelin-vector', 1610 1620 'javelin-magical-init', 1611 1621 ), 1622 + 'a48aa699' => array( 1623 + 'javelin-behavior', 1624 + 'javelin-stratcom', 1625 + 'javelin-dom', 1626 + 'javelin-util', 1627 + 'javelin-vector', 1628 + 'differential-inline-comment-editor', 1629 + ), 1612 1630 'a4ae61bf' => array( 1613 1631 'javelin-install', 1614 1632 'javelin-dom', ··· 1628 1646 'javelin-behavior', 1629 1647 'javelin-uri', 1630 1648 'phabricator-keyboard-shortcut', 1631 - ), 1632 - 'a9af1212' => array( 1633 - 'javelin-dom', 1634 - 'javelin-util', 1635 - 'javelin-stratcom', 1636 - 'javelin-install', 1637 - 'javelin-workflow', 1638 - 'javelin-router', 1639 - 'javelin-behavior-device', 1640 - 'javelin-vector', 1641 1649 ), 1642 1650 'a9f88de2' => array( 1643 1651 'javelin-behavior', ··· 1755 1763 ), 1756 1764 'd254d646' => array( 1757 1765 'javelin-util', 1758 - ), 1759 - 'd3aa4b40' => array( 1760 - 'javelin-dom', 1761 - 'javelin-util', 1762 - 'javelin-stratcom', 1763 - 'javelin-install', 1764 - 'javelin-request', 1765 - 'javelin-workflow', 1766 1766 ), 1767 1767 'd4a14807' => array( 1768 1768 'javelin-install',
+9
src/applications/audit/storage/PhabricatorAuditInlineComment.php
··· 290 290 return $this->proxy->getHasReplies(); 291 291 } 292 292 293 + public function setIsDeleted($is_deleted) { 294 + $this->proxy->setIsDeleted($is_deleted); 295 + return $this; 296 + } 297 + 298 + public function getIsDeleted() { 299 + return $this->proxy->getIsDeleted(); 300 + } 301 + 293 302 294 303 /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ 295 304
+4 -3
src/applications/differential/query/DifferentialInlineCommentQuery.php
··· 129 129 $where[] = qsprintf( 130 130 $conn_r, 131 131 'changesetID IN (%Ld) AND 132 - (authorPHID = %s OR transactionPHID IS NOT NULL)', 132 + ((authorPHID = %s AND isDeleted = 0) OR transactionPHID IS NOT NULL)', 133 133 $ids, 134 134 $phid); 135 135 } ··· 151 151 152 152 $where[] = qsprintf( 153 153 $conn_r, 154 - 'authorPHID = %s AND revisionPHID = %s AND transactionPHID IS NULL', 154 + 'authorPHID = %s AND revisionPHID = %s AND transactionPHID IS NULL 155 + AND isDeleted = 0', 155 156 $phid, 156 157 $rev_phid); 157 158 } ··· 159 160 if ($this->draftsByAuthors) { 160 161 $where[] = qsprintf( 161 162 $conn_r, 162 - 'authorPHID IN (%Ls) AND transactionPHID IS NULL', 163 + 'authorPHID IN (%Ls) AND isDeleted = 0 AND transactionPHID IS NULL', 163 164 $this->draftsByAuthors); 164 165 } 165 166
+9
src/applications/differential/storage/DifferentialInlineComment.php
··· 207 207 return $this->proxy->getHasReplies(); 208 208 } 209 209 210 + public function setIsDeleted($is_deleted) { 211 + $this->proxy->setIsDeleted($is_deleted); 212 + return $this; 213 + } 214 + 215 + public function getIsDeleted() { 216 + return $this->proxy->getIsDeleted(); 217 + } 218 + 210 219 211 220 /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ 212 221
+26 -18
src/infrastructure/diff/PhabricatorInlineCommentController.php
··· 79 79 80 80 $this->readRequestParameters(); 81 81 82 - switch ($this->getOperation()) { 82 + $op = $this->getOperation(); 83 + switch ($op) { 83 84 case 'delete': 84 - $inline = $this->loadCommentForEdit($this->getCommentID()); 85 - 86 - if ($request->isFormPost()) { 87 - $this->deleteComment($inline); 88 - return $this->buildEmptyResponse(); 85 + case 'undelete': 86 + case 'refdelete': 87 + if (!$request->validateCSRF()) { 88 + return new Aphront404Response(); 89 89 } 90 90 91 - $dialog = new AphrontDialogView(); 92 - $dialog->setUser($user); 93 - $dialog->setSubmitURI($request->getRequestURI()); 91 + // NOTE: For normal deletes, we just process the delete immediately 92 + // and show an "Undo" action. For deletes by reference from the 93 + // preview ("refdelete"), we prompt first (because the "Undo" may 94 + // not draw, or may not be easy to locate). 94 95 95 - $dialog->setTitle(pht('Really delete this comment?')); 96 - $dialog->addHiddenInput('id', $this->getCommentID()); 97 - $dialog->addHiddenInput('op', 'delete'); 98 - $dialog->appendChild( 99 - phutil_tag('p', array(), pht('Delete this inline comment?'))); 96 + if ($op == 'refdelete') { 97 + if (!$request->isFormPost()) { 98 + return $this->newDialog() 99 + ->setTitle(pht('Really delete comment?')) 100 + ->addHiddenInput('id', $this->getCommentID()) 101 + ->addHiddenInput('op', $op) 102 + ->appendParagraph(pht('Delete this inline comment?')) 103 + ->addCancelButton('#') 104 + ->addSubmitButton(pht('Delete')); 105 + } 106 + } 100 107 101 - $dialog->addCancelButton('#'); 102 - $dialog->addSubmitButton(pht('Delete')); 108 + $is_delete = ($op == 'delete' || $op == 'refdelete'); 109 + 110 + $inline = $this->loadCommentForEdit($this->getCommentID()); 111 + $inline->setIsDeleted((int)$is_delete)->save(); 103 112 104 - return id(new AphrontDialogResponse())->setDialog($dialog); 113 + return $this->buildEmptyResponse(); 105 114 case 'edit': 106 115 $inline = $this->loadCommentForEdit($this->getCommentID()); 107 - 108 116 $text = $this->getCommentText(); 109 117 110 118 if ($request->isFormPost()) {
+3
src/infrastructure/diff/interface/PhabricatorInlineCommentInterface.php
··· 25 25 public function setHasReplies($has_replies); 26 26 public function getHasReplies(); 27 27 28 + public function setIsDeleted($deleted); 29 + public function getIsDeleted(); 30 + 28 31 public function setContent($content); 29 32 public function getContent(); 30 33
+2
webroot/rsrc/js/application/differential/ChangesetViewManager.js
··· 359 359 if (response.undoTemplates) { 360 360 this._undoTemplates = response.undoTemplates; 361 361 } 362 + 363 + JX.Stratcom.invoke('differential-inline-comment-refresh'); 362 364 }, 363 365 364 366 _getContentFrame: function() {
+57 -24
webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js
··· 19 19 members : { 20 20 _uri : null, 21 21 _undoText : null, 22 + _completed: false, 22 23 _skipOverInlineCommentRows : function(node) { 23 24 // TODO: Move this semantic information out of class names. 24 25 while (node && node.className.indexOf('inline') !== -1) { ··· 73 74 JX.DOM.remove(rows[ii]); 74 75 } 75 76 } 77 + JX.DifferentialInlineCommentEditor._undoRows = []; 76 78 }, 77 79 _undo : function() { 78 80 this._removeUndoLink(); 79 81 80 - this.setText(this._undoText); 82 + if (this._undoText) { 83 + this.setText(this._undoText); 84 + } else { 85 + this.setOperation('undelete'); 86 + } 87 + 81 88 this.start(); 82 89 }, 83 90 _registerUndoListener : function() { ··· 146 153 'inline-edit-form', 147 154 onsubmit); 148 155 }, 156 + 157 + 149 158 _didCompleteWorkflow : function(response) { 150 159 var op = this.getOperation(); 151 160 161 + if (op == 'delete' || op == 'refdelete') { 162 + this._undoText = null; 163 + this._drawUndo(); 164 + } else { 165 + this._removeUndoLink(); 166 + } 167 + 152 168 // We don't get any markup back if the user deletes a comment, or saves 153 169 // an empty comment (which effects a delete). 154 170 if (response.markup) { ··· 156 172 } 157 173 158 174 // These operations remove the old row (edit adds a new row first). 159 - var remove_old = (op == 'edit' || op == 'delete'); 175 + var remove_old = (op == 'edit' || op == 'delete' || op == 'refdelete'); 160 176 if (remove_old) { 161 - JX.DOM.remove(this.getRow()); 162 - var other_rows = this.getOtherRows(); 163 - for(var i = 0; i < other_rows.length; ++i) { 164 - JX.DOM.remove(other_rows[i]); 165 - } 177 + this._setRowState('hidden'); 166 178 } 167 179 168 - // Once the user saves something, get rid of the 'undo' option. A 169 - // particular case where we need this is saving a delete, when we might 170 - // otherwise leave around an 'undo' for an earlier edit to the same 171 - // comment. 172 - this._removeUndoLink(); 180 + if (op == 'undelete') { 181 + this._setRowState('visible'); 182 + } 183 + 184 + this._completed = true; 173 185 174 186 JX.Stratcom.invoke('differential-inline-comment-update'); 175 187 this.invoke('done'); 176 188 }, 189 + 190 + 177 191 _didCancelWorkflow : function() { 178 192 this.invoke('done'); 179 193 180 - var op = this.getOperation(); 181 - if (op == 'delete') { 182 - // No undo for delete, we prompt the user explicitly. 183 - return; 194 + switch (this.getOperation()) { 195 + case 'delete': 196 + case 'refdelete': 197 + if (!this._completed) { 198 + this._setRowState('visible'); 199 + } 200 + return; 201 + case 'undelete': 202 + return; 184 203 } 185 204 186 205 var textarea; ··· 209 228 // Save the text so we can 'undo' back to it. 210 229 this._undoText = text; 211 230 231 + this._drawUndo(); 232 + }, 233 + 234 + _drawUndo: function() { 212 235 var templates = this.getTemplates(); 213 236 var template = this.getOnRight() ? templates.r : templates.l; 214 237 template = JX.$H(template).getNode(); ··· 231 254 this._registerUndoListener(); 232 255 233 256 var data = this._buildRequestData(); 234 - 235 257 var op = this.getOperation(); 236 258 237 259 238 - if (op == 'delete') { 260 + if (op == 'delete' || op == 'refdelete' || op == 'undelete') { 239 261 this._setRowState('loading'); 262 + 240 263 var oncomplete = JX.bind(this, this._didCompleteWorkflow); 241 - var onclose = JX.bind(this, function() { 242 - this._setRowState('visible'); 243 - this._didCancelWorkflow(); 244 - }); 264 + var oncancel = JX.bind(this, this._didCancelWorkflow); 245 265 246 266 new JX.Workflow(this._uri, data) 247 267 .setHandler(oncomplete) 248 - .setCloseHandler(onclose) 268 + .setCloseHandler(oncancel) 249 269 .start(); 250 270 } else { 251 271 var handler = JX.bind(this, this._didContinueWorkflow); ··· 260 280 } 261 281 262 282 return this; 283 + }, 284 + 285 + deleteByID: function(id) { 286 + var data = { 287 + op: 'refdelete', 288 + changesetID: id 289 + }; 290 + 291 + new JX.Workflow(this._uri, data) 292 + .setHandler(function() { 293 + JX.Stratcom.invoke('differential-inline-comment-update'); 294 + }) 295 + .start(); 263 296 } 297 + 264 298 }, 265 299 266 300 statics : { ··· 280 314 properties : { 281 315 operation : null, 282 316 row : null, 283 - otherRows: [], 284 317 table : null, 285 318 onRight : null, 286 319 ID : null,
+34 -25
webroot/rsrc/js/application/differential/behavior-comment-preview.js
··· 49 49 50 50 request.start(); 51 51 52 - 53 52 function refreshInlinePreview() { 54 53 new JX.Request(config.inlineuri, function(r) { 55 - var inline = JX.$(config.inline); 54 + var inline = JX.$(config.inline); 56 55 57 - JX.DOM.setContent(inline, JX.$H(r)); 58 - JX.Stratcom.invoke('differential-preview-update', null, { 59 - container: inline 60 - }); 56 + JX.DOM.setContent(inline, JX.$H(r)); 57 + JX.Stratcom.invoke('differential-preview-update', null, { 58 + container: inline 59 + }); 61 60 62 - // Go through the previews and activate any "View" links where the 63 - // actual comment appears in the document. 61 + updateLinks(); 62 + }) 63 + .setTimeout(5000) 64 + .send(); 65 + } 64 66 65 - var links = JX.DOM.scry( 66 - inline, 67 - 'a', 68 - 'differential-inline-preview-jump'); 69 - for (var ii = 0; ii < links.length; ii++) { 70 - var data = JX.Stratcom.getData(links[ii]); 71 - try { 72 - JX.$(data.anchor); 73 - links[ii].href = '#' + data.anchor; 74 - JX.DOM.setContent(links[ii], 'View'); 75 - } catch (ignored) { 76 - // This inline comment isn't visible, e.g. on some other diff. 77 - } 78 - } 79 - }) 80 - .setTimeout(5000) 81 - .send(); 67 + function updateLinks() { 68 + var inline = JX.$(config.inline); 69 + 70 + var links = JX.DOM.scry( 71 + inline, 72 + 'a', 73 + 'differential-inline-preview-jump'); 74 + 75 + for (var ii = 0; ii < links.length; ii++) { 76 + var data = JX.Stratcom.getData(links[ii]); 77 + try { 78 + JX.$(data.anchor); 79 + links[ii].href = '#' + data.anchor; 80 + JX.DOM.setContent(links[ii], 'View'); 81 + } catch (ignored) { 82 + // This inline comment isn't visible, e.g. on some other diff. 83 + } 84 + } 82 85 } 86 + 83 87 84 88 JX.Stratcom.listen( 85 89 'differential-inline-comment-update', 86 90 null, 87 91 refreshInlinePreview); 92 + 93 + JX.Stratcom.listen( 94 + 'differential-inline-comment-refresh', 95 + null, 96 + updateLinks); 88 97 89 98 refreshInlinePreview(); 90 99 });
+32 -17
webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js
··· 298 298 299 299 var handle_inline_action = function(node, op) { 300 300 var data = JX.Stratcom.getData(node); 301 - var row = node.parentNode.parentNode; 302 - var other_rows = []; 301 + 302 + // If you click an action in the preview at the bottom of the page, we 303 + // find the corresponding node and simulate clicking that, if it's 304 + // present on the page. This gives the editor a more consistent view 305 + // of the document. 303 306 if (JX.Stratcom.hasSigil(node, 'differential-inline-comment-preview')) { 304 - // The DOM structure around the comment is different if it's part of the 305 - // preview, so make sure not to pass the wrong container. 306 - row = node; 307 - if (op === 'delete') { 308 - // Furthermore, deleting a comment in the preview does not automatically 309 - // delete other occurrences of the same comment, so do that manually. 310 - var nodes = JX.DOM.scry( 311 - JX.DOM.getContentFrame(), 312 - 'div', 313 - 'differential-inline-comment'); 314 - for (var i = 0; i < nodes.length; ++i) { 315 - if (JX.Stratcom.getData(nodes[i]).id === data.id) { 316 - other_rows.push(nodes[i]); 317 - } 307 + var nodes = JX.DOM.scry( 308 + JX.DOM.getContentFrame(), 309 + 'div', 310 + 'differential-inline-comment'); 311 + 312 + var found = false; 313 + var node_data; 314 + for (var ii = 0; ii < nodes.length; ++ii) { 315 + if (nodes[ii] == node) { 316 + // Don't match the preview itself. 317 + continue; 318 + } 319 + node_data = JX.Stratcom.getData(nodes[ii]); 320 + if (node_data.id == data.id) { 321 + node = nodes[ii]; 322 + data = node_data; 323 + found = true; 324 + break; 318 325 } 319 326 } 327 + 328 + if (!found) { 329 + new JX.DifferentialInlineCommentEditor(config.uri) 330 + .deleteByID(data.id); 331 + return; 332 + } 333 + 334 + op = 'refdelete'; 320 335 } 321 336 322 337 var original = data.original; ··· 328 343 reply_phid = data.phid; 329 344 } 330 345 346 + var row = JX.DOM.findAbove(node, 'tr'); 331 347 var changeset_root = JX.DOM.findAbove( 332 348 node, 333 349 'div', ··· 344 360 .setOnRight(data.on_right) 345 361 .setOriginalText(original) 346 362 .setRow(row) 347 - .setOtherRows(other_rows) 348 363 .setTable(row.parentNode) 349 364 .setReplyToCommentPHID(reply_phid) 350 365 .setRenderer(view.getRenderer())