plutonium 0.38.0 → 0.39.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-controller/SKILL.md +13 -0
  3. data/.claude/skills/plutonium-definition-actions/SKILL.md +13 -0
  4. data/.claude/skills/plutonium-nested-resources/SKILL.md +6 -4
  5. data/CHANGELOG.md +7 -1
  6. data/docs/guides/custom-actions.md +14 -0
  7. data/docs/guides/nested-resources.md +7 -3
  8. data/docs/og-image.html +84 -0
  9. data/docs/public/og-image.png +0 -0
  10. data/docs/reference/controller/index.md +5 -1
  11. data/docs/reference/definition/actions.md +14 -0
  12. data/gemfiles/rails_7.gemfile.lock +5 -5
  13. data/gemfiles/rails_8.0.gemfile.lock +5 -5
  14. data/gemfiles/rails_8.1.gemfile.lock +5 -5
  15. data/lib/generators/pu/rodauth/install_generator.rb +7 -11
  16. data/lib/generators/pu/rodauth/templates/app/rodauth/rodauth_plugin.rb.tt +3 -5
  17. data/lib/plutonium/auth/sequel_adapter.rb +76 -0
  18. data/lib/plutonium/core/controller.rb +2 -3
  19. data/lib/plutonium/resource/controller.rb +3 -2
  20. data/lib/plutonium/resource/controllers/presentable.rb +4 -2
  21. data/lib/plutonium/routing/mapper_extensions.rb +9 -6
  22. data/lib/plutonium/ui/action_button.rb +72 -11
  23. data/lib/plutonium/ui/actions_dropdown.rb +3 -25
  24. data/lib/plutonium/ui/breadcrumbs.rb +2 -2
  25. data/lib/plutonium/ui/component/methods.rb +10 -3
  26. data/lib/plutonium/ui/form/base.rb +1 -1
  27. data/lib/plutonium/ui/form/interaction.rb +5 -5
  28. data/lib/plutonium/ui/form/query.rb +1 -1
  29. data/lib/plutonium/ui/form/resource.rb +1 -1
  30. data/lib/plutonium/ui/layout/base.rb +1 -1
  31. data/lib/plutonium/ui/layout/basic_layout.rb +2 -2
  32. data/lib/plutonium/ui/layout/resource_layout.rb +2 -2
  33. data/lib/plutonium/ui/layout/rodauth_layout.rb +2 -2
  34. data/lib/plutonium/ui/page/index.rb +1 -1
  35. data/lib/plutonium/ui/page/interactive_action.rb +1 -1
  36. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +3 -25
  37. data/lib/plutonium/version.rb +1 -1
  38. data/package.json +1 -1
  39. data/plutonium.gemspec +2 -2
  40. metadata +8 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a3ade68b80487615f8ad46432edb306cde755d712e7222b11705427f9a283ca
4
- data.tar.gz: 10e95e8c085c8b7229929f50517fce9d964a1ff2ca31b0ee20662ca863c88f47
3
+ metadata.gz: 7c8fc50a1fe8c28ef9db4c7bef8b632c2b3c15524be2572085433e7185ed6923
4
+ data.tar.gz: 3dacb628155eeb439002f145c16fac47237ce55f1dc2d90c0253df36ec7f70a4
5
5
  SHA512:
6
- metadata.gz: e4a5ac3024c168686c6c315d2d7fa280c708a7e807580a422cb20223b6ccf2f95e96876e6a56d9ca88ee8fcbb63cc906dba33ad02af73a62f167bd91431a1a08
7
- data.tar.gz: a0644bf92081d308e0ef306d38168d63138864c54534fea161c20b9f2d79e8556914239dd3122854686e0fee140758f9aa49b2b9c72248a48868045405fffbd5
6
+ metadata.gz: '03919fed054eca767c0904b360c575af0b217b90acabec6688622e17fdfeabccbf024fd6343e8cc51ea0497ac53a37eb483e567063ec08a7a499eeaa86a5e96a'
7
+ data.tar.gz: e810b4a35375822961f4aad503603d3970a4c6217d65f4a9394f5b4952bc860b519bb405c0319dcef4418aa13711c5b04f2a5a0918070a6ad6f6b6cbeacf557b
@@ -151,6 +151,19 @@ class PostsController < ::ResourceController
151
151
  end
152
152
  ```
153
153
 
154
+ **Important:** When adding custom routes, always use the `as:` option to name them:
155
+
156
+ ```ruby
157
+ # config/routes.rb or portal routes
158
+ resources :posts do
159
+ member do
160
+ post :publish, as: :publish # Named route required!
161
+ end
162
+ end
163
+ ```
164
+
165
+ This ensures `resource_url_for` can generate correct URLs, especially for nested resources.
166
+
154
167
  Note: For most custom operations, use Interactive Actions in definitions instead.
155
168
 
156
169
  ## Key Methods
@@ -37,6 +37,19 @@ class PostDefinition < ResourceDefinition
37
37
  end
38
38
  ```
39
39
 
40
+ **Important:** When adding custom routes for actions, always use the `as:` option to name them:
41
+
42
+ ```ruby
43
+ # In your portal routes or config/routes.rb
44
+ resources :posts do
45
+ collection do
46
+ get :reports, as: :reports # Named route required!
47
+ end
48
+ end
49
+ ```
50
+
51
+ This ensures `resource_url_for` can generate correct URLs, especially for nested resources.
52
+
40
53
  **Note:** For custom operations with business logic, use **Interactive Actions** with an Interaction class instead. That's the recommended approach for most custom actions.
41
54
 
42
55
  ## Interactive Actions (with Interaction)
