1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-25 08:12:40 +01:00

Update client logic for inline comment "Save" and "Cancel" actions

Summary: Ref T13559. Substantially correct the client logic for "Save" and "Cancel" actions to handle unusual cases.

Test Plan:
Quoting behavior:

  - Quoted a comment.
  - Cancelled the quoted comment without modifying anything.
  - Reloaded page.
    - Before changes: quoted comment still exists.
    - After changes: quoted comment is deleted.
  - Looked at comment count in header, saw consistent behavior (before: weird behavior).

Empty suggestion behavior:

  - Created a new comment on a suggestable file.
  - Clicked "Suggest Edit" to enable suggestions.
  - Without making any text or suggestion changes, clicked "Save".
    - Before changes: comment saves, but is empty.
    - After changes: comment deletes itself without undo.

General behavior:

  - Created and saved an empty comment (deletes itself).
  - Created and saved a nonempty comment (saves as draft).
  - Created and saved an empty comment with an edit suggestion (saves).
  - Created and saved an empty comment with a suggestion to entirely delete lines -- that is, no suggestion text (saves).
  - Edited a comment, saved without changes (save).
  - Edited a comment, save deleting all text (saves -- note that this is intentionally without undo, since this is a lot of steps to do by accident).
  - Cancel editing an unchanged comment (cancels without undo).
  - Cancel editing a changed comment (cancels with undo).
    - Undo'd, got text back.
  - Cancel new comment with no text (deletes without undo).
  - Cancel new comment with text (deletes with undo).
    - Undo'd, got text back.
  - Saved a quoted comment with no changes (saves -- note that this is intentionally not a "delete", since just quoting someone seems fine if you click "Save" -- maybe you want to come back to it later).

Maniphest Tasks: T13559

Differential Revision: https://secure.phabricator.com/D21654
This commit is contained in:
epriestley 2021-03-25 13:28:04 -07:00
parent 6fd55d692f
commit 1308a5555f
4 changed files with 179 additions and 104 deletions

View file

