Skip to content

Commit 141ae1a

Browse files
authored
Merge pull request #21972 from opf/feature/71380-inplace-edit-for-project-attributes-on-project-overview-page
[71380] Inplace edit for project attributes on project overview page
2 parents cac71a9 + 4f9a486 commit 141ae1a

File tree

84 files changed

+3921
-1251
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+3921
-1251
lines changed
Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
1-
<%= component_wrapper(tag: :div, class: "op-inplace-edit", data: { test_selector: wrapper_test_selector }) do %>
2-
<% if display_field_component.present? && !enforce_edit_mode %>
1+
<%= component_wrapper(
2+
tag: :div,
3+
class: "op-inplace-edit",
4+
uniq_by: wrapper_uniq_by,
5+
data: {
6+
test_selector: wrapper_test_selector,
7+
turbo_stream_target: wrapper_id,
8+
inplace_edit_stable_key: wrapper_uniq_by,
9+
inplace_edit_system_arguments: @system_arguments.to_json
10+
}
11+
) do %>
12+
<% if display_field_component.present? && (!enforce_edit_mode || !writable?) %>
313
<%= render display_field_component %>
414
<% else %>
5-
<%= primer_form_with(
6-
model:,
7-
url: inplace_edit_field_update_path(model: model.class.name, id: model.id, attribute:),
8-
method: :patch,
9-
data: { turbo_stream: true }
10-
) do |form|
11-
render_field_component = ->(f) { render edit_field_component(f) } # The render_inline_form method looses context and thus does not know about the `field_component` method
12-
system_arguments = @system_arguments
15+
<%= primer_form_with(**form_options) do |form|
16+
render_field_component = ->(f) { render edit_field_component(f) } # The render_inline_form method looses context and thus does not know about the `field_component` method
17+
system_arguments = @system_arguments
1318

14-
render_inline_form(form) do |f|
15-
f.hidden name: "system_arguments_json", value: system_arguments.to_json
16-
render_field_component.call(f)
17-
end
19+
render_inline_form(form) do |f|
20+
f.hidden name: "system_arguments_json", value: system_arguments.to_json
21+
render_field_component.call(f)
22+
end
1823
end %>
1924
<% end %>
2025
<% end %>

app/components/open_project/common/inplace_edit_field_component.rb

Lines changed: 141 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,28 +33,46 @@ module Common
3333
class InplaceEditFieldComponent < ViewComponent::Base
3434
include OpTurbo::Streamable
3535

36-
attr_reader :model, :attribute, :enforce_edit_mode
36+
attr_reader :model, :attribute, :enforce_edit_mode, :open_in_dialog, :show_action_buttons, :truncated
3737

38-
def initialize(model:, attribute:, enforce_edit_mode: false,
39-
update_registry: OpenProject::InplaceEdit::UpdateRegistry.default, **system_arguments)
38+
def initialize(model:,
39+
attribute:,
40+
enforce_edit_mode: false,
41+
open_in_dialog: false,
42+
show_action_buttons: true,
43+
truncated: false,
44+
update_registry: OpenProject::InplaceEdit::UpdateRegistry.default,
45+
**system_arguments)
4046
super()
4147
@model = model
4248
@attribute = attribute
4349
@enforce_edit_mode = enforce_edit_mode
50+
@open_in_dialog = open_in_dialog
51+
@show_action_buttons = show_action_buttons
52+
@truncated = truncated
4453
@update_registry = update_registry
4554
@system_arguments = system_arguments
55+
4656
@system_arguments[:id] = system_arguments[:id] || SecureRandom.uuid
57+
@system_arguments[:required] ||= required?
58+
@system_arguments[:label] ||= field_label
59+
@system_arguments[:truncated] = truncated
4760
end
4861

4962
def field_class
50-
OpenProject::InplaceEdit::FieldRegistry.fetch(attribute)
63+
if custom_field?
64+
OpenProject::InplaceEdit::FieldRegistry.fetch_for_custom_field_format(custom_field&.field_format)
65+
else
66+
OpenProject::InplaceEdit::FieldRegistry.fetch(attribute)
67+
end
5168
end
5269

