@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

Add a Copy button to Personal API Token dialog

Summary:
Allow clicking once to copy a created API token to be used in some external application anyway, instead of having to click into the text field and select text and copy it.

Note that
* `.aphront-form-input` hardcodes `margin-left: 20%; margin-right: 20%; width: 60%;`; `AphrontFormTextControl` or `AphrontFormTextAreaControl` operate within these 60%, and fill that via `.aphront-form-input input[type="text"], .aphront-form-input input[type="password"] { width: 100%; }` - this patch reduces those 100% to 85% to create enough space for the Copy Button not to end up in a separate line.
* this CSS is also applied to `.aphront-form-input.has-copy-button textarea` which is currently unused but would be needed to add a Copy button also to `PhabricatorOAuthClientSecretController` and `PassphraseCredentialRevealController`.
* `margin-left: 4px;` is also used by the same button in DiffusionCloneURIView already.
* this also makes existing `errorMessage`s across the codebase more generic if copying ever failed. Less work for translators.

Closes T16197

Test Plan:
* Go to http://phorge.localhost/settings/user/testadmin/page/apitokens/
* Click "Generate Token"
* See a Copy button next to the "API Token" text field; hover over it, click it, paste, etc
* Go back to overview at http://phorge.localhost/settings/user/testadmin/page/apitokens/, click on a token
* Repeat step 3

Reviewers: O1 Blessed Committers, mainframe98

Reviewed By: O1 Blessed Committers, mainframe98

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

Maniphest Tasks: T16197

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

