diff --git a/Gemfile.lock b/Gemfile.lock index 73046f9e47a..5e198d2b692 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -402,7 +402,7 @@ GEM smart_properties bigdecimal (4.1.2) bindata (2.5.1) - bootsnap (1.24.1) + bootsnap (1.24.3) msgpack (~> 1.2) brakeman (8.0.4) racc @@ -1843,7 +1843,7 @@ CHECKSUMS better_html (2.2.0) sha256=e68ab66ab09696b708333bbf35e8aa3c107500ba7892f528e2111624bdd8cf76 bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd bindata (2.5.1) sha256=53186a1ec2da943d4cb413583d680644eb810aacbf8902497aac8f191fad9e58 - bootsnap (1.24.1) sha256=d7faea1dc24aa5b22dacc049c9236b64ebf60b14dd49c615e15d8402375d39ef + bootsnap (1.24.3) sha256=f7fa3d20597e2f0aa52b0a1aba83fb54d4f79e9c2e210ec4fa1e8895514dcad8 brakeman (8.0.4) sha256=7bf921fa9638544835df9aa7b3e720a9a72c0267f34f92135955edd80d4dcf6f browser (6.2.0) sha256=281d5295788825c9396427c292c2d2be0a5c91875c93c390fde6e5d61a5ace2d budgets (1.0.0) diff --git a/modules/wikis/app/components/wikis/admin/destroy_confirmation_dialog_component.html.erb b/modules/wikis/app/components/wikis/admin/destroy_confirmation_dialog_component.html.erb index dd6d1c166c5..a39e133fe84 100644 --- a/modules/wikis/app/components/wikis/admin/destroy_confirmation_dialog_component.html.erb +++ b/modules/wikis/app/components/wikis/admin/destroy_confirmation_dialog_component.html.erb @@ -30,11 +30,11 @@ See COPYRIGHT and LICENSE files for more details. <%= render(Primer::OpenProject::DangerDialog.new(title: t(".title"), form_arguments:, size: :large)) do |dialog| %> - <% dialog.with_confirmation_message do |message| - message.with_heading(tag: :h2) { t(".title") } - end %> - <% dialog.with_additional_details do %> - <%= t(".warning_html", wiki_provider: content_tag(:strong, @wiki_provider.name)) %> - <% end %> - <% dialog.with_confirmation_check_box_content(t(:text_permanent_delete_confirmation_checkbox_label)) %> + <% dialog.with_confirmation_message do |message| + message.with_heading(tag: :h2) { t(".heading") } + message.with_description_content( + t(".warning_html", wiki_provider: content_tag(:strong, @wiki_provider.name)) + ) + end %> + <% dialog.with_confirmation_check_box_content(t(:text_permanent_delete_confirmation_checkbox_label)) %> <% end %> diff --git a/modules/wikis/app/components/wikis/oauth_login_component.html.erb b/modules/wikis/app/components/wikis/oauth_login_component.html.erb new file mode 100644 index 00000000000..9a299825d4b --- /dev/null +++ b/modules/wikis/app/components/wikis/oauth_login_component.html.erb @@ -0,0 +1,39 @@ +<%#-- 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. + +++#%> + +<%= + render(Primer::Beta::Blankslate.new(border: false)) do |blankslate| + blankslate.with_heading(tag: :h2).with_content(t(".heading", provider: provider.name)) + blankslate.with_description { t(".description", provider: provider.name) } + blankslate.with_primary_action(href: login_url, scheme: :secondary, data: { "turbo-frame": "_top" }) do |button| + button.with_trailing_visual_icon(icon: :"link-external") + t(".connect_button", provider: provider.name) + end + end +%> diff --git a/modules/wikis/app/components/wikis/oauth_login_component.rb b/modules/wikis/app/components/wikis/oauth_login_component.rb new file mode 100644 index 00000000000..52323425ad8 --- /dev/null +++ b/modules/wikis/app/components/wikis/oauth_login_component.rb @@ -0,0 +1,51 @@ +# 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 Wikis + class OAuthLoginComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + + alias_method :provider, :model + + def initialize(model = nil, return_url:, **) + super(model, **) + @return_url = return_url + end + + def login_url + oauth_clients_ensure_connection_url( + oauth_client_id: provider.oauth_client.client_id, + storage_id: provider.id, + destination_url: @return_url + ) + end + end +end diff --git a/modules/wikis/app/components/wikis/relation_page_links_component.html.erb b/modules/wikis/app/components/wikis/relation_page_links_component.html.erb index 9bfd9606222..e4c605df940 100644 --- a/modules/wikis/app/components/wikis/relation_page_links_component.html.erb +++ b/modules/wikis/app/components/wikis/relation_page_links_component.html.erb @@ -50,17 +50,24 @@ See COPYRIGHT and LICENSE files for more details. end end - if page_link_infos.empty? + if !user_connected? + box.with_row do + render(Wikis::OAuthLoginComponent.new( + provider, + return_url: work_package_url(@work_package, tab: :wikis) + )) + end + elsif page_link_infos.empty? box.with_row do render(Primer::Beta::Blankslate.new(border: false)) do |blankslate| blankslate.with_heading(tag: :h2).with_content(t(".empty_heading")) blankslate.with_description { t(".empty_text") } end end - end - - page_link_infos.each do |info| - box.with_row { render(Wikis::PageLinkComponent.new(info, actions: [:remove])) } + else + page_link_infos.each do |info| + box.with_row { render(Wikis::PageLinkComponent.new(info, actions: [:remove])) } + end end end %> diff --git a/modules/wikis/app/components/wikis/relation_page_links_component.rb b/modules/wikis/app/components/wikis/relation_page_links_component.rb index 41739ba0181..7baa2f950f4 100644 --- a/modules/wikis/app/components/wikis/relation_page_links_component.rb +++ b/modules/wikis/app/components/wikis/relation_page_links_component.rb @@ -44,6 +44,10 @@ module Wikis @page_link_infos ||= page_link_service.relation_page_link_infos_for(provider:, linkable: @work_package) end + def user_connected? + provider.user_connected?(User.current) + end + private def page_link_service diff --git a/modules/wikis/app/models/wikis/internal_provider.rb b/modules/wikis/app/models/wikis/internal_provider.rb index d6f3d8d8f28..776eb15eff5 100644 --- a/modules/wikis/app/models/wikis/internal_provider.rb +++ b/modules/wikis/app/models/wikis/internal_provider.rb @@ -34,6 +34,8 @@ module Wikis def registry_prefix = "internal" end + def user_connected?(_user) = true + def name model_name.human end diff --git a/modules/wikis/app/models/wikis/provider.rb b/modules/wikis/app/models/wikis/provider.rb index cc8a644138d..0911dfaba8b 100644 --- a/modules/wikis/app/models/wikis/provider.rb +++ b/modules/wikis/app/models/wikis/provider.rb @@ -48,6 +48,7 @@ module Wikis before_create :generate_universal_identifier def to_s = self.class.registry_prefix + def user_connected?(_user) = raise SubclassResponsibilityError class << self def registry_prefix = raise SubclassResponsibilityError diff --git a/modules/wikis/app/models/wikis/xwiki_provider.rb b/modules/wikis/app/models/wikis/xwiki_provider.rb index 8188c318a9a..bf2c980bf83 100644 --- a/modules/wikis/app/models/wikis/xwiki_provider.rb +++ b/modules/wikis/app/models/wikis/xwiki_provider.rb @@ -48,6 +48,12 @@ module Wikis def generate_client_id = SecureRandom.uuid end + def user_connected?(user) + return true if oauth_client.blank? + + OAuthClientToken.for_user_and_client(user, oauth_client).exists? + end + def extract_origin_user_id(token) resolve("queries.user").call(Wikis::Adapters::Input::UserQuery.new(access_token: token.access_token)) end diff --git a/modules/wikis/app/views/wikis/admin/wiki_providers/edit.html.erb b/modules/wikis/app/views/wikis/admin/wiki_providers/edit.html.erb index e7c65956080..882f4c8f04f 100644 --- a/modules/wikis/app/views/wikis/admin/wiki_providers/edit.html.erb +++ b/modules/wikis/app/views/wikis/admin/wiki_providers/edit.html.erb @@ -54,7 +54,7 @@ See COPYRIGHT and LICENSE files for more details. destination_url: edit_admin_settings_wiki_provider_url(@wiki_provider) ) ) do |button| - button.with_leading_visual_icon(icon: :plug) + button.with_trailing_visual_icon(icon: :"link-external") t("wikis.buttons.connect_account", provider: @wiki_provider.name) end %> <% end %> diff --git a/modules/wikis/config/locales/en.yml b/modules/wikis/config/locales/en.yml index a5c914dd078..7649180ec20 100644 --- a/modules/wikis/config/locales/en.yml +++ b/modules/wikis/config/locales/en.yml @@ -40,10 +40,13 @@ en: empty_heading: No related pages empty_text: Manually add links to other related wiki pages. oauth_login_component: + heading: Not connected to %{provider} + description: Log in to %{provider} to view and manage related wiki pages from this OpenProject instance. connect_button: Connect %{provider} account admin: destroy_confirmation_dialog_component: title: Delete wiki provider + heading: Delete wiki provider? warning_html: "You are about to delete %{wiki_provider}. This action is irreversible." forms: general_info_form_component: @@ -91,8 +94,5 @@ en: openproject_oauth_description: Allow XWiki to access OpenProject data using an OAuth. xwiki_oauth: XWiki OAuth xwiki_oauth_description: Allow OpenProject to access XWiki data using an OAuth. - delete: - title: Delete wiki provider - warning_html: "You are about to delete %{wiki_provider}. This action is irreversible." macro: page_not_found: Linked wiki page no longer available diff --git a/modules/wikis/spec/components/wikis/oauth_login_component_spec.rb b/modules/wikis/spec/components/wikis/oauth_login_component_spec.rb new file mode 100644 index 00000000000..8ce07f57273 --- /dev/null +++ b/modules/wikis/spec/components/wikis/oauth_login_component_spec.rb @@ -0,0 +1,60 @@ +# 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. +#++ + +require "spec_helper" +require_module_spec_helper + +RSpec.describe Wikis::OAuthLoginComponent, type: :component do + let(:work_package) { build_stubbed(:work_package) } + let(:provider) { create(:xwiki_provider) } + let(:oauth_client) { create(:oauth_client, integration: provider) } + + let(:return_url) { "https://openproject.example.com/work_packages/#{work_package.id}?tab=wikis" } + + before do + allow(provider).to receive(:oauth_client).and_return(oauth_client) + render_inline(described_class.new(provider, return_url:, work_package:)) + end + + it "renders the heading" do + expect(page).to have_text(I18n.t("wikis.oauth_login_component.heading", provider: provider.name)) + end + + it "renders the description" do + expect(page).to have_text(I18n.t("wikis.oauth_login_component.description", provider: provider.name)) + end + + it "renders the connect button with the return url" do + link = page.find_link(I18n.t("wikis.oauth_login_component.connect_button", provider: provider.name)) + expect(link[:href]).to match(/ensure_connection/) + expect(link[:href]).to include(CGI.escape(return_url)) + expect(link[:"data-turbo-frame"]).to eq("_top") + end +end diff --git a/modules/wikis/spec/components/wikis/relation_page_links_component_spec.rb b/modules/wikis/spec/components/wikis/relation_page_links_component_spec.rb new file mode 100644 index 00000000000..71b13953770 --- /dev/null +++ b/modules/wikis/spec/components/wikis/relation_page_links_component_spec.rb @@ -0,0 +1,110 @@ +# 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. +#++ + +require "spec_helper" +require_module_spec_helper + +RSpec.describe Wikis::RelationPageLinksComponent, type: :component do + let(:user) { create(:user) } + let(:work_package) { build_stubbed(:work_package) } + let(:provider) { create(:xwiki_provider) } + let(:oauth_client) { create(:oauth_client, integration: provider) } + + let(:page_link_service) { instance_double(Wikis::PageLinkService, relation_page_link_infos_for: []) } + + subject(:render_component) { render_inline(described_class.new(provider, work_package:)) } + + before do + login_as(user) + allow(Wikis::PageLinkService).to receive(:new).and_return(page_link_service) + end + + context "when the provider has no oauth client configured" do + before do + allow(provider).to receive(:oauth_client).and_return(nil) + render_component + end + + it { expect(page).to have_text(I18n.t("wikis.relation_page_links_component.empty_heading")) } + it { expect(page).to have_no_text(I18n.t("wikis.oauth_login_component.heading", provider: provider.name)) } + end + + context "when the provider does not support OAuth" do + let(:provider) { create(:internal_wiki_provider) } + + before { subject } + + it { expect(page).to have_text(I18n.t("wikis.relation_page_links_component.empty_heading")) } + it { expect(page).to have_text(I18n.t("wikis.relation_page_links_component.empty_text")) } + it { expect(page).to have_no_text(I18n.t("wikis.oauth_login_component.heading", provider: provider.name)) } + end + + context "when the provider has an oauth client but the user has no token" do + before do + allow(provider).to receive(:oauth_client).and_return(oauth_client) + render_component + end + + it { expect(page).to have_text(I18n.t("wikis.oauth_login_component.heading", provider: provider.name)) } + end + + context "when the user has a token for the provider" do + before do + allow(provider).to receive(:oauth_client).and_return(oauth_client) + create(:oauth_client_token, oauth_client:, user:) + render_component + end + + it { expect(page).to have_text(I18n.t("wikis.relation_page_links_component.empty_heading")) } + it { expect(page).to have_no_text(I18n.t("wikis.oauth_login_component.heading", provider: provider.name)) } + end + + context "when the user has a token and there are page links" do + let(:page_info) do + Wikis::Adapters::Results::PageInfo.new( + identifier: "MyPage", + provider:, + title: "My Wiki Page", + href: "https://wiki.example.com/MyPage" + ) + end + + before do + allow(provider).to receive(:user_connected?).and_return(true) + allow(page_link_service).to receive(:relation_page_link_infos_for) + .and_return([Dry::Monads::Success(page_info)]) + render_component + end + + it { expect(page).to have_text("My Wiki Page") } + it { expect(page).to have_no_text(I18n.t("wikis.relation_page_links_component.empty_heading")) } + it { expect(page).to have_no_text(I18n.t("wikis.oauth_login_component.heading", provider: provider.name)) } + end +end