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,192 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Show
|
4
|
+
class HasManyComponent < BaseComponent
|
5
|
+
private
|
6
|
+
|
7
|
+
def view_template
|
8
|
+
div(class: "field-display ea-has-many-field") do
|
9
|
+
# Only show label if show_label is not explicitly false
|
10
|
+
if field.fetch(:show_label, true)
|
11
|
+
div(class: "field-label font-medium text-sm text-gray-700 mb-3") { plain field_label }
|
12
|
+
end
|
13
|
+
|
14
|
+
div(class: "field-value") do
|
15
|
+
render_has_many_table
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def render_has_many_table
|
21
|
+
if value.respond_to?(:each) && value.any?
|
22
|
+
div(class: "overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg") do
|
23
|
+
table(class: "table-striped min-w-full divide-y divide-gray-300") do
|
24
|
+
thead(class: "bg-gray-50") do
|
25
|
+
tr do
|
26
|
+
association_fields.each do |field|
|
27
|
+
th(scope: "col", class: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide") do
|
28
|
+
plain field[:label]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
th(scope: "col", class: "relative px-6 py-3") do
|
32
|
+
span(class: "sr-only") { "Actions" }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
tbody(class: "divide-y divide-gray-200 bg-white") do
|
37
|
+
records_to_show.each do |item|
|
38
|
+
tr(class: "hover:bg-gray-100 transition-colors duration-150") do
|
39
|
+
association_fields.each do |field|
|
40
|
+
td(class: "whitespace-nowrap px-6 py-4 text-sm") do
|
41
|
+
div(class: "text-gray-900 font-medium") do
|
42
|
+
field_value = item.public_send(field[:name])
|
43
|
+
plain format_association_value(field_value, field[:type])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
td(class: "relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6") do
|
48
|
+
div(class: "flex items-center justify-end space-x-2") do
|
49
|
+
a(href: view_path_for(item),
|
50
|
+
class: "inline-flex items-center justify-center w-8 h-8 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors duration-150",
|
51
|
+
title: "View") do
|
52
|
+
unsafe_raw %(<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
53
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
54
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
55
|
+
</svg>)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if value.count > 5
|
66
|
+
div(class: "text-sm text-gray-500 mt-3 text-center") do
|
67
|
+
"Showing #{records_to_show.count} of #{value.count} #{field_label.downcase}."
|
68
|
+
end
|
69
|
+
end
|
70
|
+
else
|
71
|
+
div(class: "text-center py-8 text-gray-500") do
|
72
|
+
div(class: "text-lg") { "📄" }
|
73
|
+
div(class: "mt-2 text-sm") { "No #{field_label.downcase} found." }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def render_skeleton_loader
|
79
|
+
div(class: "ea-has-many-skeleton") do
|
80
|
+
skeleton_count.times do
|
81
|
+
div(class: "ea-skeleton-item") do
|
82
|
+
div(class: "ea-skeleton-line ea-skeleton-line--title")
|
83
|
+
div(class: "ea-skeleton-line ea-skeleton-line--subtitle")
|
84
|
+
div(class: "ea-skeleton-actions") do
|
85
|
+
3.times do
|
86
|
+
div(class: "ea-skeleton-button")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def turbo_frame_id
|
95
|
+
"has_many_#{field_name}_#{record.id}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def lazy_load_url
|
99
|
+
# TODO: Create proper route helper for has_many lazy loading
|
100
|
+
# For now, construct URL dynamically based on engine mount point
|
101
|
+
"#{EasyAdmin::Engine.routes.url_helpers.resources_path(resource_name: resource_route_key).chomp('/')}/#{record.id}/has_many/#{field_name}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def resource_route_key
|
105
|
+
record.class.name.underscore.pluralize
|
106
|
+
end
|
107
|
+
|
108
|
+
def skeleton_count
|
109
|
+
field[:skeleton_count] || 3
|
110
|
+
end
|
111
|
+
|
112
|
+
def association_fields
|
113
|
+
@association_fields ||= determine_association_fields
|
114
|
+
end
|
115
|
+
|
116
|
+
def determine_association_fields
|
117
|
+
return field[:fields] if field[:fields]
|
118
|
+
|
119
|
+
# Get the association class
|
120
|
+
association_class = determine_association_class
|
121
|
+
return [] unless association_class
|
122
|
+
|
123
|
+
# Default fields - try to get the most common ones
|
124
|
+
fields = []
|
125
|
+
|
126
|
+
# Add ID field
|
127
|
+
fields << { name: :id, label: "ID", type: :number }
|
128
|
+
|
129
|
+
# Try common display fields
|
130
|
+
if association_class.column_names.include?('name')
|
131
|
+
fields << { name: :name, label: "Name", type: :text }
|
132
|
+
elsif association_class.column_names.include?('title')
|
133
|
+
fields << { name: :title, label: "Title", type: :text }
|
134
|
+
end
|
135
|
+
|
136
|
+
# Add timestamps if available
|
137
|
+
if association_class.column_names.include?('created_at')
|
138
|
+
fields << { name: :created_at, label: "Created", type: :datetime }
|
139
|
+
end
|
140
|
+
|
141
|
+
fields.take(3) # Limit to 3 fields for readability
|
142
|
+
end
|
143
|
+
|
144
|
+
def determine_association_class
|
145
|
+
return field[:association] if field[:association]
|
146
|
+
|
147
|
+
# Try to determine from the field name or value
|
148
|
+
if value.respond_to?(:first) && value.first
|
149
|
+
value.first.class
|
150
|
+
elsif field_name.to_s.end_with?('s')
|
151
|
+
# Pluralized association name, try to singularize and classify
|
152
|
+
field_name.to_s.singularize.classify.constantize
|
153
|
+
else
|
154
|
+
field_name.to_s.classify.constantize
|
155
|
+
end
|
156
|
+
rescue NameError
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
|
160
|
+
def records_to_show
|
161
|
+
@records_to_show ||= value.limit(5)
|
162
|
+
end
|
163
|
+
|
164
|
+
def format_association_value(val, type)
|
165
|
+
return "—" if val.blank?
|
166
|
+
|
167
|
+
case type
|
168
|
+
when :datetime
|
169
|
+
val.respond_to?(:strftime) ? val.strftime('%b %d, %Y at %I:%M %p') : val.to_s
|
170
|
+
when :date
|
171
|
+
val.respond_to?(:strftime) ? val.strftime('%b %d, %Y') : val.to_s
|
172
|
+
when :boolean
|
173
|
+
val ? "Yes" : "No"
|
174
|
+
else
|
175
|
+
val.to_s
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def view_path_for(record)
|
180
|
+
return field[:view_path_proc].call(record) if field[:view_path_proc]
|
181
|
+
|
182
|
+
association_class = record.class
|
183
|
+
EasyAdmin::Engine.routes.url_helpers.resource_path(
|
184
|
+
resource_name: association_class.model_name.route_key,
|
185
|
+
id: record.id
|
186
|
+
)
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Show
|
4
|
+
class JsonComponent < Fields::BaseComponent
|
5
|
+
def view_template
|
6
|
+
div(class: "space-y-2") do
|
7
|
+
div(class: "text-sm font-medium text-gray-700") { field_label }
|
8
|
+
div(class: "bg-white border rounded-lg overflow-hidden") do
|
9
|
+
if json_value.present?
|
10
|
+
div(
|
11
|
+
data: {
|
12
|
+
controller: "jsoneditor",
|
13
|
+
jsoneditor_mode_value: "view",
|
14
|
+
jsoneditor_data_value: parsed_json_data.to_json
|
15
|
+
},
|
16
|
+
style: "height: 300px;"
|
17
|
+
)
|
18
|
+
else
|
19
|
+
div(class: "p-4 text-center text-gray-400 italic") { "No data" }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def json_value
|
28
|
+
@json_value ||= value
|
29
|
+
end
|
30
|
+
|
31
|
+
def parsed_json_data
|
32
|
+
if json_value.is_a?(String)
|
33
|
+
begin
|
34
|
+
JSON.parse(json_value)
|
35
|
+
rescue JSON::ParserError
|
36
|
+
{ error: "Invalid JSON", raw_value: json_value }
|
37
|
+
end
|
38
|
+
else
|
39
|
+
json_value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Show
|
4
|
+
class NumberComponent < BaseComponent
|
5
|
+
def view_template
|
6
|
+
div(class: "ea-show-value ea-show-number") do
|
7
|
+
format_value
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def format_value
|
14
|
+
return "" if value.blank?
|
15
|
+
value.to_s
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Show
|
4
|
+
class SelectComponent < BaseComponent
|
5
|
+
def view_template
|
6
|
+
div(class: "ea-show-value ea-show-select") do
|
7
|
+
if value.present?
|
8
|
+
if value.is_a?(Array)
|
9
|
+
div(class: "ea-select-multiple") do
|
10
|
+
value.each do |item|
|
11
|
+
span(class: "ea-select-tag") { plain item.to_s }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
else
|
15
|
+
plain value.to_s
|
16
|
+
end
|
17
|
+
else
|
18
|
+
span(class: "ea-empty-value") { plain "—" }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Show
|
4
|
+
class TextComponent < BaseComponent
|
5
|
+
def view_template
|
6
|
+
div(class: "ea-show-value") do
|
7
|
+
if value.present?
|
8
|
+
value.to_s
|
9
|
+
else
|
10
|
+
span(class: "ea-empty-value") { "—" }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Show
|
4
|
+
class TextareaComponent < BaseComponent
|
5
|
+
def view_template
|
6
|
+
div(class: "ea-show-value") do
|
7
|
+
if value.present?
|
8
|
+
div(class: "ea-text-content") do
|
9
|
+
simple_format(value)
|
10
|
+
end
|
11
|
+
else
|
12
|
+
span(class: "ea-empty-value") { "—" }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def simple_format(text)
|
20
|
+
# Simple implementation of Rails' simple_format helper
|
21
|
+
text.to_s.gsub(/\r\n?/, "\n").gsub(/\n/, "<br>").html_safe
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
class FiltersComponent < Phlex::HTML
|
3
|
+
def initialize(resource_class:, current_params: {}, search_params: {})
|
4
|
+
@resource_class = resource_class
|
5
|
+
@current_params = current_params
|
6
|
+
@search_params = search_params || {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def view_template
|
10
|
+
return unless @resource_class.filterable_fields.any?
|
11
|
+
|
12
|
+
div(class: "bg-white shadow rounded-lg mb-6") do
|
13
|
+
form(
|
14
|
+
method: "get",
|
15
|
+
action: route_helper.resources_path(@resource_class.route_key),
|
16
|
+
data: { turbo_frame: "table-frame" },
|
17
|
+
class: "space-y-4"
|
18
|
+
) do
|
19
|
+
# Preserve existing params (except search params)
|
20
|
+
preserve_params
|
21
|
+
|
22
|
+
div(class: "p-6 border-b border-gray-200") do
|
23
|
+
div(class: "flex items-center justify-between mb-4") do
|
24
|
+
div do
|
25
|
+
h3(class: "text-lg font-medium text-gray-900") { "Filters" }
|
26
|
+
p(class: "text-sm text-gray-500") { "Filter #{@resource_class.title.downcase} by specific criteria" }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Clear filters button
|
30
|
+
if has_active_filters?
|
31
|
+
a(
|
32
|
+
href: build_clear_filters_url,
|
33
|
+
class: "text-sm text-red-600 hover:text-red-800 font-medium",
|
34
|
+
data: { turbo_frame: "table-frame" }
|
35
|
+
) { "Clear all filters" }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
div(class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4") do
|
40
|
+
@resource_class.filterable_fields.each do |field|
|
41
|
+
render_filter_field(field)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
div(class: "px-6 py-4 bg-gray-50") do
|
47
|
+
div(class: "flex items-center justify-end space-x-3") do
|
48
|
+
button(
|
49
|
+
type: "submit",
|
50
|
+
class: "inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
51
|
+
) do
|
52
|
+
unsafe_raw '<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
53
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707v6.586l-4-4V9.414a1 1 0 00-.293-.707L3.293 6.293A1 1 0 013 5.586V4z"></path>
|
54
|
+
</svg>'
|
55
|
+
span { "Apply Filters" }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def route_helper
|
66
|
+
@route_helper ||= EasyAdmin::Engine.routes.url_helpers
|
67
|
+
end
|
68
|
+
|
69
|
+
def preserve_params
|
70
|
+
# Preserve non-filter params (scope, sort, direction, period, page)
|
71
|
+
preserved_params = @current_params.except(:q)
|
72
|
+
preserved_params.each do |key, value|
|
73
|
+
input(type: "hidden", name: key, value: value)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def render_filter_field(field)
|
78
|
+
component_class = filter_component_for_field(field)
|
79
|
+
render component_class.new(field: field, search_params: @search_params)
|
80
|
+
end
|
81
|
+
|
82
|
+
def filter_component_for_field(field)
|
83
|
+
case field[:type]
|
84
|
+
when :string, :email, :text
|
85
|
+
EasyAdmin::Fields::Index::Filters::StringComponent
|
86
|
+
when :boolean
|
87
|
+
EasyAdmin::Fields::Index::Filters::BooleanComponent
|
88
|
+
when :select, :belongs_to
|
89
|
+
EasyAdmin::Fields::Index::Filters::SelectComponent
|
90
|
+
when :number, :integer
|
91
|
+
EasyAdmin::Fields::Index::Filters::NumberComponent
|
92
|
+
when :date, :datetime
|
93
|
+
EasyAdmin::Fields::Index::Filters::DateComponent
|
94
|
+
else
|
95
|
+
EasyAdmin::Fields::Index::Filters::StringComponent
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def has_active_filters?
|
100
|
+
return false unless @search_params.present?
|
101
|
+
|
102
|
+
@search_params.each do |key, value|
|
103
|
+
return true if value.present?
|
104
|
+
end
|
105
|
+
|
106
|
+
false
|
107
|
+
end
|
108
|
+
|
109
|
+
def build_clear_filters_url
|
110
|
+
base_url = route_helper.resources_path(@resource_class.route_key)
|
111
|
+
clear_params = @current_params.except(:q)
|
112
|
+
|
113
|
+
if clear_params.present? && !clear_params.empty?
|
114
|
+
"#{base_url}?#{clear_params.to_query}"
|
115
|
+
else
|
116
|
+
base_url
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
class FormTabsComponent < Phlex::HTML
|
3
|
+
include EasyAdmin::FieldsHelper
|
4
|
+
|
5
|
+
def initialize(resource_class:, form:)
|
6
|
+
@resource_class = resource_class
|
7
|
+
@form = form
|
8
|
+
end
|
9
|
+
|
10
|
+
def view_template
|
11
|
+
div(
|
12
|
+
class: "form-tabs",
|
13
|
+
data: {
|
14
|
+
controller: "form-tabs",
|
15
|
+
action: "tab:selected@window->form-tabs#handleTabSelection"
|
16
|
+
}
|
17
|
+
) do
|
18
|
+
render_tab_navigation
|
19
|
+
render_tab_content
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def render_tab_navigation
|
26
|
+
nav(class: "border-b border-gray-200 mb-4 sm:mb-6") do
|
27
|
+
# Mobile dropdown for tabs
|
28
|
+
div(class: "sm:hidden relative", data: { controller: "dropdown" }) do
|
29
|
+
button(
|
30
|
+
type: "button",
|
31
|
+
class: "flex items-center justify-between w-full px-3 py-2 text-base border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-blue-500 focus:border-blue-500",
|
32
|
+
data: {
|
33
|
+
action: "click->dropdown#toggle",
|
34
|
+
dropdown_target: "trigger",
|
35
|
+
form_tabs_target: "mobileButton"
|
36
|
+
}
|
37
|
+
) do
|
38
|
+
span(data: { form_tabs_target: "mobileButtonText" }) { @resource_class.form_tabs.first[:label] }
|
39
|
+
unsafe_raw('<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>')
|
40
|
+
end
|
41
|
+
|
42
|
+
# Dropdown menu
|
43
|
+
div(
|
44
|
+
class: "absolute right-0 mt-2 w-full bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-10 hidden",
|
45
|
+
data: { dropdown_target: "menu" }
|
46
|
+
) do
|
47
|
+
div(class: "py-1") do
|
48
|
+
@resource_class.form_tabs.each do |tab|
|
49
|
+
button(
|
50
|
+
type: "button",
|
51
|
+
class: "block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900",
|
52
|
+
data: {
|
53
|
+
action: "click->dropdown#close click->form-tabs#emitTabSelection",
|
54
|
+
tab_id: tab[:name].downcase.gsub(/\s+/, '-'),
|
55
|
+
tab_label: tab[:label]
|
56
|
+
}
|
57
|
+
) do
|
58
|
+
tab[:label]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Desktop tabs
|
66
|
+
ul(class: "hidden sm:flex flex-wrap -mb-px text-sm font-medium text-center", role: "tablist") do
|
67
|
+
@resource_class.form_tabs.each_with_index do |tab, index|
|
68
|
+
render_tab_button(tab, index)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def render_tab_button(tab, index)
|
75
|
+
li(class: "mr-2", role: "presentation") do
|
76
|
+
button(
|
77
|
+
class: tab_button_classes(index == 0),
|
78
|
+
id: "#{tab[:name].downcase.gsub(/\s+/, '-')}-tab",
|
79
|
+
data: {
|
80
|
+
form_tabs_target: "tabButton",
|
81
|
+
tab_id: tab[:name].downcase.gsub(/\s+/, '-'),
|
82
|
+
action: "click->form-tabs#switchTab"
|
83
|
+
},
|
84
|
+
type: "button",
|
85
|
+
role: "tab"
|
86
|
+
) do
|
87
|
+
if tab[:icon]
|
88
|
+
span(class: "inline-flex items-center") do
|
89
|
+
unsafe_raw(tab_icon(tab[:icon]))
|
90
|
+
span(class: "ml-2") { tab[:label] }
|
91
|
+
end
|
92
|
+
else
|
93
|
+
plain tab[:label]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def render_tab_content
|
100
|
+
div(class: "tab-content") do
|
101
|
+
@resource_class.form_tabs.each_with_index do |tab, index|
|
102
|
+
render_tab_panel(tab, index)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def render_tab_panel(tab, index)
|
108
|
+
div(
|
109
|
+
class: tab_panel_classes(index == 0),
|
110
|
+
id: "#{tab[:name].downcase.gsub(/\s+/, '-')}-panel",
|
111
|
+
data: { form_tabs_target: "tabPanel" },
|
112
|
+
role: "tabpanel"
|
113
|
+
) do
|
114
|
+
div(class: "bg-white shadow-sm rounded-lg border border-gray-200") do
|
115
|
+
div(class: "px-4 py-5 sm:p-6") do
|
116
|
+
h3(class: "text-lg font-medium leading-6 text-gray-900 mb-6") do
|
117
|
+
tab[:label]
|
118
|
+
end
|
119
|
+
|
120
|
+
div(class: "grid grid-cols-1 gap-6") do
|
121
|
+
tab[:fields].each do |field|
|
122
|
+
div do
|
123
|
+
unsafe_raw render_field(field, action: :form, form: @form)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def tab_button_classes(active = false)
|
133
|
+
base_classes = "inline-flex items-center p-4 border-b-2 rounded-t-lg transition-colors duration-200"
|
134
|
+
|
135
|
+
if active
|
136
|
+
active_classes = "text-blue-600 border-blue-600"
|
137
|
+
else
|
138
|
+
inactive_classes = "text-gray-500 border-transparent hover:text-gray-600 hover:border-gray-300"
|
139
|
+
end
|
140
|
+
|
141
|
+
"#{base_classes} #{active ? active_classes : inactive_classes}"
|
142
|
+
end
|
143
|
+
|
144
|
+
def tab_panel_classes(active = false)
|
145
|
+
base_classes = "tab-panel"
|
146
|
+
visibility_classes = active ? "block" : "hidden"
|
147
|
+
|
148
|
+
"#{base_classes} #{visibility_classes}"
|
149
|
+
end
|
150
|
+
|
151
|
+
def tab_icon(icon_name)
|
152
|
+
case icon_name.to_sym
|
153
|
+
when :user
|
154
|
+
'<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>'
|
155
|
+
when :security
|
156
|
+
'<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>'
|
157
|
+
when :settings
|
158
|
+
'<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>'
|
159
|
+
when :info
|
160
|
+
'<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
|
161
|
+
else
|
162
|
+
'<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>'
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|