avo 2.42.2 → 2.44.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of avo might be problematic. Click here for more details.

Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +5 -1
  4. data/app/components/avo/fields/belongs_to_field/edit_component.html.erb +118 -96
  5. data/app/components/avo/fields/belongs_to_field/edit_component.rb +11 -0
  6. data/app/components/avo/fields/tags_field/edit_component.html.erb +1 -0
  7. data/app/components/avo/modal_component.html.erb +17 -10
  8. data/app/components/avo/modal_component.rb +21 -0
  9. data/app/components/avo/panel_component.html.erb +2 -2
  10. data/app/components/avo/referrer_params_component.html.erb +1 -0
  11. data/app/components/avo/views/resource_edit_component.html.erb +11 -6
  12. data/app/components/avo/views/resource_edit_component.rb +11 -1
  13. data/app/controllers/avo/actions_controller.rb +9 -11
  14. data/app/controllers/avo/base_controller.rb +11 -2
  15. data/app/helpers/avo/application_helper.rb +4 -0
  16. data/app/javascript/js/application.js +3 -0
  17. data/app/javascript/js/controllers/fields/reload_belongs_to_field_controller.js +51 -0
  18. data/app/javascript/js/controllers/fields/tags_field_controller.js +2 -1
  19. data/app/javascript/js/controllers.js +2 -0
  20. data/app/views/avo/actions/show.html.erb +3 -2
  21. data/app/views/avo/base/_new_via_belongs_to.html.erb +12 -0
  22. data/app/views/avo/base/close_modal_and_reload_field.turbo_stream.erb +8 -0
  23. data/app/views/avo/base/create_fail_action.turbo_stream.erb +13 -0
  24. data/app/views/avo/partials/_flash_alerts.turbo_stream.erb +3 -0
  25. data/avo.gemspec +1 -0
  26. data/lib/avo/base_action.rb +10 -26
  27. data/lib/avo/fields/base_field.rb +13 -7
  28. data/lib/avo/fields/tags_field.rb +2 -0
  29. data/lib/avo/licensing/h_q.rb +4 -2
  30. data/lib/avo/version.rb +1 -1
  31. data/public/avo-assets/avo.base.css +20 -8
  32. data/public/avo-assets/avo.base.js +176 -176
  33. data/public/avo-assets/avo.base.js.map +3 -3
  34. metadata +21 -5
  35. data/app/controllers/avo/team_users_controller.rb +0 -4
  36. data/app/views/avo/actions/keep_modal_open.turbo_stream.erb +0 -5
  37. data/lib/avo/action_model.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85975e5896ef4f49e8ca3b4874470092dfd64eafd7c0282a8715162c9dfc42bc
4
- data.tar.gz: c90db50a988c7cc7bf66cbc5671844390f28455f4d09f421b86d49caf86f588a
3
+ metadata.gz: 6b2cdd36d7332c119809608975ea38e68d2bfe09987e9203b9f39149292885a7
4
+ data.tar.gz: 9e00cbe1f725979bf1c61164ae0912e20eff340ff727b4eb27c7212ed1500897
5
5
  SHA512:
6
- metadata.gz: 98b4bc5ccb0193c5ef46da23c582a3da7ee3bf8fc8ae5516232b4ea6f0b98df0fa3a798be6498019e76aa735fd37b6cbfff1d6514d3be3a6628b6b7af56e6431
7
- data.tar.gz: ef6fee4b84d1cbf589a0160785005e568c2c8f2a6dad77da7692170c8819e89ad7202ee4f26944180c34a4bd19e6953c50ad96534ce8ca33e2582745ec7bf3c6
6
+ metadata.gz: 6fd7ac16fac942d14da9c82317a0f8c322cdd0b1af785319c4cc273badd5e528a73c0f87e8b12bf8673fe35e8c6cfe61ac3f578b044797e64cb1c7c3158de677
7
+ data.tar.gz: 87a8e856ca13470d32119b71685300dc1759f6455fc86742284e1e41f573500bb016bf0105e6e3fce48f0362a50d06f0905660d681a7345987afd5827a4ed7ff
data/Gemfile CHANGED
@@ -167,3 +167,5 @@ gem "image_processing", "~> 1.12"
167
167
  gem "prefixed_ids"
