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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/SKILL.md +43 -43
- data/.claude/skills/plutonium-app/SKILL.md +101 -59
- data/.claude/skills/plutonium-async-interactions/SKILL.md +19 -19
- data/.claude/skills/plutonium-auth/SKILL.md +119 -53
- data/.claude/skills/plutonium-behavior/SKILL.md +110 -78
- data/.claude/skills/plutonium-dashboard/SKILL.md +11 -4
- data/.claude/skills/plutonium-kanban/SKILL.md +81 -49
- data/.claude/skills/plutonium-resource/SKILL.md +144 -133
- data/.claude/skills/plutonium-tenancy/SKILL.md +104 -55
- data/.claude/skills/plutonium-testing/SKILL.md +130 -33
- data/.claude/skills/plutonium-ui/SKILL.md +151 -96
- data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
- data/CHANGELOG.md +21 -0
- data/README.md +9 -9
- data/SECURITY.md +1 -1
- data/app/assets/plutonium.css +1 -1
- data/docs/.vitepress/sync-skills.mjs +6 -3
- data/docs/blog/introducing-plutonium-dashboards.md +4 -5
- data/docs/blog/introducing-plutonium-i18n.md +4 -5
- data/docs/getting-started/installation.md +5 -5
- data/docs/getting-started/tutorial/02-first-resource.md +3 -3
- data/docs/getting-started/tutorial/03-authentication.md +7 -7
- data/docs/getting-started/tutorial/04-authorization.md +21 -4
- data/docs/getting-started/tutorial/05-custom-actions.md +2 -2
- data/docs/getting-started/tutorial/06-nested-resources.md +5 -2
- data/docs/getting-started/tutorial/07-author-portal.md +2 -2
- data/docs/getting-started/tutorial/08-customizing-ui.md +45 -30
- data/docs/getting-started/tutorial/index.md +1 -1
- data/docs/guides/adding-resources.md +10 -7
- data/docs/guides/authentication.md +25 -25
- data/docs/guides/authorization.md +24 -24
- data/docs/guides/creating-packages.md +17 -17
- data/docs/guides/custom-actions.md +32 -32
- data/docs/guides/customizing-ui.md +29 -26
- data/docs/guides/dashboards.md +1 -1
- data/docs/guides/index.md +3 -3
- data/docs/guides/kanban.md +55 -55
- data/docs/guides/multi-tenancy.md +35 -22
- data/docs/guides/nested-resources.md +21 -21
- data/docs/guides/performance.md +3 -3
- data/docs/guides/search-filtering.md +13 -13
- data/docs/guides/testing.md +16 -12
- data/docs/guides/theming.md +32 -17
- data/docs/guides/troubleshooting.md +2 -2
- data/docs/guides/user-invites.md +17 -17
- data/docs/guides/user-profile.md +51 -24
- data/docs/guides/wizards.md +55 -55
- data/docs/reference/app/generators.md +22 -22
- data/docs/reference/app/index.md +15 -18
- data/docs/reference/app/packages.md +8 -8
- data/docs/reference/app/portals.md +75 -29
- data/docs/reference/auth/accounts.md +15 -15
- data/docs/reference/auth/index.md +12 -12
- data/docs/reference/auth/profile.md +67 -29
- data/docs/reference/behavior/async-interactions.md +24 -24
- data/docs/reference/behavior/controllers.md +28 -28
- data/docs/reference/behavior/index.md +5 -5
- data/docs/reference/behavior/interactions.md +44 -44
- data/docs/reference/behavior/policies.md +48 -28
- data/docs/reference/configuration.md +6 -6
- data/docs/reference/dashboard/dsl.md +2 -2
- data/docs/reference/dashboard/index.md +1 -1
- data/docs/reference/generators/lite.md +7 -7
- data/docs/reference/i18n.md +23 -0
- data/docs/reference/index.md +1 -1
- data/docs/reference/kanban/authorization.md +9 -9
- data/docs/reference/kanban/dsl.md +32 -32
- data/docs/reference/kanban/index.md +1 -1
- data/docs/reference/kanban/positioning.md +17 -15
- data/docs/reference/resource/actions.md +51 -51
- data/docs/reference/resource/definition.md +73 -73
- data/docs/reference/resource/export.md +6 -6
- data/docs/reference/resource/index.md +16 -16
- data/docs/reference/resource/model.md +24 -24
- data/docs/reference/resource/positioning.md +78 -76
- data/docs/reference/resource/query.md +13 -13
- data/docs/reference/tenancy/entity-scoping.md +65 -35
- data/docs/reference/tenancy/index.md +11 -11
- data/docs/reference/tenancy/invites.md +20 -20
- data/docs/reference/tenancy/nested-resources.md +13 -13
- data/docs/reference/testing/index.md +116 -22
- data/docs/reference/ui/assets.md +57 -25
- data/docs/reference/ui/components.md +20 -20
- data/docs/reference/ui/displays.md +14 -14
- data/docs/reference/ui/forms.md +35 -35
- data/docs/reference/ui/index.md +17 -15
- data/docs/reference/ui/layouts.md +21 -21
- data/docs/reference/ui/pages.md +22 -22
- data/docs/reference/ui/tables.md +7 -7
- data/docs/reference/wizard/anchoring-resume.md +33 -32
- data/docs/reference/wizard/dsl.md +44 -44
- data/docs/reference/wizard/index.md +6 -6
- data/docs/reference/wizard/one-time.md +18 -18
- data/docs/reference/wizard/registration-launch.md +32 -32
- data/docs/reference/wizard/storage-config.md +23 -23
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/profile/conn_generator.rb +6 -0
- data/lib/plutonium/resource/record/associated_with.rb +23 -2
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/css/components.css +10 -10
- metadata +2 -2
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
# Actions
|
|
2
2
|
|
|
3
|
-
Custom buttons that go beyond standard CRUD
|
|
3
|
+
Custom buttons that go beyond standard CRUD: publish, archive, import, send invitation, etc. Two flavors:
|
|
4
4
|
|
|
5
|
-
- **Simple actions
|
|
6
|
-
- **Interactive actions
|
|
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
|
|
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
|
|
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)
|
|
22
|
-
| `record_action: true` | Show page
|
|
23
|
-
| `collection_record_action: true` | Per-row in the index table
|
|
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
|
|
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
|
|
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
|
|
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
|
|
66
|
+
# Conditional visibility: display-only proc, NOT authorization (see below)
|
|
67
67
|
condition: -> { params[:beta] == "1" },
|
|
68
68
|
|
|
69
|
-
# Never render, anywhere
|
|
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
|
|
82
|
-
size: :lg, # :sm / :md / :lg / :xl / :auto / :full
|
|
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
|
|
89
|
+
### HTML attributes: `link:` / `button:`
|
|
90
90
|
|
|
91
|
-
Two per-element attribute bags, deep-merged over the framework's own attributes at render time
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
202
|
+
# 🚫 WRONG: hiding the button does not stop the request.
|
|
203
203
|
action :purge_all, interaction: PurgeInteraction, hidden: true
|
|
204
204
|
|
|
205
|
-
# ✅ RIGHT
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
400
|
-
| Additional `attribute` / `input` declared | **Form
|
|
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
|
|
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
|
|
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)
|
|
573
|
-
- [Query](./query)
|
|
574
|
-
- [Behavior › Interactions](/reference/behavior/interactions)
|
|
575
|
-
- [Behavior › Policy](/reference/behavior/policies)
|
|
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
|