Adds button to copy API access key to clipboard.

Patch by Mizuki ISHIKAWA (user:ishikawa999).



git-svn-id: https://svn.redmine.org/redmine/trunk@23991 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Marius Balteanu
2025-09-20 07:41:50 +00:00
parent 7c2d967bb1
commit d39ff6e666
5 changed files with 101 additions and 12 deletions

View File

@@ -2547,3 +2547,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;
}

View File

@@ -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);
});
}
}

View File

@@ -20,17 +20,29 @@
<% if Setting.rest_api_enabled? %>
<h4><%= l(:label_api_access_key) %></h4>
<div>
<div data-controller="api-key-copy">
<div class="api-key-actions">
<%= link_to l(:button_show), my_api_key_path, :remote => true %>
<pre id='api-access-key' class='autoscroll'></pre>
<a class="copy-api-key-link icon icon-only"
title="<%= l(:button_copy) %>"
aria-label="<%= l(:button_copy) %>"
href="#"
role="button"
tabindex="0"
style="display: none;"
data-action="click->api-key-copy#copy">
<%= sprite_icon('copy') %>
</a>
</div>
<pre id='api-access-key' class='autoscroll' data-api-key-copy-target="apiKey"></pre>
<%= javascript_tag("$('#api-access-key').hide();") %>
<p>
<% 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 %>)
</p>
</div>
<%= javascript_tag("$('#api-access-key').hide();") %>
<p>
<% 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 %>)
</p>
<% end %>

View File

@@ -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();
}

View File

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