168
168
 
169
169
  gem "mapkick-rb", "~> 0.1.4"
170
+
171
+ gem "turbo_power", "~> 0.5.0"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- avo (2.42.2)
4
+ avo (2.44.0)
5
5
  actionview (>= 6.0)
6
6
  active_link_to
7
7
  activerecord (>= 6.0)
@@ -13,6 +13,7 @@ PATH
13
13
  meta-tags
14
14
  pagy
15
15
  turbo-rails
16
+ turbo_power (~> 0.5.0)
16
17
  view_component (>= 2.54.0)
17
18
  zeitwerk (>= 2.6.2)
18
19
 
@@ -419,6 +420,8 @@ GEM
419
420
  actionpack (>= 6.0.0)
420
421
  activejob (>= 6.0.0)
421
422
  railties (>= 6.0.0)
423
+ turbo_power (0.5.0)
424
+ turbo-rails (~> 1.3)
422
425
  tzinfo (2.0.6)
423
426
  concurrent-ruby (~> 1.0)
424
427
  unicode-display_width (2.4.0)
@@ -518,6 +521,7 @@ DEPENDENCIES
518
521
  sprockets-rails
519
522
  standard
520
523
  test-prof
524
+ turbo_power (~> 0.5.0)
521
525
  tzinfo-data
522
526
  web-console (>= 3.3.0)
523
527
  webdrivers (>= 5.3.0)
@@ -1,115 +1,137 @@
1
- <% if is_polymorphic? %>
2
- <%
3
- # Set the model keys so we can pass them over
4
- model_keys = @field.types.map do |type|
5
- resource = Avo::App.get_resource_by_model_name(type.to_s)
6
- [type.to_s, resource.model_key]
7
- end.to_h
8
- %>
9
- <div class="divide-y"
10
- data-controller="belongs-to-field"
11
- data-searchable="<%= @field.searchable %>"
12
- data-association="<%= @field.id %>"
13
- data-association-class="<%= @field&.target_resource&.model_class || nil %>"
14
- >
15
- <%= field_wrapper **field_wrapper_args, help: @field.polymorphic_help || '' do %>
16
- <%= @form.select @field.type_input_foreign_key, @field.types.map { |type| [::Avo::App.get_resource_by_model_name(type.to_s).name, type.to_s] },
17
- {
18
- value: @field.value,
19
- include_blank: @field.placeholder,
20
- },
21
- {
22
- class: classes("w-full"),
23
- data: {
24
- **@field.get_html(:data, view: view, element: :input),
25
- action: "change->belongs-to-field#changeType #{field_html_action}",
26
- 'belongs-to-field-target': "select",
27
- },
28
- disabled: disabled
29
- }
1
+ <div data-controller="reload-belongs-to-field"
2
+ data-action="turbo:before-stream-render@document->reload-belongs-to-field#beforeStreamRender"
3
+ data-reload-belongs-to-field-polymorphic-value="<%= is_polymorphic? %>"
4
+ data-reload-belongs-to-field-searchable-value="<%= @field.searchable %>"
5
+ data-reload-belongs-to-field-relation-name-value="<%= @field.id %>"
6
+ data-reload-belongs-to-field-target-name-value="<%= form.object_name %>[<%= @field.id_input_foreign_key %>]"
7
+ >
8
+ <% if is_polymorphic? %>
9
+ <%
10
+ # Set the model keys so we can pass them over
11
+ model_keys = @field.types.map do |type|
12
+ resource = Avo::App.get_resource_by_model_name(type.to_s)
13
+ [type.to_s, resource.model_key]
14
+ end.to_h
30
15
  %>
