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,416 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Form
|
4
|
+
class HasManyComponent < 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.to_s.singularize}_ids"
|
23
|
+
}
|
24
|
+
) do
|
25
|
+
render_multiple_select_container
|
26
|
+
render_select_dropdown
|
27
|
+
render_clear_button
|
28
|
+
render_hidden_inputs
|
29
|
+
end
|
30
|
+
|
31
|
+
if field[:help_text]
|
32
|
+
p(class: "mt-1 text-sm text-gray-500") { field[:help_text] }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def multiple?
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def suggest_mode?
|
44
|
+
result = field[:suggest].present?
|
45
|
+
Rails.logger.debug "HasMany suggest_mode? #{field_name}: #{result}, suggest config: #{field[:suggest]}"
|
46
|
+
result
|
47
|
+
end
|
48
|
+
|
49
|
+
def suggest_url
|
50
|
+
return "" unless suggest_mode?
|
51
|
+
|
52
|
+
# Extract resource name from form object
|
53
|
+
resource_name = form.object.class.name.underscore.pluralize
|
54
|
+
|
55
|
+
# For has_many fields, use the association name
|
56
|
+
search_field_name = field_name
|
57
|
+
|
58
|
+
easy_admin_url_helpers.suggest_resource_path(resource_name, field: search_field_name)
|
59
|
+
end
|
60
|
+
|
61
|
+
def placeholder
|
62
|
+
field[:placeholder] || "Search #{field_label.downcase}..."
|
63
|
+
end
|
64
|
+
|
65
|
+
def label_classes
|
66
|
+
"block text-sm font-medium text-gray-700 mb-1"
|
67
|
+
end
|
68
|
+
|
69
|
+
def select_container_classes
|
70
|
+
base_classes = "relative"
|
71
|
+
classes = [base_classes]
|
72
|
+
classes << "select-multiple"
|
73
|
+
classes.join(" ")
|
74
|
+
end
|
75
|
+
|
76
|
+
def render_multiple_select_container
|
77
|
+
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
|
78
|
+
div(class: "flex flex-wrap items-center gap-1 p-2 pr-8") do
|
79
|
+
# Selected items container - will be populated by Stimulus controller
|
80
|
+
div(class: "flex flex-wrap items-center gap-1", data: { select_field_target: "selectedItems" }) do
|
81
|
+
current_selected_values.each do |selected_value|
|
82
|
+
render_selected_item(selected_value)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Search input
|
87
|
+
input(
|
88
|
+
type: "text",
|
89
|
+
class: "flex-1 min-w-20 border-none outline-none text-sm placeholder-gray-400 bg-transparent",
|
90
|
+
placeholder: current_selected_values.empty? ? placeholder : "",
|
91
|
+
data: {
|
92
|
+
select_field_target: "search",
|
93
|
+
action: "input->select-field#filter keydown->select-field#handleKeydown focus->select-field#openDropdown"
|
94
|
+
}
|
95
|
+
)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def render_selected_item(selected_value)
|
101
|
+
span(
|
102
|
+
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",
|
103
|
+
data: {
|
104
|
+
value: selected_value,
|
105
|
+
action: "click->select-field#removeItem"
|
106
|
+
}
|
107
|
+
) do
|
108
|
+
plain display_text_for_value(selected_value)
|
109
|
+
span(class: "remove-item ml-1 text-blue-600 hover:text-blue-800 font-medium") { plain "×" }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def render_select_dropdown
|
114
|
+
div(
|
115
|
+
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",
|
116
|
+
data: { select_field_target: "dropdown" }
|
117
|
+
) do
|
118
|
+
if suggest_mode?
|
119
|
+
# For suggest mode, options will be loaded dynamically
|
120
|
+
render_no_results_message
|
121
|
+
render_loading_message
|
122
|
+
else
|
123
|
+
# For static mode, render all options
|
124
|
+
options.each do |option|
|
125
|
+
render_option(option)
|
126
|
+
end
|
127
|
+
render_no_results_message
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def render_option(option)
|
133
|
+
option_value, option_text = extract_option_value_and_text(option)
|
134
|
+
|
135
|
+
div(
|
136
|
+
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",
|
137
|
+
data: {
|
138
|
+
value: option_value,
|
139
|
+
action: "click->select-field#selectOption",
|
140
|
+
select_field_target: "option"
|
141
|
+
}
|
142
|
+
) do
|
143
|
+
plain option_text.to_s
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def render_no_results_message
|
148
|
+
div(
|
149
|
+
class: "select-no-results px-3 py-2 text-sm text-gray-500 text-center",
|
150
|
+
data: { select_field_target: "noResults" },
|
151
|
+
style: "display: none;"
|
152
|
+
) do
|
153
|
+
plain "No options found"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def render_loading_message
|
158
|
+
div(
|
159
|
+
class: "select-loading px-3 py-2 text-sm text-gray-500 text-center",
|
160
|
+
data: { select_field_target: "loading" },
|
161
|
+
style: "display: none;"
|
162
|
+
) do
|
163
|
+
plain "Loading..."
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def render_clear_button
|
168
|
+
button(
|
169
|
+
type: "button",
|
170
|
+
class: "select-clear absolute top-2 right-8 p-1 text-gray-400 hover:text-gray-600 bg-white rounded transition-colors duration-150",
|
171
|
+
data: { action: "click->select-field#clearAll" },
|
172
|
+
title: "Clear all selections"
|
173
|
+
) do
|
174
|
+
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>')
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def extract_option_value_and_text(option)
|
179
|
+
if option.is_a?(Array)
|
180
|
+
[option[1], option[0]]
|
181
|
+
elsif option.is_a?(Hash)
|
182
|
+
[option[:value] || option["value"], option[:text] || option["text"] || option[:label] || option["label"]]
|
183
|
+
else
|
184
|
+
[option, option]
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def display_text_for_value(value)
|
189
|
+
# Find the display text for the given value
|
190
|
+
options.each do |option|
|
191
|
+
option_value, option_text = extract_option_value_and_text(option)
|
192
|
+
return option_text if option_value.to_s == value.to_s
|
193
|
+
end
|
194
|
+
|
195
|
+
# If not found in options and we have a current association, try to get it from there
|
196
|
+
if form.object&.respond_to?(field_name)
|
197
|
+
association_records = form.object.public_send(field_name)
|
198
|
+
found_record = nil
|
199
|
+
|
200
|
+
if association_records.respond_to?(:find)
|
201
|
+
found_record = association_records.find { |r| r.id.to_s == value.to_s }
|
202
|
+
elsif association_records.respond_to?(:detect)
|
203
|
+
found_record = association_records.detect { |r| r.id.to_s == value.to_s }
|
204
|
+
end
|
205
|
+
|
206
|
+
if found_record
|
207
|
+
return display_text_for(found_record)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
value.to_s
|
212
|
+
end
|
213
|
+
|
214
|
+
def options
|
215
|
+
@options ||= build_options
|
216
|
+
end
|
217
|
+
|
218
|
+
def form_field_name
|
219
|
+
model_name = form.object.class.name.underscore
|
220
|
+
"#{model_name}[#{field_name.to_s.singularize}_ids]"
|
221
|
+
end
|
222
|
+
|
223
|
+
def current_selected_values
|
224
|
+
if form.object&.respond_to?(field_name)
|
225
|
+
association_records = form.object.public_send(field_name)
|
226
|
+
if association_records.respond_to?(:pluck)
|
227
|
+
association_records.pluck(:id).map(&:to_s)
|
228
|
+
elsif association_records.respond_to?(:map)
|
229
|
+
association_records.map { |r| r.id.to_s }
|
230
|
+
else
|
231
|
+
[]
|
232
|
+
end
|
233
|
+
else
|
234
|
+
[]
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Override to render hidden inputs properly for has_many associations
|
239
|
+
# def view_template
|
240
|
+
# div(class: "mb-4") do
|
241
|
+
# label(for: field_id, class: label_classes) do
|
242
|
+
# plain field_label
|
243
|
+
# if required?
|
244
|
+
# span(class: "text-red-500 ml-1") { "*" }
|
245
|
+
# end
|
246
|
+
# end
|
247
|
+
#
|
248
|
+
# div(
|
249
|
+
# class: select_container_classes,
|
250
|
+
# data: {
|
251
|
+
# controller: "select-field",
|
252
|
+
# select_field_multiple_value: multiple?.to_s,
|
253
|
+
# select_field_placeholder_value: placeholder,
|
254
|
+
# field_name: field_name
|
255
|
+
# }
|
256
|
+
# ) do
|
257
|
+
# if multiple?
|
258
|
+
# render_multiple_select_container
|
259
|
+
# else
|
260
|
+
# render_single_select_input
|
261
|
+
# end
|
262
|
+
# render_select_dropdown
|
263
|
+
# render_clear_button if multiple?
|
264
|
+
#
|
265
|
+
# # Render hidden inputs INSIDE the controller scope
|
266
|
+
# render_hidden_inputs
|
267
|
+
# end
|
268
|
+
#
|
269
|
+
# if field[:help_text]
|
270
|
+
# p(class: "mt-1 text-sm text-gray-500") { field[:help_text] }
|
271
|
+
# end
|
272
|
+
# end
|
273
|
+
# end
|
274
|
+
|
275
|
+
def render_hidden_inputs
|
276
|
+
current_selected_values.each do |selected_value|
|
277
|
+
input(
|
278
|
+
type: "hidden",
|
279
|
+
name: "#{form_field_name}[]",
|
280
|
+
value: selected_value,
|
281
|
+
data: { select_field_target: "hiddenInput" }
|
282
|
+
)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def build_options
|
287
|
+
available_options.map do |item|
|
288
|
+
[display_text_for(item), item.id]
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def available_options
|
293
|
+
@available_options ||= load_available_options
|
294
|
+
end
|
295
|
+
|
296
|
+
def load_available_options
|
297
|
+
# Get the association class
|
298
|
+
association_class = get_association_class
|
299
|
+
return [] unless association_class
|
300
|
+
|
301
|
+
# Load all available options (limit for performance if needed)
|
302
|
+
association_class.limit(field[:limit] || 1000)
|
303
|
+
end
|
304
|
+
|
305
|
+
def selected_ids
|
306
|
+
@selected_ids ||= load_selected_ids
|
307
|
+
end
|
308
|
+
|
309
|
+
def load_selected_ids
|
310
|
+
if form.object&.respond_to?(field_name)
|
311
|
+
association_records = form.object.public_send(field_name)
|
312
|
+
if association_records.respond_to?(:pluck)
|
313
|
+
association_records.pluck(:id).map(&:to_s)
|
314
|
+
elsif association_records.respond_to?(:map)
|
315
|
+
association_records.map { |r| r.id.to_s }
|
316
|
+
else
|
317
|
+
[]
|
318
|
+
end
|
319
|
+
else
|
320
|
+
[]
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def get_association_class
|
325
|
+
# Try to get the association class from the model
|
326
|
+
if form.object.class.respond_to?(:reflect_on_association)
|
327
|
+
association = form.object.class.reflect_on_association(field_name.to_sym)
|
328
|
+
return association&.klass
|
329
|
+
end
|
330
|
+
|
331
|
+
# Fallback: try to constantize the association name
|
332
|
+
begin
|
333
|
+
association_name.singularize.camelize.constantize
|
334
|
+
rescue NameError
|
335
|
+
nil
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def association_name
|
340
|
+
@association_name ||= determine_association_name
|
341
|
+
end
|
342
|
+
|
343
|
+
def determine_association_name
|
344
|
+
return field[:association_name] if field[:association_name]
|
345
|
+
field_name.to_s
|
346
|
+
end
|
347
|
+
|
348
|
+
def search_url
|
349
|
+
# TODO: Create proper route helper for associations search
|
350
|
+
field[:search_url] || "#{EasyAdmin::Engine.routes.url_helpers.resources_path(resource_name: 'associations').chomp('/')}/search"
|
351
|
+
end
|
352
|
+
|
353
|
+
def hidden_field_name_base
|
354
|
+
return field[:form_field_name] if field[:form_field_name]
|
355
|
+
model_name = form.object.class.name.underscore
|
356
|
+
"#{model_name}[#{field_name.to_s.singularize}_ids]"
|
357
|
+
end
|
358
|
+
|
359
|
+
def display_text_for(item)
|
360
|
+
display_method = field[:display_method] || :name
|
361
|
+
|
362
|
+
if display_method.respond_to?(:call)
|
363
|
+
display_method.call(item)
|
364
|
+
elsif item.respond_to?(display_method)
|
365
|
+
item.send(display_method)
|
366
|
+
elsif item.respond_to?(:name)
|
367
|
+
item.name
|
368
|
+
elsif item.respond_to?(:title)
|
369
|
+
item.title
|
370
|
+
elsif item.respond_to?(:to_label)
|
371
|
+
item.to_label
|
372
|
+
else
|
373
|
+
"##{item.id}"
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
def details_for(item)
|
378
|
+
if field[:details_method]
|
379
|
+
item.send(field[:details_method])
|
380
|
+
elsif item.respond_to?(:created_at)
|
381
|
+
"Created #{item.created_at.strftime('%b %d, %Y')}"
|
382
|
+
else
|
383
|
+
"ID: #{item.id}"
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
def view_path_for(item)
|
388
|
+
field[:view_path_proc]&.call(item) ||
|
389
|
+
EasyAdmin::Engine.routes.url_helpers.resource_path(
|
390
|
+
resource_name: item.class.name.underscore.pluralize,
|
391
|
+
id: item.id
|
392
|
+
)
|
393
|
+
end
|
394
|
+
|
395
|
+
def edit_path_for(item)
|
396
|
+
field[:edit_path_proc]&.call(item) ||
|
397
|
+
EasyAdmin::Engine.routes.url_helpers.edit_resource_path(
|
398
|
+
resource_name: item.class.name.underscore.pluralize,
|
399
|
+
id: item.id
|
400
|
+
)
|
401
|
+
end
|
402
|
+
|
403
|
+
def new_path
|
404
|
+
field[:new_path_proc]&.call ||
|
405
|
+
EasyAdmin::Engine.routes.url_helpers.new_resource_path(
|
406
|
+
resource_name: association_name.singularize.pluralize
|
407
|
+
)
|
408
|
+
end
|
409
|
+
|
410
|
+
def hidden_field_name
|
411
|
+
"#{hidden_field_name_base}[]"
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Form
|
4
|
+
class JsonComponent < Fields::BaseComponent
|
5
|
+
def view_template
|
6
|
+
div(class: "space-y-2") do
|
7
|
+
label(
|
8
|
+
for: field_id,
|
9
|
+
class: "block text-sm font-medium text-gray-700"
|
10
|
+
) { field_label }
|
11
|
+
|
12
|
+
div(
|
13
|
+
class: "relative",
|
14
|
+
data: {
|
15
|
+
controller: "jsoneditor",
|
16
|
+
jsoneditor_mode_value: "code",
|
17
|
+
jsoneditor_data_value: current_json_value.to_json
|
18
|
+
}
|
19
|
+
) do
|
20
|
+
# Hidden field to store the JSON value
|
21
|
+
input(
|
22
|
+
type: "hidden",
|
23
|
+
name: form_field_name,
|
24
|
+
id: "#{field_id}_hidden",
|
25
|
+
value: current_json_value_string,
|
26
|
+
data: { jsoneditor_target: "hiddenField" }
|
27
|
+
)
|
28
|
+
|
29
|
+
# JSONEditor container
|
30
|
+
div(
|
31
|
+
id: field_id,
|
32
|
+
data: { jsoneditor_target: "editor" },
|
33
|
+
style: "height: 400px; border: 1px solid #d1d5db; border-radius: 0.375rem;"
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
if field[:help_text].present?
|
38
|
+
p(class: "text-sm text-gray-500") { field[:help_text] }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def form_field_name
|
46
|
+
model_name = form.object.class.name.underscore
|
47
|
+
"#{model_name}[#{field_name}]"
|
48
|
+
end
|
49
|
+
|
50
|
+
def current_json_value_string
|
51
|
+
current_value = form.object.public_send(field_name) || value
|
52
|
+
return "" unless current_value.present?
|
53
|
+
|
54
|
+
if current_value.is_a?(String)
|
55
|
+
current_value
|
56
|
+
else
|
57
|
+
JSON.pretty_generate(current_value)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def current_json_value
|
62
|
+
current_value = form.object.public_send(field_name) || value
|
63
|
+
|
64
|
+
if current_value.present?
|
65
|
+
if current_value.is_a?(String)
|
66
|
+
begin
|
67
|
+
JSON.parse(current_value)
|
68
|
+
rescue JSON::ParserError
|
69
|
+
{ error: "Invalid JSON", raw_value: current_value }
|
70
|
+
end
|
71
|
+
else
|
72
|
+
current_value
|
73
|
+
end
|
74
|
+
else
|
75
|
+
{}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module EasyAdmin
|
2
|
+
module Fields
|
3
|
+
module Form
|
4
|
+
class NumberComponent < 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: "number",
|
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
|