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

Don't mangle inline comments with tables in them in Differential

Summary:
Fixes T3814. Broadly, remarkup tables in inline comments did not work properly. I ran into several messes here and cleaned up some of them:

- Some of this code is doing `JX.$N('div', {}, JX.$H(response.markup))`, to turn an HTML response into a node, passing that around, and then doing junk with it. This is super old and gross.
- The slightly more modern pattern is `JX.$H(response.markup).getFragment().firstChild`, but this is kind of yuck too and not as safe as it could be.
- Introduce `JX.$H(response.markup).getNode()`, which actually expresses intent here. We have a bunch of `getFragment().firstChild` callsites which should switch to this, but I didn't clean those up yet because I don't want to test them all.
- Switch the `JX.$N('div', {}, JX.$H(response.markup))`-style callsites to `JX.$H(response.markup).getNode()`.
- `copyRows()` is too aggressive in finding `<tr />` tags. This actually causes the bug in T3814. We only want to find these tags at top level, not all tags. Don't copy `<tr />` tags which belong to some deeper table.
- Once this is fixed, there's another bug with mousing over the cells in tables in inline comments. We select the nearest `<td />`, but that's the cell in the remarkup table. Instead, select the correct `<td />`.
- At this point, these last two callsites were looking ugly. I provided `findAbove()` to clean them up.

Test Plan: Created, edited, deleted, moused over, and reloaded a revision with inline comments including remarkup tables. Used "Show more context" links.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T3814

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

