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
@@ -0,0 +1,356 @@
1
+ ---
2
+ name: plutonium-definition-query
3
+ description: Configure search, filters, scopes, and sorting for Plutonium resources
4
+ ---
5
+
6
+ # Definition Query
7
+
8
+ Configure how users can search, filter, and sort resource collections.
9
+
10
+ ## Overview
11
+
12
+ ```ruby
13
+ class PostDefinition < ResourceDefinition
14
+ # Search - global text search
15
+ search do |scope, query|
16
+ scope.where("title ILIKE ?", "%#{query}%")
17
+ end
18
+
19
+ # Filters - dropdown filter panel
20
+ filter :title, with: :text, predicate: :contains
21
+ filter :status, with: :select, choices: %w[draft published archived]
22
+ filter :published, with: :boolean
23
+ filter :created_at, with: :date_range
24
+ filter :category, with: :association
25
+
26
+ # Scopes - quick filter buttons
27
+ scope :published
28
+ scope :draft
29
+
30
+ # Sorting - sortable columns
31
+ sort :title
32
+ sort :created_at
33
+
34
+ # Default sort
35
+ default_sort :created_at, :desc
36
+ end
37
+ ```
38
+
39
+ ## Search
40
+
41
+ Define global search across fields:
42
+
43
+ ```ruby
44
+ # Single field
45
+ search do |scope, query|
46
+ scope.where("title ILIKE ?", "%#{query}%")
47
+ end
48
+
49
+ # Multiple fields
50
+ search do |scope, query|
51
+ scope.where(
52
+ "title ILIKE :q OR content ILIKE :q OR author_name ILIKE :q",
53
+ q: "%#{query}%"
54
+ )
55
+ end
56
+
57
+ # With associations
58
+ search do |scope, query|
59
+ scope.joins(:author).where(
60
+ "posts.title ILIKE :q OR users.name ILIKE :q",
61
+ q: "%#{query}%"
62
+ ).distinct
63
+ end
64
+
65
+ # Split search terms
66
+ search do |scope, query|
67
+ terms = query.split(/\s+/)
68
+ terms.reduce(scope) do |current_scope, term|
69
+ current_scope.where("title ILIKE ?", "%#{term}%")
70
+ end
71
+ end
72
+ ```
73
+
74
+ ## Filters
75
+
76
+ Plutonium provides **6 built-in filter types**. Use shorthand symbols or full class names.
77
+
78
+ ### Text Filter
79
+
80
+ String/text filtering with pattern matching.
81
+
82
+ ```ruby
83
+ # Shorthand (recommended)
84
+ filter :title, with: :text, predicate: :contains
85
+ filter :status, with: :text, predicate: :eq
86
+
87
+ # Full class name
88
+ filter :title, with: Plutonium::Query::Filters::Text, predicate: :contains
89
+ ```
90
+
91
+ **Predicates:**
92
+
93
+ | Predicate | SQL | Description |
94
+ |-----------|-----|-------------|
95
+ | `:eq` | `= value` | Exact match (default) |
96
+ | `:not_eq` | `!= value` | Not equal |
97
+ | `:contains` | `LIKE %value%` | Contains substring |
98
+ | `:not_contains` | `NOT LIKE %value%` | Does not contain |
99
+ | `:starts_with` | `LIKE value%` | Starts with |
100
+ | `:ends_with` | `LIKE %value` | Ends with |
101
+ | `:matches` | `LIKE value` | Pattern match (`*` becomes `%`) |
102
+ | `:not_matches` | `NOT LIKE value` | Does not match pattern |
103
+
104
+ ### Boolean Filter
105
+
106
+ True/false filtering for boolean columns.
107
+
108
+ ```ruby
109
+ # Basic
110
+ filter :active, with: :boolean
111
+
112
+ # Custom labels
113
+ filter :published, with: :boolean, true_label: "Published", false_label: "Draft"
114
+ ```
115
+
116
+ Renders a select dropdown with "All", true label ("Yes"), and false label ("No").
117
+
118
+ ### Date Filter
119
+
120
+ Single date filtering with comparison predicates.
121
+
122
+ ```ruby
123
+ filter :created_at, with: :date, predicate: :gteq # On or after
124
+ filter :due_date, with: :date, predicate: :lt # Before
125
+ filter :published_at, with: :date, predicate: :eq # On exact date
126
+ ```
127
+
128
+ **Predicates:**
129
+
130
+ | Predicate | Description |
131
+ |-----------|-------------|
132
+ | `:eq` | On this date (default) |
133
+ | `:not_eq` | Not on this date |
134
+ | `:lt` | Before date |
135
+ | `:lteq` | On or before date |
136
+ | `:gt` | After date |
137
+ | `:gteq` | On or after date |
138
+
139
+ ### Date Range Filter
140
+
141
+ Filter between two dates (from/to).
142
+
143
+ ```ruby
144
+ # Basic
145
+ filter :created_at, with: :date_range
146
+
147
+ # Custom labels
148
+ filter :published_at, with: :date_range,
149
+ from_label: "Published from",
150
+ to_label: "Published to"
151
+ ```
152
+
153
+ Renders two date pickers. Both are optional - users can filter with just "from" or just "to".
154
+
155
+ ### Select Filter
156
+
157
+ Filter from predefined choices.
158
+
159
+ ```ruby
160
+ # Static choices (array)
161
+ filter :status, with: :select, choices: %w[draft published archived]
162
+
163
+ # Dynamic choices (proc)
164
+ filter :category, with: :select, choices: -> { Category.pluck(:name) }
165
+
166
+ # Multiple selection
167
+ filter :tags, with: :select, choices: %w[ruby rails js], multiple: true
168
+ ```
169
+
170
+ ### Association Filter
171
+
172
+ Filter by associated record.
173
+
174
+ ```ruby
175
+ # Basic - infers Category class from :category key
176
+ filter :category, with: :association
177
+
178
+ # Explicit class
179
+ filter :author, with: :association, class_name: User
180
+
181
+ # Multiple selection
182
+ filter :tags, with: :association, class_name: Tag, multiple: true
183
+ ```
184
+
185
+ Renders a resource select dropdown. Converts filter key to foreign key (`:category` -> `:category_id`).
186
+
187
+ ## Custom Filters
188
+
189
+ ### Custom Filter Class
190
+
191
+ ```ruby
192
+ class PriceRangeFilter < Plutonium::Query::Filter
193
+ def apply(scope, min: nil, max: nil)
194
+ scope = scope.where("price >= ?", min) if min.present?
195
+ scope = scope.where("price <= ?", max) if max.present?
196
+ scope
197
+ end
198
+
199
+ def customize_inputs
200
+ input :min, as: :number
201
+ input :max, as: :number
202
+ field :min, placeholder: "Min price..."
203
+ field :max, placeholder: "Max price..."
204
+ end
205
+ end
206
+
207
+ # Use in definition
208
+ filter :price, with: PriceRangeFilter
209
+ ```
210
+
211
+ ## Scopes
212
+
213
+ Scopes appear as quick filter buttons. They reference model scopes.
214
+
215
+ ### Basic Usage
216
+
217
+ ```ruby
218
+ class PostDefinition < ResourceDefinition
219
+ scope :published # Uses Post.published
220
+ scope :draft # Uses Post.draft
221
+ scope :featured # Uses Post.featured
222
+ end
223
+ ```
224
+
225
+ ### Inline Scope
226
+
227
+ Use block syntax with the scope passed as an argument:
228
+
229
+ ```ruby
230
+ scope(:recent) { |scope| scope.where('created_at > ?', 1.week.ago) }
231
+ scope(:this_month) { |scope| scope.where(created_at: Time.current.all_month) }
232
+ ```
233
+
234
+ ### With Controller Context
235
+
236
+ Inline scopes have access to controller context like `current_user`:
237
+
238
+ ```ruby
239
+ scope(:mine) { |scope| scope.where(author: current_user) }
240
+ scope(:my_team) { |scope| scope.where(team: current_user.team) }
241
+ ```
242
+
243
+ ### Default Scope
244
+
245
+ Set a scope as default to apply it when no scope is explicitly selected:
246
+
247
+ ```ruby
248
+ class PostDefinition < ResourceDefinition
249
+ scope :published, default: true # Applied by default
250
+ scope :draft
251
+ scope :archived
252
+ end
253
+ ```
254
+
255
+ When a default scope is set:
256
+ - The default scope is applied on initial page load
257
+ - The default scope button is highlighted (not "All")
258
+ - Clicking "All" shows all records without any scope filter
259
+ - URL without scope param uses the default; URL with `?q[scope]=` uses "All"
260
+
261
+ ## Sorting
262
+
263
+ ### Basic Sorting
264
+
265
+ ```ruby
266
+ sort :title
267
+ sort :created_at
268
+ sort :view_count
269
+
270
+ # Multiple at once
271
+ sorts :title, :created_at, :view_count
272
+ ```
273
+
274
+ ### Default Sort
275
+
276
+ ```ruby
277
+ # Field and direction
278
+ default_sort :created_at, :desc
279
+ default_sort :title, :asc
280
+
281
+ # Complex sorting with block
282
+ default_sort { |scope| scope.order(featured: :desc, created_at: :desc) }
283
+ ```
284
+
285
+ **Note:** Default sort only applies when no sort params are provided.
286
+
287
+ ## URL Parameters
288
+
289
+ Query parameters are structured under `q`:
290
+
291
+ ```
292
+ /posts?q[search]=rails
293
+ /posts?q[title][query]=widget
294
+ /posts?q[status][value]=published
295
+ /posts?q[created_at][from]=2024-01-01&q[created_at][to]=2024-12-31
296
+ /posts?q[scope]=recent
297
+ /posts?q[sort_fields][]=created_at&q[sort_directions][created_at]=desc
298
+ ```
299
+
300
+ ## Filter Summary Table
301
+
302
+ | Type | Symbol | Input Params | Options |
303
+ |------|--------|--------------|---------|
304
+ | Text | `:text` | `query` | `predicate:` |
305
+ | Boolean | `:boolean` | `value` | `true_label:`, `false_label:` |
306
+ | Date | `:date` | `value` | `predicate:` |
307
+ | Date Range | `:date_range` | `from`, `to` | `from_label:`, `to_label:` |
308
+ | Select | `:select` | `value` | `choices:`, `multiple:` |
309
+ | Association | `:association` | `value` | `class_name:`, `multiple:` |
310
+
311
+ ## Complete Example
312
+
313
+ ```ruby
314
+ class ProductDefinition < ResourceDefinition
315
+ # Full-text search
316
+ search do |scope, query|
317
+ scope.where(
318
+ "name ILIKE :q OR description ILIKE :q",
319
+ q: "%#{query}%"
320
+ )
321
+ end
322
+
323
+ # Filters
324
+ filter :name, with: :text, predicate: :contains
325
+ filter :status, with: :select, choices: %w[draft active discontinued]
326
+ filter :featured, with: :boolean
327
+ filter :created_at, with: :date_range
328
+ filter :price, with: :date, predicate: :gteq
329
+ filter :category, with: :association
330
+
331
+ # Quick scopes
332
+ scope :active, default: true
333
+ scope :featured
334
+ scope(:recent) { |scope| scope.where('created_at > ?', 1.week.ago) }
335
+
336
+ # Sortable columns
337
+ sorts :name, :price, :created_at
338
+
339
+ # Default sort
340
+ default_sort :created_at, :desc
341
+ end
342
+ ```
343
+
344
+ ## Performance Tips
345
+
346
+ 1. **Add indexes** for filtered/sorted columns
347
+ 2. **Use `.distinct`** when joining associations in search
348
+ 3. **Consider `pg_search`** for complex full-text search
349
+ 4. **Limit search fields** to indexed columns
350
+ 5. **Use scopes** instead of filters for common queries
351
+
352
+ ## Related Skills
353
+
354
+ - `plutonium-definition` - Overview and structure
355
+ - `plutonium-definition-fields` - Fields, inputs, displays
356
+ - `plutonium-definition-actions` - Actions and interactions
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: forms
2
+ name: plutonium-forms
3
3
  description: Plutonium forms - custom templates, Phlex form components, field builders, and theming
