avo 1.19.1.pre.10 → 1.20.2.pre.2

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -1
  3. data/Gemfile.lock +5 -3
  4. data/app/assets/builds/avo.css +16 -0
  5. data/app/assets/builds/avo.js +317 -234
  6. data/app/assets/builds/avo.js.map +2 -2
  7. data/app/assets/svgs/x.svg +3 -0
  8. data/app/components/avo/fields/belongs_to_field/autocomplete_component.html.erb +22 -0
  9. data/app/components/avo/fields/belongs_to_field/autocomplete_component.rb +33 -0
  10. data/app/components/avo/fields/belongs_to_field/edit_component.html.erb +39 -33
  11. data/app/components/avo/fields/common/files_list_viewer_component.html.erb +1 -1
  12. data/app/components/avo/fields/common/multiple_file_viewer_component.html.erb +2 -0
  13. data/app/components/avo/fields/common/multiple_file_viewer_component.rb +2 -1
  14. data/app/components/avo/fields/common/single_file_viewer_component.html.erb +2 -0
  15. data/app/components/avo/fields/common/single_file_viewer_component.rb +2 -1
  16. data/app/components/avo/fields/file_field/edit_component.html.erb +1 -1
  17. data/app/components/avo/fields/file_field/index_component.html.erb +3 -1
  18. data/app/components/avo/fields/file_field/show_component.html.erb +1 -1
  19. data/app/components/avo/resource_component.rb +1 -0
  20. data/app/components/avo/views/resource_edit_component.html.erb +5 -2
  21. data/app/components/avo/views/resource_index_component.html.erb +3 -3
  22. data/app/components/avo/views/resource_index_component.rb +8 -0
  23. data/app/components/avo/views/resource_new_component.html.erb +8 -1
  24. data/app/controllers/avo/application_controller.rb +11 -14
  25. data/app/controllers/avo/base_controller.rb +1 -5
  26. data/app/controllers/avo/relations_controller.rb +4 -4
  27. data/app/controllers/avo/search_controller.rb +0 -1
  28. data/app/javascript/js/controllers/fields/belongs_to_field_controller.js +96 -33
  29. data/app/javascript/js/controllers/search_controller.js +83 -17
  30. data/app/views/avo/partials/_global_search.html.erb +0 -1
  31. data/app/views/avo/partials/_javascript.html.erb +1 -0
  32. data/app/views/avo/partials/_resource_search.html.erb +0 -1
  33. data/db/factories.rb +12 -0
  34. data/lib/avo/fields/base_field.rb +1 -1
  35. data/lib/avo/fields/belongs_to_field.rb +19 -2
  36. data/lib/avo/fields/file_field.rb +2 -0
  37. data/lib/avo/fields/files_field.rb +2 -0
  38. data/lib/avo/licensing/pro_license.rb +2 -1
  39. data/lib/avo/services/authorization_service.rb +3 -0
  40. data/lib/avo/version.rb +1 -1
  41. data/lib/generators/avo/templates/locales/avo.en.yml +1 -0
  42. data/public/avo-assets/avo.css +16 -0
  43. data/public/avo-assets/avo.js +317 -234
  44. data/public/avo-assets/avo.js.map +2 -2
  45. metadata +5 -2
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
2
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
3
+ </svg>
@@ -0,0 +1,22 @@
1
+ <div data-controller="search" class="resource-search flex items-center h-full w-full" data-turbo-remove-before-cache>
2
+ <div class="w-full hidden"
3
+ data-search-target="autocomplete"
4
+ data-search-resource="<%= @model_key %>"
5
+ data-translation-keys='{"no_item_found": "<%= I18n.translate 'avo.no_item_found' %>"}'
6
+ data-via-association="belongs_to"
7
+ ></div>
8
+ <div class="relative w-full" autocomplete="off">
9
+ <%= @form.text_field @field.foreign_key,
10
+ value: field_label,
11
+ class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)), 'data-search-target': 'button clearValue',
12
+ placeholder: @field.placeholder,
13
+ disabled: true %>
14
+ <div class="absolute top-1/2 left-auto right-3 mr-px -mt-2 cursor-pointer hidden text-gray-500"
15
+ data-tippy="tooltip"
16
+ data-search-target="clearButton"
17
+ title="<%= I18n.translate 'avo.clear_value' %>"
18
+ data-action="click->search#clearValue"
19
+ ><%= helpers.svg 'x', class: 'h-4' %></div>
20
+ </div>
21
+ <%= @form.hidden_field @foreign_key, value: field_value, 'data-search-target': 'hiddenId clearValue' %>
22
+ </div>
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Avo::Fields::BelongsToField::AutocompleteComponent < ViewComponent::Base
4
+ def initialize(form:, field:, type: nil, model_key:, foreign_key:)
5
+ @form = form
6
+ @field = field
7
+ @type = type
8
+ @model_key = model_key
9
+ @foreign_key = foreign_key
10
+ end
11
+
12
+ def field_label
13
+ if searchable?
14
+ @field.value&.class == @type ? @field.field_label : nil
15
+ else
16
+ @field.field_label
17
+ end
18
+ end
19
+
20
+ def field_value
21
+ if searchable?
22
+ @field.value&.class == @type ? @field.field_value : nil
23
+ else
24
+ @field.field_value
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def searchable?
31
+ @type.present?
32
+ end
33
+ end
@@ -1,22 +1,34 @@
1
- <% if @field.types.present? %>
2
- <div data-controller="belongs-to-field">
1
+ <%
2
+ if @field.types.present? # It's a polymorphic association
3
+
4
+ # Set the model keys so we can pass them over
5
+ model_keys = @field.types.map do |type|
6
+ resource = Avo::App.get_resource_by_model_name(type.to_s)
7
+ [type.to_s, resource.model_key]
8
+ end.to_h
9
+ %>
10
+ <div 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
+ >
3
15
  <%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