@@ -316,15 +316,17 @@ Add custom member/collection routes to nested resources:
316
316
  ```ruby
317
317
  register_resource ::Property do
318
318
  member do
319
- get :analytics
320
- post :archive
319
+ get :analytics, as: :analytics
320
+ post :archive, as: :archive
321
321
  end
322
322
  end
323
323
  ```
324
324
 
325
+ **Important:** Always use the `as:` option to name custom routes. This ensures `resource_url_for` can generate correct URLs for nested resources. Without named routes, URL generation will fail.
326
+
325
327
  Generates nested routes:
326
- - `/companies/:company_id/properties/:id/analytics`
327
- - `/companies/:company_id/properties/:id/archive`
328
+ - `/companies/:company_id/nested_properties/:id/analytics`
329
+ - `/companies/:company_id/nested_properties/:id/archive`
328
330
 
329
331
  ## Related Skills
330
332
 
data/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- ## [0.38.0] - 2026-01-25
1
+ ## [0.39.0] - 2026-01-26
2
2
 
3
3
  ### 🚀 Features
4
4
 
@@ -14,9 +14,14 @@
14
14
  - Remove duplicate kv store controller and move improvements into original
15
15
  - *(filters)* Improve association filter class resolution
16
16
  - *(ui)* Improve dropdown positioning with viewport boundary
