diff --git a/app/contracts/project_custom_field_project_mappings/base_contract.rb b/app/contracts/project_custom_field_project_mappings/base_contract.rb index 106abf73b07f..0ed19cf6d5a1 100644 --- a/app/contracts/project_custom_field_project_mappings/base_contract.rb +++ b/app/contracts/project_custom_field_project_mappings/base_contract.rb @@ -46,7 +46,7 @@ def not_required # enabling a custom field which is required happens in an after_save hook within the custom field model itself return if model.project_custom_field.nil? || !model.project_custom_field.required? - errors.add :custom_field_id, :invalid + errors.add :custom_field_id, :cannot_delete_mapping end def visbile_to_user diff --git a/app/controllers/admin/settings/project_custom_fields_controller.rb b/app/controllers/admin/settings/project_custom_fields_controller.rb index a23139a70ddc..188395ad404a 100644 --- a/app/controllers/admin/settings/project_custom_fields_controller.rb +++ b/app/controllers/admin/settings/project_custom_fields_controller.rb @@ -30,6 +30,8 @@ module Admin::Settings class ProjectCustomFieldsController < ::Admin::SettingsController include CustomFields::SharedActions include OpTurbo::ComponentStream + include ApplicationComponentStreams + include FlashMessagesOutputSafetyHelper include Admin::Settings::ProjectCustomFields::ComponentStreams menu_item :project_custom_fields_settings @@ -41,6 +43,7 @@ class ProjectCustomFieldsController < ::Admin::SettingsController before_action :prepare_custom_option_position, only: %i(update create) before_action :find_custom_option, only: :delete_option before_action :project_custom_field_mappings_query, only: %i[project_mappings unlink] + before_action :find_unlink_project_custom_field_mapping, only: :unlink # rubocop:enable Rails/LexicallyScopedActionFilter def show_local_breadcrumb @@ -68,13 +71,18 @@ def edit; end def project_mappings; end def unlink - project = Project.find(permitted_params.project_custom_field_project_mapping[:project_id]) - project_custom_field_mapping = @custom_field.project_custom_field_project_mappings.find_by(project:) delete_service = ProjectCustomFieldProjectMappings::DeleteService - .new(user: current_user, model: project_custom_field_mapping) + .new(user: current_user, model: @project_custom_field_mapping) .call - delete_service.on_success { render_unlink_response(project:) } + delete_service.on_success { render_unlink_response(project: @project) } + + delete_service.on_failure do + update_flash_message_via_turbo_stream( + message: join_flash_messages(delete_service.errors.full_messages), + full: true, dismiss_scheme: :hide, scheme: :danger + ) + end respond_to_with_turbo_streams(status: delete_service.success? ? :ok : :unprocessable_entity) end @@ -150,6 +158,23 @@ def set_sections .all end + def find_unlink_project_custom_field_mapping + @project = Project.find(permitted_params.project_custom_field_project_mapping[:project_id]) + @project_custom_field_mapping = @custom_field.project_custom_field_project_mappings.find_by!(project: @project) + rescue ActiveRecord::RecordNotFound + update_flash_message_via_turbo_stream( + message: t(:notice_file_not_found), full: true, dismiss_scheme: :hide, scheme: :danger + ) + replace_via_turbo_stream( + component: Settings::ProjectCustomFields::ProjectCustomFieldMapping::TableComponent.new( + query: project_custom_field_mappings_query, + params: { custom_field: @custom_field } + ) + ) + + respond_with_turbo_streams + end + def find_custom_field @custom_field = ProjectCustomField.find(params[:id]) rescue ActiveRecord::RecordNotFound diff --git a/app/helpers/flash_messages_helper.rb b/app/helpers/flash_messages_helper.rb index 0fc868657ed8..36321e3ae4f7 100644 --- a/app/helpers/flash_messages_helper.rb +++ b/app/helpers/flash_messages_helper.rb @@ -31,8 +31,7 @@ module FlashMessagesHelper extend ActiveSupport::Concern included do - # For .safe_join in join_flash_messages - include ActionView::Helpers::OutputSafetyHelper + include FlashMessagesOutputSafetyHelper end def render_primer_banner_message? @@ -45,6 +44,11 @@ def render_primer_banner_message render(BannerMessageComponent.new(**flash[:primer_banner].to_hash)) end + # Primer's flash message component wrapped in a component which is empty initially but can be updated via turbo stream + def render_streameable_primer_banner_message + render(FlashMessageComponent.new) + end + # Renders flash messages def render_flash_messages return if render_primer_banner_message? @@ -63,14 +67,6 @@ def render_flash_messages safe_join messages, "\n" end - def join_flash_messages(messages) - if messages.respond_to?(:join) - safe_join(messages, "
".html_safe) - else - messages - end - end - def render_flash_message(type, message, html_options = {}) # rubocop:disable Metrics/AbcSize if type.to_s == "notice" type = "success" diff --git a/app/contracts/project_custom_field_project_mappings/delete_contract.rb b/app/helpers/flash_messages_output_safety_helper.rb similarity index 78% rename from app/contracts/project_custom_field_project_mappings/delete_contract.rb rename to app/helpers/flash_messages_output_safety_helper.rb index 7392d8d7bdf0..563079419797 100644 --- a/app/contracts/project_custom_field_project_mappings/delete_contract.rb +++ b/app/helpers/flash_messages_output_safety_helper.rb @@ -25,9 +25,21 @@ # # See COPYRIGHT and LICENSE files for more details. #++ +# + +module FlashMessagesOutputSafetyHelper + extend ActiveSupport::Concern + + included do + # For .safe_join in join_flash_messages + include ActionView::Helpers::OutputSafetyHelper + end -module ProjectCustomFieldProjectMappings - class DeleteContract < ::DeleteContract - delete_permission :select_project_custom_fields + def join_flash_messages(messages) + if messages.respond_to?(:join) + safe_join(messages, "
".html_safe) + else + messages + end end end diff --git a/app/services/project_custom_field_project_mappings/delete_service.rb b/app/services/project_custom_field_project_mappings/delete_service.rb index 3e0b4501a185..5c3a03feef8c 100644 --- a/app/services/project_custom_field_project_mappings/delete_service.rb +++ b/app/services/project_custom_field_project_mappings/delete_service.rb @@ -28,5 +28,10 @@ module ProjectCustomFieldProjectMappings class DeleteService < ::BaseServices::Delete + # Mappings have custom deletion rules that are similar to the update rules all derived from the base contract + # Reuse the update contract to ensure that the deletion rules are consistent with the update rules + def default_contract_class + ProjectCustomFieldProjectMappings::UpdateContract + end end end diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index a311e7f001d4..4f844ca25f2c 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -118,6 +118,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= render_primer_banner_message %> + <%= render_streameable_primer_banner_message %> <% if show_decoration %>