plutonium 0.34.1 → 0.35.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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/skill.md +53 -0
  3. data/.claude/skills/{assets → plutonium-assets}/SKILL.md +13 -8
  4. data/.claude/skills/{connect-resource → plutonium-connect-resource}/SKILL.md +1 -1
  5. data/.claude/skills/{controller → plutonium-controller}/SKILL.md +27 -13
  6. data/.claude/skills/{create-resource → plutonium-create-resource}/SKILL.md +1 -1
  7. data/.claude/skills/{definition → plutonium-definition}/SKILL.md +10 -10
  8. data/.claude/skills/{definition-actions → plutonium-definition-actions}/SKILL.md +34 -9
  9. data/.claude/skills/{definition-fields → plutonium-definition-fields}/SKILL.md +38 -10
  10. data/.claude/skills/plutonium-definition-query/SKILL.md +356 -0
  11. data/.claude/skills/{forms → plutonium-forms}/SKILL.md +6 -6
  12. data/.claude/skills/{installation → plutonium-installation}/SKILL.md +9 -9
  13. data/.claude/skills/{interaction → plutonium-interaction}/SKILL.md +20 -19
  14. data/.claude/skills/{model → plutonium-model}/SKILL.md +3 -3
  15. data/.claude/skills/{model-features → plutonium-model-features}/SKILL.md +3 -3
  16. data/.claude/skills/{nested-resources → plutonium-nested-resources}/SKILL.md +5 -5
  17. data/.claude/skills/{package → plutonium-package}/SKILL.md +7 -8
  18. data/.claude/skills/{policy → plutonium-policy}/SKILL.md +26 -4
  19. data/.claude/skills/{portal → plutonium-portal}/SKILL.md +33 -31
  20. data/.claude/skills/{resource → plutonium-resource}/SKILL.md +27 -27
  21. data/.claude/skills/{rodauth → plutonium-rodauth}/SKILL.md +5 -5
  22. data/.claude/skills/plutonium-theming/SKILL.md +424 -0
  23. data/.claude/skills/{views → plutonium-views}/SKILL.md +7 -7
  24. data/CHANGELOG.md +52 -0
  25. data/CLAUDE.md +215 -0
  26. data/CONTRIBUTING.md +72 -18
  27. data/README.md +100 -19
  28. data/app/assets/plutonium.css +1 -11
  29. data/app/assets/plutonium.js +1685 -1146
  30. data/app/assets/plutonium.js.map +4 -4
  31. data/app/assets/plutonium.min.js +70 -70
  32. data/app/assets/plutonium.min.js.map +4 -4
  33. data/app/views/resource/interactive_bulk_action.html.erb +1 -5
  34. data/app/views/rodauth/_email_auth_request_form.html.erb +1 -1
  35. data/app/views/rodauth/_login_form.html.erb +15 -55
  36. data/app/views/rodauth/_login_form_footer.html.erb +2 -2
  37. data/app/views/rodauth/_password_visibility.html.erb +2 -8
  38. data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
  39. data/app/views/rodauth/change_login.html.erb +36 -19
  40. data/app/views/rodauth/change_password.html.erb +34 -10
  41. data/app/views/rodauth/close_account.html.erb +12 -4
  42. data/app/views/rodauth/confirm_password.html.erb +19 -17
  43. data/app/views/rodauth/create_account.html.erb +30 -109
  44. data/app/views/rodauth/email_auth.html.erb +1 -1
  45. data/app/views/rodauth/logout.html.erb +4 -4
  46. data/app/views/rodauth/otp_auth.html.erb +13 -4
  47. data/app/views/rodauth/otp_disable.html.erb +12 -4
  48. data/app/views/rodauth/otp_setup.html.erb +29 -12
  49. data/app/views/rodauth/otp_unlock.html.erb +19 -10
  50. data/app/views/rodauth/otp_unlock_not_available.html.erb +7 -7
  51. data/app/views/rodauth/recovery_auth.html.erb +12 -4
  52. data/app/views/rodauth/recovery_codes.html.erb +12 -4
  53. data/app/views/rodauth/remember.html.erb +7 -7
  54. data/app/views/rodauth/reset_password.html.erb +23 -7
  55. data/app/views/rodauth/reset_password_request.html.erb +14 -10
  56. data/app/views/rodauth/sms_auth.html.erb +13 -4
  57. data/app/views/rodauth/sms_confirm.html.erb +13 -4
  58. data/app/views/rodauth/sms_disable.html.erb +12 -4
  59. data/app/views/rodauth/sms_request.html.erb +1 -1
  60. data/app/views/rodauth/sms_setup.html.erb +23 -7
  61. data/app/views/rodauth/two_factor_auth.html.erb +2 -2
  62. data/app/views/rodauth/two_factor_disable.html.erb +12 -4
  63. data/app/views/rodauth/two_factor_manage.html.erb +7 -7
  64. data/app/views/rodauth/unlock_account.html.erb +13 -5
  65. data/app/views/rodauth/unlock_account_request.html.erb +2 -2
  66. data/app/views/rodauth/verify_account.html.erb +25 -7
  67. data/app/views/rodauth/verify_account_resend.html.erb +14 -10
  68. data/app/views/rodauth/verify_login_change.html.erb +1 -1
  69. data/app/views/rodauth/webauthn_auth.html.erb +1 -1
  70. data/app/views/rodauth/webauthn_remove.html.erb +18 -8
  71. data/app/views/rodauth/webauthn_setup.html.erb +12 -4
  72. data/docs/.vitepress/config.ts +15 -26
  73. data/docs/.vitepress/theme/custom.css +388 -29
  74. data/docs/getting-started/index.md +1 -1
  75. data/docs/getting-started/tutorial/02-first-resource.md +9 -0
  76. data/docs/getting-started/tutorial/06-nested-resources.md +2 -2
  77. data/docs/getting-started/tutorial/07-author-portal.md +191 -0
  78. data/docs/getting-started/tutorial/{07-customizing-ui.md → 08-customizing-ui.md} +7 -7
  79. data/docs/getting-started/tutorial/index.md +5 -2
  80. data/docs/guides/authorization.md +33 -0
  81. data/docs/guides/creating-packages.md +12 -16
  82. data/docs/guides/custom-actions.md +36 -0
  83. data/docs/guides/search-filtering.md +121 -42
  84. data/docs/guides/theming.md +232 -36
  85. data/docs/index.md +203 -57
  86. data/docs/public/og-image.png +0 -0
  87. data/docs/reference/controller/index.md +14 -16
  88. data/docs/reference/definition/actions.md +38 -3
  89. data/docs/reference/definition/fields.md +3 -3
  90. data/docs/reference/definition/index.md +2 -2
  91. data/docs/reference/generators/index.md +0 -1
  92. data/docs/reference/interaction/index.md +14 -10
  93. data/docs/reference/model/index.md +0 -1
  94. data/docs/reference/portal/index.md +13 -27
  95. data/gemfiles/rails_7.gemfile.lock +1 -1
  96. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  97. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  98. data/lib/generators/pu/pkg/portal/portal_generator.rb +0 -2
  99. data/lib/generators/pu/pkg/portal/templates/app/views/package/dashboard/index.html.erb +28 -72
  100. data/lib/plutonium/action/interactive.rb +2 -2
  101. data/lib/plutonium/core/controller.rb +2 -1
  102. data/lib/plutonium/definition/actions.rb +2 -2
  103. data/lib/plutonium/lib/deep_freezer.rb +3 -7
  104. data/lib/plutonium/query/filter.rb +14 -0
  105. data/lib/plutonium/query/filters/association.rb +49 -0
  106. data/lib/plutonium/query/filters/boolean.rb +35 -0
  107. data/lib/plutonium/query/filters/date.rb +97 -0
  108. data/lib/plutonium/query/filters/date_range.rb +58 -0
  109. data/lib/plutonium/query/filters/select.rb +55 -0
  110. data/lib/plutonium/resource/controllers/crud_actions.rb +24 -6
  111. data/lib/plutonium/resource/controllers/interactive_actions.rb +76 -58
  112. data/lib/plutonium/resource/controllers/queryable.rb +4 -2
  113. data/lib/plutonium/resource/query_object.rb +1 -1
  114. data/lib/plutonium/ui/action_button.rb +23 -65
  115. data/lib/plutonium/ui/actions_dropdown.rb +103 -0
  116. data/lib/plutonium/ui/block.rb +1 -1
  117. data/lib/plutonium/ui/breadcrumbs.rb +12 -19
  118. data/lib/plutonium/ui/color_mode_selector.rb +1 -1
  119. data/lib/plutonium/ui/component/kit.rb +6 -0
  120. data/lib/plutonium/ui/component_classes.rb +102 -0
  121. data/lib/plutonium/ui/display/base.rb +15 -0
  122. data/lib/plutonium/ui/display/components/attachment.rb +6 -5
  123. data/lib/plutonium/ui/display/components/boolean.rb +23 -0
  124. data/lib/plutonium/ui/display/components/color.rb +23 -0
  125. data/lib/plutonium/ui/display/resource.rb +1 -1
  126. data/lib/plutonium/ui/display/theme.rb +29 -15
  127. data/lib/plutonium/ui/empty_card.rb +3 -3
  128. data/lib/plutonium/ui/form/base.rb +20 -0
  129. data/lib/plutonium/ui/form/components/key_value_store.rb +11 -11
  130. data/lib/plutonium/ui/form/components/resource_select.rb +31 -0
  131. data/lib/plutonium/ui/form/components/secure_association.rb +1 -2
  132. data/lib/plutonium/ui/form/components/uppy.rb +5 -4
  133. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +4 -4
  134. data/lib/plutonium/ui/form/interaction.rb +17 -1
  135. data/lib/plutonium/ui/form/query.rb +133 -80
  136. data/lib/plutonium/ui/form/theme.rb +50 -35
  137. data/lib/plutonium/ui/frame_navigator_panel.rb +2 -2
  138. data/lib/plutonium/ui/layout/base.rb +1 -1
  139. data/lib/plutonium/ui/layout/header.rb +4 -7
  140. data/lib/plutonium/ui/layout/rodauth_layout.rb +7 -7
  141. data/lib/plutonium/ui/layout/sidebar.rb +1 -1
  142. data/lib/plutonium/ui/nav_grid_menu.rb +7 -6
  143. data/lib/plutonium/ui/nav_user.rb +9 -8
  144. data/lib/plutonium/ui/page/interactive_action.rb +5 -5
  145. data/lib/plutonium/ui/page_header.rb +29 -10
  146. data/lib/plutonium/ui/panel.rb +4 -4
  147. data/lib/plutonium/ui/sidebar_menu.rb +8 -8
  148. data/lib/plutonium/ui/skeleton_table.rb +7 -8
  149. data/lib/plutonium/ui/tab_list.rb +5 -5
  150. data/lib/plutonium/ui/table/base.rb +3 -0
  151. data/lib/plutonium/ui/table/components/attachment.rb +4 -3
  152. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +82 -0
  153. data/lib/plutonium/ui/table/components/pagy_info.rb +2 -2
  154. data/lib/plutonium/ui/table/components/pagy_pagination.rb +13 -8
  155. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +101 -0
  156. data/lib/plutonium/ui/table/components/scopes_bar.rb +2 -2
  157. data/lib/plutonium/ui/table/components/selection_column.rb +100 -0
  158. data/lib/plutonium/ui/table/display_theme.rb +6 -6
  159. data/lib/plutonium/ui/table/resource.rb +93 -52
  160. data/lib/plutonium/ui/table/theme.rb +28 -15
  161. data/lib/plutonium/version.rb +1 -1
  162. data/package.json +2 -2
  163. data/plutonium.gemspec +5 -4
  164. data/src/css/components.css +471 -0
  165. data/src/css/intl_tel_input.css +2 -2
  166. data/src/css/plutonium.css +2 -0
  167. data/src/css/tokens.css +149 -0
  168. data/src/js/controllers/bulk_actions_controller.js +109 -0
  169. data/src/js/controllers/filter_panel_controller.js +35 -0
  170. data/src/js/controllers/register_controllers.js +5 -1
  171. data/src/js/controllers/resource_drop_down_controller.js +25 -1
  172. data/src/js/controllers/slim_select_controller.js +6 -2
  173. data/src/js/turbo/turbo_actions.js +1 -1
  174. metadata +52 -39
  175. data/.claude/skills/definition-query/SKILL.md +0 -334
  176. data/docs/concepts/architecture.md +0 -226
  177. data/docs/concepts/auto-detection.md +0 -254
  178. data/docs/concepts/index.md +0 -61
  179. data/docs/concepts/packages-portals.md +0 -304
  180. data/docs/concepts/resources.md +0 -224
  181. data/docs/cookbook/blog.md +0 -411
  182. data/docs/cookbook/index.md +0 -289
  183. data/docs/cookbook/saas.md +0 -481
  184. data/docs/public/CLAUDE.md +0 -578
  185. data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +0 -5
