From d9ccb20ab4eb44f1dfc0064588582eaeb3edca80 Mon Sep 17 00:00:00 2001 From: ulferts Date: Wed, 13 Nov 2024 10:51:41 +0100 Subject: [PATCH 01/44] boilerplate for lifecycle settings --- .../settings/life_cycles_controller.rb | 33 +++++++++++ config/initializers/menus.rb | 1 + config/initializers/permissions.rb | 7 +++ config/locales/en.yml | 3 + config/routes.rb | 1 + .../life_cycle/active_in_project_spec.rb | 59 +++++++++++++++++++ .../pages/projects/settings/life_cycle.rb | 50 ++++++++++++++++ 7 files changed, 154 insertions(+) create mode 100644 app/controllers/projects/settings/life_cycles_controller.rb create mode 100644 spec/features/projects/life_cycle/active_in_project_spec.rb create mode 100644 spec/support/pages/projects/settings/life_cycle.rb diff --git a/app/controllers/projects/settings/life_cycles_controller.rb b/app/controllers/projects/settings/life_cycles_controller.rb new file mode 100644 index 000000000000..171520e29a8f --- /dev/null +++ b/app/controllers/projects/settings/life_cycles_controller.rb @@ -0,0 +1,33 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 Projects::Settings::LifeCyclesController < Projects::SettingsController + include OpTurbo::ComponentStream + + def show; end +end diff --git a/config/initializers/menus.rb b/config/initializers/menus.rb index 33bdb9cd0d25..898635e9c435 100644 --- a/config/initializers/menus.rb +++ b/config/initializers/menus.rb @@ -625,6 +625,7 @@ project_menu_items = { general: :label_information_plural, + life_cycles: :label_life_cycle_plural, project_custom_fields: :label_project_attributes_plural, modules: :label_module_plural, types: :label_work_package_types, diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index 87f571f46a9d..2efe64416e09 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -133,6 +133,13 @@ permissible_on: :project, require: :member + map.permission :select_project_life_cycle, + { + "projects/settings/life_cycles": %i[show] + }, + permissible_on: :project, + require: :member + map.permission :manage_members, { members: %i[index new create update destroy destroy_by_principal autocomplete_for_member menu], diff --git a/config/locales/en.yml b/config/locales/en.yml index 9657cfe546b0..f610a4a04c8d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2579,6 +2579,7 @@ en: label_none_parentheses: "(none)" label_not_contains: "doesn't contain" label_not_equals: "is not" + label_life_cycle_plural: "Project lifecycle" label_on: "on" label_operator_all: "is not empty" label_operator_none: "is empty" @@ -3203,6 +3204,8 @@ en: permission_search_project: "Search project" permission_select_custom_fields: "Select custom fields" permission_select_project_custom_fields: "Select project attributes" + permission_select_project_life_cycle: "Select project stages and gates" + permission_select_project_life_cycle_explanation: "Activate/Deactivate the stages and gates in a project." permission_select_project_modules: "Select project modules" permission_share_work_packages: "Share work packages" permission_manage_types: "Select types" diff --git a/config/routes.rb b/config/routes.rb index 158ed8a6a293..36339b94060e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -244,6 +244,7 @@ put :disable_all_of_section end end + resource :life_cycle, only: %i[show] resource :custom_fields, only: %i[show update] resource :repository, only: %i[show], controller: "repository" resource :versions, only: %i[show] diff --git a/spec/features/projects/life_cycle/active_in_project_spec.rb b/spec/features/projects/life_cycle/active_in_project_spec.rb new file mode 100644 index 000000000000..d055f3ffe361 --- /dev/null +++ b/spec/features/projects/life_cycle/active_in_project_spec.rb @@ -0,0 +1,59 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 "Projects life cycle settings", :js, :with_cuprite do + shared_let(:project) { create(:project) } + + shared_let(:user_with_permission) do + create(:user, + member_with_permissions: { + # TODO: change to specific permission + project: %w[ + select_project_life_cycle + ] + }) + end + + shared_let(:initiating_stage) { create(:stage, name: "Initiating") } + shared_let(:read_to_execute_gate) { create(:gate, name: "Read to Execute") } + shared_let(:executing_stage) { create(:stage, name: "Executing") } + shared_let(:read_to_close_gate) { create(:gate, name: "Read to Close") } + shared_let(:closing_stage) { create(:stage, name: "Closing") } + + let(:project_lifecycle_page) { Pages::Projects::Settings::LifeCycle.new(project) } + + context "with sufficient permissions" do + current_user { user_with_permission } + + it "allows toggling the active/inactive state of lifecycle elements" do + project_lifecycle_page.visit! + end + end +end diff --git a/spec/support/pages/projects/settings/life_cycle.rb b/spec/support/pages/projects/settings/life_cycle.rb new file mode 100644 index 000000000000..0348a9c8c1f5 --- /dev/null +++ b/spec/support/pages/projects/settings/life_cycle.rb @@ -0,0 +1,50 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 "support/pages/page" + +module Pages + module Projects + # TODO: Ideally this would be turned into a module but it would require having the other settings pages being split up. + class Settings + class LifeCycle < Pages::Page + attr_accessor :project + + def initialize(project) + super() + + self.project = project + end + + def path + "/projects/#{project.identifier}/settings/life_cycle" + end + end + end + end +end From e8b9c639c5e73d58ac98c4a19bb17561bd6157b6 Mon Sep 17 00:00:00 2001 From: ulferts Date: Wed, 13 Nov 2024 12:08:51 +0100 Subject: [PATCH 02/44] add header component to life cycle show page --- .../show_page_header_component.html.erb | 9 ++++ .../life_cycles/show_page_header_component.rb | 46 +++++++++++++++++++ .../settings/life_cycles/show.html.erb | 36 +++++++++++++++ config/locales/en.yml | 4 ++ config/routes.rb | 3 ++ 5 files changed, 98 insertions(+) create mode 100644 app/components/projects/settings/life_cycles/show_page_header_component.html.erb create mode 100644 app/components/projects/settings/life_cycles/show_page_header_component.rb create mode 100644 app/views/projects/settings/life_cycles/show.html.erb diff --git a/app/components/projects/settings/life_cycles/show_page_header_component.html.erb b/app/components/projects/settings/life_cycles/show_page_header_component.html.erb new file mode 100644 index 000000000000..a090cf4fbbf1 --- /dev/null +++ b/app/components/projects/settings/life_cycles/show_page_header_component.html.erb @@ -0,0 +1,9 @@ +<%= render Primer::OpenProject::PageHeader.new do |header| %> + <%= header.with_title(variant: :default) { t('projects.settings.life_cycle.header.title') } %> + <%= header.with_description { + t('projects.settings.life_cycle.header.description', + overview_url: project_path(@project), + admin_settings_url: admin_settings_project_life_cycles_path).html_safe + } %> + <%= header.with_breadcrumbs(breadcrumb_items) %> +<% end %> diff --git a/app/components/projects/settings/life_cycles/show_page_header_component.rb b/app/components/projects/settings/life_cycles/show_page_header_component.rb new file mode 100644 index 000000000000..0b9f15fc85d9 --- /dev/null +++ b/app/components/projects/settings/life_cycles/show_page_header_component.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 Projects::Settings::LifeCycles + class ShowPageHeaderComponent < ApplicationComponent + include ApplicationHelper + + def initialize(project: nil) + super + @project = project + end + + def breadcrumb_items + [{ href: project_overview_path(@project.id), text: @project.name }, + { href: project_settings_general_path(@project.id), text: I18n.t("label_project_settings") }, + t("projects.settings.life_cycle.header.title")] + end + end +end diff --git a/app/views/projects/settings/life_cycles/show.html.erb b/app/views/projects/settings/life_cycles/show.html.erb new file mode 100644 index 000000000000..a57319a2026e --- /dev/null +++ b/app/views/projects/settings/life_cycles/show.html.erb @@ -0,0 +1,36 @@ +<%#-- 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. + +++#%> +
+ <%= render Projects::Settings::LifeCycles::ShowPageHeaderComponent.new(project: @project) %> + + <%#= render(Projects::Settings::ProjectCustomFieldSections::IndexComponent.new( + project: @project, + project_custom_field_sections: @project_custom_field_sections, + )) %> +
diff --git a/config/locales/en.yml b/config/locales/en.yml index f610a4a04c8d..c9af4de7824d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -388,6 +388,10 @@ en: no_results_content_text: Create a new work package category custom_fields: no_results_title_text: There are currently no custom fields available. + life_cycle: + header: + title: "Project lifecycle" + description: 'These project stages and gates in the project lifecycle will be displayed in your project overview page and are defined in the administration settings by the administrator of the instance. You can enable or disable individual attributes.' project_custom_fields: header: title: "Project attributes" diff --git a/config/routes.rb b/config/routes.rb index 36339b94060e..07707700078b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -519,6 +519,9 @@ delete :unlink end end + # TODO: This is for now only added to be able to create a link + # There is no controller behind this. + resources :project_life_cycles, only: %i[index] resources :project_custom_field_sections, controller: "/admin/settings/project_custom_field_sections", only: %i[create update destroy] do member do From c4cae9c6d24200e1eab892e1ea1f3dbfc2839031 Mon Sep 17 00:00:00 2001 From: ulferts Date: Wed, 13 Nov 2024 12:09:11 +0100 Subject: [PATCH 03/44] highlight menu item --- app/controllers/projects/settings/life_cycles_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/projects/settings/life_cycles_controller.rb b/app/controllers/projects/settings/life_cycles_controller.rb index 171520e29a8f..e3b63a75e7cc 100644 --- a/app/controllers/projects/settings/life_cycles_controller.rb +++ b/app/controllers/projects/settings/life_cycles_controller.rb @@ -29,5 +29,7 @@ class Projects::Settings::LifeCyclesController < Projects::SettingsController include OpTurbo::ComponentStream + menu_item :settings_life_cycles + def show; end end From 7eb0e4f861698c36da23b74436c9a9da8c4e80df Mon Sep 17 00:00:00 2001 From: ulferts Date: Wed, 13 Nov 2024 17:08:49 +0100 Subject: [PATCH 04/44] list all existing life cycle elements --- .../life_cycles/element_component.html.erb | 12 ++++ .../settings/life_cycles/element_component.rb | 53 ++++++++++++++++++ .../life_cycles/index_component.html.erb | 29 ++++++++++ .../settings/life_cycles/index_component.rb | 56 +++++++++++++++++++ .../settings/life_cycles_controller.rb | 8 +++ .../settings/life_cycles/show.html.erb | 5 +- config/locales/en.yml | 1 + .../life_cycle/active_in_project_spec.rb | 9 ++- .../pages/projects/settings/life_cycle.rb | 12 ++++ 9 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 app/components/projects/settings/life_cycles/element_component.html.erb create mode 100644 app/components/projects/settings/life_cycles/element_component.rb create mode 100644 app/components/projects/settings/life_cycles/index_component.html.erb create mode 100644 app/components/projects/settings/life_cycles/index_component.rb diff --git a/app/components/projects/settings/life_cycles/element_component.html.erb b/app/components/projects/settings/life_cycles/element_component.html.erb new file mode 100644 index 000000000000..f5a249fb1ffb --- /dev/null +++ b/app/components/projects/settings/life_cycles/element_component.html.erb @@ -0,0 +1,12 @@ +<%= + component_wrapper do + flex_layout(align_items: :center, + justify_content: :space_between) do |element_container| + element_container.with_column(flex_layout: true) do |title_container| + title_container.with_column(pt: 1, mr: 2) do + render(Primer::Beta::Text.new) { element.name } + end + end + end + end +%> diff --git a/app/components/projects/settings/life_cycles/element_component.rb b/app/components/projects/settings/life_cycles/element_component.rb new file mode 100644 index 000000000000..39d1f5ac39ea --- /dev/null +++ b/app/components/projects/settings/life_cycles/element_component.rb @@ -0,0 +1,53 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 Projects + module Settings + module LifeCycles + class ElementComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + attr_reader :element + + def initialize(life_cycle_element) + super + + @element = life_cycle_element + end + + private + + def wrapper_uniq_by + element.id + end + end + end + end +end diff --git a/app/components/projects/settings/life_cycles/index_component.html.erb b/app/components/projects/settings/life_cycles/index_component.html.erb new file mode 100644 index 000000000000..2cb0c2a50af3 --- /dev/null +++ b/app/components/projects/settings/life_cycles/index_component.html.erb @@ -0,0 +1,29 @@ +<%= + component_wrapper do + render(border_box_container(mb: 3, data: { test_selector: "project-life-cycle-administration" })) do |component| + component.with_header(font_weight: :bold, py: 2) do + flex_layout(justify_content: :space_between, align_items: :center) do |header_container| + header_container.with_column(py: 2) do + # adding py: 2 here to match the padding of the actions_container + # otherwise the header height changes when the actions gets hidden when filtering + render(Primer::Beta::Text.new(font_weight: :bold)) do + I18n.t('projects.settings.life_cycle.header.title') + end + end + end + end + if life_cycle_elements.empty? + component.with_row do + render(Primer::Beta::Text.new(color: :subtle)) { t("projects.settings.life_cycle.non_defined") } + end + else + life_cycle_elements.each do |life_cycle_element| + component.with_row(data: { 'projects--settings--project-custom-fields-mapping-filter-target': 'searchItem' }, + test_selector: "project-life-cycle-element-#{life_cycle_element.id}") do + render(Projects::Settings::LifeCycles::ElementComponent.new(life_cycle_element)) + end + end + end + end + end +%> diff --git a/app/components/projects/settings/life_cycles/index_component.rb b/app/components/projects/settings/life_cycles/index_component.rb new file mode 100644 index 000000000000..55261db5c059 --- /dev/null +++ b/app/components/projects/settings/life_cycles/index_component.rb @@ -0,0 +1,56 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 Projects + module Settings + module LifeCycles + class IndexComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + attr_reader :project, + :life_cycle_elements + + def initialize(project:, life_cycle_elements:) + super + + @project = project + @life_cycle_elements = life_cycle_elements + end + + private + + # TODO: Check if this is necessary + def wrapper_uniq_by + @project.id + end + end + end + end +end diff --git a/app/controllers/projects/settings/life_cycles_controller.rb b/app/controllers/projects/settings/life_cycles_controller.rb index e3b63a75e7cc..6ac4af23cad2 100644 --- a/app/controllers/projects/settings/life_cycles_controller.rb +++ b/app/controllers/projects/settings/life_cycles_controller.rb @@ -29,7 +29,15 @@ class Projects::Settings::LifeCyclesController < Projects::SettingsController include OpTurbo::ComponentStream + before_action :load_life_cycle_elements, only: %i[show] + menu_item :settings_life_cycles def show; end + + private + + def load_life_cycle_elements + @life_cycle_elements = LifeCycle.all + end end diff --git a/app/views/projects/settings/life_cycles/show.html.erb b/app/views/projects/settings/life_cycles/show.html.erb index a57319a2026e..b90027e4078a 100644 --- a/app/views/projects/settings/life_cycles/show.html.erb +++ b/app/views/projects/settings/life_cycles/show.html.erb @@ -29,8 +29,5 @@ See COPYRIGHT and LICENSE files for more details.
<%= render Projects::Settings::LifeCycles::ShowPageHeaderComponent.new(project: @project) %> - <%#= render(Projects::Settings::ProjectCustomFieldSections::IndexComponent.new( - project: @project, - project_custom_field_sections: @project_custom_field_sections, - )) %> + <%= render(Projects::Settings::LifeCycles::IndexComponent.new( project: @project, life_cycle_elements: @life_cycle_elements)) %>
diff --git a/config/locales/en.yml b/config/locales/en.yml index c9af4de7824d..b164afc1622b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -392,6 +392,7 @@ en: header: title: "Project lifecycle" description: 'These project stages and gates in the project lifecycle will be displayed in your project overview page and are defined in the administration settings by the administrator of the instance. You can enable or disable individual attributes.' + non_defined: "Neither stages nor gates are currently defined." project_custom_fields: header: title: "Project attributes" diff --git a/spec/features/projects/life_cycle/active_in_project_spec.rb b/spec/features/projects/life_cycle/active_in_project_spec.rb index d055f3ffe361..94009b45dfa6 100644 --- a/spec/features/projects/life_cycle/active_in_project_spec.rb +++ b/spec/features/projects/life_cycle/active_in_project_spec.rb @@ -34,8 +34,7 @@ shared_let(:user_with_permission) do create(:user, member_with_permissions: { - # TODO: change to specific permission - project: %w[ + project => %w[ select_project_life_cycle ] }) @@ -54,6 +53,12 @@ it "allows toggling the active/inactive state of lifecycle elements" do project_lifecycle_page.visit! + + project_lifecycle_page.expect_listed(initiating_stage, + read_to_execute_gate, + executing_stage, + read_to_close_gate, + closing_stage) end end end diff --git a/spec/support/pages/projects/settings/life_cycle.rb b/spec/support/pages/projects/settings/life_cycle.rb index 0348a9c8c1f5..63e08eac7fea 100644 --- a/spec/support/pages/projects/settings/life_cycle.rb +++ b/spec/support/pages/projects/settings/life_cycle.rb @@ -44,6 +44,18 @@ def initialize(project) def path "/projects/#{project.identifier}/settings/life_cycle" end + + def expect_listed(*life_cycle_elements) + raise "Currently depends on checking for at least two life cycle events" if life_cycle_elements.size < 2 + + life_cycle_elements.each_cons(2) do |predecessor, successor| + expect(page).to have_css("#{life_cycle_test_selector(predecessor)} + #{life_cycle_test_selector(successor)}") + end + end + + def life_cycle_test_selector(element) + test_selector("project-life-cycle-element-#{element.id}") + end end end end From 2c7b1bc3a81c29113cce57b62d9633752d3f579a Mon Sep 17 00:00:00 2001 From: ulferts Date: Thu, 14 Nov 2024 12:08:29 +0100 Subject: [PATCH 05/44] add ability to toggle the active state of a step Non existing mappings between project and step definition will be created on the fly. --- .../life_cycles/element_component.html.erb | 23 ++++++++++++ .../settings/life_cycles/element_component.rb | 19 ++++++++-- .../life_cycles/index_component.html.erb | 6 ++-- .../settings/life_cycles_controller.rb | 20 +++++++++++ config/initializers/permissions.rb | 2 +- config/locales/en.yml | 2 ++ config/routes.rb | 6 +++- .../life_cycle/active_in_project_spec.rb | 31 ++++++++++++---- .../pages/projects/settings/life_cycle.rb | 35 ++++++++++++++++--- 9 files changed, 125 insertions(+), 19 deletions(-) diff --git a/app/components/projects/settings/life_cycles/element_component.html.erb b/app/components/projects/settings/life_cycles/element_component.html.erb index f5a249fb1ffb..b992db25d905 100644 --- a/app/components/projects/settings/life_cycles/element_component.html.erb +++ b/app/components/projects/settings/life_cycles/element_component.html.erb @@ -7,6 +7,29 @@ render(Primer::Beta::Text.new) { element.name } end end + # py: 1 quick fix: prevents the row from bouncing as the toggle switch currently changes height while toggling + element_container.with_column(py: 1, mr: 2) do + # buggy currently: + # small variant + status_label_position: :start leads to a small bounce while toggling + # behavior can be seen on primer's viewbook as well -> https://view-components-storybook.eastus.cloudapp.azure.com/view-components/lookbook/inspect/primer/alpha/toggle_switch/small + # quick fix: don't display loading indicator which is causing the bounce + render(Primer::Alpha::ToggleSwitch.new( + src: toggle_project_settings_life_cycle_path( + project_life_cycle: { + project_id: project.id, + life_cycle_id: element.id, + type: element.type + } + ), + csrf_token: form_authenticity_token, + data: { test_selector: "toggle-project-life-cycle-#{element.id}" }, + aria: { label: toggle_aria_label }, + checked: active_in_project?, + size: :small, + status_label_position: :start, + classes: "op-primer-adjustments__toggle-switch--hidden-loading-indicator", + )) + end end end %> diff --git a/app/components/projects/settings/life_cycles/element_component.rb b/app/components/projects/settings/life_cycles/element_component.rb index 39d1f5ac39ea..5593bd072fa3 100644 --- a/app/components/projects/settings/life_cycles/element_component.rb +++ b/app/components/projects/settings/life_cycles/element_component.rb @@ -34,12 +34,25 @@ class ElementComponent < ApplicationComponent include OpPrimer::ComponentHelpers include OpTurbo::Streamable - attr_reader :element + attr_reader :element, + :project - def initialize(life_cycle_element) + def initialize(project:, element:) super - @element = life_cycle_element + @element = element + @project = project + end + + def active_in_project? + project + .project_life_cycles + .detect { |project_lc| project_lc.life_cycle_id == element.id } + &.active + end + + def toggle_aria_label + I18n.t("projects.settings.life_cycle.element.use_in_project", element: element.name) end private diff --git a/app/components/projects/settings/life_cycles/index_component.html.erb b/app/components/projects/settings/life_cycles/index_component.html.erb index 2cb0c2a50af3..7f3fc67985da 100644 --- a/app/components/projects/settings/life_cycles/index_component.html.erb +++ b/app/components/projects/settings/life_cycles/index_component.html.erb @@ -17,10 +17,10 @@ render(Primer::Beta::Text.new(color: :subtle)) { t("projects.settings.life_cycle.non_defined") } end else - life_cycle_elements.each do |life_cycle_element| + life_cycle_elements.each do |element| component.with_row(data: { 'projects--settings--project-custom-fields-mapping-filter-target': 'searchItem' }, - test_selector: "project-life-cycle-element-#{life_cycle_element.id}") do - render(Projects::Settings::LifeCycles::ElementComponent.new(life_cycle_element)) + test_selector: "project-life-cycle-element-#{element.id}") do + render(Projects::Settings::LifeCycles::ElementComponent.new(project:, element:)) end end end diff --git a/app/controllers/projects/settings/life_cycles_controller.rb b/app/controllers/projects/settings/life_cycles_controller.rb index 6ac4af23cad2..ff897746272d 100644 --- a/app/controllers/projects/settings/life_cycles_controller.rb +++ b/app/controllers/projects/settings/life_cycles_controller.rb @@ -30,14 +30,34 @@ class Projects::Settings::LifeCyclesController < Projects::SettingsController include OpTurbo::ComponentStream before_action :load_life_cycle_elements, only: %i[show] + before_action :load_or_create_life_cycle_element, only: %i[toggle] menu_item :settings_life_cycles def show; end + def toggle + @life_cycle_element.toggle!(:active) + end + private def load_life_cycle_elements @life_cycle_elements = LifeCycle.all end + + def load_or_create_life_cycle_element + element_params = params.require(:project_life_cycle).permit(:life_cycle_id, :project_id, :type) + + klass = case element_params.delete(:type) + when Stage.name + Project::Stage + when Gate.name + Project::Gate + else + raise NotImplementedError, "Unknown life cycle element type" + end + + @life_cycle_element = klass.find_or_create_by(element_params) + end end diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index 2efe64416e09..4151672327f1 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -135,7 +135,7 @@ map.permission :select_project_life_cycle, { - "projects/settings/life_cycles": %i[show] + "projects/settings/life_cycles": %i[show toggle] }, permissible_on: :project, require: :member diff --git a/config/locales/en.yml b/config/locales/en.yml index b164afc1622b..c9bc5bc2844f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -393,6 +393,8 @@ en: title: "Project lifecycle" description: 'These project stages and gates in the project lifecycle will be displayed in your project overview page and are defined in the administration settings by the administrator of the instance. You can enable or disable individual attributes.' non_defined: "Neither stages nor gates are currently defined." + element: + use_in_project: "Use %{element} in this project" project_custom_fields: header: title: "Project attributes" diff --git a/config/routes.rb b/config/routes.rb index 07707700078b..6ac9d0feb869 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -244,7 +244,11 @@ put :disable_all_of_section end end - resource :life_cycle, only: %i[show] + resource :life_cycle, only: %i[show] do + member do + post :toggle + end + end resource :custom_fields, only: %i[show update] resource :repository, only: %i[show], controller: "repository" resource :versions, only: %i[show] diff --git a/spec/features/projects/life_cycle/active_in_project_spec.rb b/spec/features/projects/life_cycle/active_in_project_spec.rb index 94009b45dfa6..63666b11a430 100644 --- a/spec/features/projects/life_cycle/active_in_project_spec.rb +++ b/spec/features/projects/life_cycle/active_in_project_spec.rb @@ -41,9 +41,9 @@ end shared_let(:initiating_stage) { create(:stage, name: "Initiating") } - shared_let(:read_to_execute_gate) { create(:gate, name: "Read to Execute") } + shared_let(:ready_to_execute_gate) { create(:gate, name: "Ready to Execute") } shared_let(:executing_stage) { create(:stage, name: "Executing") } - shared_let(:read_to_close_gate) { create(:gate, name: "Read to Close") } + shared_let(:ready_to_close_gate) { create(:gate, name: "Ready to Close") } shared_let(:closing_stage) { create(:stage, name: "Closing") } let(:project_lifecycle_page) { Pages::Projects::Settings::LifeCycle.new(project) } @@ -54,11 +54,28 @@ it "allows toggling the active/inactive state of lifecycle elements" do project_lifecycle_page.visit! - project_lifecycle_page.expect_listed(initiating_stage, - read_to_execute_gate, - executing_stage, - read_to_close_gate, - closing_stage) + project_lifecycle_page.expect_listed(initiating_stage => false, + ready_to_execute_gate => false, + executing_stage => false, + ready_to_close_gate => false, + closing_stage => false) + + # Activate the stages to be found within the project + project_lifecycle_page.toggle(initiating_stage) + project_lifecycle_page.toggle(ready_to_close_gate) + project_lifecycle_page.toggle(closing_stage) + + wait_for_network_idle + + # Expect the activation state to be kept after a reload + visit home_path + project_lifecycle_page.visit! + + project_lifecycle_page.expect_listed(initiating_stage => true, + ready_to_execute_gate => false, + executing_stage => false, + ready_to_close_gate => true, + closing_stage => true) end end end diff --git a/spec/support/pages/projects/settings/life_cycle.rb b/spec/support/pages/projects/settings/life_cycle.rb index 63e08eac7fea..07ddb2544d57 100644 --- a/spec/support/pages/projects/settings/life_cycle.rb +++ b/spec/support/pages/projects/settings/life_cycle.rb @@ -45,17 +45,44 @@ def path "/projects/#{project.identifier}/settings/life_cycle" end - def expect_listed(*life_cycle_elements) - raise "Currently depends on checking for at least two life cycle events" if life_cycle_elements.size < 2 + # Checks if the life cycle elements are listed in the order given and with the correct toggle state. + # @param life_cycle_elements [Hash{LifeCycleElement => Boolean}] + def expect_listed(**life_cycle_elements) + if life_cycle_elements.size > 1 + life_cycle_elements.each_cons(2) do |(predecessor, _), (successor, _)| + expect(page).to have_css("#{life_cycle_test_selector(predecessor)} + #{life_cycle_test_selector(successor)}") + end + end - life_cycle_elements.each_cons(2) do |predecessor, successor| - expect(page).to have_css("#{life_cycle_test_selector(predecessor)} + #{life_cycle_test_selector(successor)}") + life_cycle_elements.each do |element, active| + expect_toggle_state(element, active) end end + def expect_toggle_state(element, active) + within toggle_element(element) do + expect(page) + .to have_css(".ToggleSwitch-status#{expected_toggle_status(active)}"), + "Expected toggle for '#{element.name}' to be #{expected_toggle_status(active)} " \ + "but was #{expected_toggle_status(!active)}" + end + end + + def toggle(element) + toggle_element(element).click + end + def life_cycle_test_selector(element) test_selector("project-life-cycle-element-#{element.id}") end + + def toggle_element(element) + find_test_selector("toggle-project-life-cycle-#{element.id}") + end + + def expected_toggle_status(active) + active ? "On" : "Off" + end end end end From fc3b99b6f0c05abefec4448772f80ec0c0ed7cb4 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 15 Nov 2024 16:42:48 +0100 Subject: [PATCH 06/44] display life cycle type next to name --- .../life_cycle_type_component.html.erb | 9 +++ .../projects/life_cycle_type_component.rb | 57 +++++++++++++++++++ .../life_cycles/element_component.html.erb | 5 +- config/locales/en.yml | 2 + 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 app/components/projects/life_cycle_type_component.html.erb create mode 100644 app/components/projects/life_cycle_type_component.rb diff --git a/app/components/projects/life_cycle_type_component.html.erb b/app/components/projects/life_cycle_type_component.html.erb new file mode 100644 index 000000000000..dc294093ebb1 --- /dev/null +++ b/app/components/projects/life_cycle_type_component.html.erb @@ -0,0 +1,9 @@ +<%= flex_layout(align_items: :center, + justify_content: :space_between) do |element_container| + element_container.with_column(mr: 1, style: "color: #{icon_color}") do + render Primer::Beta::Octicon.new(icon: icon) + end + element_container.with_column do + render(Primer::Beta::Text.new(**text_options)) { text } + end +end %> diff --git a/app/components/projects/life_cycle_type_component.rb b/app/components/projects/life_cycle_type_component.rb new file mode 100644 index 000000000000..175a6a9decd0 --- /dev/null +++ b/app/components/projects/life_cycle_type_component.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 Projects + class LifeCycleTypeComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + + def text + model.model_name.human + end + + def icon + case model + when Stage + :"git-commit" + when Gate + :diamond + else + raise NotImplementedError, "Unknown model #{model.class} to render a LifeCycleTypeComponent with" + end + end + + def icon_color + model.color.hexcode + end + + def text_options + { color: :muted }.merge(options) + end + end +end diff --git a/app/components/projects/settings/life_cycles/element_component.html.erb b/app/components/projects/settings/life_cycles/element_component.html.erb index b992db25d905..dd66c23f679f 100644 --- a/app/components/projects/settings/life_cycles/element_component.html.erb +++ b/app/components/projects/settings/life_cycles/element_component.html.erb @@ -3,9 +3,12 @@ flex_layout(align_items: :center, justify_content: :space_between) do |element_container| element_container.with_column(flex_layout: true) do |title_container| - title_container.with_column(pt: 1, mr: 2) do + title_container.with_column(pt: 1, mr: 3) do render(Primer::Beta::Text.new) { element.name } end + title_container.with_column(pt: 1) do + render(Projects::LifeCycleTypeComponent.new(element)) + end end # py: 1 quick fix: prevents the row from bouncing as the toggle switch currently changes height while toggling element_container.with_column(py: 1, mr: 2) do diff --git a/config/locales/en.yml b/config/locales/en.yml index c9bc5bc2844f..de7ba2875a28 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1401,6 +1401,7 @@ en: custom_field: "Custom field" "doorkeeper/application": "OAuth application" forum: "Forum" + gate: "Gate" global_role: "Global role" group: "Group" member: "Member" @@ -1418,6 +1419,7 @@ en: one: "Role" other: "Roles" status: "Work package status" + stage: "Stage" token/api: one: Access token other: Access tokens From 4524b43f16f46955c2cffa2f95a83f015e054d38 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 15 Nov 2024 16:43:15 +0100 Subject: [PATCH 07/44] adapt section header --- .../projects/settings/life_cycles/index_component.html.erb | 2 +- config/locales/en.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/projects/settings/life_cycles/index_component.html.erb b/app/components/projects/settings/life_cycles/index_component.html.erb index 7f3fc67985da..c0ad7866949f 100644 --- a/app/components/projects/settings/life_cycles/index_component.html.erb +++ b/app/components/projects/settings/life_cycles/index_component.html.erb @@ -7,7 +7,7 @@ # adding py: 2 here to match the padding of the actions_container # otherwise the header height changes when the actions gets hidden when filtering render(Primer::Beta::Text.new(font_weight: :bold)) do - I18n.t('projects.settings.life_cycle.header.title') + I18n.t('projects.settings.life_cycle.section_header') end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index de7ba2875a28..be07ff2baf4a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -393,6 +393,7 @@ en: title: "Project lifecycle" description: 'These project stages and gates in the project lifecycle will be displayed in your project overview page and are defined in the administration settings by the administrator of the instance. You can enable or disable individual attributes.' non_defined: "Neither stages nor gates are currently defined." + section_header: "Stages and gates" element: use_in_project: "Use %{element} in this project" project_custom_fields: From a4e680491c92f8b81621696f56bfc587f9a098cd Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 18 Nov 2024 15:31:51 +0100 Subject: [PATCH 08/44] use css class for icon colorization --- app/components/projects/life_cycle_type_component.html.erb | 2 +- app/components/projects/life_cycle_type_component.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/components/projects/life_cycle_type_component.html.erb b/app/components/projects/life_cycle_type_component.html.erb index dc294093ebb1..cb685a9ab715 100644 --- a/app/components/projects/life_cycle_type_component.html.erb +++ b/app/components/projects/life_cycle_type_component.html.erb @@ -1,6 +1,6 @@ <%= flex_layout(align_items: :center, justify_content: :space_between) do |element_container| - element_container.with_column(mr: 1, style: "color: #{icon_color}") do + element_container.with_column(mr: 1, classes: icon_color_class) do render Primer::Beta::Octicon.new(icon: icon) end element_container.with_column do diff --git a/app/components/projects/life_cycle_type_component.rb b/app/components/projects/life_cycle_type_component.rb index 175a6a9decd0..5d0e7db34837 100644 --- a/app/components/projects/life_cycle_type_component.rb +++ b/app/components/projects/life_cycle_type_component.rb @@ -46,8 +46,8 @@ def icon end end - def icon_color - model.color.hexcode + def icon_color_class + helpers.hl_inline_class("life_cycle_step_definition", model) end def text_options From 22452c9993bc178c7312b112b386bff881ec8344 Mon Sep 17 00:00:00 2001 From: ulferts Date: Thu, 21 Nov 2024 17:27:10 +0100 Subject: [PATCH 09/44] decrease font size for type --- app/components/projects/life_cycle_type_component.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/projects/life_cycle_type_component.rb b/app/components/projects/life_cycle_type_component.rb index 5d0e7db34837..f6793e45b74c 100644 --- a/app/components/projects/life_cycle_type_component.rb +++ b/app/components/projects/life_cycle_type_component.rb @@ -51,7 +51,7 @@ def icon_color_class end def text_options - { color: :muted }.merge(options) + { color: :muted, font_size: :small }.merge(options) end end end From 65c917f7cac8baded8b2bc92704204dfd041aaa9 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 22 Nov 2024 08:42:29 +0100 Subject: [PATCH 10/44] include life cycle type component in lookbook --- .../projects/life_cycle_type_component.html.erb | 3 +-- .../projects/life_cycle_type_component.rb | 5 ++++- .../projects/life_cycle_type_component_preview.rb | 14 ++++++++++++++ .../gate.html.erb | 6 ++++++ .../stage.html.erb | 6 ++++++ 5 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 lookbook/previews/open_project/projects/life_cycle_type_component_preview.rb create mode 100644 lookbook/previews/open_project/projects/life_cycle_type_component_preview/gate.html.erb create mode 100644 lookbook/previews/open_project/projects/life_cycle_type_component_preview/stage.html.erb diff --git a/app/components/projects/life_cycle_type_component.html.erb b/app/components/projects/life_cycle_type_component.html.erb index cb685a9ab715..728082531b70 100644 --- a/app/components/projects/life_cycle_type_component.html.erb +++ b/app/components/projects/life_cycle_type_component.html.erb @@ -1,5 +1,4 @@ -<%= flex_layout(align_items: :center, - justify_content: :space_between) do |element_container| +<%= flex_layout(align_items: :center) do |element_container| element_container.with_column(mr: 1, classes: icon_color_class) do render Primer::Beta::Octicon.new(icon: icon) end diff --git a/app/components/projects/life_cycle_type_component.rb b/app/components/projects/life_cycle_type_component.rb index f6793e45b74c..9ada0c434618 100644 --- a/app/components/projects/life_cycle_type_component.rb +++ b/app/components/projects/life_cycle_type_component.rb @@ -51,7 +51,10 @@ def icon_color_class end def text_options - { color: :muted, font_size: :small }.merge(options) + # The tag: :div is is a hack to fix the line height difference + # caused by font_size: :small. That line height difference + # would otherwise lead to the text being not on the same height as the icon + { color: :muted, font_size: :small, tag: :div }.merge(options) end end end diff --git a/lookbook/previews/open_project/projects/life_cycle_type_component_preview.rb b/lookbook/previews/open_project/projects/life_cycle_type_component_preview.rb new file mode 100644 index 000000000000..dd73a64cc946 --- /dev/null +++ b/lookbook/previews/open_project/projects/life_cycle_type_component_preview.rb @@ -0,0 +1,14 @@ +module OpenProject + module Projects + # @logical_path OpenProject/Projects + class LifeCycleTypeComponentPreview < Lookbook::Preview + def gate + render_with_template(locals: { model: Gate.new(id: 1, name: "The first gate") }) + end + + def stage + render_with_template(locals: { model: Stage.new(id: 1, name: "The first stage") }) + end + end + end +end diff --git a/lookbook/previews/open_project/projects/life_cycle_type_component_preview/gate.html.erb b/lookbook/previews/open_project/projects/life_cycle_type_component_preview/gate.html.erb new file mode 100644 index 000000000000..442cbfcb2e3e --- /dev/null +++ b/lookbook/previews/open_project/projects/life_cycle_type_component_preview/gate.html.erb @@ -0,0 +1,6 @@ + + + +<%= render(::Projects::LifeCycleTypeComponent.new(model)) %> diff --git a/lookbook/previews/open_project/projects/life_cycle_type_component_preview/stage.html.erb b/lookbook/previews/open_project/projects/life_cycle_type_component_preview/stage.html.erb new file mode 100644 index 000000000000..442cbfcb2e3e --- /dev/null +++ b/lookbook/previews/open_project/projects/life_cycle_type_component_preview/stage.html.erb @@ -0,0 +1,6 @@ + + + +<%= render(::Projects::LifeCycleTypeComponent.new(model)) %> From 79e0ba45c924cae4ddb7b6ef999a792977e5b2d3 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 22 Nov 2024 14:45:47 +0100 Subject: [PATCH 11/44] adapt names after rebasing --- app/components/projects/life_cycle_type_component.rb | 4 ++-- .../settings/life_cycles/element_component.html.erb | 2 +- .../projects/settings/life_cycles/element_component.rb | 4 ++-- .../projects/settings/life_cycles_controller.rb | 8 ++++---- config/locales/en.yml | 4 ++-- .../projects/life_cycle_type_component_preview.rb | 4 ++-- .../projects/life_cycle/active_in_project_spec.rb | 10 +++++----- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/components/projects/life_cycle_type_component.rb b/app/components/projects/life_cycle_type_component.rb index 9ada0c434618..453f5d90c5d6 100644 --- a/app/components/projects/life_cycle_type_component.rb +++ b/app/components/projects/life_cycle_type_component.rb @@ -37,9 +37,9 @@ def text def icon case model - when Stage + when Project::StageDefinition :"git-commit" - when Gate + when Project::GateDefinition :diamond else raise NotImplementedError, "Unknown model #{model.class} to render a LifeCycleTypeComponent with" diff --git a/app/components/projects/settings/life_cycles/element_component.html.erb b/app/components/projects/settings/life_cycles/element_component.html.erb index dd66c23f679f..c582fb0dd6af 100644 --- a/app/components/projects/settings/life_cycles/element_component.html.erb +++ b/app/components/projects/settings/life_cycles/element_component.html.erb @@ -20,7 +20,7 @@ src: toggle_project_settings_life_cycle_path( project_life_cycle: { project_id: project.id, - life_cycle_id: element.id, + definition_id: element.id, type: element.type } ), diff --git a/app/components/projects/settings/life_cycles/element_component.rb b/app/components/projects/settings/life_cycles/element_component.rb index 5593bd072fa3..07bea6ba3d16 100644 --- a/app/components/projects/settings/life_cycles/element_component.rb +++ b/app/components/projects/settings/life_cycles/element_component.rb @@ -46,8 +46,8 @@ def initialize(project:, element:) def active_in_project? project - .project_life_cycles - .detect { |project_lc| project_lc.life_cycle_id == element.id } + .life_cycle_steps + .detect { |project_lc| project_lc.definition_id == element.id } &.active end diff --git a/app/controllers/projects/settings/life_cycles_controller.rb b/app/controllers/projects/settings/life_cycles_controller.rb index ff897746272d..f29132e73547 100644 --- a/app/controllers/projects/settings/life_cycles_controller.rb +++ b/app/controllers/projects/settings/life_cycles_controller.rb @@ -43,16 +43,16 @@ def toggle private def load_life_cycle_elements - @life_cycle_elements = LifeCycle.all + @life_cycle_elements = Project::LifeCycleStepDefinition.all end def load_or_create_life_cycle_element - element_params = params.require(:project_life_cycle).permit(:life_cycle_id, :project_id, :type) + element_params = params.require(:project_life_cycle).permit(:definition_id, :project_id, :type) klass = case element_params.delete(:type) - when Stage.name + when Project::StageDefinition.name Project::Stage - when Gate.name + when Project::GateDefinition.name Project::Gate else raise NotImplementedError, "Unknown life cycle element type" diff --git a/config/locales/en.yml b/config/locales/en.yml index be07ff2baf4a..3defcab38ed3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1402,7 +1402,6 @@ en: custom_field: "Custom field" "doorkeeper/application": "OAuth application" forum: "Forum" - gate: "Gate" global_role: "Global role" group: "Group" member: "Member" @@ -1412,6 +1411,8 @@ en: other: "Notifications" placeholder_user: "Placeholder user" project: "Project" + project/gate_definition: "Gate" + project/stage_definition: "Stage" project_query: one: "Project list" other: "Project lists" @@ -1420,7 +1421,6 @@ en: one: "Role" other: "Roles" status: "Work package status" - stage: "Stage" token/api: one: Access token other: Access tokens diff --git a/lookbook/previews/open_project/projects/life_cycle_type_component_preview.rb b/lookbook/previews/open_project/projects/life_cycle_type_component_preview.rb index dd73a64cc946..319c4b19c779 100644 --- a/lookbook/previews/open_project/projects/life_cycle_type_component_preview.rb +++ b/lookbook/previews/open_project/projects/life_cycle_type_component_preview.rb @@ -3,11 +3,11 @@ module Projects # @logical_path OpenProject/Projects class LifeCycleTypeComponentPreview < Lookbook::Preview def gate - render_with_template(locals: { model: Gate.new(id: 1, name: "The first gate") }) + render_with_template(locals: { model: Project::GateDefinition.new(id: 1, name: "The first gate") }) end def stage - render_with_template(locals: { model: Stage.new(id: 1, name: "The first stage") }) + render_with_template(locals: { model: Project::StageDefinition.new(id: 1, name: "The first stage") }) end end end diff --git a/spec/features/projects/life_cycle/active_in_project_spec.rb b/spec/features/projects/life_cycle/active_in_project_spec.rb index 63666b11a430..88f108880f19 100644 --- a/spec/features/projects/life_cycle/active_in_project_spec.rb +++ b/spec/features/projects/life_cycle/active_in_project_spec.rb @@ -40,11 +40,11 @@ }) end - shared_let(:initiating_stage) { create(:stage, name: "Initiating") } - shared_let(:ready_to_execute_gate) { create(:gate, name: "Ready to Execute") } - shared_let(:executing_stage) { create(:stage, name: "Executing") } - shared_let(:ready_to_close_gate) { create(:gate, name: "Ready to Close") } - shared_let(:closing_stage) { create(:stage, name: "Closing") } + shared_let(:initiating_stage) { create(:project_stage_definition, name: "Initiating") } + shared_let(:ready_to_execute_gate) { create(:project_gate_definition, name: "Ready to Execute") } + shared_let(:executing_stage) { create(:project_stage_definition, name: "Executing") } + shared_let(:ready_to_close_gate) { create(:project_gate_definition, name: "Ready to Close") } + shared_let(:closing_stage) { create(:project_stage_definition, name: "Closing") } let(:project_lifecycle_page) { Pages::Projects::Settings::LifeCycle.new(project) } From cf176cc96f494b15fdda7512b4dd34861ce22037 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 22 Nov 2024 15:10:54 +0100 Subject: [PATCH 12/44] apply feature flag guard --- .../settings/life_cycles_controller.rb | 6 ++++ config/initializers/menus.rb | 28 +++++++++---------- config/initializers/permissions.rb | 3 +- .../life_cycle/active_in_project_spec.rb | 2 +- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/app/controllers/projects/settings/life_cycles_controller.rb b/app/controllers/projects/settings/life_cycles_controller.rb index f29132e73547..6058e50115ba 100644 --- a/app/controllers/projects/settings/life_cycles_controller.rb +++ b/app/controllers/projects/settings/life_cycles_controller.rb @@ -29,6 +29,8 @@ class Projects::Settings::LifeCyclesController < Projects::SettingsController include OpTurbo::ComponentStream + before_action :deny_access_on_feature_flag + before_action :load_life_cycle_elements, only: %i[show] before_action :load_or_create_life_cycle_element, only: %i[toggle] @@ -60,4 +62,8 @@ def load_or_create_life_cycle_element @life_cycle_element = klass.find_or_create_by(element_params) end + + def deny_access_on_feature_flag + deny_access unless OpenProject::FeatureDecisions.stages_and_gates_active? + end end diff --git a/config/initializers/menus.rb b/config/initializers/menus.rb index 898635e9c435..6131ab47b6e3 100644 --- a/config/initializers/menus.rb +++ b/config/initializers/menus.rb @@ -624,24 +624,24 @@ allow_deeplink: true project_menu_items = { - general: :label_information_plural, - life_cycles: :label_life_cycle_plural, - project_custom_fields: :label_project_attributes_plural, - modules: :label_module_plural, - types: :label_work_package_types, - custom_fields: :label_custom_field_plural, - versions: :label_version_plural, - categories: :label_work_package_category_plural, - repository: :label_repository, - time_entry_activities: :enumeration_activities, - storage: :label_required_disk_storage + general: { caption: :label_information_plural }, + life_cycles: { caption: :label_life_cycle_plural, if: ->(*) { OpenProject::FeatureDecisions.stages_and_gates_active? } }, + project_custom_fields: { caption: :label_project_attributes_plural }, + modules: { caption: :label_module_plural }, + types: { caption: :label_work_package_types }, + custom_fields: { caption: :label_custom_field_plural }, + versions: { caption: :label_version_plural }, + categories: { caption: :label_work_package_category_plural }, + repository: { caption: :label_repository }, + time_entry_activities: { caption: :enumeration_activities }, + storage: { caption: :label_required_disk_storage } } - project_menu_items.each do |key, caption| + project_menu_items.each do |key, options| menu.push :"settings_#{key}", { controller: "/projects/settings/#{key}", action: "show" }, - caption:, - parent: :settings + parent: :settings, + **options end end diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index 4151672327f1..e3d39e40d84e 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -138,7 +138,8 @@ "projects/settings/life_cycles": %i[show toggle] }, permissible_on: :project, - require: :member + require: :member, + visible: -> { OpenProject::FeatureDecisions.stages_and_gates_active? } map.permission :manage_members, { diff --git a/spec/features/projects/life_cycle/active_in_project_spec.rb b/spec/features/projects/life_cycle/active_in_project_spec.rb index 88f108880f19..66078bddd148 100644 --- a/spec/features/projects/life_cycle/active_in_project_spec.rb +++ b/spec/features/projects/life_cycle/active_in_project_spec.rb @@ -28,7 +28,7 @@ require "spec_helper" -RSpec.describe "Projects life cycle settings", :js, :with_cuprite do +RSpec.describe "Projects life cycle settings", :js, :with_cuprite, with_flag: { stages_and_gates: true } do shared_let(:project) { create(:project) } shared_let(:user_with_permission) do From 866b739cff34b8393bc3c1836f423bc4aca1d693 Mon Sep 17 00:00:00 2001 From: ulferts Date: Fri, 22 Nov 2024 17:13:00 +0100 Subject: [PATCH 13/44] split project setting spec support pages --- .../spec/features/resolved_status_spec.rb | 5 +- .../pages/projects/settings/backlogs.rb | 49 ++++++++++ .../custom_fields/activate_in_project_spec.rb | 8 +- spec/features/projects/copy_spec.rb | 35 +++---- spec/features/projects/modules_spec.rb | 7 +- .../types/activate_in_project_spec.rb | 8 +- .../features/types/form_configuration_spec.rb | 6 +- spec/support/pages/projects/settings.rb | 91 ------------------- .../pages/projects/settings/general.rb | 49 ++++++++++ .../pages/projects/settings/life_cycle.rb | 3 +- .../pages/projects/settings/modules.rb | 49 ++++++++++ spec/support/pages/projects/settings/type.rb | 66 ++++++++++++++ .../settings/work_package_custom_fields.rb | 70 ++++++++++++++ 13 files changed, 317 insertions(+), 129 deletions(-) create mode 100644 modules/backlogs/spec/support/pages/projects/settings/backlogs.rb delete mode 100644 spec/support/pages/projects/settings.rb create mode 100644 spec/support/pages/projects/settings/general.rb create mode 100644 spec/support/pages/projects/settings/modules.rb create mode 100644 spec/support/pages/projects/settings/type.rb create mode 100644 spec/support/pages/projects/settings/work_package_custom_fields.rb diff --git a/modules/backlogs/spec/features/resolved_status_spec.rb b/modules/backlogs/spec/features/resolved_status_spec.rb index ce72102dc5ed..3e094570a960 100644 --- a/modules/backlogs/spec/features/resolved_status_spec.rb +++ b/modules/backlogs/spec/features/resolved_status_spec.rb @@ -27,6 +27,7 @@ #++ require "spec_helper" +require_relative "../support/pages/projects/settings/backlogs" RSpec.describe "Resolved status" do let!(:project) do @@ -42,14 +43,14 @@ create(:user, member_with_roles: { project => role }) end - let(:settings_page) { Pages::Projects::Settings.new(project) } + let(:settings_page) { Pages::Projects::Settings::Backlogs.new(project) } before do login_as current_user end it "allows setting a status as done although it is not closed" do - settings_page.visit_tab! "backlogs" + settings_page.visit! check status.name click_button "Save" diff --git a/modules/backlogs/spec/support/pages/projects/settings/backlogs.rb b/modules/backlogs/spec/support/pages/projects/settings/backlogs.rb new file mode 100644 index 000000000000..86f573bd7667 --- /dev/null +++ b/modules/backlogs/spec/support/pages/projects/settings/backlogs.rb @@ -0,0 +1,49 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 "support/pages/page" + +module Pages + module Projects + module Settings + class Backlogs < Pages::Page + attr_accessor :project + + def initialize(project) + super() + + self.project = project + end + + def path + "/projects/#{project.identifier}/settings/backlogs" + end + end + end + end +end diff --git a/spec/features/custom_fields/activate_in_project_spec.rb b/spec/features/custom_fields/activate_in_project_spec.rb index 796b0eef3b81..ea6fc23de27a 100644 --- a/spec/features/custom_fields/activate_in_project_spec.rb +++ b/spec/features/custom_fields/activate_in_project_spec.rb @@ -35,13 +35,13 @@ let(:for_all_cf) { create(:list_wp_custom_field, is_for_all: true) } let(:project_specific_cf) { create(:integer_wp_custom_field) } let(:work_package) do - wp = build(:work_package).tap do |wp| + build(:work_package) do |wp| wp.type.custom_fields = [for_all_cf, project_specific_cf] wp.save! end end let(:wp_page) { Pages::FullWorkPackage.new(work_package) } - let(:project_settings_page) { Pages::Projects::Settings.new(work_package.project) } + let(:project_settings_page) { Pages::Projects::Settings::WorkPackageCustomFields.new(work_package.project) } before do login_as user @@ -53,9 +53,9 @@ wp_page.expect_attributes "customField#{for_all_cf.id}": "-" wp_page.expect_no_attribute "customField#{project_specific_cf.id}" - project_settings_page.visit_tab!("custom_fields") + project_settings_page.visit! - project_settings_page.activate_wp_custom_field(project_specific_cf) + project_settings_page.activate(project_specific_cf) project_settings_page.save! diff --git a/spec/features/projects/copy_spec.rb b/spec/features/projects/copy_spec.rb index 18253e9452a8..03be48add18b 100644 --- a/spec/features/projects/copy_spec.rb +++ b/spec/features/projects/copy_spec.rb @@ -164,8 +164,7 @@ context "with correct project custom field activations" do before do - original_settings_page = Pages::Projects::Settings.new(project) - original_settings_page.visit! + Pages::Projects::Settings::General.new(project).visit! find(".toolbar a", text: "Copy").click @@ -229,8 +228,7 @@ optional_project_custom_field_with_default.id ) - original_settings_page = Pages::Projects::Settings.new(project) - original_settings_page.visit! + Pages::Projects::Settings::General.new(project).visit! find(".toolbar a", text: "Copy").click @@ -302,8 +300,7 @@ end before do - original_settings_page = Pages::Projects::Settings.new(project) - original_settings_page.visit! + Pages::Projects::Settings::General.new(project).visit! find(".toolbar a", text: "Copy").click @@ -373,8 +370,7 @@ end it "copies the project attributes" do - original_settings_page = Pages::Projects::Settings.new(project) - original_settings_page.visit! + Pages::Projects::Settings::General.new(project).visit! find(".toolbar a", text: "Copy").click @@ -406,8 +402,7 @@ end it "copies projects and the associated objects" do - original_settings_page = Pages::Projects::Settings.new(project) - original_settings_page.visit! + Pages::Projects::Settings::General.new(project).visit! find(".toolbar a", text: "Copy").click @@ -433,8 +428,7 @@ # Will redirect to the new project automatically once the copy process is done expect(page).to have_current_path(Regexp.new("#{project_path(copied_project)}/?")) - copied_settings_page = Pages::Projects::Settings.new(copied_project) - copied_settings_page.visit! + Pages::Projects::Settings::General.new(copied_project).visit! # has the parent of the original project parent_field.expect_selected parent_project.name @@ -445,19 +439,21 @@ editor.expect_value "some text cf" # has wp custom fields of original project active - copied_settings_page.visit_tab!("custom_fields") + copied_settings_wp_cf_page = Pages::Projects::Settings::WorkPackageCustomFields.new(copied_project) + copied_settings_wp_cf_page.visit! - copied_settings_page.expect_wp_custom_field_active(wp_custom_field) - copied_settings_page.expect_wp_custom_field_inactive(inactive_wp_custom_field) + copied_settings_wp_cf_page.expect_active(wp_custom_field) + copied_settings_wp_cf_page.expect_inactive(inactive_wp_custom_field) # has types of original project active - copied_settings_page.visit_tab!("types") + copied_settings_type_page = Pages::Projects::Settings::Type.new(copied_project) + copied_settings_type_page.visit! active_types.each do |type| - copied_settings_page.expect_type_active(type) + copied_settings_type_page.expect_type_active(type) end - copied_settings_page.expect_type_inactive(inactive_type) + copied_settings_type_page.expect_type_inactive(inactive_type) # Expect wiki was copied expect(copied_project.wiki.pages.count).to eq(project.wiki.pages.count) @@ -540,8 +536,7 @@ wp_table.expect_work_package_listed *order wp_table.expect_work_package_order *order - original_settings_page = Pages::Projects::Settings.new(project) - original_settings_page.visit! + Pages::Projects::Settings::General.new(project).visit! find(".toolbar a", text: "Copy").click diff --git a/spec/features/projects/modules_spec.rb b/spec/features/projects/modules_spec.rb index 045ab880ed6a..69ea63ff35b8 100644 --- a/spec/features/projects/modules_spec.rb +++ b/spec/features/projects/modules_spec.rb @@ -34,7 +34,7 @@ end let(:permissions) { %i(edit_project select_project_modules view_work_packages) } - let(:settings_page) { Pages::Projects::Settings.new(project) } + let(:modules_settings_page) { Pages::Projects::Settings::Modules.new(project) } current_user do create(:user, member_with_permissions: { project => permissions }) @@ -43,7 +43,7 @@ it "allows adding and removing modules" do project_work_packages_menu_link_selector = '//ul[contains(@class, "menu_root")]//span[text()="Work packages"]' - settings_page.visit_tab!("modules") + modules_settings_page.visit! expect(page).to have_unchecked_field "Activity" expect(page).to have_unchecked_field "Calendar" @@ -95,10 +95,11 @@ create(:user, member_with_permissions: { project => %i(edit_project) }) end + let(:general_settings_page) { Pages::Projects::Settings::General.new(project) } before do login_as user_without_permission - settings_page.visit_tab!("general") + general_settings_page.visit! end it "I can't see the modules menu item" do diff --git a/spec/features/types/activate_in_project_spec.rb b/spec/features/types/activate_in_project_spec.rb index 4f0f1e67042b..68a484b591ce 100644 --- a/spec/features/types/activate_in_project_spec.rb +++ b/spec/features/types/activate_in_project_spec.rb @@ -37,7 +37,7 @@ let!(:active_type) { create(:type) } let!(:type) { create(:type) } let!(:project) { create(:project, types: [active_type]) } - let(:project_settings_page) { Pages::Projects::Settings.new(project) } + let(:project_type_settings_page) { Pages::Projects::Settings::Type.new(project) } let(:work_packages_page) { Pages::WorkPackagesTable.new(project) } before do @@ -51,7 +51,7 @@ work_packages_page.expect_type_available_for_create(active_type) work_packages_page.expect_type_not_available_for_create(type) - project_settings_page.visit_tab!("types") + project_type_settings_page.visit! expect(page) .to have_unchecked_field(type.name) @@ -62,9 +62,9 @@ check(type.name) uncheck(active_type.name) - project_settings_page.save! + project_type_settings_page.save! - project_settings_page.expect_and_dismiss_flash(message: "Successful update.") + project_type_settings_page.expect_and_dismiss_flash(message: "Successful update.") expect(page) .to have_checked_field(type.name) diff --git a/spec/features/types/form_configuration_spec.rb b/spec/features/types/form_configuration_spec.rb index 0457df023ee0..0376c310c078 100644 --- a/spec/features/types/form_configuration_spec.rb +++ b/spec/features/types/form_configuration_spec.rb @@ -274,7 +274,7 @@ end describe "custom fields" do - let(:project_settings_page) { Pages::Projects::Settings.new(project) } + let(:project_cf_settings_page) { Pages::Projects::Settings::WorkPackageCustomFields.new(project) } let(:custom_fields) { [custom_field] } let(:custom_field) { create(:issue_custom_field, :integer, name: "MyNumber") } @@ -315,7 +315,7 @@ def add_cf_to_group wp_page.expect_attribute_hidden(cf_identifier_api) # Enable in project, should then be visible - project_settings_page.visit_tab!("custom_fields") + project_cf_settings_page.visit! expect(page).to have_css(".custom-field-#{custom_field.id} td", text: "MyNumber") expect(page).to have_css(".custom-field-#{custom_field.id} td", text: type.name) @@ -356,7 +356,7 @@ def add_cf_to_group end # Ensure CF is checked - project_settings_page.visit_tab!("custom_fields") + project_cf_settings_page.visit! expect(page).to have_css(".custom-field-#{custom_field.id} td", text: "MyNumber") expect(page).to have_css(".custom-field-#{custom_field.id} td", text: type.name) expect(page).to have_css("#project_work_package_custom_field_ids_#{custom_field.id}[checked]") diff --git a/spec/support/pages/projects/settings.rb b/spec/support/pages/projects/settings.rb deleted file mode 100644 index 7b698f23e05e..000000000000 --- a/spec/support/pages/projects/settings.rb +++ /dev/null @@ -1,91 +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. -#++ - -require "support/pages/page" - -module Pages - module Projects - class Settings < Pages::Page - attr_accessor :project - - def initialize(project) - super() - - self.project = project - end - - def visit_tab!(name) - visit "/projects/#{project.identifier}/settings/#{name}" - end - - def expect_type_active(type) - expect_type(type, true) - end - - def expect_type_inactive(type) - expect_type(type, false) - end - - def expect_type(type, active = true) - expect(page) - .to have_field("project_planning_element_type_ids_#{type.id}", checked: active) - end - - def expect_wp_custom_field_active(custom_field) - expect_wp_custom_field(custom_field, true) - end - - def expect_wp_custom_field_inactive(custom_field) - expect_wp_custom_field(custom_field, false) - end - - def activate_wp_custom_field(custom_field) - check custom_field.name - end - - def save! - click_button "Save" - end - - def expect_wp_custom_field(custom_field, active = true) - expect(page) - .to have_field(custom_field.name, checked: active) - end - - def fieldset_label - find "fieldset#project_issue_custom_fields label" - end - - private - - def path - project_settings_general_path(project) - end - end - end -end diff --git a/spec/support/pages/projects/settings/general.rb b/spec/support/pages/projects/settings/general.rb new file mode 100644 index 000000000000..eb515944db4b --- /dev/null +++ b/spec/support/pages/projects/settings/general.rb @@ -0,0 +1,49 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 "support/pages/page" + +module Pages + module Projects + module Settings + class General < Pages::Page + attr_accessor :project + + def initialize(project) + super() + + self.project = project + end + + def path + "/projects/#{project.identifier}/settings/general" + end + end + end + end +end diff --git a/spec/support/pages/projects/settings/life_cycle.rb b/spec/support/pages/projects/settings/life_cycle.rb index 07ddb2544d57..7a4a2215d6b2 100644 --- a/spec/support/pages/projects/settings/life_cycle.rb +++ b/spec/support/pages/projects/settings/life_cycle.rb @@ -30,8 +30,7 @@ module Pages module Projects - # TODO: Ideally this would be turned into a module but it would require having the other settings pages being split up. - class Settings + module Settings class LifeCycle < Pages::Page attr_accessor :project diff --git a/spec/support/pages/projects/settings/modules.rb b/spec/support/pages/projects/settings/modules.rb new file mode 100644 index 000000000000..f97681f7901f --- /dev/null +++ b/spec/support/pages/projects/settings/modules.rb @@ -0,0 +1,49 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 "support/pages/page" + +module Pages + module Projects + module Settings + class Modules < Pages::Page + attr_accessor :project + + def initialize(project) + super() + + self.project = project + end + + def path + "/projects/#{project.identifier}/settings/modules" + end + end + end + end +end diff --git a/spec/support/pages/projects/settings/type.rb b/spec/support/pages/projects/settings/type.rb new file mode 100644 index 000000000000..de34fe8566ac --- /dev/null +++ b/spec/support/pages/projects/settings/type.rb @@ -0,0 +1,66 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 "support/pages/page" + +module Pages + module Projects + module Settings + class Type < Pages::Page + attr_accessor :project + + def initialize(project) + super() + + self.project = project + end + + def path + "/projects/#{project.identifier}/settings/types" + end + + def expect_type_active(type) + expect_type(type, active: true) + end + + def expect_type_inactive(type) + expect_type(type, active: false) + end + + def expect_type(type, active: true) + expect(page) + .to have_field("project_planning_element_type_ids_#{type.id}", checked: active) + end + + def save! + click_link_or_button "Save" + end + end + end + end +end diff --git a/spec/support/pages/projects/settings/work_package_custom_fields.rb b/spec/support/pages/projects/settings/work_package_custom_fields.rb new file mode 100644 index 000000000000..4b80d0cfe0bf --- /dev/null +++ b/spec/support/pages/projects/settings/work_package_custom_fields.rb @@ -0,0 +1,70 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 "support/pages/page" + +module Pages + module Projects + module Settings + class WorkPackageCustomFields < Pages::Page + attr_accessor :project + + def initialize(project) + super() + + self.project = project + end + + def path + "/projects/#{project.identifier}/settings/custom_fields" + end + + def expect_active(custom_field) + expect_field(custom_field, active: true) + end + + def expect_inactive(custom_field) + expect_field(custom_field, active: false) + end + + def activate(custom_field) + check custom_field.name + end + + def expect_field(custom_field, active: true) + expect(page) + .to have_field(custom_field.name, checked: active) + end + + def save! + click_link_or_button "Save" + end + end + end + end +end From 801d33ffa3cd8cbce44858ff2330c4a8f0f3bde4 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 25 Nov 2024 17:42:39 +0100 Subject: [PATCH 14/44] add bulk actions for project life cycle admin --- .../life_cycles/index_component.html.erb | 30 +++++++++ .../show_component.html.erb | 8 +-- .../settings/life_cycles_controller.rb | 48 +++++++++++--- config/initializers/permissions.rb | 2 +- config/locales/en.yml | 5 +- config/routes.rb | 4 ++ ...nique_index_on_project_life_cycle_steps.rb | 8 +++ .../life_cycle/active_in_project_spec.rb | 62 ++++++++++++++----- .../pages/projects/settings/life_cycle.rb | 8 +++ 9 files changed, 143 insertions(+), 32 deletions(-) create mode 100644 db/migrate/20241125161226_unique_index_on_project_life_cycle_steps.rb diff --git a/app/components/projects/settings/life_cycles/index_component.html.erb b/app/components/projects/settings/life_cycles/index_component.html.erb index c0ad7866949f..0bd1eebb9413 100644 --- a/app/components/projects/settings/life_cycles/index_component.html.erb +++ b/app/components/projects/settings/life_cycles/index_component.html.erb @@ -10,6 +10,36 @@ I18n.t('projects.settings.life_cycle.section_header') end end + header_container.with_column(flex_layout: true, justify_content: :flex_end) do |actions_container| + actions_container.with_column(data: { 'projects--settings--project-custom-fields-mapping-filter-target': 'bulkActionContainer' }) do + render(Primer::Beta::Button.new( + tag: :a, + href: enable_all_project_settings_life_cycle_path(project_id: project), + scheme: :invisible, + font_weight: :bold, + color: :subtle, + 'aria-label': t('projects.settings.actions.label_enable_all'), + data: { 'turbo-method': :post, 'turbo': true, test_selector: "enable-all-life-cycle-elements" } + )) do |button| + button.with_leading_visual_icon(icon: 'check-circle', color: :subtle) + t('projects.settings.actions.label_enable_all') + end + end + actions_container.with_column(data: { 'projects--settings--project-custom-fields-mapping-filter-target': 'bulkActionContainer' }) do + render(Primer::Beta::Button.new( + tag: :a, + href: disable_all_project_settings_life_cycle_path(project_id: project), + scheme: :invisible, + font_weight: :bold, + color: :subtle, + 'aria-label': t('projects.settings.actions.label_disable_all'), + data: { 'turbo-method': :post, 'turbo': true, test_selector: "disable-all-life-cycle-elements" } + )) do |button| + button.with_leading_visual_icon(icon: 'x-circle', color: :subtle) + t('projects.settings.actions.label_disable_all') + end + end + end end end if life_cycle_elements.empty? diff --git a/app/components/projects/settings/project_custom_field_sections/show_component.html.erb b/app/components/projects/settings/project_custom_field_sections/show_component.html.erb index 727d8901228b..66b42dfbcbb1 100644 --- a/app/components/projects/settings/project_custom_field_sections/show_component.html.erb +++ b/app/components/projects/settings/project_custom_field_sections/show_component.html.erb @@ -25,11 +25,11 @@ scheme: :invisible, font_weight: :bold, color: :subtle, - 'aria-label': t('projects.settings.project_custom_fields.actions.label_enable_all'), + 'aria-label': t('projects.settings.actions.label_enable_all'), data: { 'turbo-method': :put, 'turbo-stream': true, test_selector: "enable-all-project-custom-field-mappings-#{@project_custom_field_section.id}" } )) do |button| button.with_leading_visual_icon(icon: 'check-circle', color: :subtle) - t('projects.settings.project_custom_fields.actions.label_enable_all') + t('projects.settings.actions.label_enable_all') end end actions_container.with_column(data: { 'projects--settings--project-custom-fields-mapping-filter-target': 'bulkActionContainer' }) do @@ -44,11 +44,11 @@ scheme: :invisible, font_weight: :bold, color: :subtle, - 'aria-label': t('projects.settings.project_custom_fields.actions.label_disable_all'), + 'aria-label': t('projects.settings.actions.label_disable_all'), data: { 'turbo-method': :put, 'turbo-stream': true, test_selector: "disable-all-project-custom-field-mappings-#{@project_custom_field_section.id}" } )) do |button| button.with_leading_visual_icon(icon: 'x-circle', color: :subtle) - t('projects.settings.project_custom_fields.actions.label_disable_all') + t('projects.settings.actions.label_disable_all') end end end diff --git a/app/controllers/projects/settings/life_cycles_controller.rb b/app/controllers/projects/settings/life_cycles_controller.rb index 6058e50115ba..ff609a753b72 100644 --- a/app/controllers/projects/settings/life_cycles_controller.rb +++ b/app/controllers/projects/settings/life_cycles_controller.rb @@ -31,7 +31,7 @@ class Projects::Settings::LifeCyclesController < Projects::SettingsController before_action :deny_access_on_feature_flag - before_action :load_life_cycle_elements, only: %i[show] + before_action :load_life_cycle_elements, only: %i[show enable_all disable_all] before_action :load_or_create_life_cycle_element, only: %i[toggle] menu_item :settings_life_cycles @@ -42,6 +42,18 @@ def toggle @life_cycle_element.toggle!(:active) end + def disable_all + upsert_all(active: false) + + redirect_to action: :show + end + + def enable_all + upsert_all(active: true) + + redirect_to action: :show + end + private def load_life_cycle_elements @@ -51,14 +63,7 @@ def load_life_cycle_elements def load_or_create_life_cycle_element element_params = params.require(:project_life_cycle).permit(:definition_id, :project_id, :type) - klass = case element_params.delete(:type) - when Project::StageDefinition.name - Project::Stage - when Project::GateDefinition.name - Project::Gate - else - raise NotImplementedError, "Unknown life cycle element type" - end + klass = project_type_for_definition_type(element_params.delete(:type)) @life_cycle_element = klass.find_or_create_by(element_params) end @@ -66,4 +71,29 @@ def load_or_create_life_cycle_element def deny_access_on_feature_flag deny_access unless OpenProject::FeatureDecisions.stages_and_gates_active? end + + def upsert_all(active: true) + Project::LifeCycleStep.upsert_all( + @life_cycle_elements.map do |element| + { + project_id: @project.id, + definition_id: element.id, + active:, + type: project_type_for_definition_type(element.type) + } + end, + unique_by: %i[project_id definition_id] + ) + end + + def project_type_for_definition_type(definition_type) + case definition_type + when Project::StageDefinition.name + Project::Stage + when Project::GateDefinition.name + Project::Gate + else + raise NotImplementedError, "Unknown life cycle element type" + end + end end diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index e3d39e40d84e..89baf66fdd95 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -135,7 +135,7 @@ map.permission :select_project_life_cycle, { - "projects/settings/life_cycles": %i[show toggle] + "projects/settings/life_cycles": %i[show toggle enable_all disable_all] }, permissible_on: :project, require: :member, diff --git a/config/locales/en.yml b/config/locales/en.yml index 3defcab38ed3..9e6be75fcfde 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -378,6 +378,9 @@ en: text: "This action will not delete any project the list contains. Are you sure you want to delete this project list?" settings: change_identifier: Change identifier + actions: + label_enable_all: "Enable all" + label_disable_all: "Disable all" activities: no_results_title_text: There are currently no activities available. forums: @@ -408,8 +411,6 @@ en: label_enable_single: "Active in this project, click to disable" label_disable_single: "Inactive in this project, click to enable" remove_from_project: "Remove from project" - label_enable_all: "Enable all" - label_disable_all: "Disable all" is_required_blank_slate: heading: Required in all projects description: This project attribute is activated in all projects since the "Required in all projects" option is checked. It cannot be deactivated for individual projects. diff --git a/config/routes.rb b/config/routes.rb index 6ac9d0feb869..28810f171289 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -248,6 +248,10 @@ member do post :toggle end + collection do + post :enable_all + post :disable_all + end end resource :custom_fields, only: %i[show update] resource :repository, only: %i[show], controller: "repository" diff --git a/db/migrate/20241125161226_unique_index_on_project_life_cycle_steps.rb b/db/migrate/20241125161226_unique_index_on_project_life_cycle_steps.rb new file mode 100644 index 000000000000..517b183985ce --- /dev/null +++ b/db/migrate/20241125161226_unique_index_on_project_life_cycle_steps.rb @@ -0,0 +1,8 @@ +class UniqueIndexOnProjectLifeCycleSteps < ActiveRecord::Migration[7.1] + def change + add_index :project_life_cycle_steps, + %i[project_id definition_id], + unique: true, + name: "index_project_life_cycle_steps_on_project_id_and_definition_id" + end +end diff --git a/spec/features/projects/life_cycle/active_in_project_spec.rb b/spec/features/projects/life_cycle/active_in_project_spec.rb index 66078bddd148..d8e45c6df297 100644 --- a/spec/features/projects/life_cycle/active_in_project_spec.rb +++ b/spec/features/projects/life_cycle/active_in_project_spec.rb @@ -46,36 +46,66 @@ shared_let(:ready_to_close_gate) { create(:project_gate_definition, name: "Ready to Close") } shared_let(:closing_stage) { create(:project_stage_definition, name: "Closing") } - let(:project_lifecycle_page) { Pages::Projects::Settings::LifeCycle.new(project) } + let(:project_life_cycle_page) { Pages::Projects::Settings::LifeCycle.new(project) } context "with sufficient permissions" do current_user { user_with_permission } it "allows toggling the active/inactive state of lifecycle elements" do - project_lifecycle_page.visit! + project_life_cycle_page.visit! - project_lifecycle_page.expect_listed(initiating_stage => false, - ready_to_execute_gate => false, - executing_stage => false, - ready_to_close_gate => false, - closing_stage => false) + project_life_cycle_page.expect_listed(initiating_stage => false, + ready_to_execute_gate => false, + executing_stage => false, + ready_to_close_gate => false, + closing_stage => false) # Activate the stages to be found within the project - project_lifecycle_page.toggle(initiating_stage) - project_lifecycle_page.toggle(ready_to_close_gate) - project_lifecycle_page.toggle(closing_stage) + project_life_cycle_page.toggle(initiating_stage) + project_life_cycle_page.toggle(ready_to_close_gate) + project_life_cycle_page.toggle(closing_stage) wait_for_network_idle # Expect the activation state to be kept after a reload visit home_path - project_lifecycle_page.visit! + project_life_cycle_page.visit! - project_lifecycle_page.expect_listed(initiating_stage => true, - ready_to_execute_gate => false, - executing_stage => false, - ready_to_close_gate => true, - closing_stage => true) + project_life_cycle_page.expect_listed(initiating_stage => true, + ready_to_execute_gate => false, + executing_stage => false, + ready_to_close_gate => true, + closing_stage => true) + + # Disable all stages at once + project_life_cycle_page.disable_all + + wait_for_network_idle + + # Expect the activation state to be kept after a reload + visit home_path + project_life_cycle_page.visit! + + project_life_cycle_page.expect_listed(initiating_stage => false, + ready_to_execute_gate => false, + executing_stage => false, + ready_to_close_gate => false, + closing_stage => false) + + # Enable all stages at once + project_life_cycle_page.enable_all + + wait_for_network_idle + + # Expect the activation state to be kept after a reload + visit home_path + project_life_cycle_page.visit! + + project_life_cycle_page.expect_listed(initiating_stage => true, + ready_to_execute_gate => true, + executing_stage => true, + ready_to_close_gate => true, + closing_stage => true) end end end diff --git a/spec/support/pages/projects/settings/life_cycle.rb b/spec/support/pages/projects/settings/life_cycle.rb index 7a4a2215d6b2..4d076709caf6 100644 --- a/spec/support/pages/projects/settings/life_cycle.rb +++ b/spec/support/pages/projects/settings/life_cycle.rb @@ -71,6 +71,14 @@ def toggle(element) toggle_element(element).click end + def disable_all + find_test_selector("disable-all-life-cycle-elements").click + end + + def enable_all + find_test_selector("enable-all-life-cycle-elements").click + end + def life_cycle_test_selector(element) test_selector("project-life-cycle-element-#{element.id}") end From 45b060d17269bcde25e0ff4518fbaf9ae7ff73fa Mon Sep 17 00:00:00 2001 From: ulferts Date: Tue, 26 Nov 2024 11:15:24 +0100 Subject: [PATCH 15/44] turn toggling into upsert --- .../life_cycles/element_component.html.erb | 1 - .../settings/life_cycles_controller.rb | 45 ++++++++++++------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/app/components/projects/settings/life_cycles/element_component.html.erb b/app/components/projects/settings/life_cycles/element_component.html.erb index c582fb0dd6af..7692b0d49df5 100644 --- a/app/components/projects/settings/life_cycles/element_component.html.erb +++ b/app/components/projects/settings/life_cycles/element_component.html.erb @@ -19,7 +19,6 @@ render(Primer::Alpha::ToggleSwitch.new( src: toggle_project_settings_life_cycle_path( project_life_cycle: { - project_id: project.id, definition_id: element.id, type: element.type } diff --git a/app/controllers/projects/settings/life_cycles_controller.rb b/app/controllers/projects/settings/life_cycles_controller.rb index ff609a753b72..3c89293e6592 100644 --- a/app/controllers/projects/settings/life_cycles_controller.rb +++ b/app/controllers/projects/settings/life_cycles_controller.rb @@ -32,24 +32,30 @@ class Projects::Settings::LifeCyclesController < Projects::SettingsController before_action :deny_access_on_feature_flag before_action :load_life_cycle_elements, only: %i[show enable_all disable_all] - before_action :load_or_create_life_cycle_element, only: %i[toggle] menu_item :settings_life_cycles def show; end def toggle - @life_cycle_element.toggle!(:active) + element_params = params + .require(:project_life_cycle) + .permit(:definition_id, :type) + .to_h + .symbolize_keys + .merge(active: params["value"] == "1") + + upsert_one_step(**element_params) end def disable_all - upsert_all(active: false) + upsert_all_steps(active: false) redirect_to action: :show end def enable_all - upsert_all(active: true) + upsert_all_steps(active: true) redirect_to action: :show end @@ -60,20 +66,23 @@ def load_life_cycle_elements @life_cycle_elements = Project::LifeCycleStepDefinition.all end - def load_or_create_life_cycle_element - element_params = params.require(:project_life_cycle).permit(:definition_id, :project_id, :type) - - klass = project_type_for_definition_type(element_params.delete(:type)) - - @life_cycle_element = klass.find_or_create_by(element_params) - end - def deny_access_on_feature_flag deny_access unless OpenProject::FeatureDecisions.stages_and_gates_active? end - def upsert_all(active: true) - Project::LifeCycleStep.upsert_all( + def upsert_one_step(definition_id:, type:, active: true) + upsert_all( + [{ + project_id: @project.id, + definition_id:, + active:, + type: project_type_for_definition_type(type) + }] + ) + end + + def upsert_all_steps(active: true) + upsert_all( @life_cycle_elements.map do |element| { project_id: @project.id, @@ -81,7 +90,13 @@ def upsert_all(active: true) active:, type: project_type_for_definition_type(element.type) } - end, + end + ) + end + + def upsert_all(upserted_steps) + Project::LifeCycleStep.upsert_all( + upserted_steps, unique_by: %i[project_id definition_id] ) end From 24a627db227342c96b021ab75bae9579ad399472 Mon Sep 17 00:00:00 2001 From: ulferts Date: Tue, 26 Nov 2024 11:30:44 +0100 Subject: [PATCH 16/44] clarify variable names --- .../life_cycle_type_component.html.erb | 6 ++-- .../life_cycles/index_component.html.erb | 12 +++---- .../settings/life_cycles/index_component.rb | 6 ++-- ...onent.html.erb => step_component.html.erb} | 16 ++++----- ...element_component.rb => step_component.rb} | 14 ++++---- .../settings/life_cycles_controller.rb | 30 ++++++++-------- .../settings/life_cycles/show.html.erb | 2 +- config/locales/en.yml | 4 +-- .../life_cycle/active_in_project_spec.rb | 2 +- .../pages/projects/settings/life_cycle.rb | 36 +++++++++---------- 10 files changed, 64 insertions(+), 64 deletions(-) rename app/components/projects/settings/life_cycles/{element_component.html.erb => step_component.html.erb} (70%) rename app/components/projects/settings/life_cycles/{element_component.rb => step_component.rb} (82%) diff --git a/app/components/projects/life_cycle_type_component.html.erb b/app/components/projects/life_cycle_type_component.html.erb index 728082531b70..921c1cf191a2 100644 --- a/app/components/projects/life_cycle_type_component.html.erb +++ b/app/components/projects/life_cycle_type_component.html.erb @@ -1,8 +1,8 @@ -<%= flex_layout(align_items: :center) do |element_container| - element_container.with_column(mr: 1, classes: icon_color_class) do +<%= flex_layout(align_items: :center) do |type_container| + type_container.with_column(mr: 1, classes: icon_color_class) do render Primer::Beta::Octicon.new(icon: icon) end - element_container.with_column do + type_container.with_column do render(Primer::Beta::Text.new(**text_options)) { text } end end %> diff --git a/app/components/projects/settings/life_cycles/index_component.html.erb b/app/components/projects/settings/life_cycles/index_component.html.erb index 0bd1eebb9413..60feef7d20ef 100644 --- a/app/components/projects/settings/life_cycles/index_component.html.erb +++ b/app/components/projects/settings/life_cycles/index_component.html.erb @@ -19,7 +19,7 @@ font_weight: :bold, color: :subtle, 'aria-label': t('projects.settings.actions.label_enable_all'), - data: { 'turbo-method': :post, 'turbo': true, test_selector: "enable-all-life-cycle-elements" } + data: { 'turbo-method': :post, 'turbo': true, test_selector: "enable-all-life-cycle-steps" } )) do |button| button.with_leading_visual_icon(icon: 'check-circle', color: :subtle) t('projects.settings.actions.label_enable_all') @@ -33,7 +33,7 @@ font_weight: :bold, color: :subtle, 'aria-label': t('projects.settings.actions.label_disable_all'), - data: { 'turbo-method': :post, 'turbo': true, test_selector: "disable-all-life-cycle-elements" } + data: { 'turbo-method': :post, 'turbo': true, test_selector: "disable-all-life-cycle-steps" } )) do |button| button.with_leading_visual_icon(icon: 'x-circle', color: :subtle) t('projects.settings.actions.label_disable_all') @@ -42,15 +42,15 @@ end end end - if life_cycle_elements.empty? + if life_cycle_definitions.empty? component.with_row do render(Primer::Beta::Text.new(color: :subtle)) { t("projects.settings.life_cycle.non_defined") } end else - life_cycle_elements.each do |element| + life_cycle_definitions.each do |definition| component.with_row(data: { 'projects--settings--project-custom-fields-mapping-filter-target': 'searchItem' }, - test_selector: "project-life-cycle-element-#{element.id}") do - render(Projects::Settings::LifeCycles::ElementComponent.new(project:, element:)) + test_selector: "project-life-cycle-step-#{definition.id}") do + render(Projects::Settings::LifeCycles::StepComponent.new(project:, definition:)) end end end diff --git a/app/components/projects/settings/life_cycles/index_component.rb b/app/components/projects/settings/life_cycles/index_component.rb index 55261db5c059..059b750e1671 100644 --- a/app/components/projects/settings/life_cycles/index_component.rb +++ b/app/components/projects/settings/life_cycles/index_component.rb @@ -35,13 +35,13 @@ class IndexComponent < ApplicationComponent include OpTurbo::Streamable attr_reader :project, - :life_cycle_elements + :life_cycle_definitions - def initialize(project:, life_cycle_elements:) + def initialize(project:, life_cycle_definitions:) super @project = project - @life_cycle_elements = life_cycle_elements + @life_cycle_definitions = life_cycle_definitions end private diff --git a/app/components/projects/settings/life_cycles/element_component.html.erb b/app/components/projects/settings/life_cycles/step_component.html.erb similarity index 70% rename from app/components/projects/settings/life_cycles/element_component.html.erb rename to app/components/projects/settings/life_cycles/step_component.html.erb index 7692b0d49df5..0e11abc46daa 100644 --- a/app/components/projects/settings/life_cycles/element_component.html.erb +++ b/app/components/projects/settings/life_cycles/step_component.html.erb @@ -1,17 +1,17 @@ <%= component_wrapper do flex_layout(align_items: :center, - justify_content: :space_between) do |element_container| - element_container.with_column(flex_layout: true) do |title_container| + justify_content: :space_between) do |definition_container| + definition_container.with_column(flex_layout: true) do |title_container| title_container.with_column(pt: 1, mr: 3) do - render(Primer::Beta::Text.new) { element.name } + render(Primer::Beta::Text.new) { definition.name } end title_container.with_column(pt: 1) do - render(Projects::LifeCycleTypeComponent.new(element)) + render(Projects::LifeCycleTypeComponent.new(definition)) end end # py: 1 quick fix: prevents the row from bouncing as the toggle switch currently changes height while toggling - element_container.with_column(py: 1, mr: 2) do + definition_container.with_column(py: 1, mr: 2) do # buggy currently: # small variant + status_label_position: :start leads to a small bounce while toggling # behavior can be seen on primer's viewbook as well -> https://view-components-storybook.eastus.cloudapp.azure.com/view-components/lookbook/inspect/primer/alpha/toggle_switch/small @@ -19,12 +19,12 @@ render(Primer::Alpha::ToggleSwitch.new( src: toggle_project_settings_life_cycle_path( project_life_cycle: { - definition_id: element.id, - type: element.type + definition_id: definition.id, + type: definition.type } ), csrf_token: form_authenticity_token, - data: { test_selector: "toggle-project-life-cycle-#{element.id}" }, + data: { test_selector: "toggle-project-life-cycle-#{definition.id}" }, aria: { label: toggle_aria_label }, checked: active_in_project?, size: :small, diff --git a/app/components/projects/settings/life_cycles/element_component.rb b/app/components/projects/settings/life_cycles/step_component.rb similarity index 82% rename from app/components/projects/settings/life_cycles/element_component.rb rename to app/components/projects/settings/life_cycles/step_component.rb index 07bea6ba3d16..2642d5ab55ef 100644 --- a/app/components/projects/settings/life_cycles/element_component.rb +++ b/app/components/projects/settings/life_cycles/step_component.rb @@ -29,36 +29,36 @@ module Projects module Settings module LifeCycles - class ElementComponent < ApplicationComponent + class StepComponent < ApplicationComponent include ApplicationHelper include OpPrimer::ComponentHelpers include OpTurbo::Streamable - attr_reader :element, + attr_reader :definition, :project - def initialize(project:, element:) + def initialize(project:, definition:) super - @element = element + @definition = definition @project = project end def active_in_project? project .life_cycle_steps - .detect { |project_lc| project_lc.definition_id == element.id } + .detect { |project_lc| project_lc.definition_id == definition.id } &.active end def toggle_aria_label - I18n.t("projects.settings.life_cycle.element.use_in_project", element: element.name) + I18n.t("projects.settings.life_cycle.step.use_in_project", step: definition.name) end private def wrapper_uniq_by - element.id + definition.id end end end diff --git a/app/controllers/projects/settings/life_cycles_controller.rb b/app/controllers/projects/settings/life_cycles_controller.rb index 3c89293e6592..6ad0c52f583f 100644 --- a/app/controllers/projects/settings/life_cycles_controller.rb +++ b/app/controllers/projects/settings/life_cycles_controller.rb @@ -31,21 +31,21 @@ class Projects::Settings::LifeCyclesController < Projects::SettingsController before_action :deny_access_on_feature_flag - before_action :load_life_cycle_elements, only: %i[show enable_all disable_all] + before_action :load_life_cycle_definitions, only: %i[show enable_all disable_all] menu_item :settings_life_cycles def show; end def toggle - element_params = params - .require(:project_life_cycle) - .permit(:definition_id, :type) - .to_h - .symbolize_keys - .merge(active: params["value"] == "1") - - upsert_one_step(**element_params) + step_params = params + .require(:project_life_cycle) + .permit(:definition_id, :type) + .to_h + .symbolize_keys + .merge(active: params["value"] == "1") + + upsert_one_step(**step_params) end def disable_all @@ -62,8 +62,8 @@ def enable_all private - def load_life_cycle_elements - @life_cycle_elements = Project::LifeCycleStepDefinition.all + def load_life_cycle_definitions + @life_cycle_definitions = Project::LifeCycleStepDefinition.all end def deny_access_on_feature_flag @@ -83,12 +83,12 @@ def upsert_one_step(definition_id:, type:, active: true) def upsert_all_steps(active: true) upsert_all( - @life_cycle_elements.map do |element| + @life_cycle_definitions.map do |definition| { project_id: @project.id, - definition_id: element.id, + definition_id: definition.id, active:, - type: project_type_for_definition_type(element.type) + type: project_type_for_definition_type(definition.type) } end ) @@ -108,7 +108,7 @@ def project_type_for_definition_type(definition_type) when Project::GateDefinition.name Project::Gate else - raise NotImplementedError, "Unknown life cycle element type" + raise NotImplementedError, "Unknown life cycle definition type" end end end diff --git a/app/views/projects/settings/life_cycles/show.html.erb b/app/views/projects/settings/life_cycles/show.html.erb index b90027e4078a..a53367c545b8 100644 --- a/app/views/projects/settings/life_cycles/show.html.erb +++ b/app/views/projects/settings/life_cycles/show.html.erb @@ -29,5 +29,5 @@ See COPYRIGHT and LICENSE files for more details.
<%= render Projects::Settings::LifeCycles::ShowPageHeaderComponent.new(project: @project) %> - <%= render(Projects::Settings::LifeCycles::IndexComponent.new( project: @project, life_cycle_elements: @life_cycle_elements)) %> + <%= render(Projects::Settings::LifeCycles::IndexComponent.new( project: @project, life_cycle_definitions: @life_cycle_definitions)) %>
diff --git a/config/locales/en.yml b/config/locales/en.yml index 9e6be75fcfde..0f5983e71bc1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -397,8 +397,8 @@ en: description: 'These project stages and gates in the project lifecycle will be displayed in your project overview page and are defined in the administration settings by the administrator of the instance. You can enable or disable individual attributes.' non_defined: "Neither stages nor gates are currently defined." section_header: "Stages and gates" - element: - use_in_project: "Use %{element} in this project" + step: + use_in_project: "Use %{step} in this project" project_custom_fields: header: title: "Project attributes" diff --git a/spec/features/projects/life_cycle/active_in_project_spec.rb b/spec/features/projects/life_cycle/active_in_project_spec.rb index d8e45c6df297..a098d75e520c 100644 --- a/spec/features/projects/life_cycle/active_in_project_spec.rb +++ b/spec/features/projects/life_cycle/active_in_project_spec.rb @@ -51,7 +51,7 @@ context "with sufficient permissions" do current_user { user_with_permission } - it "allows toggling the active/inactive state of lifecycle elements" do + it "allows toggling the active/inactive state of lifecycle steps" do project_life_cycle_page.visit! project_life_cycle_page.expect_listed(initiating_stage => false, diff --git a/spec/support/pages/projects/settings/life_cycle.rb b/spec/support/pages/projects/settings/life_cycle.rb index 4d076709caf6..3f6572c123f1 100644 --- a/spec/support/pages/projects/settings/life_cycle.rb +++ b/spec/support/pages/projects/settings/life_cycle.rb @@ -44,47 +44,47 @@ def path "/projects/#{project.identifier}/settings/life_cycle" end - # Checks if the life cycle elements are listed in the order given and with the correct toggle state. - # @param life_cycle_elements [Hash{LifeCycleElement => Boolean}] - def expect_listed(**life_cycle_elements) - if life_cycle_elements.size > 1 - life_cycle_elements.each_cons(2) do |(predecessor, _), (successor, _)| + # Checks if the life cycle steps are listed in the order given and with the correct toggle state. + # @param life_cycle_definitions [Hash{LifeCycleElement => Boolean}] + def expect_listed(**life_cycle_definitions) + if life_cycle_definitions.size > 1 + life_cycle_definitions.each_cons(2) do |(predecessor, _), (successor, _)| expect(page).to have_css("#{life_cycle_test_selector(predecessor)} + #{life_cycle_test_selector(successor)}") end end - life_cycle_elements.each do |element, active| - expect_toggle_state(element, active) + life_cycle_definitions.each do |definition, active| + expect_toggle_state(definition, active) end end - def expect_toggle_state(element, active) - within toggle_element(element) do + def expect_toggle_state(definition, active) + within toggle_step(definition) do expect(page) .to have_css(".ToggleSwitch-status#{expected_toggle_status(active)}"), - "Expected toggle for '#{element.name}' to be #{expected_toggle_status(active)} " \ + "Expected toggle for '#{definition.name}' to be #{expected_toggle_status(active)} " \ "but was #{expected_toggle_status(!active)}" end end - def toggle(element) - toggle_element(element).click + def toggle(definition) + toggle_step(definition).click end def disable_all - find_test_selector("disable-all-life-cycle-elements").click + find_test_selector("disable-all-life-cycle-steps").click end def enable_all - find_test_selector("enable-all-life-cycle-elements").click + find_test_selector("enable-all-life-cycle-steps").click end - def life_cycle_test_selector(element) - test_selector("project-life-cycle-element-#{element.id}") + def life_cycle_test_selector(definition) + test_selector("project-life-cycle-step-#{definition.id}") end - def toggle_element(element) - find_test_selector("toggle-project-life-cycle-#{element.id}") + def toggle_step(definition) + find_test_selector("toggle-project-life-cycle-#{definition.id}") end def expected_toggle_status(active) From 6771911693d3d57be349fb12ed851281b30b4536 Mon Sep 17 00:00:00 2001 From: ulferts Date: Wed, 27 Nov 2024 16:02:47 +0100 Subject: [PATCH 17/44] order definitions by position --- app/controllers/projects/settings/life_cycles_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/settings/life_cycles_controller.rb b/app/controllers/projects/settings/life_cycles_controller.rb index 6ad0c52f583f..e7dcf078a76d 100644 --- a/app/controllers/projects/settings/life_cycles_controller.rb +++ b/app/controllers/projects/settings/life_cycles_controller.rb @@ -63,7 +63,7 @@ def enable_all private def load_life_cycle_definitions - @life_cycle_definitions = Project::LifeCycleStepDefinition.all + @life_cycle_definitions = Project::LifeCycleStepDefinition.order(position: :asc) end def deny_access_on_feature_flag From ec5e6b56258448f5ecdc741b79d63789c3e58f2b Mon Sep 17 00:00:00 2001 From: ulferts Date: Wed, 27 Nov 2024 16:30:58 +0100 Subject: [PATCH 18/44] enable filtering on the life cycle page --- .../life_cycles/index_component.html.erb | 113 +++++++++++------- .../settings/life_cycles/index_component.rb | 13 +- .../index_component.html.erb | 6 +- .../index_component.rb | 6 +- .../show_component.html.erb | 6 +- config/locales/en.yml | 2 + ...ler.ts => border-box-filter.controller.ts} | 0 .../life_cycle/active_in_project_spec.rb | 8 +- .../settings/mapping_spec.rb | 2 +- .../pages/projects/settings/life_cycle.rb | 4 + 10 files changed, 100 insertions(+), 60 deletions(-) rename frontend/src/stimulus/controllers/dynamic/projects/settings/{project-custom-fields-mapping-filter.controller.ts => border-box-filter.controller.ts} (100%) diff --git a/app/components/projects/settings/life_cycles/index_component.html.erb b/app/components/projects/settings/life_cycles/index_component.html.erb index 60feef7d20ef..d53638d55546 100644 --- a/app/components/projects/settings/life_cycles/index_component.html.erb +++ b/app/components/projects/settings/life_cycles/index_component.html.erb @@ -1,56 +1,77 @@ <%= - component_wrapper do - render(border_box_container(mb: 3, data: { test_selector: "project-life-cycle-administration" })) do |component| - component.with_header(font_weight: :bold, py: 2) do - flex_layout(justify_content: :space_between, align_items: :center) do |header_container| - header_container.with_column(py: 2) do - # adding py: 2 here to match the padding of the actions_container - # otherwise the header height changes when the actions gets hidden when filtering - render(Primer::Beta::Text.new(font_weight: :bold)) do - I18n.t('projects.settings.life_cycle.section_header') - end - end - header_container.with_column(flex_layout: true, justify_content: :flex_end) do |actions_container| - actions_container.with_column(data: { 'projects--settings--project-custom-fields-mapping-filter-target': 'bulkActionContainer' }) do - render(Primer::Beta::Button.new( - tag: :a, - href: enable_all_project_settings_life_cycle_path(project_id: project), - scheme: :invisible, - font_weight: :bold, - color: :subtle, - 'aria-label': t('projects.settings.actions.label_enable_all'), - data: { 'turbo-method': :post, 'turbo': true, test_selector: "enable-all-life-cycle-steps" } - )) do |button| - button.with_leading_visual_icon(icon: 'check-circle', color: :subtle) - t('projects.settings.actions.label_enable_all') + flex_layout(data: wrapper_data_attributes) do |flex| + flex.with_row do + render(Primer::OpenProject::SubHeader.new) do |subheader| + subheader.with_filter_input(name: "border-box-filter", + label: t('projects.settings.life_cycle.filter.label'), + visually_hide_label: true, + placeholder: t('projects.settings.life_cycle.filter.label'), + leading_visual: { + icon: :search, + size: :small + }, + show_clear_button: true, + clear_button_id: clear_button_id, + data: { + action: "input->projects--settings--border-box-filter#filterLists", + "projects--settings--border-box-filter-target": "filter" + }) + end + end + + flex.with_row do + render(border_box_container(mb: 3, data: { test_selector: "project-life-cycle-administration" })) do |component| + component.with_header(font_weight: :bold, py: 2) do + flex_layout(justify_content: :space_between, align_items: :center) do |header_container| + header_container.with_column(py: 2) do + # adding py: 2 here to match the padding of the actions_container + # otherwise the header height changes when the actions gets hidden when filtering + render(Primer::Beta::Text.new(font_weight: :bold)) do + I18n.t('projects.settings.life_cycle.section_header') end end - actions_container.with_column(data: { 'projects--settings--project-custom-fields-mapping-filter-target': 'bulkActionContainer' }) do - render(Primer::Beta::Button.new( - tag: :a, - href: disable_all_project_settings_life_cycle_path(project_id: project), - scheme: :invisible, - font_weight: :bold, - color: :subtle, - 'aria-label': t('projects.settings.actions.label_disable_all'), - data: { 'turbo-method': :post, 'turbo': true, test_selector: "disable-all-life-cycle-steps" } - )) do |button| - button.with_leading_visual_icon(icon: 'x-circle', color: :subtle) - t('projects.settings.actions.label_disable_all') + header_container.with_column(flex_layout: true, justify_content: :flex_end) do |actions_container| + actions_container.with_column(data: { 'projects--settings--border-box-filter-target': 'bulkActionContainer' }) do + render(Primer::Beta::Button.new( + tag: :a, + href: enable_all_project_settings_life_cycle_path(project_id: project), + scheme: :invisible, + font_weight: :bold, + color: :subtle, + 'aria-label': t('projects.settings.actions.label_enable_all'), + data: { 'turbo-method': :post, 'turbo': true, test_selector: "enable-all-life-cycle-steps" } + )) do |button| + button.with_leading_visual_icon(icon: 'check-circle', color: :subtle) + t('projects.settings.actions.label_enable_all') + end + end + actions_container.with_column(data: { 'projects--settings--border-box-filter-target': 'bulkActionContainer' }) do + render(Primer::Beta::Button.new( + tag: :a, + href: disable_all_project_settings_life_cycle_path(project_id: project), + scheme: :invisible, + font_weight: :bold, + color: :subtle, + 'aria-label': t('projects.settings.actions.label_disable_all'), + data: { 'turbo-method': :post, 'turbo': true, test_selector: "disable-all-life-cycle-steps" } + )) do |button| + button.with_leading_visual_icon(icon: 'x-circle', color: :subtle) + t('projects.settings.actions.label_disable_all') + end end end end end - end - if life_cycle_definitions.empty? - component.with_row do - render(Primer::Beta::Text.new(color: :subtle)) { t("projects.settings.life_cycle.non_defined") } - end - else - life_cycle_definitions.each do |definition| - component.with_row(data: { 'projects--settings--project-custom-fields-mapping-filter-target': 'searchItem' }, - test_selector: "project-life-cycle-step-#{definition.id}") do - render(Projects::Settings::LifeCycles::StepComponent.new(project:, definition:)) + if life_cycle_definitions.empty? + component.with_row do + render(Primer::Beta::Text.new(color: :subtle)) { t("projects.settings.life_cycle.non_defined") } + end + else + life_cycle_definitions.each do |definition| + component.with_row(data: { 'projects--settings--border-box-filter-target': 'searchItem' }, + test_selector: "project-life-cycle-step-#{definition.id}") do + render(Projects::Settings::LifeCycles::StepComponent.new(project:, definition:)) + end end end end diff --git a/app/components/projects/settings/life_cycles/index_component.rb b/app/components/projects/settings/life_cycles/index_component.rb index 059b750e1671..654f1b70f65b 100644 --- a/app/components/projects/settings/life_cycles/index_component.rb +++ b/app/components/projects/settings/life_cycles/index_component.rb @@ -46,9 +46,16 @@ def initialize(project:, life_cycle_definitions:) private - # TODO: Check if this is necessary - def wrapper_uniq_by - @project.id + def wrapper_data_attributes + { + controller: "projects--settings--border-box-filter", + "application-target": "dynamic", + "projects--settings--border-box-filter-clear-button-id-value": clear_button_id + } + end + + def clear_button_id + "border-box-filter-clear-button" end end end diff --git a/app/components/projects/settings/project_custom_field_sections/index_component.html.erb b/app/components/projects/settings/project_custom_field_sections/index_component.html.erb index b4044ee99669..f54e78c9f20b 100644 --- a/app/components/projects/settings/project_custom_field_sections/index_component.html.erb +++ b/app/components/projects/settings/project_custom_field_sections/index_component.html.erb @@ -3,7 +3,7 @@ flex_layout do |flex| flex.with_row do render(Primer::OpenProject::SubHeader.new) do |subheader| - subheader.with_filter_input(name: "project-custom-fields-mapping-filter", + subheader.with_filter_input(name: "border-box-filter", label: t('projects.settings.project_custom_fields.filter.label'), visually_hide_label: true, placeholder: t('projects.settings.project_custom_fields.filter.label'), @@ -14,8 +14,8 @@ show_clear_button: true, clear_button_id: clear_button_id, data: { - action: "input->projects--settings--project-custom-fields-mapping-filter#filterLists", - "projects--settings--project-custom-fields-mapping-filter-target": "filter" + action: "input->projects--settings--border-box-filter#filterLists", + "projects--settings--border-box-filter-target": "filter" }) end end diff --git a/app/components/projects/settings/project_custom_field_sections/index_component.rb b/app/components/projects/settings/project_custom_field_sections/index_component.rb index 25b8a86e8b71..b405aa381d38 100644 --- a/app/components/projects/settings/project_custom_field_sections/index_component.rb +++ b/app/components/projects/settings/project_custom_field_sections/index_component.rb @@ -45,14 +45,14 @@ def initialize(project:, project_custom_field_sections:) def wrapper_data_attributes { - controller: "projects--settings--project-custom-fields-mapping-filter", + controller: "projects--settings--border-box-filter", "application-target": "dynamic", - "projects--settings--project-custom-fields-mapping-filter-clear-button-id-value": clear_button_id + "projects--settings--border-box-filter-clear-button-id-value": clear_button_id } end def clear_button_id - "project-custom-fields-mapping-filter-clear-button" + "border-box-filter-clear-button" end end end diff --git a/app/components/projects/settings/project_custom_field_sections/show_component.html.erb b/app/components/projects/settings/project_custom_field_sections/show_component.html.erb index 66b42dfbcbb1..f2c5ee4e3a7f 100644 --- a/app/components/projects/settings/project_custom_field_sections/show_component.html.erb +++ b/app/components/projects/settings/project_custom_field_sections/show_component.html.erb @@ -13,7 +13,7 @@ end end section_header_container.with_column(flex_layout: true, justify_content: :flex_end) do |actions_container| - actions_container.with_column(data: { 'projects--settings--project-custom-fields-mapping-filter-target': 'bulkActionContainer' }) do + actions_container.with_column(data: { 'projects--settings--border-box-filter-target': 'bulkActionContainer' }) do render(Primer::Beta::Button.new( tag: :a, href: enable_all_of_section_project_settings_project_custom_fields_path( @@ -32,7 +32,7 @@ t('projects.settings.actions.label_enable_all') end end - actions_container.with_column(data: { 'projects--settings--project-custom-fields-mapping-filter-target': 'bulkActionContainer' }) do + actions_container.with_column(data: { 'projects--settings--border-box-filter-target': 'bulkActionContainer' }) do render(Primer::Beta::Button.new( tag: :a, href: disable_all_of_section_project_settings_project_custom_fields_path( @@ -60,7 +60,7 @@ end else @project_custom_fields.each do |project_custom_field| - component.with_row(data: { 'projects--settings--project-custom-fields-mapping-filter-target': 'searchItem' }) do + component.with_row(data: { 'projects--settings--border-box-filter-target': 'searchItem' }) do render(Projects::Settings::ProjectCustomFieldSections::CustomFieldRowComponent.new( project: @project, project_custom_field:, diff --git a/config/locales/en.yml b/config/locales/en.yml index 0f5983e71bc1..d661b1f94c43 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -399,6 +399,8 @@ en: section_header: "Stages and gates" step: use_in_project: "Use %{step} in this project" + filter: + label: "Search project stage or gate" project_custom_fields: header: title: "Project attributes" diff --git a/frontend/src/stimulus/controllers/dynamic/projects/settings/project-custom-fields-mapping-filter.controller.ts b/frontend/src/stimulus/controllers/dynamic/projects/settings/border-box-filter.controller.ts similarity index 100% rename from frontend/src/stimulus/controllers/dynamic/projects/settings/project-custom-fields-mapping-filter.controller.ts rename to frontend/src/stimulus/controllers/dynamic/projects/settings/border-box-filter.controller.ts diff --git a/spec/features/projects/life_cycle/active_in_project_spec.rb b/spec/features/projects/life_cycle/active_in_project_spec.rb index a098d75e520c..ac1b59d45df8 100644 --- a/spec/features/projects/life_cycle/active_in_project_spec.rb +++ b/spec/features/projects/life_cycle/active_in_project_spec.rb @@ -51,7 +51,7 @@ context "with sufficient permissions" do current_user { user_with_permission } - it "allows toggling the active/inactive state of lifecycle steps" do + it "allows toggling the active/inactive state of lifecycle steps and filtering them" do project_life_cycle_page.visit! project_life_cycle_page.expect_listed(initiating_stage => false, @@ -106,6 +106,12 @@ executing_stage => true, ready_to_close_gate => true, closing_stage => true) + + # The user can filter the life cycle steps + project_life_cycle_page.filter_by("Clo") + + project_life_cycle_page.expect_listed(ready_to_close_gate => true, + closing_stage => true) end end end diff --git a/spec/features/projects/project_custom_fields/settings/mapping_spec.rb b/spec/features/projects/project_custom_fields/settings/mapping_spec.rb index f94c13871eee..3cc245f6fb6d 100644 --- a/spec/features/projects/project_custom_fields/settings/mapping_spec.rb +++ b/spec/features/projects/project_custom_fields/settings/mapping_spec.rb @@ -260,7 +260,7 @@ it "filters the project custom fields by name with given user input" do visit project_settings_project_custom_fields_path(project) - fill_in "project-custom-fields-mapping-filter", with: "Boolean" + fill_in "border-box-filter", with: "Boolean" within_custom_field_section_container(section_for_input_fields) do expect(page).to have_content("Boolean field") diff --git a/spec/support/pages/projects/settings/life_cycle.rb b/spec/support/pages/projects/settings/life_cycle.rb index 3f6572c123f1..cb325e64bdf6 100644 --- a/spec/support/pages/projects/settings/life_cycle.rb +++ b/spec/support/pages/projects/settings/life_cycle.rb @@ -87,6 +87,10 @@ def toggle_step(definition) find_test_selector("toggle-project-life-cycle-#{definition.id}") end + def filter_by(filter) + fill_in I18n.t("projects.settings.life_cycle.filter.label"), with: filter + end + def expected_toggle_status(active) active ? "On" : "Off" end From ed225b521a891c89c3f1420ddc5f2120608e3ce0 Mon Sep 17 00:00:00 2001 From: ulferts Date: Wed, 27 Nov 2024 16:35:13 +0100 Subject: [PATCH 19/44] spec insufficient access --- .../life_cycle/active_in_project_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/features/projects/life_cycle/active_in_project_spec.rb b/spec/features/projects/life_cycle/active_in_project_spec.rb index ac1b59d45df8..3d0f5ddfeed7 100644 --- a/spec/features/projects/life_cycle/active_in_project_spec.rb +++ b/spec/features/projects/life_cycle/active_in_project_spec.rb @@ -39,6 +39,14 @@ ] }) end + shared_let(:user_without_permission) do + create(:user, + member_with_permissions: { + project => %w[ + edit_project + ] + }) + end shared_let(:initiating_stage) { create(:project_stage_definition, name: "Initiating") } shared_let(:ready_to_execute_gate) { create(:project_gate_definition, name: "Ready to Execute") } @@ -114,4 +122,14 @@ closing_stage => true) end end + + context "without sufficient permissions" do + current_user { user_without_permission } + + it "does not allow the user to access the page" do + project_life_cycle_page.visit! + + project_life_cycle_page.expect_flash(message: "You are not authorized to access this page", type: :error) + end + end end From a2d8949e7bccaf3a4c12f8aa56b2e8ac74b45249 Mon Sep 17 00:00:00 2001 From: ulferts Date: Wed, 27 Nov 2024 17:21:11 +0100 Subject: [PATCH 20/44] add new permission to roles having edit_project --- ...127161228_grant_select_project_life_cycle_permission.rb | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 db/migrate/20241127161228_grant_select_project_life_cycle_permission.rb diff --git a/db/migrate/20241127161228_grant_select_project_life_cycle_permission.rb b/db/migrate/20241127161228_grant_select_project_life_cycle_permission.rb new file mode 100644 index 000000000000..8309166220a7 --- /dev/null +++ b/db/migrate/20241127161228_grant_select_project_life_cycle_permission.rb @@ -0,0 +1,7 @@ +require_relative "migration_utils/permission_adder" + +class GrantSelectProjectLifeCyclePermission < ActiveRecord::Migration[7.1] + def up + ::Migration::MigrationUtils::PermissionAdder.add(:edit_project, :select_project_life_cycle) + end +end From 55e5a52cb45fa9ed35a48a9f633685bf2a2265b7 Mon Sep 17 00:00:00 2001 From: ulferts Date: Thu, 28 Nov 2024 11:48:09 +0100 Subject: [PATCH 21/44] remove unnecessary wrapper --- .../life_cycles/step_component.html.erb | 60 +++++++++---------- .../settings/life_cycles/step_component.rb | 6 -- 2 files changed, 29 insertions(+), 37 deletions(-) diff --git a/app/components/projects/settings/life_cycles/step_component.html.erb b/app/components/projects/settings/life_cycles/step_component.html.erb index 0e11abc46daa..493c89621c30 100644 --- a/app/components/projects/settings/life_cycles/step_component.html.erb +++ b/app/components/projects/settings/life_cycles/step_component.html.erb @@ -1,37 +1,35 @@ <%= - component_wrapper do - flex_layout(align_items: :center, - justify_content: :space_between) do |definition_container| - definition_container.with_column(flex_layout: true) do |title_container| - title_container.with_column(pt: 1, mr: 3) do - render(Primer::Beta::Text.new) { definition.name } - end - title_container.with_column(pt: 1) do - render(Projects::LifeCycleTypeComponent.new(definition)) - end + flex_layout(align_items: :center, + justify_content: :space_between) do |definition_container| + definition_container.with_column(flex_layout: true) do |title_container| + title_container.with_column(pt: 1, mr: 3) do + render(Primer::Beta::Text.new) { definition.name } end - # py: 1 quick fix: prevents the row from bouncing as the toggle switch currently changes height while toggling - definition_container.with_column(py: 1, mr: 2) do - # buggy currently: - # small variant + status_label_position: :start leads to a small bounce while toggling - # behavior can be seen on primer's viewbook as well -> https://view-components-storybook.eastus.cloudapp.azure.com/view-components/lookbook/inspect/primer/alpha/toggle_switch/small - # quick fix: don't display loading indicator which is causing the bounce - render(Primer::Alpha::ToggleSwitch.new( - src: toggle_project_settings_life_cycle_path( - project_life_cycle: { - definition_id: definition.id, - type: definition.type - } - ), - csrf_token: form_authenticity_token, - data: { test_selector: "toggle-project-life-cycle-#{definition.id}" }, - aria: { label: toggle_aria_label }, - checked: active_in_project?, - size: :small, - status_label_position: :start, - classes: "op-primer-adjustments__toggle-switch--hidden-loading-indicator", - )) + title_container.with_column(pt: 1) do + render(Projects::LifeCycleTypeComponent.new(definition)) end end + # py: 1 quick fix: prevents the row from bouncing as the toggle switch currently changes height while toggling + definition_container.with_column(py: 1, mr: 2) do + # buggy currently: + # small variant + status_label_position: :start leads to a small bounce while toggling + # behavior can be seen on primer's viewbook as well -> https://view-components-storybook.eastus.cloudapp.azure.com/view-components/lookbook/inspect/primer/alpha/toggle_switch/small + # quick fix: don't display loading indicator which is causing the bounce + render(Primer::Alpha::ToggleSwitch.new( + src: toggle_project_settings_life_cycle_path( + project_life_cycle: { + definition_id: definition.id, + type: definition.type + } + ), + csrf_token: form_authenticity_token, + data: { test_selector: "toggle-project-life-cycle-#{definition.id}" }, + aria: { label: toggle_aria_label }, + checked: active_in_project?, + size: :small, + status_label_position: :start, + classes: "op-primer-adjustments__toggle-switch--hidden-loading-indicator", + )) + end end %> diff --git a/app/components/projects/settings/life_cycles/step_component.rb b/app/components/projects/settings/life_cycles/step_component.rb index 2642d5ab55ef..c9faccce001c 100644 --- a/app/components/projects/settings/life_cycles/step_component.rb +++ b/app/components/projects/settings/life_cycles/step_component.rb @@ -54,12 +54,6 @@ def active_in_project? def toggle_aria_label I18n.t("projects.settings.life_cycle.step.use_in_project", step: definition.name) end - - private - - def wrapper_uniq_by - definition.id - end end end end From aa7152f7c2c6c136cc4a05e4975a6cffabae69b8 Mon Sep 17 00:00:00 2001 From: ulferts Date: Thu, 28 Nov 2024 11:48:25 +0100 Subject: [PATCH 22/44] add permission description according to specification --- config/locales/en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index d661b1f94c43..116017d0a90c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3218,7 +3218,7 @@ en: permission_select_custom_fields: "Select custom fields" permission_select_project_custom_fields: "Select project attributes" permission_select_project_life_cycle: "Select project stages and gates" - permission_select_project_life_cycle_explanation: "Activate/Deactivate the stages and gates in a project." + permission_select_project_life_cycle_explanation: "Activate/Deactivate the stages and gates in a project. Enables the user to select the lifecycle appropriate for the project as inactive stages and gates will not be visible in the project overview page nor the project list." permission_select_project_modules: "Select project modules" permission_share_work_packages: "Share work packages" permission_manage_types: "Select types" From 054a670e71fe4c31f876f3a033e66271c114268c Mon Sep 17 00:00:00 2001 From: ulferts Date: Thu, 28 Nov 2024 11:49:36 +0100 Subject: [PATCH 23/44] remove unnecessary turbo data attribute --- .../projects/settings/life_cycles/index_component.html.erb | 4 ++-- app/views/projects/settings/life_cycles/show.html.erb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/components/projects/settings/life_cycles/index_component.html.erb b/app/components/projects/settings/life_cycles/index_component.html.erb index d53638d55546..ffb3c63f334d 100644 --- a/app/components/projects/settings/life_cycles/index_component.html.erb +++ b/app/components/projects/settings/life_cycles/index_component.html.erb @@ -39,7 +39,7 @@ font_weight: :bold, color: :subtle, 'aria-label': t('projects.settings.actions.label_enable_all'), - data: { 'turbo-method': :post, 'turbo': true, test_selector: "enable-all-life-cycle-steps" } + data: { 'turbo-method': :post, test_selector: "enable-all-life-cycle-steps" } )) do |button| button.with_leading_visual_icon(icon: 'check-circle', color: :subtle) t('projects.settings.actions.label_enable_all') @@ -53,7 +53,7 @@ font_weight: :bold, color: :subtle, 'aria-label': t('projects.settings.actions.label_disable_all'), - data: { 'turbo-method': :post, 'turbo': true, test_selector: "disable-all-life-cycle-steps" } + data: { 'turbo-method': :post, test_selector: "disable-all-life-cycle-steps" } )) do |button| button.with_leading_visual_icon(icon: 'x-circle', color: :subtle) t('projects.settings.actions.label_disable_all') diff --git a/app/views/projects/settings/life_cycles/show.html.erb b/app/views/projects/settings/life_cycles/show.html.erb index a53367c545b8..910b74c8603f 100644 --- a/app/views/projects/settings/life_cycles/show.html.erb +++ b/app/views/projects/settings/life_cycles/show.html.erb @@ -26,7 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> -
+
<%= render Projects::Settings::LifeCycles::ShowPageHeaderComponent.new(project: @project) %> <%= render(Projects::Settings::LifeCycles::IndexComponent.new( project: @project, life_cycle_definitions: @life_cycle_definitions)) %> From 713f114f7efec76fd361e16e171071374703646c Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 9 Dec 2024 09:30:43 +0100 Subject: [PATCH 24/44] use options for components --- .../projects/settings/life_cycles/index_component.rb | 11 ++--------- .../life_cycles/show_page_header_component.html.erb | 12 ++++++------ .../life_cycles/show_page_header_component.rb | 9 +++------ .../projects/settings/life_cycles/step_component.rb | 11 ++--------- .../index_page_header_component.html.erb | 7 +++---- .../index_page_header_component.rb | 9 +++------ config/locales/en.yml | 4 ++-- 7 files changed, 21 insertions(+), 42 deletions(-) diff --git a/app/components/projects/settings/life_cycles/index_component.rb b/app/components/projects/settings/life_cycles/index_component.rb index 654f1b70f65b..403c26152629 100644 --- a/app/components/projects/settings/life_cycles/index_component.rb +++ b/app/components/projects/settings/life_cycles/index_component.rb @@ -34,15 +34,8 @@ class IndexComponent < ApplicationComponent include OpPrimer::ComponentHelpers include OpTurbo::Streamable - attr_reader :project, - :life_cycle_definitions - - def initialize(project:, life_cycle_definitions:) - super - - @project = project - @life_cycle_definitions = life_cycle_definitions - end + options :project, + :life_cycle_definitions private diff --git a/app/components/projects/settings/life_cycles/show_page_header_component.html.erb b/app/components/projects/settings/life_cycles/show_page_header_component.html.erb index a090cf4fbbf1..b6845944e263 100644 --- a/app/components/projects/settings/life_cycles/show_page_header_component.html.erb +++ b/app/components/projects/settings/life_cycles/show_page_header_component.html.erb @@ -1,9 +1,9 @@ <%= render Primer::OpenProject::PageHeader.new do |header| %> - <%= header.with_title(variant: :default) { t('projects.settings.life_cycle.header.title') } %> - <%= header.with_description { - t('projects.settings.life_cycle.header.description', - overview_url: project_path(@project), - admin_settings_url: admin_settings_project_life_cycles_path).html_safe - } %> + <%= header.with_title { t('projects.settings.life_cycle.header.title') } %> + <%= header.with_description do + t('projects.settings.life_cycle.header.description_html', + overview_url: project_path(project), + admin_settings_url: admin_settings_project_life_cycles_path) + end %> <%= header.with_breadcrumbs(breadcrumb_items) %> <% end %> diff --git a/app/components/projects/settings/life_cycles/show_page_header_component.rb b/app/components/projects/settings/life_cycles/show_page_header_component.rb index 0b9f15fc85d9..9641ab894d4a 100644 --- a/app/components/projects/settings/life_cycles/show_page_header_component.rb +++ b/app/components/projects/settings/life_cycles/show_page_header_component.rb @@ -32,14 +32,11 @@ module Projects::Settings::LifeCycles class ShowPageHeaderComponent < ApplicationComponent include ApplicationHelper - def initialize(project: nil) - super - @project = project - end + options :project def breadcrumb_items - [{ href: project_overview_path(@project.id), text: @project.name }, - { href: project_settings_general_path(@project.id), text: I18n.t("label_project_settings") }, + [{ href: project_overview_path(project), text: project.name }, + { href: project_settings_general_path(project), text: I18n.t("label_project_settings") }, t("projects.settings.life_cycle.header.title")] end end diff --git a/app/components/projects/settings/life_cycles/step_component.rb b/app/components/projects/settings/life_cycles/step_component.rb index c9faccce001c..3f32e3d5c24f 100644 --- a/app/components/projects/settings/life_cycles/step_component.rb +++ b/app/components/projects/settings/life_cycles/step_component.rb @@ -34,15 +34,8 @@ class StepComponent < ApplicationComponent include OpPrimer::ComponentHelpers include OpTurbo::Streamable - attr_reader :definition, - :project - - def initialize(project:, definition:) - super - - @definition = definition - @project = project - end + options :definition, + :project def active_in_project? project diff --git a/app/components/projects/settings/project_custom_field_sections/index_page_header_component.html.erb b/app/components/projects/settings/project_custom_field_sections/index_page_header_component.html.erb index 7cbbd5903e1d..e73cbd6c5e58 100644 --- a/app/components/projects/settings/project_custom_field_sections/index_page_header_component.html.erb +++ b/app/components/projects/settings/project_custom_field_sections/index_page_header_component.html.erb @@ -1,8 +1,7 @@ <%= render Primer::OpenProject::PageHeader.new do |header| %> <%= header.with_title(variant: :default) { t('projects.settings.project_custom_fields.header.title') } %> - <%= header.with_description { t('projects.settings.project_custom_fields.header.description', - overview_url: project_path(@project), - admin_settings_url: admin_settings_project_custom_fields_path - ).html_safe } %> + <%= header.with_description { t('projects.settings.project_custom_fields.header.description_html', + overview_url: project_path(project), + admin_settings_url: admin_settings_project_custom_fields_path) } %> <%= header.with_breadcrumbs(breadcrumb_items) %> <% end %> diff --git a/app/components/projects/settings/project_custom_field_sections/index_page_header_component.rb b/app/components/projects/settings/project_custom_field_sections/index_page_header_component.rb index 13014aae6aab..fe1b93434a1f 100644 --- a/app/components/projects/settings/project_custom_field_sections/index_page_header_component.rb +++ b/app/components/projects/settings/project_custom_field_sections/index_page_header_component.rb @@ -33,14 +33,11 @@ module Projects::Settings::ProjectCustomFieldSections class IndexPageHeaderComponent < ApplicationComponent include ApplicationHelper - def initialize(project: nil) - super - @project = project - end + options :project def breadcrumb_items - [{ href: project_overview_path(@project.id), text: @project.name }, - { href: project_settings_general_path(@project.id), text: I18n.t("label_project_settings") }, + [{ href: project_overview_path(project), text: project.name }, + { href: project_settings_general_path(project), text: I18n.t("label_project_settings") }, t("settings.project_attributes.heading")] end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 116017d0a90c..a57949e0c335 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -394,7 +394,7 @@ en: life_cycle: header: title: "Project lifecycle" - description: 'These project stages and gates in the project lifecycle will be displayed in your project overview page and are defined in the administration settings by the administrator of the instance. You can enable or disable individual attributes.' + description_html: 'These project stages and gates in the project lifecycle will be displayed in your project overview page and are defined in the administration settings by the administrator of the instance. You can enable or disable individual attributes.' non_defined: "Neither stages nor gates are currently defined." section_header: "Stages and gates" step: @@ -404,7 +404,7 @@ en: project_custom_fields: header: title: "Project attributes" - description: + description_html: 'These project attributes will be displayed in your project overview page under their respective sections. You can enable or disable individual attributes. Project attributes and sections are defined in the administration settings by the administrator of the instance. ' filter: From bd2b31a632637a2eb5264533a9aed6e0371fe0c9 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 9 Dec 2024 09:36:06 +0100 Subject: [PATCH 25/44] change to a more restricted property definition --- .../backlogs/spec/support/pages/projects/settings/backlogs.rb | 4 ++-- spec/support/pages/projects/settings/general.rb | 4 ++-- spec/support/pages/projects/settings/life_cycle.rb | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/backlogs/spec/support/pages/projects/settings/backlogs.rb b/modules/backlogs/spec/support/pages/projects/settings/backlogs.rb index 86f573bd7667..8c68637908ef 100644 --- a/modules/backlogs/spec/support/pages/projects/settings/backlogs.rb +++ b/modules/backlogs/spec/support/pages/projects/settings/backlogs.rb @@ -32,12 +32,12 @@ module Pages module Projects module Settings class Backlogs < Pages::Page - attr_accessor :project + attr_reader :project def initialize(project) super() - self.project = project + @project = project end def path diff --git a/spec/support/pages/projects/settings/general.rb b/spec/support/pages/projects/settings/general.rb index eb515944db4b..64f34ee9818e 100644 --- a/spec/support/pages/projects/settings/general.rb +++ b/spec/support/pages/projects/settings/general.rb @@ -32,12 +32,12 @@ module Pages module Projects module Settings class General < Pages::Page - attr_accessor :project + attr_reader :project def initialize(project) super() - self.project = project + @project = project end def path diff --git a/spec/support/pages/projects/settings/life_cycle.rb b/spec/support/pages/projects/settings/life_cycle.rb index cb325e64bdf6..7be19d02d688 100644 --- a/spec/support/pages/projects/settings/life_cycle.rb +++ b/spec/support/pages/projects/settings/life_cycle.rb @@ -32,12 +32,12 @@ module Pages module Projects module Settings class LifeCycle < Pages::Page - attr_accessor :project + attr_reader :project def initialize(project) super() - self.project = project + @project = project end def path From 47fbe8136676f5ed4a185e0c56e600b02849feed Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 9 Dec 2024 09:36:23 +0100 Subject: [PATCH 26/44] remove unnecessary check --- spec/support/pages/projects/settings/life_cycle.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/support/pages/projects/settings/life_cycle.rb b/spec/support/pages/projects/settings/life_cycle.rb index 7be19d02d688..9c2769df814e 100644 --- a/spec/support/pages/projects/settings/life_cycle.rb +++ b/spec/support/pages/projects/settings/life_cycle.rb @@ -47,10 +47,8 @@ def path # Checks if the life cycle steps are listed in the order given and with the correct toggle state. # @param life_cycle_definitions [Hash{LifeCycleElement => Boolean}] def expect_listed(**life_cycle_definitions) - if life_cycle_definitions.size > 1 - life_cycle_definitions.each_cons(2) do |(predecessor, _), (successor, _)| - expect(page).to have_css("#{life_cycle_test_selector(predecessor)} + #{life_cycle_test_selector(successor)}") - end + life_cycle_definitions.each_cons(2) do |(predecessor, _), (successor, _)| + expect(page).to have_css("#{life_cycle_test_selector(predecessor)} + #{life_cycle_test_selector(successor)}") end life_cycle_definitions.each do |definition, active| From 59b8f149a3b13a6541602ceda18c267c31503c9f Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 9 Dec 2024 10:56:00 +0100 Subject: [PATCH 27/44] exclude searching on non user relevant text --- .../settings/life_cycles/step_component.html.erb | 8 ++++---- .../custom_field_row_component.html.erb | 2 +- .../dynamic/filter/filter-list.controller.ts | 13 ++++++++++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/components/projects/settings/life_cycles/step_component.html.erb b/app/components/projects/settings/life_cycles/step_component.html.erb index 493c89621c30..46e2899ecd55 100644 --- a/app/components/projects/settings/life_cycles/step_component.html.erb +++ b/app/components/projects/settings/life_cycles/step_component.html.erb @@ -1,16 +1,16 @@ <%= flex_layout(align_items: :center, - justify_content: :space_between) do |definition_container| - definition_container.with_column(flex_layout: true) do |title_container| + justify_content: :space_between) do |step_container| + step_container.with_column(flex_layout: true) do |title_container| title_container.with_column(pt: 1, mr: 3) do - render(Primer::Beta::Text.new) { definition.name } + render(Primer::Beta::Text.new(classes: 'filter-target-visible-text')) { definition.name } end title_container.with_column(pt: 1) do render(Projects::LifeCycleTypeComponent.new(definition)) end end # py: 1 quick fix: prevents the row from bouncing as the toggle switch currently changes height while toggling - definition_container.with_column(py: 1, mr: 2) do + step_container.with_column(py: 1, mr: 2) do # buggy currently: # small variant + status_label_position: :start leads to a small bounce while toggling # behavior can be seen on primer's viewbook as well -> https://view-components-storybook.eastus.cloudapp.azure.com/view-components/lookbook/inspect/primer/alpha/toggle_switch/small diff --git a/app/components/projects/settings/project_custom_field_sections/custom_field_row_component.html.erb b/app/components/projects/settings/project_custom_field_sections/custom_field_row_component.html.erb index 374bb4ed0642..fae8cdf15c38 100644 --- a/app/components/projects/settings/project_custom_field_sections/custom_field_row_component.html.erb +++ b/app/components/projects/settings/project_custom_field_sections/custom_field_row_component.html.erb @@ -5,7 +5,7 @@ }) do |custom_field_container| custom_field_container.with_column(flex_layout: true) do |title_container| title_container.with_column(pt: 1, mr: 2) do - render(Primer::Beta::Text.new) do + render(Primer::Beta::Text.new(classes: 'filter-target-visible-text')) do @project_custom_field.name end end diff --git a/frontend/src/stimulus/controllers/dynamic/filter/filter-list.controller.ts b/frontend/src/stimulus/controllers/dynamic/filter/filter-list.controller.ts index 420be0098525..4272a74adf8d 100644 --- a/frontend/src/stimulus/controllers/dynamic/filter/filter-list.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/filter/filter-list.controller.ts @@ -59,12 +59,23 @@ export default class FilterListController extends Controller { }); } + /** + * Filters the list of items based on the input value in the filter target. + * + * This method converts the input value to lowercase and compares it with the text content + * of each search item. To avoid unwanted text from being part of the search string. E.g. the + * labels of buttons, the method first tries to find an element with the class `filter-target-visible-text`. + * If such an element exists, only the content of that element is used for the search. + * If the text content includes the input value, the item is shown; + * otherwise, it is hidden. Additionally, it controls the visibility of the no results text + * based on whether any items match the input value. + */ filterLists() { const query = this.filterTarget.value.toLowerCase(); let showNoResultsText = true; this.searchItemTargets.forEach((item) => { - const text = item.textContent?.toLowerCase(); + const text = item.querySelector('.filter-target-visible-text')?.textContent?.toLowerCase() || item.textContent?.toLowerCase(); if (text?.includes(query)) { this.setVisibility(item, true); From e0eb17be1606e815bc3cb2899bf93b2bc7b4840a Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 9 Dec 2024 10:57:14 +0100 Subject: [PATCH 28/44] improve expressiveness of spec --- .../life_cycle/active_in_project_spec.rb | 35 +++++++++++++------ .../pages/projects/settings/life_cycle.rb | 27 +++++++++++--- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/spec/features/projects/life_cycle/active_in_project_spec.rb b/spec/features/projects/life_cycle/active_in_project_spec.rb index 3d0f5ddfeed7..c545b59004a4 100644 --- a/spec/features/projects/life_cycle/active_in_project_spec.rb +++ b/spec/features/projects/life_cycle/active_in_project_spec.rb @@ -73,11 +73,14 @@ project_life_cycle_page.toggle(ready_to_close_gate) project_life_cycle_page.toggle(closing_stage) - wait_for_network_idle + project_life_cycle_page.expect_listed(initiating_stage => true, + ready_to_execute_gate => false, + executing_stage => false, + ready_to_close_gate => true, + closing_stage => true) # Expect the activation state to be kept after a reload - visit home_path - project_life_cycle_page.visit! + project_life_cycle_page.reload_with_home_page_detour project_life_cycle_page.expect_listed(initiating_stage => true, ready_to_execute_gate => false, @@ -88,11 +91,14 @@ # Disable all stages at once project_life_cycle_page.disable_all - wait_for_network_idle + project_life_cycle_page.expect_listed(initiating_stage => false, + ready_to_execute_gate => false, + executing_stage => false, + ready_to_close_gate => false, + closing_stage => false) # Expect the activation state to be kept after a reload - visit home_path - project_life_cycle_page.visit! + project_life_cycle_page.reload_with_home_page_detour project_life_cycle_page.expect_listed(initiating_stage => false, ready_to_execute_gate => false, @@ -103,11 +109,14 @@ # Enable all stages at once project_life_cycle_page.enable_all - wait_for_network_idle + project_life_cycle_page.expect_listed(initiating_stage => true, + ready_to_execute_gate => true, + executing_stage => true, + ready_to_close_gate => true, + closing_stage => true) # Expect the activation state to be kept after a reload - visit home_path - project_life_cycle_page.visit! + project_life_cycle_page.reload_with_home_page_detour project_life_cycle_page.expect_listed(initiating_stage => true, ready_to_execute_gate => true, @@ -116,10 +125,14 @@ closing_stage => true) # The user can filter the life cycle steps - project_life_cycle_page.filter_by("Clo") + project_life_cycle_page.filter_by("ing") - project_life_cycle_page.expect_listed(ready_to_close_gate => true, + project_life_cycle_page.expect_listed(initiating_stage => true, + executing_stage => true, closing_stage => true) + + project_life_cycle_page.expect_not_listed(ready_to_execute_gate, + ready_to_close_gate) end end diff --git a/spec/support/pages/projects/settings/life_cycle.rb b/spec/support/pages/projects/settings/life_cycle.rb index 9c2769df814e..ab8ea8f0bcdd 100644 --- a/spec/support/pages/projects/settings/life_cycle.rb +++ b/spec/support/pages/projects/settings/life_cycle.rb @@ -46,13 +46,19 @@ def path # Checks if the life cycle steps are listed in the order given and with the correct toggle state. # @param life_cycle_definitions [Hash{LifeCycleElement => Boolean}] - def expect_listed(**life_cycle_definitions) - life_cycle_definitions.each_cons(2) do |(predecessor, _), (successor, _)| - expect(page).to have_css("#{life_cycle_test_selector(predecessor)} + #{life_cycle_test_selector(successor)}") + def expect_listed(**life_cycle_steps) + life_cycle_steps.each_cons(2) do |(predecessor, _), (successor, _)| + expect(page).to have_css("#{life_cycle_test_selector(predecessor)} ~ #{life_cycle_test_selector(successor)}") end - life_cycle_definitions.each do |definition, active| - expect_toggle_state(definition, active) + life_cycle_steps.each do |step, active| + expect_toggle_state(step, active) + end + end + + def expect_not_listed(*life_cycle_steps) + life_cycle_steps.each do |step| + expect(page).to have_no_css(life_cycle_test_selector(step)) end end @@ -92,6 +98,17 @@ def filter_by(filter) def expected_toggle_status(active) active ? "On" : "Off" end + + # Reloads the page and via the check carried out on the Home page + # a proper reload is ensured. + def reload_with_home_page_detour + visit home_path + + expect(page) + .to have_css(".PageHeader-title", text: "Home") + + visit! + end end end end From 9dd3d4d9511ec32eda53635340e41a7dc1d6d82e Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 9 Dec 2024 11:03:12 +0100 Subject: [PATCH 29/44] add no results text --- .../settings/life_cycles/index_component.html.erb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/components/projects/settings/life_cycles/index_component.html.erb b/app/components/projects/settings/life_cycles/index_component.html.erb index ffb3c63f334d..da00e1047a3e 100644 --- a/app/components/projects/settings/life_cycles/index_component.html.erb +++ b/app/components/projects/settings/life_cycles/index_component.html.erb @@ -76,5 +76,13 @@ end end end + flex.with_row do + render Primer::Beta::Text.new(display: :none, + data: { + "projects--settings--border-box-filter-target": "noResultsText", + }) do + I18n.t("js.autocompleter.notFoundText") + end + end end %> From 16d5bd943c68229e9d5442aadd6447e34cefb3df Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 9 Dec 2024 11:35:31 +0100 Subject: [PATCH 30/44] simplify variable handling of definition_id between view and controller --- .../life_cycles/step_component.html.erb | 7 +-- .../settings/life_cycles_controller.rb | 51 ++++--------------- app/models/project/gate_definition.rb | 4 ++ .../project/life_cycle_step_definition.rb | 4 ++ app/models/project/stage_definition.rb | 4 ++ spec/models/project/gate_definition_spec.rb | 6 +++ spec/models/project/stage_definition_spec.rb | 6 +++ 7 files changed, 34 insertions(+), 48 deletions(-) diff --git a/app/components/projects/settings/life_cycles/step_component.html.erb b/app/components/projects/settings/life_cycles/step_component.html.erb index 46e2899ecd55..d8bf4ebdd55f 100644 --- a/app/components/projects/settings/life_cycles/step_component.html.erb +++ b/app/components/projects/settings/life_cycles/step_component.html.erb @@ -16,12 +16,7 @@ # behavior can be seen on primer's viewbook as well -> https://view-components-storybook.eastus.cloudapp.azure.com/view-components/lookbook/inspect/primer/alpha/toggle_switch/small # quick fix: don't display loading indicator which is causing the bounce render(Primer::Alpha::ToggleSwitch.new( - src: toggle_project_settings_life_cycle_path( - project_life_cycle: { - definition_id: definition.id, - type: definition.type - } - ), + src: toggle_project_settings_life_cycle_path(definition_id: definition.id), csrf_token: form_authenticity_token, data: { test_selector: "toggle-project-life-cycle-#{definition.id}" }, aria: { label: toggle_aria_label }, diff --git a/app/controllers/projects/settings/life_cycles_controller.rb b/app/controllers/projects/settings/life_cycles_controller.rb index e7dcf078a76d..a9e0a4cb3a73 100644 --- a/app/controllers/projects/settings/life_cycles_controller.rb +++ b/app/controllers/projects/settings/life_cycles_controller.rb @@ -38,24 +38,19 @@ class Projects::Settings::LifeCyclesController < Projects::SettingsController def show; end def toggle - step_params = params - .require(:project_life_cycle) - .permit(:definition_id, :type) - .to_h - .symbolize_keys - .merge(active: params["value"] == "1") + definition = Project::LifeCycleStepDefinition.where(id: params[:definition_id]) - upsert_one_step(**step_params) + upsert_steps(definition, active: params["value"]) end def disable_all - upsert_all_steps(active: false) + upsert_steps(@life_cycle_definitions, active: false) redirect_to action: :show end def enable_all - upsert_all_steps(active: true) + upsert_steps(@life_cycle_definitions, active: true) redirect_to action: :show end @@ -70,45 +65,17 @@ def deny_access_on_feature_flag deny_access unless OpenProject::FeatureDecisions.stages_and_gates_active? end - def upsert_one_step(definition_id:, type:, active: true) - upsert_all( - [{ - project_id: @project.id, - definition_id:, - active:, - type: project_type_for_definition_type(type) - }] - ) - end - - def upsert_all_steps(active: true) - upsert_all( - @life_cycle_definitions.map do |definition| + def upsert_steps(definitions, active:) + Project::LifeCycleStep.upsert_all( + definitions.map do |definition| { project_id: @project.id, definition_id: definition.id, active:, - type: project_type_for_definition_type(definition.type) + type: definition.step_class } - end - ) - end - - def upsert_all(upserted_steps) - Project::LifeCycleStep.upsert_all( - upserted_steps, + end, unique_by: %i[project_id definition_id] ) end - - def project_type_for_definition_type(definition_type) - case definition_type - when Project::StageDefinition.name - Project::Stage - when Project::GateDefinition.name - Project::Gate - else - raise NotImplementedError, "Unknown life cycle definition type" - end - end end diff --git a/app/models/project/gate_definition.rb b/app/models/project/gate_definition.rb index a51561075728..a0cfef69352e 100644 --- a/app/models/project/gate_definition.rb +++ b/app/models/project/gate_definition.rb @@ -32,4 +32,8 @@ class Project::GateDefinition < Project::LifeCycleStepDefinition foreign_key: :definition_id, inverse_of: :definition, dependent: :destroy + + def step_class + Project::Gate + end end diff --git a/app/models/project/life_cycle_step_definition.rb b/app/models/project/life_cycle_step_definition.rb index d511cc284cc4..48e8e00d94bf 100644 --- a/app/models/project/life_cycle_step_definition.rb +++ b/app/models/project/life_cycle_step_definition.rb @@ -51,4 +51,8 @@ def initialize(*args) super end + + def step_class + raise NotImplementedError + end end diff --git a/app/models/project/stage_definition.rb b/app/models/project/stage_definition.rb index 91ae98584def..f373d13cd646 100644 --- a/app/models/project/stage_definition.rb +++ b/app/models/project/stage_definition.rb @@ -32,4 +32,8 @@ class Project::StageDefinition < Project::LifeCycleStepDefinition foreign_key: :definition_id, inverse_of: :definition, dependent: :destroy + + def step_class + Project::Stage + end end diff --git a/spec/models/project/gate_definition_spec.rb b/spec/models/project/gate_definition_spec.rb index 7d69be5a91ef..f943a416d9b9 100644 --- a/spec/models/project/gate_definition_spec.rb +++ b/spec/models/project/gate_definition_spec.rb @@ -40,4 +40,10 @@ .dependent(:destroy) end end + + describe "#step_class" do + it "returns Project::Stage" do + expect(subject.step_class).to eq(Project::Gate) + end + end end diff --git a/spec/models/project/stage_definition_spec.rb b/spec/models/project/stage_definition_spec.rb index f642056d4a34..1d94467a546a 100644 --- a/spec/models/project/stage_definition_spec.rb +++ b/spec/models/project/stage_definition_spec.rb @@ -40,4 +40,10 @@ .dependent(:destroy) end end + + describe "#step_class" do + it "returns Project::Stage" do + expect(subject.step_class).to eq(Project::Stage) + end + end end From 51126519f997ec3524e35862345987333c16d887 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 9 Dec 2024 12:55:39 +0100 Subject: [PATCH 31/44] use not found in case lifecycle feature flag is disabled --- app/controllers/projects/settings/life_cycles_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/settings/life_cycles_controller.rb b/app/controllers/projects/settings/life_cycles_controller.rb index a9e0a4cb3a73..c8f85edd0e89 100644 --- a/app/controllers/projects/settings/life_cycles_controller.rb +++ b/app/controllers/projects/settings/life_cycles_controller.rb @@ -62,7 +62,7 @@ def load_life_cycle_definitions end def deny_access_on_feature_flag - deny_access unless OpenProject::FeatureDecisions.stages_and_gates_active? + deny_access(not_found: true) unless OpenProject::FeatureDecisions.stages_and_gates_active? end def upsert_steps(definitions, active:) From 737f6fc7ac919a6aaf1ce1ef288f1c1c54353d73 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 9 Dec 2024 12:58:03 +0100 Subject: [PATCH 32/44] linting --- app/views/projects/settings/life_cycles/show.html.erb | 5 +++-- .../projects/settings/project_custom_fields/show.html.erb | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/projects/settings/life_cycles/show.html.erb b/app/views/projects/settings/life_cycles/show.html.erb index 910b74c8603f..47d2965a3c50 100644 --- a/app/views/projects/settings/life_cycles/show.html.erb +++ b/app/views/projects/settings/life_cycles/show.html.erb @@ -27,7 +27,8 @@ See COPYRIGHT and LICENSE files for more details. ++#%>
- <%= render Projects::Settings::LifeCycles::ShowPageHeaderComponent.new(project: @project) %> + <%= render(Projects::Settings::LifeCycles::ShowPageHeaderComponent.new(project: @project)) %> - <%= render(Projects::Settings::LifeCycles::IndexComponent.new( project: @project, life_cycle_definitions: @life_cycle_definitions)) %> + <%= render(Projects::Settings::LifeCycles::IndexComponent.new(project: @project, + life_cycle_definitions: @life_cycle_definitions)) %>
diff --git a/app/views/projects/settings/project_custom_fields/show.html.erb b/app/views/projects/settings/project_custom_fields/show.html.erb index 34103faae4c9..c3c4204a2876 100644 --- a/app/views/projects/settings/project_custom_fields/show.html.erb +++ b/app/views/projects/settings/project_custom_fields/show.html.erb @@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%>
- <%= render Projects::Settings::ProjectCustomFieldSections::IndexPageHeaderComponent.new(project: @project) %> + <%= render(Projects::Settings::ProjectCustomFieldSections::IndexPageHeaderComponent.new(project: @project)) %> <%= render(Projects::Settings::ProjectCustomFieldSections::IndexComponent.new( project: @project, From 3b9e838d4addb6ecc55b9680331d94a1806d97d5 Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 9 Dec 2024 13:01:25 +0100 Subject: [PATCH 33/44] remove by now unnecessary turbo wrapper --- .../settings/project_custom_fields/show.html.erb | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/views/projects/settings/project_custom_fields/show.html.erb b/app/views/projects/settings/project_custom_fields/show.html.erb index c3c4204a2876..be4ac50fa5f2 100644 --- a/app/views/projects/settings/project_custom_fields/show.html.erb +++ b/app/views/projects/settings/project_custom_fields/show.html.erb @@ -26,11 +26,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> -
- <%= render(Projects::Settings::ProjectCustomFieldSections::IndexPageHeaderComponent.new(project: @project)) %> - - <%= render(Projects::Settings::ProjectCustomFieldSections::IndexComponent.new( - project: @project, - project_custom_field_sections: @project_custom_field_sections, - )) %> -
+<%= render(Projects::Settings::ProjectCustomFieldSections::IndexPageHeaderComponent.new(project: @project)) %> + +<%= render(Projects::Settings::ProjectCustomFieldSections::IndexComponent.new( + project: @project, + project_custom_field_sections: @project_custom_field_sections, +)) %> From 8b9059c0fcc342d53747cd85a7a21143ebecfb5b Mon Sep 17 00:00:00 2001 From: ulferts Date: Mon, 9 Dec 2024 13:04:01 +0100 Subject: [PATCH 34/44] rely on default naming of index --- .../20241125161226_unique_index_on_project_life_cycle_steps.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/db/migrate/20241125161226_unique_index_on_project_life_cycle_steps.rb b/db/migrate/20241125161226_unique_index_on_project_life_cycle_steps.rb index 517b183985ce..e2358eb64f92 100644 --- a/db/migrate/20241125161226_unique_index_on_project_life_cycle_steps.rb +++ b/db/migrate/20241125161226_unique_index_on_project_life_cycle_steps.rb @@ -2,7 +2,6 @@ class UniqueIndexOnProjectLifeCycleSteps < ActiveRecord::Migration[7.1] def change add_index :project_life_cycle_steps, %i[project_id definition_id], - unique: true, - name: "index_project_life_cycle_steps_on_project_id_and_definition_id" + unique: true end end From ee94d56f2eadc869e8e3fb4e88b042dbdca53fe4 Mon Sep 17 00:00:00 2001 From: Ivan Kuchin Date: Wed, 4 Dec 2024 15:00:50 +0100 Subject: [PATCH 35/44] [#58161] Global stage administration https://community.openproject.org/work_packages/58161 From 5e8c99812626c19bd19b2c021e61e5cdb393c9f0 Mon Sep 17 00:00:00 2001 From: Ivan Kuchin Date: Wed, 4 Dec 2024 15:01:07 +0100 Subject: [PATCH 36/44] add menu entry --- config/initializers/menus.rb | 8 +++++++- config/locales/en.yml | 1 + config/routes.rb | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/config/initializers/menus.rb b/config/initializers/menus.rb index 6131ab47b6e3..09ed652acebc 100644 --- a/config/initializers/menus.rb +++ b/config/initializers/menus.rb @@ -337,10 +337,16 @@ menu.push :admin_projects_settings, { controller: "/admin/settings/project_custom_fields", action: :index }, - if: Proc.new { User.current.admin? }, + if: Proc.new { User.current.admin? && OpenProject::FeatureDecisions.stages_and_gates_active? }, caption: :label_project_plural, icon: "project" + menu.push :project_life_cycle_step_definitions_settings, + { controller: "/admin/settings/project_life_cycle_step_definitions", action: :index }, + if: Proc.new { User.current.admin? }, + caption: :label_project_lifecycle, + parent: :admin_projects_settings + menu.push :project_custom_fields_settings, { controller: "/admin/settings/project_custom_fields", action: :index }, if: Proc.new { User.current.admin? }, diff --git a/config/locales/en.yml b/config/locales/en.yml index a57949e0c335..d8c94c50e222 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2654,6 +2654,7 @@ en: label_project_new: "New project" label_project_plural: "Projects" label_project_list_plural: "Project lists" + label_project_lifecycle: "Project lifecycle" label_project_attributes_plural: "Project attributes" label_project_custom_field_plural: "Project attributes" label_project_settings: "Project settings" diff --git a/config/routes.rb b/config/routes.rb index 28810f171289..e00a997a6889 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -514,6 +514,7 @@ resource :progress_tracking, controller: "/admin/settings/progress_tracking", only: %i[show update] resource :projects, controller: "/admin/settings/projects_settings", only: %i[show update] resource :new_project, controller: "/admin/settings/new_project_settings", only: %i[show update] + resources :project_lifecycle, controller: "/admin/settings/project_life_cycle_step_definitions", only: %i[index] resources :project_custom_fields, controller: "/admin/settings/project_custom_fields" do member do delete "options/:option_id", action: "delete_option", as: :delete_option_of From f29eaa1ef101c771d63aa9d72b16644892a90328 Mon Sep 17 00:00:00 2001 From: Ivan Kuchin Date: Wed, 4 Dec 2024 19:26:08 +0100 Subject: [PATCH 37/44] make life cycle step definitions default to order by position --- app/models/project/life_cycle_step_definition.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/project/life_cycle_step_definition.rb b/app/models/project/life_cycle_step_definition.rb index 48e8e00d94bf..892d063aec4e 100644 --- a/app/models/project/life_cycle_step_definition.rb +++ b/app/models/project/life_cycle_step_definition.rb @@ -42,6 +42,8 @@ class Project::LifeCycleStepDefinition < ApplicationRecord acts_as_list + default_scope { order(:position) } + def initialize(*args) if instance_of? Project::LifeCycleStepDefinition # Do not allow directly instantiating this class From 64dcb5e0595d045fb6a6aa96808b3421afa7d802 Mon Sep 17 00:00:00 2001 From: Ivan Kuchin Date: Mon, 9 Dec 2024 19:04:54 +0100 Subject: [PATCH 38/44] comment out validation that Project::LifeCycleStepDefinition is not initialized itself --- .../project/life_cycle_step_definition.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/models/project/life_cycle_step_definition.rb b/app/models/project/life_cycle_step_definition.rb index 892d063aec4e..fbc9f9891cd5 100644 --- a/app/models/project/life_cycle_step_definition.rb +++ b/app/models/project/life_cycle_step_definition.rb @@ -44,15 +44,15 @@ class Project::LifeCycleStepDefinition < ApplicationRecord default_scope { order(:position) } - def initialize(*args) - if instance_of? Project::LifeCycleStepDefinition - # Do not allow directly instantiating this class - raise NotImplementedError, "Cannot instantiate the base Project::LifeCycleStepDefinition class directly. " \ - "Use Project::StageDefinition or Project::GateDefinition instead." - end - - super - end + # def initialize(*args) + # if instance_of? Project::LifeCycleStepDefinition + # # Do not allow directly instantiating this class + # raise NotImplementedError, "Cannot instantiate the base Project::LifeCycleStepDefinition class directly. " \ + # "Use Project::StageDefinition or Project::GateDefinition instead." + # end + # + # super + # end def step_class raise NotImplementedError From 4abfa7f356303887c58262bf14ce74b70347f799 Mon Sep 17 00:00:00 2001 From: Ivan Kuchin Date: Wed, 11 Dec 2024 13:40:22 +0100 Subject: [PATCH 39/44] routing --- config/routes.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index e00a997a6889..5bcbe7ae186b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -514,7 +514,13 @@ resource :progress_tracking, controller: "/admin/settings/progress_tracking", only: %i[show update] resource :projects, controller: "/admin/settings/projects_settings", only: %i[show update] resource :new_project, controller: "/admin/settings/new_project_settings", only: %i[show update] - resources :project_lifecycle, controller: "/admin/settings/project_life_cycle_step_definitions", only: %i[index] + resources :project_life_cycle_step_definitions, controller: "/admin/settings/project_life_cycle_step_definitions", + only: %i[index create edit update] do + collection do + get :new_stage + get :new_gate + end + end resources :project_custom_fields, controller: "/admin/settings/project_custom_fields" do member do delete "options/:option_id", action: "delete_option", as: :delete_option_of From d46d1c4d8db2c27488a9d0dc463bddffd825bac8 Mon Sep 17 00:00:00 2001 From: Ivan Kuchin Date: Wed, 4 Dec 2024 16:19:47 +0100 Subject: [PATCH 40/44] index header --- .../index_header_component.html.erb | 71 +++++++++++++++++++ .../index_header_component.rb | 45 ++++++++++++ .../index.html.erb | 32 +++++++++ config/locales/en.yml | 7 ++ 4 files changed, 155 insertions(+) create mode 100644 app/components/settings/project_life_cycle_step_definitions/index_header_component.html.erb create mode 100644 app/components/settings/project_life_cycle_step_definitions/index_header_component.rb create mode 100644 app/views/admin/settings/project_life_cycle_step_definitions/index.html.erb diff --git a/app/components/settings/project_life_cycle_step_definitions/index_header_component.html.erb b/app/components/settings/project_life_cycle_step_definitions/index_header_component.html.erb new file mode 100644 index 000000000000..a3c72f96e17d --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/index_header_component.html.erb @@ -0,0 +1,71 @@ +<%#-- 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 do %> + <%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { t("settings.project_life_cycle_step_definitions.heading") } + header.with_description { t("settings.project_life_cycle_step_definitions.heading_description") } + header.with_breadcrumbs(breadcrumbs_items) + end + %> + + <%= + render(Primer::OpenProject::SubHeader.new) do |subheader| + subheader.with_action_component do + render(Primer::Alpha::ActionMenu.new( + anchor_align: :end) + ) do |menu| + menu.with_show_button( + scheme: :primary, + aria: { label: I18n.t("settings.project_life_cycle_step_definitions.label_add_description") }, + ) do |button| + button.with_leading_visual_icon(icon: :plus) + button.with_trailing_action_icon(icon: :"triangle-down") + I18n.t("settings.project_life_cycle_step_definitions.label_add") + end + + menu.with_item( + label: I18n.t("settings.project_life_cycle_step_definitions.label_add_stage"), + href: new_stage_admin_settings_project_life_cycle_step_definitions_path + ) do |item| + item.with_leading_visual_icon(icon: "git-commit") + end + + menu.with_item( + label: I18n.t("settings.project_life_cycle_step_definitions.label_add_gate"), + href: new_gate_admin_settings_project_life_cycle_step_definitions_path + ) do |item| + item.with_leading_visual_icon(icon: "diamond") + end + end + end + end + %> +<% end %> diff --git a/app/components/settings/project_life_cycle_step_definitions/index_header_component.rb b/app/components/settings/project_life_cycle_step_definitions/index_header_component.rb new file mode 100644 index 000000000000..b0cc9d66a6bc --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/index_header_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 Settings + module ProjectLifeCycleStepDefinitions + class IndexHeaderComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def breadcrumbs_items + [ + { href: admin_index_path, text: t("label_administration") }, + { href: admin_settings_project_custom_fields_path, text: t("label_project_plural") }, + t("settings.project_life_cycle_step_definitions.heading") + ] + end + end + end +end diff --git a/app/views/admin/settings/project_life_cycle_step_definitions/index.html.erb b/app/views/admin/settings/project_life_cycle_step_definitions/index.html.erb new file mode 100644 index 000000000000..246406272120 --- /dev/null +++ b/app/views/admin/settings/project_life_cycle_step_definitions/index.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. + +++#%> + +<% html_title t(:label_administration), t("settings.project_life_cycle_step_definitions.heading") %> + +<%= render Settings::ProjectLifeCycleStepDefinitions::IndexHeaderComponent.new %> diff --git a/config/locales/en.yml b/config/locales/en.yml index d8c94c50e222..f28de1497043 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3697,6 +3697,13 @@ en: new: heading: "New attribute" description: "Changes to this project attribute will be reflected in all projects where it is enabled. Required attributes cannot be disabled on a per-project basis." + project_life_cycle_step_definitions: + heading: "Project lifecycle" + heading_description: "Project lifecycle define the project stages and gates that can be used for your project planning and will appear in the overview page of each project. These attributes can be enabled or disabled but not re-ordered at a project level." + label_add: "Add" + label_add_description: "Add lifecycle definition" + label_add_stage: "Stage" + label_add_gate: "Gate" projects: missing_dependencies: "Project module %{module} was checked which depends on %{dependencies}. You need to check these dependencies as well." section_new_projects: "Settings for new projects" From 622e5047da18c7325fb134d7068f29007b3e5b55 Mon Sep 17 00:00:00 2001 From: Ivan Kuchin Date: Wed, 11 Dec 2024 13:38:18 +0100 Subject: [PATCH 41/44] add form and new/edit actions --- .../form_header_component.html.erb | 40 +++++++++ .../form_header_component.rb | 55 ++++++++++++ ..._life_cycle_step_definitions_controller.rb | 87 +++++++++++++++++++ .../life_cycle_step_definitions/form.rb | 58 +++++++++++++ .../form.html.erb | 40 +++++++++ config/locales/en.yml | 13 +++ 6 files changed, 293 insertions(+) create mode 100644 app/components/settings/project_life_cycle_step_definitions/form_header_component.html.erb create mode 100644 app/components/settings/project_life_cycle_step_definitions/form_header_component.rb create mode 100644 app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb create mode 100644 app/forms/projects/life_cycle_step_definitions/form.rb create mode 100644 app/views/admin/settings/project_life_cycle_step_definitions/form.html.erb diff --git a/app/components/settings/project_life_cycle_step_definitions/form_header_component.html.erb b/app/components/settings/project_life_cycle_step_definitions/form_header_component.html.erb new file mode 100644 index 000000000000..f837b215a221 --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/form_header_component.html.erb @@ -0,0 +1,40 @@ +<%#-- 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. + +++#%> + +<% html_title t(:label_administration), + t("settings.project_life_cycle_step_definitions.heading"), + t("settings.project_life_cycle_step_definitions.#{heading_scope}.heading") %> + +<%= component_wrapper do + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { t("settings.project_life_cycle_step_definitions.#{heading_scope}.heading") } + header.with_description { t("settings.project_life_cycle_step_definitions.new.description") } + header.with_breadcrumbs(breadcrumbs_items) + end +end %> diff --git a/app/components/settings/project_life_cycle_step_definitions/form_header_component.rb b/app/components/settings/project_life_cycle_step_definitions/form_header_component.rb new file mode 100644 index 000000000000..16d97b73bbec --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/form_header_component.rb @@ -0,0 +1,55 @@ +#-- 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 Settings + module ProjectLifeCycleStepDefinitions + class FormHeaderComponent < ApplicationComponent + include ApplicationHelper + include MetaTagsHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def heading_scope + action = model.persisted? ? :edit : :new + + type = model.is_a?(Project::StageDefinition) ? :stage : :gate + + "#{action}_#{type}" + end + + def breadcrumbs_items + [ + { href: admin_index_path, text: t("label_administration") }, + { href: admin_settings_project_custom_fields_path, text: t("label_project_plural") }, + { href: admin_settings_project_life_cycle_step_definitions_path, text: t("settings.project_life_cycle_step_definitions.heading") }, + t("settings.project_life_cycle_step_definitions.#{heading_scope}.heading") + ] + end + end + end +end diff --git a/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb b/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb new file mode 100644 index 000000000000..1d7360c20fb9 --- /dev/null +++ b/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb @@ -0,0 +1,87 @@ +#-- 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::Settings + class ProjectLifeCycleStepDefinitionsController < ::Admin::SettingsController + menu_item :project_life_cycle_step_definitions_settings + + before_action :check_feature_flag + + before_action :find_definition, only: %i[edit update] + + def new_stage + @definition = Project::StageDefinition.new + + render :form + end + + def new_gate + @definition = Project::GateDefinition.new + + render :form + end + + def edit + render :form + end + + def create + @definition = Project::LifeCycleStepDefinition.new(definition_params) + + if @definition.save + flash[:notice] = I18n.t(:notice_successful_create) + redirect_to action: :index + else + render :form, status: :unprocessable_entity + end + end + + def update + if @definition.update(definition_params) + flash[:notice] = I18n.t(:notice_successful_update) + redirect_to action: :index + else + render :form, status: :unprocessable_entity + end + end + + private + + def check_feature_flag + render_404 unless OpenProject::FeatureDecisions.stages_and_gates_active? + end + + def find_definition + @definition = Project::LifeCycleStepDefinition.find(params[:id]) + end + + def definition_params + params.require(:project_life_cycle_step_definition).permit(:type, :name, :color_id) + end + end +end diff --git a/app/forms/projects/life_cycle_step_definitions/form.rb b/app/forms/projects/life_cycle_step_definitions/form.rb new file mode 100644 index 000000000000..56e39be9706e --- /dev/null +++ b/app/forms/projects/life_cycle_step_definitions/form.rb @@ -0,0 +1,58 @@ +#-- 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 Projects::LifeCycleStepDefinitions + class Form < ApplicationForm + form do |f| + f.hidden(name: :type) unless model.persisted? + + f.text_field( + label: attribute_name(:name), + name: :name, + required: true, + input_width: :medium + ) + + f.color_select_list( + label: attribute_name(:color), + name: :color, + required: true + ) + + f.submit( + scheme: :primary, + name: :submit, + label: submit_label + ) + end + + def submit_label + I18n.t(model.persisted? ? :button_update : :button_create) + end + end +end diff --git a/app/views/admin/settings/project_life_cycle_step_definitions/form.html.erb b/app/views/admin/settings/project_life_cycle_step_definitions/form.html.erb new file mode 100644 index 000000000000..571dadab4433 --- /dev/null +++ b/app/views/admin/settings/project_life_cycle_step_definitions/form.html.erb @@ -0,0 +1,40 @@ +<%#-- 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. + +++#%> + +<%= render Settings::ProjectLifeCycleStepDefinitions::FormHeaderComponent.new(@definition) %> + +<%= error_messages_for @definition %> + +<%= + primer_form_with( + model: [:admin, :settings, @definition.becomes(Project::LifeCycleStepDefinition)] + ) do |f| + render Projects::LifeCycleStepDefinitions::Form.new(f) + end +%> diff --git a/config/locales/en.yml b/config/locales/en.yml index f28de1497043..e238cacadb3b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -928,6 +928,9 @@ en: project_custom_field: is_required: "Required for all projects" custom_field_section: Section + project/life_cycle_step_definition: + name: "Name" + color: "Color" query: column_names: "Columns" relations_to_type_column: "Relations to %{type}" @@ -3704,6 +3707,16 @@ en: label_add_description: "Add lifecycle definition" label_add_stage: "Stage" label_add_gate: "Gate" + new: + description: "Changes to this project step will be reflected in all projects where it is enabled." + new_stage: + heading: "New stage" + new_gate: + heading: "New gate" + edit_stage: + heading: "Edit stage" + edit_gate: + heading: "Edit gate" projects: missing_dependencies: "Project module %{module} was checked which depends on %{dependencies}. You need to check these dependencies as well." section_new_projects: "Settings for new projects" From 6d936672fb09c9154133691de375a538fa3933a1 Mon Sep 17 00:00:00 2001 From: Ivan Kuchin Date: Wed, 11 Dec 2024 20:30:06 +0100 Subject: [PATCH 42/44] Project::LifeCycleStepDefinition.with_project_count --- .../project/life_cycle_step_definition.rb | 4 ++ .../scopes/with_project_count.rb | 48 ++++++++++++++++ .../scopes/with_project_count_spec.rb | 57 +++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 app/models/project/life_cycle_step_definitions/scopes/with_project_count.rb create mode 100644 spec/models/project/life_cycle_step_definitions/scopes/with_project_count_spec.rb diff --git a/app/models/project/life_cycle_step_definition.rb b/app/models/project/life_cycle_step_definition.rb index fbc9f9891cd5..1b2829616cfc 100644 --- a/app/models/project/life_cycle_step_definition.rb +++ b/app/models/project/life_cycle_step_definition.rb @@ -27,6 +27,8 @@ #++ class Project::LifeCycleStepDefinition < ApplicationRecord + include ::Scopes::Scoped + has_many :life_cycle_steps, class_name: "Project::LifeCycleStep", foreign_key: :definition_id, @@ -44,6 +46,8 @@ class Project::LifeCycleStepDefinition < ApplicationRecord default_scope { order(:position) } + scopes :with_project_count + # def initialize(*args) # if instance_of? Project::LifeCycleStepDefinition # # Do not allow directly instantiating this class diff --git a/app/models/project/life_cycle_step_definitions/scopes/with_project_count.rb b/app/models/project/life_cycle_step_definitions/scopes/with_project_count.rb new file mode 100644 index 000000000000..53cf99c582dc --- /dev/null +++ b/app/models/project/life_cycle_step_definitions/scopes/with_project_count.rb @@ -0,0 +1,48 @@ +#-- 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 Project::LifeCycleStepDefinitions::Scopes + module WithProjectCount + extend ActiveSupport::Concern + + class_methods do + def with_project_count + project_counts = Project::LifeCycleStep + .where(active: true) + .group(:definition_id) + .select(:definition_id, "COUNT(project_id) AS count") + + Project::LifeCycleStepDefinition + .with(project_counts:) + .joins("LEFT OUTER JOIN project_counts ON #{quoted_table_name}.id = project_counts.definition_id") + .select("#{quoted_table_name}.*") + .select("COALESCE(project_counts.count, 0) AS project_count") + end + end + end +end diff --git a/spec/models/project/life_cycle_step_definitions/scopes/with_project_count_spec.rb b/spec/models/project/life_cycle_step_definitions/scopes/with_project_count_spec.rb new file mode 100644 index 000000000000..5161d12ded63 --- /dev/null +++ b/spec/models/project/life_cycle_step_definitions/scopes/with_project_count_spec.rb @@ -0,0 +1,57 @@ +#-- 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 Project::LifeCycleStepDefinitions::Scopes::WithProjectCount do + let!(:definition_a) { create(:project_stage_definition, name: "foo") } + let!(:definition_b) { create(:project_gate_definition, name: "bar") } + let!(:definition_c) { create(:project_stage_definition, name: "baz") } + + before do + create(:project).tap do |project| + create(:project_stage, project:, definition: definition_a) + create(:project_stage, project:, definition: definition_b) + end + + create(:project).tap do |project| + create(:project_stage, project:, definition: definition_a) + create(:project_stage, project:, definition: definition_b, active: false) + end + end + + describe ".with_project_count" do + it "queries project counts alongside definitions" do + expect(Project::LifeCycleStepDefinition.with_project_count).to contain_exactly( + having_attributes(id: definition_a.id, name: "foo", project_count: 2), + having_attributes(id: definition_b.id, name: "bar", project_count: 1), + having_attributes(id: definition_c.id, name: "baz", project_count: 0) + ) + end + end +end From e4762146f277e5aa608a894fcba5321e47e51ac7 Mon Sep 17 00:00:00 2001 From: Ivan Kuchin Date: Wed, 11 Dec 2024 13:44:10 +0100 Subject: [PATCH 43/44] basic index - list definitions --- .../index_component.html.erb | 87 +++++++++++++++++++ .../index_component.rb | 48 ++++++++++ .../row_component.html.erb | 48 ++++++++++ .../row_component.rb | 39 +++++++++ ..._life_cycle_step_definitions_controller.rb | 7 ++ .../index.html.erb | 1 + config/locales/en.yml | 4 + 7 files changed, 234 insertions(+) create mode 100644 app/components/settings/project_life_cycle_step_definitions/index_component.html.erb create mode 100644 app/components/settings/project_life_cycle_step_definitions/index_component.rb create mode 100644 app/components/settings/project_life_cycle_step_definitions/row_component.html.erb create mode 100644 app/components/settings/project_life_cycle_step_definitions/row_component.rb diff --git a/app/components/settings/project_life_cycle_step_definitions/index_component.html.erb b/app/components/settings/project_life_cycle_step_definitions/index_component.html.erb new file mode 100644 index 000000000000..e86855649e38 --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/index_component.html.erb @@ -0,0 +1,87 @@ +<%#-- 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. + +++#%> + +<%= + flex_layout(data: wrapper_data_attributes) do |flex| + flex.with_row do + render(Primer::OpenProject::SubHeader.new) do |subheader| + subheader.with_filter_input( + name: "border-box-filter", + label: t("settings.project_life_cycle_step_definitions.filter.label"), + visually_hide_label: true, + placeholder: t("settings.project_life_cycle_step_definitions.filter.label"), + leading_visual: { + icon: :search, + size: :small + }, + show_clear_button: true, + data: { + action: "input->projects--settings--border-box-filter#filterLists", + "projects--settings--border-box-filter-target": "filter" + } + ) + end + end + + flex.with_row do + render(border_box_container(mb: 3)) do |component| + component.with_header(font_weight: :bold, py: 2) do + flex_layout(justify_content: :space_between, align_items: :center) do |header_container| + header_container.with_column do + render(Primer::Beta::Text.new(font_weight: :bold)) do + I18n.t("settings.project_life_cycle_step_definitions.section_header") + end + end + end + end + if definitions.empty? + component.with_row do + render(Primer::Beta::Text.new(color: :subtle)) { t("settings.project_life_cycle_step_definitions.non_defined") } + end + else + definitions.each do |definition| + component.with_row(data: { "projects--settings--border-box-filter-target": "searchItem" }) do + render(Settings::ProjectLifeCycleStepDefinitions::RowComponent.new(definition)) + end + end + end + end + end + flex.with_row do + render Primer::Beta::Text.new( + display: :none, + data: { + "projects--settings--border-box-filter-target": "noResultsText", + } + ) do + I18n.t("js.autocompleter.notFoundText") + end + end + end +%> diff --git a/app/components/settings/project_life_cycle_step_definitions/index_component.rb b/app/components/settings/project_life_cycle_step_definitions/index_component.rb new file mode 100644 index 000000000000..aea68cdca81e --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/index_component.rb @@ -0,0 +1,48 @@ +#-- 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 Settings + module ProjectLifeCycleStepDefinitions + class IndexComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + options :definitions + + private + + def wrapper_data_attributes + { + controller: "projects--settings--border-box-filter", + "application-target": "dynamic" + } + end + end + end +end diff --git a/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb b/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb new file mode 100644 index 000000000000..711efabbe011 --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb @@ -0,0 +1,48 @@ +<%#-- 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. + +++#%> + +<%= + flex_layout(align_items: :center, + justify_content: :space_between) do |step_container| + step_container.with_column(flex_layout: true) do |title_container| + title_container.with_column(pt: 1, mr: 3) do + render(Primer::Beta::Link.new( + classes: 'filter-target-visible-text', + href: edit_admin_settings_project_life_cycle_step_definition_path(definition), + font_weight: :bold + )) do + definition.name + end + end + title_container.with_column(pt: 1) do + render(Projects::LifeCycleTypeComponent.new(definition)) + end + end + end +%> diff --git a/app/components/settings/project_life_cycle_step_definitions/row_component.rb b/app/components/settings/project_life_cycle_step_definitions/row_component.rb new file mode 100644 index 000000000000..19c88cafc88c --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/row_component.rb @@ -0,0 +1,39 @@ +#-- 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 Settings + module ProjectLifeCycleStepDefinitions + class RowComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + alias_method :definition, :model + end + end +end diff --git a/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb b/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb index 1d7360c20fb9..584804e477ad 100644 --- a/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb +++ b/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb @@ -32,8 +32,11 @@ class ProjectLifeCycleStepDefinitionsController < ::Admin::SettingsController before_action :check_feature_flag + before_action :find_definitions, only: %i[index] before_action :find_definition, only: %i[edit update] + def index; end + def new_stage @definition = Project::StageDefinition.new @@ -76,6 +79,10 @@ def check_feature_flag render_404 unless OpenProject::FeatureDecisions.stages_and_gates_active? end + def find_definitions + @definitions = Project::LifeCycleStepDefinition.all + end + def find_definition @definition = Project::LifeCycleStepDefinition.find(params[:id]) end diff --git a/app/views/admin/settings/project_life_cycle_step_definitions/index.html.erb b/app/views/admin/settings/project_life_cycle_step_definitions/index.html.erb index 246406272120..cf8877a719e5 100644 --- a/app/views/admin/settings/project_life_cycle_step_definitions/index.html.erb +++ b/app/views/admin/settings/project_life_cycle_step_definitions/index.html.erb @@ -30,3 +30,4 @@ See COPYRIGHT and LICENSE files for more details. <% html_title t(:label_administration), t("settings.project_life_cycle_step_definitions.heading") %> <%= render Settings::ProjectLifeCycleStepDefinitions::IndexHeaderComponent.new %> +<%= render Settings::ProjectLifeCycleStepDefinitions::IndexComponent.new(definitions: @definitions) %> diff --git a/config/locales/en.yml b/config/locales/en.yml index e238cacadb3b..1d50ec75ce68 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3707,6 +3707,10 @@ en: label_add_description: "Add lifecycle definition" label_add_stage: "Stage" label_add_gate: "Gate" + filter: + label: "Search project stage or gate" + section_header: "Stages and gates" + non_defined: "Neither stages nor gates are currently defined." new: description: "Changes to this project step will be reflected in all projects where it is enabled." new_stage: From 516961c6f01085097fa253697d39d84c22a0edec Mon Sep 17 00:00:00 2001 From: Ivan Kuchin Date: Wed, 11 Dec 2024 20:39:05 +0100 Subject: [PATCH 44/44] show project count in index --- .../row_component.html.erb | 3 +++ .../project_life_cycle_step_definitions_controller.rb | 2 +- config/locales/en.yml | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb b/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb index 711efabbe011..94f671cdaab3 100644 --- a/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb +++ b/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb @@ -43,6 +43,9 @@ See COPYRIGHT and LICENSE files for more details. title_container.with_column(pt: 1) do render(Projects::LifeCycleTypeComponent.new(definition)) end + title_container.with_column(pt: 1, ml: 3) do + render(Primer::Beta::Text.new) { t("project.count_lowercase", count: definition.project_count) } + end end end %> diff --git a/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb b/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb index 584804e477ad..6afae30c38d9 100644 --- a/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb +++ b/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb @@ -80,7 +80,7 @@ def check_feature_flag end def find_definitions - @definitions = Project::LifeCycleStepDefinition.all + @definitions = Project::LifeCycleStepDefinition.with_project_count end def find_definition diff --git a/config/locales/en.yml b/config/locales/en.yml index 1d50ec75ce68..3f8a74baa130 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3283,6 +3283,10 @@ en: zero: "0 Projects" one: "1 Project" other: "%{count} Projects" + count_lowercase: + zero: "0 projects" + one: "1 project" + other: "%{count} projects" project_module_activity: "Activity" project_module_forums: "Forums"