4
16
  <%= @form.select "#{@field.foreign_key}_type", @field.types.map { |type| [type.to_s.underscore.humanize, type.to_s] },
5
17
  {
18
+ value: @field.value,
6
19
  include_blank: @field.placeholder,
7
20
  },
8
21
  {
9
22
  class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
10
23
  disabled: disabled,
11
24
  'data-belongs-to-field-target': "select",
12
- 'data-action': 'change->belongs-to-field#changedType'
25
+ 'data-action': 'change->belongs-to-field#changeType'
13
26
  }
14
27
  %>
15
28
  <%
16
29
  # If the select field is disabled, no value will be sent. It's how HTML works.
17
30
  # Thus the extra hidden field to actually send the related id to the server.
18
- if disabled
19
- %>
31
+ if disabled %>
20
32
  <%= @form.hidden_field "#{@field.foreign_key}_type" %>
21
33
  <% end %>
22
34
  <% end %>
@@ -26,21 +38,18 @@
26
38
  data-type="<%= type %>"
27
39
  >
28
40
  <%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal, label: type.to_s.underscore.humanize do %>
29
- <%= @form.select "#{@field.foreign_key}_id", @field.values_for_type(type),
30
- {
31
- include_blank: @field.placeholder,
32
- },
33
- {
34
- class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
35
- disabled: disabled
36
- }
41
+ <% if @field.searchable %>
42
+ <%= render Avo::Fields::BelongsToField::AutocompleteComponent.new form: @form, field: @field, type: type, model_key: model_keys[type.to_s], foreign_key: "#{@field.foreign_key}_id" %>
43
+ <% else %>
44
+ <%= @form.select "#{@field.foreign_key}_id", options_for_select(@field.values_for_type(type), @field.value&.class == type ? @field.field_value : nil),
45
+ {
46
+ include_blank: @field.placeholder,
47
+ },
48
+ {
49
+ class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
50
+ disabled: disabled
51
+ }
37
52
  %>
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
- %>
43
- <%= @form.hidden_field "#{@field.foreign_key}_id" %>
44
53
  <% end %>
45
54
  <% end %>
46
55
  </div>
@@ -48,21 +57,18 @@
48
57
  </div>
49
58
  <% else %>
50
59
  <%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
