easy-admin-rails 0.2.5 → 0.2.7
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 +95 -0
- data/app/assets/builds/easy_admin.base.js.map +3 -3
- data/app/assets/builds/easy_admin.css +226 -0
- data/app/components/easy_admin/fields/form/belongs_to_component.rb +0 -1
- data/app/components/easy_admin/form_layout_component.rb +553 -0
- data/app/components/easy_admin/navbar_component.rb +19 -4
- data/app/components/easy_admin/permissions/user_role_permissions_component.rb +1 -3
- data/app/components/easy_admin/profile/change_password_modal_component.rb +75 -0
- data/app/components/easy_admin/profile/profile_tab_component.rb +92 -0
- data/app/components/easy_admin/profile/security_tab_component.rb +53 -0
- data/app/components/easy_admin/profile/settings_component.rb +103 -0
- data/app/components/easy_admin/show_layout_component.rb +694 -24
- data/app/components/easy_admin/two_factor/backup_codes_component.rb +118 -0
- data/app/components/easy_admin/two_factor/setup_component.rb +124 -0
- data/app/components/easy_admin/two_factor/status_component.rb +92 -0
- data/app/controllers/concerns/easy_admin/two_factor.rb +110 -0
- data/app/controllers/easy_admin/application_controller.rb +10 -5
- data/app/controllers/easy_admin/batch_actions_controller.rb +0 -1
- data/app/controllers/easy_admin/concerns/inline_field_editing.rb +4 -11
- data/app/controllers/easy_admin/concerns/resource_loading.rb +10 -9
- data/app/controllers/easy_admin/concerns/resource_pagination.rb +3 -0
- data/app/controllers/easy_admin/dashboards_controller.rb +0 -1
- data/app/controllers/easy_admin/profile_controller.rb +25 -0
- data/app/controllers/easy_admin/resources_controller.rb +1 -5
- data/app/controllers/easy_admin/row_actions_controller.rb +1 -4
- data/app/controllers/easy_admin/sessions_controller.rb +107 -1
- data/app/helpers/easy_admin/fields_helper.rb +8 -22
- data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +12 -0
- data/app/javascript/easy_admin/controllers/vertical_tabs_controller.js +112 -0
- data/app/javascript/easy_admin/controllers.js +3 -1
- data/app/models/easy_admin/admin_user.rb +3 -0
- data/app/views/easy_admin/profile/backup_codes_regenerated.turbo_stream.erb +12 -0
- data/app/views/easy_admin/profile/change_password.html.erb +24 -0
- data/app/views/easy_admin/profile/index.html.erb +1 -0
- data/app/views/easy_admin/profile/password_error.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/password_invalid_current.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/password_updated.turbo_stream.erb +9 -0
- data/app/views/easy_admin/profile/two_factor_backup_codes.html.erb +24 -0
- data/app/views/easy_admin/profile/two_factor_enabled.turbo_stream.erb +12 -0
- data/app/views/easy_admin/profile/two_factor_invalid_code.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/two_factor_not_enabled.turbo_stream.erb +6 -0
- data/app/views/easy_admin/profile/two_factor_setup.html.erb +24 -0
- data/app/views/easy_admin/profile/two_factor_unavailable.turbo_stream.erb +6 -0
- data/app/views/easy_admin/resources/edit.html.erb +2 -2
- data/app/views/easy_admin/resources/new.html.erb +2 -2
- data/app/views/easy_admin/resources/show.html.erb +3 -1
- data/app/views/easy_admin/sessions/two_factor_verification.html.erb +48 -0
- data/app/views/easy_admin/sessions/verify_2fa_error.turbo_stream.erb +13 -0
- data/config/routes.rb +20 -1
- data/lib/easy-admin-rails.rb +1 -0
- data/lib/easy_admin/field.rb +3 -2
- data/lib/easy_admin/layouts/builders/base_layout_builder.rb +245 -0
- data/lib/easy_admin/layouts/builders/form_layout_builder.rb +208 -0
- data/lib/easy_admin/layouts/builders/index_layout_builder.rb +22 -0
- data/lib/easy_admin/layouts/builders/show_layout_builder.rb +199 -0
- data/lib/easy_admin/layouts/dsl.rb +200 -0
- data/lib/easy_admin/layouts/layout_context.rb +189 -0
- data/lib/easy_admin/layouts/nodes/base_node.rb +88 -0
- data/lib/easy_admin/layouts/nodes/divider.rb +27 -0
- data/lib/easy_admin/layouts/nodes/field_node.rb +57 -0
- data/lib/easy_admin/layouts/nodes/grid.rb +60 -0
- data/lib/easy_admin/layouts/nodes/render_node.rb +41 -0
- data/lib/easy_admin/layouts/nodes/root.rb +25 -0
- data/lib/easy_admin/layouts/nodes/section.rb +46 -0
- data/lib/easy_admin/layouts/nodes/spacer.rb +17 -0
- data/lib/easy_admin/layouts/nodes/stubs.rb +109 -0
- data/lib/easy_admin/layouts/nodes/tab.rb +40 -0
- data/lib/easy_admin/layouts/nodes/tabs.rb +40 -0
- data/lib/easy_admin/layouts.rb +28 -0
- data/lib/easy_admin/permissions/resource_permissions.rb +1 -5
- data/lib/easy_admin/resource/base.rb +2 -2
- data/lib/easy_admin/resource/dsl.rb +2 -11
- data/lib/easy_admin/resource/field_registry.rb +58 -2
- data/lib/easy_admin/resource.rb +0 -9
- data/lib/easy_admin/resource_modules.rb +21 -4
- data/lib/easy_admin/two_factor_authentication.rb +156 -0
- data/lib/easy_admin/version.rb +1 -1
- data/lib/generators/easy_admin/permissions/install_generator.rb +0 -10
- data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +33 -3
- data/lib/generators/easy_admin/two_factor/templates/README +29 -0
- data/lib/generators/easy_admin/two_factor/templates/migration.rb +10 -0
- data/lib/generators/easy_admin/two_factor/two_factor_generator.rb +22 -0
- metadata +49 -9
- data/lib/easy_admin/resource/form_builder.rb +0 -123
- data/lib/easy_admin/resource/layout_builder.rb +0 -249
- data/lib/easy_admin/resource/show_builder.rb +0 -359
- data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +0 -6
- data/lib/generators/easy_admin/rbac/rbac_generator.rb +0 -244
- data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +0 -23
- data/lib/generators/easy_admin/rbac/templates/super_admin.rb +0 -34
@@ -0,0 +1,553 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
class FormLayoutComponent < BaseComponent
|
3
|
+
include EasyAdmin::FieldsHelper
|
4
|
+
|
5
|
+
def initialize(resource_class:, form:, record: nil)
|
6
|
+
@resource_class = resource_class
|
7
|
+
@form = form
|
8
|
+
@record = record
|
9
|
+
end
|
10
|
+
|
11
|
+
def view_template
|
12
|
+
has_custom = @resource_class.has_custom_form_layout?
|
13
|
+
|
14
|
+
if has_custom
|
15
|
+
render_custom_layout
|
16
|
+
else
|
17
|
+
render_default_layout
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def render_custom_layout
|
24
|
+
layout_type, layout_content = @resource_class.form_layout_definition
|
25
|
+
|
26
|
+
case layout_type
|
27
|
+
when :ast
|
28
|
+
render_ast_layout(layout_content)
|
29
|
+
when :component
|
30
|
+
render layout_content
|
31
|
+
else
|
32
|
+
render_default_layout
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def render_ast_layout(root_node)
|
37
|
+
if root_node
|
38
|
+
render_children(root_node)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def render_children(node)
|
43
|
+
return unless node&.children
|
44
|
+
|
45
|
+
node.children.each do |child|
|
46
|
+
render_node(child)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def render_node(node)
|
51
|
+
case node
|
52
|
+
when EasyAdmin::Layouts::Nodes::Row
|
53
|
+
render_row_node(node)
|
54
|
+
when EasyAdmin::Layouts::Nodes::Column
|
55
|
+
render_column_node(node)
|
56
|
+
when EasyAdmin::Layouts::Nodes::Card
|
57
|
+
render_card_node(node)
|
58
|
+
when EasyAdmin::Layouts::Nodes::Section
|
59
|
+
render_section_node(node)
|
60
|
+
when EasyAdmin::Layouts::Nodes::Grid
|
61
|
+
render_grid_node(node)
|
62
|
+
when EasyAdmin::Layouts::Nodes::FieldNode
|
63
|
+
render_field_node(node)
|
64
|
+
when EasyAdmin::Layouts::Nodes::Heading
|
65
|
+
render_heading_node(node)
|
66
|
+
when EasyAdmin::Layouts::Nodes::Divider
|
67
|
+
render_divider_node(node)
|
68
|
+
when EasyAdmin::Layouts::Nodes::Tabs
|
69
|
+
render_tabs_node(node)
|
70
|
+
when EasyAdmin::Layouts::Nodes::Tab
|
71
|
+
nil
|
72
|
+
when EasyAdmin::Layouts::Nodes::Content
|
73
|
+
render_content_node(node)
|
74
|
+
when EasyAdmin::Layouts::Nodes::Spacer
|
75
|
+
render_spacer_node(node)
|
76
|
+
when EasyAdmin::Layouts::Nodes::RenderNode
|
77
|
+
render_render_node(node)
|
78
|
+
else
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def render_row_node(node)
|
84
|
+
classes = ["grid", "items-stretch"]
|
85
|
+
|
86
|
+
# Check if any child column has explicit size - if so, use 12-column grid
|
87
|
+
has_sized_columns = node.children.any? { |child|
|
88
|
+
child.is_a?(EasyAdmin::Layouts::Nodes::Column) && child.attributes[:size]
|
89
|
+
}
|
90
|
+
|
91
|
+
if has_sized_columns
|
92
|
+
# Use 12-column grid for Bootstrap-style column sizing
|
93
|
+
classes << "grid-cols-1 md:grid-cols-12"
|
94
|
+
elsif node.attributes[:columns]
|
95
|
+
# Use simple equal-width columns
|
96
|
+
columns = node.attributes[:columns]
|
97
|
+
case columns
|
98
|
+
when 1
|
99
|
+
classes << "grid-cols-1"
|
100
|
+
when 2
|
101
|
+
classes << "grid-cols-1 md:grid-cols-2"
|
102
|
+
when 3
|
103
|
+
classes << "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
|
104
|
+
when 4
|
105
|
+
classes << "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
|
106
|
+
else
|
107
|
+
classes << "grid-cols-1 md:grid-cols-#{[columns, 12].min}"
|
108
|
+
end
|
109
|
+
else
|
110
|
+
classes << "grid-cols-1"
|
111
|
+
end
|
112
|
+
|
113
|
+
# Add spacing with Tailwind gap classes
|
114
|
+
spacing = node.attributes[:spacing] || "medium"
|
115
|
+
case spacing
|
116
|
+
when "small", "sm"
|
117
|
+
classes << "gap-2"
|
118
|
+
when "medium", "md"
|
119
|
+
classes << "gap-4"
|
120
|
+
when "large", "lg"
|
121
|
+
classes << "gap-6"
|
122
|
+
else
|
123
|
+
classes << "gap-4"
|
124
|
+
end
|
125
|
+
|
126
|
+
div(class: classes.join(" ")) do
|
127
|
+
render_children(node)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def render_column_node(node)
|
132
|
+
classes = ["flex", "flex-col", "h-full"]
|
133
|
+
|
134
|
+
# Handle column spanning in Tailwind CSS Grid
|
135
|
+
if node.attributes[:size]
|
136
|
+
size = node.attributes[:size]
|
137
|
+
# On mobile, all columns take full width, on desktop use specified size
|
138
|
+
case size
|
139
|
+
when 1
|
140
|
+
classes << "col-span-1 md:col-span-1"
|
141
|
+
when 2
|
142
|
+
classes << "col-span-1 md:col-span-2"
|
143
|
+
when 3
|
144
|
+
classes << "col-span-1 md:col-span-3"
|
145
|
+
when 4
|
146
|
+
classes << "col-span-1 md:col-span-4"
|
147
|
+
when 5
|
148
|
+
classes << "col-span-1 md:col-span-5"
|
149
|
+
when 6
|
150
|
+
classes << "col-span-1 md:col-span-6"
|
151
|
+
when 7
|
152
|
+
classes << "col-span-1 md:col-span-7"
|
153
|
+
when 8
|
154
|
+
classes << "col-span-1 md:col-span-8"
|
155
|
+
when 9
|
156
|
+
classes << "col-span-1 md:col-span-9"
|
157
|
+
when 10
|
158
|
+
classes << "col-span-1 md:col-span-10"
|
159
|
+
when 11
|
160
|
+
classes << "col-span-1 md:col-span-11"
|
161
|
+
when 12
|
162
|
+
classes << "col-span-1 md:col-span-12"
|
163
|
+
else
|
164
|
+
classes << "col-span-full"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
div(class: classes.join(" ")) do
|
169
|
+
render_children(node)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def render_card_node(node)
|
174
|
+
# Build card classes with Tailwind
|
175
|
+
card_classes = ["bg-white", "rounded-lg", "shadow-sm", "border", "mb-4", "flex-1", "flex", "flex-col", "h-full"]
|
176
|
+
|
177
|
+
# Add color styling with Tailwind
|
178
|
+
if node.attributes[:color]
|
179
|
+
case node.attributes[:color]
|
180
|
+
when "primary"
|
181
|
+
card_classes << "border-blue-200"
|
182
|
+
when "secondary"
|
183
|
+
card_classes << "border-gray-200"
|
184
|
+
when "success"
|
185
|
+
card_classes << "border-green-200"
|
186
|
+
when "info"
|
187
|
+
card_classes << "border-cyan-200"
|
188
|
+
when "warning"
|
189
|
+
card_classes << "border-yellow-200"
|
190
|
+
when "danger"
|
191
|
+
card_classes << "border-red-200"
|
192
|
+
when "light"
|
193
|
+
card_classes << "border-gray-100"
|
194
|
+
when "dark"
|
195
|
+
card_classes << "border-gray-800"
|
196
|
+
else
|
197
|
+
card_classes << "border-gray-200"
|
198
|
+
end
|
199
|
+
else
|
200
|
+
card_classes << "border-gray-200"
|
201
|
+
end
|
202
|
+
|
203
|
+
div(class: card_classes.join(" ")) do
|
204
|
+
# Card header
|
205
|
+
if node.attributes[:title]
|
206
|
+
header_classes = ["px-6", "py-4", "border-b", "border-gray-200"]
|
207
|
+
|
208
|
+
# Add header color styling with Tailwind
|
209
|
+
if node.attributes[:color]
|
210
|
+
case node.attributes[:color]
|
211
|
+
when "primary"
|
212
|
+
header_classes << "bg-blue-50"
|
213
|
+
when "secondary"
|
214
|
+
header_classes << "bg-gray-50"
|
215
|
+
when "success"
|
216
|
+
header_classes << "bg-green-50"
|
217
|
+
when "info"
|
218
|
+
header_classes << "bg-cyan-50"
|
219
|
+
when "warning"
|
220
|
+
header_classes << "bg-yellow-50"
|
221
|
+
when "danger"
|
222
|
+
header_classes << "bg-red-50"
|
223
|
+
when "light"
|
224
|
+
header_classes << "bg-gray-25"
|
225
|
+
when "dark"
|
226
|
+
header_classes << "bg-gray-800 text-white"
|
227
|
+
else
|
228
|
+
header_classes << "bg-gray-50"
|
229
|
+
end
|
230
|
+
else
|
231
|
+
header_classes << "bg-gray-50"
|
232
|
+
end
|
233
|
+
|
234
|
+
div(class: header_classes.join(" ")) do
|
235
|
+
h3(class: "text-lg font-semibold text-gray-900") { node.attributes[:title] }
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Card body
|
240
|
+
div(class: "px-6 py-4 flex-1") do
|
241
|
+
render_children(node)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def render_section_node(node)
|
247
|
+
div(class: "mb-6") do
|
248
|
+
if node.attributes[:title]
|
249
|
+
h3(class: "text-lg font-medium leading-6 text-gray-900 mb-4") do
|
250
|
+
node.attributes[:title]
|
251
|
+
end
|
252
|
+
end
|
253
|
+
render_children(node)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def render_grid_node(node)
|
258
|
+
columns = node.attributes[:columns] || 2
|
259
|
+
classes = ["grid"]
|
260
|
+
|
261
|
+
# Add responsive column classes with Tailwind CSS Grid
|
262
|
+
case columns
|
263
|
+
when 1
|
264
|
+
classes << "grid-cols-1"
|
265
|
+
when 2
|
266
|
+
classes << "grid-cols-1 md:grid-cols-2"
|
267
|
+
when 3
|
268
|
+
classes << "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
|
269
|
+
when 4
|
270
|
+
classes << "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
|
271
|
+
else
|
272
|
+
classes << "grid-cols-1 md:grid-cols-#{[columns, 6].min}"
|
273
|
+
end
|
274
|
+
|
275
|
+
# Add spacing
|
276
|
+
spacing = node.attributes[:spacing] || "medium"
|
277
|
+
classes << spacing_class(spacing)
|
278
|
+
|
279
|
+
div(class: classes.join(" ")) do
|
280
|
+
render_children(node)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def render_field_node(node)
|
285
|
+
base_field_config = @resource_class.find_field_config(node.field_name) || {}
|
286
|
+
field_config = base_field_config.merge(node.attributes).merge({
|
287
|
+
name: node.field_name,
|
288
|
+
label: node.attributes[:label] || base_field_config[:label] || node.field_name.to_s.humanize,
|
289
|
+
type: node.attributes[:type] || base_field_config[:type]
|
290
|
+
})
|
291
|
+
|
292
|
+
div(class: "mb-6") do
|
293
|
+
render_form_field(field_config)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def render_form_field(field_config)
|
298
|
+
if field_config[:type] == :has_many
|
299
|
+
render_has_many_form_field(field_config)
|
300
|
+
else
|
301
|
+
field_definition = {
|
302
|
+
name: field_config[:name],
|
303
|
+
type: field_config[:type] || :text,
|
304
|
+
field_type: field_config[:type] || :text,
|
305
|
+
label: field_config[:label],
|
306
|
+
required: field_config[:required],
|
307
|
+
help_text: field_config[:help_text],
|
308
|
+
options: field_config[:options],
|
309
|
+
multiple: field_config[:multiple]
|
310
|
+
}.merge(field_config)
|
311
|
+
|
312
|
+
result = render_field(field_definition, action: :form, form: @form, record: @record)
|
313
|
+
|
314
|
+
unsafe_raw result
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def render_has_many_form_field(field_config)
|
319
|
+
div(class: "mb-6") do
|
320
|
+
div(class: "block text-sm font-medium text-gray-700 mb-2") do
|
321
|
+
field_config[:label]
|
322
|
+
end
|
323
|
+
|
324
|
+
if @record
|
325
|
+
association = @record.public_send(field_config[:name])
|
326
|
+
count = association.respond_to?(:count) ? association.count : 0
|
327
|
+
|
328
|
+
div(class: "px-4 py-3 bg-gray-50 border border-gray-200 rounded-md") do
|
329
|
+
p(class: "text-sm text-gray-600") do
|
330
|
+
"#{count} #{field_config[:name].to_s.humanize.downcase} associated with this #{@resource_class.resource_name.humanize.downcase}"
|
331
|
+
end
|
332
|
+
|
333
|
+
if field_config[:help_text]
|
334
|
+
p(class: "mt-2 text-xs text-gray-500") { field_config[:help_text] }
|
335
|
+
end
|
336
|
+
|
337
|
+
p(class: "mt-2 text-xs text-gray-400") do
|
338
|
+
"Manage #{field_config[:name].to_s.humanize.downcase} through the #{field_config[:name].to_s.humanize} section"
|
339
|
+
end
|
340
|
+
end
|
341
|
+
else
|
342
|
+
div(class: "px-4 py-3 bg-gray-50 border border-gray-200 rounded-md") do
|
343
|
+
p(class: "text-sm text-gray-600") do
|
344
|
+
"#{field_config[:name].to_s.humanize} will be available after saving this #{@resource_class.resource_name.humanize.downcase}"
|
345
|
+
end
|
346
|
+
|
347
|
+
if field_config[:help_text]
|
348
|
+
p(class: "mt-2 text-xs text-gray-500") { field_config[:help_text] }
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
|
356
|
+
def render_heading_node(node)
|
357
|
+
level = node.attributes[:level] || 2
|
358
|
+
tag_name = "h#{level}"
|
359
|
+
classes = ["mb-3", "font-semibold"]
|
360
|
+
|
361
|
+
# Add appropriate text sizing based on heading level
|
362
|
+
case level
|
363
|
+
when 1
|
364
|
+
classes << "text-xl text-gray-900"
|
365
|
+
when 2
|
366
|
+
classes << "text-lg text-gray-900"
|
367
|
+
when 3
|
368
|
+
classes << "text-base text-gray-800"
|
369
|
+
when 4
|
370
|
+
classes << "text-sm text-gray-800"
|
371
|
+
else
|
372
|
+
classes << "text-sm text-gray-700"
|
373
|
+
end
|
374
|
+
|
375
|
+
send(tag_name, class: classes.join(" ")) { node.text }
|
376
|
+
end
|
377
|
+
|
378
|
+
def render_divider_node(node)
|
379
|
+
margin = node.attributes[:margin] || 4
|
380
|
+
hr(class: "my-#{margin} border-gray-200")
|
381
|
+
end
|
382
|
+
|
383
|
+
def render_tabs_node(node)
|
384
|
+
tabs_id = node.attributes[:id] || "form-tabs-#{SecureRandom.hex(4)}"
|
385
|
+
|
386
|
+
div(class: "form-tabs",
|
387
|
+
data: {
|
388
|
+
controller: "form-tabs",
|
389
|
+
action: "tab:selected@window->form-tabs#handleTabSelection"
|
390
|
+
}) do
|
391
|
+
|
392
|
+
render_tab_navigation(node, tabs_id)
|
393
|
+
|
394
|
+
render_tab_content(node, tabs_id)
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
def render_tab_navigation(tabs_node, tabs_id)
|
399
|
+
nav(class: "border-b border-gray-200 mb-4 sm:mb-6") do
|
400
|
+
# Desktop tabs
|
401
|
+
ul(class: "flex flex-wrap -mb-px text-sm font-medium text-center", role: "tablist") do
|
402
|
+
tabs_node.children.each_with_index do |tab_node, index|
|
403
|
+
next unless tab_node.is_a?(EasyAdmin::Layouts::Nodes::Tab)
|
404
|
+
|
405
|
+
li(class: "mr-2", role: "presentation") do
|
406
|
+
button(
|
407
|
+
class: tab_button_classes(index == 0),
|
408
|
+
id: "#{tab_node.attributes[:name]}-tab",
|
409
|
+
data: {
|
410
|
+
form_tabs_target: "tabButton",
|
411
|
+
tab_id: tab_node.attributes[:name],
|
412
|
+
action: "click->form-tabs#switchTab"
|
413
|
+
},
|
414
|
+
type: "button",
|
415
|
+
role: "tab"
|
416
|
+
) do
|
417
|
+
if tab_node.attributes[:icon]
|
418
|
+
span(class: "inline-flex items-center") do
|
419
|
+
unsafe_raw(tab_icon(tab_node.attributes[:icon]))
|
420
|
+
span(class: "ml-2") { tab_node.label }
|
421
|
+
end
|
422
|
+
else
|
423
|
+
plain tab_node.label
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
def render_tab_content(tabs_node, tabs_id)
|
433
|
+
Rails.logger.info "🔍 [FormLayout] render_tab_content started"
|
434
|
+
div(class: "tab-content") do
|
435
|
+
tabs_node.children.each_with_index do |tab_node, index|
|
436
|
+
next unless tab_node.is_a?(EasyAdmin::Layouts::Nodes::Tab)
|
437
|
+
|
438
|
+
div(
|
439
|
+
class: tab_panel_classes(index == 0),
|
440
|
+
id: "#{tab_node.attributes[:name]}-panel",
|
441
|
+
data: { form_tabs_target: "tabPanel" },
|
442
|
+
role: "tabpanel"
|
443
|
+
) do
|
444
|
+
div(class: "bg-white shadow-sm rounded-lg border border-gray-200") do
|
445
|
+
div(class: "px-4 py-5 sm:p-6") do
|
446
|
+
if tab_node.label && !tab_node.label.empty?
|
447
|
+
h3(class: "text-lg font-medium leading-6 text-gray-900 mb-6") do
|
448
|
+
tab_node.label
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
div(class: "grid grid-cols-1 gap-6") do
|
453
|
+
render_children(tab_node)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
def render_content_node(node)
|
463
|
+
if node.block
|
464
|
+
context = EasyAdmin::Layouts::LayoutContext.new(
|
465
|
+
record: @record,
|
466
|
+
resource_class: @resource_class,
|
467
|
+
form_builder: @form
|
468
|
+
)
|
469
|
+
result = context.instance_exec(&node.block)
|
470
|
+
if result.respond_to?(:call)
|
471
|
+
render result
|
472
|
+
else
|
473
|
+
unsafe_raw result.to_s
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
def render_default_layout
|
479
|
+
div(class: "bg-white shadow-sm rounded-lg border border-gray-200") do
|
480
|
+
div(class: "px-4 py-5 sm:p-6") do
|
481
|
+
div(class: "grid grid-cols-1 gap-6") do
|
482
|
+
@resource_class.fields_config.each do |field_config|
|
483
|
+
next if field_config[:readonly] || field_config[:form] == false
|
484
|
+
render_form_field(field_config)
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
def tab_button_classes(active = false)
|
492
|
+
base_classes = "inline-flex items-center p-4 border-b-2 rounded-t-lg transition-colors duration-200"
|
493
|
+
|
494
|
+
if active
|
495
|
+
active_classes = "text-blue-600 border-blue-600"
|
496
|
+
else
|
497
|
+
inactive_classes = "text-gray-500 border-transparent hover:text-gray-600 hover:border-gray-300"
|
498
|
+
end
|
499
|
+
|
500
|
+
"#{base_classes} #{active ? active_classes : inactive_classes}"
|
501
|
+
end
|
502
|
+
|
503
|
+
def tab_panel_classes(active = false)
|
504
|
+
base_classes = "tab-panel"
|
505
|
+
visibility_classes = active ? "block" : "hidden"
|
506
|
+
|
507
|
+
"#{base_classes} #{visibility_classes}"
|
508
|
+
end
|
509
|
+
|
510
|
+
def tab_icon(icon_name)
|
511
|
+
case icon_name.to_sym
|
512
|
+
when :user
|
513
|
+
'<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>'
|
514
|
+
when :security
|
515
|
+
'<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>'
|
516
|
+
when :settings, :info
|
517
|
+
'<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>'
|
518
|
+
else
|
519
|
+
'<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>'
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
def render_spacer_node(node)
|
524
|
+
div(class: "my-#{node.attributes[:size] || 4}")
|
525
|
+
end
|
526
|
+
|
527
|
+
def render_render_node(node)
|
528
|
+
if node.component_class
|
529
|
+
begin
|
530
|
+
component = node.component_class.constantize.new(**node.attributes)
|
531
|
+
render component
|
532
|
+
rescue => e
|
533
|
+
div(class: "bg-yellow-50 border border-yellow-200 text-yellow-800 px-4 py-3 rounded-md") do
|
534
|
+
"Component class not found: #{node.component_class}"
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
def spacing_class(spacing)
|
541
|
+
case spacing
|
542
|
+
when "small", "sm"
|
543
|
+
"gap-2"
|
544
|
+
when "medium", "md"
|
545
|
+
"gap-4"
|
546
|
+
when "large", "lg"
|
547
|
+
"gap-6"
|
548
|
+
else
|
549
|
+
"gap-4"
|
550
|
+
end
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
@@ -160,17 +160,28 @@ module EasyAdmin
|
|
160
160
|
data: { dropdown_target: "menu" }
|
161
161
|
) do
|
162
162
|
div(class: "py-1") do
|
163
|
-
render_menu_item("
|
163
|
+
render_menu_item("Settings", settings_menu_icon, EasyAdmin::Engine.routes.url_helpers.profile_path, "text-gray-700 hover:bg-gray-100")
|
164
|
+
render_menu_item("Sign out", logout_icon, EasyAdmin::Engine.routes.url_helpers.destroy_admin_user_session_path, "text-red-600 hover:bg-red-50", method: :delete)
|
164
165
|
end
|
165
166
|
end
|
166
167
|
end
|
167
168
|
end
|
168
169
|
|
169
|
-
def render_menu_item(label, icon_svg, url, extra_classes = "text-gray-700 hover:bg-gray-100")
|
170
|
-
|
170
|
+
def render_menu_item(label, icon_svg, url, extra_classes = "text-gray-700 hover:bg-gray-100", method: :get)
|
171
|
+
link_options = {
|
171
172
|
href: url,
|
172
173
|
class: "block px-4 py-2 text-sm #{extra_classes}"
|
173
|
-
|
174
|
+
}
|
175
|
+
|
176
|
+
# Add method and confirmation for delete operations
|
177
|
+
if method == :delete
|
178
|
+
link_options[:data] = {
|
179
|
+
turbo_method: :delete,
|
180
|
+
turbo_confirm: "Are you sure you want to sign out?"
|
181
|
+
}
|
182
|
+
end
|
183
|
+
|
184
|
+
a(**link_options) do
|
174
185
|
div(class: "flex items-center space-x-2") do
|
175
186
|
unsafe_raw(icon_svg)
|
176
187
|
span { label }
|
@@ -219,6 +230,10 @@ module EasyAdmin
|
|
219
230
|
'<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>'
|
220
231
|
end
|
221
232
|
|
233
|
+
def settings_menu_icon
|
234
|
+
'<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>'
|
235
|
+
end
|
236
|
+
|
222
237
|
def logout_icon
|
223
238
|
'<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="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>'
|
224
239
|
end
|
@@ -14,8 +14,6 @@ module EasyAdmin
|
|
14
14
|
# Get actual permissions from permissions_cache
|
15
15
|
@user_permissions = get_user_permissions_from_cache(user)
|
16
16
|
@available_resources = EasyAdmin::Permissions.available_resources
|
17
|
-
|
18
|
-
Rails.logger.debug "UserRolePermissionsComponent: user=#{@user&.id}, role=#{@current_role&.name}, cached_permissions=#{@user_permissions.size}"
|
19
17
|
end
|
20
18
|
|
21
19
|
def view_template
|
@@ -192,4 +190,4 @@ module EasyAdmin
|
|
192
190
|
end
|
193
191
|
end
|
194
192
|
end
|
195
|
-
end
|
193
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Profile
|
3
|
+
class ChangePasswordModalComponent < BaseComponent
|
4
|
+
def initialize(user:)
|
5
|
+
@user = user
|
6
|
+
end
|
7
|
+
|
8
|
+
def view_template
|
9
|
+
div do
|
10
|
+
p(class: "text-sm text-gray-600 mb-6") { "Update your password to keep your account secure" }
|
11
|
+
|
12
|
+
form(action: EasyAdmin::Engine.routes.url_helpers.update_password_path, method: :patch, data: { turbo_frame: "_top" }) do
|
13
|
+
input(type: "hidden", name: "authenticity_token", value: helpers.form_authenticity_token) if helpers.respond_to?(:form_authenticity_token)
|
14
|
+
input(type: "hidden", name: "_method", value: "patch")
|
15
|
+
|
16
|
+
div(class: "space-y-4") do
|
17
|
+
# Current Password
|
18
|
+
div do
|
19
|
+
label(for: "admin_user_current_password", class: "block text-sm font-medium text-gray-700 mb-2") { "Current Password" }
|
20
|
+
input(
|
21
|
+
type: "password",
|
22
|
+
name: "admin_user[current_password]",
|
23
|
+
id: "admin_user_current_password",
|
24
|
+
class: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
25
|
+
placeholder: "Enter current password",
|
26
|
+
required: true
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
# New Password
|
31
|
+
div do
|
32
|
+
label(for: "admin_user_password", class: "block text-sm font-medium text-gray-700 mb-2") { "New Password" }
|
33
|
+
input(
|
34
|
+
type: "password",
|
35
|
+
name: "admin_user[password]",
|
36
|
+
id: "admin_user_password",
|
37
|
+
class: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
38
|
+
placeholder: "Enter new password",
|
39
|
+
required: true
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Confirm Password
|
44
|
+
div do
|
45
|
+
label(for: "admin_user_password_confirmation", class: "block text-sm font-medium text-gray-700 mb-2") { "Confirm New Password" }
|
46
|
+
input(
|
47
|
+
type: "password",
|
48
|
+
name: "admin_user[password_confirmation]",
|
49
|
+
id: "admin_user_password_confirmation",
|
50
|
+
class: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
51
|
+
placeholder: "Confirm new password",
|
52
|
+
required: true
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Submit button
|
57
|
+
div(class: "flex space-x-3 pt-4") do
|
58
|
+
input(
|
59
|
+
type: "submit",
|
60
|
+
value: "Update Password",
|
61
|
+
class: "flex-1 bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors cursor-pointer"
|
62
|
+
)
|
63
|
+
button(
|
64
|
+
type: "button",
|
65
|
+
class: "flex-1 bg-gray-300 text-gray-700 py-2 px-4 rounded-lg hover:bg-gray-400 transition-colors",
|
66
|
+
data: { action: "click->modal#close" }
|
67
|
+
) { "Cancel" }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|