17
+ - *(auth)* Dynamically detect database adapter for Rodauth (#51)
17
18
  - *(crud)* Use correct action attributes for form re-rendering on errors
18
19
  - *(form)* Distinguish empty vs not-submitted key-value store fields
19
20
  - *(controller)* Use existing record context for form param extraction
21
+ - *(controller)* Prevent UrlGenerationError when extracting params for nested resource update
22
+ - *(routing)* Correct URL generation for interactive actions on nested resources
23
+ - *(ui)* Replace deprecated phlex-rails `helpers` method with `view_context`
24
+ - *(routing)* Add named routes for commit actions and document route naming requirement
20
25
 
21
26
  ### 📚 Documentation
22
27
 
@@ -34,6 +39,7 @@
34
39
  - Use chokidar to fix dev build cyclic dependency issues
35
40
  - Warn when running tests without Appraisal
36
41
  - Switch to yarn
42
+ - Update og image
37
43
  ## [0.37.0] - 2026-01-21
38
44
 
39
45
  ### 🚀 Features
@@ -39,6 +39,20 @@ class PostDefinition < ResourceDefinition
39
39
  end
40
40
  ```
41
41
 
42
+ ::: warning Always Name Custom Routes
43
+ When adding custom routes for actions, always use the `as:` option:
44
+
45
+ ```ruby
46
+ resources :posts do
47
+ collection do
48
+ get :reports, as: :reports # Named route required!
49
+ end
50
+ end
51
+ ```
52
+
53
+ This ensures `resource_url_for` can generate correct URLs, especially for nested resources.
54
+ :::
55
+
42
56
  **Note:** For custom operations with business logic, use Interactive Actions with an Interaction class.
43
57
 
44
58
  ## Interactive Actions with Interactions
@@ -388,15 +388,19 @@ Add member/collection routes:
388
388
  ```ruby
389
389
  register_resource ::Comment do
390
390
  member do
391
- post :approve
392
- post :flag
391
+ post :approve, as: :approve
392
+ post :flag, as: :flag
393
393
  end
394
394
  collection do
395
- get :pending
395
+ get :pending, as: :pending
396
396
  end
397
397
  end
398
398
  ```
399
399
 
400
+ ::: warning Always Name Custom Routes
401
+ Always use the `as:` option when defining custom routes. This ensures `resource_url_for` can generate correct URLs. Without named routes, URL generation will fail for nested resources.
402
+ :::
403
+
400
404
  Generates nested routes:
401
405
  - `POST /posts/:post_id/nested_comments/:id/approve`
402
406
  - `POST /posts/:post_id/nested_comments/:id/flag`
@@ -0,0 +1,84 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <style>
6
+ * {
7
+ margin: 0;
8
+ padding: 0;
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ width: 1200px;
14
+ height: 630px;
15
+ background: #1e2330;
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
20
+ }
21
+
22
+ .container {
23
+ display: flex;
24
+ flex-direction: row;
25
+ align-items: center;
26
+ gap: 60px;
27
+ }
28
+
29
+ .logo {
30
+ width: 260px;
31
+ height: 260px;
32
+ object-fit: contain;
33
+ }
34
+
35
+ .content {
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 16px;
39
+ }
40
+
41
+ .title {
42
+ font-size: 88px;
43
+ font-weight: 700;
44
+ color: #ffffff;
45
+ letter-spacing: -1px;
46
+ }
47
+
48
+ .tagline {
49
+ font-size: 40px;
50
+ color: #b8bcc8;
51
+ font-weight: 400;
52
+ }
53
+
54
+ .features {
55
+ display: flex;
56
+ gap: 24px;
57
+ margin-top: 12px;
58
+ font-size: 26px;
59
+ color: #e07c5a;
60
+ font-weight: 500;
61
+ }
62
+
63
+ .features span:not(:last-child)::after {
64
+ content: '·';
65
+ margin-left: 24px;
66
+ color: #e07c5a;
67
+ }
68
+ </style>
69
+ </head>
70
+ <body>
71
+ <div class="container">
72
+ <img src="public/plutonium.png" alt="Plutonium" class="logo">
73
+ <div class="content">
74
+ <div class="title">Plutonium</div>
75
+ <div class="tagline">Build Rails Apps in Minutes, Not Days</div>
76
+ <div class="features">
77
+ <span>Convention-driven</span>
78
+ <span>AI-ready</span>
79
+ <span>Fully customizable</span>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </body>
84
+ </html>
Binary file
@@ -219,11 +219,15 @@ end
219
219
  # In portal routes or config/routes.rb
220
220
  register_resource Post do
221
221
  member do
222
- post :publish
222
+ post :publish, as: :publish # Always use as: option!
223
223
  end
224
224
  end
225
225
  ```
226
226
 
227
+ ::: warning Always Name Custom Routes
228
+ Always use the `as:` option when defining custom routes. This ensures `resource_url_for` can generate correct URLs, especially for nested resources.
229
+ :::
230
+
227
231
  ## Authorization
228
232
 
229
233
  ### Automatic Authorization
@@ -39,6 +39,20 @@ class PostDefinition < Plutonium::Resource::Definition
39
39
  end
40
40
  ```
41
41
 
42
+ ::: warning Always Name Custom Routes
43
+ When adding custom routes for actions, always use the `as:` option:
44
+
45
+ ```ruby
46
+ resources :posts do
47
+ collection do
48
+ get :reports, as: :reports # Named route required!
49
+ end
50
+ end
51
+ ```
52
+
53
+ This ensures `resource_url_for` can generate correct URLs, especially for nested resources.
54
+ :::
55
+
42
56
  **Note:** For custom operations with business logic, use **Interactive Actions** with an Interaction class.
43
57
 
44
58
  ## Interactive Actions
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.37.0)
4
+ plutonium (0.38.0)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 9.0)
@@ -11,8 +11,8 @@ PATH
11
11
  phlex-tabler_icons
12
12
  phlexi-display (>= 0.2.0)
13
13
  phlexi-field (>= 0.2.0)
14
- phlexi-form (>= 0.14.0)
15
- phlexi-menu (>= 0.4.0)
14
+ phlexi-form (>= 0.14.1)
15
+ phlexi-menu (>= 0.4.1)
16
16
  phlexi-table (>= 0.2.0)
17
17
  rabl (~> 0.16.1)
18
18
  rails (>= 7.2)
@@ -230,12 +230,12 @@ GEM
230
230
  fiber-local
231
231
  phlex (~> 2.0)
232
232
  zeitwerk
233
- phlexi-form (0.14.0)
233
+ phlexi-form (0.14.1)
234
234
  activesupport
235
235
  phlex (~> 2.0)
236
236
  phlexi-field (~> 0.2.0)
237
237
  zeitwerk
238
- phlexi-menu (0.4.0)
238
+ phlexi-menu (0.4.1)
239
239
  phlex (~> 2.0)
240
240
  phlexi-field (~> 0.2.0)
241
241
  zeitwerk
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.37.0)
4
+ plutonium (0.38.0)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 9.0)
@@ -11,8 +11,8 @@ PATH
11
11
  phlex-tabler_icons
12
12
  phlexi-display (>= 0.2.0)
13
13
  phlexi-field (>= 0.2.0)
14
- phlexi-form (>= 0.14.0)
15
- phlexi-menu (>= 0.4.0)
14
+ phlexi-form (>= 0.14.1)
15
+ phlexi-menu (>= 0.4.1)
16
16
  phlexi-table (>= 0.2.0)
17
17
  rabl (~> 0.16.1)
18
18
  rails (>= 7.2)
@@ -209,12 +209,12 @@ GEM
209
209
  fiber-local
210
210
  phlex (~> 2.0)
211
211
  zeitwerk
212
- phlexi-form (0.14.0)
212
+ phlexi-form (0.14.1)
213
213
  activesupport
214
214
  phlex (~> 2.0)
215
215
  phlexi-field (~> 0.2.0)
216
216
  zeitwerk
217
- phlexi-menu (0.4.0)
217
+ phlexi-menu (0.4.1)
218
218
  phlex (~> 2.0)
219
219
  phlexi-field (~> 0.2.0)
220
220
  zeitwerk
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.37.0)
4
+ plutonium (0.38.0)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 9.0)
@@ -11,8 +11,8 @@ PATH
11
11
  phlex-tabler_icons
12
12
  phlexi-display (>= 0.2.0)
13
13
  phlexi-field (>= 0.2.0)
14
- phlexi-form (>= 0.14.0)
15
- phlexi-menu (>= 0.4.0)
14
+ phlexi-form (>= 0.14.1)
15
+ phlexi-menu (>= 0.4.1)
16
16
  phlexi-table (>= 0.2.0)
17
17
  rabl (~> 0.16.1)
18
18
  rails (>= 7.2)
@@ -211,12 +211,12 @@ GEM
211
211
  fiber-local
212
212
  phlex (~> 2.0)
213
213
  zeitwerk
214
- phlexi-form (0.14.0)
214
+ phlexi-form (0.14.1)
215
215
  activesupport
216
216
  phlex (~> 2.0)
217
217
  phlexi-field (~> 0.2.0)
218
218
  zeitwerk
219
- phlexi-menu (0.4.0)
219
+ phlexi-menu (0.4.1)
220
220
  phlex (~> 2.0)
221
221
  phlexi-field (~> 0.2.0)
222
222
  zeitwerk
@@ -1,20 +1,13 @@
1
1
  require "rails/generators/base"
2
2
  require "rails/generators/active_record/migration"
3
3
  require "securerandom"
4
+ require "plutonium/auth/sequel_adapter"
4
5
 
5
6
  module Pu
6
7
  module Rodauth
7
8
  class InstallGenerator < ::Rails::Generators::Base
8
9
  include ::ActiveRecord::Generators::Migration
9
10
 
10
- SEQUEL_ADAPTERS = {
11
- "postgresql" => (RUBY_ENGINE == "jruby") ? "postgresql" : "postgres",
12
- "mysql2" => (RUBY_ENGINE == "jruby") ? "mysql" : "mysql2",
13
- "sqlite3" => "sqlite",
14
- "oracle_enhanced" => "oracle",
15
- "sqlserver" => (RUBY_ENGINE == "jruby") ? "mssql" : "tinytds"
16
- }
17
-
18
11
  source_root "#{__dir__}/templates"
19
12
 
20
13
  desc "Install rodauth-rails"
@@ -61,15 +54,18 @@ module Pu
61
54
 
62
55
  private
63
56
 
57
+ # Delegates to the SequelAdapter module to avoid code duplication.
64
58
  def sequel_adapter
65
- SEQUEL_ADAPTERS[activerecord_adapter] || activerecord_adapter
59
+ Plutonium::Auth::SequelAdapter.sequel_adapter
66
60
  end
67
61
 
62
+ # Delegates to the SequelAdapter module's internal ActiveRecord adapter detection.
63
+ # We still provide this method for use in create_install_migration.
68
64
  def activerecord_adapter
69
65
  if ActiveRecord::Base.respond_to?(:connection_db_config)
70
- ActiveRecord::Base.connection_db_config.adapter
66
+ ActiveRecord::Base.connection_db_config&.adapter
71
67
  else
72
- ActiveRecord::Base.connection_config.fetch(:adapter)
68
+ ActiveRecord::Base.connection_config&.fetch(:adapter, nil)
73
69
  end
74
70
  end
75
71
  end
@@ -1,4 +1,5 @@
1
1
  require "sequel/core"
2
+ require "plutonium/auth/sequel_adapter"
2
3
 
3
4
  class RodauthPlugin < Rodauth::Rails::Auth
4
5
  attr_accessor :url_options
@@ -17,11 +18,8 @@ class RodauthPlugin < Rodauth::Rails::Auth
17
18
  # ==> General
18
19
 
19
20
  # Initialize Sequel and have it reuse Active Record's database connection.
20
- <% if RUBY_ENGINE == "jruby" -%>
21
- db Sequel.connect("jdbc:<%= sequel_adapter %>://", extensions: :activerecord_connection, keep_reference: false)
22
- <% else -%>
23
- db Sequel.<%= sequel_adapter %>(extensions: :activerecord_connection, keep_reference: false)
24
- <% end -%>
21
+ # The adapter is detected dynamically at runtime to support database changes.
22
+ db Plutonium::Auth::SequelAdapter.db
25
23
 
26
24
  # Change prefix of table and foreign key column names from default "account"
27
25
  # accounts_table :users
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sequel/core"
4
+
5
+ module Plutonium
6
+ module Auth
7
+ # Provides runtime detection of the database adapter for Sequel configuration.
8
+ # This module dynamically detects the ActiveRecord adapter and returns the
9
+ # corresponding Sequel adapter, allowing users to change their database
10
+ # without needing to regenerate rodauth files.
11
+ module SequelAdapter
12
+ # Maps ActiveRecord adapter names to their corresponding Sequel adapter names.
13
+ # JRuby uses JDBC adapters which have different naming conventions.
14
+ SEQUEL_ADAPTERS = {
15
+ "postgresql" => (RUBY_ENGINE == "jruby") ? "postgresql" : "postgres",
16
+ "mysql2" => (RUBY_ENGINE == "jruby") ? "mysql" : "mysql2",
17
+ "sqlite3" => "sqlite",
18
+ "oracle_enhanced" => "oracle",
19
+ "sqlserver" => (RUBY_ENGINE == "jruby") ? "mssql" : "tinytds"
20
+ }.freeze
21
+
22
+ class << self
23
+ # Returns a Sequel database connection that reuses ActiveRecord's connection.
24
+ # Automatically detects the correct adapter based on the current ActiveRecord config.
25
+ #
26
+ # @return [Sequel::Database] configured Sequel database connection
27
+ # @raise [RuntimeError] if the Sequel adapter initialization fails
28
+ def db
29
+ adapter = sequel_adapter
30
+ begin
31
+ if RUBY_ENGINE == "jruby"
32
+ Sequel.connect("jdbc:#{adapter}://", extensions: :activerecord_connection, keep_reference: false)
33
+ else
34
+ Sequel.public_send(adapter, extensions: :activerecord_connection, keep_reference: false)
35
+ end
36
+ rescue => e
37
+ raise "Failed to initialize Sequel with adapter '#{adapter}'. " \
38
+ "Please ensure your database configuration is correct and the required " \
39
+ "database gems are installed. Original error: #{e.message}"
40
+ end
41
+ end
42
+
43
+ # Returns the Sequel adapter name based on the current ActiveRecord adapter.
44
+ # If the ActiveRecord adapter is not in the SEQUEL_ADAPTERS mapping, the
45
+ # ActiveRecord adapter name is returned as-is, which may work for adapters
46
+ # where the names match between ActiveRecord and Sequel.
47
+ #
48
+ # @return [String] the Sequel adapter name
49
+ def sequel_adapter
50
+ SEQUEL_ADAPTERS[activerecord_adapter] || activerecord_adapter
51
+ end
52
+
53
+ private
54
+
55
+ # Returns the current ActiveRecord adapter name.
56
+ #
57
+ # @return [String] the ActiveRecord adapter name
58
+ # @raise [RuntimeError] if the ActiveRecord adapter cannot be determined
59
+ def activerecord_adapter
60
+ adapter = if ActiveRecord::Base.respond_to?(:connection_db_config)
61
+ ActiveRecord::Base.connection_db_config&.adapter
62
+ else
63
+ ActiveRecord::Base.connection_config&.fetch(:adapter, nil)
64
+ end
65
+
66
+ unless adapter
67
+ raise "Unable to determine the ActiveRecord database adapter. " \
68
+ "Please ensure ActiveRecord is properly configured with a database connection."
69
+ end
70
+
71
+ adapter
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -166,9 +166,8 @@ module Plutonium
166
166
  end
167
167
 
168
168
  helper_suffix = case action
169
- when :new then "new_"
170
- when :edit then "edit_"
171
- else ""
169
+ when :show, nil then ""
170
+ else "#{action}_"
172
171
  end
173
172
 
174
173
  helper_name = :"#{helper_suffix}#{helper_base}_path"
@@ -135,8 +135,9 @@ module Plutonium
135
135
  # @return [Hash] The submitted resource parameters
136
136
  def submitted_resource_params
137
137
  # Use existing record (cloned) for context during param extraction, or new instance for create
138
+ # Pass form_action: false to prevent form from trying to generate URL (cloned record has id: nil)
138
139
  extraction_record = resource_record?&.dup || resource_class.new
139
- @submitted_resource_params ||= build_form(extraction_record).extract_input(params, view_context:)[resource_param_key.to_sym].compact
140
+ @submitted_resource_params ||= build_form(extraction_record, form_action: false).extract_input(params, view_context:)[resource_param_key.to_sym].compact
140
141
  end
141
142
 
142
143
  # Returns the resource parameters, including scoped and parent parameters
@@ -273,7 +274,7 @@ module Plutonium
273
274
  def resource_url_args_for(*, **kwargs)
274
275
  kwargs[:parent] = current_parent unless kwargs.key?(:parent)
275
276
  # Pass the current association when in a nested context
276
- if current_parent && !kwargs.key?(:association) && current_nested_association
277
+ if kwargs[:parent] && !kwargs.key?(:association) && current_nested_association
277
278
  kwargs[:association] = current_nested_association
278
279
  end
279
280
  super
@@ -46,8 +46,10 @@ module Plutonium
46
46
  current_definition.detail_class.new(resource_record!, resource_fields: presentable_attributes, resource_associations: permitted_associations, resource_definition: current_definition)
47
47
  end
48
48
 
49
- def build_form(record = resource_record!, action: action_name)
50
- current_definition.form_class.new(record, resource_fields: submittable_attributes_for(action), resource_definition: current_definition)
49
+ def build_form(record = resource_record!, action: action_name, form_action: nil, **)
50
+ form_options = {resource_fields: submittable_attributes_for(action), resource_definition: current_definition, **}
51
+ form_options[:action] = form_action unless form_action.nil?
52
+ current_definition.form_class.new(record, **form_options)
51
53
  end
52
54
 
53
55
  def present_parent? = false
@@ -127,8 +127,9 @@ module Plutonium
127
127
  def define_member_interactive_actions
128
128
  member do
129
129
  get "record_actions/:interactive_action", action: :interactive_record_action,
130
- as: :record_action
131
- post "record_actions/:interactive_action", action: :commit_interactive_record_action
130
+ as: :interactive_record_action
131
+ post "record_actions/:interactive_action", action: :commit_interactive_record_action,
132
+ as: :commit_interactive_record_action
132
133
  end
133
134
  end
134
135
 
@@ -138,12 +139,14 @@ module Plutonium
138
139
  def define_collection_interactive_actions
139
140
  collection do
140
141
  get "bulk_actions/:interactive_action", action: :interactive_bulk_action,
141
- as: :bulk_action
142
- post "bulk_actions/:interactive_action", action: :commit_interactive_bulk_action
142
+ as: :interactive_bulk_action
143
+ post "bulk_actions/:interactive_action", action: :commit_interactive_bulk_action,
144
+ as: :commit_interactive_bulk_action
143
145
 
144
146
  get "resource_actions/:interactive_action", action: :interactive_resource_action,
145
- as: :resource_action
146
- post "resource_actions/:interactive_action", action: :commit_interactive_resource_action
147
+ as: :interactive_resource_action
148
+ post "resource_actions/:interactive_action", action: :commit_interactive_resource_action,
149
+ as: :commit_interactive_resource_action
147
150
  end
148
151
  end
149
152
 
@@ -17,6 +17,13 @@ module Plutonium
17
17
  secondary: {default: "pu-btn-secondary", soft: "pu-btn-soft-secondary"}
18
18
  }.freeze
19
19
 
20
+ # Color to CSS class mapping for dropdown item variants
21
+ DROPDOWN_COLOR_CLASSES = {
22
+ danger: "text-danger-600 dark:text-danger-400 hover:bg-danger-50 dark:hover:bg-danger-900/30"
23
+ }.freeze
24
+
25
+ DROPDOWN_DEFAULT_COLOR = "text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)]"
26
+
20
27
  def initialize(action, url:, variant: :default)
21
28
  @action = action
22
29
  @url = url
@@ -24,24 +31,23 @@ module Plutonium
24
31
  end
25
32
 
26
33
  def view_template
27
- if @action.route_options.method == :get
28
- render_link
34
+ case @variant
35
+ when :dropdown, :row_dropdown
36
+ render_dropdown_item
29
37
  else
30
- render_button
38
+ if @action.route_options.method == :get
39
+ render_link
40
+ else
41
+ render_button
42
+ end
31
43
  end
32
44
  end
33
45
 
34
46
  private
35
47
 
36
48
  def render_link
37
- uri = URI.parse(@url)
38
- params = Rack::Utils.parse_nested_query(uri.query)
39
- params["return_to"] = @action.return_to.nil? ? request.original_url : @action.return_to
40
- uri.query = params.to_query
41
- uri.to_s
42
-
43
49
  link_to(
44
- uri.to_s,
50
+ url_with_return_to,
45
51
  class: button_classes,
46
52
  data: {turbo_frame: @action.turbo_frame}
47
53
  ) do
@@ -53,7 +59,7 @@ module Plutonium
53
59
  button_to(
54
60
  @url,
55
61
  method: @action.route_options.method,
56
- name: :return_to, value: (@action.return_to.nil? ? request.original_url : @action.return_to),
62
+ name: :return_to, value: return_to_url,
57
63
  class: "inline-block",
58
64
  form: {
59
65
  data: {
@@ -69,6 +75,28 @@ module Plutonium
69
75
  end
70
76
  end
71
77
 
78
+ def render_dropdown_item
79
+ link_attrs = {
80
+ href: url_with_return_to,
81
+ class: dropdown_item_classes
82
+ }
83
+
84
+ # Add turbo frame if specified
85
+ link_attrs[:data] = {turbo_frame: @action.turbo_frame} if @action.turbo_frame
86
+
87
+ # Add confirmation and method for non-GET requests
88
+ if @action.confirmation || @action.route_options.method != :get
89
+ link_attrs[:data] ||= {}
90
+ link_attrs[:data][:turbo_method] = @action.route_options.method if @action.route_options.method != :get
91
+ link_attrs[:data][:turbo_confirm] = @action.confirmation if @action.confirmation
92
+ end
93
+
94
+ a(**link_attrs) do
95
+ render @action.icon.new(class: "w-4 h-4") if @action.icon
96
+ span { @action.label }
97
+ end
98
+ end
99
+
72
100
  def render_button_content
73
101
  if @action.icon
74
102
  render @action.icon.new(class: icon_classes)
@@ -100,6 +128,39 @@ module Plutonium
100
128
  # Table variant uses soft (tinted) buttons, default uses solid buttons
101
129
  (@variant == :table) ? color_mapping[:soft] : color_mapping[:default]
102
130
  end
131
+
132
+ def dropdown_item_classes
133
+ base_classes = "flex items-center gap-2 text-sm transition-colors"
134
+ size_classes = (@variant == :row_dropdown) ? "px-3 py-1.5" : "px-4 py-2"
135
+
136
+ # Use same color determination as buttons: color || category
137
+ color_key = (@action.color || @action.category)&.to_sym
138
+ color_classes = DROPDOWN_COLOR_CLASSES[color_key] || DROPDOWN_DEFAULT_COLOR
139
+
140
+ "#{base_classes} #{size_classes} #{color_classes}"
141
+ end
142
+
143
+ def url_with_return_to
144
+ uri = URI.parse(@url)
145
+ params = Rack::Utils.parse_nested_query(uri.query)
146
+ params["return_to"] = return_to_url
147
+ uri.query = params.to_query
148
+ uri.to_s
149
+ end
150
+
151
+ def default_return_to
152
+ # When in a turbo frame with a parent, return to parent's show page
153
+ # instead of the frame's URL (which would be the nested index)
154
+ if current_turbo_frame && current_parent
155
+ resource_url_for(current_parent, parent: nil)
156
+ else
157
+ request.original_url
158
+ end
159
+ end
160
+
161
+ def return_to_url
162
+ @action.return_to.nil? ? default_return_to : @action.return_to
163
+ end
103
164
  end
104
165
  end
105
166
  end
@@ -56,35 +56,13 @@ module Plutonium
56
56
 
57
57
  def render_danger_actions
58
58
  div(class: "py-1") do
59
- danger_actions.each { |action| render_action_item(action, danger: true) }
59
+ danger_actions.each { |action| render_action_item(action) }
60
60
  end
61
61
  end
62
62
 
63
- def render_action_item(action, danger: false)
63
+ def render_action_item(action)
64
64
  url = route_options_to_url(action.route_options, @subject)
65
-
66
- link_attrs = {
67
- href: url,
68
- class: tokens(
69
- "flex items-center gap-2 px-4 py-2 text-sm transition-colors",
70
- danger ? "text-danger-600 dark:text-danger-400 hover:bg-danger-50 dark:hover:bg-danger-900/30" : "text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)]"
71
- )
72
- }
73
-
74
- # Add turbo frame if specified
75
- link_attrs[:data] = {turbo_frame: action.turbo_frame} if action.turbo_frame
76
-
77
- # Add confirmation if specified
78
- if action.confirmation
79
- link_attrs[:data] ||= {}
80
- link_attrs[:data][:turbo_method] = :delete if action.route_options.method == :delete
81
- link_attrs[:data][:turbo_confirm] = action.confirmation
82
- end
83
-
84
- a(**link_attrs) do
85
- render action.icon.new(class: "w-4 h-4") if action.icon
86
- span { action.label }
87
- end
65
+ render ActionButton.new(action, url: url, variant: :dropdown)
88
66
  end
89
67
 
90
68
  def secondary_actions
@@ -15,7 +15,7 @@ module Plutonium
15
15
  # Dashboard
16
16
  li(class: "inline-flex items-center") do
17
17
  a(
18
- href: helpers.root_path,
18
+ href: root_path,
19
19
  class: "inline-flex items-center text-sm font-medium text-[var(--pu-text-muted)] hover:text-primary-600 transition-colors"
20
20
  ) do
21
21
  svg(
@@ -101,7 +101,7 @@ module Plutonium
101
101
  d: "m1 9 4-4-4-4"
102
102
  )
103
103
  end
104
- link_to helpers.nestable_resource_name_plural(resource_class),
104
+ link_to nestable_resource_name_plural(resource_class),
105
105
  resource_url_for(resource_class),
106
106
  class: "ms-1 text-sm font-medium text-[var(--pu-text-muted)] hover:text-primary-600 md:ms-2 transition-colors"
107
107
  end
@@ -11,15 +11,19 @@ module Plutonium
11
11
  private
12
12
 
13
13
  def params
14
- helpers.controller.params
14
+ view_context.controller.params
15
15
  end
16
16
 
17
17
  def request
18
- helpers.controller.request
18
+ view_context.controller.request
19
19
  end
20
20
 
21
21
  def pagy_instance
22
- helpers.controller.instance_variable_get(:@pagy)
22
+ view_context.controller.instance_variable_get(:@pagy)
23
+ end
24
+
25
+ def controller
26
+ view_context.controller
23
27
  end
24
28
 
25
29
  delegate \
@@ -29,6 +33,7 @@ module Plutonium
29
33
  :resource_record?,
30
34
  :resource_name,
31
35
  :resource_name_plural,
36
+ :nestable_resource_name_plural,
32
37
  :display_name_of,
33
38
  :resource_url_for,
34
39
  :route_options_to_url,
@@ -46,6 +51,8 @@ module Plutonium
46
51
  :allowed_to?,
47
52
  :registered_resources,
48
53
  :root_path,
54
+ :make_page_title,
55
+ :resource_logo_tag,
49
56
  to: :view_context
50
57
  end
51
58
  end
@@ -109,7 +109,7 @@ module Plutonium
109
109
  end
110
110
 
111
111
  def form_action
112
- return @form_action unless object.present? && @form_action != false && helpers.present?
112
+ return @form_action unless object.present? && @form_action != false && view_context.present?
113
113
 
114
114
  @form_action ||= url_for(object, action: object.new_record? ? :create : :update)
115
115
  end
@@ -16,7 +16,7 @@ module Plutonium
16
16
 
17
17
  def form_action
18
18
  # Build the correct commit URL for the interactive action
19
- action = helpers.current_interactive_action
19
+ action = current_interactive_action
20
20
  return nil unless action
21
21
 
22
22
  # Create route options for the commit action (convert GET to POST action)
@@ -28,8 +28,8 @@ module Plutonium
28
28
  )
29
29
 
30
30
  # Use existing infrastructure to build the URL
31
- subject = action.record_action? ? helpers.resource_record! : helpers.resource_class
32
- helpers.route_options_to_url(commit_route_options, subject)
31
+ subject = action.record_action? ? resource_record! : resource_class
32
+ route_options_to_url(commit_route_options, subject)
33
33
  end
34
34
 
35
35
  def commit_action_name(action_name)
@@ -52,10 +52,10 @@ module Plutonium
52
52
  end
53
53
 
54
54
  def render_bulk_action_ids
55
- action = helpers.current_interactive_action
55
+ action = current_interactive_action
56
56
  return unless action&.bulk_action?
57
57
 
58
- ids = Array(helpers.params[:ids])
58
+ ids = Array(params[:ids])
59
59
  ids.each do |id|
60
60
  input(type: :hidden, name: "ids[]", value: id)
61
61
  end
@@ -207,7 +207,7 @@ module Plutonium
207
207
  def count_active_filters
208
208
  count = 0
209
209
  query_object.filter_definitions.each do |filter_name, _|
210
- filter_params = helpers.params.dig(:q, filter_name)
210
+ filter_params = params.dig(:q, filter_name)
211
211
  next unless filter_params.is_a?(Hash) || filter_params.is_a?(ActionController::Parameters)
212
212
 
213
213
  filter_params.each_value do |v|
@@ -58,7 +58,7 @@ module Plutonium
58
58
  end
59
59
 
60
60
  def form_action
61
- return @form_action unless object.present? && @form_action != false && helpers.present?
61
+ return @form_action unless object.present? && @form_action != false && view_context.present?
62
62
 
63
63
  @form_action ||= resource_url_for(object, action: object.new_record? ? :create : :update)
64
64
  end
@@ -22,7 +22,7 @@ module Plutonium
22
22
 
23
23
  def lang = nil
24
24
 
25
- def page_title = helpers.controller.instance_variable_get(:@page_title)
25
+ def page_title = view_context.controller.instance_variable_get(:@page_title)
26
26
 
27
27
  def html_attributes = {lang:, data_controller: "color-mode"}
28
28
 
@@ -5,8 +5,8 @@ module Plutonium
5
5
  private
6
6
 
7
7
  def page_title
8
- helpers.make_page_title(
9
- helpers.controller.instance_variable_get(:@page_title)
8
+ make_page_title(
9
+ controller.instance_variable_get(:@page_title)
10
10
  )
11
11
  end
12
12
  end
@@ -9,8 +9,8 @@ module Plutonium
9
9
  })
10
10
 
11
11
  def page_title
12
- helpers.make_page_title(
13
- helpers.controller.instance_variable_get(:@page_title)
12
+ make_page_title(
13
+ controller.instance_variable_get(:@page_title)
14
14
  )
15
15
  end
16
16
 
@@ -7,7 +7,7 @@ module Plutonium
7
7
  private
8
8
 
9
9
  def page_title
10
- helpers.controller.instance_variable_get(:@page_title)
10
+ controller.instance_variable_get(:@page_title)
11
11
  end
12
12
 
13
13
  def main_attributes = mix(super, {
@@ -26,7 +26,7 @@ module Plutonium
26
26
 
27
27
  def render_logo
28
28
  link_to root_path, class: "flex items-center text-2xl font-semibold text-[var(--pu-text)] mb-2" do
29
- helpers.resource_logo_tag classname: "w-24 h-24 mr-2 rounded-[var(--pu-radius-md)]"
29
+ resource_logo_tag classname: "w-24 h-24 mr-2 rounded-[var(--pu-radius-md)]"
30
30
  end
31
31
  end
32
32
 
@@ -7,7 +7,7 @@ module Plutonium
7
7
  private
8
8
 
9
9
  def page_title
10
- super || current_definition.index_page_title || helpers.nestable_resource_name_plural(resource_class)
10
+ super || current_definition.index_page_title || nestable_resource_name_plural(resource_class)
11
11
  end
12
12
 
13
13
  def page_description
@@ -17,7 +17,7 @@ module Plutonium
17
17
  end
18
18
 
19
19
  def render_default_content
20
- if helpers.current_turbo_frame == "remote_modal"
20
+ if current_turbo_frame == "remote_modal"
21
21
  dialog(
22
22
  closedby: "any",
23
23
  class: "rounded-[var(--pu-radius-lg)] w-full max-w-3xl
@@ -56,35 +56,13 @@ module Plutonium
56
56
 
57
57
  def render_danger_actions
58
58
  div(class: "py-1") do
59
- danger_actions.each { |action| render_action_item(action, danger: true) }
59
+ danger_actions.each { |action| render_action_item(action) }
60
60
  end
61
61
  end
62
62
 
63
- def render_action_item(action, danger: false)
63
+ def render_action_item(action)
64
64
  url = route_options_to_url(action.route_options, @record)
65
-
66
- link_attrs = {
67
- href: url,
68
- class: tokens(
69
- "flex items-center gap-2 px-3 py-1.5 text-sm transition-colors",
70
- danger ? "text-danger-600 dark:text-danger-400 hover:bg-danger-50 dark:hover:bg-danger-900/30" : "text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)]"
71
- )
72
- }
73
-
74
- # Add turbo frame if specified
75
- link_attrs[:data] = {turbo_frame: action.turbo_frame} if action.turbo_frame
76
-
77
- # Add confirmation if specified
78
- if action.confirmation
79
- link_attrs[:data] ||= {}
80
- link_attrs[:data][:turbo_method] = action.route_options.method if action.route_options.method
81
- link_attrs[:data][:turbo_confirm] = action.confirmation
82
- end
83
-
84
- a(**link_attrs) do
85
- render action.icon.new(class: "w-4 h-4") if action.icon
86
- span { action.label }
87
- end
65
+ render Plutonium::UI::ActionButton.new(action, url: url, variant: :row_dropdown)
88
66
  end
89
67
 
90
68
  def secondary_actions
@@ -1,5 +1,5 @@
1
1
  module Plutonium
2
- VERSION = "0.38.0"
2
+ VERSION = "0.39.0"
3
3
  NEXT_MAJOR_VERSION = VERSION.split(".").tap { |v|
4
4
  v[1] = v[1].to_i + 1
5
5
  v[2] = 0
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radioactive-labs/plutonium",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Build production-ready Rails apps in minutes, not days. Convention-driven, fully customizable, AI-ready.",
5
5
  "type": "module",
6
6
  "main": "src/js/core.js",
data/plutonium.gemspec CHANGED
@@ -44,10 +44,10 @@ Gem::Specification.new do |spec|
44
44
  spec.add_dependency "phlex-rails"
45
45
  spec.add_dependency "phlex-tabler_icons"
46
46
  spec.add_dependency "phlexi-field", ">= 0.2.0"
47
- spec.add_dependency "phlexi-form", ">= 0.14.0"
47
+ spec.add_dependency "phlexi-form", ">= 0.14.1"
48
48
  spec.add_dependency "phlexi-table", ">= 0.2.0"
49
49
  spec.add_dependency "phlexi-display", ">= 0.2.0"
50
- spec.add_dependency "phlexi-menu", ">= 0.4.0"
50
+ spec.add_dependency "phlexi-menu", ">= 0.4.1"
51
51
  spec.add_dependency "tailwind_merge"
52
52
  spec.add_dependency "phlex-slotable", ">= 1.0.0"
53
53
  spec.add_dependency "redcarpet"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plutonium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.38.0
4
+ version: 0.39.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-25 00:00:00.000000000 Z
11
+ date: 2026-01-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -184,14 +184,14 @@ dependencies:
184
184
  requirements:
185
185
  - - ">="
186
186
  - !ruby/object:Gem::Version
187
- version: 0.14.0
187
+ version: 0.14.1
188
188
  type: :runtime
189
189
  prerelease: false
190
190
  version_requirements: !ruby/object:Gem::Requirement
191
191
  requirements:
192
192
  - - ">="
193
193
  - !ruby/object:Gem::Version
194
- version: 0.14.0
194
+ version: 0.14.1
195
195
  - !ruby/object:Gem::Dependency
196
196
  name: phlexi-table
197
197
  requirement: !ruby/object:Gem::Requirement
@@ -226,14 +226,14 @@ dependencies:
226
226
  requirements:
227
227
  - - ">="
228
228
  - !ruby/object:Gem::Version
229
- version: 0.4.0
229
+ version: 0.4.1
230
230
  type: :runtime
231
231
  prerelease: false
232
232
  version_requirements: !ruby/object:Gem::Requirement
233
233
  requirements:
234
234
  - - ">="
235
235
  - !ruby/object:Gem::Version
236
- version: 0.4.0
236
+ version: 0.4.1
237
237
  - !ruby/object:Gem::Dependency
238
238
  name: tailwind_merge
239
239
  requirement: !ruby/object:Gem::Requirement
@@ -542,6 +542,7 @@ files:
542
542
  - docs/guides/theming.md
543
543
  - docs/guides/troubleshooting.md
544
544
  - docs/index.md
545
+ - docs/og-image.html
545
546
  - docs/public/android-chrome-192x192.png
546
547
  - docs/public/android-chrome-512x512.png
547
548
  - docs/public/apple-touch-icon.png
@@ -776,6 +777,7 @@ files:
776
777
  - lib/plutonium/auth.rb
777
778
  - lib/plutonium/auth/public.rb
778
779
  - lib/plutonium/auth/rodauth.rb
780
+ - lib/plutonium/auth/sequel_adapter.rb
779
781
  - lib/plutonium/configuration.rb
780
782
  - lib/plutonium/core/.DS_Store
781
783
  - lib/plutonium/core/controller.rb