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

feature: array adapter #3469

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
eb9ad52
feature: array adapter
adrianthedev Nov 28, 2024
91d8b41
update ovei list
adrianthedev Dec 7, 2024
5d2973a
wip: hide unsupported actions, implement show view
Paul-Bob Jan 9, 2025
e1ed8cd
some lint
Paul-Bob Jan 9, 2025
e639cb8
revert indentation
Paul-Bob Jan 9, 2025
c789b97
Merge branch 'main' into feature/array-adapter
Paul-Bob Jan 9, 2025
1fa0f72
lint
Paul-Bob Jan 9, 2025
b6868ae
Merge branch 'feature/array-adapter' of github.com:avo-hq/avo into fe…
Paul-Bob Jan 9, 2025
6355d8b
lint
Paul-Bob Jan 9, 2025
921330f
rm `ActiveRecordResource` for now
Paul-Bob Jan 9, 2025
3572078
wip
Paul-Bob Jan 10, 2025
30e4fb5
rm ostruct requirement
Paul-Bob Jan 10, 2025
16dd5f9
make it work with array of active records
Paul-Bob Jan 10, 2025
14cc44d
Merge branch 'main' into feature/array-adapter
Paul-Bob Jan 10, 2025
dd1c32b
has_many field array option
Paul-Bob Jan 10, 2025
da0b783
lint
Paul-Bob Jan 10, 2025
9fd7808
fix
Paul-Bob Jan 10, 2025
b4c6e01
Merge branch 'main' into feature/array-adapter
Paul-Bob Jan 13, 2025
0482080
fix flaky test
Paul-Bob Jan 13, 2025
bcb3e91
fix flaky test
Paul-Bob Jan 13, 2025
1eca2c8
prepare dummy for tests
Paul-Bob Jan 13, 2025
24e266a
lint
Paul-Bob Jan 13, 2025
6fe69cb
index tests
Paul-Bob Jan 13, 2025
0a74f15
lint & flaky test
Paul-Bob Jan 13, 2025
21dbd3c
lint
Paul-Bob Jan 13, 2025
cee07ca
more tests
Paul-Bob Jan 13, 2025
26a62de
lint
Paul-Bob Jan 13, 2025
2f5f5f3
wait_for_loaded on associations
Paul-Bob Jan 13, 2025
ea17356
rm copyable
Paul-Bob Jan 13, 2025
4955d60
rm comments
Paul-Bob Jan 13, 2025
61408b5
array resource check
Paul-Bob Jan 13, 2025
228579d
using_wait_time capybara
Paul-Bob Jan 13, 2025
feda5d5
.
Paul-Bob Jan 13, 2025
cd740df
fix test
Paul-Bob Jan 13, 2025
50de07f
.
Paul-Bob Jan 13, 2025
c42f23e
fix test
Paul-Bob Jan 13, 2025
79058a8
fix test
Paul-Bob Jan 13, 2025
163096b
wip
Paul-Bob Jan 14, 2025
0972488
wip
Paul-Bob Jan 14, 2025
af256db
wip
Paul-Bob Jan 15, 2025
70743a6
wip
Paul-Bob Jan 15, 2025
da96679
wip
Paul-Bob Jan 15, 2025
c94d9fd
fix test
Paul-Bob Jan 15, 2025
846e3dd
refactor wip
Paul-Bob Jan 15, 2025
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
3 changes: 3 additions & 0 deletions app/components/avo/index/resource_controls_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ def can_detach?
end

def can_edit?
# Disable edit for ArrayResources
return false if @resource.resource_type_array?

return authorize_association_for(:edit) if @reflection.present?

@resource.authorization.authorize_action(:edit, raise_exception: false)
Expand Down
2 changes: 1 addition & 1 deletion app/components/avo/index/table_row_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
parent_resource: @parent_resource
) %>
<% else %>
<td class="text-center">—</td>
<td class="px-3">—</td>
<% end %>
<% end %>
<% if @resource.resource_controls_render_on_the_right? %>
Expand Down
6 changes: 6 additions & 0 deletions app/components/avo/resource_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,18 @@ def detach_path
end

