admin_suite 0.1.2 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbd2bda3cff05667e7f164b70d06c7f91e4ba7cde4dfe720291720fe589cfde6
4
- data.tar.gz: c2fea65bd3721677eee67904174664038c9e256d347770775bbfc3a3714bceb2
3
+ metadata.gz: 8a3f83815ccce6bcb0e0a38e07e0b90d6d969309b2e3302e1870135f02793762
4
+ data.tar.gz: 2564e620eaa8c66030d8f774b1d4246eaa4359955ce2a5670f62f069a88a58af
5
5
  SHA512:
6
- metadata.gz: 68580753a81e6e77b5a3cda22038f5d62919ab22e3bb00746504e18578847119528872f0bf94960c6d7d0df3ceb06a7df723ae872e8cce7d6d240fc0a5c08ab6
7
- data.tar.gz: b342407db536c0cd5f7264aff7ea6fa57f224b03b66d2675b1b928394490ee743424ea7608ff9845e71e391765ecac7da0823e101b1922cea9c8613dae44c48a
6
+ metadata.gz: '071608f05dc059b788b90350e9cfbd590d417bf124f3327dbf151202d32f2c71359f3c6ba442e13fba7138d8fc7b28d96e52687825bf64beb66c4058dd640eb9'
7
+ data.tar.gz: ac7a393a423342b228c9489492d361e34f98afaaf9e9527a1a71639ecc3d0ee7ce59ddfae3988d1d83ccac474a69764ce7b7d7afed0e346026642bd9c69bb3f2
data/.gitignore CHANGED
@@ -8,4 +8,5 @@
8
8
  !/test/dummy/log/.keep
9
9
  /test/dummy/tmp/
10
10
  /test/dummy/storage/
11
- admin_suite-*.gem
11
+ admin_suite-*.gem
12
+ /.bundle/
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,51 @@
1
+ # Contributing to AdminSuite
2
+
3
+ Thank you for your interest in contributing to AdminSuite!
4
+
5
+ ## Getting Started
6
+
7
+ 1. Fork the repository
8
+ 2. Clone your fork locally
9
+ 3. Install dependencies: `bundle install`
10
+ 4. Run the test suite: `bundle exec rake test`
11
+ 5. (Optional) Run tests with coverage: `COVERAGE=true bundle exec rake test`
12
+
13
+ ## Development
14
+
15
+ See `docs/development.md` for detailed information on:
16
+ - Setting up your development environment
17
+ - Running tests
18
+ - Code style guidelines
19
+
20
+ ## Pull Requests
21
+
22
+ 1. Create a feature branch from `main`
23
+ 2. Make your changes
24
+ 3. Add or update tests as needed
25
+ 4. Ensure all tests pass: `bundle exec rake test`
26
+ 5. Push your branch and create a pull request
27
+
28
+ ### CI Checks
29
+
30
+ All pull requests must pass the following checks before merging:
31
+ - **Tests**: Automated test suite runs on Ruby 3.2 and 3.3
32
+ - **Coverage**: Code coverage is automatically generated and uploaded to Codecov
33
+ - **Code Review**: At least one maintainer approval required
34
+
35
+ The CI workflow runs automatically on every pull request.
36
+
37
+ ## Releasing
38
+
39
+ See `docs/releasing.md` for information on how releases are managed.
40
+
41
+ Releases are automated via GitHub Actions when changes are merged to `main` with a version bump.
42
+
43
+ ### Required Secrets for Maintainers
44
+
45
+ The repository requires the following secrets to be configured:
46
+ - **`RUBYGEMS_API_KEY`**: Required for automated gem publishing to RubyGems
47
+ - **`CODECOV_TOKEN`**: Optional, for uploading code coverage reports to Codecov (workflow continues without it)
48
+
49
+ ## Questions?
50
+
51
+ Feel free to open an issue for questions or discussion.
@@ -35,12 +35,11 @@ module AdminSuite
35
35
  nil
36
36
  end
37
37
 
38
- # Loads resource definition files in development when needed.
38
+ # Loads resource definition files when needed (runs in all environments).
39
39
  #
40
40
  # @return [void]
41
41
  def ensure_resources_loaded!
42
42
  require "admin/base/resource" unless defined?(Admin::Base::Resource)
