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

Improve minor client behaviors for document rendering

Summary:
Ref T13105. This adds various small client-side improvements to document rendering.

- In the menu, show which renderer is in use.
- Make linking to lines work.
- Make URIs persist information about which rendering engine is in use.
- Improve the UI feedback for transitions between document types.
- Load slower documents asynchronously by default.
- Discard irrelevant requests if you spam the view menu.

Test Plan: Loaded files, linked to lines, swapped between modes, copy/pasted URLs.

Maniphest Tasks: T13105

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

+210 -38
+18 -18
resources/celerity/map.php
··· 9 9 'names' => array( 10 10 'conpherence.pkg.css' => 'e68cf1fa', 11 11 'conpherence.pkg.js' => '15191c65', 12 - 'core.pkg.css' => 'da541195', 12 + 'core.pkg.css' => '3fd3b7b8', 13 13 'core.pkg.js' => 'b9b4a943', 14 14 'differential.pkg.css' => '113e692c', 15 15 'differential.pkg.js' => 'f6d809c0', ··· 168 168 'rsrc/css/phui/phui-object-box.css' => '9cff003c', 169 169 'rsrc/css/phui/phui-pager.css' => 'edcbc226', 170 170 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', 171 - 'rsrc/css/phui/phui-property-list-view.css' => '54c071ed', 171 + 'rsrc/css/phui/phui-property-list-view.css' => 'de4754d8', 172 172 'rsrc/css/phui/phui-remarkup-preview.css' => '54a34863', 173 173 'rsrc/css/phui/phui-segment-bar-view.css' => 'b1d1b892', 174 174 'rsrc/css/phui/phui-spacing.css' => '042804d6', ··· 392 392 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'f01586dc', 393 393 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '1db13e70', 394 394 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef', 395 - 'rsrc/js/application/files/behavior-document-engine.js' => '396ef112', 395 + 'rsrc/js/application/files/behavior-document-engine.js' => 'd3f8623c', 396 396 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 397 397 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', 398 398 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '191b4909', ··· 473 473 'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0', 474 474 'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0', 475 475 'rsrc/js/core/behavior-lightbox-attachments.js' => '6b31879a', 476 - 'rsrc/js/core/behavior-line-linker.js' => 'a9b946f8', 476 + 'rsrc/js/core/behavior-line-linker.js' => '13e39479', 477 477 'rsrc/js/core/behavior-more.js' => 'a80d0378', 478 478 'rsrc/js/core/behavior-object-selector.js' => '77c1f0b0', 479 479 'rsrc/js/core/behavior-oncopy.js' => '2926fff2', ··· 607 607 'javelin-behavior-diffusion-jump-to' => '73d09eef', 608 608 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 609 609 'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc', 610 - 'javelin-behavior-document-engine' => '396ef112', 610 + 'javelin-behavior-document-engine' => 'd3f8623c', 611 611 'javelin-behavior-doorkeeper-tag' => '1db13e70', 612 612 'javelin-behavior-drydock-live-operation-status' => '901935ef', 613 613 'javelin-behavior-durable-column' => '2ae077e1', ··· 638 638 'javelin-behavior-phabricator-gesture-example' => '558829c2', 639 639 'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0', 640 640 'javelin-behavior-phabricator-keyboard-shortcuts' => '01fca1f0', 641 - 'javelin-behavior-phabricator-line-linker' => 'a9b946f8', 641 + 'javelin-behavior-phabricator-line-linker' => '13e39479', 642 642 'javelin-behavior-phabricator-nav' => '836f966d', 643 643 'javelin-behavior-phabricator-notification-example' => '8ce821c5', 644 644 'javelin-behavior-phabricator-object-selector' => '77c1f0b0', ··· 850 850 'phui-oi-simple-ui-css' => 'a8beebea', 851 851 'phui-pager-css' => 'edcbc226', 852 852 'phui-pinboard-view-css' => '2495140e', 853 - 'phui-property-list-view-css' => '54c071ed', 853 + 'phui-property-list-view-css' => 'de4754d8', 854 854 'phui-remarkup-preview-css' => '54a34863', 855 855 'phui-segment-bar-view-css' => 'b1d1b892', 856 856 'phui-spacing-css' => '042804d6', ··· 964 964 'javelin-install', 965 965 'javelin-util', 966 966 ), 967 + '13e39479' => array( 968 + 'javelin-behavior', 969 + 'javelin-stratcom', 970 + 'javelin-dom', 971 + 'javelin-history', 972 + ), 967 973 '15d5ff71' => array( 968 974 'aphront-typeahead-control-css', 969 975 'phui-tag-view-css', ··· 1102 1108 'javelin-util', 1103 1109 'javelin-dom', 1104 1110 'javelin-vector', 1105 - ), 1106 - '396ef112' => array( 1107 - 'javelin-behavior', 1108 - 'javelin-dom', 1109 - 'javelin-stratcom', 1110 1111 ), 1111 1112 '3ab51e2c' => array( 1112 1113 'javelin-behavior', ··· 1751 1752 'javelin-uri', 1752 1753 'phabricator-keyboard-shortcut', 1753 1754 ), 1754 - 'a9b946f8' => array( 1755 - 'javelin-behavior', 1756 - 'javelin-stratcom', 1757 - 'javelin-dom', 1758 - 'javelin-history', 1759 - ), 1760 1755 'a9f88de2' => array( 1761 1756 'javelin-behavior', 1762 1757 'javelin-dom', ··· 2006 2001 ), 2007 2002 'd254d646' => array( 2008 2003 'javelin-util', 2004 + ), 2005 + 'd3f8623c' => array( 2006 + 'javelin-behavior', 2007 + 'javelin-dom', 2008 + 'javelin-stratcom', 2009 2009 ), 2010 2010 'd4505101' => array( 2011 2011 'javelin-stratcom',
+2 -2
src/__phutil_library_map__.php
··· 3015 3015 'PhabricatorFileImageMacro' => 'applications/macro/storage/PhabricatorFileImageMacro.php', 3016 3016 'PhabricatorFileImageProxyController' => 'applications/files/controller/PhabricatorFileImageProxyController.php', 3017 3017 'PhabricatorFileImageTransform' => 'applications/files/transform/PhabricatorFileImageTransform.php', 3018 - 'PhabricatorFileInfoController' => 'applications/files/controller/PhabricatorFileInfoController.php', 3019 3018 'PhabricatorFileIntegrityException' => 'applications/files/exception/PhabricatorFileIntegrityException.php', 3020 3019 'PhabricatorFileLightboxController' => 'applications/files/controller/PhabricatorFileLightboxController.php', 3021 3020 'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php', ··· 3051 3050 'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php', 3052 3051 'PhabricatorFileUploadSource' => 'applications/files/uploadsource/PhabricatorFileUploadSource.php', 3053 3052 'PhabricatorFileUploadSourceByteLimitException' => 'applications/files/uploadsource/PhabricatorFileUploadSourceByteLimitException.php', 3053 + 'PhabricatorFileViewController' => 'applications/files/controller/PhabricatorFileViewController.php', 3054 3054 'PhabricatorFileinfoSetupCheck' => 'applications/config/check/PhabricatorFileinfoSetupCheck.php', 3055 3055 'PhabricatorFilesApplication' => 'applications/files/application/PhabricatorFilesApplication.php', 3056 3056 'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php', ··· 8622 8622 ), 8623 8623 'PhabricatorFileImageProxyController' => 'PhabricatorFileController', 8624 8624 'PhabricatorFileImageTransform' => 'PhabricatorFileTransform', 8625 - 'PhabricatorFileInfoController' => 'PhabricatorFileController', 8626 8625 'PhabricatorFileIntegrityException' => 'Exception', 8627 8626 'PhabricatorFileLightboxController' => 'PhabricatorFileController', 8628 8627 'PhabricatorFileLinkView' => 'AphrontTagView', ··· 8658 8657 'PhabricatorFileUploadException' => 'Exception', 8659 8658 'PhabricatorFileUploadSource' => 'Phobject', 8660 8659 'PhabricatorFileUploadSourceByteLimitException' => 'Exception', 8660 + 'PhabricatorFileViewController' => 'PhabricatorFileController', 8661 8661 'PhabricatorFileinfoSetupCheck' => 'PhabricatorSetupCheck', 8662 8662 'PhabricatorFilesApplication' => 'PhabricatorApplication', 8663 8663 'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
+7 -2
src/applications/files/application/PhabricatorFilesApplication.php
··· 69 69 70 70 public function getRoutes() { 71 71 return array( 72 - '/F(?P<id>[1-9]\d*)' => 'PhabricatorFileInfoController', 72 + '/F(?P<id>[1-9]\d*)(?:\$(?P<lines>\d+(?:-\d+)?))?' 73 + => 'PhabricatorFileViewController', 73 74 '/file/' => array( 74 75 '(query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorFileListController', 76 + 'view/(?P<id>[^/]+)/'. 77 + '(?:(?P<engineKey>[^/]+)/)?'. 78 + '(?:\$(?P<lines>\d+(?:-\d+)?))?' 79 + => 'PhabricatorFileViewController', 80 + 'info/(?P<phid>[^/]+)/' => 'PhabricatorFileViewController', 75 81 'upload/' => 'PhabricatorFileUploadController', 76 82 'dropupload/' => 'PhabricatorFileDropUploadController', 77 83 'compose/' => 'PhabricatorFileComposeController', ··· 80 86 'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorFileDeleteController', 81 87 $this->getEditRoutePattern('edit/') 82 88 => 'PhabricatorFileEditController', 83 - 'info/(?P<phid>[^/]+)/' => 'PhabricatorFileInfoController', 84 89 'imageproxy/' => 'PhabricatorFileImageProxyController', 85 90 'transforms/(?P<id>[1-9]\d*)/' => 86 91 'PhabricatorFileTransformListController',
+32 -7
src/applications/files/controller/PhabricatorFileInfoController.php src/applications/files/controller/PhabricatorFileViewController.php
··· 1 1 <?php 2 2 3 - final class PhabricatorFileInfoController extends PhabricatorFileController { 3 + final class PhabricatorFileViewController extends PhabricatorFileController { 4 4 5 5 public function shouldAllowPublic() { 6 6 return true; ··· 404 404 405 405 private function newFileContent(PhabricatorFile $file) { 406 406 $viewer = $this->getViewer(); 407 + $request = $this->getRequest(); 407 408 408 409 $ref = id(new PhabricatorDocumentRef()) 409 410 ->setFile($file); 410 411 411 412 $engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref); 412 - $engine = head($engines); 413 413 414 - $content = $engine->newDocument($ref); 414 + $engine_key = $request->getURIData('engineKey'); 415 + if (!isset($engines[$engine_key])) { 416 + $engine_key = head_key($engines); 417 + } 418 + $engine = $engines[$engine_key]; 415 419 416 - $icon = $engine->newDocumentIcon($ref); 420 + $lines = $request->getURILineRange('lines', 1000); 421 + if ($lines) { 422 + $engine->setHighlightedLines(range($lines[0], $lines[1])); 423 + } 417 424 418 425 $views = array(); 419 - foreach ($engines as $candidate_engine) { 426 + foreach ($engines as $candidate_key => $candidate_engine) { 420 427 $label = $candidate_engine->getViewAsLabel($ref); 421 428 if ($label === null) { 422 429 continue; 423 430 } 424 431 432 + $view_uri = '/file/view/'.$file->getID().'/'.$candidate_key.'/'; 433 + 425 434 $view_icon = $candidate_engine->getViewAsIconIcon($ref); 426 435 $view_color = $candidate_engine->getViewAsIconColor($ref); 436 + $loading = $candidate_engine->newLoadingContent($ref); 427 437 428 438 $views[] = array( 429 439 'viewKey' => $candidate_engine->getDocumentEngineKey(), ··· 431 441 'color' => $view_color, 432 442 'name' => $label, 433 443 'engineURI' => $candidate_engine->getRenderURI($ref), 444 + 'viewURI' => $view_uri, 445 + 'loadingMarkup' => hsprintf('%s', $loading), 434 446 ); 435 447 } 436 448 437 - Javelin::initBehavior('document-engine'); 449 + $viewport_id = celerity_generate_unique_node_id(); 450 + $control_id = celerity_generate_unique_node_id(); 451 + $icon = $engine->newDocumentIcon($ref); 452 + 453 + if ($engine->shouldRenderAsync($ref)) { 454 + $content = $engine->newLoadingContent($ref); 455 + $config = array( 456 + 'renderControlID' => $control_id, 457 + ); 458 + } else { 459 + $content = $engine->newDocument($ref); 460 + $config = array(); 461 + } 438 462 439 - $viewport_id = celerity_generate_unique_node_id(); 463 + Javelin::initBehavior('document-engine', $config); 440 464 441 465 $viewport = phutil_tag( 442 466 'div', ··· 457 481 ->setText(pht('View Options')) 458 482 ->setIcon('fa-file-image-o') 459 483 ->setColor(PHUIButtonView::GREY) 484 + ->setID($control_id) 460 485 ->setMetadata($meta) 461 486 ->setDropdown(true) 462 487 ->addSigil('document-engine-view-dropdown');
+34
src/applications/files/document/PhabricatorDocumentEngine.php
··· 4 4 extends Phobject { 5 5 6 6 private $viewer; 7 + private $highlightedLines = array(); 7 8 8 9 final public function setViewer(PhabricatorUser $viewer) { 9 10 $this->viewer = $viewer; ··· 14 15 return $this->viewer; 15 16 } 16 17 18 + final public function setHighlightedLines(array $highlighted_lines) { 19 + $this->highlightedLines = $highlighted_lines; 20 + return $this; 21 + } 22 + 23 + final public function getHighlightedLines() { 24 + return $this->highlightedLines; 25 + } 26 + 17 27 final public function canRenderDocument(PhabricatorDocumentRef $ref) { 18 28 return $this->canRenderDocumentType($ref); 29 + } 30 + 31 + public function shouldRenderAsync(PhabricatorDocumentRef $ref) { 32 + return false; 19 33 } 20 34 21 35 abstract protected function canRenderDocumentType( ··· 47 61 48 62 protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) { 49 63 return 'fa-file-o'; 64 + } 65 + 66 + protected function getDocumentRenderingText(PhabricatorDocumentRef $ref) { 67 + return pht('Loading...'); 50 68 } 51 69 52 70 final public function getDocumentEngineKey() { ··· 175 193 'class' => 'document-engine-error', 176 194 ), 177 195 $message); 196 + } 197 + 198 + final public function newLoadingContent(PhabricatorDocumentRef $ref) { 199 + $spinner = id(new PHUIIconView()) 200 + ->setIcon('fa-gear') 201 + ->addClass('ph-spin'); 202 + 203 + return phutil_tag( 204 + 'div', 205 + array( 206 + 'class' => 'document-engine-loading', 207 + ), 208 + array( 209 + $spinner, 210 + $this->getDocumentRenderingText($ref), 211 + )); 178 212 } 179 213 180 214 }
+8
src/applications/files/document/PhabricatorJupyterDocumentEngine.php
··· 13 13 return 'fa-sun-o'; 14 14 } 15 15 16 + protected function getDocumentRenderingText(PhabricatorDocumentRef $ref) { 17 + return pht('Rendering Jupyter Notebook...'); 18 + } 19 + 20 + public function shouldRenderAsync(PhabricatorDocumentRef $ref) { 21 + return true; 22 + } 23 + 16 24 protected function getContentScore(PhabricatorDocumentRef $ref) { 17 25 $name = $ref->getName(); 18 26
+2 -2
src/applications/files/document/PhabricatorTextDocumentEngine.php
··· 11 11 $lines = phutil_split_lines($content); 12 12 13 13 $view = id(new PhabricatorSourceCodeView()) 14 - ->setLines($lines) 15 - ->disableHighlightOnClick(); 14 + ->setHighlights($this->getHighlightedLines()) 15 + ->setLines($lines); 16 16 17 17 $container = phutil_tag( 18 18 'div',
+5 -1
src/view/layout/PhabricatorSourceCodeView.php
··· 85 85 } 86 86 87 87 if ($this->canClickHighlight) { 88 - $line_href = $base_uri.'$'.$line_number; 88 + if ($base_uri) { 89 + $line_href = $base_uri.'$'.$line_number; 90 + } else { 91 + $line_href = null; 92 + } 89 93 90 94 $tag_number = phutil_tag( 91 95 'a',
+17
webroot/rsrc/css/phui/phui-property-list-view.css
··· 267 267 margin: 20px; 268 268 } 269 269 270 + .document-engine-in-flight { 271 + opacity: 0.25; 272 + } 273 + 274 + .document-engine-loading { 275 + margin: 20px; 276 + text-align: center; 277 + color: {$lightgreytext}; 278 + } 279 + 280 + .document-engine-loading .phui-icon-view { 281 + display: block; 282 + font-size: 48px; 283 + color: {$lightgreyborder}; 284 + padding: 8px; 285 + } 286 + 270 287 .jupyter-cell-raw { 271 288 white-space: pre-wrap; 272 289 background: {$lightgreybackground};
+81 -6
webroot/rsrc/js/application/files/behavior-document-engine.js
··· 5 5 * javelin-stratcom 6 6 */ 7 7 8 - JX.behavior('document-engine', function() { 8 + JX.behavior('document-engine', function(config, statics) { 9 + 10 + 9 11 10 12 function onmenu(e) { 11 13 var node = e.getNode('document-engine-view-dropdown'); ··· 21 23 var list = new JX.PHUIXActionListView(); 22 24 23 25 var view; 26 + var engines = []; 24 27 for (var ii = 0; ii < data.views.length; ii++) { 25 28 var spec = data.views[ii]; 26 29 ··· 38 41 e.prevent(); 39 42 menu.close(); 40 43 41 - onview(data, spec); 44 + onview(data, spec, false); 42 45 }, spec)); 43 46 44 47 list.addItem(view); 48 + 49 + engines.push({ 50 + spec: spec, 51 + view: view 52 + }); 45 53 } 46 54 47 55 menu.setContent(list.getNode()); 56 + 57 + menu.listen('open', function() { 58 + for (var ii = 0; ii < engines.length; ii++) { 59 + var engine = engines[ii]; 60 + 61 + // Highlight the current rendering engine. 62 + var is_selected = (engine.spec.viewKey == data.viewKey); 63 + engine.view.setSelected(is_selected); 64 + } 65 + }); 48 66 49 67 data.menu = menu; 50 68 menu.open(); 51 69 } 52 70 53 - function onview(data, spec) { 54 - var handler = JX.bind(null, onrender, data); 71 + function onview(data, spec, immediate) { 72 + data.sequence = (data.sequence || 0) + 1; 73 + var handler = JX.bind(null, onrender, data, data.sequence); 74 + 75 + data.viewKey = spec.viewKey; 76 + JX.History.replace(spec.viewURI); 55 77 56 78 new JX.Request(spec.engineURI, handler) 57 79 .send(); 80 + 81 + if (data.loadingView) { 82 + // If we're already showing "Loading...", immediately change it to 83 + // show the new document type. 84 + onloading(data, spec); 85 + } else if (!immediate) { 86 + // Otherwise, grey out the document and show "Loading..." after a 87 + // short delay. This prevents the content from flickering when rendering 88 + // is fast. 89 + var viewport = JX.$(data.viewportID); 90 + JX.DOM.alterClass(viewport, 'document-engine-in-flight', true); 91 + 92 + var load = JX.bind(null, onloading, data, spec); 93 + data.loadTimer = setTimeout(load, 333); 94 + } 58 95 } 59 96 60 - function onrender(data, r) { 97 + function onloading(data, spec) { 98 + data.loadingView = true; 99 + 100 + var viewport = JX.$(data.viewportID); 101 + JX.DOM.alterClass(viewport, 'document-engine-in-flight', false); 102 + JX.DOM.setContent(viewport, JX.$H(spec.loadingMarkup)); 103 + } 104 + 105 + function onrender(data, sequence, r) { 106 + // If this isn't the most recent request we sent, throw it away. This can 107 + // happen if the user makes multiple selections from the menu while we are 108 + // still rendering the first view. 109 + if (sequence != data.sequence) { 110 + return; 111 + } 112 + 113 + if (data.loadTimer) { 114 + clearTimeout(data.loadTimer); 115 + data.loadTimer = null; 116 + } 117 + 61 118 var viewport = JX.$(data.viewportID); 62 119 120 + JX.DOM.alterClass(viewport, 'document-engine-in-flight', false); 121 + data.loadingView = false; 122 + 63 123 JX.DOM.setContent(viewport, JX.$H(r.markup)); 64 124 } 65 125 66 - JX.Stratcom.listen('click', 'document-engine-view-dropdown', onmenu); 126 + if (!statics.initialized) { 127 + JX.Stratcom.listen('click', 'document-engine-view-dropdown', onmenu); 128 + statics.initialized = true; 129 + } 130 + 131 + if (config.renderControlID) { 132 + var control = JX.$(config.renderControlID); 133 + var data = JX.Stratcom.getData(control); 134 + 135 + for (var ii = 0; ii < data.views.length; ii++) { 136 + if (data.views[ii].viewKey == data.viewKey) { 137 + onview(data, data.views[ii], true); 138 + break; 139 + } 140 + } 141 + } 67 142 68 143 });
+4
webroot/rsrc/js/core/behavior-line-linker.js
··· 145 145 var t = getRowNumber(target); 146 146 var uri = JX.Stratcom.getData(root).uri; 147 147 148 + if (!uri) { 149 + uri = ('' + window.location).split('$')[0]; 150 + } 151 + 148 152 origin = null; 149 153 target = null; 150 154 root = null;