plutonium 0.65.0 → 0.66.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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +43 -43
  3. data/.claude/skills/plutonium-app/SKILL.md +101 -59
  4. data/.claude/skills/plutonium-async-interactions/SKILL.md +19 -19
  5. data/.claude/skills/plutonium-auth/SKILL.md +119 -53
  6. data/.claude/skills/plutonium-behavior/SKILL.md +110 -78
  7. data/.claude/skills/plutonium-dashboard/SKILL.md +11 -4
  8. data/.claude/skills/plutonium-kanban/SKILL.md +81 -49
  9. data/.claude/skills/plutonium-resource/SKILL.md +144 -133
  10. data/.claude/skills/plutonium-tenancy/SKILL.md +104 -55
  11. data/.claude/skills/plutonium-testing/SKILL.md +130 -33
  12. data/.claude/skills/plutonium-ui/SKILL.md +151 -96
  13. data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
  14. data/CHANGELOG.md +21 -0
  15. data/README.md +9 -9
  16. data/SECURITY.md +1 -1
  17. data/app/assets/plutonium.css +1 -1
  18. data/docs/.vitepress/sync-skills.mjs +6 -3
  19. data/docs/blog/introducing-plutonium-dashboards.md +4 -5
  20. data/docs/blog/introducing-plutonium-i18n.md +4 -5
  21. data/docs/getting-started/installation.md +5 -5
  22. data/docs/getting-started/tutorial/02-first-resource.md +3 -3
  23. data/docs/getting-started/tutorial/03-authentication.md +7 -7
  24. data/docs/getting-started/tutorial/04-authorization.md +21 -4
  25. data/docs/getting-started/tutorial/05-custom-actions.md +2 -2
  26. data/docs/getting-started/tutorial/06-nested-resources.md +5 -2
  27. data/docs/getting-started/tutorial/07-author-portal.md +2 -2
  28. data/docs/getting-started/tutorial/08-customizing-ui.md +45 -30
  29. data/docs/getting-started/tutorial/index.md +1 -1
  30. data/docs/guides/adding-resources.md +10 -7
  31. data/docs/guides/authentication.md +25 -25
  32. data/docs/guides/authorization.md +24 -24
  33. data/docs/guides/creating-packages.md +17 -17
  34. data/docs/guides/custom-actions.md +32 -32
  35. data/docs/guides/customizing-ui.md +29 -26
  36. data/docs/guides/dashboards.md +1 -1
  37. data/docs/guides/index.md +3 -3
  38. data/docs/guides/kanban.md +55 -55
  39. data/docs/guides/multi-tenancy.md +35 -22
  40. data/docs/guides/nested-resources.md +21 -21
  41. data/docs/guides/performance.md +3 -3
  42. data/docs/guides/search-filtering.md +13 -13
  43. data/docs/guides/testing.md +16 -12
  44. data/docs/guides/theming.md +32 -17
  45. data/docs/guides/troubleshooting.md +2 -2
  46. data/docs/guides/user-invites.md +17 -17
  47. data/docs/guides/user-profile.md +51 -24
  48. data/docs/guides/wizards.md +55 -55
  49. data/docs/reference/app/generators.md +22 -22
  50. data/docs/reference/app/index.md +15 -18
  51. data/docs/reference/app/packages.md +8 -8
  52. data/docs/reference/app/portals.md +75 -29
  53. data/docs/reference/auth/accounts.md +15 -15
  54. data/docs/reference/auth/index.md +12 -12
  55. data/docs/reference/auth/profile.md +67 -29
  56. data/docs/reference/behavior/async-interactions.md +24 -24
  57. data/docs/reference/behavior/controllers.md +28 -28
  58. data/docs/reference/behavior/index.md +5 -5
  59. data/docs/reference/behavior/interactions.md +44 -44
  60. data/docs/reference/behavior/policies.md +48 -28
  61. data/docs/reference/configuration.md +6 -6
  62. data/docs/reference/dashboard/dsl.md +2 -2
  63. data/docs/reference/dashboard/index.md +1 -1
  64. data/docs/reference/generators/lite.md +7 -7
  65. data/docs/reference/i18n.md +23 -0
  66. data/docs/reference/index.md +1 -1
  67. data/docs/reference/kanban/authorization.md +9 -9
  68. data/docs/reference/kanban/dsl.md +32 -32
  69. data/docs/reference/kanban/index.md +1 -1
  70. data/docs/reference/kanban/positioning.md +17 -15
  71. data/docs/reference/resource/actions.md +51 -51
  72. data/docs/reference/resource/definition.md +73 -73
  73. data/docs/reference/resource/export.md +6 -6
  74. data/docs/reference/resource/index.md +16 -16
  75. data/docs/reference/resource/model.md +24 -24
  76. data/docs/reference/resource/positioning.md +78 -76
  77. data/docs/reference/resource/query.md +13 -13
  78. data/docs/reference/tenancy/entity-scoping.md +65 -35
  79. data/docs/reference/tenancy/index.md +11 -11
  80. data/docs/reference/tenancy/invites.md +20 -20
  81. data/docs/reference/tenancy/nested-resources.md +13 -13
  82. data/docs/reference/testing/index.md +116 -22
  83. data/docs/reference/ui/assets.md +57 -25
  84. data/docs/reference/ui/components.md +20 -20
  85. data/docs/reference/ui/displays.md +14 -14
  86. data/docs/reference/ui/forms.md +35 -35
  87. data/docs/reference/ui/index.md +17 -15
  88. data/docs/reference/ui/layouts.md +21 -21
  89. data/docs/reference/ui/pages.md +22 -22
  90. data/docs/reference/ui/tables.md +7 -7
  91. data/docs/reference/wizard/anchoring-resume.md +33 -32
  92. data/docs/reference/wizard/dsl.md +44 -44
  93. data/docs/reference/wizard/index.md +6 -6
  94. data/docs/reference/wizard/one-time.md +18 -18
  95. data/docs/reference/wizard/registration-launch.md +32 -32
  96. data/docs/reference/wizard/storage-config.md +23 -23
  97. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  98. data/lib/generators/pu/profile/conn_generator.rb +6 -0
  99. data/lib/plutonium/resource/record/associated_with.rb +23 -2
  100. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
  101. data/lib/plutonium/version.rb +1 -1
  102. data/package.json +1 -1
  103. data/src/css/components.css +10 -10
  104. metadata +2 -2