def can_see_the_edit_button?
# Disable edit for ArrayResources
return false if @resource.resource_type_array?

return authorize_association_for(:edit) if @reflection.present?

@resource.authorization.authorize_action(:edit, raise_exception: false)
end

def can_see_the_destroy_button?
# Disable destroy for ArrayResources
return false if @resource.resource_type_array?

@resource.authorization.authorize_action(:destroy, raise_exception: false)
end

Expand Down
2 changes: 1 addition & 1 deletion app/components/avo/views/resource_index_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
description: description,
cover_photo: resource.cover_photo,
data: {component: "resources-index"},
display_breadcrumbs: @reflection.blank? || (@reflection.present? && !helpers.turbo_frame_request?)
display_breadcrumbs: (@reflection.blank? && @parent_resource.blank?) || (@reflection.present? && !helpers.turbo_frame_request?)
) do |c| %>
<% c.with_name_slot do %>
<%= render Avo::PanelNameComponent.new name: title, url: (params[:turbo_frame].present? && linkable?) ? field.frame_url(add_turbo_frame: false) : nil, target: :_blank do |panel_name_component| %>
Expand Down
3 changes: 3 additions & 0 deletions app/components/avo/views/resource_index_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ def available_view_types
# The Create button is dependent on the new? policy method.
# The create? should be called only when the user clicks the Save button so the developers gets access to the params from the form.
def can_see_the_create_button?
# Disable creation for ArrayResources
return false if @resource.resource_type_array?

return authorize_association_for(:create) if @reflection.present?

@resource.authorization.authorize_action(:new, raise_exception: false) && !has_reflection_and_is_read_only
Expand Down
7 changes: 7 additions & 0 deletions app/controllers/avo/array_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module Avo
class ArrayController < BaseController
def set_query
@query ||= @resource.fetch_records
end
end
end
15 changes: 13 additions & 2 deletions app/controllers/avo/associations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@
@parent_record = @parent_resource.find_record(params[:id], params: params)
@parent_resource.hydrate(record: @parent_record)
association_name = BaseResource.valid_association_name(@parent_record, association_from_params)
@query = @related_authorization.apply_policy @parent_record.send(association_name)

# When array field the records are fetched from the field block, from the parent record or from the resource def records
# When other field type, like has_many the @query is directly fetched from the parent record
base_query = if @field.try(:array)
@resource.fetch_records(Avo::ExecutionContext.new(target: @field.block).handle || @parent_record.try(@field.id))
else
@parent_record.send(association_name)
Dismissed Show dismissed Hide dismissed
end

@query = @related_authorization.apply_policy base_query

@association_field = find_association_field(resource: @parent_resource, association: params[:related_name])

if @association_field.present? && @association_field.scope.present?
Expand Down Expand Up @@ -125,7 +135,8 @@
end

def set_attachment_class
@attachment_class = @reflection.klass
# @reflection is nil whe using an Array field.
@attachment_class = @reflection&.klass
end

def set_attachment_resource
Expand Down
18 changes: 5 additions & 13 deletions app/controllers/avo/base_application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,6 @@ def resource
Avo.resource_manager.get_resource_by_controller_name @resource_name
end

def related_resource
# Find the field from the parent resource
field = find_association_field(resource: @resource, association: params[:related_name])

return field.use_resource if field&.use_resource.present?

reflection = @record.class.reflect_on_association(field&.for_attribute || params[:related_name])

reflected_model = reflection.klass

Avo.resource_manager.get_resource_by_model_class reflected_model
end

def set_resource_name
@resource_name = resource_name
end
Expand All @@ -118,6 +105,11 @@ def detect_fields
end

def set_related_resource
# Find the field from the parent resource
related_resource = find_association_field(resource: @resource, association: params[:related_name])
.hydrate(record: @record)
.resource_class(params)

raise Avo::MissingResourceError.new(related_resource_name) if related_resource.nil?

