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..921c1cf191a2 --- /dev/null +++ b/app/components/projects/life_cycle_type_component.html.erb @@ -0,0 +1,8 @@ +<%= 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 + type_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..453f5d90c5d6 --- /dev/null +++ b/app/components/projects/life_cycle_type_component.rb @@ -0,0 +1,60 @@ +# 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 Project::StageDefinition + :"git-commit" + when Project::GateDefinition + :diamond + else + raise NotImplementedError, "Unknown model #{model.class} to render a LifeCycleTypeComponent with" + end + end + + def icon_color_class + helpers.hl_inline_class("life_cycle_step_definition", model) + end + + def text_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/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..da00e1047a3e --- /dev/null +++ b/app/components/projects/settings/life_cycles/index_component.html.erb @@ -0,0 +1,88 @@ +<%= + 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 + 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, 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, 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 + 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 + 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/projects/settings/life_cycles/index_component.rb b/app/components/projects/settings/life_cycles/index_component.rb new file mode 100644 index 000000000000..403c26152629 --- /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 + + options :project, + :life_cycle_definitions + + private + + 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 + end +end 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..b6845944e263 --- /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 { 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 new file mode 100644 index 000000000000..9641ab894d4a --- /dev/null +++ b/app/components/projects/settings/life_cycles/show_page_header_component.rb @@ -0,0 +1,43 @@ +# 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 + + options :project + + def breadcrumb_items + [{ 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 +end diff --git a/app/components/projects/settings/life_cycles/step_component.html.erb b/app/components/projects/settings/life_cycles/step_component.html.erb new file mode 100644 index 000000000000..d8bf4ebdd55f --- /dev/null +++ b/app/components/projects/settings/life_cycles/step_component.html.erb @@ -0,0 +1,30 @@ +<%= + 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::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 + 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 + # quick fix: don't display loading indicator which is causing the bounce + render(Primer::Alpha::ToggleSwitch.new( + 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 }, + 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 new file mode 100644 index 000000000000..3f32e3d5c24f --- /dev/null +++ b/app/components/projects/settings/life_cycles/step_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 StepComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + options :definition, + :project + + def active_in_project? + project + .life_cycle_steps + .detect { |project_lc| project_lc.definition_id == definition.id } + &.active + end + + def toggle_aria_label + I18n.t("projects.settings.life_cycle.step.use_in_project", step: definition.name) + end + end + end + end +end 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/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/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/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..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( @@ -25,14 +25,14 @@ 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 + 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( @@ -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 @@ -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/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..8c2da7abe2e4 --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/form_header_component.html.erb @@ -0,0 +1,38 @@ +<%#-- 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") %> + +<%= 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 %> 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..1c7dba5d796f --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/form_header_component.rb @@ -0,0 +1,52 @@ +#-- 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 MetaTagsHelper + + 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/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..3da4d4ceb4f5 --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/index_component.html.erb @@ -0,0 +1,91 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + +<%= + 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, data: drop_target_config)) 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", **draggable_item_config(definition) }) do + render(Settings::ProjectLifeCycleStepDefinitions::RowComponent.new( + definition, + first?: definition == definitions.first, + last?: definition == definitions.last, + )) + 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..f34bf7f8a911 --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/index_component.rb @@ -0,0 +1,61 @@ +#-- 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 OpPrimer::ComponentHelpers + + options :definitions + + private + + def wrapper_data_attributes + { + controller: "projects--settings--border-box-filter generic-drag-and-drop", + "application-target": "dynamic" + } + end + + def drop_target_config + { + "is-drag-and-drop-target": true, + "target-container-accessor": "& > ul", + "target-allowed-drag-type": "life-cycle-step-definition" + } + end + + def draggable_item_config(definition) + { + "draggable-type": "life-cycle-step-definition", + "drop-url": drop_admin_settings_project_life_cycle_step_definition_path(definition) + } + end + end + end +end 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..7c1d55449bde --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/index_header_component.html.erb @@ -0,0 +1,69 @@ +<%#-- 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 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 +%> 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..6622a0f61b88 --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/index_header_component.rb @@ -0,0 +1,42 @@ +#-- 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 + + 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/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..91c3064f4c4e --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb @@ -0,0 +1,88 @@ +<%#-- 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 |row_container| + row_container.with_column(flex_layout: true, classes: "gap-2") do |title_container| + title_container.with_column do + render(Primer::OpenProject::DragHandle.new) + end + title_container.with_column 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 do + render(Projects::LifeCycleTypeComponent.new(definition)) + end + title_container.with_column do + render(Primer::Beta::Text.new) { t("project.count", count: definition.project_count) } + end + end + row_container.with_column do + render(Primer::Alpha::ActionMenu.new) do |menu| + menu.with_show_button(icon: "kebab-horizontal", "aria-label": t(:button_actions), scheme: :invisible) + + menu.with_item( + label: t(:label_edit), + href: edit_admin_settings_project_life_cycle_step_definition_path(definition) + ) do |item| + item.with_leading_visual_icon(icon: :pencil) + end + + unless first? + move_action(menu:, move_to: :highest, label: t("label_agenda_item_move_to_top"), icon: "move-to-top") + move_action(menu:, move_to: :higher, label: t("label_agenda_item_move_up"), icon: "chevron-up") + end + unless last? + move_action(menu:, move_to: :lower, label: t("label_agenda_item_move_down"), icon: "chevron-down") + move_action(menu:, move_to: :lowest, label: t("label_agenda_item_move_to_bottom"), icon: "move-to-bottom") + end + + menu.with_item( + label: t(:text_destroy), + scheme: :danger, + href: admin_settings_project_life_cycle_step_definition_path(definition), + form_arguments: { + method: :delete, + data: { + confirm: t("text_are_you_sure_with_project_life_cycle_step") + } + } + ) do |item| + item.with_leading_visual_icon(icon: :trash) + end + 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..1f165751a6c4 --- /dev/null +++ b/app/components/settings/project_life_cycle_step_definitions/row_component.rb @@ -0,0 +1,53 @@ +#-- 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 OpPrimer::ComponentHelpers + + alias_method :definition, :model + + options :first?, :last? + + private + + def move_action(menu:, move_to:, label:, icon:) + menu.with_item( + label:, + href: move_admin_settings_project_life_cycle_step_definition_path(definition, move_to:), + form_arguments: { + method: :patch + } + ) do |item| + item.with_leading_visual_icon(icon:) + end + 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..402101a25870 --- /dev/null +++ b/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb @@ -0,0 +1,127 @@ +#-- 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_definitions, only: %i[index] + before_action :find_definition, only: %i[edit update destroy move drop] + + def index; end + + 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, status: :see_other + 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, status: :see_other + else + render :form, status: :unprocessable_entity + end + end + + def destroy + if @definition.destroy + flash[:notice] = I18n.t(:notice_successful_delete) + else + # TODO: handle better + flash[:error] = I18n.t(:notice_bad_request) + end + + redirect_to action: :index, status: :see_other + end + + def move + if @definition.update(params.permit(:move_to)) + flash[:notice] = I18n.t(:notice_successful_update) + else + # TODO: handle better + flash[:error] = I18n.t(:notice_bad_request) + end + + redirect_to action: :index, status: :see_other + end + + def drop + if @definition.update(params.permit(:position)) + flash[:notice] = I18n.t(:notice_successful_update) + else + # TODO: handle better + flash[:error] = I18n.t(:notice_bad_request) + end + + redirect_to action: :index, status: :see_other + end + + private + + def check_feature_flag + render_404 unless OpenProject::FeatureDecisions.stages_and_gates_active? + end + + def find_definitions + @definitions = Project::LifeCycleStepDefinition.with_project_count + 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/controllers/projects/settings/life_cycles_controller.rb b/app/controllers/projects/settings/life_cycles_controller.rb new file mode 100644 index 000000000000..c8f85edd0e89 --- /dev/null +++ b/app/controllers/projects/settings/life_cycles_controller.rb @@ -0,0 +1,81 @@ +# -- 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 + + before_action :deny_access_on_feature_flag + + before_action :load_life_cycle_definitions, only: %i[show enable_all disable_all] + + menu_item :settings_life_cycles + + def show; end + + def toggle + definition = Project::LifeCycleStepDefinition.where(id: params[:definition_id]) + + upsert_steps(definition, active: params["value"]) + end + + def disable_all + upsert_steps(@life_cycle_definitions, active: false) + + redirect_to action: :show + end + + def enable_all + upsert_steps(@life_cycle_definitions, active: true) + + redirect_to action: :show + end + + private + + def load_life_cycle_definitions + @life_cycle_definitions = Project::LifeCycleStepDefinition.order(position: :asc) + end + + def deny_access_on_feature_flag + deny_access(not_found: true) unless OpenProject::FeatureDecisions.stages_and_gates_active? + end + + def upsert_steps(definitions, active:) + Project::LifeCycleStep.upsert_all( + definitions.map do |definition| + { + project_id: @project.id, + definition_id: definition.id, + active:, + type: definition.step_class + } + end, + unique_by: %i[project_id definition_id] + ) + 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/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..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, @@ -42,13 +44,21 @@ class Project::LifeCycleStepDefinition < ApplicationRecord acts_as_list - 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 + default_scope { order(:position) } + + scopes :with_project_count + + # 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 - super + def step_class + raise NotImplementedError end end 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/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/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/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..cf8877a719e5 --- /dev/null +++ b/app/views/admin/settings/project_life_cycle_step_definitions/index.html.erb @@ -0,0 +1,33 @@ +<%#-- 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 %> +<%= render Settings::ProjectLifeCycleStepDefinitions::IndexComponent.new(definitions: @definitions) %> 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..47d2965a3c50 --- /dev/null +++ b/app/views/projects/settings/life_cycles/show.html.erb @@ -0,0 +1,34 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> +
+ <%= render(Projects::Settings::LifeCycles::ShowPageHeaderComponent.new(project: @project)) %> + + <%= 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..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, +)) %> diff --git a/config/initializers/menus.rb b/config/initializers/menus.rb index 33bdb9cd0d25..37ff61b365e3 100644 --- a/config/initializers/menus.rb +++ b/config/initializers/menus.rb @@ -341,6 +341,12 @@ 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? && OpenProject::FeatureDecisions.stages_and_gates_active? }, + 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? }, @@ -624,23 +630,24 @@ allow_deeplink: true project_menu_items = { - general: :label_information_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 87f571f46a9d..89baf66fdd95 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -133,6 +133,14 @@ permissible_on: :project, require: :member + map.permission :select_project_life_cycle, + { + "projects/settings/life_cycles": %i[show toggle enable_all disable_all] + }, + permissible_on: :project, + require: :member, + visible: -> { OpenProject::FeatureDecisions.stages_and_gates_active? } + 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..d86b1bf1a684 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: @@ -388,10 +391,20 @@ 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_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: + use_in_project: "Use %{step} in this project" + filter: + label: "Search project stage or gate" 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: @@ -400,8 +413,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. @@ -917,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}" @@ -1403,6 +1417,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" @@ -2579,6 +2595,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" @@ -2640,6 +2657,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" @@ -3203,6 +3221,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. 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" @@ -3260,9 +3280,9 @@ en: are_you_sure: "Are you sure you want to archive the project '%{name}'?" archived: "Archived" count: - zero: "0 Projects" - one: "1 Project" - other: "%{count} Projects" + zero: "0 projects" + one: "1 project" + other: "%{count} projects" project_module_activity: "Activity" project_module_forums: "Forums" @@ -3680,6 +3700,27 @@ 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" + 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: + 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" @@ -3717,6 +3758,7 @@ en: text_are_you_sure_continue: "Are you sure you want to continue?" text_are_you_sure_with_children: "Delete work package and all child work packages?" text_are_you_sure_with_project_custom_fields: "Deleting this attribute will also delete its values in all projects. Are you sure you want to do this?" + text_are_you_sure_with_project_life_cycle_step: "Deleting this step will also delete its usages in all projects. Are you sure you want to do this?" text_assign_to_project: "Assign to the project" text_form_configuration: > You can customize which fields will be displayed in work package forms. diff --git a/config/routes.rb b/config/routes.rb index 158ed8a6a293..466ff9690dc5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -244,6 +244,15 @@ put :disable_all_of_section end end + resource :life_cycle, only: %i[show] do + 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" resource :versions, only: %i[show] @@ -505,6 +514,17 @@ 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_life_cycle_step_definitions, controller: "/admin/settings/project_life_cycle_step_definitions", + only: %i[index create edit update destroy] do + collection do + get :new_stage + get :new_gate + end + member do + patch :move + put :drop # should be patch, but requires passing method to generic-drag-and-drop controller + 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 @@ -518,6 +538,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 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..e2358eb64f92 --- /dev/null +++ b/db/migrate/20241125161226_unique_index_on_project_life_cycle_steps.rb @@ -0,0 +1,7 @@ +class UniqueIndexOnProjectLifeCycleSteps < ActiveRecord::Migration[7.1] + def change + add_index :project_life_cycle_steps, + %i[project_id definition_id], + unique: true + end +end 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 diff --git a/frontend/src/global_styles/openproject/_gap.scss b/frontend/src/global_styles/openproject/_gap.scss new file mode 100644 index 000000000000..9925956de44e --- /dev/null +++ b/frontend/src/global_styles/openproject/_gap.scss @@ -0,0 +1,44 @@ +//-- 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. +//++ + +// copied from https://github.com/primer/css/blob/main/src/support/variables/layout.scss +$spacer-map-rem: ( + 0: 0, + 1: var(--base-size-4, 4px), + 2: var(--base-size-8, 8px), + 3: var(--base-size-16, 16px), + 4: var(--base-size-24, 24px), + 5: var(--base-size-32, 32px), + 6: var(--base-size-40, 40px), +) !default; + +@each $scale, $size in $spacer-map-rem { + .gap-#{$scale} { gap: $size !important; } + .row-gap-#{$scale} { row-gap: $size !important; } + .column-gap-#{$scale} { column-gap: $size !important; } +} diff --git a/frontend/src/global_styles/openproject/_index.sass b/frontend/src/global_styles/openproject/_index.sass index 85db8b17cf31..c689b5d4bcf3 100644 --- a/frontend/src/global_styles/openproject/_index.sass +++ b/frontend/src/global_styles/openproject/_index.sass @@ -7,6 +7,7 @@ @import generic @import base @import forms +@import gap @import mixins @import onboarding 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); 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/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..319c4b19c779 --- /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: Project::GateDefinition.new(id: 1, name: "The first gate") }) + end + + def stage + render_with_template(locals: { model: Project::StageDefinition.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)) %> 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..8c68637908ef --- /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_reader :project + + def initialize(project) + super() + + @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/life_cycle/active_in_project_spec.rb b/spec/features/projects/life_cycle/active_in_project_spec.rb new file mode 100644 index 000000000000..c545b59004a4 --- /dev/null +++ b/spec/features/projects/life_cycle/active_in_project_spec.rb @@ -0,0 +1,148 @@ +# -- 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, with_flag: { stages_and_gates: true } do + shared_let(:project) { create(:project) } + + shared_let(:user_with_permission) do + create(:user, + member_with_permissions: { + project => %w[ + select_project_life_cycle + ] + }) + 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") } + 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_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 steps and filtering them" do + 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) + + # Activate the stages to be found within the project + project_life_cycle_page.toggle(initiating_stage) + project_life_cycle_page.toggle(ready_to_close_gate) + project_life_cycle_page.toggle(closing_stage) + + 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 + project_life_cycle_page.reload_with_home_page_detour + + 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 + + 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 + project_life_cycle_page.reload_with_home_page_detour + + 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 + + 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 + project_life_cycle_page.reload_with_home_page_detour + + 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) + + # The user can filter the life cycle steps + project_life_cycle_page.filter_by("ing") + + 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 + + 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 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/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/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/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/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 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 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..64f34ee9818e --- /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_reader :project + + def initialize(project) + super() + + @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 new file mode 100644 index 000000000000..ab8ea8f0bcdd --- /dev/null +++ b/spec/support/pages/projects/settings/life_cycle.rb @@ -0,0 +1,115 @@ +# -- 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 LifeCycle < Pages::Page + attr_reader :project + + def initialize(project) + super() + + @project = project + end + + def path + "/projects/#{project.identifier}/settings/life_cycle" + end + + # 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_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_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 + + 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 '#{definition.name}' to be #{expected_toggle_status(active)} " \ + "but was #{expected_toggle_status(!active)}" + end + end + + def toggle(definition) + toggle_step(definition).click + end + + def disable_all + find_test_selector("disable-all-life-cycle-steps").click + end + + def enable_all + find_test_selector("enable-all-life-cycle-steps").click + end + + def life_cycle_test_selector(definition) + test_selector("project-life-cycle-step-#{definition.id}") + end + + 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 + + # 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 +end 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