4
4
  ---
5
5
 
@@ -432,8 +432,8 @@ end
432
432
 
433
433
  ## Related Skills
434
434
 
435
- - `definition-fields` - Input configuration (as:, hint:, condition:)
436
- - `views` - Custom page classes
437
- - `assets` - TailwindCSS and component theming
438
- - `interaction` - Interactive action forms
439
- - `nested-resources` - Parent/child forms
435
+ - `plutonium-definition-fields` - Input configuration (as:, hint:, condition:)
436
+ - `plutonium-views` - Custom page classes
437
+ - `plutonium-assets` - TailwindCSS and component theming
438
+ - `plutonium-interaction` - Interactive action forms
439
+ - `plutonium-nested-resources` - Parent/child forms
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: installation
2
+ name: plutonium-installation
3
3
  description: Installing Plutonium in a Rails application - setup, generators, and configuration
4
4
  ---
5
5
 
@@ -290,11 +290,11 @@ For models that already exist in your app:
290
290
 
291
291
  ## Related Skills
292
292
 
293
- - `resource` - Resource architecture overview
294
- - `rodauth` - Authentication setup and configuration
295
- - `package` - Feature and portal packages
296
- - `portal` - Portal configuration
297
- - `views` - Custom pages, layouts, and Phlex components
298
- - `assets` - TailwindCSS and custom styling
299
- - `create-resource` - Resource scaffold options
300
- - `connect-resource` - Portal connection
293
+ - `plutonium-resource` - Resource architecture overview
294
+ - `plutonium-rodauth` - Authentication setup and configuration
295
+ - `plutonium-package` - Feature and portal packages
296
+ - `plutonium-portal` - Portal configuration
297
+ - `plutonium-views` - Custom pages, layouts, and Phlex components
298
+ - `plutonium-assets` - TailwindCSS and custom styling
299
+ - `plutonium-create-resource` - Resource scaffold options
300
+ - `plutonium-connect-resource` - Portal connection
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: interaction
2
+ name: plutonium-interaction
3
3
  description: Plutonium interactions - encapsulated business logic for custom actions