@@ -1,32 +1,32 @@
1
1
  # Actions
2
2
 
3
- Custom buttons that go beyond standard CRUD — publish, archive, import, send invitation, etc. Two flavors:
3
+ Custom buttons that go beyond standard CRUD: publish, archive, import, send invitation, etc. Two flavors:
4
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.
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
7
 
8
8
  ## 🚨 Critical
9
9
 
10
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
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
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).
13
+ - **Always pass `as:`** on custom routes: without it, `resource_url_for` can't generate URLs (critical for nested resources).
14
14
  - **Prefer interactive actions over hand-written controller routes.** Anything a user triggers from a page belongs behind an interaction.
15
- - **An interaction is the button, not the operation.** Logic may start in `execute`; once a job or an API also needs it, it moves to the model — see [Behavior › Interactions](/reference/behavior/interactions#what-an-interaction-is-for).
15
+ - **An interaction is the button, not the operation.** Logic may start in `execute`; once a job or an API also needs it, it moves to the model, see [Behavior › Interactions](/reference/behavior/interactions#what-an-interaction-is-for).
16
16
 
17
17
  ## Action visibility flags
18
18
 
19
19
  | Flag | Where the button appears |
20
20
  |---|---|
21
- | `resource_action: true` | Index page (top toolbar) — for actions that operate on the collection (Import, Export, Create) |
22
- | `record_action: true` | Show page — for actions on a single record (Edit, Archive, Delete) |
23
- | `collection_record_action: true` | Per-row in the index table — for quick actions (Edit, Show) |
21
+ | `resource_action: true` | Index page (top toolbar): for actions that operate on the collection (Import, Export, Create) |
22
+ | `record_action: true` | Show page: for actions on a single record (Edit, Archive, Delete) |
23
+ | `collection_record_action: true` | Per-row in the index table: for quick actions (Edit, Show) |
24
24
  | `bulk_action: true` | Bulk-actions toolbar (shown when records are selected) |
25
- | `hidden: true` | **Nowhere.** Suppresses all four surfaces at once, while keeping the route and policy live — see [Hidden actions](#hidden-actions) |
25
+ | `hidden: true` | **Nowhere.** Suppresses all four surfaces at once, while keeping the route and policy live, see [Hidden actions](#hidden-actions) |
26
26
 
27
27
  ### Inferred visibility (interactive actions)
28
28
 
29
- For `interaction:`-based actions, all four flags are **inferred from the interaction's attributes** — don't declare them by hand:
29
+ For `interaction:`-based actions, all four flags are **inferred from the interaction's attributes**; don't declare them by hand:
30
30
 
31
31
  | Interaction declares | Inferred flags |
32
32
  |---|---|
@@ -34,7 +34,7 @@ For `interaction:`-based actions, all four flags are **inferred from the interac
34
34
  | `attribute :resources` (plural) | `bulk_action: true` |
35
35
  | neither | `resource_action: true` |
36
36
 
37
- 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:
37
+ 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:
38
38
 
39
39
  ```ruby
40
40
  # :resource interaction → defaults to record_action + collection_record_action.
@@ -63,10 +63,10 @@ action :name,
63
63
  collection_record_action: true,
64
64
  bulk_action: true,
65
65
 
66
- # Conditional visibility — display-only proc, NOT authorization (see below)
66
+ # Conditional visibility: display-only proc, NOT authorization (see below)
67
67
  condition: -> { params[:beta] == "1" },
68
68
 
69
- # Never render, anywhere — route + policy stay live (see below)
69
+ # Never render, anywhere: route + policy stay live (see below)
70
70
  hidden: true,
71
71
 
72
72
  # Grouping
@@ -78,19 +78,19 @@ action :name,
78
78
  turbo_frame: "_top",
79
79
  return_to: "/custom/path",
80
80
  route_options: {action: :foo},
81
- modal: :slideover, # :slideover / :centered — overrides the definition's modal mode
82
- size: :lg, # :sm / :md / :lg / :xl / :auto / :full — overrides the definition's modal size
81
+ modal: :slideover, # :slideover / :centered; overrides the definition's modal mode
82
+ size: :lg, # :sm / :md / :lg / :xl / :auto / :full; overrides the definition's modal size
83
83
 
84
84
  # HTML attributes (see below)
85
85
  link: {target: "_blank", rel: "noopener"}, # merged onto the action's <a> renderings
86
86
  button: {data: {analytics: "archive"}} # merged onto the button_to <form> (non-GET)
87
87
  ```
88
88
 
89
- ### HTML attributes — `link:` / `button:`
89
+ ### HTML attributes: `link:` / `button:`
90
90
 
91
- Two per-element attribute bags, deep-merged over the framework's own attributes at render time — **the author wins on every key**, recursively through nested `data`:
91
+ Two per-element attribute bags, deep-merged over the framework's own attributes at render time; **the author wins on every key**, recursively through nested `data`:
92
92
 
93
- - **`link:`** applies to every `<a>` rendered for the action: the toolbar link (GET), dropdown items (**any** HTTP method — dropdown items are always anchors, submitting via `data-turbo-method`), bulk-action links, kanban column action links, and the grid/kanban card's hidden show link (for `:show`).
93
+ - **`link:`** applies to every `<a>` rendered for the action: the toolbar link (GET), dropdown items (**any** HTTP method, dropdown items are always anchors, submitting via `data-turbo-method`), bulk-action links, kanban column action links, and the grid/kanban card's hidden show link (for `:show`).
94
94
  - **`button:`** applies to the `button_to` **`<form>`** element of the non-GET toolbar rendering (the form wrapper, not the inner `<button>`).
95
95
 
96
96
  ```ruby
@@ -100,14 +100,14 @@ action :documentation,
100
100
  link: {target: "_blank", rel: "noopener noreferrer", data: {analytics: "docs"}}
101
101
  ```
102
102
 
103
- Because the author wins, you can override anything — `turbo_frame`, `class`, `data-*` — at your own risk. Two things to know:
103
+ Because the author wins, you can override anything (`turbo_frame`, `class`, `data-*`) at your own risk. Two things to know:
104
104
 
105
- - `class:` **replaces** the framework's classes (no token append) — a bare `link: {class: "mt-2"}` removes the button styling entirely.
105
+ - `class:` **replaces** the framework's classes (no token append): a bare `link: {class: "mt-2"}` removes the button styling entirely.
106
106
  - Pass `data:` as a **hash**. The merge only recurses when both sides are hashes, so a scalar `data:` replaces the framework's data wholesale (dropping `turbo_confirm`/`turbo_frame`).
107
107
 
108
108
  Both bags round-trip through [`with(...)`](#deriving-variants-action-with), so `defined_actions[:edit].with(link: {target: "_blank"})` works in `customize_actions`.
109
109
 
110
- ### Deriving variants — `Action#with(...)`
110
+ ### Deriving variants: `Action#with(...)`
111
111
 
112
112
  Action records are frozen value objects. Inside `customize_actions`, derive a copy with overrides:
113
113
 
@@ -117,11 +117,11 @@ def customize_actions
117
117
  end
118
118
  ```
119
119
 
120
- ## Conditional visibility — `condition:` {#conditional-visibility}
120
+ ## Conditional visibility: `condition:` {#conditional-visibility}
121
121
 
122
- Like the `condition:` proc on [inputs/displays/columns](/reference/resource/definition), an action can be **defined but only rendered when a runtime proc is truthy**. It's purely a toggle on whether the **button is shown** — the action (and its route) stays fully live either way.
122
+ Like the `condition:` proc on [inputs/displays/columns](/reference/resource/definition), an action can be **defined but only rendered when a runtime proc is truthy**. It's purely a toggle on whether the **button is shown**; the action (and its route) stays fully live either way.
123
123
 
124
- The headline use case: **expose an action's endpoint without surfacing it in the UI** — e.g. one you call from the API, a webhook, or another service. Hide the button with an always-falsy condition; the route still works:
124
+ The headline use case: **expose an action's endpoint without surfacing it in the UI**, e.g. one you call from the API, a webhook, or another service. Hide the button with an always-falsy condition; the route still works:
125
125
 
126
126
  ```ruby
127
127
  # Defined and callable (API / programmatic), but no button anywhere in the UI:
@@ -133,7 +133,7 @@ It also works as a dynamic toggle driven by the **record** or the **view/request
133
133
  ```ruby
134
134
  # object → the row/shown record (record & collection-record actions):
135
135
  action :reopen, interaction: ReopenInteraction, condition: -> { object.closed? }
136
- # view/request state — feature flag, preview/beta mode:
136
+ # view/request state: feature flag, preview/beta mode:
137
137
  action :preview, interaction: PreviewInteraction, condition: -> { params[:beta] == "1" }
138
138
  ```
139
139
 
@@ -144,34 +144,34 @@ Inside the proc, `object`/`record` is the contextual record, and every other cal
144
144
  | `object` / `record` | The row/shown record for **record** and **collection-record** actions; **`nil`** for resource and bulk actions (no single record). Guard with `object&.…` if a condition is shared across action kinds. |
145
145
  | `params`, `request` | Current request. |
146
146
  | `current_user`, `current_parent` | The signed-in user and (nested) parent. |
147
- | `resource_record!` | The shown record on the show page; raises on index/table — prefer `object`. |
147
+ | `resource_record!` | The shown record on the show page; raises on index/table: prefer `object`. |
148
148
  | `allowed_to?`, `policy_for`, other helpers | The usual view helpers. |
149
149
 
150
150
  `object` is evaluated **per row** in tables and grids, so per-record show/hide works there too.
151
151
 
152
- ::: danger `condition:` is NOT authorization — it only hides the button
152
+ ::: danger `condition:` is NOT authorization: it only hides the button
153
153
  A hidden action still has a **live route**: anyone who knows the URL can still trigger it. `condition:` decides whether the *button renders*, never whether the *request is allowed*.
154
154
 
155
155
  ```ruby
156
- # 🚫 WRONG — this does NOT stop non-admins. The route is live; they can POST to it.
156
+ # 🚫 WRONG: this does NOT stop non-admins. The route is live; they can POST to it.
157
157
  action :wipe, interaction: WipeInteraction, condition: -> { current_user.admin? }
158
158
 
159
- # ✅ RIGHT — authorization belongs in the policy. The action only runs if this returns true.
159
+ # ✅ RIGHT: authorization belongs in the policy. The action only runs if this returns true.
160
160
  class WidgetPolicy < ResourcePolicy
161
161
  def wipe? = current_user.admin?
162
162
  end
163
163
  ```
164
164
 
165
- **Rule of thumb:** "who may run this" → **policy** (`def action_name?`). "is this UI relevant right now" → `condition:`. Authorization is enforced regardless of `condition:`; the two compose — an action appears only when the policy permits **and** the condition is truthy.
165
+ **Rule of thumb:** "who may run this" → **policy** (`def action_name?`). "is this UI relevant right now" → `condition:`. Authorization is enforced regardless of `condition:`; the two compose: an action appears only when the policy permits **and** the condition is truthy.
166
166
  :::
167
167
 
168
168
  ::: tip Per-record display vs. per-record authorization
169
- `condition: -> { object.draft? }` is fine for **showing/hiding** a per-record button. But if the rule is about **who may run it** ("only while draft *and* nobody else has it locked"), put it in the policy — `def publish? = record.draft?` is also evaluated per record (per row), and unlike `condition:` it actually gates execution.
169
+ `condition: -> { object.draft? }` is fine for **showing/hiding** a per-record button. But if the rule is about **who may run it** ("only while draft *and* nobody else has it locked"), put it in the policy: `def publish? = record.draft?` is also evaluated per record (per row), and unlike `condition:` it actually gates execution.
170
170
  :::
171
171
 
172
- ## Hidden actions — `hidden: true` {#hidden-actions}
172
+ ## Hidden actions: `hidden: true` {#hidden-actions}
173
173
 
174
- An action declared `hidden: true` renders in **no** toolbar, row dropdown, card, or bulk bar — regardless of its visibility flags, the policy, or `condition:`. Everything else about it stays live:
174
+ An action declared `hidden: true` renders in **no** toolbar, row dropdown, card, or bulk bar, regardless of its visibility flags, the policy, or `condition:`. Everything else about it stays live:
175
175
 
176
176
  - the **route** is mounted;
177
177
  - the **policy predicate** (`def name?`) is defined and enforced;
@@ -181,7 +181,7 @@ An action declared `hidden: true` renders in **no** toolbar, row dropdown, card,
181
181
  action :reposition, hidden: true
182
182
  ```
183
183
 
184
- The use case is an endpoint reached by **something other than a button** — a drag gesture, a custom Stimulus controller, a client-side widget you wrote yourself. The framework uses it for exactly that: [`position_on`](/reference/resource/positioning) expands to `action :reposition, hidden: true`, and the kanban board's drop endpoint is declared the same way.
184
+ The use case is an endpoint reached by **something other than a button**: a drag gesture, a custom Stimulus controller, a client-side widget you wrote yourself. The framework uses it for exactly that: [`position_on`](/reference/resource/positioning) expands to `action :reposition, hidden: true`, and the kanban board's drop endpoint is declared the same way.
185
185
 
186
186
  ### `hidden:` vs `condition: -> { false }`
187
187
 
@@ -190,7 +190,7 @@ Both suppress the button, so pick by intent:
190
190
  | | `hidden: true` | `condition: -> { false }` |
191
191
  |---|---|---|
192
192
  | Decided | at **class-load**, once | at **render time**, per row/request |
193
- | Costs | nothing — the surfaces filter it out before any policy or condition runs | a proc evaluation per rendering |
193
+ | Costs | nothing: the surfaces filter it out before any policy or condition runs | a proc evaluation per rendering |
194
194
  | Says | "this is never a button" | "this is a button, just not right now" |
195
195
 
196
196
  Use `hidden:` for an action that is *structurally* not a button. Use `condition:` when visibility genuinely depends on the record, the user, or the request.
@@ -199,10 +199,10 @@ Use `hidden:` for an action that is *structurally* not a button. Use `condition:
199
199
  This is the same trap as [`condition:`](#conditional-visibility), and it bears repeating because "hidden" reads more absolute than it is. A hidden action has a **live route**: anyone who can construct the URL can call it.
200
200
 
201
201
  ```ruby
202
- # 🚫 WRONG — hiding the button does not stop the request.
202
+ # 🚫 WRONG: hiding the button does not stop the request.
203
203
  action :purge_all, interaction: PurgeInteraction, hidden: true
204
204
 
205
- # ✅ RIGHT — authorization belongs in the policy.
205
+ # ✅ RIGHT: authorization belongs in the policy.
206
206
  class WidgetPolicy < ResourcePolicy
207
207
  def purge_all? = current_user.admin?
208
208
  end
@@ -239,16 +239,16 @@ resources :posts do
239
239
  end
240
240
  ```
241
241
 
242
- Without it, `resource_url_for` can't build the URL — particularly critical for nested resources.
242
+ Without it, `resource_url_for` can't build the URL, particularly critical for nested resources.
243
243
  :::
244
244
 
245
245
  For anything with business logic, use an **interactive action** instead.
246
246
 
247
247
  ## Interactive actions
248
248
 
249
- Run an [Interaction](/reference/behavior/interactions) — automatically renders a form if the interaction declares attributes beyond `:resource`/`:resources`, otherwise executes immediately with a confirmation.
249
+ Run an [Interaction](/reference/behavior/interactions): automatically renders a form if the interaction declares attributes beyond `:resource`/`:resources`, otherwise executes immediately with a confirmation.
250
250
 
251
- The interactions below call named model methods (`archive!`, `invite!`) rather than doing the work inline. That's not mandatory for a one-off — the trigger to extract is the second caller, since an interaction can only be built with a `view_context`. See [Interactions › What an interaction is for](/reference/behavior/interactions#what-an-interaction-is-for).
251
+ The interactions below call named model methods (`archive!`, `invite!`) rather than doing the work inline. That's not mandatory for a one-off; the trigger to extract is the second caller, since an interaction can only be built with a `view_context`. See [Interactions › What an interaction is for](/reference/behavior/interactions#what-an-interaction-is-for).
252
252
 
253
253
  ```ruby
254
254
  class PostDefinition < Plutonium::Resource::Definition
@@ -341,7 +341,7 @@ action :bulk_archive, interaction: BulkArchiveInteraction
341
341
  # bulk_action: true inferred from `attribute :resources`
342
342
  ```
343
343
 
344
- Policy — checked per record; fails the whole request if ANY record is unauthorized:
344
+ Policy: checked per record; fails the whole request if ANY record is unauthorized:
345
345
 
346
346
  ```ruby
347
347
  def bulk_archive?
@@ -349,7 +349,7 @@ def bulk_archive?
349
349
  end
350
350
  ```
351
351
 
352
- 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.
352
+ 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.
353
353
 
354
354
  ### Resource action (no record)
355
355
 
@@ -375,7 +375,7 @@ action :import, interaction: ImportInteraction
375
375
 
376
376
  ## Running the work in the background
377
377
 
378
- An interactive action executes inside the request. When that is too slow — a bulk action over thousands of records, or a single call to something slow — replace `execute` with `async` and the interaction dispatches a persisted, resumable run instead:
378
+ An interactive action executes inside the request. When that is too slow (a bulk action over thousands of records, or a single call to something slow), replace `execute` with `async` and the interaction dispatches a persisted, resumable run instead:
379
379
 
380
380
  ```ruby
381
381
  class BulkArchiveInteraction < ResourceInteraction
@@ -396,8 +396,8 @@ See [Async Interactions](/reference/behavior/async-interactions) for failure pol
396
396
 
397
397
  | Interaction shape | Behavior |
398
398
  |---|---|
399
- | Only `:resource` / `:resources` (no extra inputs) | **Immediate** — browser confirmation (`"#{label}?"`, e.g. `"Archive?"`), then runs. Override with `confirmation: "Custom"` or `confirmation: false`. |
400
- | Additional `attribute` / `input` declared | **Form** — renders the action's form in a modal first; no auto-confirmation (the form is the confirmation). |
399
+ | Only `:resource` / `:resources` (no extra inputs) | **Immediate**: browser confirmation (`"#{label}?"`, e.g. `"Archive?"`), then runs. Override with `confirmation: "Custom"` or `confirmation: false`. |
400
+ | Additional `attribute` / `input` declared | **Form**: renders the action's form in a modal first; no auto-confirmation (the form is the confirmation). |
401
401
 
402
402
  ## Built-in CRUD actions
403
403
 
@@ -454,13 +454,13 @@ end
454
454
  ```
455
455
 
456
456
  > **CSV export is not an action.** It's a built-in, policy-gated capability with its own
457
- > button — see [CSV Export](./export.md). Don't declare it with `action :export_csv`.
457
+ > button; see [CSV Export](./export.md). Don't declare it with `action :export_csv`.
458
458
 
459
459
  ## Interaction responses
460
460
 
461
461
  ```ruby
462
462
  def execute
463
- # Success — redirects to resource automatically
463
+ # Success: redirects to resource automatically
464
464
  succeed(resource).with_message("Done!")
465
465
 
466
466
  # Different redirect destination
@@ -569,7 +569,7 @@ action :export,
569
569
 
570
570
  ## Related
571
571
 
572
- - [Definition](./definition) — fields, page chrome
573
- - [Query](./query) — search, filters, scopes
574
- - [Behavior › Interactions](/reference/behavior/interactions) — writing interaction classes
575
- - [Behavior › Policy](/reference/behavior/policies) — authorizing custom actions
572
+ - [Definition](./definition): fields, page chrome
573
+ - [Query](./query): search, filters, scopes
574
+ - [Behavior › Interactions](/reference/behavior/interactions): writing interaction classes
575
+ - [Behavior › Policy](/reference/behavior/policies): authorizing custom actions