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,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plutonium-kanban
|
|
3
|
-
description: Use BEFORE building or customizing a kanban board view for any Plutonium resource
|
|
3
|
+
description: 'Use BEFORE building or customizing a kanban board view for any Plutonium resource: the kanban do…end DSL, column declarations, card_fields, position_on modes, realtime, column actions, kanban_move? policy, and quick-add. The single source for "how do I add a kanban board to a resource".'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Plutonium Kanban
|
|
@@ -11,13 +11,14 @@ For field-level rendering on cards (card_fields slots), see [[plutonium-resource
|
|
|
11
11
|
|
|
12
12
|
## 🚨 Critical (read first)
|
|
13
13
|
|
|
14
|
-
- **`kanban do…end` in the Definition auto-enables `:kanban`** in `defined_index_views
|
|
14
|
+
- **`kanban do…end` in the Definition auto-enables `:kanban`** in `defined_index_views`, exactly like `grid_fields` enables `:grid`. You do not need to call `index_views :kanban` separately unless you want to remove the table view.
|
|
15
15
|
- **The model needs `include Plutonium::Positioning::Model`** (and a decimal `position` column + `positioned_on` call) for drag ordering to work. Without it, cards render unordered and moves raise an error. Use `position_on false` to explicitly opt out.
|
|
16
|
-
- **Static column actions are auto-registered** as interactive resource actions at class-load time. Dynamic boards (`columns do…end`) cannot introspect their columns at load time
|
|
17
|
-
- **Moves bypass `permitted_attributes_for_update
|
|
16
|
+
- **Static column actions are auto-registered** as interactive resource actions at class-load time. Dynamic boards (`columns do…end`) cannot introspect their columns at load time, so declare any column-action interactions separately with top-level `action` calls.
|
|
17
|
+
- **Moves bypass `permitted_attributes_for_update`**: the `on_enter` callback runs with full model access. Gate the move itself with `kanban_move?` in the policy.
|
|
18
18
|
- **Quick-add (`add: true`) only appears when `create?` is true** in the policy.
|
|
19
|
-
- **Same-column drops = positioning only
|
|
20
|
-
- **`on_exit:` is the source-side hook
|
|
19
|
+
- **Same-column drops = positioning only**: a reorder within a column fires neither `on_exit`, `on_enter`, nor an `enter_interaction`; they represent *leaving*/*entering* a column, so only cross-column drops trigger them.
|
|
20
|
+
- **`on_exit:` is the source-side hook**: fired when a card LEAVES a column (before the destination's `on_enter`, in the same transaction). "When a card leaves X, wherever it goes" is exactly `on_exit:` on X; don't rebuild it as a `before_save` status-diff callback. It fires only on drag-moves via `kanban_move`, NOT on destroy/programmatic changes/quick-add, so reach for a model callback only when the requirement explicitly covers those other paths (see `on_exit:` below).
|
|
21
|
+
- **`locked: true` stops cards LEAVING the column**, not entering it. To refuse incoming drops, use `accepts: false` (or an `accepts:` Array) on the destination.
|
|
21
22
|
- **Use `on_enter:` / `enter_interaction:`, NOT `on_drop:` / `drop_interaction:`.** The old names were renamed. They still exist as deprecated aliases but **raise in development/test** (and only warn-and-map in production), so a definition using them fails your test suite. Always write the new names.
|
|
22
23
|
|
|
23
24
|
---
|
|
@@ -37,7 +38,7 @@ class Task < ApplicationRecord
|
|
|
37
38
|
end
|
|
38
39
|
```
|
|
39
40
|
|
|
40
|
-
Migration
|
|
41
|
+
Migration: add the position column with the `t.position` helper (a tuned `decimal(16,8)`; works in `create_table` and `change_table`). Don't hand-roll a small scale: `scale: 6` exactly matches the `1e-6` rebalance threshold and can round to a duplicate. Use `t.position` (scale 8) or ≥ 8 if hand-written:
|
|
41
42
|
|
|
42
43
|
```ruby
|
|
43
44
|
create_table :tasks do |t|
|
|
@@ -50,6 +51,20 @@ create_table :tasks do |t|
|
|
|
50
51
|
end
|
|
51
52
|
```
|
|
52
53
|
|
|
54
|
+
**Package models: the migration goes in the package.** For a model owned by a feature package (e.g. `Catalog::Product`), write the position migration under `packages/<package>/db/migrate/`, next to the table's create migration. `pu:res:scaffold --dest=<package>` puts migrations there (it roots generation at `packages/<package>`), and `Plutonium::Package::Engine` appends each package's `db/migrate` to the app's migration paths, so `rails db:migrate` picks it up. Plain `rails g migration` always writes to the main app's `db/migrate`; move the file into the package afterwards. Keeping a table's migrations together keeps the package self-contained. (A couple of older dummy-app migrations for catalog tables sit in `test/dummy/db/migrate`; don't copy that.)
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# packages/catalog/db/migrate/20261002000000_add_position_to_catalog_products.rb
|
|
58
|
+
class AddPositionToCatalogProducts < ActiveRecord::Migration[8.0]
|
|
59
|
+
def change
|
|
60
|
+
change_table :catalog_products do |t|
|
|
61
|
+
t.position
|
|
62
|
+
t.index [:category_id, :position]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
53
68
|
---
|
|
54
69
|
|
|
55
70
|
## Minimal definition
|
|
@@ -87,13 +102,13 @@ index_views :kanban # remove table; kanban is the only view
|
|
|
87
102
|
|---|---|---|
|
|
88
103
|
| `per_column N` | Cap cards rendered per column; `+N more` footer when exceeded | unlimited |
|
|
89
104
|
| `position_on :attr` | Custom attribute name for ordering (Mode A) | `:position` |
|
|
90
|
-
| `position_on :attr do \|move\| … end` | BYO positioning block (Mode B) |
|
|
91
|
-
| `position_on false` | No ordering or repositioning (Mode C) |
|
|
105
|
+
| `position_on :attr do \|move\| … end` | BYO positioning block (Mode B) | - |
|
|
106
|
+
| `position_on false` | No ordering or repositioning (Mode C) | - |
|
|
92
107
|
| `card_fields(**slots)` | Override grid slot layout for cards; same slot keys as `grid_fields` | inherits `grid_fields` |
|
|
93
108
|
| `realtime true` | ActionCable broadcast after every move | false |
|
|
94
109
|
| `lazy false` | Eager-load all column frames on the initial request | `true` (lazy) |
|
|
95
110
|
| `show_in :modal` / `:page` | Open a card's show page in a centered modal (`:modal`) or full-page (`:page`). Overrides the definition's `show_in` for this board | inherits definition (`:page`) |
|
|
96
|
-
| `columns do … end` | Dynamic columns evaluated at request time with view context |
|
|
111
|
+
| `columns do … end` | Dynamic columns evaluated at request time with view context | - |
|
|
97
112
|
|
|
98
113
|
### `card_fields`
|
|
99
114
|
|
|
@@ -103,25 +118,25 @@ Overrides the grid card layout for kanban cards. Uses the same slot keys as `gri
|
|
|
103
118
|
card_fields header: :title, meta: [:status, :priority], footer: :due_at
|
|
104
119
|
```
|
|
105
120
|
|
|
106
|
-
Every slot is optional and omitting it drops that line
|
|
121
|
+
Every slot is optional and omitting it drops that line, **except `footer`, which
|
|
107
122
|
falls back to `:created_at`**. To render no footer at all, opt out explicitly:
|
|
108
123
|
|
|
109
124
|
```ruby
|
|
110
125
|
card_fields header: :title, meta: [:status], footer: false
|
|
111
126
|
```
|
|
112
127
|
|
|
113
|
-
Omitting `footer:` is the common cause of a card ending in a stray
|
|
128
|
+
Omitting `footer:` is the common cause of a card ending in a stray dash placeholder: the
|
|
114
129
|
fallback lands on `:created_at`, and if that isn't in the policy's
|
|
115
130
|
`permitted_attributes_for_index` the value resolves to nil and renders as the
|
|
116
131
|
blank placeholder. Either permit `created_at`, point `footer:` at a permitted
|
|
117
132
|
field, or pass `footer: false`. (A *declared* slot that's merely blank still
|
|
118
|
-
shows
|
|
133
|
+
shows the dash by design, so cards keep an even height.)
|
|
119
134
|
|
|
120
135
|
### `position_on` modes
|
|
121
136
|
|
|
122
|
-
- **Mode A (default)
|
|
123
|
-
- **Mode B (block)
|
|
124
|
-
- **Mode C (`false`)
|
|
137
|
+
- **Mode A (default)**: delegates to `record.reposition!(prev_record:, next_record:)` from `Plutonium::Positioning::Model`. Requires the model concern and a decimal column.
|
|
138
|
+
- **Mode B (block)**: you write the persistence. Plutonium still orders by the attribute; the block only persists the new value. Block receives a `Plutonium::Kanban::Positioning::Move` (fields: `record`, `column`, `prev`, `next`, `index`).
|
|
139
|
+
- **Mode C (`false`)**: no ordering, no repositioning. `on_enter` still fires.
|
|
125
140
|
|
|
126
141
|
### `realtime`
|
|
127
142
|
|
|
@@ -133,9 +148,9 @@ realtime true
|
|
|
133
148
|
|
|
134
149
|
### `show_in`
|
|
135
150
|
|
|
136
|
-
Where a card click opens the record's show page. `:modal` renders the show page in a **centered** dialog; `:page` is a full-page navigation. The show modal is always centered
|
|
151
|
+
Where a card click opens the record's show page. `:modal` renders the show page in a **centered** dialog; `:page` is a full-page navigation. The show modal is always centered, deliberately NOT the definition's `modal_mode` (which styles `new`/`edit`). No per-card wiring: the `Show` page detects the modal frame (`in_modal?`) and wraps its details in the centered modal chrome.
|
|
137
152
|
|
|
138
|
-
`show_in` also exists **on the definition** (`show_in :modal` / `:page`, default `:page`), where it governs the table and grid show links too. The kanban board inherits the definition's value unless it sets its own
|
|
153
|
+
`show_in` also exists **on the definition** (`show_in :modal` / `:page`, default `:page`), where it governs the table and grid show links too. The kanban board inherits the definition's value unless it sets its own, so set it once on the definition for everywhere, or on the board to override just the board.
|
|
139
154
|
|
|
140
155
|
From inside the show modal, an expand icon (or ⌘/Ctrl/middle-click on the card) opens the full page in a new tab.
|
|
141
156
|
|
|
@@ -150,14 +165,14 @@ end
|
|
|
150
165
|
|
|
151
166
|
### Dynamic columns
|
|
152
167
|
|
|
153
|
-
Evaluates the block at request time with the view context as `self` (`current_user`, `params`, `current_scoped_entity`, helpers all available). The block must return an Array of `Plutonium::Kanban::Column` objects
|
|
168
|
+
Evaluates the block at request time with the view context as `self` (`current_user`, `params`, `current_scoped_entity`, helpers all available). The block must return an Array of `Plutonium::Kanban::Column` objects; `column` is a DSL method only available outside the `columns` block. Declare any column-action interactions as top-level definition `action` calls, since the block is not introspectable at class-load time.
|
|
154
169
|
|
|
155
|
-
> **`enter_interaction:` is NOT supported on dynamic boards.** Its hidden action is registered from the static column list at class-load time, which a `columns do…end` board doesn't have, and the key is column-scoped/internal so there's no manual-registration escape hatch (unlike column actions). A drop into such a column is rejected with a snap-back
|
|
170
|
+
> **`enter_interaction:` is NOT supported on dynamic boards.** Its hidden action is registered from the static column list at class-load time, which a `columns do…end` board doesn't have, and the key is column-scoped/internal so there's no manual-registration escape hatch (unlike column actions). A drop into such a column is rejected with a snap-back; it does not crash. Use a static board if you need `enter_interaction:`.
|
|
156
171
|
|
|
157
172
|
```ruby
|
|
158
173
|
kanban do
|
|
159
174
|
columns do
|
|
160
|
-
# `self` is the view context here
|
|
175
|
+
# `self` is the view context here; use Plutonium::Kanban::Column.new, NOT `column`.
|
|
161
176
|
current_user.teams.map do |team|
|
|
162
177
|
Plutonium::Kanban::Column.new(
|
|
163
178
|
:"team_#{team.id}",
|
|
@@ -186,7 +201,7 @@ column :key,
|
|
|
186
201
|
collapsed: true, # starts collapsed (Stimulus persists toggle to localStorage)
|
|
187
202
|
add: true, # show "+ Add" button (requires create?)
|
|
188
203
|
accepts: true, # true (default), false, or Array of source keys (Proc raises)
|
|
189
|
-
locked: false, #
|
|
204
|
+
locked: false, # cards cannot be dragged OUT of this column (server-enforced)
|
|
190
205
|
role: :backlog # :backlog, :done or :lost (see presets below)
|
|
191
206
|
```
|
|
192
207
|
|
|
@@ -198,7 +213,7 @@ column :key,
|
|
|
198
213
|
| `:done` | `color: :green`, `collapsed: true` |
|
|
199
214
|
| `:lost` | `color: :red`, `collapsed: true` |
|
|
200
215
|
|
|
201
|
-
`:done` and `:lost` are the two terminal roles
|
|
216
|
+
`:done` and `:lost` are the two terminal roles, collapsed by default, colour
|
|
202
217
|
signalling the outcome (`:done` = positive close, `:lost` = negative close). The
|
|
203
218
|
natural pair for won/lost pipelines (leads, deals, tickets).
|
|
204
219
|
|
|
@@ -206,13 +221,15 @@ Explicit options override the preset (e.g. `role: :done, collapsed: false`).
|
|
|
206
221
|
|
|
207
222
|
### `accepts:`
|
|
208
223
|
|
|
209
|
-
Structural drop topology
|
|
224
|
+
Structural drop topology: which **source columns** may drop cards here:
|
|
210
225
|
|
|
211
|
-
- `true` (default)
|
|
212
|
-
- `false
|
|
213
|
-
- `Array
|
|
226
|
+
- `true` (default): any source allowed
|
|
227
|
+
- `false`: column is a drop target but refuses everything (snap-back)
|
|
228
|
+
- `Array`: list of source column keys allowed: `accepts: [:doing]`
|
|
214
229
|
|
|
215
|
-
Checked server-side; client-side visual hints read `data-kanban-accepts` (so the drag UI can grey out disallowed sources). **No Proc form
|
|
230
|
+
Checked server-side; client-side visual hints read `data-kanban-accepts` (so the drag UI can grey out disallowed sources). **No Proc form**: a `Proc` raises `ArgumentError`. Record- or user-conditional rules belong in `kanban_move?`, which sees the record and the `from`/`to` columns (see Authorization below).
|
|
231
|
+
|
|
232
|
+
`locked:` is the source-side counterpart: `locked: true` means no card can leave the column, whatever the destination. "Once verified it can never go back to Pending" can be written either way: `accepts: false` on Pending (nothing may enter it) or `locked: true` on Verified (nothing may leave it). Pick the one that matches the rule as stated, and remember `locked:` on Pending would not stop cards entering it.
|
|
216
233
|
|
|
217
234
|
### `on_enter:`
|
|
218
235
|
|
|
@@ -220,7 +237,7 @@ Runs inside a transaction after authorization and before repositioning. Receives
|
|
|
220
237
|
|
|
221
238
|
```ruby
|
|
222
239
|
on_enter: ->(r) { r.update!(status: "done") } # update! directly
|
|
223
|
-
on_enter: ->(r) { r.status = "done" } # attribute assignment
|
|
240
|
+
on_enter: ->(r) { r.status = "done" } # attribute assignment, saved automatically
|
|
224
241
|
on_enter: :mark_done! # dispatched as record.mark_done!
|
|
225
242
|
```
|
|
226
243
|
|
|
@@ -237,13 +254,28 @@ column :doing,
|
|
|
237
254
|
on_exit: ->(r) { r.stop_timer! } # leaving Doing (wherever it goes)
|
|
238
255
|
```
|
|
239
256
|
|
|
240
|
-
Use it for side effects tied to the column being **left
|
|
257
|
+
Use it for side effects tied to the column being **left**; the destination's `on_enter` doesn't know where a card came from, so source concerns (stop a timer, release a WIP/lock, un-assign) belong here.
|
|
258
|
+
|
|
259
|
+
⚠️ It fires **only** on a drag-move through `kanban_move`, not on `destroy`, a programmatic `status` change elsewhere, or quick-add. Skipped on same-column reorders.
|
|
260
|
+
|
|
261
|
+
**`on_exit:` vs a model callback.** Default to `on_exit:` for anything phrased in board terms ("when a ticket leaves In progress, wherever it goes, add the elapsed time"). "Wherever it goes" means any destination column, which is what `on_exit:` covers. A `before_save` callback keyed on a status diff duplicates what the column already knows (which column the card left) and has to re-derive column membership from attributes, which breaks as soon as a column's `scope:` is more than one attribute value. It also fires on every save path, which is a different requirement. If the column owns its membership attribute (the column's `on_enter` writes `status`, so `status` stays out of `permitted_attributes_for_update`), the board is the only way a card leaves, and `on_exit:` is complete. Use a model callback only when the user explicitly wants other paths (edit form, API, import) covered too, and then drop the `on_exit:` rather than keeping both, or the time is counted twice.
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
column :in_progress,
|
|
265
|
+
label: "In progress",
|
|
266
|
+
scope: -> { where(status: "in_progress") },
|
|
267
|
+
on_enter: ->(t) { t.assign_attributes(status: "in_progress", started_at: Time.current) },
|
|
268
|
+
on_exit: ->(t) {
|
|
269
|
+
t.time_spent_seconds += (Time.current - t.started_at).to_i if t.started_at
|
|
270
|
+
t.started_at = nil
|
|
271
|
+
}
|
|
272
|
+
```
|
|
241
273
|
|
|
242
|
-
|
|
274
|
+
Assigned attributes are saved by the controller's `record.save!` after the hooks, along with the destination's `on_enter` changes.
|
|
243
275
|
|
|
244
276
|
### `enter_interaction:`
|
|
245
277
|
|
|
246
|
-
Run an input-collecting interaction when a card is dropped **into** this column from another column
|
|
278
|
+
Run an input-collecting interaction when a card is dropped **into** this column from another column, for entries that need more than a membership flip (a reason, a mail, an audit entry).
|
|
247
279
|
|
|
248
280
|
```ruby
|
|
249
281
|
column :lost, scope: -> { where(status: "lost") }, enter_interaction: MarkLostInteraction
|
|
@@ -260,12 +292,12 @@ class MarkLostInteraction < ResourceInteraction
|
|
|
260
292
|
end
|
|
261
293
|
```
|
|
262
294
|
|
|
263
|
-
- **Auto-registered as a HIDDEN record action** under a column-scoped key (`:lost` → `:lost_enter_interaction`)
|
|
264
|
-
- **Move flow:** cross-column drop opens the interaction's form as a modal; on submit `on_enter` + interaction + repositioning commit in **one atomic transaction**. Validation failure rolls it all back (membership write included) and re-renders the modal with errors
|
|
265
|
-
- **Same-column reorder = positioning only
|
|
295
|
+
- **Auto-registered as a HIDDEN record action** under a column-scoped key (`:lost` → `:lost_enter_interaction`), unique by construction, so two columns can reuse the same interaction class. No button on show/table/grid; reachable only by dropping. **No policy method of its own**; authorized by `kanban_move?` (see Authorization).
|
|
296
|
+
- **Move flow:** cross-column drop opens the interaction's form as a modal; on submit `on_enter` + interaction + repositioning commit in **one atomic transaction**. Validation failure rolls it all back (membership write included) and re-renders the modal with errors; nothing persists. Put side-effects on `deliver_later` so a rollback sends no stray mail.
|
|
297
|
+
- **Same-column reorder = positioning only**: neither `on_enter` nor the interaction fires (both = *entering* a column).
|
|
266
298
|
- **Quick-add (`+ Add`)** applies `on_enter` + positioning post-create; the interaction is not involved.
|
|
267
299
|
- **Author contract:** with both present, `on_enter` owns the membership attribute (`status`) and the interaction owns extras. If the interaction also writes membership it must set the **same** value (idempotent). With no `on_enter`, the interaction owns everything (like `:lost`).
|
|
268
|
-
- **Limitation:** custom success *responses* (`with_redirect_response`, `with_file_response`, …) are NOT honored on the drop path
|
|
300
|
+
- **Limitation:** custom success *responses* (`with_redirect_response`, `with_file_response`, …) are NOT honored on the drop path: the board re-renders and the modal closes. Use `.with_message` for feedback.
|
|
269
301
|
|
|
270
302
|
### Column actions
|
|
271
303
|
|
|
@@ -282,7 +314,7 @@ column :done, … do
|
|
|
282
314
|
end
|
|
283
315
|
```
|
|
284
316
|
|
|
285
|
-
`on: :all`
|
|
317
|
+
`on: :all` passes every record in the column scope. `on: :visible` passes only the currently rendered subset (respects `per_column`).
|
|
286
318
|
|
|
287
319
|
---
|
|
288
320
|
|
|
@@ -306,9 +338,9 @@ class TaskPolicy < ResourcePolicy
|
|
|
306
338
|
end
|
|
307
339
|
```
|
|
308
340
|
|
|
309
|
-
When `kanban_move?` returns `false`, the board renders read-only
|
|
341
|
+
When `kanban_move?` returns `false`, the board renders read-only, with no drag handles or drop zones.
|
|
310
342
|
|
|
311
|
-
**`kanban_move?` is the ONLY move authorization
|
|
343
|
+
**`kanban_move?` is the ONLY move authorization**, plain moves and `enter_interaction:` columns alike (the interaction has no policy method of its own). To gate a *specific* transition, read the destination (and source) column from the authorization context via the optional `kanban_to` / `kanban_from` policy readers (the `Column` objects; `nil` for every non-move check):
|
|
312
344
|
|
|
313
345
|
```ruby
|
|
314
346
|
def kanban_move?
|
|
@@ -317,17 +349,17 @@ def kanban_move?
|
|
|
317
349
|
end
|
|
318
350
|
```
|
|
319
351
|
|
|
320
|
-
Rules take no positional args in ActionPolicy
|
|
352
|
+
Rules take no positional args in ActionPolicy; the columns arrive as declared optional context (`authorize :kanban_from/:kanban_to, optional: true` on the base policy), supplied by the controller on the move check.
|
|
321
353
|
|
|
322
354
|
### Move authorization flow
|
|
323
355
|
|
|
324
356
|
1. Record loaded via current `relation_scope` (same as index).
|
|
325
|
-
2. `kanban_move?` checked (with `kanban_from`/`kanban_to` in context)
|
|
326
|
-
3.
|
|
327
|
-
4. `wip:` limit checked for cross-column moves
|
|
328
|
-
5. `on_enter
|
|
357
|
+
2. `kanban_move?` checked (with `kanban_from`/`kanban_to` in context), HTTP 403 on failure. This is the sole authorization; an `enter_interaction` rides on it.
|
|
358
|
+
3. Destination `accepts:` and source `locked:` checked: HTTP 422 + card snap-back on failure.
|
|
359
|
+
4. `wip:` limit checked for cross-column moves, HTTP 422 on failure.
|
|
360
|
+
5. Source `on_exit`, then destination `on_enter`, fire + record repositioned, all in a transaction.
|
|
329
361
|
|
|
330
|
-
On a 422 rejection (steps 3–4) the response re-renders the source column (snap-back) **and** appends a dismissable warning toast naming the reason (e.g. `“Pending” is at its WIP limit (5).`) to the board's `#kanban-flash` region
|
|
362
|
+
On a 422 rejection (steps 3–4) the response re-renders the source column (snap-back) **and** appends a dismissable warning toast naming the reason (e.g. `“Pending” is at its WIP limit (5).`) to the board's `#kanban-flash` region, so the snap-back is never silent. The toast renders the shared `plutonium/toast` partial directly (not via `flash`), so a stale undisplayed flash can't leak into the turbo-stream response.
|
|
331
363
|
|
|
332
364
|
### No permitted-attributes gate
|
|
333
365
|
|
|
@@ -337,7 +369,7 @@ Moves do not pass through `permitted_attributes_for_update`. `on_enter` is trust
|
|
|
337
369
|
|
|
338
370
|
The `+ Add` button (column `add: true`) only renders when the policy's `create?` is true. The opened form is the standard new-resource form.
|
|
339
371
|
|
|
340
|
-
The record is created normally, **then** the column's `on_enter` + positioning are applied to the **saved** record (it lands in the clicked column, appended to the bottom)
|
|
372
|
+
The record is created normally, **then** the column's `on_enter` + positioning are applied to the **saved** record (it lands in the clicked column, appended to the bottom), so `on_enter` runs against a real record, exactly as on a drag. **Your grouping column must have a default** (DB or model), because `on_enter` runs after save; a `NOT NULL` grouping column with no default fails quick-add create. A raising `on_enter` keeps the created record in its default column and toasts the error (the create is not rolled back).
|
|
341
373
|
|
|
342
374
|
---
|
|
343
375
|
|
|
@@ -387,6 +419,6 @@ end
|
|
|
387
419
|
|
|
388
420
|
## Related skills
|
|
389
421
|
|
|
390
|
-
- [[plutonium-resource]]
|
|
391
|
-
- [[plutonium-behavior]]
|
|
392
|
-
- [[plutonium-ui]]
|
|
422
|
+
- [[plutonium-resource]]: Definition layer, `grid_fields`, index views, actions
|
|
423
|
+
- [[plutonium-behavior]]: Policy methods, `kanban_move?`, interactions
|
|
424
|
+
- [[plutonium-ui]]: Custom Phlex components for card rendering
|