Use BorderBoxTable to render access token information

This is part of primerizing the access tokens view and
makes everything look more consistent.
This commit is contained in:
Jan Sandbrink
2026-01-08 15:31:57 +01:00
parent 91104f57f7
commit 478b24af4b
25 changed files with 653 additions and 402 deletions
@@ -67,7 +67,7 @@ See COPYRIGHT and LICENSE files for more details.
end
flex.with_row do
render(Primer::Alpha::Banner.new(scheme: :warning, icon: :alert)) do
I18n.t(:warning, scope: i18n_scope)
I18n.t("my.access_token.created_dialog.one_time_warning")
end
end
end
@@ -23,7 +23,7 @@
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
@@ -31,14 +31,14 @@
module My
module AccessToken
module API
class RowComponent < ::RowComponent
class RowComponent < OpPrimer::BorderBoxRowComponent
def api_token
model
end
def token_name
if api_token.token_name.nil?
t("my_account.access_tokens.api.static_token_name")
if !api_token.respond_to?(:token_name) || api_token.token_name.nil?
t(:static_token_name, scope: i18n_token_scope)
else
api_token.token_name
end
@@ -57,27 +57,33 @@ module My
end
def delete_link
link_to "",
{
action: delete_action,
access_token_id: api_token.id
},
data: {
turbo_method: :delete,
turbo_confirm: t("my_account.access_tokens.simple_revoke_confirmation"),
test_selector: "api-token-revoke"
},
class: "icon icon-delete"
render(Primer::Beta::IconButton.new(
icon: :trash,
scheme: :danger,
tag: :a,
href: delete_path,
"aria-label": t(:button_delete),
test_selector: "api-token-revoke",
data: {
turbo_method: :delete,
turbo_confirm: t("my_account.access_tokens.simple_revoke_confirmation")
}
))
end
private
def delete_action
def delete_path
case model
when Token::API then :revoke_api_key
when Token::ICalMeeting then :revoke_ical_meeting_token
when Token::API then my_access_token_revoke_api_key_path(api_token.id)
when Token::ICalMeeting then my_access_token_revoke_ical_meeting_token_path(api_token.id)
when Token::RSS then revoke_rss_key_my_access_tokens_path(api_token.id)
end
end
def i18n_token_scope
[:my_account, :access_tokens, api_token.class.model_name.i18n_key]
end
end
end
end
@@ -31,25 +31,54 @@
module My
module AccessToken
module API
class TableComponent < ::TableComponent
def initial_sort
%i[id asc]
end
class TableComponent < OpPrimer::BorderBoxTableComponent
columns :token_name, :created_at, :expires_on
main_column :token_name
mobile_labels :created_at, :expires_on
def sortable?
false
def initialize(title:, token_type:, **)
super(**)
@title = title
@token_type = token_type
end
def headers
[
["token_name", { caption: I18n.t("attributes.name") }],
["created_at", { caption: User.human_attribute_name(:created_at) }],
["expires_on", { caption: I18n.t("my_account.access_tokens.headers.expiration") }]
[:token_name, { caption: I18n.t("attributes.name") }],
[:created_at, { caption: User.human_attribute_name(:created_at) }],
[:expires_on, { caption: I18n.t("my_account.access_tokens.headers.expiration") }]
]
end
def columns
headers.map(&:first)
def mobile_title
@title
end
def row_class
RowComponent
end
def has_actions?
true
end
def blank_title
I18n.t(:blank_title, scope: i18n_token_scope)
end
def blank_description
I18n.t(:blank_description, scope: i18n_token_scope)
end
def blank_icon
nil
end
private
def i18n_token_scope
[:my_account, :access_tokens, @token_type.model_name.i18n_key]
end
end
end
@@ -38,25 +38,25 @@ See COPYRIGHT and LICENSE files for more details.
end %>
<% if token_available? %>
<% if @tokens.any? %>
<%= render My::AccessToken::API::TableComponent.new(rows: @tokens) %>
<%= render My::AccessToken::API::TableComponent.new(rows: @tokens, title: t(:table_title, scope: i18n_scope), token_type:) %>
<% if show_add_button? %>
<%= render(
Primer::Beta::Button.new(
tag: :a,
mt: 3,
scheme: :secondary,
test_selector: "#{token_type.model_name.element}-token-add",
href: add_button_path,
data: { turbo_stream: true, turbo_method: add_button_method },
aria: { label: t(:add_button, scope: i18n_scope) },
role: "button",
title: t(:add_button, scope: i18n_scope)
)
) do |button|
button.with_leading_visual_icon(icon: add_button_icon)
t(:add_button, scope: i18n_scope)
end %>
<% end %>
<%= render(
Primer::Beta::Button.new(
tag: :a,
mt: 3,
scheme: :secondary,
test_selector: "#{token_type.model_name.element}-token-add",
href: dialog_my_access_tokens_path(token_type: token_type.model_name.element),
data: { turbo_stream: true },
aria: { label: t(:add_button, scope: i18n_scope) },
role: "button",
title: t(:add_button, scope: i18n_scope)
)
) do |button|
button.with_leading_visual_icon(icon: add_button_icon)
t(:add_button, scope: i18n_scope)
end %>
<% else %>
<div tabindex="0" class="-info op-toast">
<div role="alert" aria-atomic="true" class="op-toast--content">
@@ -58,16 +58,37 @@ module My
case token_type.to_s
when "Token::API" then Setting.rest_api_enabled?
when "Token::ICalMeeting" then Setting.ical_enabled?
when "Token::RSS" then Setting.feeds_enabled?
else raise ArgumentError, "Unknown token type: #{token_type}"
end
end
def show_add_button?
return @tokens.empty? if token_type.to_s == "Token::RSS"
true
end
def add_button_icon
case token_type.to_s
when "Token::ICalMeeting" then :rss
when "Token::RSS", "Token::ICalMeeting" then :rss
else :plus
end
end
def add_button_method
case token_type.to_s
when "Token::RSS" then :post
else :get
end
end
def add_button_path
case token_type.to_s
when "Token::RSS" then generate_rss_key_my_access_tokens_path
else dialog_my_access_tokens_path(token_type: token_type.model_name.element)
end
end
end
end
end
@@ -0,0 +1,89 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module My
module AccessToken
module ICal
class RowComponent < OpPrimer::BorderBoxRowComponent
def api_token
model
end
def name
render(Primer::Beta::Text.new(test_selector: "ical-token-#{api_token.id}-name")) do
api_token.ical_token_query_assignment.name
end
end
def calendar
render(
Primer::Beta::Link.new(
href: project_calendar_path(id: api_token.query.id, project_id: api_token.query.project_id),
test_selector: "ical-token-#{api_token.id}-query-name"
)
) { api_token.query.name }
end
def project
render(Primer::Beta::Text.new(test_selector: "ical-token-#{api_token.id}-project-name")) do
api_token.query.project.name
end
end
def created_at
helpers.format_time(api_token.created_at)
end
def expires_on
I18n.t("my_account.access_tokens.indefinite_expiration")
end
def button_links
[delete_link].compact
end
def delete_link
render(Primer::Beta::IconButton.new(
icon: :trash,
scheme: :danger,
tag: :a,
href: my_access_token_revoke_ical_token_path(access_token_id: api_token.id),
"aria-label": t(:button_delete),
test_selector: "ical-token-#{api_token.id}-revoke",
data: {
turbo_method: :delete,
turbo_confirm: t("my_account.access_tokens.simple_revoke_confirmation")
}
))
end
end
end
end
end
@@ -0,0 +1,75 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module My
module AccessToken
module ICal
class TableComponent < OpPrimer::BorderBoxTableComponent
columns :name, :calendar, :project, :created_at, :expires_on
main_column :name
mobile_labels :created_at, :expires_on
def headers
[
[:name, { caption: I18n.t("attributes.name") }],
[:calendar, { caption: Token::ICal.human_attribute_name(:calendar) }],
[:project, { caption: WorkPackage.human_attribute_name(:project) }],
[:created_at, { caption: User.human_attribute_name(:created_at) }],
[:expires_on, { caption: I18n.t("my_account.access_tokens.headers.expiration") }]
]
end
def mobile_title
I18n.t("my_account.access_tokens.ical.table_title")
end
def row_class
RowComponent
end
def has_actions?
true
end
def blank_title
I18n.t("my_account.access_tokens.ical.blank_title")
end
def blank_description
I18n.t("my_account.access_tokens.ical.blank_description")
end
def blank_icon
nil
end
end
end
end
end
@@ -0,0 +1,88 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module My
module AccessToken
module OAuthApplication
class RowComponent < OpPrimer::BorderBoxRowComponent
def oauth_application
model.first
end
def oauth_application_tokens
model.last
end
def name
render(Primer::Beta::Text.new(test_selector: "oauth-application-#{oauth_application.id}-name")) do
oauth_application.name
end
end
def active_tokens
render(Primer::Beta::Text.new(test_selector: "oauth-application-#{oauth_application.id}-active-tokens")) do
oauth_application_tokens.count { |t| !t.expired? && !t.revoked? }.to_s
end
end
def last_used_at
return "" if oauth_application_tokens.empty?
helpers.format_time(oauth_application_tokens.max_by(&:created_at).created_at)
end
def button_links
[delete_link].compact
end
def delete_link
render(Primer::Beta::IconButton.new(
icon: :trash,
scheme: :danger,
tag: :a,
href: revoke_my_oauth_application_path(application_id: oauth_application.id),
"aria-label": t(:button_delete),
test_selector: "oauth-token-row-#{oauth_application.id}-revoke",
data: {
turbo_method: :post,
turbo_confirm: t(
"oauth.revoke_my_application_confirmation",
token_count: t(
"oauth.x_active_tokens",
count: oauth_application_tokens.count
)
)
}
))
end
end
end
end
end
@@ -0,0 +1,73 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module My
module AccessToken
module OAuthApplication
class TableComponent < OpPrimer::BorderBoxTableComponent
columns :name, :active_tokens, :last_used_at
main_column :name
mobile_labels :active_tokens, :last_used_at
def headers
[
[:name, { caption: I18n.t("attributes.name") }],
[:active_tokens, { caption: I18n.t("my_account.access_tokens.oauth.active_tokens") }],
[:last_used_at, { caption: I18n.t("my_account.access_tokens.oauth.last_used_at") }]
]
end
def mobile_title
I18n.t("my_account.access_tokens.oauth.table_title")
end
def row_class
RowComponent
end
def has_actions?
true
end
def blank_title
I18n.t("my_account.access_tokens.oauth.blank_title")
end
def blank_description
I18n.t("my_account.access_tokens.oauth.blank_description")
end
def blank_icon
nil
end
end
end
end
end
@@ -0,0 +1,75 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module My
module AccessToken
module Storages
class RowComponent < OpPrimer::BorderBoxRowComponent
def client_token
model
end
def name
client_token.oauth_client.integration.name
end
def created_at
helpers.format_time(client_token.created_at)
end
def expires_on
helpers.format_time(client_token.updated_at + client_token.expires_in.seconds)
end
def button_links
[delete_link].compact
end
def delete_link
render(Primer::Beta::IconButton.new(
icon: :trash,
scheme: :danger,
tag: :a,
href: my_access_token_revoke_storage_token_path(client_token),
"aria-label": t(:button_delete),
test_selector: "storages-token-row-#{client_token.id}-revoke",
data: {
turbo_method: :delete,
turbo_confirm: t(
"my_account.access_tokens.storages.revoke_token",
storage: client_token.oauth_client.integration.name
)
}
))
end
end
end
end
end
@@ -0,0 +1,73 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module My
module AccessToken
module Storages
class TableComponent < OpPrimer::BorderBoxTableComponent
columns :name, :created_at, :expires_on
main_column :name
mobile_labels :created_at, :expires_on
def headers
[
[:name, { caption: I18n.t("attributes.name") }],
[:created_at, { caption: User.human_attribute_name(:created_at) }],
[:expires_on, { caption: I18n.t("my_account.access_tokens.headers.expiration") }]
]
end
def mobile_title
I18n.t("my_account.access_tokens.storages.table_title")
end
def row_class
RowComponent
end
def has_actions?
true
end
def blank_title
I18n.t("my_account.access_tokens.storages.blank_title")
end
def blank_description
I18n.t("my_account.access_tokens.storages.blank_description")
end
def blank_icon
nil
end
end
end
end
end
@@ -80,15 +80,14 @@ module My
def generate_rss_key # rubocop:disable Metrics/AbcSize
token = Token::RSS.create!(user: current_user)
flash[:info] = [
t("my.access_token.notice_reset_token", type: "RSS").html_safe,
helpers.content_tag(:strong, helpers.content_tag(:code, token.plain_value)),
t("my.access_token.token_value_warning")
]
update_via_turbo_stream(
component: My::AccessToken::APITokensSectionComponent.new(tokens: [token], token_type: Token::RSS)
)
respond_with_dialog(My::AccessToken::AccessTokenCreatedDialogComponent.new(token:))
rescue StandardError => e
Rails.logger.error "Failed to reset user ##{current_user.id} RSS key: #{e}"
flash[:error] = t("my.access_token.failed_to_reset_token", error: e.message)
ensure
redirect_to action: :index, status: :see_other
end
@@ -41,62 +41,7 @@ See COPYRIGHT and LICENSE files for more details.
end
end %>
<% if Setting.ical_enabled? %>
<% if ical_tokens_grouped_by_query.any? %>
<div class="generic-table--container">
<div class="generic-table--results-container">
<table class="generic-table" data-controller="table-highlighting">
<%= render partial: "token_table_header",
locals: {
column_headers: [
t("attributes.name"),
Token::ICal.human_attribute_name(:calendar),
WorkPackage.human_attribute_name(:project),
User.human_attribute_name(:created_at),
t("my_account.access_tokens.headers.expiration")
]
} %>
<tbody>
<% ical_tokens_grouped_by_query.each do |query_id, tokens| %>
<% tokens.sort_by(&:created_at).each do |token| %>
<tr>
<td class="-w-rel-20 -mw-abs-200" data-test-selector="ical-token-row-<%= token.id %>-name"><%= token.ical_token_query_assignment.name %></td>
<td class="-w-rel-20 -mw-abs-200" data-test-selector="ical-token-row-<%= token.id %>-query-name">
<%= link_to token.query.name,
project_calendar_url(
id: query_id,
project_id: token.query.project_id
) %>
</td>
<td class="-w-rel-20 -mw-abs-200" data-test-selector="ical-token-row-<%= token.id %>-project-name">
<%= token.query.project.name %>
</td>
<td>
<span title="<%= format_time(token.created_at) %>">
<%= format_time(token.created_at.to_s) %>
</span>
</td>
<td><%= t("my_account.access_tokens.indefinite_expiration") %></td>
<td class="buttons">
<%= link_to "",
{ action: "revoke_ical_token", access_token_id: token.id },
data: {
turbo_method: :delete,
turbo_confirm: t("my_account.access_tokens.simple_revoke_confirmation"),
test_selector: "ical-token-row-#{token.id}-revoke"
},
class: "icon icon-delete" %>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<%= render Primer::Beta::Octicon.new(icon: :info, mr: 1) %>
<i><%= t("my_account.access_tokens.ical.empty_text_hint") %></i>
<% end %>
<%= render(My::AccessToken::ICal::TableComponent.new(rows: ical_tokens_grouped_by_query.values.flatten)) %>
<% else %>
<div tabindex="0" class="-info op-toast">
<div role="alert" aria-atomic="true" class="op-toast--content">
@@ -35,57 +35,6 @@ See COPYRIGHT and LICENSE files for more details.
t("my_account.access_tokens.oauth.text_hint")
end
end %>
<% if granted_applications.any? %>
<div class="generic-table--container">
<div class="generic-table--results-container">
<table class="generic-table" data-controller="table-highlighting">
<%= render partial: "token_table_header",
locals: {
column_headers: [
t("attributes.name"),
User.human_attribute_name(:created_at),
t("my_account.access_tokens.headers.expiration")
]
} %>
<tbody>
<% granted_applications.each do |application, tokens| %>
<% latest = tokens.max_by(&:created_at) %>
<tr id="oauth-application-grant-<%= application.id %>">
<td class="-w-rel-60" data-test-selector="oauth-token-row-<%= application.id %>-name">
<%= t("oauth.application.named", name: application.name) %>
&nbsp;
(<%= t("oauth.x_active_tokens", count: tokens.count) %>)
</td>
<td>
<span><%= format_time(latest.created_at) %></span>
</td>
<td>
<span><%= format_time(latest.created_at + latest.expires_in.seconds) %></span>
</td>
<td class="buttons">
<%= link_to "",
revoke_my_oauth_application_path(application_id: application.id),
data: {
turbo_method: :post,
turbo_confirm: t(
"oauth.revoke_my_application_confirmation",
token_count: t(
"oauth.x_active_tokens",
count: tokens.count
)
),
test_selector: "oauth-token-row-#{application.id}-revoke"
},
class: "icon icon-delete" %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<%= render Primer::Beta::Octicon.new(icon: :info, mr: 1) %>
<i><%= t("my_account.access_tokens.oauth.empty_text_hint") %></i>
<% end %>
<%= render(My::AccessToken::OAuthApplication::TableComponent.new(rows: granted_applications)) %>
</div>
@@ -1,92 +0,0 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<div class="attributes-group" id="rss-token-section">
<%= render(Primer::Beta::Subhead.new) do |component|
component.with_heading(size: :medium) do
t("my_account.access_tokens.rss.title")
end
component.with_description do
t("my_account.access_tokens.rss.text_hint")
end
end %>
<% if Setting.feeds_enabled? %>
<% if rss_token %>
<div class="generic-table--container">
<div class="generic-table--results-container">
<table class="generic-table" data-controller="table-highlighting">
<%= render partial: "token_table_header",
locals: {
column_headers: [
t("attributes.name"),
User.human_attribute_name(:created_at),
t("my_account.access_tokens.headers.expiration")
]
} %>
<tbody>
<tr>
<td class="-w-rel-60" data-test-selector="rss-token-row-name"><%= t("my_account.access_tokens.rss.static_token_name") %></td>
<td>
<span title="<%= format_time(rss_token.created_at) %>">
<%= format_time(rss_token.created_at.to_s) %>
</span>
</td>
<td><%= t("my_account.access_tokens.indefinite_expiration") %></td>
<td class="buttons">
<%= link_to "",
{ action: "revoke_rss_key" },
data: {
turbo_method: :delete,
turbo_confirm: t("my_account.access_tokens.simple_revoke_confirmation"),
test_selector: "rss-token-revoke"
},
class: "icon icon-delete" %>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<% else %>
<%=
render Primer::Beta::Link.new(href: generate_rss_key_my_access_tokens_path, data: { turbo_method: :post }, test_selector: "rss-token-add") do |link|
link.with_leading_visual_icon(icon: :plus)
t("my_account.access_tokens.rss.static_token_name")
end
%>
<% end %>
<% else %>
<div tabindex="0" class="-info op-toast">
<div role="alert" aria-atomic="true" class="op-toast--content">
<p>
<span><%= t("my_account.access_tokens.rss.disabled_text") %></span>
</p>
</div>
</div>
<% end %>
</div>
@@ -35,48 +35,6 @@ See COPYRIGHT and LICENSE files for more details.
t("my_account.access_tokens.storages.text_hint")
end
end %>
<% if @storage_tokens.any? %>
<div class="generic-table--container">
<div class="generic-table--results-container">
<table class="generic-table" data-controller="table-highlighting">
<%= render partial: "token_table_header",
locals: {
column_headers: [
t("attributes.name"),
User.human_attribute_name(:created_at),
t("my_account.access_tokens.headers.expiration")
]
} %>
<tbody>
<% storage_tokens.each do |token| %>
<tr id="storage-oauth-token-<%= token.id %>">
<td class="-w-rel-60" data-test-selector="oauth-token-row-<%= token.oauth_client.integration.id %>-name">
<%= token.oauth_client.integration.name %>
</td>
<td>
<span><%= format_time(token.created_at) %></span>
</td>
<td>
<span><%= format_time(token.updated_at + token.expires_in.seconds) %></span>
</td>
<td class="buttons">
<%= link_to "",
my_access_token_revoke_storage_token_path(token),
data: {
turbo_method: :delete,
turbo_confirm: t("my_account.access_tokens.storages.revoke_token", storage: token.oauth_client.integration.name),
test_selector: "storages-token-row-#{token.id}-revoke"
},
class: "icon icon-delete" %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<%= render Primer::Beta::Octicon.new(icon: :info, mr: 1) %>
<i><%= t("my_account.access_tokens.storages.empty_text_hint") %></i>
<% end %>
<%= render(My::AccessToken::Storages::TableComponent.new(rows: @storage_tokens)) %>
</div>
@@ -1,52 +0,0 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<colgroup>
<% column_headers&.length&.times do %>
<col>
<% end %>
<col>
</colgroup>
<thead>
<tr>
<% column_headers&.each do |column_header| %>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<%= column_header %>
</div>
</div>
</th>
<% end %>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header"></div>
</div>
</th>
</tr>
</thead>
+1 -1
View File
@@ -43,7 +43,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= render partial: "icalendar_tokens_section", locals: { ical_tokens_grouped_by_query: @ical_tokens_grouped_by_query } %>
<%= render ::My::AccessToken::APITokensSectionComponent.new(tokens: @user.ical_meeting_tokens, token_type: Token::ICalMeeting) %>
<%= render partial: "oauth_tokens_section", locals: { granted_applications: granted_applications } %>
<%= render partial: "rss_tokens_section", locals: { rss_token: @user.rss_token } %>
<%= render ::My::AccessToken::APITokensSectionComponent.new(tokens: Array(@user.rss_token), token_type: Token::RSS) %>
<%= render partial: "storage_tokens_section", locals: { storage_tokens: @storage_tokens } %>
<div class="generic-table--container">
<div class="generic-table--results-container">
+26 -7
View File
@@ -764,9 +764,11 @@ en:
create_button: "Create"
name_label: "Token name"
created_dialog:
one_time_warning: "This is the only time you will see this token. Make sure to copy it now."
token/api:
title: "The API token has been generated"
warning: "This is the only time you will see this token. Make sure to copy it now."
token/rss:
title: "The RSS token has been generated"
failed_to_reset_token: "Failed to reset access token: %{error}"
failed_to_create_token: "Failed to create access token: %{error}"
failed_to_revoke_token: "Failed to revoke access token: %{error}"
@@ -2022,6 +2024,9 @@ en:
token/api:
one: Access token
other: Access tokens
token/rss:
one: "RSS token"
other: "RSS tokens"
type:
one: "Type"
other: "Types"
@@ -3068,30 +3073,44 @@ en:
indefinite_expiration: "Never"
simple_revoke_confirmation: "Are you sure you want to revoke this token?"
token/api:
blank_description: "There is no API token yet. You can create one using the button below."
blank_title: "No API token"
title: "API"
table_title: "API tokens"
text_hint: "API tokens allow third-party applications to communicate with this OpenProject instance via REST APIs."
static_token_name: "API token"
disabled_text: "API tokens are not enabled by the administrator. Please contact your administrator to use this feature."
add_button: "API Token"
api:
static_token_name: "API token"
ical:
blank_description: "To add an iCalendar token, subscribe to a new or existing calendar from within the Calendar module of a project. You must have the necessary permissions."
blank_title: "No iCalendar token"
title: "iCalendar"
table_title: "iCalendar tokens"
text_hint_link: "iCalendar tokens allow users to [subscribe to OpenProject calendars](docs_url) and view up-to-date work package information from external clients."
disabled_text: "iCalendar subscriptions are not enabled by the administrator. Please contact your administrator to use this feature."
empty_text_hint: "To add an iCalendar token, subscribe to a new or existing calendar from within the Calendar module of a project. You must have the necessary permissions."
oauth:
active_tokens: "Active tokens"
blank_description: "There is no third-party application access configured and active for you. Please contact your administrator to activate this feature."
blank_title: "No OAuth token"
last_used_at: "Last used at"
title: "OAuth"
table_title: "OAuth tokens"
text_hint: "OAuth tokens allow third-party applications to connect with this OpenProject instance."
empty_text_hint: "There is no third-party application access configured and active for you. Please contact your administrator to activate this feature."
rss:
token/rss:
add_button: "RSS Token"
blank_description: "There is no RSS token yet. You can create one using the button below."
blank_title: "No RSS token"
title: "RSS"
table_title: "RSS tokens"
text_hint: "RSS tokens allow users to keep up with the latest changes in this OpenProject instance via an external RSS reader."
static_token_name: "RSS token"
disabled_text: "RSS tokens are not enabled by the administrator. Please contact your administrator to use this feature."
storages:
blank_description: "There is no storage access linked to your account."
blank_title: "No file storage tokens"
title: "File storages"
table_title: "File storage tokens"
text_hint: "File Storage tokens connect this OpenProject instance with an external File Storage."
empty_text_hint: "There is no storage access linked to your account."
revoke_token: "Do you really want to remove this token? You will need to login again on %{storage}"
removed: "File Storage token successfully removed"
failed: "An error occurred and the token couldn't be removed. Please try again later."
@@ -121,8 +121,7 @@ RSpec.describe "authorization for BCF api",
visit my_account_path
click_on "Access token"
expect(page).to have_css("#oauth-application-grant-#{app.id}", text: app.name)
expect(page).to have_css("td", text: app.name)
expect(page).to have_test_selector("oauth-application-#{app.id}-name", text: app.name)
# While being logged in, the api can be accessed with the session
visit("/api/bcf/2.1/projects/#{project.id}")
+3 -2
View File
@@ -683,10 +683,12 @@ en:
my_account:
access_tokens:
token/ical_meeting:
blank_description: "You can create one using the button below."
blank_title: "No iCalendar meeting token"
title: "iCalendar for meetings"
table_title: "iCalendar meeting tokens"
text_hint: "iCalendar meeting tokens allow users to subscribe to all their meetings and view up-to-date meeting information in external clients."
disabled_text: "iCalendar meeting subscriptions are not enabled by the administrator. Please contact your administrator to use this feature."
empty_text_hint: 'To add an iCalendar meeting token, go to the <a href="%{path}">meetings section</a> and subscribe from there.'
add_button: "Subscribe to calendar"
my:
@@ -702,7 +704,6 @@ en:
token/ical_meeting:
title: "An iCal meeting subscription token has been generated"
body: "Treat the following URL as you would a password. Anyone who has access to it can view all your meetings."
warning: "This is the only time you will see this token. Make sure to copy it now."
revocation:
token/ical_meeting:
notice_success: "The iCalendar meeting subscription has been revoked successfully."
@@ -41,12 +41,13 @@ RSpec.describe My::AccessTokensController do
it "creates a key" do
expect(user.rss_token).to be_nil
post :generate_rss_key
post :generate_rss_key, format: :turbo_stream
expect(user.reload.rss_token).to be_present
expect(flash[:info]).to be_present
expect(flash[:error]).not_to be_present
expect(response).to redirect_to action: :index
expect(flash[:error]).to be_blank
expect(response).to be_successful
expect(response.body).to include(user.rss_token.value)
end
context "with existing key" do
@@ -55,15 +56,15 @@ RSpec.describe My::AccessTokensController do
it "replaces the key" do
expect(user.rss_token).to eq(key)
post :generate_rss_key
post :generate_rss_key, format: :turbo_stream
new_token = user.reload.rss_token
expect(new_token).not_to eq(key)
expect(new_token.value).not_to eq(key.value)
expect(new_token.value).to eq(user.rss_key)
expect(flash[:info]).to be_present
expect(flash[:error]).not_to be_present
expect(response).to redirect_to action: :index
expect(response).to be_successful
expect(response.body).to include(new_token.value)
end
end
end
@@ -91,20 +91,16 @@ RSpec.describe "OAuth authorization code flow", :js, :selenium do
visit my_account_path
click_on "Access token"
expect(page).to have_css("#oauth-application-grant-#{app.id}", text: app.name)
expect(page).to have_css("td", text: app.name)
expect(page).to have_test_selector("oauth-application-#{app.id}-name", text: app.name)
# Revoke the application
within("#oauth-application-grant-#{app.id}") do
SeleniumHubWaiter.wait
find_test_selector("oauth-token-row-#{app.id}-revoke").click
end
find_test_selector("oauth-token-row-#{app.id}-revoke").click
page.driver.browser.switch_to.alert.accept
# Should be back on access_token path
expect_flash(message: "Revocation of application Cool API app! successful.")
expect(page).to have_no_css("[id^=oauth-application-grant]")
expect(page).to have_no_test_selector("oauth-application-#{app.id}-name")
expect(page).to have_current_path /\/my\/access_token/
+1 -2
View File
@@ -108,7 +108,6 @@ RSpec.describe "OAuth authorization code flow with PKCE", :js do
visit my_account_path
click_on "Access token"
expect(page).to have_css("#oauth-application-grant-#{app.id}", text: app.name)
expect(page).to have_css("td", text: app.name)
expect(page).to have_test_selector("oauth-application-#{app.id}-name", text: app.name)
end
end
+22 -22
View File
@@ -117,9 +117,9 @@ RSpec.describe "my access tokens", :js do
it "shows notice about disabled token" do
visit my_access_tokens_path
within "#rss-token-section" do
within "#rss-token-component" do
expect(page).to have_content("RSS tokens are not enabled by the administrator.")
expect(page).not_to have_test_selector("rss-token-add", text: "RSS token")
expect(page).not_to have_test_selector("rss-token-add", text: "RSS Token")
end
end
end
@@ -130,25 +130,25 @@ RSpec.describe "my access tokens", :js do
expect(page).to have_no_content("RSS tokens are not enabled by the administrator.")
within "#rss-token-section" do
expect(page).to have_test_selector("rss-token-add", text: "RSS token")
within "#rss-token-component" do
expect(page).to have_test_selector("rss-token-add", text: "RSS Token")
find_test_selector("rss-token-add").click
end
expect(page).to have_content "A new RSS token has been generated. Your access token is"
expect(page).to have_content "The RSS token has been generated"
User.current.reload
visit my_access_tokens_path
# only one RSS token can be created
within "#rss-token-section" do
expect(page).not_to have_test_selector("rss-token-add", text: "RSS token")
within "#rss-token-component" do
expect(page).not_to have_test_selector("rss-token-add", text: "RSS Token")
end
# revoke RSS token
within "#rss-token-section" do
within "#rss-token-component" do
accept_confirm do
find_test_selector("rss-token-revoke").click
find_test_selector("api-token-revoke").click
end
end
@@ -158,8 +158,8 @@ RSpec.describe "my access tokens", :js do
visit my_access_tokens_path
# RSS token can be created again
within "#rss-token-section" do
expect(page).to have_test_selector("rss-token-add", text: "RSS token")
within "#rss-token-component" do
expect(page).to have_test_selector("rss-token-add", text: "RSS Token")
end
end
end
@@ -209,9 +209,9 @@ RSpec.describe "my access tokens", :js do
token_name = ical_token.ical_token_query_assignment.name
query = ical_token.ical_token_query_assignment.query
expect(page).to have_test_selector("ical-token-row-#{ical_token.id}-name", text: token_name)
expect(page).to have_test_selector("ical-token-row-#{ical_token.id}-query-name", text: query.name)
expect(page).to have_test_selector("ical-token-row-#{ical_token.id}-project-name",
expect(page).to have_test_selector("ical-token-#{ical_token.id}-name", text: token_name)
expect(page).to have_test_selector("ical-token-#{ical_token.id}-query-name", text: query.name)
expect(page).to have_test_selector("ical-token-#{ical_token.id}-project-name",
text: query.project.name)
end
end
@@ -222,7 +222,7 @@ RSpec.describe "my access tokens", :js do
within "#icalendar-token-section" do
accept_confirm do
find_test_selector("ical-token-row-#{ical_token_for_query.id}-revoke").click
find_test_selector("ical-token-#{ical_token_for_query.id}-revoke").click
end
end
@@ -232,7 +232,7 @@ RSpec.describe "my access tokens", :js do
visit my_access_tokens_path
within "#icalendar-token-section" do
expect(page).not_to have_test_selector("ical-token-row-#{ical_token_for_query.id}-revoke")
expect(page).not_to have_test_selector("ical-token-#{ical_token_for_query.id}-revoke")
end
end
end
@@ -347,9 +347,9 @@ RSpec.describe "my access tokens", :js do
visit my_access_tokens_path
[app, second_app].each do |app|
within "#oauth-token-section" do
expect(page).to have_test_selector("oauth-token-row-#{app.id}-name", text: app.name)
expect(page).to have_test_selector("oauth-token-row-#{app.id}-name", text: "(one active token)")
within "#oauth-application-token-section" do
expect(page).to have_test_selector("oauth-application-#{app.id}-name", text: app.name)
expect(page).to have_test_selector("oauth-application-#{app.id}-active-tokens", text: "1")
end
end
end
@@ -393,9 +393,9 @@ RSpec.describe "my access tokens", :js do
visit my_access_tokens_path
[app, second_app].each do |app|
within "#oauth-token-section" do
expect(page).to have_test_selector("oauth-token-row-#{app.id}-name", text: app.name)
expect(page).to have_test_selector("oauth-token-row-#{app.id}-name", text: "(2 active token)")
within "#oauth-application-token-section" do
expect(page).to have_test_selector("oauth-application-#{app.id}-name", text: app.name)
expect(page).to have_test_selector("oauth-application-#{app.id}-active-tokens", text: "2")
end
end
end