Skip to content

Commit 150560d

Browse files
committed
Some more test fixing.. There is light and the end of the tunnel :fingers_crossed:
1 parent b943d56 commit 150560d

File tree

12 files changed

+241
-174
lines changed

12 files changed

+241
-174
lines changed

app/components/open_project/common/inplace_edit_field_component.rb

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,21 +84,7 @@ def display_field_class
8484
def display_field_component
8585
return nil if display_field_class.nil?
8686

87-
@display_field_component ||= begin
88-
has_comment = custom_field? && custom_field&.has_comment?
89-
additional_args = open_in_dialog? ? dialog_display_arguments : {}
90-
display_field_class.new(
91-
model:,
92-
attribute:,
93-
writable: writable?,
94-
truncated:,
95-
has_comment:,
96-
# Show comment as read-only text when a non-writable user opens the dialog.
97-
# enforce_edit_mode identifies the dialog context.
98-
show_comment: enforce_edit_mode && !writable? && has_comment,
99-
**@system_arguments.merge(additional_args)
100-
)
101-
end
87+
@display_field_component ||= build_display_field_component
10288
end
10389

10490
def wrapper_key
@@ -163,6 +149,22 @@ def model_class
163149

164150
private
165151

152+
def build_display_field_component
153+
has_comment = custom_field? && custom_field&.has_comment?
154+
additional_args = open_in_dialog? ? dialog_display_arguments : {}
155+
display_field_class.new(
156+
model:,
157+
attribute:,
158+
writable: writable?,
159+
truncated:,
160+
has_comment:,
161+
# Show comment as read-only text when a non-writable user opens the dialog.
162+
# enforce_edit_mode identifies the dialog context.
163+
show_comment: enforce_edit_mode && !writable? && has_comment,
164+
**@system_arguments.merge(additional_args)
165+
)
166+
end
167+
166168
def dialog_trigger_arguments
167169
{
168170
dialog_controller_name: "inplace-edit",

app/components/open_project/common/inplace_edit_fields/date_input_component.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ def additional_arguments
5656
{
5757
data: { controller: "inplace-edit",
5858
inplace_edit_url_value: reset_url,
59-
action: "keydown.esc->inplace-edit#request change->inplace-edit#submitForm",
59+
action: "keydown.esc->inplace-edit#request " \
60+
"keydown.enter->inplace-edit#submitForm " \
61+
"change->inplace-edit#submitForm",
6062
qa_field_name: }
6163
}
6264
else

app/components/open_project/common/inplace_edit_fields/display_fields/calculated_value_input_component.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def render_calculation_error
4949

5050
render(Primer::OpenProject::FlexLayout.new(
5151
align_items: :flex_start,
52-
data: { test_selector: "error-cf-#{custom_field.id}" }
52+
data: { test_selector: "error--custom_field_#{custom_field.id}" }
5353
)) do |container|
5454
container.with_column do
5555
render Primer::Beta::Octicon.new(icon: :"alert-fill", color: :danger)

app/components/open_project/common/inplace_edit_fields/display_fields/select_list_component.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,22 @@ def render_display_value
4141
value = model.public_send(attribute)
4242

4343
if value.present? && value != [nil]
44-
if custom_field?
45-
formatted_custom_field_values.presence || t("placeholders.default")
46-
else
47-
value.is_a?(Array) ? value.map(&:to_s).join(", ") : value.to_s
48-
end
44+
render_value(value)
4945
else
5046
t("placeholders.default")
5147
end
5248
end
5349

50+
private
51+
52+
def render_value(value)
53+
if custom_field?
54+
formatted_custom_field_values.presence || t("placeholders.default")
55+
else
56+
value.is_a?(Array) ? value.join(", ") : value.to_s
57+
end
58+
end
59+
5460
def formatted_custom_field_values
5561
return @formatted_custom_field_values if defined?(@formatted_custom_field_values)
5662

app/components/open_project/common/inplace_edit_fields/rich_text_area_component.rb

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,29 +54,36 @@ def initialize(form:, attribute:, model:, show_action_buttons: true, **system_ar
5454

5555
def call
5656
form.rich_text_area(name: attribute,
57-
wrapper_data_attributes: {
58-
controller: "ckeditor-focus",
59-
ckeditor_focus_target: "editor",
60-
ckeditor_focus_autofocus_value: true
61-
},
57+
wrapper_data_attributes: ckeditor_wrapper_data,
6258
**@system_arguments)
6359

6460
comment_field_if_enabled(form)
61+
render_action_buttons if show_action_buttons
62+
end
63+
64+
private
65+
66+
def ckeditor_wrapper_data
67+
{
68+
controller: "ckeditor-focus",
69+
ckeditor_focus_target: "editor",
70+
ckeditor_focus_autofocus_value: true
71+
}
72+
end
6573

66-
if show_action_buttons
67-
form.group(layout: :horizontal, justify_content: :flex_end) do |button_group|
68-
button_group.submit(name: :reset,
69-
type: :submit,
70-
label: I18n.t(:button_cancel),
71-
scheme: :default,
72-
formaction: inplace_edit_field_reset_path(model: model.class.name, id: model.id, attribute:),
73-
formmethod: :get,
74-
test_selector: "op-inplace-edit-field--textarea-cancel")
75-
button_group.submit(name: :submit,
76-
label: I18n.t(:button_save),
77-
scheme: :primary,
78-
test_selector: "op-inplace-edit-field--textarea-save")
79-
end
74+
def render_action_buttons
75+
form.group(layout: :horizontal, justify_content: :flex_end) do |button_group|
76+
button_group.submit(name: :reset,
77+
type: :submit,
78+
label: I18n.t(:button_cancel),
79+
scheme: :default,
80+
formaction: inplace_edit_field_reset_path(model: model.class.name, id: model.id, attribute:),
81+
formmethod: :get,
82+
test_selector: "op-inplace-edit-field--textarea-cancel")
83+
button_group.submit(name: :submit,
84+
label: I18n.t(:button_save),
85+
scheme: :primary,
86+
test_selector: "op-inplace-edit-field--textarea-save")
8087
end
8188
end
8289
end

app/components/open_project/common/inplace_edit_fields/select_list_component.rb

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,7 @@ def initialize(form:, attribute:, model:, show_action_buttons: true, **system_ar
4040
super
4141

4242
@system_arguments[:autocomplete_options] ||= {}
43-
@system_arguments[:autocomplete_options][:model] ||= { id: model.id, name: model.name }
44-
@system_arguments[:autocomplete_options][:inputName] ||= attribute
45-
@system_arguments[:autocomplete_options][:wrapper_id] ||= @system_arguments[:wrapper_id]
46-
if @system_arguments[:autocomplete_options][:focusDirectly].nil?
47-
@system_arguments[:autocomplete_options][:focusDirectly] =
48-
true
49-
end
50-
if @system_arguments[:autocomplete_options][:closeOnSelect].nil?
51-
@system_arguments[:autocomplete_options][:closeOnSelect] =
52-
false
53-
end
43+
set_autocomplete_defaults(model, attribute)
5444
end
5545

5646
def call
@@ -61,24 +51,34 @@ def call
6151
end
6252

6353
comment_field_if_enabled(form)
64-
65-
if show_action_buttons
66-
form.group(layout: :horizontal, justify_content: :flex_end) do |button_group|
67-
button_group.submit(name: :reset,
68-
type: :submit,
69-
label: I18n.t(:button_cancel),
70-
scheme: :default,
71-
formaction: inplace_edit_field_reset_path(model: model.class.name, id: model.id, attribute:),
72-
formmethod: :get)
73-
button_group.submit(name: :submit,
74-
label: I18n.t(:button_save),
75-
scheme: :primary)
76-
end
77-
end
54+
render_action_buttons if show_action_buttons
7855
end
7956

8057
private
8158

59+
def set_autocomplete_defaults(model, attribute)
60+
opts = @system_arguments[:autocomplete_options]
61+
opts[:model] ||= { id: model.id, name: model.name }
62+
opts[:inputName] ||= attribute
63+
opts[:wrapper_id] ||= @system_arguments[:wrapper_id]
64+
opts[:focusDirectly] = true if opts[:focusDirectly].nil?
65+
opts[:closeOnSelect] = false if opts[:closeOnSelect].nil?
66+
end
67+
68+
def render_action_buttons
69+
form.group(layout: :horizontal, justify_content: :flex_end) do |button_group|
70+
button_group.submit(name: :reset,
71+
type: :submit,
72+
label: I18n.t(:button_cancel),
73+
scheme: :default,
74+
formaction: inplace_edit_field_reset_path(model: model.class.name, id: model.id, attribute:),
75+
formmethod: :get)
76+
button_group.submit(name: :submit,
77+
label: I18n.t(:button_save),
78+
scheme: :primary)
79+
end
80+
end
81+
8282
def render_custom_field_input
8383
input_class = if custom_field.multi_value?
8484
CustomFields::Inputs::MultiSelectList

app/controllers/inplace_edit_fields_controller.rb

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,7 @@ def edit
4545
end
4646

4747
def update
48-
handler = update_registry.fetch_handler(@model)
49-
50-
if handler.present?
51-
success = handler.call(
52-
model: @model,
53-
params: permitted_params,
54-
user: current_user
55-
)
56-
else
57-
raise ArgumentError, "Missing update handler for #{@model}"
58-
end
48+
success = invoke_update_handler
5949

6050
if success
6151
render_success_flash_message_via_turbo_stream(
@@ -65,10 +55,17 @@ def update
6555
refresh_calculated_dependents
6656
end
6757

68-
replace_via_turbo_stream(
69-
component: component(enforce_edit_mode: !success),
70-
status: success ? :ok : :unprocessable_entity
71-
)
58+
if !success && dialog_id
59+
replace_via_turbo_stream(
60+
component: dialog_field_component,
61+
status: :unprocessable_entity
62+
)
63+
else
64+
replace_via_turbo_stream(
65+
component: component(enforce_edit_mode: !success),
66+
status: success ? :ok : :unprocessable_entity
67+
)
68+
end
7269

7370
respond_with_turbo_streams
7471
rescue ArgumentError
@@ -92,6 +89,13 @@ def dialog
9289

9390
private
9491

92+
def invoke_update_handler
93+
handler = update_registry.fetch_handler(@model)
94+
raise ArgumentError, "Missing update handler for #{@model}" if handler.blank?
95+
96+
handler.call(model: @model, params: permitted_params, user: current_user)
97+
end
98+
9599
def find_model
96100
model_class = resolve_model_class(params[:model])
97101
@model = model_class.visible.find(params[:id])
@@ -158,19 +162,18 @@ def transform_custom_field_values_params
158162
cf_values = params.dig(model_key, :custom_field_values)
159163
raw_value = cf_values.is_a?(Array) ? cf_values : cf_values&.dig(custom_field_id)
160164

161-
# Handle both single-select and multi-select
162-
processed_value = if raw_value.is_a?(Array)
163-
# Remove empty strings from the hidden field, then extract the actual value.
164-
# FilterableTreeView encodes each selected item as a JSON payload
165-
# {"path":[...],"value":"<id>"} — extract only the "value" field.
166-
cleaned_values = raw_value.compact_blank.filter_map { |v| extract_tree_view_value(v) }
167-
# For single-select, unwrap the array to get the single value
168-
cleaned_values.size <= 1 ? cleaned_values.first : cleaned_values
169-
else
170-
raw_value
171-
end
172-
173-
{ @attribute => processed_value }
165+
{ @attribute => process_cf_raw_value(raw_value) }
166+
end
167+
168+
def process_cf_raw_value(raw_value)
169+
return raw_value unless raw_value.is_a?(Array)
170+
171+
# Remove empty strings from the hidden field, then extract the actual value.
172+
# FilterableTreeView encodes each selected item as a JSON payload
173+
# {"path":[...],"value":"<id>"} — extract only the "value" field.
174+
cleaned_values = raw_value.compact_blank.filter_map { |v| extract_tree_view_value(v) }
175+
# For single-select, unwrap the array to get the single value
176+
cleaned_values.size <= 1 ? cleaned_values.first : cleaned_values
174177
end
175178

176179
def extract_tree_view_value(raw)
@@ -198,6 +201,25 @@ def component(enforce_edit_mode: false)
198201
)
199202
end
200203

204+
# Builds the edit-mode component targeting the field *inside* the dialog.
205+
# Used when an update fails while submitting from a dialog: the error state
206+
# should be shown within the dialog, not at the page trigger location.
207+
# Keeps the dialog field's own :id (not page_component_id) so the Turbo
208+
# Stream targets the correct wrapper inside the dialog, and preserves
209+
# :wrapper_id / :form_id so the re-rendered form still submits via the dialog.
210+
def dialog_field_component
211+
args = system_arguments.to_h.symbolize_keys
212+
213+
OpenProject::Common::InplaceEditFieldComponent.new(
214+
model: @model,
215+
attribute: @attribute,
216+
enforce_edit_mode: true,
217+
show_action_buttons: false,
218+
update_registry:,
219+
**args
220+
)
221+
end
222+
201223
def dialog_id
202224
wrapper_id = system_arguments.to_h["wrapper_id"]
203225
wrapper_id&.delete_prefix("#")
@@ -244,14 +266,16 @@ def calculated_field_turbo_stream(custom_field)
244266
end
245267

246268
def stable_key_system_arguments
247-
@stable_key_system_arguments ||= begin
248-
raw = params[:stable_key_system_arguments]
249-
return {} if raw.blank?
269+
@stable_key_system_arguments ||= parse_stable_key_system_arguments
270+
end
250271

251-
JSON.parse(raw)
252-
rescue JSON::ParserError
253-
{}
254-
end
272+
def parse_stable_key_system_arguments
273+
raw = params[:stable_key_system_arguments]
274+
return {} if raw.blank?
275+
276+
JSON.parse(raw)
277+
rescue JSON::ParserError
278+
{}
255279
end
256280

257281
def update_registry

spec/features/projects/project_custom_fields/overview_page/inputs_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,15 @@ def dialog = overview_page.open_modal_for_custom_field(custom_field).dialog
158158
shared_examples "a rich text custom field input" do
159159
it "shows the correct value if given" do
160160
dialog.within_async_content(close_after_yield: true) do
161-
field.expect_value(expected_initial_value)
161+
form_field.expect_value(expected_initial_value)
162162
end
163163
end
164164

165165
it "shows a blank input if no value or default value is given" do
166166
custom_field.custom_values.destroy_all
167167

168168
dialog.within_async_content(close_after_yield: true) do
169-
field.expect_value(expected_blank_value)
169+
form_field.expect_value(expected_blank_value)
170170
end
171171
end
172172

@@ -175,7 +175,7 @@ def dialog = overview_page.open_modal_for_custom_field(custom_field).dialog
175175
custom_field.update!(default_value:)
176176

177177
dialog.within_async_content(close_after_yield: true) do
178-
field.expect_value(default_value)
178+
form_field.expect_value(default_value)
179179
end
180180
end
181181

@@ -244,7 +244,7 @@ def dialog = overview_page.open_modal_for_custom_field(custom_field).dialog
244244
let(:default_value) { "https://openproject.org" }
245245
let(:expected_blank_value) { "" }
246246
let(:expected_initial_value) { "https://www.openproject.org" }
247-
let(:field) { FormFields::Primerized::InputField.new(custom_field) }
247+
let(:form_field) { FormFields::Primerized::InputField.new(custom_field) }
248248

249249
it_behaves_like "a custom field input"
250250

0 commit comments

Comments
 (0)