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

Implements copy button in clone repo modal

Summary:
This diff adds a copy button to every repo uri in the clone repo modal. I have made the button to select the text to a merely structural span before the input - it just shows the type of the repository uri. When you click inside the input, the entire uri will be selected. Also I have uncluttered the HTML structure. A table is not needed here, nothing a flex block can't handle.

| Before | After |
|-----------|-----------|
| {F1360344} | {F1368592} |

While at it, I have extended the used javascript copy behavior. First of all: `document.execCommand('copy')` [[ https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand | could stop working every moment in every browser ]]. The [[ https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard | new clipboard API ]] is the way to go, so I have implemented it as the preferred method. The old method is kept as a fallback. And I have added a very nice feature: If defined, the behavior will now issue success or error notifications. See the changed UIExamples for that.

To support the shrinking of JS code with async functions I have patched the JsShrink source.

Test Plan: Go to a repository, hit the clone button and use the new copy button. You will see a shiny notification as a reward.

Reviewers: O1 Blessed Committers, avivey, valerio.bozzolan

Reviewed By: O1 Blessed Committers, avivey, valerio.bozzolan

Subscribers: avivey, tobiaswiese, valerio.bozzolan, Matthew, Cigaryno

Differential Revision: https://we.phorge.it/D25536

+137 -59
+1 -1
externals/JsShrink/jsShrink.php
··· 35 35 list(, $context, $regexp, $result, $word, $operator) = $match; 36 36 if ($word != '') { 37 37 $result = ($last == 'word' ? "\n" : ($last == 'return' ? " " : "")) . $result; 38 - $last = ($word == 'return' || $word == 'throw' || $word == 'break' ? 'return' : 'word'); 38 + $last = ($word == 'return' || $word == 'throw' || $word == 'break' || $word == 'async' ? 'return' : 'word'); 39 39 } elseif ($operator) { 40 40 $result = ($last == $operator[0] ? "\n" : "") . $result; 41 41 $last = $operator[0];
+18 -17
resources/celerity/map.php
··· 14 14 'dark-console.pkg.js' => '187792c2', 15 15 'differential.pkg.css' => '6d3700f0', 16 16 'differential.pkg.js' => '46fcb3af', 17 - 'diffusion.pkg.css' => '42c75c37', 17 + 'diffusion.pkg.css' => '354279ea', 18 18 'diffusion.pkg.js' => '78c9885d', 19 19 'maniphest.pkg.css' => '35995d6d', 20 20 'maniphest.pkg.js' => 'c9308721', ··· 69 69 'rsrc/css/application/differential/revision-history.css' => '237a2979', 70 70 'rsrc/css/application/differential/revision-list.css' => '93d2df7d', 71 71 'rsrc/css/application/differential/table-of-contents.css' => 'bba788b9', 72 - 'rsrc/css/application/diffusion/diffusion-icons.css' => '23b31a1b', 72 + 'rsrc/css/application/diffusion/diffusion-icons.css' => 'e812add2', 73 73 'rsrc/css/application/diffusion/diffusion-readme.css' => 'b68a76e4', 74 74 'rsrc/css/application/diffusion/diffusion-repository.css' => 'b89e8c6c', 75 75 'rsrc/css/application/diffusion/diffusion.css' => 'e46232d6', ··· 470 470 'rsrc/js/core/behavior-badge-view.js' => '92cdd7b6', 471 471 'rsrc/js/core/behavior-bulk-editor.js' => 'aa6d2308', 472 472 'rsrc/js/core/behavior-choose-control.js' => '04f8a1e3', 473 - 'rsrc/js/core/behavior-copy.js' => 'cf32921f', 473 + 'rsrc/js/core/behavior-copy.js' => '96b63a02', 474 474 'rsrc/js/core/behavior-detect-timezone.js' => '78bc5d94', 475 475 'rsrc/js/core/behavior-device.js' => 'ac2b1e01', 476 476 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '6bc7ccf7', ··· 499 499 'rsrc/js/core/behavior-reveal-content.js' => 'b105a3a6', 500 500 'rsrc/js/core/behavior-scrollbar.js' => '92388bae', 501 501 'rsrc/js/core/behavior-search-typeahead.js' => '1cb7d027', 502 - 'rsrc/js/core/behavior-select-content.js' => 'e8240b50', 502 + 'rsrc/js/core/behavior-select-content.js' => 'c538cbfc', 503 503 'rsrc/js/core/behavior-select-on-click.js' => '66365ee2', 504 504 'rsrc/js/core/behavior-setup-check-https.js' => '01384686', 505 505 'rsrc/js/core/behavior-time-typeahead.js' => '5803b9e7', ··· 567 567 'differential-revision-list-css' => '93d2df7d', 568 568 'differential-table-of-contents-css' => 'bba788b9', 569 569 'diffusion-css' => 'e46232d6', 570 - 'diffusion-icons-css' => '23b31a1b', 570 + 'diffusion-icons-css' => 'e812add2', 571 571 'diffusion-readme-css' => 'b68a76e4', 572 572 'diffusion-repository-css' => 'b89e8c6c', 573 573 'diviner-shared-css' => '4bd263b0', ··· 644 644 'javelin-behavior-owners-path-editor' => 'ff688a7a', 645 645 'javelin-behavior-passphrase-credential-control' => '48fe33d0', 646 646 'javelin-behavior-phabricator-autofocus' => '65bb0011', 647 - 'javelin-behavior-phabricator-clipboard-copy' => 'cf32921f', 647 + 'javelin-behavior-phabricator-clipboard-copy' => '96b63a02', 648 648 'javelin-behavior-phabricator-gesture' => 'b58d1a2a', 649 649 'javelin-behavior-phabricator-gesture-example' => '242dedd0', 650 650 'javelin-behavior-phabricator-keyboard-pager' => '1325b731', ··· 687 687 'javelin-behavior-repository-crossreference' => '44d48cd1', 688 688 'javelin-behavior-scrollbar' => '92388bae', 689 689 'javelin-behavior-search-reorder-queries' => 'b86f297f', 690 - 'javelin-behavior-select-content' => 'e8240b50', 690 + 'javelin-behavior-select-content' => 'c538cbfc', 691 691 'javelin-behavior-select-on-click' => '66365ee2', 692 692 'javelin-behavior-setup-check-https' => '01384686', 693 693 'javelin-behavior-stripe-payment-form' => '02cb4398', ··· 1770 1770 'javelin-dom', 1771 1771 'javelin-router', 1772 1772 ), 1773 + '96b63a02' => array( 1774 + 'javelin-behavior', 1775 + 'javelin-dom', 1776 + 'javelin-stratcom', 1777 + 'phabricator-notification', 1778 + ), 1773 1779 '98ef467f' => array( 1774 1780 'javelin-behavior', 1775 1781 'javelin-dom', ··· 2022 2028 'javelin-workboard-card', 2023 2029 'javelin-workboard-header', 2024 2030 ), 2031 + 'c538cbfc' => array( 2032 + 'javelin-behavior', 2033 + 'javelin-stratcom', 2034 + 'javelin-dom', 2035 + ), 2025 2036 'c687e867' => array( 2026 2037 'javelin-behavior', 2027 2038 'javelin-dom', ··· 2062 2073 'javelin-dom', 2063 2074 'phuix-formation-column-view', 2064 2075 'phuix-formation-flank-view', 2065 - ), 2066 - 'cf32921f' => array( 2067 - 'javelin-behavior', 2068 - 'javelin-dom', 2069 - 'javelin-stratcom', 2070 2076 ), 2071 2077 'd12d214f' => array( 2072 2078 'javelin-install', ··· 2145 2151 'javelin-workflow', 2146 2152 'javelin-dom', 2147 2153 'phabricator-draggable-list', 2148 - ), 2149 - 'e8240b50' => array( 2150 - 'javelin-behavior', 2151 - 'javelin-stratcom', 2152 - 'javelin-dom', 2153 2154 ), 2154 2155 'e9a2940f' => array( 2155 2156 'javelin-behavior',
+32 -14
src/applications/diffusion/view/DiffusionCloneURIView.php
··· 40 40 require_celerity_resource('diffusion-icons-css'); 41 41 42 42 Javelin::initBehavior('select-content'); 43 + Javelin::initBehavior('phabricator-clipboard-copy'); 43 44 44 45 $uri_id = celerity_generate_unique_node_id(); 45 46 ··· 53 54 'value' => $display, 54 55 'class' => 'diffusion-clone-uri', 55 56 'readonly' => 'true', 57 + 'sigil' => 'select-content', 58 + 'meta' => array( 59 + 'selectID' => $uri_id, 60 + 'once' => true, 61 + ), 56 62 )); 57 63 58 64 $uri = $this->getRepositoryURI(); ··· 71 77 break; 72 78 } 73 79 74 - $io = id(new PHUIButtonView()) 80 + $io = javelin_tag( 81 + 'span', 82 + array( 83 + 'class' => 'diffusion-clone-uri-io', 84 + 'sigil' => 'has-tooltip', 85 + 'meta' => array( 86 + 'tip' => $io_tip, 87 + ), 88 + ), 89 + id(new PHUIIconView())->setIcon($io_icon)); 90 + 91 + $copy = id(new PHUIButtonView()) 75 92 ->setTag('a') 76 93 ->setColor(PHUIButtonView::GREY) 77 - ->setIcon($io_icon) 94 + ->setIcon('fa-clipboard') 78 95 ->setHref('#') 79 - ->addSigil('select-content') 96 + ->addSigil('clipboard-copy') 80 97 ->addSigil('has-tooltip') 81 98 ->setMetadata( 82 99 array( 83 - 'tip' => $io_tip, 84 - 'selectID' => $uri_id, 100 + 'tip' => pht('Copy repository URI'), 101 + 'text' => $display, 102 + 'successMessage' => pht('Repository URI copied.'), 103 + 'errorMessage' => pht('Copy of Repository URI failed.'), 85 104 )); 86 105 87 106 switch ($uri->getEffectiveIOType()) { ··· 121 140 ->setHref($auth_uri) 122 141 ->setDisabled($auth_disabled); 123 142 124 - $cells = array(); 125 - $cells[] = phutil_tag('td', array(), $input); 126 - $cells[] = phutil_tag('th', array(), $io); 127 - $cells[] = phutil_tag('th', array(), $credentials); 128 - 129 - $row = phutil_tag('tr', array(), $cells); 143 + $elements = array(); 144 + $elements[] = $io; 145 + $elements[] = $input; 146 + $elements[] = $copy; 147 + $elements[] = $credentials; 130 148 131 149 return phutil_tag( 132 - 'table', 150 + 'div', 133 151 array( 134 - 'class' => 'diffusion-clone-uri-table', 152 + 'class' => 'diffusion-clone-uri-wrapper', 135 153 ), 136 - $row); 154 + $elements); 137 155 } 138 156 139 157 }
+2
src/applications/uiexample/examples/PHUIButtonExample.php
··· 115 115 $button->setMetadata( 116 116 array( 117 117 'text' => $copy, 118 + 'successMessage' => pht('Text copied into clipboard.'), 119 + 'errorMessage' => pht('Failed to copy text into clipboard.'), 118 120 )); 119 121 } 120 122
+21 -12
webroot/rsrc/css/application/diffusion/diffusion-icons.css
··· 2 2 * @provides diffusion-icons-css 3 3 */ 4 4 5 - input.diffusion-clone-uri { 6 - display: block; 7 - width: 100%; 8 - } 9 - 10 5 .diffusion-clone-extras { 11 6 font-size: 11px; 12 7 text-align: right; ··· 35 30 padding: 0; 36 31 } 37 32 38 - .diffusion-clone-uri-table { 39 - width: 100%; 33 + .diffusion-clone-uri-wrapper { 34 + display: flex; 40 35 } 41 36 42 - .diffusion-clone-uri-table th { 43 - width: 24px; 44 - padding: 0 0 0 4px; 37 + .diffusion-clone-uri-wrapper .diffusion-clone-uri-io { 38 + align-items: center; 39 + justify-content: center; 40 + border: 1px solid {$greyborder}; 41 + border-top-left-radius: 3px; 42 + border-bottom-left-radius: 3px; 43 + display: flex; 44 + width: 38px; 45 45 } 46 46 47 - .diffusion-clone-uri-table th a.button { 47 + .diffusion-clone-uri-wrapper a.button { 48 48 width: 12px; 49 49 height: 19px; 50 + margin-left: 4px; 50 51 } 51 52 52 - .diffusion-clone-uri-table th a.button .phui-icon-view { 53 + .diffusion-clone-uri-wrapper a.button .phui-icon-view { 53 54 left: 15px; 54 55 top: 7px; 55 56 } 57 + 58 + .diffusion-clone-uri-wrapper input.diffusion-clone-uri { 59 + border-left: none; 60 + border-top-left-radius: 0px; 61 + border-bottom-left-radius: 0px; 62 + display: block; 63 + width: 100%; 64 + }
+55 -15
webroot/rsrc/js/core/behavior-copy.js
··· 3 3 * @requires javelin-behavior 4 4 * javelin-dom 5 5 * javelin-stratcom 6 + * phabricator-notification 6 7 * @javelin 7 8 */ 8 9 9 10 JX.behavior('phabricator-clipboard-copy', function() { 10 11 11 - if (!document.queryCommandSupported) { 12 - return; 13 - } 12 + var fallback_working = document.queryCommandSupported && 13 + document.queryCommandSupported('copy'); 14 14 15 - if (!document.queryCommandSupported('copy')) { 15 + if (!navigator.clipboard && !fallback_working) { 16 16 return; 17 17 } 18 18 19 19 JX.DOM.alterClass(document.body, 'supports-clipboard', true); 20 20 21 - JX.Stratcom.listen('click', 'clipboard-copy', function(e) { 22 - e.kill(); 23 - 24 - var data = e.getNodeData('clipboard-copy'); 21 + var copy_fallback = function(text) { 25 22 var attr = { 26 - value: data.text || '', 23 + value: text || '', 27 24 className: 'clipboard-buffer' 28 25 }; 29 26 30 27 var node = JX.$N('textarea', attr); 31 28 document.body.appendChild(node); 32 29 33 - try { 34 - node.select(); 35 - document.execCommand('copy'); 36 - } catch (ignored) { 37 - // Ignore any errors we hit. 30 + node.select(); 31 + document.execCommand('copy'); 32 + 33 + JX.DOM.remove(node); 34 + }; 35 + 36 + var show_success_message = function(message) { 37 + if (!message) { 38 + return; 38 39 } 40 + new JX.Notification() 41 + .setContent(message) 42 + .alterClassName('jx-notification-done', true) 43 + .setDuration(8000) 44 + .show(); 45 + }; 39 46 40 - JX.DOM.remove(node); 47 + var show_error_message = function(message) { 48 + if (!message) { 49 + return; 50 + } 51 + new JX.Notification() 52 + .setContent(message) 53 + .alterClassName('jx-notification-error', true) 54 + .setDuration(8000) 55 + .show(); 56 + }; 57 + 58 + JX.Stratcom.listen('click', 'clipboard-copy', function(e) { 59 + var data = e.getNodeData('clipboard-copy'); 60 + var text = data.text || ''; 61 + 62 + var copy = async function( // jshint ignore:line 63 + text, 64 + successMessage, 65 + errorMessage 66 + ) { 67 + try { 68 + if (navigator.clipboard) { 69 + await navigator.clipboard.writeText(text); 70 + } else { 71 + copy_fallback(text); 72 + } 73 + show_success_message(successMessage); 74 + } catch (ex) { 75 + show_error_message(errorMessage); 76 + } 77 + }; 78 + 79 + e.kill(); 80 + copy(text, data.successMessage, data.errorMessage); 41 81 }); 42 82 43 83 });
+8
webroot/rsrc/js/core/behavior-select-content.js
··· 16 16 var node = e.getNode('select-content'); 17 17 var data = JX.Stratcom.getData(node); 18 18 19 + if (data.once && data.selected) { 20 + return; 21 + } 22 + 19 23 var target = JX.$(data.selectID); 20 24 JX.DOM.focus(target); 21 25 target.select(); 26 + 27 + if (data.once) { 28 + JX.Stratcom.addData(node, {selected: true}); 29 + } 22 30 }); 23 31 });