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
|
# Customizing the UI
|
|
2
2
|
|
|
3
|
-
Plutonium's UI is built on Phlex, Tailwind 4, and Stimulus. Almost everything you see
|
|
3
|
+
Plutonium's UI is built on Phlex, Tailwind 4, and Stimulus. Almost everything you see (pages, forms, displays, tables, components, layouts, even the design tokens) is open for override. This guide is the map. Each section shows the smallest useful example for one kind of customization, then points to the reference for the full surface.
|
|
4
4
|
|
|
5
5
|
When you're not sure where to start, read this top to bottom. When you know what you need, jump to the right section and follow the link to reference.
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ class PostDefinition < ResourceDefinition
|
|
|
17
17
|
end
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Each nested class inherits from Plutonium's defaults and lets you override only the methods you care about. Don't reimplement the whole layer
|
|
20
|
+
Each nested class inherits from Plutonium's defaults and lets you override only the methods you care about. Don't reimplement the whole layer; use the render hooks below.
|
|
21
21
|
|
|
22
22
|
→ See [Reference › UI › Pages](/reference/ui/pages) for the full hook list.
|
|
23
23
|
|
|
@@ -29,15 +29,17 @@ Most page customization is "I want to add something before/after this section."
|
|
|
29
29
|
class PostDefinition < ResourceDefinition
|
|
30
30
|
class ShowPage < ShowPage
|
|
31
31
|
def render_before_content
|
|
32
|
-
div(class: "pu-
|
|
33
|
-
|
|
32
|
+
div(class: "pu-alert pu-alert-info", role: "status") do
|
|
33
|
+
div(class: "pu-alert-message") { t("blog.posts.show.comment_count", count: object.comments.count) }
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
`pu-alert pu-alert-<success|warning|danger|info>` is the same banner the flash messages use, so it already has its dark-mode colors. The text comes from a locale key; see [i18n](/reference/i18n#your-own-components-and-pages).
|
|
41
|
+
|
|
42
|
+
Hooks exist around the header, breadcrumbs, page header, toolbar, content, and footer: pick the one closest to where you want the thing to appear.
|
|
41
43
|
|
|
42
44
|
→ See [Reference › UI › Pages](/reference/ui/pages) › Page hooks.
|
|
43
45
|
|
|
@@ -56,7 +58,7 @@ class Form < Form
|
|
|
56
58
|
render_resource_field :published_at
|
|
57
59
|
render_resource_field :category
|
|
58
60
|
end
|
|
59
|
-
render_actions # REQUIRED
|
|
61
|
+
render_actions # REQUIRED: without this, no submit button
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
private
|
|
@@ -126,14 +128,14 @@ class PostCardComponent < Plutonium::UI::Component::Base
|
|
|
126
128
|
div(class: "pu-card pu-card-body") do
|
|
127
129
|
h3(class: "font-bold text-[var(--pu-text)]") { @post.title }
|
|
128
130
|
p(class: "text-[var(--pu-text-muted)] mt-2") { @post.excerpt }
|
|
129
|
-
a(href: resource_url_for(@post), class: "pu-btn pu-btn-sm pu-btn-ghost") { "
|
|
131
|
+
a(href: resource_url_for(@post), class: "pu-btn pu-btn-sm pu-btn-ghost") { t("blog.posts.card.read_more") }
|
|
130
132
|
end
|
|
131
133
|
end
|
|
132
134
|
end
|
|
133
135
|
```
|
|
134
136
|
|
|
135
137
|
Use it directly in a page, or wire it as a field in the definition. A component
|
|
136
|
-
with its own constructor takes the block form
|
|
138
|
+
with its own constructor takes the block form: you build it, so you decide what
|
|
137
139
|
it receives:
|
|
138
140
|
|
|
139
141
|
```ruby
|
|
@@ -143,13 +145,13 @@ end
|
|
|
143
145
|
```
|
|
144
146
|
|
|
145
147
|
`as: SomeComponent` is for a *field* component, which Plutonium constructs with
|
|
146
|
-
the field builder (`Phlexi::Form::Components::Base` / `Phlexi::Display::Components::Base`)
|
|
148
|
+
the field builder (`Phlexi::Form::Components::Base` / `Phlexi::Display::Components::Base`); see the reference below.
|
|
147
149
|
|
|
148
150
|
→ See [Reference › UI › Components](/reference/ui/components).
|
|
149
151
|
|
|
150
152
|
## Phlexi themes (recolor without rewriting)
|
|
151
153
|
|
|
152
|
-
If all you want is to recolor or restyle the form/display/table, write a `Theme` class instead of overriding the template. Always `super.merge(...)
|
|
154
|
+
If all you want is to recolor or restyle the form/display/table, write a `Theme` class instead of overriding the template. Always `super.merge(...)`; never replace wholesale:
|
|
153
155
|
|
|
154
156
|
```ruby
|
|
155
157
|
class Form < Form
|
|
@@ -173,7 +175,7 @@ By default `:new`, `:edit`, and every interactive action render in a slideover p
|
|
|
173
175
|
|
|
174
176
|
```ruby
|
|
175
177
|
class PostDefinition < ResourceDefinition
|
|
176
|
-
modal :slideover # default
|
|
178
|
+
modal :slideover # default: slide-in from the right
|
|
177
179
|
# modal :centered # centered dialog
|
|
178
180
|
# modal :centered, size: :lg # centered, wider container
|
|
179
181
|
# modal false # full standalone page
|
|
@@ -184,7 +186,7 @@ end
|
|
|
184
186
|
|
|
185
187
|
## Layouts and the shell
|
|
186
188
|
|
|
187
|
-
The layout is the chrome around every resource page
|
|
189
|
+
The layout is the chrome around every resource page: topbar, sidebar, flash region, scripts. Two ways to customize it:
|
|
188
190
|
|
|
189
191
|
```bash
|
|
190
192
|
# Per-portal: eject the shell partials and edit them directly
|
|
@@ -199,24 +201,24 @@ For programmatic overrides, subclass `Plutonium::UI::Layout::ResourceLayout` and
|
|
|
199
201
|
|
|
200
202
|
## Tailwind, Stimulus, and assets
|
|
201
203
|
|
|
202
|
-
Plutonium ships with a Tailwind config, design tokens, and a set of Stimulus controllers.
|
|
204
|
+
Plutonium ships with a Tailwind config, design tokens, and a set of Stimulus controllers. Out of the box the app serves the gem's prebuilt `plutonium.css` and `plutonium.min.js`, so app-side Tailwind classes, a new brand palette, token overrides and your own Stimulus controllers need your own bundles first. Run the generator rather than wiring the pipeline by hand:
|
|
203
205
|
|
|
204
206
|
```bash
|
|
205
207
|
rails generate pu:core:assets
|
|
206
208
|
```
|
|
207
209
|
|
|
208
|
-
This installs the npm packages, creates a `tailwind.config.js` that extends Plutonium's defaults via `plutoniumTailwindConfig.merge`, imports Plutonium's CSS,
|
|
210
|
+
This installs the npm packages, creates a `tailwind.config.js` that extends Plutonium's defaults via `plutoniumTailwindConfig.merge`, imports Plutonium's CSS, registers its Stimulus controllers, and points `config.assets.stylesheet` / `config.assets.script` at your `application` bundles. It needs `app/assets/stylesheets/application.tailwind.css` and `app/javascript/controllers/index.js` to exist (an app created with `-j esbuild -c tailwind` plus Stimulus). After that, you can:
|
|
209
211
|
|
|
210
|
-
- Extend the palette under `theme.extend.colors` (always inside `plutoniumTailwindConfig.merge
|
|
211
|
-
- Use `.pu-btn`, `.pu-card`, `.pu-input`, `.pu-table`, etc. instead of hand-rolling Tailwind chains.
|
|
212
|
+
- Extend the palette under `theme.extend.colors` (always inside `plutoniumTailwindConfig.merge`: a plain spread drops Plutonium's defaults), then rebuild the CSS.
|
|
213
|
+
- Use `.pu-btn`, `.pu-card`, `.pu-input`, `.pu-table`, `.pu-alert`, `.pu-badge`, etc. instead of hand-rolling Tailwind chains.
|
|
212
214
|
- Reference design tokens directly: `bg-[var(--pu-surface)]`, `text-[var(--pu-text-muted)]`, `border-[var(--pu-border)]`. These auto-switch with dark mode.
|
|
213
|
-
- Register your own Stimulus controllers alongside Plutonium's
|
|
215
|
+
- Register your own Stimulus controllers alongside Plutonium's. Your `application.js` now replaces the gem's bundle, so `registerControllers(application)` (added by the generator) must stay, or the entire interactive layer is dead.
|
|
214
216
|
|
|
215
217
|
→ See [Reference › UI › Assets](/reference/ui/assets) for the full toolchain, the `.pu-*` class catalog, and design-token reference.
|
|
216
218
|
|
|
217
219
|
## ERB views (escape hatch)
|
|
218
220
|
|
|
219
|
-
When the Phlex page class is the wrong tool
|
|
221
|
+
When the Phlex page class is the wrong tool (you want to keep an existing ERB layout, you're integrating with a designer's HTML, or you just want to surround the generated page with custom markup), drop an ERB view at the controller path:
|
|
220
222
|
|
|
221
223
|
```
|
|
222
224
|
app/views/posts/show.html.erb
|
|
@@ -237,7 +239,7 @@ Keep that line and wrap it to add chrome without giving up the generated page:
|
|
|
237
239
|
<%= render partial: "related" %>
|
|
238
240
|
```
|
|
239
241
|
|
|
240
|
-
Or replace the line entirely for full control. ERB views always win over the Phlex page class when both exist for the same action
|
|
242
|
+
Or replace the line entirely for full control. ERB views always win over the Phlex page class when both exist for the same action, so reach for this only when Phlex hooks + overrides genuinely can't do the job.
|
|
241
243
|
|
|
242
244
|
## When to reach for what
|
|
243
245
|
|
|
@@ -249,18 +251,19 @@ Or replace the line entirely for full control. ERB views always win over the Phl
|
|
|
249
251
|
| Reuse a UI block across pages | Custom Phlex component |
|
|
250
252
|
| Recolor without changing structure | Phlexi `Theme` class |
|
|
251
253
|
| Swap the topbar or sidebar | `pu:eject:shell` or custom layout class |
|
|
252
|
-
| Change brand color
|
|
254
|
+
| Change brand color | Tailwind `primary` palette via `plutoniumTailwindConfig.merge`, then rebuild. See [Theming](/guides/theming) |
|
|
255
|
+
| Change surfaces, borders or radius | Design tokens (mirrored in `.dark`). See [Theming](/guides/theming) |
|
|
253
256
|
| Add a custom JS interaction | Stimulus controller registered alongside Plutonium's |
|
|
254
257
|
|
|
255
258
|
## Gotchas
|
|
256
259
|
|
|
257
|
-
- **Don't override `view_template` in pages** when a render hook fits
|
|
258
|
-
- **`render_actions` is mandatory** when you write a custom `form_template
|
|
259
|
-
- **
|
|
260
|
+
- **Don't override `view_template` in pages** when a render hook fits, or you lose breadcrumbs, header, and DynaFrame (turbo-frame) behavior.
|
|
261
|
+
- **`render_actions` is mandatory** when you write a custom `form_template`, otherwise the form has no submit button.
|
|
262
|
+
- **Once the app serves its own JS bundle, `registerControllers(application)`** must be in `app/javascript/controllers/index.js` (`pu:core:assets` adds it). Without it, every Plutonium-shipped Stimulus controller is dead (color mode, slim-select, flatpickr, easymde, form pre-submit).
|
|
260
263
|
- **Use `plutoniumTailwindConfig.merge`** when extending the Tailwind theme. A plain object spread drops Plutonium's defaults.
|
|
261
|
-
- **
|
|
264
|
+
- **Style with `.pu-*` classes first, `var(--pu-*)` tokens second, raw palette pairs last.** The classes and tokens switch with dark mode; a hand-written `gray-X/dark:gray-Y` pair needs its own dark twin and won't follow a rebrand.
|
|
262
265
|
|
|
263
266
|
## Related
|
|
264
267
|
|
|
265
|
-
- [Theming](/guides/theming)
|
|
266
|
-
- [Reference › UI](/reference/ui/)
|
|
268
|
+
- [Theming](/guides/theming): design tokens, brand colors.
|
|
269
|
+
- [Reference › UI](/reference/ui/): the full surface area for every override above.
|
data/docs/guides/dashboards.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Dashboards are experimental: the DSL and behavior may change in a future release.
|
|
5
5
|
:::
|
|
6
6
|
|
|
7
|
-
A dashboard is a page of cards: headline numbers, charts and free-form panels, declared in one Ruby class and mounted with a single routes line.
|
|
7
|
+
A dashboard is a page of cards: headline numbers, charts and free-form panels, declared in one Ruby class and mounted with a single routes line. By default every card loads in its own lazy turbo frame, so the page paints immediately and each card's queries run in a separate request as it scrolls into view.
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
data/docs/guides/index.md
CHANGED
|
@@ -25,12 +25,12 @@ aside: false
|
|
|
25
25
|
{ name: 'Nested resources', link: '/plutonium-core/guides/nested-resources' },
|
|
26
26
|
{ name: 'Multi-tenancy', link: '/plutonium-core/guides/multi-tenancy' },
|
|
27
27
|
{ name: 'Search & filtering', link: '/plutonium-core/guides/search-filtering' },
|
|
28
|
-
{ name: 'Wizards', desc: 'Multi-step flows
|
|
29
|
-
{ name: 'Kanban boards', desc: 'Drag-and-drop board view
|
|
28
|
+
{ name: 'Wizards', desc: 'Multi-step flows: onboarding, checkout, branching create.', link: '/plutonium-core/guides/wizards' },
|
|
29
|
+
{ name: 'Kanban boards', desc: 'Drag-and-drop board view: columns, moves, positioning, WIP limits.', link: '/plutonium-core/guides/kanban' },
|
|
30
30
|
{ name: 'Dashboards', desc: 'Metric, chart and free-form cards, each loaded in its own lazy turbo frame.', link: '/plutonium-core/guides/dashboards' },
|
|
31
31
|
]},
|
|
32
32
|
{ group: 'Customization', items: [
|
|
33
|
-
{ name: 'Customizing the UI', desc: 'A map of the override surface
|
|
33
|
+
{ name: 'Customizing the UI', desc: 'A map of the override surface: pages, forms, displays, tables, components, layouts.', link: '/plutonium-core/guides/customizing-ui' },
|
|
34
34
|
{ name: 'Theming', desc: 'Design tokens and brand colors.', link: '/plutonium-core/guides/theming' },
|
|
35
35
|
]},
|
|
36
36
|
{ group: 'Quality', items: [
|
data/docs/guides/kanban.md
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
# Kanban Boards
|
|
2
2
|
|
|
3
3
|
::: warning Experimental
|
|
4
|
-
Kanban boards are experimental
|
|
4
|
+
Kanban boards are experimental: the DSL and behavior may change in a future release.
|
|
5
5
|
:::
|
|
6
6
|
|
|
7
|
-
Turn any resource index into a drag-and-drop kanban board
|
|
7
|
+
Turn any resource index into a drag-and-drop kanban board: columns, WIP limits, quick-add, column actions, and opt-in realtime, all from a single `kanban do…end` block in your definition.
|
|
8
8
|
|
|
9
|
-

|
|
10
10
|
|
|
11
11
|
## What you get
|
|
12
12
|
|
|
13
13
|
- Drag cards between columns; the server persists the column change and the position within the column.
|
|
14
|
-
- Decimal fractional positioning
|
|
14
|
+
- Decimal fractional positioning: cards always land exactly where you drop them without renumbering.
|
|
15
15
|
- Per-column `+ Add` button opens the resource's normal new form; the new card is placed in that column (`on_enter` + positioning applied post-create).
|
|
16
16
|
- Column actions run an interaction against all (or visible) cards in a column.
|
|
17
17
|
- WIP limits, locked columns, and cross-column drop restrictions enforced server-side.
|
|
18
18
|
- Opt-in realtime: every connected viewer sees the same board state after any move.
|
|
19
19
|
|
|
20
|
-
## Worked example
|
|
20
|
+
## Worked example: Task board
|
|
21
21
|
|
|
22
|
-
A complete board for a `Task` model grouped by status
|
|
22
|
+
A complete board for a `Task` model grouped by status, migration, model, definition, and policy.
|
|
23
23
|
|
|
24
24
|
### 1. Migration
|
|
25
25
|
|
|
26
|
-
The model needs a `decimal` position column. Use the **`t.position`** helper
|
|
26
|
+
The model needs a `decimal` position column. Use the **`t.position`** helper, it adds a `decimal` column already tuned for fractional ordering (`precision: 16, scale: 8`), so you can't pick a scale too small to rebalance cleanly (see [Positioning › Migration](/reference/kanban/positioning#migration)).
|
|
27
27
|
|
|
28
28
|
```ruby
|
|
29
29
|
class CreateTasks < ActiveRecord::Migration[8.1]
|
|
@@ -102,25 +102,25 @@ class TaskPolicy < ResourcePolicy
|
|
|
102
102
|
end
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
### 5. Routes
|
|
105
|
+
### 5. Routes: no changes needed
|
|
106
106
|
|
|
107
107
|
The `kanban_move` member route is wired automatically when the controller includes `Plutonium::Resource::Controllers::KanbanActions` (included by default in all Plutonium resource controllers).
|
|
108
108
|
|
|
109
109
|
Visit the resource index and use the view switcher to select the Kanban view.
|
|
110
110
|
|
|
111
|
-

|
|
112
112
|
|
|
113
113
|
### When a drop is rejected
|
|
114
114
|
|
|
115
|
-
If a move is refused server-side
|
|
115
|
+
If a move is refused server-side: the destination is at its `wip:` limit, its `accepts:` policy rejects the card, or the source column is `locked:`, then the card snaps back to where it started **and** a dismissable toast explains why:
|
|
116
116
|
|
|
117
117
|

|
|
118
118
|
|
|
119
|
-
The toast is appended to a `#kanban-flash` region in the board shell (outside the per-column frames, so it survives the snap-back re-render). The client-side drag hints already grey out columns a card plainly can't enter, so the toast mainly surfaces the cases the browser can't pre-check
|
|
119
|
+
The toast is appended to a `#kanban-flash` region in the board shell (outside the per-column frames, so it survives the snap-back re-render). The client-side drag hints already grey out columns a card plainly can't enter, so the toast mainly surfaces the cases the browser can't pre-check, most commonly a WIP-full column or a `kanban_move?` denial.
|
|
120
120
|
|
|
121
121
|
### Opening a card
|
|
122
122
|
|
|
123
|
-
Clicking a card opens its show page. Where it opens is controlled by [`show_in`](/reference/kanban/dsl#show_in)
|
|
123
|
+
Clicking a card opens its show page. Where it opens is controlled by [`show_in`](/reference/kanban/dsl#show_in), full-page by default, or a **centered modal** that keeps the board visible behind it:
|
|
124
124
|
|
|
125
125
|

|
|
126
126
|
|
|
@@ -135,14 +135,14 @@ end
|
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
- Set `show_in :modal` on the **definition** to open show in a modal from the table, grid, and board alike. Set it on the **kanban block** to change only the board. An unset board inherits the definition (which defaults to `:page`).
|
|
138
|
-
- The show modal is always **centered
|
|
138
|
+
- The show modal is always **centered**: distinct from `new`/`edit`, which follow the definition's `modal_mode` (a slideover by default).
|
|
139
139
|
- From inside the modal, an expand icon opens the record's full page in a new tab. ⌘/Ctrl-click (or middle-click) on a card does the same directly.
|
|
140
140
|
|
|
141
141
|
---
|
|
142
142
|
|
|
143
|
-
## Worked example
|
|
143
|
+
## Worked example: Status enum board
|
|
144
144
|
|
|
145
|
-
A shorter example that groups by a Rails enum for status. Cards reuse `grid_fields` for their slot layout
|
|
145
|
+
A shorter example that groups by a Rails enum for status. Cards reuse `grid_fields` for their slot layout, so no explicit `card_fields` needed.
|
|
146
146
|
|
|
147
147
|
```ruby
|
|
148
148
|
class KitchenSinkDefinition < ResourceDefinition
|
|
@@ -168,7 +168,7 @@ Key points:
|
|
|
168
168
|
- `role: :backlog` enables the `+ Add` button (equivalent to `add: true`).
|
|
169
169
|
- `wip: 5` caps the Pending column; a cross-column drop that would push it past 5 is rejected server-side.
|
|
170
170
|
- `role: :done` collapses the Archived column by default and shows a green header dot.
|
|
171
|
-
- `on_enter` here assigns the attribute in memory (`ks.status = :active`). The framework calls `record.save!` automatically when the record has unsaved changes after `on_enter` returns
|
|
171
|
+
- `on_enter` here assigns the attribute in memory (`ks.status = :active`). The framework calls `record.save!` automatically when the record has unsaved changes after `on_enter` returns; you do not need to call `update!` explicitly.
|
|
172
172
|
|
|
173
173
|
---
|
|
174
174
|
|
|
@@ -195,7 +195,7 @@ Use `columns do…end` when the column list depends on request context (`current
|
|
|
195
195
|
```ruby
|
|
196
196
|
kanban do
|
|
197
197
|
columns do
|
|
198
|
-
# `self` is the view_context
|
|
198
|
+
# `self` is the view_context: current_user, params, helpers all work.
|
|
199
199
|
current_user.projects.map do |project|
|
|
200
200
|
Plutonium::Kanban::Column.new(
|
|
201
201
|
:"project_#{project.id}",
|
|
@@ -228,7 +228,7 @@ class TaskDefinition < ResourceDefinition
|
|
|
228
228
|
end
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
-
Note that **`enter_interaction:` is not supported on dynamic boards
|
|
231
|
+
Note that **`enter_interaction:` is not supported on dynamic boards**: its hidden action is registered from the static column list at class-load time, and its key is internal (column-scoped) so it can't be registered manually the way a column action can. A drop into such a column snaps back rather than committing (it doesn't crash). Use a static board if a column needs an `enter_interaction:`.
|
|
232
232
|
:::
|
|
233
233
|
|
|
234
234
|
### Column options
|
|
@@ -236,11 +236,11 @@ Note that **`enter_interaction:` is not supported on dynamic boards** — its hi
|
|
|
236
236
|
| Option | Type | Default | Description |
|
|
237
237
|
|--------|------|---------|-------------|
|
|
238
238
|
| `label:` | String | `key.to_s.titleize` | Column header text |
|
|
239
|
-
| `color:` | Symbol or String | `nil` | Dot color in the column header
|
|
239
|
+
| `color:` | Symbol or String | `nil` | Dot color in the column header, `:red`, `:orange`, `:amber`, `:yellow`, `:green`, `:blue`, `:purple`, `:pink`, `:gray`, or a raw CSS value |
|
|
240
240
|
| `scope:` | Symbol or Proc | `nil` | Filters the resource relation to this column's cards. Symbol → named scope; Proc → 0-arg lambda called with `instance_exec` on the relation (e.g. `-> { where(status: "todo") }`) |
|
|
241
241
|
| `on_enter:` | Symbol or Proc | `nil` | Called when a card lands in this column. Symbol → `record.public_send(sym)`; Proc → 1-arg lambda `->(record) { … }` where `self` is the view context |
|
|
242
|
-
| `on_exit:` | Symbol or Proc | `nil` | Source-side counterpart to `on_enter
|
|
243
|
-
| `enter_interaction:` | Class | `nil` | Record-scoped interaction run on a cross-column drop into this column
|
|
242
|
+
| `on_exit:` | Symbol or Proc | `nil` | Source-side counterpart to `on_enter:`: called when a card **leaves** this column on a cross-column move, before the destination's `on_enter`, in the same transaction. For source-tied side effects (stop a timer, release a slot). Drag-moves only (not destroy/programmatic/quick-add); skipped on same-column reorders. Use a model callback instead only when other save paths (edit form, API, import) must be covered too, and then drop the `on_exit:` so the work isn't done twice. |
|
|
243
|
+
| `enter_interaction:` | Class | `nil` | Record-scoped interaction run on a cross-column drop into this column; opens a modal to collect input, then commits atomically. See [Interaction on drop](#interaction-on-drop) |
|
|
244
244
|
| `role:` | `:backlog`, `:done`, `:lost` | `nil` | Preset shorthand (see below) |
|
|
245
245
|
| `collapsed:` | Boolean | `false` | Start collapsed |
|
|
246
246
|
| `add:` | Boolean | `false` | Show `+ Add` quick-add button |
|
|
@@ -256,7 +256,7 @@ Note that **`enter_interaction:` is not supported on dynamic boards** — its hi
|
|
|
256
256
|
| `:done` | `color: :green, collapsed: true` |
|
|
257
257
|
| `:lost` | `color: :red, collapsed: true` |
|
|
258
258
|
|
|
259
|
-
`:done` and `:lost` are the two terminal roles (both collapsed by default)
|
|
259
|
+
`:done` and `:lost` are the two terminal roles (both collapsed by default), the
|
|
260
260
|
won/lost pair for pipelines like leads, deals, or tickets; the colour signals the
|
|
261
261
|
outcome.
|
|
262
262
|
|
|
@@ -284,8 +284,8 @@ column :done,
|
|
|
284
284
|
end
|
|
285
285
|
```
|
|
286
286
|
|
|
287
|
-
- `on: :all
|
|
288
|
-
- `on: :visible
|
|
287
|
+
- `on: :all`: passes IDs of **all** cards in the column (ignoring `per_column`).
|
|
288
|
+
- `on: :visible`: passes IDs of only the rendered, `per_column`-capped cards.
|
|
289
289
|
|
|
290
290
|
Column actions are rendered as buttons in the column header. They open the normal interactive-action modal (with form, authorization, success/failure handling) pre-loaded with the column's card IDs.
|
|
291
291
|
|
|
@@ -293,7 +293,7 @@ Column actions are rendered as buttons in the column header. They open the norma
|
|
|
293
293
|
|
|
294
294
|
## Interaction on drop
|
|
295
295
|
|
|
296
|
-
A column can declare `enter_interaction:` to run an authorization-aware, input-collecting [Interaction](/reference/behavior/interactions) when a card is dropped **into** it from another column. Use it when entering a column needs more than a membership flip
|
|
296
|
+
A column can declare `enter_interaction:` to run an authorization-aware, input-collecting [Interaction](/reference/behavior/interactions) when a card is dropped **into** it from another column. Use it when entering a column needs more than a membership flip, a reason, a notification email, an audit entry.
|
|
297
297
|
|
|
298
298
|
```ruby
|
|
299
299
|
column :lost,
|
|
@@ -301,13 +301,13 @@ column :lost,
|
|
|
301
301
|
enter_interaction: MarkLostInteraction
|
|
302
302
|
```
|
|
303
303
|
|
|
304
|
-
`enter_interaction:` takes an **Interaction class**. It must be **record-scoped
|
|
304
|
+
`enter_interaction:` takes an **Interaction class**. It must be **record-scoped**: it declares `attribute :resource` and acts on the single dropped card. A bulk (`attribute :resources`) interaction is not valid here; that shape is for [column actions](#column-actions).
|
|
305
305
|
|
|
306
|
-
The interaction is **auto-registered as a hidden record action** under a column-scoped key (`:lost` → `:lost_enter_interaction`), so two columns can reuse the same interaction class without colliding. "Hidden" means it does **not** appear as an action button on the show page, table rows, or grid cards
|
|
306
|
+
The interaction is **auto-registered as a hidden record action** under a column-scoped key (`:lost` → `:lost_enter_interaction`), so two columns can reuse the same interaction class without colliding. "Hidden" means it does **not** appear as an action button on the show page, table rows, or grid cards, it is reachable only by dropping a card into the column.
|
|
307
307
|
|
|
308
308
|
### The interaction
|
|
309
309
|
|
|
310
|
-
A drop interaction is an ordinary record-scoped interaction
|
|
310
|
+
A drop interaction is an ordinary record-scoped interaction with nothing kanban-specific in the class:
|
|
311
311
|
|
|
312
312
|
```ruby
|
|
313
313
|
class MarkLostInteraction < ResourceInteraction
|
|
@@ -330,7 +330,7 @@ end
|
|
|
330
330
|
|
|
331
331
|
### Authorization
|
|
332
332
|
|
|
333
|
-
The drop is authorized by the single **`kanban_move?`** predicate
|
|
333
|
+
The drop is authorized by the single **`kanban_move?`** predicate, the interaction has **no policy method of its own**. To gate this specific transition, branch on the destination column, which `kanban_move?` reads from its authorization context (`kanban_to`):
|
|
334
334
|
|
|
335
335
|
```ruby
|
|
336
336
|
class TaskPolicy < ResourcePolicy
|
|
@@ -341,7 +341,7 @@ class TaskPolicy < ResourcePolicy
|
|
|
341
341
|
end
|
|
342
342
|
```
|
|
343
343
|
|
|
344
|
-
This keeps authorization in one place: `kanban_move?` gates every move, and the `to` (and `from`) column context lets it gate a specific transition
|
|
344
|
+
This keeps authorization in one place: `kanban_move?` gates every move, and the `to` (and `from`) column context lets it gate a specific transition: no per-interaction predicate, no `condition:` proc. If the check fails the drop is refused and the card stays put. See [Authorization](../reference/kanban/authorization) for the full `from`/`to` context.
|
|
345
345
|
|
|
346
346
|
### Two flows, split by intent
|
|
347
347
|
|
|
@@ -353,9 +353,9 @@ This keeps authorization in one place: `kanban_move?` gates every move, and the
|
|
|
353
353
|
A column can declare `on_enter:` and `enter_interaction:` together. When it does:
|
|
354
354
|
|
|
355
355
|
- `on_enter` owns the **membership attribute** (the column's grouping value, e.g. `status`).
|
|
356
|
-
- `enter_interaction` owns the **extras
|
|
356
|
+
- `enter_interaction` owns the **extras**: the reason, the mail, the audit trail.
|
|
357
357
|
|
|
358
|
-
If the interaction also writes the membership attribute it **must set the same value** `on_enter` sets (idempotent). In this dummy-app example the `:blocked` column does exactly that
|
|
358
|
+
If the interaction also writes the membership attribute it **must set the same value** `on_enter` sets (idempotent). In this dummy-app example the `:blocked` column does exactly that, `on_enter` sets `status = "blocked"` and the interaction's `execute` re-asserts `status: "blocked"` while adding the reason:
|
|
359
359
|
|
|
360
360
|
```ruby
|
|
361
361
|
column :blocked,
|
|
@@ -364,25 +364,25 @@ column :blocked,
|
|
|
364
364
|
enter_interaction: BlockTaskInteraction
|
|
365
365
|
```
|
|
366
366
|
|
|
367
|
-
When a column declares **only** a `enter_interaction` (no `on_enter`, like `:lost` above), the interaction owns everything
|
|
367
|
+
When a column declares **only** a `enter_interaction` (no `on_enter`, like `:lost` above), the interaction owns everything, including the membership write, because there is no `on_enter` to do it.
|
|
368
368
|
|
|
369
369
|
### Same-column drops run positioning only
|
|
370
370
|
|
|
371
|
-
Reordering a card **within** its current column runs positioning only. Neither `on_enter` nor the `enter_interaction` fires
|
|
371
|
+
Reordering a card **within** its current column runs positioning only. Neither `on_enter` nor the `enter_interaction` fires; both represent *entering* a column, and a same-column reorder is not an entry. Only cross-column drops trigger them.
|
|
372
372
|
|
|
373
373
|
### Atomicity and failure
|
|
374
374
|
|
|
375
|
-
Interaction validation failure rolls the **whole transaction back
|
|
375
|
+
Interaction validation failure rolls the **whole transaction back**, the membership write included, and re-renders the modal with errors. The move context is preserved, so the user can fix the input and resubmit. Nothing is persisted on failure. Keep side-effects on `deliver_later` (mailers, jobs): a rolled-back failure then sends no stray mail, because the enqueue never commits.
|
|
376
376
|
|
|
377
377
|
### Success feedback and the response limitation
|
|
378
378
|
|
|
379
379
|
On success the board's column frames re-render and the modal closes. The interaction's success **message** (`succeed(resource).with_message("Marked as lost")`) is surfaced as a toast.
|
|
380
380
|
|
|
381
381
|
::: warning Custom success responses are not honored on the drop path
|
|
382
|
-
A drop interaction's custom success *response
|
|
382
|
+
A drop interaction's custom success *response*, `with_redirect_response`, `with_file_response`, etc., is **not** honored when it runs from a drop: the board simply re-renders and closes the modal. Keep drop interactions to simple state + extras mutations, and use `.with_message` for feedback.
|
|
383
383
|
:::
|
|
384
384
|
|
|
385
|
-
There is no card "snap-back" to worry about on cancel
|
|
385
|
+
There is no card "snap-back" to worry about on cancel: native drag never moves the card's DOM node, so canceling the modal just closes it and the card stays where it was.
|
|
386
386
|
|
|
387
387
|
---
|
|
388
388
|
|
|
@@ -390,35 +390,35 @@ There is no card "snap-back" to worry about on cancel — native drag never move
|
|
|
390
390
|
|
|
391
391
|
By default Plutonium uses decimal fractional positioning: cards always slot exactly where you drop them without ever renumbering the whole column. You need:
|
|
392
392
|
|
|
393
|
-
1. A `decimal` database column
|
|
393
|
+
1. A `decimal` database column: use the `t.position` helper (`precision: 16, scale: 8`). Hand-rolling it, keep `scale` at **8 or more**: `scale: 6` exactly matches the `1e-6` rebalance threshold and the last subdivision can round into a neighbour.
|
|
394
394
|
2. `include Plutonium::Positioning::Model` in the model.
|
|
395
|
-
3. `positioned_on :position, scope: :status
|
|
395
|
+
3. `positioned_on :position, scope: :status`: the `scope:` option groups positions by the grouping attribute so cards in different columns don't compete.
|
|
396
396
|
|
|
397
397
|
### Position modes
|
|
398
398
|
|
|
399
|
-
`position_on` is the same verb inside and outside `kanban do…end`. Declared on the **definition** it makes the resource's index table and card grid [drag-reorderable](/reference/resource/positioning); declared inside the board it configures the board. A board that declares none **inherits the definition's** (falling back to `:position`, Mode A), so a resource that already reorders in its table needs nothing extra here. Declaration order in the class body doesn't matter
|
|
399
|
+
`position_on` is the same verb inside and outside `kanban do…end`. Declared on the **definition** it makes the resource's index table and card grid [drag-reorderable](/reference/resource/positioning); declared inside the board it configures the board. A board that declares none **inherits the definition's** (falling back to `:position`, Mode A), so a resource that already reorders in its table needs nothing extra here. Declaration order in the class body doesn't matter; the board resolves this lazily.
|
|
400
400
|
|
|
401
401
|
```ruby
|
|
402
402
|
kanban do
|
|
403
|
-
# Mode A (default)
|
|
403
|
+
# Mode A (default): delegate to Plutonium::Positioning::Model.
|
|
404
404
|
# Uses :position attribute, requires the model concern.
|
|
405
405
|
position_on :position
|
|
406
406
|
|
|
407
407
|
# Mode A with a custom attribute name:
|
|
408
408
|
position_on :sort_order
|
|
409
409
|
|
|
410
|
-
# Mode B
|
|
410
|
+
# Mode B: BYO positioning. The block receives a Move struct.
|
|
411
411
|
# Use when you want to call a custom service or use a different ordering scheme.
|
|
412
412
|
position_on :sort_order do |move|
|
|
413
|
-
# move.record
|
|
414
|
-
# move.column
|
|
415
|
-
# move.prev
|
|
416
|
-
# move.next
|
|
417
|
-
# move.index
|
|
413
|
+
# move.record : the dropped record
|
|
414
|
+
# move.column : the destination column key (Symbol)
|
|
415
|
+
# move.prev : the record immediately before the drop slot (or nil)
|
|
416
|
+
# move.next : the record immediately after the drop slot (or nil)
|
|
417
|
+
# move.index : 0-based insertion index within the destination column
|
|
418
418
|
MyPositioningService.call(move.record, prev: move.prev, next: move.next)
|
|
419
419
|
end
|
|
420
420
|
|
|
421
|
-
# Mode C
|
|
421
|
+
# Mode C: no ordering. Cards render in the relation's default order.
|
|
422
422
|
# On-drop still fires; position is just never updated.
|
|
423
423
|
position_on false
|
|
424
424
|
end
|
|
@@ -445,7 +445,7 @@ Each column loads at most 25 cards. When the total exceeds the limit, a `+N more
|
|
|
445
445
|
|
|
446
446
|
When `add: true` (or `role: :backlog`) is set on a column, a `+ Add` button appears in the column header. Clicking it opens the resource's normal new form in a modal.
|
|
447
447
|
|
|
448
|
-
The record is created normally, and **then** the column's `on_enter` and positioning are applied to the **saved** record
|
|
448
|
+
The record is created normally, and **then** the column's `on_enter` and positioning are applied to the **saved** record, so the new card lands in the clicked column, appended to the bottom. `on_enter` runs against a real, persisted record (exactly as it does for a drag), so `update!`-style callbacks and any side effects behave identically and fire once, on the actual create.
|
|
449
449
|
|
|
450
450
|
::: warning Give your grouping column a default
|
|
451
451
|
Because `on_enter` runs **after** the record is saved, the record must be creatable **without** a grouping value. Give your grouping column (e.g. `status`) a database or model default. If it is `NOT NULL` with no default, quick-add create fails validation before `on_enter` can set it.
|
|
@@ -495,10 +495,10 @@ After a successful move, Plutonium broadcasts the updated column frames to all c
|
|
|
495
495
|
|
|
496
496
|
Plutonium emits the `<turbo-cable-stream-source>` subscription element and broadcasts on the server, but the **client must have an ActionCable consumer** to receive it. Plutonium's bundled JavaScript ships `@hotwired/turbo` only (no cable client), so you must wire the rest up yourself:
|
|
497
497
|
|
|
498
|
-
1. **Gems
|
|
499
|
-
2. **Cable adapter** (`config/cable.yml`)
|
|
500
|
-
3. **Mount ActionCable
|
|
501
|
-
4. **Load the cable client in your app's JavaScript
|
|
498
|
+
1. **Gems**: `turbo-rails` and `actioncable` (Rails includes ActionCable; `turbo-rails` provides `Turbo::StreamsChannel` and `turbo_stream_from`).
|
|
499
|
+
2. **Cable adapter** (`config/cable.yml`): `async` is fine for a single-process dev server; use **Redis** (or Solid Cable) for multi-process production, otherwise a broadcast from one worker won't reach clients connected to another.
|
|
500
|
+
3. **Mount ActionCable**: `mount ActionCable.server => "/cable"` (Rails mounts it by default when `action_cable/engine` is loaded).
|
|
501
|
+
4. **Load the cable client in your app's JavaScript**: this is the step most people miss. Add **one** of:
|
|
502
502
|
```js
|
|
503
503
|
// app pack, alongside your other imports
|
|
504
504
|
import "@hotwired/turbo-rails" // registers <turbo-cable-stream-source> + a consumer
|
|
@@ -511,7 +511,7 @@ Plutonium emits the `<turbo-cable-stream-source>` subscription element and broad
|
|
|
511
511
|
Without this, the server broadcasts but no browser is subscribed, so other viewers won't update until they reload.
|
|
512
512
|
|
|
513
513
|
::: tip Verify it
|
|
514
|
-
With two browser tabs on the same board, move a card in one
|
|
514
|
+
With two browser tabs on the same board, move a card in one; the other should update without a reload. If it doesn't, check the browser console/network for a `/cable` WebSocket connection; a missing connection means the cable client (step 4) isn't loaded.
|
|
515
515
|
:::
|
|
516
516
|
|
|
517
517
|
---
|
|
@@ -539,7 +539,7 @@ class TaskDefinition < ResourceDefinition
|
|
|
539
539
|
# ...
|
|
540
540
|
end
|
|
541
541
|
|
|
542
|
-
# Call AFTER the kanban block
|
|
542
|
+
# Call AFTER the kanban block: :kanban isn't a valid default until
|
|
543
543
|
# `kanban` has enabled the view. Reversing the order raises ArgumentError
|
|
544
544
|
# at class load.
|
|
545
545
|
default_index_view :kanban
|