4
4
  ---
5
5
 
@@ -68,7 +68,7 @@ input :content, as: :text
68
68
  input :date, as: :date
69
69
  ```
70
70
 
71
- See `definition-fields` skill for all input types and options.
71
+ See `plutonium-definition-fields` skill for all input types and options.
72
72
 
73
73
  ## Presentation
74
74
 
@@ -103,25 +103,22 @@ end
103
103
  ### Success Outcomes
104
104
 
105
105
  ```ruby
106
- # Basic success
106
+ # Basic success (redirects automatically to resource)
107
107
  succeed(resource)
108
108
 
109
109
  # With message
110
110
  succeed(resource).with_message("Done!")
111
111
  succeed(resource).with_message("Warning!", :alert)
112
112
 
113
- # With redirect
114
- succeed(resource).with_redirect_response(posts_path)
113
+ # With custom redirect (only if different from default)
114
+ succeed(resource).with_redirect_response(custom_path)
115
115
 
116
116
  # With file download
117
117
  succeed(resource).with_file_response(file_path, filename: "report.pdf")
118
-
119
- # Chaining
120
- succeed(resource)
121
- .with_message("Created!")
122
- .with_redirect_response(resource_path(resource))
123
118
  ```
124
119
 
120
+ **Note:** Redirect is automatic on success - the controller redirects to the resource by default. Only use `with_redirect_response` if you need a different destination.
121
+
125
122
  ### Failure Outcomes
126
123
 
127
124
  ```ruby
