admin_suite 0.3.0 → 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 +88 -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/_chart.html.erb +3 -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/dashboard_test.rb +40 -0
- 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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
# Reopened (never redefined) so RendererTest can define/remove scratch
|
|
6
|
+
# `Admin::Renderers::<Key>Renderer` probe classes per test, for Finding 2's
|
|
7
|
+
# host-class-vs-gem-default precedence tests below.
|
|
8
|
+
module Admin
|
|
9
|
+
module Renderers
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module AdminSuite
|
|
14
|
+
class RendererTest < ActionView::TestCase
|
|
15
|
+
# See ShowValueFormatterTest for why this is needed: BaseHelper's
|
|
16
|
+
# primitives (via render_json_block / render_text_block) call bare `h`,
|
|
17
|
+
# which ActionView::TestCase::Behavior does not provide on its own.
|
|
18
|
+
include ::ERB::Util
|
|
19
|
+
include AdminSuite::BaseHelper
|
|
20
|
+
|
|
21
|
+
Record = Struct.new(:id, :payload)
|
|
22
|
+
|
|
23
|
+
class GreetingRenderer < AdminSuite::Renderer
|
|
24
|
+
def render
|
|
25
|
+
content_tag(:p, "Hello #{record.id}")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class PrimitivesRenderer < AdminSuite::Renderer
|
|
30
|
+
def render
|
|
31
|
+
safe_join([
|
|
32
|
+
key_value_list([ [ "Status", "active" ] ]),
|
|
33
|
+
data_table([ { name: "a", count: 1 } ], columns: %i[name count]),
|
|
34
|
+
badge("live", color: :green),
|
|
35
|
+
empty_state("nothing here")
|
|
36
|
+
])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
setup { AdminSuite::RendererRegistry.register(:greeting, GreetingRenderer) }
|
|
41
|
+
teardown { AdminSuite::RendererRegistry.unregister(:greeting) }
|
|
42
|
+
|
|
43
|
+
test "a renderer subclass receives the record and renders" do
|
|
44
|
+
assert_includes GreetingRenderer.new(Record.new(7), self).render, "Hello 7"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
test "the base class requires #render" do
|
|
48
|
+
assert_raises(NotImplementedError) { AdminSuite::Renderer.new(Record.new(1), self).render }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
test "registry lookup returns the registered class and raises for unknown keys" do
|
|
52
|
+
assert_equal GreetingRenderer, AdminSuite::RendererRegistry.lookup(:greeting)
|
|
53
|
+
assert_nil AdminSuite::RendererRegistry.lookup(:nope)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
test "registered lists every registered key" do
|
|
57
|
+
assert_includes AdminSuite::RendererRegistry.registered, :greeting
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
test "primitives render key-value pairs, tables, badges and empty states" do
|
|
61
|
+
html = PrimitivesRenderer.new(Record.new(1), self).render
|
|
62
|
+
assert_includes html, "Status"
|
|
63
|
+
assert_includes html, "active"
|
|
64
|
+
# data_table humanizes column names for the header (see Renderer#data_table),
|
|
65
|
+
# so ":count" renders as "Count", not "count".
|
|
66
|
+
assert_includes html, "Count"
|
|
67
|
+
assert_includes html, "live"
|
|
68
|
+
assert_includes html, "nothing here"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
test "data_table renders its empty message when rows are blank" do
|
|
72
|
+
html = AdminSuite::Renderer.new(Record.new(1), self).send(:data_table, [], columns: %i[a], empty: "no rows")
|
|
73
|
+
assert_includes html, "no rows"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
test "render_custom_section resolves a registered renderer class" do
|
|
77
|
+
assert_includes render_custom_section(Record.new(9), :greeting), "Hello 9"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Lookup-precedence regression guard. Tasks 4 and 5 both insert new
|
|
81
|
+
# branches into render_custom_section (built-ins, then the deprecated
|
|
82
|
+
# Gleania four) between the registry lookup and the "Unknown" fallback —
|
|
83
|
+
# this pins the two boundaries already in place today so neither task can
|
|
84
|
+
# silently invert them.
|
|
85
|
+
test "a legacy custom_renderers proc wins over a registered renderer class" do
|
|
86
|
+
key = :precedence_probe
|
|
87
|
+
AdminSuite::RendererRegistry.register(key, GreetingRenderer)
|
|
88
|
+
saved = AdminSuite.config.custom_renderers[key]
|
|
89
|
+
AdminSuite.config.custom_renderers[key] = ->(record, _view) { "legacy-proc-#{record.id}" }
|
|
90
|
+
|
|
91
|
+
html = render_custom_section(Record.new(5), key)
|
|
92
|
+
|
|
93
|
+
assert_includes html, "legacy-proc-5"
|
|
94
|
+
refute_includes html, "Hello 5"
|
|
95
|
+
ensure
|
|
96
|
+
AdminSuite::RendererRegistry.unregister(key)
|
|
97
|
+
AdminSuite.config.custom_renderers.delete(key)
|
|
98
|
+
AdminSuite.config.custom_renderers[key] = saved if saved
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Finding 4 of the whole-branch review: `config.custom_renderers` procs
|
|
102
|
+
# were deprecated on paper only (CHANGELOG, 0.4.0) with no runtime
|
|
103
|
+
# signal -- unlike the four legacy Gleania renderers, which warn once
|
|
104
|
+
# per key. trust_growth has 23 of these procs and needs a way to notice
|
|
105
|
+
# during the migration window.
|
|
106
|
+
test "using a legacy custom_renderers proc logs a deprecation once per key" do
|
|
107
|
+
key = :legacy_proc_warning_probe
|
|
108
|
+
AdminSuite::LegacyCustomRendererProcs.reset_deprecation_notices!
|
|
109
|
+
AdminSuite.config.custom_renderers[key] = ->(record, _view) { "legacy-proc-#{record.id}" }
|
|
110
|
+
|
|
111
|
+
logged = []
|
|
112
|
+
AdminSuite::LegacyCustomRendererProcs.stub(:warn_once_sink, ->(msg) { logged << msg }) do
|
|
113
|
+
2.times { render_custom_section(Record.new(5), key) }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
assert_equal 1, logged.size
|
|
117
|
+
assert_includes logged.first, key.to_s
|
|
118
|
+
assert_includes logged.first, "deprecated"
|
|
119
|
+
assert_includes logged.first, "0.5.0"
|
|
120
|
+
ensure
|
|
121
|
+
AdminSuite.config.custom_renderers.delete(key)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Finding 2 of the whole-branch review: render_custom_section used to
|
|
125
|
+
# resolve `RendererRegistry.lookup(key) || host_renderer_class(key)`,
|
|
126
|
+
# but the gem's own built-ins and the deprecated Gleania four were
|
|
127
|
+
# registered into that same `lookup` store at require time -- so a host
|
|
128
|
+
# that followed the deprecation advice ("Move it to app/admin/renderers
|
|
129
|
+
# in your app") and defined its own `Admin::Renderers::<Key>Renderer`
|
|
130
|
+
# was silently shadowed, and the deprecation warning never stopped.
|
|
131
|
+
# Built-ins/deprecated-four now live in a separate `register_default`
|
|
132
|
+
# store, checked only *after* a host renderer class.
|
|
133
|
+
test "a host renderer class beats a gem default registered under the same key" do
|
|
134
|
+
key = :host_beats_default_probe
|
|
135
|
+
AdminSuite::RendererRegistry.register_default(key, GreetingRenderer)
|
|
136
|
+
Admin::Renderers.const_set(:HostBeatsDefaultProbeRenderer, Class.new(AdminSuite::Renderer) do
|
|
137
|
+
def render
|
|
138
|
+
"host-class-#{record.id}"
|
|
139
|
+
end
|
|
140
|
+
end)
|
|
141
|
+
|
|
142
|
+
html = render_custom_section(Record.new(7), key)
|
|
143
|
+
|
|
144
|
+
assert_includes html, "host-class-7"
|
|
145
|
+
refute_includes html, "Hello 7"
|
|
146
|
+
ensure
|
|
147
|
+
if Admin::Renderers.const_defined?(:HostBeatsDefaultProbeRenderer, false)
|
|
148
|
+
Admin::Renderers.send(:remove_const, :HostBeatsDefaultProbeRenderer)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
test "an explicit registration still beats a host renderer class" do
|
|
153
|
+
key = :explicit_beats_host_probe
|
|
154
|
+
AdminSuite::RendererRegistry.register(key, GreetingRenderer)
|
|
155
|
+
Admin::Renderers.const_set(:ExplicitBeatsHostProbeRenderer, Class.new(AdminSuite::Renderer) do
|
|
156
|
+
def render
|
|
157
|
+
"host-class-#{record.id}"
|
|
158
|
+
end
|
|
159
|
+
end)
|
|
160
|
+
|
|
161
|
+
html = render_custom_section(Record.new(8), key)
|
|
162
|
+
|
|
163
|
+
assert_includes html, "Hello 8"
|
|
164
|
+
refute_includes html, "host-class-8"
|
|
165
|
+
ensure
|
|
166
|
+
AdminSuite::RendererRegistry.unregister(key)
|
|
167
|
+
if Admin::Renderers.const_defined?(:ExplicitBeatsHostProbeRenderer, false)
|
|
168
|
+
Admin::Renderers.send(:remove_const, :ExplicitBeatsHostProbeRenderer)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
test "a registered renderer class wins over the built-in case branches" do
|
|
173
|
+
# :json_preview has a legacy case branch; a registered class must take it over.
|
|
174
|
+
#
|
|
175
|
+
# Task 4 registers :json_preview as a permanent alias to JsonRenderer
|
|
176
|
+
# at require time, so — unlike when this test was first written —
|
|
177
|
+
# there is now always a prior registration to restore. Unregistering
|
|
178
|
+
# unconditionally in `ensure` would delete that alias process-wide for
|
|
179
|
+
# every test that runs after this one (a real, order-dependent bug
|
|
180
|
+
# this exact test used to hide).
|
|
181
|
+
previous = AdminSuite::RendererRegistry.lookup(:json_preview)
|
|
182
|
+
AdminSuite::RendererRegistry.register(:json_preview, GreetingRenderer)
|
|
183
|
+
assert_includes render_custom_section(Record.new(6), :json_preview), "Hello 6"
|
|
184
|
+
ensure
|
|
185
|
+
if previous
|
|
186
|
+
AdminSuite::RendererRegistry.register(:json_preview, previous)
|
|
187
|
+
else
|
|
188
|
+
AdminSuite::RendererRegistry.unregister(:json_preview)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# json_block/code_block/badge each delegate to a BaseHelper primitive whose
|
|
193
|
+
# real signature differs from the brief's illustrative sketch
|
|
194
|
+
# (render_json_block takes no `title:`; render_label_badge takes `color:`
|
|
195
|
+
# as a keyword, not positional) — these primitives were adapted to match,
|
|
196
|
+
# per the task's explicit instruction. Covered directly since the brief's
|
|
197
|
+
# own PrimitivesRenderer test never exercises json_block or code_block.
|
|
198
|
+
test "json_block renders pretty-printed JSON, with an optional title heading" do
|
|
199
|
+
renderer = AdminSuite::Renderer.new(Record.new(1), self)
|
|
200
|
+
html = renderer.send(:json_block, { a: 1 })
|
|
201
|
+
assert_includes html, "language-json"
|
|
202
|
+
assert_includes html, ""a""
|
|
203
|
+
|
|
204
|
+
titled = renderer.send(:json_block, { a: 1 }, title: "Payload")
|
|
205
|
+
assert_includes titled, "Payload"
|
|
206
|
+
assert_includes titled, "language-json"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
test "code_block renders a syntax-highlighted text block" do
|
|
210
|
+
html = AdminSuite::Renderer.new(Record.new(1), self).send(:code_block, "SELECT 1", language: :sql)
|
|
211
|
+
assert_includes html, "SELECT 1"
|
|
212
|
+
assert_includes html, "language-sql"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
test "badge renders the label with its color class, passed as a keyword" do
|
|
216
|
+
html = AdminSuite::Renderer.new(Record.new(1), self).send(:badge, "live", color: :green)
|
|
217
|
+
assert_includes html, "live"
|
|
218
|
+
assert_includes html, "bg-green-100"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
module AdminSuite
|
|
6
|
+
class ResourceExportableDeprecationTest < ActiveSupport::TestCase
|
|
7
|
+
# Regression fixture for Finding 1 of the whole-branch review: Task 8
|
|
8
|
+
# removed `exportable` after grepping the *gem* for readers, but host
|
|
9
|
+
# apps are *callers* of this DSL (gleania: 30 resource files;
|
|
10
|
+
# trust_growth: 1). A real removal raises `NoMethodError` at
|
|
11
|
+
# definition-load time -- and in production `DefinitionLoader` logs and
|
|
12
|
+
# swallows that, so the resource silently vanishes from the admin.
|
|
13
|
+
class LegacyExportableResource < Admin::Base::Resource
|
|
14
|
+
exportable :json, :csv
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
setup { LegacyExportableResource.reset_deprecation_notices! }
|
|
18
|
+
|
|
19
|
+
test "calling exportable in a resource body is harmless" do
|
|
20
|
+
assert_nothing_raised { LegacyExportableResource.exportable(:json, :csv) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
test "exportable does not restore any export-format reader" do
|
|
24
|
+
refute LegacyExportableResource.respond_to?(:export_formats)
|
|
25
|
+
refute LegacyExportableResource.instance_variable_defined?(:@export_formats)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
test "exportable warns once per resource class, naming the class and the 0.5.0 removal" do
|
|
29
|
+
logged = []
|
|
30
|
+
LegacyExportableResource.stub(:warn_once_sink, ->(msg) { logged << msg }) do
|
|
31
|
+
2.times { LegacyExportableResource.exportable(:json, :csv) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
assert_equal 1, logged.size
|
|
35
|
+
assert_includes logged.first, "LegacyExportableResource"
|
|
36
|
+
assert_includes logged.first, "0.5.0"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
module AdminSuite
|
|
6
|
+
class ShowValueFormatterTest < ActionView::TestCase
|
|
7
|
+
# ActionView::Base includes ::ERB::Util (giving views access to `h`), but
|
|
8
|
+
# ActionView::TestCase::Behavior only includes ActionView::Helpers, not
|
|
9
|
+
# ERB::Util. Include it here so this test's context matches what
|
|
10
|
+
# BaseHelper actually runs inside at runtime (`h` is used by
|
|
11
|
+
# render_text_block / highlight_json).
|
|
12
|
+
include ::ERB::Util
|
|
13
|
+
include AdminSuite::BaseHelper
|
|
14
|
+
|
|
15
|
+
# A record stand-in exposing the value under the field name the formatter
|
|
16
|
+
# reads via `record.public_send(field_name)`.
|
|
17
|
+
Record = Struct.new(:some_field)
|
|
18
|
+
|
|
19
|
+
def fmt(value)
|
|
20
|
+
format_show_value(Record.new(value), :some_field)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
test "nil renders the em dash placeholder" do
|
|
24
|
+
assert_includes fmt(nil), "—"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
test "booleans render Yes/No with an icon" do
|
|
28
|
+
assert_includes fmt(true), "Yes"
|
|
29
|
+
assert_includes fmt(false), "No"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
test "dates and times render humanized with relative age" do
|
|
33
|
+
assert_includes fmt(Date.new(2026, 1, 2)), "January"
|
|
34
|
+
assert_includes fmt(Time.utc(2026, 1, 2, 3, 4)), "January"
|
|
35
|
+
assert_includes fmt(DateTime.new(2026, 1, 2, 3, 4)), "January"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
test "integers and floats render delimited" do
|
|
39
|
+
assert_includes fmt(1_234_567), "1,234,567"
|
|
40
|
+
assert_includes fmt(1234.5), "1,234"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
test "BigDecimal renders delimited rather than as an object string" do
|
|
44
|
+
result = fmt(BigDecimal("9876.5"))
|
|
45
|
+
assert_includes result, "9,876"
|
|
46
|
+
refute_includes result, "0.98765e4"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
test "hashes render as a JSON block" do
|
|
50
|
+
assert_includes fmt({ "a" => 1 }), "JSON"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
test "arrays of hashes render as a JSON block" do
|
|
54
|
+
assert_includes fmt([ { "a" => 1 } ]), "JSON"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
test "empty arrays render a placeholder, populated arrays render chips" do
|
|
58
|
+
assert_includes fmt([]), "Empty array"
|
|
59
|
+
assert_includes fmt(%w[alpha beta]), "alpha"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
test "long strings render as a text block, short strings render inline" do
|
|
63
|
+
long = "x" * 250
|
|
64
|
+
assert_includes fmt(long), "x"
|
|
65
|
+
assert_includes fmt("short"), "short"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
test "strings containing JSON render as a JSON block" do
|
|
69
|
+
assert_includes fmt('{"mode":"fast"}'), "mode"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
test "malformed JSON-looking strings fall back to plain rendering" do
|
|
73
|
+
assert_includes fmt("{not actually json"), "not actually json"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
test "short strings containing a newline render as a text block" do
|
|
77
|
+
result = fmt("first line\nsecond line")
|
|
78
|
+
assert_includes result, "bg-slate-900"
|
|
79
|
+
assert_includes result, "second line"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# ActiveRecord::Base branch (show_formatter_registry.rb:72-77) is
|
|
83
|
+
# untestable in this harness: the dummy app is database-free and
|
|
84
|
+
# ActiveRecord::Base is never loaded/defined here, so the registry's
|
|
85
|
+
# `if defined?(ActiveRecord::Base)` guard never registers that handler.
|
|
86
|
+
# See task-1-report.md's fix report for the verification.
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -30,43 +30,13 @@ module AdminSuite
|
|
|
30
30
|
[ loader, ignored_dirs, pushed_dirs ]
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if admin_dir.exist?
|
|
41
|
-
rb_files = Dir[admin_dir.join("**/*.rb").to_s]
|
|
42
|
-
rb_files.reject! { |f| f.include?("/portals/") }
|
|
43
|
-
|
|
44
|
-
host_uses_admin_namespace =
|
|
45
|
-
rb_files.any? do |file|
|
|
46
|
-
content = File.binread(file).encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
|
|
47
|
-
content.match?(/\b(module|class)\s+Admin\b/) || content.match?(/\b(module|class)\s+Admin::/)
|
|
48
|
-
rescue StandardError
|
|
49
|
-
false
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
loader.push_dir(admin_dir, namespace: Admin) if host_uses_admin_namespace
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
loader.ignore(admin_suite_app_dir) if admin_suite_app_dir.exist?
|
|
56
|
-
|
|
57
|
-
# Ignore portal DSL files (side-effect DSL, not constants).
|
|
58
|
-
if admin_portals_dir.exist?
|
|
59
|
-
portal_files = Dir[admin_portals_dir.join("**/*.rb").to_s]
|
|
60
|
-
contains_admin_suite_portals =
|
|
61
|
-
portal_files.any? do |file|
|
|
62
|
-
content = File.binread(file).encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
|
|
63
|
-
content.match?(/(::)?AdminSuite\s*\.\s*portal\b/)
|
|
64
|
-
rescue StandardError
|
|
65
|
-
false
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
loader.ignore(admin_portals_dir) if contains_admin_suite_portals
|
|
69
|
-
end
|
|
33
|
+
# A minimal stand-in for the Rails::Application instance HostAutoloadPolicy
|
|
34
|
+
# is invoked with in production: it only needs `#root` and a
|
|
35
|
+
# `#config.autoload_paths` / `#config.eager_load_paths` that respond to
|
|
36
|
+
# `#delete` (exercised only by the "maps app/admin..." test below).
|
|
37
|
+
def create_fake_app(root)
|
|
38
|
+
config = Struct.new(:autoload_paths, :eager_load_paths).new([], [])
|
|
39
|
+
Struct.new(:root, :config).new(Pathname.new(root), config)
|
|
70
40
|
end
|
|
71
41
|
|
|
72
42
|
test "ignores app/admin/portals when it contains AdminSuite portal DSL" do
|
|
@@ -78,13 +48,12 @@ module AdminSuite
|
|
|
78
48
|
"AdminSuite.portal :ops do\n # portal config\nend"
|
|
79
49
|
)
|
|
80
50
|
|
|
81
|
-
|
|
82
|
-
app_root = Pathname.new(@temp_dir)
|
|
51
|
+
app = create_fake_app(@temp_dir)
|
|
83
52
|
loader, ignored_dirs, _pushed_dirs = create_tracked_loader
|
|
84
|
-
|
|
53
|
+
AdminSuite::HostAutoloadPolicy.apply!(app, loaders: [ loader ])
|
|
85
54
|
|
|
86
55
|
# Verify that app/admin/portals was ignored
|
|
87
|
-
expected_path =
|
|
56
|
+
expected_path = app.root.join("app/admin/portals").to_s
|
|
88
57
|
assert_includes ignored_dirs, expected_path,
|
|
89
58
|
"Expected app/admin/portals to be ignored when it contains AdminSuite portal DSL"
|
|
90
59
|
end
|
|
@@ -98,13 +67,12 @@ module AdminSuite
|
|
|
98
67
|
"module Admin\n module Portals\n class AdminUser\n end\n end\nend"
|
|
99
68
|
)
|
|
100
69
|
|
|
101
|
-
|
|
102
|
-
app_root = Pathname.new(@temp_dir)
|
|
70
|
+
app = create_fake_app(@temp_dir)
|
|
103
71
|
loader, ignored_dirs, _pushed_dirs = create_tracked_loader
|
|
104
|
-
|
|
72
|
+
AdminSuite::HostAutoloadPolicy.apply!(app, loaders: [ loader ])
|
|
105
73
|
|
|
106
74
|
# Verify that app/admin/portals was NOT ignored
|
|
107
|
-
unexpected_path =
|
|
75
|
+
unexpected_path = app.root.join("app/admin/portals").to_s
|
|
108
76
|
assert_not_includes ignored_dirs, unexpected_path,
|
|
109
77
|
"Expected app/admin/portals to NOT be ignored when it contains only real constants"
|
|
110
78
|
end
|
|
@@ -118,13 +86,12 @@ module AdminSuite
|
|
|
118
86
|
"# Some DSL configuration"
|
|
119
87
|
)
|
|
120
88
|
|
|
121
|
-
|
|
122
|
-
app_root = Pathname.new(@temp_dir)
|
|
89
|
+
app = create_fake_app(@temp_dir)
|
|
123
90
|
loader, ignored_dirs, _pushed_dirs = create_tracked_loader
|
|
124
|
-
|
|
91
|
+
AdminSuite::HostAutoloadPolicy.apply!(app, loaders: [ loader ])
|
|
125
92
|
|
|
126
93
|
# Verify that app/admin_suite was ignored
|
|
127
|
-
expected_path =
|
|
94
|
+
expected_path = app.root.join("app/admin_suite").to_s
|
|
128
95
|
assert_includes ignored_dirs, expected_path,
|
|
129
96
|
"Expected app/admin_suite to always be ignored"
|
|
130
97
|
end
|
|
@@ -146,13 +113,12 @@ module AdminSuite
|
|
|
146
113
|
"AdminSuite.portal :ops do\n # portal config\nend"
|
|
147
114
|
)
|
|
148
115
|
|
|
149
|
-
|
|
150
|
-
app_root = Pathname.new(@temp_dir)
|
|
116
|
+
app = create_fake_app(@temp_dir)
|
|
151
117
|
loader, ignored_dirs, _pushed_dirs = create_tracked_loader
|
|
152
|
-
|
|
118
|
+
AdminSuite::HostAutoloadPolicy.apply!(app, loaders: [ loader ])
|
|
153
119
|
|
|
154
120
|
# Verify that app/admin/portals was ignored due to presence of portal DSL
|
|
155
|
-
expected_path =
|
|
121
|
+
expected_path = app.root.join("app/admin/portals").to_s
|
|
156
122
|
assert_includes ignored_dirs, expected_path,
|
|
157
123
|
"Expected app/admin/portals to be ignored when any file contains portal DSL"
|
|
158
124
|
end
|
|
@@ -164,8 +130,7 @@ module AdminSuite
|
|
|
164
130
|
test_file = File.join(portals_dir, "test.rb")
|
|
165
131
|
File.write(test_file, "AdminSuite.portal :ops do; end")
|
|
166
132
|
|
|
167
|
-
|
|
168
|
-
app_root = Pathname.new(@temp_dir)
|
|
133
|
+
app = create_fake_app(@temp_dir)
|
|
169
134
|
loader, ignored_dirs, _pushed_dirs = create_tracked_loader
|
|
170
135
|
|
|
171
136
|
# Temporarily override File.binread to simulate read errors
|
|
@@ -180,11 +145,10 @@ module AdminSuite
|
|
|
180
145
|
end
|
|
181
146
|
|
|
182
147
|
begin
|
|
183
|
-
|
|
184
|
-
simulate_zeitwerk_integration(app_root, loader)
|
|
148
|
+
AdminSuite::HostAutoloadPolicy.apply!(app, loaders: [ loader ])
|
|
185
149
|
|
|
186
150
|
# Verify that app/admin/portals was NOT ignored due to read error
|
|
187
|
-
unexpected_path =
|
|
151
|
+
unexpected_path = app.root.join("app/admin/portals").to_s
|
|
188
152
|
assert_not_includes ignored_dirs, unexpected_path,
|
|
189
153
|
"Expected app/admin/portals to NOT be ignored when file read fails"
|
|
190
154
|
ensure
|
|
@@ -201,11 +165,11 @@ module AdminSuite
|
|
|
201
165
|
"module Admin\n module Resources\n class UserResource; end\n end\nend\n"
|
|
202
166
|
)
|
|
203
167
|
|
|
204
|
-
|
|
168
|
+
app = create_fake_app(@temp_dir)
|
|
205
169
|
loader, _ignored_dirs, pushed_dirs = create_tracked_loader
|
|
206
|
-
|
|
170
|
+
AdminSuite::HostAutoloadPolicy.apply!(app, loaders: [ loader ])
|
|
207
171
|
|
|
208
|
-
assert pushed_dirs.any? { |h| h[:path] ==
|
|
172
|
+
assert pushed_dirs.any? { |h| h[:path] == app.root.join("app/admin").to_s && h[:namespace] == Admin },
|
|
209
173
|
"Expected app/admin to be pushed with namespace Admin when files define Admin::* constants"
|
|
210
174
|
end
|
|
211
175
|
|
|
@@ -217,11 +181,11 @@ module AdminSuite
|
|
|
217
181
|
"module Resources\n class UserResource; end\nend\n"
|
|
218
182
|
)
|
|
219
183
|
|
|
220
|
-
|
|
184
|
+
app = create_fake_app(@temp_dir)
|
|
221
185
|
loader, _ignored_dirs, pushed_dirs = create_tracked_loader
|
|
222
|
-
|
|
186
|
+
AdminSuite::HostAutoloadPolicy.apply!(app, loaders: [ loader ])
|
|
223
187
|
|
|
224
|
-
assert pushed_dirs.none? { |h| h[:path] ==
|
|
188
|
+
assert pushed_dirs.none? { |h| h[:path] == app.root.join("app/admin").to_s },
|
|
225
189
|
"Expected app/admin to NOT be pushed when files contain only top-level constants"
|
|
226
190
|
end
|
|
227
191
|
end
|
data/test/test_helper.rb
CHANGED
|
@@ -32,6 +32,17 @@ unless defined?(ActiveRecord::RecordNotFound)
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Same rationale as above: base_helper.rb's show-page/association rendering
|
|
36
|
+
# checks `record.is_a?(ActiveRecord::Base)` to distinguish a real AR
|
|
37
|
+
# association from a plain in-memory relation stand-in. Supply just the bare
|
|
38
|
+
# constant so that check can run (and return false for our PORO fixtures)
|
|
39
|
+
# without requiring the full active_record/railtie in this DB-free dummy app.
|
|
40
|
+
unless defined?(ActiveRecord::Base)
|
|
41
|
+
module ActiveRecord
|
|
42
|
+
class Base; end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
35
46
|
module TurboFrameTestHelper
|
|
36
47
|
def turbo_frame_tag(name, **options, &block)
|
|
37
48
|
content_tag(:turbo_frame, capture(&block), id: name, **options)
|
|
@@ -65,3 +76,69 @@ module ReadOnlyResourceFixtures
|
|
|
65
76
|
end
|
|
66
77
|
end
|
|
67
78
|
end
|
|
79
|
+
|
|
80
|
+
module RedirectFixtures
|
|
81
|
+
class Widget
|
|
82
|
+
extend ActiveModel::Naming
|
|
83
|
+
|
|
84
|
+
def persisted? = true
|
|
85
|
+
def to_param = "42"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
module Admin
|
|
90
|
+
module Resources
|
|
91
|
+
class RedirectWidgetResource < Admin::Base::Resource
|
|
92
|
+
model RedirectFixtures::Widget
|
|
93
|
+
portal :ops
|
|
94
|
+
section :observability
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
RedirectFixtures::WidgetResource = Admin::Resources::RedirectWidgetResource
|
|
99
|
+
|
|
100
|
+
# Fixtures for exercising execute_model_method's exception-handling chain
|
|
101
|
+
# end-to-end, in the dummy app's DB-free configuration (no ActiveRecord
|
|
102
|
+
# loaded), which is where a bare `rescue ActiveRecord::RecordInvalid` or
|
|
103
|
+
# `rescue AASM::InvalidTransition` would raise NameError while handling the
|
|
104
|
+
# original exception instead of reporting it.
|
|
105
|
+
module ExceptionHandlingFixtures
|
|
106
|
+
class Boomer
|
|
107
|
+
extend ActiveModel::Naming
|
|
108
|
+
|
|
109
|
+
def kaboom
|
|
110
|
+
raise "actual failure message"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Named to mirror AASM::InvalidTransition (and similar state-machine gem
|
|
115
|
+
# exceptions) without depending on AASM: only the class name matters to
|
|
116
|
+
# the rescue chain's class-name match.
|
|
117
|
+
class InvalidTransitionError < StandardError; end
|
|
118
|
+
|
|
119
|
+
class StateMachineWidget
|
|
120
|
+
extend ActiveModel::Naming
|
|
121
|
+
|
|
122
|
+
def transition
|
|
123
|
+
raise InvalidTransitionError, "cannot transition from draft to published"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
module Admin
|
|
129
|
+
module Resources
|
|
130
|
+
class ExceptionHandlingBoomerResource < Admin::Base::Resource
|
|
131
|
+
model ExceptionHandlingFixtures::Boomer
|
|
132
|
+
portal :ops
|
|
133
|
+
section :observability
|
|
134
|
+
actions { action :kaboom }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
class ExceptionHandlingStateMachineResource < Admin::Base::Resource
|
|
138
|
+
model ExceptionHandlingFixtures::StateMachineWidget
|
|
139
|
+
portal :ops
|
|
140
|
+
section :observability
|
|
141
|
+
actions { action :transition }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: admin_suite
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- TechWright Labs
|
|
@@ -126,6 +126,8 @@ files:
|
|
|
126
126
|
- app/assets/builds/admin_suite_tailwind.css
|
|
127
127
|
- app/assets/rouge.css
|
|
128
128
|
- app/assets/tailwind/admin_suite.css
|
|
129
|
+
- app/assets/vendor/easymde.min.css
|
|
130
|
+
- app/assets/vendor/easymde.min.js
|
|
129
131
|
- app/controllers/admin_suite/application_controller.rb
|
|
130
132
|
- app/controllers/admin_suite/dashboard_controller.rb
|
|
131
133
|
- app/controllers/admin_suite/docs_controller.rb
|
|
@@ -167,6 +169,7 @@ files:
|
|
|
167
169
|
- app/views/admin_suite/shared/_flash.html.erb
|
|
168
170
|
- app/views/admin_suite/shared/_form.html.erb
|
|
169
171
|
- app/views/admin_suite/shared/_json_editor_field.html.erb
|
|
172
|
+
- app/views/admin_suite/shared/_pagination.html.erb
|
|
170
173
|
- app/views/admin_suite/shared/_sidebar.html.erb
|
|
171
174
|
- app/views/admin_suite/shared/_toggle_cell.html.erb
|
|
172
175
|
- app/views/admin_suite/shared/_topbar.html.erb
|
|
@@ -183,10 +186,22 @@ files:
|
|
|
183
186
|
- lib/admin_suite/auth/http_basic.rb
|
|
184
187
|
- lib/admin_suite/auth/strategy.rb
|
|
185
188
|
- lib/admin_suite/configuration.rb
|
|
189
|
+
- lib/admin_suite/definition_loader.rb
|
|
190
|
+
- lib/admin_suite/deprecation.rb
|
|
186
191
|
- lib/admin_suite/engine.rb
|
|
192
|
+
- lib/admin_suite/host_autoload_policy.rb
|
|
193
|
+
- lib/admin_suite/legacy_custom_renderer_procs.rb
|
|
187
194
|
- lib/admin_suite/markdown_renderer.rb
|
|
188
195
|
- lib/admin_suite/portal_definition.rb
|
|
189
196
|
- lib/admin_suite/portal_registry.rb
|
|
197
|
+
- lib/admin_suite/renderer.rb
|
|
198
|
+
- lib/admin_suite/renderer_registry.rb
|
|
199
|
+
- lib/admin_suite/renderers/code_renderer.rb
|
|
200
|
+
- lib/admin_suite/renderers/json_renderer.rb
|
|
201
|
+
- lib/admin_suite/renderers/key_value_renderer.rb
|
|
202
|
+
- lib/admin_suite/renderers/legacy_gleania.rb
|
|
203
|
+
- lib/admin_suite/renderers/table_from_renderer.rb
|
|
204
|
+
- lib/admin_suite/section_definition.rb
|
|
190
205
|
- lib/admin_suite/theme_palette.rb
|
|
191
206
|
- lib/admin_suite/ui/dashboard_definition.rb
|
|
192
207
|
- lib/admin_suite/ui/field_renderer_registry.rb
|
|
@@ -249,14 +264,26 @@ files:
|
|
|
249
264
|
- test/integration/authorization_test.rb
|
|
250
265
|
- test/integration/dashboard_test.rb
|
|
251
266
|
- test/integration/docs_test.rb
|
|
267
|
+
- test/integration/layout_assets_test.rb
|
|
268
|
+
- test/integration/navigation_sections_test.rb
|
|
269
|
+
- test/integration/pagination_and_stats_test.rb
|
|
252
270
|
- test/integration/read_only_resource_test.rb
|
|
253
271
|
- test/integration/theme_test.rb
|
|
272
|
+
- test/lib/action_executor_redirect_test.rb
|
|
254
273
|
- test/lib/action_executor_test.rb
|
|
255
274
|
- test/lib/auth_http_basic_test.rb
|
|
256
275
|
- test/lib/auth_resolution_test.rb
|
|
257
276
|
- test/lib/auth_test.rb
|
|
277
|
+
- test/lib/builtin_renderers_test.rb
|
|
278
|
+
- test/lib/definition_loader_test.rb
|
|
279
|
+
- test/lib/engine_defaults_test.rb
|
|
280
|
+
- test/lib/form_field_renderer_test.rb
|
|
281
|
+
- test/lib/legacy_renderer_deprecation_test.rb
|
|
258
282
|
- test/lib/markdown_renderer_test.rb
|
|
283
|
+
- test/lib/renderer_test.rb
|
|
284
|
+
- test/lib/resource_exportable_deprecation_test.rb
|
|
259
285
|
- test/lib/resource_observability_extensions_test.rb
|
|
286
|
+
- test/lib/show_value_formatter_test.rb
|
|
260
287
|
- test/lib/theme_palette_test.rb
|
|
261
288
|
- test/lib/zeitwerk_integration_test.rb
|
|
262
289
|
- test/test_helper.rb
|