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,326 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Form
|
4
|
+
class SelectComponent < EasyAdmin::Fields::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
|
+
|
14
|
+
div(
|
15
|
+
class: select_container_classes,
|
16
|
+
data: {
|
17
|
+
controller: "select-field",
|
18
|
+
select_field_multiple_value: multiple?.to_s,
|
19
|
+
select_field_placeholder_value: placeholder,
|
20
|
+
select_field_suggest_value: suggest_mode?.to_s,
|
21
|
+
select_field_suggest_url_value: suggest_mode? ? suggest_url : "",
|
22
|
+
field_name: field_name
|
23
|
+
}
|
24
|
+
) do
|
25
|
+
if multiple?
|
26
|
+
render_multiple_select_container
|
27
|
+
else
|
28
|
+
render_single_select_input
|
29
|
+
end
|
30
|
+
render_select_dropdown
|
31
|
+
render_clear_button if multiple?
|
32
|
+
render_hidden_inputs
|
33
|
+
end
|
34
|
+
|
35
|
+
if field[:help_text]
|
36
|
+
p(class: "mt-1 text-sm text-gray-500") { field[:help_text] }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def multiple?
|
44
|
+
result = field[:multiple] == true
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
def options
|
49
|
+
field[:options] || []
|
50
|
+
end
|
51
|
+
|
52
|
+
def placeholder
|
53
|
+
field[:placeholder] || "Search #{field_label.downcase}..."
|
54
|
+
end
|
55
|
+
|
56
|
+
def suggest_mode?
|
57
|
+
field[:suggest].present?
|
58
|
+
end
|
59
|
+
|
60
|
+
def suggest_url
|
61
|
+
return "" unless suggest_mode?
|
62
|
+
|
63
|
+
# Extract resource name from form object
|
64
|
+
resource_name = form.object.class.name.underscore.pluralize
|
65
|
+
|
66
|
+
# For belongs_to fields that were converted to select, use the original association name
|
67
|
+
search_field_name = if field[:original_type] == :belongs_to && field[:original_name]
|
68
|
+
field[:original_name]
|
69
|
+
else
|
70
|
+
field_name
|
71
|
+
end
|
72
|
+
|
73
|
+
easy_admin_url_helpers.suggest_resource_path(resource_name, field: search_field_name)
|
74
|
+
end
|
75
|
+
|
76
|
+
def label_classes
|
77
|
+
"block text-sm font-medium text-gray-700 mb-1"
|
78
|
+
end
|
79
|
+
|
80
|
+
def select_container_classes
|
81
|
+
base_classes = "relative"
|
82
|
+
classes = [base_classes]
|
83
|
+
classes << "select-multiple" if multiple?
|
84
|
+
classes.join(" ")
|
85
|
+
end
|
86
|
+
|
87
|
+
def render_multiple_select_container
|
88
|
+
div(class: "relative border border-gray-300 rounded-md bg-white min-h-10 focus-within:ring-1 focus-within:ring-blue-500 focus-within:border-blue-500 transition-colors duration-200") do
|
89
|
+
div(class: "flex flex-wrap items-center gap-1 p-2 pr-8") do
|
90
|
+
# Selected items container - will be populated by Stimulus controller
|
91
|
+
div(class: "flex flex-wrap items-center gap-1", data: { select_field_target: "selectedItems" }) do
|
92
|
+
current_selected_values.each do |selected_value|
|
93
|
+
render_selected_item(selected_value)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Search input
|
98
|
+
input(
|
99
|
+
type: "text",
|
100
|
+
class: "flex-1 min-w-20 border-none outline-none text-sm placeholder-gray-400 bg-transparent",
|
101
|
+
placeholder: current_selected_values.empty? ? placeholder : "",
|
102
|
+
data: {
|
103
|
+
select_field_target: "search",
|
104
|
+
action: "input->select-field#filter keydown->select-field#handleKeydown focus->select-field#openDropdown"
|
105
|
+
}
|
106
|
+
)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def render_search_input
|
112
|
+
input(
|
113
|
+
type: "text",
|
114
|
+
class: search_input_classes,
|
115
|
+
placeholder: placeholder,
|
116
|
+
data: {
|
117
|
+
select_field_target: "search",
|
118
|
+
action: "input->select-field#filter keydown->select-field#handleKeydown"
|
119
|
+
}
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
def render_single_select_input
|
124
|
+
div(class: "relative") do
|
125
|
+
input(
|
126
|
+
type: "text",
|
127
|
+
class: single_select_input_classes,
|
128
|
+
placeholder: suggest_mode? ? placeholder : (field[:placeholder] || "Select #{field_label.downcase}..."),
|
129
|
+
readonly: !suggest_mode?,
|
130
|
+
value: suggest_mode? ? "" : current_display_value,
|
131
|
+
data: suggest_mode? ? {
|
132
|
+
select_field_target: "search",
|
133
|
+
action: "input->select-field#filter keydown->select-field#handleKeydown focus->select-field#openDropdown"
|
134
|
+
} : {
|
135
|
+
select_field_target: "display",
|
136
|
+
action: "click->select-field#toggleDropdown"
|
137
|
+
}
|
138
|
+
)
|
139
|
+
# Dropdown arrow
|
140
|
+
div(class: "absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none") do
|
141
|
+
unsafe_raw('<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>')
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def search_input_classes
|
149
|
+
base_classes = "block w-full px-3 py-2 text-sm border rounded-md"
|
150
|
+
state_classes = "border-gray-300 placeholder-gray-400 bg-white"
|
151
|
+
focus_classes = "focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
152
|
+
hover_classes = "hover:border-gray-400"
|
153
|
+
transition_classes = "transition-colors duration-200"
|
154
|
+
|
155
|
+
"#{base_classes} #{state_classes} #{focus_classes} #{hover_classes} #{transition_classes}"
|
156
|
+
end
|
157
|
+
|
158
|
+
def single_select_input_classes
|
159
|
+
base_classes = "block w-full px-3 py-2 pr-10 text-sm border rounded-md cursor-pointer"
|
160
|
+
state_classes = "border-gray-300 placeholder-gray-400 bg-white"
|
161
|
+
focus_classes = "focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
162
|
+
hover_classes = "hover:border-gray-400"
|
163
|
+
transition_classes = "transition-colors duration-200"
|
164
|
+
|
165
|
+
"#{base_classes} #{state_classes} #{focus_classes} #{hover_classes} #{transition_classes}"
|
166
|
+
end
|
167
|
+
|
168
|
+
def selected_items_classes
|
169
|
+
base_classes = "selected-items flex flex-wrap gap-1"
|
170
|
+
padding_classes = "p-2 min-h-10"
|
171
|
+
border_classes = "border border-gray-300 rounded-md bg-white"
|
172
|
+
|
173
|
+
"#{base_classes} #{padding_classes} #{border_classes}"
|
174
|
+
end
|
175
|
+
|
176
|
+
def render_selected_item(selected_value)
|
177
|
+
span(
|
178
|
+
class: "selected-item inline-flex items-center px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded cursor-pointer hover:bg-blue-200 transition-colors duration-150",
|
179
|
+
data: {
|
180
|
+
value: selected_value,
|
181
|
+
action: "click->select-field#removeItem"
|
182
|
+
}
|
183
|
+
) do
|
184
|
+
plain display_text_for_value(selected_value)
|
185
|
+
span(class: "remove-item ml-1 text-blue-600 hover:text-blue-800 font-medium") { plain "×" }
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def render_select_dropdown
|
190
|
+
div(
|
191
|
+
class: "select-dropdown absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto opacity-0 invisible transform scale-95 transition-all duration-200 ease-out",
|
192
|
+
data: { select_field_target: "dropdown" }
|
193
|
+
) do
|
194
|
+
if suggest_mode?
|
195
|
+
# For suggest mode, options will be loaded dynamically
|
196
|
+
render_no_results_message
|
197
|
+
render_loading_message
|
198
|
+
else
|
199
|
+
# For static mode, render all options
|
200
|
+
options.each do |option|
|
201
|
+
render_option(option)
|
202
|
+
end
|
203
|
+
render_no_results_message
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def render_option(option)
|
209
|
+
option_value, option_text = extract_option_value_and_text(option)
|
210
|
+
|
211
|
+
div(
|
212
|
+
class: "select-option px-3 py-2 text-sm text-gray-900 cursor-pointer hover:bg-blue-50 hover:text-blue-900 transition-colors duration-150",
|
213
|
+
data: {
|
214
|
+
value: option_value,
|
215
|
+
action: "click->select-field#selectOption",
|
216
|
+
select_field_target: "option"
|
217
|
+
}
|
218
|
+
) do
|
219
|
+
plain option_text.to_s
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def render_no_results_message
|
224
|
+
div(
|
225
|
+
class: "select-no-results px-3 py-2 text-sm text-gray-500 text-center",
|
226
|
+
data: { select_field_target: "noResults" },
|
227
|
+
style: "display: none;"
|
228
|
+
) do
|
229
|
+
plain "No options found"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def render_loading_message
|
234
|
+
div(
|
235
|
+
class: "select-loading px-3 py-2 text-sm text-gray-500 text-center",
|
236
|
+
data: { select_field_target: "loading" },
|
237
|
+
style: "display: none;"
|
238
|
+
) do
|
239
|
+
plain "Loading..."
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def render_clear_button
|
244
|
+
button(
|
245
|
+
type: "button",
|
246
|
+
class: "select-clear absolute top-2 right-8 p-1 text-gray-400 hover:text-gray-600 bg-white rounded transition-colors duration-150",
|
247
|
+
data: { action: "click->select-field#clearAll" },
|
248
|
+
title: "Clear all selections"
|
249
|
+
) do
|
250
|
+
unsafe_raw('<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>')
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def render_hidden_inputs
|
255
|
+
if multiple?
|
256
|
+
current_selected_values.each do |selected_value|
|
257
|
+
input(
|
258
|
+
type: "hidden",
|
259
|
+
name: "#{form_field_name}[]",
|
260
|
+
value: selected_value,
|
261
|
+
data: { select_field_target: "hiddenInput" }
|
262
|
+
)
|
263
|
+
end
|
264
|
+
else
|
265
|
+
# Single select - one hidden input
|
266
|
+
current_value = form.object.public_send(field_name) if form.object.respond_to?(field_name)
|
267
|
+
input(
|
268
|
+
type: "hidden",
|
269
|
+
name: form_field_name,
|
270
|
+
value: current_value || "",
|
271
|
+
data: { select_field_target: "hiddenInput" }
|
272
|
+
)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def extract_option_value_and_text(option)
|
277
|
+
if option.is_a?(Array)
|
278
|
+
[option[1], option[0]]
|
279
|
+
elsif option.is_a?(Hash)
|
280
|
+
[option[:value] || option["value"], option[:text] || option["text"] || option[:label] || option["label"]]
|
281
|
+
else
|
282
|
+
[option, option]
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def form_field_name
|
287
|
+
model_name = form.object.class.name.underscore
|
288
|
+
"#{model_name}[#{field_name}]"
|
289
|
+
end
|
290
|
+
|
291
|
+
def current_selected_values
|
292
|
+
current_value = form.object.public_send(field_name) if form.object.respond_to?(field_name)
|
293
|
+
|
294
|
+
if multiple?
|
295
|
+
current_value.is_a?(Array) ? current_value : []
|
296
|
+
else
|
297
|
+
current_value.present? ? [current_value] : []
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def current_display_value
|
302
|
+
current_value = form.object.public_send(field_name) if form.object.respond_to?(field_name)
|
303
|
+
return "" if current_value.blank?
|
304
|
+
|
305
|
+
# Find the display text for the current value
|
306
|
+
options.each do |option|
|
307
|
+
option_value, option_text = extract_option_value_and_text(option)
|
308
|
+
return option_text if option_value.to_s == current_value.to_s
|
309
|
+
end
|
310
|
+
|
311
|
+
current_value.to_s
|
312
|
+
end
|
313
|
+
|
314
|
+
def display_text_for_value(value)
|
315
|
+
# Find the display text for the given value
|
316
|
+
options.each do |option|
|
317
|
+
option_value, option_text = extract_option_value_and_text(option)
|
318
|
+
return option_text if option_value.to_s == value.to_s
|
319
|
+
end
|
320
|
+
|
321
|
+
value.to_s
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Form
|
4
|
+
class TextComponent < 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: "text",
|
15
|
+
name: form_field_name,
|
16
|
+
id: field_id,
|
17
|
+
value: current_value,
|
18
|
+
class: input_classes,
|
19
|
+
required: required?,
|
20
|
+
placeholder: field[:placeholder] || "Enter #{field_label.downcase}"
|
21
|
+
)
|
22
|
+
if field[:help_text]
|
23
|
+
p(class: "mt-1 text-sm text-gray-500") { field[:help_text] }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def label_classes
|
31
|
+
"block text-sm font-medium text-gray-700 mb-1"
|
32
|
+
end
|
33
|
+
|
34
|
+
def input_classes
|
35
|
+
base_classes = "block w-full px-3 py-2 border rounded-md shadow-sm text-sm"
|
36
|
+
state_classes = "border-gray-300 placeholder-gray-400"
|
37
|
+
focus_classes = "focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
38
|
+
hover_classes = "hover:border-gray-400"
|
39
|
+
transition_classes = "transition-colors duration-200"
|
40
|
+
|
41
|
+
"#{base_classes} #{state_classes} #{focus_classes} #{hover_classes} #{transition_classes}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def form_field_name
|
45
|
+
model_name = form.object.class.name.underscore
|
46
|
+
"#{model_name}[#{field_name}]"
|
47
|
+
end
|
48
|
+
|
49
|
+
def current_value
|
50
|
+
form.object.public_send(field_name) if form.object.respond_to?(field_name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Form
|
4
|
+
class TextareaComponent < 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
|
+
textarea(
|
14
|
+
name: form_field_name,
|
15
|
+
id: field_id,
|
16
|
+
class: textarea_classes,
|
17
|
+
required: required?,
|
18
|
+
rows: field[:rows] || 4,
|
19
|
+
placeholder: field[:placeholder] || "Enter #{field_label.downcase}"
|
20
|
+
) { current_value }
|
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 textarea_classes
|
34
|
+
base_classes = "block w-full px-3 py-2 border rounded-md shadow-sm text-sm resize-y"
|
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
|
+
form.object.public_send(field_name) if form.object.respond_to?(field_name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Index
|
4
|
+
class BelongsToComponent < BaseComponent
|
5
|
+
private
|
6
|
+
|
7
|
+
def view_template
|
8
|
+
div(class: "field-cell") do
|
9
|
+
if association_record
|
10
|
+
if field[:show_link]
|
11
|
+
a(
|
12
|
+
href: view_path_for(association_record),
|
13
|
+
class: "field-link field-link--compact"
|
14
|
+
) do
|
15
|
+
plain display_text
|
16
|
+
end
|
17
|
+
else
|
18
|
+
span(class: "field-text") { plain display_text }
|
19
|
+
end
|
20
|
+
else
|
21
|
+
span(class: "field-empty") { plain "—" }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def association_record
|
27
|
+
@association_record ||= load_association_record
|
28
|
+
end
|
29
|
+
|
30
|
+
def load_association_record
|
31
|
+
return nil if value.blank?
|
32
|
+
|
33
|
+
association_class = determine_association
|
34
|
+
|
35
|
+
if value.respond_to?(:id)
|
36
|
+
# Already loaded record
|
37
|
+
value
|
38
|
+
else
|
39
|
+
# ID value, need to load the record
|
40
|
+
association_class.find_by(id: value)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def determine_association
|
45
|
+
return field[:association] if field[:association]
|
46
|
+
|
47
|
+
# Try to determine from attribute name
|
48
|
+
if field_name.to_s.end_with?('_id')
|
49
|
+
field_name.to_s.gsub('_id', '').classify.constantize
|
50
|
+
else
|
51
|
+
field_name.to_s.classify.constantize
|
52
|
+
end
|
53
|
+
rescue NameError
|
54
|
+
raise ArgumentError, "Could not determine association for #{field_name}. Please specify :association option."
|
55
|
+
end
|
56
|
+
|
57
|
+
def display_text
|
58
|
+
return "—" unless association_record
|
59
|
+
|
60
|
+
text = if field[:display_method]
|
61
|
+
association_record.send(field[:display_method])
|
62
|
+
elsif association_record.respond_to?(:name)
|
63
|
+
association_record.name
|
64
|
+
elsif association_record.respond_to?(:title)
|
65
|
+
association_record.title
|
66
|
+
elsif association_record.respond_to?(:to_label)
|
67
|
+
association_record.to_label
|
68
|
+
else
|
69
|
+
"##{association_record.id}"
|
70
|
+
end
|
71
|
+
|
72
|
+
# Truncate for index display
|
73
|
+
if field[:truncate] != false
|
74
|
+
truncate_length = field[:truncate] || 30
|
75
|
+
text.to_s.length > truncate_length ? "#{text.to_s[0...truncate_length]}..." : text.to_s
|
76
|
+
else
|
77
|
+
text.to_s
|
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,29 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Index
|
4
|
+
class BooleanComponent < BaseComponent
|
5
|
+
def view_template
|
6
|
+
span(class: badge_classes) do
|
7
|
+
if value
|
8
|
+
unsafe_raw '<span class="flex items-center"><svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" 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" clip-rule="evenodd"></path></svg>Active</span>'
|
9
|
+
else
|
10
|
+
unsafe_raw '<span class="flex items-center"><svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>Inactive</span>'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def badge_classes
|
18
|
+
base_classes = "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
19
|
+
|
20
|
+
if value
|
21
|
+
"#{base_classes} bg-green-100 text-green-800"
|
22
|
+
else
|
23
|
+
"#{base_classes} bg-gray-100 text-gray-800"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Index
|
4
|
+
class EmailComponent < BaseComponent
|
5
|
+
def view_template
|
6
|
+
if value.present?
|
7
|
+
a(href: "mailto:#{value}", class: "ea-link") do
|
8
|
+
truncate(value.to_s, length: 30)
|
9
|
+
end
|
10
|
+
else
|
11
|
+
span(class: "ea-empty-value") { "—" }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def truncate(text, length:)
|
18
|
+
return text if text.length <= length
|
19
|
+
"#{text[0, length]}..."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Index
|
4
|
+
module Filters
|
5
|
+
class BaseComponent < Phlex::HTML
|
6
|
+
def initialize(field:, search_params: {})
|
7
|
+
@field = field
|
8
|
+
@search_params = search_params
|
9
|
+
end
|
10
|
+
|
11
|
+
def view_template
|
12
|
+
div(class: "space-y-2") do
|
13
|
+
render_label
|
14
|
+
render_filter_input
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def render_label
|
21
|
+
label(
|
22
|
+
for: field_id,
|
23
|
+
class: "block text-sm font-medium text-gray-700"
|
24
|
+
) { @field[:label] }
|
25
|
+
end
|
26
|
+
|
27
|
+
def render_filter_input
|
28
|
+
# Override in subclasses
|
29
|
+
end
|
30
|
+
|
31
|
+
def field_id
|
32
|
+
"filter_#{@field[:name]}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def base_input_classes
|
36
|
+
base_classes = "block w-full px-3 py-2 border rounded-md shadow-sm text-sm"
|
37
|
+
state_classes = "border-gray-300 placeholder-gray-400"
|
38
|
+
focus_classes = "focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
39
|
+
hover_classes = "hover:border-gray-400"
|
40
|
+
transition_classes = "transition-colors duration-200"
|
41
|
+
|
42
|
+
"#{base_classes} #{state_classes} #{focus_classes} #{hover_classes} #{transition_classes}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|