@@ -183,10 +180,14 @@ class ArchiveInteraction < Plutonium::Resource::Interaction
183
180
  def execute
184
181
  resource.update!(archived: true)
185
182
  succeed(resource)
183
+ rescue ActiveRecord::RecordInvalid => e
184
+ failed(e.record.errors)
186
185
  end
187
186
  end
188
187
  ```
189
188
 
189
+ **Note:** `ActiveRecord::RecordInvalid` is NOT rescued automatically. Always rescue it when using bang methods (`create!`, `update!`, `save!`).
190
+
190
191
  ### Resource Actions
191
192
 
192
193
  Act at the collection/class level (no specific record):
@@ -208,11 +209,11 @@ end
208
209
 
209
210
  ### Bulk Actions (Multiple Records)
210
211
 
211
- Act on multiple selected records:
212
+ Act on multiple selected records. When registered, the table shows checkboxes and a toolbar appears when records are selected.
212
213
 
213
214
  ```ruby
214
215
  class BulkArchiveInteraction < Plutonium::Resource::Interaction
215
- attribute :resources # Collection of records
216
+ attribute :resources # Collection of records (note: plural)
216
217
 
217
218
  def execute
218
219
  resources.update_all(archived: true)
@@ -221,6 +222,8 @@ class BulkArchiveInteraction < Plutonium::Resource::Interaction
221
222
  end
