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.
Files changed (203) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/builds/easy_admin.base.js +43505 -0
  6. data/app/assets/builds/easy_admin.base.js.map +7 -0
  7. data/app/assets/builds/easy_admin.css +6141 -0
  8. data/app/assets/config/easy_admin_manifest.js +1 -0
  9. data/app/assets/images/jsoneditor-icons.svg +749 -0
  10. data/app/assets/stylesheets/easy_admin/application.tailwind.css +390 -0
  11. data/app/components/easy_admin/base_component.rb +35 -0
  12. data/app/components/easy_admin/batch_action_bar_component.rb +125 -0
  13. data/app/components/easy_admin/batch_action_form_component.rb +124 -0
  14. data/app/components/easy_admin/combined_filters_component.rb +232 -0
  15. data/app/components/easy_admin/confirmation_modal_component.rb +61 -0
  16. data/app/components/easy_admin/context_menu_component.rb +161 -0
  17. data/app/components/easy_admin/dashboards/base_card_component.rb +152 -0
  18. data/app/components/easy_admin/dashboards/card_error_component.rb +23 -0
  19. data/app/components/easy_admin/dashboards/card_factory.rb +90 -0
  20. data/app/components/easy_admin/dashboards/card_stream_component.rb +22 -0
  21. data/app/components/easy_admin/dashboards/cards/base_card_component.rb +54 -0
  22. data/app/components/easy_admin/dashboards/cards/chart_card_component.rb +175 -0
  23. data/app/components/easy_admin/dashboards/cards/custom_card_component.rb +50 -0
  24. data/app/components/easy_admin/dashboards/cards/metric_card_component.rb +164 -0
  25. data/app/components/easy_admin/dashboards/cards/table_card_component.rb +148 -0
  26. data/app/components/easy_admin/dashboards/chart_card_component.rb +44 -0
  27. data/app/components/easy_admin/dashboards/metric_card_component.rb +56 -0
  28. data/app/components/easy_admin/dashboards/refresh_stream_component.rb +279 -0
  29. data/app/components/easy_admin/dashboards/show_component.rb +163 -0
  30. data/app/components/easy_admin/dashboards/table_card_component.rb +52 -0
  31. data/app/components/easy_admin/date_picker_component.rb +188 -0
  32. data/app/components/easy_admin/fields/base_component.rb +101 -0
  33. data/app/components/easy_admin/fields/belongs_to_edit_modal_component.rb +117 -0
  34. data/app/components/easy_admin/fields/form/belongs_to_component.rb +82 -0
  35. data/app/components/easy_admin/fields/form/boolean_component.rb +100 -0
  36. data/app/components/easy_admin/fields/form/date_component.rb +55 -0
  37. data/app/components/easy_admin/fields/form/datetime_component.rb +55 -0
  38. data/app/components/easy_admin/fields/form/email_component.rb +55 -0
  39. data/app/components/easy_admin/fields/form/file_component.rb +190 -0
  40. data/app/components/easy_admin/fields/form/has_many_component.rb +416 -0
  41. data/app/components/easy_admin/fields/form/json_component.rb +81 -0
  42. data/app/components/easy_admin/fields/form/number_component.rb +55 -0
  43. data/app/components/easy_admin/fields/form/select_component.rb +326 -0
  44. data/app/components/easy_admin/fields/form/text_component.rb +55 -0
  45. data/app/components/easy_admin/fields/form/textarea_component.rb +54 -0
  46. data/app/components/easy_admin/fields/index/belongs_to_component.rb +93 -0
  47. data/app/components/easy_admin/fields/index/boolean_component.rb +29 -0
  48. data/app/components/easy_admin/fields/index/date_component.rb +13 -0
  49. data/app/components/easy_admin/fields/index/datetime_component.rb +13 -0
  50. data/app/components/easy_admin/fields/index/email_component.rb +24 -0
  51. data/app/components/easy_admin/fields/index/filters/base_component.rb +48 -0
  52. data/app/components/easy_admin/fields/index/filters/boolean_component.rb +96 -0
  53. data/app/components/easy_admin/fields/index/filters/date_component.rb +182 -0
  54. data/app/components/easy_admin/fields/index/filters/number_component.rb +30 -0
  55. data/app/components/easy_admin/fields/index/filters/select_component.rb +101 -0
  56. data/app/components/easy_admin/fields/index/filters/string_component.rb +32 -0
  57. data/app/components/easy_admin/fields/index/json_component.rb +23 -0
  58. data/app/components/easy_admin/fields/index/number_component.rb +20 -0
  59. data/app/components/easy_admin/fields/index/select_component.rb +25 -0
  60. data/app/components/easy_admin/fields/index/text_component.rb +20 -0
  61. data/app/components/easy_admin/fields/inline_edit_modal_component.rb +135 -0
  62. data/app/components/easy_admin/fields/inline_edit_trigger_component.rb +144 -0
  63. data/app/components/easy_admin/fields/show/belongs_to_component.rb +93 -0
  64. data/app/components/easy_admin/fields/show/boolean_component.rb +21 -0
  65. data/app/components/easy_admin/fields/show/date_component.rb +13 -0
  66. data/app/components/easy_admin/fields/show/datetime_component.rb +13 -0
  67. data/app/components/easy_admin/fields/show/email_component.rb +19 -0
  68. data/app/components/easy_admin/fields/show/file_component.rb +304 -0
  69. data/app/components/easy_admin/fields/show/has_many_component.rb +192 -0
  70. data/app/components/easy_admin/fields/show/json_component.rb +45 -0
  71. data/app/components/easy_admin/fields/show/number_component.rb +20 -0
  72. data/app/components/easy_admin/fields/show/select_component.rb +25 -0
  73. data/app/components/easy_admin/fields/show/text_component.rb +17 -0
  74. data/app/components/easy_admin/fields/show/textarea_component.rb +26 -0
  75. data/app/components/easy_admin/filters_component.rb +120 -0
  76. data/app/components/easy_admin/form_tabs_component.rb +166 -0
  77. data/app/components/easy_admin/infinite_scroll_component.rb +82 -0
  78. data/app/components/easy_admin/lazy_chart_card_component.rb +128 -0
  79. data/app/components/easy_admin/lazy_metric_card_component.rb +76 -0
  80. data/app/components/easy_admin/modal_frame_component.rb +26 -0
  81. data/app/components/easy_admin/navbar_component.rb +226 -0
  82. data/app/components/easy_admin/notification_component.rb +83 -0
  83. data/app/components/easy_admin/pagination_component.rb +188 -0
  84. data/app/components/easy_admin/quick_filters_component.rb +65 -0
  85. data/app/components/easy_admin/resource_pagination_component.rb +14 -0
  86. data/app/components/easy_admin/resources/index_component.rb +211 -0
  87. data/app/components/easy_admin/resources/index_frame_component.rb +88 -0
  88. data/app/components/easy_admin/resources/show_page_actions_component.rb +324 -0
  89. data/app/components/easy_admin/resources/table_cell_component.rb +145 -0
  90. data/app/components/easy_admin/resources/table_component.rb +206 -0
  91. data/app/components/easy_admin/resources/table_row_component.rb +160 -0
  92. data/app/components/easy_admin/row_action_form_component.rb +127 -0
  93. data/app/components/easy_admin/scopes_component.rb +224 -0
  94. data/app/components/easy_admin/settings_sidebar_component.rb +140 -0
  95. data/app/components/easy_admin/show_layout_component.rb +600 -0
  96. data/app/components/easy_admin/sidebar_component.rb +174 -0
  97. data/app/components/easy_admin/turbo/response_component.rb +40 -0
  98. data/app/components/easy_admin/turbo/stream_component.rb +28 -0
  99. data/app/controllers/easy_admin/application_controller.rb +66 -0
  100. data/app/controllers/easy_admin/batch_actions_controller.rb +166 -0
  101. data/app/controllers/easy_admin/confirmation_modal_controller.rb +20 -0
  102. data/app/controllers/easy_admin/dashboard_controller.rb +6 -0
  103. data/app/controllers/easy_admin/dashboards_controller.rb +123 -0
  104. data/app/controllers/easy_admin/passwords_controller.rb +15 -0
  105. data/app/controllers/easy_admin/registrations_controller.rb +52 -0
  106. data/app/controllers/easy_admin/resources_controller.rb +907 -0
  107. data/app/controllers/easy_admin/row_actions_controller.rb +216 -0
  108. data/app/controllers/easy_admin/sessions_controller.rb +32 -0
  109. data/app/controllers/easy_admin/settings_controller.rb +94 -0
  110. data/app/helpers/easy_admin/application_helper.rb +4 -0
  111. data/app/helpers/easy_admin/dashboards_helper.rb +121 -0
  112. data/app/helpers/easy_admin/fields_helper.rb +27 -0
  113. data/app/helpers/easy_admin/pagy_helper.rb +30 -0
  114. data/app/helpers/easy_admin/resources_helper.rb +39 -0
  115. data/app/javascript/easy_admin/application.js +12 -0
  116. data/app/javascript/easy_admin/controllers/batch_modal_controller.js +66 -0
  117. data/app/javascript/easy_admin/controllers/batch_selection_controller.js +223 -0
  118. data/app/javascript/easy_admin/controllers/chart_controller.js +216 -0
  119. data/app/javascript/easy_admin/controllers/collapsible_filters_controller.js +118 -0
  120. data/app/javascript/easy_admin/controllers/confirmation_modal_controller.js +64 -0
  121. data/app/javascript/easy_admin/controllers/context_menu_controller.js +227 -0
  122. data/app/javascript/easy_admin/controllers/date_picker_controller.js +309 -0
  123. data/app/javascript/easy_admin/controllers/dropdown_controller.js +63 -0
  124. data/app/javascript/easy_admin/controllers/event_emitter_controller.js +19 -0
  125. data/app/javascript/easy_admin/controllers/file_controller.js +121 -0
  126. data/app/javascript/easy_admin/controllers/form_tabs_controller.js +100 -0
  127. data/app/javascript/easy_admin/controllers/has_many_search_controller.js +76 -0
  128. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +174 -0
  129. data/app/javascript/easy_admin/controllers/ios_alert_controller.js +195 -0
  130. data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +88 -0
  131. data/app/javascript/easy_admin/controllers/modal_controller.js +75 -0
  132. data/app/javascript/easy_admin/controllers/navbar_scroll_controller.js +76 -0
  133. data/app/javascript/easy_admin/controllers/notification_controller.js +48 -0
  134. data/app/javascript/easy_admin/controllers/row_action_controller.js +124 -0
  135. data/app/javascript/easy_admin/controllers/row_modal_controller.js +59 -0
  136. data/app/javascript/easy_admin/controllers/select_field_controller.js +618 -0
  137. data/app/javascript/easy_admin/controllers/settings_button_controller.js +8 -0
  138. data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +186 -0
  139. data/app/javascript/easy_admin/controllers/sidebar_controller.js +102 -0
  140. data/app/javascript/easy_admin/controllers/sidebar_mobile_controller.js +23 -0
  141. data/app/javascript/easy_admin/controllers/sidebar_nav_controller.js +96 -0
  142. data/app/javascript/easy_admin/controllers/table_controller.js +28 -0
  143. data/app/javascript/easy_admin/controllers/table_row_controller.js +16 -0
  144. data/app/javascript/easy_admin/controllers/toggle_switch_controller.js +22 -0
  145. data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +9 -0
  146. data/app/javascript/easy_admin/controllers.js +54 -0
  147. data/app/javascript/easy_admin.base.js +4 -0
  148. data/app/models/easy_admin/admin_user.rb +53 -0
  149. data/app/models/easy_admin/application_record.rb +5 -0
  150. data/app/views/easy_admin/dashboard/index.html.erb +3 -0
  151. data/app/views/easy_admin/dashboards/show.html.erb +7 -0
  152. data/app/views/easy_admin/passwords/edit.html.erb +42 -0
  153. data/app/views/easy_admin/passwords/new.html.erb +41 -0
  154. data/app/views/easy_admin/registrations/new.html.erb +65 -0
  155. data/app/views/easy_admin/resources/_redirect.turbo_stream.erb +3 -0
  156. data/app/views/easy_admin/resources/_table_rows.html.erb +46 -0
  157. data/app/views/easy_admin/resources/edit.html.erb +151 -0
  158. data/app/views/easy_admin/resources/index.html.erb +12 -0
  159. data/app/views/easy_admin/resources/index.turbo_stream.erb +139 -0
  160. data/app/views/easy_admin/resources/index_frame.html.erb +142 -0
  161. data/app/views/easy_admin/resources/new.html.erb +100 -0
  162. data/app/views/easy_admin/resources/show.html.erb +31 -0
  163. data/app/views/easy_admin/sessions/new.html.erb +55 -0
  164. data/app/views/easy_admin/settings/_form.html.erb +51 -0
  165. data/app/views/easy_admin/settings/index.html.erb +53 -0
  166. data/app/views/layouts/easy_admin/application.html.erb +48 -0
  167. data/app/views/layouts/easy_admin/auth.html.erb +34 -0
  168. data/config/initializers/easy_admin_card_factory.rb +27 -0
  169. data/config/initializers/pagy.rb +15 -0
  170. data/config/initializers/rack_mini_profiler.rb +67 -0
  171. data/config/routes.rb +70 -0
  172. data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +45 -0
  173. data/lib/easy-admin.rb +32 -0
  174. data/lib/easy_admin/action.rb +159 -0
  175. data/lib/easy_admin/batch_action.rb +134 -0
  176. data/lib/easy_admin/configuration.rb +75 -0
  177. data/lib/easy_admin/dashboard.rb +110 -0
  178. data/lib/easy_admin/dashboard_registry.rb +30 -0
  179. data/lib/easy_admin/delete_action.rb +22 -0
  180. data/lib/easy_admin/engine.rb +54 -0
  181. data/lib/easy_admin/field.rb +118 -0
  182. data/lib/easy_admin/resource.rb +806 -0
  183. data/lib/easy_admin/resource_registry.rb +22 -0
  184. data/lib/easy_admin/types/json_type.rb +25 -0
  185. data/lib/easy_admin/version.rb +3 -0
  186. data/lib/generators/easy_admin/auth_generator.rb +69 -0
  187. data/lib/generators/easy_admin/card/card_generator.rb +94 -0
  188. data/lib/generators/easy_admin/card/templates/card_component.rb.erb +127 -0
  189. data/lib/generators/easy_admin/card/templates/card_component_spec.rb.erb +122 -0
  190. data/lib/generators/easy_admin/install/templates/easy_admin.rb +31 -0
  191. data/lib/generators/easy_admin/install_generator.rb +25 -0
  192. data/lib/generators/easy_admin/rbac/rbac_generator.rb +244 -0
  193. data/lib/generators/easy_admin/rbac/templates/add_rbac_to_admin_users.rb +23 -0
  194. data/lib/generators/easy_admin/rbac/templates/super_admin.rb +34 -0
  195. data/lib/generators/easy_admin/resource_generator.rb +43 -0
  196. data/lib/generators/easy_admin/templates/AUTH_README +35 -0
  197. data/lib/generators/easy_admin/templates/README +27 -0
  198. data/lib/generators/easy_admin/templates/create_easy_admin_admin_users.rb +45 -0
  199. data/lib/generators/easy_admin/templates/devise.rb +267 -0
  200. data/lib/generators/easy_admin/templates/easy_admin.rb +24 -0
  201. data/lib/generators/easy_admin/templates/resource.rb +29 -0
  202. data/lib/tasks/easy_admin_tasks.rake +4 -0
  203. metadata +445 -0
