admin_suite 0.3.1 → 0.4.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 +82 -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 +8 -1
- data/app/helpers/admin_suite/base_helper.rb +49 -385
- data/app/javascript/controllers/admin_suite/markdown_editor_controller.js +21 -2
- data/app/views/admin_suite/panels/_stat.html.erb +10 -0
- data/app/views/admin_suite/resources/index.html.erb +7 -77
- data/app/views/admin_suite/shared/_pagination.html.erb +74 -0
- data/app/views/admin_suite/shared/_sidebar.html.erb +1 -1
- data/app/views/layouts/admin_suite/application.html.erb +5 -3
- data/lib/admin/base/action_executor.rb +19 -51
- data/lib/admin/base/resource.rb +53 -14
- data/lib/admin_suite/configuration.rb +28 -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 +70 -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 +230 -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/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 +37 -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/layout_assets_test.rb +112 -0
- data/test/integration/navigation_sections_test.rb +41 -0
- data/test/integration/pagination_and_stats_test.rb +152 -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 +264 -0
- data/test/lib/engine_defaults_test.rb +39 -0
- data/test/lib/form_field_renderer_test.rb +64 -0
- data/test/lib/legacy_renderer_deprecation_test.rb +35 -0
- data/test/lib/renderer_test.rb +221 -0
- data/test/lib/resource_exportable_deprecation_test.rb +39 -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 +77 -0
- metadata +28 -1
|
@@ -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
|
#
|
|
@@ -148,79 +145,6 @@ module AdminSuite
|
|
|
148
145
|
end
|
|
149
146
|
end
|
|
150
147
|
|
|
151
|
-
# Formats a value for display on show pages
|
|
152
|
-
#
|
|
153
|
-
# @param record [ActiveRecord::Base] The record
|
|
154
|
-
# @param field_name [Symbol, String] Field name
|
|
155
|
-
# @return [String] HTML safe formatted value
|
|
156
|
-
def format_show_value(record, field_name)
|
|
157
|
-
value = record.public_send(field_name) rescue nil
|
|
158
|
-
|
|
159
|
-
if value.is_a?(ActiveStorage::Attached::One)
|
|
160
|
-
return render_attachment_preview(value)
|
|
161
|
-
elsif value.is_a?(ActiveStorage::Attached::Many)
|
|
162
|
-
return render_attachments_preview(value)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
case value
|
|
166
|
-
when nil
|
|
167
|
-
content_tag(:span, "—", class: "text-slate-400")
|
|
168
|
-
when true
|
|
169
|
-
content_tag(:span, class: "inline-flex items-center gap-1") do
|
|
170
|
-
svg = '<svg class="w-4 h-4 text-green-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>'.html_safe
|
|
171
|
-
concat(svg)
|
|
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
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
148
|
def render_attachment_preview(attachment)
|
|
225
149
|
return content_tag(:span, "—", class: "text-slate-400") unless attachment.attached?
|
|
226
150
|
|
|
@@ -341,209 +265,50 @@ module AdminSuite
|
|
|
341
265
|
nil
|
|
342
266
|
end
|
|
343
267
|
|
|
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
|
|
268
|
+
def render_custom_section(resource, render_type, options = {})
|
|
269
|
+
key = render_type.to_sym
|
|
365
270
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
highlighted_template = h(template).gsub(/\{\{(\w+)\}\}/) do
|
|
372
|
-
"<span class=\"text-amber-400 bg-amber-900/30 px-1 rounded\">{{#{$1}}}</span>"
|
|
271
|
+
legacy_proc = AdminSuite.config.custom_renderers[key]
|
|
272
|
+
if legacy_proc
|
|
273
|
+
AdminSuite::LegacyCustomRendererProcs.warn_once(key)
|
|
274
|
+
return legacy_proc.call(resource, self)
|
|
373
275
|
end
|
|
374
276
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
highlighted_template.html_safe
|
|
388
|
-
end)
|
|
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
|
|
277
|
+
# Precedence, after the legacy proc above (unchanged, Task 3 has tests
|
|
278
|
+
# pinning that): a host's explicit registration, then a host renderer
|
|
279
|
+
# class, then the gem's own defaults (built-ins + the deprecated four
|
|
280
|
+
# Gleania renderers). Checking the gem defaults last means a host that
|
|
281
|
+
# defines `Admin::Renderers::<Key>Renderer` -- exactly the migration
|
|
282
|
+
# path the deprecation warnings recommend -- actually takes effect
|
|
283
|
+
# instead of being silently shadowed by the gem's boot-time
|
|
284
|
+
# registrations (see `RendererRegistry`).
|
|
285
|
+
klass = AdminSuite::RendererRegistry.lookup(key) ||
|
|
286
|
+
host_renderer_class(key) ||
|
|
287
|
+
AdminSuite::RendererRegistry.lookup_default(key)
|
|
288
|
+
return klass.new(resource, self, options).render if klass
|
|
403
289
|
|
|
404
|
-
|
|
405
|
-
data = resource.respond_to?(:data) ? resource.data : resource.attributes
|
|
406
|
-
render_json_block(data)
|
|
290
|
+
content_tag(:p, "Unknown render type: #{render_type}", class: "text-slate-500 italic")
|
|
407
291
|
end
|
|
408
292
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
293
|
+
# Resolves `Admin::Renderers::<Key>Renderer` in the host app, if defined.
|
|
294
|
+
def host_renderer_class(key)
|
|
295
|
+
"Admin::Renderers::#{key.to_s.camelize}Renderer".safe_constantize
|
|
412
296
|
end
|
|
413
297
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
def render_tool_args_preview(resource)
|
|
484
|
-
args = resource.respond_to?(:args) ? resource.args : (resource.respond_to?(:arguments) ? resource.arguments : {})
|
|
485
|
-
result = resource.respond_to?(:result) ? resource.result : nil
|
|
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
|
|
514
|
-
end
|
|
515
|
-
|
|
516
|
-
def render_turn_messages_preview(resource)
|
|
517
|
-
user_msg = resource.respond_to?(:user_message) ? resource.user_message : nil
|
|
518
|
-
asst_msg = resource.respond_to?(:assistant_message) ? resource.assistant_message : nil
|
|
519
|
-
|
|
520
|
-
content_tag(:div, class: "space-y-4") do
|
|
521
|
-
if user_msg
|
|
522
|
-
concat(content_tag(:div, class: "rounded-lg border p-4 bg-blue-50 border-blue-200") do
|
|
523
|
-
concat(content_tag(:div, class: "flex items-center gap-2 mb-2") do
|
|
524
|
-
concat('<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)
|
|
525
|
-
concat(content_tag(:span, "User", class: "text-sm font-medium text-slate-700"))
|
|
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
|
|
298
|
+
# A bare `object.is_a?(ActiveRecord::Base)` crashes with `NameError` in a
|
|
299
|
+
# host without ActiveRecord loaded (this gem's own reference DB-free
|
|
300
|
+
# configuration among them) -- not masking, since it isn't inside a
|
|
301
|
+
# rescue, just a plain 500. Same idiom already used by
|
|
302
|
+
# `AdminSuite::UI::ShowFormatterRegistry` (`if defined?(ActiveRecord::Base)`).
|
|
303
|
+
#
|
|
304
|
+
# @param object [Object]
|
|
305
|
+
# @return [Boolean]
|
|
306
|
+
def active_record_base?(object)
|
|
307
|
+
defined?(ActiveRecord::Base) && object.is_a?(ActiveRecord::Base)
|
|
543
308
|
end
|
|
544
309
|
|
|
545
310
|
def auto_admin_suite_path_for(item)
|
|
546
|
-
return nil unless
|
|
311
|
+
return nil unless active_record_base?(item)
|
|
547
312
|
|
|
548
313
|
ensure_admin_resources_loaded_for!(item.class)
|
|
549
314
|
|
|
@@ -559,12 +324,7 @@ module AdminSuite
|
|
|
559
324
|
already_loaded = Admin::Base::Resource.registered_resources.any? { |r| r.model_class == model_class }
|
|
560
325
|
return if already_loaded
|
|
561
326
|
|
|
562
|
-
|
|
563
|
-
require file
|
|
564
|
-
end
|
|
565
|
-
rescue NameError
|
|
566
|
-
require "admin/base/resource"
|
|
567
|
-
retry
|
|
327
|
+
AdminSuite::DefinitionLoader.load!(:resources)
|
|
568
328
|
end
|
|
569
329
|
|
|
570
330
|
# ---- show page sections / associations ----
|
|
@@ -573,7 +333,7 @@ module AdminSuite
|
|
|
573
333
|
# `/internal/developer`. This is intentionally "UI heavy".
|
|
574
334
|
|
|
575
335
|
def render_show_section(resource, section, position = :main)
|
|
576
|
-
is_association = section.association.present? && !resource.public_send(section.association)
|
|
336
|
+
is_association = section.association.present? && !active_record_base?(resource.public_send(section.association)) rescue false
|
|
577
337
|
|
|
578
338
|
content_tag(:div, class: "bg-white rounded-xl border border-slate-200 overflow-hidden") do
|
|
579
339
|
header_padding = position == :sidebar ? "px-4 py-2.5" : "px-6 py-3"
|
|
@@ -585,7 +345,7 @@ module AdminSuite
|
|
|
585
345
|
|
|
586
346
|
if section.association.present?
|
|
587
347
|
assoc = resource.public_send(section.association) rescue nil
|
|
588
|
-
if assoc && !
|
|
348
|
+
if assoc && !active_record_base?(assoc)
|
|
589
349
|
count = assoc.count rescue 0
|
|
590
350
|
color_class = count > 0 ? "bg-indigo-100 text-indigo-700" : "bg-slate-200 text-slate-600"
|
|
591
351
|
concat(content_tag(:span, number_with_delimiter(count), class: "text-xs font-semibold px-2 py-0.5 rounded-full #{color_class}"))
|
|
@@ -601,7 +361,7 @@ module AdminSuite
|
|
|
601
361
|
|
|
602
362
|
concat(content_tag(:div, class: content_padding) do
|
|
603
363
|
if section.render.present?
|
|
604
|
-
render_custom_section(resource, section.render)
|
|
364
|
+
render_custom_section(resource, section.render, section.options || {})
|
|
605
365
|
elsif section.association.present?
|
|
606
366
|
render_association_section(resource, section)
|
|
607
367
|
elsif section.fields.any?
|
|
@@ -681,7 +441,7 @@ module AdminSuite
|
|
|
681
441
|
associated = resource.public_send(section.association) rescue nil
|
|
682
442
|
return content_tag(:p, "None found", class: "text-slate-400 italic text-sm") if associated.nil?
|
|
683
443
|
|
|
684
|
-
is_single = !associated.respond_to?(:to_a) ||
|
|
444
|
+
is_single = !associated.respond_to?(:to_a) || active_record_base?(associated)
|
|
685
445
|
return render_association_card_single(associated, section) if is_single
|
|
686
446
|
|
|
687
447
|
items = associated
|
|
@@ -711,62 +471,12 @@ module AdminSuite
|
|
|
711
471
|
else
|
|
712
472
|
concat(render_association_list(items, section))
|
|
713
473
|
end
|
|
714
|
-
concat(
|
|
474
|
+
concat(render("admin_suite/shared/pagination", pagy: pagy, page_param: association_page_param(section))) if pagy
|
|
715
475
|
end
|
|
716
476
|
end
|
|
717
477
|
|
|
718
478
|
def association_page_param(section) = "#{section.association}_page"
|
|
719
479
|
|
|
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
480
|
def render_association_card_single(item, section)
|
|
771
481
|
link_path = build_association_link(item, section)
|
|
772
482
|
|
|
@@ -901,8 +611,14 @@ module AdminSuite
|
|
|
901
611
|
when true, false then value ? "Yes" : "No"
|
|
902
612
|
when Time, DateTime then value.strftime("%b %d, %H:%M")
|
|
903
613
|
when Date then value.strftime("%b %d, %Y")
|
|
904
|
-
|
|
905
|
-
|
|
614
|
+
else
|
|
615
|
+
# A `when ActiveRecord::Base` clause would evaluate that constant
|
|
616
|
+
# reference unconditionally (crashing in a host without ActiveRecord
|
|
617
|
+
# loaded), so this branch is checked explicitly via the predicate
|
|
618
|
+
# instead of folded into the `case`.
|
|
619
|
+
return item_display_title(value) if active_record_base?(value)
|
|
620
|
+
|
|
621
|
+
value.to_s.truncate(50)
|
|
906
622
|
end
|
|
907
623
|
end
|
|
908
624
|
|
|
@@ -988,58 +704,6 @@ module AdminSuite
|
|
|
988
704
|
end
|
|
989
705
|
end
|
|
990
706
|
|
|
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
707
|
def render_toggle_field(_f, field, resource)
|
|
1044
708
|
checked = !!resource.public_send(field.name)
|
|
1045
709
|
param_key = resource.class.model_name.param_key
|
|
@@ -4,18 +4,37 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
4
4
|
* Markdown Editor Controller (Admin Suite)
|
|
5
5
|
*
|
|
6
6
|
* Initializes EasyMDE on a textarea element for rich markdown editing.
|
|
7
|
-
* EasyMDE is loaded
|
|
7
|
+
* EasyMDE is loaded from the engine's vendored asset (see app/assets/vendor)
|
|
8
|
+
* only on pages that render a markdown field.
|
|
8
9
|
*/
|
|
10
|
+
|
|
11
|
+
// Bounded retry: ~50 attempts at 100ms each (~5s) before giving up. Guards
|
|
12
|
+
// against an unbounded poll if the EasyMDE asset fails to load (e.g. blocked
|
|
13
|
+
// by a CSP, 404, or a page that never actually included it).
|
|
14
|
+
const MAX_INIT_ATTEMPTS = 50
|
|
15
|
+
const INIT_RETRY_DELAY_MS = 100
|
|
16
|
+
|
|
9
17
|
export default class extends Controller {
|
|
10
18
|
static targets = ["textarea"]
|
|
11
19
|
|
|
12
20
|
connect() {
|
|
21
|
+
this.initAttempts = 0
|
|
13
22
|
this.initEditor()
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
initEditor() {
|
|
17
26
|
if (typeof window.EasyMDE === "undefined") {
|
|
18
|
-
|
|
27
|
+
this.initAttempts += 1
|
|
28
|
+
if (this.initAttempts >= MAX_INIT_ATTEMPTS) {
|
|
29
|
+
console.warn(
|
|
30
|
+
"admin-suite--markdown-editor: EasyMDE failed to load after " +
|
|
31
|
+
MAX_INIT_ATTEMPTS +
|
|
32
|
+
" attempts; falling back to a plain textarea."
|
|
33
|
+
)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setTimeout(() => this.initEditor(), INIT_RETRY_DELAY_MS)
|
|
19
38
|
return
|
|
20
39
|
}
|
|
21
40
|
|
|
@@ -31,6 +31,16 @@
|
|
|
31
31
|
border: "border border-violet-200",
|
|
32
32
|
text: "text-violet-700",
|
|
33
33
|
label: "text-violet-600" }
|
|
34
|
+
when :slate
|
|
35
|
+
{ bg: "bg-white",
|
|
36
|
+
border: "border border-slate-200",
|
|
37
|
+
text: "text-slate-600",
|
|
38
|
+
label: "text-slate-500" }
|
|
39
|
+
when :indigo
|
|
40
|
+
{ bg: "bg-gradient-to-br from-indigo-50 to-indigo-100",
|
|
41
|
+
border: "border border-indigo-200",
|
|
42
|
+
text: "text-indigo-700",
|
|
43
|
+
label: "text-indigo-600" }
|
|
34
44
|
else
|
|
35
45
|
{ bg: "bg-white",
|
|
36
46
|
border: "border border-slate-200",
|