easy-admin-rails 0.1.15 → 0.2.1
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 +4 -4
- data/app/assets/builds/easy_admin.base.js +254 -18
- data/app/assets/builds/easy_admin.base.js.map +4 -4
- data/app/assets/builds/easy_admin.css +112 -18
- data/app/components/easy_admin/base_component.rb +1 -0
- data/app/components/easy_admin/form_tabs_component.rb +5 -2
- data/app/components/easy_admin/navbar_component.rb +5 -1
- data/app/components/easy_admin/permissions/user_role_assignment_component.rb +254 -0
- data/app/components/easy_admin/permissions/user_role_permissions_component.rb +186 -0
- data/app/components/easy_admin/resources/index_component.rb +1 -4
- data/app/components/easy_admin/sidebar_component.rb +67 -2
- data/app/controllers/easy_admin/application_controller.rb +131 -1
- data/app/controllers/easy_admin/batch_actions_controller.rb +27 -0
- data/app/controllers/easy_admin/concerns/belongs_to_editing.rb +201 -0
- data/app/controllers/easy_admin/concerns/inline_field_editing.rb +297 -0
- data/app/controllers/easy_admin/concerns/resource_authorization.rb +55 -0
- data/app/controllers/easy_admin/concerns/resource_filtering.rb +178 -0
- data/app/controllers/easy_admin/concerns/resource_loading.rb +149 -0
- data/app/controllers/easy_admin/concerns/resource_pagination.rb +135 -0
- data/app/controllers/easy_admin/dashboard_controller.rb +2 -1
- data/app/controllers/easy_admin/dashboards_controller.rb +6 -40
- data/app/controllers/easy_admin/resources_controller.rb +13 -762
- data/app/controllers/easy_admin/row_actions_controller.rb +25 -0
- data/app/helpers/easy_admin/fields_helper.rb +61 -9
- data/app/javascript/easy_admin/controllers/event_emitter_controller.js +2 -4
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +0 -10
- data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +1 -4
- data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +227 -0
- data/app/javascript/easy_admin/controllers/role_preview_controller.js +93 -0
- data/app/javascript/easy_admin/controllers/select_field_controller.js +1 -2
- data/app/javascript/easy_admin/controllers/settings_button_controller.js +1 -2
- data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +1 -4
- data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +0 -2
- data/app/javascript/easy_admin/controllers.js +5 -1
- data/app/models/easy_admin/admin_user.rb +6 -0
- data/app/policies/admin_user_policy.rb +36 -0
- data/app/policies/application_policy.rb +83 -0
- data/app/views/easy_admin/application/authorization_failure.turbo_stream.erb +8 -0
- data/app/views/easy_admin/dashboards/card.html.erb +5 -0
- data/app/views/easy_admin/dashboards/card.turbo_stream.erb +7 -0
- data/app/views/easy_admin/dashboards/card_error.html.erb +3 -0
- data/app/views/easy_admin/dashboards/card_error.turbo_stream.erb +5 -0
- data/app/views/easy_admin/dashboards/show.turbo_stream.erb +7 -0
- data/app/views/easy_admin/resources/belongs_to_edit_attached.html.erb +6 -0
- data/app/views/easy_admin/resources/belongs_to_edit_attached.turbo_stream.erb +8 -0
- data/app/views/easy_admin/resources/belongs_to_reattach.html.erb +5 -0
- data/app/views/easy_admin/resources/edit.html.erb +1 -1
- data/app/views/easy_admin/resources/edit_field.html.erb +5 -0
- data/app/views/easy_admin/resources/edit_field.turbo_stream.erb +7 -0
- data/app/views/easy_admin/resources/index.html.erb +1 -1
- data/app/views/easy_admin/resources/index_frame.html.erb +8 -142
- data/app/views/easy_admin/resources/update_belongs_to_attached.turbo_stream.erb +25 -0
- data/app/views/layouts/easy_admin/application.html.erb +15 -2
- data/config/initializers/easy_admin_permissions.rb +73 -0
- data/db/seeds/easy_admin_permissions.rb +121 -0
- data/lib/easy-admin-rails.rb +2 -0
- data/lib/easy_admin/permissions/component.rb +168 -0
- data/lib/easy_admin/permissions/configuration.rb +37 -0
- data/lib/easy_admin/permissions/controller.rb +164 -0
- data/lib/easy_admin/permissions/dsl.rb +160 -0
- data/lib/easy_admin/permissions/models.rb +44 -0
- data/lib/easy_admin/permissions/permission_denied_component.rb +121 -0
- data/lib/easy_admin/permissions/resource_permissions.rb +231 -0
- data/lib/easy_admin/permissions/role_definition.rb +45 -0
- data/lib/easy_admin/permissions/role_denied_component.rb +159 -0
- data/lib/easy_admin/permissions/role_dsl.rb +73 -0
- data/lib/easy_admin/permissions/user_extensions.rb +129 -0
- data/lib/easy_admin/permissions.rb +113 -0
- data/lib/easy_admin/resource/base.rb +119 -0
- data/lib/easy_admin/resource/configuration.rb +148 -0
- data/lib/easy_admin/resource/dsl.rb +117 -0
- data/lib/easy_admin/resource/field_registry.rb +189 -0
- data/lib/easy_admin/resource/form_builder.rb +123 -0
- data/lib/easy_admin/resource/layout_builder.rb +249 -0
- data/lib/easy_admin/resource/scope_manager.rb +252 -0
- data/lib/easy_admin/resource/show_builder.rb +359 -0
- data/lib/easy_admin/resource.rb +8 -835
- data/lib/easy_admin/resource_modules.rb +11 -0
- data/lib/easy_admin/version.rb +1 -1
- data/lib/generators/easy_admin/permissions/install_generator.rb +90 -0
- data/lib/generators/easy_admin/permissions/templates/initializers/permissions.rb +37 -0
- data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +27 -0
- data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +6 -0
- data/lib/generators/easy_admin/permissions/templates/models/permission.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/role.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/role_permission.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/models/user_role.rb +9 -0
- data/lib/generators/easy_admin/permissions/templates/policies/application_policy.rb +47 -0
- data/lib/generators/easy_admin/permissions/templates/policies/user_policy.rb +36 -0
- data/lib/generators/easy_admin/permissions/templates/seeds/permissions.rb +89 -0
- metadata +62 -5
- data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +0 -45
@@ -0,0 +1,359 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module ResourceModules
|
3
|
+
# ShowBuilder class for show layout DSL
|
4
|
+
# Handles all show page layout definitions with rows, columns, cards, and tabs
|
5
|
+
class ShowBuilder
|
6
|
+
PREDEFINED_COLORS = %w[primary secondary success danger warning info light dark white transparent].freeze
|
7
|
+
COLUMN_SIZES = [1, 2, 3, 4, 6, 8, 12].freeze
|
8
|
+
SPACING_OPTIONS = %w[none small medium large].freeze
|
9
|
+
PADDING_OPTIONS = %w[none small medium large].freeze
|
10
|
+
HEADING_SIZES = %w[small medium large].freeze
|
11
|
+
DIVIDER_STYLES = %w[default dashed dotted thick].freeze
|
12
|
+
SHADOW_OPTIONS = %w[none small medium large].freeze
|
13
|
+
|
14
|
+
def initialize(resource_class)
|
15
|
+
@resource_class = resource_class
|
16
|
+
@context_stack = []
|
17
|
+
@current_row = nil
|
18
|
+
@current_column = nil
|
19
|
+
@current_card = nil
|
20
|
+
@current_tab_container = nil
|
21
|
+
@current_tab = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
# Layout methods
|
25
|
+
def row(columns: 1, spacing: "medium", &block)
|
26
|
+
validate_spacing!(spacing)
|
27
|
+
|
28
|
+
row_config = {
|
29
|
+
type: :row,
|
30
|
+
columns_count: columns,
|
31
|
+
spacing: spacing,
|
32
|
+
columns: []
|
33
|
+
}
|
34
|
+
|
35
|
+
push_context
|
36
|
+
@current_row = row_config
|
37
|
+
|
38
|
+
if @current_column
|
39
|
+
@current_column[:elements] << row_config
|
40
|
+
else
|
41
|
+
@resource_class.add_show_layout_element(row_config)
|
42
|
+
end
|
43
|
+
|
44
|
+
instance_eval(&block) if block_given?
|
45
|
+
pop_context
|
46
|
+
end
|
47
|
+
|
48
|
+
def column(size: nil, &block)
|
49
|
+
raise "column must be called within a row block" unless @current_row
|
50
|
+
|
51
|
+
calculated_size = size || (12 / [@current_row[:columns_count], 1].max)
|
52
|
+
validate_column_size!(calculated_size)
|
53
|
+
|
54
|
+
column_config = {
|
55
|
+
type: :column,
|
56
|
+
size: calculated_size,
|
57
|
+
elements: []
|
58
|
+
}
|
59
|
+
|
60
|
+
push_context
|
61
|
+
@current_column = column_config
|
62
|
+
@current_row[:columns] << column_config
|
63
|
+
|
64
|
+
instance_eval(&block) if block_given?
|
65
|
+
pop_context
|
66
|
+
end
|
67
|
+
|
68
|
+
# Card methods
|
69
|
+
def card(title: nil, color: "light", padding: "medium", shadow: "small", &block)
|
70
|
+
raise "card must be called within a column block" unless @current_column
|
71
|
+
validate_color!(color)
|
72
|
+
validate_padding!(padding)
|
73
|
+
validate_shadow!(shadow)
|
74
|
+
|
75
|
+
card_config = {
|
76
|
+
type: :card,
|
77
|
+
title: title,
|
78
|
+
color: color,
|
79
|
+
padding: padding,
|
80
|
+
shadow: shadow,
|
81
|
+
elements: []
|
82
|
+
}
|
83
|
+
|
84
|
+
push_context
|
85
|
+
@current_card = card_config
|
86
|
+
@current_column[:elements] << card_config
|
87
|
+
|
88
|
+
instance_eval(&block) if block_given?
|
89
|
+
pop_context
|
90
|
+
end
|
91
|
+
|
92
|
+
def metric_card(title:, value:, color: "primary", icon: nil, trend: nil)
|
93
|
+
raise "metric_card must be called within a column block" unless @current_column
|
94
|
+
validate_color!(color)
|
95
|
+
|
96
|
+
metric_config = {
|
97
|
+
type: :metric_card,
|
98
|
+
title: title,
|
99
|
+
value: value,
|
100
|
+
icon: icon,
|
101
|
+
color: color,
|
102
|
+
trend: trend
|
103
|
+
}
|
104
|
+
|
105
|
+
@current_column[:elements] << metric_config
|
106
|
+
end
|
107
|
+
|
108
|
+
def chart_card(title:, chart_type:, **options, &block)
|
109
|
+
raise "chart_card must be called within a column block" unless @current_column
|
110
|
+
|
111
|
+
chart_config = {
|
112
|
+
type: :chart_card,
|
113
|
+
title: title,
|
114
|
+
chart_type: chart_type,
|
115
|
+
height: options[:height] || 350,
|
116
|
+
data_source: options[:data_source],
|
117
|
+
css_classes: options[:class] || "card",
|
118
|
+
config: {}
|
119
|
+
}
|
120
|
+
|
121
|
+
if block_given?
|
122
|
+
chart_builder = ChartBuilder.new
|
123
|
+
chart_builder.instance_eval(&block)
|
124
|
+
chart_config[:config] = chart_builder.config
|
125
|
+
end
|
126
|
+
|
127
|
+
@current_column[:elements] << chart_config
|
128
|
+
end
|
129
|
+
|
130
|
+
# Tab methods
|
131
|
+
def tabs(**options, &block)
|
132
|
+
container = @current_card || @current_column
|
133
|
+
raise "tabs must be called within a card or column block" unless container
|
134
|
+
|
135
|
+
tabs_config = {
|
136
|
+
type: :tabs,
|
137
|
+
css_classes: options[:class] || "tabs-menu",
|
138
|
+
tabs: []
|
139
|
+
}
|
140
|
+
|
141
|
+
push_context
|
142
|
+
@current_tab_container = tabs_config
|
143
|
+
container[:elements] << tabs_config
|
144
|
+
|
145
|
+
instance_eval(&block) if block_given?
|
146
|
+
pop_context
|
147
|
+
end
|
148
|
+
|
149
|
+
def tab(name, **options, &block)
|
150
|
+
raise "tab must be called within a tabs block" unless @current_tab_container
|
151
|
+
|
152
|
+
tab_config = {
|
153
|
+
name: name,
|
154
|
+
label: options[:label] || name,
|
155
|
+
icon: options[:icon],
|
156
|
+
active: options[:active] || false,
|
157
|
+
elements: []
|
158
|
+
}
|
159
|
+
|
160
|
+
push_context
|
161
|
+
@current_tab = tab_config
|
162
|
+
@current_tab_container[:tabs] << tab_config
|
163
|
+
|
164
|
+
instance_eval(&block) if block_given?
|
165
|
+
pop_context
|
166
|
+
end
|
167
|
+
|
168
|
+
# Field display methods
|
169
|
+
def field(name, **options)
|
170
|
+
container = @current_tab || @current_card || @current_column
|
171
|
+
raise "field must be called within a tab, card, or column block" unless container
|
172
|
+
|
173
|
+
label_value = if options.key?(:label) && options[:label] == false
|
174
|
+
nil
|
175
|
+
else
|
176
|
+
options[:label] || name.to_s.humanize
|
177
|
+
end
|
178
|
+
|
179
|
+
field_config = {
|
180
|
+
type: :field,
|
181
|
+
name: name,
|
182
|
+
label: label_value,
|
183
|
+
field_type: options[:field_type] || :default,
|
184
|
+
format: options[:format],
|
185
|
+
css_classes: options[:class],
|
186
|
+
show_label: options.fetch(:show_label, true)
|
187
|
+
}
|
188
|
+
|
189
|
+
container[:elements] << field_config
|
190
|
+
end
|
191
|
+
|
192
|
+
def fields(*field_names, **options)
|
193
|
+
field_names.each do |name|
|
194
|
+
field(name, **options)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Component rendering support with context
|
199
|
+
def render(component_class_or_instance, **options)
|
200
|
+
container = @current_tab || @current_card || @current_column
|
201
|
+
raise "render must be called within a tab, card, or column block" unless container
|
202
|
+
|
203
|
+
component_config = {
|
204
|
+
type: :custom_component,
|
205
|
+
component_class: component_class_or_instance.class,
|
206
|
+
component_options: options,
|
207
|
+
component_instance: component_class_or_instance
|
208
|
+
}
|
209
|
+
|
210
|
+
container[:elements] << component_config
|
211
|
+
component_class_or_instance
|
212
|
+
end
|
213
|
+
|
214
|
+
# Content methods
|
215
|
+
def content(html = nil, **options, &block)
|
216
|
+
container = @current_tab || @current_card || @current_column
|
217
|
+
raise "content must be called within a tab, card, or column block" unless container
|
218
|
+
|
219
|
+
content_config = {
|
220
|
+
type: :content,
|
221
|
+
html: html,
|
222
|
+
css_classes: options[:class],
|
223
|
+
block: block
|
224
|
+
}
|
225
|
+
|
226
|
+
container[:elements] << content_config
|
227
|
+
end
|
228
|
+
|
229
|
+
def heading(text, level: 2, size: "medium")
|
230
|
+
container = @current_tab || @current_card || @current_column
|
231
|
+
raise "heading must be called within a tab, card, or column block" unless container
|
232
|
+
|
233
|
+
validate_heading_level!(level)
|
234
|
+
validate_heading_size!(size)
|
235
|
+
|
236
|
+
heading_config = {
|
237
|
+
type: :heading,
|
238
|
+
text: text,
|
239
|
+
level: level,
|
240
|
+
size: size
|
241
|
+
}
|
242
|
+
|
243
|
+
container[:elements] << heading_config
|
244
|
+
end
|
245
|
+
|
246
|
+
def divider(style: "default")
|
247
|
+
container = @current_tab || @current_card || @current_column
|
248
|
+
raise "divider must be called within a tab, card, or column block" unless container
|
249
|
+
|
250
|
+
validate_divider_style!(style)
|
251
|
+
|
252
|
+
divider_config = {
|
253
|
+
type: :divider,
|
254
|
+
style: style
|
255
|
+
}
|
256
|
+
|
257
|
+
container[:elements] << divider_config
|
258
|
+
end
|
259
|
+
|
260
|
+
private
|
261
|
+
|
262
|
+
def validate_color!(color)
|
263
|
+
return if PREDEFINED_COLORS.include?(color.to_s)
|
264
|
+
raise ArgumentError, "Invalid color '#{color}'. Available colors: #{PREDEFINED_COLORS.join(', ')}"
|
265
|
+
end
|
266
|
+
|
267
|
+
def validate_column_size!(size)
|
268
|
+
return if COLUMN_SIZES.include?(size)
|
269
|
+
raise ArgumentError, "Invalid column size '#{size}'. Available sizes: #{COLUMN_SIZES.join(', ')}"
|
270
|
+
end
|
271
|
+
|
272
|
+
def validate_spacing!(spacing)
|
273
|
+
return if SPACING_OPTIONS.include?(spacing.to_s)
|
274
|
+
raise ArgumentError, "Invalid spacing '#{spacing}'. Available options: #{SPACING_OPTIONS.join(', ')}"
|
275
|
+
end
|
276
|
+
|
277
|
+
def validate_padding!(padding)
|
278
|
+
return if PADDING_OPTIONS.include?(padding.to_s)
|
279
|
+
raise ArgumentError, "Invalid padding '#{padding}'. Available options: #{PADDING_OPTIONS.join(', ')}"
|
280
|
+
end
|
281
|
+
|
282
|
+
def validate_shadow!(shadow)
|
283
|
+
return if SHADOW_OPTIONS.include?(shadow.to_s)
|
284
|
+
raise ArgumentError, "Invalid shadow '#{shadow}'. Available options: #{SHADOW_OPTIONS.join(', ')}"
|
285
|
+
end
|
286
|
+
|
287
|
+
def validate_heading_level!(level)
|
288
|
+
return if (1..6).include?(level)
|
289
|
+
raise ArgumentError, "Invalid heading level '#{level}'. Must be between 1 and 6"
|
290
|
+
end
|
291
|
+
|
292
|
+
def validate_heading_size!(size)
|
293
|
+
return if HEADING_SIZES.include?(size.to_s)
|
294
|
+
raise ArgumentError, "Invalid heading size '#{size}'. Available sizes: #{HEADING_SIZES.join(', ')}"
|
295
|
+
end
|
296
|
+
|
297
|
+
def validate_divider_style!(style)
|
298
|
+
return if DIVIDER_STYLES.include?(style.to_s)
|
299
|
+
raise ArgumentError, "Invalid divider style '#{style}'. Available styles: #{DIVIDER_STYLES.join(', ')}"
|
300
|
+
end
|
301
|
+
|
302
|
+
def push_context
|
303
|
+
@context_stack.push({
|
304
|
+
row: @current_row,
|
305
|
+
column: @current_column,
|
306
|
+
card: @current_card,
|
307
|
+
tab_container: @current_tab_container,
|
308
|
+
tab: @current_tab
|
309
|
+
})
|
310
|
+
end
|
311
|
+
|
312
|
+
def pop_context
|
313
|
+
if @context_stack.any?
|
314
|
+
context = @context_stack.pop
|
315
|
+
@current_row = context[:row]
|
316
|
+
@current_column = context[:column]
|
317
|
+
@current_card = context[:card]
|
318
|
+
@current_tab_container = context[:tab_container]
|
319
|
+
@current_tab = context[:tab]
|
320
|
+
else
|
321
|
+
@current_row = nil
|
322
|
+
@current_column = nil
|
323
|
+
@current_card = nil
|
324
|
+
@current_tab_container = nil
|
325
|
+
@current_tab = nil
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# ChartBuilder class for chart configuration
|
331
|
+
class ChartBuilder
|
332
|
+
attr_reader :config
|
333
|
+
|
334
|
+
def initialize
|
335
|
+
@config = {}
|
336
|
+
end
|
337
|
+
|
338
|
+
def series(data)
|
339
|
+
@config[:series] = data
|
340
|
+
end
|
341
|
+
|
342
|
+
def categories(data)
|
343
|
+
@config[:categories] = data
|
344
|
+
end
|
345
|
+
|
346
|
+
def colors(data)
|
347
|
+
@config[:colors] = data
|
348
|
+
end
|
349
|
+
|
350
|
+
def title(text)
|
351
|
+
@config[:title] = text
|
352
|
+
end
|
353
|
+
|
354
|
+
def subtitle(text)
|
355
|
+
@config[:subtitle] = text
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|