From 912569b48865a961c6dd95d1d6d92afa02a0a615 Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Wed, 7 Aug 2024 16:07:44 +0300 Subject: [PATCH 1/4] [#56284] Implement "Edit project folder" button in context menu https://community.openproject.org/work_packages/56284 --- ...cts_storage_form_modal_component.html.erb} | 26 +++++---- ... projects_storage_form_modal_component.rb} | 32 +++++++++-- ...projects_storage_modal_component.html.erb} | 2 +- ...rb => projects_storage_modal_component.rb} | 14 +++-- .../projects/row_component.rb | 56 ++++++++++++++----- .../storages/project_storages_controller.rb | 27 +++++++-- .../project_folder_mode_form.rb | 6 +- modules/storages/config/locales/en.yml | 2 + modules/storages/config/routes.rb | 2 +- 9 files changed, 123 insertions(+), 44 deletions(-) rename modules/storages/app/components/storages/admin/storages/{add_projects_form_modal_component.html.erb => projects_storage_form_modal_component.html.erb} (81%) rename modules/storages/app/components/storages/admin/storages/{add_projects_form_modal_component.rb => projects_storage_form_modal_component.rb} (63%) rename modules/storages/app/components/storages/admin/storages/{add_projects_modal_component.html.erb => projects_storage_modal_component.html.erb} (74%) rename modules/storages/app/components/storages/admin/storages/{add_projects_modal_component.rb => projects_storage_modal_component.rb} (80%) diff --git a/modules/storages/app/components/storages/admin/storages/add_projects_form_modal_component.html.erb b/modules/storages/app/components/storages/admin/storages/projects_storage_form_modal_component.html.erb similarity index 81% rename from modules/storages/app/components/storages/admin/storages/add_projects_form_modal_component.html.erb rename to modules/storages/app/components/storages/admin/storages/projects_storage_form_modal_component.html.erb index a44507d96818..7bc3acaccdcb 100644 --- a/modules/storages/app/components/storages/admin/storages/add_projects_form_modal_component.html.erb +++ b/modules/storages/app/components/storages/admin/storages/projects_storage_form_modal_component.html.erb @@ -32,34 +32,36 @@ See COPYRIGHT and LICENSE files for more details. primer_form_with( class: "op-new-project-storage-form", model: @project_storage, - url: admin_settings_storage_project_storages_path(project_storage.storage), + url:, data: { turbo: true }, - method: :post + method: ) do |form| concat(render(Primer::Alpha::Dialog::Body.new( id: dialog_body_id, test_selector: dialog_body_id, - aria: { label: title } + aria: { label: aria_label } )) do flex_layout do |flex| flex.with_row do angular_component_tag 'opce-custom-modal-overlay' end - flex.with_row(mb: 3) do - render(Storages::Admin::Storages::AddProjectsAutocompleterForm.new(form, project_storage:)) - end + if new_record? + flex.with_row(mb: 3) do + render(Storages::Admin::Storages::AddProjectsAutocompleterForm.new(form, project_storage:)) + end - flex.with_row(mb: 3) do - render(Primer::Alpha::ActionList::Divider.new(scheme: :subtle)) - end + flex.with_row(mb: 3) do + render(Primer::Alpha::ActionList::Divider.new(scheme: :subtle)) + end - flex.with_row(mb: 2) do - render(Primer::Beta::Heading.new(tag: :h4)) { t(:"storages.label_project_folder") } + flex.with_row(mb: 2) do + render(Primer::Beta::Heading.new(tag: :h4)) { t(:"storages.label_project_folder") } + end end flex.with_row(mb: 3) do - render(Primer::Beta::Text.new) { t(:"storages.help_texts.project_folder_bulk") } + render(Primer::Beta::Text.new) { help_text } end flex.with_row( diff --git a/modules/storages/app/components/storages/admin/storages/add_projects_form_modal_component.rb b/modules/storages/app/components/storages/admin/storages/projects_storage_form_modal_component.rb similarity index 63% rename from modules/storages/app/components/storages/admin/storages/add_projects_form_modal_component.rb rename to modules/storages/app/components/storages/admin/storages/projects_storage_form_modal_component.rb index 9093db2cd38b..dcbcd5dd7162 100644 --- a/modules/storages/app/components/storages/admin/storages/add_projects_form_modal_component.rb +++ b/modules/storages/app/components/storages/admin/storages/projects_storage_form_modal_component.rb @@ -29,7 +29,7 @@ module Storages module Admin module Storages - class AddProjectsFormModalComponent < ApplicationComponent + class ProjectsStorageFormModalComponent < ApplicationComponent include OpPrimer::ComponentHelpers include OpTurbo::Streamable include StimulusHelper @@ -45,11 +45,29 @@ def initialize(project_storage:, **) attr_reader :project_storage, :storage - def dialog_id = Storages::AddProjectsModalComponent::DIALOG_ID - def dialog_body_id = Storages::AddProjectsModalComponent::DIALOG_BODY_ID + def dialog_id = Storages::ProjectsStorageModalComponent::DIALOG_ID + def dialog_body_id = Storages::ProjectsStorageModalComponent::DIALOG_BODY_ID - def title - I18n.t(:label_add_projects) + def url + if new_record? + admin_settings_storage_project_storages_path(project_storage.storage) + else + admin_settings_storage_project_storage_path(storage_id: project_storage.storage.id, id: project_storage.id) + end + end + + def method = new_record? ? :post : :patch + + def help_text + if new_record? + I18n.t(:"storages.help_texts.project_folder_bulk") + else + I18n.t(:"storages.help_texts.project_folder") + end + end + + def aria_label + new_record? ? I18n.t(:label_add_projects) : I18n.t(:"project_storages.edit_project_folder.label") end def cancel_button_text @@ -57,8 +75,10 @@ def cancel_button_text end def submit_button_text - I18n.t("button_add") + new_record? ? I18n.t("button_add") : I18n.t("button_save") end + + delegate :persisted?, :new_record?, to: :project_storage end end end diff --git a/modules/storages/app/components/storages/admin/storages/add_projects_modal_component.html.erb b/modules/storages/app/components/storages/admin/storages/projects_storage_modal_component.html.erb similarity index 74% rename from modules/storages/app/components/storages/admin/storages/add_projects_modal_component.html.erb rename to modules/storages/app/components/storages/admin/storages/projects_storage_modal_component.html.erb index 6f27eaac490c..c9e52ec9a58b 100644 --- a/modules/storages/app/components/storages/admin/storages/add_projects_modal_component.html.erb +++ b/modules/storages/app/components/storages/admin/storages/projects_storage_modal_component.html.erb @@ -13,6 +13,6 @@ variant: :large ) - render(::Storages::Admin::Storages::AddProjectsFormModalComponent.new(project_storage:)) + render(::Storages::Admin::Storages::ProjectsStorageFormModalComponent.new(project_storage:)) end %> diff --git a/modules/storages/app/components/storages/admin/storages/add_projects_modal_component.rb b/modules/storages/app/components/storages/admin/storages/projects_storage_modal_component.rb similarity index 80% rename from modules/storages/app/components/storages/admin/storages/add_projects_modal_component.rb rename to modules/storages/app/components/storages/admin/storages/projects_storage_modal_component.rb index cecbff56777b..c5093ae5c8af 100644 --- a/modules/storages/app/components/storages/admin/storages/add_projects_modal_component.rb +++ b/modules/storages/app/components/storages/admin/storages/projects_storage_modal_component.rb @@ -29,11 +29,11 @@ module Storages module Admin module Storages - class AddProjectsModalComponent < ApplicationComponent + class ProjectsStorageModalComponent < ApplicationComponent include OpTurbo::Streamable - DIALOG_ID = "storages--add-projects-modal".freeze - DIALOG_BODY_ID = "storages--add-projects-modal-body".freeze + DIALOG_ID = "storages--projects-storage-modal".freeze + DIALOG_BODY_ID = "storages--projects-storage-modal-body".freeze def initialize(project_storage:, **) @project_storage = project_storage @@ -49,8 +49,14 @@ def dialog_body_id = DIALOG_BODY_ID attr_reader :project_storage, :storage def title - I18n.t(:label_add_projects) + if new_record? + I18n.t(:label_add_projects) + else + I18n.t(:"storages.label_project_folder") + end end + + delegate :new_record?, to: :project_storage end end end diff --git a/modules/storages/app/components/storages/project_storages/projects/row_component.rb b/modules/storages/app/components/storages/project_storages/projects/row_component.rb index e23b584c1300..05b63b0ef3c3 100644 --- a/modules/storages/app/components/storages/project_storages/projects/row_component.rb +++ b/modules/storages/app/components/storages/project_storages/projects/row_component.rb @@ -33,31 +33,57 @@ class RowComponent < Projects::RowComponent include OpTurbo::Streamable def project_folder_type - project_folder_mode = table.project_storages[project.id].project_folder_mode + project_folder_mode = project_storage.project_folder_mode I18n.t("project_storages.project_folder_mode.#{project_folder_mode}") end def more_menu_items - @more_menu_items ||= [more_menu_detach_project].compact + return [] unless can_view_more_menu_items? + + @more_menu_items ||= [more_menu_edit_project_storage, more_menu_detach_project] end private + def more_menu_edit_project_storage + { + scheme: :default, + icon: :pencil, + label: I18n.t("project_storages.edit_project_folder.label"), + href: edit_admin_settings_storage_project_storage_path( + storage_id: project_storage.storage.id, + id: project_storage.id + ), + data: { + controller: "async-dialog" + } + } + end + def more_menu_detach_project - project = model.first - if User.current.admin && project.active? - { - scheme: :danger, - icon: :trash, - label: I18n.t("project_storages.remove_project.label"), - href: destroy_confirmation_dialog_admin_settings_storage_project_storage_path( - id: table.project_storages[project.id].id - ), - data: { - controller: "async-dialog" - } + { + scheme: :danger, + icon: :trash, + label: I18n.t("project_storages.remove_project.label"), + href: destroy_confirmation_dialog_admin_settings_storage_project_storage_path( + id: project_storage.id + ), + data: { + controller: "async-dialog" } - end + } + end + + def can_view_more_menu_items? + User.current.admin && project.active? + end + + def project_storage + table.project_storages[project.id] + end + + def project + model.first end end end diff --git a/modules/storages/app/controllers/storages/admin/storages/project_storages_controller.rb b/modules/storages/app/controllers/storages/admin/storages/project_storages_controller.rb index 8b667704ab44..f6dfcdc63ff4 100644 --- a/modules/storages/app/controllers/storages/admin/storages/project_storages_controller.rb +++ b/modules/storages/app/controllers/storages/admin/storages/project_storages_controller.rb @@ -40,7 +40,7 @@ class Storages::Admin::Storages::ProjectStoragesController < ApplicationControll before_action :require_admin before_action :find_model_object - before_action :load_project_storage, only: %i(destroy destroy_confirmation_dialog) + before_action :load_project_storage, only: %i(edit update destroy destroy_confirmation_dialog) before_action :storage_projects_query, only: :index before_action :ensure_storage_configured!, only: %i(new create) @@ -52,7 +52,7 @@ class Storages::Admin::Storages::ProjectStoragesController < ApplicationControll def index; end def new - respond_with_dialog Storages::Admin::Storages::AddProjectsModalComponent.new(project_storage: @project_storage) + respond_with_dialog Storages::Admin::Storages::ProjectsStorageModalComponent.new(project_storage: @project_storage) end def create # rubocop:disable Metrics/AbcSize @@ -66,13 +66,32 @@ def create # rubocop:disable Metrics/AbcSize create_service.on_failure do project_storage = create_service.result project_storage.errors.merge!(create_service.errors) - component = Storages::Admin::Storages::AddProjectsFormModalComponent.new(project_storage:) + component = Storages::Admin::Storages::ProjectsStorageFormModalComponent.new(project_storage:) update_via_turbo_stream(component:, status: :bad_request) end respond_with_turbo_streams(status: create_service.success? ? :ok : :unprocessable_entity) end + def edit + respond_with_dialog Storages::Admin::Storages::ProjectsStorageModalComponent.new(project_storage: @project_storage) + end + + def update + update_service = ::Storages::ProjectStorages::UpdateService + .new(user: current_user, model: @project_storage) + .call(params.to_unsafe_h[:storages_project_storage].merge(storage: @storage)) + + update_service.on_success { update_project_list_via_turbo_stream(url_for_action: :index) } + + update_service.on_failure do + component = Storages::Admin::Storages::ProjectsStorageFormModalComponent.new(project_storage: @project_storage) + update_via_turbo_stream(component:, status: :bad_request) + end + + respond_with_turbo_streams(status: update_service.success? ? :ok : :unprocessable_entity) + end + def destroy_confirmation_dialog respond_with_dialog Storages::ProjectStorages::DestroyConfirmationDialogComponent.new( storage: @storage, @@ -109,7 +128,7 @@ def find_projects_to_activate_for_storage else initialize_project_storage @project_storage.errors.add(:project_ids, :blank) - component = Storages::Admin::Storages::AddProjectsFormModalComponent.new(project_storage: @project_storage) + component = Storages::Admin::Storages::ProjectsStorageFormModalComponent.new(project_storage: @project_storage) update_via_turbo_stream(component:, status: :bad_request) respond_with_turbo_streams end diff --git a/modules/storages/app/forms/storages/admin/project_storages/project_folder_mode_form.rb b/modules/storages/app/forms/storages/admin/project_storages/project_folder_mode_form.rb index 77fd0031a898..09ef2bbdb8fc 100644 --- a/modules/storages/app/forms/storages/admin/project_storages/project_folder_mode_form.rb +++ b/modules/storages/app/forms/storages/admin/project_storages/project_folder_mode_form.rb @@ -130,9 +130,13 @@ def validation_message def project_folder_selection_classes [].tap do |classes| - classes << "d-none" unless @project_storage.errors.include?(:project_folder_id) + classes << "d-none" unless show_project_folder_selection? end end + + def show_project_folder_selection? + @project_storage.project_folder_manual? || @project_storage.errors.include?(:project_folder_id) + end end end end diff --git a/modules/storages/config/locales/en.yml b/modules/storages/config/locales/en.yml index d0ca24fac6fe..a9b04c623129 100644 --- a/modules/storages/config/locales/en.yml +++ b/modules/storages/config/locales/en.yml @@ -60,6 +60,8 @@ en: permission_write_files: 'Automatically managed project folders: Write files' project_module_storages: Files project_storages: + edit_project_folder: + label: Edit project folder project_folder_mode: automatic: Automatically managed inactive: No specific folder diff --git a/modules/storages/config/routes.rb b/modules/storages/config/routes.rb index 554e3db6bfe5..b26381b72720 100644 --- a/modules/storages/config/routes.rb +++ b/modules/storages/config/routes.rb @@ -47,7 +47,7 @@ scope module: :storages do resources :project_storages, controller: "/storages/admin/storages/project_storages", - only: %i[index new create destroy] do + only: %i[index new create edit update destroy] do get :destroy_confirmation_dialog, on: :member end end From e9f87210798b31b750686fe14a18578d2287b60e Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Wed, 7 Aug 2024 19:33:46 +0300 Subject: [PATCH 2/4] feat[Op#56284]: specify last project folders in edit --- .../projects_storage_modal_component.html.erb | 2 +- .../storages/projects_storage_modal_component.rb | 6 ++++-- .../admin/storages/project_storages_controller.rb | 13 +++++++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/modules/storages/app/components/storages/admin/storages/projects_storage_modal_component.html.erb b/modules/storages/app/components/storages/admin/storages/projects_storage_modal_component.html.erb index c9e52ec9a58b..ea922f722acf 100644 --- a/modules/storages/app/components/storages/admin/storages/projects_storage_modal_component.html.erb +++ b/modules/storages/app/components/storages/admin/storages/projects_storage_modal_component.html.erb @@ -13,6 +13,6 @@ variant: :large ) - render(::Storages::Admin::Storages::ProjectsStorageFormModalComponent.new(project_storage:)) + render(::Storages::Admin::Storages::ProjectsStorageFormModalComponent.new(project_storage:, last_project_folders:)) end %> diff --git a/modules/storages/app/components/storages/admin/storages/projects_storage_modal_component.rb b/modules/storages/app/components/storages/admin/storages/projects_storage_modal_component.rb index c5093ae5c8af..a07edd7b0441 100644 --- a/modules/storages/app/components/storages/admin/storages/projects_storage_modal_component.rb +++ b/modules/storages/app/components/storages/admin/storages/projects_storage_modal_component.rb @@ -35,9 +35,11 @@ class ProjectsStorageModalComponent < ApplicationComponent DIALOG_ID = "storages--projects-storage-modal".freeze DIALOG_BODY_ID = "storages--projects-storage-modal-body".freeze - def initialize(project_storage:, **) + def initialize(project_storage:, last_project_folders:, **) @project_storage = project_storage @storage = project_storage.storage + @last_project_folders = last_project_folders + super(@project_storage, **) end @@ -46,7 +48,7 @@ def initialize(project_storage:, **) def dialog_id = DIALOG_ID def dialog_body_id = DIALOG_BODY_ID - attr_reader :project_storage, :storage + attr_reader :project_storage, :storage, :last_project_folders def title if new_record? diff --git a/modules/storages/app/controllers/storages/admin/storages/project_storages_controller.rb b/modules/storages/app/controllers/storages/admin/storages/project_storages_controller.rb index f6dfcdc63ff4..1d763048323e 100644 --- a/modules/storages/app/controllers/storages/admin/storages/project_storages_controller.rb +++ b/modules/storages/app/controllers/storages/admin/storages/project_storages_controller.rb @@ -52,7 +52,9 @@ class Storages::Admin::Storages::ProjectStoragesController < ApplicationControll def index; end def new - respond_with_dialog Storages::Admin::Storages::ProjectsStorageModalComponent.new(project_storage: @project_storage) + respond_with_dialog Storages::Admin::Storages::ProjectsStorageModalComponent.new( + project_storage: @project_storage, last_project_folders: {} + ) end def create # rubocop:disable Metrics/AbcSize @@ -74,7 +76,14 @@ def create # rubocop:disable Metrics/AbcSize end def edit - respond_with_dialog Storages::Admin::Storages::ProjectsStorageModalComponent.new(project_storage: @project_storage) + last_project_folders = Storages::LastProjectFolder + .where(project_storage: @project_storage) + .pluck(:mode, :origin_folder_id) + .to_h + + respond_with_dialog Storages::Admin::Storages::ProjectsStorageModalComponent.new( + project_storage: @project_storage, last_project_folders: + ) end def update From 4c2dfa468137040facf26ce5301069644d7f9e89 Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Thu, 8 Aug 2024 13:46:27 +0300 Subject: [PATCH 3/4] tests[Op#56284]: cover test case for edit --- .../storages/admin/project_storages_spec.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/modules/storages/spec/features/storages/admin/project_storages_spec.rb b/modules/storages/spec/features/storages/admin/project_storages_spec.rb index 2db9ff004b39..1cfdb72bbc1c 100644 --- a/modules/storages/spec/features/storages/admin/project_storages_spec.rb +++ b/modules/storages/spec/features/storages/admin/project_storages_spec.rb @@ -250,6 +250,19 @@ expect(page).to have_text(project.name) expect(page).to have_text(subproject.name) + + aggregate_failures "can edit the project folder" do + project_storages_index_page.click_menu_item_of("Edit project folder", project) + + within("dialog") do + choose "No specific folder" + click_on "Save" + end + + project_storages_index_page.within_the_table_row_containing(project.name) do + expect(page).to have_text("No specific folder") + end + end end context "when the user does not select a folder" do From eaf0fbccaa22265c103b069b2549a8466cecd121 Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Thu, 8 Aug 2024 13:57:05 +0300 Subject: [PATCH 4/4] chore[Op#55967]: ensure last project folders is passed down to stimulus controller --- .../admin/storages/projects_storage_form_modal_component.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/storages/app/components/storages/admin/storages/projects_storage_form_modal_component.rb b/modules/storages/app/components/storages/admin/storages/projects_storage_form_modal_component.rb index dcbcd5dd7162..ab6e4a89c5bc 100644 --- a/modules/storages/app/components/storages/admin/storages/projects_storage_form_modal_component.rb +++ b/modules/storages/app/components/storages/admin/storages/projects_storage_form_modal_component.rb @@ -35,9 +35,9 @@ class ProjectsStorageFormModalComponent < ApplicationComponent include StimulusHelper include AngularHelper - def initialize(project_storage:, **) + def initialize(project_storage:, last_project_folders: {}, **) @project_storage = project_storage - @last_project_folders = {} + @last_project_folders = last_project_folders super(@project_storage, **) end