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
|
@@ -60,20 +60,26 @@
|
|
|
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>
|
|
67
|
-
<% section[:items].
|
|
68
|
-
<%
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
<% if section[:items].any? %>
|
|
68
|
+
<% section[:items].sort_by { |it| [ (it[:order] || 100).to_i, it[:label].to_s ] }.each do |item| %>
|
|
69
|
+
<% item_active = request.path.start_with?(item[:path].to_s) %>
|
|
70
|
+
<%= link_to item[:path],
|
|
71
|
+
class: "flex items-center gap-2 px-3 py-1.5 rounded #{item_active ? "text-white bg-white/20" : "text-white/90 hover:bg-white/10 hover:text-white"}",
|
|
72
|
+
data: { turbo_frame: "_top" } do %>
|
|
73
|
+
<% if item[:icon].present? %>
|
|
74
|
+
<%= admin_suite_icon(item[:icon], class: "w-3.5 h-3.5 text-white/90") %>
|
|
75
|
+
<% end %>
|
|
76
|
+
<span class="truncate"><%= item[:label] %></span>
|
|
74
77
|
<% end %>
|
|
75
|
-
<span class="truncate"><%= item[:label] %></span>
|
|
76
78
|
<% end %>
|
|
79
|
+
<% else %>
|
|
80
|
+
<div class="px-3 py-1.5 text-xs text-white/50 italic">
|
|
81
|
+
No resources in this section yet.
|
|
82
|
+
</div>
|
|
77
83
|
<% end %>
|
|
78
84
|
<% end %>
|
|
79
85
|
</div>
|
|
@@ -29,9 +29,17 @@
|
|
|
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 %>
|
|
37
|
+
|
|
38
|
+
<%# Chart.js: vendored (see app/assets/vendor), loaded only when the page
|
|
39
|
+
actually renders a chart panel with data (see
|
|
40
|
+
app/views/admin_suite/panels/_chart.html.erb, which sets this
|
|
41
|
+
content_for). %>
|
|
42
|
+
<%= yield :chart_assets %>
|
|
35
43
|
|
|
36
44
|
<% if respond_to?(:javascript_importmap_tags) %>
|
|
37
45
|
<%= javascript_importmap_tags %>
|
data/config/routes.rb
CHANGED
|
@@ -14,6 +14,9 @@ AdminSuite::Engine.routes.draw do
|
|
|
14
14
|
scope ":portal/:resource_name" do
|
|
15
15
|
get "/", to: "resources#index", as: :resources
|
|
16
16
|
get "/new", to: "resources#new", as: :new_resource
|
|
17
|
+
# Must precede "/:id" -- a literal segment match wins over the dynamic
|
|
18
|
+
# one only because Rails tries routes in declaration order.
|
|
19
|
+
get "/search", to: "resources#search", as: :search_resources
|
|
17
20
|
post "/", to: "resources#create"
|
|
18
21
|
get "/:id", to: "resources#show", as: :resource
|
|
19
22
|
get "/:id/edit", to: "resources#edit", as: :edit_resource
|
|
@@ -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
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
module Admin
|
|
4
4
|
module Base
|
|
5
5
|
class FilterBuilder
|
|
6
|
+
# Matches the index search box's existing floor: below this,
|
|
7
|
+
# `apply_search` leaves the scope untouched rather than emitting a
|
|
8
|
+
# near-useless single/double-character ILIKE. `search_predicate`
|
|
9
|
+
# (below) is where this now lives, since it's shared with
|
|
10
|
+
# `ResourcesController#search`.
|
|
11
|
+
MIN_SEARCH_LENGTH = 3
|
|
12
|
+
|
|
6
13
|
attr_reader :resource_class, :params
|
|
7
14
|
|
|
8
15
|
def initialize(resource_class, params)
|
|
@@ -26,6 +33,43 @@ module Admin
|
|
|
26
33
|
params.permit(*permitted_keys).to_h.symbolize_keys
|
|
27
34
|
end
|
|
28
35
|
|
|
36
|
+
# Builds the shared ILIKE-over-searchable_fields predicate -- the exact
|
|
37
|
+
# logic `apply_search` below uses for the index's own search box --
|
|
38
|
+
# extracted so `ResourcesController#search` (the searchable_select
|
|
39
|
+
# endpoint) can reuse it verbatim instead of growing a second,
|
|
40
|
+
# divergent search path over the same whitelist.
|
|
41
|
+
#
|
|
42
|
+
# The term is always returned as a value to be bound (`"%term%"`),
|
|
43
|
+
# never interpolated into the SQL text -- only the (whitelisted) field
|
|
44
|
+
# *names* from `index_config.searchable_fields` go into the conditions
|
|
45
|
+
# string, exactly as `apply_search` has always built it. That keeps
|
|
46
|
+
# this safe against SQL metacharacters in `term`: whatever a caller
|
|
47
|
+
# passes ends up as a single bound parameter, not part of the query
|
|
48
|
+
# text.
|
|
49
|
+
#
|
|
50
|
+
# Returns nil when no predicate should be applied at all: no index
|
|
51
|
+
# config, no declared `searchable` fields, a blank term, or a term
|
|
52
|
+
# shorter than `MIN_SEARCH_LENGTH`. What a nil predicate *means* is
|
|
53
|
+
# left to the caller -- `apply_search` treats it as "don't filter"
|
|
54
|
+
# (falls back to the unfiltered scope, correct for an index filter
|
|
55
|
+
# box), while `ResourcesController#search` treats it as "no results"
|
|
56
|
+
# (correct for a raw JSON data endpoint, which must never hand back
|
|
57
|
+
# the unfiltered table just because the query was blank/too short or
|
|
58
|
+
# the resource has no searchable fields to check).
|
|
59
|
+
#
|
|
60
|
+
# @param index_config [Admin::Base::Resource::IndexConfig, nil]
|
|
61
|
+
# @param term [String, nil]
|
|
62
|
+
# @return [Array(String, String), nil] [sql_conditions, "%term%"]
|
|
63
|
+
def self.search_predicate(index_config, term)
|
|
64
|
+
return nil if index_config.nil?
|
|
65
|
+
return nil if term.blank?
|
|
66
|
+
return nil if index_config.searchable_fields.empty?
|
|
67
|
+
return nil if term.to_s.length < MIN_SEARCH_LENGTH
|
|
68
|
+
|
|
69
|
+
conditions = index_config.searchable_fields.map { |field| "#{field} ILIKE :search" }.join(" OR ")
|
|
70
|
+
[ conditions, "%#{term}%" ]
|
|
71
|
+
end
|
|
72
|
+
|
|
29
73
|
private
|
|
30
74
|
|
|
31
75
|
def index_config
|
|
@@ -34,12 +78,11 @@ module Admin
|
|
|
34
78
|
|
|
35
79
|
def apply_search(scope)
|
|
36
80
|
return scope unless index_config
|
|
37
|
-
return scope if params[:search].blank?
|
|
38
|
-
return scope if index_config.searchable_fields.empty?
|
|
39
|
-
return scope if params[:search].to_s.length < 3
|
|
40
81
|
|
|
41
|
-
|
|
42
|
-
|
|
82
|
+
predicate = self.class.search_predicate(index_config, params[:search])
|
|
83
|
+
return scope unless predicate
|
|
84
|
+
|
|
85
|
+
conditions, search_term = predicate
|
|
43
86
|
scope.where(conditions, search: search_term)
|
|
44
87
|
end
|
|
45
88
|
|
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.6.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,32 @@ module Admin
|
|
|
176
172
|
@read_only == true
|
|
177
173
|
end
|
|
178
174
|
|
|
175
|
+
# Deprecated no-op; removal targeted at 0.6.0 (moved from the
|
|
176
|
+
# originally-planned 0.5.0 -- pending the gleania/trust_growth host
|
|
177
|
+
# migrations).
|
|
178
|
+
#
|
|
179
|
+
# `exportable` was write-only in every prior release -- it never had
|
|
180
|
+
# a reader and never drove any export behavior -- but hosts still
|
|
181
|
+
# call it from resource-definition bodies (gleania: 30 files;
|
|
182
|
+
# trust_growth: 1). A real removal would raise `NoMethodError` at
|
|
183
|
+
# definition-load time, and in production `DefinitionLoader` logs
|
|
184
|
+
# and swallows that, so the resource just silently vanishes from the
|
|
185
|
+
# admin. Kept as a no-op instead, so those files keep loading.
|
|
186
|
+
#
|
|
187
|
+
# Deliberately does not restore `@export_formats` or any reader --
|
|
188
|
+
# only the harmless no-op comes back.
|
|
189
|
+
#
|
|
190
|
+
# @param _formats [Array<Symbol>] ignored
|
|
191
|
+
# @return [void]
|
|
192
|
+
def exportable(*_formats)
|
|
193
|
+
# `warn_once`, `warn_once_sink` and `reset_deprecation_notices!`
|
|
194
|
+
# come from `AdminSuite::Deprecation`, extended above. Keyed on the
|
|
195
|
+
# resource class itself, so each resource warns independently (and
|
|
196
|
+
# only once) rather than one call anywhere silencing every other
|
|
197
|
+
# resource's first call.
|
|
198
|
+
warn_once(name, format(EXPORTABLE_DEPRECATION_MESSAGE_FORMAT, resource: name))
|
|
199
|
+
end
|
|
200
|
+
|
|
179
201
|
# Returns the resource name derived from class name
|
|
180
202
|
#
|
|
181
203
|
# @return [String]
|
|
@@ -252,7 +274,7 @@ module Admin
|
|
|
252
274
|
# Index view configuration
|
|
253
275
|
class IndexConfig
|
|
254
276
|
attr_reader :searchable_fields, :sortable_fields, :default_sort, :default_sort_direction,
|
|
255
|
-
:columns_list, :filters_list, :stats_list, :per_page
|
|
277
|
+
:columns_list, :filters_list, :stats_list, :per_page, :includes_list
|
|
256
278
|
|
|
257
279
|
def initialize
|
|
258
280
|
@searchable_fields = []
|
|
@@ -263,6 +285,7 @@ module Admin
|
|
|
263
285
|
@filters_list = []
|
|
264
286
|
@stats_list = []
|
|
265
287
|
@per_page = 25
|
|
288
|
+
@includes_list = []
|
|
266
289
|
end
|
|
267
290
|
|
|
268
291
|
def searchable(*fields)
|
|
@@ -279,6 +302,23 @@ module Admin
|
|
|
279
302
|
@per_page = count
|
|
280
303
|
end
|
|
281
304
|
|
|
305
|
+
# Kills the index's association N+1 (Task 3 made `belongs_to`
|
|
306
|
+
# columns render as links, so each one now genuinely dereferences
|
|
307
|
+
# the association -- one query per row without this). The
|
|
308
|
+
# controller applies this to the filtered scope only when the
|
|
309
|
+
# scope actually responds to `#includes` -- PORO test doubles and
|
|
310
|
+
# some host scopes don't -- and swallows a bad association name
|
|
311
|
+
# (typo, renamed association) by logging and rendering
|
|
312
|
+
# unoptimized rather than 500ing. `nil` entries (e.g. a stray
|
|
313
|
+
# `includes nil`) are dropped rather than stored and handed to the
|
|
314
|
+
# scope verbatim.
|
|
315
|
+
#
|
|
316
|
+
# @param associations [Array<Symbol, String>]
|
|
317
|
+
# @return [void]
|
|
318
|
+
def includes(*associations)
|
|
319
|
+
@includes_list = associations.flatten.compact
|
|
320
|
+
end
|
|
321
|
+
|
|
282
322
|
def columns(&block)
|
|
283
323
|
builder = ColumnsBuilder.new
|
|
284
324
|
builder.instance_eval(&block) if block_given?
|
|
@@ -309,19 +349,19 @@ module Admin
|
|
|
309
349
|
@columns << ColumnDefinition.new(
|
|
310
350
|
name: name,
|
|
311
351
|
content: content,
|
|
312
|
-
render: options[:render],
|
|
313
352
|
header: options[:header] || name.to_s.humanize,
|
|
314
353
|
css_class: options[:class],
|
|
315
354
|
type: options[:type],
|
|
316
355
|
toggle_field: options[:toggle_field],
|
|
317
356
|
label_color: options[:label_color],
|
|
318
357
|
label_size: options[:label_size],
|
|
319
|
-
sortable: options[:sortable] || false
|
|
358
|
+
sortable: options[:sortable] || false,
|
|
359
|
+
align: options[:align]
|
|
320
360
|
)
|
|
321
361
|
end
|
|
322
362
|
end
|
|
323
363
|
|
|
324
|
-
ColumnDefinition = Struct.new(:name, :content, :
|
|
364
|
+
ColumnDefinition = Struct.new(:name, :content, :header, :css_class, :type, :toggle_field, :label_color, :label_size, :sortable, :align, keyword_init: true)
|
|
325
365
|
|
|
326
366
|
class FiltersBuilder
|
|
327
367
|
attr_reader :filters
|
|
@@ -393,7 +433,8 @@ module Admin
|
|
|
393
433
|
variants: options[:variants],
|
|
394
434
|
label_color: options[:label_color],
|
|
395
435
|
label_size: options[:label_size],
|
|
396
|
-
parent_field: options[:parent_field]
|
|
436
|
+
parent_field: options[:parent_field],
|
|
437
|
+
resource: options[:resource]
|
|
397
438
|
)
|
|
398
439
|
end
|
|
399
440
|
|
|
@@ -420,6 +461,12 @@ module Admin
|
|
|
420
461
|
:collection, :create_url, :accept, :rows, :readonly,
|
|
421
462
|
:if_condition, :unless_condition, :multiple, :creatable,
|
|
422
463
|
:preview, :variants, :label_color, :label_size, :parent_field,
|
|
464
|
+
# `resource:` (e.g. `resource: :companies`) lets a `searchable_select`
|
|
465
|
+
# field resolve its search URL automatically against the gem's own
|
|
466
|
+
# search endpoint, rather than requiring every host to supply a
|
|
467
|
+
# `collection:` String pointing at a hand-rolled route. See
|
|
468
|
+
# `render_searchable_select` in `app/helpers/admin_suite/base_helper.rb`.
|
|
469
|
+
:resource,
|
|
423
470
|
keyword_init: true
|
|
424
471
|
)
|
|
425
472
|
|
|
@@ -470,7 +517,25 @@ module Admin
|
|
|
470
517
|
|
|
471
518
|
private
|
|
472
519
|
|
|
520
|
+
# Struct members already given a dedicated home above. Everything
|
|
521
|
+
# else in the panel/section DSL call (`source:`, `empty:`,
|
|
522
|
+
# `language:`, plus `columns:` again — see below) is forwarded
|
|
523
|
+
# verbatim to the renderer via `ShowSectionDefinition#options`.
|
|
524
|
+
RESERVED_SECTION_OPTION_KEYS = %i[
|
|
525
|
+
fields association limit render title display link_to resource
|
|
526
|
+
paginate pagination per_page collapsible collapsed hide_blank
|
|
527
|
+
].freeze
|
|
528
|
+
|
|
473
529
|
def build_section(name, options)
|
|
530
|
+
# `columns:` is deliberately NOT excluded here even though it also
|
|
531
|
+
# populates the dedicated `columns` member below (used today by
|
|
532
|
+
# association-table display). Built-in renderers like
|
|
533
|
+
# `:table_from` only ever see `section.options`, not the section
|
|
534
|
+
# itself, so `columns:` has to reach `options[:columns]` too or a
|
|
535
|
+
# DSL author's explicit column list would be silently dropped in
|
|
536
|
+
# favor of the renderer's own inference.
|
|
537
|
+
leftover_options = options.except(*RESERVED_SECTION_OPTION_KEYS)
|
|
538
|
+
|
|
474
539
|
ShowSectionDefinition.new(
|
|
475
540
|
name: name,
|
|
476
541
|
fields: options[:fields] || [],
|
|
@@ -485,7 +550,15 @@ module Admin
|
|
|
485
550
|
paginate: options[:paginate] || options[:pagination] || false,
|
|
486
551
|
per_page: options[:per_page],
|
|
487
552
|
collapsible: options[:collapsible] || false,
|
|
488
|
-
collapsed: options[:collapsed] || false
|
|
553
|
+
collapsed: options[:collapsed] || false,
|
|
554
|
+
# `fields:`-only rows whose value is blank (nil or `.empty?`)
|
|
555
|
+
# are omitted entirely -- see `show_value_blank?` in
|
|
556
|
+
# `AdminSuite::BaseHelper`, which reads this member. Default
|
|
557
|
+
# `false`: no existing show page changes unless it opts in.
|
|
558
|
+
# Deliberately NOT `value.blank?` -- that's true for `false`
|
|
559
|
+
# and `0`, both of which are meaningful, rendered values today.
|
|
560
|
+
hide_blank: options[:hide_blank] || false,
|
|
561
|
+
options: leftover_options
|
|
489
562
|
)
|
|
490
563
|
end
|
|
491
564
|
end
|
|
@@ -493,6 +566,8 @@ module Admin
|
|
|
493
566
|
ShowSectionDefinition = Struct.new(
|
|
494
567
|
:name, :fields, :association, :limit, :render, :title,
|
|
495
568
|
:display, :columns, :link_to, :resource, :paginate, :per_page, :collapsible, :collapsed,
|
|
569
|
+
:hide_blank,
|
|
570
|
+
:options,
|
|
496
571
|
keyword_init: true
|
|
497
572
|
)
|
|
498
573
|
|
|
@@ -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,20 @@ module AdminSuite
|
|
|
64
78
|
@on_action_executed = nil
|
|
65
79
|
@resolve_action_handler = nil
|
|
66
80
|
end
|
|
81
|
+
|
|
82
|
+
# Sets the built-in default portals without marking portals as
|
|
83
|
+
# host-configured.
|
|
84
|
+
#
|
|
85
|
+
# Engine-internal: public so `AdminSuite::Engine.apply_default_portals!`
|
|
86
|
+
# can call it directly, without reaching past `Configuration`'s privacy
|
|
87
|
+
# via `send`. Not part of the host-facing configuration API -- a host
|
|
88
|
+
# app should never call this itself; use `config.portals = { ... }` (or
|
|
89
|
+
# `config.portals = {}` to suppress the built-in defaults). This
|
|
90
|
+
# deliberately bypasses the public `portals=` writer so gem-applied
|
|
91
|
+
# defaults stay distinguishable from an explicit host assignment
|
|
92
|
+
# (including an explicit `{}` meant to suppress defaults).
|
|
93
|
+
def default_portals!(value)
|
|
94
|
+
@portals = value
|
|
95
|
+
end
|
|
67
96
|
end
|
|
68
97
|
end
|