Skip to content

Commit fccba85

Browse files
restore nested inputs support (#6)
* fix actions definition * finalize nested fields support * don't show associations only if we are in a frame
1 parent 4419a82 commit fccba85

File tree

12 files changed

+355
-43
lines changed

12 files changed

+355
-43
lines changed

lib/generators/pu/gem/annotate/templates/lib/tasks/auto_annotate_models.rake

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ task :set_annotation_options do
4444
"format_bare" => "true",
4545
"format_rdoc" => "false",
4646
"format_yard" => "false",
47-
"format_markdown" => "true",
47+
"format_markdown" => "false",
4848
"sort" => "false",
4949
"force" => "false",
5050
"frozen" => "false",

lib/plutonium/definition/actions.rb

+6-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@ def self.action(name, interaction: nil, **)
1414
end
1515
end
1616

17-
def action(name, **)
18-
instance_defined_actions[name] = Plutonium::Action::Simple.new(name, **)
17+
def action(name, interaction: nil, **)
18+
instance_defined_actions[name] = if interaction
19+
Plutonium::Action::Interactive::Factory.create(name, interaction:, **)
20+
else
21+
Plutonium::Action::Simple.new(name, **)
22+
end
1923
end
2024

2125
def defined_actions

lib/plutonium/definition/base.rb

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class Base
2929
include Actions
3030
include Sorting
3131
include Search
32+
include NestedInputs
3233

3334
class IndexPage < Plutonium::UI::Page::Index; end
3435

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module Plutonium
2+
module Definition
3+
module NestedInputs
4+
extend ActiveSupport::Concern
5+
6+
included do
7+
defineable_prop :nested_input
8+
9+
# def self.nested_input(name, with: nil, **)
10+
# defined_nested_inputs[name] = {}
11+
# end
12+
13+
# def nested_input(name, with: nil, **)
14+
# instance_defined_nested_inputs[name] = {}
15+
# end
16+
end
17+
end
18+
end
19+
end

lib/plutonium/resource/controller.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def resource_record
5959
# Returns the submitted resource parameters
6060
# @return [Hash] The submitted resource parameters
6161
def submitted_resource_params
62-
@submitted_resource_params ||= build_form(resource_class.new).extract_input(params)[resource_param_key.to_sym]
62+
@submitted_resource_params ||= build_form(resource_class.new).extract_input(params, view_context:)[resource_param_key.to_sym]
6363
end
6464

6565
# Returns the resource parameters, including scoped and parent parameters

lib/plutonium/resource/controllers/interactive_actions.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ def submitted_interaction_params
232232
@submitted_interaction_params ||= current_interactive_action
233233
.interaction
234234
.build_form(nil)
235-
.extract_input(params)[:interaction]
235+
.extract_input(params, view_context:)[:interaction]
236236
end
237237

238238
def redirect_url_after_action_on(resource_record_or_resource_class)

lib/plutonium/resource/controllers/presentable.rb

+1-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ module Presentable
55
extend ActiveSupport::Concern
66

77
included do
8-
helper_method :presentable_attributes, :present_associations?
8+
helper_method :presentable_attributes
99
helper_method :build_form, :build_detail, :build_collection
1010
end
1111

@@ -31,10 +31,6 @@ def build_detail
3131
def build_form(record = resource_record)
3232
current_definition.form_class.new(record, resource_fields: presentable_attributes, resource_definition: current_definition)
3333
end
34-
35-
def present_associations?
36-
current_parent.nil?
37-
end
3834
end
3935
end
4036
end

lib/plutonium/ui/display/resource.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def when_permitted(name, &)
9191
end
9292

9393
def present_associations?
94-
current_parent.nil?
94+
current_turbo_frame.nil?
9595
end
9696
end
9797
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# frozen_string_literal: true
2+
3+
module Plutonium
4+
module UI
5+
module Form
6+
module Concerns
7+
# Handles rendering of nested resource fields in forms
8+
# @api private
9+
module RendersNestedResourceFields
10+
extend ActiveSupport::Concern
11+
12+
DEFAULT_NESTED_LIMIT = 10
13+
NESTED_OPTION_KEYS = [:allow_destroy, :update_only, :macro, :class].freeze
14+
SINGULAR_MACROS = %i[belongs_to has_one].freeze
15+
16+
class NestedInputsDefinition
17+
include Plutonium::Definition::DefineableProps
18+
19+
defineable_props :field, :input
20+
end
21+
22+
# Template object for new nested records
23+
class NotPersisted
24+
def persisted?
25+
false
26+
end
27+
end
28+
29+
private
30+
31+
# Renders a nested resource field with associated inputs
32+
# @param [Symbol] name The name of the nested resource field
33+
# @raise [ArgumentError] if the nested input definition is missing required configuration
34+
def render_nested_resource_field(name)
35+
context = NestedFieldContext.new(
36+
name: name,
37+
definition: build_nested_definition(name),
38+
resource_class: resource_class,
39+
resource_definition: resource_definition
40+
)
41+
42+
render_nested_field_container(context) do
43+
render_nested_field_header(context)
44+
render_nested_field_content(context)
45+
render_nested_add_button(context)
46+
end
47+
end
48+
49+
private
50+
51+
class NestedFieldContext
52+
attr_reader :name, :definition, :options, :permitted_fields
53+
54+
def initialize(name:, definition:, resource_class:, resource_definition:)
55+
@name = name
56+
@definition = definition
57+
@resource_definition = resource_definition
58+
@resource_class = resource_class
59+
@options = build_options
60+
@permitted_fields = build_permitted_fields
61+
end
62+
63+
def nested_attribute_options
64+
@nested_attribute_options ||= @resource_class.all_nested_attributes_options[@name] || {}
65+
end
66+
67+
def nested_input_param
68+
@options[:as] || :"#{@name}_attributes"
69+
end
70+
71+
def multiple?
72+
@options[:multiple]
73+
end
74+
75+
private
76+
77+
def build_options
78+
options = @resource_definition.defined_nested_inputs[@name][:options].dup || {}
79+
merge_nested_options(options)
80+
set_nested_limits(options)
81+
options
82+
end
83+
84+
def merge_nested_options(options)
85+
NESTED_OPTION_KEYS.each do |key|
86+
options.fetch(key) { options[key] = nested_attribute_options[key] }
87+
end
88+
end
89+
90+
def set_nested_limits(options)
91+
options.fetch(:limit) do
92+
options[:limit] = if SINGULAR_MACROS.include?(nested_attribute_options[:macro])
93+
1
94+
else
95+
nested_attribute_options[:limit] || DEFAULT_NESTED_LIMIT
96+
end
97+
end
98+
99+
options.fetch(:multiple) do
100+
options[:multiple] = !SINGULAR_MACROS.include?(nested_attribute_options[:macro])
101+
end
102+
end
103+
104+
def build_permitted_fields
105+
@options[:fields] || @definition.defined_inputs.keys
106+
end
107+
end
108+
109+
def build_nested_definition(name)
110+
nested_input_definition = resource_definition.defined_nested_inputs[name]
111+
112+
if nested_input_definition[:options]&.fetch(:using, nil)
113+
nested_input_definition[:options][:using]
114+
elsif nested_input_definition[:block]
115+
build_definition_from_block(nested_input_definition[:block])
116+
else
117+
raise_missing_nested_definition_error(name)
118+
end
119+
end
120+
121+
def build_definition_from_block(block)
122+
definition = NestedInputsDefinition.new
123+
block.call(definition)
124+
definition
125+
end
126+
127+
def render_nested_field_container(context, &)
128+
div(
129+
class: "col-span-full space-y-2 my-4",
130+
data: {
131+
controller: "nested-resource-form-fields",
132+
nested_resource_form_fields_limit_value: context.options[:limit]
133+
},
134+
&
135+
)
136+
end
137+
138+
def render_nested_field_header(context)
139+
div do
140+
h2(class: "text-lg font-semibold text-gray-900 dark:text-white") { context.name.to_s.humanize }
141+
render_description(context.options[:description]) if context.options[:description]
142+
end
143+
end
144+
145+
def render_description(description)
146+
p(class: "text-md font-normal text-gray-500 dark:text-gray-400") { description }
147+
end
148+
149+
def render_nested_field_content(context)
150+
if context.multiple?
151+
render_multiple_nested_fields(context)
152+
else
153+
render_single_nested_field(context)
154+
end
155+
156+
div(data_nested_resource_form_fields_target: :target, hidden: true)
157+
end
158+
159+
def render_multiple_nested_fields(context)
160+
render_template_for_nested_fields(context, collection: {NEW_RECORD: NotPersisted.new})
161+
render_existing_nested_fields(context)
162+
end
163+
164+
def render_single_nested_field(context)
165+
render_template_for_nested_fields(context, object: NotPersisted.new)
166+
render_existing_nested_fields(context, single: true)
167+
end
168+
169+
def render_template_for_nested_fields(context, field_options)
170+
template_tag data_nested_resource_form_fields_target: "template" do
171+
nesting_method = field_options[:collection] ? :nest_many : :nest_one
172+
send(nesting_method, context.name, as: context.nested_input_param, template: true, **field_options) do |nested|
173+
render_fieldset(nested, context)
174+
end
175+
end
176+
end
177+
178+
def render_existing_nested_fields(context, single: false)
179+
nesting_method = single ? :nest_one : :nest_many
180+
send(nesting_method, context.name, as: context.nested_input_param) do |nested|
181+
render_fieldset(nested, context)
182+
end
183+
end
184+
185+
def render_fieldset(nested, context)
186+
fieldset(
187+
data_new_record: !nested.object&.persisted?,
188+
class: "nested-resource-form-fields border border-gray-200 dark:border-gray-700 rounded-lg p-4 space-y-4 relative"
189+
) do
190+
render_fieldset_content(nested, context)
191+
render_delete_button(nested, context.options)
192+
end
193+
end
194+
195+
def render_fieldset_content(nested, context)
196+
div(class: "grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-4 gap-4 grid-flow-row-dense") do
197+
render_hidden_fields(nested, context)
198+
render_input_fields(nested, context)
199+
end
200+
end
201+
202+
def render_hidden_fields(nested, context)
203+
if !context.options[:update_only] && context.options[:class]&.respond_to?(:primary_key)
204+
render nested.field(context.options[:class].primary_key).hidden_tag
205+
end
206+
render nested.field(:_destroy).hidden_tag if context.options[:allow_destroy]
207+
end
208+
209+
def render_input_fields(nested, context)
210+
context.permitted_fields.each do |input|
211+
render_simple_resource_field(input, context.definition, nested)
212+
end
213+
end
214+
215+
def render_delete_button(nested, options)
216+
return unless !nested.object&.persisted? || options[:allow_destroy]
217+
218+
render_delete_button_content
219+
end
220+
221+
def render_delete_button_content
222+
div(class: "flex items-center justify-end") do
223+
label(class: "inline-flex items-center text-md font-medium text-red-900 cursor-pointer") do
224+
plain "Delete"
225+
render_delete_checkbox
226+
end
227+
end
228+
end
229+
230+
def render_delete_checkbox
231+
input(
232+
type: :checkbox,
233+
class: "w-4 h-4 ms-2 text-red-600 bg-red-100 border-red-300 rounded focus:ring-red-500 dark:focus:ring-red-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 cursor-pointer",
234+
data_action: "nested-resource-form-fields#remove"
235+
)
236+
end
237+
238+
def render_nested_add_button(context)
239+
div do
240+
button(
241+
type: :button,
242+
class: "inline-block",
243+
data: {
244+
action: "nested-resource-form-fields#add",
245+
nested_resource_form_fields_target: "addButton"
246+
}
247+
) do
248+
render_add_button_content(context.name)
249+
end
250+
end
251+
end
252+
253+
def render_add_button_content(name)
254+
span(class: "bg-secondary-700 text-white hover:bg-secondary-800 focus:ring-secondary-300 dark:bg-secondary-600 dark:hover:bg-secondary-700 dark:focus:ring-secondary-800 flex items-center justify-center px-4 py-1.5 text-sm font-medium rounded-lg focus:outline-none focus:ring-4") do
255+
render Phlex::TablerIcons::Plus.new(class: "w-4 h-4 mr-1")
256+
span { "Add #{name.to_s.singularize.humanize}" }
257+
end
258+
end
259+
260+
def raise_missing_nested_definition_error(name)
261+
raise ArgumentError, %(
262+
`nested_input :#{name}` is missing a definition
263+
264+
you can either pass in a block:
265+
```ruby
266+
nested_input :#{name} do |definition|
267+
input :city
268+
input :country
269+
end
270+
```
271+
272+
or pass in options:
273+
```ruby
274+
nested_input :#{name}, using: #{name.to_s.classify}Definition, fields: %i[city country]
275+
```
276+
)
277+
end
278+
end
279+
end
280+
end
281+
end
282+
end

0 commit comments

Comments
 (0)