51
- <%= @form.select @field.foreign_key, @field.options.map { |o| [o[:label], o[:value]] },
52
- {
53
- include_blank: @field.placeholder,
54
- },
55
- {
56
- class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
57
- disabled: disabled
58
- }
60
+ <% if @field.searchable %>
61
+ <%= render Avo::Fields::BelongsToField::AutocompleteComponent.new form: @form, field: @field, model_key: @field.target_resource&.model_key, foreign_key: @field.foreign_key %>
62
+ <% else %>
63
+ <%= @form.select @field.foreign_key, @field.options.map { |o| [o[:label], o[:value]] },
64
+ {
65
+ include_blank: @field.placeholder,
66
+ },
67
+ {
68
+ class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
69
+ disabled: disabled
70
+ }
59
71
  %>
60
- <%
61
- # If the select field is disabled, no value will be sent. It's how HTML works.
62
- # Thus the extra hidden field to actually send the related id to the server.
63
- if disabled
64
- %>
65
- <%= @form.hidden_field @field.foreign_key %>
66
72
  <% end %>
67
73
  <% end %>
68
74
  <% end %>
@@ -1,5 +1,5 @@
1
1
  <div class="relative p-3 bg-slate-200 grid grid-cols-3 xl:grid-cols-4 gap-3 rounded-xl">
2
2
  <% @field.value.attachments.each do |file| %>
3
- <%= render Avo::Fields::Common::MultipleFileViewerComponent.new id: @field.id, file: file, is_image: @field.is_image, button_size: :xs, resource: @resource %>
3
+ <%= render Avo::Fields::Common::MultipleFileViewerComponent.new id: @field.id, file: file, is_image: @field.is_image, is_audio: @field.is_audio, button_size: :xs, resource: @resource %>
4
4
  <% end %>
5
5
  </div>
@@ -2,6 +2,8 @@
2
2
  <% if @file.present? %>
3
3
  <% if @file.representable? && @is_image %>
4
4
  <%= image_tag helpers.main_app.url_for(@file), class: 'rounded-lg max-h-168 max-w-full' %>
5
+ <% elsif @is_audio %>
6
+ <%= audio_tag(helpers.main_app.url_for(@file), controls: true, preload: false, class: 'w-full')%>
5
7
  <% else %>
6
8
  <div class="relative flex flex-col justify-evenly items-center px-2 rounded-lg border bg-white border-gray-500 py-6 flex-1">
7
9
  <div class="flex flex-col justify-center items-center w-full">
@@ -3,10 +3,11 @@
3
3
  class Avo::Fields::Common::MultipleFileViewerComponent < ViewComponent::Base
4
4
  include Avo::ApplicationHelper
5
5
 
6
- def initialize(id:, file:, is_image:, direct_upload: false, resource:, button_size: :md)
6
+ def initialize(id:, file:, is_image:, is_audio:, direct_upload: false, resource:, button_size: :md)
7
7
  @id = id
8
8
  @file = file
9
9
  @is_image = is_image
10
+ @is_audio = is_audio
10
11
  @direct_upload = direct_upload
11
12
  @button_size = button_size
12
13
  @resource = resource
@@ -2,6 +2,8 @@
2
2
  <% if @file.present? %>
3
3
  <% if @file.representable? && @is_image %>
4
4
  <%= image_tag helpers.main_app.url_for(@file), class: 'rounded-lg max-h-168 max-w-full' %>
5
+ <% elsif @is_audio %>
6
+ <%= audio_tag(helpers.main_app.url_for(@file), controls: true, preload: false, class: 'w-full')%>
5
7
  <% else %>
6
8
  <div class="relative flex flex-col justify-evenly items-center px-2 rounded-lg border bg-white border-gray-500 min-h-48">
7
9
  <div class="flex flex-col justify-center items-center w-full">
@@ -3,10 +3,11 @@
3
3
  class Avo::Fields::Common::SingleFileViewerComponent < ViewComponent::Base
4
4
  include Avo::ApplicationHelper
5
5
 
6
- def initialize(id:, file:, is_image:, direct_upload: false, resource:, button_size: :md)
6
+ def initialize(id:, file:, is_image:, is_audio:, direct_upload: false, resource:, button_size: :md)
7
7
  @id = id
8
8
  @file = file
9
9
  @is_image = is_image
10
+ @is_audio = is_audio
10
11
  @direct_upload = direct_upload
11
12
  @button_size = button_size
12
13
  @resource = resource
@@ -1,7 +1,7 @@
1
1
  <%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
