From c9c50ff86b4a5b49ce9c155f03bf76b582c51884 Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Fri, 30 Aug 2024 11:23:28 +0300 Subject: [PATCH 1/8] [Op#5715]: Include custom fields and project attributes in available custom fields scope --- .../scopes/available_custom_fields.rb | 22 +++++++++++++----- .../scopes/available_custom_fields_spec.rb | 23 +++++++++++++++++-- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/app/models/projects/scopes/available_custom_fields.rb b/app/models/projects/scopes/available_custom_fields.rb index 8d5860250afb..e8a18cbd97be 100644 --- a/app/models/projects/scopes/available_custom_fields.rb +++ b/app/models/projects/scopes/available_custom_fields.rb @@ -32,20 +32,30 @@ module AvailableCustomFields class_methods do def with_available_custom_fields(custom_field_ids) - subquery = project_custom_fields_project_mapping_subquery(custom_field_ids:) - where(id: subquery) + where(id: project_custom_fields_join_table_subquery(custom_field_ids:)) + end + + def with_available_project_custom_fields(custom_field_ids) + where(id: project_custom_fields_project_mapping_subquery(custom_field_ids:)) end def without_available_custom_fields(custom_field_ids) - subquery = project_custom_fields_project_mapping_subquery(custom_field_ids:) - where.not(id: subquery) + where.not(id: project_custom_fields_join_table_subquery(custom_field_ids:)) + end + + def without_available_project_custom_fields(custom_field_ids) + where.not(id: project_custom_fields_project_mapping_subquery(custom_field_ids:)) end private def project_custom_fields_project_mapping_subquery(custom_field_ids:) - ProjectCustomFieldProjectMapping.select(:project_id) - .where(custom_field_id: custom_field_ids) + project_custom_fields_join_table_subquery(custom_field_ids:, join_table: ProjectCustomFieldProjectMapping) + end + + def project_custom_fields_join_table_subquery(custom_field_ids:, join_table: CustomFieldsProject) + join_table.select(:project_id) + .where(custom_field_id: custom_field_ids) end end end diff --git a/spec/models/projects/scopes/available_custom_fields_spec.rb b/spec/models/projects/scopes/available_custom_fields_spec.rb index 09edf5cd4b2d..5891726e0cbb 100644 --- a/spec/models/projects/scopes/available_custom_fields_spec.rb +++ b/spec/models/projects/scopes/available_custom_fields_spec.rb @@ -30,21 +30,40 @@ RSpec.describe Projects::Scopes::AvailableCustomFields do shared_let(:project) { create(:project) } + + shared_let(:custom_field) { create(:custom_field) } shared_let(:project_custom_field) { create(:project_custom_field) } shared_let(:project_custom_field_mapping) do create(:project_custom_field_project_mapping, project:, project_custom_field:) end + shared_let(:custom_field_mapping) do + create(:custom_fields_project, project:, custom_field:) + end + describe ".with_available_custom_fields" do it "returns projects with the given custom fields" do - expect(Project.with_available_custom_fields([project_custom_field.id])).to contain_exactly(project) + expect(Project.with_available_custom_fields([custom_field.id])).to contain_exactly(project) + end + end + + describe ".with_available_project_custom_fields" do + it "returns projects with the given project custom fields" do + expect(Project.with_available_project_custom_fields([project_custom_field.id])) + .to contain_exactly(project) end end describe ".without_available_custom_fields" do it "returns projects without the given custom fields" do - expect(Project.without_available_custom_fields([project_custom_field.id])).to be_empty + expect(Project.without_available_custom_fields([custom_field.id])).to be_empty + end + end + + describe ".without_available_project_custom_fields" do + it "returns projects without the given project custom fields" do + expect(Project.without_available_project_custom_fields([project_custom_field.id])).to be_empty end end end From b608b108d841f80b94ffc4d2a36faaea9a1eba12 Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Fri, 30 Aug 2024 11:24:07 +0300 Subject: [PATCH 2/8] [Op#5715]: Define `AvailableCustomFieldsProjectFilter` --- app/models/queries/projects.rb | 1 + .../available_project_attributes_filter.rb | 4 +- .../available_project_custom_fields_filter.rb | 70 ++++++++++++ config/locales/en.yml | 1 + .../custom_fields_project_factory.rb | 34 ++++++ ...lable_project_custom_fields_filter_spec.rb | 107 ++++++++++++++++++ 6 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 app/models/queries/projects/filters/available_project_custom_fields_filter.rb create mode 100644 spec/factories/custom_fields_project_factory.rb create mode 100644 spec/models/queries/projects/filters/available_project_custom_fields_filter_spec.rb diff --git a/app/models/queries/projects.rb b/app/models/queries/projects.rb index f1dc49b0b1ca..4cf3c4f501c7 100644 --- a/app/models/queries/projects.rb +++ b/app/models/queries/projects.rb @@ -30,6 +30,7 @@ module Queries::Projects ::Queries::Register.register(ProjectQuery) do filter Filters::AncestorFilter filter Filters::AvailableProjectAttributesFilter + filter Filters::AvailableProjectCustomFieldsFilter filter Filters::TypeFilter filter Filters::ActiveFilter filter Filters::TemplatedFilter diff --git a/app/models/queries/projects/filters/available_project_attributes_filter.rb b/app/models/queries/projects/filters/available_project_attributes_filter.rb index 0fe1ca825261..56bdf29449dd 100644 --- a/app/models/queries/projects/filters/available_project_attributes_filter.rb +++ b/app/models/queries/projects/filters/available_project_attributes_filter.rb @@ -52,9 +52,9 @@ def available? def apply_to(_query_scope) case operator when "=" - super.with_available_custom_fields(values) + super.with_available_project_custom_fields(values) when "!" - super.without_available_custom_fields(values) + super.without_available_project_custom_fields(values) else raise "unsupported operator" end diff --git a/app/models/queries/projects/filters/available_project_custom_fields_filter.rb b/app/models/queries/projects/filters/available_project_custom_fields_filter.rb new file mode 100644 index 000000000000..459e52686571 --- /dev/null +++ b/app/models/queries/projects/filters/available_project_custom_fields_filter.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ +# + +class Queries::Projects::Filters::AvailableProjectCustomFieldsFilter < Queries::Projects::Filters::Base + def self.key + :available_project_custom_fields + end + + def type + :list + end + + def allowed_values + @allowed_values ||= CustomFieldsProject + .includes(:custom_field) + .distinct + .pluck(:name, :custom_field_id) + end + + def available? + User.current.admin? + end + + def apply_to(_query_scope) + case operator + when "=" + super.with_available_custom_fields(values) + when "!" + super.without_available_custom_fields(values) + else + raise "unsupported operator" + end + end + + def where + nil + end + + def human_name + I18n.t(:label_available_project_custom_fields) + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index d647c65ab1db..0f1dbeced650 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2066,6 +2066,7 @@ en: label_authentication_settings: "Authentication settings" label_available_global_roles: "Available global roles" label_available_project_attributes: "Available project attributes" + label_available_project_custom_fields: "Available project custom fields" label_available_project_forums: "Available forums" label_available_project_repositories: "Available repositories" label_available_project_versions: "Available versions" diff --git a/spec/factories/custom_fields_project_factory.rb b/spec/factories/custom_fields_project_factory.rb new file mode 100644 index 000000000000..3f8f56fa79e7 --- /dev/null +++ b/spec/factories/custom_fields_project_factory.rb @@ -0,0 +1,34 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +FactoryBot.define do + factory :custom_fields_project do + custom_field + project + end +end diff --git a/spec/models/queries/projects/filters/available_project_custom_fields_filter_spec.rb b/spec/models/queries/projects/filters/available_project_custom_fields_filter_spec.rb new file mode 100644 index 000000000000..ec484664b73e --- /dev/null +++ b/spec/models/queries/projects/filters/available_project_custom_fields_filter_spec.rb @@ -0,0 +1,107 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" + +RSpec.describe Queries::Projects::Filters::AvailableProjectCustomFieldsFilter do + it_behaves_like "basic query filter" do + let(:class_key) { :available_project_custom_fields } + let(:type) { :list } + let(:human_name) { "Available project custom fields" } + end + + it_behaves_like "list query filter", scope: false do + shared_let(:project) { create(:project) } + + let(:custom_field_project1) { create(:custom_fields_project, project:) } + let(:custom_field_project2) { create(:custom_fields_project, project:) } + + let(:valid_values) do + [custom_field_project1.custom_field_id.to_s, custom_field_project2.custom_field_id.to_s] + end + let(:name) { "Available project attributes" } + + describe "#apply_to" do + let(:values) { valid_values } + + let(:custom_field_project_handwritten_sql_subquery) do + <<-SQL.squish + SELECT "custom_fields_projects"."project_id" + FROM "custom_fields_projects" + WHERE "custom_fields_projects"."custom_field_id" + IN (#{values.join(', ')}) + SQL + end + + context 'for "="' do + let(:operator) { "=" } + + it "is the same as handwriting the query" do + handwritten_scope_sql = <<-SQL.squish + SELECT "projects".* FROM "projects" + WHERE "projects"."id" IN (#{custom_field_project_handwritten_sql_subquery}) + SQL + + expect(instance.apply_to(Project).to_sql).to eql handwritten_scope_sql + end + end + + context 'for "!"' do + let(:operator) { "!" } + + it "is the same as handwriting the query" do + handwritten_scope_sql = <<-SQL.squish + SELECT "projects".* FROM "projects" + WHERE "projects"."id" NOT IN (#{custom_field_project_handwritten_sql_subquery}) + SQL + + expect(instance.apply_to(Project).to_sql).to eql handwritten_scope_sql + end + end + + context "for an unsupported operator" do + let(:operator) { "!=" } + + it "raises an error" do + expect { instance.apply_to(Project) }.to raise_error("unsupported operator") + end + end + end + + describe "#allowed_values" do + it "is a list of the possible values" do + expected = [ + [custom_field_project1.custom_field.name, custom_field_project1.custom_field_id], + [custom_field_project2.custom_field.name, custom_field_project2.custom_field_id] + ] + + expect(instance.allowed_values).to match_array(expected) + end + end + end +end From 8118ecdb8d83e6f539dc70c592e3da28bd6f3f5d Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Fri, 30 Aug 2024 12:06:19 +0300 Subject: [PATCH 3/8] [Op#57515]: Reuse custom fields view components --- .../row_component.html.erb | 0 .../custom_field_projects/row_component.rb | 45 ++++++++++++++ .../table_component.html.erb | 32 ++++++++++ .../custom_field_projects/table_component.rb | 62 +++++++++++++++++++ .../row_component.rb | 8 +-- .../table_component.rb | 28 +-------- 6 files changed, 141 insertions(+), 34 deletions(-) rename app/components/{settings/project_custom_fields/project_custom_field_mapping => admin/custom_fields/custom_field_projects}/row_component.html.erb (100%) create mode 100644 app/components/admin/custom_fields/custom_field_projects/row_component.rb create mode 100644 app/components/admin/custom_fields/custom_field_projects/table_component.html.erb create mode 100644 app/components/admin/custom_fields/custom_field_projects/table_component.rb diff --git a/app/components/settings/project_custom_fields/project_custom_field_mapping/row_component.html.erb b/app/components/admin/custom_fields/custom_field_projects/row_component.html.erb similarity index 100% rename from app/components/settings/project_custom_fields/project_custom_field_mapping/row_component.html.erb rename to app/components/admin/custom_fields/custom_field_projects/row_component.html.erb diff --git a/app/components/admin/custom_fields/custom_field_projects/row_component.rb b/app/components/admin/custom_fields/custom_field_projects/row_component.rb new file mode 100644 index 000000000000..fe94c8e9f656 --- /dev/null +++ b/app/components/admin/custom_fields/custom_field_projects/row_component.rb @@ -0,0 +1,45 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Admin + module CustomFields + module CustomFieldProjects + class RowComponent < Projects::RowComponent + include OpTurbo::Streamable + + def wrapper_uniq_by + "project-#{project.id}" + end + + def more_menu_items + [] + end + end + end + end +end diff --git a/app/components/admin/custom_fields/custom_field_projects/table_component.html.erb b/app/components/admin/custom_fields/custom_field_projects/table_component.html.erb new file mode 100644 index 000000000000..3488dfd23538 --- /dev/null +++ b/app/components/admin/custom_fields/custom_field_projects/table_component.html.erb @@ -0,0 +1,32 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + +<%= component_wrapper(tag: "div", data: { turbo: true }) do %> + <%= render_parent %> +<% end %> diff --git a/app/components/admin/custom_fields/custom_field_projects/table_component.rb b/app/components/admin/custom_fields/custom_field_projects/table_component.rb new file mode 100644 index 000000000000..9ef7650dab1a --- /dev/null +++ b/app/components/admin/custom_fields/custom_field_projects/table_component.rb @@ -0,0 +1,62 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Admin + module CustomFields + module CustomFieldProjects + class TableComponent < Projects::TableComponent + include OpTurbo::Streamable + + def columns + @columns ||= query.selects.reject { |select| select.is_a?(Queries::Selects::NotExistingSelect) } + end + + def sortable? + false + end + + # @override optional_pagination_options are passed to the pagination_options + # which are passed to #pagination_links_full in pagination_helper.rb + # + # In Turbo streamable components, we need to be able to specify the url_for(action:) so that links are + # generated in the context of the component index action, instead of any turbo stream actions performing + # partial updates on the page. + # + # params[:url_for_action] is passed to the pagination_options making it's way down to any pagination links + # that are generated via link_to which calls url_for which uses the params[:url_for_action] to specify + # the controller action that link_to should use. + # + def optional_pagination_options + return super unless params[:url_for_action] + + super.merge(params: { action: params[:url_for_action] }) + end + end + end + end +end diff --git a/app/components/settings/project_custom_fields/project_custom_field_mapping/row_component.rb b/app/components/settings/project_custom_fields/project_custom_field_mapping/row_component.rb index 0a4a86a2d3a4..f785bfd2d764 100644 --- a/app/components/settings/project_custom_fields/project_custom_field_mapping/row_component.rb +++ b/app/components/settings/project_custom_fields/project_custom_field_mapping/row_component.rb @@ -29,13 +29,7 @@ module Settings module ProjectCustomFields module ProjectCustomFieldMapping - class RowComponent < Projects::RowComponent - include OpTurbo::Streamable - - def wrapper_uniq_by - "project-#{project.id}" - end - + class RowComponent < Admin::CustomFields::CustomFieldProjects::RowComponent def more_menu_items @more_menu_items ||= [more_menu_detach_project].compact end diff --git a/app/components/settings/project_custom_fields/project_custom_field_mapping/table_component.rb b/app/components/settings/project_custom_fields/project_custom_field_mapping/table_component.rb index 0d62babb1d3b..b63ca837dbcf 100644 --- a/app/components/settings/project_custom_fields/project_custom_field_mapping/table_component.rb +++ b/app/components/settings/project_custom_fields/project_custom_field_mapping/table_component.rb @@ -29,33 +29,7 @@ module Settings module ProjectCustomFields module ProjectCustomFieldMapping - class TableComponent < Projects::TableComponent # rubocop:disable OpenProject/AddPreviewForViewComponent - include OpTurbo::Streamable - - def columns - @columns ||= query.selects.reject { |select| select.is_a?(Queries::Selects::NotExistingSelect) } - end - - def sortable? - false - end - - # @override optional_pagination_options are passed to the pagination_options - # which are passed to #pagination_links_full in pagination_helper.rb - # - # In Turbo streamable components, we need to be able to specify the url_for(action:) so that links are - # generated in the context of the component index action, instead of any turbo stream actions performing - # partial updates on the page. - # - # params[:url_for_action] is passed to the pagination_options making it's way down to any pagination links - # that are generated via link_to which calls url_for which uses the params[:url_for_action] to specify - # the controller action that link_to should use. - # - def optional_pagination_options - return super unless params[:url_for_action] - - super.merge(params: { action: params[:url_for_action] }) - end + class TableComponent < Admin::CustomFields::CustomFieldProjects::TableComponent end end end From 96bf91acf6da7f54be3b96c4ae37eee23968df49 Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Fri, 30 Aug 2024 12:06:49 +0300 Subject: [PATCH 4/8] [Op#57515]: Set up first base view for custom field projects --- .../custom_field_projects_controller.rb | 10 +++++++++- .../custom_field_projects/index.html.erb | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) 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 1e0678764920..99b51b37db2f 100644 --- a/app/controllers/admin/custom_fields/custom_field_projects_controller.rb +++ b/app/controllers/admin/custom_fields/custom_field_projects_controller.rb @@ -38,7 +38,15 @@ class Admin::CustomFields::CustomFieldProjectsController < ApplicationController menu_item :custom_fields - def index; end + 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") + end + end private 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 d5f9e0d0ff7b..2c081b199f4a 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 @@ -33,3 +33,18 @@ See COPYRIGHT and LICENSE files for more details. render(Admin::CustomFields::EditFormHeaderComponent.new(custom_field: @custom_field, selected: :custom_field_projects)) %> + +<%= + if @custom_field.is_for_all? + render Primer::Beta::Blankslate.new(border: true) do |component| + component.with_visual_icon(icon: :checklist) + component.with_heading(tag: :h2).with_content(I18n.t("projects.settings.project_custom_fields.is_required_blank_slate.heading")) + component.with_description { I18n.t("projects.settings.project_custom_fields.is_required_blank_slate.description") } + end + else + render(Admin::CustomFields::CustomFieldProjects::TableComponent.new( + query: @available_project_custom_fields_query, + params: params.merge({ custom_field: @custom_field })) + ) + end +%> From d1c45cd27521293b18999151263a602229b145dd Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Fri, 30 Aug 2024 16:26:25 +0300 Subject: [PATCH 5/8] [Op#57515]: avail custom field projects list --- .../custom_field_projects_controller.rb | 6 ++ .../custom_field_projects/index.html.erb | 4 +- config/locales/en.yml | 6 ++ .../custom_fields_project_spec.rb | 91 +++++++++++++++++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 spec/features/admin/custom_fields/custom_fields_project_spec.rb 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 99b51b37db2f..d392aedde22d 100644 --- a/app/controllers/admin/custom_fields/custom_field_projects_controller.rb +++ b/app/controllers/admin/custom_fields/custom_field_projects_controller.rb @@ -48,6 +48,12 @@ def index end end + def default_breadcrumb; end + + def show_local_breadcrumb + false + end + private def find_model_object(object_id = :custom_field_id) 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 2c081b199f4a..7bf383a31848 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 @@ -38,8 +38,8 @@ See COPYRIGHT and LICENSE files for more details. if @custom_field.is_for_all? render Primer::Beta::Blankslate.new(border: true) do |component| component.with_visual_icon(icon: :checklist) - component.with_heading(tag: :h2).with_content(I18n.t("projects.settings.project_custom_fields.is_required_blank_slate.heading")) - component.with_description { I18n.t("projects.settings.project_custom_fields.is_required_blank_slate.description") } + component.with_heading(tag: :h2).with_content(I18n.t("custom_fields.admin.custom_field_projects.is_for_all_blank_slate.heading")) + component.with_description { I18n.t("custom_fields.admin.custom_field_projects.is_for_all_blank_slate.description") } end else render(Admin::CustomFields::CustomFieldProjects::TableComponent.new( diff --git a/config/locales/en.yml b/config/locales/en.yml index 0f1dbeced650..b69c3b9919b4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -225,6 +225,12 @@ en: description: "Custom actions are one-click shortcuts to a set of pre-defined actions that you can make available on certain work packages based on status, role, type or project." custom_fields: + admin: + custom_field_projects: + is_for_all_blank_slate: + heading: For all projects + description: This custom field is enabled in all projects since the "For all projects" option is checked. It cannot be deactivated for individual projects. + text_add_new_custom_field: > To add new custom fields to a project you first need to create them before you can add them to this project. diff --git a/spec/features/admin/custom_fields/custom_fields_project_spec.rb b/spec/features/admin/custom_fields/custom_fields_project_spec.rb new file mode 100644 index 000000000000..0900a4dcf7b3 --- /dev/null +++ b/spec/features/admin/custom_fields/custom_fields_project_spec.rb @@ -0,0 +1,91 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" + +RSpec.describe "Custom Fields Multi-Project Activation", :js do + shared_let(:admin) { create(:admin) } + shared_let(:non_admin) { create(:user) } + shared_let(:project) { create(:project) } + shared_let(:archived_project) { create(:project, active: false) } + shared_let(:custom_field) { create(:wp_custom_field) } + shared_let(:custom_field_project) { create(:custom_fields_project, custom_field:, project:) } + + shared_let(:archived_project_custom_field) do + create(:custom_fields_project, custom_field:, project: archived_project) + end + + context "with insufficient permissions" do + it "is not accessible" do + login_as(non_admin) + visit custom_field_projects_path(custom_field) + + expect(page).to have_text("You are not authorized to access this page.") + end + end + + context "with sufficient permissions" do + before do + login_as(admin) + visit custom_field_projects_path(custom_field) + end + + it "renders a list of projects linked to the custom field" do + aggregate_failures "shows a correct breadcrumb menu" do + within ".PageHeader-breadcrumbs" do + expect(page).to have_link("Administration") + expect(page).to have_link("Custom fields") + expect(page).to have_link("Work packages") + expect(page).to have_text(custom_field.name) + end + end + + aggregate_failures "shows tab navigation" do + within_test_selector("custom_field_detail_header") do + expect(page).to have_link("Details") + expect(page).to have_link("Projects") + end + end + + aggregate_failures "shows the correct project mappings" do + within "#project-table" do + expect(page).to have_text(project.name) + expect(page).to have_text(archived_project.name) + end + end + end + + context "and the project custom field is required" do + shared_let(:custom_field) { create(:user_custom_field, is_for_all: true) } + + it "renders a blank slate" do + expect(page).to have_text("For all projects") + end + end + end +end From 3b7840f076753663eebb98d26e0036b91ac555a0 Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Wed, 4 Sep 2024 11:42:55 +0300 Subject: [PATCH 6/8] tests[Op#5715]: update test description --- spec/features/admin/custom_fields/custom_fields_project_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0900a4dcf7b3..fc847d44ca9a 100644 --- a/spec/features/admin/custom_fields/custom_fields_project_spec.rb +++ b/spec/features/admin/custom_fields/custom_fields_project_spec.rb @@ -80,7 +80,7 @@ end end - context "and the project custom field is required" do + context "and the project custom field is for all projects" do shared_let(:custom_field) { create(:user_custom_field, is_for_all: true) } it "renders a blank slate" do From d0b68e13c438e3d55522d27ae247391dd85b8fa0 Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Wed, 4 Sep 2024 12:04:57 +0300 Subject: [PATCH 7/8] chore[Op#57515]: Remove unneeded template override --- .../table_component.html.erb | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 app/components/admin/custom_fields/custom_field_projects/table_component.html.erb diff --git a/app/components/admin/custom_fields/custom_field_projects/table_component.html.erb b/app/components/admin/custom_fields/custom_field_projects/table_component.html.erb deleted file mode 100644 index 3488dfd23538..000000000000 --- a/app/components/admin/custom_fields/custom_field_projects/table_component.html.erb +++ /dev/null @@ -1,32 +0,0 @@ -<%#-- copyright -OpenProject is an open source project management software. -Copyright (C) the OpenProject GmbH - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License version 3. - -OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -Copyright (C) 2006-2013 Jean-Philippe Lang -Copyright (C) 2010-2013 the ChiliProject Team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -See COPYRIGHT and LICENSE files for more details. - -++#%> - -<%= component_wrapper(tag: "div", data: { turbo: true }) do %> - <%= render_parent %> -<% end %> From f240ef707725f375f6cd2678e54c17e0b0d8d861 Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Wed, 4 Sep 2024 18:16:55 +0300 Subject: [PATCH 8/8] chore[Op#57515]: Redefine filter as `Filters::AvailableCustomFieldsProjectsFilter` clarify that we're querying against `CustomFieldsProject` which is the join table for all custom field types, except "ProjectCustomField" aka "Project Attributes" which uses a special join table `ProjectCustomFieldsProjectMapping` --- .../custom_fields/custom_field_projects_controller.rb | 4 ++-- app/models/queries/projects.rb | 2 +- ...lter.rb => available_custom_fields_projects_filter.rb} | 6 +++--- .../custom_fields/custom_field_projects/index.html.erb | 2 +- config/locales/en.yml | 2 +- ...rb => available_custom_fields_projects_filter_spec.rb} | 8 ++++---- 6 files changed, 12 insertions(+), 12 deletions(-) rename app/models/queries/projects/filters/{available_project_custom_fields_filter.rb => available_custom_fields_projects_filter.rb} (90%) rename spec/models/queries/projects/filters/{available_project_custom_fields_filter_spec.rb => available_custom_fields_projects_filter_spec.rb} (93%) 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..6f40ea7e6a7f 100644 --- a/app/controllers/admin/custom_fields/custom_field_projects_controller.rb +++ b/app/controllers/admin/custom_fields/custom_field_projects_controller.rb @@ -39,10 +39,10 @@ class Admin::CustomFields::CustomFieldProjectsController < ApplicationController menu_item :custom_fields def index - @available_project_custom_fields_query = ProjectQuery.new( + @available_custom_fields_projects_query = ProjectQuery.new( name: "custom-fields-projects-#{@custom_field.id}" ) do |query| - query.where(:available_project_custom_fields, "=", [@custom_field.id]) + query.where(:available_custom_fields_projects, "=", [@custom_field.id]) query.select(:name) query.order("lft" => "asc") end diff --git a/app/models/queries/projects.rb b/app/models/queries/projects.rb index 4cf3c4f501c7..bb25098c5322 100644 --- a/app/models/queries/projects.rb +++ b/app/models/queries/projects.rb @@ -30,7 +30,7 @@ module Queries::Projects ::Queries::Register.register(ProjectQuery) do filter Filters::AncestorFilter filter Filters::AvailableProjectAttributesFilter - filter Filters::AvailableProjectCustomFieldsFilter + filter Filters::AvailableCustomFieldsProjectsFilter filter Filters::TypeFilter filter Filters::ActiveFilter filter Filters::TemplatedFilter diff --git a/app/models/queries/projects/filters/available_project_custom_fields_filter.rb b/app/models/queries/projects/filters/available_custom_fields_projects_filter.rb similarity index 90% rename from app/models/queries/projects/filters/available_project_custom_fields_filter.rb rename to app/models/queries/projects/filters/available_custom_fields_projects_filter.rb index 459e52686571..0aa5a0d9531d 100644 --- a/app/models/queries/projects/filters/available_project_custom_fields_filter.rb +++ b/app/models/queries/projects/filters/available_custom_fields_projects_filter.rb @@ -29,9 +29,9 @@ # ++ # -class Queries::Projects::Filters::AvailableProjectCustomFieldsFilter < Queries::Projects::Filters::Base +class Queries::Projects::Filters::AvailableCustomFieldsProjectsFilter < Queries::Projects::Filters::Base def self.key - :available_project_custom_fields + :available_custom_fields_projects end def type @@ -65,6 +65,6 @@ def where end def human_name - I18n.t(:label_available_project_custom_fields) + I18n.t(:label_available_custom_fields_projects) 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..4aeae6bedc0a 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 @@ -43,7 +43,7 @@ See COPYRIGHT and LICENSE files for more details. end else render(Admin::CustomFields::CustomFieldProjects::TableComponent.new( - query: @available_project_custom_fields_query, + query: @available_custom_fields_projects_query, params: params.merge({ custom_field: @custom_field })) ) end diff --git a/config/locales/en.yml b/config/locales/en.yml index b69c3b9919b4..16bb1555c318 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2070,9 +2070,9 @@ en: label_attribute_expand_text: "The complete text for '%{attribute}'" label_authentication: "Authentication" label_authentication_settings: "Authentication settings" + label_available_custom_fields_projects: "Available custom fields projects" label_available_global_roles: "Available global roles" label_available_project_attributes: "Available project attributes" - label_available_project_custom_fields: "Available project custom fields" label_available_project_forums: "Available forums" label_available_project_repositories: "Available repositories" label_available_project_versions: "Available versions" diff --git a/spec/models/queries/projects/filters/available_project_custom_fields_filter_spec.rb b/spec/models/queries/projects/filters/available_custom_fields_projects_filter_spec.rb similarity index 93% rename from spec/models/queries/projects/filters/available_project_custom_fields_filter_spec.rb rename to spec/models/queries/projects/filters/available_custom_fields_projects_filter_spec.rb index ec484664b73e..fa0df067fa84 100644 --- a/spec/models/queries/projects/filters/available_project_custom_fields_filter_spec.rb +++ b/spec/models/queries/projects/filters/available_custom_fields_projects_filter_spec.rb @@ -28,11 +28,11 @@ require "spec_helper" -RSpec.describe Queries::Projects::Filters::AvailableProjectCustomFieldsFilter do +RSpec.describe Queries::Projects::Filters::AvailableCustomFieldsProjectsFilter do it_behaves_like "basic query filter" do - let(:class_key) { :available_project_custom_fields } + let(:class_key) { :available_custom_fields_projects } let(:type) { :list } - let(:human_name) { "Available project custom fields" } + let(:human_name) { "Available custom fields projects" } end it_behaves_like "list query filter", scope: false do @@ -44,7 +44,7 @@ let(:valid_values) do [custom_field_project1.custom_field_id.to_s, custom_field_project2.custom_field_id.to_s] end - let(:name) { "Available project attributes" } + let(:name) { "Available custom fields projects" } describe "#apply_to" do let(:values) { valid_values }