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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +88 -0
  3. data/app/assets/vendor/easymde.min.css +7 -0
  4. data/app/assets/vendor/easymde.min.js +7 -0
  5. data/app/controllers/admin_suite/application_controller.rb +26 -47
  6. data/app/controllers/admin_suite/portals_controller.rb +2 -2
  7. data/app/controllers/admin_suite/resources_controller.rb +8 -1
  8. data/app/helpers/admin_suite/base_helper.rb +49 -385
  9. data/app/javascript/controllers/admin_suite/markdown_editor_controller.js +21 -2
  10. data/app/views/admin_suite/panels/_chart.html.erb +3 -2
  11. data/app/views/admin_suite/panels/_stat.html.erb +10 -0
  12. data/app/views/admin_suite/resources/index.html.erb +7 -77
  13. data/app/views/admin_suite/shared/_pagination.html.erb +74 -0
  14. data/app/views/admin_suite/shared/_sidebar.html.erb +1 -1
  15. data/app/views/layouts/admin_suite/application.html.erb +5 -3
  16. data/lib/admin/base/action_executor.rb +19 -51
  17. data/lib/admin/base/resource.rb +53 -14
  18. data/lib/admin_suite/configuration.rb +28 -3
  19. data/lib/admin_suite/definition_loader.rb +194 -0
  20. data/lib/admin_suite/deprecation.rb +48 -0
  21. data/lib/admin_suite/engine.rb +55 -76
  22. data/lib/admin_suite/host_autoload_policy.rb +132 -0
  23. data/lib/admin_suite/legacy_custom_renderer_procs.rb +29 -0
  24. data/lib/admin_suite/portal_definition.rb +11 -0
  25. data/lib/admin_suite/renderer.rb +133 -0
  26. data/lib/admin_suite/renderer_registry.rb +70 -0
  27. data/lib/admin_suite/renderers/code_renderer.rb +15 -0
  28. data/lib/admin_suite/renderers/json_renderer.rb +15 -0
  29. data/lib/admin_suite/renderers/key_value_renderer.rb +39 -0
  30. data/lib/admin_suite/renderers/legacy_gleania.rb +230 -0
  31. data/lib/admin_suite/renderers/table_from_renderer.rb +22 -0
  32. data/lib/admin_suite/section_definition.rb +42 -0
  33. data/lib/admin_suite/ui/field_renderer_registry.rb +31 -4
  34. data/lib/admin_suite/ui/form_field_renderer.rb +1 -7
  35. data/lib/admin_suite/ui/show_formatter_registry.rb +9 -0
  36. data/lib/admin_suite/ui/show_value_formatter.rb +7 -3
  37. data/lib/admin_suite/version.rb +1 -1
  38. data/lib/admin_suite.rb +37 -0
  39. data/lib/generators/admin_suite/install/templates/admin_suite.rb +0 -4
  40. data/test/controllers/resources_controller_test.rb +76 -1
  41. data/test/integration/dashboard_test.rb +40 -0
  42. data/test/integration/layout_assets_test.rb +112 -0
  43. data/test/integration/navigation_sections_test.rb +41 -0
  44. data/test/integration/pagination_and_stats_test.rb +152 -0
  45. data/test/lib/action_executor_redirect_test.rb +41 -0
  46. data/test/lib/builtin_renderers_test.rb +202 -0
  47. data/test/lib/definition_loader_test.rb +264 -0
  48. data/test/lib/engine_defaults_test.rb +39 -0
  49. data/test/lib/form_field_renderer_test.rb +64 -0
  50. data/test/lib/legacy_renderer_deprecation_test.rb +35 -0
  51. data/test/lib/renderer_test.rb +221 -0
  52. data/test/lib/resource_exportable_deprecation_test.rb +39 -0
  53. data/test/lib/show_value_formatter_test.rb +88 -0
  54. data/test/lib/zeitwerk_integration_test.rb +28 -64
  55. data/test/test_helper.rb +77 -0
  56. 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, "&quot;a&quot;"
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
- # Helper method that simulates the Zeitwerk integration logic from engine.rb
34
- def simulate_zeitwerk_integration(app_root, loader)
35
- admin_suite_app_dir = app_root.join("app/admin_suite")
36
- admin_dir = app_root.join("app/admin")
37
- admin_portals_dir = app_root.join("app/admin/portals")
38
-
39
- # Map app/admin -> Admin namespace if files define Admin::* constants.
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
- # Create loader and simulate initializer logic
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
- simulate_zeitwerk_integration(app_root, loader)
53
+ AdminSuite::HostAutoloadPolicy.apply!(app, loaders: [ loader ])
85
54
 
86
55
  # Verify that app/admin/portals was ignored
87
- expected_path = app_root.join("app/admin/portals").to_s
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
- # Create loader and simulate initializer logic
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
- simulate_zeitwerk_integration(app_root, loader)
72
+ AdminSuite::HostAutoloadPolicy.apply!(app, loaders: [ loader ])
105
73
 
106
74
  # Verify that app/admin/portals was NOT ignored
107
- unexpected_path = app_root.join("app/admin/portals").to_s
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
- # Create loader and simulate initializer logic
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
- simulate_zeitwerk_integration(app_root, loader)
91
+ AdminSuite::HostAutoloadPolicy.apply!(app, loaders: [ loader ])
125
92
 
126
93
  # Verify that app/admin_suite was ignored
127
- expected_path = app_root.join("app/admin_suite").to_s
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
- # Create loader and simulate initializer logic
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
- simulate_zeitwerk_integration(app_root, loader)
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 = app_root.join("app/admin/portals").to_s
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
- # Create loader
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
- # Simulate the initializer logic
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 = app_root.join("app/admin/portals").to_s
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
- app_root = Pathname.new(@temp_dir)
168
+ app = create_fake_app(@temp_dir)
205
169
  loader, _ignored_dirs, pushed_dirs = create_tracked_loader
206
- simulate_zeitwerk_integration(app_root, loader)
170
+ AdminSuite::HostAutoloadPolicy.apply!(app, loaders: [ loader ])
207
171
 
208
- assert pushed_dirs.any? { |h| h[:path] == app_root.join("app/admin").to_s && h[:namespace] == Admin },
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
- app_root = Pathname.new(@temp_dir)
184
+ app = create_fake_app(@temp_dir)
221
185
  loader, _ignored_dirs, pushed_dirs = create_tracked_loader
222
- simulate_zeitwerk_integration(app_root, loader)
186
+ AdminSuite::HostAutoloadPolicy.apply!(app, loaders: [ loader ])
223
187
 
224
- assert pushed_dirs.none? { |h| h[:path] == app_root.join("app/admin").to_s },
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.3.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