222
223
  ```
223
224
 
225
+ **Authorization:** Bulk actions use per-record authorization. The policy method is checked for each selected record - if any fails, the entire request is rejected. The UI only shows actions that all selected records support.
226
+
224
227
  ## Connecting to Definitions
225
228
 
226
229
  Register interactions as actions:
@@ -344,9 +347,7 @@ class Company::InviteUserInteraction < Plutonium::Resource::Interaction
344
347
  )
345
348
  UserInviteMailer.invitation(invite).deliver_later
346
349
 
347
- succeed(resource)
348
- .with_message("Invitation sent to #{email}")
349
- .with_redirect_response(resource)
350
+ succeed(resource).with_message("Invitation sent to #{email}")
350
351
  rescue ActiveRecord::RecordInvalid => e
351
352
  failed(e.record.errors)
352
353
  end
@@ -376,7 +377,7 @@ end
376
377
 
377
378
  ## Related Skills
378
379
 
379
- - `definition-actions` - Declaring actions in definitions
380
- - `forms` - Custom interaction form templates
381
- - `policy` - Controlling access to actions
382
- - `resource` - How interactions fit in the architecture
380
+ - `plutonium-definition-actions` - Declaring actions in definitions
381
+ - `plutonium-forms` - Custom interaction form templates
382
+ - `plutonium-policy` - Controlling access to actions
383
+ - `plutonium-resource` - How interactions fit in the architecture
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: model
2
+ name: plutonium-model
3
3
  description: Overview of Plutonium resource models - structure, setup, and best practices
4
4
  ---
5
5
 
@@ -263,5 +263,5 @@ Models integrate with:
263
263
 
264
264
  ## Related Skills
265
265
 
266
- - `model-features` - has_cents, associations, scopes, routes
267
- - `create-resource` - Scaffold generator for new resources
266
+ - `plutonium-model-features` - has_cents, associations, scopes, routes
267
+ - `plutonium-create-resource` - Scaffold generator for new resources
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: model-features
2
+ name: plutonium-model-features
3
3
  description: Plutonium model features - has_cents, associations, scopes, and routing
4
4
  ---
5
5
 
@@ -282,5 +282,5 @@ sgid_array.each { |sgid| user.add_post_sgid(sgid) }
282
282
 
283
283
  ## Related Skills
284
284
 
285
- - `model` - Model overview and structure
286
- - `create-resource` - Scaffold generator
285
+ - `plutonium-model` - Model overview and structure
286
+ - `plutonium-create-resource` - Scaffold generator
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: nested-resources
2
+ name: plutonium-nested-resources
3
3
  description: Plutonium nested resources - parent/child routes, scoping, and URL generation
4
4
  ---
5
5
 
@@ -268,7 +268,7 @@ Generates nested routes:
268
268
 
269
269
  ## Related Skills
270
270
 
271
- - `portal` - Route registration
272
- - `policy` - Authorization and scoping
273
- - `controller` - Presentation hooks
274
- - `model-features` - associated_with scope
271
+ - `plutonium-portal` - Route registration
272
+ - `plutonium-policy` - Authorization and scoping
273
+ - `plutonium-controller` - Presentation hooks
274
+ - `plutonium-model-features` - associated_with scope
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: package
2
+ name: plutonium-package
3
3
  description: Plutonium packages - modular Rails engines for organizing features and portals
4
4
  ---
5
5
 
@@ -74,8 +74,7 @@ packages/admin_portal/
74
74
  │ ├── controllers/admin_portal/
75
75
  │ │ ├── concerns/controller.rb
76
76
  │ │ ├── dashboard_controller.rb
77
- │ │ ├── plutonium_controller.rb
78
- │ │ └── resource_controller.rb
77
+ │ │ └── plutonium_controller.rb
79
78
  │ ├── definitions/admin_portal/ # Portal-specific overrides
80
79
  │ ├── policies/admin_portal/ # Portal-specific overrides
81
80
  │ └── views/
@@ -101,7 +100,7 @@ module AdminPortal
101
100
  end
102
101
  ```