31
- <%
32
- # If the select field is disabled, no value will be sent. It's how HTML works.
33
- # Thus the extra hidden field to actually send the related id to the server.
34
- if disabled %>
35
- <%= @form.hidden_field @field.type_input_foreign_key %>
16
+ <div class="divide-y"
17
+ data-controller="belongs-to-field"
18
+ data-searchable="<%= @field.searchable %>"
19
+ data-association="<%= @field.id %>"
20
+ data-association-class="<%= @field&.target_resource&.model_class || nil %>"
21
+ >
22
+ <%= field_wrapper **field_wrapper_args, help: @field.polymorphic_help || '' do %>
23
+ <%= @form.select @field.type_input_foreign_key, @field.types.map { |type| [::Avo::App.get_resource_by_model_name(type.to_s).name, type.to_s] },
24
+ {
25
+ value: @field.value,
26
+ include_blank: @field.placeholder,
27
+ },
28
+ {
29
+ class: classes("w-full"),
30
+ data: {
31
+ **@field.get_html(:data, view: view, element: :input),
32
+ action: "change->belongs-to-field#changeType #{field_html_action}",
33
+ 'belongs-to-field-target': "select",
34
+ },
35
+ disabled: disabled
36
+ }
37
+ %>
38
+ <%
39
+ # If the select field is disabled, no value will be sent. It's how HTML works.
40
+ # Thus the extra hidden field to actually send the related id to the server.
41
+ if disabled %>
42
+ <%= @form.hidden_field @field.type_input_foreign_key %>
43
+ <% end %>
36
44
  <% end %>
37
- <% end %>
38
- <% @field.types.each do |type| %>
39
- <div class="hidden"
40
- data-belongs-to-field-target="type"
41
- data-type="<%= type %>"
42
- >
43
- <%= field_wrapper **field_wrapper_args, label: ::Avo::App.get_resource_by_model_name(type.to_s).name do %>
44
- <% if @field.searchable %>
45
- <%= render Avo::Fields::BelongsToField::AutocompleteComponent.new form: @form,
46
- disabled: disabled,
47
- field: @field,
48
- foreign_key: @field.id_input_foreign_key,
49
- model_key: model_keys[type.to_s],
50
- polymorphic_record: polymorphic_record,
51
- resource: @resource,
52
- style: @field.get_html(:style, view: view, element: :input),
53
- type: type,
54
- classes: classes("w-full"),
55
- view: view
45
+ <% @field.types.each do |type| %>
46
+ <div class="hidden"
47
+ data-belongs-to-field-target="type"
48
+ data-type="<%= type %>"
49
+ >
50
+ <%= field_wrapper **field_wrapper_args, label: ::Avo::App.get_resource_by_model_name(type.to_s).name do %>
51
+ <div class="flex flex-col gap-1">
52
+ <% if @field.searchable %>
53
+ <%= render Avo::Fields::BelongsToField::AutocompleteComponent.new form: @form,
54
+ disabled: disabled,
55
+ field: @field,
56
+ foreign_key: @field.id_input_foreign_key,
57
+ model_key: model_keys[type.to_s],
58
+ polymorphic_record: polymorphic_record,
59
+ resource: @resource,
60
+ style: @field.get_html(:style, view: view, element: :input),
61
+ type: type,
62
+ classes: classes("w-full"),
63
+ view: view
64
+ %>
65
+ <% else %>
66
+ <%= @form.select @field.id_input_foreign_key,
67
+ options_for_select(@field.values_for_type(type), @resource.present? && @resource.model.present? ? @resource.model[@field.id_input_foreign_key] : nil),
68
+ {
69
+ value: @resource.model[@field.id_input_foreign_key].to_s,
70
+ include_blank: @field.placeholder,
71
+ },
72
+ {
73
+ class: classes("w-full"),
74
+ data: @field.get_html(:data, view: view, element: :input),
75
+ disabled: disabled
76
+ }
77
+ %>
78
+ <%
79
+ # If the select field is disabled, no value will be sent. It's how HTML works.
80
+ # Thus the extra hidden field to actually send the related id to the server.
81
+ if disabled %>
82
+ <%= @form.hidden_field @field.id_input_foreign_key %>
83
+ <% end %>
84
+ <% end %>
85
+ <% create_href = create_path(::Avo::App.get_resource_by_model_name(type.to_s)) %>
86
+ <% if !disabled && create_href.present? %>
87
+ <%= link_to t("avo.create_new_item", item: type.to_s.downcase),
88
+ create_href,
89
+ class: "text-sm"
90
+ %>
91
+ <% end %>
92
+ </div>
93
+ <% end %>
94
+ </div>
95
+ <% end %>
96
+ </div>
97
+ <% else %>
98
+ <%= field_wrapper **field_wrapper_args do %>
99
+ <div class="flex flex-col gap-1">
100
+ <% if @field.searchable %>
101
+ <%= render Avo::Fields::BelongsToField::AutocompleteComponent.new form: @form,
102
+ field: @field,
103
+ model_key: @field.target_resource&.model_key,
104
+ foreign_key: @field.id_input_foreign_key,
105
+ resource: @resource,
106
+ disabled: disabled,
107
+ classes: classes("w-full"),
108
+ view: view,
109
+ style: @field.get_html(:style, view: view, element: :input)
56
110
  %>