@@ -277,6 +277,10 @@ end
277
277
 
278
278
  ### Bulk Action
279
279
 
280
+ Bulk actions operate on multiple selected records. When registered, the resource table automatically shows:
281
+ - **Selection checkboxes** in each row
282
+ - **Bulk actions toolbar** that appears when records are selected
283
+
280
284
  ```ruby
281
285
  class BulkArchiveInteraction < Plutonium::Resource::Interaction
282
286
  presents label: "Archive Selected", icon: Phlex::TablerIcons::Archive
@@ -296,6 +300,33 @@ class BulkArchiveInteraction < Plutonium::Resource::Interaction
296
300
  end
297
301
  ```
298
302
 
303
+ Register in definition:
304
+
305
+ ```ruby
306
+ class PostDefinition < ResourceDefinition
307
+ action :bulk_archive, interaction: BulkArchiveInteraction
308
+ # bulk_action: true is automatically inferred from `resources` attribute
309
+ end
310
+ ```
311
+
312
+ Add the policy method (checked per-record):
313
+
314
+ ```ruby
315
+ class PostPolicy < ResourcePolicy
316
+ def bulk_archive?
317
+ # Can use record attributes - checked for each selected record
318
+ user.admin? || record.author == user
319
+ end
320
+ end
321
+ ```
322
+
323
+ ::: tip Bulk Action Authorization
324
+ Bulk actions use **per-record authorization**:
325
+ - Policy method (e.g., `bulk_archive?`) is checked for **each selected record** - you can use `record` attributes
326
+ - Backend rejects the entire request if any record fails authorization
327
+ - UI only shows actions that **all** selected records support
328
+ :::
329
+
299
330
  ### Resource Action (No Record)
