admin_suite 0.3.1 → 0.5.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 +4 -4
- data/CHANGELOG.md +210 -1
- data/app/assets/vendor/chart.umd.min.js +14 -0
- data/app/assets/vendor/easymde.min.css +7 -0
- data/app/assets/vendor/easymde.min.js +7 -0
- data/app/controllers/admin_suite/application_controller.rb +26 -47
- data/app/controllers/admin_suite/portals_controller.rb +2 -2
- data/app/controllers/admin_suite/resources_controller.rb +138 -6
- data/app/helpers/admin_suite/base_helper.rb +232 -391
- data/app/javascript/admin_suite_application.js +3 -0
- data/app/javascript/controllers/admin_suite/chart_controller.js +173 -0
- data/app/javascript/controllers/admin_suite/markdown_editor_controller.js +21 -2
- data/app/views/admin_suite/panels/_chart.html.erb +158 -18
- data/app/views/admin_suite/panels/_stat.html.erb +10 -0
- data/app/views/admin_suite/resources/index.html.erb +31 -82
- data/app/views/admin_suite/shared/_pagination.html.erb +74 -0
- data/app/views/admin_suite/shared/_sidebar.html.erb +15 -9
- data/app/views/layouts/admin_suite/application.html.erb +11 -3
- data/config/routes.rb +3 -0
- data/lib/admin/base/action_executor.rb +19 -51
- data/lib/admin/base/filter_builder.rb +48 -5
- data/lib/admin/base/resource.rb +92 -17
- data/lib/admin_suite/configuration.rb +32 -3
- data/lib/admin_suite/definition_loader.rb +194 -0
- data/lib/admin_suite/deprecation.rb +48 -0
- data/lib/admin_suite/engine.rb +55 -76
- data/lib/admin_suite/host_autoload_policy.rb +132 -0
- data/lib/admin_suite/legacy_custom_renderer_procs.rb +29 -0
- data/lib/admin_suite/portal_definition.rb +11 -0
- data/lib/admin_suite/renderer.rb +133 -0
- data/lib/admin_suite/renderer_registry.rb +81 -0
- data/lib/admin_suite/renderers/code_renderer.rb +15 -0
- data/lib/admin_suite/renderers/json_renderer.rb +15 -0
- data/lib/admin_suite/renderers/key_value_renderer.rb +39 -0
- data/lib/admin_suite/renderers/legacy_gleania.rb +232 -0
- data/lib/admin_suite/renderers/table_from_renderer.rb +22 -0
- data/lib/admin_suite/section_definition.rb +42 -0
- data/lib/admin_suite/ui/dashboard_definition.rb +6 -0
- data/lib/admin_suite/ui/field_renderer_registry.rb +31 -4
- data/lib/admin_suite/ui/form_field_renderer.rb +1 -7
- data/lib/admin_suite/ui/show_formatter_registry.rb +9 -0
- data/lib/admin_suite/ui/show_value_formatter.rb +7 -3
- data/lib/admin_suite/version.rb +1 -1
- data/lib/admin_suite.rb +48 -0
- data/lib/generators/admin_suite/install/templates/admin_suite.rb +0 -4
- data/test/controllers/resources_controller_test.rb +76 -1
- data/test/integration/association_linking_test.rb +292 -0
- data/test/integration/chart_panel_test.rb +491 -0
- data/test/integration/dashboard_test.rb +9 -2
- data/test/integration/index_table_test.rb +289 -0
- data/test/integration/layout_assets_test.rb +112 -0
- data/test/integration/navigation_sections_test.rb +62 -0
- data/test/integration/pagination_and_stats_test.rb +152 -0
- data/test/integration/searchable_select_search_test.rb +368 -0
- data/test/integration/show_hide_blank_test.rb +195 -0
- data/test/integration/toggle_test.rb +106 -0
- data/test/lib/action_executor_redirect_test.rb +41 -0
- data/test/lib/builtin_renderers_test.rb +202 -0
- data/test/lib/definition_loader_test.rb +276 -0
- data/test/lib/engine_defaults_test.rb +39 -0
- data/test/lib/form_field_renderer_test.rb +136 -0
- data/test/lib/format_table_cell_test.rb +49 -0
- data/test/lib/index_includes_test.rb +223 -0
- data/test/lib/legacy_renderer_deprecation_test.rb +42 -0
- data/test/lib/renderer_test.rb +237 -0
- data/test/lib/resource_exportable_deprecation_test.rb +47 -0
- data/test/lib/show_value_formatter_test.rb +88 -0
- data/test/lib/zeitwerk_integration_test.rb +28 -64
- data/test/test_helper.rb +121 -5
- metadata +62 -5
|
@@ -6,11 +6,9 @@ module AdminSuite
|
|
|
6
6
|
# This is intentionally very close to the `/internal/developer` helper so we can
|
|
7
7
|
# keep both UIs side-by-side and compare behavior while migrating.
|
|
8
8
|
module BaseHelper
|
|
9
|
-
include Pagy::Frontend
|
|
10
9
|
include AdminSuite::IconHelper
|
|
11
10
|
include AdminSuite::PanelsHelper
|
|
12
11
|
include AdminSuite::ThemeHelper
|
|
13
|
-
include ::Internal::Developer::CustomRenderersHelper if defined?(::Internal::Developer::CustomRenderersHelper)
|
|
14
12
|
# ActiveStorage route helpers live on the host app (main_app), not the isolated engine.
|
|
15
13
|
def admin_suite_rails_blob_path(...)
|
|
16
14
|
if respond_to?(:main_app) && main_app.respond_to?(:rails_blob_path)
|
|
@@ -86,9 +84,8 @@ module AdminSuite
|
|
|
86
84
|
end
|
|
87
85
|
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
prepend AdminSuite::UI::FormFieldRenderer
|
|
87
|
+
include AdminSuite::UI::ShowValueFormatter
|
|
88
|
+
include AdminSuite::UI::FormFieldRenderer
|
|
92
89
|
|
|
93
90
|
# Returns the color scheme for a portal
|
|
94
91
|
#
|
|
@@ -144,80 +141,43 @@ module AdminSuite
|
|
|
144
141
|
elsif column.content.is_a?(Proc)
|
|
145
142
|
column.content.call(record)
|
|
146
143
|
else
|
|
147
|
-
record.public_send(column.name) rescue "—"
|
|
144
|
+
value = record.public_send(column.name) rescue "—"
|
|
145
|
+
if active_record_base?(value)
|
|
146
|
+
render_association_value(value)
|
|
147
|
+
else
|
|
148
|
+
# `rescue "—"` above only fires on an exception; a genuinely nil
|
|
149
|
+
# attribute reaches here untouched and used to render as an empty
|
|
150
|
+
# cell. Every other surface in the gem (`format_table_cell`,
|
|
151
|
+
# `render_association_value`'s own rescues) already shows "—" for
|
|
152
|
+
# nil, so this closes the one place that didn't -- including a
|
|
153
|
+
# nil `belongs_to` value, which Task 3 deliberately left alone
|
|
154
|
+
# because this task owns it.
|
|
155
|
+
value.nil? ? "—" : value
|
|
156
|
+
end
|
|
148
157
|
end
|
|
149
158
|
end
|
|
150
159
|
|
|
151
|
-
#
|
|
160
|
+
# Maps a column's `align:` DSL option to a literal Tailwind class.
|
|
152
161
|
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
when
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
concat(content_tag(:span, "Yes", class: "text-green-600 font-medium"))
|
|
173
|
-
end
|
|
174
|
-
when false
|
|
175
|
-
content_tag(:span, class: "inline-flex items-center gap-1") do
|
|
176
|
-
svg = '<svg class="w-4 h-4 text-slate-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>'.html_safe
|
|
177
|
-
concat(svg)
|
|
178
|
-
concat(content_tag(:span, "No", class: "text-slate-500"))
|
|
179
|
-
end
|
|
180
|
-
when Time, DateTime
|
|
181
|
-
content_tag(:span, class: "inline-flex items-center gap-2") do
|
|
182
|
-
concat(content_tag(:span, value.strftime("%B %d, %Y at %H:%M"), class: "font-medium"))
|
|
183
|
-
concat(content_tag(:span, "(#{time_ago_in_words(value)} ago)", class: "text-slate-500 text-xs"))
|
|
184
|
-
end
|
|
185
|
-
when Date
|
|
186
|
-
value.strftime("%B %d, %Y")
|
|
187
|
-
when ActiveRecord::Base
|
|
188
|
-
link_text = value.respond_to?(:name) ? value.name : "#{value.class.name} ##{value.id}"
|
|
189
|
-
content_tag(:span, link_text, class: "text-indigo-600")
|
|
190
|
-
when Hash
|
|
191
|
-
render_json_block(value)
|
|
192
|
-
when Array
|
|
193
|
-
if value.empty?
|
|
194
|
-
content_tag(:span, "Empty array", class: "text-slate-400 italic")
|
|
195
|
-
elsif value.first.is_a?(Hash)
|
|
196
|
-
render_json_block(value)
|
|
197
|
-
else
|
|
198
|
-
content_tag(:div, class: "flex flex-wrap gap-1") do
|
|
199
|
-
value.each do |item|
|
|
200
|
-
concat(content_tag(:span, item.to_s, class: "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-700"))
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
when Integer, Float, BigDecimal
|
|
205
|
-
content_tag(:span, number_with_delimiter(value), class: "font-mono")
|
|
206
|
-
else
|
|
207
|
-
value_str = value.to_s
|
|
208
|
-
|
|
209
|
-
if value_str.start_with?("{", "[") && value_str.length > 10
|
|
210
|
-
begin
|
|
211
|
-
parsed = JSON.parse(value_str)
|
|
212
|
-
render_json_block(parsed)
|
|
213
|
-
rescue JSON::ParserError
|
|
214
|
-
render_text_block(value_str)
|
|
215
|
-
end
|
|
216
|
-
elsif value_str.include?("\n") || value_str.length > 200
|
|
217
|
-
render_text_block(value_str, detect_language(field_name, value_str))
|
|
218
|
-
else
|
|
219
|
-
value_str
|
|
220
|
-
end
|
|
162
|
+
# `align` is resource-author-supplied, not end-user input, but it's
|
|
163
|
+
# still an arbitrary value handed to us from outside this method, so
|
|
164
|
+
# this follows the same precedent as the stat panel's `color` mapping
|
|
165
|
+
# (`app/views/admin_suite/panels/_stat.html.erb`) and the dashboard
|
|
166
|
+
# row's `span` clamp (`panels_helper.rb#render_panel`): a closed
|
|
167
|
+
# `case`/`else` over known values, never `"text-#{align}"` string
|
|
168
|
+
# interpolation. Dynamic Tailwind class names are invisible to the
|
|
169
|
+
# content scanner (unstyled in production) even when they happen to be
|
|
170
|
+
# spelled correctly, and an unrecognized or malformed `align:` here
|
|
171
|
+
# must degrade to no alignment class rather than emit a broken one.
|
|
172
|
+
#
|
|
173
|
+
# @param align [Symbol, String, nil]
|
|
174
|
+
# @return [String]
|
|
175
|
+
def column_align_class(align)
|
|
176
|
+
case align.respond_to?(:to_sym) ? align.to_sym : align
|
|
177
|
+
when :right then "text-right"
|
|
178
|
+
when :center then "text-center"
|
|
179
|
+
when :left then "text-left"
|
|
180
|
+
else ""
|
|
221
181
|
end
|
|
222
182
|
end
|
|
223
183
|
|
|
@@ -341,213 +301,52 @@ module AdminSuite
|
|
|
341
301
|
nil
|
|
342
302
|
end
|
|
343
303
|
|
|
344
|
-
def render_custom_section(resource, render_type)
|
|
345
|
-
|
|
346
|
-
return renderer.call(resource, self) if renderer
|
|
347
|
-
|
|
348
|
-
case render_type.to_sym
|
|
349
|
-
when :prompt_template_preview
|
|
350
|
-
render_prompt_template(resource)
|
|
351
|
-
when :json_preview
|
|
352
|
-
render_json_preview(resource)
|
|
353
|
-
when :code_preview
|
|
354
|
-
render_code_preview(resource)
|
|
355
|
-
when :messages_preview
|
|
356
|
-
render_messages_preview(resource)
|
|
357
|
-
when :tool_args_preview
|
|
358
|
-
render_tool_args_preview(resource)
|
|
359
|
-
when :turn_messages_preview
|
|
360
|
-
render_turn_messages_preview(resource)
|
|
361
|
-
else
|
|
362
|
-
content_tag(:p, "Unknown render type: #{render_type}", class: "text-slate-500 italic")
|
|
363
|
-
end
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
# --- generic custom renderers (fallbacks) ---
|
|
367
|
-
def render_prompt_template(resource)
|
|
368
|
-
template = resource.respond_to?(:prompt_template) ? resource.prompt_template : nil
|
|
369
|
-
return content_tag(:p, "No template defined", class: "text-slate-500 italic") if template.blank?
|
|
304
|
+
def render_custom_section(resource, render_type, options = {})
|
|
305
|
+
key = render_type.to_sym
|
|
370
306
|
|
|
371
|
-
|
|
372
|
-
|
|
307
|
+
legacy_proc = AdminSuite.config.custom_renderers[key]
|
|
308
|
+
if legacy_proc
|
|
309
|
+
AdminSuite::LegacyCustomRendererProcs.warn_once(key)
|
|
310
|
+
return legacy_proc.call(resource, self)
|
|
373
311
|
end
|
|
374
312
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
variables = template.scan(/\{\{(\w+)\}\}/).flatten.uniq
|
|
391
|
-
if variables.any?
|
|
392
|
-
concat(content_tag(:div, class: "mt-3 pt-3 border-t border-slate-700") do
|
|
393
|
-
concat(content_tag(:span, "Variables: ", class: "text-sm text-slate-400"))
|
|
394
|
-
concat(content_tag(:div, class: "inline-flex flex-wrap gap-1 mt-1") do
|
|
395
|
-
variables.each do |var|
|
|
396
|
-
concat(content_tag(:code, "{{#{var}}}", class: "text-xs px-2 py-0.5 bg-amber-900/30 text-amber-400 rounded"))
|
|
397
|
-
end
|
|
398
|
-
end)
|
|
399
|
-
end)
|
|
400
|
-
end
|
|
401
|
-
end
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
def render_json_preview(resource)
|
|
405
|
-
data = resource.respond_to?(:data) ? resource.data : resource.attributes
|
|
406
|
-
render_json_block(data)
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
def render_code_preview(resource)
|
|
410
|
-
code = resource.respond_to?(:code) ? resource.code : resource.to_s
|
|
411
|
-
render_text_block(code, :ruby)
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
def render_messages_preview(resource)
|
|
415
|
-
messages = resource.respond_to?(:messages) ? resource.messages : []
|
|
416
|
-
if messages.respond_to?(:chronological)
|
|
417
|
-
messages = messages.chronological
|
|
418
|
-
end
|
|
419
|
-
messages = messages.limit(50) if messages.respond_to?(:limit)
|
|
420
|
-
messages = Array.wrap(messages)
|
|
421
|
-
|
|
422
|
-
return content_tag(:p, "No messages", class: "text-slate-500 italic") if messages.blank?
|
|
423
|
-
|
|
424
|
-
content_tag(:div, class: "space-y-4 max-h-[600px] overflow-y-auto -mx-6 -mb-6 p-6 pt-0") do
|
|
425
|
-
messages.each_with_index do |msg, idx|
|
|
426
|
-
if msg.respond_to?(:role)
|
|
427
|
-
role = msg.role
|
|
428
|
-
content = msg.content
|
|
429
|
-
created_at = msg.respond_to?(:created_at) ? msg.created_at : nil
|
|
430
|
-
else
|
|
431
|
-
role = msg["role"] || msg[:role] || "unknown"
|
|
432
|
-
content = msg["content"] || msg[:content] || ""
|
|
433
|
-
created_at = msg["created_at"] || msg[:created_at]
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
role_class = case role.to_s
|
|
437
|
-
when "user" then "bg-blue-50 border-blue-200"
|
|
438
|
-
when "assistant" then "bg-emerald-50 border-emerald-200"
|
|
439
|
-
when "tool" then "bg-amber-50 border-amber-200"
|
|
440
|
-
when "system" then "bg-slate-50 border-slate-200"
|
|
441
|
-
else "bg-slate-50 border-slate-200"
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
role_icon = case role.to_s
|
|
445
|
-
when "user"
|
|
446
|
-
'<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>'.html_safe
|
|
447
|
-
when "assistant"
|
|
448
|
-
'<svg class="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/></svg>'.html_safe
|
|
449
|
-
when "tool"
|
|
450
|
-
'<svg class="w-4 h-4 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/></svg>'.html_safe
|
|
451
|
-
else
|
|
452
|
-
'<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>'.html_safe
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
concat(content_tag(:div, class: "rounded-lg border p-4 #{role_class}") do
|
|
456
|
-
concat(content_tag(:div, class: "flex items-center justify-between mb-3") do
|
|
457
|
-
concat(content_tag(:div, class: "flex items-center gap-2") do
|
|
458
|
-
concat(role_icon)
|
|
459
|
-
concat(content_tag(:span, role.to_s.capitalize, class: "text-sm font-medium text-slate-700"))
|
|
460
|
-
end)
|
|
461
|
-
concat(content_tag(:div, class: "flex items-center gap-2 text-xs text-slate-400") do
|
|
462
|
-
concat(content_tag(:span, created_at.strftime("%H:%M:%S"))) if created_at.respond_to?(:strftime)
|
|
463
|
-
concat(content_tag(:span, "##{idx + 1}"))
|
|
464
|
-
end)
|
|
465
|
-
end)
|
|
466
|
-
|
|
467
|
-
content_str = content.to_s
|
|
468
|
-
if role.to_s == "tool" && content_str.start_with?("{", "[")
|
|
469
|
-
begin
|
|
470
|
-
parsed = JSON.parse(content_str)
|
|
471
|
-
concat(render_json_block(parsed))
|
|
472
|
-
rescue JSON::ParserError
|
|
473
|
-
concat(content_tag(:div, simple_format(h(content_str)), class: "prose prose-sm max-w-none"))
|
|
474
|
-
end
|
|
475
|
-
else
|
|
476
|
-
concat(content_tag(:div, simple_format(h(content_str)), class: "prose prose-sm max-w-none"))
|
|
477
|
-
end
|
|
478
|
-
end)
|
|
479
|
-
end
|
|
480
|
-
end
|
|
313
|
+
# Precedence, after the legacy proc above (unchanged, Task 3 has tests
|
|
314
|
+
# pinning that): a host's explicit registration, then a host renderer
|
|
315
|
+
# class, then the gem's own defaults (built-ins + the deprecated four
|
|
316
|
+
# Gleania renderers). Checking the gem defaults last means a host that
|
|
317
|
+
# defines `Admin::Renderers::<Key>Renderer` -- exactly the migration
|
|
318
|
+
# path the deprecation warnings recommend -- actually takes effect
|
|
319
|
+
# instead of being silently shadowed by the gem's boot-time
|
|
320
|
+
# registrations (see `RendererRegistry`).
|
|
321
|
+
klass = AdminSuite::RendererRegistry.lookup(key) ||
|
|
322
|
+
host_renderer_class(key) ||
|
|
323
|
+
AdminSuite::RendererRegistry.lookup_default(key)
|
|
324
|
+
return klass.new(resource, self, options).render if klass
|
|
325
|
+
|
|
326
|
+
content_tag(:p, "Unknown render type: #{render_type}", class: "text-slate-500 italic")
|
|
481
327
|
end
|
|
482
328
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
error = resource.respond_to?(:error) ? resource.error : nil
|
|
487
|
-
|
|
488
|
-
content_tag(:div, class: "space-y-6") do
|
|
489
|
-
concat(content_tag(:div) do
|
|
490
|
-
concat(content_tag(:h4, "Arguments", class: "text-sm font-medium text-slate-500 mb-2"))
|
|
491
|
-
if args.present? && args != {}
|
|
492
|
-
concat(render_json_block(args))
|
|
493
|
-
else
|
|
494
|
-
concat(content_tag(:p, "No arguments", class: "text-slate-400 italic text-sm"))
|
|
495
|
-
end
|
|
496
|
-
end)
|
|
497
|
-
|
|
498
|
-
if result.present? && result != {}
|
|
499
|
-
concat(content_tag(:div, class: "pt-4 border-t border-slate-200") do
|
|
500
|
-
concat(content_tag(:h4, "Result", class: "text-sm font-medium text-slate-500 mb-2"))
|
|
501
|
-
concat(render_json_block(result))
|
|
502
|
-
end)
|
|
503
|
-
end
|
|
504
|
-
|
|
505
|
-
if error.present?
|
|
506
|
-
concat(content_tag(:div, class: "pt-4 border-t border-slate-200") do
|
|
507
|
-
concat(content_tag(:h4, "Error", class: "text-sm font-medium text-red-500 mb-2"))
|
|
508
|
-
concat(content_tag(:div, class: "bg-red-50 border border-red-200 rounded-lg p-4") do
|
|
509
|
-
content_tag(:pre, h(error.to_s), class: "text-sm text-red-700 whitespace-pre-wrap font-mono")
|
|
510
|
-
end)
|
|
511
|
-
end)
|
|
512
|
-
end
|
|
513
|
-
end
|
|
329
|
+
# Resolves `Admin::Renderers::<Key>Renderer` in the host app, if defined.
|
|
330
|
+
def host_renderer_class(key)
|
|
331
|
+
"Admin::Renderers::#{key.to_s.camelize}Renderer".safe_constantize
|
|
514
332
|
end
|
|
515
333
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
end)
|
|
527
|
-
concat(content_tag(:div, simple_format(h(user_msg.respond_to?(:content) ? user_msg.content.to_s : user_msg.to_s)), class: "prose prose-sm max-w-none"))
|
|
528
|
-
end)
|
|
529
|
-
end
|
|
530
|
-
|
|
531
|
-
if asst_msg
|
|
532
|
-
concat(content_tag(:div, class: "rounded-lg border p-4 bg-emerald-50 border-emerald-200") do
|
|
533
|
-
concat(content_tag(:div, class: "flex items-center gap-2 mb-2") do
|
|
534
|
-
concat('<svg class="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/></svg>'.html_safe)
|
|
535
|
-
concat(content_tag(:span, "Assistant", class: "text-sm font-medium text-slate-700"))
|
|
536
|
-
end)
|
|
537
|
-
concat(content_tag(:div, simple_format(h(asst_msg.respond_to?(:content) ? asst_msg.content.to_s : asst_msg.to_s)), class: "prose prose-sm max-w-none"))
|
|
538
|
-
end)
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
concat(content_tag(:p, "No messages found", class: "text-slate-400 italic text-sm")) unless user_msg || asst_msg
|
|
542
|
-
end
|
|
334
|
+
# A bare `object.is_a?(ActiveRecord::Base)` crashes with `NameError` in a
|
|
335
|
+
# host without ActiveRecord loaded (this gem's own reference DB-free
|
|
336
|
+
# configuration among them) -- not masking, since it isn't inside a
|
|
337
|
+
# rescue, just a plain 500. Same idiom already used by
|
|
338
|
+
# `AdminSuite::UI::ShowFormatterRegistry` (`if defined?(ActiveRecord::Base)`).
|
|
339
|
+
#
|
|
340
|
+
# @param object [Object]
|
|
341
|
+
# @return [Boolean]
|
|
342
|
+
def active_record_base?(object)
|
|
343
|
+
defined?(ActiveRecord::Base) && object.is_a?(ActiveRecord::Base)
|
|
543
344
|
end
|
|
544
345
|
|
|
545
346
|
def auto_admin_suite_path_for(item)
|
|
546
|
-
return nil unless
|
|
547
|
-
|
|
548
|
-
ensure_admin_resources_loaded_for!(item.class)
|
|
347
|
+
return nil unless active_record_base?(item)
|
|
549
348
|
|
|
550
|
-
resource =
|
|
349
|
+
resource = admin_suite_resource_for(item.class)
|
|
551
350
|
return nil unless resource&.portal_name && resource.respond_to?(:resource_name_plural)
|
|
552
351
|
|
|
553
352
|
resource_path(portal: resource.portal_name, resource_name: resource.resource_name_plural, id: item.to_param)
|
|
@@ -555,16 +354,30 @@ module AdminSuite
|
|
|
555
354
|
nil
|
|
556
355
|
end
|
|
557
356
|
|
|
357
|
+
# Memoized per-request (the helper is mixed into a view instance created
|
|
358
|
+
# fresh per request) resource-class lookup. Before this, every rendered
|
|
359
|
+
# association value re-ran `ensure_admin_resources_loaded_for!`'s
|
|
360
|
+
# `registered_resources.any?` scan *plus* a `registered_resources.find`
|
|
361
|
+
# scan -- both full linear scans of the registry (28-38 resources in the
|
|
362
|
+
# real hosts) -- once per rendered row. Keying the memo on the model
|
|
363
|
+
# class (not the item's identity) means a page with N rows of the same
|
|
364
|
+
# class costs one lookup, not N.
|
|
365
|
+
#
|
|
366
|
+
# @param model_class [Class]
|
|
367
|
+
# @return [Class, nil] the registered `Admin::Base::Resource` subclass, if any
|
|
368
|
+
def admin_suite_resource_for(model_class)
|
|
369
|
+
@admin_suite_resource_for ||= {}
|
|
370
|
+
return @admin_suite_resource_for[model_class] if @admin_suite_resource_for.key?(model_class)
|
|
371
|
+
|
|
372
|
+
ensure_admin_resources_loaded_for!(model_class)
|
|
373
|
+
@admin_suite_resource_for[model_class] = Admin::Base::Resource.registered_resources.find { |r| r.model_class == model_class }
|
|
374
|
+
end
|
|
375
|
+
|
|
558
376
|
def ensure_admin_resources_loaded_for!(model_class)
|
|
559
377
|
already_loaded = Admin::Base::Resource.registered_resources.any? { |r| r.model_class == model_class }
|
|
560
378
|
return if already_loaded
|
|
561
379
|
|
|
562
|
-
|
|
563
|
-
require file
|
|
564
|
-
end
|
|
565
|
-
rescue NameError
|
|
566
|
-
require "admin/base/resource"
|
|
567
|
-
retry
|
|
380
|
+
AdminSuite::DefinitionLoader.load!(:resources)
|
|
568
381
|
end
|
|
569
382
|
|
|
570
383
|
# ---- show page sections / associations ----
|
|
@@ -573,7 +386,7 @@ module AdminSuite
|
|
|
573
386
|
# `/internal/developer`. This is intentionally "UI heavy".
|
|
574
387
|
|
|
575
388
|
def render_show_section(resource, section, position = :main)
|
|
576
|
-
is_association = section.association.present? && !resource.public_send(section.association)
|
|
389
|
+
is_association = section.association.present? && !active_record_base?(resource.public_send(section.association)) rescue false
|
|
577
390
|
|
|
578
391
|
content_tag(:div, class: "bg-white rounded-xl border border-slate-200 overflow-hidden") do
|
|
579
392
|
header_padding = position == :sidebar ? "px-4 py-2.5" : "px-6 py-3"
|
|
@@ -585,7 +398,7 @@ module AdminSuite
|
|
|
585
398
|
|
|
586
399
|
if section.association.present?
|
|
587
400
|
assoc = resource.public_send(section.association) rescue nil
|
|
588
|
-
if assoc && !
|
|
401
|
+
if assoc && !active_record_base?(assoc)
|
|
589
402
|
count = assoc.count rescue 0
|
|
590
403
|
color_class = count > 0 ? "bg-indigo-100 text-indigo-700" : "bg-slate-200 text-slate-600"
|
|
591
404
|
concat(content_tag(:span, number_with_delimiter(count), class: "text-xs font-semibold px-2 py-0.5 rounded-full #{color_class}"))
|
|
@@ -601,11 +414,15 @@ module AdminSuite
|
|
|
601
414
|
|
|
602
415
|
concat(content_tag(:div, class: content_padding) do
|
|
603
416
|
if section.render.present?
|
|
604
|
-
render_custom_section(resource, section.render)
|
|
417
|
+
render_custom_section(resource, section.render, section.options || {})
|
|
605
418
|
elsif section.association.present?
|
|
606
419
|
render_association_section(resource, section)
|
|
607
420
|
elsif section.fields.any?
|
|
608
|
-
position == :sidebar
|
|
421
|
+
if position == :sidebar
|
|
422
|
+
render_sidebar_fields(resource, section.fields, hide_blank: section.hide_blank)
|
|
423
|
+
else
|
|
424
|
+
render_main_fields(resource, section.fields, hide_blank: section.hide_blank)
|
|
425
|
+
end
|
|
609
426
|
else
|
|
610
427
|
content_tag(:p, "No content", class: "text-slate-400 italic text-sm")
|
|
611
428
|
end
|
|
@@ -613,11 +430,13 @@ module AdminSuite
|
|
|
613
430
|
end
|
|
614
431
|
end
|
|
615
432
|
|
|
616
|
-
def render_sidebar_fields(resource, fields)
|
|
433
|
+
def render_sidebar_fields(resource, fields, hide_blank: false)
|
|
617
434
|
content_tag(:div, class: "space-y-3") do
|
|
618
435
|
fields.each do |field_name|
|
|
619
436
|
value = resource.public_send(field_name) rescue nil
|
|
620
|
-
if
|
|
437
|
+
next if hide_blank && show_value_blank?(value)
|
|
438
|
+
|
|
439
|
+
if attached_file_value?(value)
|
|
621
440
|
concat(render_sidebar_attachment(value))
|
|
622
441
|
else
|
|
623
442
|
concat(content_tag(:div, class: "flex justify-between items-start gap-2") do
|
|
@@ -665,9 +484,18 @@ module AdminSuite
|
|
|
665
484
|
end
|
|
666
485
|
end
|
|
667
486
|
|
|
668
|
-
def render_main_fields(resource, fields)
|
|
487
|
+
def render_main_fields(resource, fields, hide_blank: false)
|
|
669
488
|
content_tag(:dl, class: "space-y-6") do
|
|
670
489
|
fields.each do |field_name|
|
|
490
|
+
# Only re-read the value (and only when `hide_blank` is actually
|
|
491
|
+
# on) so a plain `field :foo` row with no `hide_blank:` costs
|
|
492
|
+
# exactly what it cost before this option existed --
|
|
493
|
+
# `format_show_value` re-reads the value itself below regardless.
|
|
494
|
+
if hide_blank
|
|
495
|
+
value = resource.public_send(field_name) rescue nil
|
|
496
|
+
next if show_value_blank?(value)
|
|
497
|
+
end
|
|
498
|
+
|
|
671
499
|
concat(content_tag(:div) do
|
|
672
500
|
concat(content_tag(:dt, field_name.to_s.humanize, class: "text-sm font-medium text-slate-500 mb-2"))
|
|
673
501
|
concat(content_tag(:dd, class: "text-sm text-slate-900") { format_show_value(resource, field_name) })
|
|
@@ -676,12 +504,44 @@ module AdminSuite
|
|
|
676
504
|
end
|
|
677
505
|
end
|
|
678
506
|
|
|
507
|
+
# Whether `resource.public_send(field_name)` should be omitted entirely
|
|
508
|
+
# from a `hide_blank: true` show panel.
|
|
509
|
+
#
|
|
510
|
+
# Deliberately not `value.blank?`: `false.blank?` is `true` in Ruby, and
|
|
511
|
+
# `false` renders as a real, meaningful grey "No" toggle icon today (see
|
|
512
|
+
# `ShowFormatterRegistry`'s `FalseClass` handler) -- hiding it would
|
|
513
|
+
# erase a real signal, not absence of one. Same reasoning for `0` and
|
|
514
|
+
# `0.0`: neither responds to `:empty?`, so both are kept. A
|
|
515
|
+
# whitespace-only string (`" "`) is also kept -- `String#empty?`
|
|
516
|
+
# checks length, not content, and this option only claims to hide
|
|
517
|
+
# values with *zero* content, not "content a human would consider
|
|
518
|
+
# meaningless."
|
|
519
|
+
#
|
|
520
|
+
# @param value [Object]
|
|
521
|
+
# @return [Boolean]
|
|
522
|
+
def show_value_blank?(value)
|
|
523
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
524
|
+
end
|
|
525
|
+
private :show_value_blank?
|
|
526
|
+
|
|
527
|
+
# True for an `ActiveStorage::Attached::One`/`::Many` proxy -- guarded
|
|
528
|
+
# with `defined?` because the host (and this gem's own dummy test app)
|
|
529
|
+
# may not load ActiveStorage at all.
|
|
530
|
+
#
|
|
531
|
+
# @param value [Object]
|
|
532
|
+
# @return [Boolean]
|
|
533
|
+
def attached_file_value?(value)
|
|
534
|
+
(defined?(ActiveStorage::Attached::One) && value.is_a?(ActiveStorage::Attached::One)) ||
|
|
535
|
+
(defined?(ActiveStorage::Attached::Many) && value.is_a?(ActiveStorage::Attached::Many))
|
|
536
|
+
end
|
|
537
|
+
private :attached_file_value?
|
|
538
|
+
|
|
679
539
|
# ---- association rendering ----
|
|
680
540
|
def render_association_section(resource, section)
|
|
681
541
|
associated = resource.public_send(section.association) rescue nil
|
|
682
542
|
return content_tag(:p, "None found", class: "text-slate-400 italic text-sm") if associated.nil?
|
|
683
543
|
|
|
684
|
-
is_single = !associated.respond_to?(:to_a) ||
|
|
544
|
+
is_single = !associated.respond_to?(:to_a) || active_record_base?(associated)
|
|
685
545
|
return render_association_card_single(associated, section) if is_single
|
|
686
546
|
|
|
687
547
|
items = associated
|
|
@@ -711,62 +571,12 @@ module AdminSuite
|
|
|
711
571
|
else
|
|
712
572
|
concat(render_association_list(items, section))
|
|
713
573
|
end
|
|
714
|
-
concat(
|
|
574
|
+
concat(render("admin_suite/shared/pagination", pagy: pagy, page_param: association_page_param(section))) if pagy
|
|
715
575
|
end
|
|
716
576
|
end
|
|
717
577
|
|
|
718
578
|
def association_page_param(section) = "#{section.association}_page"
|
|
719
579
|
|
|
720
|
-
def render_association_pagination(pagy)
|
|
721
|
-
content_tag(:div, class: "-mx-6 border-t border-slate-200 bg-slate-50/50 px-6 py-3") do
|
|
722
|
-
content_tag(:nav, class: "flex items-center justify-between", "aria-label" => "Pagination") do
|
|
723
|
-
concat(pagy_prev_link(pagy))
|
|
724
|
-
concat(pagy_page_links(pagy))
|
|
725
|
-
concat(pagy_next_link(pagy))
|
|
726
|
-
end
|
|
727
|
-
end
|
|
728
|
-
end
|
|
729
|
-
|
|
730
|
-
def pagy_prev_link(pagy)
|
|
731
|
-
if pagy.prev
|
|
732
|
-
link_to("Prev", pagy_url_for(pagy, pagy.prev),
|
|
733
|
-
class: "px-3 py-1.5 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors")
|
|
734
|
-
else
|
|
735
|
-
content_tag(:span, "Prev",
|
|
736
|
-
class: "px-3 py-1.5 text-sm font-medium text-slate-400 bg-slate-100 border border-slate-200 rounded-lg cursor-not-allowed")
|
|
737
|
-
end
|
|
738
|
-
end
|
|
739
|
-
|
|
740
|
-
def pagy_next_link(pagy)
|
|
741
|
-
if pagy.next
|
|
742
|
-
link_to("Next", pagy_url_for(pagy, pagy.next),
|
|
743
|
-
class: "px-3 py-1.5 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors")
|
|
744
|
-
else
|
|
745
|
-
content_tag(:span, "Next",
|
|
746
|
-
class: "px-3 py-1.5 text-sm font-medium text-slate-400 bg-slate-100 border border-slate-200 rounded-lg cursor-not-allowed")
|
|
747
|
-
end
|
|
748
|
-
end
|
|
749
|
-
|
|
750
|
-
def pagy_page_links(pagy)
|
|
751
|
-
content_tag(:div, class: "flex items-center gap-1") do
|
|
752
|
-
pagy.series.each { |item| concat(render_pagy_series_item(pagy, item)) }
|
|
753
|
-
end
|
|
754
|
-
end
|
|
755
|
-
|
|
756
|
-
def render_pagy_series_item(pagy, item)
|
|
757
|
-
case item
|
|
758
|
-
when Integer
|
|
759
|
-
link_to(item, pagy_url_for(pagy, item),
|
|
760
|
-
class: "px-2.5 py-1 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded hover:bg-slate-50 transition-colors")
|
|
761
|
-
when String
|
|
762
|
-
content_tag(:span, item, class: "px-2.5 py-1 text-sm font-semibold text-white bg-indigo-600 border border-indigo-600 rounded")
|
|
763
|
-
when :gap
|
|
764
|
-
content_tag(:span, "…", class: "px-2 text-sm text-slate-400")
|
|
765
|
-
else
|
|
766
|
-
""
|
|
767
|
-
end
|
|
768
|
-
end
|
|
769
|
-
|
|
770
580
|
def render_association_card_single(item, section)
|
|
771
581
|
link_path = build_association_link(item, section)
|
|
772
582
|
|
|
@@ -901,10 +711,55 @@ module AdminSuite
|
|
|
901
711
|
when true, false then value ? "Yes" : "No"
|
|
902
712
|
when Time, DateTime then value.strftime("%b %d, %H:%M")
|
|
903
713
|
when Date then value.strftime("%b %d, %Y")
|
|
904
|
-
|
|
905
|
-
|
|
714
|
+
# Aligns with `format_show_value`'s Integer/Float/BigDecimal handling
|
|
715
|
+
# (see `AdminSuite::UI::ShowFormatterRegistry`) -- table cells used to
|
|
716
|
+
# render large numbers with no delimiter at all (via the `else`
|
|
717
|
+
# branch's plain `to_s`), inconsistent with the same value shown on
|
|
718
|
+
# the record's own show page.
|
|
719
|
+
when Integer, Float then number_with_delimiter(value)
|
|
720
|
+
# `.to_f` avoids number_with_delimiter rendering exponential notation
|
|
721
|
+
# for BigDecimal input, matching the show-value formatter's tradeoff.
|
|
722
|
+
when BigDecimal then number_with_delimiter(value.to_f)
|
|
723
|
+
else
|
|
724
|
+
# A `when ActiveRecord::Base` clause would evaluate that constant
|
|
725
|
+
# reference unconditionally (crashing in a host without ActiveRecord
|
|
726
|
+
# loaded), so this branch is checked explicitly via the predicate
|
|
727
|
+
# instead of folded into the `case`.
|
|
728
|
+
return render_association_value(value) if active_record_base?(value)
|
|
729
|
+
|
|
730
|
+
value.to_s.truncate(50)
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
# Renders a `belongs_to`-shaped association value (typically reached via
|
|
735
|
+
# `render_column_value`'s fallback branch or `format_table_cell`'s AR
|
|
736
|
+
# branch) as its display title, wrapped in a link to the record's own
|
|
737
|
+
# admin page when one resolves -- never the bare `#<Company:0x...>` that
|
|
738
|
+
# ERB's implicit `to_s` would otherwise produce for an AR object, and
|
|
739
|
+
# never the indigo-styled-but-unlinked plain text `format_table_cell`
|
|
740
|
+
# used to render on its own.
|
|
741
|
+
#
|
|
742
|
+
# `item_display_title` can raise on a host record whose `name`/`title`
|
|
743
|
+
# method blows up, and `auto_admin_suite_path_for` already swallows its
|
|
744
|
+
# own errors (unpersisted records, no registered resource, etc.) -- but
|
|
745
|
+
# this wraps the whole thing anyway so one bad row degrades to a plain
|
|
746
|
+
# dash instead of 500ing the entire index or show page.
|
|
747
|
+
#
|
|
748
|
+
# @param value [ActiveRecord::Base]
|
|
749
|
+
# @return [String, ActiveSupport::SafeBuffer]
|
|
750
|
+
def render_association_value(value)
|
|
751
|
+
title = begin
|
|
752
|
+
item_display_title(value).to_s
|
|
753
|
+
rescue StandardError
|
|
754
|
+
"—"
|
|
906
755
|
end
|
|
756
|
+
|
|
757
|
+
path = auto_admin_suite_path_for(value)
|
|
758
|
+
path ? link_to(title, path, class: "text-indigo-600 hover:underline") : title
|
|
759
|
+
rescue StandardError
|
|
760
|
+
"—"
|
|
907
761
|
end
|
|
762
|
+
private :render_association_value
|
|
908
763
|
|
|
909
764
|
def item_display_title(item)
|
|
910
765
|
return item.name if item.respond_to?(:name) && item.name.present?
|
|
@@ -988,58 +843,6 @@ module AdminSuite
|
|
|
988
843
|
end
|
|
989
844
|
end
|
|
990
845
|
|
|
991
|
-
# ---- form fields ----
|
|
992
|
-
def render_form_field(f, field, resource)
|
|
993
|
-
return if field.if_condition.present? && !field.if_condition.call(resource)
|
|
994
|
-
return if field.unless_condition.present? && field.unless_condition.call(resource)
|
|
995
|
-
|
|
996
|
-
capture do
|
|
997
|
-
concat(content_tag(:div, class: "form-group") do
|
|
998
|
-
concat(f.label(field.name, class: "form-label") do
|
|
999
|
-
concat(field.label)
|
|
1000
|
-
concat(content_tag(:span, " *", class: "text-red-500")) if field.required
|
|
1001
|
-
end)
|
|
1002
|
-
|
|
1003
|
-
field_class = "form-input w-full"
|
|
1004
|
-
field_class += " border-red-500" if resource.errors[field.name].any?
|
|
1005
|
-
|
|
1006
|
-
field_html = case field.type
|
|
1007
|
-
when :textarea then f.text_area(field.name, class: field_class, rows: field.rows || 4, placeholder: field.placeholder, readonly: field.readonly)
|
|
1008
|
-
when :url then f.url_field(field.name, class: field_class, placeholder: field.placeholder, readonly: field.readonly)
|
|
1009
|
-
when :email then f.email_field(field.name, class: field_class, placeholder: field.placeholder, readonly: field.readonly)
|
|
1010
|
-
when :number then f.number_field(field.name, class: field_class, placeholder: field.placeholder, readonly: field.readonly)
|
|
1011
|
-
when :toggle then render_toggle_field(f, field, resource)
|
|
1012
|
-
when :label
|
|
1013
|
-
label_value = resource.public_send(field.name) rescue nil
|
|
1014
|
-
render_label_badge(label_value, color: field.label_color, size: field.label_size, record: resource)
|
|
1015
|
-
when :select
|
|
1016
|
-
collection = field.collection.is_a?(Proc) ? field.collection.call : field.collection
|
|
1017
|
-
f.select(field.name, collection, { include_blank: true }, class: field_class, disabled: field.readonly)
|
|
1018
|
-
when :searchable_select then render_searchable_select(f, field, resource)
|
|
1019
|
-
when :multi_select, :tags then render_multi_select(f, field, resource)
|
|
1020
|
-
when :image, :attachment then render_file_upload(f, field, resource)
|
|
1021
|
-
when :trix, :rich_text then f.rich_text_area(field.name, class: "prose max-w-none")
|
|
1022
|
-
when :markdown
|
|
1023
|
-
f.text_area(field.name, class: "#{field_class} font-mono", rows: field.rows || 12, data: { controller: "admin-suite--markdown-editor" }, placeholder: field.placeholder)
|
|
1024
|
-
when :file then f.file_field(field.name, class: "form-input-file", accept: field.accept)
|
|
1025
|
-
when :datetime then f.datetime_local_field(field.name, class: field_class, readonly: field.readonly)
|
|
1026
|
-
when :date then f.date_field(field.name, class: field_class, readonly: field.readonly)
|
|
1027
|
-
when :time then f.time_field(field.name, class: field_class, readonly: field.readonly)
|
|
1028
|
-
when :json
|
|
1029
|
-
render("admin_suite/shared/json_editor_field", f: f, field: field, resource: resource)
|
|
1030
|
-
when :code then render_code_editor(f, field, resource)
|
|
1031
|
-
else
|
|
1032
|
-
f.text_field(field.name, class: field_class, placeholder: field.placeholder, readonly: field.readonly)
|
|
1033
|
-
end
|
|
1034
|
-
|
|
1035
|
-
concat(field_html)
|
|
1036
|
-
|
|
1037
|
-
concat(content_tag(:p, field.help, class: "mt-1 text-sm text-slate-500")) if field.help.present?
|
|
1038
|
-
concat(content_tag(:p, resource.errors[field.name].first, class: "mt-1 text-sm text-red-600")) if resource.errors[field.name].any?
|
|
1039
|
-
end)
|
|
1040
|
-
end
|
|
1041
|
-
end
|
|
1042
|
-
|
|
1043
846
|
def render_toggle_field(_f, field, resource)
|
|
1044
847
|
checked = !!resource.public_send(field.name)
|
|
1045
848
|
param_key = resource.class.model_name.param_key
|
|
@@ -1065,10 +868,48 @@ module AdminSuite
|
|
|
1065
868
|
end
|
|
1066
869
|
end
|
|
1067
870
|
|
|
871
|
+
# Resolves the default search URL for a `searchable_select` field
|
|
872
|
+
# declared with `resource:` (e.g. `field :company_id,
|
|
873
|
+
# type: :searchable_select, resource: :companies`), so it can reach the
|
|
874
|
+
# gem's own search endpoint (`ResourcesController#search`) without the
|
|
875
|
+
# host wiring up a `collection:` URL by hand. A String `collection:`
|
|
876
|
+
# option always takes precedence over this -- see `render_searchable_select`.
|
|
877
|
+
#
|
|
878
|
+
# Looks up the resource by its plural (or singular) resource name among
|
|
879
|
+
# `Admin::Base::Resource.registered_resources`; returns nil (never raises)
|
|
880
|
+
# when the key is blank, unregistered, or has no `portal_name` to route
|
|
881
|
+
# through, so a typo'd `resource:` degrades to "no remote search" rather
|
|
882
|
+
# than a 500 on the very page meant to render a form.
|
|
883
|
+
#
|
|
884
|
+
# @param resource_key [Symbol, String, nil]
|
|
885
|
+
# @return [String, nil]
|
|
886
|
+
def admin_suite_search_url_for(resource_key)
|
|
887
|
+
return nil if resource_key.blank?
|
|
888
|
+
|
|
889
|
+
AdminSuite::DefinitionLoader.load!(:resources)
|
|
890
|
+
key = resource_key.to_s
|
|
891
|
+
target = Admin::Base::Resource.registered_resources.find do |r|
|
|
892
|
+
r.resource_name_plural == key || r.resource_name == key
|
|
893
|
+
end
|
|
894
|
+
return nil unless target&.portal_name
|
|
895
|
+
|
|
896
|
+
search_resources_path(portal: target.portal_name, resource_name: target.resource_name_plural)
|
|
897
|
+
rescue StandardError => e
|
|
898
|
+
Rails.logger&.warn(
|
|
899
|
+
"AdminSuite: resolving the search URL for `resource: #{resource_key.inspect}` raised " \
|
|
900
|
+
"#{e.class}: #{e.message}; rendering the field with no remote search."
|
|
901
|
+
)
|
|
902
|
+
nil
|
|
903
|
+
end
|
|
904
|
+
|
|
1068
905
|
def render_searchable_select(_f, field, resource)
|
|
1069
906
|
param_key = resource.class.model_name.param_key
|
|
1070
907
|
current_value = resource.public_send(field.name)
|
|
1071
908
|
collection = field.collection.is_a?(Proc) ? field.collection.call : field.collection
|
|
909
|
+
# A String `collection:` is an explicit search-URL override and always
|
|
910
|
+
# wins (unchanged host behavior); otherwise fall back to the field's
|
|
911
|
+
# `resource:` option resolved via `admin_suite_search_url_for`.
|
|
912
|
+
search_url = collection.is_a?(String) ? collection : admin_suite_search_url_for(field.resource).to_s
|
|
1072
913
|
|
|
1073
914
|
options_json = if collection.is_a?(Array)
|
|
1074
915
|
collection.map { |opt| opt.is_a?(Array) ? { value: opt[1], label: opt[0] } : { value: opt, label: opt.to_s.humanize } }.to_json
|
|
@@ -1098,7 +939,7 @@ module AdminSuite
|
|
|
1098
939
|
controller: "admin-suite--searchable-select",
|
|
1099
940
|
"admin-suite--searchable-select-options-value": options_json,
|
|
1100
941
|
"admin-suite--searchable-select-creatable-value": field.create_url.present?,
|
|
1101
|
-
"admin-suite--searchable-select-search-url-value":
|
|
942
|
+
"admin-suite--searchable-select-search-url-value": search_url,
|
|
1102
943
|
"admin-suite--searchable-select-create-url-value": field.create_url.to_s
|
|
1103
944
|
},
|
|
1104
945
|
class: "relative") do
|