57
- <% else %>
58
- <%= @form.select @field.id_input_foreign_key,
59
- options_for_select(@field.values_for_type(type), @resource.present? && @resource.model.present? ? @resource.model[@field.id_input_foreign_key] : nil),
111
+ <% else %>
112
+ <%= @form.select @field.id_input_foreign_key, @field.options,
60
113
  {
61
- value: @resource.model[@field.id_input_foreign_key].to_s,
62
114
  include_blank: @field.placeholder,
115
+ value: @field.value
63
116
  },
64
117
  {
65
118
  class: classes("w-full"),
66
119
  data: @field.get_html(:data, view: view, element: :input),
67
- disabled: disabled
120
+ disabled: disabled,
121
+ style: @field.get_html(:style, view: view, element: :input)
68
122
  }
69
123
  %>
70
- <%
124
+ <%
71
125
  # If the select field is disabled, no value will be sent. It's how HTML works.
72
126
  # Thus the extra hidden field to actually send the related id to the server.
73
127
  if disabled %>
74
- <%= @form.hidden_field @field.id_input_foreign_key %>
75
- <% end %>
128
+ <%= @form.hidden_field @field.id_input_foreign_key %>
76
129
  <% end %>
77
130
  <% end %>
131
+ <% if !disabled && create_path.present? %>
132
+ <%= link_to t("avo.create_new_item", item: @field.name.downcase), create_path, class: "text-sm" %>
133
+ <% end %>
78
134
  </div>
79
135
  <% end %>
80
- </div>
81
- <% else %>
82
- <%= field_wrapper **field_wrapper_args do %>
83
- <% if @field.searchable %>
84
- <%= render Avo::Fields::BelongsToField::AutocompleteComponent.new form: @form,
85
- field: @field,
86
- model_key: @field.target_resource&.model_key,
87
- foreign_key: @field.id_input_foreign_key,
88
- resource: @resource,
89
- disabled: disabled,
90
- classes: classes("w-full"),
91
- view: view,
92
- style: @field.get_html(:style, view: view, element: :input)
93
- %>
94
- <% else %>
95
- <%= @form.select @field.id_input_foreign_key, @field.options,
96
- {
97
- include_blank: @field.placeholder,
98
- value: @field.value
99
- },
100
- {
101
- class: classes("w-full"),
102
- data: @field.get_html(:data, view: view, element: :input),
103
- disabled: disabled,
104
- style: @field.get_html(:style, view: view, element: :input)
105
- }
106
- %>
107
- <%
108
- # If the select field is disabled, no value will be sent. It's how HTML works.
109
- # Thus the extra hidden field to actually send the related id to the server.
110
- if disabled %>
111
- <%= @form.hidden_field @field.id_input_foreign_key %>
112
- <% end %>
113
- <% end %>
114
136
  <% end %>
