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,806 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
class Resource
|
3
|
+
class_attribute :model_class_name, :resource_name, :fields_config, :form_tabs_config, :show_layout_config, :scopes_config, :pagination_config, :includes_config, :batch_actions_config, :batch_actions_enabled, :row_actions_config
|
4
|
+
|
5
|
+
def self.inherited(subclass)
|
6
|
+
super
|
7
|
+
# Automatically set model class from resource name
|
8
|
+
subclass.resource_name = subclass.name.demodulize.gsub(/Resource$/, '')
|
9
|
+
subclass.model_class_name = subclass.resource_name
|
10
|
+
subclass.fields_config = []
|
11
|
+
subclass.form_tabs_config = []
|
12
|
+
subclass.show_layout_config = []
|
13
|
+
subclass.scopes_config = []
|
14
|
+
subclass.pagination_config = {
|
15
|
+
type: :standard,
|
16
|
+
per_page: 20,
|
17
|
+
max_per_page: 100
|
18
|
+
}
|
19
|
+
subclass.includes_config = {
|
20
|
+
index: [],
|
21
|
+
show: [],
|
22
|
+
form: []
|
23
|
+
}
|
24
|
+
subclass.batch_actions_config = []
|
25
|
+
subclass.batch_actions_enabled = false
|
26
|
+
|
27
|
+
# Register the resource
|
28
|
+
EasyAdmin::ResourceRegistry.register(subclass)
|
29
|
+
end
|
30
|
+
|
31
|
+
# DSL for defining fields
|
32
|
+
def self.field(name, type = :string, **options)
|
33
|
+
self.fields_config += [{
|
34
|
+
name: name,
|
35
|
+
type: type,
|
36
|
+
label: options[:label] || name.to_s.humanize,
|
37
|
+
sortable: options.fetch(:sortable, true),
|
38
|
+
searchable: options.fetch(:searchable, false),
|
39
|
+
filterable: options.fetch(:filterable, false),
|
40
|
+
required: options.fetch(:required, false),
|
41
|
+
readonly: options.fetch(:readonly, false),
|
42
|
+
format: options[:format],
|
43
|
+
options: options[:options], # for select fields
|
44
|
+
multiple: options[:multiple], # for select fields
|
45
|
+
placeholder: options[:placeholder], # for select fields
|
46
|
+
suggest: options[:suggest], # for dynamic option loading
|
47
|
+
display_method: options[:display_method], # for association fields
|
48
|
+
help_text: options[:help_text],
|
49
|
+
editable: options[:editable]
|
50
|
+
}]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Convenience methods for common field types
|
54
|
+
def self.id_field(**options)
|
55
|
+
field(:id, :number, readonly: true, **options)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.text_field(name, **options)
|
59
|
+
field(name, :string, **options)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.textarea_field(name, **options)
|
63
|
+
field(name, :text, **options)
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.number_field(name, **options)
|
67
|
+
field(name, :number, **options)
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.email_field(name, **options)
|
71
|
+
field(name, :email, **options)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.date_field(name, **options)
|
75
|
+
field(name, :date, **options)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.datetime_field(name, **options)
|
79
|
+
field(name, :datetime, **options)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.boolean_field(name, **options)
|
83
|
+
field(name, :boolean, **options)
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.select_field(name, options:, **other_options)
|
87
|
+
field(name, :select, options: options, **other_options)
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.belongs_to_field(name, **options)
|
91
|
+
field(name, :belongs_to, **options)
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.has_many_field(name, **options)
|
95
|
+
field(name, :has_many, **options)
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.file_field(name, **options)
|
99
|
+
field(name, :file, **options)
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.json_field(name, **options)
|
103
|
+
field(name, :json, **options)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Scope definition
|
107
|
+
def self.scope(name, **options)
|
108
|
+
scope_config = {
|
109
|
+
name: name,
|
110
|
+
label: options[:label] || name.to_s.humanize,
|
111
|
+
scope_method: options[:scope] || name,
|
112
|
+
default: options.fetch(:default, false),
|
113
|
+
icon: options[:icon],
|
114
|
+
color: options[:color] || 'blue',
|
115
|
+
count: options.fetch(:show_count, true)
|
116
|
+
}
|
117
|
+
|
118
|
+
self.scopes_config += [scope_config]
|
119
|
+
end
|
120
|
+
|
121
|
+
# Pagination configuration
|
122
|
+
def self.pagination(type: :standard, per_page: 20, **options)
|
123
|
+
self.pagination_config = {
|
124
|
+
type: type,
|
125
|
+
per_page: per_page,
|
126
|
+
max_per_page: options[:max_per_page] || 100,
|
127
|
+
infinite_scroll: options[:infinite_scroll] || false,
|
128
|
+
countless: options[:countless] || false
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
# Configure eager loading for associations
|
133
|
+
def self.includes(associations, actions: [:index, :show, :form])
|
134
|
+
actions = Array(actions)
|
135
|
+
|
136
|
+
actions.each do |action|
|
137
|
+
if self.includes_config.key?(action)
|
138
|
+
self.includes_config = self.includes_config.merge(
|
139
|
+
action => Array(associations)
|
140
|
+
)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Convenience methods for pagination configuration
|
146
|
+
def self.per_page(count)
|
147
|
+
pagination(per_page: count)
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.infinite_scroll(**options)
|
151
|
+
pagination(type: :infinite_scroll, infinite_scroll: true, **options)
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.countless_pagination(**options)
|
155
|
+
pagination(countless: true, **options)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Get the associated model class
|
159
|
+
def self.model_class
|
160
|
+
model_class_name.constantize
|
161
|
+
rescue NameError
|
162
|
+
raise "Model class #{model_class_name} not found. Make sure the model exists."
|
163
|
+
end
|
164
|
+
|
165
|
+
# Get resource title/name
|
166
|
+
def self.title
|
167
|
+
resource_name.humanize.pluralize
|
168
|
+
end
|
169
|
+
|
170
|
+
def self.singular_title
|
171
|
+
resource_name.humanize
|
172
|
+
end
|
173
|
+
|
174
|
+
# Get fields for different contexts
|
175
|
+
def self.index_fields
|
176
|
+
fields_config.select { |field| ![:text, :has_many, :file].include?(field[:type]) }
|
177
|
+
end
|
178
|
+
|
179
|
+
def self.sortable_fields
|
180
|
+
fields_config.select { |field| field[:sortable] }
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.filterable_fields
|
184
|
+
fields_config.select { |field| field[:filterable] }
|
185
|
+
end
|
186
|
+
|
187
|
+
def self.show_fields
|
188
|
+
fields_config
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.form_fields
|
192
|
+
if form_tabs_config.any?
|
193
|
+
# If tabs are configured, return fields from all tabs
|
194
|
+
form_tabs_config.flat_map { |tab| tab[:fields] }.reject { |field| field[:readonly] }
|
195
|
+
else
|
196
|
+
# Default behavior: return all non-readonly fields
|
197
|
+
fields_config.reject { |field| field[:readonly] }
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.form_tabs
|
202
|
+
form_tabs_config
|
203
|
+
end
|
204
|
+
|
205
|
+
def self.has_form_tabs?
|
206
|
+
form_tabs_config.any?
|
207
|
+
end
|
208
|
+
|
209
|
+
# DSL for defining form with tabs
|
210
|
+
def self.form(&block)
|
211
|
+
form_builder = FormBuilder.new(self)
|
212
|
+
form_builder.instance_eval(&block)
|
213
|
+
end
|
214
|
+
|
215
|
+
# DSL for defining show layout
|
216
|
+
def self.show(&block)
|
217
|
+
show_builder = ShowBuilder.new(self)
|
218
|
+
show_builder.instance_eval(&block)
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.show_layout
|
222
|
+
show_layout_config
|
223
|
+
end
|
224
|
+
|
225
|
+
def self.has_show_layout?
|
226
|
+
show_layout_config.any?
|
227
|
+
end
|
228
|
+
|
229
|
+
def self.scopes
|
230
|
+
scopes_config
|
231
|
+
end
|
232
|
+
|
233
|
+
def self.has_scopes?
|
234
|
+
scopes_config.any?
|
235
|
+
end
|
236
|
+
|
237
|
+
def self.default_scope
|
238
|
+
scopes_config.find { |scope| scope[:default] } || scopes_config.first
|
239
|
+
end
|
240
|
+
|
241
|
+
# Pagination helper methods
|
242
|
+
def self.items_per_page
|
243
|
+
pagination_config[:per_page]
|
244
|
+
end
|
245
|
+
|
246
|
+
def self.max_items_per_page
|
247
|
+
pagination_config[:max_per_page]
|
248
|
+
end
|
249
|
+
|
250
|
+
def self.pagination_type
|
251
|
+
pagination_config[:type]
|
252
|
+
end
|
253
|
+
|
254
|
+
def self.infinite_scroll_enabled?
|
255
|
+
pagination_config[:infinite_scroll]
|
256
|
+
end
|
257
|
+
|
258
|
+
def self.countless_enabled?
|
259
|
+
pagination_config[:countless]
|
260
|
+
end
|
261
|
+
|
262
|
+
# Includes configuration helpers
|
263
|
+
def self.index_includes
|
264
|
+
includes_config[:index] || []
|
265
|
+
end
|
266
|
+
|
267
|
+
def self.show_includes
|
268
|
+
includes_config[:show] || []
|
269
|
+
end
|
270
|
+
|
271
|
+
def self.form_includes
|
272
|
+
includes_config[:form] || []
|
273
|
+
end
|
274
|
+
|
275
|
+
class FormBuilder
|
276
|
+
def initialize(resource_class)
|
277
|
+
@resource_class = resource_class
|
278
|
+
@current_tab = nil
|
279
|
+
end
|
280
|
+
|
281
|
+
def tab(name = "General", **options, &block)
|
282
|
+
tab_config = {
|
283
|
+
name: name,
|
284
|
+
label: options[:label] || name,
|
285
|
+
icon: options[:icon],
|
286
|
+
fields: []
|
287
|
+
}
|
288
|
+
|
289
|
+
@current_tab = tab_config
|
290
|
+
@resource_class.form_tabs_config << tab_config
|
291
|
+
|
292
|
+
instance_eval(&block) if block_given?
|
293
|
+
@current_tab = nil
|
294
|
+
end
|
295
|
+
|
296
|
+
# Field definition methods within tabs
|
297
|
+
def field(name, type = :string, **options)
|
298
|
+
field_config = {
|
299
|
+
name: name,
|
300
|
+
type: type,
|
301
|
+
label: options[:label] || name.to_s.humanize,
|
302
|
+
sortable: options.fetch(:sortable, true),
|
303
|
+
searchable: options.fetch(:searchable, false),
|
304
|
+
filterable: options.fetch(:filterable, false),
|
305
|
+
required: options.fetch(:required, false),
|
306
|
+
readonly: options.fetch(:readonly, false),
|
307
|
+
format: options[:format],
|
308
|
+
options: options[:options],
|
309
|
+
multiple: options[:multiple],
|
310
|
+
placeholder: options[:placeholder],
|
311
|
+
help_text: options[:help_text],
|
312
|
+
editable: options[:editable]
|
313
|
+
}
|
314
|
+
|
315
|
+
if @current_tab
|
316
|
+
@current_tab[:fields] << field_config
|
317
|
+
end
|
318
|
+
|
319
|
+
# Also add to main fields_config for compatibility
|
320
|
+
@resource_class.fields_config << field_config unless @resource_class.fields_config.any? { |f| f[:name] == name }
|
321
|
+
end
|
322
|
+
|
323
|
+
def text_field(name, **options)
|
324
|
+
field(name, :string, **options)
|
325
|
+
end
|
326
|
+
|
327
|
+
def textarea_field(name, **options)
|
328
|
+
field(name, :text, **options)
|
329
|
+
end
|
330
|
+
|
331
|
+
def number_field(name, **options)
|
332
|
+
field(name, :number, **options)
|
333
|
+
end
|
334
|
+
|
335
|
+
def email_field(name, **options)
|
336
|
+
field(name, :email, **options)
|
337
|
+
end
|
338
|
+
|
339
|
+
def date_field(name, **options)
|
340
|
+
field(name, :date, **options)
|
341
|
+
end
|
342
|
+
|
343
|
+
def datetime_field(name, **options)
|
344
|
+
field(name, :datetime, **options)
|
345
|
+
end
|
346
|
+
|
347
|
+
def boolean_field(name, **options)
|
348
|
+
field(name, :boolean, **options)
|
349
|
+
end
|
350
|
+
|
351
|
+
def select_field(name, options:, **other_options)
|
352
|
+
field(name, :select, options: options, **other_options)
|
353
|
+
end
|
354
|
+
|
355
|
+
def belongs_to_field(name, **options)
|
356
|
+
field(name, :belongs_to, **options)
|
357
|
+
end
|
358
|
+
|
359
|
+
def has_many_field(name, **options)
|
360
|
+
field(name, :has_many, **options)
|
361
|
+
end
|
362
|
+
|
363
|
+
def file_field(name, **options)
|
364
|
+
field(name, :file, **options)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
class ShowBuilder
|
369
|
+
PREDEFINED_COLORS = %w[primary secondary success danger warning info light dark white transparent].freeze
|
370
|
+
COLUMN_SIZES = [1, 2, 3, 4, 6, 8, 12].freeze
|
371
|
+
SPACING_OPTIONS = %w[none small medium large].freeze
|
372
|
+
PADDING_OPTIONS = %w[none small medium large].freeze
|
373
|
+
HEADING_SIZES = %w[small medium large].freeze
|
374
|
+
DIVIDER_STYLES = %w[default dashed dotted thick].freeze
|
375
|
+
SHADOW_OPTIONS = %w[none small medium large].freeze
|
376
|
+
|
377
|
+
def initialize(resource_class)
|
378
|
+
@resource_class = resource_class
|
379
|
+
@context_stack = []
|
380
|
+
@current_row = nil
|
381
|
+
@current_column = nil
|
382
|
+
@current_card = nil
|
383
|
+
@current_tab_container = nil
|
384
|
+
@current_tab = nil
|
385
|
+
end
|
386
|
+
|
387
|
+
# Layout methods
|
388
|
+
def row(columns: 1, spacing: "medium", &block)
|
389
|
+
validate_spacing!(spacing)
|
390
|
+
|
391
|
+
row_config = {
|
392
|
+
type: :row,
|
393
|
+
columns_count: columns,
|
394
|
+
spacing: spacing,
|
395
|
+
columns: []
|
396
|
+
}
|
397
|
+
|
398
|
+
# Save current context
|
399
|
+
push_context
|
400
|
+
@current_row = row_config
|
401
|
+
|
402
|
+
# Add to parent context
|
403
|
+
if @current_column
|
404
|
+
@current_column[:elements] << row_config
|
405
|
+
else
|
406
|
+
@resource_class.show_layout_config << row_config
|
407
|
+
end
|
408
|
+
|
409
|
+
instance_eval(&block) if block_given?
|
410
|
+
|
411
|
+
# Restore context
|
412
|
+
pop_context
|
413
|
+
end
|
414
|
+
|
415
|
+
def column(size: nil, &block)
|
416
|
+
raise "column must be called within a row block" unless @current_row
|
417
|
+
|
418
|
+
# Auto-calculate size if not provided
|
419
|
+
calculated_size = size || (12 / [@current_row[:columns_count], 1].max)
|
420
|
+
validate_column_size!(calculated_size)
|
421
|
+
|
422
|
+
column_config = {
|
423
|
+
type: :column,
|
424
|
+
size: calculated_size,
|
425
|
+
elements: []
|
426
|
+
}
|
427
|
+
|
428
|
+
# Save current context
|
429
|
+
push_context
|
430
|
+
@current_column = column_config
|
431
|
+
@current_row[:columns] << column_config
|
432
|
+
|
433
|
+
instance_eval(&block) if block_given?
|
434
|
+
|
435
|
+
# Restore context
|
436
|
+
pop_context
|
437
|
+
end
|
438
|
+
|
439
|
+
# Card methods
|
440
|
+
def card(title: nil, color: "light", padding: "medium", shadow: "small", &block)
|
441
|
+
raise "card must be called within a column block" unless @current_column
|
442
|
+
validate_color!(color)
|
443
|
+
validate_padding!(padding)
|
444
|
+
validate_shadow!(shadow)
|
445
|
+
|
446
|
+
card_config = {
|
447
|
+
type: :card,
|
448
|
+
title: title,
|
449
|
+
color: color,
|
450
|
+
padding: padding,
|
451
|
+
shadow: shadow,
|
452
|
+
elements: []
|
453
|
+
}
|
454
|
+
|
455
|
+
# Save current context
|
456
|
+
push_context
|
457
|
+
@current_card = card_config
|
458
|
+
@current_column[:elements] << card_config
|
459
|
+
|
460
|
+
instance_eval(&block) if block_given?
|
461
|
+
|
462
|
+
# Restore context
|
463
|
+
pop_context
|
464
|
+
end
|
465
|
+
|
466
|
+
def metric_card(title:, value:, color: "primary", icon: nil, trend: nil)
|
467
|
+
raise "metric_card must be called within a column block" unless @current_column
|
468
|
+
validate_color!(color)
|
469
|
+
|
470
|
+
metric_config = {
|
471
|
+
type: :metric_card,
|
472
|
+
title: title,
|
473
|
+
value: value,
|
474
|
+
icon: icon,
|
475
|
+
color: color,
|
476
|
+
trend: trend
|
477
|
+
}
|
478
|
+
|
479
|
+
@current_column[:elements] << metric_config
|
480
|
+
end
|
481
|
+
|
482
|
+
def chart_card(title:, chart_type:, **options, &block)
|
483
|
+
raise "chart_card must be called within a column block" unless @current_column
|
484
|
+
|
485
|
+
chart_config = {
|
486
|
+
type: :chart_card,
|
487
|
+
title: title,
|
488
|
+
chart_type: chart_type, # :line, :bar, :pie, :donut, :area
|
489
|
+
height: options[:height] || 350,
|
490
|
+
data_source: options[:data_source],
|
491
|
+
css_classes: options[:class] || "card",
|
492
|
+
config: {}
|
493
|
+
}
|
494
|
+
|
495
|
+
if block_given?
|
496
|
+
chart_builder = ChartBuilder.new
|
497
|
+
chart_builder.instance_eval(&block)
|
498
|
+
chart_config[:config] = chart_builder.config
|
499
|
+
end
|
500
|
+
|
501
|
+
@current_column[:elements] << chart_config
|
502
|
+
end
|
503
|
+
|
504
|
+
# Tab methods
|
505
|
+
def tabs(**options, &block)
|
506
|
+
container = @current_card || @current_column
|
507
|
+
raise "tabs must be called within a card or column block" unless container
|
508
|
+
|
509
|
+
tabs_config = {
|
510
|
+
type: :tabs,
|
511
|
+
css_classes: options[:class] || "tabs-menu",
|
512
|
+
tabs: []
|
513
|
+
}
|
514
|
+
|
515
|
+
# Save current context
|
516
|
+
push_context
|
517
|
+
@current_tab_container = tabs_config
|
518
|
+
container[:elements] << tabs_config
|
519
|
+
|
520
|
+
instance_eval(&block) if block_given?
|
521
|
+
|
522
|
+
# Restore context
|
523
|
+
pop_context
|
524
|
+
end
|
525
|
+
|
526
|
+
def tab(name, **options, &block)
|
527
|
+
raise "tab must be called within a tabs block" unless @current_tab_container
|
528
|
+
|
529
|
+
tab_config = {
|
530
|
+
name: name,
|
531
|
+
label: options[:label] || name,
|
532
|
+
icon: options[:icon],
|
533
|
+
active: options[:active] || false,
|
534
|
+
elements: []
|
535
|
+
}
|
536
|
+
|
537
|
+
# Save current context
|
538
|
+
push_context
|
539
|
+
@current_tab = tab_config
|
540
|
+
@current_tab_container[:tabs] << tab_config
|
541
|
+
|
542
|
+
instance_eval(&block) if block_given?
|
543
|
+
|
544
|
+
# Restore context
|
545
|
+
pop_context
|
546
|
+
end
|
547
|
+
|
548
|
+
# Field display methods
|
549
|
+
def field(name, **options)
|
550
|
+
container = @current_tab || @current_card || @current_column
|
551
|
+
raise "field must be called within a tab, card, or column block" unless container
|
552
|
+
|
553
|
+
# Allow hiding labels with label: false
|
554
|
+
label_value = if options.key?(:label) && options[:label] == false
|
555
|
+
nil
|
556
|
+
else
|
557
|
+
options[:label] || name.to_s.humanize
|
558
|
+
end
|
559
|
+
|
560
|
+
field_config = {
|
561
|
+
type: :field,
|
562
|
+
name: name,
|
563
|
+
label: label_value,
|
564
|
+
field_type: options[:field_type] || :default,
|
565
|
+
format: options[:format],
|
566
|
+
css_classes: options[:class],
|
567
|
+
show_label: options.fetch(:show_label, true)
|
568
|
+
}
|
569
|
+
|
570
|
+
container[:elements] << field_config
|
571
|
+
end
|
572
|
+
|
573
|
+
def fields(*field_names, **options)
|
574
|
+
field_names.each do |name|
|
575
|
+
field(name, **options)
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
# Content methods
|
580
|
+
def content(html = nil, **options, &block)
|
581
|
+
container = @current_tab || @current_card || @current_column
|
582
|
+
raise "content must be called within a tab, card, or column block" unless container
|
583
|
+
|
584
|
+
content_config = {
|
585
|
+
type: :content,
|
586
|
+
html: html,
|
587
|
+
css_classes: options[:class],
|
588
|
+
block: block
|
589
|
+
}
|
590
|
+
|
591
|
+
container[:elements] << content_config
|
592
|
+
end
|
593
|
+
|
594
|
+
def heading(text, level: 2, size: "medium")
|
595
|
+
container = @current_tab || @current_card || @current_column
|
596
|
+
raise "heading must be called within a tab, card, or column block" unless container
|
597
|
+
|
598
|
+
validate_heading_level!(level)
|
599
|
+
validate_heading_size!(size)
|
600
|
+
|
601
|
+
heading_config = {
|
602
|
+
type: :heading,
|
603
|
+
text: text,
|
604
|
+
level: level,
|
605
|
+
size: size
|
606
|
+
}
|
607
|
+
|
608
|
+
container[:elements] << heading_config
|
609
|
+
end
|
610
|
+
|
611
|
+
def divider(style: "default")
|
612
|
+
container = @current_tab || @current_card || @current_column
|
613
|
+
raise "divider must be called within a tab, card, or column block" unless container
|
614
|
+
|
615
|
+
validate_divider_style!(style)
|
616
|
+
|
617
|
+
divider_config = {
|
618
|
+
type: :divider,
|
619
|
+
style: style
|
620
|
+
}
|
621
|
+
|
622
|
+
container[:elements] << divider_config
|
623
|
+
end
|
624
|
+
|
625
|
+
private
|
626
|
+
|
627
|
+
def validate_color!(color)
|
628
|
+
return if PREDEFINED_COLORS.include?(color.to_s)
|
629
|
+
raise ArgumentError, "Invalid color '#{color}'. Available colors: #{PREDEFINED_COLORS.join(', ')}"
|
630
|
+
end
|
631
|
+
|
632
|
+
def validate_column_size!(size)
|
633
|
+
return if COLUMN_SIZES.include?(size)
|
634
|
+
raise ArgumentError, "Invalid column size '#{size}'. Available sizes: #{COLUMN_SIZES.join(', ')}"
|
635
|
+
end
|
636
|
+
|
637
|
+
def validate_spacing!(spacing)
|
638
|
+
return if SPACING_OPTIONS.include?(spacing.to_s)
|
639
|
+
raise ArgumentError, "Invalid spacing '#{spacing}'. Available options: #{SPACING_OPTIONS.join(', ')}"
|
640
|
+
end
|
641
|
+
|
642
|
+
def validate_padding!(padding)
|
643
|
+
return if PADDING_OPTIONS.include?(padding.to_s)
|
644
|
+
raise ArgumentError, "Invalid padding '#{padding}'. Available options: #{PADDING_OPTIONS.join(', ')}"
|
645
|
+
end
|
646
|
+
|
647
|
+
def validate_shadow!(shadow)
|
648
|
+
return if SHADOW_OPTIONS.include?(shadow.to_s)
|
649
|
+
raise ArgumentError, "Invalid shadow '#{shadow}'. Available options: #{SHADOW_OPTIONS.join(', ')}"
|
650
|
+
end
|
651
|
+
|
652
|
+
def validate_heading_level!(level)
|
653
|
+
return if (1..6).include?(level)
|
654
|
+
raise ArgumentError, "Invalid heading level '#{level}'. Must be between 1 and 6"
|
655
|
+
end
|
656
|
+
|
657
|
+
def validate_heading_size!(size)
|
658
|
+
return if HEADING_SIZES.include?(size.to_s)
|
659
|
+
raise ArgumentError, "Invalid heading size '#{size}'. Available sizes: #{HEADING_SIZES.join(', ')}"
|
660
|
+
end
|
661
|
+
|
662
|
+
def validate_divider_style!(style)
|
663
|
+
return if DIVIDER_STYLES.include?(style.to_s)
|
664
|
+
raise ArgumentError, "Invalid divider style '#{style}'. Available styles: #{DIVIDER_STYLES.join(', ')}"
|
665
|
+
end
|
666
|
+
|
667
|
+
def push_context
|
668
|
+
@context_stack.push({
|
669
|
+
row: @current_row,
|
670
|
+
column: @current_column,
|
671
|
+
card: @current_card,
|
672
|
+
tab_container: @current_tab_container,
|
673
|
+
tab: @current_tab
|
674
|
+
})
|
675
|
+
end
|
676
|
+
|
677
|
+
def pop_context
|
678
|
+
if @context_stack.any?
|
679
|
+
context = @context_stack.pop
|
680
|
+
@current_row = context[:row]
|
681
|
+
@current_column = context[:column]
|
682
|
+
@current_card = context[:card]
|
683
|
+
@current_tab_container = context[:tab_container]
|
684
|
+
@current_tab = context[:tab]
|
685
|
+
else
|
686
|
+
@current_row = nil
|
687
|
+
@current_column = nil
|
688
|
+
@current_card = nil
|
689
|
+
@current_tab_container = nil
|
690
|
+
@current_tab = nil
|
691
|
+
end
|
692
|
+
end
|
693
|
+
end
|
694
|
+
|
695
|
+
class ChartBuilder
|
696
|
+
attr_reader :config
|
697
|
+
|
698
|
+
def initialize
|
699
|
+
@config = {}
|
700
|
+
end
|
701
|
+
|
702
|
+
def series(data)
|
703
|
+
@config[:series] = data
|
704
|
+
end
|
705
|
+
|
706
|
+
def categories(data)
|
707
|
+
@config[:categories] = data
|
708
|
+
end
|
709
|
+
|
710
|
+
def colors(data)
|
711
|
+
@config[:colors] = data
|
712
|
+
end
|
713
|
+
|
714
|
+
def title(text)
|
715
|
+
@config[:title] = text
|
716
|
+
end
|
717
|
+
|
718
|
+
def subtitle(text)
|
719
|
+
@config[:subtitle] = text
|
720
|
+
end
|
721
|
+
end
|
722
|
+
|
723
|
+
# Query scopes
|
724
|
+
def self.all_records(sort_field: nil, sort_direction: 'asc')
|
725
|
+
records = model_class.all
|
726
|
+
|
727
|
+
if sort_field.present? && sortable_fields.any? { |f| f[:name].to_s == sort_field.to_s }
|
728
|
+
records = records.order("#{sort_field} #{sort_direction}")
|
729
|
+
end
|
730
|
+
|
731
|
+
records
|
732
|
+
end
|
733
|
+
|
734
|
+
def self.find_record(id)
|
735
|
+
model_class.find(id)
|
736
|
+
end
|
737
|
+
|
738
|
+
def self.search_records(query, sort_field: nil, sort_direction: 'asc', records: nil)
|
739
|
+
records ||= model_class.all
|
740
|
+
searchable_fields = fields_config.select { |field| field[:searchable] }
|
741
|
+
|
742
|
+
if searchable_fields.any? && query.present?
|
743
|
+
conditions = searchable_fields.map do |field|
|
744
|
+
"#{field[:name]} ILIKE ?"
|
745
|
+
end.join(' OR ')
|
746
|
+
|
747
|
+
values = Array.new(searchable_fields.length, "%#{query}%")
|
748
|
+
records = records.where(conditions, *values)
|
749
|
+
end
|
750
|
+
|
751
|
+
if sort_field.present? && sortable_fields.any? { |f| f[:name].to_s == sort_field.to_s }
|
752
|
+
records = records.order("#{sort_field} #{sort_direction}")
|
753
|
+
end
|
754
|
+
|
755
|
+
records
|
756
|
+
end
|
757
|
+
|
758
|
+
# Batch Actions DSL
|
759
|
+
def self.enable_batch_actions
|
760
|
+
self.batch_actions_enabled = true
|
761
|
+
end
|
762
|
+
|
763
|
+
def self.batch_action(action_class, options = {})
|
764
|
+
self.batch_actions_config = batch_actions_config + [{
|
765
|
+
class: action_class,
|
766
|
+
condition: options[:if]
|
767
|
+
}]
|
768
|
+
end
|
769
|
+
|
770
|
+
def self.available_batch_actions(context = {})
|
771
|
+
return [] unless batch_actions_enabled
|
772
|
+
|
773
|
+
batch_actions_config.select do |config|
|
774
|
+
condition = config[:condition]
|
775
|
+
condition.nil? || (condition.respond_to?(:call) ? context.instance_eval(&condition) : condition)
|
776
|
+
end.map { |config| config[:class] }
|
777
|
+
end
|
778
|
+
|
779
|
+
# Row Actions DSL (single record actions)
|
780
|
+
def self.row_action(action_class, options = {})
|
781
|
+
self.row_actions_config ||= []
|
782
|
+
|
783
|
+
self.row_actions_config = row_actions_config + [{
|
784
|
+
class: action_class,
|
785
|
+
condition: options[:if]
|
786
|
+
}]
|
787
|
+
end
|
788
|
+
|
789
|
+
def self.row_actions
|
790
|
+
row_actions_config || []
|
791
|
+
end
|
792
|
+
|
793
|
+
def self.has_row_actions?
|
794
|
+
row_actions_config.present?
|
795
|
+
end
|
796
|
+
|
797
|
+
# Route helpers
|
798
|
+
def self.route_key
|
799
|
+
resource_name.underscore.pluralize
|
800
|
+
end
|
801
|
+
|
802
|
+
def self.param_key
|
803
|
+
resource_name.underscore
|
804
|
+
end
|
805
|
+
end
|
806
|
+
end
|