action_view = action_name.to_sym
Expand Down
28 changes: 8 additions & 20 deletions app/controllers/avo/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ def index
set_index_params
set_filters
set_actions

# If we don't get a query object predefined from a child controller like associations, just spin one up
unless defined? @query
@query = @resource.class.query_scope
end
set_query

# Eager load the associations
if @resource.includes.present?
Expand All @@ -46,7 +42,7 @@ def index
end
end

apply_sorting
apply_sorting if @index_params[:sort_by]

# Apply filters to the current query
filters_to_be_applied.each do |filter_class, filter_value|
Expand Down Expand Up @@ -310,18 +306,7 @@ def set_index_params
set_pagination_params

# Sorting
if params[:sort_by].present?
@index_params[:sort_by] = params[:sort_by]
elsif @resource.model_class.present?
available_columns = @resource.model_class.column_names
default_sort_column = @resource.default_sort_column

if available_columns.include?(default_sort_column.to_s)
@index_params[:sort_by] = default_sort_column
elsif available_columns.include?("created_at")
@index_params[:sort_by] = :created_at
end
end
@index_params[:sort_by] = params[:sort_by] || @resource.sort_by_param

@index_params[:sort_direction] = params[:sort_direction] || @resource.default_sort_direction

Expand Down Expand Up @@ -608,8 +593,6 @@ def apply_pagination
end

def apply_sorting
return if @index_params[:sort_by].nil?

sort_by = @index_params[:sort_by].to_sym
if sort_by != :created_at
@query = @query.unscope(:order)
Expand Down Expand Up @@ -650,5 +633,10 @@ def set_pagination_params

@index_params[:per_page] = cookies[:per_page] || Avo.configuration.per_page
end

# If we don't get a query object predefined from a child controller like associations, just spin one up
def set_query
@query ||= @resource.class.query_scope
end
end
end
3 changes: 2 additions & 1 deletion app/views/avo/partials/_table_header.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
} do %>
<%= content_tag :div,
class: "relative flex items-center justify-between w-full" do %>
<% if field.sortable %>
<% # Disable sort for array resources %>
<% if field.sortable && !@resource.resource_type_array? %>
<%= link_to params.permit!.merge(sort_by: sort_by, sort_direction: sort_direction),
class: class_names("flex-1 flex justify-between", text_classes),
'data-turbo-frame': params[:turbo_frame] do %>
Expand Down
1 change: 1 addition & 0 deletions config/initializers/pagy.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "pagy/extras/trim"
require "pagy/extras/countless"
require "pagy/extras/array"
if ::Pagy::VERSION >= ::Gem::Version.new("9.0")
require "pagy/extras/size"
end
Expand Down
7 changes: 5 additions & 2 deletions lib/avo/concerns/has_items.rb
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,11 @@ def is_standalone?(item)
def hydrate_item(item)
return unless item.respond_to? :hydrate

res = self.class.ancestors.include?(Avo::BaseResource) ? self : resource
item.hydrate(view: view, resource: res)
item.hydrate(
view: view,
# Use self when this is executed from a resource context, call resource otherwise.
resource: self.class.ancestors.include?(Avo::Resources::Base) ? self : resource
)
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/avo/concerns/pagination.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module Pagination
PAGINATION_METHOD = {
default: :pagy,
countless: :pagy_countless,
array: :pagy_array,
}
end

Expand Down
12 changes: 12 additions & 0 deletions lib/avo/fields/has_base_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ def query_params(add_turbo_frame: true)
}.compact
end

def resource_class(params)
return use_resource if use_resource.present?

return Avo.resource_manager.get_resource_by_name @id.to_s if @array

reflection = @record.class.reflect_on_association(@for_attribute || params[:related_name])

reflected_model = reflection.klass

Avo.resource_manager.get_resource_by_model_class reflected_model
end

private

def frame_id
Expand Down
3 changes: 3 additions & 0 deletions lib/avo/fields/has_many_field.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
module Avo
module Fields
class HasManyField < HasBaseField
attr_reader :array
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Will we refactor it to ArrayField? I left our explorations in the review.


