plutonium 0.49.1 → 0.51.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 (206) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +572 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +163 -300
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +6 -5
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +37 -0
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1323 -1184
  14. data/app/assets/plutonium.js.map +4 -4
  15. data/app/assets/plutonium.min.js +50 -49
  16. data/app/assets/plutonium.min.js.map +4 -4
  17. data/app/views/plutonium/_resource_header.html.erb +4 -4
  18. data/app/views/plutonium/_resource_sidebar.html.erb +9 -9
  19. data/app/views/resource/_resource_grid.html.erb +1 -0
  20. data/config/brakeman.ignore +25 -2
  21. data/docs/.vitepress/config.ts +37 -27
  22. data/docs/getting-started/index.md +22 -29
  23. data/docs/getting-started/installation.md +37 -80
  24. data/docs/getting-started/tutorial/index.md +4 -5
  25. data/docs/guides/adding-resources.md +66 -377
  26. data/docs/guides/authentication.md +94 -463
  27. data/docs/guides/authorization.md +124 -370
  28. data/docs/guides/creating-packages.md +94 -296
  29. data/docs/guides/custom-actions.md +121 -441
  30. data/docs/guides/index.md +22 -42
  31. data/docs/guides/multi-tenancy.md +116 -187
  32. data/docs/guides/nested-resources.md +103 -431
  33. data/docs/guides/search-filtering.md +123 -240
  34. data/docs/guides/testing.md +5 -4
  35. data/docs/guides/theming.md +157 -407
  36. data/docs/guides/troubleshooting.md +5 -3
  37. data/docs/guides/user-invites.md +106 -425
  38. data/docs/guides/user-profile.md +76 -243
  39. data/docs/index.md +1 -1
  40. data/docs/reference/app/generators.md +517 -0
  41. data/docs/reference/app/index.md +158 -0
  42. data/docs/reference/app/packages.md +146 -0
  43. data/docs/reference/app/portals.md +377 -0
  44. data/docs/reference/auth/accounts.md +230 -0
  45. data/docs/reference/auth/index.md +88 -0
  46. data/docs/reference/auth/profile.md +185 -0
  47. data/docs/reference/behavior/controllers.md +395 -0
  48. data/docs/reference/behavior/index.md +22 -0
  49. data/docs/reference/behavior/interactions.md +341 -0
  50. data/docs/reference/behavior/policies.md +417 -0
  51. data/docs/reference/index.md +56 -49
  52. data/docs/reference/resource/actions.md +423 -0
  53. data/docs/reference/resource/definition.md +508 -0
  54. data/docs/reference/resource/index.md +50 -0
  55. data/docs/reference/resource/model.md +348 -0
  56. data/docs/reference/resource/query.md +305 -0
  57. data/docs/reference/tenancy/entity-scoping.md +361 -0
  58. data/docs/reference/tenancy/index.md +36 -0
  59. data/docs/reference/tenancy/invites.md +393 -0
  60. data/docs/reference/tenancy/nested-resources.md +267 -0
  61. data/docs/reference/testing/index.md +287 -0
  62. data/docs/reference/ui/assets.md +400 -0
  63. data/docs/reference/ui/components.md +165 -0
  64. data/docs/reference/ui/displays.md +104 -0
  65. data/docs/reference/ui/forms.md +284 -0
  66. data/docs/reference/ui/index.md +30 -0
  67. data/docs/reference/ui/layouts.md +106 -0
  68. data/docs/reference/ui/pages.md +189 -0
  69. data/docs/reference/ui/tables.md +117 -0
  70. data/docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md +841 -0
  71. data/docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md.tasks.json +103 -0
  72. data/docs/superpowers/specs/2026-05-07-ui-layout-overhaul-design.md +270 -0
  73. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  74. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  75. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  76. data/gemfiles/rails_7.gemfile.lock +1 -1
  77. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  78. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  79. data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +1 -0
  80. data/lib/generators/pu/invites/install_generator.rb +1 -0
  81. data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +54 -5
  82. data/lib/plutonium/action/base.rb +44 -1
  83. data/lib/plutonium/action/interactive.rb +1 -1
  84. data/lib/plutonium/configuration.rb +4 -0
  85. data/lib/plutonium/definition/actions.rb +3 -0
  86. data/lib/plutonium/definition/base.rb +8 -0
  87. data/lib/plutonium/definition/index_views.rb +95 -0
  88. data/lib/plutonium/definition/metadata.rb +40 -0
  89. data/lib/plutonium/helpers/turbo_helper.rb +12 -1
  90. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  91. data/lib/plutonium/interaction/response/redirect.rb +1 -1
  92. data/lib/plutonium/query/base.rb +8 -0
  93. data/lib/plutonium/query/filters/association.rb +30 -8
  94. data/lib/plutonium/query/filters/boolean.rb +5 -0
  95. data/lib/plutonium/resource/controller.rb +1 -0
  96. data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
  97. data/lib/plutonium/resource/controllers/presentable.rb +11 -2
  98. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  99. data/lib/plutonium/resource/definition.rb +42 -0
  100. data/lib/plutonium/resource/policy.rb +7 -0
  101. data/lib/plutonium/resource/query_object.rb +64 -6
  102. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  103. data/lib/plutonium/testing/resource_definition.rb +2 -2
  104. data/lib/plutonium/ui/action_button.rb +4 -2
  105. data/lib/plutonium/ui/component/kit.rb +12 -0
  106. data/lib/plutonium/ui/component/methods.rb +4 -0
  107. data/lib/plutonium/ui/display/base.rb +3 -1
  108. data/lib/plutonium/ui/display/resource.rb +109 -25
  109. data/lib/plutonium/ui/display/theme.rb +2 -1
  110. data/lib/plutonium/ui/dyna_frame/content.rb +8 -14
  111. data/lib/plutonium/ui/empty_card.rb +1 -1
  112. data/lib/plutonium/ui/form/base.rb +35 -3
  113. data/lib/plutonium/ui/form/components/hidden_wrapper.rb +25 -0
  114. data/lib/plutonium/ui/form/components/json.rb +58 -0
  115. data/lib/plutonium/ui/form/components/resource_select.rb +133 -1
  116. data/lib/plutonium/ui/form/components/secure_association.rb +105 -24
  117. data/lib/plutonium/ui/form/components/sticky_footer.rb +17 -0
  118. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  119. data/lib/plutonium/ui/form/resource.rb +45 -10
  120. data/lib/plutonium/ui/form/theme.rb +1 -1
  121. data/lib/plutonium/ui/frame_navigator_panel.rb +7 -4
  122. data/lib/plutonium/ui/grid/card.rb +235 -0
  123. data/lib/plutonium/ui/grid/resource.rb +149 -0
  124. data/lib/plutonium/ui/layout/base.rb +38 -1
  125. data/lib/plutonium/ui/layout/header.rb +1 -2
  126. data/lib/plutonium/ui/layout/icon_rail.rb +212 -0
  127. data/lib/plutonium/ui/layout/resource_layout.rb +10 -3
  128. data/lib/plutonium/ui/layout/sidebar.rb +12 -24
  129. data/lib/plutonium/ui/layout/topbar.rb +100 -0
  130. data/lib/plutonium/ui/modal/base.rb +109 -0
  131. data/lib/plutonium/ui/modal/centered.rb +21 -0
  132. data/lib/plutonium/ui/modal/slideover.rb +26 -0
  133. data/lib/plutonium/ui/page/base.rb +18 -6
  134. data/lib/plutonium/ui/page/edit.rb +13 -1
  135. data/lib/plutonium/ui/page/index.rb +40 -1
  136. data/lib/plutonium/ui/page/interactive_action.rb +8 -39
  137. data/lib/plutonium/ui/page/new.rb +13 -1
  138. data/lib/plutonium/ui/page/show.rb +8 -1
  139. data/lib/plutonium/ui/page_header.rb +8 -13
  140. data/lib/plutonium/ui/panel.rb +10 -19
  141. data/lib/plutonium/ui/sidebar_menu.rb +2 -25
  142. data/lib/plutonium/ui/tab_list.rb +29 -7
  143. data/lib/plutonium/ui/table/base.rb +106 -0
  144. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +12 -4
  145. data/lib/plutonium/ui/table/components/filter_form.rb +171 -0
  146. data/lib/plutonium/ui/table/components/filter_pills.rb +89 -0
  147. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +13 -12
  148. data/lib/plutonium/ui/table/components/scopes_pills.rb +67 -0
  149. data/lib/plutonium/ui/table/components/selection_column.rb +2 -11
  150. data/lib/plutonium/ui/table/components/toolbar.rb +104 -0
  151. data/lib/plutonium/ui/table/components/view_switcher.rb +81 -0
  152. data/lib/plutonium/ui/table/resource.rb +158 -89
  153. data/lib/plutonium/ui/table/theme.rb +14 -5
  154. data/lib/plutonium/version.rb +1 -1
  155. data/lib/plutonium.rb +14 -0
  156. data/lib/tasks/release.rake +15 -1
  157. data/package.json +10 -10
  158. data/src/css/components.css +304 -131
  159. data/src/css/slim_select.css +4 -0
  160. data/src/css/tokens.css +101 -85
  161. data/src/js/controllers/autosubmit_controller.js +24 -0
  162. data/src/js/controllers/bulk_actions_controller.js +15 -16
  163. data/src/js/controllers/capture_url_controller.js +14 -0
  164. data/src/js/controllers/filter_panel_controller.js +77 -19
  165. data/src/js/controllers/frame_navigator_controller.js +34 -6
  166. data/src/js/controllers/icon_rail_controller.js +22 -0
  167. data/src/js/controllers/icon_rail_flyout_controller.js +128 -0
  168. data/src/js/controllers/register_controllers.js +16 -0
  169. data/src/js/controllers/resource_tab_list_controller.js +56 -3
  170. data/src/js/controllers/row_click_controller.js +21 -0
  171. data/src/js/controllers/slim_select_controller.js +61 -0
  172. data/src/js/controllers/table_column_menu_controller.js +43 -0
  173. data/src/js/controllers/table_header_controller.js +16 -0
  174. data/src/js/controllers/view_switcher_controller.js +29 -0
  175. data/src/js/turbo/turbo_actions.js +33 -0
  176. data/yarn.lock +553 -543
  177. metadata +71 -32
  178. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  179. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  180. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  181. data/.claude/skills/plutonium-definition/SKILL.md +0 -1138
  182. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  183. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  184. data/.claude/skills/plutonium-installation/SKILL.md +0 -325
  185. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  186. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  187. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  188. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  189. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  190. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  191. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  192. data/.claude/skills/plutonium-views/SKILL.md +0 -592
  193. data/docs/reference/assets/index.md +0 -496
  194. data/docs/reference/controller/index.md +0 -412
  195. data/docs/reference/definition/actions.md +0 -449
  196. data/docs/reference/definition/fields.md +0 -383
  197. data/docs/reference/definition/index.md +0 -268
  198. data/docs/reference/definition/query.md +0 -351
  199. data/docs/reference/generators/index.md +0 -648
  200. data/docs/reference/interaction/index.md +0 -449
  201. data/docs/reference/model/features.md +0 -248
  202. data/docs/reference/model/index.md +0 -218
  203. data/docs/reference/policy/index.md +0 -456
  204. data/docs/reference/portal/index.md +0 -379
  205. data/docs/reference/views/forms.md +0 -411
  206. data/docs/reference/views/index.md +0 -501