2
2
  <% if @field.value.present? %>
3
3
  <div class="mb-2">
4
- <%= render Avo::Fields::Common::SingleFileViewerComponent.new resource: @resource, id: @field.id, file: @field.value, is_image: @field.is_image, button_size: :md %>
4
+ <%= render Avo::Fields::Common::SingleFileViewerComponent.new resource: @resource, id: @field.id, file: @field.value, is_image: @field.is_image, is_audio: @field.is_audio, button_size: :md %>
5
5
  </div>
6
6
  <% end %>
7
7
 
@@ -1,7 +1,9 @@
1
1
  <%= index_field_wrapper field: @field do %>
2
2
  <% if @field.value.present? %>
3
3
  <% if @field.value.attached? && @field.value.representable? && @field.is_image %>
4
- <%= link_to_if @field.link_to_resource, image_tag(helpers.main_app.url_for(@field.value), class: 'max-h-full'), resource_path, class: 'block h-8' %>
4
+ <%= link_to_if @field.link_to_resource, image_tag(helpers.main_app.url_for(@field.value), class: 'max-h-full'), resource_path, class: 'block' %>
5
+ <% elsif @field.value.attached? && @field.is_audio %>
6
+ <%= link_to_if @field.link_to_resource, audio_tag(helpers.main_app.url_for(@field.value), controls: true, preload: false, class: 'max-h-full'), resource_path, class: 'block h-8' %>
5
7
  <% else %>
6
8
  <%= @field.value.filename %>
7
9
  <% end %>
@@ -1,3 +1,3 @@
1
1
  <%= show_field_wrapper field: @field do %>
2
- <%= render Avo::Fields::Common::SingleFileViewerComponent.new resource: @resource, id: @field.id, file: @field.value.attachment, is_image: @field.is_image, button_size: :md %>
2
+ <%= render Avo::Fields::Common::SingleFileViewerComponent.new resource: @resource, id: @field.id, file: @field.value.attachment, is_image: @field.is_image, is_audio: @field.is_audio, button_size: :md %>
3
3
  <% end %>
@@ -16,6 +16,7 @@ class Avo::ResourceComponent < ViewComponent::Base
16
16
 
17
17
  if @reflection.present?
18
18
  reflection_resource = ::Avo::App.get_resource_by_model_name(@reflection.active_record.name)
19
+ reflection_resource.hydrate(model: @parent_model) if @parent_model.present?
19
20
  association_name = params['related_name']
20
21
 
21
22
  if association_name.present?
@@ -1,7 +1,10 @@
1
1
  <div data-model-id="<%= @resource.model.id %>">
2
- <%= @resource.form_scope %>
3
2
  <% @resource.panels.each do |resource_panel| %>
4
- <%= form_with model: @resource.model, scope: @resource.form_scope, url: helpers.resource_path(model: @resource.model, resource: @resource), method: :put, multipart: true do |form| %>
3
+ <%= form_with model: @resource.model,
4
+ scope: @resource.form_scope,
5
+ url: helpers.resource_path(model: @resource.model, resource: @resource),
6
+ method: :put,
7
+ multipart: true do |form| %>
5
8
  <%= hidden_field_tag :referrer, back_path if params[:via_resource_class] %>
6
9
 
7
10
  <%= render Avo::PanelComponent.new(title: resource_panel[:name], description: @resource.resource_description, display_breadcrumbs: true) do |c| %>
@@ -1,8 +1,8 @@
1
1
  <div>
2
- <%= render Avo::PanelComponent.new(title: title, description: @resource.resource_description, body_classes: 'py-4', data: { component: 'resources-index' }, display_breadcrumbs: @reflection.blank?) do |c| %>
2
+ <%= render Avo::PanelComponent.new(title: title, description: description, body_classes: 'py-4', data: { component: 'resources-index' }, display_breadcrumbs: @reflection.blank?) do |c| %>
3
3
  <% c.tools do %>
4
4
  <% if can_see_the_actions_button? %>
5
- <%= render 'actions' if @actions.present? %>
5
+ <%= render 'actions' %>
6
6
  <% end %>
7
7
 
8
8
  <% if can_see_the_create_button? %>
@@ -24,7 +24,7 @@
24
24
  data-selected-resources="[]"