5370
def edit_field_component(form)
5471
field_class.new(
5572
form:,
5673
attribute:,
5774
model:,
75+
show_action_buttons:,
5876
**@system_arguments
5977
)
6078
end
@@ -70,20 +88,102 @@ def display_field_class
7088
def display_field_component
7189
return nil if display_field_class.nil?
7290

73-
display_field_class.new(model:, attribute:, writable: writable?, **@system_arguments)
91+
@display_field_component ||= build_display_field_component
7492
end
7593

7694
def wrapper_key
77-
model_class = @model.class.name.parameterize(separator: "_")
7895
"op-inplace-edit-field--#{model_class}-#{model.id}--#{attribute.name}--#{@system_arguments[:id]}"
7996
end
8097

8198
def wrapper_test_selector
82-
"op-inplace-edit-field"
99+
"op-inplace-edit-field--#{model_class}-#{model.id}--#{attribute.name}"
100+
end
101+
102+
def wrapper_uniq_by
103+
"#{model_class}_#{@model.id}_#{@attribute}"
104+
end
105+
106+
def form_id
107+
@system_arguments[:form_id]
108+
end
109+
110+
def wrapper_id
111+
@system_arguments[:wrapper_id]
112+
end
113+
114+
def form_options
115+
options = {
116+
model: @model,
117+
url: inplace_edit_field_update_path(
118+
model: @model.class.name,
119+
id: @model.id,
120+
attribute: @attribute
121+
),
122+
method: :patch,
123+
data: { turbo_stream: true,
124+
test_selector: "op-inplace-edit-field--form" }
125+
126+
}
127+
128+
options[:id] = form_id if form_id.present?
129+
options
130+
end
131+
132+
def open_in_dialog?
133+
@open_in_dialog || field_class.open_in_dialog? || (custom_field? && custom_field&.has_comment?)
134+
end
135+
136+
def dialog_edit_url
137+
return unless open_in_dialog?
138+
139+
inplace_edit_field_dialog_path(
140+
model: model.class.name,
141+
id: model.id,
142+
attribute:,
143+
system_arguments_json: @system_arguments
144+
.except(:id)
145+
.merge(page_component_id: @system_arguments[:id], writable: writable?)
146+
.to_json
147+
)
148+
end
149+
150+
def model_class
151+
@model_class ||= @model.class.name.parameterize(separator: "_")
83152
end
84153

85154
private
86155

156+
def build_display_field_component
157+
has_comment = custom_field? && custom_field&.has_comment?
158+
additional_args = open_in_dialog? ? dialog_display_arguments : {}
159+
display_field_class.new(
160+
model:,
161+
attribute:,
162+
writable: writable?,
163+
truncated:,
164+
has_comment:,
165+
# Show comment as read-only text when a non-writable user opens the dialog.
166+
# enforce_edit_mode identifies the dialog context.
167+
show_comment: enforce_edit_mode && !writable? && has_comment,
168+
**@system_arguments.merge(additional_args)
169+
)
170+
end
171+
172+
def dialog_trigger_arguments
173+
{
174+
dialog_controller_name: "inplace-edit",
175+
dialog_url: dialog_edit_url
176+
}
177+
end
178+
179+
# When inside a dialog and the field is not writable, strip dialog trigger args
180+
# to prevent opening a nested dialog from the display component.
181+
def dialog_display_arguments
182+
return {} if enforce_edit_mode && !writable?
183+
184+
dialog_trigger_arguments
185+
end
186+
87187
def writable?
88188
return @writable if defined?(@writable)
89189