43
- return unless Rails.env.development?
44
43
  return if Admin::Base::Resource.registered_resources.any?
45
44
 
46
45
  Array(AdminSuite.config.resource_globs).flat_map { |g| Dir[g] }.uniq.each do |file|
@@ -32,10 +32,27 @@ These are the defaults in `AdminSuite::Configuration` / `AdminSuite::Engine`:
32
32
  - `resource_globs`: defaults to:
33
33
  - `Rails.root/config/admin_suite/resources/*.rb`
34
34
  - `Rails.root/app/admin/resources/*.rb`
35
+ - `action_globs`: defaults to:
36
+ - `Rails.root/config/admin_suite/actions/*.rb`
37
+ - `Rails.root/app/admin/actions/*.rb`
35
38
  - `portal_globs`: defaults to:
36
39
  - `Rails.root/config/admin_suite/portals/*.rb`
37
40
  - `Rails.root/app/admin/portals/*.rb`
38
41
  - `Rails.root/app/admin_suite/portals/*.rb`
42
+
43
+ Note: AdminSuite definition files (resources, actions, portals) often don't follow
44
+ Zeitwerk's path-to-constant naming conventions. To prevent eager-load `Zeitwerk::NameError`s
45
+ in production, the engine only configures Zeitwerk to ignore these directories and load them via globs instead:
46
+ - `app/admin_suite`
47
+ - `app/admin/portals` (when portal DSL usage is detected)
48
+
49
+ Other `app/admin/*` directories (such as `app/admin/resources`, `app/admin/actions`, and `app/admin/base`) are
50
+ not ignored by default and may be treated as normal Zeitwerk autoload paths if they are added to the loader
51
+ (for example, via `loader.push_dir("app/admin")` in the host app). Do not rely on these directories being
52
+ ignored for autoloading; instead, keep files there Zeitwerk-compatible.
53
+
54
+ We recommend placing non-Zeitwerk-compatible definition files under `config/admin_suite/*` or `app/admin_suite/*`
55
+ for clearer separation from standard Rails autoloading.
39
56
  - `portals`: default portal metadata for `:ops`, `:email`, `:ai`, `:assistant`
40
57
  - `custom_renderers`: `{}`
41
58
  - `icon_renderer`: `nil` (uses lucide-rails by default when available)
@@ -89,6 +106,20 @@ config.resource_globs = [
89
106
  ]
90
107
  ```
91
108
 
109
+ ### `action_globs`
110
+
111
+ Where AdminSuite should load action handler files from (files that define custom action handlers, typically subclasses of `Admin::Base::ActionHandler`).
112
+
113
+ - **Type**: `Array<String>`
114
+
115
+ Example:
116
+
117
+ ```ruby
118
+ config.action_globs = [
119
+ Rails.root.join("app/admin/actions/**/*.rb").to_s
120
+ ]
121
+ ```
122
+
92
123
  ### `portal_globs`
93
124
 
94
125
  Where AdminSuite should load portal definition files from (files typically call `AdminSuite.portal(:key) { ... }`).
data/docs/releasing.md CHANGED
@@ -2,15 +2,39 @@
2
2
 
3
3
  This page is intended for maintainers publishing `admin_suite` to RubyGems.
4
4
 
5
- ## Checklist
5
+ ## Automated Release Process (Recommended)
6
6
 
7
- 1. Bump the version
7
+ The gem is automatically published to RubyGems when changes are merged to `main`, provided the version has been bumped.
8
8
 
9
- - Update `lib/admin_suite/version.rb`
9
+ ### Steps
10
10
 
11
- 2. Update changelog
11
+ 1. **Bump the version**
12
+ - Update `lib/admin_suite/version.rb`
13
+
14
+ 2. **Update changelog**
15
+ - Add an entry to `CHANGELOG.md`
16
+
17
+ 3. **Create a PR and get it merged**
18
+ - The CI workflow will run tests automatically on the PR
19
+ - Once merged to `main`, after CI passes, the publish workflow will:
20
+ - Check if the version already exists on RubyGems
21
+ - Build and publish the gem (if it's a new version)
22
+ - Create a Git tag for the release (if it doesn't already exist)
23
+
24
+ ### Requirements
25
+
26
+ - The `RUBYGEMS_API_KEY` secret must be configured in the repository settings
27
+ - The version in `lib/admin_suite/version.rb` must be unique (not already published)
12
28
 
13
- - Add an entry to `CHANGELOG.md`
29
+ ## Manual Release Process
30
+
31
+ If you need to publish manually:
32
+
33
+ 1. Bump the version
34
+ - Update `lib/admin_suite/version.rb`
35
+
36
+ 2. Update changelog
37
+ - Add an entry to `CHANGELOG.md`
14
38
 
15
39
  3. Run tests and build the gem
16
40
 
@@ -19,7 +43,7 @@ bundle exec rake test
19
43
  gem build admin_suite.gemspec
20
44
  ```
