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 index ce0ba8d7c447..fd14e35a504a 100644 --- 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 @@ -30,22 +30,26 @@ 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" - } - ) + if allowed_to_customize_life_cycle? + 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 + else + render EnterpriseEdition::BannerComponent.new(:customize_life_cycle, mb: 3) end end @@ -76,6 +80,7 @@ See COPYRIGHT and LICENSE files for more details. ) do render(Settings::ProjectLifeCycleStepDefinitions::RowComponent.new( definition, + allowed_to_customize_life_cycle?: allowed_to_customize_life_cycle?, first?: definition == definitions.first, last?: definition == definitions.last, )) 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 index f34bf7f8a911..04e8f51be272 100644 --- a/app/components/settings/project_life_cycle_step_definitions/index_component.rb +++ b/app/components/settings/project_life_cycle_step_definitions/index_component.rb @@ -31,7 +31,8 @@ module ProjectLifeCycleStepDefinitions class IndexComponent < ApplicationComponent include OpPrimer::ComponentHelpers - options :definitions + options :definitions, + :allowed_to_customize_life_cycle? private 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 index 7c1d55449bde..335da67f8f8f 100644 --- 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 @@ -36,32 +36,34 @@ See COPYRIGHT and LICENSE files for more details. %> <%= - 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 + if allowed_to_customize_life_cycle? + 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_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") + 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 index 6622a0f61b88..3c5e28a27d2c 100644 --- 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 @@ -29,6 +29,7 @@ module Settings module ProjectLifeCycleStepDefinitions class IndexHeaderComponent < ApplicationComponent + options :allowed_to_customize_life_cycle? def breadcrumbs_items [ diff --git a/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb b/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb index b52223e98748..079296d46b33 100644 --- a/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb +++ b/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb @@ -30,17 +30,27 @@ 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( - data: { "projects--settings--border-box-filter-target": "hideWhenFiltering" } - )) + if allowed_to_customize_life_cycle? + title_container.with_column do + render(Primer::OpenProject::DragHandle.new( + data: { "projects--settings--border-box-filter-target": "hideWhenFiltering" } + )) + end 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 + render( + if allowed_to_customize_life_cycle? + Primer::Beta::Link.new( + classes: 'filter-target-visible-text', + href: edit_admin_settings_project_life_cycle_step_definition_path(definition), + font_weight: :bold + ) + else + Primer::Beta::Text.new( + font_weight: :bold + ) + end + ) do definition.name end end @@ -51,38 +61,41 @@ See COPYRIGHT and LICENSE files for more details. 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 + if allowed_to_customize_life_cycle? + 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) - 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(: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") + 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) + ) 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 index a1a0be5edbe2..2ce3a97664c3 100644 --- a/app/components/settings/project_life_cycle_step_definitions/row_component.rb +++ b/app/components/settings/project_life_cycle_step_definitions/row_component.rb @@ -33,7 +33,9 @@ class RowComponent < ApplicationComponent alias_method :definition, :model - options :first?, :last? + options :allowed_to_customize_life_cycle?, + :first?, + :last? private diff --git a/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb b/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb index 402101a25870..eee77da504e2 100644 --- a/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb +++ b/app/controllers/admin/settings/project_life_cycle_step_definitions_controller.rb @@ -30,7 +30,10 @@ module Admin::Settings class ProjectLifeCycleStepDefinitionsController < ::Admin::SettingsController menu_item :project_life_cycle_step_definitions_settings + helper_method :allowed_to_customize_life_cycle? + before_action :check_feature_flag + before_action :require_enterprise_token, except: %i[index] before_action :find_definitions, only: %i[index] before_action :find_definition, only: %i[edit update destroy move drop] @@ -108,10 +111,18 @@ def drop private + def allowed_to_customize_life_cycle? + EnterpriseToken.allows_to?(:customize_life_cycle) + end + def check_feature_flag render_404 unless OpenProject::FeatureDecisions.stages_and_gates_active? end + def require_enterprise_token + render_402 unless allowed_to_customize_life_cycle? + end + def find_definitions @definitions = Project::LifeCycleStepDefinition.with_project_count end diff --git a/app/helpers/errors_helper.rb b/app/helpers/errors_helper.rb index 561164407eeb..1de153afb781 100644 --- a/app/helpers/errors_helper.rb +++ b/app/helpers/errors_helper.rb @@ -33,6 +33,12 @@ def render_400(options = {}) # rubocop:disable Naming/VariableNumber false end + def render_402(options = {}) # rubocop:disable Naming/VariableNumber + unset_template_magic + render_error({ message: :notice_requires_enterprise_token, status: 402 }.merge(options)) + false + end + def render_403(options = {}) # rubocop:disable Naming/VariableNumber unset_template_magic render_error({ message: :notice_not_authorized, status: 403 }.merge(options)) diff --git a/app/services/authorization/enterprise_service.rb b/app/services/authorization/enterprise_service.rb index 4e419b03d20e..33b1fecda1a3 100644 --- a/app/services/authorization/enterprise_service.rb +++ b/app/services/authorization/enterprise_service.rb @@ -35,6 +35,7 @@ class Authorization::EnterpriseService conditional_highlighting custom_actions custom_field_hierarchies + customize_life_cycle date_alerts define_custom_style edit_attribute_groups diff --git a/app/views/admin/settings/project_life_cycle_step_definitions/index.html.erb b/app/views/admin/settings/project_life_cycle_step_definitions/index.html.erb index cf8877a719e5..ac37177f5b51 100644 --- a/app/views/admin/settings/project_life_cycle_step_definitions/index.html.erb +++ b/app/views/admin/settings/project_life_cycle_step_definitions/index.html.erb @@ -29,5 +29,10 @@ 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) %> +<%= render Settings::ProjectLifeCycleStepDefinitions::IndexHeaderComponent.new( + allowed_to_customize_life_cycle?: allowed_to_customize_life_cycle? +) %> +<%= render Settings::ProjectLifeCycleStepDefinitions::IndexComponent.new( + definitions: @definitions, + allowed_to_customize_life_cycle?: allowed_to_customize_life_cycle? +) %> diff --git a/config/locales/en.yml b/config/locales/en.yml index d99bf11dfe32..812f7dc6896d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1881,6 +1881,8 @@ en: upsale: title: "Enterprise add-on" link_title: "More information" + customize_life_cycle: + description: "Create and organise different project stages and gates than the ones provided by PM2 project cycle planning." form_configuration: description: "Customize the form configuration with these additional add-ons:" add_groups: "Add new attribute groups" @@ -3134,6 +3136,7 @@ en: notice_bad_request: "Bad Request." notice_not_authorized: "You are not authorized to access this page." notice_not_authorized_archived_project: "The project you're trying to access has been archived." + notice_requires_enterprise_token: "Enterprise token missing or doesn't allow access to this page." notice_password_confirmation_failed: "Your password is not correct. Cannot continue." notice_principals_found_multiple: "There are %{number} results found. \n Tab to focus the first result." notice_principals_found_single: "There is one result. \n Tab to focus it." diff --git a/lib/open_project/static/links.rb b/lib/open_project/static/links.rb index ed1dcb06eef0..65b27bb9f650 100644 --- a/lib/open_project/static/links.rb +++ b/lib/open_project/static/links.rb @@ -268,6 +268,9 @@ def static_links }, status_read_only: { href: "https://www.openproject.org/docs/system-admin-guide/manage-work-packages/work-package-status/#create-a-new-work-package-status" + }, + customize_life_cycle: { + href: "https://www.openproject.org/enterprise-edition" # TODO: update } }, sysadmin_docs: { diff --git a/lookbook/previews/open_project/enterprise_edition/banner_component_preview.rb b/lookbook/previews/open_project/enterprise_edition/banner_component_preview.rb index a6ce8c932c6d..d76a3f9f58bc 100644 --- a/lookbook/previews/open_project/enterprise_edition/banner_component_preview.rb +++ b/lookbook/previews/open_project/enterprise_edition/banner_component_preview.rb @@ -57,7 +57,7 @@ class BannerComponentPreview < Lookbook::Preview def default render( ::EnterpriseEdition::BannerComponent - .new(:form_configuration, + .new(:customize_life_cycle, skip_render: false) ) end