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.
- checksums.yaml +4 -4
- data/Gemfile +3 -1
- data/Gemfile.lock +5 -3
- data/app/assets/builds/avo.css +16 -0
- data/app/assets/builds/avo.js +317 -234
- data/app/assets/builds/avo.js.map +2 -2
- data/app/assets/svgs/x.svg +3 -0
- data/app/components/avo/fields/belongs_to_field/autocomplete_component.html.erb +22 -0
- data/app/components/avo/fields/belongs_to_field/autocomplete_component.rb +33 -0
- data/app/components/avo/fields/belongs_to_field/edit_component.html.erb +39 -33
- data/app/components/avo/fields/common/files_list_viewer_component.html.erb +1 -1
- data/app/components/avo/fields/common/multiple_file_viewer_component.html.erb +2 -0
- data/app/components/avo/fields/common/multiple_file_viewer_component.rb +2 -1
- data/app/components/avo/fields/common/single_file_viewer_component.html.erb +2 -0
- data/app/components/avo/fields/common/single_file_viewer_component.rb +2 -1
- data/app/components/avo/fields/file_field/edit_component.html.erb +1 -1
- data/app/components/avo/fields/file_field/index_component.html.erb +3 -1
- data/app/components/avo/fields/file_field/show_component.html.erb +1 -1
- data/app/components/avo/resource_component.rb +1 -0
- data/app/components/avo/views/resource_edit_component.html.erb +5 -2
- data/app/components/avo/views/resource_index_component.html.erb +3 -3
- data/app/components/avo/views/resource_index_component.rb +8 -0
- data/app/components/avo/views/resource_new_component.html.erb +8 -1
- data/app/controllers/avo/application_controller.rb +11 -14
- data/app/controllers/avo/base_controller.rb +1 -5
- data/app/controllers/avo/relations_controller.rb +4 -4
- data/app/controllers/avo/search_controller.rb +0 -1
- data/app/javascript/js/controllers/fields/belongs_to_field_controller.js +96 -33
- data/app/javascript/js/controllers/search_controller.js +83 -17
- data/app/views/avo/partials/_global_search.html.erb +0 -1
- data/app/views/avo/partials/_javascript.html.erb +1 -0
- data/app/views/avo/partials/_resource_search.html.erb +0 -1
- data/db/factories.rb +12 -0
- data/lib/avo/fields/base_field.rb +1 -1
- data/lib/avo/fields/belongs_to_field.rb +19 -2
- data/lib/avo/fields/file_field.rb +2 -0
- data/lib/avo/fields/files_field.rb +2 -0
- data/lib/avo/licensing/pro_license.rb +2 -1
- data/lib/avo/services/authorization_service.rb +3 -0
- data/lib/avo/version.rb +1 -1
- data/lib/generators/avo/templates/locales/avo.en.yml +1 -0
- data/public/avo-assets/avo.css +16 -0
- data/public/avo-assets/avo.js +317 -234
- data/public/avo-assets/avo.js.map +2 -2
- metadata +5 -2
@@ -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
|
-
<%
|
2
|
-
|
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#
|
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
|
-
|
30
|
-
{
|
31
|
-
|
32
|
-
},
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
},
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
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,
|
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:
|
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'
|
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.
|
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,
|
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
|
-
|
115
|
-
|
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
|
|
@@ -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
|
-
|
11
|
-
this.
|
12
|
-
this.changedType()
|
12
|
+
get isSearchable() {
|
13
|
+
return this.context.scope.element.dataset.searchable === 'true'
|
13
14
|
}
|
14
15
|
|
15
|
-
|
16
|
-
this.
|
17
|
-
|
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
|
-
|
22
|
-
|
23
|
-
select.selectedIndex = 0
|
24
|
-
}
|
25
|
-
})
|
20
|
+
get associationClass() {
|
21
|
+
return this.context.scope.element.dataset.associationClass
|
26
22
|
}
|
27
23
|
|
28
|
-
|
29
|
-
this.
|
30
|
-
this.
|
24
|
+
connect() {
|
25
|
+
this.copyValidNames()
|
26
|
+
this.changeType() // Do the initial type change
|
31
27
|
}
|
32
28
|
|
33
|
-
|
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
|
-
|
38
|
+
target.classList.add('hidden')
|
39
|
+
|
36
40
|
this.invalidateTarget(target)
|
37
41
|
})
|
38
42
|
}
|
39
43
|
|
40
|
-
|
41
|
-
|
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
|
-
|
45
|
-
|
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
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
78
|
+
if (this.selectedType !== type) {
|
79
|
+
select.selectedIndex = 0
|
80
|
+
}
|
81
|
+
}
|
82
|
+
})
|
55
83
|
}
|
56
84
|
|
57
|
-
|
58
|
-
const target = this.typeTargets.find(
|
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
|
}
|