From b1e9b367a841334820cda80d162903aab937d134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 14 May 2026 21:04:55 +0200 Subject: [PATCH] Primerize cost type list --- app/components/row_component.rb | 8 +- .../admin/cost_types/row_component.rb | 46 ++++-- .../admin/cost_types/table_component.rb | 42 ++++-- .../admin/cost_types_controller.rb | 21 +-- .../settings/cost_types_controller.rb | 7 +- .../app/views/admin/cost_types/_list.html.erb | 131 ------------------ .../admin/cost_types/_list_deleted.html.erb | 98 ------------- .../app/views/admin/cost_types/index.html.erb | 55 ++------ modules/costs/config/locales/en.yml | 3 +- .../cost_types/delete_cost_type_spec.rb | 15 +- 10 files changed, 104 insertions(+), 322 deletions(-) delete mode 100644 modules/costs/app/views/admin/cost_types/_list.html.erb delete mode 100644 modules/costs/app/views/admin/cost_types/_list_deleted.html.erb diff --git a/app/components/row_component.rb b/app/components/row_component.rb index 00ae80c0993..d9ae018c387 100644 --- a/app/components/row_component.rb +++ b/app/components/row_component.rb @@ -89,8 +89,12 @@ class RowComponent < ApplicationComponent :default end - def checkmark(condition) - if condition + def checkmark(condition, primerized: false) + return unless condition + + if primerized + render(Primer::Beta::Octicon.new(icon: :check)) + else helpers.op_icon "icon icon-checkmark" end end diff --git a/modules/costs/app/components/admin/cost_types/row_component.rb b/modules/costs/app/components/admin/cost_types/row_component.rb index d875400acef..5f951ac69ae 100644 --- a/modules/costs/app/components/admin/cost_types/row_component.rb +++ b/modules/costs/app/components/admin/cost_types/row_component.rb @@ -31,6 +31,8 @@ module Admin module CostTypes class RowComponent < ::RowComponent + delegate :unit, :unit_plural, to: :cost_type + def cost_type model end @@ -39,20 +41,27 @@ module Admin helpers.link_to(cost_type.name, helpers.edit_admin_cost_type_path(cost_type)) end - def unit - cost_type.unit - end - - def unit_plural - cost_type.unit_plural - end - def current_rate - helpers.to_currency_with_empty(cost_type.rate_at(table.fixed_date)) + helpers.to_currency_with_empty(cost_type.rate_at(Date.current)) end def default - checkmark(cost_type.is_default?) + checkmark(cost_type.is_default?, primerized: true) + end + + def active_projects + if cost_type.is_for_all? + I18n.t("settings.project_attributes.label_for_all_projects") + else + count = Project.active.joins(:cost_types_projects) + .where(cost_types_projects: { cost_type_id: cost_type.id }) + .count + count.zero? ? I18n.t(:label_none) : count + end + end + + def deleted_at + helpers.format_date(cost_type.deleted_at) if cost_type.deleted_at end def column_css_class(column) @@ -64,7 +73,7 @@ module Admin end def button_links - [lock_link] + table.locked? ? [restore_link] : [lock_link] end def lock_link @@ -84,6 +93,21 @@ module Admin ) ) end + + def restore_link + render( + Primer::Beta::IconButton.new( + icon: :unlock, + scheme: :invisible, + tag: :a, + href: helpers.restore_admin_cost_type_path(cost_type), + "aria-label": t(:button_unlock), + tooltip_direction: :w, + test_selector: "op-admin-cost-type-#{cost_type.id}-restore", + data: { turbo_method: :patch } + ) + ) + end end end end diff --git a/modules/costs/app/components/admin/cost_types/table_component.rb b/modules/costs/app/components/admin/cost_types/table_component.rb index 1c04d381fa5..3c317aa76fc 100644 --- a/modules/costs/app/components/admin/cost_types/table_component.rb +++ b/modules/costs/app/components/admin/cost_types/table_component.rb @@ -31,30 +31,48 @@ module Admin module CostTypes class TableComponent < ::TableComponent - columns :name, :unit, :unit_plural, :current_rate, :default - sortable_columns :name, :unit, :unit_plural - options :fixed_date + options status: "active" + + def columns + if status == "locked" + %i[name unit unit_plural current_rate deleted_at] + else + %i[name unit unit_plural current_rate active_projects default] + end + end + + def sortable_columns + %i[name unit unit_plural] + end def initial_sort %i[name asc] end def headers - [ - ["name", { caption: CostType.model_name.human }], - ["unit", { caption: CostType.human_attribute_name(:unit) }], - ["unit_plural", { caption: CostType.human_attribute_name(:unit_plural) }], - ["current_rate", { caption: CostType.human_attribute_name(:current_rate) }], - ["default", { caption: I18n.t(:caption_default) }] - ] + columns.map { |column| [column.to_s, { caption: header_caption(column) }] } end def sortable? true end - def fixed_date - options.fetch(:fixed_date) { Date.current } + def locked? + status == "locked" + end + + private + + def header_caption(column) + case column + when :name then CostType.model_name.human + when :unit then CostType.human_attribute_name(:unit) + when :unit_plural then CostType.human_attribute_name(:unit_plural) + when :current_rate then CostType.human_attribute_name(:current_rate) + when :active_projects then I18n.t("cost_types.admin.columns.active_projects") + when :default then I18n.t(:caption_default) + when :deleted_at then I18n.t(:caption_locked_on) + end end end end diff --git a/modules/costs/app/controllers/admin/cost_types_controller.rb b/modules/costs/app/controllers/admin/cost_types_controller.rb index a232b36892e..c57f0f475f6 100644 --- a/modules/costs/app/controllers/admin/cost_types_controller.rb +++ b/modules/costs/app/controllers/admin/cost_types_controller.rb @@ -49,20 +49,9 @@ module Admin "unit_plural" => "#{CostType.table_name}.unit_plural" } sort_update sort_columns - @active_cost_types = CostType.active - @cost_types = CostType.order(sort_clause) - - if params[:clear_filter] - @fixed_date = Time.zone.today - @include_deleted = nil - else - @fixed_date = begin - Date.parse(params[:fixed_date]) - rescue StandardError - Time.zone.today - end - @include_deleted = params[:include_deleted] - end + @status = params[:status] == "locked" ? "locked" : "active" + @cost_types = (@status == "locked" ? CostType.where.not(deleted_at: nil) : CostType.active) + .order(sort_clause) render action: "index", layout: !request.xhr? end @@ -119,7 +108,7 @@ module Admin if @cost_type.save flash[:notice] = t(:notice_successful_lock) - redirect_back_or_default({ action: "index" }) + redirect_back_or_default({ action: "index" }, status: :see_other) end end @@ -130,7 +119,7 @@ module Admin if @cost_type.save flash[:notice] = t(:notice_successful_restore) - redirect_back_or_default({ action: "index" }) + redirect_back_or_default({ action: "index" }, status: :see_other) end end diff --git a/modules/costs/app/controllers/projects/settings/cost_types_controller.rb b/modules/costs/app/controllers/projects/settings/cost_types_controller.rb index a4779d9d8e9..4dbb7f85ed9 100644 --- a/modules/costs/app/controllers/projects/settings/cost_types_controller.rb +++ b/modules/costs/app/controllers/projects/settings/cost_types_controller.rb @@ -64,8 +64,11 @@ class Projects::Settings::CostTypesController < Projects::SettingsController respond_to do |format| format.json { render json: {}, status: } format.html do - flash[:notice] = I18n.t(:notice_successful_update) if status == :ok - flash[:error] = I18n.t("activerecord.errors.messages.is_for_all_cannot_modify") if status != :ok + if status == :ok + flash[:notice] = I18n.t(:notice_successful_update) + else + flash[:error] = I18n.t("activerecord.errors.messages.is_for_all_cannot_modify") + end redirect_to project_settings_cost_types_path(@project) end end diff --git a/modules/costs/app/views/admin/cost_types/_list.html.erb b/modules/costs/app/views/admin/cost_types/_list.html.erb deleted file mode 100644 index 299d68a6699..00000000000 --- a/modules/costs/app/views/admin/cost_types/_list.html.erb +++ /dev/null @@ -1,131 +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. - -++#%> - -<% cost_types = @cost_types.reject(&:deleted_at) -%> - -<% if cost_types.empty? %> - <%= no_results_box %> -<% else %> -
-
- - - - - - - - - - - - - <%= sort_header_tag "name", caption: CostType.model_name.human %> - <%= sort_header_tag "unit", caption: CostType.human_attribute_name(:unit) %> - <%= sort_header_tag "unit_plural", caption: CostType.human_attribute_name(:unit_plural) %> - - - - - - - - <% cost_types.each do |cost_type| %> - - <%= content_tag :td, link_to(cost_type.name, { controller: "/admin/cost_types", action: "edit", id: cost_type }) %> - <%= content_tag :td, cost_type.unit %> - <%= content_tag :td, cost_type.unit_plural %> - <%= content_tag :td, to_currency_with_empty(cost_type.rate_at(@fixed_date)), class: "currency", id: "cost_type_#{cost_type.id}_rate" %> - - <%= content_tag :td, cost_type.is_default? ? icon_wrapper("icon icon-checkmark", I18n.t(:general_text_Yes)) : "" %> - - - - - <% end %> - -
-
-
- - <%= CostType.human_attribute_name(:current_rate) %> - -
-
-
-
-
- - <%= t(:caption_set_rate) %> - -
-
-
-
-
- - <%= t(:caption_default) %> - -
-
-
- <%= form_for cost_type, url: { controller: "/admin/cost_types", action: "set_rate", id: cost_type }, method: :put, html: { class: "inline-label" } do |f| %> - - <%= content_tag :input, - "", - value: "", - name: :rate, - size: 7, - inputmode: :decimal, - placeholder: t(:label_example_placeholder, decimal: unitless_currency_number(1000.50)), - id: "rate_field_#{cost_type.id}" %> - - <%= Setting.costs_currency %> - - - <% end %> - - <%= form_for cost_type, url: admin_cost_type_path(cost_type), - method: :delete, - html: { id: "delete_cost_type_#{cost_type.id}", - class: "delete_cost_type", - title: t(:button_lock) } do |f| %> - - <% end %> -
- -
-
-<% end %> diff --git a/modules/costs/app/views/admin/cost_types/_list_deleted.html.erb b/modules/costs/app/views/admin/cost_types/_list_deleted.html.erb deleted file mode 100644 index f38ff7c72b6..00000000000 --- a/modules/costs/app/views/admin/cost_types/_list_deleted.html.erb +++ /dev/null @@ -1,98 +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. - -++#%> - -<% cost_types = @cost_types.select(&:deleted_at) -%> - -<% if cost_types.empty? %> - <%= no_results_box %> -<% else %> -
-
- - - - - - - - - - - - <%= sort_header_tag("name", caption: CostType.model_name.human) %> - <%= sort_header_tag("unit", caption: CostType.human_attribute_name(:unit)) %> - <%= sort_header_tag("unit_plural", caption: CostType.human_attribute_name(:unit_plural)) %> - - - - - - - <% cost_types.each do |cost_type| %> - - <%= content_tag :td, cost_type.name %> - <%= content_tag :td, cost_type.unit %> - <%= content_tag :td, cost_type.unit_plural %> - <%= content_tag :td, to_currency_with_empty(cost_type.rate_at(@fixed_date)), class: "currency" %> - <%= content_tag :td, cost_type.deleted_at.to_date %> - - - <% end %> - -
-
-
- - <%= CostType.human_attribute_name(:current_rate) %> - -
-
-
-
-
- - <%= t(:caption_locked_on) %> - -
-
-
- <%= form_for cost_type, url: restore_admin_cost_type_path(cost_type), - method: :patch, - html: { id: "restore_cost_type_#{cost_type.id}", - class: "restore_cost_type" } do |f| %> - - <% end %> -
- -
-
-<% end %> diff --git a/modules/costs/app/views/admin/cost_types/index.html.erb b/modules/costs/app/views/admin/cost_types/index.html.erb index ebb694a31a5..6ad93cfb580 100644 --- a/modules/costs/app/views/admin/cost_types/index.html.erb +++ b/modules/costs/app/views/admin/cost_types/index.html.erb @@ -42,6 +42,19 @@ See COPYRIGHT and LICENSE files for more details. %> <%= render(Primer::OpenProject::SubHeader.new) do |subheader| %> + <% subheader.with_filter_component do %> + <%= render(Primer::Alpha::SegmentedControl.new("aria-label": t(:label_filter_plural), full_width: false)) do |control| %> + <% control.with_item(tag: :a, + href: admin_cost_types_path(status: "active"), + label: t(:label_active), + selected: @status == "active") %> + <% control.with_item(tag: :a, + href: admin_cost_types_path(status: "locked"), + label: t("members.menu.locked"), + selected: @status == "locked") %> + <% end %> + <% end %> + <% subheader.with_action_button( scheme: :primary, leading_icon: :plus, @@ -51,44 +64,6 @@ See COPYRIGHT and LICENSE files for more details. ) do CostType.model_name.human end %> - - <% subheader.with_bottom_pane_component do %> - <%= styled_form_tag(admin_cost_types_path, { method: :get, id: "query_form" }) do %> -
- <%= t(:label_filter_plural) %> - -
- <% end %> - <% end %> - <% end %> - -<%= render(Admin::CostTypes::TableComponent.new(rows: @active_cost_types, fixed_date: @fixed_date)) %> - -<% if @include_deleted %> -
-

