diff --git a/app/components/settings/project_custom_field_sections/show_component.html.erb b/app/components/settings/project_custom_field_sections/show_component.html.erb index cfd8b1712ce..feda29d90c6 100644 --- a/app/components/settings/project_custom_field_sections/show_component.html.erb +++ b/app/components/settings/project_custom_field_sections/show_component.html.erb @@ -13,7 +13,42 @@ end end end + section_header_container.with_column(flex_layout: true, justify_content: :flex_end) do |actions_container| + if OpenProject::FeatureDecisions.new_project_overview_active? + actions_container.with_column do + render(Primer::Alpha::ActionMenu.new(select_variant: :single, size: :small, test_selector: "section-position-selector")) do |menu| + menu.with_show_button( + mr: 2, + aria: { label: t("settings.project_attributes.sections.display_representation.overview.label") } + ) do |button| + button.with_trailing_visual_icon(icon: :"triangle-down") + button.with_leading_visual_icon(icon: display_representation_icon_for_section(@project_custom_field_section)) + display_representation_label_for_section(@project_custom_field_section) + end + + menu.with_item( + label: t("settings.project_attributes.sections.display_representation.overview.side_panel.label"), + active: @project_custom_field_section.shown_in_overview_sidebar?, + test_selector: "section-position-selector--side-panel-option", + **menu_item_options_for(@project_custom_field_section, ProjectCustomFieldSection::OVERVIEW__SIDEBAR_KEY) + ) do |item| + item.with_leading_visual_icon(icon: :"op-view-split") + item.with_description.with_content(t("settings.project_attributes.sections.display_representation.overview.side_panel.description")) + end + menu.with_item( + label: t("settings.project_attributes.sections.display_representation.overview.main_area.label"), + active: @project_custom_field_section.shown_in_overview_main_area?, + test_selector: "section-position-selector--main-section-option", + **menu_item_options_for(@project_custom_field_section, ProjectCustomFieldSection::OVERVIEW__MAIN_AREA_KEY) + ) do |item| + item.with_leading_visual_icon(icon: :"op-view-cards") + item.with_description.with_content(t("settings.project_attributes.sections.display_representation.overview.main_area.description")) + end + end + end + end + actions_container.with_column do render(Primer::Alpha::ActionMenu.new(data: { test_selector: "project-custom-field-section-action-menu" })) do |menu| menu.with_show_button(icon: "kebab-horizontal", "aria-label": t("settings.project_attributes.label_section_actions"), scheme: :invisible) @@ -26,6 +61,7 @@ end end end + actions_container.with_column do render( Primer::Alpha::Dialog.new( diff --git a/app/components/settings/project_custom_field_sections/show_component.rb b/app/components/settings/project_custom_field_sections/show_component.rb index 38882fd9a3e..7fdb147f34c 100644 --- a/app/components/settings/project_custom_field_sections/show_component.rb +++ b/app/components/settings/project_custom_field_sections/show_component.rb @@ -161,6 +161,31 @@ module Settings test_selector: "new-project-custom-field-in-section-button-#{format.name}" } } ) end + + def display_representation_icon_for_section(section) + section.shown_in_overview_sidebar? ? :"op-view-split" : :"op-view-cards" + end + + def display_representation_label_for_section(section) + if section.shown_in_overview_sidebar? + t("settings.project_attributes.sections.display_representation.overview.side_panel.label") + else + t("settings.project_attributes.sections.display_representation.overview.main_area.label") + end + end + + def menu_item_options_for(section, key) + { + href: admin_settings_project_custom_field_section_path(section), + form_arguments: { + method: :put, + inputs: [{ + name: "project_custom_field_section[overview]", + value: key + }] + } + } + end end end end diff --git a/app/contracts/project_custom_field_sections/base_contract.rb b/app/contracts/project_custom_field_sections/base_contract.rb index 1ca0d6f0045..e528026298e 100644 --- a/app/contracts/project_custom_field_sections/base_contract.rb +++ b/app/contracts/project_custom_field_sections/base_contract.rb @@ -35,5 +35,6 @@ module ProjectCustomFieldSections attribute :name attribute :position attribute :type + attribute :display_representation end end diff --git a/app/controllers/admin/settings/project_custom_field_sections_controller.rb b/app/controllers/admin/settings/project_custom_field_sections_controller.rb index a64d132221e..3fb520adfa6 100644 --- a/app/controllers/admin/settings/project_custom_field_sections_controller.rb +++ b/app/controllers/admin/settings/project_custom_field_sections_controller.rb @@ -121,7 +121,9 @@ module Admin::Settings end def project_custom_field_section_params - params.require(:project_custom_field_section).permit(:name) + # Set the sidebar as default + params.require(:project_custom_field_section)[:overview] ||= CustomFieldSection::OVERVIEW__SIDEBAR_KEY + params.expect(project_custom_field_section: %i[name overview]) end end end diff --git a/app/models/custom_field_section.rb b/app/models/custom_field_section.rb index 5595f9891a5..2eba38951e3 100644 --- a/app/models/custom_field_section.rb +++ b/app/models/custom_field_section.rb @@ -29,9 +29,23 @@ #++ class CustomFieldSection < ApplicationRecord + OVERVIEW__SIDEBAR_KEY = "sidebar" + OVERVIEW__MAIN_AREA_KEY = "main_area" + DEFAULT_OVERVIEW_KEY = OVERVIEW__SIDEBAR_KEY.freeze + acts_as_list scope: [:type] validates :name, presence: true default_scope { order(:position) } + + store_attribute :display_representation, :overview, :string + + def shown_in_overview_sidebar? + overview == OVERVIEW__SIDEBAR_KEY + end + + def shown_in_overview_main_area? + overview == OVERVIEW__MAIN_AREA_KEY + end end diff --git a/app/services/project_custom_fields/load_service.rb b/app/services/project_custom_fields/load_service.rb new file mode 100644 index 00000000000..50e3cbc4cdc --- /dev/null +++ b/app/services/project_custom_fields/load_service.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module ProjectCustomFields + class LoadService + def initialize(project:, project_custom_fields:) + super() + @project = project + @project_custom_fields = project_custom_fields + eager_load_project_custom_field_values + end + + def get_eager_loaded_project_custom_field_values_for(custom_field_id) + @eager_loaded_project_custom_field_values.select { |pcfv| pcfv.custom_field_id == custom_field_id } + end + + private + + def eager_load_project_custom_field_values + @eager_loaded_project_custom_field_values = CustomValue + .includes(custom_field: :custom_options) + .where( + custom_field_id: @project_custom_fields.pluck(:id), + customized_id: @project.id + ) + .to_a + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index b11f33fa594..d5389697cd5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -4610,6 +4610,16 @@ 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." + sections: + display_representation: + overview: + label: "Project attribute shown in:" + main_area: + label: "Main area" + description: "Add all the project attributes as individual widgets in the main section of the project overview." + side_panel: + label: "Side panel" + description: "Add all the project attributes in a section inside the right side panel in the project overview." project_initiation_request: blankslate: title: "Initiation request not enabled" diff --git a/db/migrate/20251104151450_add_display_representation_column_to_cf_section.rb b/db/migrate/20251104151450_add_display_representation_column_to_cf_section.rb new file mode 100644 index 00000000000..701f90e4589 --- /dev/null +++ b/db/migrate/20251104151450_add_display_representation_column_to_cf_section.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddDisplayRepresentationColumnToCfSection < ActiveRecord::Migration[8.0] + def change + add_column :custom_field_sections, + :display_representation, + :jsonb, + default: { overview: CustomFieldSection::DEFAULT_OVERVIEW_KEY }, + null: false + end +end diff --git a/modules/grids/app/components/_index.sass b/modules/grids/app/components/_index.sass index 3dca22fc70f..b835590e337 100644 --- a/modules/grids/app/components/_index.sass +++ b/modules/grids/app/components/_index.sass @@ -1 +1,2 @@ @import "./grids/widgets/news/item.sass" +@import "./grids/project_attribute_widgets.sass" diff --git a/modules/grids/app/components/grids/project_attribute_widgets.html.erb b/modules/grids/app/components/grids/project_attribute_widgets.html.erb new file mode 100644 index 00000000000..194b337c718 --- /dev/null +++ b/modules/grids/app/components/grids/project_attribute_widgets.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. + +++#%> +<%= component_wrapper(class: "grids-project-attribute-widgets", data: { test_selector: "grids-project-attribute-widgets" }) do %> + <% widgets.each do |widget| %> + <%= render widget %> + <% end %> +<% end %> diff --git a/modules/grids/app/components/grids/project_attribute_widgets.rb b/modules/grids/app/components/grids/project_attribute_widgets.rb new file mode 100644 index 00000000000..52d9cf57ac4 --- /dev/null +++ b/modules/grids/app/components/grids/project_attribute_widgets.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Grids + class ProjectAttributeWidgets < Grids::WidgetComponent + include OpTurbo::Streamable + + renders_many :widgets, Grids::Widgets::ProjectAttributeSection + + param :project + + def title + "" + end + + # For each configured section, call the the `with_widget` slot + def before_render + available_project_attributes_grouped_by_section.each do |section, project_custom_fields| + with_widget(section, project_custom_fields, @project) + end + end + + private + + def available_project_attributes_grouped_by_section + if OpenProject::FeatureDecisions.new_project_overview_active? + @available_project_attributes_grouped_by_section ||= + @project.available_custom_fields + .group_by(&:project_custom_field_section) + .select { |section, _| section.shown_in_overview_main_area? } + else + @available_project_attributes_grouped_by_section ||= + @project.available_custom_fields + .group_by(&:project_custom_field_section) + end + end + end +end diff --git a/modules/grids/app/components/grids/project_attribute_widgets.sass b/modules/grids/app/components/grids/project_attribute_widgets.sass new file mode 100644 index 00000000000..433bd042389 --- /dev/null +++ b/modules/grids/app/components/grids/project_attribute_widgets.sass @@ -0,0 +1,4 @@ +.grids-project-attribute-widgets + display: flex + flex-wrap: wrap + flex-basis: 100% diff --git a/modules/grids/app/components/grids/widget_component.rb b/modules/grids/app/components/grids/widget_component.rb index 22d120a173f..b3f8337cab6 100644 --- a/modules/grids/app/components/grids/widget_component.rb +++ b/modules/grids/app/components/grids/widget_component.rb @@ -43,6 +43,7 @@ module Grids delegate :wrapper_key, to: :class + option :tag, default: -> { :div } option :current_user, default: -> { User.current } # @abstract Subclasses must implement this method. diff --git a/modules/grids/app/components/grids/widgets/project_attribute_section.html.erb b/modules/grids/app/components/grids/widgets/project_attribute_section.html.erb new file mode 100644 index 00000000000..50faf72c67b --- /dev/null +++ b/modules/grids/app/components/grids/widgets/project_attribute_section.html.erb @@ -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. + +++#%> + +<%= + widget_wrapper( + classes: "op-project-custom-field-section-container", + test_selector: "project-custom-field-section-widget-#{section.id}" + ) do + render( + Overviews::ProjectCustomFields::ItemsComponent.new( + project_custom_fields:, + project: + ) + ) + end +%> diff --git a/modules/grids/app/components/grids/widgets/project_attribute_section.rb b/modules/grids/app/components/grids/widgets/project_attribute_section.rb new file mode 100644 index 00000000000..619d73d5dda --- /dev/null +++ b/modules/grids/app/components/grids/widgets/project_attribute_section.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Grids + module Widgets + class ProjectAttributeSection < Grids::WidgetComponent + param :section + param :project_custom_fields + param :project + + def title + section.name + end + end + end +end diff --git a/modules/overviews/app/components/_index.sass b/modules/overviews/app/components/_index.sass index 6fef6c9c4ae..426e98c0a51 100644 --- a/modules/overviews/app/components/_index.sass +++ b/modules/overviews/app/components/_index.sass @@ -1,3 +1,4 @@ @import "./overviews/project_custom_fields/item_component.sass" +@import "./overviews/project_custom_fields/items_component.sass" @import "./overviews/project_phases/item_component.sass" @import "./overviews/overview_grid_component.sass" diff --git a/modules/overviews/app/components/overviews/project_custom_fields/items_component.html.erb b/modules/overviews/app/components/overviews/project_custom_fields/items_component.html.erb new file mode 100644 index 00000000000..189ae5c0f70 --- /dev/null +++ b/modules/overviews/app/components/overviews/project_custom_fields/items_component.html.erb @@ -0,0 +1,15 @@ +<%= + flex_layout(classes: "project-custom-field-items") do |flex| + @project_custom_fields.each do |project_custom_field| + flex.with_row do + render( + Overviews::ProjectCustomFields::ItemComponent.new( + project_custom_field:, + project_custom_field_values: attribute_load_service.get_eager_loaded_project_custom_field_values_for(project_custom_field.id), + project: @project + ) + ) + end + end + end +%> diff --git a/modules/overviews/app/components/overviews/project_custom_fields/items_component.rb b/modules/overviews/app/components/overviews/project_custom_fields/items_component.rb new file mode 100644 index 00000000000..93ca069c423 --- /dev/null +++ b/modules/overviews/app/components/overviews/project_custom_fields/items_component.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Overviews + module ProjectCustomFields + class ItemsComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + + def initialize(project_custom_fields:, project:) + super + + @project_custom_fields = project_custom_fields + @project = project + end + + def attribute_load_service + @attribute_load_service ||= ::ProjectCustomFields::LoadService.new(project: @project, + project_custom_fields: @project_custom_fields) + end + end + end +end diff --git a/modules/overviews/app/components/overviews/project_custom_fields/items_component.sass b/modules/overviews/app/components/overviews/project_custom_fields/items_component.sass new file mode 100644 index 00000000000..f324138a4ac --- /dev/null +++ b/modules/overviews/app/components/overviews/project_custom_fields/items_component.sass @@ -0,0 +1,2 @@ +.project-custom-field-items + gap: var(--base-size-16, 16px) diff --git a/modules/overviews/app/components/overviews/project_custom_fields/show_component.html.erb b/modules/overviews/app/components/overviews/project_custom_fields/show_component.html.erb index 354669475fd..f0201fd3e9a 100644 --- a/modules/overviews/app/components/overviews/project_custom_fields/show_component.html.erb +++ b/modules/overviews/app/components/overviews/project_custom_fields/show_component.html.erb @@ -8,20 +8,12 @@ ) do |section| section.with_title { @project_custom_field_section.name } - flex_layout do |details_container| - @project_custom_fields.each_with_index do |project_custom_field, i| - margin = i == @project_custom_fields.size - 1 ? 0 : 3 - details_container.with_row(mb: margin) do - render( - Overviews::ProjectCustomFields::ItemComponent.new( - project_custom_field:, - project_custom_field_values: get_eager_loaded_project_custom_field_values_for(project_custom_field.id), - project: @project - ) - ) - end - end - end + render( + Overviews::ProjectCustomFields::ItemsComponent.new( + project_custom_fields: @project_custom_fields, + project: @project + ) + ) end end %> diff --git a/modules/overviews/app/components/overviews/project_custom_fields/show_component.rb b/modules/overviews/app/components/overviews/project_custom_fields/show_component.rb index 48832dd37de..dcf8abc9b71 100644 --- a/modules/overviews/app/components/overviews/project_custom_fields/show_component.rb +++ b/modules/overviews/app/components/overviews/project_custom_fields/show_component.rb @@ -41,25 +41,6 @@ module Overviews @project = project @project_custom_field_section = project_custom_field_section @project_custom_fields = project_custom_fields - - eager_load_project_custom_field_values - end - - private - - def eager_load_project_custom_field_values - # TODO: move to service - @eager_loaded_project_custom_field_values = CustomValue - .includes(custom_field: :custom_options) - .where( - custom_field_id: @project_custom_fields.pluck(:id), - customized_id: @project.id - ) - .to_a - end - - def get_eager_loaded_project_custom_field_values_for(custom_field_id) - @eager_loaded_project_custom_field_values.select { |pcfv| pcfv.custom_field_id == custom_field_id } end end end diff --git a/modules/overviews/app/components/overviews/project_custom_fields/side_panel_component.rb b/modules/overviews/app/components/overviews/project_custom_fields/side_panel_component.rb index b5b06430851..fe0d4bc9c5f 100644 --- a/modules/overviews/app/components/overviews/project_custom_fields/side_panel_component.rb +++ b/modules/overviews/app/components/overviews/project_custom_fields/side_panel_component.rb @@ -49,8 +49,16 @@ module Overviews private def available_project_custom_fields_grouped_by_section - @available_project_custom_fields_grouped_by_section ||= - @project.available_custom_fields.group_by(&:project_custom_field_section) + if OpenProject::FeatureDecisions.new_project_overview_active? + @available_project_custom_fields_grouped_by_section ||= + @project.available_custom_fields + .group_by(&:project_custom_field_section) + .select { |section, _| section.shown_in_overview_sidebar? } + else + @available_project_custom_fields_grouped_by_section ||= + @project.available_custom_fields + .group_by(&:project_custom_field_section) + end end end end diff --git a/modules/overviews/app/components/overviews/workspaces/portfolio_overview_grid_component.html.erb b/modules/overviews/app/components/overviews/workspaces/portfolio_overview_grid_component.html.erb index 8f2f24f5bbe..93ad48c28a7 100644 --- a/modules/overviews/app/components/overviews/workspaces/portfolio_overview_grid_component.html.erb +++ b/modules/overviews/app/components/overviews/workspaces/portfolio_overview_grid_component.html.erb @@ -33,5 +33,7 @@ See COPYRIGHT and LICENSE files for more details. grid.with_widget(Grids::Widgets::ProjectStatus, @portfolio) grid.with_widget(Grids::Widgets::Subitems, @portfolio) grid.with_widget(Grids::Widgets::Members, @portfolio) + + grid.with_widget(Grids::ProjectAttributeWidgets, @project) end %> diff --git a/modules/overviews/app/components/overviews/workspaces/program_overview_grid_component.html.erb b/modules/overviews/app/components/overviews/workspaces/program_overview_grid_component.html.erb index 87dd0f05b99..b4e82446d96 100644 --- a/modules/overviews/app/components/overviews/workspaces/program_overview_grid_component.html.erb +++ b/modules/overviews/app/components/overviews/workspaces/program_overview_grid_component.html.erb @@ -33,5 +33,7 @@ See COPYRIGHT and LICENSE files for more details. grid.with_widget(Grids::Widgets::ProjectStatus, @program) grid.with_widget(Grids::Widgets::Subitems, @program) grid.with_widget(Grids::Widgets::Members, @program) + + grid.with_widget(Grids::ProjectAttributeWidgets, @project) end %> diff --git a/modules/overviews/app/components/overviews/workspaces/project_overview_grid_component.html.erb b/modules/overviews/app/components/overviews/workspaces/project_overview_grid_component.html.erb index 1f3c4bae7e9..60f3c9fe77b 100644 --- a/modules/overviews/app/components/overviews/workspaces/project_overview_grid_component.html.erb +++ b/modules/overviews/app/components/overviews/workspaces/project_overview_grid_component.html.erb @@ -34,5 +34,7 @@ See COPYRIGHT and LICENSE files for more details. grid.with_widget(Grids::Widgets::Subitems, @project) grid.with_widget(Grids::Widgets::Members, @project) grid.with_widget(Grids::Widgets::News, @project) + + grid.with_widget(Grids::ProjectAttributeWidgets, @project) end %> diff --git a/modules/overviews/app/controllers/overviews/project_custom_fields_controller.rb b/modules/overviews/app/controllers/overviews/project_custom_fields_controller.rb index ef8dbc6a4fe..eac1e1a2db3 100644 --- a/modules/overviews/app/controllers/overviews/project_custom_fields_controller.rb +++ b/modules/overviews/app/controllers/overviews/project_custom_fields_controller.rb @@ -60,7 +60,11 @@ class Overviews::ProjectCustomFieldsController < ApplicationController .call(permitted_params.project) if service_call.success? - update_sidebar_component + if field_shown_in_sidebar?(@custom_field) + update_sidebar_component + else + update_widgets_component + end else handle_errors(service_call.result, @custom_field) end @@ -98,4 +102,14 @@ class Overviews::ProjectCustomFieldsController < ApplicationController component: Overviews::ProjectCustomFields::SidePanelComponent.new(project: @project) ) end + + def update_widgets_component + update_via_turbo_stream( + component: Grids::ProjectAttributeWidgets.new(@project) + ) + end + + def field_shown_in_sidebar?(custom_field) + CustomFieldSection.find(custom_field.custom_field_section_id).shown_in_overview_sidebar? + end end diff --git a/spec/factories/custom_field_section.rb b/spec/factories/custom_field_section.rb index e83aeaed25b..bc71a5d1b76 100644 --- a/spec/factories/custom_field_section.rb +++ b/spec/factories/custom_field_section.rb @@ -31,6 +31,7 @@ FactoryBot.define do factory :custom_field_section do sequence(:name) { |n| "Section No. #{n}" } + overview { "sidebar" } created_at { Time.zone.now } updated_at { Time.zone.now } diff --git a/spec/features/projects/project_custom_fields/overview_page/shared_context.rb b/spec/features/projects/project_custom_fields/overview_page/shared_context.rb index 83d4cd8af87..b86de1c6462 100644 --- a/spec/features/projects/project_custom_fields/overview_page/shared_context.rb +++ b/spec/features/projects/project_custom_fields/overview_page/shared_context.rb @@ -275,6 +275,14 @@ RSpec.shared_context "with seeded projects, members and project custom fields" d field end + let!(:sections) do + [ + section_for_input_fields, + section_for_select_fields, + section_for_multi_select_fields + ] + end + let!(:input_fields) do [ boolean_project_custom_field, diff --git a/spec/features/projects/project_custom_fields/overview_page/widget_spec.rb b/spec/features/projects/project_custom_fields/overview_page/widget_spec.rb new file mode 100644 index 00000000000..ecc1fe273ff --- /dev/null +++ b/spec/features/projects/project_custom_fields/overview_page/widget_spec.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" +require_relative "shared_context" + +RSpec.describe "Show project custom fields on project overview page", :js, with_flag: { new_project_overview: true } do + include TestSelectorFinders + + include_context "with seeded projects, members and project custom fields" + + let(:overview_page) { Pages::Projects::Show.new(project) } + + before do + login_as admin + end + + describe "within the Administration" do + before do + visit admin_settings_project_custom_fields_path + end + + it "shows an ActionMenu for each section" do + sections.each do |section| + within_test_selector("project-custom-field-section-container-#{section.id}") do + # Per default, the section is shown in the side panel + expect(page).to have_test_selector("section-position-selector", text: "Side panel") + end + end + end + + it "can change the position to main section" do + within_test_selector("project-custom-field-section-container-#{section_for_input_fields.id}") do + # Change it to main section + page.find_test_selector("section-position-selector").click + expect(page).to have_test_selector("section-position-selector--side-panel-option") + expect(page).to have_test_selector("section-position-selector--main-section-option") + + page.find_test_selector("section-position-selector--main-section-option").click + wait_for_network_idle + + # The section is updated directly + section = CustomFieldSection.find(section_for_input_fields.id) + expect(section.shown_in_overview_main_area?).to be(true) + expect(page).to have_test_selector("section-position-selector", text: "Main area") + end + end + + it "can add a new section which is shown per default in the sidebar" do + within "#settings-project-custom-fields-header-component" do + page.find_test_selector("project-attributes-add-menu-button").click + click_on("dialog-show-project-custom-field-section-dialog") + end + + fill_in("project_custom_field_section_name", with: "An awesome new section") + + click_on("Save") + + expect(page).to have_text("An awesome new section") + + # The section is shown in the sidebar per default + section = CustomFieldSection.last + expect(section.shown_in_overview_sidebar?).to be(true) + within_test_selector("project-custom-field-section-container-#{section.id}") do + expect(page).to have_test_selector("section-position-selector", text: "Side panel") + end + end + end + + describe "within the Overview page" do + before do + # Move one section to the main section + section = CustomFieldSection.find(section_for_input_fields.id) + section.display_representation = { overview: "main_area" } + section.save! + + overview_page.visit! + end + + it "shows the sections in either the sidebar or the main section" do + # The section is shown in the main section of the overview page ... + overview_page.within_main_area do + sections = page.all(".op-project-custom-field-section-container") + + expect(sections.size).to eq(1) + + expect(sections[0].text).to include("Input fields") + end + + # ... while the others remain in the sidebar + overview_page.within_project_attributes_sidebar do + sections = page.all(".op-project-custom-field-section-container") + + expect(sections.size).to eq(2) + + expect(sections[0].text).to include("Select fields") + expect(sections[1].text).to include("Multi select fields") + end + end + + it "shows the project custom fields in the correct order within the widget" do + overview_page.within_main_area do + overview_page.within_custom_field_section_widget(section_for_input_fields) do + fields = page.all(".op-project-custom-field-container") + + expect(fields.size).to eq(9) + + expect(fields[0].text).to include("Boolean field") + expect(fields[1].text).to include("String field") + expect(fields[2].text).to include("Integer field") + expect(fields[3].text).to include("Float field") + expect(fields[4].text).to include("Date field") + expect(fields[5].text).to include("Link field") + expect(fields[6].text).to include("Text field") + expect(fields[7].text).to include("Calculated field using int") + expect(fields[8].text).to include("Calculated field using int and float") + end + end + end + + it "does not show project custom fields not enabled for this project in a widget" do + create(:string_project_custom_field, projects: [other_project], name: "String field enabled for other project") + + overview_page.visit_page + + overview_page.within_main_area do + expect(page).to have_no_text "String field enabled for other project" + end + end + + it "can edit a project custom field from within the widget" do + overview_page.open_edit_dialog_for_custom_field(string_project_custom_field) + page.fill_in(string_project_custom_field.name, with: "My super awesome new value") + page.click_on "Save" + + # The new value is shown in the widget + overview_page.within_main_area do + overview_page.within_custom_field_section_widget(section_for_input_fields) do + expect(page).to have_text "My super awesome new value" + end + end + + expect(project.reload.custom_value_for(string_project_custom_field).value).to eq("My super awesome new value") + end + end +end diff --git a/spec/support/pages/projects/show.rb b/spec/support/pages/projects/show.rb index b9165b8e42e..1556315f5b7 100644 --- a/spec/support/pages/projects/show.rb +++ b/spec/support/pages/projects/show.rb @@ -65,10 +65,18 @@ module Pages within_test_selector("project-custom-fields-sidebar", &) end + def within_main_area(&) + within_test_selector("grids-project-attribute-widgets", &) + end + def within_custom_field_section_container(section, &) within_test_selector("project-custom-field-section-#{section.id}", &) end + def within_custom_field_section_widget(section, &) + within_test_selector("project-custom-field-section-widget-#{section.id}", &) + end + def within_custom_field_container(custom_field, &) within_test_selector("project-custom-field-#{custom_field.id}", &) end @@ -78,17 +86,15 @@ module Pages end def open_edit_dialog_for_custom_field(custom_field) - within_project_attributes_sidebar do - scroll_to_element(page.find("[data-test-selector='project-custom-field-#{custom_field.id}']")) - within_custom_field_container(custom_field) do - # Link and user type custom fields might contain a clickable link inside the edit container. - # Use JavaScript to directly trigger the click event on the container to avoid nested links. - # Once we create the project custom field inline editing, this can be reverted to a normal - # capybara click method call. - page.execute_script( - "document.querySelector('[data-test-selector=\"project-custom-field-edit-button-#{custom_field.id}\"]').click()" - ) - end + scroll_to_element(page.find("[data-test-selector='project-custom-field-#{custom_field.id}']")) + within_custom_field_container(custom_field) do + # Link and user type custom fields might contain a clickable link inside the edit container. + # Use JavaScript to directly trigger the click event on the container to avoid nested links. + # Once we create the project custom field inline editing, this can be reverted to a normal + # capybara click method call. + page.execute_script( + "document.querySelector('[data-test-selector=\"project-custom-field-edit-button-#{custom_field.id}\"]').click()" + ) end wait_for_size_animation_completion("[data-test-selector='async-dialog-content']")