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,160 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Resources
|
3
|
+
class TableRowComponent < BaseComponent
|
4
|
+
def initialize(record:, resource_class:)
|
5
|
+
@record = record
|
6
|
+
@resource_class = resource_class
|
7
|
+
end
|
8
|
+
|
9
|
+
def view_template
|
10
|
+
tr(
|
11
|
+
id: helpers.dom_id(@record),
|
12
|
+
class: "group hover:bg-gray-100 transition-colors duration-150",
|
13
|
+
data: {
|
14
|
+
controller: "context-menu",
|
15
|
+
action: "contextmenu->context-menu#showMenu",
|
16
|
+
context_menu_record_id_value: @record.id,
|
17
|
+
context_menu_resource_name_value: @resource_class.route_key
|
18
|
+
}
|
19
|
+
) do
|
20
|
+
# Checkbox cell for batch actions
|
21
|
+
if @resource_class.batch_actions_enabled
|
22
|
+
td(class: "relative w-16 sm:w-12 px-3 sm:px-6 py-4") do
|
23
|
+
div(class: "absolute left-2 sm:left-4 top-1/2 -mt-3") do
|
24
|
+
render_ios_checkbox(
|
25
|
+
target: "checkbox",
|
26
|
+
action: "change->batch-selection#toggleItem",
|
27
|
+
value: @record.id
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
@resource_class.index_fields.each_with_index do |field_config, index|
|
34
|
+
render EasyAdmin::Resources::TableCellComponent.new(
|
35
|
+
record: @record,
|
36
|
+
field_config: field_config,
|
37
|
+
resource_class: @resource_class,
|
38
|
+
mobile_hide: index > 2
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
render_actions_cell
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def render_actions_cell
|
49
|
+
td(class: "relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6") do
|
50
|
+
div(class: "flex items-center justify-end space-x-2 sm:mobile-actions") do
|
51
|
+
render_view_action
|
52
|
+
render_edit_action
|
53
|
+
render_delete_action
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def render_view_action
|
59
|
+
a(
|
60
|
+
href: resource_show_url,
|
61
|
+
class: "inline-flex items-center justify-center w-8 h-8 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors duration-150",
|
62
|
+
data: {
|
63
|
+
action: "click->table-row#stopPropagation",
|
64
|
+
turbo: "false"
|
65
|
+
},
|
66
|
+
title: "View"
|
67
|
+
) do
|
68
|
+
unsafe_raw <<~SVG
|
69
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
70
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
71
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
72
|
+
</svg>
|
73
|
+
SVG
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def render_edit_action
|
78
|
+
a(
|
79
|
+
href: resource_edit_url,
|
80
|
+
class: "inline-flex items-center justify-center w-8 h-8 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors duration-150",
|
81
|
+
data: {
|
82
|
+
action: "click->table-row#stopPropagation",
|
83
|
+
turbo: "false"
|
84
|
+
},
|
85
|
+
title: "Edit"
|
86
|
+
) do
|
87
|
+
unsafe_raw <<~SVG
|
88
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
89
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
90
|
+
</svg>
|
91
|
+
SVG
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def render_delete_action
|
96
|
+
a(
|
97
|
+
href: resource_show_url,
|
98
|
+
method: :delete,
|
99
|
+
class: "inline-flex items-center justify-center w-8 h-8 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-lg transition-colors duration-150",
|
100
|
+
data: {
|
101
|
+
action: "click->table-row#stopPropagation",
|
102
|
+
confirm: "Are you sure you want to delete this #{@resource_class.singular_title.downcase}?",
|
103
|
+
method: :delete,
|
104
|
+
turbo: "false"
|
105
|
+
},
|
106
|
+
title: "Delete"
|
107
|
+
) do
|
108
|
+
unsafe_raw <<~SVG
|
109
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
110
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
111
|
+
</svg>
|
112
|
+
SVG
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def resource_show_url
|
117
|
+
easy_admin_url_helpers.resource_path(@resource_class.route_key, @record)
|
118
|
+
end
|
119
|
+
|
120
|
+
def resource_edit_url
|
121
|
+
easy_admin_url_helpers.edit_resource_path(@resource_class.route_key, @record)
|
122
|
+
end
|
123
|
+
|
124
|
+
def render_ios_checkbox(target: nil, action: nil, value: nil, data: {})
|
125
|
+
label(class: "inline-flex items-center cursor-pointer group") do
|
126
|
+
input(
|
127
|
+
type: "checkbox",
|
128
|
+
value: value,
|
129
|
+
class: "sr-only peer",
|
130
|
+
data: data.merge({
|
131
|
+
batch_selection_target: target,
|
132
|
+
action: action
|
133
|
+
}.compact)
|
134
|
+
)
|
135
|
+
|
136
|
+
# Custom checkbox design
|
137
|
+
div(
|
138
|
+
class: "relative w-6 h-6 bg-white border-2 border-gray-300 rounded-md
|
139
|
+
transition-all duration-200 ease-out
|
140
|
+
peer-checked:bg-blue-500 peer-checked:border-blue-500
|
141
|
+
peer-focus:ring-2 peer-focus:ring-blue-500 peer-focus:ring-opacity-50
|
142
|
+
group-hover:border-gray-400 peer-checked:group-hover:border-blue-600
|
143
|
+
shadow-sm"
|
144
|
+
) do
|
145
|
+
# Checkmark icon (hidden by default, shown when checked)
|
146
|
+
unsafe_raw <<~SVG
|
147
|
+
<svg class="absolute inset-0 w-4 h-4 m-auto text-white opacity-0
|
148
|
+
transition-opacity duration-200 ease-out
|
149
|
+
peer-checked:opacity-100 pointer-events-none"
|
150
|
+
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
151
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3"
|
152
|
+
d="M5 13l4 4L19 7"/>
|
153
|
+
</svg>
|
154
|
+
SVG
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
class RowActionFormComponent < BaseComponent
|
3
|
+
def initialize(record:, action_class:, resource_class:, submit_url:)
|
4
|
+
@record = record
|
5
|
+
@action_class = action_class
|
6
|
+
@resource_class = resource_class
|
7
|
+
@submit_url = submit_url
|
8
|
+
end
|
9
|
+
|
10
|
+
def view_template
|
11
|
+
div(
|
12
|
+
id: "modal",
|
13
|
+
class: "fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50",
|
14
|
+
data: {
|
15
|
+
controller: "row-modal",
|
16
|
+
row_modal_submit_url_value: @submit_url,
|
17
|
+
row_modal_record_id_value: @record.id,
|
18
|
+
row_modal_action_class_value: @action_class.name
|
19
|
+
}
|
20
|
+
) do
|
21
|
+
div(class: "relative top-20 mx-auto p-5 border w-11/12 max-w-md shadow-lg rounded-md bg-white") do
|
22
|
+
# Header
|
23
|
+
div(class: "flex items-center justify-between pb-4 mb-4 border-b border-gray-200") do
|
24
|
+
h3(class: "text-lg font-semibold text-gray-900") do
|
25
|
+
@action_class.modal_title || @action_class.label || @action_class.name.humanize
|
26
|
+
end
|
27
|
+
button(
|
28
|
+
type: "button",
|
29
|
+
class: "text-gray-400 hover:text-gray-600 text-xl",
|
30
|
+
data: { action: "click->row-modal#close" }
|
31
|
+
) { "×" }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Description
|
35
|
+
if @action_class.modal_description.present?
|
36
|
+
p(class: "text-sm text-gray-600 mb-4") { @action_class.modal_description }
|
37
|
+
end
|
38
|
+
|
39
|
+
# Record info
|
40
|
+
div(class: "mb-4 p-3 bg-gray-50 rounded") do
|
41
|
+
div(class: "text-sm text-gray-600") do
|
42
|
+
"Record: #{@record.respond_to?(:title) ? @record.title : @record.to_s}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Simple form fields
|
47
|
+
div(class: "space-y-4", data: { row_modal_target: "form" }) do
|
48
|
+
render_simple_fields
|
49
|
+
end
|
50
|
+
|
51
|
+
# Action buttons
|
52
|
+
div(class: "flex justify-end space-x-3 pt-4 border-t border-gray-200 mt-4") do
|
53
|
+
button(
|
54
|
+
type: "button",
|
55
|
+
class: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50",
|
56
|
+
data: { action: "click->row-modal#close" }
|
57
|
+
) { "Cancel" }
|
58
|
+
|
59
|
+
button(
|
60
|
+
type: "button",
|
61
|
+
class: "px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700",
|
62
|
+
data: { action: "click->row-modal#submit" }
|
63
|
+
) { @action_class.label }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def render_simple_fields
|
72
|
+
@action_class.attribute_types.each do |attr_name, attr_type|
|
73
|
+
next if attr_name.in?(['record', 'current_user', 'resource_class', 'params', 'selected_records'])
|
74
|
+
|
75
|
+
field_config = {
|
76
|
+
name: attr_name,
|
77
|
+
type: determine_field_type(attr_type),
|
78
|
+
label: attr_name.humanize,
|
79
|
+
form_name: "row_action"
|
80
|
+
}
|
81
|
+
|
82
|
+
# Get current value from the action instance
|
83
|
+
action_instance = @action_class.new(record: @record, resource_class: @resource_class)
|
84
|
+
current_value = action_instance.public_send(attr_name)
|
85
|
+
|
86
|
+
# Create a simple record-like object for the field component
|
87
|
+
field_record = OpenStruct.new(attr_name => current_value)
|
88
|
+
|
89
|
+
# Create a mock form object
|
90
|
+
mock_form = OpenStruct.new(
|
91
|
+
object: OpenStruct.new(class: OpenStruct.new(name: OpenStruct.new(underscore: 'row_action')))
|
92
|
+
)
|
93
|
+
|
94
|
+
component = field_component(
|
95
|
+
field_config,
|
96
|
+
action: :form,
|
97
|
+
value: current_value,
|
98
|
+
record: field_record,
|
99
|
+
form: mock_form
|
100
|
+
)
|
101
|
+
|
102
|
+
render component
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def determine_field_type(attr_type)
|
107
|
+
case attr_type.type
|
108
|
+
when :string
|
109
|
+
:text
|
110
|
+
when :text
|
111
|
+
:textarea
|
112
|
+
when :boolean
|
113
|
+
:boolean
|
114
|
+
when :integer, :float
|
115
|
+
:number
|
116
|
+
when :date
|
117
|
+
:date
|
118
|
+
when :datetime
|
119
|
+
:datetime
|
120
|
+
when :json
|
121
|
+
:json
|
122
|
+
else
|
123
|
+
:text
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
class ScopesComponent < Phlex::HTML
|
3
|
+
def initialize(resource_class:, current_scope: nil, counts: {})
|
4
|
+
@resource_class = resource_class
|
5
|
+
@current_scope = current_scope
|
6
|
+
@counts = counts
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def view_template
|
12
|
+
return unless @resource_class.has_scopes?
|
13
|
+
|
14
|
+
div(class: "mb-6") do
|
15
|
+
div(class: "sm:hidden") do
|
16
|
+
# Mobile dropdown for scopes
|
17
|
+
render_mobile_scope_selector
|
18
|
+
end
|
19
|
+
|
20
|
+
div(class: "hidden sm:block") do
|
21
|
+
# Desktop iOS-style segmented control
|
22
|
+
render_desktop_scope_tabs
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def render_mobile_scope_selector
|
28
|
+
# Beautiful mobile dropdown using dropdown controller
|
29
|
+
div(class: "relative", data: { controller: "dropdown" }) do
|
30
|
+
# Trigger button showing current scope
|
31
|
+
button(
|
32
|
+
type: "button",
|
33
|
+
class: "w-full flex items-center justify-between px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500",
|
34
|
+
data: {
|
35
|
+
action: "click->dropdown#toggle",
|
36
|
+
dropdown_target: "trigger"
|
37
|
+
}
|
38
|
+
) do
|
39
|
+
# Current scope display
|
40
|
+
div(class: "flex items-center space-x-3") do
|
41
|
+
current_scope_config = @resource_class.scopes.find { |s| is_current_scope?(s) }
|
42
|
+
if current_scope_config
|
43
|
+
if current_scope_config[:icon]
|
44
|
+
span(class: "text-xl") { current_scope_config[:icon] }
|
45
|
+
end
|
46
|
+
div(class: "flex flex-col items-start") do
|
47
|
+
span(class: "font-medium text-gray-900") { current_scope_config[:label] }
|
48
|
+
if current_scope_config[:count] && @counts[current_scope_config[:name]]
|
49
|
+
span(class: "text-xs text-gray-500") { "#{@counts[current_scope_config[:name]]} items" }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Chevron
|
56
|
+
svg(
|
57
|
+
class: "w-5 h-5 text-gray-400 transition-transform duration-200",
|
58
|
+
fill: "none",
|
59
|
+
stroke: "currentColor",
|
60
|
+
viewBox: "0 0 24 24",
|
61
|
+
data: { dropdown_target: "chevron" }
|
62
|
+
) do |svg|
|
63
|
+
svg.path(
|
64
|
+
stroke_linecap: "round",
|
65
|
+
stroke_linejoin: "round",
|
66
|
+
stroke_width: "2",
|
67
|
+
d: "M19 9l-7 7-7-7"
|
68
|
+
)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Dropdown menu
|
73
|
+
div(
|
74
|
+
class: "absolute z-10 w-full mt-1 bg-white border border-gray-200 rounded-xl shadow-lg hidden",
|
75
|
+
data: { dropdown_target: "menu" }
|
76
|
+
) do
|
77
|
+
div(class: "py-2") do
|
78
|
+
@resource_class.scopes.each do |scope|
|
79
|
+
render_mobile_scope_option(scope)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def render_mobile_scope_option(scope)
|
87
|
+
is_current = is_current_scope?(scope)
|
88
|
+
|
89
|
+
a(
|
90
|
+
href: scope_path(scope),
|
91
|
+
class: "flex items-center justify-between px-4 py-3 text-sm hover:bg-gray-50 #{is_current ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-700'}",
|
92
|
+
data: { turbo_prefetch: "false" }
|
93
|
+
) do
|
94
|
+
div(class: "flex items-center space-x-3") do
|
95
|
+
# Icon
|
96
|
+
if scope[:icon]
|
97
|
+
span(class: "text-lg") { scope[:icon] }
|
98
|
+
end
|
99
|
+
|
100
|
+
# Label and count
|
101
|
+
div(class: "flex flex-col items-start") do
|
102
|
+
span(class: "font-medium") { scope[:label] }
|
103
|
+
if scope[:count] && @counts[scope[:name]]
|
104
|
+
span(class: "text-xs #{is_current ? 'text-blue-600' : 'text-gray-500'}") do
|
105
|
+
"#{@counts[scope[:name]]} items"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Checkmark for current selection
|
112
|
+
if is_current
|
113
|
+
svg(
|
114
|
+
class: "w-5 h-5 text-blue-600",
|
115
|
+
fill: "currentColor",
|
116
|
+
viewBox: "0 0 20 20"
|
117
|
+
) do |svg|
|
118
|
+
svg.path(
|
119
|
+
fill_rule: "evenodd",
|
120
|
+
d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",
|
121
|
+
clip_rule: "evenodd"
|
122
|
+
)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def render_desktop_scope_tabs
|
129
|
+
nav(class: "isolate flex rounded-xl bg-gray-100 p-1 shadow-sm") do
|
130
|
+
@resource_class.scopes.each_with_index do |scope, index|
|
131
|
+
render_scope_tab(scope, index)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def render_scope_tab(scope, index)
|
137
|
+
is_current = is_current_scope?(scope)
|
138
|
+
is_first = index == 0
|
139
|
+
is_last = index == @resource_class.scopes.length - 1
|
140
|
+
|
141
|
+
a(
|
142
|
+
href: scope_path(scope),
|
143
|
+
class: scope_tab_classes(is_current, is_first, is_last),
|
144
|
+
data: { turbo_prefetch: "false" }
|
145
|
+
) do
|
146
|
+
div(class: "flex items-center space-x-2") do
|
147
|
+
# Icon
|
148
|
+
if scope[:icon]
|
149
|
+
span(class: "text-lg") { scope[:icon] }
|
150
|
+
end
|
151
|
+
|
152
|
+
# Label and count
|
153
|
+
div do
|
154
|
+
span(class: "font-medium") { scope[:label] }
|
155
|
+
if scope[:count] && @counts[scope[:name]]
|
156
|
+
span(class: count_badge_classes(is_current)) do
|
157
|
+
@counts[scope[:name]].to_s
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def scope_tab_classes(is_current, is_first, is_last)
|
166
|
+
classes = [
|
167
|
+
"group relative min-w-0 flex-1 overflow-hidden py-3 px-4 text-center text-sm font-medium transition-all duration-200 ease-in-out focus:z-10"
|
168
|
+
]
|
169
|
+
|
170
|
+
if is_current
|
171
|
+
classes << "bg-white text-gray-900 shadow-sm ring-1 ring-gray-300"
|
172
|
+
else
|
173
|
+
classes << "text-gray-600 hover:bg-white/50 hover:text-gray-900"
|
174
|
+
end
|
175
|
+
|
176
|
+
if is_first
|
177
|
+
classes << "rounded-l-lg"
|
178
|
+
end
|
179
|
+
|
180
|
+
if is_last
|
181
|
+
classes << "rounded-r-lg"
|
182
|
+
end
|
183
|
+
|
184
|
+
classes.join(" ")
|
185
|
+
end
|
186
|
+
|
187
|
+
def count_badge_classes(is_current)
|
188
|
+
base_classes = "ml-2 inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
|
189
|
+
|
190
|
+
if is_current
|
191
|
+
"#{base_classes} bg-blue-100 text-blue-800"
|
192
|
+
else
|
193
|
+
"#{base_classes} bg-gray-200 text-gray-700 group-hover:bg-blue-100 group-hover:text-blue-800"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def scope_path(scope)
|
198
|
+
query_params = {}
|
199
|
+
query_params[:scope] = scope[:name] unless scope[:name] == :all
|
200
|
+
|
201
|
+
# Use engine route helper and build query string manually
|
202
|
+
base_path = EasyAdmin::Engine.routes.url_helpers.resources_path(@resource_class.route_key)
|
203
|
+
query_string = query_params.any? ? query_params.to_query : ""
|
204
|
+
query_string.present? ? "#{base_path}?#{query_string}" : base_path
|
205
|
+
end
|
206
|
+
|
207
|
+
def is_current_scope?(scope)
|
208
|
+
if @current_scope
|
209
|
+
@current_scope.to_s == scope[:name].to_s
|
210
|
+
else
|
211
|
+
scope[:default] || scope[:name] == :all
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def scope_label_with_count(scope)
|
216
|
+
label = scope[:label]
|
217
|
+
if scope[:count] && @counts[scope[:name]]
|
218
|
+
"#{label} (#{@counts[scope[:name]]})"
|
219
|
+
else
|
220
|
+
label
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
class SettingsSidebarComponent < Phlex::HTML
|
3
|
+
register_element :turbo_frame
|
4
|
+
|
5
|
+
def initialize(feature_toggles: [], expanded: false)
|
6
|
+
@feature_toggles = feature_toggles
|
7
|
+
@expanded = expanded
|
8
|
+
end
|
9
|
+
|
10
|
+
def view_template
|
11
|
+
div(
|
12
|
+
id: "settings-sidebar",
|
13
|
+
class: "fixed inset-y-0 right-0 z-50 w-96 bg-white shadow-2xl transform transition-all duration-500 ease-out #{@expanded ? 'translate-x-0' : 'translate-x-full'} backdrop-blur-sm",
|
14
|
+
style: "background: rgba(255, 255, 255, 0.95);",
|
15
|
+
data: {
|
16
|
+
controller: "settings-sidebar",
|
17
|
+
settings_sidebar_target: "container"
|
18
|
+
}
|
19
|
+
) do
|
20
|
+
# Header
|
21
|
+
div(class: "relative px-6 py-6 bg-gradient-to-br from-blue-50 via-white to-purple-50 border-b border-gray-100") do
|
22
|
+
div(class: "text-center") do
|
23
|
+
div(class: "inline-flex items-center justify-center w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full mb-3 shadow-lg") do
|
24
|
+
unsafe_raw <<~SVG
|
25
|
+
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
26
|
+
<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"/>
|
27
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
28
|
+
</svg>
|
29
|
+
SVG
|
30
|
+
end
|
31
|
+
h2(class: "text-xl font-bold bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent") { "Settings" }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Close button - iOS style
|
35
|
+
button(
|
36
|
+
class: "absolute top-4 right-4 w-8 h-8 flex items-center justify-center bg-gray-100 hover:bg-gray-200 rounded-full transition-all duration-200 transform hover:scale-105",
|
37
|
+
data: { action: "click->settings-sidebar#close" },
|
38
|
+
title: "Close"
|
39
|
+
) do
|
40
|
+
unsafe_raw <<~SVG
|
41
|
+
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
42
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"></path>
|
43
|
+
</svg>
|
44
|
+
SVG
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Content
|
49
|
+
div(class: "flex-1 overflow-y-auto px-6 py-8 bg-gradient-to-b from-gray-50/30 to-white") do
|
50
|
+
turbo_frame(
|
51
|
+
id: "settings-form",
|
52
|
+
class: "space-y-8"
|
53
|
+
) do
|
54
|
+
form(
|
55
|
+
action: route_helper.settings_path,
|
56
|
+
method: "patch",
|
57
|
+
class: "space-y-8"
|
58
|
+
) do
|
59
|
+
# Feature Toggles Section
|
60
|
+
div do
|
61
|
+
h3(class: "text-md font-medium text-gray-900 mb-4") { "Feature Toggles" }
|
62
|
+
|
63
|
+
div(class: "space-y-4") do
|
64
|
+
@feature_toggles.each do |feature|
|
65
|
+
render_feature_toggle(feature)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Save button
|
71
|
+
div(class: "pt-6 border-t border-gray-200") do
|
72
|
+
button(
|
73
|
+
type: "submit",
|
74
|
+
class: "w-full inline-flex items-center justify-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
75
|
+
) do
|
76
|
+
unsafe_raw <<~SVG
|
77
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
78
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
79
|
+
</svg>
|
80
|
+
SVG
|
81
|
+
span { "Save Settings" }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def route_helper
|
94
|
+
@route_helper ||= EasyAdmin::Engine.routes.url_helpers
|
95
|
+
end
|
96
|
+
|
97
|
+
def render_feature_toggle(feature)
|
98
|
+
div(class: "flex items-center justify-between p-4 bg-gray-50 rounded-lg border border-gray-200") do
|
99
|
+
div(class: "flex-1") do
|
100
|
+
label(
|
101
|
+
for: "features_#{feature[:name]}",
|
102
|
+
class: "block text-sm font-medium text-gray-900 cursor-pointer"
|
103
|
+
) { feature[:name].humanize }
|
104
|
+
|
105
|
+
if feature[:description].present?
|
106
|
+
p(class: "text-xs text-gray-600 mt-1") { feature[:description] }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Toggle switch
|
111
|
+
div(class: "ml-4") do
|
112
|
+
input(
|
113
|
+
type: "hidden",
|
114
|
+
name: "features[#{feature[:name]}]",
|
115
|
+
value: "0"
|
116
|
+
)
|
117
|
+
|
118
|
+
input(
|
119
|
+
type: "checkbox",
|
120
|
+
id: "features_#{feature[:name]}",
|
121
|
+
name: "features[#{feature[:name]}]",
|
122
|
+
value: "1",
|
123
|
+
checked: feature[:enabled],
|
124
|
+
class: "sr-only",
|
125
|
+
data: { action: "change->settings-sidebar#toggleFeature" }
|
126
|
+
)
|
127
|
+
|
128
|
+
label(
|
129
|
+
for: "features_#{feature[:name]}",
|
130
|
+
class: "relative inline-flex items-center h-6 rounded-full w-11 cursor-pointer transition-colors ease-in-out duration-200 #{feature[:enabled] ? 'bg-blue-600' : 'bg-gray-300'}"
|
131
|
+
) do
|
132
|
+
span(
|
133
|
+
class: "inline-block w-4 h-4 transform bg-white rounded-full transition ease-in-out duration-200 #{feature[:enabled] ? 'translate-x-6' : 'translate-x-1'}"
|
134
|
+
)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|