plutonium 0.60.5 → 0.61.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 +19 -1
- data/.claude/skills/plutonium-app/SKILL.md +41 -0
- data/.claude/skills/plutonium-auth/SKILL.md +40 -0
- data/.claude/skills/plutonium-behavior/SKILL.md +47 -1
- data/.claude/skills/plutonium-kanban/SKILL.md +313 -0
- data/.claude/skills/plutonium-resource/SKILL.md +40 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +43 -0
- data/.claude/skills/plutonium-testing/SKILL.md +38 -0
- data/.claude/skills/plutonium-ui/SKILL.md +51 -0
- data/.claude/skills/plutonium-wizard/SKILL.md +469 -0
- data/.cliff.toml +6 -0
- data/Appraisals +3 -0
- data/CHANGELOG.md +549 -439
- data/CLAUDE.md +15 -7
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +895 -193
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +53 -53
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/layouts/basic.html.erb +7 -0
- data/app/views/plutonium/_flash_toasts.html.erb +2 -46
- data/app/views/plutonium/_toast.html.erb +52 -0
- data/app/views/resource/_resource_kanban.html.erb +1 -0
- data/db/migrate/wizard/20260615000001_create_plutonium_wizard_sessions.rb +57 -0
- data/docs/.vitepress/config.ts +24 -0
- data/docs/guides/index.md +2 -0
- data/docs/guides/kanban.md +447 -0
- data/docs/guides/wizards.md +447 -0
- data/docs/public/images/guides/kanban-after-move.png +0 -0
- data/docs/public/images/guides/kanban-board-light.png +0 -0
- data/docs/public/images/guides/kanban-board.png +0 -0
- data/docs/public/images/guides/kanban-show-centered-modal.png +0 -0
- data/docs/public/images/guides/kanban-wip-toast.png +0 -0
- data/docs/public/images/guides/wizards-chooser.png +0 -0
- data/docs/public/images/guides/wizards-completed.png +0 -0
- data/docs/public/images/guides/wizards-index-action.png +0 -0
- data/docs/public/images/guides/wizards-repeater.png +0 -0
- data/docs/public/images/guides/wizards-review.png +0 -0
- data/docs/public/images/guides/wizards-step.png +0 -0
- data/docs/reference/behavior/policies.md +1 -1
- data/docs/reference/index.md +14 -0
- data/docs/reference/kanban/authorization.md +62 -0
- data/docs/reference/kanban/dsl.md +293 -0
- data/docs/reference/kanban/index.md +40 -0
- data/docs/reference/kanban/positioning.md +162 -0
- data/docs/reference/resource/definition.md +16 -0
- data/docs/reference/ui/forms.md +36 -0
- data/docs/reference/ui/pages.md +2 -0
- data/docs/reference/wizard/anchoring-resume.md +194 -0
- data/docs/reference/wizard/dsl.md +332 -0
- data/docs/reference/wizard/index.md +33 -0
- data/docs/reference/wizard/one-time.md +129 -0
- data/docs/reference/wizard/registration-launch.md +177 -0
- data/docs/reference/wizard/storage-config.md +151 -0
- data/docs/superpowers/plans/2026-06-14-form-sectioning.md +2 -2
- data/docs/superpowers/plans/2026-06-15-wizard-dsl.md +1619 -0
- data/docs/superpowers/plans/2026-06-15-wizard-dsl.md.tasks.json +68 -0
- data/docs/superpowers/plans/2026-06-26-kanban-dsl.md +1128 -0
- data/docs/superpowers/plans/2026-06-26-kanban-dsl.md.tasks.json +24 -0
- data/docs/superpowers/specs/2026-06-15-wizard-dsl-design.md +836 -0
- data/docs/superpowers/specs/2026-06-15-wizard-dsl-examples.rb +245 -0
- data/docs/superpowers/specs/2026-06-17-wizard-relaunch-prompt-design.md +86 -0
- data/docs/superpowers/specs/2026-06-18-wizard-attachments-design.md +101 -0
- data/docs/superpowers/specs/2026-06-18-wizard-hosting-design.md +220 -0
- data/docs/superpowers/specs/2026-06-26-kanban-dsl-design.md +388 -0
- data/gemfiles/postgres.gemfile +8 -0
- data/gemfiles/postgres.gemfile.lock +321 -0
- data/gemfiles/rails_7.gemfile +1 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile +1 -0
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile +1 -0
- data/gemfiles/rails_8.1.gemfile.lock +14 -1
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +6 -1
- data/lib/plutonium/action/base.rb +9 -0
- data/lib/plutonium/auth/rodauth.rb +1 -2
- data/lib/plutonium/configuration.rb +4 -0
- data/lib/plutonium/core/controller.rb +20 -1
- data/lib/plutonium/definition/base.rb +25 -0
- data/lib/plutonium/definition/form_layout.rb +54 -35
- data/lib/plutonium/definition/index_views.rb +54 -1
- data/lib/plutonium/definition/wizards.rb +209 -0
- data/lib/plutonium/invites/concerns/invite_token.rb +9 -0
- data/lib/plutonium/invites/concerns/invite_user.rb +9 -0
- data/lib/plutonium/invites/controller.rb +4 -1
- data/lib/plutonium/kanban/action.rb +7 -0
- data/lib/plutonium/kanban/board.rb +40 -0
- data/lib/plutonium/kanban/broadcaster.rb +54 -0
- data/lib/plutonium/kanban/column.rb +69 -0
- data/lib/plutonium/kanban/context.rb +15 -0
- data/lib/plutonium/kanban/dsl.rb +71 -0
- data/lib/plutonium/kanban/grouping.rb +51 -0
- data/lib/plutonium/kanban/positioning.rb +75 -0
- data/lib/plutonium/kanban.rb +11 -0
- data/lib/plutonium/migrations.rb +40 -0
- data/lib/plutonium/positioning.rb +146 -0
- data/lib/plutonium/railtie.rb +33 -0
- data/lib/plutonium/resource/controller.rb +2 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +1 -1
- data/lib/plutonium/resource/controllers/kanban_actions.rb +455 -0
- data/lib/plutonium/resource/controllers/wizard_actions.rb +165 -0
- data/lib/plutonium/resource/policy.rb +8 -0
- data/lib/plutonium/routing/mapper_extensions.rb +44 -0
- data/lib/plutonium/routing/wizard_registration.rb +289 -0
- data/lib/plutonium/ui/display/resource.rb +17 -12
- data/lib/plutonium/ui/form/base.rb +19 -5
- data/lib/plutonium/ui/form/components/password.rb +126 -0
- data/lib/plutonium/ui/form/components/uppy.rb +6 -3
- data/lib/plutonium/ui/form/options/inferred_types.rb +20 -0
- data/lib/plutonium/ui/form/resource.rb +1 -1
- data/lib/plutonium/ui/form/wizard.rb +63 -0
- data/lib/plutonium/ui/grid/card.rb +16 -5
- data/lib/plutonium/ui/kanban/card.rb +67 -0
- data/lib/plutonium/ui/kanban/color_dot.rb +36 -0
- data/lib/plutonium/ui/kanban/column.rb +324 -0
- data/lib/plutonium/ui/kanban/resource.rb +212 -0
- data/lib/plutonium/ui/layout/resource_layout.rb +7 -1
- data/lib/plutonium/ui/modal/base.rb +30 -3
- data/lib/plutonium/ui/modal/centered.rb +5 -2
- data/lib/plutonium/ui/page/index.rb +1 -0
- data/lib/plutonium/ui/page/show.rb +23 -0
- data/lib/plutonium/ui/page/wizard.rb +371 -0
- data/lib/plutonium/ui/page/wizard_chooser.rb +97 -0
- data/lib/plutonium/ui/page/wizard_completed.rb +86 -0
- data/lib/plutonium/ui/table/base.rb +1 -1
- data/lib/plutonium/ui/table/components/view_switcher.rb +2 -1
- data/lib/plutonium/ui/wizard/review.rb +196 -0
- data/lib/plutonium/ui/wizard/stepper.rb +122 -0
- data/lib/plutonium/ui/wizard/summary_display.rb +59 -0
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium/wizard/attachment_data.rb +42 -0
- data/lib/plutonium/wizard/attachments.rb +226 -0
- data/lib/plutonium/wizard/base.rb +216 -0
- data/lib/plutonium/wizard/base_controller.rb +31 -0
- data/lib/plutonium/wizard/configuration.rb +42 -0
- data/lib/plutonium/wizard/controller.rb +162 -0
- data/lib/plutonium/wizard/data.rb +134 -0
- data/lib/plutonium/wizard/driving.rb +639 -0
- data/lib/plutonium/wizard/dsl.rb +336 -0
- data/lib/plutonium/wizard/errors.rb +27 -0
- data/lib/plutonium/wizard/field_capture.rb +157 -0
- data/lib/plutonium/wizard/field_importer.rb +208 -0
- data/lib/plutonium/wizard/gate.rb +171 -0
- data/lib/plutonium/wizard/instance_key.rb +97 -0
- data/lib/plutonium/wizard/lazy_persisted.rb +77 -0
- data/lib/plutonium/wizard/resume.rb +250 -0
- data/lib/plutonium/wizard/review_step.rb +48 -0
- data/lib/plutonium/wizard/route_resolution.rb +40 -0
- data/lib/plutonium/wizard/runner.rb +684 -0
- data/lib/plutonium/wizard/session.rb +53 -0
- data/lib/plutonium/wizard/state.rb +35 -0
- data/lib/plutonium/wizard/step.rb +61 -0
- data/lib/plutonium/wizard/step_adapter.rb +103 -0
- data/lib/plutonium/wizard/store/active_record.rb +174 -0
- data/lib/plutonium/wizard/store/base.rb +42 -0
- data/lib/plutonium/wizard/store/memory.rb +44 -0
- data/lib/plutonium/wizard/sweep_job.rb +76 -0
- data/lib/plutonium/wizard.rb +86 -0
- data/lib/plutonium.rb +5 -0
- data/lib/rodauth/features/case_insensitive_login.rb +1 -1
- data/lib/tasks/release.rake +144 -191
- data/package.json +3 -3
- data/src/css/components.css +132 -0
- data/src/js/controllers/attachment_input_controller.js +15 -1
- data/src/js/controllers/dirty_form_guard_controller.js +155 -27
- data/src/js/controllers/kanban_controller.js +330 -0
- data/src/js/controllers/password_sentinel_controller.js +39 -0
- data/src/js/controllers/register_controllers.js +6 -0
- data/src/js/controllers/remote_modal_controller.js +10 -0
- data/src/js/controllers/row_click_controller.js +14 -1
- data/src/js/controllers/wizard_controller.js +54 -0
- data/src/js/turbo/turbo_confirm.js +1 -1
- data/yarn.lock +271 -282
- metadata +100 -1
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# Kanban DSL — Design
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-06-26
|
|
4
|
+
**Status:** Design complete and approved through iterative brainstorming. Author-facing DSL and internals settled. No fenced-off explorations remain — terminal/completed column treatment is resolved as **column-scoped actions** (§3.5) plus a **`role: :done`** preset (§3.3).
|
|
5
|
+
**Scope:** A declarative kanban board for Plutonium resources, authored in the resource Definition and surfaced as a first-class **index view** (`:kanban`) alongside the existing `:table` and `:grid` views. Lives under a new `Plutonium::Kanban` namespace for its internals.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Goal & Motivation
|
|
10
|
+
|
|
11
|
+
Plutonium resources can already be viewed as a **table** or a **grid** — these are toggleable *index views* selected by the existing view switcher (`?view=` → per-resource cookie → `default_index_view`). What's missing is a **board**: the same scoped collection rendered as columns of draggable cards, where dragging a card mutates the underlying record.
|
|
12
|
+
|
|
13
|
+
A kanban is the natural **third member** of the index-view family. It is *not* a separate page, route, or generated file — like grid, you add a block to an existing resource Definition and the `:kanban` view auto-enables.
|
|
14
|
+
|
|
15
|
+
The board differs from table/grid in one fundamental way: **table and grid are passive layouts; kanban is interactive and mutates data.** That difference is the source of all the genuinely new surface (a move endpoint, positioning, policy-checked drops, optional broadcasting).
|
|
16
|
+
|
|
17
|
+
### Why mirror the wizard DSL
|
|
18
|
+
|
|
19
|
+
The `feat/wizard-dsl` branch establishes the pattern this design follows: a declarative author-facing DSL that **compiles to a first-class internal engine** rather than scattering logic across a view. The kanban applies the same shape — DSL in the definition, a real `Plutonium::Kanban::Board` config object internally, a controller concern for the endpoint, a Stimulus controller for drag-drop, an opt-in broadcaster, docs, and a skill.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. Locked Decisions
|
|
24
|
+
|
|
25
|
+
These were settled through brainstorming and are **not open for re-litigation** without explicit user sign-off:
|
|
26
|
+
|
|
27
|
+
| # | Decision | Choice |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| 1 | **Core model** | Hybrid — authored in the resource Definition, compiles to a first-class internal `Plutonium::Kanban::Board` construct. |
|
|
30
|
+
| 2 | **Integration** | Extend the existing `IndexViews` system. `:kanban` joins `KNOWN_VIEWS`; the board rides the existing view switcher, `?view=`/cookie resolution, and the index query pipeline. |
|
|
31
|
+
| 3 | **Columns** | Explicit column DSL: `column :key, label:, scope:, on_drop:`. `scope:` selects the column's cards; `on_drop:` places a dropped card. They are independent (not assumed inverses). |
|
|
32
|
+
| 4 | **Within-column order** | **Persist position, default-on, pluggable** via the `position_on` macro (§5.1). Default (Mode A): shipped decimal positioning on `:position`. Block (Mode B): delegate the write to an existing gem. `position_on false` (Mode C): disabled — scope order, cross-column moves only. |
|
|
33
|
+
| 5 | **Access** | View toggle on the index (no separate route). Reuses the existing switcher + param/cookie resolution. |
|
|
34
|
+
| 6 | **Real-time** | Opt-in via `realtime true` in the kanban block (Turbo Streams). Default is single-user. |
|
|
35
|
+
| 7 | **Card volume** | Per-column cap with a "+N more" indicator (`per_column N`). No global pagination (it produces a meaningless slice across columns). |
|
|
36
|
+
| 8 | **Card appearance** | Defaults to the **grid card** rendering (reuses `grid_fields` slots). `card_fields(**slots)` — same API as `grid_fields` — overrides for the board card only. |
|
|
37
|
+
| 9 | **Query features** | Search, filters, and scopes are **reused as-is** (they pre-narrow the collection, then columns group). Sorting is **suppressed** in kanban (position governs order). |
|
|
38
|
+
| 10 | **Storage** | **No Plutonium-owned tables.** Cards are the resource's own rows; a move is a domain-data update. Only schema requirement: one decimal `position` column on the user's model. |
|
|
39
|
+
| 11 | **Column-scoped actions** | A new general primitive (§3.5): `column … do action key, interaction:, on: end`. Acts on a set of the column's cards (`on: :all`/`:visible`) by routing their ids to the existing **`interactive_bulk_action`** (per-record auth). Resolves the "terminal column"; pairs with `role: :done`. `on: :selected` + auto-archive deferred (§8). |
|
|
40
|
+
| 12 | **Rendering** | **Lazy per-column turbo frames** (§4.2). The board paints a shell of `<turbo-frame loading="lazy">` column shells; a `kanban_column` endpoint renders each frame's cards. Moves/actions/realtime update only the affected frame(s). Board uses the **un-paginated** relation (Pagy bypassed) and orders each column by `position` (overriding `default_sort`). |
|
|
41
|
+
| 13 | **Authorization** | Existing ActionPolicy layer only (§5.3). A move authorizes via one predicate `kanban_move?` → **`update?`** (read-only board if false). **No `permitted_attributes` filtering** — `on_drop` is author code, not user params. Column actions use per-record bulk auth; quick-add uses `create?`. |
|
|
42
|
+
| 14 | **Move is an action** | The move is a **direct, non-form record action** (like `destroy`), not a bespoke endpoint — gets routing + policy predicate from the action system, invoked by the drag controller (not a rendered button). Unifies with column actions (bulk) + quick-add (create). |
|
|
43
|
+
| 15 | **Rollback ⟂ real-time** | The move POST's **own direct response** re-renders the source+dest frames authoritatively; failure re-renders the unchanged source (snap-back). Works with `realtime` off — broadcasting only mirrors success to *other* viewers. |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 3. Author-facing DSL
|
|
48
|
+
|
|
49
|
+
A kanban is purely additive to a resource Definition. Declaring the `kanban` block auto-enables the `:kanban` index view, exactly as `grid_fields` auto-enables `:grid`.
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
class TaskDefinition < Plutonium::Resource::Definition
|
|
53
|
+
# Cards reuse the GRID card appearance by default. If grid_fields is
|
|
54
|
+
# declared, kanban cards already look right — no extra card config needed.
|
|
55
|
+
grid_fields(
|
|
56
|
+
header: :name,
|
|
57
|
+
subheader: :assignee,
|
|
58
|
+
meta: [:priority, :due_on]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
kanban do
|
|
62
|
+
# --- columns: explicit, ordered; each a scope + a drop rule ---
|
|
63
|
+
column :backlog, label: "Backlog", color: :slate, role: :backlog, # preset: quick-add intake
|
|
64
|
+
scope: -> { where(status: :todo, archived: false) },
|
|
65
|
+
on_drop: ->(task) { task.status = :todo }
|
|
66
|
+
|
|
67
|
+
column :active, label: "In Progress", color: :amber, wip: 3, # per-column WIP limit lives ON the column
|
|
68
|
+
scope: -> { where(status: :doing) },
|
|
69
|
+
on_drop: ->(task) { task.status = :doing; task.started_at ||= Time.current }
|
|
70
|
+
|
|
71
|
+
column :done, label: "Done", role: :done, # preset: success styling + collapsed
|
|
72
|
+
accepts: [:active], # drop policy: only cards from :active may land here
|
|
73
|
+
scope: -> { where(status: :done) },
|
|
74
|
+
on_drop: ->(task) { task.status = :done } do
|
|
75
|
+
# column-scoped action (§3.5): runs against the set its `on:` names
|
|
76
|
+
action :archive_all, interaction: ArchiveCompletedTasks, on: :all,
|
|
77
|
+
label: "Archive all", icon: Phlex::TablerIcons::Archive,
|
|
78
|
+
confirmation: "Archive every card in Done?"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# --- within-column ordering (Decision #4); positioning is pluggable, default-on (§5.1) ---
|
|
82
|
+
# Mode A (DEFAULT): built-in decimal on :position — nothing to declare.
|
|
83
|
+
# position_on :rank # Mode A on a different attribute
|
|
84
|
+
# position_on :position do |move| # Mode B: you apply the write (BYO gem)
|
|
85
|
+
# move.record.insert_at(move.index) # move => record + column + prev + next + index
|
|
86
|
+
# end
|
|
87
|
+
# position_on false # Mode C: disabled — scope order, cross-column moves only
|
|
88
|
+
|
|
89
|
+
# --- card content (optional; defaults to grid_fields) ---
|
|
90
|
+
# Same API as grid_fields; overrides the grid slots for the board card only.
|
|
91
|
+
# Absent -> reuse grid_fields. Absent both -> a minimal default card.
|
|
92
|
+
card_fields(
|
|
93
|
+
header: :name,
|
|
94
|
+
subheader: :assignee,
|
|
95
|
+
meta: [:priority]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# --- volume (Decision #7) ---
|
|
99
|
+
per_column 25
|
|
100
|
+
|
|
101
|
+
# --- multi-user (Decision #6) ---
|
|
102
|
+
realtime true # broadcast moves via Turbo Streams
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 3.1 Concepts
|
|
108
|
+
|
|
109
|
+
| Concept | Meaning |
|
|
110
|
+
|---|---|
|
|
111
|
+
| **`column key, label:, color:, wip:, scope:, on_drop:`** | An ordered board column. `scope:` selects its cards; `on_drop:` places a dropped card. **Both accept a lambda or a symbol** (§3.2.1): a `scope:` symbol calls that named scope/class method on the resource class (applied to the already-filtered relation); an `on_drop:` symbol calls that method on the dropped record. A lambda `on_drop:` **mutates the record in memory**; the engine performs the `save!`, repositioning, and authorization. `wip:` (optional) is this column's **WIP (Work In Progress)** limit — lives on the column so there's no second list to keep in sync. Plus the **behaviour options** below (§3.3). |
|
|
112
|
+
| **column behaviour options** | `collapsed:` (start folded to a count strip, user-toggleable), `add:` (inline quick-add that seeds a new record into the column via its `on_drop`), `accepts:` (drop intake: `true`/`false`/`[source keys]`/predicate), `locked:` (cards can't be dragged out), `role:` (a preset bundling these for an archetype: `:backlog` or `:done`). All optional and individually overridable. See §3.3. |
|
|
113
|
+
| **`column … do action … end`** | A column may take a block declaring **column-scoped actions** (§3.5): `action key, interaction:, on:, label:, icon:, confirmation:`. Each acts on a set of the column's cards (`on: :all`/`:visible`) via an interaction, rendered in the column header. |
|
|
114
|
+
| **`position_on [:attr] [do \|move\| … end]`** | The single positioning macro (§5.1). **Default-on** (omit it → built-in decimal on `:position`). A symbol picks the order/read attribute; a block makes you own the write (BYO gem), receiving `move` (`record`, `column`, `prev`, `next`, `index`). **`position_on false`** disables positioning (Mode C): scope order, cross-column moves only. |
|
|
115
|
+
| **`card_fields(**slots)`** | Card body, **same API as `grid_fields`** (`image/header/subheader/body/meta/footer`). Overrides the grid slots for the board card only. Absent → reuse `grid_fields`; absent both → a minimal default card. |
|
|
116
|
+
| **`per_column n`** | Max cards rendered per column; extras collapse into a "+N more" indicator linking to a narrowed view. |
|
|
117
|
+
| **`realtime true`** | Opt-in Turbo Stream broadcasting of moves to other open boards. |
|
|
118
|
+
| **`columns do … end`** | Dynamic, **request-bound** column builder (§3.4). Evaluated per-request in the board's context (`current_user`, `current_scoped_entity`, `params`, helpers) so columns can be loaded from runtime/tenant data. Alternative to static `column` declarations. |
|
|
119
|
+
|
|
120
|
+
### 3.2 Key DSL decisions baked in
|
|
121
|
+
|
|
122
|
+
1. **Cards reuse grid rendering by default** — a kanban card *is* a grid card. Keeps the two card-based views consistent and means grid adopters get kanban cards for free.
|
|
123
|
+
2. **`on_drop` mutates in memory; the engine persists** — the lambda only sets attributes; the engine wraps `save!`, recomputes `position`, and enforces the policy so a drop can never bypass `update?` or permitted attributes.
|
|
124
|
+
3. **`scope:` selects, `on_drop:` places** — independent by design; the engine never assumes the drop rule is the inverse of the scope.
|
|
125
|
+
4. **No generator, no route file** — authoring is additive to a definition; it rides the existing index route plus a new `kanban_move` member action (§5).
|
|
126
|
+
|
|
127
|
+
### 3.2.1 `scope:` and `on_drop:` accept a lambda **or** a symbol
|
|
128
|
+
|
|
129
|
+
Both refer to behaviour that often already lives on the model — so a symbol that names it avoids re-wrapping it in a lambda:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
column :active, label: "In Progress",
|
|
133
|
+
scope: :in_progress, # => resource_class.in_progress (a named AR scope / class method)
|
|
134
|
+
on_drop: :start! # => record.start! (an instance method on the model)
|
|
135
|
+
|
|
136
|
+
column :backlog, label: "Backlog",
|
|
137
|
+
scope: -> { where(status: :todo) }, # lambda still works
|
|
138
|
+
on_drop: ->(t) { t.status = :todo } # for inline logic
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Resolution:
|
|
142
|
+
|
|
143
|
+
| Form | `scope:` | `on_drop:` |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| **Symbol** | `relation.public_send(:sym)` — a named scope / class method on the resource class, applied to the already-filtered relation. | `record.public_send(:sym)` — an instance method on the model. The method mutates (and may persist); the engine still wraps `save!`, so a non-persisting method works too. |
|
|
146
|
+
| **Lambda** | evaluated against the **relation** (Rails scope semantics; `current_user` is *not* in scope here — tenant/user filtering is the policy's job — §6.1). | runs in `ConditionContext(view_context, record)`: mutates the record in memory and *may* use `current_user`; engine persists. |
|
|
147
|
+
|
|
148
|
+
This keeps simple boards declarative (`scope: :active, on_drop: :start!`) while leaving lambdas for inline/contextual logic. Both forms run through the same policy + positioning + (optional) broadcast path.
|
|
149
|
+
|
|
150
|
+
### 3.3 Column behaviours & archetypes
|
|
151
|
+
|
|
152
|
+
Columns are not all interchangeable: an intake column and a terminal column behave differently on screen. Three behaviours are in scope for v1, expressed as overridable options on `column`:
|
|
153
|
+
|
|
154
|
+
| Option | Behaviour |
|
|
155
|
+
|---|---|
|
|
156
|
+
| **`collapsed: true`** | Column starts folded to a thin count strip; the user can expand it. Common for long backlogs and finished columns. Collapsibility is always available; this only sets the initial state. |
|
|
157
|
+
| **`add: true`** | Renders an inline "+ Add" affordance. Creating a card here seeds a fresh record *into this column* by applying the column's `on_drop` to it, then hands off to the resource's create path. Natural for a backlog/intake column. |
|
|
158
|
+
| **`accepts:` / `locked:`** | Drop policy. `accepts:` controls intake — `true` (default), `false` (rejects drops), `[:keys]` (only cards from these source columns), or `->(card) { … }` (predicate). `locked: true` prevents cards being dragged back *out* (a drop-only sink). |
|
|
159
|
+
|
|
160
|
+
**Archetypes (`role:`)** are thin presets that bundle these for a recognizable column type. Every preset value remains individually overridable on the same `column`. v1 ships two:
|
|
161
|
+
|
|
162
|
+
- **`role: :backlog`** ⇒ `{ add: true }` — an intake column with quick-add.
|
|
163
|
+
- **`role: :done`** ⇒ `{ color: :green, collapsed: true }` — terminal styling, starts folded. The "completed" feel comes from pairing it with a column-scoped action (§3.5, e.g. "Archive all") and/or an `on_drop` that stamps/finishes; those stay explicit rather than baked into the role, because the archive/finish semantics belong to the app's model.
|
|
164
|
+
|
|
165
|
+
### 3.4 Static vs dynamic (tenant-driven) columns
|
|
166
|
+
|
|
167
|
+
Boards come in two shapes:
|
|
168
|
+
|
|
169
|
+
- **Static** — a fixed set of `column` declarations (the §3 example). The columns are known at definition time.
|
|
170
|
+
- **Dynamic** — columns built per-request from runtime data via a `columns do … end` block, evaluated in the board's request context (§6.1). This covers tenant-defined stages, user-customizable boards, etc.:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
kanban do
|
|
174
|
+
columns do
|
|
175
|
+
current_scoped_entity.stages.order(:position).map do |stage|
|
|
176
|
+
column stage.slug, label: stage.name, wip: stage.wip_limit,
|
|
177
|
+
scope: -> { where(stage_id: stage.id) },
|
|
178
|
+
on_drop: ->(t) { t.stage_id = stage.id }
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`current_scoped_entity` is the **ambient tenant**, always available via the portal's entity strategy. Tenant-driven columns are plain tenancy and stay in the locked design.
|
|
185
|
+
|
|
186
|
+
### 3.5 Column-scoped actions
|
|
187
|
+
|
|
188
|
+
A column-scoped action operates on a **set of the column's cards at once** — the new primitive that makes "Archive all in Done", "Assign all backlog to me", or "Export this column" possible. It is *general* (any column, not just terminal), backed by the existing **interaction** system, and declared **inside the column's block** so it lives next to the column it belongs to:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
column :done, label: "Done", role: :done,
|
|
192
|
+
scope: -> { where(status: :done) },
|
|
193
|
+
on_drop: ->(t) { t.status = :done } do
|
|
194
|
+
|
|
195
|
+
action :archive_all,
|
|
196
|
+
interaction: ArchiveCompletedTasks, # an existing Plutonium interaction
|
|
197
|
+
on: :all, # which cards it targets (below)
|
|
198
|
+
label: "Archive all",
|
|
199
|
+
icon: Phlex::TablerIcons::Archive,
|
|
200
|
+
confirmation: "Archive every card in Done?"
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**`on:` — the target set (the action specifies it, not a global rule):**
|
|
205
|
+
|
|
206
|
+
| `on:` | Cards passed to the interaction |
|
|
207
|
+
|---|---|
|
|
208
|
+
| `:all` (default) | Every record matching the column's `scope` within the **current** filters/search/scope — i.e. the whole column, including overflow beyond `per_column`. The "clear/archive all" semantics. |
|
|
209
|
+
| `:visible` | Only the cards currently rendered (respecting the `per_column` cap). Smaller blast radius. |
|
|
210
|
+
| `:selected` | The user-selected subset — **requires card-selection UI; deferred to a follow-up** (noted in §8), not built in v1. |
|
|
211
|
+
|
|
212
|
+
**Mechanics:**
|
|
213
|
+
- Rendered in the **column header** (button / overflow menu), gated by the action's policy like any other action.
|
|
214
|
+
- On invoke: the engine resolves the target set's **ids** (`on: :all` → the column `scope` ∩ current query; `on: :visible` → the rendered, `per_column`-capped subset) and routes them to the **existing `interactive_bulk_action`** (`GET /…/bulk_actions/:action?ids[]=…`), which **authorizes each record** (`authorize_interactive_bulk_action!`) before running the interaction.
|
|
215
|
+
- On success, refreshes only the affected **column frame** (§4.2) via Turbo Stream (and broadcasts if `realtime`). No parallel action stack — column actions *are* bulk actions with a column-derived id set.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 4. Index-view integration (reuses existing machinery)
|
|
220
|
+
|
|
221
|
+
The board plugs into code that already exists:
|
|
222
|
+
|
|
223
|
+
| Existing file | Change |
|
|
224
|
+
|---|---|
|
|
225
|
+
| `lib/plutonium/definition/index_views.rb` | Add `:kanban` to `KNOWN_VIEWS`; declaring `kanban do…end` appends `:kanban` to `defined_index_views` (mirrors `grid_fields`). New class-attributes hold the compiled board config. |
|
|
226
|
+
| `lib/plutonium/ui/page/index.rb` | Add `when :kanban then render partial("resource_kanban")` to `render_default_content`. `selected_view` already resolves `:kanban` via `?view=`/cookie/default. |
|
|
227
|
+
| `lib/plutonium/ui/table/components/view_switcher.rb` | Add a `kanban` segment (icon + label) to `SEGMENT_LABELS`. (The switcher already renders unknown keys via its fallback, so this is polish.) |
|
|
228
|
+
| `lib/plutonium/ui/grid/components/card.rb` | Reused for card rendering (`Card.new(record, resource_definition:, resource_fields:)`) so kanban cards match grid cards; `card_fields` supplies alternate slots. |
|
|
229
|
+
|
|
230
|
+
Everything else — the toggle UI, param/cookie stickiness, and **search / filters / scopes** feeding the collection — comes for free from the index pipeline.
|
|
231
|
+
|
|
232
|
+
### 4.1 Query-feature composability
|
|
233
|
+
|
|
234
|
+
| Feature | On kanban | Rationale |
|
|
235
|
+
|---|---|---|
|
|
236
|
+
| **Search** | Reused as-is | Narrows the card set across all columns before grouping. |
|
|
237
|
+
| **Filters** | Reused as-is | Same — pre-narrows, then columns group. |
|
|
238
|
+
| **Scopes** | Reused, with guidance | Pre-filter then group. Compose cleanly **when orthogonal to the column dimension**; a scope on the same attribute the columns group by is a documented authoring smell, not a blocked case. |
|
|
239
|
+
| **Sorting** | Suppressed **and `default_sort` overridden** | Within-column order *is* `position`. The board orders each column by `position`, overriding any `default_sort_config` on the query object; the table's column-header sort UI is also absent. |
|
|
240
|
+
| **Pagination** | **Bypassed**, replaced by `per_column` | The table view paginates via **Pagy** (`pagy_instance`). The board must consume the **un-paginated, query-applied** relation and cap *per column* (`per_column` + "+N more") — global offset yields a meaningless slice across columns. |
|
|
241
|
+
|
|
242
|
+
### 4.2 Per-column turbo frames (light rendering)
|
|
243
|
+
|
|
244
|
+
To keep the board cheap, **each column is its own `<turbo-frame>`**, lazy-loaded:
|
|
245
|
+
|
|
246
|
+
- The index/kanban render returns the **board shell** — column headers as `<turbo-frame id="kanban-col-<key>" loading="lazy" src="…?view=kanban&column=<key>">`. Cards are *not* in the first paint; each frame fetches **its own** cards in parallel (default **lazy**; eager is a fallback for tiny boards).
|
|
247
|
+
- A new lightweight controller action (`kanban_column`) renders one column's cards (the column `scope` ∩ current query, ordered by `position`, capped at `per_column`). This is the frame `src` and the unit of every update.
|
|
248
|
+
- **Scoped updates:** a move replaces only the **source + destination** frames (§5 step 8); a column action, "+N more" expansion, and `realtime` broadcasts each target a single frame. The whole board never re-renders.
|
|
249
|
+
- **Drag spans frames:** turbo frames are render/update units, not drag boundaries — the Stimulus controller drags across the board; on drop the server returns frame-scoped streams.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 5. The move (a direct, non-form action)
|
|
254
|
+
|
|
255
|
+
A move is **modeled as a Plutonium action**, not a bespoke endpoint — a *direct* (non-interactive, no form) record action, the same shape as the built-in `destroy` (`action :destroy, route_options: {method: :delete}, record_action: true`). Registering it as an action gives it **routing** and a **policy predicate** for free and puts it in the same family as column actions (bulk) and quick-add (create) — one mental model, no parallel stack.
|
|
256
|
+
|
|
257
|
+
- **Not a rendered button.** Unlike `destroy`, the move action is invoked by the drag controller, so it's excluded from rendered toolbars/menus — it exists for its route, policy, and handler.
|
|
258
|
+
- **Direct, not interactive.** It carries drag params, so it skips the interactive form/commit cycle; `Plutonium::Resource::Controllers::KanbanActions` implements the handler.
|
|
259
|
+
|
|
260
|
+
**Request payload:** `{ from_column:, to_column:, to_index: }` POSTed to the move action's member route for the card. `from_column` is carried so the engine can enforce the destination's `accepts:`/`locked:` policy (§3.3).
|
|
261
|
+
|
|
262
|
+
**Server flow (single transaction):**
|
|
263
|
+
1. Authorize the move action — predicate **`kanban_move?`**, which **defaults to `update?`** (§5.3). If denied the board is read-only (drag never wires up); a forged request is rejected.
|
|
264
|
+
2. Validate the destination's `accepts:`/`locked:` drop policy against `from_column`; reject if disallowed.
|
|
265
|
+
3. Look up the target column; apply its `on_drop` (lambda → in-memory mutation; symbol → `record.public_send(:sym)`) against the record (§3.2.1). **No attribute filtering** — `on_drop` is author code, not user params (§5.3).
|
|
266
|
+
4. Compute the **fractional** `position` between the neighbors at `to_index` in the destination column (average; ±1 at ends — §5.1).
|
|
267
|
+
5. `save!`.
|
|
268
|
+
6. (Optional) enforce the destination column's `wip` — reject if `column.scope.count` is already at the limit.
|
|
269
|
+
7. Respond with **frame-scoped** Turbo Streams re-rendering the **source + destination** column frames (§4.2) to their **authoritative** server state — never the whole board.
|
|
270
|
+
|
|
271
|
+
**Success, failure, and why rollback needs no real-time:** the move POST gets a **direct response** (it's the mover's own request). On success the frame re-render reflects the move; on failure (steps 1/2/6) the server re-renders the **unchanged** source column, snapping the dragged card back — an effective rollback driven entirely by the move's own response. **Real-time plays no part here**: broadcasting (§6) only *mirrors* a successful move to *other* viewers' boards; the mover's own correction always comes from its direct response. So rollback works identically whether `realtime` is on or off.
|
|
272
|
+
|
|
273
|
+
### 5.3 Authorization (minimal — a move is just an update)
|
|
274
|
+
|
|
275
|
+
A move writes through the author's own `on_drop`; there are **no user-supplied attribute params** to sanitize, so there is **no `permitted_attributes` filtering** for moves. That mechanism guards mass-assignment from *form params* — a move has none, so filtering would be pointless ceremony. The author's lambda *is* the spec of what gets written.
|
|
276
|
+
|
|
277
|
+
The only check is the yes/no "may this user move cards," answered by the move **action's policy predicate**, a one-line delegating default (exactly like `edit?` delegates to `update?` today):
|
|
278
|
+
|
|
279
|
+
| Operation | Authorization | Default |
|
|
280
|
+
|---|---|---|
|
|
281
|
+
| **See a card** | `relation_scope` (`authorized_scope`) | — (board only groups visible records) |
|
|
282
|
+
| **Move / reorder** | the move action's predicate `kanban_move?` | **`update?`** → read-only board if false |
|
|
283
|
+
| **Column action** | the action's policy method (e.g. `archive_completed?`), **per-record** via `interactive_bulk_action` | the interaction's policy |
|
|
284
|
+
| **Quick-add** (`add:`) | `create?` | — |
|
|
285
|
+
|
|
286
|
+
Want a board draggable by viewers who can't open the edit form? Override `kanban_move?`. Otherwise "can move" = "can update," and **nothing constrains which attributes `on_drop` writes** — that's the author's code, by design.
|
|
287
|
+
|
|
288
|
+
### 5.1 Positioning (pluggable, default-on)
|
|
289
|
+
|
|
290
|
+
Positioning is **on by default**, managed by a shipped, kanban-independent module. One macro, `position_on`, expresses all three modes:
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
# (omit position_on) -> Mode A: built-in decimal on :position (DEFAULT)
|
|
294
|
+
position_on :rank -> Mode A on a different attribute
|
|
295
|
+
position_on :position do |move| -> Mode B: order by :position; YOUR block does the write
|
|
296
|
+
move.record.insert_at(move.index) # (delegate to acts_as_list / positioning / ranked-model)
|
|
297
|
+
end
|
|
298
|
+
position_on false -> Mode C: disabled — scope order, cross-column moves only
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
- **`Plutonium::Positioning` (shipped module).** A general-purpose model concern doing decimal/fractional ordering — kanban-independent, usable anywhere. It is the board's default strategy (Mode A).
|
|
302
|
+
- **Decimal mechanics.** A single decimal attribute (conventionally `:position`). Each column renders `column.scope.order(:position)`, so ordering is always *within* a column — no separate "which column am I in" field. On drop at index *i*, `position = (prev + next) / 2` (or `first − 1` / `last + 1` at the ends): O(1), touches only the moved row. When a neighbor gap erodes below a threshold, the engine renumbers that one column in a single pass (rare, column-local).
|
|
303
|
+
- **Read vs write.** The symbol names the attribute the board **orders by** when rendering. In Mode A the module also performs the write; in Mode B your block performs the write while the board still orders by that attribute.
|
|
304
|
+
- **Block context (`move`).** Carries `record` plus the drop context that's useful: `column` (destination key), `prev`/`next` (neighbor records, `nil` at ends), `index` (target slot). The bundle is extensible.
|
|
305
|
+
- **Graceful default.** Mode A assumes a decimal `:position` column. If the model has none, the board **degrades to Mode C** (cross-column moves still work) and emits a dev-mode warning rather than raising — so any resource renders as a board out of the box. *(Flagged for sign-off: alternative is to hard-require the column.)*
|
|
306
|
+
- **Seeding & backfill.** Existing rows with `NULL` position have undefined order, so `Plutonium::Positioning` (a) provides a one-shot backfill (number existing rows by a chosen order, e.g. `created_at`) and (b) sets a position on **create** (append to the end of the row's column) — including quick-added cards. Without a position value, a column falls back to its `scope`'s natural order for the un-positioned rows.
|
|
307
|
+
- **No owned table** — the attribute lives on the user's model; see §5.2.
|
|
308
|
+
|
|
309
|
+
### 5.2 No Plutonium-owned tables
|
|
310
|
+
|
|
311
|
+
Unlike the wizard (which ships `plutonium_wizard_sessions` for transient, cross-request, non-domain state), a kanban has **no transient state**: the cards are the resource's own rows and a move is a plain update to domain data. Therefore kanban introduces **zero owned tables**. The only schema requirement is the decimal `position` column on the user's model (plus the grouping attribute they already have). A migration helper may be provided to add the column, but it lives on the user's table.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## 6. Internal architecture (`Plutonium::Kanban` namespace)
|
|
316
|
+
|
|
317
|
+
Following the wizard pattern — DSL compiles to a real object:
|
|
318
|
+
|
|
319
|
+
| Unit | Responsibility |
|
|
320
|
+
|---|---|
|
|
321
|
+
| `Plutonium::Kanban::DSL` | The `kanban do…end` builder; collects columns, ordering, card config, limits, flags. |
|
|
322
|
+
| `Plutonium::Kanban::Board` | Compiled config object: ordered columns, card renderer, positioning attr, flags. The first-class construct the engine and components consume. |
|
|
323
|
+
| `Plutonium::Kanban::Column` | One column: key, label, color, behaviour options (§3.3), scope lambda, on_drop lambda, wip limit, and any column-scoped `action`s (§3.5). |
|
|
324
|
+
| `Plutonium::Kanban::Action` | A compiled column-scoped action: key, interaction, `on:` target (`:all`/`:visible`), label/icon/confirmation. Resolves its target set from the column relation ∩ current query and hands it to the interaction. |
|
|
325
|
+
| `Plutonium::Kanban::Context` | The per-request context for the **builder** (`columns do…end`) and **`on_drop`** blocks — a `ConditionContext`-style `SimpleDelegator` over `view_context` exposing `current_user`, `current_scoped_entity`, `params`, helpers (`on_drop` also gets the dragged `record`). **`scope:` lambdas are different** — they evaluate against the **relation** (Rails semantics; tenant/user scoping comes from the policy, not the lambda). See §6.1. |
|
|
326
|
+
| `Plutonium::Positioning` | **Shipped, kanban-independent** model concern: decimal/fractional ordering (average neighbors; ends ±1; column-local rebalance on precision exhaustion). The default positioning strategy. |
|
|
327
|
+
| `Plutonium::Kanban::Positioning` | Strategy resolution behind the `position_on` macro: Mode A (delegate to `Plutonium::Positioning`), Mode B (invoke the author's block with the `move` context), Mode C / degrade (disabled). |
|
|
328
|
+
| `Plutonium::UI::Kanban::Resource` | Phlex board **shell** (lazy column turbo-frames — §4.2), parallel to `Grid::Resource` / `Table::Resource`. |
|
|
329
|
+
| `Plutonium::UI::Kanban::Column` | Phlex render of one column's cards (frame body): cards, "+N more", column-action header, WIP badge. |
|
|
330
|
+
| `Plutonium::Resource::Controllers::KanbanActions` | Controller concern: the **move** action handler (a direct, non-form action — §5), the lightweight `kanban_column` frame endpoint (§4.2), and column actions (reusing **`interactive_bulk_action`** with the column's card `ids[]` — §3.5). |
|
|
331
|
+
| Stimulus `kanban_controller.js` | Cross-frame drag-drop; POSTs the move; **the response re-renders source+dest frames** (success = moved, failure = unchanged source = snap-back). Rollback comes from the move's own response — no dependence on `realtime` (§5). |
|
|
332
|
+
| `Plutonium::Kanban::Broadcaster` (opt-in) | `realtime true` → **mirror** a successful move's frame updates to *other* viewers via Turbo Streams, **scoped to tenant + board** (stream name includes `current_scoped_entity` + resource) so updates never leak across tenants. Not involved in the mover's own rollback. |
|
|
333
|
+
| Policy hook (in `Plutonium::Resource::Policy`) | `kanban_move?` → `update?` — a single one-line delegating default (like `edit?` → `update?`). No permitted-attributes hook (§5.3). |
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## 6.1 Request binding & evaluation context
|
|
338
|
+
|
|
339
|
+
The compiled `Board` is **static config**; it becomes useful only when **bound to a request**. The controller supplies the **already authorized + tenant-scoped** base relation (from `authorized_scope` + the policy's `relation_scope`) and a `Plutonium::Kanban::Context`. **Different blocks bind differently** — this is the precise rule (and a correction to an earlier over-claim that *all* blocks see `current_user`):
|
|
340
|
+
|
|
341
|
+
| Block | `self` / evaluation | Reaches `current_user` / `current_scoped_entity`? |
|
|
342
|
+
|---|---|---|
|
|
343
|
+
| **`scope:` lambda** | the **relation** (Rails scope semantics — `where(...)` works), exactly like the definition's `define_scope` bodies | **No.** Tenant/user scoping is the **policy's** job; the base relation is already policy-scoped. Refine it here, don't re-filter by user. |
|
|
344
|
+
| **`on_drop:` lambda** | `ConditionContext(view_context, record)` — gets the dragged `record` and delegates to `view_context` | **Yes** (e.g. `t.completed_by = current_user`). |
|
|
345
|
+
| **`columns do … end`** builder | the `Kanban::Context` over `view_context` | **Yes** — that's how tenant-driven columns load (`current_scoped_entity.stages`). |
|
|
346
|
+
|
|
347
|
+
- **Ordering of concerns:** authorization/tenancy filter the collection *first* (controller pipeline) → the Board *groups* the safe relation into columns → search/filters/scopes (§4.1) have already narrowed it.
|
|
348
|
+
- **The move action shares the binding** — `on_drop` and positioning run request-bound and policy-checked (§5), so a tenant-driven board mutates correctly and safely.
|
|
349
|
+
|
|
350
|
+
## 7. Testing strategy
|
|
351
|
+
|
|
352
|
+
Mirror the wizard branch's depth:
|
|
353
|
+
|
|
354
|
+
- **Unit** — DSL compilation (`Board`/`Column`/`Action` shape), positioning math (average-of-neighbors, ends, rebalance, on-create seed, backfill), query-feature composition (scope+filter+search feeding columns; `default_sort` overridden).
|
|
355
|
+
- **Policy** — `kanban_move?` false ⇒ read-only board (no drag handles, no quick-add); `kanban_move?` defaults to `update?`; a column action over `on: :all` only touches records the per-record bulk auth permits.
|
|
356
|
+
- **Integration** — lazy column frames load via `kanban_column`; move across columns updates only source+dest frames; `accepts:`/`locked:` rejection rolls back; reorder within a column; "+N more" overflow; `wip` rejection; a column action routing ids to `interactive_bulk_action`.
|
|
357
|
+
- **Realtime** — broadcast fires only when `realtime true`, scoped to tenant+board.
|
|
358
|
+
- Dummy-app fixtures: a `Task`-style resource with `status` + decimal `position`.
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## 8. Out of scope (v1)
|
|
363
|
+
|
|
364
|
+
- Swimlanes (horizontal grouping).
|
|
365
|
+
- Per-column lazy loading / infinite scroll (using per-column cap instead).
|
|
366
|
+
- Real-time presence/cursors.
|
|
367
|
+
- A scaffold flag on `pu:res:scaffold` (authoring is additive to an existing definition; revisit later).
|
|
368
|
+
- **Scheduled auto-archive** for terminal columns (TTL sweep job) — deferred until there's concrete demand (§10).
|
|
369
|
+
- **Card selection UI** (`on: :selected` column actions, §3.5) — deferred; v1 ships `on: :all` / `on: :visible`.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## 9. Docs & skill
|
|
374
|
+
|
|
375
|
+
- A `docs/guides/kanban.md` guide + `docs/reference/kanban/*` reference (mirroring the wizard docs).
|
|
376
|
+
- A `.claude/skills/plutonium-kanban/SKILL.md` and a router entry in the `plutonium` skill (mirroring `plutonium-wizard`).
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## 10. Terminal / completed column treatment (RESOLVED)
|
|
381
|
+
|
|
382
|
+
Formerly an open exploration; now settled and folded into the locked design. The "completed" feel is composed from existing + new locked pieces, not a special column type:
|
|
383
|
+
|
|
384
|
+
- **Styling + fold:** `role: :done` ⇒ `{ color: :green, collapsed: true }` (§3.3).
|
|
385
|
+
- **Stamp / archive on entry:** the column's `on_drop` (e.g. `t.completed_at = Time.current`, or the app's own archive method) — already expressible (§3.2.1).
|
|
386
|
+
- **"Clear / archive all":** a **column-scoped action** (§3.5) with `on: :all`, backed by an app interaction.
|
|
387
|
+
|
|
388
|
+
Deliberately **not** built: scheduled auto-archive (a TTL sweep job) and `on: :selected` card-selection — deferred per §8, to be revisited on demand.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Minimal bundle for the "Positioning on PostgreSQL" CI check
|
|
2
|
+
# (test/positioning_postgres_check.rb). Kept separate from the main Gemfile and
|
|
3
|
+
# the appraisal gemfiles so `pg`'s native build is never forced on contributors
|
|
4
|
+
# who only run the SQLite suite.
|
|
5
|
+
source "https://rubygems.org"
|
|
6
|
+
|
|
7
|
+
gem "rails", "~> 8.1.0"
|
|
8
|
+
gem "pg", "~> 1.5"
|