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
|
@@ -48,11 +48,12 @@
|
|
|
48
48
|
<% if @stats.present? %>
|
|
49
49
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
50
50
|
<% @stats.each do |stat| %>
|
|
51
|
-
<%
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
<% stat_panel = AdminSuite::UI::PanelDefinition.new(
|
|
52
|
+
type: :stat,
|
|
53
|
+
title: stat[:name],
|
|
54
|
+
options: { value: stat[:value], color: stat[:color] || :slate, variant: :mini }
|
|
55
|
+
) %>
|
|
56
|
+
<%= render "admin_suite/panels/stat", panel: stat_panel %>
|
|
56
57
|
<% end %>
|
|
57
58
|
</div>
|
|
58
59
|
<% end %>
|
|
@@ -217,78 +218,7 @@
|
|
|
217
218
|
</div>
|
|
218
219
|
|
|
219
220
|
<!-- Pagination -->
|
|
220
|
-
|
|
221
|
-
<div class="px-6 py-4 border-t border-slate-200">
|
|
222
|
-
<div class="flex items-center justify-between">
|
|
223
|
-
<div class="text-sm text-slate-500">
|
|
224
|
-
Showing <span class="font-medium text-slate-700"><%= @pagy.from %></span> to
|
|
225
|
-
<span class="font-medium text-slate-700"><%= @pagy.to %></span> of
|
|
226
|
-
<span class="font-medium text-slate-700"><%= number_with_delimiter(@pagy.count) %></span> results
|
|
227
|
-
</div>
|
|
228
|
-
|
|
229
|
-
<nav class="flex items-center gap-1">
|
|
230
|
-
<% if @pagy.prev %>
|
|
231
|
-
<%= link_to url_for(params.permit!.merge(page: @pagy.prev)),
|
|
232
|
-
class: "inline-flex items-center gap-1 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" do %>
|
|
233
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
234
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
235
|
-
</svg>
|
|
236
|
-
Prev
|
|
237
|
-
<% end %>
|
|
238
|
-
<% else %>
|
|
239
|
-
<span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-400 bg-slate-100 border border-slate-200 rounded-lg cursor-not-allowed">
|
|
240
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
241
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
242
|
-
</svg>
|
|
243
|
-
Prev
|
|
244
|
-
</span>
|
|
245
|
-
<% end %>
|
|
246
|
-
|
|
247
|
-
<div class="hidden sm:flex items-center gap-1 mx-2">
|
|
248
|
-
<% @pagy.series.each do |item| %>
|
|
249
|
-
<% if item == :gap %>
|
|
250
|
-
<span class="px-2 py-1 text-sm text-slate-400">…</span>
|
|
251
|
-
<% elsif item.is_a?(Integer) %>
|
|
252
|
-
<%= link_to item, url_for(params.permit!.merge(page: item)),
|
|
253
|
-
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" %>
|
|
254
|
-
<% elsif item.is_a?(String) %>
|
|
255
|
-
<span class="px-3 py-1.5 text-sm font-medium text-white rounded-lg" style="background: var(--admin-suite-primary); border: 1px solid var(--admin-suite-primary);">
|
|
256
|
-
<%= item %>
|
|
257
|
-
</span>
|
|
258
|
-
<% end %>
|
|
259
|
-
<% end %>
|
|
260
|
-
</div>
|
|
261
|
-
|
|
262
|
-
<span class="sm:hidden px-3 py-1.5 text-sm text-slate-500">
|
|
263
|
-
Page <%= @pagy.page %> of <%= @pagy.pages %>
|
|
264
|
-
</span>
|
|
265
|
-
|
|
266
|
-
<% if @pagy.next %>
|
|
267
|
-
<%= link_to url_for(params.permit!.merge(page: @pagy.next)),
|
|
268
|
-
class: "inline-flex items-center gap-1 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" do %>
|
|
269
|
-
Next
|
|
270
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
271
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
272
|
-
</svg>
|
|
273
|
-
<% end %>
|
|
274
|
-
<% else %>
|
|
275
|
-
<span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-400 bg-slate-100 border border-slate-200 rounded-lg cursor-not-allowed">
|
|
276
|
-
Next
|
|
277
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
278
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
279
|
-
</svg>
|
|
280
|
-
</span>
|
|
281
|
-
<% end %>
|
|
282
|
-
</nav>
|
|
283
|
-
</div>
|
|
284
|
-
</div>
|
|
285
|
-
<% else %>
|
|
286
|
-
<div class="px-6 py-3 border-t border-slate-200">
|
|
287
|
-
<div class="text-sm text-slate-500">
|
|
288
|
-
Showing <span class="font-medium text-slate-700"><%= @pagy.count %></span> results
|
|
289
|
-
</div>
|
|
290
|
-
</div>
|
|
291
|
-
<% end %>
|
|
221
|
+
<%= render "admin_suite/shared/pagination", pagy: @pagy, page_param: :page %>
|
|
292
222
|
<% else %>
|
|
293
223
|
<div class="p-12 text-center">
|
|
294
224
|
<div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-slate-100 mb-4">
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<% page_param = local_assigns.fetch(:page_param, :page) %>
|
|
2
|
+
|
|
3
|
+
<% if pagy.pages > 1 %>
|
|
4
|
+
<div class="px-6 py-4 border-t border-slate-200">
|
|
5
|
+
<div class="flex items-center justify-between">
|
|
6
|
+
<div class="text-sm text-slate-500">
|
|
7
|
+
Showing <span class="font-medium text-slate-700"><%= pagy.from %></span> to
|
|
8
|
+
<span class="font-medium text-slate-700"><%= pagy.to %></span> of
|
|
9
|
+
<span class="font-medium text-slate-700"><%= number_with_delimiter(pagy.count) %></span> results
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<nav class="flex items-center gap-1">
|
|
13
|
+
<% if pagy.prev %>
|
|
14
|
+
<%= link_to url_for(params.permit!.merge(page_param => pagy.prev)),
|
|
15
|
+
class: "inline-flex items-center gap-1 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" do %>
|
|
16
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
17
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
18
|
+
</svg>
|
|
19
|
+
Prev
|
|
20
|
+
<% end %>
|
|
21
|
+
<% else %>
|
|
22
|
+
<span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-400 bg-slate-100 border border-slate-200 rounded-lg cursor-not-allowed">
|
|
23
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
24
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
25
|
+
</svg>
|
|
26
|
+
Prev
|
|
27
|
+
</span>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
<div class="hidden sm:flex items-center gap-1 mx-2">
|
|
31
|
+
<% pagy.series.each do |item| %>
|
|
32
|
+
<% if item == :gap %>
|
|
33
|
+
<span class="px-2 py-1 text-sm text-slate-400">…</span>
|
|
34
|
+
<% elsif item.is_a?(Integer) %>
|
|
35
|
+
<%= link_to item, url_for(params.permit!.merge(page_param => item)),
|
|
36
|
+
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" %>
|
|
37
|
+
<% elsif item.is_a?(String) %>
|
|
38
|
+
<span class="px-3 py-1.5 text-sm font-medium text-white rounded-lg" style="background: var(--admin-suite-primary); border: 1px solid var(--admin-suite-primary);">
|
|
39
|
+
<%= item %>
|
|
40
|
+
</span>
|
|
41
|
+
<% end %>
|
|
42
|
+
<% end %>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<span class="sm:hidden px-3 py-1.5 text-sm text-slate-500">
|
|
46
|
+
Page <%= pagy.page %> of <%= pagy.pages %>
|
|
47
|
+
</span>
|
|
48
|
+
|
|
49
|
+
<% if pagy.next %>
|
|
50
|
+
<%= link_to url_for(params.permit!.merge(page_param => pagy.next)),
|
|
51
|
+
class: "inline-flex items-center gap-1 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" do %>
|
|
52
|
+
Next
|
|
53
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
54
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
55
|
+
</svg>
|
|
56
|
+
<% end %>
|
|
57
|
+
<% else %>
|
|
58
|
+
<span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-400 bg-slate-100 border border-slate-200 rounded-lg cursor-not-allowed">
|
|
59
|
+
Next
|
|
60
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
61
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
62
|
+
</svg>
|
|
63
|
+
</span>
|
|
64
|
+
<% end %>
|
|
65
|
+
</nav>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<% else %>
|
|
69
|
+
<div class="px-6 py-3 border-t border-slate-200">
|
|
70
|
+
<div class="text-sm text-slate-500">
|
|
71
|
+
Showing <span class="font-medium text-slate-700"><%= pagy.count %></span> results
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<% end %>
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
|
|
61
61
|
<% if active %>
|
|
62
62
|
<div class="mt-1 ml-11 space-y-0.5 text-sm">
|
|
63
|
-
<% portal[:sections].sort_by { |(_k, s)| s[:label].to_s }.each do |_section_key, section| %>
|
|
63
|
+
<% portal[:sections].sort_by { |(_k, s)| [ (s[:order] || 100).to_i, s[:label].to_s ] }.each do |_section_key, section| %>
|
|
64
64
|
<div class="text-xs font-medium text-white/70 uppercase tracking-wider px-3 pt-2 pb-1">
|
|
65
65
|
<%= section[:label] %>
|
|
66
66
|
</div>
|
|
@@ -29,9 +29,11 @@
|
|
|
29
29
|
|
|
30
30
|
<%= stylesheet_link_tag "rouge", "data-turbo-track": "reload" %>
|
|
31
31
|
|
|
32
|
-
<%# EasyMDE Markdown Editor
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
<%# EasyMDE Markdown Editor: vendored (see app/assets/vendor), loaded only
|
|
33
|
+
when the page actually renders a :markdown field (see
|
|
34
|
+
AdminSuite::UI::FieldRendererRegistry's :markdown handler, which sets
|
|
35
|
+
this content_for). %>
|
|
36
|
+
<%= yield :easymde_assets %>
|
|
35
37
|
|
|
36
38
|
<% if respond_to?(:javascript_importmap_tags) %>
|
|
37
39
|
<%= javascript_importmap_tags %>
|
|
@@ -116,14 +116,20 @@ module Admin
|
|
|
116
116
|
"#{action.label} completed successfully",
|
|
117
117
|
redirect_url: redirect_url_for_action(action, action_result)
|
|
118
118
|
)
|
|
119
|
-
rescue
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
# Neither constant is referenced unless the host actually defines it:
|
|
121
|
+
# a bare `rescue SomeGem::Error` raises NameError *while handling* the
|
|
122
|
+
# original exception in hosts without that gem, masking the real error.
|
|
123
|
+
if defined?(ActiveRecord::RecordInvalid) && e.is_a?(ActiveRecord::RecordInvalid)
|
|
124
|
+
failure_result("Validation failed: #{e.record.errors.full_messages.join(', ')}")
|
|
125
|
+
elsif e.class.name.to_s.include?("InvalidTransition")
|
|
126
|
+
failure_result("Invalid state transition: #{e.message}")
|
|
127
|
+
else
|
|
128
|
+
raise
|
|
129
|
+
end
|
|
123
130
|
end
|
|
124
131
|
|
|
125
132
|
def redirect_url_for_action(action, action_result)
|
|
126
|
-
return nil unless action.name.to_sym == :duplicate
|
|
127
133
|
return nil unless action_result.respond_to?(:persisted?) && action_result.persisted?
|
|
128
134
|
return nil unless resource_class.respond_to?(:portal_name) && resource_class.respond_to?(:resource_name_plural)
|
|
129
135
|
|
|
@@ -188,55 +194,17 @@ module Admin
|
|
|
188
194
|
nil
|
|
189
195
|
end
|
|
190
196
|
|
|
197
|
+
# Loads action handler files configured via `AdminSuite.config.action_globs`.
|
|
198
|
+
#
|
|
199
|
+
# Kept as a thin wrapper (rather than calling DefinitionLoader directly
|
|
200
|
+
# from `find_handler_class`) because `test/lib/action_executor_test.rb`
|
|
201
|
+
# exercises this exact method name against `self.class.handlers_loaded`.
|
|
202
|
+
#
|
|
203
|
+
# @return [void]
|
|
191
204
|
def load_action_handlers_for_admin_suite!
|
|
192
205
|
return unless defined?(AdminSuite)
|
|
193
206
|
|
|
194
|
-
|
|
195
|
-
# In development, this flag is reset by the Rails reloader (see engine.rb).
|
|
196
|
-
# In production/test, it persists for the process lifetime.
|
|
197
|
-
return if self.class.handlers_loaded
|
|
198
|
-
|
|
199
|
-
files = Array(AdminSuite.config.action_globs).flat_map { |g| Dir[g] }.uniq
|
|
200
|
-
|
|
201
|
-
# Set the flag even if no files found - we've done the glob and shouldn't repeat it
|
|
202
|
-
if files.empty?
|
|
203
|
-
self.class.handlers_loaded = true
|
|
204
|
-
return
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
files.each do |file|
|
|
208
|
-
begin
|
|
209
|
-
if Rails.env.development?
|
|
210
|
-
load file
|
|
211
|
-
else
|
|
212
|
-
require file
|
|
213
|
-
end
|
|
214
|
-
rescue StandardError, ScriptError => e
|
|
215
|
-
log_action_handler_load_error(file, e)
|
|
216
|
-
|
|
217
|
-
# Fail fast in dev/test so broken handler files are immediately discoverable.
|
|
218
|
-
raise if Rails.env.development? || Rails.env.test?
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# We attempted to load the configured handlers. Avoid repeating expensive globs
|
|
223
|
-
# and file loads for the rest of the process lifetime.
|
|
224
|
-
self.class.handlers_loaded = true
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def log_action_handler_load_error(file, error)
|
|
228
|
-
message = "[AdminSuite] Failed to load action handler file #{file}: #{error.class}: #{error.message}"
|
|
229
|
-
|
|
230
|
-
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
231
|
-
Rails.logger.error(message)
|
|
232
|
-
|
|
233
|
-
backtrace = Array(error.backtrace).take(20).join("\n")
|
|
234
|
-
Rails.logger.error(backtrace) unless backtrace.empty?
|
|
235
|
-
else
|
|
236
|
-
warn(message)
|
|
237
|
-
end
|
|
238
|
-
rescue StandardError
|
|
239
|
-
nil
|
|
207
|
+
AdminSuite::DefinitionLoader.load!(:actions)
|
|
240
208
|
end
|
|
241
209
|
end
|
|
242
210
|
end
|
data/lib/admin/base/resource.rb
CHANGED
|
@@ -39,6 +39,13 @@ module Admin
|
|
|
39
39
|
# end
|
|
40
40
|
# end
|
|
41
41
|
class Resource
|
|
42
|
+
extend AdminSuite::Deprecation
|
|
43
|
+
|
|
44
|
+
EXPORTABLE_DEPRECATION_MESSAGE_FORMAT =
|
|
45
|
+
"AdminSuite: %<resource>s calls `exportable`, which is a deprecated " \
|
|
46
|
+
"no-op and will be removed in 0.5.0. It never actually implemented " \
|
|
47
|
+
"export in any released version — safe to delete the call."
|
|
48
|
+
|
|
42
49
|
class << self
|
|
43
50
|
# Model configuration
|
|
44
51
|
attr_reader :model_class, :portal_name, :section_name, :nav_label, :nav_icon, :nav_order
|
|
@@ -55,9 +62,6 @@ module Admin
|
|
|
55
62
|
# Actions configuration
|
|
56
63
|
attr_reader :actions_config
|
|
57
64
|
|
|
58
|
-
# Export configuration
|
|
59
|
-
attr_reader :export_formats
|
|
60
|
-
|
|
61
65
|
# Sets the model class for this resource
|
|
62
66
|
#
|
|
63
67
|
# @param klass [Class] The ActiveRecord model class
|
|
@@ -157,14 +161,6 @@ module Admin
|
|
|
157
161
|
@actions_config.instance_eval(&block) if block_given?
|
|
158
162
|
end
|
|
159
163
|
|
|
160
|
-
# Configures export formats
|
|
161
|
-
#
|
|
162
|
-
# @param formats [Array<Symbol>] Export formats (:json, :csv)
|
|
163
|
-
# @return [void]
|
|
164
|
-
def exportable(*formats)
|
|
165
|
-
@export_formats = formats
|
|
166
|
-
end
|
|
167
|
-
|
|
168
164
|
# Declares whether the generic resource routes may mutate records.
|
|
169
165
|
# Read-only resources retain index/show access while the controller
|
|
170
166
|
# rejects direct requests to every built-in mutation endpoint.
|
|
@@ -176,6 +172,30 @@ module Admin
|
|
|
176
172
|
@read_only == true
|
|
177
173
|
end
|
|
178
174
|
|
|
175
|
+
# Deprecated no-op, removed in 0.5.0.
|
|
176
|
+
#
|
|
177
|
+
# `exportable` was write-only in every prior release -- it never had
|
|
178
|
+
# a reader and never drove any export behavior -- but hosts still
|
|
179
|
+
# call it from resource-definition bodies (gleania: 30 files;
|
|
180
|
+
# trust_growth: 1). A real removal would raise `NoMethodError` at
|
|
181
|
+
# definition-load time, and in production `DefinitionLoader` logs
|
|
182
|
+
# and swallows that, so the resource just silently vanishes from the
|
|
183
|
+
# admin. Kept as a no-op instead, so those files keep loading.
|
|
184
|
+
#
|
|
185
|
+
# Deliberately does not restore `@export_formats` or any reader --
|
|
186
|
+
# only the harmless no-op comes back.
|
|
187
|
+
#
|
|
188
|
+
# @param _formats [Array<Symbol>] ignored
|
|
189
|
+
# @return [void]
|
|
190
|
+
def exportable(*_formats)
|
|
191
|
+
# `warn_once`, `warn_once_sink` and `reset_deprecation_notices!`
|
|
192
|
+
# come from `AdminSuite::Deprecation`, extended above. Keyed on the
|
|
193
|
+
# resource class itself, so each resource warns independently (and
|
|
194
|
+
# only once) rather than one call anywhere silencing every other
|
|
195
|
+
# resource's first call.
|
|
196
|
+
warn_once(name, format(EXPORTABLE_DEPRECATION_MESSAGE_FORMAT, resource: name))
|
|
197
|
+
end
|
|
198
|
+
|
|
179
199
|
# Returns the resource name derived from class name
|
|
180
200
|
#
|
|
181
201
|
# @return [String]
|
|
@@ -309,7 +329,6 @@ module Admin
|
|
|
309
329
|
@columns << ColumnDefinition.new(
|
|
310
330
|
name: name,
|
|
311
331
|
content: content,
|
|
312
|
-
render: options[:render],
|
|
313
332
|
header: options[:header] || name.to_s.humanize,
|
|
314
333
|
css_class: options[:class],
|
|
315
334
|
type: options[:type],
|
|
@@ -321,7 +340,7 @@ module Admin
|
|
|
321
340
|
end
|
|
322
341
|
end
|
|
323
342
|
|
|
324
|
-
ColumnDefinition = Struct.new(:name, :content, :
|
|
343
|
+
ColumnDefinition = Struct.new(:name, :content, :header, :css_class, :type, :toggle_field, :label_color, :label_size, :sortable, keyword_init: true)
|
|
325
344
|
|
|
326
345
|
class FiltersBuilder
|
|
327
346
|
attr_reader :filters
|
|
@@ -470,7 +489,25 @@ module Admin
|
|
|
470
489
|
|
|
471
490
|
private
|
|
472
491
|
|
|
492
|
+
# Struct members already given a dedicated home above. Everything
|
|
493
|
+
# else in the panel/section DSL call (`source:`, `empty:`,
|
|
494
|
+
# `language:`, plus `columns:` again — see below) is forwarded
|
|
495
|
+
# verbatim to the renderer via `ShowSectionDefinition#options`.
|
|
496
|
+
RESERVED_SECTION_OPTION_KEYS = %i[
|
|
497
|
+
fields association limit render title display link_to resource
|
|
498
|
+
paginate pagination per_page collapsible collapsed
|
|
499
|
+
].freeze
|
|
500
|
+
|
|
473
501
|
def build_section(name, options)
|
|
502
|
+
# `columns:` is deliberately NOT excluded here even though it also
|
|
503
|
+
# populates the dedicated `columns` member below (used today by
|
|
504
|
+
# association-table display). Built-in renderers like
|
|
505
|
+
# `:table_from` only ever see `section.options`, not the section
|
|
506
|
+
# itself, so `columns:` has to reach `options[:columns]` too or a
|
|
507
|
+
# DSL author's explicit column list would be silently dropped in
|
|
508
|
+
# favor of the renderer's own inference.
|
|
509
|
+
leftover_options = options.except(*RESERVED_SECTION_OPTION_KEYS)
|
|
510
|
+
|
|
474
511
|
ShowSectionDefinition.new(
|
|
475
512
|
name: name,
|
|
476
513
|
fields: options[:fields] || [],
|
|
@@ -485,7 +522,8 @@ module Admin
|
|
|
485
522
|
paginate: options[:paginate] || options[:pagination] || false,
|
|
486
523
|
per_page: options[:per_page],
|
|
487
524
|
collapsible: options[:collapsible] || false,
|
|
488
|
-
collapsed: options[:collapsed] || false
|
|
525
|
+
collapsed: options[:collapsed] || false,
|
|
526
|
+
options: leftover_options
|
|
489
527
|
)
|
|
490
528
|
end
|
|
491
529
|
end
|
|
@@ -493,6 +531,7 @@ module Admin
|
|
|
493
531
|
ShowSectionDefinition = Struct.new(
|
|
494
532
|
:name, :fields, :association, :limit, :render, :title,
|
|
495
533
|
:display, :columns, :link_to, :resource, :paginate, :per_page, :collapsible, :collapsed,
|
|
534
|
+
:options,
|
|
496
535
|
keyword_init: true
|
|
497
536
|
)
|
|
498
537
|
|
|
@@ -17,7 +17,6 @@ module AdminSuite
|
|
|
17
17
|
:action_globs,
|
|
18
18
|
:portal_globs,
|
|
19
19
|
:dashboard_globs,
|
|
20
|
-
:portals,
|
|
21
20
|
:custom_renderers,
|
|
22
21
|
:icon_renderer,
|
|
23
22
|
:docs_url,
|
|
@@ -25,7 +24,6 @@ module AdminSuite
|
|
|
25
24
|
:partials,
|
|
26
25
|
:theme,
|
|
27
26
|
:host_stylesheet,
|
|
28
|
-
:tailwind_cdn,
|
|
29
27
|
:root_dashboard_title,
|
|
30
28
|
:root_dashboard_description,
|
|
31
29
|
:root_dashboard_definition,
|
|
@@ -33,6 +31,22 @@ module AdminSuite
|
|
|
33
31
|
:on_action_executed,
|
|
34
32
|
:resolve_action_handler
|
|
35
33
|
|
|
34
|
+
attr_reader :portals
|
|
35
|
+
|
|
36
|
+
# Records that the host explicitly assigned portals (even to `{}`), so
|
|
37
|
+
# the engine's built-in defaults are never re-applied over explicit
|
|
38
|
+
# host intent. See #portals_configured?.
|
|
39
|
+
def portals=(value)
|
|
40
|
+
@portals_configured = true
|
|
41
|
+
@portals = value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# True once the host has assigned `config.portals` itself, distinct
|
|
45
|
+
# from the engine having applied its own defaults.
|
|
46
|
+
def portals_configured?
|
|
47
|
+
@portals_configured == true
|
|
48
|
+
end
|
|
49
|
+
|
|
36
50
|
def initialize
|
|
37
51
|
@authenticate = nil
|
|
38
52
|
@current_actor = nil
|
|
@@ -49,6 +63,7 @@ module AdminSuite
|
|
|
49
63
|
@portal_globs = []
|
|
50
64
|
@dashboard_globs = []
|
|
51
65
|
@portals = {}
|
|
66
|
+
@portals_configured = false
|
|
52
67
|
@custom_renderers = {}
|
|
53
68
|
@icon_renderer = nil
|
|
54
69
|
@docs_url = nil
|
|
@@ -56,7 +71,6 @@ module AdminSuite
|
|
|
56
71
|
@partials = {}
|
|
57
72
|
@theme = { primary: :indigo, secondary: :purple }
|
|
58
73
|
@host_stylesheet = nil
|
|
59
|
-
@tailwind_cdn = true
|
|
60
74
|
@root_dashboard_title = nil
|
|
61
75
|
@root_dashboard_description = nil
|
|
62
76
|
@root_dashboard_definition = nil
|
|
@@ -64,5 +78,16 @@ module AdminSuite
|
|
|
64
78
|
@on_action_executed = nil
|
|
65
79
|
@resolve_action_handler = nil
|
|
66
80
|
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# Sets the built-in default portals without marking portals as
|
|
85
|
+
# host-configured. Only the engine's `apply_default_portals!` should
|
|
86
|
+
# call this — it deliberately bypasses the public `portals=` writer so
|
|
87
|
+
# gem-applied defaults stay distinguishable from an explicit host
|
|
88
|
+
# assignment (including an explicit `{}` meant to suppress defaults).
|
|
89
|
+
def default_portals!(value)
|
|
90
|
+
@portals = value
|
|
91
|
+
end
|
|
67
92
|
end
|
|
68
93
|
end
|