25
25
  >
26
26
  <div class="flex items-center px-6 w-64">
27
- <%= render partial: 'avo/partials/resource_search', locals: {resource: @resource.model_class.model_name.plural} if @resource.search_query.present? %>
27
+ <%= render partial: 'avo/partials/resource_search', locals: {resource: @resource.model_key} if @resource.search_query.present? %>
28
28
  </div>
29
29
  <div class="flex justify-end items-center px-6 space-x-3">
30
30
  <%= render partial: 'avo/partials/view_toggle_button', locals: { available_view_types: available_view_types, view_type: view_type, turbo_frame: @turbo_frame } if @models.present? %>
@@ -55,6 +55,8 @@ class Avo::Views::ResourceIndexComponent < Avo::ResourceComponent
55
55
  end
56
56
 
57
57
  def can_see_the_actions_button?
58
+ return false if @actions.blank?
59
+
58
60
  return authorize_association_for("act_on") if @reflection.present?
59
61
 
60
62
  @resource.authorization.authorize_action(:act_on, raise_exception: false) && !has_reflection_and_is_read_only
@@ -123,6 +125,12 @@ class Avo::Views::ResourceIndexComponent < Avo::ResourceComponent
123
125
  end
124
126
  end
125
127
 
128
+ def description
129
+ return if @reflection.present?
130
+
131
+ @resource.resource_description
132
+ end
133
+
126
134
  private
127
135
 
128
136
  def reflection_model_class
@@ -1,6 +1,13 @@
1
1
  <div>
2
2
  <% @resource.panels.each do |resource_panel| %>
3
- <%= form_with model: @resource.model, scope: @resource.form_scope, url: helpers.resources_path(resource: @resource, via_relation_class: params[:via_relation_class], via_relation: params[:via_relation], via_resource_id: params[:via_resource_id]), local: true, multipart: true do |form| %>
3
+ <%= form_with model: @resource.model,
4
+ scope: @resource.form_scope,
5
+ url: helpers.resources_path(resource: @resource,
6
+ via_relation_class: params[:via_relation_class],
7
+ via_relation: params[:via_relation],
8
+ via_resource_id: params[:via_resource_id]),
9
+ local: true,
10
+ multipart: true do |form| %>
4
11
  <%= render Avo::PanelComponent.new(title: resource_panel[:name], description: @resource.resource_description, display_breadcrumbs: true) do |c| %>
5
12
  <% c.tools do %>
6
13
  <div class="flex justify-end space-x-2">
@@ -111,8 +111,13 @@ module Avo
111
111
  end
112
112
 
113
113
  def fill_model
114
- puts ['@model_to_fill->', @model_to_fill].inspect
115
- @model = @resource.fill_model(@model_to_fill, cast_nullable(model_params))
114
+ # We have to skip filling the the model if this is an attach action
115
+ is_attach_action = params[model_param_key].blank? && params[:related_name].present? && params[:fields].present?
116
+ # puts ['fill_model->', is_attach_action, model_param_key].inspect
117
+
118
+ unless is_attach_action
119
+ @model = @resource.fill_model(@model_to_fill, cast_nullable(model_params))
120
+ end
116
121
  end
117
122
 
118
123
  def hydrate_resource
@@ -188,18 +193,6 @@ module Avo
188
193
  query
189
194
  end
190
195
 
191
- # def authorize_user
192
- # return if params[:controller] == 'avo/search'
193
-
194
- # model = record = resource.model
195
-
196
- # if ['show', 'edit', 'update'].include?(params[:action]) && params[:controller] == 'avo/resources'
197
- # record = resource
198
- # end
199
-
200
- # # AuthorizationService::authorize_action _current_user, record, params[:action] return render_unauthorized unless
201
- # end
202
-
203
196
  def _authenticate!
204
197
  instance_eval(&Avo.configuration.authenticate)
205
198
  end
@@ -244,5 +237,9 @@ module Avo
244
237
  def on_api_path