300
331
 
301
332
  ```ruby
@@ -322,12 +353,12 @@ end
322
353
 
323
354
  ```ruby
324
355
  def execute
325
- # Success with message
356
+ # Success with message (redirects to resource automatically)
326
357
  succeed(resource).with_message("Done!")
327
358
 
328
- # Success with redirect
359
+ # Success with custom redirect (only if different from default)
329
360
  succeed(resource)
330
- .with_redirect_response(resource_url_for(resource))
361
+ .with_redirect_response(custom_dashboard_path)
331
362
  .with_message("Redirecting...")
332
363
 
333
364
  # Failure with field errors
@@ -338,6 +369,10 @@ def execute
338
369
  end
339
370
  ```
340
371
 
372
+ ::: tip Automatic Redirect
373
+ Redirect is automatic on success. You can use `with_redirect_response` for a different destination.
374
+ :::
375
+
341
376
  ## Inherited Actions
342
377
 
343
378
  Actions defined in `ResourceDefinition` are inherited by all definitions:
@@ -16,7 +16,7 @@ Complete reference for field configuration in definitions.
16
16
  ```ruby
17
17
  class PostDefinition < Plutonium::Resource::Definition
18
18
  # field - changes type everywhere
19
- field :content, as: :rich_text
19
+ field :content, as: :markdown
20
20
 
