admin_suite 0.2.7 → 0.2.9

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.
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module AdminSuite
6
+ class ResourcesControllerTest < ActiveSupport::TestCase
7
+ class TestController < ResourcesController
8
+ attr_writer :test_resource_config
9
+ attr_reader :filter_calls, :paginated_scope
10
+
11
+ def initialize
12
+ super
13
+ @filter_calls = 0
14
+ end
15
+
16
+ private
17
+
18
+ def resource_config
19
+ @test_resource_config
20
+ end
21
+
22
+ def filtered_collection
23
+ @filter_calls += 1
24
+ { total: 37 }
25
+ end
26
+
27
+ def paginate_collection(scope)
28
+ @paginated_scope = scope
29
+ [ Object.new, :paginated ]
30
+ end
31
+ end
32
+
33
+ class StatsResource < Admin::Base::Resource
34
+ index do
35
+ stats do
36
+ stat :legacy, -> { 11 }
37
+ stat :filtered, ->(scope) { scope.fetch(:total) }
38
+ end
39
+ end
40
+ end
41
+
42
+ class BrokenStatsResource < Admin::Base::Resource
43
+ index do
44
+ stats do
45
+ stat :broken, ->(_scope) { raise "boom" }
46
+ end
47
+ end
48
+ end
49
+
50
+ test "stats preserve zero arity calculators and pass the filtered scope to one arity calculators" do
51
+ controller = TestController.new
52
+ controller.test_resource_config = StatsResource
53
+ scope = { total: 37 }
54
+
55
+ stats = controller.send(:calculate_stats, scope)
56
+
57
+ assert_equal 11, stats.first[:value]
58
+ assert_equal 37, stats.second[:value]
59
+ end
60
+
61
+ test "stats preserve the existing calculator rescue behavior" do
62
+ controller = TestController.new
63
+ controller.test_resource_config = BrokenStatsResource
64
+
65
+ assert_equal "N/A", controller.send(:calculate_stats, Object.new).first[:value]
66
+ end
67
+
68
+ test "index reuses one filtered unpaginated scope for stats and pagination" do
69
+ controller = TestController.new
70
+ controller.test_resource_config = StatsResource
71
+
72
+ controller.index
73
+
74
+ assert_equal 1, controller.filter_calls
75
+ assert_equal({ total: 37 }, controller.paginated_scope)
76
+ assert_equal 37, controller.instance_variable_get(:@stats).second[:value]
77
+ assert_equal :paginated, controller.instance_variable_get(:@collection)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ # The dummy app is intentionally database-free, while the generic controller
6
+ # supports Active Record hosts. Supply only the exception type its lookup path
7
+ # rescues so show-page behavior can be exercised with an in-memory fixture.
8
+ unless defined?(ActiveRecord::RecordNotFound)
9
+ module ActiveRecord
10
+ class RecordNotFound < StandardError; end
11
+ end
12
+ end
13
+
14
+ module TurboFrameTestHelper
15
+ def turbo_frame_tag(name, **options, &block)
16
+ content_tag(:turbo_frame, capture(&block), id: name, **options)
17
+ end
18
+ end
19
+
20
+ ActionView::Base.include(TurboFrameTestHelper)
21
+
22
+ module ReadOnlyResourceFixtures
23
+ class Relation
24
+ include Enumerable
25
+
26
+ def initialize(records)
27
+ @records = records
28
+ end
29
+
30
+ def each(&block)
31
+ @records.each(&block)
32
+ end
33
+
34
+ def count(*)
35
+ @records.count
36
+ end
37
+
38
+ def offset(*)
39
+ self
40
+ end
41
+
42
+ def limit(*)
43
+ self
44
+ end
45
+ end
46
+
47
+ class Widget
48
+ extend ActiveModel::Naming
49
+
50
+ attr_reader :id, :name
51
+
52
+ def initialize(id: 1, name: "Observed widget")
53
+ @id = id
54
+ @name = name
55
+ end
56
+
57
+ def self.all
58
+ Relation.new([ new ])
59
+ end
60
+
61
+ def self.column_names
62
+ %w[id name]
63
+ end
64
+
65
+ def self.primary_key
66
+ "id"
67
+ end
68
+
69
+ def self.columns_hash
70
+ { "id" => Struct.new(:type).new(:integer) }
71
+ end
72
+
73
+ def self.find(id)
74
+ raise ActiveRecord::RecordNotFound unless id.to_s == "1"
75
+
76
+ new
77
+ end
78
+
79
+ def to_param
80
+ id.to_s
81
+ end
82
+
83
+ def attributes
84
+ { "id" => id, "name" => name }
85
+ end
86
+ end
87
+ end
88
+
89
+ module Admin
90
+ module Resources
91
+ class ReadOnlyWidgetResource < Admin::Base::Resource
92
+ model ReadOnlyResourceFixtures::Widget
93
+ portal :ops
94
+ section :observability
95
+ read_only
96
+
97
+ index do
98
+ columns do
99
+ column :name
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ module AdminSuite
107
+ class ReadOnlyResourceTest < ActionDispatch::IntegrationTest
108
+ BASE_PATH = "/internal/admin_suite/ops/read_only_widgets"
109
+
110
+ test "direct built in mutation endpoints are rejected" do
111
+ get "#{BASE_PATH}/new"
112
+ assert_response :not_found
113
+
114
+ post BASE_PATH, params: { read_only_resource_fixtures_widget: { name: "changed" } }
115
+ assert_response :not_found
116
+
117
+ get "#{BASE_PATH}/1/edit"
118
+ assert_response :not_found
119
+
120
+ patch "#{BASE_PATH}/1", params: { read_only_resource_fixtures_widget: { name: "changed" } }
121
+ assert_response :not_found
122
+
123
+ delete "#{BASE_PATH}/1"
124
+ assert_response :not_found
125
+ end
126
+
127
+ test "index hides create and edit controls" do
128
+ get BASE_PATH
129
+
130
+ assert_response :success
131
+ assert_includes response.body, "Observed widget"
132
+ refute_includes response.body, "New Widget"
133
+ refute_match(/>\s*Edit\s*</, response.body)
134
+ end
135
+
136
+ test "show mutation controls are conditional on write access" do
137
+ template = AdminSuite::Engine.root.join("app/views/admin_suite/resources/show.html.erb").read
138
+
139
+ assert_includes template, "has_edit_route = !resource_config.read_only?"
140
+ assert_includes template, "has_destroy_route = !resource_config.read_only?"
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module AdminSuite
6
+ class ResourceObservabilityExtensionsTest < ActiveSupport::TestCase
7
+ FakeScope = Struct.new(:filters) do
8
+ def where(*arguments)
9
+ self.class.new(filters + [ arguments ])
10
+ end
11
+ end
12
+
13
+ class FilteredResource < Admin::Base::Resource
14
+ index do
15
+ filters do
16
+ filter :window, type: :select, default: "24h",
17
+ apply: ->(scope, value) { scope.where(window: value) }
18
+ filter :status, type: :select
19
+ end
20
+ end
21
+ end
22
+
23
+ class ReadOnlyResource < Admin::Base::Resource
24
+ read_only
25
+ end
26
+
27
+ test "resources are writable by default and may be declared read only" do
28
+ refute Admin::Base::Resource.read_only?
29
+ assert ReadOnlyResource.read_only?
30
+ end
31
+
32
+ test "filter defaults apply when the parameter is absent" do
33
+ scope = Admin::Base::FilterBuilder.new(FilteredResource, ActionController::Parameters.new)
34
+ .apply(FakeScope.new([]))
35
+
36
+ assert_equal [ [ { window: "24h" } ] ], scope.filters
37
+ end
38
+
39
+ test "filter defaults apply when the parameter is blank and compose with explicit filters" do
40
+ params = ActionController::Parameters.new(window: "", status: "failed")
41
+ scope = Admin::Base::FilterBuilder.new(FilteredResource, params).apply(FakeScope.new([]))
42
+
43
+ assert_equal [ [ { window: "24h" } ], [ { status: "failed" } ] ], scope.filters
44
+ end
45
+
46
+ test "an explicit filter overrides its default" do
47
+ params = ActionController::Parameters.new(window: "7d")
48
+ scope = Admin::Base::FilterBuilder.new(FilteredResource, params).apply(FakeScope.new([]))
49
+
50
+ assert_equal [ [ { window: "7d" } ] ], scope.filters
51
+ end
52
+ end
53
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: admin_suite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
4
+ version: 0.2.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - TechWright Labs
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-28 00:00:00.000000000 Z
11
+ date: 2026-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -136,9 +136,11 @@ files:
136
136
  - app/helpers/admin_suite/panels_helper.rb