245
238
  request.original_url.match?(/.*#{Avo::App.root_path}\/avo_api\/.*/)
246
239
  end
240
+
241
+ def model_param_key
242
+ @resource.form_scope
243
+ end
247
244
  end
248
245
  end
@@ -7,6 +7,7 @@ module Avo
7
7
  before_action :hydrate_resource
8
8
  before_action :set_model, only: [:show, :edit, :destroy, :update]
9
9
  before_action :set_model_to_fill
10
+ before_action :fill_model, only: [:create, :update]
10
11
  before_action :authorize_action
11
12
  before_action :reset_pagination_if_filters_changed, only: :index
12
13
  before_action :cache_applied_filters, only: :index
@@ -112,7 +113,6 @@ module Avo
112
113
 
113
114
  def create
114
115
  # model gets instantiated and filled in the fill_model method
115
- fill_model
116
116
  saved = @model.save
117
117
  @resource.hydrate(model: @model, view: :new, user: _current_user)
118
118
 
@@ -166,7 +166,6 @@ module Avo
166
166
 
167
167
  def update
168
168
  # model gets instantiated and filled in the fill_model method
169
- fill_model
170
169
  saved = @model.save
171
170
  @resource = @resource.hydrate(model: @model, view: :edit, user: _current_user)
172
171
 
@@ -194,11 +193,8 @@ module Avo
194
193
  private
195
194
 
196
195
  def model_params
197
- model_param_key = @resource.form_scope
198
-
199
196
  request_params = params.require(model_param_key).permit(permitted_params)
200
197
 
201
- puts ['model_param_key->', model_param_key, params, request_params].inspect
202
198
  if @resource.devise_password_optional && request_params[:password].blank? && request_params[:password_confirmation].blank?
203
199
  request_params.delete(:password_confirmation)
204
200
  request_params.delete(:password)
@@ -4,11 +4,11 @@ module Avo
4
4
  class RelationsController < BaseController
5
5
  before_action :set_model, only: [:show, :index, :new, :create, :destroy]
6
6
  before_action :set_related_resource_name
7
- before_action :set_related_resource
8
- before_action :hydrate_related_resource
7
+ before_action :set_related_resource, only: [:show, :index, :new, :create, :destroy]
8
+ before_action :hydrate_related_resource, only: [:show, :index, :new, :create, :destroy]
9
9
  before_action :set_related_model, only: [:show]
10
- before_action :set_attachment_class
11
- before_action :set_attachment_resource
10
+ before_action :set_attachment_class, only: [:show, :index, :new, :create, :destroy]
11
+ before_action :set_attachment_resource, only: [:show, :index, :new, :create, :destroy]
12
12
  before_action :set_attachment_model, only: [:create, :destroy]
13
13
  before_action :set_reflection, only: [:index, :show]
14
14
 
@@ -60,7 +60,6 @@ module Avo
60
60
  _id: model.id,
61
61
  _label: resource.label,
62
62
  _url: resource.record_path,
63
- model: model
64
63
  }
65
64
 
66
65
  if App.license.has_with_trial(:enhanced_search_results)
@@ -1,65 +1,128 @@
1
1
  import { Controller } from 'stimulus'
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ['select', 'type']
4
+ static targets = ['select', 'type', 'loadAssociationLink'];
5
+
6
+ defaults = {};
5
7
 
6
8
  get selectedType() {
7
9
  return this.selectTarget.value
8
10
  }
9
11
 