@ -13,7 +13,7 @@ return array(
'core.pkg.js' => '68f29322', 'core.pkg.js' => '68f29322',
'dark-console.pkg.js' => '187792c2', 'dark-console.pkg.js' => '187792c2',
'differential.pkg.css' => 'ffb69e3d', 'differential.pkg.css' => 'ffb69e3d',
'differential.pkg.js' => '59453886', 'differential.pkg.js' => '8deec4cd',
'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.css' => '42c75c37',
'diffusion.pkg.js' => '78c9885d', 'diffusion.pkg.js' => '78c9885d',
'maniphest.pkg.css' => '35995d6d', 'maniphest.pkg.css' => '35995d6d',
@ -385,8 +385,8 @@ return array(
'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '0116d3e8', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '0116d3e8',
'rsrc/js/application/diff/DiffChangeset.js' => 'd7d3ba75', 'rsrc/js/application/diff/DiffChangeset.js' => 'd7d3ba75',
'rsrc/js/application/diff/DiffChangesetList.js' => 'cc2c5de5', 'rsrc/js/application/diff/DiffChangesetList.js' => 'cc2c5de5',
'rsrc/js/application/diff/DiffInline.js' => '26664c24', 'rsrc/js/application/diff/DiffInline.js' => '9c775532',
'rsrc/js/application/diff/DiffInlineContentState.js' => '68e6339d', 'rsrc/js/application/diff/DiffInlineContentState.js' => 'aa51efb4',
'rsrc/js/application/diff/DiffPathView.js' => '8207abf9', 'rsrc/js/application/diff/DiffPathView.js' => '8207abf9',
'rsrc/js/application/diff/DiffTreeView.js' => '5d83623b', 'rsrc/js/application/diff/DiffTreeView.js' => '5d83623b',
'rsrc/js/application/differential/behavior-diff-radios.js' => '925fe8cd', 'rsrc/js/application/differential/behavior-diff-radios.js' => '925fe8cd',
@ -788,8 +788,8 @@ return array(
'phabricator-dashboard-css' => '5a205b9d', 'phabricator-dashboard-css' => '5a205b9d',
'phabricator-diff-changeset' => 'd7d3ba75', 'phabricator-diff-changeset' => 'd7d3ba75',
'phabricator-diff-changeset-list' => 'cc2c5de5', 'phabricator-diff-changeset-list' => 'cc2c5de5',
'phabricator-diff-inline' => '26664c24', 'phabricator-diff-inline' => '9c775532',
'phabricator-diff-inline-content-state' => '68e6339d', 'phabricator-diff-inline-content-state' => 'aa51efb4',
'phabricator-diff-path-view' => '8207abf9', 'phabricator-diff-path-view' => '8207abf9',
'phabricator-diff-tree-view' => '5d83623b', 'phabricator-diff-tree-view' => '5d83623b',
'phabricator-drag-and-drop-file-upload' => '4370900d', 'phabricator-drag-and-drop-file-upload' => '4370900d',
@ -1162,10 +1162,6 @@ return array(
'javelin-json', 'javelin-json',
'phabricator-prefab', 'phabricator-prefab',
), ),
'26664c24' => array(
'javelin-dom',
'phabricator-diff-inline-content-state',
),
'289bf236' => array( '289bf236' => array(
'javelin-install', 'javelin-install',
'javelin-util', 'javelin-util',
@ -1549,9 +1545,6 @@ return array(
'javelin-install', 'javelin-install',
'javelin-dom', 'javelin-dom',
), ),
'68e6339d' => array(
'javelin-dom',
),
'6a1583a8' => array( '6a1583a8' => array(
'javelin-behavior', 'javelin-behavior',
'javelin-history', 'javelin-history',
@ -1823,6 +1816,10 @@ return array(
'javelin-dom', 'javelin-dom',
'javelin-workflow', 'javelin-workflow',
), ),
'9c775532' => array(
'javelin-dom',
'phabricator-diff-inline-content-state',
),
'9cec214e' => array( '9cec214e' => array(
'javelin-behavior', 'javelin-behavior',
'javelin-stratcom', 'javelin-stratcom',
@ -1913,6 +1910,9 @@ return array(
'javelin-typeahead-ondemand-source', 'javelin-typeahead-ondemand-source',
'javelin-dom', 'javelin-dom',
), ),
'aa51efb4' => array(
'javelin-dom',
),
'aa6d2308' => array( 'aa6d2308' => array(
'javelin-behavior', 'javelin-behavior',
'javelin-dom', 'javelin-dom',

View file

@ -172,7 +172,9 @@ abstract class PhabricatorInlineCommentController
$inline = $this->loadCommentByIDForEdit($this->getCommentID()); $inline = $this->loadCommentByIDForEdit($this->getCommentID());
if ($is_delete) { if ($is_delete) {
$inline->setIsDeleted(1); $inline
->setIsEditing(false)
->setIsDeleted(1);
} else { } else {
$inline->setIsDeleted(0); $inline->setIsDeleted(0);
} }

View file

@ -44,7 +44,6 @@ JX.install('DiffInline', {
_undoRow: null, _undoRow: null,
_undoType: null, _undoType: null,
_undoState: null, _undoState: null,
_preventUndo: false,
_draftRequest: null, _draftRequest: null,
_skipFocus: false, _skipFocus: false,
@ -161,14 +160,6 @@ JX.install('DiffInline', {
return this._endOffset; return this._endOffset;
}, },
_setPreventUndo: function(prevent_undo) {
this._preventUndo = prevent_undo;
},
_getPreventUndo: function() {
return this._preventUndo;
},
setIsSelected: function(is_selected) { setIsSelected: function(is_selected) {
this._isSelected = is_selected; this._isSelected = is_selected;
@ -452,21 +443,12 @@ JX.install('DiffInline', {
this._undoText = null; this._undoText = null;
} }
var uri = this._getInlineURI(); this._applyEdit(content_state);
var handler = JX.bind(this, this._oneditresponse);
var data = this._newRequestData('edit', content_state);
this.setLoading(true);
new JX.Request(uri, handler)
.setData(data)
.send();
}, },
delete: function(is_ref) { delete: function(is_ref) {
var uri = this._getInlineURI(); var uri = this._getInlineURI();
var handler = JX.bind(this, this._ondeleteresponse); var handler = JX.bind(this, this._ondeleteresponse, false);
// NOTE: This may be a direct delete (the user clicked on the inline // NOTE: This may be a direct delete (the user clicked on the inline
// itself) or a "refdelete" (the user clicked somewhere else, like the // itself) or a "refdelete" (the user clicked somewhere else, like the
@ -586,7 +568,6 @@ JX.install('DiffInline', {
this._readInlineState(response.inline); this._readInlineState(response.inline);
this._drawEditRows(rows); this._drawEditRows(rows);
this.setLoading(false);
this.setInvisible(true); this.setInvisible(true);
}, },
@ -617,24 +598,24 @@ JX.install('DiffInline', {
return new JX.DiffInlineContentState().readWireFormat(map); return new JX.DiffInlineContentState().readWireFormat(map);
}, },
_ondeleteresponse: function() { _ondeleteresponse: function(prevent_undo) {
// If there's an existing "unedit" undo element, remove it. if (!prevent_undo) {
if (this._undoRow) { // If there's an existing "unedit" undo element, remove it.
JX.DOM.remove(this._undoRow); if (this._undoRow) {
this._undoRow = null; JX.DOM.remove(this._undoRow);
} this._undoRow = null;
}
// If there's an existing editor, remove it. This happens when you // If there's an existing editor, remove it. This happens when you
// delete a comment from the comment preview area. In this case, we // delete a comment from the comment preview area. In this case, we
// read and preserve the text so "Undo" restores it. // read and preserve the text so "Undo" restores it.
var state = null; var state = null;
if (this._editRow) { if (this._editRow) {
state = this._getActiveContentState().getWireFormat(); state = this._getActiveContentState().getWireFormat();
JX.DOM.remove(this._editRow); JX.DOM.remove(this._editRow);
this._editRow = null; this._editRow = null;
} }
if (!this._getPreventUndo()) {
this._drawUndeleteRows(state); this._drawUndeleteRows(state);
} }
@ -693,7 +674,6 @@ JX.install('DiffInline', {
var anchor = cursor || this._row; var anchor = cursor || this._row;
cursor = cursor || this._row.nextSibling; cursor = cursor || this._row.nextSibling;
var result_row; var result_row;
var next_row; var next_row;
while (row) { while (row) {
@ -837,8 +817,10 @@ JX.install('DiffInline', {
save: function() { save: function() {
if (this._shouldDeleteOnSave()) { if (this._shouldDeleteOnSave()) {
this._setPreventUndo(true); JX.DOM.remove(this._editRow);
this._applyDelete(); this._editRow = null;
this._applyDelete(true);
return; return;
} }
@ -846,35 +828,56 @@ JX.install('DiffInline', {
}, },
_shouldDeleteOnSave: function() { _shouldDeleteOnSave: function() {
var state = this._getActiveContentState(); var active = this._getActiveContentState();
var initial = this._getInitialContentState();
// TODO: This is greatly simplified because we don't track all the // When a user clicks "Save", it counts as a "delete" if the content
// state we need yet. // of the comment is functionally empty.
return !state.getText().length; // This isn't a delete if there's any text. Even if the text is a
}, // quote (so the state is the same as the initial state), we preserve
// it when the user clicks "Save".
if (!active.isTextEmpty()) {
return false;
}
_shouldDeleteOnCancel: function() { // This isn't a delete if there's a suggestion and that suggestion is
var state = this._getActiveContentState(); // different from the initial state. (This means that an inline which
// purely suggests a block of code should be deleted is non-empty.)
if (active.getHasSuggestion()) {
if (!active.isSuggestionSimilar(initial)) {
return false;
}
}
// TODO: This is greatly simplified, too. // Otherwise, this comment is functionally empty, so we can just treat
// a "Save" as a "delete".
return !state.getText().length; return true;
}, },
_shouldUndoOnCancel: function() { _shouldUndoOnCancel: function() {
var new_state = this._getActiveContentState().getWireFormat(); var committed = this._getCommittedContentState();
var old_state = this._getCommittedContentState().getWireFormat(); var active = this._getActiveContentState();
var initial = this._getInitialContentState();
// TODO: This is also simplified. // When a user clicks "Cancel", we only offer to let them "Undo" the
// action if the undo would be substantive.
var is_empty = this._isVoidContentState(new_state); // The undo is substantive if the text is nonempty, and not similar to
var is_same = this._isSameContentState(new_state, old_state); // the last state.
var versus = committed || initial;
if (!is_empty && !is_same) { if (!active.isTextEmpty() && !active.isTextSimilar(versus)) {
return true; return true;
} }
// The undo is substantive if there's a suggestion, and the suggestion
// is not similar to the last state.
if (active.getHasSuggestion()) {
if (!active.isSuggestionSimilar(versus)) {
return true;
}
}
return false; return false;
}, },
@ -887,8 +890,8 @@ JX.install('DiffInline', {
this._applyCall(handler, data); this._applyCall(handler, data);
}, },
_applyDelete: function() { _applyDelete: function(prevent_undo) {
var handler = JX.bind(this, this._ondeleteresponse); var handler = JX.bind(this, this._ondeleteresponse, prevent_undo);
var data = this._newRequestData('delete'); var data = this._newRequestData('delete');
@ -903,6 +906,14 @@ JX.install('DiffInline', {
this._applyCall(handler, data); this._applyCall(handler, data);
}, },
_applyEdit: function(state) {
var handler = JX.bind(this, this._oneditresponse);
var data = this._newRequestData('edit', state);
this._applyCall(handler, data);
},
_applyCall: function(handler, data) { _applyCall: function(handler, data) {
var uri = this._getInlineURI(); var uri = this._getInlineURI();
@ -946,31 +957,41 @@ JX.install('DiffInline', {
}, },
cancel: function() { cancel: function() {
// NOTE: Read the state before we remove the editor. Otherwise, we might
// miss text the user has entered into the textarea.
var state = this._getActiveContentState().getWireFormat();
JX.DOM.remove(this._editRow); JX.DOM.remove(this._editRow);
this._editRow = null; this._editRow = null;
if (this._shouldDeleteOnCancel()) { // When a user clicks "Cancel", we delete the comment if it has never
this._setPreventUndo(true); // been saved: we don't have a non-empty display state to revert to.
this._applyDelete(); var is_delete = (this._getCommittedContentState() === null);
return;
}
if (this._shouldUndoOnCancel()) { var is_undo = this._shouldUndoOnCancel();
var state = this._getActiveContentState().getWireFormat();
this._drawUneditRows(state);
}
// If you "undo" to restore text ("AB") and then "Cancel", we put you // If you "undo" to restore text ("AB") and then "Cancel", we put you
// back in the original text state ("A"). We also send the original // back in the original text state ("A"). We also send the original
// text ("A") to the server as the current persistent state. // text ("A") to the server as the current persistent state.
this.setEditing(false); if (is_undo) {
this.setInvisible(false); this._drawUneditRows(state);
}
var old_state = this._getCommittedContentState(); if (is_delete) {
this._applyCancel(old_state.getWireFormat()); // NOTE: We're always suppressing the undo from "delete". We want to
// use the "undo" we just added above instead, which will get us
// back to the ephemeral, client-side editor state.
this._applyDelete(true);
} else {
this.setEditing(false);
this.setInvisible(false);
this._didUpdate(true); var old_state = this._getCommittedContentState();
this._applyCancel(old_state.getWireFormat());
this._didUpdate(true);
}
}, },
_onCancelResponse: function(response) { _onCancelResponse: function(response) {
@ -1067,8 +1088,8 @@ JX.install('DiffInline', {
return null; return null;
} }
var state = this._getActiveContentState().getWireFormat(); var state = this._getActiveContentState();
if (this._isVoidContentState(state)) { if (state.isStateEmpty()) {
return null; return null;
} }
@ -1077,7 +1098,7 @@ JX.install('DiffInline', {
id: this.getID(), id: this.getID(),
}; };
JX.copy(draft_data, state); JX.copy(draft_data, state.getWireFormat());
return draft_data; return draft_data;
}, },
@ -1193,22 +1214,8 @@ JX.install('DiffInline', {
suggestionText: '', suggestionText: '',
hasSuggestion: false hasSuggestion: false
}; };
},
_isVoidContentState: function(state) {
if (!state.text) {
return true;
}
return (!state.text.length && !state.suggestionText.length);
},
_isSameContentState: function(u, v) {
return (
((u === null) === (v === null)) &&
(u.text === v.text) &&
(u.suggestionText === v.suggestionText) &&
(u.hasSuggestion === v.hasSuggestion));
} }
} }
}); });

View file

@ -59,6 +59,72 @@ JX.install('DiffInlineContentState', {
return text; return text;
}, },
isStateEmpty: function() {
return (this.isTextEmpty() && this.isSuggestionEmpty());
},
isTextEmpty: function() {
var text = this.getText();
if (text === null) {
return true;
}
if (this._isStringSimilar(text, '')) {
return true;
}
return false;
},
isSuggestionEmpty: function() {
if (!this.getHasSuggestion()) {
return true;
}
var suggestion = this.getSuggestionText();
if (suggestion === null) {
return true;
}
if (this._isStringSimilar(suggestion, '')) {
return true;
}
return false;
},
isTextSimilar: function(v) {
if (!v) {
return false;
}
var us = this.getText();
var vs = v.getText();
return this._isStringSimilar(us, vs);
},
isSuggestionSimilar: function(v) {
// If we don't have a comparison state, treat them as dissimilar. This
// is expected to occur in old inline comments that did not save an
// initial state.
if (!v) {
return false;
}
var us = this.getSuggestionText();
var vs = v.getSuggestionText();
return this._isStringSimilar(us, vs);
},
_isStringSimilar: function(u, v) {
u = u || '';
v = v || '';
return (u === v);
},
_getSuggestionNode: function(row) { _getSuggestionNode: function(row) {
try { try {
return JX.DOM.find(row, 'textarea', 'inline-content-suggestion'); return JX.DOM.find(row, 'textarea', 'inline-content-suggestion');