remove old implementation of editing project attributes in the sidebar

This commit is contained in:
Henriette Darge
2026-03-12 15:38:07 +01:00
parent 1f8853a024
commit ec2fea03fb
16 changed files with 8 additions and 874 deletions
@@ -1,3 +1,2 @@
@import "./overviews/project_custom_fields/show_component.sass"
@import "./overviews/project_phases/item_component.sass"
@import "./overviews/overview_grid_component.sass"
@@ -1,30 +0,0 @@
<%=
render(
Primer::Alpha::Dialog.new(
title: dialog_title,
classes: "Overlay--size-large-portrait",
size: :large,
id: dialog_id
)
) do |d|
d.with_header(variant: :large)
d.with_body(classes: "Overlay-body_autocomplete_height") do
render(body_component)
end
d.with_footer do
component_collection do |footer_collection|
footer_collection.with_component(
Primer::Beta::Button.new(
data: {
"close-dialog-id": dialog_id
}
)
) do
close_button_title
end
footer_buttons(footer_collection)
end
end
end
%>
@@ -1,71 +0,0 @@
# 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 DialogComponent < ApplicationComponent
include ApplicationHelper
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
def initialize(project:, project_custom_field:)
super
@project = project
@project_custom_field = project_custom_field
end
private
def dialog_title
@project_custom_field.project_custom_field_section.name
end
def dialog_id
"project-custom-field-dialog-#{@project_custom_field.id}"
end
def wrapper_id
"##{dialog_id}"
end
def body_component
fail NoMethodError, "Must be overridden in subclass"
end
def close_button_title
fail NoMethodError, "Must be overridden in subclass"
end
def footer_buttons(footer_collection)
# noop
end
end
end
end
@@ -1,68 +0,0 @@
<%= helpers.angular_component_tag "opce-custom-modal-overlay" %>
<%=
component_wrapper do
primer_form_with(
id: "project-custom-field-edit-form",
model: @project,
method: :put,
data: { turbo: true, turbo_stream: true, "test-selector": "async-dialog-content" },
url: project_custom_field_path(project_id: @project.id, id: @project_custom_field.id)
) do |f|
if @project_custom_field.hierarchical_list?
form_field_name = "project[custom_field_values][]"
concat(
render_inline_form(f) do |hidden_form|
hidden_form.hidden(name: form_field_name, value: "", scope_name_to_model: false)
end
)
concat(
render(
Primer::OpenProject::FilterableTreeView.new(
form_arguments: { builder: f, name: "custom_field_values" },
include_sub_items_check_box_arguments: { hidden: true },
filter_mode_control_arguments: { hidden: true }
)
) do |tree_view|
current_values = Array(@project.custom_value_for(@project_custom_field)).map(&:value)
checked_fn = lambda { |item| current_values.include?(item.id.to_s) }
item_formatter = standard_tree_view_item_formatter
label_fn = lambda { |item| item_formatter.format(item:) }
item_options = {
expanded_fn: ->(*) { true },
label_fn:,
checked_fn:,
select_variant: @project_custom_field.multi_value? ? :multiple : :single
}
populate_tree_view(tree_view, @project_custom_field, item_options:)
end
)
if @project_custom_field.has_comment?
concat(
f.fields_for(:custom_comments) do |builder|
render(
CustomFields::CommentField.new(
builder,
custom_field: @project_custom_field,
object: @project
)
)
end
)
end
else
render(
Projects::CustomFields::Form.new(
f,
project: @project,
custom_field: @project_custom_field,
wrapper_id:
)
)
end
end
end
%>
@@ -1,53 +0,0 @@
# 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 EditComponent < ApplicationComponent
include ApplicationHelper
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
include CustomFieldHierarchyTreeViewHelper
attr_reader :wrapper_id
def initialize(project:, project_custom_field:, wrapper_id: nil)
super
@project = project
@project_custom_field = project_custom_field
@wrapper_id = wrapper_id
end
def wrapper_uniq_by
@project_custom_field.id
end
end
end
end
@@ -1,65 +0,0 @@
# 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 EditDialogComponent < DialogComponent
private
def body_component
Overviews::ProjectCustomFields::EditComponent.new(
project_custom_field: @project_custom_field,
project: @project,
wrapper_id:
)
end
def close_button_title
t("button_cancel")
end
def footer_buttons(footer_collection)
footer_collection.with_component(
Primer::Beta::Button.new(
scheme: :primary,
type: :submit,
form: "project-custom-field-edit-form",
data: {
test_selector: "save-project-attributes-button",
turbo: true
}
)
) do
t("button_save")
end
end
end
end
end
@@ -36,75 +36,6 @@ module Overviews
def limited_space?
@project_custom_field.field_format == "text" &&
@project_custom_field.project_custom_field_section&.shown_in_overview_sidebar?
end
def show_comment? = false
def value_wrapper_attributes
if allowed_to_edit?
if calculated_value? && !has_comment?
non_editable_wrapper(id: calculated_value_tooltip_id)
else
modal_wrapper
end
elsif has_comment?
modal_wrapper
else
non_editable_wrapper
end
end
def allowed_to_edit?
User.current.allowed_in_project?(:edit_project_attributes, @project)
end
def modal_wrapper
action_label_key = allowed_to_edit? ? :label_edit_x : :label_view_x
url = if allowed_to_edit?
edit_project_custom_field_path(project_id: @project, id: @project_custom_field)
else
project_custom_field_path(project_id: @project, id: @project_custom_field)
end
{
tag: :div,
classes: "project-custom-field-clickable",
data: {
controller: "project-custom-field-modal async-dialog",
"project-custom-field-modal-url-value": url,
action: "click->project-custom-field-modal#open " \
"keydown.enter->project-custom-field-modal#open " \
"keydown.space->project-custom-field-modal#open " \
"project-custom-field-modal:open-dialog->async-dialog#handleOpenDialog"
},
aria: {
label: [
I18n.t(action_label_key, x: @project_custom_field.name),
I18n.t(:label_value_x, x: accessible_value_text)
].join(", ")
},
role: "button",
tabindex: 0,
test_selector: "project-custom-field-modal-button-#{@project_custom_field.id}"
}
end
def non_editable_wrapper(**)
{
tag: :div,
classes: "project-custom-field-non-editable",
aria: {
disabled: true,
label: [
@project_custom_field.name,
I18n.t(:label_value_x, x: accessible_value_text)
].join(", ")
},
tabindex: 0,
**
}
end
end
end
@@ -5,7 +5,6 @@
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
)
)
@@ -40,11 +40,6 @@ module Overviews
@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
@@ -1,73 +1,6 @@
<%= render OpenProject::Common::InplaceEditFieldComponent.new(
model: @project,
attribute: @project_custom_field.attribute_name.to_sym,
open_in_dialog: limited_space?,
truncated: limited_space?
) %>
<!--
<%# if show_special_field? %>
<%#=
flex_layout(
align_items: :flex_start,
justify_content: :space_between,
classes: "op-project-custom-field-container",
data: {
test_selector: "project-custom-field-#{@project_custom_field.id}"
}
) do |custom_field_value_container|
# temporarily using inline styles in order to align the content as desired
custom_field_value_container.with_row(mb: 1) do
render OpenProject::Common::AttributeLabelComponent.new(
attribute: @project_custom_field.attribute_name,
model: @project,
required: required?
) do
render(Primer::Beta::Text.new(font_weight: :bold)) { @project_custom_field.name }
end
end
custom_field_value_container.with_row(w: :full) do
render(authorized_edit_wrapper) do
if not_set?
render(Primer::Beta::Text.new) { t("placeholders.default") }
else
render_value
end
end
end
custom_field_value_container.with_row(w: :full) do
render OpenProject::Common::AttributeHelpTextCaptionComponent.new(
help_text: helpers.help_text_for(
@project,
@project_custom_field.attribute_name,
current_user: helpers.current_user
)
)
end
if calculation_error?
custom_field_value_container.with_row(w: :full) do
render_calculation_error
end
end
if show_comment?
custom_field_value_container.with_row(w: :full, mt: 1) do
render Primer::Alpha::TextArea.new(
name: :custom_comment,
label: I18n.t("attributes.comment"),
value: @project_custom_field.comment_for(@project)&.text,
rows: 5,
readonly: true
)
end
end
end
%>
<%#= render_calculated_value_tooltip if calculated_value? %>
<%# else %>-->
<%# end %>
model: @project,
attribute: @project_custom_field.attribute_name.to_sym,
open_in_dialog: limited_space?,
truncated: limited_space?
) %>
@@ -31,139 +31,17 @@
module Overviews
module ProjectCustomFields
class ShowComponent < ApplicationComponent
include ApplicationHelper
include CalculatedValues::ErrorsHelper
include CustomFieldsHelper
include OpPrimer::ComponentHelpers
delegate :has_comment?, :calculated_value?, to: :@project_custom_field
def initialize(project_custom_field:, project_custom_field_values:, project:)
def initialize(project_custom_field:, project:)
super
@project_custom_field = project_custom_field
@project_custom_field_values = Array(project_custom_field_values)
@project = project
end
private
def show_comment? = @project_custom_field.has_comment?
def value_wrapper_attributes = {}
def value_wrapper
Primer::Beta::Text.new(**value_wrapper_attributes)
end
def required?
@project_custom_field.required? && !@project_custom_field.calculated_value?
end
def not_set?
@project_custom_field_values.none?(&:value?)
end
def calculation_error?
@project_custom_field.first_calculation_error(@project).present?
end
def render_calculation_error
error = @project_custom_field.first_calculation_error(@project)
render(Primer::OpenProject::FlexLayout.new(align_items: :flex_start,
data: {
test_selector: "error-cf-#{@project_custom_field.id}"
})) do |container|
container.with_column do
render Primer::Beta::Octicon.new(icon: :"alert-fill", color: :danger)
end
container.with_column(ml: 2) do
render Primer::Beta::Text.new(color: :danger) do
calculated_value_error_msg(error)
end
end
end
end
def render_calculated_value_tooltip
render Primer::Alpha::Tooltip.new(
for_id: calculated_value_tooltip_id,
type: :description,
text: I18n.t("custom_fields.calculated_field_not_editable"),
direction: :s
)
end
def calculated_value_tooltip_id
calculated_value? ? "calculated-field-tooltip-#{@project_custom_field.id}" : nil
end
def render_value
case @project_custom_field.field_format
when "link"
render_link
when "text"
render_long_text
when "user"
render_user
else
render_custom_field_values
end
end
def render_long_text
render OpenProject::Common::AttributeComponent.new("dialog-cf-#{@project_custom_field.id}",
@project_custom_field.name,
@project_custom_field_values.first&.value,
lines: 3)
end
def render_user
if @project_custom_field.multi_value?
flex_layout do |avatar_container|
@project_custom_field_values.each do |cf_value|
avatar_container.with_row do
render_avatar(cf_value.typed_value)
end
end
end
else
render_avatar(@project_custom_field_values.first&.typed_value)
end
end
def render_avatar(user)
render(Users::AvatarComponent.new(user:, size: :mini))
end
def render_link
href = @project_custom_field_values.first&.value
link = Addressable::URI.parse(href)
return href unless link
target = link.host == Setting.host_without_protocol ? "_top" : "_blank"
render(Primer::Beta::Link.new(href:, rel: "noopener noreferrer", target:)) do
href
end
end
def render_custom_field_values
render(Primer::Beta::Text.new) { custom_field_values }
end
def accessible_value_text
return I18n.t("placeholders.default") if not_set?
custom_field_values
end
def custom_field_values
return @custom_field_values if defined?(@custom_field_values)
values = @project_custom_field_values.map { |v| format_value(v.value, @project_custom_field) }
@custom_field_values = @project_custom_field.multi_value? ? values.join(", ") : values.first
def limited_space?
false
end
end
end
@@ -1,22 +0,0 @@
.project-custom-fields-rich-text-preview
:last-child
display: inline
// Style non-editable fields to match hover-input dimensions
.project-custom-field-non-editable
@include hover-input-base
min-height: 1.2em // Ensure consistent height even when empty
@media (hover: none) and (pointer: coarse)
background-color: var(--bgColor-disabled)
color: var(--fgColor-disabled)
@media (hover: hover)
&:hover, &:focus-visible
background-color: var(--bgColor-disabled)
color: var(--fgColor-disabled)
// Style clickable fields
.project-custom-field-clickable
@include hover-input-base
cursor: pointer
min-height: 1.2em
@@ -1,49 +0,0 @@
# 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 ShowDialogComponent < DialogComponent
private
def body_component
Overviews::ProjectCustomFields::ShowComponent.new(
project_custom_field: @project_custom_field,
project_custom_field_values: @project.custom_values_for_custom_field(@project_custom_field),
project: @project
)
end
def close_button_title
t("button_close")
end
end
end
end
@@ -1,124 +0,0 @@
# 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.
#++
class Overviews::ProjectCustomFieldsController < ApplicationController
include OpTurbo::ComponentStream
before_action :find_project_by_project_id
before_action :find_project_custom_field
before_action :authorize
def show
respond_with_dialog(
Overviews::ProjectCustomFields::ShowDialogComponent.new(
project: @project,
project_custom_field: @custom_field
)
)
end
def edit
respond_with_dialog(
Overviews::ProjectCustomFields::EditDialogComponent.new(
project: @project,
project_custom_field: @custom_field
)
)
end
def update
# FIXME: submitted format of form parameters are not configurable for the tree view component. Hence, we
# need to process it before giving them in standard format to the update service.
if @custom_field.hierarchical_list?
process_hierarchy_params
end
service_call = ::Projects::UpdateService
.new(
user: current_user,
model: @project,
contract_options: { project_attributes_only: true }
)
.call(permitted_params.project)
if service_call.success?
if field_shown_in_sidebar?(@custom_field)
update_sidebar_component
else
update_widgets_component
end
else
handle_errors(service_call.result, @custom_field)
end
respond_to_with_turbo_streams(status: service_call.success? ? :ok : :unprocessable_entity)
end
private
def process_hierarchy_params
values = params.dig(:project, :custom_field_values)
ids = Array(values).reject(&:empty?).map do |value|
MultiJson.load(value, symbolize_keys: true)[:value]
end
params[:project][:custom_field_values] = { @custom_field.id.to_s => ids.one? ? ids.first : ids }
end
def find_project_custom_field
@custom_field = @project.available_custom_fields.find(params[:id])
end
def handle_errors(project_with_errors, custom_field)
update_via_turbo_stream(
component: Overviews::ProjectCustomFields::EditComponent.new(
project: project_with_errors,
project_custom_field: custom_field
)
)
end
def update_sidebar_component
update_via_turbo_stream(
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
-2
View File
@@ -12,8 +12,6 @@ Rails.application.routes.draw do
get "project_custom_fields_sidebar" => :project_custom_fields_sidebar, as: :custom_fields_sidebar
get "project_life_cycle_sidebar" => :project_life_cycle_sidebar, as: :life_cycle_sidebar
end
resources :project_custom_fields, only: %i[show edit update], as: :custom_fields
end
end
end
@@ -1,117 +0,0 @@
# 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 "rails_helper"
RSpec.describe Overviews::ProjectCustomFieldsController do
describe "routing" do
describe "#show" do
it do
expect(get("/projects/my-project/project_custom_fields/33"))
.to route_to(
controller: "overviews/project_custom_fields",
action: "show",
project_id: "my-project",
id: "33"
)
end
end
describe "#edit" do
it do
expect(get("/projects/my-project/project_custom_fields/33/edit"))
.to route_to(
controller: "overviews/project_custom_fields",
action: "edit",
project_id: "my-project",
id: "33"
)
end
end
describe "PUT/PATCH #update" do
it do
expect(put("/projects/my-project/project_custom_fields/44"))
.to route_to(
controller: "overviews/project_custom_fields",
action: "update",
project_id: "my-project",
id: "44"
)
end
it do
expect(patch("/projects/my-project/project_custom_fields/44"))
.to route_to(
controller: "overviews/project_custom_fields",
action: "update",
project_id: "my-project",
id: "44"
)
end
end
end
describe "named routing" do
describe "GET #edit" do
it do
expect(get(edit_project_custom_field_path("my-project", 33)))
.to route_to(
controller: "overviews/project_custom_fields",
action: "edit",
project_id: "my-project",
id: "33"
)
end
end
describe "PUT/PATCH #update" do
it do
expect(put(project_custom_field_path("my-project", 44)))
.to route_to(
controller: "overviews/project_custom_fields",
action: "update",
project_id: "my-project",
id: "44"
)
end
it do
expect(patch("/projects/my-project/project_custom_fields/44"))
.to route_to(
controller: "overviews/project_custom_fields",
action: "update",
project_id: "my-project",
id: "44"
)
end
end
end
end