21
45
 
22
- 4. (Recommended) Tag the release
46
+ 4. Tag the release
23
47
 
24
48
  ```bash
25
49
  git tag -a "vX.Y.Z" -m "AdminSuite vX.Y.Z"
@@ -32,7 +56,10 @@ git push --tags
32
56
  gem push "admin_suite-X.Y.Z.gem"
33
57
  ```
34
58
 
35
- Notes:
59
+ ## Notes
36
60
 
37
- - RubyGems commonly requires MFA/OTP for pushes (this gem is configured with `rubygems_mfa_required`).
61
+ - RubyGems commonly requires MFA/OTP for pushes (this gem is configured with `rubygems_mfa_required`)
62
+ - The automated workflow uses a GitHub Actions bot to push tags
63
+ - The publish workflow only runs after the CI workflow completes successfully
64
+ - You can manually trigger the publish workflow from the GitHub Actions tab if needed
38
65
 
@@ -12,6 +12,13 @@ module Admin
12
12
  def failure? = !success
13
13
  end
14
14
 
15
+ # Track whether action handlers have been loaded to avoid repeated expensive globs
16
+ @handlers_loaded = false
17
+
18
+ class << self
19
+ attr_accessor :handlers_loaded
20
+ end
21
+
15
22
  def initialize(resource_class, action_name, actor)
16
23
  @resource_class = resource_class
17
24
  @action_name = action_name
@@ -116,6 +123,17 @@ module Admin
116
123
  return resolved if resolved
117
124
  end
118
125
 
126
+ handler_name = "#{resource_class.resource_name.camelize}#{action.name.to_s.camelize}Action"
127
+ handler_constant = "Admin::Actions::#{handler_name}"
128
+ handler_constant.constantize
129
+ rescue NameError
130
+ # In many host apps, action handlers live under `app/admin/actions/**`.
131
+ # Rails treats `app/admin` as a Zeitwerk root, which means Zeitwerk expects
132
+ # top-level constants (e.g. `Actions::Foo`) unless the host configures
133
+ # a namespace mapping. AdminSuite avoids requiring host Zeitwerk setup by
134
+ # loading handler files via `AdminSuite.config.action_globs` when needed.
135
+ load_action_handlers_for_admin_suite!
136
+
119
137
  handler_name = "#{resource_class.resource_name.camelize}#{action.name.to_s.camelize}Action"
120
138
  "Admin::Actions::#{handler_name}".constantize
121
139
  rescue NameError
@@ -150,6 +168,57 @@ module Admin
150
168
  rescue StandardError
151
169
  nil
152
170
  end
171
+
172
+ def load_action_handlers_for_admin_suite!
173
+ return unless defined?(AdminSuite)
174
+
175
+ # Track whether we've already loaded handlers to avoid expensive repeated globs.
176
+ # In development, this flag is reset by the Rails reloader (see engine.rb).
177
+ # In production/test, it persists for the process lifetime.
178
+ return if self.class.handlers_loaded
179
+
180
+ files = Array(AdminSuite.config.action_globs).flat_map { |g| Dir[g] }.uniq
181
+
182
+ # Set the flag even if no files found - we've done the glob and shouldn't repeat it
183
+ if files.empty?
184
+ self.class.handlers_loaded = true
185
+ return
186
+ end
187
+
188
+ files.each do |file|
189
+ begin
190
+ if Rails.env.development?
191
+ load file
192
+ else
193
+ require file
194
+ end
195
+ rescue StandardError, ScriptError => e
196
+ log_action_handler_load_error(file, e)
197
+
198
+ # Fail fast in dev/test so broken handler files are immediately discoverable.
199
+ raise if Rails.env.development? || Rails.env.test?
200
+ end
201
+ end
202
+
203
+ # We attempted to load the configured handlers. Avoid repeating expensive globs
204
+ # and file loads for the rest of the process lifetime.
205
+ self.class.handlers_loaded = true
206
+ end
207
+
208
+ def log_action_handler_load_error(file, error)
209
+ message = "[AdminSuite] Failed to load action handler file #{file}: #{error.class}: #{error.message}"
210
+
211
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
212
+ Rails.logger.error(message)
213
+
214
+ backtrace = Array(error.backtrace).take(20).join("\n")
215
+ Rails.logger.error(backtrace) unless backtrace.empty?
216
+ else
217
+ warn(message)
218
+ end
219
+ rescue StandardError
220
+ nil
221
+ end
153
222
  end
