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,52 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Dashboards
|
3
|
+
class TableCardComponent < BaseCardComponent
|
4
|
+
private
|
5
|
+
|
6
|
+
def render_card_actions
|
7
|
+
if card[:show_view_all]
|
8
|
+
a(
|
9
|
+
href: card[:view_all_url] || "#",
|
10
|
+
class: "inline-flex items-center px-3 py-1.5 bg-blue-50 text-blue-700 text-sm font-medium rounded-lg hover:bg-blue-100 transition-colors"
|
11
|
+
) do
|
12
|
+
plain "View All"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
if card[:allow_export]
|
17
|
+
button(
|
18
|
+
class: "inline-flex items-center px-3 py-1.5 bg-gray-100 text-gray-700 text-sm font-medium rounded-lg hover:bg-gray-200 transition-colors ml-2",
|
19
|
+
type: "button"
|
20
|
+
) do
|
21
|
+
plain "Export"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def render_skeleton
|
27
|
+
div(class: "animate-pulse") do
|
28
|
+
# Table header skeleton
|
29
|
+
div(class: "grid gap-4 mb-4", style: "grid-template-columns: repeat(#{card[:columns] || 4}, minmax(0, 1fr));") do
|
30
|
+
columns = card[:columns] || 4
|
31
|
+
columns.times do
|
32
|
+
div(class: "h-4 bg-gray-200 rounded")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Table rows skeleton
|
37
|
+
div(class: "space-y-3") do
|
38
|
+
rows = card[:preview_rows] || 5
|
39
|
+
rows.times do
|
40
|
+
div(class: "grid gap-4", style: "grid-template-columns: repeat(#{card[:columns] || 4}, minmax(0, 1fr));") do
|
41
|
+
columns = card[:columns] || 4
|
42
|
+
columns.times do
|
43
|
+
div(class: "h-4 bg-gray-100 rounded")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
class DatePickerComponent < Phlex::HTML
|
3
|
+
attr_reader :name, :value, :label, :options
|
4
|
+
|
5
|
+
def initialize(name:, value: nil, label: nil, **options)
|
6
|
+
@name = name
|
7
|
+
@value = value
|
8
|
+
@label = label
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def view_template
|
13
|
+
div(
|
14
|
+
class: "relative",
|
15
|
+
data: { controller: "date-picker" }
|
16
|
+
) do
|
17
|
+
if label
|
18
|
+
label(for: input_id, class: "block text-sm font-medium text-gray-700 mb-1") { plain label }
|
19
|
+
end
|
20
|
+
|
21
|
+
div(class: "relative") do
|
22
|
+
input(
|
23
|
+
type: "text",
|
24
|
+
id: input_id,
|
25
|
+
name: name,
|
26
|
+
value: formatted_value,
|
27
|
+
class: input_classes,
|
28
|
+
placeholder: options[:placeholder] || "Select date",
|
29
|
+
readonly: true,
|
30
|
+
data: {
|
31
|
+
date_picker_target: "input",
|
32
|
+
action: "click->date-picker#toggle focus->date-picker#toggle"
|
33
|
+
}
|
34
|
+
)
|
35
|
+
|
36
|
+
div(class: "absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-gray-400") do
|
37
|
+
calendar_icon
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
render_date_picker_modal
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def input_id
|
48
|
+
@input_id ||= "#{name}_#{SecureRandom.hex(4)}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def input_classes
|
52
|
+
classes = ["block w-full px-3 py-2 pr-10 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 placeholder-gray-400"]
|
53
|
+
classes << options[:class] if options[:class]
|
54
|
+
classes.join(" ")
|
55
|
+
end
|
56
|
+
|
57
|
+
def formatted_value
|
58
|
+
return "" unless value
|
59
|
+
|
60
|
+
case value
|
61
|
+
when Date
|
62
|
+
value.strftime("%B %d, %Y")
|
63
|
+
when Time
|
64
|
+
value.to_date.strftime("%B %d, %Y")
|
65
|
+
when String
|
66
|
+
begin
|
67
|
+
Date.parse(value).strftime("%B %d, %Y")
|
68
|
+
rescue ArgumentError
|
69
|
+
value
|
70
|
+
end
|
71
|
+
else
|
72
|
+
value.to_s
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def calendar_icon
|
77
|
+
unsafe_raw <<~SVG
|
78
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
79
|
+
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
80
|
+
<line x1="16" y1="2" x2="16" y2="6"></line>
|
81
|
+
<line x1="8" y1="2" x2="8" y2="6"></line>
|
82
|
+
<line x1="3" y1="10" x2="21" y2="10"></line>
|
83
|
+
</svg>
|
84
|
+
SVG
|
85
|
+
end
|
86
|
+
|
87
|
+
def render_date_picker_modal
|
88
|
+
div(
|
89
|
+
class: "absolute z-[9999] mt-2 hidden opacity-0 scale-95 transition-all duration-200 ease-out left-0 right-0 sm:left-auto sm:right-auto sm:min-w-[320px]",
|
90
|
+
data: {
|
91
|
+
date_picker_target: "modal",
|
92
|
+
action: "click->date-picker#clickOutside"
|
93
|
+
}
|
94
|
+
) do
|
95
|
+
div(
|
96
|
+
class: "bg-white rounded-lg shadow-xl border border-gray-200 p-4 mx-2 sm:mx-0 max-w-sm sm:max-w-none",
|
97
|
+
data: { action: "click->date-picker#preventClose" }
|
98
|
+
) do
|
99
|
+
render_date_picker_header
|
100
|
+
render_date_picker_calendar
|
101
|
+
render_date_picker_footer
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def render_date_picker_header
|
107
|
+
div(class: "flex items-center justify-between mb-4") do
|
108
|
+
button(
|
109
|
+
type: "button",
|
110
|
+
class: "p-2 hover:bg-gray-100 rounded-lg transition-colors group",
|
111
|
+
data: { action: "click->date-picker#previousMonth" }
|
112
|
+
) do
|
113
|
+
unsafe_raw <<~SVG
|
114
|
+
<svg class="w-4 h-4 text-gray-600 group-hover:text-gray-900" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
115
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
116
|
+
</svg>
|
117
|
+
SVG
|
118
|
+
end
|
119
|
+
|
120
|
+
div(
|
121
|
+
class: "text-base font-semibold text-gray-900",
|
122
|
+
data: { date_picker_target: "monthYear" }
|
123
|
+
) do
|
124
|
+
# Will be populated by JavaScript
|
125
|
+
end
|
126
|
+
|
127
|
+
button(
|
128
|
+
type: "button",
|
129
|
+
class: "p-2 hover:bg-gray-100 rounded-lg transition-colors group",
|
130
|
+
data: { action: "click->date-picker#nextMonth" }
|
131
|
+
) do
|
132
|
+
unsafe_raw <<~SVG
|
133
|
+
<svg class="w-4 h-4 text-gray-600 group-hover:text-gray-900" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
134
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
135
|
+
</svg>
|
136
|
+
SVG
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def render_date_picker_calendar
|
142
|
+
div do
|
143
|
+
# Days of week header
|
144
|
+
div(class: "grid grid-cols-7 gap-1 mb-3") do
|
145
|
+
%w[Sun Mon Tue Wed Thu Fri Sat].each do |day|
|
146
|
+
div(class: "text-xs sm:text-sm text-center text-gray-500 font-medium py-2") { plain day }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Calendar grid (populated by JavaScript)
|
151
|
+
div(
|
152
|
+
class: "grid grid-cols-7 gap-1 sm:gap-2",
|
153
|
+
data: { date_picker_target: "grid" }
|
154
|
+
)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def render_date_picker_footer
|
159
|
+
div(class: "flex flex-col sm:flex-row sm:items-center sm:justify-between mt-4 pt-4 border-t border-gray-200 space-y-3 sm:space-y-0") do
|
160
|
+
button(
|
161
|
+
type: "button",
|
162
|
+
class: "px-4 py-2.5 sm:px-3 sm:py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors touch-manipulation",
|
163
|
+
data: { action: "click->date-picker#today" }
|
164
|
+
) do
|
165
|
+
plain "Today"
|
166
|
+
end
|
167
|
+
|
168
|
+
div(class: "flex items-center space-x-3 sm:space-x-2") do
|
169
|
+
button(
|
170
|
+
type: "button",
|
171
|
+
class: "flex-1 sm:flex-none px-4 py-2.5 sm:px-3 sm:py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors touch-manipulation",
|
172
|
+
data: { action: "click->date-picker#clear" }
|
173
|
+
) do
|
174
|
+
plain "Clear"
|
175
|
+
end
|
176
|
+
|
177
|
+
button(
|
178
|
+
type: "button",
|
179
|
+
class: "flex-1 sm:flex-none px-4 py-2.5 sm:px-3 sm:py-1.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors touch-manipulation",
|
180
|
+
data: { action: "click->date-picker#close" }
|
181
|
+
) do
|
182
|
+
plain "Done"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
class BaseComponent < Phlex::HTML
|
4
|
+
# Register Turbo custom elements
|
5
|
+
register_element :turbo_frame
|
6
|
+
register_element :turbo_stream
|
7
|
+
register_element :template
|
8
|
+
|
9
|
+
# Include Rails helpers for form building, URL generation, and text manipulation
|
10
|
+
include ActionView::Helpers::DateHelper
|
11
|
+
include ActionView::Helpers::TextHelper
|
12
|
+
include ActionView::Helpers::UrlHelper
|
13
|
+
include ActionView::Helpers::NumberHelper
|
14
|
+
include EasyAdmin::DashboardsHelper
|
15
|
+
include EasyAdmin::FieldsHelper
|
16
|
+
|
17
|
+
# Add method to access all Rails helpers if needed
|
18
|
+
def helpers
|
19
|
+
@helpers ||= Class.new do
|
20
|
+
include ActionView::Helpers
|
21
|
+
include Rails.application.routes.url_helpers
|
22
|
+
include EasyAdmin::Engine.routes.url_helpers
|
23
|
+
end.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# Direct access to EasyAdmin URL helpers
|
27
|
+
def easy_admin_url_helpers
|
28
|
+
@easy_admin_url_helpers ||= EasyAdmin::Engine.routes.url_helpers
|
29
|
+
end
|
30
|
+
|
31
|
+
# Direct access to Rails URL helpers
|
32
|
+
def rails_url_helpers
|
33
|
+
@rails_url_helpers ||= Rails.application.routes.url_helpers
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :field, :value, :record, :form
|
37
|
+
|
38
|
+
def initialize(field:, value: nil, record: nil, form: nil, **options)
|
39
|
+
@field = field
|
40
|
+
@value = value
|
41
|
+
@record = record
|
42
|
+
@form = form
|
43
|
+
@options = options
|
44
|
+
end
|
45
|
+
|
46
|
+
def view_template
|
47
|
+
# Override this in subclasses
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def field_name
|
53
|
+
field[:name]
|
54
|
+
end
|
55
|
+
|
56
|
+
def field_label
|
57
|
+
field[:label]
|
58
|
+
end
|
59
|
+
|
60
|
+
def field_type
|
61
|
+
field[:type]
|
62
|
+
end
|
63
|
+
|
64
|
+
def required?
|
65
|
+
field[:required]
|
66
|
+
end
|
67
|
+
|
68
|
+
def readonly?
|
69
|
+
field[:readonly]
|
70
|
+
end
|
71
|
+
|
72
|
+
def field_id
|
73
|
+
"#{field_name}_#{SecureRandom.hex(3)}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def css_classes(*additional_classes)
|
77
|
+
classes = additional_classes.compact
|
78
|
+
classes.join(" ")
|
79
|
+
end
|
80
|
+
|
81
|
+
def format_value(val = value)
|
82
|
+
return "" if val.blank?
|
83
|
+
|
84
|
+
case field_type
|
85
|
+
when :boolean
|
86
|
+
val ? "Yes" : "No"
|
87
|
+
when :date
|
88
|
+
val.respond_to?(:strftime) ? val.strftime('%B %d, %Y') : val
|
89
|
+
when :datetime
|
90
|
+
val.respond_to?(:strftime) ? val.strftime('%B %d, %Y at %I:%M %p') : val
|
91
|
+
when :email
|
92
|
+
val.to_s
|
93
|
+
when :text
|
94
|
+
val.to_s
|
95
|
+
else
|
96
|
+
val.to_s
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
class BelongsToEditModalComponent < EasyAdmin::BaseComponent
|
4
|
+
def initialize(record:, resource_class:, parent_record:, parent_field:)
|
5
|
+
@record = record
|
6
|
+
@resource_class = resource_class
|
7
|
+
@parent_record = parent_record
|
8
|
+
@parent_field = parent_field
|
9
|
+
end
|
10
|
+
|
11
|
+
def view_template
|
12
|
+
turbo_frame(id: "modal") do
|
13
|
+
div(
|
14
|
+
id: "modal-backdrop",
|
15
|
+
class: "fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 opacity-100 transition-opacity duration-300",
|
16
|
+
data: {
|
17
|
+
controller: "modal",
|
18
|
+
action: "click->modal#closeOnBackdrop"
|
19
|
+
}
|
20
|
+
) do
|
21
|
+
div(class: "relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 xl:w-2/5 shadow-lg rounded-md bg-white") do
|
22
|
+
div(class: "mt-3") do
|
23
|
+
# Modal header
|
24
|
+
div(class: "flex items-center justify-between mb-4") do
|
25
|
+
h3(class: "text-lg font-medium text-gray-900") do
|
26
|
+
"Edit #{@resource_class.singular_title}"
|
27
|
+
end
|
28
|
+
button(
|
29
|
+
class: "text-gray-400 hover:text-gray-600 focus:outline-none",
|
30
|
+
data: { action: "click->modal#close" }
|
31
|
+
) do
|
32
|
+
unsafe_raw <<~SVG
|
33
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
34
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
35
|
+
</svg>
|
36
|
+
SVG
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Modal form
|
41
|
+
form(
|
42
|
+
action: update_attached_url,
|
43
|
+
method: "patch",
|
44
|
+
data: {
|
45
|
+
turbo_frame: "_top",
|
46
|
+
controller: "form",
|
47
|
+
action: "submit->form#submit turbo:submit-end->modal#handleSubmitEnd"
|
48
|
+
}
|
49
|
+
) do
|
50
|
+
render_form_fields
|
51
|
+
|
52
|
+
# Form actions
|
53
|
+
div(class: "flex justify-end space-x-3 mt-6") do
|
54
|
+
button(
|
55
|
+
type: "button",
|
56
|
+
class: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
|
57
|
+
data: { action: "click->modal#close" }
|
58
|
+
) do
|
59
|
+
"Cancel"
|
60
|
+
end
|
61
|
+
|
62
|
+
button(
|
63
|
+
type: "submit",
|
64
|
+
class: "px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
65
|
+
) do
|
66
|
+
"Save Changes"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def render_form_fields
|
79
|
+
# Create a form builder for the associated record
|
80
|
+
form_builder = ActionView::Helpers::FormBuilder.new(
|
81
|
+
@resource_class.param_key,
|
82
|
+
@record,
|
83
|
+
helpers,
|
84
|
+
{}
|
85
|
+
)
|
86
|
+
|
87
|
+
# Render each form field for the associated record
|
88
|
+
@resource_class.form_fields.each do |field_config|
|
89
|
+
next if field_config[:readonly]
|
90
|
+
|
91
|
+
current_value = @record.public_send(field_config[:name]) if @record.respond_to?(field_config[:name])
|
92
|
+
|
93
|
+
field_component = field_component(
|
94
|
+
field_config,
|
95
|
+
action: :form,
|
96
|
+
value: current_value,
|
97
|
+
record: @record,
|
98
|
+
form: form_builder
|
99
|
+
)
|
100
|
+
|
101
|
+
render field_component
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def update_attached_url
|
106
|
+
# We need to create a route that handles updating the attached record
|
107
|
+
# and then refreshes the parent table cell
|
108
|
+
easy_admin_url_helpers.update_belongs_to_attached_resource_path(
|
109
|
+
@parent_record.class.name.underscore.pluralize,
|
110
|
+
id: @parent_record.id,
|
111
|
+
field: @parent_field[:name],
|
112
|
+
attached_id: @record.id
|
113
|
+
)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Form
|
4
|
+
class BelongsToComponent < SelectComponent
|
5
|
+
private
|
6
|
+
|
7
|
+
# Override to ensure single select mode
|
8
|
+
def multiple?
|
9
|
+
false
|
10
|
+
end
|
11
|
+
|
12
|
+
# Override to use proper field name for belongs_to associations
|
13
|
+
def form_field_name
|
14
|
+
model_name = form.object.class.name.underscore
|
15
|
+
"#{model_name}[#{association_key}]"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Override to handle belongs_to association field name
|
19
|
+
def association_key
|
20
|
+
# Convert belongs_to association name to foreign key
|
21
|
+
# e.g., user -> user_id, category -> category_id
|
22
|
+
if field_name.to_s.end_with?('_id')
|
23
|
+
field_name
|
24
|
+
else
|
25
|
+
"#{field_name}_id"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Override to always use suggest mode for belongs_to
|
30
|
+
def suggest_mode?
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
# Override to ensure suggest configuration exists
|
35
|
+
def field
|
36
|
+
@field_with_suggest ||= begin
|
37
|
+
original_field = super
|
38
|
+
original_field.merge(
|
39
|
+
suggest: original_field[:suggest] || { search_fields: [:name], limit: 10 }
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Override to generate proper suggest URL for belongs_to fields
|
45
|
+
def suggest_url
|
46
|
+
# Extract resource name from form object
|
47
|
+
resource_name = form.object.class.name.underscore.pluralize
|
48
|
+
|
49
|
+
# For belongs_to fields, use the association name (not the foreign key)
|
50
|
+
search_field_name = field_name
|
51
|
+
|
52
|
+
url = easy_admin_url_helpers.suggest_resource_path(resource_name, field: search_field_name)
|
53
|
+
Rails.logger.debug "BelongsTo suggest URL for #{field_name}: #{url} (resource: #{resource_name}, field: #{search_field_name})"
|
54
|
+
url
|
55
|
+
end
|
56
|
+
|
57
|
+
# Override to always return empty options since we use suggest mode
|
58
|
+
def options
|
59
|
+
[]
|
60
|
+
end
|
61
|
+
|
62
|
+
# Override to get current value using association key
|
63
|
+
def current_selected_values
|
64
|
+
current_value = form.object.public_send(association_key) if form.object.respond_to?(association_key)
|
65
|
+
current_value.present? ? [current_value] : []
|
66
|
+
end
|
67
|
+
|
68
|
+
# Override to ensure hidden input uses the foreign key value
|
69
|
+
def render_hidden_inputs
|
70
|
+
# Single select - one hidden input with foreign key value
|
71
|
+
current_value = form.object.public_send(association_key) if form.object.respond_to?(association_key)
|
72
|
+
input(
|
73
|
+
type: "hidden",
|
74
|
+
name: form_field_name,
|
75
|
+
value: current_value || "",
|
76
|
+
data: { select_field_target: "hiddenInput" }
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Form
|
4
|
+
class BooleanComponent < BaseComponent
|
5
|
+
def view_template
|
6
|
+
div(class: "mb-4") do
|
7
|
+
div(class: "flex items-center justify-between") do
|
8
|
+
div(class: "flex flex-col") do
|
9
|
+
label(for: field_id, class: label_classes) do
|
10
|
+
plain field_label
|
11
|
+
if required?
|
12
|
+
span(class: "text-red-500 ml-1") { "*" }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
if field[:help_text]
|
17
|
+
p(class: "mt-1 text-sm text-gray-500") { field[:help_text] }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
render_toggle_switch
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def render_toggle_switch
|
29
|
+
container_attributes = {
|
30
|
+
class: "relative inline-flex items-center",
|
31
|
+
data: { controller: "toggle-switch" }
|
32
|
+
}
|
33
|
+
|
34
|
+
div(**container_attributes) do
|
35
|
+
# Hidden input for unchecked state
|
36
|
+
input(type: "hidden", name: form_field_name, value: "0")
|
37
|
+
|
38
|
+
# Hidden checkbox that holds the actual value
|
39
|
+
input(
|
40
|
+
type: "checkbox",
|
41
|
+
name: form_field_name,
|
42
|
+
id: field_id,
|
43
|
+
value: "1",
|
44
|
+
checked: current_value,
|
45
|
+
class: "sr-only",
|
46
|
+
data: { toggle_switch_target: "checkbox" }
|
47
|
+
)
|
48
|
+
|
49
|
+
# Visual toggle switch (clickable)
|
50
|
+
div(
|
51
|
+
class: "toggle-switch cursor-pointer",
|
52
|
+
data: { action: "click->toggle-switch#toggle" }
|
53
|
+
) do
|
54
|
+
span(
|
55
|
+
class: "toggle-slider",
|
56
|
+
data: { toggle_switch_target: "slider" }
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Optional: Status text
|
61
|
+
if field[:show_status]
|
62
|
+
span(class: "ml-3 text-sm font-medium text-gray-700") do
|
63
|
+
span(class: "toggle-status-text", data: { toggle_switch_target: "statusText" }) do
|
64
|
+
current_value ? "Enabled" : "Disabled"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def toggle_switch_classes
|
72
|
+
base_classes = "relative inline-flex items-center cursor-pointer"
|
73
|
+
disabled_classes = field[:disabled] ? "opacity-50 cursor-not-allowed" : ""
|
74
|
+
|
75
|
+
"#{base_classes} #{disabled_classes}".strip
|
76
|
+
end
|
77
|
+
|
78
|
+
def label_classes
|
79
|
+
"block text-sm font-medium text-gray-700 mb-1"
|
80
|
+
end
|
81
|
+
|
82
|
+
def form_field_name
|
83
|
+
model_name = form.object.class.name.underscore
|
84
|
+
"#{model_name}[#{field_name}]"
|
85
|
+
end
|
86
|
+
|
87
|
+
def current_value
|
88
|
+
value = form.object.public_send(field_name) if form.object.respond_to?(field_name)
|
89
|
+
# Convert various truthy values to boolean
|
90
|
+
case value
|
91
|
+
when true, 1, "1", "true", "on", "yes"
|
92
|
+
true
|
93
|
+
else
|
94
|
+
false
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Form
|
4
|
+
class DateComponent < BaseComponent
|
5
|
+
def view_template
|
6
|
+
div(class: "mb-4") do
|
7
|
+
label(for: field_id, class: label_classes) do
|
8
|
+
plain field_label
|
9
|
+
if required?
|
10
|
+
span(class: "text-red-500 ml-1") { "*" }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
input(
|
14
|
+
type: "date",
|
15
|
+
name: form_field_name,
|
16
|
+
id: field_id,
|
17
|
+
value: current_value,
|
18
|
+
class: input_classes,
|
19
|
+
required: required?
|
20
|
+
)
|
21
|
+
if field[:help_text]
|
22
|
+
p(class: "mt-1 text-sm text-gray-500") { field[:help_text] }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def label_classes
|
30
|
+
"block text-sm font-medium text-gray-700 mb-1"
|
31
|
+
end
|
32
|
+
|
33
|
+
def input_classes
|
34
|
+
base_classes = "block w-full px-3 py-2 border rounded-md shadow-sm text-sm"
|
35
|
+
state_classes = "border-gray-300 placeholder-gray-400"
|
36
|
+
focus_classes = "focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
37
|
+
hover_classes = "hover:border-gray-400"
|
38
|
+
transition_classes = "transition-colors duration-200"
|
39
|
+
|
40
|
+
"#{base_classes} #{state_classes} #{focus_classes} #{hover_classes} #{transition_classes}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def form_field_name
|
44
|
+
model_name = form.object.class.name.underscore
|
45
|
+
"#{model_name}[#{field_name}]"
|
46
|
+
end
|
47
|
+
|
48
|
+
def current_value
|
49
|
+
value = form.object.public_send(field_name) if form.object.respond_to?(field_name)
|
50
|
+
value.respond_to?(:strftime) ? value.strftime("%Y-%m-%d") : value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|