21
21
  # input - form-specific
22
22
  input :title,
@@ -43,7 +43,7 @@ end
43
43
  | Category | Types |
44
44
  |----------|-------|
45
45
  | **Text** | `:string`, `:text`, `:email`, `:url`, `:tel`, `:password` |
46
- | **Rich Text** | `:rich_text`, `:markdown` |
46
+ | **Rich Text** | `:markdown` (uses EasyMDE editor) |
47
47
  | **Numeric** | `:number`, `:integer`, `:decimal`, `:range` |
48
48
  | **Boolean** | `:boolean` |
49
49
  | **Date/Time** | `:date`, `:time`, `:datetime` |
@@ -302,7 +302,7 @@ input :documents, as: :uppy,
302
302
  ### Rich Text Content
303
303
 
304
304
  ```ruby
305
- field :content, as: :rich_text # Form: rich editor
305
+ field :content, as: :markdown # Form: EasyMDE editor
306
306
  display :content, as: :markdown # Show: rendered markdown
307
307
  ```
308
308
 
@@ -10,7 +10,7 @@ Definitions control how resources render - which fields appear in forms, how tab
10
10
  class PostDefinition < Plutonium::Resource::Definition
11
11
  # Field configuration
12
12
  field :title
13
- field :body, as: :rich_text
13
+ field :body, as: :markdown
14
14
 
15
15
  # Form-specific
16
16
  input :title, placeholder: "Enter title"
