Skip to content

Commit 8d3406f

Browse files
authored
feature: nested (#3738)
* wip * wip * wip * nested controller * translate avo.nested.add * lint * rubocop:disable Style/ArgumentsForwarding * rename * i18n * keep nested fields position * . * lint / comment * wip * confurm * nested_on * fix bug * refactor option as hash * lint
1 parent 4c47f65 commit 8d3406f

32 files changed

+226
-29
lines changed

Diff for: app/components/avo/field_wrapper_component.html.erb

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838
</div>
3939
<% elsif on_edit? %>
4040
<%= content %>
41-
<% if record.present? and record.errors.include? @field.id %>
42-
<div class="text-red-600 mt-2 text-sm"><%= record.errors.full_messages_for(@field.id).to_sentence %></div>
41+
<% if @field.record_errors.include?(@field.id) %>
42+
<div class="text-red-600 mt-2 text-sm"><%= @field.record_errors.full_messages_for(@field.id).to_sentence %></div>
4343
<% end %>
4444
<% if help.present? %>
4545
<div class="text-gray-600 mt-2 text-sm"><%= sanitize help %></div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<%= render Avo::Advanced::Nested::FieldComponent.new(
2+
field: @field,
3+
parent_resource: @field.resource,
4+
view: @view,
5+
form: @form
6+
) %>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
class Avo::Fields::Common::NestedFieldComponent < Avo::BaseComponent
4+
prop :field
5+
prop :view
6+
prop :form
7+
prop :kwargs, kind: :**
8+
9+
def render?
10+
Avo.plugin_manager.installed?("avo-advanced")
11+
end
12+
end

Diff for: app/components/avo/items/switcher_component.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def tab_group_component
6565

6666
def field_component
6767
final_item = item.dup.hydrate(resource: @resource, record: @resource.record, user: resource.user, view: view)
68-
final_item.component_for_view(@view).new(field: final_item, resource: @resource, index: index, form: form, turbo_frame_loading: :lazy, **@field_component_extra_args)
68+
final_item.component_for_view(@view).new(field: final_item, resource: @resource, index: index, form: form, view: @view, turbo_frame_loading: :lazy, **@field_component_extra_args)
6969
end
7070

7171
def panel_component

Diff for: app/controllers/avo/base_controller.rb

+7-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
module Avo
44
class BaseController < ApplicationController
55
include Avo::Concerns::FiltersSessionHandler
6+
include Avo::Concerns::SafeCall
67

78
before_action :set_resource_name
89
before_action :set_resource
@@ -285,7 +286,12 @@ def model_params
285286
end
286287

287288
def permitted_params
288-
@resource.get_field_definitions.select(&:updatable).map(&:to_permitted_param).concat(extra_params).uniq
289+
@resource.get_field_definitions
290+
.select(&:updatable)
291+
.map(&:to_permitted_param)
292+
.concat(extra_params)
293+
.push(@resource.safe_call(:nested_params))
294+
.uniq
289295
end
290296

291297
def extra_params
@@ -577,10 +583,6 @@ def set_pagy_locale
577583
@pagy_locale = locale.to_s || Avo.configuration.default_locale || "en"
578584
end
579585

580-
def safe_call(method)
581-
send(method) if respond_to?(method, true)
582-
end
583-
584586
def pagy_query
585587
@query
586588
end

Diff for: app/javascript/js/controllers.js

+7-5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import MediaLibraryController from './controllers/media_library_controller'
2727
import MenuController from './controllers/menu_controller'
2828
import ModalController from './controllers/modal_controller'
2929
import MultipleSelectFilterController from './controllers/multiple_select_filter_controller'
30+
import NestedFormController from './controllers/nested_form_controller'
3031
import PanelRefreshController from './controllers/fields/panel_refresh_controller'
3132
import PerPageController from './controllers/per_page_controller'
3233
import PreviewController from './controllers/preview_controller'
@@ -64,31 +65,32 @@ application.register('dashboard-card', DashboardCardController)
6465
application.register('date-time-filter', DateTimeFilterController)
6566
application.register('filter', FilterController)
6667
application.register('form', FormController)
67-
application.register('panel-refresh', PanelRefreshController)
6868
application.register('hidden-input', HiddenInputController)
6969
application.register('input-autofocus', InputAutofocusController)
7070
application.register('item-select-all', ItemSelectAllController)
7171
application.register('item-selector', ItemSelectorController)
7272
application.register('loading-button', LoadingButtonController)
73-
application.register('media-library', MediaLibraryController)
7473
application.register('media-library-attach', MediaLibraryAttachController)
74+
application.register('media-library', MediaLibraryController)
7575
application.register('menu', MenuController)
7676
application.register('modal', ModalController)
7777
application.register('multiple-select-filter', MultipleSelectFilterController)
78+
application.register('nested-form', NestedFormController)
79+
application.register('panel-refresh', PanelRefreshController)
7880
application.register('per-page', PerPageController)
7981
application.register('preview', PreviewController)
8082
application.register('record-selector', RecordSelectorController)
8183
application.register('resource-edit', ResourceEditController)
8284
application.register('resource-index', ResourceIndexController)
8385
application.register('resource-show', ResourceShowController)
8486
application.register('search', SearchController)
85-
application.register('select', SelectController)
8687
application.register('select-filter', SelectFilterController)
88+
application.register('select', SelectController)
8789
application.register('self-destroy', SelfDestroyController)
8890
application.register('sidebar', SidebarController)
8991
application.register('sign-out', SignOutController)
90-
application.register('tabs', TabsController)
9192
application.register('table-row', TableRowController)
93+
application.register('tabs', TabsController)
9294
application.register('text-filter', TextFilterController)
9395
application.register('tippy', TippyController)
9496
application.register('toggle', ToggleController)
@@ -103,7 +105,7 @@ application.register('key-value', KeyValueController)
103105
application.register('progress-bar-field', ProgressBarFieldController)
104106
application.register('reload-belongs-to-field', ReloadBelongsToFieldController)
105107
application.register('tags-field', TagsFieldController)
106-
application.register('trix-field', TrixFieldController)
107108
application.register('tiptap-field', TiptapFieldController)
109+
application.register('trix-field', TrixFieldController)
108110

109111
// Custom controllers
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import NestedForm from 'stimulus-rails-nested-form'
2+
3+
export default class extends NestedForm {
4+
static targets = [
5+
'addButton',
6+
'removeButton',
7+
'nestedRecord',
8+
]
9+
10+
static values = {
11+
// `limit` restricts `has_one` to a single nested record or allows unlimited nesting.
12+
// This could easily be extended into a configurable field option, allowing developers
13+
// to specify a custom limit for nested records during creation.
14+
// Default is 0 which is considered unlimited
15+
limit: Number,
16+
confirmMessage: String,
17+
}
18+
19+
add(event) {
20+
super.add(event)
21+
this.toggleAddButton()
22+
}
23+
24+
remove(event) {
25+
if (confirm(this.confirmMessageValue)) {
26+
super.remove(event)
27+
this.toggleAddButton()
28+
}
29+
}
30+
31+
toggleAddButton() {
32+
if (this.limitValue > 0) {
33+
this.addButtonTarget.classList.toggle('hidden', this.nestedRecordTargets.length >= this.limitValue)
34+
}
35+
}
36+
}

Diff for: lib/avo/concerns/has_items.rb

+26-8
Original file line numberDiff line numberDiff line change
@@ -116,16 +116,25 @@ def only_fields(only_root: false)
116116
end
117117

118118
if item.is_row?
119-
fields << extract_fields(tab)
119+
fields << extract_fields(item)
120120
end
121121
end
122122

123123
fields.flatten
124124
end
125125

126126
def get_field_definitions(only_root: false)
127-
only_fields(only_root: only_root).map do |field|
128-
field.hydrate(resource: self, user: user, view: view)
127+
only_fields(only_root:).map do |field|
128+
# When nested field hydrate the field with the nested resource
129+
resource = if field.try(:nested_on?, view)
130+
Avo.resource_manager.get_resource_by_model_class(model_class.reflections[field.id.to_s].klass)
131+
.new(view:, params:)
132+
.detect_fields
133+
else
134+
self
135+
end
136+
137+
field.hydrate(resource:, user:, view:)
129138
end
130139
end
131140

@@ -273,6 +282,9 @@ def visible_items
273282
item.is_heading? ||
274283
item.is_a?(Avo::Fields::LocationField)
275284

285+
# Skip nested fields
286+
next true if item.try(:nested_on?, view)
287+
276288
item.resource.record.respond_to?(:"#{item.try(:for_attribute) || item.id}=")
277289
end
278290
.select do |item|
@@ -327,11 +339,17 @@ def is_standalone?(item)
327339
def hydrate_item(item)
328340
return unless item.respond_to? :hydrate
329341

330-
item.hydrate(
331-
view: view,
332-
# Use self when this is executed from a resource context, call resource otherwise.
333-
resource: self.class.ancestors.include?(Avo::Resources::Base) ? self : resource
334-
)
342+
# Use self when this is executed from a resource context, call resource otherwise.
343+
the_resource = self.class.ancestors.include?(Avo::Resources::Base) ? self : resource
344+
345+
if view.form? && item.try(:nested_on?, view)
346+
nested_resource = Avo.resource_manager
347+
.get_resource_by_model_class(the_resource.model_class.reflections[item.id.to_s].klass)
348+
.new(view:)
349+
.detect_fields
350+
end
351+
352+
item.hydrate(view:, resource: nested_resource || the_resource)
335353
end
336354
end
337355
end

Diff for: lib/avo/concerns/safe_call.rb

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module Avo
2+
module Concerns
3+
module SafeCall
4+
# rubocop:disable Style/ArgumentsForwarding
5+
def safe_call(method, **kwargs)
6+
send(method, **kwargs) if respond_to?(method, true)
7+
end
8+
# rubocop:enable Style/ArgumentsForwarding
9+
end
10+
end
11+
end

Diff for: lib/avo/fields/concerns/nested.rb

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Adds the ability to set the visibility of an item in the execution context.
2+
module Avo
3+
module Fields
4+
module Concerns
5+
module Nested
6+
extend ActiveSupport::Concern
7+
8+
attr_reader :nested
9+
10+
unless defined?(NESTED_DEFAULT)
11+
NESTED_DEFAULT = {
12+
on: [:new, :edit]
13+
}
14+
end
15+
16+
def initialize_nested(**args)
17+
if args[:nested].nil?
18+
@nested = {}
19+
return
20+
end
21+
22+
@nested = (args[:nested] == true) ? NESTED_DEFAULT : args[:nested]
23+
24+
@nested[:on] = [:new, :edit] if @nested[:on] == :forms
25+
end
26+
27+
def component_for_view(view = Avo::ViewInquirer.new("index"))
28+
view = Avo::ViewInquirer.new(view)
29+
30+
nested_on?(view) ? Avo::Fields::Common::NestedFieldComponent : super(view)
31+
end
32+
33+
def nested_on?(view)
34+
return false if view.display? || @nested[:on].nil?
35+
36+
view = if view.create?
37+
"new"
38+
elsif view.update?
39+
"edit"
40+
else
41+
view
42+
end
43+
44+
Array.wrap(@nested[:on]).map(&:to_s).include?(view)
45+
end
46+
end
47+
end
48+
end
49+
end

Diff for: lib/avo/fields/has_one_field.rb

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
module Avo
22
module Fields
33
class HasOneField < FrameBaseField
4+
include Avo::Fields::Concerns::Nested
5+
46
attr_reader :attach_fields,
57
:attach_scope
68

79
def initialize(id, **args, &block)
8-
hide_on :forms
10+
initialize_nested(**args)
11+
12+
if @nested[:on]
13+
nested_on = Array.wrap(@nested[:on])
14+
15+
if !nested_on.include?(:new)
16+
hide_on :new
17+
elsif !nested_on.include?(:edit)
18+
hide_on :edit
19+
end
20+
else
21+
hide_on :forms
22+
end
923

1024
super(id, **args, &block)
1125

Diff for: lib/avo/fields/many_frame_base_field.rb

+12-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module Avo
22
module Fields
33
class ManyFrameBaseField < FrameBaseField
44
include Avo::Fields::Concerns::IsSearchable
5+
include Avo::Fields::Concerns::Nested
56

67
attr_reader :scope,
78
:hide_search_input,
@@ -10,7 +11,17 @@ class ManyFrameBaseField < FrameBaseField
1011
def initialize(id, **args, &block)
1112
args[:updatable] = false
1213

13-
only_on Avo.configuration.resource_default_view
14+
initialize_nested(**args)
15+
16+
if @nested[:on]
17+
if Avo.configuration.resource_default_view.edit?
18+
only_on Array.wrap(@nested[:on]) + [:edit]
19+
else
20+
only_on Array.wrap(@nested[:on]) + [:show]
21+
end
22+
else
23+
only_on Avo.configuration.resource_default_view
24+
end
1425

1526
super(id, **args, &block)
1627

Diff for: lib/avo/resources/base.rb

+6-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class Base
1818
include Avo::Concerns::Pagination
1919
include Avo::Concerns::HasDiscreetInformation
2020
include Avo::Concerns::RowControlsConfiguration
21+
include Avo::Concerns::SafeCall
2122

2223
# Avo::Current methods
2324
delegate :context, to: Avo::Current
@@ -451,8 +452,9 @@ def attachment_fields
451452
end
452453

453454
# Map the received params to their actual fields
454-
def fields_by_database_id
455-
get_field_definitions
455+
# 'resource' argument is used on avo-advanced, don't remove
456+
def fields_by_database_id(resource: self)
457+
resource.get_field_definitions
456458
.reject do |field|
457459
field.computed
458460
end
@@ -479,14 +481,14 @@ def fill_record(record, permitted_params, extra_params: [], fields: nil)
479481
# Write the user configured extra params to the record
480482
if extra_params.present?
481483
# Pick only the extra params
482-
# params at this point are already permited, only need the keys to access them
484+
# params at this point are already permitted, only need the keys to access them
483485
extra_attributes = permitted_params.slice(*flatten_keys(extra_params))
484486

485487
# Let Rails fill in the rest of the params
486488
record.assign_attributes extra_attributes
487489
end
488490

489-
record
491+
safe_call(:fill_nested_records, record:, permitted_params:) || record
490492
end
491493

492494
def authorization(user: nil)

Diff for: lib/generators/avo/templates/locales/avo.ar.yml

+2
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ ar:
7878
more: المزيد
7979
more_content: المزيد من المحتوى
8080
more_records_available: هناك المزيد من السجلات المتاحة.
81+
nested:
82+
add_new_item: إضافة %{item} جديدة
8183
new: جديد
8284
next_page: الصفحة التالية
8385
no_cancel: لا، إلغاء

Diff for: lib/generators/avo/templates/locales/avo.de.yml

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ de:
6868
more: Mehr
6969
more_content: Mehr Inhalt
7070
more_records_available: Es sind weitere Datensätze verfügbar.
71+
nested:
72+
add_new_item: Neues %{item} hinzufügen
7173
new: neu
7274
next_page: Nächste Seite
7375
no_cancel: Nein, abbrechen

0 commit comments

Comments
 (0)