Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a belongs_to record from the associated record screen #1876

Merged
merged 20 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
26126ad
wip
sdcoffey Jul 29, 2023
d292661
move keep_modal_open view to shared partial
sdcoffey Jul 30, 2023
b9c7b9e
update base controller to show modal when creating via a belongs_to r…
sdcoffey Jul 30, 2023
b7754fd
WIP add create button to belongs_to edit field with return-handling c…
sdcoffey Jul 30, 2023
afc20c3
hook up panel back button from modal
sdcoffey Jul 30, 2023
c3a958b
pass params[:via_belongs_to_resource_class] through #create
sdcoffey Jul 30, 2023
c913b65
add WIP system spec for belongs_to cases
sdcoffey Jul 30, 2023
6b3effb
update specs to cover polymorphic and searchable polymorphic belongs_to
sdcoffey Jul 31, 2023
6336054
style and cleanup
sdcoffey Jul 31, 2023
f68c175
decompose long method in reload_belongs_to_controller
sdcoffey Jul 31, 2023
368533b
fixup! decompose long method in reload_belongs_to_controller
sdcoffey Jul 31, 2023
0454a34
remove change to .gitignore
sdcoffey Jul 31, 2023
25f91b6
remove usage of class_names
sdcoffey Aug 1, 2023
1e44d70
rename _keep_modal_open -> _flash_alerts
sdcoffey Aug 25, 2023
642136a
ensure validation errors are still shown after create failure
sdcoffey Aug 28, 2023
540a92b
add in_modal param to PanelComponent to allow better form display ins…
sdcoffey Aug 28, 2023
c94dc00
fixup! add in_modal param to PanelComponent to allow better form disp…
sdcoffey Aug 28, 2023
21042ec
use to_param/find_record instead of bare id
sdcoffey Aug 29, 2023
528432c
optionally render heading and content in modal
sdcoffey Aug 29, 2023
8427405
tweaks
adrianthedev Oct 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 118 additions & 96 deletions app/components/avo/fields/belongs_to_field/edit_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,115 +1,137 @@
<% if is_polymorphic? %>
<%
# Set the model keys so we can pass them over
model_keys = @field.types.map do |type|
resource = Avo::App.get_resource_by_model_name(type.to_s)
[type.to_s, resource.model_key]
end.to_h
%>
<div class="divide-y"
data-controller="belongs-to-field"
data-searchable="<%= @field.searchable %>"
data-association="<%= @field.id %>"
data-association-class="<%= @field&.target_resource&.model_class || nil %>"
>
<%= field_wrapper **field_wrapper_args, help: @field.polymorphic_help || '' do %>
<%= @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] },
{
value: @field.value,
include_blank: @field.placeholder,
},
{
class: classes("w-full"),
data: {
**@field.get_html(:data, view: view, element: :input),
action: "change->belongs-to-field#changeType #{field_html_action}",
'belongs-to-field-target': "select",
},
disabled: disabled
}
<div data-controller="reload-belongs-to-field"
data-action="turbo:before-stream-render@document->reload-belongs-to-field#beforeStreamRender"
data-reload-belongs-to-field-polymorphic-value="<%= is_polymorphic? %>"
data-reload-belongs-to-field-searchable-value="<%= @field.searchable %>"
data-reload-belongs-to-field-relation-name-value="<%= @field.id %>"
data-reload-belongs-to-field-target-name-value="<%= form.object_name %>[<%= @field.id_input_foreign_key %>]"
>
<% if is_polymorphic? %>
<%
# Set the model keys so we can pass them over
model_keys = @field.types.map do |type|
resource = Avo::App.get_resource_by_model_name(type.to_s)
[type.to_s, resource.model_key]
end.to_h
%>
<%
# If the select field is disabled, no value will be sent. It's how HTML works.
# Thus the extra hidden field to actually send the related id to the server.
if disabled %>
<%= @form.hidden_field @field.type_input_foreign_key %>
<div class="divide-y"
data-controller="belongs-to-field"
data-searchable="<%= @field.searchable %>"
data-association="<%= @field.id %>"
data-association-class="<%= @field&.target_resource&.model_class || nil %>"
>
<%= field_wrapper **field_wrapper_args, help: @field.polymorphic_help || '' do %>
<%= @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] },
{
value: @field.value,
include_blank: @field.placeholder,
},
{
class: classes("w-full"),
data: {
**@field.get_html(:data, view: view, element: :input),
action: "change->belongs-to-field#changeType #{field_html_action}",
'belongs-to-field-target': "select",
},
disabled: disabled
}
%>
<%
# If the select field is disabled, no value will be sent. It's how HTML works.
# Thus the extra hidden field to actually send the related id to the server.
if disabled %>
<%= @form.hidden_field @field.type_input_foreign_key %>
<% end %>
<% end %>
<% end %>
<% @field.types.each do |type| %>
<div class="hidden"
data-belongs-to-field-target="type"
data-type="<%= type %>"
>
<%= field_wrapper **field_wrapper_args, label: ::Avo::App.get_resource_by_model_name(type.to_s).name do %>
<% if @field.searchable %>
<%= render Avo::Fields::BelongsToField::AutocompleteComponent.new form: @form,
disabled: disabled,
field: @field,
foreign_key: @field.id_input_foreign_key,
model_key: model_keys[type.to_s],
polymorphic_record: polymorphic_record,
resource: @resource,
style: @field.get_html(:style, view: view, element: :input),
type: type,
classes: classes("w-full"),
view: view
<% @field.types.each do |type| %>
<div class="hidden"
data-belongs-to-field-target="type"
data-type="<%= type %>"
>
<%= field_wrapper **field_wrapper_args, label: ::Avo::App.get_resource_by_model_name(type.to_s).name do %>
<div class="flex flex-col gap-1">
<% if @field.searchable %>
<%= render Avo::Fields::BelongsToField::AutocompleteComponent.new form: @form,
disabled: disabled,
field: @field,
foreign_key: @field.id_input_foreign_key,
model_key: model_keys[type.to_s],
polymorphic_record: polymorphic_record,
resource: @resource,
style: @field.get_html(:style, view: view, element: :input),
type: type,
classes: classes("w-full"),
view: view
%>
<% else %>
<%= @form.select @field.id_input_foreign_key,
options_for_select(@field.values_for_type(type), @resource.present? && @resource.model.present? ? @resource.model[@field.id_input_foreign_key] : nil),
{
value: @resource.model[@field.id_input_foreign_key].to_s,
include_blank: @field.placeholder,
},
{
class: classes("w-full"),
data: @field.get_html(:data, view: view, element: :input),
disabled: disabled
}
%>
<%
# If the select field is disabled, no value will be sent. It's how HTML works.
# Thus the extra hidden field to actually send the related id to the server.
if disabled %>
<%= @form.hidden_field @field.id_input_foreign_key %>
<% end %>
<% end %>
<% create_href = create_path(::Avo::App.get_resource_by_model_name(type.to_s)) %>
<% if !disabled && create_href.present? %>
<%= link_to t("avo.create_new_item", item: type.to_s.downcase),
create_href,
class: "text-sm"
%>
<% end %>
</div>
<% end %>
</div>
<% end %>
</div>
<% else %>
<%= field_wrapper **field_wrapper_args do %>
<div class="flex flex-col gap-1">
<% if @field.searchable %>
<%= render Avo::Fields::BelongsToField::AutocompleteComponent.new form: @form,
field: @field,
model_key: @field.target_resource&.model_key,
foreign_key: @field.id_input_foreign_key,
resource: @resource,
disabled: disabled,
classes: classes("w-full"),
view: view,
style: @field.get_html(:style, view: view, element: :input)
%>
<% else %>
<%= @form.select @field.id_input_foreign_key,
options_for_select(@field.values_for_type(type), @resource.present? && @resource.model.present? ? @resource.model[@field.id_input_foreign_key] : nil),
<% else %>
<%= @form.select @field.id_input_foreign_key, @field.options,
{
value: @resource.model[@field.id_input_foreign_key].to_s,
include_blank: @field.placeholder,
value: @field.value
},
{
class: classes("w-full"),
data: @field.get_html(:data, view: view, element: :input),
disabled: disabled
disabled: disabled,
style: @field.get_html(:style, view: view, element: :input)
}
%>
<%
<%
# If the select field is disabled, no value will be sent. It's how HTML works.
# Thus the extra hidden field to actually send the related id to the server.
if disabled %>
<%= @form.hidden_field @field.id_input_foreign_key %>
<% end %>
<%= @form.hidden_field @field.id_input_foreign_key %>
<% end %>
<% end %>
<% if !disabled && create_path.present? %>
<%= link_to t("avo.create_new_item", item: @field.name.downcase), create_path, class: "text-sm" %>
<% end %>
</div>
<% end %>
</div>
<% else %>
<%= field_wrapper **field_wrapper_args do %>
<% if @field.searchable %>
<%= render Avo::Fields::BelongsToField::AutocompleteComponent.new form: @form,
field: @field,
model_key: @field.target_resource&.model_key,
foreign_key: @field.id_input_foreign_key,
resource: @resource,
disabled: disabled,
classes: classes("w-full"),
view: view,
style: @field.get_html(:style, view: view, element: :input)
%>
<% else %>
<%= @form.select @field.id_input_foreign_key, @field.options,
{
include_blank: @field.placeholder,
value: @field.value
},
{
class: classes("w-full"),
data: @field.get_html(:data, view: view, element: :input),
disabled: disabled,
style: @field.get_html(:style, view: view, element: :input)
}
%>
<%
# If the select field is disabled, no value will be sent. It's how HTML works.
# Thus the extra hidden field to actually send the related id to the server.
if disabled %>
<%= @form.hidden_field @field.id_input_foreign_key %>
<% end %>
<% end %>
<% end %>
<% end %>
</div>
11 changes: 11 additions & 0 deletions app/components/avo/fields/belongs_to_field/edit_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ def field_html_action
@field.get_html(:data, view: view, element: :input).fetch(:action, nil)
end