137
137
  - app/helpers/admin_suite/resources_helper.rb
138
138
  - app/helpers/admin_suite/theme_helper.rb
139
+ - app/javascript/admin_suite_application.js
139
140
  - app/javascript/controllers/admin_suite/click_actions_controller.js
140
141
  - app/javascript/controllers/admin_suite/clipboard_controller.js
141
142
  - app/javascript/controllers/admin_suite/code_editor_controller.js
143
+ - app/javascript/controllers/admin_suite/dependent_searchable_select_controller.js
142
144
  - app/javascript/controllers/admin_suite/file_upload_controller.js
143
145
  - app/javascript/controllers/admin_suite/flash_controller.js
144
146
  - app/javascript/controllers/admin_suite/json_editor_controller.js
@@ -171,18 +173,6 @@ files:
171
173
  - app/views/layouts/admin_suite/application.html.erb
172
174
  - config/importmap.rb
173
175
  - config/routes.rb
174
- - docs/README.md
175
- - docs/actions.md
176
- - docs/configuration.md
177
- - docs/development.md
178
- - docs/docs_viewer.md
179
- - docs/fields.md
180
- - docs/installation.md
181
- - docs/portals.md
182
- - docs/releasing.md
183
- - docs/resources.md
184
- - docs/theming.md
185
- - docs/troubleshooting.md
186
176
  - lib/admin/base/action_executor.rb