115
- <% end %>
137
+ </div>
@@ -57,6 +57,17 @@ class Avo::Fields::BelongsToField::EditComponent < Avo::Fields::EditComponent
57
57
  @field.get_html(:data, view: view, element: :input).fetch(:action, nil)
58
58
  end
59
59
 
60
+ def create_path(target_resource = nil)
61
+ return nil if @resource.blank?
62
+
63
+ helpers.new_resource_path(**{
64
+ via_relation: @field.id.to_s,
65
+ resource: target_resource || @field.target_resource,
66
+ via_resource_id: resource.model.to_param,
67
+ via_belongs_to_resource_class: resource.class.name
68
+ }.compact)
69
+ end
70
+
60
71
  private
61
72
 
62
73
  def visit_through_association?
@@ -4,6 +4,7 @@
4
4
  tags_field_whitelist_items_value: @field.suggestions.to_json,
5
5
  tags_field_disallowed_items_value: @field.disallowed.to_json,
6
6
  tags_field_enforce_suggestions_value: @field.enforce_suggestions,
7
+ tags_field_suggestions_max_items_value: @field.suggestions_max_items,
7
8
  tags_field_close_on_select_value: @field.close_on_select,
8
9
  tags_field_delimiters_value: @field.delimiters,
9
10
  tags_field_fetch_values_from_value: @field.fetch_values_from,
@@ -3,19 +3,26 @@
3
3
  data-modal-target="modal"
4
4
  >
5
5
  <div aria-expanded="true" class="modal-overlay absolute w-full h-full bg-opacity-25 bg-gray-800 flex justify-center items-center" data-action="click->modal#close"></div>
6
- <div aria-expanded="true" role="dialog" aria-modal="true" class="modal-body rounded-lg inset-auto w-11/12 lg:w-1/2 sm:max-w-168 min-h-1/4 bg-white flex z-50 relative shadow-modal overflow-auto max-h-full">
6
+ <div aria-expanded="true" role="dialog" aria-modal="true" class="modal-body rounded-lg inset-auto bg-white flex z-50 relative shadow-modal overflow-auto <%= width_classes %> <%= height_classes %>">
7
7
  <div class="flex-1 flex flex-col justify-between">
8
8
  <div>
9
- <div class="p-6 text-2xl tracking-normal font-semibold text-black">
10
- <%= heading %>
11
- </div>
12
- <div class="px-6 text-base text-gray-500">
13
- <%= content %>
14
- </div>
15
- </div>
16
- <div class="flex justify-end items-baseline space-x-4 p-4 bg-gray-100">
17
- <%= controls %>
9
+ <% if heading? %>
10
+ <div class="p-6 text-2xl tracking-normal font-semibold text-black">
11
+ <%= heading %>
12
+ </div>
13
+ <% end %>
14
+
15
+ <% if content? %>
16
+ <div class="px-6 text-base text-gray-500 <%= body_class %>">
17
+ <%= content %>
18
+ </div>
19
+ <% end %>
18
20
  </div>
21
+ <% if controls? %>
22
+ <div class="flex justify-end items-baseline space-x-4 p-4 bg-gray-100">
23
+ <%= controls %>
24
+ </div>
25
+ <% end %>
19
26
  </div>
20
27
  </div>
21
28
  </div>
@@ -3,4 +3,25 @@
3
3
  class Avo::ModalComponent < ViewComponent::Base
4
4
  renders_one :heading
5
5
  renders_one :controls
6
+
7
+ attr_reader :width
8
+ attr_reader :body_class
9
+
10
+ def initialize(width: :md, body_class: nil)
11
+ @width = width
12
+ @body_class = body_class
13
+ end
14
+
15
+ def width_classes
16
+ case width.to_sym
17
+ when :md
18
+ "w-11/12 lg:w-1/2 sm:max-w-168"
19
+ when :xl
20
+ "w-11/12 lg:w-3/4"
21
+ end
22
+ end
23
+
24
+ def height_classes
25
+ "max-h-full min-h-1/4 max-h-11/12"
26
+ end
6
27
  end