def initialize(id, **args, &block)
args[:updatable] = false
@array = args[:array]

only_on Avo.configuration.resource_default_view

Expand Down
94 changes: 94 additions & 0 deletions lib/avo/resources/array_resource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
module Avo
module Resources
class ArrayResource < Base
extend ActiveSupport::DescendantsTracker

include Avo::Concerns::FindAssociationField

delegate :model_class, to: :class

class_attribute :pagination, default: {
type: :array
}

class << self
def model_class
@@model_class ||= ActiveSupport::OrderedOptions.new.tap do |obj|
obj.model_name = ActiveSupport::OrderedOptions.new.tap do |thing|
thing.plural = route_key
end
end
end
end

Paul-Bob marked this conversation as resolved.
Show resolved Hide resolved
def records = []

Paul-Bob marked this conversation as resolved.
Show resolved Hide resolved
def find_record(id, query: nil, params: nil)
fetched_records = fetch_records

return super(id, query: fetched_records, params:) if is_active_record_relation?(fetched_records)

fetched_records.find { |i| i.id.to_s == id.to_s }
end

def fetch_records(array_of_records = records)
raise "Unable to fetch any #{name}" if array_of_records.nil?

# When the array of records is declared in a field's block, we need to get that block from the parent resource
# If there is no block try to pick those from the parent_record
# Fallback to resource's def records method
if params[:via_resource_class].present?
via_resource = Avo.resource_manager.get_resource(params[:via_resource_class])
via_record = via_resource.find_record params[:via_record_id], params: params
via_resource = via_resource.new record: via_record, view: :show
via_resource.detect_fields

association_field = find_association_field(resource: via_resource, association: route_key)

records_from_field_or_record = Avo::ExecutionContext.new(target: association_field.block).handle || via_record.try(route_key)

array_of_records = records_from_field_or_record || array_of_records
end

@fetched_records ||= if is_array_of_active_records?(array_of_records)
@@model_class = array_of_records.first.class
@@model_class.where(id: array_of_records.map(&:id))
elsif is_active_record_relation?(array_of_records)
@@model_class = array_of_records.try(:model)
array_of_records
else
# Dynamically create a class with accessors for all unique keys from the records
keys = array_of_records.flat_map(&:keys).uniq

custom_class = Class.new do
include ActiveModel::Model

# Dynamically define accessors
attr_accessor(*keys)

define_method(:to_param) do
id
end
end

# Map the records to instances of the dynamically created class
array_of_records.map do |item|
custom_class.new(item)
end
end
end

def is_array_of_active_records?(array_of_records = records)
@is_array_of_active_records ||= array_of_records.all? { |element| element.is_a?(ActiveRecord::Base) }
end

def is_active_record_relation?(array_of_records = records)
Paul-Bob marked this conversation as resolved.
Show resolved Hide resolved
@is_active_record_relation ||= array_of_records.is_a?(ActiveRecord::Relation)
end

def resource_type_array? = true

def sort_by_param = nil
end
end
end
12 changes: 12 additions & 0 deletions lib/avo/resources/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,18 @@ def get_external_link
Avo::ExecutionContext.new(target: external_link, resource: self, record: record).handle
end

def resource_type_array? = false

def sort_by_param
available_columns = model_class.column_names

if available_columns.include?(default_sort_column.to_s)
default_sort_column
elsif available_columns.include?("created_at")
:created_at
end
end

private

def flatten_keys(array)
Expand Down
3 changes: 2 additions & 1 deletion lib/avo/resources/resource_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ def fetch_resources
load_resources_namespace
end

BaseResource.descendants
# All descendants from Avo::Resources::Base except the internal ones
Base.descendants - [Avo::BaseResource, Avo::Resources::ArrayResource]
end

def load_resources_namespace
Expand Down
2 changes: 2 additions & 0 deletions lib/generators/avo/concerns/parent_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ module ParentController
end

def parent_controller
return "Avo::ArrayController" if options["array"]

options["parent-controller"] || ::Avo.configuration.resource_parent_controller
end
end
Expand Down
Loading
Loading