diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 08ad8bac4..b9981653e 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -2541,3 +2541,18 @@ th[role=columnheader]:not(.no-sort):hover:after { padding: 0.5rem; width: calc(200px - 0.5rem * 2); } + +.api-key-actions { + display: flex; + justify-content: space-between; + align-items: center; +} + +.api-key-actions .copy-api-key-link { + padding: 4px 6px; + cursor: pointer; +} + +#sidebar .api-key-actions .copy-api-key-link svg { + opacity: 1; +} diff --git a/app/javascript/controllers/api_key_copy_controller.js b/app/javascript/controllers/api_key_copy_controller.js new file mode 100644 index 000000000..286d0932f --- /dev/null +++ b/app/javascript/controllers/api_key_copy_controller.js @@ -0,0 +1,22 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = ["apiKey"]; + + copy(event) { + event.preventDefault(); + + const apiKeyText = this.apiKeyTarget.textContent?.trim(); + if (!apiKeyText) return; + + const svgIcon = event.target.closest('.copy-api-key-link').querySelector('svg') + if (!svgIcon) return; + + copyToClipboard(apiKeyText).then(() => { + updateSVGIcon(svgIcon, 'checked'); + setTimeout(() => { + updateSVGIcon(svgIcon, 'copy'); + }, 2000); + }); + } +} \ No newline at end of file diff --git a/app/views/my/_sidebar.html.erb b/app/views/my/_sidebar.html.erb index c2b64bd53..82a22cef2 100644 --- a/app/views/my/_sidebar.html.erb +++ b/app/views/my/_sidebar.html.erb @@ -20,17 +20,29 @@ <% if Setting.rest_api_enabled? %>
+ <% if @user.api_token %> + <%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %> + <% else %> + <%= l(:label_missing_api_access_key) %> + <% end %> + (<%= link_to l(:button_reset), my_api_key_path, :method => :post %>) +
-<% if @user.api_token %> -<%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %> -<% else %> -<%= l(:label_missing_api_access_key) %> -<% end %> -(<%= link_to l(:button_reset), my_api_key_path, :method => :post %>) -
<% end %> diff --git a/app/views/my/show_api_key.js.erb b/app/views/my/show_api_key.js.erb index 73b0ee029..f97ca15f5 100644 --- a/app/views/my/show_api_key.js.erb +++ b/app/views/my/show_api_key.js.erb @@ -1 +1,7 @@ $('#api-access-key').html('<%= escape_javascript @user.api_key %>').toggle(); + +if ($('#api-access-key').is(':visible')) { + $('.api-key-actions .copy-api-key-link').show(); +} else { + $('.api-key-actions .copy-api-key-link').hide(); +} diff --git a/test/system/api_key_copy_test.rb b/test/system/api_key_copy_test.rb new file mode 100644 index 000000000..d5d10bf2f --- /dev/null +++ b/test/system/api_key_copy_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative '../application_system_test_case' + +class ApiKeyCopySystemTest < ApplicationSystemTestCase + def test_api_key_copy_to_clipboard + with_settings :rest_api_enabled => '1' do + log_user('jsmith', 'jsmith') + + user = User.find_by_login('jsmith') + expected_value = user.api_key + + visit '/my/account' + click_link 'Show' + + assert_selector '#api-access-key', visible: true + assert_selector '.api-key-actions .copy-api-key-link', visible: true + assert_equal expected_value, find('#api-access-key').text.strip + + find('.copy-api-key-link').click + + find('#quick-search input').set('') + find('#quick-search input').send_keys([modifier_key, 'v']) + assert_equal expected_value, find('#quick-search input').value + end + end + + private + + def modifier_key + modifier = osx? ? 'command' : 'control' + modifier.to_sym + end +end