@@ -32,12 +32,12 @@
32
32
  </div>
33
33
  </div>
34
34
  <% if sidebar? %>
35
- <div class="w-full sm:w-1/3 flex-shrink-0 h-full <%= white_panel_classes %>">
35
+ <div class="max-w-full sm:w-1/3 flex-shrink-0 h-full <%= white_panel_classes %>">
36
36
  <%= sidebar %>
37
37
  </div>
38
38
  <% end %>
39
39
  <% if bare_sidebar? %>
40
- <div class="w-full sm:w-1/3 flex-shrink-0 h-full">
40
+ <div class="max-w-full sm:w-1/3 flex-shrink-0 h-full">
41
41
  <%= bare_sidebar %>
42
42
  </div>
43
43
  <% end %>
@@ -3,4 +3,5 @@
3
3
  <%= hidden_field_tag :via_resource_class, params[:via_resource_class] if params[:via_resource_class] %>
4
4
  <%= hidden_field_tag :via_resource_id, params[:via_resource_id] if params[:via_resource_id] %>
5
5
  <%= hidden_field_tag :via_relation, params[:via_relation] if params[:via_relation] %>
6
+ <%= hidden_field_tag :via_belongs_to_resource_class, params[:via_belongs_to_resource_class] if params[:via_belongs_to_resource_class] %>
6
7
  <%= hidden_field_tag :referrer, back_path if params[:via_resource_class] %>
@@ -1,4 +1,5 @@
1
1
  <%= content_tag :div,