@@ -0,0 +1,423 @@
1
+ # Actions
2
+
3
+ Custom buttons that go beyond standard CRUD — publish, archive, import, send invitation, etc. Two flavors:
4
+
5
+ - **Simple actions** — navigate to an existing URL.
6
+ - **Interactive actions** — run an [Interaction](/reference/behavior/interactions), optionally collecting input via a modal form.
7
+
8
+ ## 🚨 Critical
9
+
10
+ - **Every custom action needs a policy method.** `action :publish` requires `def publish?` on the policy. Undefined methods return `false`, so the action silently disappears.
11
+ - **For interactive actions, visibility is inferred from the interaction's attributes.** Don't declare `record_action: true` / `bulk_action: true` etc. by hand unless you're opting OUT.
12
+ - **Bulk action authorization is per-record.** If any selected record fails the policy check, the entire request is rejected.
13
+ - **Always pass `as:`** on custom routes — without it, `resource_url_for` can't generate URLs (critical for nested resources).
14
+ - **Prefer interactive actions over hand-written controller routes.** Anything with business logic belongs in an interaction.
15
+
16
+ ## Action visibility flags
17
+
18
+ | Flag | Where the button appears |
19
+ |---|---|
20
+ | `resource_action: true` | Index page (top toolbar) — for actions that operate on the collection (Import, Export, Create) |
21
+ | `record_action: true` | Show page — for actions on a single record (Edit, Archive, Delete) |
22
+ | `collection_record_action: true` | Per-row in the index table — for quick actions (Edit, Show) |
23
+ | `bulk_action: true` | Bulk-actions toolbar (shown when records are selected) |
24
+
25
+ ### Inferred visibility (interactive actions)
26
+
27
+ For `interaction:`-based actions, all four flags are **inferred from the interaction's attributes** — don't declare them by hand:
28
+
29
+ | Interaction declares | Inferred flags |
30
+ |---|---|
31
+ | `attribute :resource` | `record_action: true` + `collection_record_action: true` |
32
+ | `attribute :resources` (plural) | `bulk_action: true` |
33
+ | neither | `resource_action: true` |
34
+
35
+ User-supplied flags override the inferred ones, but only **opt-out** makes sense — the interaction's `attribute :resource` / `attribute :resources` already fixes its semantic shape:
36
+
37
+ ```ruby
38
+ # :resource interaction → defaults to record_action + collection_record_action.
39
+ # Hide from per-row menu, keep on show page:
40
+ action :archive, interaction: ArchiveInteraction, collection_record_action: false
41
+
42
+ # Hide from show page, keep per-row button:
43
+ action :preview, interaction: PreviewInteraction, record_action: false
44
+ ```
45
+
46
+ Declare the flags manually for **simple/navigation actions** (no `interaction:`) or when opting out of an inferred slot.
47
+
48
+ ## Action options
49
+
50
+ ```ruby
51
+ action :name,
52
+ # Display
53
+ label: "Custom Label", # default: name.titleize
54
+ description: "What it does",
55
+ icon: Phlex::TablerIcons::Star,
56
+ color: :danger, # :primary, :secondary, :danger
57
+
58
+ # Visibility (combine as needed)
59
+ resource_action: true,
60
+ record_action: true,
61
+ collection_record_action: true,
62
+ bulk_action: true,
63
+
64
+ # Grouping
65
+ category: :primary, # :primary, :secondary, :danger
66
+ position: 50, # display order (lower = first)
67
+
68
+ # Behavior
69
+ confirmation: "Are you sure?",
70
+ turbo_frame: "_top",
71
+ return_to: "/custom/path",
72
+ route_options: {action: :foo},
73
+ modal: :slideover # :centered (default) or :slideover — chrome for the action's interaction form
74
+ ```
75
+
76
+ ### Deriving variants — `Action#with(...)`
77
+
78
+ Action records are frozen value objects. Inside `customize_actions`, derive a copy with overrides:
79
+
80
+ ```ruby
81
+ def customize_actions
82
+ defined_actions[:edit] = defined_actions[:edit].with(turbo_frame: "_top")
83
+ end
84
+ ```
85
+
86
+ ## Simple actions (navigation)
87
+
88
+ Link to an existing route. The target route MUST exist.
89
+
90
+ ```ruby
91
+ class PostDefinition < Plutonium::Resource::Definition
92
+ # External URL
93
+ action :documentation,
94
+ label: "Documentation",
95
+ route_options: {url: "https://docs.example.com"},
96
+ icon: Phlex::TablerIcons::Book,
97
+ resource_action: true
98
+
99
+ # Custom controller action
100
+ action :reports,
101
+ route_options: {action: :reports},
102
+ icon: Phlex::TablerIcons::ChartBar,
103
+ resource_action: true
104
+ end
105
+ ```
106
+
107
+ ::: warning Custom routes need `as:`
108
+ ```ruby
109
+ resources :posts do
110
+ collection { get :reports, as: :reports } # ← `as:` required
111
+ end
112
+ ```
113
+
114
+ Without it, `resource_url_for` can't build the URL — particularly critical for nested resources.
115
+ :::
116
+
117
+ For anything with business logic, use an **interactive action** instead.
118
+
119
+ ## Interactive actions
120
+
121
+ Run an [Interaction](/reference/behavior/interactions) — automatically renders a form if the interaction declares attributes beyond `:resource`/`:resources`, otherwise executes immediately with a confirmation.
122
+
123
+ ```ruby
124
+ class PostDefinition < Plutonium::Resource::Definition
125
+ action :publish, interaction: PublishInteraction
126
+
127
+ action :archive, interaction: ArchiveInteraction,
128
+ color: :danger,
129
+ category: :danger,
130
+ position: 1000,
131
+ confirmation: "Are you sure?"
132
+ end
133
+ ```
134
+
135
+ ### Per-record interaction (record action)
136
+
137
+ ```ruby
138
+ class ArchiveInteraction < ResourceInteraction
139
+ presents label: "Archive",
140
+ icon: Phlex::TablerIcons::Archive,
141
+ description: "Move to archive"
142
+
143
+ attribute :resource
144
+
145
+ def execute
146
+ resource.archived!
147
+ succeed(resource).with_message("Record archived.")
148
+ rescue ActiveRecord::RecordInvalid => e
149
+ failed(e.record.errors)
150
+ end
151
+ end
152
+ ```
153
+
154
+ Register:
155
+
156
+ ```ruby
157
+ action :archive, interaction: ArchiveInteraction
158
+ # record_action + collection_record_action inferred automatically
159
+ ```
160
+
161
+ ### With form inputs
162
+
163
+ The interaction declares extra `attribute` and `input` lines → a modal form renders before execution.
164
+
165
+ ```ruby
166
+ class InviteUserInteraction < Plutonium::Resource::Interaction
167
+ presents label: "Invite User", icon: Phlex::TablerIcons::Mail
168
+
169
+ attribute :resource # the company
170
+ attribute :email
171
+ attribute :role
172
+
173
+ input :email, as: :email
174
+ input :role, as: :select, choices: %w[admin member viewer]
175
+
176
+ validates :email, presence: true, format: {with: URI::MailTo::EMAIL_REGEXP}
177
+ validates :role, presence: true
178
+
179
+ def execute
180
+ UserInvite.create!(company: resource, email: email, role: role)
181
+ succeed(resource).with_message("Invitation sent to #{email}.")
182
+ rescue ActiveRecord::RecordInvalid => e
183
+ failed(e.record.errors)
184
+ end
185
+ end
186
+ ```
187
+
188
+ ### Bulk action
189
+
190
+ Plural `attribute :resources` → bulk action. The index table shows checkboxes and a bulk-actions toolbar.
191
+
192
+ ```ruby
193
+ class BulkArchiveInteraction < Plutonium::Resource::Interaction
194
+ presents label: "Archive Selected", icon: Phlex::TablerIcons::Archive
195
+
196
+ attribute :resources # array of records
197
+
198
+ def execute
199
+ resources.each(&:archived!)
200
+ succeed(resources).with_message("#{resources.size} records archived.")
201
+ rescue => error
202
+ failed("Bulk archive failed: #{error.message}")
203
+ end
204
+ end
205
+ ```
206
+
207
+ Register:
208
+
209
+ ```ruby
210
+ action :bulk_archive, interaction: BulkArchiveInteraction
211
+ # bulk_action: true inferred from `attribute :resources`
212
+ ```
213
+
214
+ Policy — checked per record; fails the whole request if ANY record is unauthorized:
215
+
216
+ ```ruby
217
+ def bulk_archive?
218
+ user.admin? || record.author == user
219
+ end
220
+ ```
221
+
222
+ The UI only shows bulk actions that ALL selected records support. Records are fetched via `current_authorized_scope` — users can only select records they can access.
223
+
224
+ ### Resource action (no record)
225
+
226
+ Neither `:resource` nor `:resources` → resource action (shown on the index page).
227
+
228
+ ```ruby
229
+ class ImportInteraction < Plutonium::Resource::Interaction
230
+ presents label: "Import CSV", icon: Phlex::TablerIcons::Upload
231
+
232
+ attribute :file
233
+ input :file, as: :file
234
+ validates :file, presence: true
235
+
236
+ def execute
237
+ succeed(nil).with_message("Import completed.")
238
+ end
239
+ end
240
+ ```
241
+
242
+ ```ruby
243
+ action :import, interaction: ImportInteraction
244
+ ```
245
+
246
+ ## Immediate vs form
247
+
248
+ | Interaction shape | Behavior |
249
+ |---|---|
250
+ | Only `:resource` / `:resources` (no extra inputs) | **Immediate** — browser confirmation (`"#{label}?"`, e.g. `"Archive?"`), then runs. Override with `confirmation: "Custom"` or `confirmation: false`. |
251
+ | Additional `attribute` / `input` declared | **Form** — renders the action's form in a modal first; no auto-confirmation (the form is the confirmation). |
252
+
253
+ ## Built-in CRUD actions
254
+
255
+ These are defined by default on every definition:
256
+
257
+ ```ruby
258
+ action :new,
259
+ route_options: {action: :new},
260
+ resource_action: true,
261
+ category: :primary,
262
+ icon: Phlex::TablerIcons::Plus,
263
+ position: 10
264
+
265
+ action :show,
266
+ route_options: {action: :show},
267
+ collection_record_action: true,
268
+ icon: Phlex::TablerIcons::Eye,
269
+ position: 10
270
+
271
+ action :edit,
272
+ route_options: {action: :edit},
273
+ record_action: true,
274
+ collection_record_action: true,
275
+ icon: Phlex::TablerIcons::Edit,
276
+ position: 20
277
+
278
+ action :destroy,
279
+ route_options: {method: :delete},
280
+ record_action: true,
281
+ collection_record_action: true,
282
+ category: :danger,
283
+ icon: Phlex::TablerIcons::Trash,
284
+ position: 100,
285
+ confirmation: "Are you sure?",
286
+ turbo_frame: "_top"
287
+ ```
288
+
289
+ ### Customizing built-ins
290
+
291
+ Re-declare with the options you want changed:
292
+
293
+ ```ruby
294
+ class PostDefinition < ResourceDefinition
295
+ action :destroy,
296
+ confirmation: "This will permanently delete the post and all comments.",
297
+ route_options: {method: :delete},
298
+ record_action: true,
299
+ collection_record_action: true,
300
+ category: :danger,
301
+ icon: Phlex::TablerIcons::Trash,
302
+ position: 100,
303
+ turbo_frame: "_top"
304
+ end
305
+ ```
306
+
307
+ ## Interaction responses
308
+
309
+ ```ruby
310
+ def execute
311
+ # Success — redirects to resource automatically
312
+ succeed(resource).with_message("Done!")
313
+
314
+ # Different redirect destination
315
+ succeed(resource)
316
+ .with_redirect_response(custom_dashboard_path)
317
+ .with_message("Redirecting...")
318
+
319
+ # Failures
320
+ failed(resource.errors)
321
+ failed("Something went wrong")
322
+ failed("Invalid value", :email) # attaches error to a specific attribute
323
+ failed(email: "is invalid", name: "is required") # hash form
324
+ end
325
+ ```
326
+
327
+ ::: tip Automatic redirect on success
328
+ You only need `with_redirect_response` for a non-default destination. The controller redirects to the resource (show page) by default.
329
+ :::
330
+
331
+ ## Route options
332
+
333
+ ```ruby
334
+ # Simple route to controller action
335
+ action :preview,
336
+ route_options: {action: :preview},
337
+ record_action: true
338
+
339
+ # Custom HTTP method
340
+ action :archive,
341
+ route_options: {method: :post, action: :archive},
342
+ record_action: true
343
+
344
+ # External URL
345
+ action :docs,
346
+ route_options: {url: "https://docs.example.com"},
347
+ resource_action: true
348
+
349
+ # Custom URL resolver
350
+ action :create_deployment,
351
+ route_options: Plutonium::Action::RouteOptions.new(
352
+ url_resolver: ->(subject) {
353
+ resource_url_for(Deployment, action: :new, parent: subject)
354
+ }
355
+ ),
356
+ record_action: true
357
+ ```
358
+
359
+ ## Inherited actions
360
+
361
+ Actions defined on the base `ResourceDefinition` are inherited by every resource:
362
+
363
+ ```ruby
364
+ # app/definitions/resource_definition.rb
365
+ class ResourceDefinition < Plutonium::Resource::Definition
366
+ action :archive, interaction: ArchiveInteraction, color: :danger, position: 1000
367
+ end
368
+
369
+ # All resources inherit :archive automatically
370
+ class PostDefinition < ResourceDefinition
371
+ end
372
+ ```
373
+
374
+ ## Portal-specific actions
375
+
376
+ ```ruby
377
+ class AdminPortal::PostDefinition < ::PostDefinition
378
+ action :feature, interaction: FeaturePostInteraction
379
+ action :bulk_publish, interaction: BulkPublishInteraction
380
+ end
381
+ ```
382
+
383
+ ## Authorization
384
+
385
+ The policy method name matches the action name plus `?`:
386
+
387
+ ```ruby
388
+ class PostPolicy < ResourcePolicy
389
+ def publish? = user.admin? || record.author == user
390
+ def archive? = user.admin?
391
+ def import? = create?
392
+ end
393
+ ```
394
+
395
+ Undefined → action returns `false` → button doesn't appear. See [Behavior › Policy](/reference/behavior/policies) for the full policy surface.
396
+
397
+ ## Common patterns
398
+
399
+ ### Archive / restore
400
+
401
+ ```ruby
402
+ action :archive,
403
+ interaction: ArchiveInteraction,
404
+ color: :danger
405
+
406
+ action :restore,
407
+ interaction: RestoreInteraction
408
+ ```
409
+
410
+ ### Export
411
+
412
+ ```ruby
413
+ action :export,
414
+ interaction: ExportInteraction,
415
+ icon: Phlex::TablerIcons::Download
416
+ ```
417
+
418
+ ## Related
419
+
420
+ - [Definition](./definition) — fields, page chrome
421
+ - [Query](./query) — search, filters, scopes
422
+ - [Behavior › Interactions](/reference/behavior/interactions) — writing interaction classes
423
+ - [Behavior › Policy](/reference/behavior/policies) — authorizing custom actions