diff --git a/app/components/op_turbo/frame_component.rb b/app/components/op_turbo/frame_component.rb index 6f3876a84ada..4db96dd27a07 100644 --- a/app/components/op_turbo/frame_component.rb +++ b/app/components/op_turbo/frame_component.rb @@ -29,7 +29,19 @@ module OpTurbo class FrameComponent < ApplicationComponent def turbo_frame_id + optional_custom_id || turbo_frame_id_from_model + end + + private + + def turbo_frame_id_from_model ActionView::RecordIdentifier.dom_id(model, options[:context]) end + + def optional_custom_id + return if options[:id].blank? + + options.values_at(:context, :id).compact.join("_") + end end end diff --git a/app/components/open_project/common/clipboard_copy_component.rb b/app/components/open_project/common/clipboard_copy_component.rb index c00b75183337..c186345acf98 100644 --- a/app/components/open_project/common/clipboard_copy_component.rb +++ b/app/components/open_project/common/clipboard_copy_component.rb @@ -34,7 +34,8 @@ class ClipboardCopyComponent < ApplicationComponent options visually_hide_label: true, readonly: true, - required: false + required: false, + input_group_options: {} def text_field_options { name: options[:name], @@ -44,13 +45,13 @@ def text_field_options value: value_to_copy, inset: true, readonly:, - required: } + required: }.merge(input_group_options) end def clipboard_copy_options { value: value_to_copy, aria: { label: clipboard_copy_aria_label }, - classes: clipboard_copy_classes } + classes: clipboard_copy_classes }.merge(input_group_options) end private diff --git a/frontend/src/stimulus/controllers/dynamic/storages/oauth-client-form.controller.ts b/frontend/src/stimulus/controllers/dynamic/storages/oauth-client-form.controller.ts index 3bd334907638..74953341175e 100644 --- a/frontend/src/stimulus/controllers/dynamic/storages/oauth-client-form.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/storages/oauth-client-form.controller.ts @@ -30,27 +30,53 @@ import { Controller } from '@hotwired/stimulus'; -export default class StorageFormController extends Controller { +export default class OAuthClientFormController extends Controller { static targets = [ 'clientId', + 'clientSecret', 'redirectUri', + 'submitButton', ]; static values = { clientIdMissing: String, }; + declare readonly hasClientIdTarget:boolean; + declare readonly hasClientSecretTarget:boolean; + declare readonly hasSubmitButtonTarget:boolean; declare readonly clientIdTarget:HTMLInputElement; - declare readonly redirectUriTarget:HTMLInputElement; + declare readonly clientSecretTarget:HTMLInputElement; + declare readonly redirectUriTargets:HTMLInputElement[]; + declare readonly submitButtonTarget:HTMLInputElement; declare readonly clientIdMissingValue:string; connect():void { - this.clientIdTarget.addEventListener('input', () => { - this.redirectUriTarget.value = this.redirectUri(); - }); + this.setRedirectUriValue(); + this.toggleSubmitButtonDisabled(); + } + + public toggleSubmitButtonDisabled():void { + const targetsConfigured = this.hasClientIdTarget && this.hasClientSecretTarget && this.hasSubmitButtonTarget; + + if (!targetsConfigured) { + return; + } - this.redirectUriTarget.value = this.redirectUri(); + if (this.clientIdTarget.value === '' || this.clientSecretTarget.value === '') { + this.submitButtonTarget.disabled = true; + } else { + this.submitButtonTarget.disabled = false; + } + } + + public setRedirectUriValue():void { + const newValue = this.redirectUri(); + this.redirectUriTargets.forEach((target) => { + target.value = newValue; + target.setAttribute('value', newValue); + }); } private redirectUri():string { diff --git a/frontend/src/stimulus/controllers/dynamic/storages/select-provider-form.controller.ts b/frontend/src/stimulus/controllers/dynamic/storages/select-provider-form.controller.ts new file mode 100644 index 000000000000..83447ea62a84 --- /dev/null +++ b/frontend/src/stimulus/controllers/dynamic/storages/select-provider-form.controller.ts @@ -0,0 +1,58 @@ +/* + * -- copyright + * OpenProject is an open source project management software. + * Copyright (C) 2023 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. + * ++ + */ + +import { Controller } from '@hotwired/stimulus'; + +export default class SelectProviderFormController extends Controller { + static targets = [ + 'providerForm', + 'providerSelect', + ]; + + declare readonly providerFormTarget:HTMLFormElement; + declare readonly providerSelectTarget:HTMLSelectElement; + + connect():void { + // On first load if providerTypeValue is already selected, show the provider form + this.fetchProviderTypeForm(this.providerSelectTarget.value); + } + + public showProviderForm(evt:Event):void { + this.fetchProviderTypeForm((evt.target as HTMLSelectElement).value); + } + + fetchProviderTypeForm(providerTypeValue:string):void { + if (providerTypeValue === '') { + return; + } + + this.providerFormTarget.requestSubmit(); + } +} diff --git a/lib/open_project/static/links.rb b/lib/open_project/static/links.rb index 2c1596131ce1..126c12fe79c6 100644 --- a/lib/open_project/static/links.rb +++ b/lib/open_project/static/links.rb @@ -258,6 +258,12 @@ def static_links storage_docs: { setup: { href: 'https://www.openproject.org/docs/system-admin-guide/integrations/nextcloud/' + }, + nextcloud_oauth_application: { + href: 'https://apps.nextcloud.com/apps/integration_openproject' + }, + one_drive_oauth_application: { + href: 'https://docs.microsoft.com/en-us/graph/auth-register-app-v2' } }, ical_docs: { diff --git a/modules/storages/app/components/storages/admin/automatically_managed_project_folders_info_component.html.erb b/modules/storages/app/components/storages/admin/automatically_managed_project_folders_info_component.html.erb new file mode 100644 index 000000000000..a6e32e1a44b8 --- /dev/null +++ b/modules/storages/app/components/storages/admin/automatically_managed_project_folders_info_component.html.erb @@ -0,0 +1,32 @@ +<%= + grid_layout('op-storage-view--row', tag: :div, align_items: :center) do |grid| + grid.with_area(:item, tag: :div, mr: 3) do + concat(render(Primer::Beta::Text.new(font_weight: :bold, mr: 1, test_selector: 'storage-managed-project-folders-label')) { I18n.t('storages.file_storage_view.automatically_managed_folders') }) + concat(automatically_managed_project_folders_status_label) + end + + grid.with_area(:description, tag: :div, color: :subtle, test_selector: 'storage-automatically-managed-project-folders-description') do + render(Primer::Beta::Truncate.new) { I18n.t('storages.page_titles.managed_project_folders.subtitle_short') } + end + + if editable_storage? + grid.with_area(:"icon-button", tag: :div, color: :muted) do + flex_layout(justify_content: :flex_end) do |icons_container| + icons_container.with_column do + render( + Primer::Beta::IconButton.new( + icon: :pencil, + tag: :a, + href: edit_button_path, + scheme: :invisible, + aria: { label: I18n.t('storages.label_edit_storage_automatically_managed_folders') }, + test_selector: 'storage-edit-automatically-managed-project-folders-button', + data: { turbo_stream: true } + ) + ) + end + end + end + end + end +%> diff --git a/modules/storages/app/components/storages/admin/automatically_managed_project_folders_info_component.rb b/modules/storages/app/components/storages/admin/automatically_managed_project_folders_info_component.rb new file mode 100644 index 000000000000..cc1e83256b2b --- /dev/null +++ b/modules/storages/app/components/storages/admin/automatically_managed_project_folders_info_component.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 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 Storages::Admin + class AutomaticallyManagedProjectFoldersInfoComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include StorageViewInformation + + alias_method :storage, :model + + def edit_button_path + if storage.automatic_management_unspecified? + new_admin_settings_storage_automatically_managed_project_folders_path(storage) + else + edit_admin_settings_storage_automatically_managed_project_folders_path(storage) + end + end + end +end diff --git a/modules/storages/app/components/storages/admin/forms/automatically_managed_project_folders_form_component.html.erb b/modules/storages/app/components/storages/admin/forms/automatically_managed_project_folders_form_component.html.erb index 21698f170998..ceed94efda44 100644 --- a/modules/storages/app/components/storages/admin/forms/automatically_managed_project_folders_form_component.html.erb +++ b/modules/storages/app/components/storages/admin/forms/automatically_managed_project_folders_form_component.html.erb @@ -1,9 +1,9 @@ <%= render(Primer::Beta::Text.new(tag: :div, test_selector: 'storage-automatically-managed-project-folders-form')) do primer_form_with( - model: storage, - url: admin_settings_storage_automatically_managed_project_folders_path(storage), - method: :patch, + model:, + url: form_url, + method: form_method, data: { controller: "storages--automatically-managed-project-folders-form", 'application-target': "dynamic", @@ -12,11 +12,11 @@ ) do |form| flex_layout do |project_folders_form| project_folders_form.with_row(mb: 3) do - render(Primer::Beta::Text.new(font_weight: :semibold)) { I18n.t(:'storages.label_managed_project_folders.automatically_managed_folders') } + render(Primer::Beta::Text.new(font_weight: :bold)) { I18n.t(:'storages.label_managed_project_folders.automatically_managed_folders') } end project_folders_form.with_row(mb: 3) do - render(Primer::Beta::Text.new(font_weight: :light)) { I18n.t("storages.page_titles.managed_project_folders.subtitle") } + render(Primer::Beta::Text.new) { I18n.t("storages.page_titles.managed_project_folders.subtitle") } end project_folders_form.with_row(mb: 3) do @@ -28,7 +28,14 @@ end project_folders_form.with_row do - render(Storages::Admin::SaveOrCancelForm.new(form, storage:, submit_label: I18n.t("storages.buttons.done_complete_setup"))) + render( + Storages::Admin::SubmitOrCancelForm.new( + form, + storage:, + submit_button_options:, + cancel_button_options: + ) + ) end end end diff --git a/modules/storages/app/components/storages/admin/forms/automatically_managed_project_folders_form_component.rb b/modules/storages/app/components/storages/admin/forms/automatically_managed_project_folders_form_component.rb index f962f4819aaf..7ad495021f52 100644 --- a/modules/storages/app/components/storages/admin/forms/automatically_managed_project_folders_form_component.rb +++ b/modules/storages/app/components/storages/admin/forms/automatically_managed_project_folders_form_component.rb @@ -33,8 +33,39 @@ class AutomaticallyManagedProjectFoldersFormComponent < ApplicationComponent include OpPrimer::ComponentHelpers alias_method :storage, :model + def form_method + options[:form_method] || default_form_method + end + + def form_url + options[:form_url] || default_form_url + end + + def submit_button_options + { label: I18n.t("storages.buttons.done_complete_setup") }.tap do |options_hash| + # For create action, break from Turbo Frame and follow full page redirect + options_hash[:data] = { turbo: false } if new_record? + end + end + + def cancel_button_options + { href: edit_admin_settings_storage_path(storage) } + end + private + def default_form_method + new_record? ? :post : :patch + end + + def new_record? + storage.automatic_management_new_record? + end + + def default_form_url + admin_settings_storage_automatically_managed_project_folders_path(storage) + end + def storage_provider_credentials_copy_instructions "#{I18n.t('storages.instructions.copy_from')}: #{provider_credentials_instructions_link}".html_safe end diff --git a/modules/storages/app/components/storages/admin/forms/general_info_form_component.html.erb b/modules/storages/app/components/storages/admin/forms/general_info_form_component.html.erb index 0b5db97d24ea..eb635a067dc9 100644 --- a/modules/storages/app/components/storages/admin/forms/general_info_form_component.html.erb +++ b/modules/storages/app/components/storages/admin/forms/general_info_form_component.html.erb @@ -1,17 +1,50 @@ <%= render(Primer::Beta::Text.new(tag: :div, test_selector: 'storage-general-info-form')) do primer_form_with( - model: storage, - url: admin_settings_storage_path(storage), - method: :patch + model:, + url: form_url, + method: form_method ) do |form| flex_layout do |general_info_row| general_info_row.with_row(mb: 3) do - render(Storages::Admin::StorageProviderForm.new(form, storage:)) + render( + Storages::Admin::ProviderTypeSelectForm.new( + form, + storage:, + select_list_options: { caption: provider_type_select_caption, readonly: storage.new_record? } + ) + ) + end + + general_info_row.with_row(mb: 3) do + render(Storages::Admin::ProviderNameInputForm.new(form)) + end + + if storage.provider_type_nextcloud? + general_info_row.with_row(mb: 3) do + render(Storages::Admin::ProviderHostInputForm.new(form)) + end + end + + if storage.provider_type_one_drive? + general_info_row.with_row(mb: 3) do + render(Storages::Admin::ProviderTenantIdInputForm.new(form)) + end + + general_info_row.with_row(mb: 3) do + render(Storages::Admin::ProviderDriveIdInputForm.new(form)) + end end general_info_row.with_row do - render(Storages::Admin::SaveOrCancelForm.new(form, storage:)) + render( + Storages::Admin::SubmitOrCancelForm.new( + form, + storage:, + submit_button_options:, + cancel_button_options: + ) + ) end end end diff --git a/modules/storages/app/components/storages/admin/forms/general_info_form_component.rb b/modules/storages/app/components/storages/admin/forms/general_info_form_component.rb index cd8de8524c66..a979d8779a18 100644 --- a/modules/storages/app/components/storages/admin/forms/general_info_form_component.rb +++ b/modules/storages/app/components/storages/admin/forms/general_info_form_component.rb @@ -32,5 +32,67 @@ module Storages::Admin::Forms class GeneralInfoFormComponent < ApplicationComponent include OpPrimer::ComponentHelpers alias_method :storage, :model + + options form_method: :post, + submit_button_disabled: false + + def form_url + options[:form_url] || default_form_url + end + + def submit_button_options + { disabled: submit_button_disabled } + end + + def cancel_button_options + { href: cancel_button_path, + data: { turbo_stream: true } }.tap do |options_hash| + if storage.new_record? + options_hash[:data][:turbo_stream] = false + options_hash[:target] = '_top' # Break out of Turbo Frame, follow full page redirect + end + end + end + + private + + def default_form_url + case form_method + when :get, :post + admin_settings_storages_path + when :patch, :put + admin_settings_storage_path(storage) + end + end + + def cancel_button_path + options.fetch(:cancel_button_path) do + if storage.persisted? + edit_admin_settings_storage_path(storage) + else + admin_settings_storages_path + end + end + end + + def provider_type_select_caption + return if storage.provider_type.blank? + + caption_for_provider_type(storage.short_provider_type) + end + + def caption_for_provider_type(provider_type) + I18n.t( + "storages.instructions.#{provider_type}.provider_configuration", + application_link_text: application_link_text_for( + ::OpenProject::Static::Links[:storage_docs][:"#{provider_type}_oauth_application"][:href], + I18n.t("storages.instructions.#{provider_type}.application_link_text") + ) + ).html_safe + end + + def application_link_text_for(href, link_text) + render(Primer::Beta::Link.new(href:, target: '_blank')) { link_text } + end end end diff --git a/modules/storages/app/components/storages/admin/forms/oauth_client_form_component.html.erb b/modules/storages/app/components/storages/admin/forms/oauth_client_form_component.html.erb index 190820e5413e..3423ebf17e96 100644 --- a/modules/storages/app/components/storages/admin/forms/oauth_client_form_component.html.erb +++ b/modules/storages/app/components/storages/admin/forms/oauth_client_form_component.html.erb @@ -2,23 +2,81 @@ render(Primer::Beta::Text.new(tag: :div, test_selector: 'storage-oauth-client-form')) do primer_form_with( model: oauth_client, - url: admin_settings_storage_oauth_client_path(storage) + url: admin_settings_storage_oauth_client_path(storage), + method: form_method, + data: { + controller: 'storages--oauth-client-form', + application_target: 'dynamic', + 'storages--oauth-client-form-client-id-missing-value': t(:"storages.instructions.one_drive.missing_client_id_for_redirect_uri") + } ) do |form| flex_layout do |oauth_client_row| oauth_client_row.with_row(mb: 3) do - render(Primer::Beta::Text.new(font_weight: :semibold)) { I18n.t("storages.file_storage_view.#{storage.short_provider_type}_oauth") } + render(Primer::Beta::Text.new(font_weight: :bold)) { I18n.t("storages.file_storage_view.#{storage.short_provider_type}_oauth") } end oauth_client_row.with_row(mb: 3) do - render(Primer::Beta::Text.new(font_weight: :light)) { storage_provider_credentials_copy_instructions } + render(Primer::Beta::Text.new(test_selector: 'storage-provider-credentials-instructions')) { storage_provider_credentials_instructions } end oauth_client_row.with_row(mb: 3) do - render(Storages::Admin::OAuthClientForm.new(form, storage:)) + render( + Storages::Admin::OAuthClientForm.new( + form, + storage:, + client_id_input_options: { + data: { + 'storages--oauth-client-form-target': 'clientId', + action: 'input->storages--oauth-client-form#toggleSubmitButtonDisabled '\ + 'input->storages--oauth-client-form#setRedirectUriValue' + } + }, + client_secret_input_options: { + data: { + 'storages--oauth-client-form-target': 'clientSecret', + action: 'input->storages--oauth-client-form#toggleSubmitButtonDisabled' + } + } + ) + ) + end + + if storage.provider_type_one_drive? + oauth_client_row.with_row(mb: 3) do + render( + OpenProject::Common::ClipboardCopyComponent.new( + name: :oauth_client_redirect_uri, + visually_hide_label: false, + value_to_copy: redirect_uri_or_instructions, + label: I18n.t('storages.label_redirect_uri'), + required: true, + input_group_options: { + data: { + 'storages--oauth-client-form-target': 'redirectUri' + } + }, + ) + ) + end end oauth_client_row.with_row do - render(Storages::Admin::SaveOrCancelForm.new(form, storage:)) + render( + Storages::Admin::SubmitOrCancelForm.new( + form, + storage:, + submit_button_options: { + disabled: submit_button_disabled?, + data: { + 'storages--oauth-client-form-target': 'submitButton', + }.merge(submit_button_data_options), + test_selector: 'storage-oauth-client-submit-button' + }, + cancel_button_options: { + href: cancel_button_path + } + ) + ) end end end diff --git a/modules/storages/app/components/storages/admin/forms/oauth_client_form_component.rb b/modules/storages/app/components/storages/admin/forms/oauth_client_form_component.rb index a7fa4df6bf03..1a5e0db536dd 100644 --- a/modules/storages/app/components/storages/admin/forms/oauth_client_form_component.rb +++ b/modules/storages/app/components/storages/admin/forms/oauth_client_form_component.rb @@ -40,20 +40,65 @@ def initialize(oauth_client:, storage:, **options) @storage = storage end + def form_method + options[:form_method] || default_form_method + end + + def cancel_button_path + storage.persisted? ? edit_admin_settings_storage_path(storage) : admin_settings_storages_path + end + + def submit_button_disabled? + !oauth_client_configured? + end + + def submit_button_data_options + {}.tap do |data| + # For One Drive create action, break from Turbo Frame and follow full page redirect + data[:turbo] = false if one_drive_first_time_configuration? + end + end + + def redirect_uri_or_instructions + if oauth_client_configured? + oauth_client.redirect_uri + else + I18n.t("storages.instructions.one_drive.missing_client_id_for_redirect_uri") + end + end + + def storage_provider_credentials_instructions + I18n.t("storages.instructions.#{storage.short_provider_type}.oauth_configuration", + application_link_text: send(:"#{storage.short_provider_type}_integration_link")).html_safe + end + private - def storage_provider_credentials_copy_instructions - "#{I18n.t('storages.instructions.copy_from')}: #{provider_credentials_instructions_link}".html_safe + def one_drive_integration_link(target: '_blank') + href = ::OpenProject::Static::Links[:storage_docs][:one_drive_oauth_application][:href] + render(Primer::Beta::Link.new(href:, target:)) { I18n.t('storages.instructions.one_drive.application_link_text') } + end + + def nextcloud_integration_link(target: '_blank') + href = Storages::Peripherals::StorageInteraction::Nextcloud::Util + .join_uri_path(storage.host, 'settings/admin/openproject') + render(Primer::Beta::Link.new(href:, target:)) { I18n.t('storages.instructions.nextcloud.integration') } + end + + def one_drive_first_time_configuration? + storage.provider_type_one_drive? && first_time_configuration? + end + + def first_time_configuration? + storage.oauth_client.blank? || storage.oauth_client.new_record? + end + + def default_form_method + first_time_configuration? ? :post : :patch end - def provider_credentials_instructions_link - render( - Primer::Beta::Link.new( - href: Storages::Peripherals::StorageInteraction::Nextcloud::Util.join_uri_path(storage.host, - 'settings/admin/openproject'), - target: '_blank' - ) - ) { I18n.t("storages.instructions.#{@storage.short_provider_type}.integration") } + def oauth_client_configured? + oauth_client.present? && oauth_client.client_id.present? && oauth_client.client_secret.present? end end end diff --git a/modules/storages/app/components/storages/admin/general_info_component.html.erb b/modules/storages/app/components/storages/admin/general_info_component.html.erb new file mode 100644 index 000000000000..59036d5d399b --- /dev/null +++ b/modules/storages/app/components/storages/admin/general_info_component.html.erb @@ -0,0 +1,32 @@ +<%= + grid_layout('op-storage-view--row', tag: :div, align_items: :center) do |grid| + grid.with_area(:item, tag: :div, mr: 3) do + concat(render(Primer::Beta::Text.new(font_weight: :bold, mr: 1, test_selector: 'storage-provider-label')) { I18n.t('storages.file_storage_view.storage_provider') }) + concat(configuration_check_label) + end + + grid.with_area(:description, tag: :div, color: :subtle, test_selector: 'storage-description') do + render(Primer::Beta::Truncate.new) { storage_description } + end + + if editable_storage? + grid.with_area(:"icon-button", tag: :div, color: :muted) do + flex_layout(justify_content: :flex_end) do |icons_container| + icons_container.with_column do + render( + Primer::Beta::IconButton.new( + icon: :pencil, + tag: :a, + scheme: :invisible, + href: edit_host_admin_settings_storage_path(storage), + aria: { label: I18n.t('storages.label_edit_storage_host') } , + test_selector: 'storage-edit-host-button', + data: { turbo_stream: true } + ) + ) + end + end + end + end + end +%> diff --git a/modules/storages/app/components/storages/admin/storage_general_info_component.rb b/modules/storages/app/components/storages/admin/general_info_component.rb similarity index 95% rename from modules/storages/app/components/storages/admin/storage_general_info_component.rb rename to modules/storages/app/components/storages/admin/general_info_component.rb index f7f45b866a03..ad3dc0c61c0d 100644 --- a/modules/storages/app/components/storages/admin/storage_general_info_component.rb +++ b/modules/storages/app/components/storages/admin/general_info_component.rb @@ -29,9 +29,10 @@ #++ # module Storages::Admin - class StorageGeneralInfoComponent < ApplicationComponent + class GeneralInfoComponent < ApplicationComponent include OpPrimer::ComponentHelpers - alias_method :storage, :model include StorageViewInformation + + alias_method :storage, :model end end diff --git a/modules/storages/app/components/storages/admin/oauth_application_info_component.html.erb b/modules/storages/app/components/storages/admin/oauth_application_info_component.html.erb new file mode 100644 index 000000000000..28ff70c482ca --- /dev/null +++ b/modules/storages/app/components/storages/admin/oauth_application_info_component.html.erb @@ -0,0 +1,40 @@ +<%= + grid_layout('op-storage-view--row', tag: :div, align_items: :center) do |grid| + grid.with_area(:item, tag: :div, mr: 3) do + concat(render(Primer::Beta::Text.new(font_weight: :bold, mr: 1, test_selector: 'storage-openproject-oauth-label')) { I18n.t('storages.file_storage_view.openproject_oauth') }) + concat(configuration_check_label_for(:openproject_oauth_application_configured)) + end + + grid.with_area(:description, tag: :div, color: :subtle, test_selector: 'storage-openproject-oauth-application-description') do + render(Primer::Beta::Truncate.new) { openproject_oauth_client_description } + end + + if editable_storage? + grid.with_area(:"icon-button", tag: :div, color: :muted, test_selector: 'storage-replace-openproject-oauth-application') do + flex_layout(justify_content: :flex_end) do |icons_container| + icons_container.with_column do + primer_form_with( + model: storage, + url: replace_oauth_application_admin_settings_storage_path(storage), + method: :delete + ) do |_form| + render( + Primer::Beta::IconButton.new( + icon: :sync, + scheme: :invisible, + type: :submit, + aria: { + label: I18n.t("storages.buttons.replace_provider_type_oauth", + provider_type: I18n.t("storages.provider_types.#{storage.short_provider_type}.name")) + }, + data: { confirm: I18n.t("storages.confirm_replace_oauth_application") }, + test_selector: 'storage-replace-openproject-oauth-application-button' + ) + ) + end + end + end + end + end + end +%> diff --git a/modules/storages/app/forms/storages/admin/save_or_cancel_form.rb b/modules/storages/app/components/storages/admin/oauth_application_info_component.rb similarity index 64% rename from modules/storages/app/forms/storages/admin/save_or_cancel_form.rb rename to modules/storages/app/components/storages/admin/oauth_application_info_component.rb index 47d724deffc7..13caa32fb210 100644 --- a/modules/storages/app/forms/storages/admin/save_or_cancel_form.rb +++ b/modules/storages/app/components/storages/admin/oauth_application_info_component.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) 2012-2023 the OpenProject GmbH @@ -25,26 +27,18 @@ # # See COPYRIGHT and LICENSE files for more details. #++ - +# module Storages::Admin - class SaveOrCancelForm < ApplicationForm - form do |buttons| - buttons.group(layout: :horizontal) do |button_group| - button_group.submit(name: :submit, - scheme: :primary, - label: @submit_label) - button_group.button(name: :cancel, - scheme: :default, - tag: :a, - href: Rails.application.routes.url_helpers.edit_admin_settings_storage_path(@storage), - label: I18n.t('button_cancel')) - end - end + class OAuthApplicationInfoComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include StorageViewInformation + + attr_reader :storage + alias_method :oauth_application, :model - def initialize(storage:, submit_label: I18n.t('storages.buttons.save_and_continue')) - super() + def initialize(oauth_application:, storage:, **options) + super(oauth_application, **options) @storage = storage - @submit_label = submit_label end end end diff --git a/modules/storages/app/components/storages/admin/oauth_application_credentials_copy_component.html.erb b/modules/storages/app/components/storages/admin/oauth_application_info_copy_component.html.erb similarity index 81% rename from modules/storages/app/components/storages/admin/oauth_application_credentials_copy_component.html.erb rename to modules/storages/app/components/storages/admin/oauth_application_info_copy_component.html.erb index a30a2f73e5cb..6cf35ab5e472 100644 --- a/modules/storages/app/components/storages/admin/oauth_application_credentials_copy_component.html.erb +++ b/modules/storages/app/components/storages/admin/oauth_application_info_copy_component.html.erb @@ -32,8 +32,7 @@ end credentials_row.with_row do - concat(render(Primer::Beta::Button.new(scheme: :primary, tag: :a, href: edit_admin_settings_storage_path(storage))) { I18n.t('storages.buttons.done_continue') }) - concat(render(Primer::Beta::Button.new(scheme: :default, tag: :a, href: edit_admin_settings_storage_path(storage))) { I18n.t('button_cancel') }) + concat(render(Primer::Beta::Button.new(**submit_button_options)) { I18n.t('storages.buttons.done_continue') }) end end end diff --git a/modules/storages/app/components/storages/admin/oauth_application_credentials_copy_component.rb b/modules/storages/app/components/storages/admin/oauth_application_info_copy_component.rb similarity index 83% rename from modules/storages/app/components/storages/admin/oauth_application_credentials_copy_component.rb rename to modules/storages/app/components/storages/admin/oauth_application_info_copy_component.rb index 8f5a9a932937..2bf6f2432b48 100644 --- a/modules/storages/app/components/storages/admin/oauth_application_credentials_copy_component.rb +++ b/modules/storages/app/components/storages/admin/oauth_application_info_copy_component.rb @@ -29,7 +29,7 @@ #++ # module Storages::Admin - class OAuthApplicationCredentialsCopyComponent < ApplicationComponent + class OAuthApplicationInfoCopyComponent < ApplicationComponent include OpPrimer::ComponentHelpers attr_reader :storage @@ -49,5 +49,19 @@ def oauth_application_details_link ) ) { I18n.t('storages.instructions.oauth_application_details_link_text') } end + + def submit_button_options + { + scheme: :primary, + tag: :a, + href: submit_button_path + }.merge(options.fetch(:submit_button_options, {})) + end + + private + + def submit_button_path + options[:submit_button_path] || edit_admin_settings_storage_path(storage) + end end end diff --git a/modules/storages/app/components/storages/admin/oauth_client_info_component.html.erb b/modules/storages/app/components/storages/admin/oauth_client_info_component.html.erb new file mode 100644 index 000000000000..7f1163bf0cb0 --- /dev/null +++ b/modules/storages/app/components/storages/admin/oauth_client_info_component.html.erb @@ -0,0 +1,36 @@ +<%= + grid_layout('op-storage-view--row', tag: :div, align_items: :center) do |grid| + grid.with_area(:item, tag: :div, mr: 3) do + concat( + render( + Primer::Beta::Text.new(font_weight: :bold, mr: 1, test_selector: 'storage-oauth-client-label') + ) { I18n.t("storages.file_storage_view.#{storage.short_provider_type}_oauth") } + ) + concat(configuration_check_label_for(:storage_oauth_client_configured)) + end + + grid.with_area(:description, tag: :div, color: :subtle, test_selector: 'storage-oauth-client-id-description') do + render(Primer::Beta::Truncate.new) { provider_oauth_client_description } + end + + if editable_storage? + grid.with_area(:"icon-button", tag: :div, color: :muted) do + flex_layout(justify_content: :flex_end) do |icons_container| + icons_container.with_column do + render( + Primer::Beta::IconButton.new( + icon: :sync, + tag: :a, + href: new_admin_settings_storage_oauth_client_path(storage), + scheme: :invisible, + aria: { label: I18n.t("storages.label_edit_storage_oauth_client") }, + data: { confirm: I18n.t("storages.confirm_replace_oauth_client") }, + test_selector: 'storage-edit-oauth-client-button' + ) + ) + end + end + end + end + end +%> diff --git a/modules/storages/app/components/storages/admin/oauth_client_info_component.rb b/modules/storages/app/components/storages/admin/oauth_client_info_component.rb new file mode 100644 index 000000000000..27f358d5e567 --- /dev/null +++ b/modules/storages/app/components/storages/admin/oauth_client_info_component.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 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 Storages::Admin + class OAuthClientInfoComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include StorageViewInformation + + attr_reader :storage + alias_method :oauth_client, :model + + def initialize(oauth_client:, storage:, **options) + super(oauth_client, **options) + @storage = storage + end + end +end diff --git a/modules/storages/app/components/storages/admin/select_storage_provider_component.html.erb b/modules/storages/app/components/storages/admin/select_storage_provider_component.html.erb new file mode 100644 index 000000000000..ebe760e5d2cb --- /dev/null +++ b/modules/storages/app/components/storages/admin/select_storage_provider_component.html.erb @@ -0,0 +1,66 @@ +<%= + render(OpTurbo::FrameComponent.new(id: :select_storage_provider)) do + render(Primer::Beta::BorderBox.new) do |component| + component.with_header(color: :default) do + render(Primer::Beta::Text.new(font_weight: :semibold)) { I18n.t('storages.file_storage_view.general_information') } + end + + component.with_row(scheme: :default) do + render( + Primer::Beta::Text.new( + tag: :div, + test_selector: 'storage-select-provider-form', + data: { + controller: "storages--select-provider-form", + application_target: "dynamic" + } + ) + ) do + primer_form_with( + model: storage, + url: select_provider_admin_settings_storages_path, + data: { + storages__select_provider_form_target: 'providerForm', + } + ) do |form| + flex_layout do |general_info_row| + general_info_row.with_row(mb: 3) do + render(Storages::Admin::ProviderTypeSelectForm.new( + form, + storage:, + select_list_options: { + data: { + action: 'change->storages--select-provider-form#showProviderForm', + storages__select_provider_form_target: 'providerSelect' + }, + include_blank: I18n.t('storages.label_select_provider') + } + ) + + ) + end + + general_info_row.with_row do + render( + Storages::Admin::SubmitOrCancelForm.new( + form, + storage:, + submit_button_options: { + disabled: true, + test_selector: 'storage-select-provider-submit-button' + }, + cancel_button_options: { + href: admin_settings_storages_path, + test_selector: 'storage-select-provider-cancel-button', + target: '_top' + } + ) + ) + end + end + end + end + end + end + end +%> diff --git a/modules/storages/app/components/storages/admin/select_storage_provider_component.rb b/modules/storages/app/components/storages/admin/select_storage_provider_component.rb new file mode 100644 index 000000000000..3061a77acc67 --- /dev/null +++ b/modules/storages/app/components/storages/admin/select_storage_provider_component.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 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 Storages::Admin + class SelectStorageProviderComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + alias_method :storage, :model + end +end diff --git a/modules/storages/app/components/storages/admin/storage_general_info_component.html.erb b/modules/storages/app/components/storages/admin/storage_general_info_component.html.erb deleted file mode 100644 index fed831a65d93..000000000000 --- a/modules/storages/app/components/storages/admin/storage_general_info_component.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -<%= - grid_layout('op-storage-view--row', tag: :div, align_items: :center) do |grid| - grid.with_area(:item, tag: :div, mr: 3) do - concat(render(Primer::Beta::Text.new(font_weight: :semibold, mr: 1, test_selector: 'storage-provider-label')) { I18n.t('storages.file_storage_view.storage_provider') }) - concat(configuration_check_label_for(:host_name_configured)) - end - - grid.with_area(:description, tag: :div, color: :subtle, test_selector: 'storage-description') do - render(Primer::Beta::Truncate.new(font_weight: :light)) { storage_description } - end - - grid.with_area(:"icon-button", tag: :div, color: :subtle) do - flex_layout(justify_content: :flex_end) do |icons_container| - icons_container.with_column do - render( - Primer::Beta::IconButton.new( - icon: :pencil, - tag: :a, - scheme: :invisible, - href: edit_host_admin_settings_storage_path(storage), - aria: { label: I18n.t('storages.label_edit_storage_host') } , - test_selector: 'storage-edit-host-button' - ) - ) - end - end - end - end -%> diff --git a/modules/storages/app/components/storages/admin/storage_list_component.html.erb b/modules/storages/app/components/storages/admin/storage_list_component.html.erb index 53d43573d2d0..6af245efce51 100644 --- a/modules/storages/app/components/storages/admin/storage_list_component.html.erb +++ b/modules/storages/app/components/storages/admin/storage_list_component.html.erb @@ -1,7 +1,7 @@ <%= if storages.present? render(Primer::Beta::BorderBox.new) do |component| - component.with_header(color: :subtle) do + component.with_header(color: :muted) do grid_layout('op-storage-list--header', tag: :div, align_items: :center) do |grid| grid.with_area(:name, tag: :div, mr: 3) do render(Primer::Beta::Text.new(font_weight: :semibold)) { I18n.t('storages.label_name') } diff --git a/modules/storages/app/components/storages/admin/storage_view_component.html.erb b/modules/storages/app/components/storages/admin/storage_view_component.html.erb index 5e633236fc0e..2219a21e0747 100644 --- a/modules/storages/app/components/storages/admin/storage_view_component.html.erb +++ b/modules/storages/app/components/storages/admin/storage_view_component.html.erb @@ -1,133 +1,81 @@ <%= render(Primer::Beta::BorderBox.new) do |component| - component.with_header(color: :default) do + component.with_header(color: :muted) do render(Primer::Beta::Text.new(font_weight: :semibold)) { I18n.t('storages.file_storage_view.general_information') } end component.with_row(scheme: :default) do - render(OpTurbo::FrameComponent.new(storage, context: :storage_view_general_info)) do - render(Storages::Admin::StorageGeneralInfoComponent.new(storage)) - end - end - - component.with_row(scheme: :neutral) do - grid_layout('op-storage-view--row', tag: :div, align_items: :center) do |grid| - grid.with_area(:item, tag: :div, mr: 3) do - render(Primer::Beta::Text.new(font_weight: :semibold, mr: 1)) { I18n.t('storages.file_storage_view.oauth_applications') } + render(OpTurbo::FrameComponent.new(id: :storage_general_info_section)) do + if storage.new_record? + render(Storages::Admin::Forms::GeneralInfoFormComponent.new(storage)) + else + render(Storages::Admin::GeneralInfoComponent.new(storage)) end end end - component.with_row(scheme: :default) do - render(OpTurbo::FrameComponent.new(storage, context: :openproject_oauth)) do + if storage.provider_type_nextcloud? + component.with_row(scheme: :neutral, color: :muted) do grid_layout('op-storage-view--row', tag: :div, align_items: :center) do |grid| grid.with_area(:item, tag: :div, mr: 3) do - concat(render(Primer::Beta::Text.new(font_weight: :semibold, mr: 1, test_selector: 'storage-openproject-oauth-label')) { I18n.t('storages.file_storage_view.openproject_oauth') }) - concat(configuration_check_label_for(:openproject_oauth_application_configured)) + render(Primer::Beta::Text.new(font_weight: :semibold, mr: 1)) { I18n.t('storages.file_storage_view.oauth_applications') } end + end + end - grid.with_area(:description, tag: :div, color: :subtle, test_selector: 'storage-openproject-oauth-application-description') do - render(Primer::Beta::Truncate.new(font_weight: :light)) { openproject_oauth_client_description } + component.with_row(scheme: :default) do + render(OpTurbo::FrameComponent.new(id: :storage_openproject_oauth_section)) do + if storage.new_record? || openproject_oauth_application_section_closed? + render(Storages::Admin::OAuthApplicationInfoComponent.new(oauth_application:, storage:)) + else + render( + Storages::Admin::OAuthApplicationInfoCopyComponent.new( + oauth_application:, + storage:, + submit_button_path: show_oauth_application_admin_settings_storage_path(storage) + ) + ) end + end + end - grid.with_area(:"icon-button", tag: :div, color: :subtle, test_selector: 'storage-replace-openproject-oauth-application') do - flex_layout(justify_content: :flex_end) do |icons_container| - icons_container.with_column do - primer_form_with( - model: storage, - url: replace_oauth_application_admin_settings_storage_path(storage), - method: :delete - ) do |_form| - render( - Primer::Beta::IconButton.new( - icon: :sync, - scheme: :invisible, - type: :submit, - aria: { - label: I18n.t("storages.buttons.replace_provider_type_oauth", - provider_type: I18n.t("storages.provider_types.#{storage.short_provider_type}.name")) - }, - data: { confirm: I18n.t("storages.confirm_replace_oauth_application") }, - test_selector: 'storage-replace-openproject-oauth-application-button' - ) - ) - end - end - end - end + component.with_row(scheme: :default) do + render(OpTurbo::FrameComponent.new(id: :storage_oauth_client_section)) do + render(Storages::Admin::OAuthClientInfoComponent.new(oauth_client: storage.oauth_client, storage:)) end end - end - component.with_row(scheme: :default) do - render(OpTurbo::FrameComponent.new(storage, context: :oauth_client)) do + component.with_row(scheme: :neutral, color: :muted) do grid_layout('op-storage-view--row', tag: :div, align_items: :center) do |grid| - grid.with_area(:item, tag: :div, mr: 3) do - concat(render(Primer::Beta::Text.new(font_weight: :semibold, mr: 1, test_selector: 'storage-oauth-client-label')) { I18n.t('storages.file_storage_view.nextcloud_oauth') }) - concat(configuration_check_label_for(:storage_oauth_client_configured)) - end - - grid.with_area(:description, tag: :div, color: :subtle, test_selector: 'storage-oauth-client-id-description') do - render(Primer::Beta::Truncate.new(font_weight: :light)) { provider_oauth_client_description } - end - - grid.with_area(:"icon-button", tag: :div, color: :subtle) do - flex_layout(justify_content: :flex_end) do |icons_container| - icons_container.with_column do - render( - Primer::Beta::IconButton.new( - icon: :sync, - tag: :a, - href: new_admin_settings_storage_oauth_client_path(storage), - scheme: :invisible, - aria: { label: I18n.t("storages.label_edit_storage_oauth_client") }, - data: { confirm: I18n.t("storages.confirm_replace_oauth_client") }, - test_selector: 'storage-edit-oauth-client-button' - ) - ) - end - end + grid.with_area(:item, tag: :div) do + render(Primer::Beta::Text.new(font_weight: :semibold, mr: 1)) { I18n.t('storages.file_storage_view.project_folders') } end end end - end - component.with_row(scheme: :neutral) do - grid_layout('op-storage-view--row', tag: :div, align_items: :center) do |grid| - grid.with_area(:item, tag: :div) do - render(Primer::Beta::Text.new(font_weight: :semibold, mr: 1)) { I18n.t('storages.file_storage_view.project_folders') } + component.with_row(scheme: :default) do + render(OpTurbo::FrameComponent.new(id: :automatically_managed_project_folders_section)) do + if automatically_managed_project_folders_section_closed? + render(Storages::Admin::AutomaticallyManagedProjectFoldersInfoComponent.new(storage)) + else + render(Storages::Admin::Forms::AutomaticallyManagedProjectFoldersFormComponent.new(storage)) + end end end end - component.with_row(scheme: :default) do - render(OpTurbo::FrameComponent.new(storage, context: :automatically_managed_project_folders)) do + if storage.provider_type_one_drive? + component.with_row(scheme: :neutral, color: :muted) do grid_layout('op-storage-view--row', tag: :div, align_items: :center) do |grid| grid.with_area(:item, tag: :div, mr: 3) do - concat(render(Primer::Beta::Text.new(font_weight: :semibold, mr: 1, test_selector: 'storage-managed-project-folders-label')) { I18n.t('storages.file_storage_view.automatically_managed_folders') }) - concat(automatically_managed_project_folders_status_label) - end - - grid.with_area(:description, tag: :div, color: :subtle, test_selector: 'storage-automatically-managed-project-folders-description') do - render(Primer::Beta::Truncate.new(font_weight: :light)) { I18n.t('storages.page_titles.managed_project_folders.subtitle_short') } + render(Primer::Beta::Text.new(font_weight: :semibold, mr: 1)) { I18n.t('storages.file_storage_view.oauth_applications') } end + end + end - grid.with_area(:"icon-button", tag: :div, color: :subtle) do - flex_layout(justify_content: :flex_end) do |icons_container| - icons_container.with_column do - render( - Primer::Beta::IconButton.new( - icon: :pencil, - tag: :a, - href: new_admin_settings_storage_automatically_managed_project_folders_path(storage), - scheme: :invisible, - aria: { label: I18n.t('storages.label_edit_storage_automatically_managed_folders') }, - test_selector: 'storage-edit-automatically-managed-project-folders-button' - ) - ) - end - end - end + component.with_row(scheme: :default) do + render(OpTurbo::FrameComponent.new(id: :storage_oauth_client_section)) do + render(Storages::Admin::OAuthClientInfoComponent.new(oauth_client: storage.oauth_client, storage:)) end end end diff --git a/modules/storages/app/components/storages/admin/storage_view_component.rb b/modules/storages/app/components/storages/admin/storage_view_component.rb index af50ab1c83ee..650d075cb424 100644 --- a/modules/storages/app/components/storages/admin/storage_view_component.rb +++ b/modules/storages/app/components/storages/admin/storage_view_component.rb @@ -31,7 +31,23 @@ module Storages::Admin class StorageViewComponent < ApplicationComponent include OpPrimer::ComponentHelpers - alias_method :storage, :model include StorageViewInformation + + options openproject_oauth_application_section_open: false, + automatically_managed_project_folders_section_open: false + + alias_method :storage, :model + alias_method :openproject_oauth_application_section_open?, :openproject_oauth_application_section_open + alias_method :automatically_managed_project_folders_section_open?, :automatically_managed_project_folders_section_open + + delegate :oauth_application, to: :model + + def openproject_oauth_application_section_closed? + !openproject_oauth_application_section_open? + end + + def automatically_managed_project_folders_section_closed? + !automatically_managed_project_folders_section_open? + end end end diff --git a/modules/storages/app/components/storages/admin/storage_view_information.rb b/modules/storages/app/components/storages/admin/storage_view_information.rb index 4fd088acdb63..1c673f8189f8 100644 --- a/modules/storages/app/components/storages/admin/storage_view_information.rb +++ b/modules/storages/app/components/storages/admin/storage_view_information.rb @@ -4,17 +4,29 @@ module Storages::Admin module StorageViewInformation private + def editable_storage? + storage.persisted? + end + def storage_description - [storage.short_provider_type.capitalize, + [I18n.t("storages.provider_types.#{storage.short_provider_type}.name"), storage.name, storage.host].compact.join(' - ') end - def configuration_check_label_for(config) - if storage.configuration_checks[config.to_sym] - status_label(I18n.t('storages.label_connected'), scheme: :success, test_selector: "label-#{config}-status") + def configuration_check_label + if storage.provider_type_nextcloud? + configuration_check_label_for(:host_name_configured) + elsif storage.provider_type_one_drive? + configuration_check_label_for(:host_name_configured, :storage_tenant_drive_configured) + end + end + + def configuration_check_label_for(*configs) + if storage.configuration_checks.slice(*configs.map(&:to_sym)).values.all? + status_label(I18n.t('storages.label_completed'), scheme: :success, test_selector: "label-#{configs.join('-')}-status") else - status_label(I18n.t('storages.label_incomplete'), scheme: :attention, test_selector: "label-#{config}-status") + status_label(I18n.t('storages.label_incomplete'), scheme: :attention, test_selector: "label-#{configs.join('-')}-status") end end @@ -44,7 +56,7 @@ def provider_oauth_client_description if storage.oauth_client "#{I18n.t('storages.label_oauth_client_id')}: #{storage.oauth_client.client_id}" else - I18n.t('storages.configuration_checks.oauth_client_incomplete', provider: storage.short_provider_type.capitalize) + I18n.t("storages.configuration_checks.oauth_client_incomplete.#{storage.short_provider_type}") end end end diff --git a/modules/storages/app/contracts/storages/storages/base_contract.rb b/modules/storages/app/contracts/storages/storages/base_contract.rb index 2209cdc0c16f..d5ac7880ef85 100644 --- a/modules/storages/app/contracts/storages/storages/base_contract.rb +++ b/modules/storages/app/contracts/storages/storages/base_contract.rb @@ -47,7 +47,8 @@ class BaseContract < ::BaseContract attribute :provider_fields - validate :provider_type_strategy, unless: -> { errors.include?(:provider_type) } + validate :provider_type_strategy, + unless: -> { errors.include?(:provider_type) || @options.delete(:skip_provider_type_strategy) } private diff --git a/modules/storages/app/controllers/storages/admin/automatically_managed_project_folders_controller.rb b/modules/storages/app/controllers/storages/admin/automatically_managed_project_folders_controller.rb index fa33ed893659..08c6ccabbbcd 100644 --- a/modules/storages/app/controllers/storages/admin/automatically_managed_project_folders_controller.rb +++ b/modules/storages/app/controllers/storages/admin/automatically_managed_project_folders_controller.rb @@ -41,7 +41,7 @@ class Storages::Admin::AutomaticallyManagedProjectFoldersController < Applicatio # specify which model #find_model_object should look up model_object Storages::NextcloudStorage - before_action :find_model_object, only: %i[new edit update] + before_action :find_model_object, only: %i[new create edit update] # menu_item is defined in the Redmine::MenuManager::MenuController # module, included from ApplicationController. @@ -60,27 +60,45 @@ def new @storage = ::Storages::Storages::SetNextcloudProviderFieldsAttributesService .new(user: current_user, model: @object, - contract_class: ::Storages::Storages::BaseContract) + contract_class: EmptyContract) .call .result - render '/storages/admin/storages/automatically_managed_project_folders/edit' + + respond_to do |format| + format.html { render '/storages/admin/storages/automatically_managed_project_folders/edit' } + format.turbo_stream { render :edit } + end + end + + def create + service_result = call_update_service + + if service_result.success? + flash[:notice] = I18n.t(:'storages.notice_successful_storage_connection') + redirect_to admin_settings_storages_path + else + respond_to do |format| + format.html { render '/storages/admin/storages/automatically_managed_project_folders/edit' } + format.turbo_stream + end + end end # Renders an edit page (allowing the user to change automatically_managed bool and password). # Used by: The StoragesController#edit, when user wants to update application credentials. # Called by: Global app/config/routes.rb to serve Web page def edit - render '/storages/admin/storages/automatically_managed_project_folders/edit' + respond_to do |format| + format.html { render '/storages/admin/storages/automatically_managed_project_folders/edit' } + format.turbo_stream + end end # Update is similar to create above # See also: create above # Called by: Global app/config/routes.rb to serve Web page def update - service_result = ::Storages::Storages::UpdateService - .new(user: current_user, - model: @storage) - .call(permitted_storage_params_with_defaults) + service_result = call_update_service if service_result.success? flash[:notice] = I18n.t(:notice_successful_update) @@ -113,6 +131,13 @@ def find_model_object(object_id = :storage_id) @storage = @object end + def call_update_service + ::Storages::Storages::UpdateService + .new(user: current_user, + model: @storage) + .call(permitted_storage_params_with_defaults) + end + def permitted_storage_params_with_defaults permitted_storage_params.tap do |permitted_params| # If a checkbox is unchecked when its form is submitted, neither the name nor the value is submitted to the server. diff --git a/modules/storages/app/controllers/storages/admin/oauth_clients_controller.rb b/modules/storages/app/controllers/storages/admin/oauth_clients_controller.rb index 2bb256f6610c..9dd1a2088f70 100644 --- a/modules/storages/app/controllers/storages/admin/oauth_clients_controller.rb +++ b/modules/storages/app/controllers/storages/admin/oauth_clients_controller.rb @@ -37,7 +37,7 @@ class Storages::Admin::OAuthClientsController < ApplicationController before_action :require_admin before_action :find_storage - before_action :delete_current_oauth_client, only: %i[create] + before_action :delete_current_oauth_client, only: %i[create update] # menu_item is defined in the Redmine::MenuManager::MenuController # module, included from ApplicationController. @@ -46,37 +46,68 @@ class Storages::Admin::OAuthClientsController < ApplicationController # Show the admin page to create a new OAuthClient object. def new - @oauth_client = ::OAuthClients::SetAttributesService.new(user: User.current, - model: OAuthClient.new, - contract_class: EmptyContract) - .call - .result - render '/storages/admin/storages/new_oauth_client' + @oauth_client = ::OAuthClients::SetAttributesService + .new(user: User.current, + model: OAuthClient.new, + contract_class: EmptyContract) + .call + .result + + respond_to do |format| + format.html { render '/storages/admin/storages/new_oauth_client' } + format.turbo_stream + end end # Actually create a OAuthClient object. # Use service pattern to create a new OAuthClient # Called by: Global app/config/routes.rb to serve Web page - def create # rubocop:disable Metrics/AbcSize - service_result = ::OAuthClients::CreateService.new(user: User.current) - .call(oauth_client_params.merge(integration: @storage)) - @oauth_client = service_result.result + def create # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity + call_oauth_clients_create_service service_result.on_failure do render '/storages/admin/storages/new_oauth_client' end service_result.on_success do - flash[:notice] = I18n.t(:notice_successful_create) - - if @storage.provider_type_nextcloud? && @storage.automatic_management_unspecified? - if OpenProject::FeatureDecisions.storage_primer_design_active? - redirect_to edit_admin_settings_storage_path(@storage) + if OpenProject::FeatureDecisions.storage_primer_design_active? + if @storage.provider_type_nextcloud? + prepare_storage_for_automatic_management_form + + respond_to do |format| + format.turbo_stream { render :create } + end + elsif @storage.provider_type_one_drive? + flash[:notice] = I18n.t(:'storages.notice_successful_storage_connection') + redirect_to admin_settings_storages_path else - redirect_to new_admin_settings_storage_automatically_managed_project_folders_path(@storage) + raise "Unsupported provider type: #{@storage.short_provider_type}" end else - redirect_to edit_admin_settings_storage_path(@storage) + flash[:notice] = I18n.t(:notice_successful_create) + + if @storage.provider_type_nextcloud? && @storage.automatic_management_unspecified? + prepare_storage_for_automatic_management_form + redirect_to new_admin_settings_storage_automatically_managed_project_folders_path(@storage) + else + redirect_to edit_admin_settings_storage_path(@storage) + end + end + end + end + + def update + call_oauth_clients_create_service + + service_result.on_failure do + respond_to do |format| + format.turbo_stream { render :new } + end + end + + service_result.on_success do + respond_to do |format| + format.turbo_stream { render :update } end end end @@ -96,6 +127,25 @@ def show_local_breadcrumb private + attr_reader :service_result + + def call_oauth_clients_create_service + @service_result = ::OAuthClients::CreateService + .new(user: User.current) + .call(oauth_client_params.merge(integration: @storage)) + @oauth_client = service_result.result + @storage = @storage.reload + end + + def prepare_storage_for_automatic_management_form + return unless @storage.automatic_management_unspecified? + + @storage = ::Storages::Storages::SetNextcloudProviderFieldsAttributesService + .new(user: current_user, model: @storage, contract_class: EmptyContract) + .call + .result + end + # Called by create and update above in order to check if the # update parameters are correctly set. def oauth_client_params diff --git a/modules/storages/app/controllers/storages/admin/storages_controller.rb b/modules/storages/app/controllers/storages/admin/storages_controller.rb index 639b723b75a9..de1d75325290 100644 --- a/modules/storages/app/controllers/storages/admin/storages_controller.rb +++ b/modules/storages/app/controllers/storages/admin/storages_controller.rb @@ -41,7 +41,8 @@ class Storages::Admin::StoragesController < ApplicationController # Before executing any action below: Make sure the current user is an admin # and set the @ variable to the object referenced in the URL. before_action :require_admin - before_action :find_model_object, only: %i[show destroy edit edit_host update replace_oauth_application] + before_action :find_model_object, + only: %i[show show_oauth_application destroy edit edit_host update replace_oauth_application] # menu_item is defined in the Redmine::MenuManager::MenuController # module, included from ApplicationController. @@ -64,53 +65,94 @@ def new # Set default parameters using a "service". # See also: storages/services/storages/storages/set_attributes_services.rb # That service inherits from ::BaseServices::SetAttributes + model_class = OpenProject::FeatureDecisions.storage_primer_design_active? ? Storages::Storage : Storages::NextcloudStorage @storage = ::Storages::Storages::SetAttributesService .new(user: current_user, - model: Storages::NextcloudStorage.new, + model: model_class.new, contract_class: EmptyContract) .call .result + + respond_to do |format| + format.html + format.turbo_stream + end end - # rubocop:disable Metrics/AbcSize - def create - service_result = Storages::Storages::CreateService.new(user: current_user).call(permitted_storage_params) + def select_provider + @object = Storages::Storage.new(permitted_storage_params(:storages_storage)) + service_result = ::Storages::Storages::SetAttributesService + .new(user: current_user, + model: @object, + contract_class: Storages::Storages::BaseContract, + contract_options: { skip_provider_type_strategy: true }) + .call + @storage = service_result.result + + service_result.on_failure { render :new } + + service_result.on_success do + respond_to { |format| format.turbo_stream } + end + end + + def create # rubocop:disable Metrics/AbcSize + service_result = Storages::Storages::CreateService + .new(user: current_user) + .call(permitted_storage_params) @storage = service_result.result @oauth_application = oauth_application(service_result) service_result.on_failure do - render :new + if OpenProject::FeatureDecisions.storage_primer_design_active? + respond_to do |format| + format.turbo_stream { render :select_provider } + end + else + render :new + end end service_result.on_success do - case @storage.provider_type - when ::Storages::Storage::PROVIDER_TYPE_ONE_DRIVE - flash[:notice] = I18n.t(:notice_successful_create) - render '/storages/admin/storages/one_drive/edit' - when ::Storages::Storage::PROVIDER_TYPE_NEXTCLOUD - if @oauth_application.present? + if OpenProject::FeatureDecisions.storage_primer_design_active? + respond_to { |format| format.turbo_stream } + else + case @storage.provider_type + when ::Storages::Storage::PROVIDER_TYPE_ONE_DRIVE flash.now[:notice] = I18n.t(:notice_successful_create) - render :show_oauth_application + render '/storages/admin/storages/one_drive/edit' + when ::Storages::Storage::PROVIDER_TYPE_NEXTCLOUD + if @oauth_application.present? + flash.now[:notice] = I18n.t(:notice_successful_create) + render :show_oauth_application + end + else + raise "Unknown provider type: #{storage_params['provider_type']}" end - else - raise "Unknown provider type: #{storage_params['provider_type']}" end end end - # rubocop:enable Metrics/AbcSize + def show_oauth_application + @oauth_application = @storage.oauth_application + + respond_to { |format| format.turbo_stream } + end # Edit page is very similar to new page, except that we don't need to set # default attribute values because the object already exists; # Called by: Global app/config/routes.rb to serve Web page def edit; end - def edit_host; end + + def edit_host + respond_to { |format| format.turbo_stream } + end # Update is similar to create above # See also: create above # Called by: Global app/config/routes.rb to serve Web page - def update + def update # rubocop:disable Metrics/AbcSize service_result = ::Storages::Storages::UpdateService .new(user: current_user, model: @storage) .call(permitted_storage_params) @@ -118,14 +160,16 @@ def update if service_result.success? flash[:notice] = I18n.t(:notice_successful_update) + respond_to do |format| format.html { redirect_to edit_admin_settings_storage_path(@storage) } format.turbo_stream end - elsif OpenProject::FeatureDecisions.storage_primer_design_active? - render :edit_host else - render :edit + respond_to do |format| + format.html { render :edit } + format.turbo_stream { render :edit_host } + end end end @@ -151,8 +195,6 @@ def replace_oauth_application if service_result.success? flash[:notice] = I18n.t('storages.notice_oauth_application_replaced') render :show_oauth_application - elsif OpenProject::FeatureDecisions.storage_primer_design_active? - render :show_oauth_application else render :edit end @@ -183,20 +225,17 @@ def oauth_application(service_result) # Called by create and update above in order to check if the # update parameters are correctly set. - def permitted_storage_params + def permitted_storage_params(model_parameter_name = storage_provider_parameter_name) params - .require(storage_provider_parameter_name) + .require(model_parameter_name) .permit('name', 'provider_type', 'host', 'oauth_client_id', 'oauth_client_secret', 'tenant_id', 'drive_id') end - # TODO: Work out how to retrieve the storage provider resource name as it's based on the provider type - # PrimerForms implements Rails `form_with` which doesn't support overriding the form name as we would with - # `form_for`. - # See: https://github.com/opf/primer_view_components/blob/79fb58474771bd06946554f8325cd0b1bdd6dd31/app/helpers/primer/form_helper.rb#L7 - # def storage_provider_parameter_name if params.key?(:storages_nextcloud_storage) :storages_nextcloud_storage + elsif params.key?(:storages_one_drive_storage) + :storages_one_drive_storage else :storages_storage end diff --git a/modules/storages/app/forms/storages/admin/managed_project_folders/application_password_input.rb b/modules/storages/app/forms/storages/admin/managed_project_folders/application_password_input.rb index 0e0bde9dd148..a00d3432ad6f 100644 --- a/modules/storages/app/forms/storages/admin/managed_project_folders/application_password_input.rb +++ b/modules/storages/app/forms/storages/admin/managed_project_folders/application_password_input.rb @@ -33,7 +33,9 @@ class ApplicationPasswordInput < ApplicationForm name: :password, label: I18n.t(:'storages.label_managed_project_folders.application_password'), required: true, - caption: application_password_caption + caption: application_password_caption, + value: nil, # IMPORTANT: We don't want to show the password in the form + placeholder: @storage.password.present? ? "••••••••••••••••" : nil ) end diff --git a/modules/storages/app/forms/storages/admin/oauth_client_form.rb b/modules/storages/app/forms/storages/admin/oauth_client_form.rb index 8bad45364cdc..53568462b5c9 100644 --- a/modules/storages/app/forms/storages/admin/oauth_client_form.rb +++ b/modules/storages/app/forms/storages/admin/oauth_client_form.rb @@ -29,22 +29,35 @@ module Storages::Admin class OAuthClientForm < ApplicationForm form do |oauth_client_form| - oauth_client_form.text_field(name: :client_id, - label: label_client_id, - required: true) - - oauth_client_form.text_field(name: :client_secret, - label: label_client_secret, - required: true) + oauth_client_form.text_field(**@client_id_input_options) + oauth_client_form.text_field(**@client_secret_input_options) end - def initialize(storage:) + def initialize(storage:, client_id_input_options: {}, client_secret_input_options: {}) super() @storage = storage + @client_id_input_options = default_client_id_input_options.merge(client_id_input_options) + @client_secret_input_options = default_client_secret_input_options.merge(client_secret_input_options) end private + def default_client_id_input_options + { + name: :client_id, + label: label_client_id, + required: true + } + end + + def default_client_secret_input_options + { + name: :client_secret, + label: label_client_secret, + required: true + } + end + def label_client_id [label_provider_name, I18n.t('storages.label_oauth_client_id')].join(' ') end diff --git a/modules/storages/app/forms/storages/admin/provider_drive_id_input_form.rb b/modules/storages/app/forms/storages/admin/provider_drive_id_input_form.rb new file mode 100644 index 000000000000..126fc7f90a23 --- /dev/null +++ b/modules/storages/app/forms/storages/admin/provider_drive_id_input_form.rb @@ -0,0 +1,42 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 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 Storages::Admin + class ProviderDriveIdInputForm < ApplicationForm + form do |storage_form| + storage_form.text_field( + name: :drive_id, + label: Storages::Admin::LABEL_DRIVE_ID, + visually_hide_label: false, + required: true, + caption: I18n.t("storages.instructions.one_drive.drive_id"), + placeholder: I18n.t("storages.instructions.one_drive.drive_id_placeholder") + ) + end + end +end diff --git a/modules/storages/app/forms/storages/admin/provider_host_input_form.rb b/modules/storages/app/forms/storages/admin/provider_host_input_form.rb new file mode 100644 index 000000000000..fb49ed372a78 --- /dev/null +++ b/modules/storages/app/forms/storages/admin/provider_host_input_form.rb @@ -0,0 +1,44 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 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 Storages::Admin + class ProviderHostInputForm < ApplicationForm + form do |storage_form| + storage_form.text_field( + name: :host, + label: I18n.t('activerecord.attributes.storages/storage.host'), + visually_hide_label: false, + required: true, + type: :url, + pattern: ".{1,255}", + placeholder: "https://my-file-storage.com", + caption: I18n.t('storages.instructions.host') + ) + end + end +end diff --git a/modules/storages/app/forms/storages/admin/provider_name_input_form.rb b/modules/storages/app/forms/storages/admin/provider_name_input_form.rb new file mode 100644 index 000000000000..a963517e713d --- /dev/null +++ b/modules/storages/app/forms/storages/admin/provider_name_input_form.rb @@ -0,0 +1,41 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 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 Storages::Admin + class ProviderNameInputForm < ApplicationForm + form do |storage_form| + storage_form.text_field( + name: :name, + label: I18n.t('activerecord.attributes.storages/storage.name'), + required: true, + caption: I18n.t('storages.instructions.name'), + placeholder: I18n.t("storages.label_file_storage") + ) + end + end +end diff --git a/modules/storages/app/forms/storages/admin/provider_tenant_id_input_form.rb b/modules/storages/app/forms/storages/admin/provider_tenant_id_input_form.rb new file mode 100644 index 000000000000..91296346b197 --- /dev/null +++ b/modules/storages/app/forms/storages/admin/provider_tenant_id_input_form.rb @@ -0,0 +1,42 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 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 Storages::Admin + class ProviderTenantIdInputForm < ApplicationForm + form do |storage_form| + storage_form.text_field( + name: :tenant_id, + label: I18n.t('activerecord.attributes.storages/storage.tenant'), + visually_hide_label: false, + required: true, + caption: I18n.t("storages.instructions.one_drive.tenant_id"), + placeholder: I18n.t("storages.instructions.one_drive.tenant_id_placeholder") + ) + end + end +end diff --git a/modules/storages/app/forms/storages/admin/storage_provider_form.rb b/modules/storages/app/forms/storages/admin/provider_type_select_form.rb similarity index 69% rename from modules/storages/app/forms/storages/admin/storage_provider_form.rb rename to modules/storages/app/forms/storages/admin/provider_type_select_form.rb index 068004abeb4b..167135024829 100644 --- a/modules/storages/app/forms/storages/admin/storage_provider_form.rb +++ b/modules/storages/app/forms/storages/admin/provider_type_select_form.rb @@ -27,18 +27,10 @@ #++ module Storages::Admin - class StorageProviderForm < ApplicationForm - form do |storage_provider_form| - storage_provider_form.select_list( - name: :provider_type, - label: I18n.t('activerecord.attributes.storages/storage.provider_type'), - caption: I18n.t('storages.instructions.provider_type', - type_link_text: I18n.t('storages.instructions.type_link_text')), - include_blank: false, - required: true, - disabled: @storage.persisted? - ) do |storage_provider_list| - if @storage.persisted? + class ProviderTypeSelectForm < ApplicationForm + form do |storage_form| + storage_form.select_list(**@select_list_options) do |storage_provider_list| + if @storage.provider_type.present? storage_provider_list.option( label: I18n.t("storages.provider_types.#{@storage.short_provider_type}.name"), value: @storage.provider_type @@ -52,26 +44,25 @@ class StorageProviderForm < ApplicationForm end end end - - storage_provider_form.text_field( - name: :name, - label: I18n.t('activerecord.attributes.storages/storage.name'), - required: true, - caption: I18n.t('storages.instructions.name') - ) - - storage_provider_form.text_field( - name: :host, - label: I18n.t('activerecord.attributes.storages/storage.host'), - visually_hide_label: false, - required: true, - caption: I18n.t('storages.instructions.host') - ) end - def initialize(storage:) + def initialize(storage:, select_list_options: {}) super() @storage = storage + @select_list_options = default_select_list_options(storage).merge(select_list_options) + end + + private + + def default_select_list_options(storage) + { + name: :provider_type, + label: I18n.t('activerecord.attributes.storages/storage.provider_type'), + caption: nil, + include_blank: false, + required: true, + disabled: storage.persisted? + } end end end diff --git a/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb b/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb new file mode 100644 index 000000000000..0bc365e2bceb --- /dev/null +++ b/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb @@ -0,0 +1,66 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2023 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 Storages::Admin + class SubmitOrCancelForm < ApplicationForm + form do |buttons| + buttons.group(layout: :horizontal) do |button_group| + button_group.submit(**@submit_button_options) + button_group.button(**@cancel_button_options) + end + end + + def initialize(storage:, submit_button_options: {}, cancel_button_options: {}) + super() + @storage = storage + @submit_button_options = default_submit_button_options.merge(submit_button_options) + @cancel_button_options = default_cancel_button_options.merge(cancel_button_options) + end + + private + + def default_submit_button_options + { + name: :submit, + scheme: :primary, + label: I18n.t('storages.buttons.save_and_continue'), + disabled: false + } + end + + def default_cancel_button_options + { + name: :cancel, + scheme: :default, + tag: :a, + href: Rails.application.routes.url_helpers.admin_settings_storages_path, + label: I18n.t('button_cancel') + } + end + end +end diff --git a/modules/storages/app/models/storages/nextcloud_storage.rb b/modules/storages/app/models/storages/nextcloud_storage.rb index 06451b2beb78..95f73c2d5169 100644 --- a/modules/storages/app/models/storages/nextcloud_storage.rb +++ b/modules/storages/app/models/storages/nextcloud_storage.rb @@ -60,6 +60,15 @@ def oauth_configuration Peripherals::OAuthConfigurations::NextcloudConfiguration.new(self) end + def automatic_management_new_record? + if provider_fields_changed? + previous_configuration = provider_fields_change.first + previous_configuration.values_at('automatically_managed', 'password').compact.empty? + else + automatic_management_unspecified? + end + end + def automatic_management_unspecified? automatically_managed.nil? end diff --git a/modules/storages/app/models/storages/one_drive_storage.rb b/modules/storages/app/models/storages/one_drive_storage.rb index 1d2e116cc328..d6c6e0ccc6bd 100644 --- a/modules/storages/app/models/storages/one_drive_storage.rb +++ b/modules/storages/app/models/storages/one_drive_storage.rb @@ -36,7 +36,11 @@ class OneDriveStorage < Storage using ::Storages::Peripherals::ServiceResultRefinements def configuration_checks - { storage_oauth_client_configured: oauth_client.present? } + { + storage_oauth_client_configured: oauth_client.present?, + storage_tenant_drive_configured: tenant_id.present? && drive_id.present?, + host_name_configured: name.present? + } end def oauth_configuration diff --git a/modules/storages/app/views/storages/admin/automatically_managed_project_folders/create.turbo_stream.erb b/modules/storages/app/views/storages/admin/automatically_managed_project_folders/create.turbo_stream.erb new file mode 100644 index 000000000000..3a4acabc9b4c --- /dev/null +++ b/modules/storages/app/views/storages/admin/automatically_managed_project_folders/create.turbo_stream.erb @@ -0,0 +1,32 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 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. + +++#%> + +<%= turbo_stream.update :automatically_managed_project_folders_section do %> + <%= render(Storages::Admin::Forms::AutomaticallyManagedProjectFoldersFormComponent.new(@storage)) %> +<% end %> diff --git a/modules/storages/app/views/storages/admin/automatically_managed_project_folders/edit.turbo_stream.erb b/modules/storages/app/views/storages/admin/automatically_managed_project_folders/edit.turbo_stream.erb new file mode 100644 index 000000000000..3a4acabc9b4c --- /dev/null +++ b/modules/storages/app/views/storages/admin/automatically_managed_project_folders/edit.turbo_stream.erb @@ -0,0 +1,32 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 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. + +++#%> + +<%= turbo_stream.update :automatically_managed_project_folders_section do %> + <%= render(Storages::Admin::Forms::AutomaticallyManagedProjectFoldersFormComponent.new(@storage)) %> +<% end %> diff --git a/modules/storages/app/views/storages/admin/oauth_clients/create.turbo_stream.erb b/modules/storages/app/views/storages/admin/oauth_clients/create.turbo_stream.erb new file mode 100644 index 000000000000..3fd98e27146c --- /dev/null +++ b/modules/storages/app/views/storages/admin/oauth_clients/create.turbo_stream.erb @@ -0,0 +1,38 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 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. + +++#%> + +<%= turbo_stream.update :storage_oauth_client_section do %> + <%= render(Storages::Admin::OAuthClientInfoComponent.new(oauth_client: @oauth_client, storage: @storage)) %> +<% end %> + +<% if @storage.provider_type_nextcloud? && @storage.automatic_management_new_record? %> + <%= turbo_stream.update :automatically_managed_project_folders_section do %> + <%= render(Storages::Admin::Forms::AutomaticallyManagedProjectFoldersFormComponent.new(@storage)) %> + <% end %> +<% end %> diff --git a/modules/storages/app/views/storages/admin/oauth_clients/edit.html.erb b/modules/storages/app/views/storages/admin/oauth_clients/edit.html.erb deleted file mode 100644 index 5c5f1cd484a3..000000000000 --- a/modules/storages/app/views/storages/admin/oauth_clients/edit.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<%= - render(OpTurbo::FrameComponent.new(@storage, context: :oauth_client)) do - render(Storages::Admin::Forms::OAuthClientFormComponent.new(@storage)) - end -%> diff --git a/modules/storages/app/views/storages/admin/oauth_clients/new.turbo_stream.erb b/modules/storages/app/views/storages/admin/oauth_clients/new.turbo_stream.erb new file mode 100644 index 000000000000..c29aafa2e050 --- /dev/null +++ b/modules/storages/app/views/storages/admin/oauth_clients/new.turbo_stream.erb @@ -0,0 +1,45 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 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. + +++#%> + +<%= turbo_stream.update :storage_openproject_oauth_section do %> + <%= + render( + Storages::Admin::OAuthApplicationInfoComponent.new( + oauth_application: @oauth_application, + storage: @storage, + ) + ) + %> +<% end %> + +<% if @storage.provider_type_nextcloud? && @storage.oauth_client.blank? %> + <%= turbo_stream.update :storage_oauth_client_section do %> + <%= render(Storages::Admin::Forms::OAuthClientFormComponent.new(oauth_client: @oauth_client, storage: @storage)) %> + <% end %> +<% end %> diff --git a/modules/storages/app/views/storages/admin/oauth_clients/update.turbo_stream.erb b/modules/storages/app/views/storages/admin/oauth_clients/update.turbo_stream.erb new file mode 100644 index 000000000000..9a27fd06284a --- /dev/null +++ b/modules/storages/app/views/storages/admin/oauth_clients/update.turbo_stream.erb @@ -0,0 +1,32 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 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. + +++#%> + +<%= turbo_stream.update :storage_oauth_client_section do %> + <%= render(Storages::Admin::OAuthClientInfoComponent.new(oauth_client: @oauth_client, storage: @storage)) %> +<% end %> diff --git a/modules/storages/app/views/storages/admin/storages/automatically_managed_project_folders/edit.html.erb b/modules/storages/app/views/storages/admin/storages/automatically_managed_project_folders/edit.html.erb index 5efe03a9cbf2..a2d761da78b0 100644 --- a/modules/storages/app/views/storages/admin/storages/automatically_managed_project_folders/edit.html.erb +++ b/modules/storages/app/views/storages/admin/storages/automatically_managed_project_folders/edit.html.erb @@ -31,9 +31,7 @@ See COPYRIGHT and LICENSE files for more details. <% if OpenProject::FeatureDecisions.storage_primer_design_active? %> <%= - render(OpTurbo::FrameComponent.new(@storage, context: :automatically_managed_project_folders)) do - render(Storages::Admin::Forms::AutomaticallyManagedProjectFoldersFormComponent.new(@storage)) - end + render(Storages::Admin::StorageViewComponent.new(@storage, automatically_managed_project_folders_section_open: true)) %> <% else %> <% local_assigns[:additional_breadcrumb] = [ diff --git a/modules/storages/app/views/storages/admin/storages/create.turbo_stream.erb b/modules/storages/app/views/storages/admin/storages/create.turbo_stream.erb new file mode 100644 index 000000000000..155a9b7fd5c2 --- /dev/null +++ b/modules/storages/app/views/storages/admin/storages/create.turbo_stream.erb @@ -0,0 +1,56 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 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. + +++#%> + +<%= turbo_stream.update :storage_general_info_section do %> + <%= render(Storages::Admin::GeneralInfoComponent.new(@storage)) %> +<% end %> + + +<% if @storage.provider_type_nextcloud? %> + <%= turbo_stream.update :storage_openproject_oauth_section do %> + <%= + render( + Storages::Admin::OAuthApplicationInfoCopyComponent.new( + oauth_application: @oauth_application, + storage: @storage, + submit_button_options: { + data: { turbo_stream: true } + }, + submit_button_path: new_admin_settings_storage_oauth_client_path(@storage) + ) + ) + %> + <% end %> +<% end %> + +<% if @storage.provider_type_one_drive? %> + <%= turbo_stream.update :storage_oauth_client_section do %> + <%= render(Storages::Admin::Forms::OAuthClientFormComponent.new(oauth_client: @storage.build_oauth_client, storage: @storage)) %> + <% end %> +<% end %> diff --git a/modules/storages/app/views/storages/admin/storages/edit_host.html.erb b/modules/storages/app/views/storages/admin/storages/edit_host.html.erb deleted file mode 100644 index 6c566f6fdb98..000000000000 --- a/modules/storages/app/views/storages/admin/storages/edit_host.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<%= - render(OpTurbo::FrameComponent.new(@storage, context: :storage_view_general_info)) do - render(Storages::Admin::Forms::GeneralInfoFormComponent.new(@storage)) - end -%> diff --git a/modules/storages/app/views/storages/admin/storages/edit_host.turbo_stream.erb b/modules/storages/app/views/storages/admin/storages/edit_host.turbo_stream.erb new file mode 100644 index 000000000000..3e37f5e0dc52 --- /dev/null +++ b/modules/storages/app/views/storages/admin/storages/edit_host.turbo_stream.erb @@ -0,0 +1,40 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 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. + +++#%> + +<%= turbo_stream.update :storage_general_info_section do %> + <%= + render( + Storages::Admin::Forms::GeneralInfoFormComponent.new( + @storage, + form_method: :patch, + cancel_button_path: edit_admin_settings_storage_path(@storage) + ) + ) + %> +<% end %> diff --git a/modules/storages/app/views/storages/admin/storages/new.html.erb b/modules/storages/app/views/storages/admin/storages/new.html.erb index b575b763bc41..2de44f42eb4b 100644 --- a/modules/storages/app/views/storages/admin/storages/new.html.erb +++ b/modules/storages/app/views/storages/admin/storages/new.html.erb @@ -1,14 +1,38 @@ -<% html_title t(:label_administration), t("project_module_storages"), t('storages.label_new_storage') %> -<% local_assigns[:additional_breadcrumb] = t('storages.label_new_storage') %> -<%= toolbar title: t("storages.label_new_storage") %> - -<%= labelled_tabular_form_for @storage, url: admin_settings_storages_path(@storage), as: :storages_storage do |f| -%> - <%= render partial: 'new', locals: { f: f } %> - <% if @storage.oauth_client %> - <%= styled_button_tag t(:button_save), class: "-highlight -with-icon icon-checkmark" %> - <% else %> - <%= styled_button_tag t("storages.buttons.save_and_continue_setup"), class: "-highlight -with-icon icon-checkmark" %> +<% if OpenProject::FeatureDecisions.storage_primer_design_active? %> + <% html_title t(:label_administration), t("project_module_storages"), t('storages.label_new_file_storage') %> + <% local_assigns[:additional_breadcrumb] = t('storages.label_new_file_storage') %> + + <%= render(Primer::OpenProject::PageHeader.new) do |header| %> + <% header.with_title(test_selector: 'storage-name-title') do %> + <%= t("storages.label_new_file_storage") %> + <% end %> + + <% header.with_description do %> + <%= + t("storages.instructions.new_storage", + new_storage_link_text: render( + Primer::Beta::Link.new(href: ::OpenProject::Static::Links[:storage_docs][:setup][:href], target: '_blank') + ) { t("storages.instructions.new_storage_link_text") } + ).html_safe + %> + <% end %> + + <% end %> + + <%= render(::Storages::Admin::SelectStorageProviderComponent.new(@storage)) %> +<% else %> + <% html_title t(:label_administration), t("project_module_storages"), t('storages.label_new_storage') %> + <% local_assigns[:additional_breadcrumb] = t('storages.label_new_storage') %> + <%= toolbar title: t("storages.label_new_storage") %> + + <%= labelled_tabular_form_for @storage, url: admin_settings_storages_path(@storage), as: :storages_storage do |f| -%> + <%= render partial: 'new', locals: { f: f } %> + <% if @storage.oauth_client %> + <%= styled_button_tag t(:button_save), class: "-highlight -with-icon icon-checkmark" %> + <% else %> + <%= styled_button_tag t("storages.buttons.save_and_continue_setup"), class: "-highlight -with-icon icon-checkmark" %> + <% end %> + <%= link_to t(:button_cancel), admin_settings_storages_path, class: 'button -with-icon icon-close' %> <% end %> - <%= link_to t(:button_cancel), admin_settings_storages_path, class: 'button -with-icon icon-close' %> <% end %> diff --git a/modules/storages/app/views/storages/admin/storages/new.turbo_stream.erb b/modules/storages/app/views/storages/admin/storages/new.turbo_stream.erb new file mode 100644 index 000000000000..036c0a4897ce --- /dev/null +++ b/modules/storages/app/views/storages/admin/storages/new.turbo_stream.erb @@ -0,0 +1,34 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 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. + +++#%> + +<%= turbo_stream.replace :select_storage_provider do %> + <%= + render(::Storages::Admin::SelectStorageProviderComponent.new(@storage)) + %> +<% end %> diff --git a/modules/storages/app/views/storages/admin/storages/new_oauth_client.html.erb b/modules/storages/app/views/storages/admin/storages/new_oauth_client.html.erb index a01eb5eeac35..2d2513fabe92 100644 --- a/modules/storages/app/views/storages/admin/storages/new_oauth_client.html.erb +++ b/modules/storages/app/views/storages/admin/storages/new_oauth_client.html.erb @@ -31,7 +31,7 @@ See COPYRIGHT and LICENSE files for more details. <% if OpenProject::FeatureDecisions.storage_primer_design_active? %> <%= - render(OpTurbo::FrameComponent.new(@storage, context: :oauth_client)) do + render(OpTurbo::FrameComponent.new(id: :storage_oauth_client_section)) do render(Storages::Admin::Forms::OAuthClientFormComponent.new(oauth_client: @oauth_client, storage: @storage)) end %> diff --git a/modules/storages/app/views/storages/admin/storages/one_drive/_oauth_client.html.erb b/modules/storages/app/views/storages/admin/storages/one_drive/_oauth_client.html.erb index 5ab621612776..f007793e9628 100644 --- a/modules/storages/app/views/storages/admin/storages/one_drive/_oauth_client.html.erb +++ b/modules/storages/app/views/storages/admin/storages/one_drive/_oauth_client.html.erb @@ -39,7 +39,8 @@ See COPYRIGHT and LICENSE files for more details. size: 40, container_class: '-wide', data: { - 'storages--oauth-client-form-target': "clientId" + 'storages--oauth-client-form-target': "clientId", + action: 'input->storages--oauth-client-form#setRedirectUriValue' } %> <%= t("storages.instructions.#{@storage.short_provider_type}.oauth_client_id") %> diff --git a/modules/storages/app/views/storages/admin/storages/select_provider.turbo_stream.erb b/modules/storages/app/views/storages/admin/storages/select_provider.turbo_stream.erb new file mode 100644 index 000000000000..c55a50808861 --- /dev/null +++ b/modules/storages/app/views/storages/admin/storages/select_provider.turbo_stream.erb @@ -0,0 +1,34 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 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. + +++#%> + +<%= turbo_stream.update :select_storage_provider do %> + <%= + render(Storages::Admin::StorageViewComponent.new(@storage)) + %> +<% end %> diff --git a/modules/storages/app/views/storages/admin/storages/show_oauth_application.html.erb b/modules/storages/app/views/storages/admin/storages/show_oauth_application.html.erb index 3d546019ee1f..1d12b121b1a4 100644 --- a/modules/storages/app/views/storages/admin/storages/show_oauth_application.html.erb +++ b/modules/storages/app/views/storages/admin/storages/show_oauth_application.html.erb @@ -1,6 +1,6 @@ <% if OpenProject::FeatureDecisions.storage_primer_design_active? %> - <%= render(OpTurbo::FrameComponent.new(@storage, context: :openproject_oauth)) do %> - <%= render(Storages::Admin::OAuthApplicationCredentialsCopyComponent.new(oauth_application: @oauth_application, storage: @storage)) %> + <%= render(OpTurbo::FrameComponent.new(id: :storage_openproject_oauth_section)) do %> + <%= render(Storages::Admin::OAuthApplicationInfoCopyComponent.new(oauth_application: @oauth_application, storage: @storage)) %> <% end %> <% else %> <% html_title t(:label_administration), t("project_module_storages"), @storage.name, "#{t("storages.provider_types.#{@storage.short_provider_type}.name")} #{t("storages.label_oauth_application_details")}" %> diff --git a/modules/storages/app/views/storages/admin/storages/show_oauth_application.turbo_stream.erb b/modules/storages/app/views/storages/admin/storages/show_oauth_application.turbo_stream.erb new file mode 100644 index 000000000000..95531304160f --- /dev/null +++ b/modules/storages/app/views/storages/admin/storages/show_oauth_application.turbo_stream.erb @@ -0,0 +1,47 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2023 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. + +++#%> + +<%= turbo_stream.update :storage_general_info_section do %> + <%= render(Storages::Admin::GeneralInfoComponent.new(@storage)) %> +<% end %> + +<%= turbo_stream.update :storage_openproject_oauth_section do %> + <%= + render( + Storages::Admin::OAuthApplicationInfoCopyComponent.new( + oauth_application: @oauth_application, + storage: @storage, + submit_button_options: { + data: { turbo_stream: true } + }, + submit_button_path: new_admin_settings_storage_oauth_client_path(@storage) + ) + ) + %> +<% end %> diff --git a/modules/storages/app/views/storages/admin/storages/update.turbo_stream.erb b/modules/storages/app/views/storages/admin/storages/update.turbo_stream.erb index 6da3cda73937..25bcfabe222c 100644 --- a/modules/storages/app/views/storages/admin/storages/update.turbo_stream.erb +++ b/modules/storages/app/views/storages/admin/storages/update.turbo_stream.erb @@ -1,6 +1,7 @@ -<%= turbo_stream.update dom_id(@storage, :storage_view_general_info) do %> - <%= render(::Storages::Admin::StorageGeneralInfoComponent.new(@storage)) %> -<% end %> <%= turbo_stream.update dom_id(@storage, :edit_storage_header) do %> <%= @storage.name %> <% end %> + +<%= turbo_stream.update :storage_general_info_section do %> + <%= render(::Storages::Admin::GeneralInfoComponent.new(@storage)) %> +<% end %> diff --git a/modules/storages/config/locales/en.yml b/modules/storages/config/locales/en.yml index 1491f912041f..ae5b7bfb5de5 100644 --- a/modules/storages/config/locales/en.yml +++ b/modules/storages/config/locales/en.yml @@ -30,6 +30,7 @@ en: creator: "Creator" provider_type: "Provider type" host: "Host" + tenant: "Tenant" storages/file_link: origin_id: "Origin Id" @@ -88,6 +89,7 @@ en: project_folders: "Project folders" storage_provider: "Storage provider" openproject_oauth: "OpenProject OAuth" + one_drive_oauth: "Azure OAuth" nextcloud_oauth: "Nextcloud OAuth" automatically_managed_folders: "Automatically managed folders" page_titles: @@ -108,7 +110,6 @@ en: title: "Members connection status" subtitle: "Check the connection status for the storage %{storage_name_link} of all project members." instructions: - provider_type: "Please make sure you have administration privileges in your Nextcloud instance and the application %{type_link_text} is installed before doing the setup." type: "Please make sure you have administration privileges in your Nextcloud instance and have the following application installed before doing the setup:" type_link_text: "“Integration OpenProject”" name: "Give your storage a name so that users can differentiate between multiple storages." @@ -116,6 +117,8 @@ en: managed_project_folders_application_password: > Copy this value from: managed_project_folders_application_password_caption: "Enable automatic managed folders by copying this value from: %{provider_type_link}." + new_storage: "Read our %{new_storage_link_text} for more information about this setup." + new_storage_link_text: "file storage documentation" no_storage_set_up: "There are no file storages set up yet." no_specific_folder: "By default, each user will start at their own home folder when they upload a file." automatic_folder: "This will automatically create a root folder for this project and manage the access permissions for each project member." @@ -133,26 +136,34 @@ en: oauth_application_details_link_text: "Nextcloud OpenProject Integration settings" copy_from: "Copy this value from" nextcloud: + provider_configuration: "Please make sure you have administration privileges in your Nextcloud instance and the %{application_link_text} is installed before doing the setup." + oauth_configuration: "Copy these values from %{application_link_text}." + application_link_text: "application “Integration OpenProject”" integration: "Nextcloud Administration / OpenProject" one_drive: + provider_configuration: "Please make sure you have administration privileges in the %{application_link_text} before doing the setup." + oauth_configuration: "Copy these values from the %{application_link_text}. After that, copy the redirect URI back to the %{application_link_text}." + application_link_text: "Azure application" integration: "OneDrive/SharePoint" oauth_client_id: > Copy the client id from the Azure portal. This is needed to generate the redirect URI. oauth_client_secret: > Copy the client secret from the Azure portal. For a newly created application the secret first needs to be created manually. For authorization of web applications a secret is mandatory. - missing_client_id_for_redirect_uri: "Client ID missing to provide redirect URI." - tenant_id: > - Please insert the tenant ID you got from your SharePoint administrator. - drive_id: > - The drive ID can be obtained by you SharePoint administrator. + missing_client_id_for_redirect_uri: "Please fill the OAuth values to generate a URI" + tenant_id: "Please copy the tenant from your Azure application." + tenant_id_placeholder: "Name or UUID" + drive_id: "Please copy the drive ID from your Azure application." + drive_id_placeholder: "UUID or triple ID" help_texts: project_folder: > The project folder is the default folder for file uploads for this project. Users can nevertheless still upload files to other locations. configuration_checks: incomplete: "The setup of this storage is incomplete." - oauth_client_incomplete: "Allow OpenProject to access %{provider} data using an OAuth." + oauth_client_incomplete: + nextcloud: "Allow OpenProject to access Nextcloud data using OAuth." + one_drive: "Allow OpenProject to access Azure data using OAuth to connect OneDrive/Sharepoint." delete_warning: storage: > Are you sure you want to delete this storage? This will also delete the storage from all projects where it is used. @@ -175,8 +186,9 @@ en: label_provider: "Provider" label_file_link: "File link" label_file_links: "File links" + label_file_storage: "File storage" label_creation_time: "Creation time" - label_connected: "Connected" + label_completed: "Completed" label_incomplete: "Incomplete" label_name: "Name" label_host: "Host URL" @@ -197,7 +209,9 @@ en: label_information: "Additional information" label_provider_type: "Provider type" label_project_folder: "Project folder" + label_redirect_uri: "Redirect URI" label_new_storage: "New storage" + label_new_file_storage: "New file storage" label_edit_storage: "Edit storage" label_edit_storage_host: "Edit storage host" label_edit_storage_oauth_client: "Edit storage OAuth client" @@ -206,6 +220,7 @@ en: label_no_specific_folder: "No specific folder" label_automatic_folder: "New folder with automatically managed permissions" label_no_selected_folder: "No selected folder" + label_select_provider: "Select provider" label_storage: "Storage" label_storages: "Storages" label_status: "Status" @@ -229,3 +244,4 @@ en: oauth_client_details_missing: "To complete the setup, please add OAuth client credentials from your storage." automatically_managed_project_folder_missing: "To complete the setup, please configure automatically managed project folders for your storage." notice_oauth_application_replaced: "The OpenProject OAuth application was successfully replaced." + notice_successful_storage_connection: "Storage connected successfully! Remember to activate the module and the specific storage in the project settings of each desired project to use it." diff --git a/modules/storages/config/routes.rb b/modules/storages/config/routes.rb index 7d9c98d2978c..f221bdef73fc 100644 --- a/modules/storages/config/routes.rb +++ b/modules/storages/config/routes.rb @@ -32,13 +32,19 @@ namespace :admin do namespace :settings do resources :storages, controller: '/storages/admin/storages', except: [:show] do - resource :oauth_client, controller: '/storages/admin/oauth_clients', only: %i[new create] + resource :oauth_client, controller: '/storages/admin/oauth_clients', only: %i[new create] do + patch :update, on: :member + end + resource :automatically_managed_project_folders, controller: '/storages/admin/automatically_managed_project_folders', - only: %i[new edit update] + only: %i[new create edit update] + + post :select_provider, on: :collection member do - get '/edit_host' => '/storages/admin/storages#edit_host' - delete '/replace_oauth_application' => '/storages/admin/storages#replace_oauth_application' + get :show_oauth_application + get :edit_host + delete :replace_oauth_application end end end diff --git a/modules/storages/spec/contracts/storages/storages/shared_contract_examples.rb b/modules/storages/spec/contracts/storages/storages/shared_contract_examples.rb index bfdd78817e51..0c1c126ea009 100644 --- a/modules/storages/spec/contracts/storages/storages/shared_contract_examples.rb +++ b/modules/storages/spec/contracts/storages/storages/shared_contract_examples.rb @@ -89,7 +89,7 @@ it_behaves_like 'storage contract' end -RSpec.shared_examples_for 'nextcloud storage contract', :storage_server_helpers, webmock: true do +RSpec.shared_examples_for 'nextcloud storage contract', :storage_server_helpers, :webmock do include_context 'ModelContract shared context' # Only admins have the right to create/delete storages. @@ -289,5 +289,32 @@ it_behaves_like 'contract is invalid', username: :present, password: :present end + + describe 'provider_type_strategy' do + before do + allow(contract).to receive(:provider_type_strategy) + end + + context 'without `skip_provider_type_strategy` option' do + it 'validates the provider type contract' do + contract.validate + + expect(contract).to have_received(:provider_type_strategy) + end + end + + context 'with `skip_provider_type_strategy` option' do + let(:contract) do + described_class.new(storage, build_stubbed(:admin), + options: { skip_provider_type_strategy: true }) + end + + it 'does not validate the provider type' do + contract.validate + + expect(contract).not_to have_received(:provider_type_strategy) + end + end + end end end diff --git a/modules/storages/spec/factories/storage_factory.rb b/modules/storages/spec/factories/storage_factory.rb index 7f10d56c559f..7e1b1e445a89 100644 --- a/modules/storages/spec/factories/storage_factory.rb +++ b/modules/storages/spec/factories/storage_factory.rb @@ -38,6 +38,7 @@ trait :with_oauth_client do oauth_client { build(:oauth_client) } end + # rubocop:enable FactoryBot/FactoryAssociationWithStrategy trait :as_generic do provider_type { 'Storages::Storage' } @@ -109,6 +110,8 @@ parent: :storage, class: '::Storages::OneDriveStorage' do host { nil } + tenant_id { SecureRandom.uuid } + drive_id { SecureRandom.uuid } end factory :sharepoint_dev_drive_storage, diff --git a/modules/storages/spec/features/admin_storages_spec.rb b/modules/storages/spec/features/admin_storages_spec.rb index 88950db51650..68035f8403ad 100644 --- a/modules/storages/spec/features/admin_storages_spec.rb +++ b/modules/storages/spec/features/admin_storages_spec.rb @@ -89,156 +89,202 @@ end end - describe 'File storage edit view', with_flag: { storage_primer_design: true } do - let(:storage) { create(:nextcloud_storage) } - let(:oauth_application) { create(:oauth_application, integration: storage) } - - before { oauth_application } + describe 'New file storage', with_flag: { storage_primer_design: true } do + context 'with Nextcloud Storage' do + it 'renders a Nextcloud specific multi-step form', :webmock do + visit admin_settings_storages_path - it 'renders the edit view', :webmock do - visit edit_admin_settings_storage_path(storage) + within('.blankslate') { click_link("Storage") } + expect(page).to have_current_path(new_admin_settings_storage_path) + + aggregate_failures 'Select provider view' do + # General information + expect(page).to have_select('storages_storage[provider_type]', with_options: %w[Nextcloud OneDrive/SharePoint]) + expect(find_test_selector('storage-select-provider-submit-button')).to be_disabled + + # Select Nextcloud + select('Nextcloud', from: 'storages_storage[provider_type]') + + # OAuth application + expect(page).to have_test_selector('storage-openproject-oauth-label', text: 'OpenProject OAuth') + expect(page).to have_test_selector('label-openproject_oauth_application_configured-status', text: 'Incomplete') + + # OAuth client + wait_for(page).to have_test_selector('storage-oauth-client-label', text: 'Nextcloud OAuth') + expect(page).to have_test_selector('label-storage_oauth_client_configured-status', text: 'Incomplete') + expect(page).to have_test_selector('storage-oauth-client-id-description', + text: "Allow OpenProject to access Nextcloud data using OAuth.") + + # Automatically managed project folders + expect(page).to have_test_selector('storage-managed-project-folders-label', + text: 'Automatically managed folders') + expect(page).to have_test_selector('label-managed-project-folders-status', text: 'Incomplete') + expect(page).to have_test_selector('storage-automatically-managed-project-folders-description', + text: 'Let OpenProject create folders per project automatically.') + end - expect(page).to have_test_selector('storage-name-title', text: storage.name.capitalize) + aggregate_failures 'General information' do + within_test_selector('storage-general-info-form') do + fill_in 'storages_nextcloud_storage_name', with: 'My Nextcloud', fill_options: { clear: :backspace } + click_button 'Save and continue' - aggregate_failures 'General information' do - expect(page).to have_test_selector('storage-provider-label', text: 'Storage provider') - expect(page).to have_test_selector('label-host_name_configured-status', text: 'Connected') - expect(page).to have_test_selector('storage-description', text: [storage.short_provider_type.capitalize, - storage.name, - storage.host].join(' - ')) + expect(page).to have_text("Host is not a valid URL.") - # Update a storage - happy path - find_test_selector('storage-edit-host-button').click - within_test_selector('storage-general-info-form') do - expect(page).to have_css('#storages_nextcloud_storage_provider_type[disabled]') + mock_server_capabilities_response("https://example.com") + mock_server_config_check_response("https://example.com") + fill_in 'storages_nextcloud_storage_host', with: 'https://example.com' + click_button 'Save and continue' + end - fill_in 'storages_nextcloud_storage_name', with: 'My Nextcloud' - click_button 'Save and continue' + expect(page).to have_test_selector('label-host_name_configured-status', text: 'Completed') + expect(page).to have_test_selector('storage-description', text: "Nextcloud - My Nextcloud - https://example.com") end - expect(page).to have_test_selector('storage-name-title', text: 'My Nextcloud') - expect(page).to have_test_selector('storage-description', text: [storage.short_provider_type.capitalize, - 'My Nextcloud', - storage.host].join(' - ')) - - # Update a storage - unhappy path - find_test_selector('storage-edit-host-button').click - within_test_selector('storage-general-info-form') do - fill_in 'storages_nextcloud_storage_name', with: nil - fill_in 'storages_nextcloud_storage_host', with: nil - click_button 'Save and continue' + aggregate_failures 'OAuth application' do + within_test_selector('storage-openproject-oauth-application-form') do + warning_section = find_test_selector('storage-openproject_oauth_application_warning') + expect(warning_section).to have_text('The client secret value will not be accessible again after you close ' \ + 'this window. Please copy these values into the Nextcloud ' \ + 'OpenProject Integration settings.') + expect(warning_section).to have_link('Nextcloud OpenProject Integration settings', + href: "https://example.com/settings/admin/openproject") + + storage = Storages::NextcloudStorage.find_by(host: 'https://example.com') + expect(page).to have_css('#openproject_oauth_application_uid', + value: storage.reload.oauth_application.uid) + expect(page).to have_css('#openproject_oauth_application_secret', + value: storage.reload.oauth_application.secret) + + click_link 'Done, continue' + end + end - expect(page).to have_text("Name can't be blank.") - expect(page).to have_text("Host is not a valid URL.") + aggregate_failures 'OAuth Client' do + within_test_selector('storage-oauth-client-form') do + expect(page).to have_test_selector('storage-provider-credentials-instructions', + text: 'Copy these values from Nextcloud Administration / OpenProject.') + + # With null values, submit button should be disabled + expect(page).to have_css('#oauth_client_client_id', value: '') + expect(page).to have_css('#oauth_client_client_secret', value: '') + expect(find_test_selector('storage-oauth-client-submit-button')).to be_disabled + + # Happy path - Submit valid values + fill_in 'oauth_client_client_id', with: '1234567890' + fill_in 'oauth_client_client_secret', with: '0987654321' + expect(find_test_selector('storage-oauth-client-submit-button')).not_to be_disabled + click_button 'Save and continue' + end + + expect(page).to have_test_selector('label-storage_oauth_client_configured-status', text: 'Completed') + expect(page).to have_test_selector('storage-oauth-client-id-description', text: "OAuth Client ID: 1234567890") + end - click_link 'Cancel' + aggregate_failures 'Automatically managed project folders' do + within_test_selector('storage-automatically-managed-project-folders-form') do + automatically_managed_switch = page.find('[name="storages_nextcloud_storage[automatically_managed]"]') + application_password_input = page.find_by_id('storages_nextcloud_storage_password') + expect(automatically_managed_switch).to be_checked + expect(application_password_input.value).to be_empty + + # Clicking submit with application password empty should show an error + click_button('Done, complete setup') + expect(page).to have_text("Password can't be blank.") + + # Test the error path for an invalid storage password. + # Mock a valid response (=401) for example.com, so the password validation should fail + mock_nextcloud_application_credentials_validation('https://example.com', password: "1234567890", + response_code: 401) + automatically_managed_switch = page.find('[name="storages_nextcloud_storage[automatically_managed]"]') + expect(automatically_managed_switch).to be_checked + fill_in 'storages_nextcloud_storage_password', with: "1234567890" + # Clicking submit with application password empty should show an error + click_button('Done, complete setup') + expect(page).to have_text("Password is not valid.") + + # Test the happy path for a valid storage password. + # Mock a valid response (=200) for example.com, so the password validation should succeed + # Fill in application password and submit + mock_nextcloud_application_credentials_validation('https://example.com', password: "1234567890") + automatically_managed_switch = page.find('[name="storages_nextcloud_storage[automatically_managed]"]') + expect(automatically_managed_switch).to be_checked + fill_in 'storages_nextcloud_storage_password', with: "1234567890" + click_button('Done, complete setup') + end + + expect(page).to have_current_path(admin_settings_storages_path) + expect(page).to have_text("Storage connected successfully! Remember to activate the module and the specific " \ + "storage in the project settings of each desired project to use it.") end end + end - aggregate_failures 'OAuth application' do - expect(page).to have_test_selector('storage-openproject-oauth-label', text: 'OpenProject OAuth') - expect(page).to have_test_selector('label-openproject_oauth_application_configured-status', text: 'Connected') - expect(page).to have_test_selector('storage-openproject-oauth-application-description', - text: "OAuth Client ID: #{oauth_application.uid}") + context 'with OneDrive Storage' do + it 'renders a One Drive specific multi-step form', :webmock do + visit admin_settings_storages_path - accept_confirm do - find_test_selector('storage-replace-openproject-oauth-application-button').click - end + within('.PageHeader') { click_link("Storage") } + expect(page).to have_current_path(new_admin_settings_storage_path) - within_test_selector('storage-openproject-oauth-application-form') do - warning_section = find_test_selector('storage-openproject_oauth_application_warning') - expect(warning_section).to have_text('The client secret value will not be accessible again after you close ' \ - 'this window. Please copy these values into the Nextcloud ' \ - 'OpenProject Integration settings.') - expect(warning_section).to have_link('Nextcloud OpenProject Integration settings', - href: "#{storage.host}/settings/admin/openproject") + aggregate_failures 'Select provider view' do + # General information + expect(page).to have_select('storages_storage[provider_type]', with_options: %w[Nextcloud OneDrive/SharePoint]) + expect(find_test_selector('storage-select-provider-submit-button')).to be_disabled - expect(page).to have_css('#openproject_oauth_application_uid', - value: storage.reload.oauth_application.uid) - expect(page).to have_css('#openproject_oauth_application_secret', - value: storage.reload.oauth_application.secret) + # Select OneDrive + select('OneDrive/SharePoint', from: 'storages_storage[provider_type]') - click_link 'Done, continue' + # OAuth client + wait_for(page).to have_test_selector('storage-oauth-client-label', text: 'Azure OAuth') + expect(page).to have_test_selector('label-storage_oauth_client_configured-status', text: 'Incomplete') + expect(page).to have_test_selector('storage-oauth-client-id-description', + text: "Allow OpenProject to access Azure data using OAuth " \ + "to connect OneDrive/Sharepoint.") end - end - aggregate_failures 'Nextcloud OAuth' do - expect(page).to have_test_selector('storage-oauth-client-label', text: 'Nextcloud OAuth') - expect(page).to have_test_selector('label-storage_oauth_client_configured-status', text: 'Incomplete') - expect(page).to have_test_selector('storage-oauth-client-id-description', - text: "Allow OpenProject to access Nextcloud data using an OAuth.") + aggregate_failures 'General information' do + within_test_selector('storage-general-info-form') do + fill_in 'storages_one_drive_storage_name', with: 'My OneDrive', fill_options: { clear: :backspace } + click_button 'Save and continue' - accept_confirm do - find_test_selector('storage-edit-oauth-client-button').click - end - - within_test_selector('storage-oauth-client-form') do - expect(page).to have_css('#oauth_client_client_id', value: '') - expect(page).to have_css('#oauth_client_client_secret', value: '') + expect(page).to have_text("Drive can't be blank.") - # Unhappy path - Attempt to submit null values - click_button 'Save and continue' - # TODO: This should be "Client ID can't be blank." but primer assumes the label is "Client" - expect(page).to have_text("Client can't be blank.") - expect(page).to have_text("Client secret can't be blank.") + fill_in 'storages_one_drive_storage_drive_id', with: '1234567890' + click_button 'Save and continue' + end - # Happy path - Submit valid values - fill_in 'oauth_client_client_id', with: '1234567890' - fill_in 'oauth_client_client_secret', with: '0987654321' - click_button 'Save and continue' + wait_for(page).to have_test_selector('label-host_name_configured-storage_tenant_drive_configured-status', + text: 'Completed') + expect(page).to have_test_selector('storage-description', text: 'OneDrive/SharePoint - My OneDrive') end - expect(page).to have_test_selector('label-storage_oauth_client_configured-status', text: 'Connected') - expect(page).to have_test_selector('storage-oauth-client-id-description', - text: "OAuth Client ID: 1234567890") - end - - aggregate_failures 'Automatically managed project folders' do - expect(page).to have_test_selector('storage-managed-project-folders-label', - text: 'Automatically managed folders') - expect(page).to have_test_selector('label-managed-project-folders-status', - text: 'Incomplete') - expect(page).to have_test_selector('storage-automatically-managed-project-folders-description', - text: 'Let OpenProject create folders per project automatically.') - - find_test_selector('storage-edit-automatically-managed-project-folders-button').click - - within_test_selector('storage-automatically-managed-project-folders-form') do - automatically_managed_switch = page.find('[name="storages_nextcloud_storage[automatically_managed]"]') - application_password_input = page.find_by_id('storages_nextcloud_storage_password') - expect(automatically_managed_switch).to be_checked - expect(application_password_input.value).to be_empty - - # Clicking submit with application password empty should show an error - click_button('Done, complete setup') - expect(page).to have_text("Password can't be blank.") - - # Test the error path for an invalid storage password. - # Mock a valid response (=401) for example.com, so the password validation should fail - mock_nextcloud_application_credentials_validation(storage.host, password: "1234567890", - response_code: 401) - automatically_managed_switch = page.find('[name="storages_nextcloud_storage[automatically_managed]"]') - expect(automatically_managed_switch).to be_checked - fill_in 'storages_nextcloud_storage_password', with: "1234567890" - # Clicking submit with application password empty should show an error - click_button('Done, complete setup') - expect(page).to have_text("Password is not valid.") - - # Test the happy path for a valid storage password. - # Mock a valid response (=200) for example.com, so the password validation should succeed - # Fill in application password and submit - mock_nextcloud_application_credentials_validation(storage.host, password: "1234567890") - automatically_managed_switch = page.find('[name="storages_nextcloud_storage[automatically_managed]"]') - expect(automatically_managed_switch).to be_checked - fill_in 'storages_nextcloud_storage_password', with: "1234567890" - click_button('Done, complete setup') + aggregate_failures 'OAuth Client' do + within_test_selector('storage-oauth-client-form') do + expect(page).to have_test_selector('storage-provider-credentials-instructions', + text: 'Copy these values from the Azure application. ' \ + 'After that, copy the redirect URI back to the Azure application.') + + # With null values, submit button should be disabled + expect(page).to have_css('#oauth_client_client_id', value: '') + expect(page).to have_css('#oauth_client_client_secret', value: '') + expect(find_test_selector('storage-oauth-client-submit-button')).to be_disabled + + # Happy path - Submit valid values + fill_in 'oauth_client_client_id', with: '1234567890' + fill_in 'oauth_client_client_secret', with: '0987654321' + expect(find_test_selector('storage-oauth-client-submit-button')).not_to be_disabled + click_button 'Save and continue' + end + + expect(page).to have_current_path(admin_settings_storages_path) + wait_for(page).to have_text("Storage connected successfully! Remember to activate the module and the specific " \ + "storage in the project settings of each desired project to use it.") end - - expect(page).to have_test_selector('label-managed-project-folders-status', - text: 'Active') end end + end + describe 'File storage edit view', with_flag: { storage_primer_design: true } do it 'renders a delete button' do storage = create(:nextcloud_storage, name: "Foo Nextcloud") visit edit_admin_settings_storage_path(storage) @@ -253,6 +299,234 @@ expect(page).to have_current_path(admin_settings_storages_path) expect(page).not_to have_text("Foo Nextcloud") end + + context 'with Nextcloud Storage' do + let(:storage) { create(:nextcloud_storage, :as_automatically_managed) } + let(:oauth_application) { create(:oauth_application, integration: storage) } + let(:oauth_client) { create(:oauth_client, integration: storage) } + + before do + oauth_application + oauth_client + end + + it 'renders an edit view', :webmock do + visit edit_admin_settings_storage_path(storage) + + expect(page).to have_test_selector('storage-name-title', text: storage.name.capitalize) + + aggregate_failures 'Storage edit view' do + # General information + expect(page).to have_test_selector('storage-provider-label', text: 'Storage provider') + expect(page).to have_test_selector('label-host_name_configured-status', text: 'Completed') + expect(page).to have_test_selector('storage-description', text: "Nextcloud - #{storage.name} - #{storage.host}") + + # OAuth application + expect(page).to have_test_selector('storage-openproject-oauth-label', text: 'OpenProject OAuth') + expect(page).to have_test_selector('label-openproject_oauth_application_configured-status', text: 'Completed') + expect(page).to have_test_selector('storage-openproject-oauth-application-description', + text: "OAuth Client ID: #{oauth_application.uid}") + + # OAuth client + expect(page).to have_test_selector('storage-oauth-client-label', text: 'Nextcloud OAuth') + expect(page).to have_test_selector('label-storage_oauth_client_configured-status', text: 'Completed') + expect(page).to have_test_selector('storage-oauth-client-id-description', + text: "OAuth Client ID: #{oauth_client.client_id}") + + # Automatically managed project folders + expect(page).to have_test_selector('storage-managed-project-folders-label', + text: 'Automatically managed folders') + + expect(page).to have_test_selector('label-managed-project-folders-status', text: 'Active') + expect(page).to have_test_selector('storage-automatically-managed-project-folders-description', + text: 'Let OpenProject create folders per project automatically.') + end + + aggregate_failures 'General information' do + # Update a storage - happy path + find_test_selector('storage-edit-host-button').click + within_test_selector('storage-general-info-form') do + expect(page).to have_css('#storages_nextcloud_storage_provider_type[disabled]') + + fill_in 'storages_nextcloud_storage_name', with: 'My Nextcloud' + click_button 'Save and continue' + end + + expect(page).to have_test_selector('storage-name-title', text: 'My Nextcloud') + expect(page).to have_test_selector('storage-description', text: "Nextcloud - My Nextcloud - #{storage.host}") + + # Update a storage - unhappy path + find_test_selector('storage-edit-host-button').click + within_test_selector('storage-general-info-form') do + fill_in 'storages_nextcloud_storage_name', with: nil + fill_in 'storages_nextcloud_storage_host', with: nil + click_button 'Save and continue' + + expect(page).to have_text("Name can't be blank.") + expect(page).to have_text("Host is not a valid URL.") + + click_link 'Cancel' + end + end + + aggregate_failures 'OAuth application' do + accept_confirm do + find_test_selector('storage-replace-openproject-oauth-application-button').click + end + + within_test_selector('storage-openproject-oauth-application-form') do + warning_section = find_test_selector('storage-openproject_oauth_application_warning') + expect(warning_section).to have_text('The client secret value will not be accessible again after you close ' \ + 'this window. Please copy these values into the Nextcloud ' \ + 'OpenProject Integration settings.') + expect(warning_section).to have_link('Nextcloud OpenProject Integration settings', + href: "#{storage.host}/settings/admin/openproject") + + expect(page).to have_css('#openproject_oauth_application_uid', + value: storage.reload.oauth_application.uid) + expect(page).to have_css('#openproject_oauth_application_secret', + value: storage.reload.oauth_application.secret) + + click_link 'Done, continue' + end + end + + aggregate_failures 'OAuth Client' do + accept_confirm do + find_test_selector('storage-edit-oauth-client-button').click + end + + within_test_selector('storage-oauth-client-form') do + # With null values, submit button should be disabled + expect(page).to have_css('#oauth_client_client_id', value: '') + expect(page).to have_css('#oauth_client_client_secret', value: '') + expect(find_test_selector('storage-oauth-client-submit-button')).to be_disabled + + # Happy path - Submit valid values + fill_in 'oauth_client_client_id', with: '1234567890' + fill_in 'oauth_client_client_secret', with: '0987654321' + expect(find_test_selector('storage-oauth-client-submit-button')).not_to be_disabled + click_button 'Save and continue' + end + + expect(page).to have_test_selector('label-storage_oauth_client_configured-status', text: 'Completed') + expect(page).to have_test_selector('storage-oauth-client-id-description', text: "OAuth Client ID: 1234567890") + end + + aggregate_failures 'Automatically managed project folders' do + find_test_selector('storage-edit-automatically-managed-project-folders-button').click + + within_test_selector('storage-automatically-managed-project-folders-form') do + automatically_managed_switch = page.find('[name="storages_nextcloud_storage[automatically_managed]"]') + application_password_input = page.find_by_id('storages_nextcloud_storage_password') + expect(automatically_managed_switch).to be_checked + expect(application_password_input.value).to be_empty + + # Clicking submit with application password empty should show an error + click_button('Done, complete setup') + expect(page).to have_text("Password can't be blank.") + + # Test the error path for an invalid storage password. + # Mock a valid response (=401) for example.com, so the password validation should fail + mock_nextcloud_application_credentials_validation(storage.host, password: "1234567890", + response_code: 401) + automatically_managed_switch = page.find('[name="storages_nextcloud_storage[automatically_managed]"]') + expect(automatically_managed_switch).to be_checked + fill_in 'storages_nextcloud_storage_password', with: "1234567890" + # Clicking submit with application password empty should show an error + click_button('Done, complete setup') + expect(page).to have_text("Password is not valid.") + + # Test the happy path for a valid storage password. + # Mock a valid response (=200) for example.com, so the password validation should succeed + # Fill in application password and submit + mock_nextcloud_application_credentials_validation(storage.host, password: "1234567890") + automatically_managed_switch = page.find('[name="storages_nextcloud_storage[automatically_managed]"]') + expect(automatically_managed_switch).to be_checked + fill_in 'storages_nextcloud_storage_password', with: "1234567890" + click_button('Done, complete setup') + end + + expect(page).to have_test_selector('label-managed-project-folders-status', text: 'Active') + end + end + end + + context 'with OneDrive Storage' do + let(:storage) { create(:one_drive_storage, name: 'Test Drive') } + let(:oauth_client) { create(:oauth_client, integration: storage) } + + before { oauth_client } + + it 'renders an edit view', :webmock do + visit edit_admin_settings_storage_path(storage) + + expect(page).to have_test_selector('storage-name-title', text: 'Test Drive') + + aggregate_failures 'Storage edit view' do + # General information + expect(page).to have_test_selector('storage-provider-label', text: 'Storage provider') + expect(page).to have_test_selector('label-host_name_configured-storage_tenant_drive_configured-status', + text: 'Completed') + expect(page).to have_test_selector('storage-description', text: 'OneDrive/SharePoint - Test Drive') + + # OAuth client + expect(page).to have_test_selector('storage-oauth-client-label', text: 'Azure OAuth') + expect(page).to have_test_selector('label-storage_oauth_client_configured-status', text: 'Completed') + expect(page).to have_test_selector('storage-oauth-client-id-description', + text: "OAuth Client ID: #{oauth_client.client_id}") + end + + aggregate_failures 'General information' do + # Update a storage - happy path + find_test_selector('storage-edit-host-button').click + within_test_selector('storage-general-info-form') do + expect(page).to have_css('#storages_one_drive_storage_provider_type[disabled]') + + fill_in 'storages_one_drive_storage_name', with: 'My OneDrive' + click_button 'Save and continue' + end + + expect(page).to have_test_selector('storage-name-title', text: 'My OneDrive') + expect(page).to have_test_selector('storage-description', text: 'OneDrive/SharePoint - My OneDrive') + + # Update a storage - unhappy path + find_test_selector('storage-edit-host-button').click + within_test_selector('storage-general-info-form') do + fill_in 'storages_one_drive_storage_name', with: nil + fill_in 'storages_one_drive_storage_drive_id', with: nil + click_button 'Save and continue' + + expect(page).to have_text("Name can't be blank.") + expect(page).to have_text("Drive can't be blank.") + + click_link 'Cancel' + end + end + + aggregate_failures 'OAuth Client' do + accept_confirm do + find_test_selector('storage-edit-oauth-client-button').click + end + + within_test_selector('storage-oauth-client-form') do + # With null values, submit button should be disabled + expect(page).to have_css('#oauth_client_client_id', value: '') + expect(page).to have_css('#oauth_client_client_secret', value: '') + expect(find_test_selector('storage-oauth-client-submit-button')).to be_disabled + + # Happy path - Submit valid values + fill_in 'oauth_client_client_id', with: '1234567890' + fill_in 'oauth_client_client_secret', with: '0987654321' + expect(find_test_selector('storage-oauth-client-submit-button')).not_to be_disabled + click_button 'Save and continue' + end + + expect(page).to have_test_selector('label-storage_oauth_client_configured-status', text: 'Completed') + expect(page).to have_test_selector('storage-oauth-client-id-description', text: "OAuth Client ID: 1234567890") + end + end + end end it 'creates, edits and deletes storages', :webmock do diff --git a/modules/storages/spec/models/nextcloud_storage_spec.rb b/modules/storages/spec/models/nextcloud_storage_spec.rb index 25a03819a16f..7996b84c89fc 100644 --- a/modules/storages/spec/models/nextcloud_storage_spec.rb +++ b/modules/storages/spec/models/nextcloud_storage_spec.rb @@ -180,6 +180,31 @@ it_behaves_like 'a stored boolean attribute', :automatically_managed end + describe '#automatic_management_new_record?' do + context 'when automatic management has just been specified but not yet persisted' do + let(:storage) { build_stubbed(:nextcloud_storage, provider_fields: {}) } + + before { storage.automatically_managed = false } + + it { expect(storage).to be_provider_fields_changed } + it { expect(storage).to be_automatic_management_new_record } + end + + context 'when automatic management was already specified' do + let(:storage) { build_stubbed(:nextcloud_storage, :as_not_automatically_managed) } + + it { expect(storage).not_to be_provider_fields_changed } + it { expect(storage).not_to be_automatic_management_new_record } + end + + context 'when automatic management is unspecified' do + let(:storage) { build_stubbed(:nextcloud_storage, provider_fields: {}) } + + it { expect(storage).not_to be_provider_fields_changed } + it { expect(storage).to be_automatic_management_new_record } + end + end + describe '#automatic_management_unspecified?' do context 'when automatically_managed is nil' do let(:storage) { build(:nextcloud_storage, automatically_managed: nil) } diff --git a/modules/storages/spec/models/one_drive_storage_spec.rb b/modules/storages/spec/models/one_drive_storage_spec.rb index 47d4f45980b8..417994ff1dfe 100644 --- a/modules/storages/spec/models/one_drive_storage_spec.rb +++ b/modules/storages/spec/models/one_drive_storage_spec.rb @@ -50,7 +50,9 @@ aggregate_failures 'configuration_checks' do expect(storage.configuration_checks) - .to eq(storage_oauth_client_configured: true) + .to eq(host_name_configured: true, + storage_oauth_client_configured: true, + storage_tenant_drive_configured: true) end end end diff --git a/spec/components/op_turbo/frame_component_spec.rb b/spec/components/op_turbo/frame_component_spec.rb index 9a056f968102..6cbd030a1df6 100644 --- a/spec/components/op_turbo/frame_component_spec.rb +++ b/spec/components/op_turbo/frame_component_spec.rb @@ -47,5 +47,21 @@ expect(component.turbo_frame_id).to eq('storages_nextcloud_storage_1') end end + + context 'with `id:` option' do + it 'returns the turbo frame id' do + component = described_class.new(id: 'test_id') + + expect(component.turbo_frame_id).to eq('test_id') + end + end + + context 'with `id:` and `context:` option' do + it 'returns the turbo frame id' do + component = described_class.new(id: 'test_id', context: :general_info) + + expect(component.turbo_frame_id).to eq('general_info_test_id') + end + end end end