@@ -81,7 +81,7 @@ Override selectively:
81
81
  ```ruby
82
82
  class PostDefinition < Plutonium::Resource::Definition
83
83
  # Override just body field
84
- field :body, as: :rich_text
84
+ field :body, as: :markdown
85
85
 
86
86
  # Other fields still auto-detected
87
87
  end
@@ -182,7 +182,6 @@ packages/admin_portal/
182
182
  │ ├── controllers/admin_portal/
183
183
  │ │ ├── concerns/controller.rb
184
184
  │ │ ├── plutonium_controller.rb
185
- │ │ ├── resource_controller.rb
186
185
  │ │ └── dashboard_controller.rb
187
186
  │ ├── definitions/admin_portal/
188
187
  │ ├── policies/admin_portal/
@@ -156,6 +156,10 @@ rescue ActiveRecord::RecordInvalid => e
156
156
  end
157
157
  ```
158
158
 
159
+ ::: warning Handle RecordInvalid
160
+ `ActiveRecord::RecordInvalid` is **not** rescued automatically. Always rescue it when using bang methods (`create!`, `update!`, `save!`).
161
+ :::
162
+
159
163
  ## Constructor
160
164
 
161
165
  Interactions require `view_context:` and accept attributes as keyword arguments:
@@ -193,10 +197,14 @@ outcome = interaction.call
193
197
 
194
198
  ## Success Outcomes
195
199
 
200
+ ::: tip Automatic Redirect
201
+ On success, the controller automatically redirects to the resource.
202
+ :::
203
+
196
204
  ### Basic Success
197
205
 
198
206
  ```ruby
199
- succeed(resource)
207
+ succeed(resource) # Redirects to resource automatically
200
208
  ```
201
209
 
202
210
  ### With Message
@@ -206,11 +214,12 @@ succeed(resource).with_message("Post published!")
206
214
  succeed(resource).with_message("Warning: limited visibility", :alert)
207
215
  ```
208
216
 
209
- ### With Redirect
217
+ ### With Custom Redirect
218
+
219
+ Useful when redirecting somewhere other than the default:
210
220
 
211
221
  ```ruby
212
- succeed(resource).with_redirect_response(posts_path)
213
- succeed(resource).with_redirect_response(resource, status: :see_other)
222
+ succeed(resource).with_redirect_response(custom_dashboard_path)
214
223
  ```
215
224
 
216
225
  ### With File Download
@@ -327,9 +336,7 @@ class Company::InviteUserInteraction < Plutonium::Resource::Interaction
327
336
  )
328
337
  UserInviteMailer.invitation(invite).deliver_later
329
338
 
330
- succeed(resource)
331
- .with_message("Invitation sent to #{email}")
332
- .with_redirect_response(resource)
339
+ succeed(resource).with_message("Invitation sent to #{email}")
333
340
  rescue ActiveRecord::RecordInvalid => e
334
341
  failed(e.record.errors)
335
342
  end
@@ -434,9 +441,6 @@ end
434
441
  1. **Keep interactions focused** - One action per interaction
435
442
  2. **Use validations** - Validate all inputs before execution
436
443
  3. **Handle errors gracefully** - Rescue exceptions and return `failed()`
437
- 4. **Return meaningful messages** - Help users understand what happened
438
- 5. **Use `and_then` for chains** - Compose complex workflows from simple interactions
439
- 6. **Declare attributes explicitly** - Always declare `resource` or `resources` attributes
440
444
 
441
445
  ## Related
442
446
 
@@ -215,5 +215,4 @@ scope :filtered, -> { where(status: 'active') }
215
215
  ## Related
216
216
 
217
217
  - [Model Features](./features)
218
- - [Resources Concept](/concepts/resources)
219
218
  - [Definition Reference](/reference/definition/)
@@ -174,40 +174,27 @@ end
174
174
 
175
175
  ## Controllers
176
176
 
177
- ### Base Controller
178
-
179
- ```ruby
180
- # packages/admin_portal/app/controllers/admin_portal/resource_controller.rb
181
- module AdminPortal
182
- class ResourceController < Plutonium::Portal::ResourceController
183
- layout "admin_portal/application"
184
-
185
- private
186
-
187
- def after_sign_in_path
188
- admin_root_path
189
- end
190
- end
191
- end
192
- ```
193
-
194
177
  ### Resource Controllers
195
178
 
179
+ Portal-specific controllers inherit from the feature package's controller and include the portal's controller concern:
180
+
196
181
  ```ruby
197
182
  # packages/admin_portal/app/controllers/admin_portal/posts_controller.rb
198
- module AdminPortal
199
- class PostsController < ResourceController
200
- private
183
+ class AdminPortal::PostsController < ::PostsController
184
+ include AdminPortal::Concerns::Controller
201
185
 
