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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8f8adf3a1153ed95a357bf90ab145279c7f7b72540c6f160c8f52f859ed14e4e
|
|
4
|
+
data.tar.gz: 96f58112aff1ffd9a21311c169169c31efe61c16470c65f1805b9860e987baca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3cec89b62845ecb9177e21bb7307c18f182056ce01164f657b07b2dd5e165c041aa0172b7f22b967061170b3a39398834b2966c8d62ba333e279ce0ff8ee0f8d
|
|
7
|
+
data.tar.gz: d8369fec9bd3e3ce5951248e03ee065c6dd9e31fa05bccd9ab7bc49abb69a0586a1b94adbcf5c0bec11b0dc8269927daa8ca41ad62ef7d6f20d045cc02004ca8
|
|
@@ -18,8 +18,22 @@ Entry point for all Plutonium work. Does three things:
|
|
|
18
18
|
- **For targeted edits** — use the **router table** to jump to the right skill.
|
|
19
19
|
- **For anything touching tenant scoping** — load `plutonium-tenancy`. Don't reach for `where(organization: ...)` in a policy; fix the model instead.
|
|
20
20
|
- **Unattended execution:** always pass `--dest=`, `--force` (when re-running meta-generators), `--auth=`, `--skip-bundle`, `--quiet` so generators don't block on prompts. See [Unattended execution](#unattended-execution).
|
|
21
|
+
- **Inspect before you act.** Every targeted skill now opens with a CHECK gate — read the relevant files yourself before scaffolding or editing. Don't ask the user to describe their app when you can read it.
|
|
21
22
|
|
|
22
|
-
##
|
|
23
|
+
## ✅ Orient before you route (CHECK — read the app, don't assume)
|
|
24
|
+
|
|
25
|
+
A one-line request rarely says whether this is a new app, a half-built one, or a multi-tenant one — and those change which path you take. Spend 30 seconds reading the app **before** loading a bundle or running anything:
|
|
26
|
+
|
|
27
|
+
| Read | Tells you |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `git log --oneline \| head`; is there a populated `Gemfile` + `app/`? | Greenfield vs existing → install path (`plutonium-app`: `base.rb`, **never** `plutonium.rb` on an existing app) |
|
|
30
|
+
| grep `Gemfile` for `plutonium`; `ls config/packages.rb` | Already installed? → skip install |
|
|
31
|
+
| `ls packages/` | What portals / feature packages already exist |
|
|
32
|
+
| Does any model `belongs_to` an org/team/tenant? | Multi-tenant → load `plutonium-tenancy` **before** declaring scoping |
|
|
33
|
+
|
|
34
|
+
This is the global "look before you leap"; each targeted skill carries its own ASK/CHECK gate for the specifics. **Never run an installer or scaffold from a one-line request without first reading what's already there.**
|
|
35
|
+
|
|
36
|
+
## The skills
|
|
23
37
|
|
|
24
38
|
| Skill | Covers |
|
|
25
39
|
|---|---|
|
|
@@ -27,9 +41,11 @@ Entry point for all Plutonium work. Does three things:
|
|
|
27
41
|
| **[[plutonium-resource]]** | The resource itself — `pu:res:scaffold`, field types, model layer (`Plutonium::Resource::Record`, `has_cents`, SGID, routing), definition layer (fields/inputs/displays/columns, search/filters/scopes/sorting, custom actions, bulk actions, index views, page customization) |
|
|
28
42
|
| **[[plutonium-behavior]]** | Controllers (hooks, key methods, presentation), policies (action methods, `permitted_attributes_for_*`, `permitted_associations`), interactions (structure, outcomes, chaining, URL generation) |
|
|
29
43
|
| **[[plutonium-ui]]** | Page classes, forms, displays, tables, custom Phlex components, layouts, modals & tabs, Tailwind config, Stimulus, design tokens, `.pu-*` classes, Phlexi themes |
|
|
44
|
+
| **[[plutonium-kanban]]** | `kanban do…end` DSL in a Definition — columns, `card_fields`, `position_on`, `realtime`, column actions, `kanban_move?` policy, quick-add, static vs dynamic boards |
|
|
30
45
|
| **[[plutonium-auth]]** | Rodauth install, account types (basic / admin / SaaS), profile resource, security section |
|
|
31
46
|
| **[[plutonium-tenancy]]** | Entity scoping (`associated_with`, `default_relation_scope`, three model shapes), nested resources, invites |
|
|
32
47
|
| **[[plutonium-testing]]** | `pu:test:install`, `pu:test:scaffold`, `ResourceCrud`/`ResourcePolicy`/`ResourceDefinition`/`ResourceModel`/`NestedResource`/`PortalAccess`/`ResourceInteraction`, `AuthHelpers` |
|
|
48
|
+
| **[[plutonium-wizard]]** | Multi-step flows — the wizard DSL (`step`/`review`/`using:`/`condition:`, per-step `on_submit`/`persist`/`on_rollback`, `execute`), anchoring & resume, one-time wizards + gate, registration (`wizard` macro + `register_wizard`), storage/config + SweepJob |
|
|
33
49
|
|
|
34
50
|
## Greenfield bootstrap bundle
|
|
35
51
|
|
|
@@ -61,10 +77,12 @@ Add when relevant:
|
|
|
61
77
|
| Scope a model to a tenant, write `associated_with`, set portal entity strategy | **[[plutonium-tenancy]]** |
|
|
62
78
|
| Configure parent/child nested routes, custom parent resolution | **[[plutonium-tenancy]]** |
|
|
63
79
|
| Set up user invitations or entity membership | **[[plutonium-tenancy]]** |
|
|
80
|
+
| Build or customize a kanban board view — `kanban do…end`, columns, `card_fields`, `position_on`, `realtime`, column actions, `kanban_move?` policy | **[[plutonium-kanban]]** |
|
|
64
81
|
| Build a custom page (override `ShowPage`/`IndexPage`/`NewPage`/`EditPage`), custom form, custom display, custom table, custom Phlex component | **[[plutonium-ui]]** |
|
|
65
82
|
| Configure Tailwind, register Stimulus controllers, edit design tokens, theme forms/displays/tables, write a custom layout | **[[plutonium-ui]]** |
|
|
66
83
|
| Install Rodauth, set up accounts, configure login flow, add the profile resource | **[[plutonium-auth]]** |
|
|
67
84
|
| Write tests for a resource, run `pu:test:scaffold`, include `Plutonium::Testing::*` concerns | **[[plutonium-testing]]** |
|
|
85
|
+
| Build a multi-step flow — onboarding, checkout, branching create — register a `wizard` / `register_wizard`, gate a one-time wizard | **[[plutonium-wizard]]** |
|
|
68
86
|
|
|
69
87
|
## Resource architecture at a glance
|
|
70
88
|
|
|
@@ -21,6 +21,47 @@ For the resources themselves (model + definition + scaffold options), see [[plut
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
+
## 🛑 Before you install or scaffold structure: confirm the shape (ASK — don't infer)
|
|
25
|
+
|
|
26
|
+
"Set up Plutonium" / "make an admin area" / "create a billing package" each hide a high-blast-radius decision. Get one wrong and you **clobber a year of git history**, build the wrong kind of package, or ship a portal nobody can log into. Resolve each — confirming by inspection (next section), not assumption:
|
|
27
|
+
|
|
28
|
+
1. **Fresh app or existing one?** Existing ⇒ `bundle add plutonium` + `pu:core:install` (the `base.rb` path). **NEVER the `plutonium.rb` fresh-app template on an existing app** — it re-bootstraps (dotenv/annotate/solid_*/assets) and drops "initial commit" commits that clobber history. This is the single most dangerous mistake in this skill — confirm it's greenfield *before* reaching for `plutonium.rb`.
|
|
29
|
+
2. **Feature package or portal package?** Business logic (models/policies/definitions/interactions) ⇒ `pu:pkg:package` (feature, no UI). A web surface (controllers/views/routes/auth) ⇒ `pu:pkg:portal`. Hard split — "billing" is a *feature*; "admin area" is a *portal*. A feature package is invisible until its resources are `pu:res:conn`'d into a portal.
|
|
30
|
+
3. **Auth per portal.** `--auth=<account>` / `--public` / `--byo` / `--scope=<Entity>` (multi-tenant). Unguessable from "admin area" — decide, don't default silently.
|
|
31
|
+
4. **Don't stop half-wired.** A resource reaches the browser only after: scaffold → migrate → `pu:res:conn --dest=portal` → portal engine `mount`ed in `config/routes.rb` → registered (conn does the last). Name the whole chain before you start.
|
|
32
|
+
|
|
33
|
+
**Never ship a guessed schema, portal name, or auth flag as applied commands** — read them off the app first; fall back to `AskUserQuestion` only for genuine product choices (separate staff accounts vs shared, which payment backend). The decisions compound: *existing app ⇒ base.rb path*; *feature package ⇒ needs a portal to be visible*; *new portal ⇒ pick auth + mount it*.
|
|
34
|
+
|
|
35
|
+
## ✅ Before you run a generator: verify the ground truth (CHECK — read it, don't ask for it)
|
|
36
|
+
|
|
37
|
+
You have file access — **inspect**; don't ask the user to describe their app.
|
|
38
|
+
|
|
39
|
+
| Check | How | Why it matters |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| Greenfield vs existing | `git log --oneline \| head`; is there a populated `Gemfile`/`app/`? | An existing app must use the `base.rb` path — **never** `plutonium.rb` |
|
|
42
|
+
| Plutonium already installed | grep `Gemfile` for `plutonium`; `ls config/packages.rb app/controllers/resource_controller.rb` | Avoid re-installing / double bootstrap |
|
|
43
|
+
| Package/portal already exists | `ls packages/<name>` | Don't duplicate — connect to / extend the existing one |
|
|
44
|
+
| Existing auth | grep `Gemfile`/`app/models` for `rodauth`/`devise`/`has_secure_password` | Drives `--auth` vs `--byo` |
|
|
45
|
+
| Portal engine mounted | grep `config/routes.rb` for `mount <Portal>::Engine` | An unmounted portal 404s |
|
|
46
|
+
| Resource registered | grep the portal's `config/routes.rb` for `register_resource ::<X>` | Unregistered ⇒ no URLs (`resource_url_for` fails) |
|
|
47
|
+
| Migrations applied | `rails db:migrate:status` before `pu:res:conn` | `conn` seeds the policy from columns |
|
|
48
|
+
|
|
49
|
+
Inspect with your own tools **before** running any generator.
|
|
50
|
+
|
|
51
|
+
## 🛠 Use the generator — pick the right install path
|
|
52
|
+
|
|
53
|
+
Never hand-write base controllers, engine files, layouts, or route registration. Pass `--dest`/`--auth`/`--force`/`--skip-bundle` for unattended runs.
|
|
54
|
+
|
|
55
|
+
| Task | Generator | Verify first |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| Install — **existing** app | `bundle add plutonium` + `pu:core:install` | It's an existing app (use `base.rb`, **not** `plutonium.rb`) |
|
|
58
|
+
| Install — **fresh** app | `rails new … -m …/plutonium.rb` | Brand-new app **only** |
|
|
59
|
+
| Feature package | `pu:pkg:package <name>` | Not already present |
|
|
60
|
+
| Portal package | `pu:pkg:portal <name> --auth=…/--public/--byo/--scope=…` | Auth strategy decided; then `mount` the engine by hand |
|
|
61
|
+
| Connect a resource | `pu:res:conn <Res> --dest=portal` | Migrated; target portal exists |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
24
65
|
# Part 1 — Installation
|
|
25
66
|
|
|
26
67
|
## Fresh Rails app (recommended)
|
|
@@ -21,6 +21,46 @@ For multi-tenant invitations and membership, see [[plutonium-tenancy]] › Invit
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
+
## 🛑 Before you set up auth: confirm the shape (ASK — don't infer)
|
|
25
|
+
|
|
26
|
+
"Set up login" hides the one decision that determines everything: **is this multi-tenant SaaS or a single auth surface?** Pick wrong and you either hand-assemble what a meta-generator does in one shot, or scaffold a SaaS spine an app doesn't need. Resolve each — confirming by inspection (next section):
|
|
27
|
+
|
|
28
|
+
1. **Single auth or multi-tenant SaaS?** "Each user belongs to / manages an org/team" ⇒ SaaS ⇒ **`pu:saas:setup`** (the meta-generator: user + entity + membership + portal + profile + welcome + invites in one). A plain login with no tenant ⇒ `pu:rodauth:install` + `pu:rodauth:account`.
|
|
29
|
+
2. **Account type.** Basic user, **hardened admin** (`pu:rodauth:admin` — 2FA/lockout/audit, no public signup), or **API** (`--api_only --jwt`)? They're different generators.
|
|
30
|
+
3. **Public signup allowed?** Default yes for `account`; admin accounts are invite-only.
|
|
31
|
+
4. **Profile / account-settings page?** Needs `pu:profile:install` **and** `pu:profile:conn` (without conn there's no `/profile` route).
|
|
32
|
+
5. **Roles.** Index 0 is most privileged (`owner`/`super_admin`); invites default new members to `roles[1]`. `pu:saas:setup` prepends `owner` — don't list it.
|
|
33
|
+
|
|
34
|
+
**Never ship a guessed account-type, model name, or `--roles` as applied commands.** Read them off the app first; fall back to `AskUserQuestion` only for product choices (separate staff accounts vs shared, is signup open).
|
|
35
|
+
|
|
36
|
+
## ✅ Before you run a generator: verify the ground truth (CHECK — read it, don't ask for it)
|
|
37
|
+
|
|
38
|
+
You have file access — **inspect**; don't ask the user to describe their app.
|
|
39
|
+
|
|
40
|
+
| Check | How | Why it matters |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| Rodauth already installed | `ls app/rodauth/rodauth_app.rb`; grep `Gemfile` for `rodauth` | Re-running `pu:rodauth:install` clobbers config |
|
|
43
|
+
| Existing account models | grep `app/models` for `Rodauth::Rails.model` | Which account type exists / don't duplicate |
|
|
44
|
+
| SaaS spine already run | `ls` for the entity portal + membership model | **`pu:saas:setup` chains 4 generators — don't re-run them separately** |
|
|
45
|
+
| Profile wired | grep the user model for `has_one :profile` + `after_create`; is `profile_url` defined? | Else `current_user.profile` is nil / no route (`pu:profile:conn` missing) |
|
|
46
|
+
| Role ordering | Read the membership `enum :role` | Index 0 = most privileged; invites default to `roles[1]` |
|
|
47
|
+
|
|
48
|
+
Inspect with your own tools **before** running any generator.
|
|
49
|
+
|
|
50
|
+
## 🛠 Use the generator — never hand-write Rodauth
|
|
51
|
+
|
|
52
|
+
Never hand-write Rodauth plugin files, account models, or profile resources.
|
|
53
|
+
|
|
54
|
+
| Task | Generator | Verify first |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| Multi-tenant SaaS spine | `pu:saas:setup --user U --entity E --roles=…` | Not already run (don't re-run the 4 sub-generators it chains) |
|
|
57
|
+
| Rodauth base | `pu:rodauth:install` | Not already installed |
|
|
58
|
+
| Basic account | `pu:rodauth:account NAME --defaults` | Rodauth installed |
|
|
59
|
+
| Hardened admin | `pu:rodauth:admin NAME --roles=…` | Rodauth installed |
|
|
60
|
+
| Profile page | `pu:profile:install …` + `pu:profile:conn --dest=portal` | Migrated; portal exists |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
24
64
|
## Install
|
|
25
65
|
|
|
26
66
|
```bash
|
|
@@ -25,6 +25,52 @@ For tenant-scoped `relation_scope` and entity scoping, load [[plutonium-tenancy]
|
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
+
## 🛑 Before you write behavior: place it in the right layer (ASK — don't infer)
|
|
29
|
+
|
|
30
|
+
"Make X happen" doesn't say **where** X lives. Put it in the wrong layer and you get authorization that doesn't authorize, a 500 on the happy path, or a CRUD override that breaks params/auth. First place the requirement, then confirm names against the real code (next section):
|
|
31
|
+
|
|
32
|
+
| The requirement (in plain words) | Goes in | **NOT** in |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| "only \<role/owner\> may do X" — *who is allowed* | **Policy** `def x?` | a `condition:` proc — that only hides the button; the route stays live and callable |
|
|
35
|
+
| "doing X changes state / sends mail / charges a card" — *the work* | **Interaction** `execute`, registered as an action | a hand-written controller action; an override of `create`/`update` |
|
|
36
|
+
| "after create/update go to Y" · "munge a param" · "reshape the index query" | **Controller hook** (`redirect_url_after_submit`, `resource_params`, `filtered_resource_collection`) | overriding `create`/`update`/`index` |
|
|
37
|
+
| "which fields are visible / editable" | **Policy** `permitted_attributes_for_*` | the definition — that only controls *how* a field renders |
|
|
38
|
+
|
|
39
|
+
Then resolve the specifics:
|
|
40
|
+
|
|
41
|
+
1. **A custom action needs BOTH:** an interaction (the work) **and** a policy `def <action>?` (the authorization). Miss the policy method ⇒ the action silently returns `false` (dead button). Put the role check in `condition:` ⇒ it isn't enforced — a direct POST still runs.
|
|
42
|
+
2. **`create?`/`read?` default to `false`** — override explicitly; derived methods (`update?`/`show?`/…) inherit.
|
|
43
|
+
3. **Any `create!`/`update!`/`save!` in `execute`** ⇒ rescue `ActiveRecord::RecordInvalid` → `failed(e.record.errors)`. Not auto-rescued — otherwise a validation failure 500s.
|
|
44
|
+
4. **`has_cents`** ⇒ permit `:price`, never `:price_cents`.
|
|
45
|
+
5. **New vs editing** — never re-scaffold a controller/policy/interaction that's been customized.
|
|
46
|
+
|
|
47
|
+
**Never ship a guessed role method, column, enum value, or association as applied code.** `user.finance?`, `record.status_approved?`, `expense.submitted_by` either exist in the app or they don't — confirm them before writing, don't assume. Fall back to `AskUserQuestion` only for genuine product choices (what the rule *should* be), never for facts you can read.
|
|
48
|
+
|
|
49
|
+
## ✅ Before you edit: verify the ground truth (CHECK — read it, don't ask for it)
|
|
50
|
+
|
|
51
|
+
You have file access — **inspect**; don't ask the user to describe their own app.
|
|
52
|
+
|
|
53
|
+
| Check | How | Why it matters |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| File already customized | Read `app/policies/<x>_policy.rb`, the controller, `app/interactions/*` | Edit incrementally — re-scaffolding clobbers customizations |
|
|
56
|
+
| The role/method you authorize on exists | grep the user model for `def finance?` / `enum :role` / `has_role?` | `user.finance?` 500s (or is silently `false`) if absent |
|
|
57
|
+
| The columns/enum your interaction writes | Read the model + `db/schema.rb` for the enum value, `approved_by`/`approved_at`, the submitter assoc | `update!(status: :approved)` raises if the value/column is missing |
|
|
58
|
+
| Action not already wired | grep the definition for `action :<x>`; grep the policy for `def <x>?` | Avoids duplicate or dead actions |
|
|
59
|
+
| Cross-resource access | Use `authorized_resource_scope` / `allowed_to?`, never raw `where`/`find` | Raw queries bypass the other resource's tenancy + visibility |
|
|
60
|
+
|
|
61
|
+
Inspect with your own tools **before** proposing code.
|
|
62
|
+
|
|
63
|
+
## 🛠 Use the generator — and know what's hand-authored
|
|
64
|
+
|
|
65
|
+
| Task | How | Verify first |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| Base trio (controller + policy + interaction-base) | `pu:res:scaffold` | New resource |
|
|
68
|
+
| Portal-specific controller/policy | `pu:res:conn … --dest=portal` | Resource exists |
|
|
69
|
+
| **A custom-action interaction** | **Hand-author** in `app/interactions/<name>_interaction.rb` (subclass `ResourceInteraction`) — **there is NO `pu:res:interaction` generator; don't invent one** | — |
|
|
70
|
+
| Edit an existing customized policy/controller/interaction | Hand-edit the file | It was already generated — re-scaffolding clobbers it |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
28
74
|
# Part 1 — Controllers
|
|
29
75
|
|
|
30
76
|
Plutonium controllers ship full CRUD out of the box; nearly all customization lives in definitions / policies / interactions. The controller stays thin.
|
|
@@ -494,7 +540,7 @@ def permitted_associations
|
|
|
494
540
|
end
|
|
495
541
|
```
|
|
496
542
|
|
|
497
|
-
Declares which associations get their own **tab on the show page**. When `permitted_associations` is non-empty, the show page renders a tablist: a "Details" tab (the main field card + metadata aside) plus one tab per association — each lazy-loaded via a frame navigator panel pointing at the associated `has_many` collection, `has_one` record, or `belongs_to` target. When empty, the show page renders without tabs.
|
|
543
|
+
Declares which associations get their own **tab on the show page**. When `permitted_associations` is non-empty, the show page renders a tablist: a "Details" tab (the main field card + metadata aside) plus one tab per association — each lazy-loaded via a frame navigator panel pointing at the associated `has_many` collection, `has_one` record, or `belongs_to` target. When empty, the show page renders without tabs. If `permitted_attributes_for_show` resolves to **no fields**, the empty Details tab is omitted and the first association tab leads instead.
|
|
498
544
|
|
|
499
545
|
Each named association must:
|
|
500
546
|
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plutonium-kanban
|
|
3
|
+
description: Use BEFORE building or customizing a kanban board view for any Plutonium resource — the kanban do…end DSL, column declarations, card_fields, position_on modes, realtime, column actions, kanban_move? policy, and quick-add. The single source for "how do I add a kanban board to a resource".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Plutonium Kanban
|
|
7
|
+
|
|
8
|
+
Turn any resource index into a drag-and-drop kanban board with a single `kanban do…end` block in the resource Definition. This skill covers the full DSL surface, model setup, authorization, and the caveats.
|
|
9
|
+
|
|
10
|
+
For field-level rendering on cards (card_fields slots), see [[plutonium-resource]] › Index Views. For policy structure, see [[plutonium-behavior]]. For custom Phlex components on cards, see [[plutonium-ui]].
|
|
11
|
+
|
|
12
|
+
## 🚨 Critical (read first)
|
|
13
|
+
|
|
14
|
+
- **`kanban do…end` in the Definition auto-enables `:kanban`** in `defined_index_views` — exactly like `grid_fields` enables `:grid`. You do not need to call `index_views :kanban` separately unless you want to remove the table view.
|
|
15
|
+
- **The model needs `include Plutonium::Positioning`** (and a decimal `position` column + `positioned_on` call) for drag ordering to work. Without it, cards render unordered and moves raise an error. Use `position_on false` to explicitly opt out.
|
|
16
|
+
- **Static column actions are auto-registered** as interactive resource actions at class-load time. Dynamic boards (`columns do…end`) cannot introspect their columns at load time — declare any column-action interactions separately with top-level `action` calls.
|
|
17
|
+
- **Moves bypass `permitted_attributes_for_update`** — the `on_drop` callback runs with full model access. Gate the move itself with `kanban_move?` in the policy.
|
|
18
|
+
- **Quick-add (`add: true`) only appears when `create?` is true** in the policy.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Model setup
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
class Task < ApplicationRecord
|
|
26
|
+
include Plutonium::Positioning
|
|
27
|
+
|
|
28
|
+
# position_on :position (default attr) scoped to the grouping column
|
|
29
|
+
positioned_on :position, scope: :status
|
|
30
|
+
|
|
31
|
+
def mark_done!
|
|
32
|
+
update!(status: "done")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Migration — add the position column with the `t.position` helper (a tuned `decimal(16,8)`; works in `create_table` and `change_table`). Don't hand-roll a small scale — `scale: 6` exactly matches the `1e-6` rebalance threshold and can round to a duplicate. Use `t.position` (scale 8) or ≥ 8 if hand-written:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
create_table :tasks do |t|
|
|
41
|
+
t.string :title, null: false
|
|
42
|
+
t.string :status, null: false, default: "todo"
|
|
43
|
+
t.position # decimal :position, precision: 16, scale: 8
|
|
44
|
+
t.timestamps
|
|
45
|
+
|
|
46
|
+
t.index [:status, :position]
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Minimal definition
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
class TaskDefinition < ResourceDefinition
|
|
56
|
+
kanban do
|
|
57
|
+
column :todo,
|
|
58
|
+
scope: -> { where(status: "todo") },
|
|
59
|
+
on_drop: ->(r) { r.update!(status: "todo") }
|
|
60
|
+
|
|
61
|
+
column :doing,
|
|
62
|
+
scope: -> { where(status: "doing") },
|
|
63
|
+
on_drop: ->(r) { r.update!(status: "doing") }
|
|
64
|
+
|
|
65
|
+
column :done,
|
|
66
|
+
scope: -> { where(status: "done") },
|
|
67
|
+
on_drop: :mark_done! # Symbol → record.mark_done!
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Set it as the default or only view:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
default_index_view :kanban # still shows view switcher with :table
|
|
76
|
+
index_views :kanban # remove table; kanban is the only view
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Board-level options
|
|
82
|
+
|
|
83
|
+
| DSL call | Purpose | Default |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `per_column N` | Cap cards rendered per column; `+N more` footer when exceeded | unlimited |
|
|
86
|
+
| `position_on :attr` | Custom attribute name for ordering (Mode A) | `:position` |
|
|
87
|
+
| `position_on :attr do \|move\| … end` | BYO positioning block (Mode B) | — |
|
|
88
|
+
| `position_on false` | No ordering or repositioning (Mode C) | — |
|
|
89
|
+
| `card_fields(**slots)` | Override grid slot layout for cards; same slot keys as `grid_fields` | inherits `grid_fields` |
|
|
90
|
+
| `realtime true` | ActionCable broadcast after every move | false |
|
|
91
|
+
| `lazy false` | Eager-load all column frames on the initial request | `true` (lazy) |
|
|
92
|
+
| `show_in :modal` / `:page` | Open a card's show page in a centered modal (`:modal`) or full-page (`:page`). Overrides the definition's `show_in` for this board | inherits definition (`:page`) |
|
|
93
|
+
| `columns do … end` | Dynamic columns evaluated at request time with view context | — |
|
|
94
|
+
|
|
95
|
+
### `card_fields`
|
|
96
|
+
|
|
97
|
+
Overrides the grid card layout for kanban cards. Uses the same slot keys as `grid_fields`:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
card_fields header: :title, meta: [:status, :priority], footer: :due_at
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `position_on` modes
|
|
104
|
+
|
|
105
|
+
- **Mode A (default)** — delegates to `record.reposition!(prev_record:, next_record:)` from `Plutonium::Positioning`. Requires the model concern and a decimal column.
|
|
106
|
+
- **Mode B (block)** — you write the persistence. Plutonium still orders by the attribute; the block only persists the new value. Block receives a `Plutonium::Kanban::Positioning::Move` (fields: `record`, `column`, `prev`, `next`, `index`).
|
|
107
|
+
- **Mode C (`false`)** — no ordering, no repositioning. `on_drop` still fires.
|
|
108
|
+
|
|
109
|
+
### `realtime`
|
|
110
|
+
|
|
111
|
+
Broadcasts refreshed column turbo-frames to all board subscribers after every successful move. Requires ActionCable. Opt in per-board:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
realtime true
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `show_in`
|
|
118
|
+
|
|
119
|
+
Where a card click opens the record's show page. `:modal` renders the show page in a **centered** dialog; `:page` is a full-page navigation. The show modal is always centered — deliberately NOT the definition's `modal_mode` (which styles `new`/`edit`). No per-card wiring: the `Show` page detects the modal frame (`in_modal?`) and wraps its details in the centered modal chrome.
|
|
120
|
+
|
|
121
|
+
`show_in` also exists **on the definition** (`show_in :modal` / `:page`, default `:page`), where it governs the table and grid show links too. The kanban board inherits the definition's value unless it sets its own — so set it once on the definition for everywhere, or on the board to override just the board.
|
|
122
|
+
|
|
123
|
+
From inside the show modal, an expand icon (or ⌘/Ctrl/middle-click on the card) opens the full page in a new tab.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
class TaskDefinition < ResourceDefinition
|
|
127
|
+
show_in :modal # table + grid + board open show in a centered modal
|
|
128
|
+
kanban do
|
|
129
|
+
# show_in :page # override: this board navigates full-page
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Dynamic columns
|
|
135
|
+
|
|
136
|
+
Evaluates the block at request time with the view context as `self` (`current_user`, `params`, `current_scoped_entity`, helpers all available). The block must return an Array of `Plutonium::Kanban::Column` objects — `column` is a DSL method only available outside the `columns` block. Declare any column-action interactions as top-level definition `action` calls — the block is not introspectable at class-load time.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
kanban do
|
|
140
|
+
columns do
|
|
141
|
+
# `self` is the view context here — use Plutonium::Kanban::Column.new, NOT `column`.
|
|
142
|
+
current_user.teams.map do |team|
|
|
143
|
+
Plutonium::Kanban::Column.new(
|
|
144
|
+
:"team_#{team.id}",
|
|
145
|
+
label: team.name,
|
|
146
|
+
scope: -> { where(team_id: team.id) },
|
|
147
|
+
on_drop: ->(r) { r.update!(team_id: team.id) }
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Column options
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
column :key,
|
|
160
|
+
label: "Custom Label", # defaults to key.to_s.titleize
|
|
161
|
+
color: :green, # Tailwind-mapped color hint
|
|
162
|
+
wip: 3, # max cross-column moves into this column
|
|
163
|
+
scope: -> { where(…) }, # 0-arg lambda or Symbol (sent to relation)
|
|
164
|
+
on_drop: ->(r) { … }, # 1-arg lambda or Symbol → record.method!
|
|
165
|
+
collapsed: true, # starts collapsed (Stimulus persists toggle to localStorage)
|
|
166
|
+
add: true, # show "+ Add" button (requires create?)
|
|
167
|
+
accepts: true, # true (default), false, Array of source keys, or 1-arg Proc
|
|
168
|
+
locked: false, # reject all incoming drops (server-enforced)
|
|
169
|
+
role: :backlog # :backlog or :done (see presets below)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Column role presets
|
|
173
|
+
|
|
174
|
+
| Role | Preset behaviour |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `:backlog` | `add: true` |
|
|
177
|
+
| `:done` | `color: :green`, `collapsed: true` |
|
|
178
|
+
|
|
179
|
+
Explicit options override the preset (e.g. `role: :done, collapsed: false`).
|
|
180
|
+
|
|
181
|
+
### `accepts:`
|
|
182
|
+
|
|
183
|
+
Controls which source columns may drop cards here:
|
|
184
|
+
|
|
185
|
+
- `true` (default) — any source allowed
|
|
186
|
+
- `false` — column is a drop target but refuses everything (snap-back)
|
|
187
|
+
- `Array` — list of source column keys allowed: `accepts: [:doing]`
|
|
188
|
+
- `Proc` (1-arg) — per-card predicate: `accepts: ->(record) { record.state == "doing" }`
|
|
189
|
+
|
|
190
|
+
Checked server-side. Client-side visual hints read `data-kanban-accepts`.
|
|
191
|
+
|
|
192
|
+
### `on_drop:`
|
|
193
|
+
|
|
194
|
+
Runs inside a transaction after authorization and before repositioning. Receives the record for lambda form:
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
on_drop: ->(r) { r.update!(status: "done") } # update! directly
|
|
198
|
+
on_drop: ->(r) { r.status = "done" } # attribute assignment — saved automatically
|
|
199
|
+
on_drop: :mark_done! # dispatched as record.mark_done!
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
If `on_drop` only assigns attributes without calling `save!`/`update!`, the controller calls `record.save!` automatically when the record has unsaved changes after `on_drop` returns.
|
|
203
|
+
|
|
204
|
+
### Column actions
|
|
205
|
+
|
|
206
|
+
Declared inside the column block. Auto-registered as interactive resource actions:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
column :done, … do
|
|
210
|
+
action :archive_all,
|
|
211
|
+
interaction: ArchiveTasksInteraction,
|
|
212
|
+
on: :all, # :all or :visible
|
|
213
|
+
label: "Archive all",
|
|
214
|
+
icon: Phlex::TablerIcons::Archive,
|
|
215
|
+
confirmation: "Archive all done tasks?"
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`on: :all` — passes every record in the column scope. `on: :visible` — passes only the currently rendered subset (respects `per_column`).
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Authorization
|
|
224
|
+
|
|
225
|
+
### `kanban_move?` → `update?`
|
|
226
|
+
|
|
227
|
+
Every drag-and-drop is authorized via `kanban_move?` in the policy. Default:
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
def kanban_move?
|
|
231
|
+
update?
|
|
232
|
+
end
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Override for finer control:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
class TaskPolicy < ResourcePolicy
|
|
239
|
+
def kanban_move? = user.member? # members can drag; only admins edit
|
|
240
|
+
end
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
When `kanban_move?` returns `false`, the board renders read-only — no drag handles, no drop zones.
|
|
244
|
+
|
|
245
|
+
### Move authorization flow
|
|
246
|
+
|
|
247
|
+
1. Record loaded via current `relation_scope` (same as index).
|
|
248
|
+
2. `kanban_move?` checked — HTTP 403 on failure.
|
|
249
|
+
3. Column `accepts:` / `locked:` checked — HTTP 422 + card snap-back on failure.
|
|
250
|
+
4. `wip:` limit checked for cross-column moves — HTTP 422 on failure.
|
|
251
|
+
5. `on_drop` fires + record repositioned, all in a transaction.
|
|
252
|
+
|
|
253
|
+
On a 422 rejection (steps 3–4) the response re-renders the source column (snap-back) **and** appends a dismissable warning toast naming the reason (e.g. `“Pending” is at its WIP limit (5).`) to the board's `#kanban-flash` region — so the snap-back is never silent. The toast renders the shared `plutonium/toast` partial directly (not via `flash`), so a stale undisplayed flash can't leak into the turbo-stream response.
|
|
254
|
+
|
|
255
|
+
### No permitted-attributes gate
|
|
256
|
+
|
|
257
|
+
Moves do not pass through `permitted_attributes_for_update`. `on_drop` is trusted author code; it is responsible for assigning only the appropriate attributes.
|
|
258
|
+
|
|
259
|
+
### Quick-add
|
|
260
|
+
|
|
261
|
+
The `+ Add` button (column `add: true`) only renders when the policy's `create?` is true. The opened form is the standard new-resource form.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Worked example (full)
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
class TaskDefinition < ResourceDefinition
|
|
269
|
+
kanban do
|
|
270
|
+
per_column 25
|
|
271
|
+
card_fields header: :title, meta: [:status]
|
|
272
|
+
|
|
273
|
+
column :todo,
|
|
274
|
+
scope: -> { where(status: "todo") },
|
|
275
|
+
on_drop: ->(r) { r.update!(status: "todo") },
|
|
276
|
+
role: :backlog # add: true
|
|
277
|
+
|
|
278
|
+
column :doing,
|
|
279
|
+
scope: -> { where(status: "doing") },
|
|
280
|
+
on_drop: ->(r) { r.update!(status: "doing") },
|
|
281
|
+
wip: 3
|
|
282
|
+
|
|
283
|
+
column :done,
|
|
284
|
+
scope: -> { where(status: "done") },
|
|
285
|
+
on_drop: :mark_done!,
|
|
286
|
+
accepts: ->(task) { task.status == "doing" },
|
|
287
|
+
role: :done do # color: :green, collapsed: true
|
|
288
|
+
action :archive_all,
|
|
289
|
+
interaction: ArchiveTasksInteraction,
|
|
290
|
+
on: :all,
|
|
291
|
+
label: "Archive all"
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Full docs
|
|
300
|
+
|
|
301
|
+
- Guide: `docs/guides/kanban.md`
|
|
302
|
+
- DSL reference: `docs/reference/kanban/dsl.md`
|
|
303
|
+
- Positioning reference: `docs/reference/kanban/positioning.md`
|
|
304
|
+
- Authorization reference: `docs/reference/kanban/authorization.md`
|
|
305
|
+
- Working example: `test/dummy/app/definitions/task_definition.rb`
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Related skills
|
|
310
|
+
|
|
311
|
+
- [[plutonium-resource]] — Definition layer, `grid_fields`, index views, actions
|
|
312
|
+
- [[plutonium-behavior]] — Policy methods, `kanban_move?`, interactions
|
|
313
|
+
- [[plutonium-ui]] — Custom Phlex components for card rendering
|
|
@@ -22,6 +22,46 @@ For tenancy / `associated_with` / `relation_scope`, load [[plutonium-tenancy]].
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
+
## 🛑 Before you scaffold or edit: confirm the shape (ASK — don't infer)
|
|
26
|
+
|
|
27
|
+
"Add a Product" / "add a status field" is underspecified. Guess wrong and you scaffold into the wrong package, churn migrations, store money as a lossy float, reference a model that doesn't exist, or **clobber files the user has customized**. Resolve each — **by inspecting (next section), not guessing** — then restate the resolved shape and confirm:
|
|
28
|
+
|
|
29
|
+
1. **New resource, or editing an existing one?** Existing ⇒ **NEVER re-run `pu:res:scaffold`** (it overwrites the customized model/definition/policy/controller). Add an incremental migration + hand-edit. Confirm by reading the files *first*.
|
|
30
|
+
2. **Destination & portal.** `--dest=main_app` or a package? Which portal does `pu:res:conn` wire it to? Both are **required** and unguessable from the request.
|
|
31
|
+
3. **Field types — and the money question.** A monetary field ⇒ `has_cents` (`price_cents:integer` + `has_cents :price_cents`, reference `:price`), **never a bare `decimal`**. Enums, attachments (`:attachment`/`:attachments`), rich text, references each have specific syntax (§ Field Type Syntax). Confirm types rather than inventing them.
|
|
32
|
+
4. **Referenced associations must already exist.** `category:belongs_to` silently targets a `Category` — if that model isn't there, scaffold it first.
|
|
33
|
+
5. **Beyond columns:** does it need search / filters / scopes / custom or bulk actions? Those live in the definition + policy, not the scaffold — name them now so you don't half-build.
|
|
34
|
+
|
|
35
|
+
**Never emit applied scaffold commands from a guessed `--dest`, portal, or money-shape.** Confirm or read them first; fall back to `AskUserQuestion` only for product choices you can't read off the code (which portal, is `price` money). The decisions compound: *existing+customized ⇒ migration not scaffold*; *money ⇒ `has_cents` + `:price` in the policy*; *new reference ⇒ target model must exist*.
|
|
36
|
+
|
|
37
|
+
## ✅ Before you touch files: verify the ground truth (CHECK — read it, don't ask for it)
|
|
38
|
+
|
|
39
|
+
You have file access — **use it.** "Paste me the model" is a fallback for when you genuinely can't read the repo, not the default.
|
|
40
|
+
|
|
41
|
+
| Check | How | Why it matters |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| Resource already exists | Read `app/models/<x>.rb` + `app/**/definitions/<x>_definition.rb`; `ls` the policy/controller | Existing + customized ⇒ **incremental edit, never re-scaffold** |
|
|
44
|
+
| Model is a resource record | grep `Plutonium::Resource::Record` / `< ResourceRecord` | Auto-detection + the field DSL depend on it |
|
|
45
|
+
| Name collision | Read `db/schema.rb` for the column; grep the model for an existing method/enum/assoc of that name | A clashing `status` silently breaks the enum |
|
|
46
|
+
| Referenced association target | `ls app/models` for the `belongs_to` target class | `x:belongs_to` to a missing model fails the scaffold |
|
|
47
|
+
| `has_cents` policy leak | grep the policy/definition for `_cents` | Generator emits `:price_cents`; fix to the virtual `:price` |
|
|
48
|
+
| Migrations applied | `rails db:migrate:status` before `pu:res:conn` | Unmigrated / unconnected ⇒ the resource is invisible |
|
|
49
|
+
|
|
50
|
+
Inspect with your own tools **before** proposing commands or edits.
|
|
51
|
+
|
|
52
|
+
## 🛠 Use the generator — and don't clobber
|
|
53
|
+
|
|
54
|
+
Never hand-write the initial model, migration, policy, definition, or controller. Reach for the generator; quote args with `?`/`{}`; pass `--dest=`.
|
|
55
|
+
|
|
56
|
+
| Task | Generator | Verify first |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| New resource | `pu:res:scaffold Model field:type … --dest=` | `--dest` confirmed; referenced models exist |
|
|
59
|
+
| Connect to a portal | `pu:res:conn Model --dest=portal` | Migrations are run |
|
|
60
|
+
| Regenerate model from columns | `pu:res:scaffold Model --no-migration` | ⚠ **regenerates the model file** — review the diff; overwrites customizations |
|
|
61
|
+
| Add a field to an **existing, customized** resource | `rails g migration AddXToYs …` + hand-edit model/definition/policy | This resource was already scaffolded — re-scaffolding clobbers it |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
25
65
|
# Part 1 — Creating a Resource
|
|
26
66
|
|
|
27
67
|
## Quick checklist
|
|
@@ -25,6 +25,49 @@ Cross-references back to [[plutonium-resource]] (models, definitions) and [[plut
|
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
+
## 🛑 Before you scope anything: confirm the shape (ASK — don't infer)
|
|
29
|
+
|
|
30
|
+
Tenancy decisions are **underspecified by a one-line request and have high blast radius**: guess the entity, the strategy, or the association path and you ship a model that *compiles but leaks across tenants*, *raises at runtime*, or *produces the wrong URL*. "Scope X to the tenant" does **not** determine any of the below.
|
|
31
|
+
|
|
32
|
+
Resolve each decision — **by inspecting the app (next section), not by guessing** — then restate the resolved shape in a sentence and confirm:
|
|
33
|
+
|
|
34
|
+
1. **Is this portal even entity-scoped?** A model is only tenant-filtered inside a portal that declares `scope_to_entity`. No `scope_to_entity` ⇒ your model change does nothing. (Verify it exists *before* touching the model.)
|
|
35
|
+
2. **Which entity model, and which strategy?** `Organization` / `Account` / `Tenant` / `Company`? `:path` (most common) or custom (subdomain/session)? **Never default to `Organization` + `:path`** — read it.
|
|
36
|
+
3. **What is the association PATH from this model to the entity?** Direct `belongs_to`, multi-hop `has_one :through`, a membership/join, or polymorphic needing a custom `associated_with_<entity>` scope (§ Three model shapes). This is the #1 thing to confirm against the **actual model** — wrong path ⇒ leak *or* raise.
|
|
37
|
+
4. **Nested (parent-scoped) or entity-scoped?** Reached through a parent ⇒ parent scoping wins, don't double-scope. And **nesting is ONE level only** — a three-level URL request can't be met with `register_resource` nesting; say so before wiring it.
|
|
38
|
+
5. **Uniqueness scoped to the tenant FK?** Any `validates … uniqueness` must scope to the tenant FK (`scope: :organization_id`) or it leaks across tenants.
|
|
39
|
+
|
|
40
|
+
**Never emit applied scoping code from a *guessed* association path.** Confirm the path against the real model first; fall back to `AskUserQuestion` only for genuinely product-level choices you can't read off the code (which entity, which strategy). The decisions compound: *no scoped portal ⇒ nothing filters*; *nested ⇒ parent-scoped, not entity-scoped*; *multi-hop ⇒ needs `has_one :through` or a custom scope*.
|
|
41
|
+
|
|
42
|
+
## ✅ Before you edit: verify the ground truth (CHECK — read it, don't ask for it)
|
|
43
|
+
|
|
44
|
+
You have file access — **use it.** "Paste me the model" is a fallback for when you genuinely can't read the repo, **not** the default. Inspect first, then act:
|
|
45
|
+
|
|
46
|
+
| Check | How | Why it matters |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| Portal is scoped | `rg "scope_to_entity" -n` in the portal engine(s) | Confirms entity class + strategy; absent ⇒ scoping is a no-op |
|
|
49
|
+
| Model is a resource | Read the model — `include Plutonium::Resource::Record` / `< ResourceRecord` | `associated_with` only exists on resource records |
|
|
50
|
+
| Association path resolves | Read the model's `belongs_to`/`has_one :through` chain to the entity (or a `associated_with_<entity>` scope) | This is the real fix site; missing path ⇒ raise |
|
|
51
|
+
| Denormalized FK already present | Read the schema/migration for an existing `<entity>_id` column | Collapses a multi-hop chain to a one-line `belongs_to` |
|
|
52
|
+
| No leaky override | `rg "relation_scope" -n` in the policy | A manual `where(<entity>:…)` is the leak — **remove it**, don't patch it |
|
|
53
|
+
| (Invites) prerequisites | Membership model exists with `enum :role`; AR encryption keys set (`bin/rails db:encryption:init`) | `pu:invites:install` fails loudly without both |
|
|
54
|
+
|
|
55
|
+
Do this inspection with your own tools **before** proposing code. Surfacing a concrete edit you haven't grounded in the real files is how the "looks right, leaks anyway" bug ships.
|
|
56
|
+
|
|
57
|
+
## 🛠 Use the generator — and verify its precondition first
|
|
58
|
+
|
|
59
|
+
Hand-wiring tenancy (invite models, membership tables, join records) is how leaks happen. Reach for the generator, run it with `--dest=` to avoid prompts, and **confirm the precondition before running**:
|
|
60
|
+
|
|
61
|
+
| Task | Generator | Verify first |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| New SaaS spine (user + entity + membership + join) | `pu:saas:setup --user U --entity E` | None — this is the bootstrap |
|
|
64
|
+
| Scope a portal to an entity | `pu:pkg:portal --scope=Entity` | Entity model exists |
|
|
65
|
+
| New tenant-scoped model | `pu:res:scaffold Model entity:belongs_to …` then `pu:res:conn` | Migrations from prior scaffolds are run |
|
|
66
|
+
| Invite flow | `pu:invites:install` | Membership model exists (`enum :role`) **and** AR encryption keys configured |
|
|
67
|
+
| App model notified on accept | `pu:invites:invitable Model` | Invites already installed |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
28
71
|
# Part 1 — Entity Scoping
|
|
29
72
|
|
|
30
73
|
Built on three cooperating pieces:
|