diff --git a/app/controllers/admin/custom_fields/custom_field_projects_controller.rb b/app/controllers/admin/custom_fields/custom_field_projects_controller.rb index d392aedde22d..785ba7a0147e 100644 --- a/app/controllers/admin/custom_fields/custom_field_projects_controller.rb +++ b/app/controllers/admin/custom_fields/custom_field_projects_controller.rb @@ -29,6 +29,9 @@ #++ class Admin::CustomFields::CustomFieldProjectsController < ApplicationController + include OpTurbo::ComponentStream + include OpTurbo::DialogStreamHelper + layout "admin" model_object CustomField @@ -36,16 +39,37 @@ class Admin::CustomFields::CustomFieldProjectsController < ApplicationController before_action :require_admin before_action :find_model_object + before_action :available_project_custom_fields_query, only: :index + before_action :initialize_custom_field_project, only: :new + before_action :find_projects_to_activate_for_custom_field, only: :create + menu_item :custom_fields - def index - @available_project_custom_fields_query = ProjectQuery.new( - name: "custom-fields-projects-#{@custom_field.id}" - ) do |query| - query.where(:available_project_custom_fields, "=", [@custom_field.id]) - query.select(:name) - query.order("lft" => "asc") + def index; end + + def new + respond_with_dialog Admin::CustomFields::CustomFieldProjects::NewCustomFieldProjectsModalComponent.new( + custom_field_project_mapping: @custom_field_project, + custom_field: @custom_field + ) + end + + def create + create_service = ::CustomFields::CustomFieldProjects::BulkCreateService + .new(user: current_user, projects: @projects, custom_field: @custom_field, + include_sub_projects: include_sub_projects?) + .call + + create_service.on_success { render_project_list(url_for_action: :index) } + + create_service.on_failure do + update_flash_message_via_turbo_stream( + message: join_flash_messages(create_service.errors), + full: true, dismiss_scheme: :hide, scheme: :danger + ) end + + respond_to_with_turbo_streams(status: create_service.success? ? :ok : :unprocessable_entity) end def default_breadcrumb; end @@ -56,8 +80,70 @@ def show_local_breadcrumb private + def render_project_list(url_for_action: action_name) + update_via_turbo_stream( + component: Admin::CustomFields::CustomFieldProjects::TableComponent.new( + query: available_project_custom_fields_query, + params: { custom_field: @custom_field, url_for_action: } + ) + ) + end + def find_model_object(object_id = :custom_field_id) super @custom_field = @object end + + def find_projects_to_activate_for_custom_field + if (project_ids = params.to_unsafe_h[:custom_fields_project][:project_ids]).present? + @projects = Project.find(project_ids) + else + initialize_custom_field_project + @custom_field_project.errors.add(:project_ids, :blank) + update_via_turbo_stream( + component: Admin::CustomFields::CustomFieldProjects::NewCustomFieldProjectsFormModalComponent.new( + custom_field_project_mapping: @custom_field_project, + custom_field: @custom_field + ), + status: :bad_request + ) + respond_with_turbo_streams + end + rescue ActiveRecord::RecordNotFound + update_flash_message_via_turbo_stream message: t(:notice_project_not_found), full: true, dismiss_scheme: :hide, + scheme: :danger + update_project_list_via_turbo_stream + + respond_with_turbo_streams + end + + def update_project_list_via_turbo_stream(url_for_action: action_name) + update_via_turbo_stream( + component: Admin::CustomFields::CustomFieldProjects::TableComponent.new( + query: available_project_custom_fields_query, + params: { custom_field: @custom_field, url_for_action: } + ) + ) + end + + def available_project_custom_fields_query + @available_project_custom_fields_query = ProjectQuery.new( + name: "custom-fields-projects-#{@custom_field.id}" + ) do |query| + query.where(:available_project_custom_fields, "=", [@custom_field.id]) + query.select(:name) + query.order("lft" => "asc") + end + end + + def initialize_custom_field_project + @custom_field_project = ::CustomFields::CustomFieldProjects::SetAttributesService + .new(user: current_user, model: CustomFieldsProject.new, contract_class: EmptyContract) + .call(custom_field: @custom_field) + .result + end + + def include_sub_projects? + ActiveRecord::Type::Boolean.new.cast(params.to_unsafe_h[:custom_fields_project][:include_sub_projects]) + end end diff --git a/app/forms/projects/custom_fields/custom_field_mapping_form.rb b/app/forms/projects/custom_fields/custom_field_mapping_form.rb index 1f598a730673..f637ea059934 100644 --- a/app/forms/projects/custom_fields/custom_field_mapping_form.rb +++ b/app/forms/projects/custom_fields/custom_field_mapping_form.rb @@ -44,7 +44,7 @@ class CustomFieldMappingForm < ApplicationForm multiple: true, dropdownPosition: "bottom", disabledProjects: projects_with_custom_field_mapping, - inputName: "project_custom_field_project_mapping[project_ids]" + inputName: "#{input_name}[project_ids]" } ) @@ -82,5 +82,9 @@ def projects_with_custom_field_mapping def join_table @project_mapping.class end + + def input_name + join_table.model_name.singular + end end end diff --git a/app/views/admin/custom_fields/custom_field_projects/index.html.erb b/app/views/admin/custom_fields/custom_field_projects/index.html.erb index 7bf383a31848..b665e8902251 100644 --- a/app/views/admin/custom_fields/custom_field_projects/index.html.erb +++ b/app/views/admin/custom_fields/custom_field_projects/index.html.erb @@ -34,6 +34,22 @@ See COPYRIGHT and LICENSE files for more details. selected: :custom_field_projects)) %> +<%= + render(Primer::OpenProject::SubHeader.new) do |component| + component.with_action_component do + render(Primer::Beta::Button.new( + scheme: :primary, + tag: :a, + href: new_custom_field_project_path(@custom_field), + data: { controller: "async-dialog" } + )) do |button| + button.with_leading_visual_icon(icon: 'op-include-projects') + I18n.t(:label_add_projects) + end + end + end unless @custom_field.required? +%> + <%= if @custom_field.is_for_all? render Primer::Beta::Blankslate.new(border: true) do |component| diff --git a/config/locales/en.yml b/config/locales/en.yml index b69c3b9919b4..3913755c241c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -957,6 +957,10 @@ en: minimum: "need to include at least one filter for principal, context or id with the '=' operator." custom_field: at_least_one_custom_option: "At least one option needs to be available." + custom_fields_project: + attributes: + project_ids: + blank: "Please select a project." custom_actions: only_one_allowed: "(%{name}) only one value is allowed." empty: "(%{name}) value can't be empty." diff --git a/config/routes.rb b/config/routes.rb index 599deee6f166..223d674cfbec 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -174,7 +174,7 @@ scope module: :custom_fields do resources :projects, controller: "/admin/custom_fields/custom_field_projects", - only: %i[index] + only: %i[index new create] end end end diff --git a/spec/features/admin/custom_fields/custom_fields_project_spec.rb b/spec/features/admin/custom_fields/custom_fields_project_spec.rb index fc847d44ca9a..0bd024cf0322 100644 --- a/spec/features/admin/custom_fields/custom_fields_project_spec.rb +++ b/spec/features/admin/custom_fields/custom_fields_project_spec.rb @@ -80,6 +80,51 @@ end end + it "shows an error in the dialog when no project is selected before adding" do + create(:project) + expect(page).to have_no_css("dialog") + click_on "Add projects" + + page.within("dialog") do + click_on "Add" + + expect(page).to have_text("Please select a project.") + end + end + + it "allows linking a project to a custom field" do + project = create(:project) + subproject = create(:project, parent: project) + click_on "Add projects" + + within_test_selector("new-custom-field-projects-modal") do + autocompleter = page.find(".op-project-autocompleter") + autocompleter.fill_in with: project.name + + expect(page).to have_no_text(archived_project.name) + + find(".ng-option-label", text: project.name).click + check "Include sub-projects" + + click_on "Add" + end + + expect(page).to have_text(project.name) + expect(page).to have_text(subproject.name) + + aggregate_failures "pagination links maintain the correct url" do + within ".op-pagination" do + pagination_links = page.all(".op-pagination--item-link") + expect(pagination_links.size).to be_positive + + pagination_links.each do |pagination_link| + uri = URI.parse(pagination_link["href"]) + expect(uri.path).to eq(custom_field_projects_path(custom_field)) + end + end + end + end + context "and the project custom field is for all projects" do shared_let(:custom_field) { create(:user_custom_field, is_for_all: true) }