Get inplaceEditField for customField dynamically by the format instead of registring them all directly

This commit is contained in:
Henriette Darge
2026-03-19 15:12:38 +01:00
parent adb88c979b
commit c7afb4968f
11 changed files with 60 additions and 80 deletions
@@ -60,7 +60,11 @@ module OpenProject
end
def field_class
OpenProject::InplaceEdit::FieldRegistry.fetch(attribute)
if custom_field?
OpenProject::InplaceEdit::FieldRegistry.fetch_for_custom_field_format(custom_field&.field_format)
else
OpenProject::InplaceEdit::FieldRegistry.fetch(attribute)
end
end
def edit_field_component(form)
@@ -212,8 +216,7 @@ module OpenProject
# For custom fields, check the is_required attribute
custom_field&.is_required || false
else
# For regular model attributes, check ActiveRecord validations
model.class.validators_on(attribute).any?(ActiveRecord::Validations::PresenceValidator)
false
end
end
@@ -38,10 +38,14 @@ module OpenProject
attr_reader :model, :attribute
# If the writable attribute is not explicitly listed as an argument,
# it will be interpreted as one of the system_arguments and thus overwrite the `writable: false`
# rubocop:disable Lint/UnusedMethodArgument
def initialize(model:, attribute:, writable: nil, truncated: false, has_comment: false, show_comment: false,
**system_arguments)
super(model:, attribute:, writable: false, truncated:, has_comment:, show_comment:, **system_arguments)
end
# rubocop:enable Lint/UnusedMethodArgument
def render_calculation_error
error = custom_field&.first_calculation_error(model)
@@ -3,6 +3,7 @@
padding: var(--base-size-4) var(--base-size-8)
border: 1px solid transparent
border-radius: var(--borderRadius-medium)
@include text-shortener(false)
&:hover, &:focus
border-color: var(--borderColor-default)
@@ -51,8 +51,7 @@ module OpenProject
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
render(Primer::Beta::Link.new(href:, rel: "noopener noreferrer")) do
href
end
end
@@ -46,27 +46,8 @@ class InplaceEditFieldsController < ApplicationController
def update
success = invoke_update_handler
if success
render_success_flash_message_via_turbo_stream(
message: I18n.t(:notice_successful_update)
)
close_dialog_via_turbo_stream(dialog_id) if dialog_id
refresh_calculated_dependents
end
if !success && dialog_id
replace_via_turbo_stream(
component: dialog_field_component,
status: :unprocessable_entity
)
else
replace_via_turbo_stream(
component: component(enforce_edit_mode: !success),
status: success ? :ok : :unprocessable_entity
)
end
handle_update_success if success
replace_field_component(success)
respond_with_turbo_streams
rescue ArgumentError
head :not_found
@@ -96,6 +77,28 @@ class InplaceEditFieldsController < ApplicationController
handler.call(model: @model, params: permitted_params, user: current_user)
end
def handle_update_success
render_success_flash_message_via_turbo_stream(
message: I18n.t(:notice_successful_update)
)
close_dialog_via_turbo_stream(dialog_id) if dialog_id
refresh_calculated_dependents
end
def replace_field_component(success)
if !success && dialog_id
replace_via_turbo_stream(
component: dialog_field_component,
status: :unprocessable_entity
)
else
replace_via_turbo_stream(
component: component(enforce_edit_mode: !success),
status: success ? :ok : :unprocessable_entity
)
end
end
def find_model
model_class = resolve_model_class(params[:model])
@model = model_class.visible.find(params[:id])
+1 -5
View File
@@ -88,7 +88,7 @@ class CustomField < ApplicationRecord
validates :has_comment, absence: true, unless: :can_have_comment?
before_validation :check_searchability
after_create :register_inplace_edit_component
after_destroy :destroy_help_text
# make sure int, float, date, and bool are not searchable
@@ -482,8 +482,4 @@ class CustomField < ApplicationRecord
.where(attribute_name:)
.destroy_all
end
def register_inplace_edit_component
OpenProject::InplaceEdit::FieldRegistry.register_custom_field(id, field_format)
end
end
@@ -53,12 +53,6 @@ Rails.application.config.to_prepare do
OpenProject::InplaceEdit::FieldRegistry.register_custom_field_format_mappings(custom_field_format_mappings)
if CustomField.table_exists?
CustomField.pluck(:id, :field_format).each do |id, field_format|
OpenProject::InplaceEdit::FieldRegistry.register_custom_field(id, field_format)
end
end
# Register the update handler per model
OpenProject::InplaceEdit::UpdateRegistry.register(Project,
handler: OpenProject::InplaceEdit::Handlers::ProjectUpdate,
@@ -44,21 +44,22 @@ module OpenProject
@custom_field_format_mappings = mappings
end
def register_custom_field(id, field_format)
component = @custom_field_format_mappings[field_format]
register("custom_field_#{id}", component) if component
end
def fetch(attribute_name)
@registry.fetch(attribute_name.to_s) { Common::InplaceEditFields::TextInputComponent }
end
def fetch_for_custom_field_format(field_format)
return Common::InplaceEditFields::TextInputComponent if field_format.nil?
@custom_field_format_mappings.fetch(field_format.to_s) { Common::InplaceEditFields::TextInputComponent }
end
@default = new
class << self
attr_reader :default
delegate :register, :fetch, :register_custom_field_format_mappings, :register_custom_field, to: :@default
delegate :register, :fetch, :fetch_for_custom_field_format, :register_custom_field_format_mappings, to: :@default
end
end
end
@@ -46,7 +46,11 @@ RSpec.describe "Edit project custom fields on project overview page", "attribute
context "without attribute help texts defined" do
it "shows field labels without help text link" do
input_fields.each do |custom_field|
field = overview_page.open_inplace_edit_field_for_custom_field(custom_field)
field = if custom_field == text_project_custom_field
overview_page.open_modal_for_custom_field(custom_field)
else
overview_page.open_inplace_edit_field_for_custom_field(custom_field)
end
field.expect_field_label_without_help_text custom_field.name
field.close
end
@@ -63,33 +63,32 @@ RSpec.describe OpenProject::InplaceEdit::FieldRegistry do
end
describe "#register_custom_field_format_mappings" do
it "stores format-to-component mappings used by register_custom_field" do
it "stores format-to-component mappings used by fetch_for_custom_field_format" do
text_component = Class.new
registry.register_custom_field_format_mappings("text" => text_component)
registry.register_custom_field(42, "text")
expect(registry.fetch("custom_field_42")).to eq(text_component)
expect(registry.fetch_for_custom_field_format("text")).to eq(text_component)
end
end
describe "#register_custom_field" do
describe "#fetch_for_custom_field_format" do
let(:text_component) { Class.new }
before do
registry.register_custom_field_format_mappings("text" => text_component)
end
it "registers the correct component for the given field format" do
registry.register_custom_field(1, "text")
expect(registry.fetch("custom_field_1")).to eq(text_component)
it "returns the correct component for the given field format" do
expect(registry.fetch_for_custom_field_format("text")).to eq(text_component)
end
it "does nothing when the format has no mapping" do
registry.register_custom_field(2, "unknown_format")
it "falls back to TextInputComponent when the format has no mapping" do
expect(registry.fetch_for_custom_field_format("unknown_format"))
.to eq(OpenProject::Common::InplaceEditFields::TextInputComponent)
end
expect(registry.fetch("custom_field_2"))
it "falls back to TextInputComponent when field_format is nil" do
expect(registry.fetch_for_custom_field_format(nil))
.to eq(OpenProject::Common::InplaceEditFields::TextInputComponent)
end
end
-24
View File
@@ -742,28 +742,4 @@ RSpec.describe CustomField do
end
end
end
describe "after_create callback" do
it "registers the custom field in the inplace edit field registry" do
custom_field = build(:custom_field, field_format: "string")
allow(OpenProject::InplaceEdit::FieldRegistry).to receive(:register_custom_field)
custom_field.save!
expect(OpenProject::InplaceEdit::FieldRegistry)
.to have_received(:register_custom_field)
.with(anything, "string")
end
it "does not re-register when updated" do
custom_field = create(:custom_field, field_format: "string")
allow(OpenProject::InplaceEdit::FieldRegistry).to receive(:register_custom_field)
custom_field.update!(name: "Updated name")
expect(OpenProject::InplaceEdit::FieldRegistry).not_to have_received(:register_custom_field)
end
end
end