154
223
  end
155
224
  end
@@ -7,6 +7,7 @@ module AdminSuite
7
7
  :current_actor,
8
8
  :authorize,
9
9
  :resource_globs,
10
+ :action_globs,
10
11
  :portal_globs,
11
12
  :portals,
12
13
  :custom_renderers,
@@ -25,6 +26,7 @@ module AdminSuite
25
26
  @current_actor = nil
26
27
  @authorize = nil
27
28
  @resource_globs = []
29
+ @action_globs = []
28
30
  @portal_globs = []
29
31
  @portals = {}
30
32
  @custom_renderers = {}
@@ -13,15 +13,65 @@ module AdminSuite
13
13
  end
14
14
  end
15
15
 
16
- initializer "admin_suite.host_dsl_ignore" do
17
- # Host apps sometimes store AdminSuite DSL files under `app/admin_suite/**`.
18
- # Those are not constant definitions, so we must prevent Zeitwerk from
19
- # expecting constants like `Portals::Ai` during eager load.
20
- host_dsl_dir = Rails.root.join("app/admin_suite")
21
- next unless host_dsl_dir.exist?
16
+ initializer "admin_suite.host_dsl_ignore", before: :setup_main_autoloader do |app|
17
+ # Host apps may store AdminSuite DSL files under `app/admin_suite/**` and
18
+ # `app/admin/portals/**`.
19
+ #
20
+ # These are side-effect DSL files (they do not define constants), so Zeitwerk
21
+ # must ignore them to avoid eager-load `Zeitwerk::NameError`s in production.
22
+
23
+ admin_suite_app_dir = Rails.root.join("app/admin_suite")
24
+ admin_dir = Rails.root.join("app/admin")
25
+ admin_portals_dir = Rails.root.join("app/admin/portals")
26
+
27
+ # If the host uses `Admin::*` constants inside `app/admin/**`, Rails' default
28
+ # autoload root (`app/admin`) would expect top-level constants like
29
+ # `Resources::UserResource`. We fix that by mapping `app/admin` to `Admin`.
30
+ # This avoids requiring host apps to add their own Zeitwerk initializer.
31
+ if admin_dir.exist? && self.class.host_admin_namespace_files?(admin_dir)
32
+ admin_dir_s = admin_dir.to_s
33
+ app.config.autoload_paths.delete(admin_dir_s)
34
+ app.config.eager_load_paths.delete(admin_dir_s)
35
+
36
+ # Ensure `Admin` exists so Zeitwerk can use it as a namespace.
37
+ module ::Admin; end
38
+
39
+ Rails.autoloaders.main.push_dir(admin_dir, namespace: ::Admin)
40
+ end
22
41
 
23
42
  Rails.autoloaders.each do |loader|
24
- loader.ignore(host_dsl_dir)
43
+ loader.ignore(admin_suite_app_dir) if admin_suite_app_dir.exist?
44
+
45
+ next unless admin_portals_dir.exist?
46
+
47
+ loader.ignore(admin_portals_dir) if self.class.contains_admin_suite_portal_dsl?(admin_portals_dir)
48
+ end
49
+ end
50
+
51
+ def self.host_admin_namespace_files?(admin_dir)
52
+ # True if any file under app/admin appears to define `Admin::*` constants.
53
+ Dir[admin_dir.join("**/*.rb").to_s].any? do |file|
54
+ next false if file.include?("/portals/")
55
+
56
+ content = File.binread(file)
57
+ content = content.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
58
+
59
+ content.match?(/\b(module|class)\s+Admin\b/) ||
60
+ content.match?(/\b(module|class)\s+Admin::/)
61
+ rescue StandardError
62
+ false
63
+ end
64
+ end
65
+
66
+ def self.contains_admin_suite_portal_dsl?(admin_portals_dir)
67
+ portal_files = Dir[admin_portals_dir.join("**/*.rb").to_s]
68
+ portal_files.any? do |file|
69
+ content = File.binread(file)
70
+ content = content.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
71
+ portal_dsl_pattern = /(::)?AdminSuite\s*\.\s*portal\b/
72
+ portal_dsl_pattern.match?(content)
73
+ rescue StandardError
74
+ false
25
75
  end