<%= t(:label_locked_cost_types) %>

- <%= render partial: "list_deleted" %> -
<% end %> + +<%= render(Admin::CostTypes::TableComponent.new(rows: @cost_types, status: @status)) %> diff --git a/modules/costs/config/locales/en.yml b/modules/costs/config/locales/en.yml index 3af93f4a01f..49eb5f222e5 100644 --- a/modules/costs/config/locales/en.yml +++ b/modules/costs/config/locales/en.yml @@ -144,7 +144,6 @@ en: label_current_default_rate: "Current default rate" label_date_on: "on" label_deleted_cost_types: "Deleted cost types" - label_locked_cost_types: "Locked cost types" label_display_cost_entries: "Display unit costs" label_display_time_entries: "Display reported hours" label_display_types: "Display types" @@ -259,6 +258,8 @@ en: errors: no_cost_types_available: "No cost types are available in this project. Please contact an administrator." admin: + columns: + active_projects: "Active projects" cost_type_projects: is_for_all_blank_slate: heading: "This cost type is enabled in all projects" diff --git a/modules/costs/spec/features/cost_types/delete_cost_type_spec.rb b/modules/costs/spec/features/cost_types/delete_cost_type_spec.rb index f4ab70b10da..a9a2b6a74d5 100644 --- a/modules/costs/spec/features/cost_types/delete_cost_type_spec.rb +++ b/modules/costs/spec/features/cost_types/delete_cost_type_spec.rb @@ -47,20 +47,17 @@ RSpec.describe "deleting a cost type", :js do scroll_to_and_click(find("[data-test-selector='op-admin-cost-type-#{cost_type.id}-lock']")) end - # Expect no results if not locked + # Active list becomes empty expect_angular_frontend_initialized expect(page).to have_css ".generic-table--empty-row", wait: 10 - # Show locked - find_by_id("include_deleted").set true - click_on "Apply" + # Switch to the locked tab via the segmented control + click_on I18n.t(:label_locked) wait_for_network_idle - # Expect locked list to render with the cost type - expect(page).to have_text I18n.t(:label_locked_cost_types) - - expect(page).to have_css(".restore_cost_type") - expect(page).to have_css(".cost-types--list-deleted td", text: "Translations") + # The locked cost type appears with a restore action + expect(page).to have_css("[data-test-selector='op-admin-cost-type-#{cost_type.id}-restore']") + expect(page).to have_css("td", text: "Translations") end end