2
+ id: helpers.frame_id(@resource),
2
3
  data: {
3
4
  model_name: @resource.model_name.to_s,
4
5
  resource_name: @resource.class.to_s,
@@ -18,12 +19,16 @@
18
19
  multipart: true do |form| %>
19
20
  <%= render Avo::ReferrerParamsComponent.new back_path: back_path %>
20
21
  <%= content_tag :div, class: 'space-y-12' do %>
21
- <%= render Avo::PanelComponent.new(name: title, description: @resource.resource_description, display_breadcrumbs: @reflection.blank?, index: 0, data: { panel_id: "main" }) do |c| %>
22
+ <%= render Avo::PanelComponent.new(name: title, description: @resource.resource_description,
23
+ display_breadcrumbs: display_breadcrumbs?,
24
+ index: 0, data: { panel_id: "main" }) do |c| %>
22
25
  <% c.with_tools do %>
23
- <%= a_link back_path,
24
- style: :text,
25
- icon: 'arrow-left' do %>
26
- <%= t('avo.cancel').capitalize %>
26
+ <% if back_path.present? %>
27
+ <%= a_link back_path,
28
+ style: :text,
29
+ icon: 'arrow-left' do %>
30
+ <%= t('avo.cancel').capitalize %>
31
+ <% end %>
27
32
  <% end %>
28
33
  <% if can_see_the_destroy_button? %>
29
34
  <%= a_link destroy_path,
@@ -68,7 +73,7 @@
68
73
  </div>
69
74
  <% end %>
70
75
  <% end %>
71
- <% if sidebar.present? %>
76
+ <% if sidebar.present? && sidebar.visible_items.present? %>
72
77
  <% c.with_sidebar do %>
73
78
  <%= render sidebar_component form: form %>
74
79
  <% end %>
@@ -4,11 +4,12 @@ class Avo::Views::ResourceEditComponent < Avo::ResourceComponent
4
4
  include Avo::ResourcesHelper
5
5
  include Avo::ApplicationHelper
6
6
 
7
- def initialize(resource: nil, model: nil, actions: [], view: :edit)
7
+ def initialize(resource: nil, model: nil, actions: [], view: :edit, display_breadcrumbs: true)
8
8
  @resource = resource
9
9
  @model = model
10
10
  @actions = actions
11
11
  @view = view
12
+ @display_breadcrumbs = display_breadcrumbs
12
13
  end
13
14
 
14
15
  def title
@@ -16,6 +17,7 @@ class Avo::Views::ResourceEditComponent < Avo::ResourceComponent
16
17
  end
17
18
 
18
19
  def back_path
20
+ return if via_belongs_to?
19
21
  return resource_view_path if via_resource?
20
22
  return resources_path if via_index?
21
23
 
@@ -46,12 +48,20 @@ class Avo::Views::ResourceEditComponent < Avo::ResourceComponent
46
48
  @resource.authorization.authorize_action @view, raise_exception: false
47
49
  end
48
50
 
51
+ def display_breadcrumbs?
52
+ @reflection.blank? && @display_breadcrumbs
53
+ end
54
+
49
55
  private
50
56
 
51
57
  def via_index?
52
58
  params[:via_view] == "index"
53
59
  end
54
60
 
61
+ def via_belongs_to?
62
+ params[:via_belongs_to_resource_class].present?
63
+ end
64
+
55
65
  def is_edit?
56
66
  view.in?([:edit, :update])
57
67
  end
@@ -16,7 +16,6 @@ module Avo
16
16
  @view = :new
17
17
 
18
18
  @resource.hydrate(model: @model, view: @view, user: _current_user, params: params)
19
- @model = ActionModel.new @action.get_attributes_for_action
20
19
  end
21
20
 
22
21
  def handle
@@ -79,19 +78,18 @@ module Avo
79
78
  end
80
79
 
81
80
  respond_to do |format|
82
- format.html do
81
+ format.turbo_stream do
83
82
  # Flash the messages collected from the action
84
83
  flash_messages messages
85
84
 
86
85
  if response[:type] == :redirect
87
- path = response[:path]
88
-
89
- if path.respond_to? :call
90
- path = instance_eval(&path)
91
- end
92
-
93
- redirect_to path, **{allow_other_host: response[:allow_other_host], status: response[:status]}.compact
94
- elsif response[:type] == :reload
86
+ render turbo_stream: turbo_stream.redirect_to(
87
+ Avo::ExecutionContext.new(target: response[:path]).handle,
88
+ nil,
89
+ response[:redirect_args][:turbo_frame],
90
+ **response[:redirect_args].except(:turbo_frame)
91
+ )
92
+ else
95
93
  redirect_back fallback_location: resources_path(resource: @resource)
96
94
  end
97
95
  end
@@ -132,7 +130,7 @@ module Avo
132
130
 
133
131
  respond_to do |format|
134
132
  format.turbo_stream do
135
- render "keep_modal_open"
133
+ render partial: "avo/partials/flash_alerts"
136
134
  end
137
135
  end
138
136
  end
@@ -107,6 +107,11 @@ module Avo
107
107
  @model = @resource.model_class.new
108
108
  @resource = @resource.hydrate(model: @model, view: :new, user: _current_user)
109
109
 
110
+ # Handle special cases when creating a new record via a belongs_to relationship
111
+ if params[:via_belongs_to_resource_class].present?
112
+ return render turbo_stream: turbo_stream.append('attach_modal', partial: 'avo/base/new_via_belongs_to')
113
+ end
114
+
110
115
  set_actions
111
116
 
112
117
  @page_title = @resource.default_panel_name.to_s
@@ -130,7 +135,7 @@ module Avo
130
135
  @resource.hydrate(model: @model, view: :new, user: _current_user)
131
136
 
132
137
  # This means that the record has been created through another parent record and we need to attach it somehow.
133
- if params[:via_resource_id].present?
138
+ if params[:via_resource_id].present? && params[:via_belongs_to_resource_class].nil?
134
139
  @reflection = @model._reflections[params[:via_relation]]
135
140
  # Figure out what kind of association does the record have with the parent record
136
141
 
@@ -420,15 +425,19 @@ module Avo
420
425
  end
421
426
 
422
427
  def create_success_action
428
+ return render "close_modal_and_reload_field" if params[:via_belongs_to_resource_class].present?
429
+
423
430
  respond_to do |format|
424
431
  format.html { redirect_to after_create_path, notice: create_success_message}
425
432
  end
426
433
  end
427
434
 
428
435
  def create_fail_action
436
+ flash.now[:error] = create_fail_message
437
+
429
438
  respond_to do |format|
430
- flash.now[:error] = create_fail_message
431
439
  format.html { render :new, status: :unprocessable_entity }
440
+ format.turbo_stream { render "create_fail_action" }
432
441
  end
433
442
  end
434
443
 
@@ -119,6 +119,10 @@ module Avo
119
119
  Avo::Filters::BaseFilter.encode_filters(filter_params)
120
120
  end
121
121
 
122
+ def frame_id(resource)
123
+ ["frame", resource.model_name.singular, resource.model.id].compact.join("-")
124
+ end
125
+
122
126
  private
123
127
 
124
128
  # Taken from the original library
@@ -2,6 +2,9 @@ import 'mapkick/bundle'
2
2
 
3
3
  import { Alert, Popover } from 'tailwindcss-stimulus-components'
4
4
  import { Application } from '@hotwired/stimulus'
5
+ import TurboPower from 'turbo_power'
6
+
7
+ TurboPower.initialize(Turbo.StreamActions)
5
8
 
6
9
  const application = Application.start()
7
10
 
@@ -0,0 +1,51 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ // Used as a custom Stream Action <turbo-stream action="update-belongs-to" />
4
+ export default class extends Controller {
5
+ static values = {
6
+ polymorphic: Boolean,
7
+ searchable: Boolean,
8
+ targetName: String,
9
+ relationName: String
10
+ }
11
+
12
+ beforeStreamRender(event) {
13
+ const { relationName } = event.target.dataset
14
+ if (event.target.action !== "update-belongs-to" || this.relationNameValue !== relationName) {
15
+ return false;
16
+ }
17
+
18
+ event.detail.render = (stream) => {
19
+ if (this.searchableValue) {
20
+ this.updateSearchable(stream)
21
+ } else{
22
+ this.updateNonSearchable(stream)
23
+ }
24
+ };
25
+ }
26
+
27
+ updateSearchable(stream) {
28
+ // Update the id component
29
+ document.querySelector(`input[name="${this.targetNameValue}"][type="hidden"]`).value = stream.dataset.targetResourceId
30
+ // Update the label
31
+ document.querySelector(`input[name="${this.targetNameValue}"][type="text"]`).value = stream.dataset.targetResourceLabel
32
+ }
33
+
34
+ updateNonSearchable(stream) {
35
+ const select = this.selectorContext(stream).querySelector(`select[name="${this.targetNameValue}"]`)
36
+ const option = document.createElement('option')
37
+ option.value = stream.dataset.targetResourceId
38
+ option.text = stream.dataset.targetResourceLabel
39
+ option.selected = true
40
+ select.appendChild(option)
41
+ }
42
+
43
+ selectorContext(stream) {
44
+ // if polymorphic, search for the select in the correct sub-container
45
+ if (this.polymorphicValue) {
46
+ return document.querySelector(`[data-type="${stream.dataset.targetResourceClass}"]`)
47
+ }
48
+
49
+ return document
50
+ }
51
+ }
@@ -15,6 +15,7 @@ export default class extends BaseController {
15
15
  enforceSuggestions: { type: Boolean, default: false },
16
16
  closeOnSelect: { type: Boolean, default: false },
17
17
  delimiters: { type: Array, default: [] },
18
+ suggestionsMaxItems: { type: Number, default: 20 },
18
19
  mode: String,
19
20
  fetchValuesFrom: String,
20
21
  }
@@ -36,7 +37,7 @@ export default class extends BaseController {
36
37
  enforceWhitelist: this.enforceSuggestionsValue || this.fetchValuesFromValue,
37
38
  delimiters: this.delimitersValue.join('|'),
38
39
  dropdown: {
39
- maxItems: 20,
40
+ maxItems: this.suggestionsMaxItemsValue,
40
41
  enabled: 0,
41
42
  searchKeys: [this.labelAttributeValue],
42
43
  closeOnSelect: this.closeOnSelectValue,