187
177
  - lib/admin/base/action_handler.rb
188
178
  - lib/admin/base/filter_builder.rb
@@ -207,6 +197,7 @@ files:
207
197
  - lib/generators/admin_suite/scaffold/scaffold_generator.rb
208
198
  - lib/tasks/admin_suite_tailwind.rake
209
199
  - lib/tasks/admin_suite_test.rake
200
+ - test/controllers/resources_controller_test.rb
210
201
  - test/dummy/Gemfile
211
202
  - test/dummy/README.md
212
203
  - test/dummy/Rakefile
@@ -251,9 +242,11 @@ files:
251
242
  - test/fixtures/docs/progress/PROGRESS_REPORT.md
252
243
  - test/integration/dashboard_test.rb
253
244
  - test/integration/docs_test.rb
245
+ - test/integration/read_only_resource_test.rb
254
246
  - test/integration/theme_test.rb
255
247
  - test/lib/action_executor_test.rb
256
248
  - test/lib/markdown_renderer_test.rb
249
+ - test/lib/resource_observability_extensions_test.rb
257
250
  - test/lib/theme_palette_test.rb
258
251
  - test/lib/zeitwerk_integration_test.rb
259
252
  - test/test_helper.rb
data/docs/README.md DELETED
@@ -1,26 +0,0 @@
1
- # AdminSuite Documentation
2
-
3
- AdminSuite is a mountable Rails engine that provides:
4
-
5
- - A **resource DSL** for CRUD + search/sort/filter + show/form configuration
6
- - A **portal system** (navigation + optional portal dashboards)
7
- - A built-in **docs viewer** (renders Markdown from your host app filesystem)
8
- - A small baseline **UI layer** (Tailwind optional)
9
-
10
- ## Getting started
11
-
12
- - [Installation](installation.md)
13
- - [Configuration](configuration.md)
14
- - [Portals & dashboards](portals.md)
15
- - [Resources](resources.md)
16
- - [Fields](fields.md)
17
- - [Actions](actions.md)
18
- - [Theming & assets](theming.md)
19
- - [Docs viewer](docs_viewer.md)
20
- - [Troubleshooting](troubleshooting.md)
21
-
22
- ## Contributing / maintainers
23
-
24
- - [Development](development.md)
25
- - [Releasing](releasing.md)
26
-
data/docs/actions.md DELETED
@@ -1,98 +0,0 @@
1
- # Actions
2
-
3
- AdminSuite supports three action “shapes” in the resource DSL:
4
-
5
- - `action` (member action on a single record)
6
- - `bulk_action` (runs across selected records)
7
- - `collection_action` (runs on a scope / collection)
8
-
9
- ## Defining actions
10
-
11
- ```ruby
12
- actions do
13
- action :reindex, label: "Reindex", method: :post, confirm: "Reindex this record?"
14
- bulk_action :archive, label: "Archive", confirm: "Archive selected records?"
15
- end
16
- ```
17
-
18
- Supported action options:
19
-
20
- - `method:` HTTP method (default `:post`)
21
- - `label:` button label (default is humanized action name)
22
- - `icon:` lucide icon name (optional)
23
- - `color:` (optional)
24
- - `confirm:` string confirmation (optional)
25
- - `type:` reserved (default `:button`)
26
- - `if:` Proc condition (member actions only)
27
- - `unless:` Proc condition (member actions only)
28
-
29
- ## How actions execute
30
-
31
- When you trigger an action, AdminSuite resolves behavior in this order:
32
-
33
- 1. **Model method**: if the target responds to `action_name`, it calls that method.
34
- 2. **Bang model method**: else if it responds to `action_name!`, it calls that.
35
- 3. **Action handler class**: else it tries to find a handler class.
36
-
37
- ### Handler class naming convention
38
-
39
- By default, AdminSuite looks for:
40
-
41
- ```ruby
42
- Admin::Actions::<ResourceName><ActionName>Action
43
- ```
44
-
45
- Example for `UserResource` + `:reset_password`:
46
-
47
- ```ruby
48
- Admin::Actions::UserResetPasswordAction
49
- ```
50
-
51
- Handlers should inherit from `Admin::Base::ActionHandler`:
52
-
53
- ```ruby
54
- # app/admin/actions/user_reset_password_action.rb
55
- module Admin
56
- module Actions
57
- class UserResetPasswordAction < Admin::Base::ActionHandler
58
- def call
59
- # record is available as `record`, actor as `actor`, request params as `params`
60
- record.send_reset_password_instructions!
61
- success "Reset email sent."
62
- rescue StandardError => e
63
- failure "Failed to send reset: #{e.message}"
64
- end
65
- end
66
- end
67
- end
68
- ```
69
-
70
- ## Overriding handler resolution (`config.resolve_action_handler`)
71
-
72
- If your app doesn’t want to follow the default naming convention, you can provide a resolver:
73
-
74
- ```ruby
75
- AdminSuite.configure do |config|
76
- config.resolve_action_handler = ->(resource_class, action_name) do
77
- # return a Class or nil
78
- if resource_class.name == "Admin::Resources::UserResource" && action_name.to_sym == :reset_password
79
- Admin::Actions::UserResetPasswordAction
80
- end
81
- end
82
- end
83
- ```
84
-
85
- ## Auditing hook (`config.on_action_executed`)
86
-
87
- You can record or log all actions after they run:
88
-
89
- ```ruby
90
- AdminSuite.configure do |config|
91
- config.on_action_executed = ->(actor:, action_name:, resource_class:, subject:, params:, result:) do
92
- Rails.logger.info(
93
- "[admin_suite] actor=#{actor&.id} action=#{resource_class.name}##{action_name} success=#{result.success?}"
94
- )
95
- end
96
- end
97
- ```
98
-