@@ -95,6 +195,40 @@ def writable?
95195
false
96196
end
97197
end
198+
199+
def field_label
200+
# Check if this is a custom field attribute
201+
if custom_field? && custom_field
202+
return custom_field.name
203+
end
204+
205+
label = model.class.human_attribute_name(attribute)
206+
label = label.titleize if attribute.to_s.include?("_")
207+
label
208+
end
209+
210+
def required?
211+
return @required if instance_variable_defined?(:@required)
212+
213+
@required = if @system_arguments.key?(:required)
214+
@system_arguments[:required]
215+
elsif custom_field?
216+
# For custom fields, check the is_required attribute
217+
custom_field&.is_required || false
218+
else
219+
false
220+
end
221+
end
222+
223+
def custom_field?
224+
attribute.to_s.start_with?("custom_field_")
225+
end
226+
227+
def custom_field
228+
return @custom_field if defined?(@custom_field)
229+
230+
@custom_field = CustomField.find_by(id: attribute.to_s.sub("custom_field_", "").to_i)
231+
end
98232
end
99233
end
100234
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<%=
2+
render(
3+
Primer::Alpha::Dialog.new(
4+
title: dialog_title,
5+
classes: "Overlay--size-large-portrait op-inplace-edit--dialog",
6+
size: :large,
7+
id: dialog_id
8+
)
9+
) do |d|
10+
d.with_header(variant: :large)
11+
d.with_body(classes: "Overlay-body_autocomplete_height",
12+
test_selector: "async-dialog-content") do
13+
render(edit_component)
14+
end
15+
d.with_footer do
16+
component_collection do |footer_collection|
17+
footer_collection.with_component(
18+
Primer::Beta::Button.new(data: { "close-dialog-id": dialog_id })
19+
) do
20+
writable? ? t("button_cancel") : t("button_close")
21+
end
22+
if writable?
23+
footer_collection.with_component(
24+
Primer::Beta::Button.new(scheme: :primary,
25+
type: :submit,
26+
form: form_id,
27+
data: {
28+
test_selector: "save-inplace-edit-field-button",
29+
turbo: true
30+
})
31+
) do
32+
t("button_save")
33+
end
34+
end
35+
end
36+
end
37+
end
38+
%>

modules/overviews/app/components/overviews/project_custom_fields/dialog_component.rb renamed to app/components/open_project/common/inplace_edit_field_dialog_component.rb

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,43 +28,53 @@
2828
# See COPYRIGHT and LICENSE files for more details.
2929
#++
3030

31-
module Overviews
32-
module ProjectCustomFields
33-
class DialogComponent < ApplicationComponent
34-
include ApplicationHelper
31+
module OpenProject
32+
module Common
33+
class InplaceEditFieldDialogComponent < ViewComponent::Base
3534
include OpTurbo::Streamable
3635
include OpPrimer::ComponentHelpers
3736

38-
def initialize(project:, project_custom_field:)
39-
super
40-
@project = project
41-
@project_custom_field = project_custom_field
37+
def initialize(model:, attribute:, system_arguments: {})
38+
super()
39+
@model = model
40+
@attribute = attribute
41+
@system_arguments = system_arguments
4242
end
4343

4444
private
4545

46+
def writable?
47+
@system_arguments[:writable] == true
48+
end
49+
4650
def dialog_title
47-
@project_custom_field.project_custom_field_section.name
51+
@system_arguments[:label] || @model.class.human_attribute_name(@attribute)
4852
end
4953

5054
def dialog_id
51-
"project-custom-field-dialog-#{@project_custom_field.id}"
55+
model_class = @model.class.name.parameterize(separator: "_")
56+
"inplace-edit-field-dialog--#{model_class}-#{@model.id}--#{@attribute}"
5257
end
5358

5459
def wrapper_id
5560
"##{dialog_id}"
5661
end
5762

58-
def body_component
59-
fail NoMethodError, "Must be overridden in subclass"
60-
end
61-
62-
def close_button_title
63-
fail NoMethodError, "Must be overridden in subclass"
63+
def form_id
64+
"inplace-edit-field-form-#{dialog_id}"
6465
end
6566

66-
def footer_buttons(footer_collection)
67-
# noop
67+
def edit_component
68+
OpenProject::Common::InplaceEditFieldComponent.new(
69+
model: @model,
70+
attribute: @attribute,
71+
enforce_edit_mode: true,
72+
**@system_arguments.merge(
73+
wrapper_id:,
74+
form_id:,
75+
show_action_buttons: false
76+
)
77+
)
6878
end
6979
end
7080
end

0 commit comments

Comments
 (0)