103
102
 
104
- See `portal` skill for portal-specific features.
103
+ See `plutonium-portal` skill for portal-specific features.
105
104
 
106
105
  ## Package Loading
107
106
 
@@ -185,7 +184,7 @@ rails db:migrate # Runs migrations from all packages
185
184
 
186
185
  ## Related Skills
187
186
 
188
- - `portal` - Portal-specific features (auth, entity scoping, routes)
189
- - `resource` - Resource architecture overview
190
- - `connect-resource` - Connecting resources to portals
191
- - `create-resource` - Creating resources
187
+ - `plutonium-portal` - Portal-specific features (auth, entity scoping, routes)
188
+ - `plutonium-resource` - Resource architecture overview
189
+ - `plutonium-connect-resource` - Connecting resources to portals
190
+ - `plutonium-create-resource` - Creating resources
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: policy
2
+ name: plutonium-policy
3
3
  description: Plutonium resource policies - authorization, attribute permissions, and scoping
4
4
  ---
5
5
 
@@ -87,6 +87,28 @@ end
87
87
 
88
88
  Actions are secure by default - undefined methods return `false`.
89
89
 
90
+ ### Bulk Action Authorization
91
+
92
+ Bulk actions (operating on multiple selected records) support **per-record authorization**:
93
+
94
+ ```ruby
95
+ def bulk_archive?
96
+ create? && !record.locked? # Per-record check
97
+ end
98
+
99
+ def bulk_publish?
100
+ user.admin? || record.author == user
101
+ end
102
+ ```
103
+
104
+ **How bulk authorization works:**
105
+ 1. Policy method (e.g., `bulk_archive?`) is checked **per record** in the selection
106
+ 2. **Backend:** If any selected record fails authorization, the entire request is rejected
107
+ 3. **UI:** Only actions that **all** selected records support are shown (intersection)
108
+ 4. Records are fetched via `current_authorized_scope` - only accessible records can be selected
109
+
110
+ This provides full per-record authorization while keeping the UI clean - users only see actions they can actually perform on their entire selection.
111
+
90
112
  ## Attribute Permissions
91
113
 
92
114
  ### Core Methods (Must Override for Production)
@@ -347,6 +369,6 @@ end
347
369
 
348
370
  ## Related Skills
349
371
 
350
- - `resource` - How policies fit in the resource architecture
351
- - `definition-actions` - Actions that need policy methods
352
- - `controller` - How controllers use policies
372
+ - `plutonium-resource` - How policies fit in the resource architecture
373
+ - `plutonium-definition-actions` - Actions that need policy methods
374
+ - `plutonium-controller` - How controllers use policies