mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Primerize cost type list
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %>
|
||||
<div class="generic-table--container">
|
||||
<div class="generic-table--results-container">
|
||||
<table class="generic-table" data-controller="table-highlighting">
|
||||
<colgroup>
|
||||
<col>
|
||||
<col>
|
||||
<col>
|
||||
<col>
|
||||
<col>
|
||||
<col>
|
||||
<col data-highlight="false">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<%= 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) %>
|
||||
<th>
|
||||
<div class="generic-table--sort-header-outer">
|
||||
<div class="generic-table--sort-header">
|
||||
<span>
|
||||
<%= CostType.human_attribute_name(:current_rate) %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="generic-table--sort-header-outer">
|
||||
<div class="generic-table--sort-header">
|
||||
<span>
|
||||
<%= t(:caption_set_rate) %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="generic-table--sort-header-outer">
|
||||
<div class="generic-table--sort-header">
|
||||
<span>
|
||||
<%= t(:caption_default) %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th><div class="generic-table--empty-header"></div></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% cost_types.each do |cost_type| %>
|
||||
<tr>
|
||||
<%= 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" %>
|
||||
<td>
|
||||
<%= form_for cost_type, url: { controller: "/admin/cost_types", action: "set_rate", id: cost_type }, method: :put, html: { class: "inline-label" } do |f| %>
|
||||
<label class="sr-only" for="<%= "rate_field_#{cost_type.id}" %>"><%= t(:caption_set_rate) %></label>
|
||||
<%= 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}" %>
|
||||
<span class="form-label">
|
||||
<%= Setting.costs_currency %>
|
||||
</span>
|
||||
<button type="submit"
|
||||
class="button submit_cost_type">
|
||||
<%= icon_wrapper("icon icon-save", I18n.t(:caption_save_rate)) %>
|
||||
</button>
|
||||
<% end %>
|
||||
</td>
|
||||
<%= content_tag :td, cost_type.is_default? ? icon_wrapper("icon icon-checkmark", I18n.t(:general_text_Yes)) : "" %>
|
||||
<td class="buttons">
|
||||
<%= 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| %>
|
||||
<button type="submit"
|
||||
class="button--link submit_cost_type">
|
||||
<%= icon_wrapper("icon icon-locked", I18n.t(:button_lock)) %>
|
||||
</button>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="display:none;">
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -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 %>
|
||||
<div class="generic-table--container">
|
||||
<div class="generic-table--results-container">
|
||||
<table class="generic-table locked_cost_types">
|
||||
<colgroup>
|
||||
<col>
|
||||
<col>
|
||||
<col>
|
||||
<col>
|
||||
<col>
|
||||
<col data-highlight="false">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<%= 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)) %>
|
||||
<th>
|
||||
<div class="generic-table--sort-header-outer">
|
||||
<div class="generic-table--sort-header">
|
||||
<span>
|
||||
<%= CostType.human_attribute_name(:current_rate) %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="generic-table--sort-header-outer">
|
||||
<div class="generic-table--sort-header">
|
||||
<span>
|
||||
<%= t(:caption_locked_on) %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th><div class="generic-table--empty-header"></div></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% cost_types.each do |cost_type| %>
|
||||
<tr>
|
||||
<%= 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 %>
|
||||
<td class="buttons">
|
||||
<%= 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| %>
|
||||
<button type="submit"
|
||||
class="button--link submit_cost_type">
|
||||
<%= icon_wrapper("icon icon-unlocked", I18n.t(:button_unlock)) %>
|
||||
</button>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -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 %>
|
||||
<fieldset id="filters" class="simple-filters--container">
|
||||
<legend><%= t(:label_filter_plural) %></legend>
|
||||
<ul class="simple-filters--filters">
|
||||
<li class="simple-filters--filter">
|
||||
<%= styled_label_tag :fixed_date, t(:"attributes.fixed_date"), class: "simple-filters--filter-name" %>
|
||||
<div class='simple-filters--filter-value'>
|
||||
<%= angular_component_tag "opce-basic-single-date-picker",
|
||||
inputs: {
|
||||
value: @fixed_date,
|
||||
id: :start_date,
|
||||
name: :fixed_date
|
||||
} %>
|
||||
</div>
|
||||
</li>
|
||||
<li class="simple-filters--filter">
|
||||
<%= styled_label_tag :include_deleted, t(:caption_show_locked), class: "simple-filters--filter-name -small" %>
|
||||
<div class="simple-filters--filter-value">
|
||||
<%= styled_check_box_tag :include_deleted, "1", @include_deleted, autocomplete: "off" %>
|
||||
</div>
|
||||
</li>
|
||||
<li class="simple-filters--controls">
|
||||
<%= submit_tag t(:button_apply), class: "button -primary -small" %>
|
||||
<%= link_to t(:button_clear), cost_types_path, class: "button -small -with-icon icon-undo" %>
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= render(Admin::CostTypes::TableComponent.new(rows: @active_cost_types, fixed_date: @fixed_date)) %>
|
||||
|
||||
<% if @include_deleted %>
|
||||
<div class="cost-types--list-deleted">
|
||||
<h3><%= t(:label_locked_cost_types) %></h3>
|
||||
<%= render partial: "list_deleted" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render(Admin::CostTypes::TableComponent.new(rows: @cost_types, status: @status)) %>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user