+135 -55
+47 -47
src/__celerity_resource_map__.php
··· 1035 1035 ), 1036 1036 'differential-inline-comment-editor' => 1037 1037 array( 1038 - 'uri' => '/res/37e0564f/rsrc/js/application/differential/DifferentialInlineCommentEditor.js', 1038 + 'uri' => '/res/e952d210/rsrc/js/application/differential/DifferentialInlineCommentEditor.js', 1039 1039 'type' => 'js', 1040 1040 'requires' => 1041 1041 array( ··· 1519 1519 ), 1520 1520 'javelin-behavior-differential-edit-inline-comments' => 1521 1521 array( 1522 - 'uri' => '/res/86f459a4/rsrc/js/application/differential/behavior-edit-inline-comments.js', 1522 + 'uri' => '/res/935d4012/rsrc/js/application/differential/behavior-edit-inline-comments.js', 1523 1523 'type' => 'js', 1524 1524 'requires' => 1525 1525 array( ··· 1603 1603 ), 1604 1604 'javelin-behavior-differential-show-more' => 1605 1605 array( 1606 - 'uri' => '/res/b9f93090/rsrc/js/application/differential/behavior-show-more.js', 1606 + 'uri' => '/res/03b7bc9e/rsrc/js/application/differential/behavior-show-more.js', 1607 1607 'type' => 'js', 1608 1608 'requires' => 1609 1609 array( ··· 2472 2472 ), 2473 2473 'javelin-dom' => 2474 2474 array( 2475 - 'uri' => '/res/175211d6/rsrc/externals/javelin/lib/DOM.js', 2475 + 'uri' => '/res/580c0aeb/rsrc/externals/javelin/lib/DOM.js', 2476 2476 'type' => 'js', 2477 2477 'requires' => 2478 2478 array( ··· 4300 4300 'uri' => '/res/pkg/44bfe40c/differential.pkg.css', 4301 4301 'type' => 'css', 4302 4302 ), 4303 - 'd07a3bc2' => 4303 + '5e9e5c4e' => 4304 4304 array( 4305 4305 'name' => 'differential.pkg.js', 4306 4306 'symbols' => ··· 4325 4325 17 => 'javelin-behavior-differential-toggle-files', 4326 4326 18 => 'javelin-behavior-differential-user-select', 4327 4327 ), 4328 - 'uri' => '/res/pkg/d07a3bc2/differential.pkg.js', 4328 + 'uri' => '/res/pkg/5e9e5c4e/differential.pkg.js', 4329 4329 'type' => 'js', 4330 4330 ), 4331 4331 'c8ce2d88' => ··· 4351 4351 'uri' => '/res/pkg/96909266/diffusion.pkg.js', 4352 4352 'type' => 'js', 4353 4353 ), 4354 - '2dbbb7d1' => 4354 + 'f32597c9' => 4355 4355 array( 4356 4356 'name' => 'javelin.pkg.js', 4357 4357 'symbols' => ··· 4377 4377 18 => 'javelin-tokenizer', 4378 4378 19 => 'javelin-history', 4379 4379 ), 4380 - 'uri' => '/res/pkg/2dbbb7d1/javelin.pkg.js', 4380 + 'uri' => '/res/pkg/f32597c9/javelin.pkg.js', 4381 4381 'type' => 'js', 4382 4382 ), 4383 4383 '0a9e494f' => ··· 4420 4420 'aphront-typeahead-control-css' => '6d1ec88f', 4421 4421 'differential-changeset-view-css' => '44bfe40c', 4422 4422 'differential-core-view-css' => '44bfe40c', 4423 - 'differential-inline-comment-editor' => 'd07a3bc2', 4423 + 'differential-inline-comment-editor' => '5e9e5c4e', 4424 4424 'differential-local-commits-view-css' => '44bfe40c', 4425 4425 'differential-results-table-css' => '44bfe40c', 4426 4426 'differential-revision-add-comment-css' => '44bfe40c', ··· 4434 4434 'global-drag-and-drop-css' => '6d1ec88f', 4435 4435 'inline-comment-summary-css' => '44bfe40c', 4436 4436 'javelin-aphlict' => '8977e356', 4437 - 'javelin-behavior' => '2dbbb7d1', 4437 + 'javelin-behavior' => 'f32597c9', 4438 4438 'javelin-behavior-aphlict-dropdown' => '8977e356', 4439 4439 'javelin-behavior-aphlict-listen' => '8977e356', 4440 4440 'javelin-behavior-aphront-basic-tokenizer' => '8977e356', 4441 - 'javelin-behavior-aphront-drag-and-drop-textarea' => 'd07a3bc2', 4441 + 'javelin-behavior-aphront-drag-and-drop-textarea' => '5e9e5c4e', 4442 4442 'javelin-behavior-aphront-form-disable-on-submit' => '8977e356', 4443 4443 'javelin-behavior-audit-preview' => '96909266', 4444 4444 'javelin-behavior-dark-console' => '4ccfeb47', 4445 4445 'javelin-behavior-device' => '8977e356', 4446 - 'javelin-behavior-differential-accept-with-errors' => 'd07a3bc2', 4447 - 'javelin-behavior-differential-add-reviewers-and-ccs' => 'd07a3bc2', 4448 - 'javelin-behavior-differential-comment-jump' => 'd07a3bc2', 4449 - 'javelin-behavior-differential-diff-radios' => 'd07a3bc2', 4450 - 'javelin-behavior-differential-dropdown-menus' => 'd07a3bc2', 4451 - 'javelin-behavior-differential-edit-inline-comments' => 'd07a3bc2', 4452 - 'javelin-behavior-differential-feedback-preview' => 'd07a3bc2', 4453 - 'javelin-behavior-differential-keyboard-navigation' => 'd07a3bc2', 4454 - 'javelin-behavior-differential-populate' => 'd07a3bc2', 4455 - 'javelin-behavior-differential-show-more' => 'd07a3bc2', 4456 - 'javelin-behavior-differential-toggle-files' => 'd07a3bc2', 4457 - 'javelin-behavior-differential-user-select' => 'd07a3bc2', 4446 + 'javelin-behavior-differential-accept-with-errors' => '5e9e5c4e', 4447 + 'javelin-behavior-differential-add-reviewers-and-ccs' => '5e9e5c4e', 4448 + 'javelin-behavior-differential-comment-jump' => '5e9e5c4e', 4449 + 'javelin-behavior-differential-diff-radios' => '5e9e5c4e', 4450 + 'javelin-behavior-differential-dropdown-menus' => '5e9e5c4e', 4451 + 'javelin-behavior-differential-edit-inline-comments' => '5e9e5c4e', 4452 + 'javelin-behavior-differential-feedback-preview' => '5e9e5c4e', 4453 + 'javelin-behavior-differential-keyboard-navigation' => '5e9e5c4e', 4454 + 'javelin-behavior-differential-populate' => '5e9e5c4e', 4455 + 'javelin-behavior-differential-show-more' => '5e9e5c4e', 4456 + 'javelin-behavior-differential-toggle-files' => '5e9e5c4e', 4457 + 'javelin-behavior-differential-user-select' => '5e9e5c4e', 4458 4458 'javelin-behavior-diffusion-commit-graph' => '96909266', 4459 4459 'javelin-behavior-diffusion-pull-lastmodified' => '96909266', 4460 4460 'javelin-behavior-error-log' => '4ccfeb47', ··· 4462 4462 'javelin-behavior-history-install' => '8977e356', 4463 4463 'javelin-behavior-konami' => '8977e356', 4464 4464 'javelin-behavior-lightbox-attachments' => '8977e356', 4465 - 'javelin-behavior-load-blame' => 'd07a3bc2', 4465 + 'javelin-behavior-load-blame' => '5e9e5c4e', 4466 4466 'javelin-behavior-maniphest-batch-selector' => '83a3853e', 4467 4467 'javelin-behavior-maniphest-subpriority-editor' => '83a3853e', 4468 4468 'javelin-behavior-maniphest-transaction-controls' => '83a3853e', ··· 4474 4474 'javelin-behavior-phabricator-hovercards' => '8977e356', 4475 4475 'javelin-behavior-phabricator-keyboard-shortcuts' => '8977e356', 4476 4476 'javelin-behavior-phabricator-nav' => '8977e356', 4477 - 'javelin-behavior-phabricator-object-selector' => 'd07a3bc2', 4477 + 'javelin-behavior-phabricator-object-selector' => '5e9e5c4e', 4478 4478 'javelin-behavior-phabricator-oncopy' => '8977e356', 4479 4479 'javelin-behavior-phabricator-remarkup-assist' => '8977e356', 4480 4480 'javelin-behavior-phabricator-reveal-content' => '8977e356', ··· 4482 4482 'javelin-behavior-phabricator-tooltips' => '8977e356', 4483 4483 'javelin-behavior-phabricator-watch-anchor' => '8977e356', 4484 4484 'javelin-behavior-refresh-csrf' => '8977e356', 4485 - 'javelin-behavior-repository-crossreference' => 'd07a3bc2', 4485 + 'javelin-behavior-repository-crossreference' => '5e9e5c4e', 4486 4486 'javelin-behavior-toggle-class' => '8977e356', 4487 4487 'javelin-behavior-workflow' => '8977e356', 4488 - 'javelin-dom' => '2dbbb7d1', 4489 - 'javelin-event' => '2dbbb7d1', 4490 - 'javelin-history' => '2dbbb7d1', 4491 - 'javelin-install' => '2dbbb7d1', 4492 - 'javelin-json' => '2dbbb7d1', 4493 - 'javelin-mask' => '2dbbb7d1', 4494 - 'javelin-request' => '2dbbb7d1', 4495 - 'javelin-resource' => '2dbbb7d1', 4496 - 'javelin-stratcom' => '2dbbb7d1', 4497 - 'javelin-tokenizer' => '2dbbb7d1', 4498 - 'javelin-typeahead' => '2dbbb7d1', 4499 - 'javelin-typeahead-normalizer' => '2dbbb7d1', 4500 - 'javelin-typeahead-ondemand-source' => '2dbbb7d1', 4501 - 'javelin-typeahead-preloaded-source' => '2dbbb7d1', 4502 - 'javelin-typeahead-source' => '2dbbb7d1', 4503 - 'javelin-uri' => '2dbbb7d1', 4504 - 'javelin-util' => '2dbbb7d1', 4505 - 'javelin-vector' => '2dbbb7d1', 4506 - 'javelin-workflow' => '2dbbb7d1', 4488 + 'javelin-dom' => 'f32597c9', 4489 + 'javelin-event' => 'f32597c9', 4490 + 'javelin-history' => 'f32597c9', 4491 + 'javelin-install' => 'f32597c9', 4492 + 'javelin-json' => 'f32597c9', 4493 + 'javelin-mask' => 'f32597c9', 4494 + 'javelin-request' => 'f32597c9', 4495 + 'javelin-resource' => 'f32597c9', 4496 + 'javelin-stratcom' => 'f32597c9', 4497 + 'javelin-tokenizer' => 'f32597c9', 4498 + 'javelin-typeahead' => 'f32597c9', 4499 + 'javelin-typeahead-normalizer' => 'f32597c9', 4500 + 'javelin-typeahead-ondemand-source' => 'f32597c9', 4501 + 'javelin-typeahead-preloaded-source' => 'f32597c9', 4502 + 'javelin-typeahead-source' => 'f32597c9', 4503 + 'javelin-uri' => 'f32597c9', 4504 + 'javelin-util' => 'f32597c9', 4505 + 'javelin-vector' => 'f32597c9', 4506 + 'javelin-workflow' => 'f32597c9', 4507 4507 'lightbox-attachment-css' => '6d1ec88f', 4508 4508 'maniphest-task-summary-css' => '0a9e494f', 4509 4509 'maniphest-transaction-detail-css' => '0a9e494f', ··· 4513 4513 'phabricator-content-source-view-css' => '44bfe40c', 4514 4514 'phabricator-core-css' => '6d1ec88f', 4515 4515 'phabricator-crumbs-view-css' => '6d1ec88f', 4516 - 'phabricator-drag-and-drop-file-upload' => 'd07a3bc2', 4516 + 'phabricator-drag-and-drop-file-upload' => '5e9e5c4e', 4517 4517 'phabricator-dropdown-menu' => '8977e356', 4518 4518 'phabricator-file-upload' => '8977e356', 4519 4519 'phabricator-filetree-view-css' => '6d1ec88f', ··· 4535 4535 'phabricator-project-tag-css' => '0a9e494f', 4536 4536 'phabricator-property-list-view-css' => '6d1ec88f', 4537 4537 'phabricator-remarkup-css' => '6d1ec88f', 4538 - 'phabricator-shaped-request' => 'd07a3bc2', 4538 + 'phabricator-shaped-request' => '5e9e5c4e', 4539 4539 'phabricator-side-menu-view-css' => '6d1ec88f', 4540 4540 'phabricator-standard-page-view' => '6d1ec88f', 4541 4541 'phabricator-tag-view-css' => '6d1ec88f',
+69 -2
webroot/rsrc/externals/javelin/lib/DOM.js
··· 157 157 fragment.appendChild(wrapper.removeChild(wrapper.firstChild)); 158 158 } 159 159 return fragment; 160 + }, 161 + 162 + /** 163 + * Convert the raw HTML string into a single DOM node. This only works 164 + * if the element has a single top-level element. Otherwise, use 165 + * @{method:getFragment} to get a document fragment instead. 166 + * 167 + * @return Node Single node represented by the object. 168 + * @task nodes 169 + */ 170 + getNode : function() { 171 + var fragment = this.getFragment(); 172 + if (__DEV__) { 173 + if (fragment.childNodes.length < 1) { 174 + JX.$E('JX.HTML.getNode(): Markup has no root node!'); 175 + } 176 + if (fragment.childNodes.length > 1) { 177 + JX.$E('JX.HTML.getNode(): Markup has more than one root node!'); 178 + } 179 + } 180 + return fragment.firstChild; 160 181 } 182 + 161 183 } 162 184 }); 163 185 ··· 856 878 } 857 879 858 880 if (!result.length) { 859 - JX.$E('JX.DOM.find(<node>, "' + 860 - tagname + '", "' + sigil + '"): '+ 'matched no nodes.'); 881 + JX.$E( 882 + 'JX.DOM.find(<node>, "' + tagname + '", "' + sigil + '"): ' + 883 + 'matched no nodes.'); 861 884 } 862 885 863 886 return result[0]; 887 + }, 888 + 889 + 890 + /** 891 + * Select a node uniquely identified by an anchor, tagname, and sigil. This 892 + * is similar to JX.DOM.find() but walks up the DOM tree instead of down 893 + * it. 894 + * 895 + * @param Node Node to look above. 896 + * @param string Tag name, like 'a' or 'textarea'. 897 + * @param string Optionally, sigil which selected node must have. 898 + * @return Node Matching node. 899 + * 900 + * @task query 901 + */ 902 + findAbove : function(anchor, tagname, sigil) { 903 + if (__DEV__) { 904 + if (!JX.DOM.isNode(anchor)) { 905 + JX.$E( 906 + 'JX.DOM.findAbove(<glop>, "' + tagname + '", "' + sigil + '"): ' + 907 + 'first argument must be a DOM node.'); 908 + } 909 + } 910 + 911 + var result = anchor.parentNode; 912 + while (true) { 913 + if (!result) { 914 + break; 915 + } 916 + if (JX.DOM.isType(result, tagname)) { 917 + if (!sigil || JX.Stratcom.hasSigil(result, sigil)) { 918 + break; 919 + } 920 + } 921 + result = result.parentNode; 922 + } 923 + 924 + if (!result) { 925 + JX.$E( 926 + 'JX.DOM.findAbove(<node>, "' + tagname + '", "' + sigil + '"): ' + 927 + 'no matching node.'); 928 + } 929 + 930 + return result; 864 931 }, 865 932 866 933
+3 -3
webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js
··· 79 79 JX.DOM.alterClass(row, 'differential-inline-loading', is_loading); 80 80 }, 81 81 _didContinueWorkflow : function(response) { 82 - var drawn = this._draw(JX.$N('div', JX.$H(response))); 82 + var drawn = this._draw(JX.$H(response).getNode()); 83 83 84 84 var op = this.getOperation(); 85 85 if (op == 'edit') { ··· 131 131 // We don't get any markup back if the user deletes a comment, or saves 132 132 // an empty comment (which effects a delete). 133 133 if (response.markup) { 134 - this._draw(JX.$N('div', JX.$H(response.markup))); 134 + this._draw(JX.$H(response.markup).getNode()); 135 135 } 136 136 137 137 // These operations remove the old row (edit adds a new row first). ··· 190 190 191 191 var templates = this.getTemplates(); 192 192 var template = this.getOnRight() ? templates.r : templates.l; 193 - template = JX.$N('div', JX.$H(template)); 193 + template = JX.$H(template).getNode(); 194 194 195 195 // NOTE: Operation order matters here; we can't remove anything until 196 196 // after we draw the new rows because _draw uses the old rows to figure
+7 -1
webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js
··· 175 175 var change = e.getNodeData('differential-changeset'); 176 176 177 177 var id_part = data.on_right ? change.right : change.left; 178 - var th = e.getNode('tag:td').previousSibling; 178 + 179 + // NOTE: We can't just look for 'tag:td' because the event might be 180 + // inside a table which is inside an inline comment. 181 + var comment = e.getNode('differential-inline-comment'); 182 + var td = JX.DOM.findAbove(comment, 'td'); 183 + var th = td.previousSibling; 184 + 179 185 var new_part = isNewFile(th) ? 'N' : 'O'; 180 186 var prefix = 'C' + id_part + new_part + 'L'; 181 187
+9 -2
webroot/rsrc/js/application/differential/behavior-show-more.js
··· 10 10 JX.behavior('differential-show-more', function(config) { 11 11 12 12 function onresponse(context, response) { 13 - var div = JX.$N('div', {}, JX.$H(response.changeset)); 13 + var table = JX.$H(response.changeset).getNode(); 14 14 var root = context.parentNode; 15 - copyRows(root, div, context); 15 + copyRows(root, table, context); 16 16 root.removeChild(context); 17 17 } 18 18 ··· 54 54 function copyRows(dst, src, before) { 55 55 var rows = JX.DOM.scry(src, 'tr'); 56 56 for (var ii = 0; ii < rows.length; ii++) { 57 + 58 + // Find the table this <tr /> belongs to. If it's a sub-table, like a 59 + // table in an inline comment, don't copy it. 60 + if (JX.DOM.findAbove(rows[ii], 'table') !== src) { 61 + continue; 62 + } 63 + 57 64 if (before) { 58 65 dst.insertBefore(rows[ii], before); 59 66 } else {