+85 -7
+3 -3
resources/celerity/map.php
··· 9 9 'names' => array( 10 10 'conpherence.pkg.css' => 'b2d6f4b8', 11 11 'conpherence.pkg.js' => '020aebcf', 12 - 'core.pkg.css' => '8ee41b37', 12 + 'core.pkg.css' => '5fb9829e', 13 13 'core.pkg.js' => '83c66b30', 14 14 'dark-console.pkg.js' => '187792c2', 15 15 'differential.pkg.css' => '0dac8831', ··· 147 147 'rsrc/css/phui/phui-document.css' => 'a9154763', 148 148 'rsrc/css/phui/phui-feed-story.css' => '1dd2e4c0', 149 149 'rsrc/css/phui/phui-fontkit.css' => '1ec937e5', 150 - 'rsrc/css/phui/phui-form-view.css' => '029b1ef9', 150 + 'rsrc/css/phui/phui-form-view.css' => '0febcbf6', 151 151 'rsrc/css/phui/phui-form.css' => '0ce5b5b8', 152 152 'rsrc/css/phui/phui-formation-view.css' => 'b172a0b3', 153 153 'rsrc/css/phui/phui-head-thing.css' => 'd7f293df', ··· 775 775 'phui-font-icon-base-css' => 'b7608e58', 776 776 'phui-fontkit-css' => '1ec937e5', 777 777 'phui-form-css' => '0ce5b5b8', 778 - 'phui-form-view-css' => '029b1ef9', 778 + 'phui-form-view-css' => '0febcbf6', 779 779 'phui-formation-view-css' => 'b172a0b3', 780 780 'phui-head-thing-view-css' => 'd7f293df', 781 781 'phui-header-view-css' => '521ef411',
+1
src/applications/conduit/controller/PhabricatorConduitTokenEditController.php
··· 96 96 ->setLabel(pht('Token')) 97 97 ->setReadOnly(true) 98 98 ->setSigil('select-on-click') 99 + ->setHasCopyButton(true) 99 100 ->setValue($token->getToken())); 100 101 101 102 $dialog->appendForm($form);
+1 -1
src/applications/differential/view/DifferentialChangesetDetailView.php
··· 246 246 'tip' => pht('Copy file path'), 247 247 'text' => $display_filename, 248 248 'successMessage' => pht('File path copied.'), 249 - 'errorMessage' => pht('Copy of file path failed.'), 249 + 'errorMessage' => pht('Copying failed.'), 250 250 )); 251 251 252 252 return javelin_tag(
+1 -1
src/applications/diffusion/view/DiffusionCloneURIView.php
··· 100 100 'tip' => pht('Copy repository URI'), 101 101 'text' => $display, 102 102 'successMessage' => pht('Repository URI copied.'), 103 - 'errorMessage' => pht('Copy of Repository URI failed.'), 103 + 'errorMessage' => pht('Copying failed.'), 104 104 )); 105 105 106 106 switch ($uri->getEffectiveIOType()) {
+34 -1
src/view/form/control/AphrontFormControl.php
··· 15 15 private $required; 16 16 private $hidden; 17 17 private $classes; 18 + private $hasCopyButton; 18 19 19 20 public function setHidden($hidden) { 20 21 $this->hidden = $hidden; ··· 114 115 return $this; 115 116 } 116 117 118 + /** 119 + * Whether a button is displayed next to the control which allows the user to 120 + * copy the value in the form control. Common use cases include <input> 121 + * (AphrontFormTextControl) and <textarea> (AphrontFormTextAreaControl) 122 + * elements displaying read-only data such as tokens or passphrases. This is 123 + * only to get the CSS right; actual button implementation is in subclasses. 124 + * 125 + * @return bool 126 + */ 127 + public function getHasCopyButton() { 128 + return $this->hasCopyButton; 129 + } 130 + 131 + /** 132 + * Whether to display a button next to the control which allows the user to 133 + * copy the value in the form control. Common use cases include <input> 134 + * (AphrontFormTextControl) and <textarea> (AphrontFormTextAreaControl) 135 + * elements displaying read-only data such as tokens or passphrases. This is 136 + * only to get the CSS right; actual button implementation is in subclasses. 137 + * 138 + * @param bool $has_copy_button 139 + */ 140 + public function setHasCopyButton($has_copy_button) { 141 + $this->hasCopyButton = $has_copy_button; 142 + return $this; 143 + } 144 + 117 145 public function getValue() { 118 146 return $this->value; 119 147 } ··· 192 220 $this->setID(celerity_generate_unique_node_id()); 193 221 } 194 222 223 + $class = 'aphront-form-input'; 224 + if ($this->getHasCopyButton()) { 225 + $class = $class.' has-copy-button'; 226 + } 227 + 195 228 $input = phutil_tag( 196 229 'div', 197 - array('class' => 'aphront-form-input'), 230 + array('class' => $class), 198 231 $this->renderInput()); 199 232 200 233 $error = null;
+34 -1
src/view/form/control/AphrontFormTextControl.php
··· 58 58 } 59 59 60 60 protected function renderInput() { 61 - return javelin_tag( 61 + $input = array(); 62 + $input[] = javelin_tag( 62 63 'input', 63 64 array( 64 65 'type' => 'text', ··· 72 73 'placeholder' => $this->getPlaceholder(), 73 74 'autofocus' => ($this->getAutofocus() ? 'autofocus' : null), 74 75 )); 76 + 77 + if ($this->getHasCopyButton()) { 78 + Javelin::initBehavior('select-content'); 79 + Javelin::initBehavior('phabricator-clipboard-copy'); 80 + Javelin::initBehavior('phabricator-tooltips'); 81 + 82 + $field_label = $this->getLabel(); 83 + if (phutil_nonempty_string($field_label)) { 84 + // TODO: 'Copy %s' is broken i18n as it ignores grammatical case 85 + $tip_message = pht('Copy %s', $field_label); 86 + $success_message = pht('%s copied.', $field_label); 87 + } else { 88 + $tip_message = pht('Copy text'); 89 + $success_message = pht('Text copied.'); 90 + } 91 + $copy = id(new PHUIButtonView()) 92 + ->setTag('a') 93 + ->setColor(PHUIButtonView::GREY) 94 + ->setIcon('fa-clipboard') 95 + ->setHref('#') 96 + ->addSigil('clipboard-copy') 97 + ->addSigil('has-tooltip') 98 + ->setMetadata( 99 + array( 100 + 'tip' => $tip_message, 101 + 'text' => $this->getValue(), 102 + 'successMessage' => $success_message, 103 + 'errorMessage' => pht('Copying failed.'), 104 + )); 105 + $input[] = $copy; 106 + } 107 + return $input; 75 108 } 76 109 77 110 }
+11
webroot/rsrc/css/phui/phui-form-view.css
··· 104 104 width: 100%; 105 105 } 106 106 107 + /* Reduce from 100% to 85% to avoid linebreak when displaying a Copy button */ 108 + .aphront-form-input.has-copy-button input[type="text"], 109 + .aphront-form-input.has-copy-button textarea { 110 + width: 85%; 111 + } 112 + 113 + .aphront-form-input.has-copy-button .button { 114 + vertical-align: middle; 115 + margin-left: 4px; 116 + } 117 + 107 118 .aphront-form-cvc-input input { 108 119 width: 64px; 109 120 }