26
76
  end
27
77
 
@@ -33,6 +83,17 @@ module AdminSuite
33
83
  require "admin/base/action_handler"
34
84
  end
35
85
 
86
+ initializer "admin_suite.reloader" do |app|
87
+ # Reset the handlers_loaded flag in development so handlers are reloaded
88
+ # when code changes. This ensures the expensive glob operation happens at
89
+ # most once per request (or code reload) rather than on every NameError.
90
+ if Rails.env.development?
91
+ app.reloader.to_prepare do
92
+ Admin::Base::ActionExecutor.handlers_loaded = false
93
+ end
94
+ end
95
+ end
96
+
36
97
  initializer "admin_suite.watchable_dirs" do |app|
37
98
  next unless Rails.env.development?
38
99
 
@@ -65,6 +126,13 @@ module AdminSuite
65
126
  ]
66
127
  end
67
128
 
129
+ if config.action_globs.blank?
130
+ config.action_globs = [
131
+ Rails.root.join("config/admin_suite/actions/*.rb").to_s,
132
+ Rails.root.join("app/admin/actions/*.rb").to_s
133
+ ]
134
+ end
135
+
68
136
  if config.portal_globs.blank?
69
137
  config.portal_globs = [
70
138
  Rails.root.join("config/admin_suite/portals/*.rb").to_s,
@@ -2,7 +2,7 @@
2
2
 
3
3
  module AdminSuite
4
4
  module Version
5
- VERSION = "0.1.2"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
 
8
8
  # Backward-compatible constant.
data/lib/admin_suite.rb CHANGED
@@ -34,7 +34,10 @@ module AdminSuite
34
34
 
35
35
  # Defines (or updates) a portal using a Ruby DSL.
36
36
  #
37
- # Host apps typically place these in `app/admin/portals/*.rb`.
37
+ # Host apps typically place these in:
38
+ # - `config/admin_suite/portals/*.rb` (recommended; not a Zeitwerk autoload path)
39
+ # - `app/admin_suite/portals/*.rb` (supported; AdminSuite ignores for Zeitwerk)
40
+ # - `app/admin/portals/*.rb` (supported; AdminSuite will ignore for Zeitwerk if files contain `AdminSuite.portal`)
38
41
  #
39
42
  # @param key [Symbol, String]
40
43
  # @yield Portal definition DSL
@@ -20,10 +20,23 @@ AdminSuite.configure do |config|
20
20
  Rails.root.join("app/admin/resources/*.rb").to_s
21
21
  ]
22
22
 
23
+ # Action handler file globs (host app can override).
24
+ #
25
+ # Files typically define `Admin::Actions::<Resource><Action>Action` handlers.
26
+ config.action_globs = [
27
+ Rails.root.join("config/admin_suite/actions/*.rb").to_s,
28
+ Rails.root.join("app/admin/actions/*.rb").to_s
29
+ ]
30
+
23
31
  # Portal dashboard DSL globs (host app can override).
24
32
  # Files typically call `AdminSuite.portal :ops do ... end`
25
33
  config.portal_globs = [
26
34
  Rails.root.join("config/admin_suite/portals/*.rb").to_s,
35
+ # Prefer `app/admin_suite/portals` for DSL files so Zeitwerk never expects
36
+ # application constants (e.g. `Admin::Portals::OpsPortal` for
37
+ # `app/admin/portals/ops_portal.rb`) during eager load.
38
+ Rails.root.join("app/admin_suite/portals/*.rb").to_s,
39
+ # Legacy/fallback: still support portals defined under app/admin/portals.
27
40
  Rails.root.join("app/admin/portals/*.rb").to_s
28
41
  ]
29
42