202
- def build_resource
203
- super.tap do |post|
204
- post.user = current_user
205
- end
186
+ private
187
+
188
+ def build_resource
189
+ super.tap do |post|
190
+ post.user = current_user
206
191
  end
207
192
  end
208
193
  end
209
194
  ```
210
195
 
196
+ Controllers are auto-created if not defined. When accessing `AdminPortal::PostsController`, Plutonium will dynamically create it by inheriting from `::PostsController` and including `AdminPortal::Concerns::Controller`.
197
+
211
198
  ## Portal-Specific Overrides
212
199
 
213
200
  ### Definitions
@@ -298,7 +285,7 @@ end
298
285
  To allow unauthenticated access to specific actions:
299
286
 
300
287
  ```ruby
301
- class AdminPortal::PagesController < AdminPortal::ResourceController
288
+ class AdminPortal::PagesController < AdminPortal::PlutoniumController
302
289
  skip_before_action :authenticate, only: [:health]
303
290
 
304
291
  def health
@@ -314,7 +301,7 @@ end
314
301
  ```ruby
315
302
  # packages/admin_portal/app/controllers/admin_portal/dashboard_controller.rb
316
303
  module AdminPortal
317
- class DashboardController < ResourceController
304
+ class DashboardController < PlutoniumController
318
305
  def index
319
306
  @stats = {
320
307
  posts: Post.count,
@@ -377,6 +364,5 @@ end
377
364
 
378
365
  ## Related
379
366
 
380
- - [Packages and Portals Concept](/concepts/packages-portals)
381
367
  - [Authentication Guide](/guides/authentication)
382
368
  - [Multi-tenancy Guide](/guides/multi-tenancy)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.33.1)
4
+ plutonium (0.34.1)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 9.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.33.1)
4
+ plutonium (0.34.1)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 9.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.33.1)
4
+ plutonium (0.34.1)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 9.0)
@@ -28,8 +28,6 @@ module Pu
28
28
  "packages/#{package_namespace}/app/controllers/#{package_namespace}/concerns/controller.rb"
29
29
  template "app/controllers/plutonium_controller.rb",
30
30
  "packages/#{package_namespace}/app/controllers/#{package_namespace}/plutonium_controller.rb"
31
- template "app/controllers/resource_controller.rb",
32
- "packages/#{package_namespace}/app/controllers/#{package_namespace}/resource_controller.rb"
33
31
 
34
32
  template "app/controllers/dashboard_controller.rb",
35
33
  "packages/#{package_namespace}/app/controllers/#{package_namespace}/dashboard_controller.rb"
@@ -1,81 +1,37 @@
1
- <div class="bg-gray-50 dark:bg-gray-900 p-8 rounded-xl">
2
- <h2 class="text-2xl font-bold text-gray-800 dark:text-white mb-6">
3
- Resources Management Dashboard
4
- </h2>
1
+ <div class="space-y-6">
2
+ <div>
3
+ <h1 class="text-2xl font-bold text-[var(--pu-text)]">Dashboard</h1>
4
+ <p class="text-[var(--pu-text-muted)]">Manage your resources</p>
5
+ </div>
6
+
5
7
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
6
8
  <% registered_resources.each do |resource| %>
7
9
  <% next unless allowed_to? :index?, resource %>
8
- <div
9
- class="
10
- bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-md border-l-4
11
- border-primary-600 dark:border-primary-500 transition-all duration-300
12
- hover:translate-y-[-2px] hover:shadow-lg
13
- "
14
- >
15
- <div class="flex p-6">
16
- <div
17
- class="
18
- flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-md
19
- bg-primary-50 dark:bg-primary-900/30
20
- "
21
- >
22
- <svg
23
- class="h-6 w-6 text-primary-600 dark:text-primary-400"
24
- xmlns="http://www.w3.org/2000/svg"
25
- fill="none"
26
- viewBox="0 0 24 24"
27
- stroke="currentColor"
28
- >
29
- <path
30
- stroke-linecap="round"
31
- stroke-linejoin="round"
32
- stroke-width="2"
33
- d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
34
- />
35
- </svg>
36
- </div>
37
- <div class="ml-4 flex-1">
38
- <h3
39
- class="
40
- text-lg font-semibold text-gray-900 dark:text-white flex items-center
41
- "
42
- >
43
- <%= resource.model_name.human.titleize.pluralize %>
44
- <span
45
- class="
46
- ml-2 px-2 py-0.5 text-xs rounded-full bg-primary-100 text-primary-800
47
- dark:bg-primary-900 dark:text-primary-200
48
- "
49
- ><%= authorized_resource_scope(resource).count %></span>
50
- </h3>
10
+ <div class="pu-card group hover:shadow-[var(--pu-shadow-lg)] transition-shadow">
11
+ <div class="pu-card-body space-y-4">
12
+ <div class="flex items-start justify-between">
13
+ <div class="flex items-center gap-3">
14
+ <div class="flex items-center justify-center h-10 w-10 rounded-[var(--pu-radius-md)] bg-primary-100 dark:bg-primary-900/30">
15
+ <svg class="h-5 w-5 text-primary-600 dark:text-primary-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
16
+ <path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
17
+ </svg>
18
+ </div>
19
+ <div>
20
+ <h3 class="font-semibold text-[var(--pu-text)]"><%= resource.model_name.human.titleize.pluralize %></h3>
21
+ <p class="text-sm text-[var(--pu-text-muted)]"><%= authorized_resource_scope(resource).count %> records</p>
22
+ </div>
23
+ </div>
51
24
  </div>
52
- </div>
53
- <div class="bg-gray-50 dark:bg-gray-700/50 px-6 py-3">
54
- <div
55
- class="
56
- text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider
57
- "
58
- >Quick Actions</div>
59
- <div class="mt-2 flex space-x-2">
60
- <%# Check for resource creation permission %>
25
+
26
+ <div class="flex items-center gap-2 pt-2 border-t border-[var(--pu-border-muted)]">
27
+ <a href="<%= resource_url_for(resource, parent: nil) %>" class="pu-btn pu-btn-sm pu-btn-primary">
28
+ View All
29
+ </a>
61
30
  <% if allowed_to? :new?, resource %>
62
- <a
63
- href="<%= resource_url_for(resource, parent: nil, action: :new) %>"
64
- class="
65
- px-2 py-1 text-xs font-medium rounded bg-white dark:bg-gray-700 text-gray-700
66
- dark:text-gray-300 border border-gray-200 dark:border-gray-600 hover:bg-gray-50
67
- dark:hover:bg-gray-600 transition-colors
68
- "
69
- >Add New</a>
31
+ <a href="<%= resource_url_for(resource, parent: nil, action: :new) %>" class="pu-btn pu-btn-sm pu-btn-outline">
32
+ Add New
33
+ </a>
70
34
  <% end %>
71
- <a
72
- href="<%= resource_url_for(resource, parent: nil) %>"
73
- class="
74
- px-2 py-1 text-xs font-medium rounded bg-white dark:bg-gray-700 text-gray-700
75
- dark:text-gray-300 border border-gray-200 dark:border-gray-600 hover:bg-gray-50
76
- dark:hover:bg-gray-600 transition-colors
77
- "
78
- >View All</a>
79
35
  </div>
80
36
  </div>
81
37
  </div>
@@ -79,7 +79,7 @@ module Plutonium
79
79
  if attribute_names.include?(:resource)
80
80
  :interactive_record_action
81
81
  elsif attribute_names.include?(:resources)
82
- :interactive_collection_action
82
+ :interactive_bulk_action
83
83
  else
84
84
  :interactive_resource_action
85
85
  end
@@ -99,7 +99,7 @@ module Plutonium
99
99
  # @return [Hash] The action options
100
100
  def self.determine_action_options(action_type)
101
101
  {
102
- bulk_action: action_type == :interactive_collection_action,
102
+ bulk_action: action_type == :interactive_bulk_action,
103
103
  record_action: action_type == :interactive_record_action,
104
104
  collection_record_action: action_type == :interactive_record_action,
105
105
  resource_action: action_type == :interactive_resource_action
@@ -101,7 +101,8 @@ module Plutonium
101
101
  end
102
102
 
103
103
  # Preserve the request format unless explicitly specified
104
- if !url_args.key?(:format) && request.present? && request.format.present? && request.format.symbol != :html
104
+ # Don't preserve turbo_stream as it's for streaming updates, not page navigation
105
+ if !url_args.key?(:format) && request.present? && request.format.present? && !request.format.symbol.in?([:html, :turbo_stream])
105
106
  url_args[:format] = request.format.symbol
106
107
  end
107
108
 
@@ -37,11 +37,11 @@ module Plutonium
37
37
  icon: Phlex::TablerIcons::Plus, position: 10)
38
38
 
39
39
  action(:show, route_options: {action: :show},
40
- collection_record_action: true,
40
+ collection_record_action: true, category: :primary,
41
41
  icon: Phlex::TablerIcons::Eye, position: 10)
42
42
 
43
43
  action(:edit, route_options: {action: :edit},
44
- record_action: true, collection_record_action: true,
44
+ record_action: true, collection_record_action: true, category: :primary,
45
45
  icon: Phlex::TablerIcons::Edit, position: 20)
46
46
 
47
47
  action(:destroy, route_options: {method: :delete},
@@ -4,6 +4,9 @@ module Plutonium
4
4
  module Lib
5
5
  class DeepFreezer
6
6
  def self.freeze(object)
7
+ # Never freeze Class or Module objects - they have mutable state that Rails needs
8
+ return object if object.is_a?(Class) || object.is_a?(Module)
9
+
7
10
  # Recursive calling #deep_freeze for enumerable objects.
8
11
  if object.respond_to? :each
9
12
  if object.instance_of?(Hash)
@@ -13,13 +16,6 @@ module Plutonium
13
16
  end
14
17
  end
15
18
 
16
- # # Freezing of all instance variable values.
17
- # object.instance_variables.each do |var|
18
- # frozen_val = instance_variable_get(var)
19
- # frozen_val.deep_freeze
20
- # instance_variable_set(var, frozen_val)
21
- # end
22
-
23
19
  if object.frozen?
24
20
  object
25
21
  else
@@ -3,6 +3,20 @@ module Plutonium
3
3
  class Filter < Base
4
4
  attr_reader :key
5
5
 
6
+ class << self
7
+ # Lookup a filter class by type symbol or return the class if already a Filter
8
+ # @param type [Symbol, Class] The type symbol (e.g., :text, :select) or a Filter class
9
+ # @return [Class] The filter class
10
+ def lookup(type)
11
+ return type if type.is_a?(Class) && type < Filter
12
+
13
+ class_name = "Plutonium::Query::Filters::#{type.to_s.classify}"
14
+ class_name.constantize
15
+ rescue NameError
16
+ raise ArgumentError, "Unknown filter type: #{type}. Expected #{class_name} to exist."
17
+ end
18
+ end
19
+
6
20
  def initialize(key:)
7
21
  super()
8
22
  @key = key
@@ -0,0 +1,49 @@
1
+ module Plutonium
2
+ module Query
3
+ module Filters
4
+ # Select filter for association records
5
+ #
6
+ # @example Basic - infers Category class from :category key
7
+ # filter :category, with: :association
8
+ #
9
+ # @example With explicit class
10
+ # filter :author, with: :association, class_name: User
11
+ #
12
+ # @example With multiple selection
13
+ # filter :tags, with: :association, class_name: Tag, multiple: true
14
+ #
15
+ class Association < Filter
16
+ def initialize(class_name: nil, multiple: false, **)
17
+ super(**)
18
+ @association_class = class_name
19
+ @multiple = multiple
20
+ end
21
+
22
+ def apply(scope, value:)
23
+ return scope if value.blank?
24
+
25
+ foreign_key = :"#{key}_id"
26
+ if @multiple && value.is_a?(Array)
27
+ scope.where(foreign_key => value.reject(&:blank?))
28
+ else
29
+ scope.where(foreign_key => value)
30
+ end
31
+ end
32
+
33
+ def customize_inputs
34
+ input :value,
35
+ as: :resource_select,
36
+ association_class: association_class,
37
+ multiple: @multiple,
38
+ include_blank: @multiple ? false : "All"
39
+ end
40
+
41
+ private
42
+
43
+ def association_class
44
+ @association_class || key.to_s.classify.constantize
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,35 @@
1
+ module Plutonium
2
+ module Query
3
+ module Filters
4
+ # Boolean filter for true/false columns
5
+ #
6
+ # @example Basic usage
7
+ # filter :active, with: :boolean
8
+ #
9
+ # @example With custom labels
10
+ # filter :published, with: :boolean, true_label: "Published", false_label: "Draft"
11
+ #
12
+ class Boolean < Filter
13
+ def initialize(true_label: "Yes", false_label: "No", **)
14
+ super(**)
15
+ @true_label = true_label
16
+ @false_label = false_label
17
+ end
18
+
19
+ def apply(scope, value:)
20
+ return scope if value.blank?
21
+
22
+ bool_value = ActiveModel::Type::Boolean.new.cast(value)
23
+ scope.where(key => bool_value)
24
+ end
25
+
26
+ def customize_inputs
27
+ input :value,
28
+ as: :select,
29
+ choices: [[@true_label, "true"], [@false_label, "false"]],
30
+ include_blank: "All"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end