10
- connect() {
11
- this.setValidNames()
12
- this.changedType()
12
+ get isSearchable() {
13
+ return this.context.scope.element.dataset.searchable === 'true'
13
14
  }
14
15
 
15
- setValidNames() {
16
- this.typeTargets.forEach((target) => {
17
- const { type } = target.dataset
18
- const select = target.querySelector('select')
19
- const name = select.getAttribute('name')
16
+ get association() {
17
+ return this.context.scope.element.dataset.association
18
+ }
20
19
 
21
- select.setAttribute('valid-name', name)
22
- if (this.selectedType !== type) {
23
- select.selectedIndex = 0
24
- }
25
- })
20
+ get associationClass() {
21
+ return this.context.scope.element.dataset.associationClass
26
22
  }
27
23
 
28
- changedType() {
29
- this.hideAllTypeTargets()
30
- this.enableType(this.selectTarget.value)
24
+ connect() {
25
+ this.copyValidNames()
26
+ this.changeType() // Do the initial type change
31
27
  }
32
28
 
33
- hideAllTypeTargets() {
29
+ changeType() {
30
+ this.hideAllTypes()
31
+ this.showType(this.selectTarget.value)
32
+ }
33
+
34
+ // Private
35
+
36
+ hideAllTypes() {
34
37
  this.typeTargets.forEach((target) => {
35
- this.hideTarget(target)
38
+ target.classList.add('hidden')
39
+
36
40
  this.invalidateTarget(target)
37
41
  })
38
42
  }
39
43
 
40
- hideTarget(target) {
41
- target.classList.add('hidden')
42
- }
44
+ /**
45
+ * Used for invalidating select fields when switching between types so they don't automatically override the previous id.
46
+ * Ex: There are two types Article and Project and the Comment has commentable_id 3 and commentable_type: Article
47
+ * When you change the type from Project to Article the Project field will override the commentable_id value
48
+ * because it was rendered later (alphabetical sorting) and the browser will pick that one up.
49
+ * So we go and copy the name attribute to valid-name for all types and then copy it back to name when the user selects it.
50
+ */
43
51
 
44
- invalidateTarget(target) {
45
- const select = target.querySelector('select')
52
+ /**
53
+ * This method does the initial copying from name to valid-name.
54
+ */
55
+ copyValidNames() {
56
+ this.typeTargets.forEach((target) => {
57
+ const { type } = target.dataset
46
58
 
47
- select.setAttribute('name', '')
48
- }
59
+ if (this.isSearchable) {
60
+ const textInput = target.querySelector('input[type="text"]')
61
+ if (textInput) {
62
+ textInput.setAttribute('valid-name', textInput.getAttribute('name'))
63
+ }
49
64
 
50
- validateTarget(target) {
51
- const select = target.querySelector('select')
52
- const validName = select.getAttribute('valid-name')
65
+ const hiddenInput = target.querySelector('input[type="hidden"]')
66
+ if (hiddenInput) {
67
+ hiddenInput.setAttribute(
68
+ 'valid-name',
69
+ hiddenInput.getAttribute('name'),
70
+ )
71
+ }
72
+ } else {
73
+ const select = target.querySelector('select')
74
+ if (select) {
75
+ select.setAttribute('valid-name', select.getAttribute('name'))
76
+ }
53
77
 
54
- select.setAttribute('name', validName)
78
+ if (this.selectedType !== type) {
79
+ select.selectedIndex = 0
80
+ }
81
+ }
82
+ })
55
83
  }
56
84
 
57
- enableType(type) {
58
- const target = this.typeTargets.find((typeTarget) => typeTarget.dataset.type === type)
59
-
85
+ showType(type) {
86
+ const target = this.typeTargets.find(
87
+ (typeTarget) => typeTarget.dataset.type === type,
88
+ )
60
89
  if (target) {
61
90
  target.classList.remove('hidden')
91
+
62
92
  this.validateTarget(target)
63
93
  }
64
94
  }
95
+
96
+ /**
97
+ * Copy value from `valid-name` to `name`
98
+ */
99
+ validateTarget(target) {
100
+ if (this.isSearchable) {
101
+ const textInput = target.querySelector('input[type="text"]')
102
+ const hiddenInput = target.querySelector('input[type="hidden"]')
103
+
104
+ textInput.setAttribute('name', textInput.getAttribute('valid-name'))
105
+ hiddenInput.setAttribute('name', hiddenInput.getAttribute('valid-name'))
106
+ } else {
107
+ const select = target.querySelector('select')
108
+ select.setAttribute('name', select.getAttribute('valid-name'))
109
+ }
110
+ }
111
+
112
+ /**
113
+ * nullify the `name` attribute
114
+ */
115
+ invalidateTarget(target) {
116
+ if (this.isSearchable) {
117
+ // Wrapping it in a try/catch to counter turbo's cache system (going back to the edit page after initial save)
118
+ try {
119
+ target.querySelector('input[type="text"]').setAttribute('name', '')
120
+ target.querySelector('input[type="hidden"]').setAttribute('name', '')
121
+ } catch {}
122
+ } else if (target) {
123
+ try {
124
+ target.querySelector('select').setAttribute('name', '')
125
+ } catch (error) {}
126
+ }
127
+ }
65
128
  }