@@ -0,0 +1,144 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ class InlineEditTriggerComponent < EasyAdmin::BaseComponent
4
+ def initialize(record:, field:, position: :right)
5
+ @record = record
6
+ @field = field
7
+ @position = position
8
+ end
9
+
10
+ def view_template
11
+ return unless @field.editable?
12
+
13
+ if @field.editable_via_menu?
14
+ render_context_menu_trigger
15
+ else
16
+ render_modal_trigger
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def render_modal_trigger
23
+ a(
24
+ href: edit_field_url,
25
+ class: trigger_classes,
26
+ data: { turbo_frame: "modal" },
27
+ title: "Edit #{@field.label}"
28
+ ) do
29
+ render_pencil_icon
30
+ end
31
+ end
32
+
33
+ def render_context_menu_trigger
34
+ div(class: "relative", data: { controller: "dropdown" }) do
35
+ button(
36
+ class: trigger_classes,
37
+ data: {
38
+ action: "click->dropdown#toggle",
39
+ dropdown_target: "trigger"
40
+ },
41
+ title: "Edit #{@field.label}"
42
+ ) do
43
+ render_pencil_icon
44
+ end
45
+
46
+ # Context menu (hidden by default)
47
+ div(
48
+ class: "absolute z-50 mt-1 w-48 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 hidden #{position_classes}",
49
+ data: { dropdown_target: "menu" }
50
+ ) do
51
+ div(class: "py-1") do
52
+ a(
53
+ href: belongs_to_reattach_url,
54
+ class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors",
55
+ data: { turbo_frame: "modal" }
56
+ ) do
57
+ div(class: "flex items-center") do
58
+ unsafe_raw <<~SVG
59
+ <svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
60
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
61
+ </svg>
62
+ SVG
63
+ span { "Change #{@field.label}" }
64
+ end
65
+ end
66
+
67
+ a(
68
+ href: belongs_to_edit_attached_url,
69
+ class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors",
70
+ data: { turbo_frame: "modal" }
71
+ ) do
72
+ div(class: "flex items-center") do
73
+ unsafe_raw <<~SVG
74
+ <svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
75
+ <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"/>
76
+ </svg>
77
+ SVG
78
+ span { "Edit #{@field.label} Details" }
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ def render_pencil_icon
87
+ unsafe_raw <<~SVG
88
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
89
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
90
+ </svg>
91
+ SVG
92
+ end
93
+
94
+ def trigger_classes
95
+ [
96
+ "inline-flex items-center justify-center w-6 h-6",
97
+ "text-gray-400 hover:text-blue-600 hover:bg-blue-50",
98
+ "rounded transition-colors duration-150",
99
+ "focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1"
100
+ ].join(" ")
101
+ end
102
+
103
+ def position_classes
104
+ case @position
105
+ when :left then "right-0"
106
+ when :right then "left-0"
107
+ when :center then "left-1/2 transform -translate-x-1/2"
108
+ else "left-0"
109
+ end
110
+ end
111
+
112
+ def edit_field_url
113
+ easy_admin_url_helpers.edit_field_resource_path(
114
+ resource.route_key,
115
+ id: @record.id,
116
+ field: @field.name
117
+ )
118
+ end
119
+
120
+ def belongs_to_reattach_url
121
+ easy_admin_url_helpers.belongs_to_reattach_resource_path(
122
+ resource.route_key,
123
+ id: @record.id,
124
+ field: @field.name
125
+ )
126
+ end
127
+
128
+ def belongs_to_edit_attached_url
129
+ easy_admin_url_helpers.belongs_to_edit_attached_resource_path(
130
+ resource.route_key,
131
+ id: @record.id,
132
+ field: @field.name
133
+ )
134
+ end
135
+
136
+ def resource
137
+ # Find resource by model class name
138
+ model_name = @record.class.name
139
+ route_key = "#{model_name.downcase.pluralize}"
140
+ EasyAdmin::ResourceRegistry.find_resource(route_key)
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,93 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Show
4
+ class BelongsToComponent < BaseComponent
5
+ private
6
+
7
+ def view_template
8
+ div(class: "field-display") do
9
+ # Only show label if show_label is not explicitly false
10
+ if field.fetch(:show_label, true)
11
+ div(class: "field-label") { plain field_label }
12
+ end
13
+
14
+ div(class: "field-value") do
15
+ if association_record
16
+ if field[:show_link]
17
+ a(
18
+ href: view_path_for(association_record),
19
+ class: "field-link",
20
+ target: field[:link_target] || "_blank"
21
+ ) do
22
+ plain display_text
23
+ end
24
+ else
25
+ plain display_text
26
+ end
27
+ else
28
+ span(class: "field-empty") { plain "—" }
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def association_record
35
+ @association_record ||= load_association_record
36
+ end
37
+
38
+ def load_association_record
39
+ return nil if value.blank?
40
+
41
+ association_class = determine_association
42
+
43
+ if value.respond_to?(:id)
44
+ # Already loaded record
45
+ value
46
+ else
47
+ # ID value, need to load the record
48
+ association_class.find_by(id: value)
49
+ end
50
+ end
51
+
52
+ def determine_association
53
+ return field[:association] if field[:association]
54
+
55
+ # Try to determine from attribute name
56
+ if field_name.to_s.end_with?('_id')
57
+ field_name.to_s.gsub('_id', '').classify.constantize
58
+ else
59
+ field_name.to_s.classify.constantize
60
+ end
61
+ rescue NameError
62
+ raise ArgumentError, "Could not determine association for #{field_name}. Please specify :association option."
63
+ end
64
+
65
+ def display_text
66
+ return "—" unless association_record
67
+
68
+ if field[:display_method]
69
+ association_record.send(field[:display_method])
70
+ elsif association_record.respond_to?(:name)
71
+ association_record.name
72
+ elsif association_record.respond_to?(:title)
73
+ association_record.title
74
+ elsif association_record.respond_to?(:to_label)
75
+ association_record.to_label
76
+ else
77
+ "##{association_record.id}"
78
+ end
79
+ end
80
+
81
+ def view_path_for(record)
82
+ return field[:view_path_proc].call(record) if field[:view_path_proc]
83
+
84
+ association_class = record.class
85
+ EasyAdmin::Engine.routes.url_helpers.resource_path(
86
+ resource_name: association_class.model_name.route_key,
87
+ id: record.id
88
+ )
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,21 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Show
4
+ class BooleanComponent < BaseComponent
5
+ def view_template
6
+ div(class: "ea-show-value") do
7
+ span(class: css_classes("ea-badge", badge_class)) do
8
+ value ? "Yes" : "No"
9
+ end
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def badge_class
16
+ value ? "ea-badge--success" : "ea-badge--secondary"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Show
4
+ class DateComponent < BaseComponent
5
+ def view_template
6
+ div(class: "ea-show-value") do
7
+ format_value
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Show
4
+ class DatetimeComponent < BaseComponent
5
+ def view_template
6
+ div(class: "ea-show-value") do
7
+ format_value
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Show
4
+ class EmailComponent < BaseComponent
5
+ def view_template
6
+ div(class: "ea-show-value") do
7
+ if value.present?
8
+ a(href: "mailto:#{value}", class: "ea-show-link") do
9
+ value.to_s
10
+ end
11
+ else
12
+ span(class: "ea-empty-value") { "—" }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,304 @@
1
+ module EasyAdmin
2
+ module Fields
3
+ module Show
4
+ class FileComponent < BaseComponent
5
+ private
6
+
7
+ def view_template
8
+ div(class: "field-display ea-file-field") do
9
+ if file_exists?
10
+ render_file_display
11
+ else
12
+ render_empty_state
13
+ end
14
+ end
15
+ end
16
+
17
+ def render_file_display
18
+ if image_file?
19
+ # Hero image display for images
20
+ div(class: "relative w-full") do
21
+ img(src: file_url, alt: file_name,
22
+ class: "w-full h-auto rounded-lg shadow-lg object-cover",
23
+ style: "max-height: 500px;")
24
+
25
+ # Overlay with file info - only show if we have file info to display
26
+ if file_name != "Unknown file" || file_size_text.present? || uploaded_date
27
+ div(class: "absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent p-4 rounded-b-lg") do
28
+ div(class: "flex items-end justify-between") do
29
+ div(class: "text-white") do
30
+ if file_name != "Unknown file"
31
+ div(class: "font-semibold text-lg drop-shadow-sm") { plain file_name }
32
+ end
33
+ if file_size_text.present? || uploaded_date
34
+ div(class: "text-sm opacity-90 drop-shadow-sm") do
35
+ if file_size_text.present?
36
+ span { plain file_size_text }
37
+ end
38
+ if uploaded_date
39
+ span(class: "ml-3") { plain "• #{uploaded_date}" }
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ div(class: "flex gap-2") do
46
+ render_action_buttons(style: "overlay")
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ else
53
+ # Card display for non-image files
54
+ div(class: "bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden") do
55
+ div(class: "bg-gradient-to-r from-blue-50 to-indigo-50 p-6") do
56
+ div(class: "flex items-center") do
57
+ div(class: "mr-4") do
58
+ render_file_icon
59
+ end
60
+
61
+ div(class: "flex-1") do
62
+ div(class: "font-semibold text-gray-900 text-lg") { plain file_name }
63
+ div(class: "flex items-center text-sm text-gray-600 mt-1") do
64
+ span { plain file_size_text }
65
+ if file_type_text
66
+ span(class: "ml-3 px-2 py-0.5 bg-blue-100 text-blue-700 rounded-full text-xs font-medium") do
67
+ plain file_type_text
68
+ end
69
+ end
70
+ if uploaded_date
71
+ span(class: "ml-3") { plain uploaded_date }
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ div(class: "px-6 py-4 bg-gray-50 border-t border-gray-100") do
79
+ div(class: "flex gap-3") do
80
+ render_action_buttons(style: "card")
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ def render_image_preview
88
+ img(src: file_url, alt: file_name,
89
+ class: "img-thumbnail rounded",
90
+ style: "max-height: 80px; max-width: 80px; object-fit: cover;")
91
+ end
92
+
93
+ def render_file_icon
94
+ div(class: "flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 text-white rounded-xl shadow-md") do
95
+ if pdf_file?
96
+ unsafe_raw %(<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
97
+ <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
98
+ </svg>)
99
+ elsif video_file?
100
+ unsafe_raw %(<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
101
+ <path d="M17,10.5V7A1,1 0 0,0 16,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16A1,1 0 0,0 17,17V13.5L21,17.5V6.5L17,10.5Z"/>
102
+ </svg>)
103
+ elsif audio_file?
104
+ unsafe_raw %(<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
105
+ <path d="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.85 14,18.71V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12Z"/>
106
+ </svg>)
107
+ else
108
+ unsafe_raw %(<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
109
+ <path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M15,18V16H6V18H15M18,14V12H6V14H18Z"/>
110
+ </svg>)
111
+ end
112
+ end
113
+ end
114
+
115
+ def render_action_buttons(style: "default")
116
+ if style == "overlay"
117
+ # Buttons for image overlay
118
+ if file_url
119
+ a(href: file_url, target: "_blank",
120
+ class: "inline-flex items-center px-3 py-1.5 bg-white/90 backdrop-blur-sm text-gray-700 rounded-lg hover:bg-white transition-colors text-sm font-medium") do
121
+ unsafe_raw %(<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
122
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
123
+ </svg>)
124
+ plain "Open"
125
+ end
126
+
127
+ a(href: "#{file_url}?download=true",
128
+ class: "inline-flex items-center px-3 py-1.5 bg-white/90 backdrop-blur-sm text-gray-700 rounded-lg hover:bg-white transition-colors text-sm font-medium") do
129
+ unsafe_raw %(<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
130
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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"/>
131
+ </svg>)
132
+ plain "Download"
133
+ end
134
+ end
135
+ else
136
+ # Buttons for card display
137
+ if file_url
138
+ a(href: file_url, target: "_blank",
139
+ class: "inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium") do
140
+ unsafe_raw %(<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
141
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
142
+ <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"/>
143
+ </svg>)
144
+ plain "View File"
145
+ end
146
+
147
+ a(href: "#{file_url}?download=true",
148
+ class: "inline-flex items-center px-4 py-2 border border-gray-300 text-gray-700 bg-white rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium") do
149
+ unsafe_raw %(<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
150
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
151
+ </svg>)
152
+ plain "Download"
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ def render_empty_state
159
+ div(class: "bg-gray-50 rounded-xl border-2 border-dashed border-gray-300 p-12") do
160
+ div(class: "text-center") do
161
+ div(class: "flex justify-center mb-4") do
162
+ div(class: "w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center") do
163
+ unsafe_raw %(<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
164
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
165
+ </svg>)
166
+ end
167
+ end
168
+ div(class: "text-gray-600 font-medium") { plain "No file uploaded" }
169
+ div(class: "text-gray-400 text-sm mt-1") { plain "Upload a file to see it here" }
170
+ end
171
+ end
172
+ end
173
+
174
+ def file_exists?
175
+ return false unless value.present?
176
+
177
+ # Handle Active Storage attachments
178
+ if value.respond_to?(:attached?)
179
+ value.attached?
180
+ elsif value.respond_to?(:attachment)
181
+ value.attachment.present?
182
+ else
183
+ true
184
+ end
185
+ end
186
+
187
+ def file_url
188
+ return nil unless file_exists?
189
+
190
+ begin
191
+ if value.respond_to?(:url)
192
+ value.url
193
+ elsif value.respond_to?(:service_url)
194
+ value.service_url
195
+ elsif value.respond_to?(:attachment) && value.attachment.present?
196
+ Rails.application.routes.url_helpers.rails_blob_path(value.attachment, only_path: true)
197
+ elsif value.is_a?(String)
198
+ value
199
+ end
200
+ rescue => e
201
+ Rails.logger.warn "Failed to generate file URL: #{e.message}"
202
+ nil
203
+ end
204
+ end
205
+
206
+ def file_name
207
+ return "Unknown file" unless file_exists?
208
+
209
+ if value.respond_to?(:filename) && value.filename.present?
210
+ value.filename.to_s
211
+ elsif value.respond_to?(:original_filename) && value.original_filename.present?
212
+ value.original_filename
213
+ elsif value.respond_to?(:attachment) && value.attachment.present? && value.attachment.filename.present?
214
+ value.attachment.filename.to_s
215
+ elsif value.is_a?(String)
216
+ File.basename(value)
217
+ else
218
+ "Uploaded file"
219
+ end
220
+ end
221
+
222
+ def file_size_text
223
+ return "" unless file_exists?
224
+
225
+ size = nil
226
+ if value.respond_to?(:byte_size) && value.byte_size.present?
227
+ size = value.byte_size
228
+ elsif value.respond_to?(:size) && value.size.present?
229
+ size = value.size
230
+ elsif value.respond_to?(:attachment) && value.attachment.present? && value.attachment.byte_size.present?
231
+ size = value.attachment.byte_size
232
+ end
233
+
234
+ return "" unless size
235
+
236
+ if size < 1024
237
+ "#{size} bytes"
238
+ elsif size < 1024 * 1024
239
+ "#{(size / 1024.0).round(1)} KB"
240
+ else
241
+ "#{(size / (1024.0 * 1024)).round(1)} MB"
242
+ end
243
+ end
244
+
245
+ def file_type_text
246
+ return nil unless file_exists?
247
+
248
+ filename = file_name.downcase
249
+ ext = File.extname(filename)
250
+
251
+ case ext
252
+ when '.pdf' then 'PDF'
253
+ when '.jpg', '.jpeg' then 'JPEG'
254
+ when '.png' then 'PNG'
255
+ when '.gif' then 'GIF'
256
+ when '.svg' then 'SVG'
257
+ when '.mp4' then 'MP4'
258
+ when '.mov' then 'MOV'
259
+ when '.mp3' then 'MP3'
260
+ when '.wav' then 'WAV'
261
+ when '.doc', '.docx' then 'Word'
262
+ when '.xls', '.xlsx' then 'Excel'
263
+ when '.zip' then 'ZIP'
264
+ else ext.upcase.gsub('.', '') if ext.present?
265
+ end
266
+ end
267
+
268
+ def uploaded_date
269
+ return nil unless file_exists?
270
+
271
+ date = nil
272
+ if value.respond_to?(:created_at) && value.created_at.present?
273
+ date = value.created_at
274
+ elsif value.respond_to?(:attachment) && value.attachment.present? && value.attachment.created_at.present?
275
+ date = value.attachment.created_at
276
+ end
277
+
278
+ date&.strftime('%b %d, %Y')
279
+ rescue
280
+ nil
281
+ end
282
+
283
+ def image_file?
284
+ filename = file_name.downcase
285
+ %w[.jpg .jpeg .png .gif .webp .svg].any? { |ext| filename.end_with?(ext) }
286
+ end
287
+
288
+ def pdf_file?
289
+ file_name.downcase.end_with?('.pdf')
290
+ end
291
+
292
+ def video_file?
293
+ filename = file_name.downcase
294
+ %w[.mp4 .mov .avi .mkv .webm].any? { |ext| filename.end_with?(ext) }
295
+ end
296
+
297
+ def audio_file?
298
+ filename = file_name.downcase
299
+ %w[.mp3 .wav .ogg .m4a].any? { |ext| filename.end_with?(ext) }
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end