def create_path(target_resource = nil)
return nil if @resource.blank?

helpers.new_resource_path(**{
via_relation: @field.id.to_s,
resource: target_resource || @field.target_resource,
via_resource_id: resource.model.id,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use resource.model.to_param instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Looks like that's also happening here:

via_resource_class: @resource.class.to_s,
via_resource_id: @resource.model.to_param

via_belongs_to_resource_class: resource.class.name
}.compact)
end

private

def visit_through_association?
Expand Down
1 change: 1 addition & 0 deletions app/components/avo/referrer_params_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
<%= hidden_field_tag :via_resource_class, params[:via_resource_class] if params[:via_resource_class] %>
<%= hidden_field_tag :via_resource_id, params[:via_resource_id] if params[:via_resource_id] %>
<%= hidden_field_tag :via_relation, params[:via_relation] if params[:via_relation] %>
<%= hidden_field_tag :via_belongs_to_resource_class, params[:via_belongs_to_resource_class] if params[:via_belongs_to_resource_class] %>
<%= hidden_field_tag :referrer, back_path if params[:via_resource_class] %>
3 changes: 2 additions & 1 deletion app/components/avo/views/resource_edit_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<%= content_tag :div,
class: params[:via_belongs_to_resource_class].present? ? "w-full" : "",
data: {
model_name: @resource.model_name.to_s,
resource_name: @resource.class.to_s,
Expand All @@ -18,7 +19,7 @@
multipart: true do |form| %>
<%= render Avo::ReferrerParamsComponent.new back_path: back_path %>
<%= content_tag :div, class: 'space-y-12' do %>
<%= render Avo::PanelComponent.new(name: title, description: @resource.resource_description, display_breadcrumbs: @reflection.blank?, index: 0, data: { panel_id: "main" }) do |c| %>
<%= render Avo::PanelComponent.new(name: title, description: @resource.resource_description, display_breadcrumbs: display_breadcrumbs?, index: 0, data: { panel_id: "main" }) do |c| %>
<% c.with_tools do %>
<%= a_link back_path,
style: :text,
Expand Down
29 changes: 28 additions & 1 deletion app/components/avo/views/resource_edit_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ class Avo::Views::ResourceEditComponent < Avo::ResourceComponent
include Avo::ResourcesHelper
include Avo::ApplicationHelper

def initialize(resource: nil, model: nil, actions: [], view: :edit)
def initialize(resource: nil, model: nil, actions: [], view: :edit, display_breadcrumbs: true)
@resource = resource
@model = model
@actions = actions
@view = view
@display_breadcrumbs = display_breadcrumbs
end

def title
@resource.default_panel_name
end

def back_path
return resource_edit_or_new_path if via_belongs_to?
return resource_view_path if via_resource?
return resources_path if via_index?

Expand All @@ -34,6 +36,23 @@ def resource_view_path
helpers.resource_view_path(model: association_resource.model, resource: association_resource)
end

def resource_edit_or_new_path
modal_id = "new_via_belongs_to"

if params[:via_resource_id].present?
# Back to edit path with param indicating turbo stream should simply close modal
related_resource = params[:via_belongs_to_resource_class].constantize.new
related_record = related_resource.model_class.find(params[:via_resource_id])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we start using resource.model.to_param on the create_path we need to use find_record_method here

helpers.edit_resource_path(resource: related_resource,
model: related_record,
close_modal: modal_id)
else
# Back to new path with param indicating turbo stream should close modal
helpers.new_resource_path(resource: @resource,
close_modal: modal_id)
end
end

def can_see_the_destroy_button?
return super if is_edit? && Avo.configuration.resource_default_view == :edit

Expand All @@ -46,12 +65,20 @@ def can_see_the_save_button?
@resource.authorization.authorize_action @view, raise_exception: false
end

def display_breadcrumbs?
@reflection.blank? && @display_breadcrumbs
end

private

def via_index?
params[:via_view] == "index"
end

def via_belongs_to?
params[:via_belongs_to_resource_class].present?
end

def is_edit?
view.in?([:edit, :update])
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/avo/actions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def keep_modal_open(messages)

respond_to do |format|
format.turbo_stream do
render "keep_modal_open"
render partial: "avo/partials/keep_modal_open"
end
end
end
Expand Down
16 changes: 15 additions & 1 deletion app/controllers/avo/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ def new
@model = @resource.model_class.new
@resource = @resource.hydrate(model: @model, view: :new, user: _current_user)

# Handle special cases when creating a new record via a belongs_to relationship
if params[:via_belongs_to_resource_class].present?
return render turbo_stream: turbo_stream.append('attach_modal', partial: 'avo/base/new_via_belongs_to')
elsif params[:close_modal].present?
return render turbo_stream: turbo_stream.remove(params[:close_modal])
end

set_actions

@page_title = @resource.default_panel_name.to_s
Expand All @@ -130,7 +137,7 @@ def create
@resource.hydrate(model: @model, view: :new, user: _current_user)

# This means that the record has been created through another parent record and we need to attach it somehow.
if params[:via_resource_id].present?
if params[:via_resource_id].present? && params[:via_belongs_to_resource_class].nil?
@reflection = @model._reflections[params[:via_relation]]
# Figure out what kind of association does the record have with the parent record

Expand Down Expand Up @@ -172,6 +179,10 @@ def create
end

def edit
if params[:close_modal].present?
return render turbo_stream: turbo_stream.remove(params[:close_modal])
end

set_actions
end

Expand Down Expand Up @@ -420,6 +431,8 @@ def set_edit_title_and_breadcrumbs
end

def create_success_action
return render "close_modal_and_reload_field" if params[:via_belongs_to_resource_class].present?

respond_to do |format|
format.html { redirect_to after_create_path, notice: create_success_message}
end
Expand All @@ -429,6 +442,7 @@ def create_fail_action
respond_to do |format|
flash.now[:error] = create_fail_message
format.html { render :new, status: :unprocessable_entity }
format.turbo_stream { render partial: "avo/partials/keep_modal_open" }
end
end

Expand Down
Loading
Loading