easy-admin-rails 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/assets/builds/easy_admin.base.js +43505 -0
- data/app/assets/builds/easy_admin.base.js.map +7 -0
- data/app/assets/builds/easy_admin.css +6141 -0
- data/app/assets/config/easy_admin_manifest.js +1 -0
- data/app/assets/images/jsoneditor-icons.svg +749 -0
- data/app/assets/stylesheets/easy_admin/application.tailwind.css +390 -0
- data/app/components/easy_admin/base_component.rb +35 -0
- data/app/components/easy_admin/batch_action_bar_component.rb +125 -0
- data/app/components/easy_admin/batch_action_form_component.rb +124 -0
- data/app/components/easy_admin/combined_filters_component.rb +232 -0
- data/app/components/easy_admin/confirmation_modal_component.rb +61 -0
- data/app/components/easy_admin/context_menu_component.rb +161 -0
- data/app/components/easy_admin/dashboards/base_card_component.rb +152 -0
- data/app/components/easy_admin/dashboards/card_error_component.rb +23 -0
- data/app/components/easy_admin/dashboards/card_factory.rb +90 -0
- data/app/components/easy_admin/dashboards/card_stream_component.rb +22 -0
- data/app/components/easy_admin/dashboards/cards/base_card_component.rb +54 -0
- data/app/components/easy_admin/dashboards/cards/chart_card_component.rb +175 -0
- data/app/components/easy_admin/dashboards/cards/custom_card_component.rb +50 -0
- data/app/components/easy_admin/dashboards/cards/metric_card_component.rb +164 -0
- data/app/components/easy_admin/dashboards/cards/table_card_component.rb +148 -0
- data/app/components/easy_admin/dashboards/chart_card_component.rb +44 -0
- data/app/components/easy_admin/dashboards/metric_card_component.rb +56 -0
- data/app/components/easy_admin/dashboards/refresh_stream_component.rb +279 -0
- data/app/components/easy_admin/dashboards/show_component.rb +163 -0
- data/app/components/easy_admin/dashboards/table_card_component.rb +52 -0
- data/app/components/easy_admin/date_picker_component.rb +188 -0
- data/app/components/easy_admin/fields/base_component.rb +101 -0
- data/app/components/easy_admin/fields/belongs_to_edit_modal_component.rb +117 -0
- data/app/components/easy_admin/fields/form/belongs_to_component.rb +82 -0
- data/app/components/easy_admin/fields/form/boolean_component.rb +100 -0
- data/app/components/easy_admin/fields/form/date_component.rb +55 -0
- data/app/components/easy_admin/fields/form/datetime_component.rb +55 -0
- data/app/components/easy_admin/fields/form/email_component.rb +55 -0
- data/app/components/easy_admin/fields/form/file_component.rb +190 -0
- data/app/components/easy_admin/fields/form/has_many_component.rb +416 -0
- data/app/components/easy_admin/fields/form/json_component.rb +81 -0
- data/app/components/easy_admin/fields/form/number_component.rb +55 -0
- data/app/components/easy_admin/fields/form/select_component.rb +326 -0
- data/app/components/easy_admin/fields/form/text_component.rb +55 -0
- data/app/components/easy_admin/fields/form/textarea_component.rb +54 -0
- data/app/components/easy_admin/fields/index/belongs_to_component.rb +93 -0
- data/app/components/easy_admin/fields/index/boolean_component.rb +29 -0
- data/app/components/easy_admin/fields/index/date_component.rb +13 -0
- data/app/components/easy_admin/fields/index/datetime_component.rb +13 -0
- data/app/components/easy_admin/fields/index/email_component.rb +24 -0
- data/app/components/easy_admin/fields/index/filters/base_component.rb +48 -0
- data/app/components/easy_admin/fields/index/filters/boolean_component.rb +96 -0
- data/app/components/easy_admin/fields/index/filters/date_component.rb +182 -0
- data/app/components/easy_admin/fields/index/filters/number_component.rb +30 -0
- data/app/components/easy_admin/fields/index/filters/select_component.rb +101 -0
- data/app/components/easy_admin/fields/index/filters/string_component.rb +32 -0
- data/app/components/easy_admin/fields/index/json_component.rb +23 -0
- data/app/components/easy_admin/fields/index/number_component.rb +20 -0
- data/app/components/easy_admin/fields/index/select_component.rb +25 -0
- data/app/components/easy_admin/fields/index/text_component.rb +20 -0
- data/app/components/easy_admin/fields/inline_edit_modal_component.rb +135 -0
- data/app/components/easy_admin/fields/inline_edit_trigger_component.rb +144 -0
- data/app/components/easy_admin/fields/show/belongs_to_component.rb +93 -0
- data/app/components/easy_admin/fields/show/boolean_component.rb +21 -0
- data/app/components/easy_admin/fields/show/date_component.rb +13 -0
- data/app/components/easy_admin/fields/show/datetime_component.rb +13 -0
- data/app/components/easy_admin/fields/show/email_component.rb +19 -0
- data/app/components/easy_admin/fields/show/file_component.rb +304 -0
- data/app/components/easy_admin/fields/show/has_many_component.rb +192 -0
- data/app/components/easy_admin/fields/show/json_component.rb +45 -0
- data/app/components/easy_admin/fields/show/number_component.rb +20 -0
- data/app/components/easy_admin/fields/show/select_component.rb +25 -0
- data/app/components/easy_admin/fields/show/text_component.rb +17 -0
- data/app/components/easy_admin/fields/show/textarea_component.rb +26 -0
- data/app/components/easy_admin/filters_component.rb +120 -0
- data/app/components/easy_admin/form_tabs_component.rb +166 -0
- data/app/components/easy_admin/infinite_scroll_component.rb +82 -0
- data/app/components/easy_admin/lazy_chart_card_component.rb +128 -0
- data/app/components/easy_admin/lazy_metric_card_component.rb +76 -0
- data/app/components/easy_admin/modal_frame_component.rb +26 -0
- data/app/components/easy_admin/navbar_component.rb +226 -0
- data/app/components/easy_admin/notification_component.rb +83 -0
- data/app/components/easy_admin/pagination_component.rb +188 -0
- data/app/components/easy_admin/quick_filters_component.rb +65 -0
- data/app/components/easy_admin/resource_pagination_component.rb +14 -0
- data/app/components/easy_admin/resources/index_component.rb +211 -0
- data/app/components/easy_admin/resources/index_frame_component.rb +88 -0
- data/app/components/easy_admin/resources/show_page_actions_component.rb +324 -0
- data/app/components/easy_admin/resources/table_cell_component.rb +145 -0
- data/app/components/easy_admin/resources/table_component.rb +206 -0
- data/app/components/easy_admin/resources/table_row_component.rb +160 -0
- data/app/components/easy_admin/row_action_form_component.rb +127 -0
- data/app/components/easy_admin/scopes_component.rb +224 -0
- data/app/components/easy_admin/settings_sidebar_component.rb +140 -0
- data/app/components/easy_admin/show_layout_component.rb +600 -0
- data/app/components/easy_admin/sidebar_component.rb +174 -0
- data/app/components/easy_admin/turbo/response_component.rb +40 -0
- data/app/components/easy_admin/turbo/stream_component.rb +28 -0
- data/app/controllers/easy_admin/application_controller.rb +66 -0
- data/app/controllers/easy_admin/batch_actions_controller.rb +166 -0
- data/app/controllers/easy_admin/confirmation_modal_controller.rb +20 -0
- data/app/controllers/easy_admin/dashboard_controller.rb +6 -0
- data/app/controllers/easy_admin/dashboards_controller.rb +123 -0
- data/app/controllers/easy_admin/passwords_controller.rb +15 -0
- data/app/controllers/easy_admin/registrations_controller.rb +52 -0
- data/app/controllers/easy_admin/resources_controller.rb +907 -0
- data/app/controllers/easy_admin/row_actions_controller.rb +216 -0
- data/app/controllers/easy_admin/sessions_controller.rb +32 -0
- data/app/controllers/easy_admin/settings_controller.rb +94 -0
- data/app/helpers/easy_admin/application_helper.rb +4 -0
- data/app/helpers/easy_admin/dashboards_helper.rb +121 -0
- data/app/helpers/easy_admin/fields_helper.rb +27 -0
- data/app/helpers/easy_admin/pagy_helper.rb +30 -0
- data/app/helpers/easy_admin/resources_helper.rb +39 -0
- data/app/javascript/easy_admin/application.js +12 -0
- data/app/javascript/easy_admin/controllers/batch_modal_controller.js +66 -0
- data/app/javascript/easy_admin/controllers/batch_selection_controller.js +223 -0
- data/app/javascript/easy_admin/controllers/chart_controller.js +216 -0
- data/app/javascript/easy_admin/controllers/collapsible_filters_controller.js +118 -0
- data/app/javascript/easy_admin/controllers/confirmation_modal_controller.js +64 -0
- data/app/javascript/easy_admin/controllers/context_menu_controller.js +227 -0
- data/app/javascript/easy_admin/controllers/date_picker_controller.js +309 -0
- data/app/javascript/easy_admin/controllers/dropdown_controller.js +63 -0
- data/app/javascript/easy_admin/controllers/event_emitter_controller.js +19 -0
- data/app/javascript/easy_admin/controllers/file_controller.js +121 -0
- data/app/javascript/easy_admin/controllers/form_tabs_controller.js +100 -0
- data/app/javascript/easy_admin/controllers/has_many_search_controller.js +76 -0
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +174 -0
- data/app/javascript/easy_admin/controllers/ios_alert_controller.js +195 -0
- data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +88 -0
- data/app/javascript/easy_admin/controllers/modal_controller.js +75 -0
- data/app/javascript/easy_admin/controllers/navbar_scroll_controller.js +76 -0
- data/app/javascript/easy_admin/controllers/notification_controller.js +48 -0
- data/app/javascript/easy_admin/controllers/row_action_controller.js +124 -0
- data/app/javascript/easy_admin/controllers/row_modal_controller.js +59 -0
- data/app/javascript/easy_admin/controllers/select_field_controller.js +618 -0
- data/app/javascript/easy_admin/controllers/settings_button_controller.js +8 -0
- data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +186 -0
- data/app/javascript/easy_admin/controllers/sidebar_controller.js +102 -0
- data/app/javascript/easy_admin/controllers/sidebar_mobile_controller.js +23 -0
- data/app/javascript/easy_admin/controllers/sidebar_nav_controller.js +96 -0
- data/app/javascript/easy_admin/controllers/table_controller.js +28 -0
- data/app/javascript/easy_admin/controllers/table_row_controller.js +16 -0
- data/app/javascript/easy_admin/controllers/toggle_switch_controller.js +22 -0
- data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +9 -0
- data/app/javascript/easy_admin/controllers.js +54 -0
- data/app/javascript/easy_admin.base.js +4 -0
- data/app/models/easy_admin/admin_user.rb +53 -0
- data/app/models/easy_admin/application_record.rb +5 -0
- data/app/views/easy_admin/dashboard/index.html.erb +3 -0
- data/app/views/easy_admin/dashboards/show.html.erb +7 -0
- data/app/views/easy_admin/passwords/edit.html.erb +42 -0
- data/app/views/easy_admin/passwords/new.html.erb +41 -0
- data/app/views/easy_admin/registrations/new.html.erb +65 -0
- data/app/views/easy_admin/resources/_redirect.turbo_stream.erb +3 -0
- data/app/views/easy_admin/resources/_table_rows.html.erb +46 -0
- data/app/views/easy_admin/resources/edit.html.erb +151 -0
- data/app/views/easy_admin/resources/index.html.erb +12 -0
- data/app/views/easy_admin/resources/index.turbo_stream.erb +139 -0
- data/app/views/easy_admin/resources/index_frame.html.erb +142 -0
- data/app/views/easy_admin/resources/new.html.erb +100 -0
- data/app/views/easy_admin/resources/show.html.erb +31 -0
- data/app/views/easy_admin/sessions/new.html.erb +55 -0
- data/app/views/easy_admin/settings/_form.html.erb +51 -0
- data/app/views/easy_admin/settings/index.html.erb +53 -0
- data/app/views/layouts/easy_admin/application.html.erb +48 -0
- data/app/views/layouts/easy_admin/auth.html.erb +34 -0
- data/config/initializers/easy_admin_card_factory.rb +27 -0
- data/config/initializers/pagy.rb +15 -0
- data/config/initializers/rack_mini_profiler.rb +67 -0
- data/config/routes.rb +70 -0
- data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +45 -0
- data/lib/easy-admin.rb +32 -0
- data/lib/easy_admin/action.rb +159 -0
- data/lib/easy_admin/batch_action.rb +134 -0
- data/lib/easy_admin/configuration.rb +75 -0
- data/lib/easy_admin/dashboard.rb +110 -0
- data/lib/easy_admin/dashboard_registry.rb +30 -0
- data/lib/easy_admin/delete_action.rb +22 -0
- data/lib/easy_admin/engine.rb +54 -0
- data/lib/easy_admin/field.rb +118 -0
- data/lib/easy_admin/resource.rb +806 -0
- data/lib/easy_admin/resource_registry.rb +22 -0
- data/lib/easy_admin/types/json_type.rb +25 -0
- data/lib/easy_admin/version.rb +3 -0
- data/lib/generators/easy_admin/auth_generator.rb +69 -0
- data/lib/generators/easy_admin/card/card_generator.rb +94 -0
- data/lib/generators/easy_admin/card/templates/card_component.rb.erb +127 -0
- data/lib/generators/easy_admin/card/templates/card_component_spec.rb.erb +122 -0
- data/lib/generators/easy_admin/install/templates/easy_admin.rb +31 -0
- data/lib/generators/easy_admin/install_generator.rb +25 -0
- data/lib/generators/easy_admin/rbac/rbac_generator.rb +244 -0
- data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +23 -0
- data/lib/generators/easy_admin/rbac/templates/super_admin.rb +34 -0
- data/lib/generators/easy_admin/resource_generator.rb +43 -0
- data/lib/generators/easy_admin/templates/AUTH_README +35 -0
- data/lib/generators/easy_admin/templates/README +27 -0
- data/lib/generators/easy_admin/templates/create_easy_admin_admin_users.rb +45 -0
- data/lib/generators/easy_admin/templates/devise.rb +267 -0
- data/lib/generators/easy_admin/templates/easy_admin.rb +24 -0
- data/lib/generators/easy_admin/templates/resource.rb +29 -0
- data/lib/tasks/easy_admin_tasks.rake +4 -0
- metadata +445 -0
@@ -0,0 +1,907 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
class ResourcesController < ApplicationController
|
3
|
+
before_action :load_resource_class
|
4
|
+
before_action :load_record, only: [:show, :edit, :update, :destroy, :edit_field, :update_field, :belongs_to_reattach, :belongs_to_edit_attached, :update_belongs_to_attached]
|
5
|
+
|
6
|
+
def index
|
7
|
+
# Set up data needed for both HTML and turbo stream
|
8
|
+
sort_field = params[:sort]
|
9
|
+
sort_direction = params[:direction] == 'desc' ? 'desc' : 'asc'
|
10
|
+
|
11
|
+
# Handle scope filtering
|
12
|
+
@current_scope = determine_current_scope
|
13
|
+
|
14
|
+
# Only calculate scope counts for HTML requests, not for infinite scroll (turbo_stream)
|
15
|
+
if @resource_class.has_scopes? && !request.format.turbo_stream?
|
16
|
+
@scope_counts = calculate_scope_counts
|
17
|
+
end
|
18
|
+
|
19
|
+
# Start with base records (ensure we have a relation, not just a class)
|
20
|
+
@records = apply_scope(@resource_class.model_class.all)
|
21
|
+
|
22
|
+
# Apply eager loading for index action
|
23
|
+
if @resource_class.index_includes.any?
|
24
|
+
@records = @records.includes(@resource_class.index_includes)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Apply Ransack filtering if present
|
28
|
+
if params[:q].present?
|
29
|
+
@search_query = @records.ransack(params[:q])
|
30
|
+
@records = @search_query.result
|
31
|
+
end
|
32
|
+
|
33
|
+
# Apply text search if present (fallback/additional search)
|
34
|
+
if params[:search].present?
|
35
|
+
@records = @resource_class.search_records(params[:search], sort_field: sort_field, sort_direction: sort_direction, records: @records)
|
36
|
+
else
|
37
|
+
@records = apply_sorting(@records, sort_field, sort_direction)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Apply period filtering
|
41
|
+
@records = apply_period_filter(@records)
|
42
|
+
|
43
|
+
# Apply pagination using Pagy with resource-specific configuration
|
44
|
+
items_per_page = @resource_class.respond_to?(:items_per_page) ? @resource_class.items_per_page : 20
|
45
|
+
|
46
|
+
# Reset to page 1 if filtering/searching is applied to avoid overflow
|
47
|
+
current_page = if has_active_filters?
|
48
|
+
1
|
49
|
+
else
|
50
|
+
params[:page] || 1
|
51
|
+
end
|
52
|
+
|
53
|
+
pagination_options = {
|
54
|
+
items: items_per_page,
|
55
|
+
limit: items_per_page,
|
56
|
+
page: current_page,
|
57
|
+
overflow: :last_page # Handle overflow by redirecting to last valid page
|
58
|
+
}
|
59
|
+
|
60
|
+
# Use countless pagination if enabled for performance
|
61
|
+
if @resource_class.respond_to?(:countless_enabled?) && @resource_class.countless_enabled?
|
62
|
+
@pagy, @records = pagy_countless(@records, **pagination_options)
|
63
|
+
else
|
64
|
+
@pagy, @records = pagy(@records, **pagination_options)
|
65
|
+
end
|
66
|
+
@current_sort_field = sort_field
|
67
|
+
@current_sort_direction = sort_direction
|
68
|
+
@current_period = params[:period]
|
69
|
+
|
70
|
+
respond_to do |format|
|
71
|
+
format.html do
|
72
|
+
# Check if this is a turbo frame request
|
73
|
+
if turbo_frame_request?
|
74
|
+
render plain: EasyAdmin::Resources::IndexFrameComponent.new(
|
75
|
+
resource_class: @resource_class,
|
76
|
+
records: @records,
|
77
|
+
pagy: @pagy,
|
78
|
+
current_params: params.permit(:search, :scope, :sort, :direction, :period, :page, q: {}),
|
79
|
+
current_path: request.path,
|
80
|
+
current_user: current_admin_user
|
81
|
+
).call
|
82
|
+
else
|
83
|
+
# Regular page load - shows skeleton with layout
|
84
|
+
# Will render app/views/easy_admin/resources/index.html.erb
|
85
|
+
end
|
86
|
+
end
|
87
|
+
format.turbo_stream do
|
88
|
+
# For infinite scroll - append new rows to existing table
|
89
|
+
table_rows_html = @records.map do |record|
|
90
|
+
EasyAdmin::Resources::TableRowComponent.new(
|
91
|
+
record: record,
|
92
|
+
resource_class: @resource_class
|
93
|
+
).call
|
94
|
+
end.join.html_safe
|
95
|
+
|
96
|
+
render turbo_stream: [
|
97
|
+
turbo_stream.append("records-container", table_rows_html),
|
98
|
+
turbo_stream.replace("infinite-scroll-container", EasyAdmin::InfiniteScrollComponent.new(
|
99
|
+
pagy: @pagy,
|
100
|
+
resource_class: @resource_class,
|
101
|
+
current_params: params.permit(:search, :scope, :sort, :direction, :period, :page, q: {}),
|
102
|
+
current_path: request.path
|
103
|
+
).call),
|
104
|
+
turbo_stream.replace("combined-filters", EasyAdmin::CombinedFiltersComponent.new(
|
105
|
+
resource_class: @resource_class,
|
106
|
+
current_params: params.permit(:search, :scope, :sort, :direction, :period, :page, q: {}),
|
107
|
+
search_params: params[:q] || {},
|
108
|
+
current_period: @current_period
|
109
|
+
).call)
|
110
|
+
]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def show
|
116
|
+
end
|
117
|
+
|
118
|
+
# Lazy loading endpoints for show page components
|
119
|
+
def metric
|
120
|
+
metric_config = find_metric_config(params[:metric_id])
|
121
|
+
render turbo_stream: turbo_stream.replace(
|
122
|
+
"metric-#{params[:metric_id]}",
|
123
|
+
EasyAdmin::LazyMetricCardComponent.new(metric_config: metric_config, record: @record)
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
def chart
|
128
|
+
chart_config = find_chart_config(params[:chart_id])
|
129
|
+
render turbo_stream: turbo_stream.replace(
|
130
|
+
"chart-#{params[:chart_id]}",
|
131
|
+
EasyAdmin::LazyChartCardComponent.new(chart_config: chart_config, record: @record)
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
def tab_content
|
136
|
+
tab_config = find_tab_config(params[:tab_name])
|
137
|
+
render turbo_stream: turbo_stream.replace(
|
138
|
+
"tab-#{params[:tab_name]}-content",
|
139
|
+
render_tab_elements(tab_config[:elements])
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
def new
|
144
|
+
@record = @resource_class.model_class.new
|
145
|
+
end
|
146
|
+
|
147
|
+
def create
|
148
|
+
@record = @resource_class.model_class.new(record_params)
|
149
|
+
|
150
|
+
if @record.save
|
151
|
+
redirect_to easy_admin.resource_path(@resource_class.route_key, @record),
|
152
|
+
notice: "#{@resource_class.singular_title} was successfully created."
|
153
|
+
else
|
154
|
+
render :new
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def edit
|
159
|
+
end
|
160
|
+
|
161
|
+
def update
|
162
|
+
Rails.logger.debug "Update called with params: #{record_params}"
|
163
|
+
Rails.logger.debug "Record before update: #{@record.inspect}"
|
164
|
+
|
165
|
+
if @record.update(record_params)
|
166
|
+
Rails.logger.debug "Record after update: #{@record.reload.inspect}"
|
167
|
+
respond_to do |format|
|
168
|
+
format.html {
|
169
|
+
redirect_to easy_admin.resource_path(@resource_class.route_key, @record),
|
170
|
+
notice: "#{@resource_class.singular_title} was successfully updated."
|
171
|
+
}
|
172
|
+
format.turbo_stream {
|
173
|
+
render turbo_stream: [
|
174
|
+
turbo_stream.update("notifications",
|
175
|
+
EasyAdmin::NotificationComponent.new(
|
176
|
+
type: :success,
|
177
|
+
message: "#{@resource_class.singular_title} was successfully updated!",
|
178
|
+
title: "Success"
|
179
|
+
).call
|
180
|
+
)
|
181
|
+
]
|
182
|
+
}
|
183
|
+
end
|
184
|
+
else
|
185
|
+
render :edit
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def destroy
|
190
|
+
@record.destroy
|
191
|
+
redirect_to easy_admin.resources_path(@resource_class.route_key),
|
192
|
+
notice: "#{@resource_class.singular_title} was successfully deleted."
|
193
|
+
end
|
194
|
+
|
195
|
+
# Inline field editing actions
|
196
|
+
def edit_field
|
197
|
+
@field_name = params[:field]
|
198
|
+
@field_config = find_field_config(@field_name)
|
199
|
+
|
200
|
+
unless @field_config
|
201
|
+
render json: { error: "Field '#{@field_name}' not found" }, status: :not_found
|
202
|
+
return
|
203
|
+
end
|
204
|
+
|
205
|
+
respond_to do |format|
|
206
|
+
format.html do
|
207
|
+
render plain: EasyAdmin::Fields::InlineEditModalComponent.new(
|
208
|
+
record: @record,
|
209
|
+
field_config: @field_config,
|
210
|
+
resource_class: @resource_class
|
211
|
+
).call
|
212
|
+
end
|
213
|
+
format.turbo_stream do
|
214
|
+
render turbo_stream: turbo_stream.update("modal",
|
215
|
+
EasyAdmin::Fields::InlineEditModalComponent.new(
|
216
|
+
record: @record,
|
217
|
+
field_config: @field_config,
|
218
|
+
resource_class: @resource_class
|
219
|
+
).call
|
220
|
+
)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def update_field
|
226
|
+
@field_name = params[:field]
|
227
|
+
@field_config = find_field_config(@field_name)
|
228
|
+
|
229
|
+
unless @field_config
|
230
|
+
render json: { error: "Field '#{@field_name}' not found" }, status: :not_found
|
231
|
+
return
|
232
|
+
end
|
233
|
+
|
234
|
+
# For belongs_to fields, we need to handle foreign key updates
|
235
|
+
if @field_config[:type] == :belongs_to
|
236
|
+
association_name = @field_config[:name]
|
237
|
+
foreign_key = get_foreign_key_name(association_name)
|
238
|
+
field_value = params.dig(@resource_class.param_key, foreign_key)
|
239
|
+
update_attrs = { foreign_key.to_sym => field_value }
|
240
|
+
else
|
241
|
+
# Create a field object to handle normalization
|
242
|
+
field_obj = EasyAdmin::Field.new(@field_name, @field_config[:type], @field_config)
|
243
|
+
|
244
|
+
# Get the field value from params
|
245
|
+
field_value = params.dig(@resource_class.param_key, @field_name)
|
246
|
+
|
247
|
+
# Normalize the input
|
248
|
+
update_attrs = { @field_name => field_value }
|
249
|
+
field_obj.normalize_input!(update_attrs)
|
250
|
+
end
|
251
|
+
|
252
|
+
if @record.update(update_attrs)
|
253
|
+
respond_to do |format|
|
254
|
+
format.turbo_stream do
|
255
|
+
# Update the table cell with the new value
|
256
|
+
render turbo_stream: [
|
257
|
+
turbo_stream.replace(
|
258
|
+
"#{helpers.dom_id(@record)}_#{@field_name}",
|
259
|
+
EasyAdmin::Resources::TableCellComponent.new(
|
260
|
+
record: @record.reload,
|
261
|
+
field_config: @field_config,
|
262
|
+
resource_class: @resource_class
|
263
|
+
).call
|
264
|
+
),
|
265
|
+
turbo_stream.update("notifications",
|
266
|
+
EasyAdmin::NotificationComponent.new(
|
267
|
+
type: :success,
|
268
|
+
message: "#{@field_config[:label]} updated successfully!",
|
269
|
+
title: "Success"
|
270
|
+
).call
|
271
|
+
)
|
272
|
+
]
|
273
|
+
end
|
274
|
+
format.json { render json: { status: 'success' } }
|
275
|
+
end
|
276
|
+
else
|
277
|
+
respond_to do |format|
|
278
|
+
format.turbo_stream do
|
279
|
+
render turbo_stream: turbo_stream.update("notifications",
|
280
|
+
EasyAdmin::NotificationComponent.new(
|
281
|
+
type: :error,
|
282
|
+
message: @record.errors.full_messages.join(', '),
|
283
|
+
title: "Error"
|
284
|
+
).call
|
285
|
+
)
|
286
|
+
end
|
287
|
+
format.json { render json: { errors: @record.errors } }
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def belongs_to_reattach
|
293
|
+
@field_name = params[:field]
|
294
|
+
@field_config = find_field_config(@field_name)
|
295
|
+
|
296
|
+
unless @field_config
|
297
|
+
render json: { error: "Field '#{@field_name}' not found" }, status: :not_found
|
298
|
+
return
|
299
|
+
end
|
300
|
+
|
301
|
+
# Convert belongs_to field to select field format for reattaching
|
302
|
+
select_field_config = prepare_belongs_to_as_select(@field_config)
|
303
|
+
|
304
|
+
respond_to do |format|
|
305
|
+
format.html do
|
306
|
+
render plain: EasyAdmin::Fields::InlineEditModalComponent.new(
|
307
|
+
record: @record,
|
308
|
+
field_config: select_field_config,
|
309
|
+
resource_class: @resource_class
|
310
|
+
).call
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def belongs_to_edit_attached
|
316
|
+
@field_name = params[:field]
|
317
|
+
@field_config = find_field_config(@field_name)
|
318
|
+
|
319
|
+
unless @field_config
|
320
|
+
render json: { error: "Field '#{@field_name}' not found" }, status: :not_found
|
321
|
+
return
|
322
|
+
end
|
323
|
+
|
324
|
+
unless @field_config[:type] == :belongs_to
|
325
|
+
render json: { error: "Field '#{@field_name}' is not a belongs_to field" }, status: :bad_request
|
326
|
+
return
|
327
|
+
end
|
328
|
+
|
329
|
+
# Get the associated record
|
330
|
+
associated_record = @record.public_send(@field_config[:name])
|
331
|
+
|
332
|
+
unless associated_record
|
333
|
+
render json: { error: "No associated record found" }, status: :not_found
|
334
|
+
return
|
335
|
+
end
|
336
|
+
|
337
|
+
# Get the resource class for the associated record
|
338
|
+
association_name = @field_config[:name]
|
339
|
+
associated_model_class = @resource_class.model_class.reflect_on_association(association_name).klass
|
340
|
+
associated_resource_class = find_resource_class_for_model(associated_model_class)
|
341
|
+
|
342
|
+
unless associated_resource_class
|
343
|
+
render json: { error: "No resource class found for #{associated_model_class.name}" }, status: :not_found
|
344
|
+
return
|
345
|
+
end
|
346
|
+
|
347
|
+
respond_to do |format|
|
348
|
+
format.html do
|
349
|
+
render plain: EasyAdmin::Fields::BelongsToEditModalComponent.new(
|
350
|
+
record: associated_record,
|
351
|
+
resource_class: associated_resource_class,
|
352
|
+
parent_record: @record,
|
353
|
+
parent_field: @field_config
|
354
|
+
).call
|
355
|
+
end
|
356
|
+
format.turbo_stream do
|
357
|
+
render turbo_stream: turbo_stream.update("modal",
|
358
|
+
EasyAdmin::Fields::BelongsToEditModalComponent.new(
|
359
|
+
record: associated_record,
|
360
|
+
resource_class: associated_resource_class,
|
361
|
+
parent_record: @record,
|
362
|
+
parent_field: @field_config
|
363
|
+
).call
|
364
|
+
)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def update_belongs_to_attached
|
370
|
+
@field_name = params[:field]
|
371
|
+
@field_config = find_field_config(@field_name)
|
372
|
+
@attached_id = params[:attached_id]
|
373
|
+
|
374
|
+
unless @field_config
|
375
|
+
render json: { error: "Field '#{@field_name}' not found" }, status: :not_found
|
376
|
+
return
|
377
|
+
end
|
378
|
+
|
379
|
+
# Get the associated record
|
380
|
+
association_name = @field_config[:name]
|
381
|
+
associated_model_class = @resource_class.model_class.reflect_on_association(association_name).klass
|
382
|
+
associated_resource_class = find_resource_class_for_model(associated_model_class)
|
383
|
+
|
384
|
+
# Find the attached record
|
385
|
+
attached_record = associated_model_class.find(@attached_id)
|
386
|
+
|
387
|
+
unless attached_record
|
388
|
+
render json: { error: "Attached record not found" }, status: :not_found
|
389
|
+
return
|
390
|
+
end
|
391
|
+
|
392
|
+
# Get the update parameters for the attached record
|
393
|
+
raw_params = params.dig(associated_resource_class.param_key) || {}
|
394
|
+
|
395
|
+
# Get permitted attributes from the resource class
|
396
|
+
permitted_attributes = get_permitted_attributes(associated_resource_class)
|
397
|
+
update_params = raw_params.permit(*permitted_attributes)
|
398
|
+
|
399
|
+
if attached_record.update(update_params)
|
400
|
+
respond_to do |format|
|
401
|
+
format.turbo_stream do
|
402
|
+
# Update the parent table cell to reflect any changes in the associated record
|
403
|
+
render turbo_stream: [
|
404
|
+
turbo_stream.replace(
|
405
|
+
"#{helpers.dom_id(@record)}_#{@field_name}",
|
406
|
+
EasyAdmin::Resources::TableCellComponent.new(
|
407
|
+
record: @record.reload,
|
408
|
+
field_config: @field_config,
|
409
|
+
resource_class: @resource_class
|
410
|
+
).call
|
411
|
+
),
|
412
|
+
turbo_stream.update("notifications",
|
413
|
+
EasyAdmin::NotificationComponent.new(
|
414
|
+
type: :success,
|
415
|
+
message: "#{associated_resource_class.singular_title} updated successfully!",
|
416
|
+
title: "Success"
|
417
|
+
).call
|
418
|
+
)
|
419
|
+
]
|
420
|
+
end
|
421
|
+
end
|
422
|
+
else
|
423
|
+
respond_to do |format|
|
424
|
+
format.turbo_stream do
|
425
|
+
render turbo_stream: turbo_stream.update("notifications",
|
426
|
+
EasyAdmin::NotificationComponent.new(
|
427
|
+
type: :error,
|
428
|
+
message: attached_record.errors.full_messages.join(', '),
|
429
|
+
title: "Error"
|
430
|
+
).call
|
431
|
+
)
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
def suggest
|
438
|
+
field_config = @resource_class.fields_config.find { |f| f[:name].to_s == params[:field] }
|
439
|
+
|
440
|
+
Rails.logger.debug "Suggest field_config for #{params[:field]}: #{field_config}"
|
441
|
+
|
442
|
+
unless field_config && field_config[:suggest]
|
443
|
+
render json: { error: "Field not found or suggest not configured" }, status: :not_found
|
444
|
+
return
|
445
|
+
end
|
446
|
+
|
447
|
+
search_term = params[:q].to_s.strip
|
448
|
+
limit = field_config[:suggest][:limit] || 10
|
449
|
+
|
450
|
+
results = get_suggest_options(field_config, search_term, limit)
|
451
|
+
|
452
|
+
render json: { options: results }
|
453
|
+
end
|
454
|
+
|
455
|
+
private
|
456
|
+
|
457
|
+
def load_resource_class
|
458
|
+
resource_name = params[:resource_name]
|
459
|
+
@resource_class = EasyAdmin::ResourceRegistry.find_resource(resource_name)
|
460
|
+
|
461
|
+
unless @resource_class
|
462
|
+
raise ActionController::RoutingError, "Resource '#{resource_name}' not found"
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
def load_record
|
467
|
+
@record = @resource_class.find_record(params[:id])
|
468
|
+
end
|
469
|
+
|
470
|
+
def record_params
|
471
|
+
permitted_attrs = get_permitted_attributes(@resource_class)
|
472
|
+
Rails.logger.debug "Permitted attributes: #{permitted_attrs}"
|
473
|
+
result = params.require(@resource_class.param_key).permit(*permitted_attrs)
|
474
|
+
Rails.logger.debug "Record params after permit: #{result}"
|
475
|
+
result
|
476
|
+
end
|
477
|
+
|
478
|
+
def determine_current_scope
|
479
|
+
if params[:scope].present?
|
480
|
+
scope_config = @resource_class.scopes.find { |s| s[:name].to_s == params[:scope].to_s }
|
481
|
+
scope_config ? scope_config[:name] : @resource_class.default_scope&.dig(:name)
|
482
|
+
else
|
483
|
+
@resource_class.default_scope&.dig(:name)
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
def apply_scope(records)
|
488
|
+
return records unless @resource_class.has_scopes? && @current_scope
|
489
|
+
|
490
|
+
scope_config = @resource_class.scopes.find { |s| s[:name] == @current_scope }
|
491
|
+
|
492
|
+
if scope_config && scope_config[:name] != :all
|
493
|
+
scope_method = scope_config[:scope_method]
|
494
|
+
if records.respond_to?(scope_method)
|
495
|
+
records.public_send(scope_method)
|
496
|
+
else
|
497
|
+
records
|
498
|
+
end
|
499
|
+
else
|
500
|
+
records
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
def calculate_scope_counts
|
505
|
+
counts = {}
|
506
|
+
|
507
|
+
@resource_class.scopes.each do |scope_config|
|
508
|
+
scope_name = scope_config[:name]
|
509
|
+
|
510
|
+
if scope_name == :all
|
511
|
+
counts[scope_name] = @resource_class.model_class.count
|
512
|
+
else
|
513
|
+
scope_method = scope_config[:scope_method]
|
514
|
+
if @resource_class.model_class.respond_to?(scope_method)
|
515
|
+
counts[scope_name] = @resource_class.model_class.public_send(scope_method).count
|
516
|
+
else
|
517
|
+
counts[scope_name] = 0
|
518
|
+
end
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
counts
|
523
|
+
end
|
524
|
+
|
525
|
+
def apply_period_filter(records)
|
526
|
+
return records unless params[:period].present? && records.respond_to?(:where)
|
527
|
+
|
528
|
+
date_range = case params[:period]
|
529
|
+
when 'today'
|
530
|
+
Date.current.beginning_of_day..Date.current.end_of_day
|
531
|
+
when '7d'
|
532
|
+
7.days.ago.beginning_of_day..Date.current.end_of_day
|
533
|
+
when '30d'
|
534
|
+
30.days.ago.beginning_of_day..Date.current.end_of_day
|
535
|
+
when '90d'
|
536
|
+
90.days.ago.beginning_of_day..Date.current.end_of_day
|
537
|
+
when '1y'
|
538
|
+
1.year.ago.beginning_of_day..Date.current.end_of_day
|
539
|
+
else
|
540
|
+
return records
|
541
|
+
end
|
542
|
+
|
543
|
+
# Try to filter by created_at, updated_at, or the first timestamp field
|
544
|
+
timestamp_field = determine_timestamp_field(records)
|
545
|
+
return records unless timestamp_field
|
546
|
+
|
547
|
+
records.where(timestamp_field => date_range)
|
548
|
+
end
|
549
|
+
|
550
|
+
def find_field_config(field_name)
|
551
|
+
@resource_class.fields_config.find { |field| field[:name].to_s == field_name.to_s }
|
552
|
+
end
|
553
|
+
|
554
|
+
def find_resource_class_for_model(model_class)
|
555
|
+
# Try to find the resource class by convention (e.g., User -> UserResource)
|
556
|
+
resource_class_name = "#{model_class.name}Resource"
|
557
|
+
begin
|
558
|
+
resource_class_name.constantize
|
559
|
+
rescue NameError
|
560
|
+
# If convention doesn't work, search through all resource classes
|
561
|
+
EasyAdmin.resource_registry.values.find { |rc| rc.model_class == model_class }
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
def get_permitted_attributes(resource_class)
|
566
|
+
# Get all editable field names from the resource class
|
567
|
+
permitted_attrs = []
|
568
|
+
|
569
|
+
resource_class.form_fields.each do |field_config|
|
570
|
+
next if field_config[:readonly]
|
571
|
+
|
572
|
+
field_name = field_config[:name]
|
573
|
+
|
574
|
+
# Handle different field types that might need special parameter handling
|
575
|
+
case field_config[:type]
|
576
|
+
when :has_many
|
577
|
+
# For has_many relationships, we need to permit an array of IDs
|
578
|
+
permitted_attrs << { "#{field_name.to_s.singularize}_ids" => [] }
|
579
|
+
when :belongs_to
|
580
|
+
# For belongs_to, we need the foreign key
|
581
|
+
if field_config[:multiple]
|
582
|
+
permitted_attrs << { "#{field_name}_ids" => [] }
|
583
|
+
else
|
584
|
+
foreign_key = get_foreign_key_name(field_name)
|
585
|
+
permitted_attrs << foreign_key.to_sym
|
586
|
+
end
|
587
|
+
when :select
|
588
|
+
# For select fields with multiple option
|
589
|
+
if field_config[:multiple]
|
590
|
+
permitted_attrs << { field_name => [] }
|
591
|
+
else
|
592
|
+
permitted_attrs << field_name
|
593
|
+
end
|
594
|
+
else
|
595
|
+
# For regular fields
|
596
|
+
permitted_attrs << field_name
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
permitted_attrs
|
601
|
+
end
|
602
|
+
|
603
|
+
def prepare_belongs_to_as_select(field_config)
|
604
|
+
# Convert belongs_to field to select field for reattaching
|
605
|
+
association_name = field_config[:name]
|
606
|
+
foreign_key = get_foreign_key_name(association_name)
|
607
|
+
|
608
|
+
# Get available options for the select
|
609
|
+
options = get_belongs_to_options(association_name, field_config)
|
610
|
+
|
611
|
+
# Return field config formatted as a select field, preserving original info
|
612
|
+
field_config.merge(
|
613
|
+
type: :select,
|
614
|
+
name: foreign_key, # Use foreign key instead of association name
|
615
|
+
label: "Select #{field_config[:label]}",
|
616
|
+
options: options,
|
617
|
+
original_type: :belongs_to,
|
618
|
+
original_name: association_name
|
619
|
+
)
|
620
|
+
end
|
621
|
+
|
622
|
+
def get_foreign_key_name(association_name)
|
623
|
+
model_class = @resource_class.model_class
|
624
|
+
if model_class.reflect_on_association(association_name)
|
625
|
+
model_class.reflect_on_association(association_name).foreign_key
|
626
|
+
else
|
627
|
+
"#{association_name}_id"
|
628
|
+
end
|
629
|
+
end
|
630
|
+
|
631
|
+
def get_belongs_to_options(association_name, field_config)
|
632
|
+
model_class = @resource_class.model_class
|
633
|
+
|
634
|
+
# Get the associated model class
|
635
|
+
if model_class.reflect_on_association(association_name)
|
636
|
+
association_class = model_class.reflect_on_association(association_name).klass
|
637
|
+
else
|
638
|
+
association_class = association_name.to_s.classify.constantize rescue nil
|
639
|
+
end
|
640
|
+
|
641
|
+
return [] unless association_class
|
642
|
+
|
643
|
+
# Get display method from field config
|
644
|
+
display_method = field_config[:display_method] || :name
|
645
|
+
|
646
|
+
# Get all records and format as [label, value] pairs
|
647
|
+
association_class.all.limit(100).map do |record|
|
648
|
+
label = if record.respond_to?(display_method)
|
649
|
+
record.public_send(display_method)
|
650
|
+
elsif record.respond_to?(:name)
|
651
|
+
record.name
|
652
|
+
elsif record.respond_to?(:title)
|
653
|
+
record.title
|
654
|
+
else
|
655
|
+
record.to_s
|
656
|
+
end
|
657
|
+
[label, record.id]
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
def get_suggest_options(field_config, search_term, limit)
|
662
|
+
# Handle different field types that support suggest
|
663
|
+
case field_config[:type]
|
664
|
+
when :belongs_to
|
665
|
+
get_belongs_to_suggest_options(field_config[:name], field_config, search_term, limit)
|
666
|
+
when :has_many
|
667
|
+
get_has_many_suggest_options(field_config[:name], field_config, search_term, limit)
|
668
|
+
when :select
|
669
|
+
# For regular select fields, filter static options
|
670
|
+
get_static_suggest_options(field_config, search_term, limit)
|
671
|
+
else
|
672
|
+
[]
|
673
|
+
end
|
674
|
+
end
|
675
|
+
|
676
|
+
def get_belongs_to_suggest_options(association_name, field_config, search_term, limit)
|
677
|
+
model_class = @resource_class.model_class
|
678
|
+
|
679
|
+
# Get the associated model class
|
680
|
+
if model_class.reflect_on_association(association_name)
|
681
|
+
association_class = model_class.reflect_on_association(association_name).klass
|
682
|
+
else
|
683
|
+
association_class = association_name.to_s.classify.constantize rescue nil
|
684
|
+
end
|
685
|
+
|
686
|
+
return [] unless association_class
|
687
|
+
|
688
|
+
# Get display method and search fields from field config
|
689
|
+
display_method = field_config[:display_method] || :name
|
690
|
+
suggest_config = field_config[:suggest] || {}
|
691
|
+
search_fields = suggest_config[:search_fields] || [display_method]
|
692
|
+
|
693
|
+
# Build search query
|
694
|
+
records = association_class.all
|
695
|
+
|
696
|
+
if search_term.present?
|
697
|
+
# Build WHERE conditions for search across multiple fields
|
698
|
+
conditions = search_fields.map do |field|
|
699
|
+
if association_class.columns_hash[field.to_s]&.type == :string
|
700
|
+
"#{field} LIKE ?"
|
701
|
+
else
|
702
|
+
"CAST(#{field} AS TEXT) LIKE ?"
|
703
|
+
end
|
704
|
+
end.join(" OR ")
|
705
|
+
|
706
|
+
search_pattern = "%#{search_term}%"
|
707
|
+
records = records.where(conditions, *([search_pattern] * search_fields.length))
|
708
|
+
end
|
709
|
+
|
710
|
+
# Apply limit and format as [label, value] pairs
|
711
|
+
records.limit(limit).map do |record|
|
712
|
+
label = if record.respond_to?(display_method)
|
713
|
+
record.public_send(display_method)
|
714
|
+
elsif record.respond_to?(:name)
|
715
|
+
record.name
|
716
|
+
elsif record.respond_to?(:title)
|
717
|
+
record.title
|
718
|
+
else
|
719
|
+
record.to_s
|
720
|
+
end
|
721
|
+
[label, record.id]
|
722
|
+
end
|
723
|
+
end
|
724
|
+
|
725
|
+
def get_has_many_suggest_options(association_name, field_config, search_term, limit)
|
726
|
+
model_class = @resource_class.model_class
|
727
|
+
|
728
|
+
# Get the associated model class
|
729
|
+
if model_class.reflect_on_association(association_name)
|
730
|
+
association_class = model_class.reflect_on_association(association_name).klass
|
731
|
+
else
|
732
|
+
association_class = association_name.to_s.singularize.classify.constantize rescue nil
|
733
|
+
end
|
734
|
+
|
735
|
+
return [] unless association_class
|
736
|
+
|
737
|
+
# Get display method and search fields from field config
|
738
|
+
display_method = field_config[:display_method] || :name
|
739
|
+
suggest_config = field_config[:suggest] || {}
|
740
|
+
search_fields = suggest_config[:search_fields] || [display_method]
|
741
|
+
|
742
|
+
Rails.logger.debug "HasMany suggest: association=#{association_name}, display_method=#{display_method}, search_fields=#{search_fields}"
|
743
|
+
|
744
|
+
# Build search query
|
745
|
+
records = association_class.all
|
746
|
+
|
747
|
+
if search_term.present?
|
748
|
+
# Build WHERE conditions for search across multiple fields
|
749
|
+
conditions = search_fields.map do |field|
|
750
|
+
if association_class.columns_hash[field.to_s]&.type == :string
|
751
|
+
"#{field} LIKE ?"
|
752
|
+
else
|
753
|
+
"CAST(#{field} AS TEXT) LIKE ?"
|
754
|
+
end
|
755
|
+
end.join(" OR ")
|
756
|
+
|
757
|
+
search_pattern = "%#{search_term}%"
|
758
|
+
records = records.where(conditions, *([search_pattern] * search_fields.length))
|
759
|
+
end
|
760
|
+
|
761
|
+
# Apply limit and format as [label, value] pairs
|
762
|
+
results = records.limit(limit).map do |record|
|
763
|
+
label = if record.respond_to?(display_method)
|
764
|
+
raw_label = record.public_send(display_method)
|
765
|
+
Rails.logger.debug "HasMany record #{record.id}: raw_label=#{raw_label.class}:#{raw_label}"
|
766
|
+
raw_label.to_s
|
767
|
+
elsif record.respond_to?(:name)
|
768
|
+
record.name.to_s
|
769
|
+
elsif record.respond_to?(:title)
|
770
|
+
record.title.to_s
|
771
|
+
else
|
772
|
+
record.to_s
|
773
|
+
end
|
774
|
+
[label, record.id]
|
775
|
+
end
|
776
|
+
|
777
|
+
Rails.logger.debug "HasMany suggest final results: #{results}"
|
778
|
+
results
|
779
|
+
end
|
780
|
+
|
781
|
+
def get_static_suggest_options(field_config, search_term, limit)
|
782
|
+
options = field_config[:options] || []
|
783
|
+
|
784
|
+
if search_term.present?
|
785
|
+
# Filter options based on search term
|
786
|
+
filtered_options = options.select do |option|
|
787
|
+
text = option.is_a?(Array) ? option[0] : option.to_s
|
788
|
+
text.downcase.include?(search_term.downcase)
|
789
|
+
end
|
790
|
+
filtered_options.take(limit)
|
791
|
+
else
|
792
|
+
options.take(limit)
|
793
|
+
end
|
794
|
+
end
|
795
|
+
|
796
|
+
def determine_timestamp_field(records)
|
797
|
+
# Get the model class from the relation or class
|
798
|
+
model_class = if records.is_a?(Class) && records < ActiveRecord::Base
|
799
|
+
# If records is already an ActiveRecord model class
|
800
|
+
records
|
801
|
+
elsif records.respond_to?(:klass)
|
802
|
+
# For ActiveRecord relations
|
803
|
+
records.klass
|
804
|
+
elsif records.respond_to?(:model)
|
805
|
+
records.model
|
806
|
+
else
|
807
|
+
records.class
|
808
|
+
end
|
809
|
+
|
810
|
+
# Try common timestamp fields in order of preference
|
811
|
+
%w[created_at updated_at published_at date timestamp].find do |field|
|
812
|
+
model_class.column_names.include?(field)
|
813
|
+
end
|
814
|
+
end
|
815
|
+
|
816
|
+
# Helper methods for lazy loading
|
817
|
+
def find_metric_config(metric_id)
|
818
|
+
find_element_in_layout(@resource_class.show_layout, :metric_card) do |element|
|
819
|
+
element.object_id.to_s == metric_id
|
820
|
+
end
|
821
|
+
end
|
822
|
+
|
823
|
+
def find_chart_config(chart_id)
|
824
|
+
find_element_in_layout(@resource_class.show_layout, :chart_card) do |element|
|
825
|
+
element.object_id.to_s == chart_id
|
826
|
+
end
|
827
|
+
end
|
828
|
+
|
829
|
+
def find_tab_config(tab_name)
|
830
|
+
tabs_container = find_element_in_layout(@resource_class.show_layout, :tabs)
|
831
|
+
tabs_container[:tabs].find { |tab| tab[:name].downcase == tab_name }
|
832
|
+
end
|
833
|
+
|
834
|
+
def find_element_in_layout(layout, element_type, &block)
|
835
|
+
layout.each do |row|
|
836
|
+
next unless row[:type] == :row
|
837
|
+
|
838
|
+
row[:columns].each do |column|
|
839
|
+
column[:elements].each do |element|
|
840
|
+
case element[:type]
|
841
|
+
when element_type
|
842
|
+
return element if block.nil? || block.call(element)
|
843
|
+
when :card
|
844
|
+
result = find_element_in_card(element, element_type, &block)
|
845
|
+
return result if result
|
846
|
+
when :tabs
|
847
|
+
result = find_element_in_tabs(element, element_type, &block)
|
848
|
+
return result if result
|
849
|
+
end
|
850
|
+
end
|
851
|
+
end
|
852
|
+
end
|
853
|
+
nil
|
854
|
+
end
|
855
|
+
|
856
|
+
def find_element_in_card(card, element_type, &block)
|
857
|
+
card[:elements].each do |element|
|
858
|
+
case element[:type]
|
859
|
+
when element_type
|
860
|
+
return element if block.nil? || block.call(element)
|
861
|
+
when :tabs
|
862
|
+
result = find_element_in_tabs(element, element_type, &block)
|
863
|
+
return result if result
|
864
|
+
end
|
865
|
+
end
|
866
|
+
nil
|
867
|
+
end
|
868
|
+
|
869
|
+
def find_element_in_tabs(tabs, element_type, &block)
|
870
|
+
tabs[:tabs].each do |tab|
|
871
|
+
tab[:elements].each do |element|
|
872
|
+
return element if element[:type] == element_type && (block.nil? || block.call(element))
|
873
|
+
end
|
874
|
+
end
|
875
|
+
nil
|
876
|
+
end
|
877
|
+
|
878
|
+
def render_tab_elements(elements)
|
879
|
+
# This would render the tab elements - for now return a simple div
|
880
|
+
# In a real implementation, this would use the ShowLayoutComponent logic
|
881
|
+
content_tag(:div, class: "tab-content-loaded") do
|
882
|
+
"Tab content loaded"
|
883
|
+
end
|
884
|
+
end
|
885
|
+
|
886
|
+
def apply_sorting(records, sort_field, sort_direction)
|
887
|
+
return records unless sort_field.present?
|
888
|
+
|
889
|
+
# Get the model class from the relation
|
890
|
+
model_class = records.respond_to?(:klass) ? records.klass : records.class
|
891
|
+
|
892
|
+
if records.respond_to?(:order) && model_class.column_names.include?(sort_field.to_s)
|
893
|
+
records.order("#{sort_field} #{sort_direction}")
|
894
|
+
else
|
895
|
+
records
|
896
|
+
end
|
897
|
+
end
|
898
|
+
|
899
|
+
def has_active_filters?
|
900
|
+
# Check if any filtering parameters are present
|
901
|
+
params[:q].present? ||
|
902
|
+
params[:search].present? ||
|
903
|
+
params[:period].present? ||
|
904
|
+
(params[:scope].present? && params[:scope] != determine_current_scope[:scope])
|
905
|
+
end
|
906
|
+
end
|
907
|
+
end
|