easy-admin-rails 0.1.14 → 0.2.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 +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/components/easy_admin/versions/diff_modal_component.rb +5 -1
- 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 +180 -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,252 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module ResourceModules
|
3
|
+
# ScopeManager module for handling resource scopes and filtering
|
4
|
+
# Manages scope definitions, default scopes, and scope-related queries
|
5
|
+
module ScopeManager
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
class_attribute :scopes_config
|
10
|
+
|
11
|
+
def self.initialize_scope_manager
|
12
|
+
self.scopes_config = []
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
# Core scope definition method
|
18
|
+
def scope(name, **options)
|
19
|
+
scope_config = {
|
20
|
+
name: name,
|
21
|
+
label: options[:label] || name.to_s.humanize,
|
22
|
+
scope_method: options[:scope] || name,
|
23
|
+
default: options.fetch(:default, false),
|
24
|
+
icon: options[:icon],
|
25
|
+
color: options[:color] || 'blue',
|
26
|
+
count: options.fetch(:show_count, true),
|
27
|
+
condition: options[:if] # Conditional scope visibility
|
28
|
+
}
|
29
|
+
|
30
|
+
add_scope(scope_config)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Add scope to configuration
|
34
|
+
def add_scope(scope_config)
|
35
|
+
self.scopes_config = (self.scopes_config || []) + [scope_config]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Scope query methods
|
39
|
+
def scopes
|
40
|
+
scopes_config
|
41
|
+
end
|
42
|
+
|
43
|
+
def has_scopes?
|
44
|
+
scopes_config.any?
|
45
|
+
end
|
46
|
+
|
47
|
+
def scope_names
|
48
|
+
scopes_config.map { |scope| scope[:name] }
|
49
|
+
end
|
50
|
+
|
51
|
+
def find_scope(name)
|
52
|
+
scopes_config.find { |scope| scope[:name].to_s == name.to_s }
|
53
|
+
end
|
54
|
+
|
55
|
+
def scope_exists?(name)
|
56
|
+
find_scope(name).present?
|
57
|
+
end
|
58
|
+
|
59
|
+
# Default scope management
|
60
|
+
def default_scope
|
61
|
+
scopes_config.find { |scope| scope[:default] } || scopes_config.first
|
62
|
+
end
|
63
|
+
|
64
|
+
def set_default_scope(name)
|
65
|
+
# Remove existing default
|
66
|
+
scopes_config.each { |scope| scope[:default] = false }
|
67
|
+
|
68
|
+
# Set new default
|
69
|
+
scope = find_scope(name)
|
70
|
+
scope[:default] = true if scope
|
71
|
+
end
|
72
|
+
|
73
|
+
def has_default_scope?
|
74
|
+
default_scope.present?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Conditional scopes
|
78
|
+
def visible_scopes(context = {})
|
79
|
+
scopes_config.select do |scope_config|
|
80
|
+
condition = scope_config[:condition]
|
81
|
+
condition.nil? || (condition.respond_to?(:call) ? context.instance_eval(&condition) : condition)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def conditional_scopes
|
86
|
+
scopes_config.select { |scope| scope[:condition].present? }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Scope application methods
|
90
|
+
def apply_scope_to_records(records, scope_name)
|
91
|
+
return records unless scope_exists?(scope_name)
|
92
|
+
|
93
|
+
scope_config = find_scope(scope_name)
|
94
|
+
return records if scope_config[:name] == :all
|
95
|
+
|
96
|
+
scope_method = scope_config[:scope_method]
|
97
|
+
if records.respond_to?(scope_method)
|
98
|
+
records.public_send(scope_method)
|
99
|
+
else
|
100
|
+
records
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def apply_default_scope_to_records(records)
|
105
|
+
return records unless has_default_scope?
|
106
|
+
|
107
|
+
default = default_scope
|
108
|
+
return records if default[:name] == :all
|
109
|
+
|
110
|
+
apply_scope_to_records(records, default[:name])
|
111
|
+
end
|
112
|
+
|
113
|
+
# Scope counts calculation
|
114
|
+
def calculate_scope_counts(base_records = nil)
|
115
|
+
base_records ||= model_class.all
|
116
|
+
counts = {}
|
117
|
+
|
118
|
+
scopes_config.each do |scope_config|
|
119
|
+
scope_name = scope_config[:name]
|
120
|
+
next unless scope_config[:count] # Skip if count is disabled
|
121
|
+
|
122
|
+
if scope_name == :all
|
123
|
+
counts[scope_name] = base_records.count
|
124
|
+
else
|
125
|
+
scope_method = scope_config[:scope_method]
|
126
|
+
if model_class.respond_to?(scope_method)
|
127
|
+
counts[scope_name] = model_class.public_send(scope_method).count
|
128
|
+
else
|
129
|
+
counts[scope_name] = 0
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
counts
|
135
|
+
end
|
136
|
+
|
137
|
+
def scope_count(scope_name, base_records = nil)
|
138
|
+
return 0 unless scope_exists?(scope_name)
|
139
|
+
|
140
|
+
scope_config = find_scope(scope_name)
|
141
|
+
return 0 unless scope_config[:count]
|
142
|
+
|
143
|
+
if scope_name == :all
|
144
|
+
base_records ||= model_class.all
|
145
|
+
base_records.count
|
146
|
+
else
|
147
|
+
scope_method = scope_config[:scope_method]
|
148
|
+
if model_class.respond_to?(scope_method)
|
149
|
+
model_class.public_send(scope_method).count
|
150
|
+
else
|
151
|
+
0
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Scope utilities
|
157
|
+
def scope_label(scope_name)
|
158
|
+
scope_config = find_scope(scope_name)
|
159
|
+
scope_config ? scope_config[:label] : scope_name.to_s.humanize
|
160
|
+
end
|
161
|
+
|
162
|
+
def scope_icon(scope_name)
|
163
|
+
scope_config = find_scope(scope_name)
|
164
|
+
scope_config&.dig(:icon)
|
165
|
+
end
|
166
|
+
|
167
|
+
def scope_color(scope_name)
|
168
|
+
scope_config = find_scope(scope_name)
|
169
|
+
scope_config&.dig(:color) || 'blue'
|
170
|
+
end
|
171
|
+
|
172
|
+
def scope_method_name(scope_name)
|
173
|
+
scope_config = find_scope(scope_name)
|
174
|
+
scope_config ? scope_config[:scope_method] : scope_name
|
175
|
+
end
|
176
|
+
|
177
|
+
# Scope validation
|
178
|
+
def validate_scopes
|
179
|
+
errors = []
|
180
|
+
|
181
|
+
scopes_config.each do |scope_config|
|
182
|
+
scope_name = scope_config[:name]
|
183
|
+
scope_method = scope_config[:scope_method]
|
184
|
+
|
185
|
+
# Check if scope method exists on model (skip :all)
|
186
|
+
if scope_name != :all && !model_class.respond_to?(scope_method)
|
187
|
+
errors << "Scope method '#{scope_method}' does not exist on #{model_class.name}"
|
188
|
+
end
|
189
|
+
|
190
|
+
# Check for duplicate scope names
|
191
|
+
duplicate_count = scopes_config.count { |s| s[:name] == scope_name }
|
192
|
+
if duplicate_count > 1
|
193
|
+
errors << "Duplicate scope name '#{scope_name}' found"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
errors
|
198
|
+
end
|
199
|
+
|
200
|
+
def valid_scopes?
|
201
|
+
validate_scopes.empty?
|
202
|
+
end
|
203
|
+
|
204
|
+
# Scope management
|
205
|
+
def clear_scopes
|
206
|
+
self.scopes_config = []
|
207
|
+
end
|
208
|
+
|
209
|
+
def remove_scope(scope_name)
|
210
|
+
self.scopes_config = scopes_config.reject { |scope| scope[:name] == scope_name }
|
211
|
+
end
|
212
|
+
|
213
|
+
def total_scopes_count
|
214
|
+
scopes_config.count
|
215
|
+
end
|
216
|
+
|
217
|
+
def enabled_scope_counts_count
|
218
|
+
scopes_config.count { |scope| scope[:count] }
|
219
|
+
end
|
220
|
+
|
221
|
+
# Predefined scope helpers
|
222
|
+
def add_all_scope(label: "All", default: true)
|
223
|
+
scope(:all, label: label, scope: :all, default: default)
|
224
|
+
end
|
225
|
+
|
226
|
+
def add_recent_scope(days: 7, label: "Recent")
|
227
|
+
return unless model_class.column_names.include?('created_at')
|
228
|
+
|
229
|
+
scope(:recent,
|
230
|
+
label: label,
|
231
|
+
scope: -> { where('created_at >= ?', days.days.ago) })
|
232
|
+
end
|
233
|
+
|
234
|
+
def add_published_scope(column: :published, label: "Published")
|
235
|
+
return unless model_class.column_names.include?(column.to_s)
|
236
|
+
|
237
|
+
scope(:published,
|
238
|
+
label: label,
|
239
|
+
scope: -> { where(column => true) })
|
240
|
+
end
|
241
|
+
|
242
|
+
def add_unpublished_scope(column: :published, label: "Unpublished")
|
243
|
+
return unless model_class.column_names.include?(column.to_s)
|
244
|
+
|
245
|
+
scope(:unpublished,
|
246
|
+
label: label,
|
247
|
+
scope: -> { where(column => [false, nil]) })
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
@@ -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
|