plutonium 0.65.0 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/SKILL.md +43 -43
- data/.claude/skills/plutonium-app/SKILL.md +101 -59
- data/.claude/skills/plutonium-async-interactions/SKILL.md +19 -19
- data/.claude/skills/plutonium-auth/SKILL.md +119 -53
- data/.claude/skills/plutonium-behavior/SKILL.md +110 -78
- data/.claude/skills/plutonium-dashboard/SKILL.md +11 -4
- data/.claude/skills/plutonium-kanban/SKILL.md +81 -49
- data/.claude/skills/plutonium-resource/SKILL.md +144 -133
- data/.claude/skills/plutonium-tenancy/SKILL.md +104 -55
- data/.claude/skills/plutonium-testing/SKILL.md +130 -33
- data/.claude/skills/plutonium-ui/SKILL.md +151 -96
- data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
- data/CHANGELOG.md +21 -0
- data/README.md +9 -9
- data/SECURITY.md +1 -1
- data/app/assets/plutonium.css +1 -1
- data/docs/.vitepress/sync-skills.mjs +6 -3
- data/docs/blog/introducing-plutonium-dashboards.md +4 -5
- data/docs/blog/introducing-plutonium-i18n.md +4 -5
- data/docs/getting-started/installation.md +5 -5
- data/docs/getting-started/tutorial/02-first-resource.md +3 -3
- data/docs/getting-started/tutorial/03-authentication.md +7 -7
- data/docs/getting-started/tutorial/04-authorization.md +21 -4
- data/docs/getting-started/tutorial/05-custom-actions.md +2 -2
- data/docs/getting-started/tutorial/06-nested-resources.md +5 -2
- data/docs/getting-started/tutorial/07-author-portal.md +2 -2
- data/docs/getting-started/tutorial/08-customizing-ui.md +45 -30
- data/docs/getting-started/tutorial/index.md +1 -1
- data/docs/guides/adding-resources.md +10 -7
- data/docs/guides/authentication.md +25 -25
- data/docs/guides/authorization.md +24 -24
- data/docs/guides/creating-packages.md +17 -17
- data/docs/guides/custom-actions.md +32 -32
- data/docs/guides/customizing-ui.md +29 -26
- data/docs/guides/dashboards.md +1 -1
- data/docs/guides/index.md +3 -3
- data/docs/guides/kanban.md +55 -55
- data/docs/guides/multi-tenancy.md +35 -22
- data/docs/guides/nested-resources.md +21 -21
- data/docs/guides/performance.md +3 -3
- data/docs/guides/search-filtering.md +13 -13
- data/docs/guides/testing.md +16 -12
- data/docs/guides/theming.md +32 -17
- data/docs/guides/troubleshooting.md +2 -2
- data/docs/guides/user-invites.md +17 -17
- data/docs/guides/user-profile.md +51 -24
- data/docs/guides/wizards.md +55 -55
- data/docs/reference/app/generators.md +22 -22
- data/docs/reference/app/index.md +15 -18
- data/docs/reference/app/packages.md +8 -8
- data/docs/reference/app/portals.md +75 -29
- data/docs/reference/auth/accounts.md +15 -15
- data/docs/reference/auth/index.md +12 -12
- data/docs/reference/auth/profile.md +67 -29
- data/docs/reference/behavior/async-interactions.md +24 -24
- data/docs/reference/behavior/controllers.md +28 -28
- data/docs/reference/behavior/index.md +5 -5
- data/docs/reference/behavior/interactions.md +44 -44
- data/docs/reference/behavior/policies.md +48 -28
- data/docs/reference/configuration.md +6 -6
- data/docs/reference/dashboard/dsl.md +2 -2
- data/docs/reference/dashboard/index.md +1 -1
- data/docs/reference/generators/lite.md +7 -7
- data/docs/reference/i18n.md +23 -0
- data/docs/reference/index.md +1 -1
- data/docs/reference/kanban/authorization.md +9 -9
- data/docs/reference/kanban/dsl.md +32 -32
- data/docs/reference/kanban/index.md +1 -1
- data/docs/reference/kanban/positioning.md +17 -15
- data/docs/reference/resource/actions.md +51 -51
- data/docs/reference/resource/definition.md +73 -73
- data/docs/reference/resource/export.md +6 -6
- data/docs/reference/resource/index.md +16 -16
- data/docs/reference/resource/model.md +24 -24
- data/docs/reference/resource/positioning.md +78 -76
- data/docs/reference/resource/query.md +13 -13
- data/docs/reference/tenancy/entity-scoping.md +65 -35
- data/docs/reference/tenancy/index.md +11 -11
- data/docs/reference/tenancy/invites.md +20 -20
- data/docs/reference/tenancy/nested-resources.md +13 -13
- data/docs/reference/testing/index.md +116 -22
- data/docs/reference/ui/assets.md +57 -25
- data/docs/reference/ui/components.md +20 -20
- data/docs/reference/ui/displays.md +14 -14
- data/docs/reference/ui/forms.md +35 -35
- data/docs/reference/ui/index.md +17 -15
- data/docs/reference/ui/layouts.md +21 -21
- data/docs/reference/ui/pages.md +22 -22
- data/docs/reference/ui/tables.md +7 -7
- data/docs/reference/wizard/anchoring-resume.md +33 -32
- data/docs/reference/wizard/dsl.md +44 -44
- data/docs/reference/wizard/index.md +6 -6
- data/docs/reference/wizard/one-time.md +18 -18
- data/docs/reference/wizard/registration-launch.md +32 -32
- data/docs/reference/wizard/storage-config.md +23 -23
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/profile/conn_generator.rb +6 -0
- data/lib/plutonium/resource/record/associated_with.rb +23 -2
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/css/components.css +10 -10
- metadata +2 -2
data/docs/guides/user-profile.md
CHANGED
|
@@ -8,15 +8,15 @@ A `/profile` URL that shows the user's personal fields plus a "Security" section
|
|
|
8
8
|
|
|
9
9
|
## 🚨 Critical
|
|
10
10
|
|
|
11
|
-
- **Profile association is always `:profile`** regardless of the model class
|
|
12
|
-
- **Profile needs `pu:profile:conn` to be visible
|
|
13
|
-
- **
|
|
11
|
+
- **Profile association is always `:profile`** regardless of the model class: `current_user.profile`, `build_profile`, `params.require(:profile)` always work.
|
|
12
|
+
- **Profile needs `pu:profile:conn` to be visible.** Without it there is no `/profile` route, and `profile_url` stays `nil` so the user menu has no Profile link.
|
|
13
|
+
- **A user without a profile row is fine.** `profile_url` sends them to the new-profile form. Creating the row up front is optional (see step 4).
|
|
14
14
|
|
|
15
15
|
The show page renders the user's fields, then the **Security Settings** block with Rodauth-backed actions:
|
|
16
16
|
|
|
17
17
|

|
|
18
18
|
|
|
19
|
-
Editing produces a regular Plutonium form
|
|
19
|
+
Editing produces a regular Plutonium form, same form generator the rest of your resources use:
|
|
20
20
|
|
|
21
21
|

|
|
22
22
|
|
|
@@ -28,7 +28,7 @@ rails g pu:profile:setup date_of_birth:date bio:text \
|
|
|
28
28
|
--portal=competition_portal
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
`pu:profile:setup` is a meta-generator
|
|
31
|
+
`pu:profile:setup` is a meta-generator: runs `pu:profile:install` + `pu:profile:conn` in one shot.
|
|
32
32
|
|
|
33
33
|
## Step-by-step
|
|
34
34
|
|
|
@@ -64,31 +64,38 @@ rails db:prepare
|
|
|
64
64
|
rails g pu:profile:conn --dest=customer_portal
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
This registers the profile as a **singular** resource
|
|
67
|
+
This registers the profile as a **singular** resource: exposes `/profile` (no `:id`) and the `profile_url` helper.
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
It also generates a portal policy scoped to the current user: `create?` is `user.profile.nil?` (one profile per user), `update?` is `true`, and `destroy?` is `false`. The controller sets `user` from `current_user`, so `:user` is not a permitted attribute.
|
|
70
|
+
|
|
71
|
+
::: tip Profile can't be edited?
|
|
72
|
+
Policies generated by older versions of `pu:profile:conn` lack `update?`, so it falls back to `create?` and the profile becomes read-only once it exists. Add `def update? = true` to the portal's profile policy.
|
|
73
|
+
:::
|
|
74
|
+
|
|
75
|
+
### 4. (Optional) Create the row up front
|
|
76
|
+
|
|
77
|
+
Without a profile row, the first visit to the profile is the "create profile" form, and the Security section appears once it is saved. To land users on the show page from the first click, create the row when the user is created:
|
|
70
78
|
|
|
71
79
|
```ruby
|
|
72
|
-
# app/models/user.rb (
|
|
80
|
+
# app/models/user.rb (has_one added by pu:profile:install)
|
|
73
81
|
class User < ApplicationRecord
|
|
74
82
|
has_one :profile, class_name: "UserProfile", dependent: :destroy
|
|
75
83
|
|
|
76
|
-
after_create :create_profile
|
|
77
|
-
|
|
78
|
-
private
|
|
79
|
-
def create_profile! = create_profile
|
|
84
|
+
after_create :create_profile
|
|
80
85
|
end
|
|
81
86
|
```
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
and backfill existing users:
|
|
84
89
|
|
|
85
90
|
```bash
|
|
86
|
-
rails runner "User.find_each
|
|
91
|
+
rails runner "User.find_each { |u| u.profile || u.create_profile }"
|
|
87
92
|
```
|
|
88
93
|
|
|
94
|
+
Skip this when the profile has required fields the user must fill in, or when other code already creates profiles.
|
|
95
|
+
|
|
89
96
|
## What you get
|
|
90
97
|
|
|
91
|
-
The generated definition injects a custom `ShowPage` that renders `SecuritySection
|
|
98
|
+
The generated definition injects a custom `ShowPage` that renders `SecuritySection`: dynamically lists Rodauth security links based on which features are enabled:
|
|
92
99
|
|
|
93
100
|
| Feature enabled | Link rendered |
|
|
94
101
|
|---|---|
|
|
@@ -100,15 +107,34 @@ The generated definition injects a custom `ShowPage` that renders `SecuritySecti
|
|
|
100
107
|
| `active_sessions` | Active Sessions |
|
|
101
108
|
| `close_account` | Close Account |
|
|
102
109
|
|
|
103
|
-
If a feature isn't enabled, its link doesn't render
|
|
110
|
+
If a feature isn't enabled, its link doesn't render; no configuration needed.
|
|
104
111
|
|
|
105
112
|
## Linking to the profile
|
|
106
113
|
|
|
114
|
+
The topbar avatar menu already shows a "Profile" entry once `pu:profile:conn` has run. `Plutonium::Auth::Rodauth` defines `profile_url` as `nil`, and the generator overrides it in the portal's controller concern. To link from your own views:
|
|
115
|
+
|
|
107
116
|
```ruby
|
|
108
|
-
link_to("Profile", profile_url) if
|
|
117
|
+
link_to("Profile", profile_url) if profile_url
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Account features on the profile page
|
|
121
|
+
|
|
122
|
+
Per-user settings that aren't profile fields (API tokens, connected accounts, notification preferences) belong on the profile page. Link them from the same `ShowPage` hook, next to `SecuritySection`:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
class ShowPage < ShowPage
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def render_after_content
|
|
129
|
+
render Plutonium::Profile::SecuritySection.new
|
|
130
|
+
div(class: "mt-8") do
|
|
131
|
+
a(href: resource_url_for(ApiToken), class: "font-medium text-[var(--pu-text)] hover:underline") { "API tokens" }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
109
135
|
```
|
|
110
136
|
|
|
111
|
-
|
|
137
|
+
See [Reference › Auth › Profile](/reference/auth/profile#account-features-on-the-profile-page) for scoping such a resource to the user.
|
|
112
138
|
|
|
113
139
|
## Customizing the definition
|
|
114
140
|
|
|
@@ -152,12 +178,13 @@ rails g pu:profile:conn StaffUserProfile --dest=admin_portal
|
|
|
152
178
|
|
|
153
179
|
## Common issues
|
|
154
180
|
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
181
|
+
- **First visit shows a "create profile" form.** The user has no profile row yet. That's expected; see step 4 if they should land on the show page.
|
|
182
|
+
- **Profile is read-only after creation.** The policy predates the generated `update?`. Add `def update? = true`.
|
|
183
|
+
- **No Profile link in the user menu.** The profile isn't connected to this portal, so `profile_url` returns `nil`. Run `pu:profile:conn --dest=<portal>`.
|
|
184
|
+
- **`SecuritySection` shows nothing**: none of the relevant Rodauth features are enabled. Enable `change_password`, `otp`, etc. on the Rodauth plugin.
|
|
158
185
|
|
|
159
186
|
## Related
|
|
160
187
|
|
|
161
|
-
- [Reference › Auth › Profile](/reference/auth/profile)
|
|
162
|
-
- [Reference › Auth › Accounts](/reference/auth/accounts)
|
|
163
|
-
- [Authentication](./authentication)
|
|
188
|
+
- [Reference › Auth › Profile](/reference/auth/profile): full surface
|
|
189
|
+
- [Reference › Auth › Accounts](/reference/auth/accounts): Rodauth feature flags that gate SecuritySection
|
|
190
|
+
- [Authentication](./authentication): the underlying auth setup
|
data/docs/guides/wizards.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Wizards
|
|
2
2
|
|
|
3
3
|
::: warning Experimental
|
|
4
|
-
Wizards are experimental
|
|
4
|
+
Wizards are experimental. The DSL and behavior may change in a future release.
|
|
5
5
|
:::
|
|
6
6
|
|
|
7
|
-
Build multi-step flows
|
|
7
|
+
Build multi-step flows (onboarding, checkout, "create several related records across screens", branching questionnaires) as a single declarative Ruby class.
|
|
8
8
|
|
|
9
|
-
A wizard collects typed `data` across ordered `step`s, optionally branches with `condition:`, and commits at the end via `execute`. It reuses Plutonium's existing field DSL (`attribute`/`input`/`validates`/`structured_input`/`form_layout`), form rendering, actions, and policies
|
|
9
|
+
A wizard collects typed `data` across ordered `step`s, optionally branches with `condition:`, and commits at the end via `execute`. It reuses Plutonium's existing field DSL (`attribute`/`input`/`validates`/`structured_input`/`form_layout`), form rendering, actions, and policies, it does **not** invent a parallel stack.
|
|
10
10
|
|
|
11
11
|
## Goal
|
|
12
12
|
|
|
13
|
-
The user lands on the first step, fills it in, clicks Next, and walks through the flow. Branching steps appear or disappear based on earlier answers. A built-in review step recaps everything and gates a Finish button. On finish, `execute` writes the records
|
|
13
|
+
The user lands on the first step, fills it in, clicks Next, and walks through the flow. Branching steps appear or disappear based on earlier answers. A built-in review step recaps everything and gates a Finish button. On finish, `execute` writes the records, atomically by default.
|
|
14
14
|
|
|
15
|
-
## Prerequisites
|
|
15
|
+
## Prerequisites: enable the subsystem
|
|
16
16
|
|
|
17
17
|
Wizards are core code, but the storage table is **opt-in** so apps that don't use wizards stay schema-clean. Enable it in your Plutonium initializer:
|
|
18
18
|
|
|
@@ -24,7 +24,7 @@ Plutonium.configure do |config|
|
|
|
24
24
|
end
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Then run the migration (it ships in the gem and runs in place
|
|
27
|
+
Then run the migration (it ships in the gem and runs in place, no copy step):
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
rails db:migrate
|
|
@@ -68,28 +68,28 @@ class CompanyOnboardingWizard < Plutonium::Wizard::Base
|
|
|
68
68
|
end
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
- A wizard is a plain class
|
|
71
|
+
- A wizard is a plain class, `< Plutonium::Wizard::Base`. There is no generator (just like interactions); author it by hand.
|
|
72
72
|
- `presents label:/icon:` sets the launch button's label and icon, exactly like interactions; an optional `description:` renders as the wizard's header subheading.
|
|
73
73
|
- Each `step :key, label: do ... end` is one screen. Inside the block, declare its fields with the same DSL you use on a definition or interaction.
|
|
74
74
|
- `data` is **step-keyed**: `data.company.name` reads the **typed** value entered on the `:company` step (cast to the declared type), available from any step and from `execute`. Each step has its own sub-object, so two steps may use the same field name without colliding.
|
|
75
75
|
- `review` is a built-in terminal step (auto-summary + gated Finish). It must be **last**.
|
|
76
|
-
- `execute` runs once at the end and returns an `Outcome` (`succeed(...)` / `failed(...)`). **Use bang methods** (`create!`/`update!`)
|
|
76
|
+
- `execute` runs once at the end and returns an `Outcome` (`succeed(...)` / `failed(...)`). **Use bang methods** (`create!`/`update!`); failure is signalled by a raised exception, never a return value.
|
|
77
77
|
|
|
78
78
|
::: warning Use bang methods in `execute`
|
|
79
|
-
The engine detects failure by a **raised exception**. Non-bang `create`/`save`/`update` return `false` on failure without raising
|
|
79
|
+
The engine detects failure by a **raised exception**. Non-bang `create`/`save`/`update` return `false` on failure without raising; the engine can't see that, treats the step as successful, and advances, silently losing the data. Always use `create!`/`update!`/`save!`, or call `fail!("message")`.
|
|
80
80
|
:::
|
|
81
81
|
|
|
82
82
|
::: tip `execute` is a presentation boundary, same as an interaction's
|
|
83
|
-
A wizard is built with `view_context:` too, so everything reachable only through `execute` is reachable only from a wizard run. Steps and `execute` own the *flow
|
|
83
|
+
A wizard is built with `view_context:` too, so everything reachable only through `execute` is reachable only from a wizard run. Steps and `execute` own the *flow*: which screens, in what order, writing what. What the write **means** belongs on the model as soon as a second caller wants it: the API signup that skips onboarding, the admin backfill, the importer. `Company.create!(...)` inline is fine for a one-off; `Company.onboard!(...)` is what you reach for when it isn't. See [Interactions › What an interaction is for](/reference/behavior/interactions#what-an-interaction-is-for), the rule is identical.
|
|
84
84
|
:::
|
|
85
85
|
|
|
86
86
|
Each step renders as a focused card with a numbered stepper rail (the terminal `review` shows a finish flag, not a number) and a Back / Next / Cancel strip:
|
|
87
87
|
|
|
88
|
-

|
|
89
89
|
|
|
90
90
|
## Branching with `condition:`
|
|
91
91
|
|
|
92
|
-
A step's `condition:` lambda decides whether the step is included. Branching is **subtractive
|
|
92
|
+
A step's `condition:` lambda decides whether the step is included. Branching is **subtractive**: a falsy `condition:` removes the step from the visible path.
|
|
93
93
|
|
|
94
94
|
```ruby
|
|
95
95
|
step :plan, label: "Plan" do
|
|
@@ -107,17 +107,17 @@ end
|
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
::: warning `condition:` lambdas must be nil-safe
|
|
110
|
-
A `condition:` runs against the typed `data` snapshot at **every** transition
|
|
110
|
+
A `condition:` runs against the typed `data` snapshot at **every** transition, including before its deciding step has been filled, when the value is still `nil`. `-> { data.plan.plan == "pro" }` is fine (`nil == "pro"` is `false`); `-> { data.plan.plan.upcase == "PRO" }` raises on `nil`. Always write conditions that tolerate `nil`.
|
|
111
111
|
:::
|
|
112
112
|
|
|
113
113
|
The condition can also read `anchor` (for [anchored wizards](#anchored-wizards)). Data belonging to branch-hidden steps is pruned before `execute`, so `execute` only ever sees data for steps that actually applied.
|
|
114
114
|
|
|
115
|
-
## Reusing a model's fields
|
|
115
|
+
## Reusing a model's fields: `using:`
|
|
116
116
|
|
|
117
117
|
Instead of re-declaring fields a model already defines, import them with `using:`. It is a **step option** (not a block method), and it targets a **model (record class) only**.
|
|
118
118
|
|
|
119
119
|
```ruby
|
|
120
|
-
# Whole-step import
|
|
120
|
+
# Whole-step import, no block needed.
|
|
121
121
|
step :branding, label: "Branding", using: Company, fields: %i[logo brand_color]
|
|
122
122
|
|
|
123
123
|
# Mix imported + wizard-local fields: using: plus a block for the extras.
|
|
@@ -130,13 +130,13 @@ end
|
|
|
130
130
|
What `using:` imports from the model:
|
|
131
131
|
|
|
132
132
|
- **Field universe + types** from `Model.attribute_names` / `Model.attribute_types`. Selectors `fields:` (alias `only:`) and `except:` pick a subset.
|
|
133
|
-
- **Input styling** overlaid from the auto-resolved `<Model>Definition` (its `as:`, options, labels)
|
|
133
|
+
- **Input styling** overlaid from the auto-resolved `<Model>Definition` (its `as:`, options, labels), best-effort; no definition found is fine.
|
|
134
134
|
- **Validations** run via a transient `Model.new(slice).valid?`, keeping errors on the imported fields plus `:base`. Pass `validate: false` to skip and write your own inline `validates`.
|
|
135
135
|
- **`form_layout`** inherited from the `<Model>Definition` (filtered to imported fields). Pass `layout: false` to opt out.
|
|
136
136
|
|
|
137
|
-
`using:` is **declaration reuse only
|
|
137
|
+
`using:` is **declaration reuse only**: it never pulls in the model's persistence or callbacks. Data still stages into `data`; your `execute` does the writes. Full detail: [DSL reference › `using:`](/reference/wizard/dsl#using-a-model).
|
|
138
138
|
|
|
139
|
-
## Sectioning a step
|
|
139
|
+
## Sectioning a step: `form_layout`
|
|
140
140
|
|
|
141
141
|
A step is its own form, so you can group its fields with the same `form_layout` DSL you use on a definition, scoped to that step:
|
|
142
142
|
|
|
@@ -175,11 +175,11 @@ end
|
|
|
175
175
|
|
|
176
176
|
Repeater rows rehydrate from staged `data` on GET, so navigating back (or resuming) re-renders the rows you already filled.
|
|
177
177
|
|
|
178
|
-

|
|
179
179
|
|
|
180
180
|
## File uploads (attachments)
|
|
181
181
|
|
|
182
|
-
A step can collect a file. You declare it like any other field
|
|
182
|
+
A step can collect a file. You declare it like any other field: a **`:string`** attribute (it holds the upload **token**, not the bytes) plus a file input:
|
|
183
183
|
|
|
184
184
|
```ruby
|
|
185
185
|
step :photo, label: "Photo" do
|
|
@@ -188,7 +188,7 @@ step :photo, label: "Photo" do
|
|
|
188
188
|
end
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
-
A wizard stages its `data` as JSON across several requests, so a file can't ride along
|
|
191
|
+
A wizard stages its `data` as JSON across several requests, so a file can't ride along, only a **token** does. The field stages the backend's upload token (an ActiveStorage signed_id, or active_shrine/Shrine cached-file data); your `execute` assigns that token to the model's attachment, which both backends accept natively:
|
|
192
192
|
|
|
193
193
|
```ruby
|
|
194
194
|
def execute
|
|
@@ -199,19 +199,19 @@ def execute
|
|
|
199
199
|
end
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
-
The review summary and the step's preview (when you go Back or resume) render the file for you
|
|
202
|
+
The review summary and the step's preview (when you go Back or resume) render the file for you; reading `data.photo.photo` resolves the token to a displayable attachment automatically.
|
|
203
203
|
|
|
204
204
|
### Server-side vs direct upload
|
|
205
205
|
|
|
206
206
|
The same field works two ways:
|
|
207
207
|
|
|
208
|
-
- **Server-side (default)
|
|
209
|
-
- **Direct upload
|
|
208
|
+
- **Server-side (default)**: `input :photo, as: :file`. The file is submitted with the step (a plain file input) and the wizard uploads it to the backend's cache while staging. Nothing else to wire up; works for both ActiveStorage and active_shrine.
|
|
209
|
+
- **Direct upload**: `input :photo, as: :uppy, direct_upload: true, endpoint: "/upload"`. The browser uploads straight to the endpoint (with a progress UI) and posts back a token. Use this for large files or an async UX; it needs the backend's direct-upload endpoint reachable (ActiveStorage's direct uploads, or Shrine's `upload_endpoint`).
|
|
210
210
|
|
|
211
211
|
::: tip Match the backend to the model
|
|
212
|
-
In server-side mode the backend defaults to `config.wizards.attachment_backend
|
|
212
|
+
In server-side mode the backend defaults to `config.wizards.attachment_backend`, auto-detected as Shrine when active_shrine is installed, else ActiveStorage. Override per field with `backend:` (`input :photo, as: :file, backend: :active_storage`). It must match the model your `execute` assigns to: an ActiveStorage model can't accept a Shrine token, and vice-versa.
|
|
213
213
|
|
|
214
|
-
For Shrine, you can also cache through a specific uploader
|
|
214
|
+
For Shrine, you can also cache through a specific uploader, `input :photo, as: :file, backend: :shrine, uploader: PhotoUploader`, so that uploader's cache-stage plugins (mime/dimension extraction, `generate_location`, processing) run while staging. The minted token stays uploader-agnostic, so display and `execute` promotion are unchanged. That uploader's **validations are enforced on the step** too: a file that violates them is rejected right there with a field error (validated against the field's effective uploader, its `uploader:`, or base `Shrine`), rather than slipping through to `execute`.
|
|
215
215
|
:::
|
|
216
216
|
|
|
217
217
|
For **multiple** files, use an array attribute with `multiple: true`; the staged value is then an array of tokens. A staged-but-abandoned upload (cancel/sweep) is an unattached blob / cached file that each storage backend's own cleanup reaps.
|
|
@@ -224,7 +224,7 @@ For **multiple** files, use an array attribute with `multiple: true`; the staged
|
|
|
224
224
|
- Lists invalid/unvisited visible steps as "fix this" jump links.
|
|
225
225
|
- Disables Finish until all visible steps are valid; clicking it runs `execute`.
|
|
226
226
|
|
|
227
|
-

|
|
228
228
|
|
|
229
229
|
```ruby
|
|
230
230
|
review label: "Review & submit"
|
|
@@ -245,9 +245,9 @@ review summary: false, header: false # no header, no summary → "ready to com
|
|
|
245
245
|
|
|
246
246
|
See the [DSL reference](/reference/wizard/dsl#review) for the complete state table.
|
|
247
247
|
|
|
248
|
-
## Per-step writes
|
|
248
|
+
## Per-step writes: `on_submit` / `persist` / `on_rollback`
|
|
249
249
|
|
|
250
|
-
`execute` is the default
|
|
250
|
+
`execute` is the default: atomic, no orphans. Reach for per-step `on_submit` **only** when a real record must exist mid-flow (handing off to an external system that webhooks back, a reviewer who must see partial data, a payload too large for the session row).
|
|
251
251
|
|
|
252
252
|
```ruby
|
|
253
253
|
class ConfigureCompanyWizard < Plutonium::Wizard::Base
|
|
@@ -268,7 +268,7 @@ class ConfigureCompanyWizard < Plutonium::Wizard::Base
|
|
|
268
268
|
end
|
|
269
269
|
|
|
270
270
|
# ADDITIONAL cleanup on Cancel/abandonment. The engine ALWAYS destroys the
|
|
271
|
-
# persist'd Billing record
|
|
271
|
+
# persist'd Billing record; on_rollback is only for side effects it can't see
|
|
272
272
|
# (here, refunding the external charge). It runs BEFORE the destroy, so
|
|
273
273
|
# persisted[:billing] is still alive to read.
|
|
274
274
|
on_rollback { PaymentApi.refund!(persisted[:billing].charge_id) }
|
|
@@ -281,11 +281,11 @@ class ConfigureCompanyWizard < Plutonium::Wizard::Base
|
|
|
281
281
|
end
|
|
282
282
|
```
|
|
283
283
|
|
|
284
|
-
- `on_submit` runs in its own transaction when the step completes. Inside it, `persist record` registers record(s) the engine tracks for resume and cleanup
|
|
284
|
+
- `on_submit` runs in its own transaction when the step completes. Inside it, `persist record` registers record(s) the engine tracks for resume and cleanup, reachable later as `persisted[:step_key]`.
|
|
285
285
|
- `fail!("msg")` aborts the step with a base (form-level) error; `fail!(:field, "msg")` attaches it to a field. Both roll back the step's transaction and re-render with input intact.
|
|
286
286
|
- The engine **always** destroys every `persist`'d record on rollback (Cancel, abandonment-sweep, branch-prune), in reverse order, via `destroy!` (which respects a model's own soft-delete override). `on_rollback` is an **optional, additive** compensating block for side effects the engine can't see (refund a charge, call an external API), and runs **before** the destroy, so `persisted[:key]` is still alive inside it. Don't destroy the tracked record yourself; the engine does.
|
|
287
|
-
- Because `on_submit` writes mid-flow, it isn't atomic across steps
|
|
288
|
-
- `on_submit` / `on_rollback` are **wizard-flow hooks**, not a home for domain logic. They belong to a single wizard step and can't be called from anywhere else, so keep them to *when* and *what gets tracked
|
|
287
|
+
- Because `on_submit` writes mid-flow, it isn't atomic across steps; that's why `cleanup_after` + the SweepJob exist. See [Storage & config](/reference/wizard/storage-config) and the [DSL reference](/reference/wizard/dsl#per-step-hooks).
|
|
288
|
+
- `on_submit` / `on_rollback` are **wizard-flow hooks**, not a home for domain logic. They belong to a single wizard step and can't be called from anywhere else, so keep them to *when* and *what gets tracked*: `persist Billing.create!(...)` above is one call to a model. Once "authorize a card and record the billing row" is something the API also does, it becomes `Billing.authorize!(company:, token:)` and the hook shrinks to `persist Billing.authorize!(...)`.
|
|
289
289
|
|
|
290
290
|
## Anchored wizards
|
|
291
291
|
|
|
@@ -305,7 +305,7 @@ end
|
|
|
305
305
|
```
|
|
306
306
|
|
|
307
307
|
- `anchored with: Company` → a single type. `anchored with: [Company, Organization]` → polymorphic. `anchored` (no `with:`) → generic, bound at registration.
|
|
308
|
-
- `anchor` raises `Plutonium::Wizard::NotAnchoredError` if the wizard wasn't declared `anchored
|
|
308
|
+
- `anchor` raises `Plutonium::Wizard::NotAnchoredError` if the wizard wasn't declared `anchored`; it never returns `nil`.
|
|
309
309
|
- Omit `anchored` for a pure create flow (the wizard creates the records it names itself).
|
|
310
310
|
|
|
311
311
|
See [Anchoring & resume](/reference/wizard/anchoring-resume).
|
|
@@ -332,12 +332,12 @@ class WelcomeWizard < Plutonium::Wizard::Base
|
|
|
332
332
|
def execute
|
|
333
333
|
# User#complete_onboarding! sets the name and stamps onboarded_at. It lives on
|
|
334
334
|
# the model because the gate below, an admin backfill, and the invite-accept
|
|
335
|
-
# flow all need to mark a user onboarded
|
|
335
|
+
# flow all need to mark a user onboarded, and none of them is a wizard.
|
|
336
336
|
current_user.complete_onboarding!(full_name: data.profile.full_name)
|
|
337
337
|
succeed.with_message("Welcome aboard!")
|
|
338
338
|
end
|
|
339
339
|
|
|
340
|
-
# Standalone wizards have no resource policy
|
|
340
|
+
# Standalone wizards have no resource policy; gate entry with `authorize?`.
|
|
341
341
|
def authorize?
|
|
342
342
|
current_user.present?
|
|
343
343
|
end
|
|
@@ -357,15 +357,15 @@ end
|
|
|
357
357
|
|
|
358
358
|
An un-completed user hitting the gate is redirected into the wizard (their destination stashed); on completion they're bounced back (PRG). Completed users pass straight through. Re-opening a finished one-time wizard renders an "already completed" page (override its body with a `completed do |wizard| … end` block) rather than re-running it:
|
|
359
359
|
|
|
360
|
-

|
|
361
361
|
|
|
362
362
|
See [One-time wizards](/reference/wizard/one-time).
|
|
363
363
|
|
|
364
364
|
## Registration & launch
|
|
365
365
|
|
|
366
|
-
A wizard reaches a user as a **resource action** (the `wizard` macro) or a **route-mounted entry** (`register_wizard`)
|
|
366
|
+
A wizard reaches a user as a **resource action** (the `wizard` macro) or a **route-mounted entry** (`register_wizard`), inside a portal or on the main app. A portal mount inherits the portal's auth, tenant scoping, layout, and rendering; a main-app mount runs standalone.
|
|
367
367
|
|
|
368
|
-
### On a resource
|
|
368
|
+
### On a resource: the `wizard` macro
|
|
369
369
|
|
|
370
370
|
Register a wizard on a resource definition. Placement follows `anchored?` automatically: an anchored wizard becomes a **record** action (the show page *and* each index row, like `edit`/`destroy`); a non-anchored wizard becomes a collection-level **resource** action.
|
|
371
371
|
|
|
@@ -376,13 +376,13 @@ class CompanyDefinition < Plutonium::Resource::Definition
|
|
|
376
376
|
end
|
|
377
377
|
```
|
|
378
378
|
|
|
379
|
-

|
|
380
380
|
|
|
381
|
-
The anchor resolves through the resource controller's scoped, policy-gated `resource_record!` (IDOR-safe
|
|
381
|
+
The anchor resolves through the resource controller's scoped, policy-gated `resource_record!` (IDOR-safe: an out-of-scope or missing id 404s), and the action is gated by a policy predicate named after the wizard key (`def configure? = update?`). For placement flags, routes, and the full option list see [Registration & launch › the `wizard` macro](/reference/wizard/registration-launch#on-a-resource-the-wizard-macro).
|
|
382
382
|
|
|
383
|
-
### Route-mounted
|
|
383
|
+
### Route-mounted: `register_wizard`
|
|
384
384
|
|
|
385
|
-
For a wizard not tied to a single resource (onboarding, welcome, set-up), mount it alongside `register_resource
|
|
385
|
+
For a wizard not tied to a single resource (onboarding, welcome, set-up), mount it alongside `register_resource`, in a portal engine's routes or on the main app:
|
|
386
386
|
|
|
387
387
|
```ruby
|
|
388
388
|
# packages/admin_portal/config/routes.rb
|
|
@@ -394,17 +394,17 @@ end
|
|
|
394
394
|
|
|
395
395
|
This draws the step routes within the host and gives you an `onboarding_wizard_path` helper. See [Registration & launch › `register_wizard`](/reference/wizard/registration-launch#route-mounted-register_wizard) for `at:`/`as:`/`public:`/`layout:`, the per-host layout defaults, and the controller override hook.
|
|
396
396
|
|
|
397
|
-
::: danger
|
|
398
|
-
A `register_wizard` wizard has no resource policy and
|
|
397
|
+
::: danger `register_wizard` wizards are open to any authenticated user by default
|
|
398
|
+
A `register_wizard` wizard (portal or main-app) has no resource policy and its default is `def authorize? = true`, so any authenticated user of that host can run it. Define `def authorize?` on every `register_wizard` wizard, including a gated `one_time` one (the gate forces users into the wizard but doesn't decide who may run it). Resource-mounted wizards are also gated by their action's policy predicate.
|
|
399
399
|
:::
|
|
400
400
|
|
|
401
401
|
::: tip Authenticated main-app wizards: define your own controller
|
|
402
|
-
A portal mount inherits the portal's auth; a bare main-app mount has no `current_user`. An authenticated main-app wizard therefore needs you to define `::WizardsController` yourself (`include Plutonium::Wizard::Controller` + your auth concern)
|
|
402
|
+
A portal mount inherits the portal's auth; a bare main-app mount has no `current_user`. An authenticated main-app wizard therefore needs you to define `::WizardsController` yourself (`include Plutonium::Wizard::Controller` + your auth concern), the same "app owns the controller" contract as `register_resource`. See [Hosting & the controller override hook](/reference/wizard/registration-launch#hosting-the-controller-override-hook).
|
|
403
403
|
:::
|
|
404
404
|
|
|
405
405
|
### Guest (unauthenticated) wizards
|
|
406
406
|
|
|
407
|
-
Wizards require authentication by default
|
|
407
|
+
Wizards require authentication by default, and every resume is **owner-scoped**, so a run id leaked in a URL can't be picked up by another user. Opt into pre-login access with the `anonymous` macro and mount it `public: true` (the default for `anonymous`). A guest run's identity is a server-minted id held in the **Rails session** (never a URL, no leak surface); it may authenticate only at its terminal `execute` (e.g. a signup that creates the account and logs in):
|
|
408
408
|
|
|
409
409
|
```ruby
|
|
410
410
|
class GuestSignupWizard < Plutonium::Wizard::Base
|
|
@@ -429,7 +429,7 @@ Full detail (owner-scoping, session-keying, the synthesized public controller):
|
|
|
429
429
|
|
|
430
430
|
### Listing in-progress & resume-or-new
|
|
431
431
|
|
|
432
|
-
Build a "continue where you left off" dashboard with `Plutonium::Wizard.in_progress_for(view_context)
|
|
432
|
+
Build a "continue where you left off" dashboard with `Plutonium::Wizard.in_progress_for(view_context)`, it derives the owner, tenant scope, and portal from the view context and returns that user's in-progress runs for the current portal, each carrying `label` / `icon` / `current_step` / `updated_at` / `resume_url`:
|
|
433
433
|
|
|
434
434
|
```ruby
|
|
435
435
|
Plutonium::Wizard.in_progress_for(view_context).each do |entry|
|
|
@@ -440,16 +440,16 @@ end
|
|
|
440
440
|
Plutonium::Wizard.in_progress_for(view_context, wizard: ConfigureCompanyWizard, anchor: @company).first
|
|
441
441
|
```
|
|
442
442
|
|
|
443
|
-
A **tokened** wizard (no `concurrency_key`) doesn't silently fork on relaunch
|
|
443
|
+
A **tokened** wizard (no `concurrency_key`) doesn't silently fork on relaunch; by default it shows a resume-or-new chooser when a pending run exists (`on_relaunch :new` opts out). Keyed and guest wizards auto-resume their single run.
|
|
444
444
|
|
|
445
|
-

|
|
446
446
|
|
|
447
447
|
See [Anchoring & resume › Listing](/reference/wizard/anchoring-resume#listing-in-progress-wizards) for the full entry fields, portal-scoping rules, `resume_unresolved_reason`, and the filter performance notes.
|
|
448
448
|
|
|
449
449
|
## Where to go next
|
|
450
450
|
|
|
451
|
-
- [DSL reference](/reference/wizard/dsl)
|
|
452
|
-
- [Anchoring & resume](/reference/wizard/anchoring-resume)
|
|
453
|
-
- [Storage & config](/reference/wizard/storage-config)
|
|
454
|
-
- [Registration & launch](/reference/wizard/registration-launch)
|
|
455
|
-
- [One-time wizards](/reference/wizard/one-time)
|
|
451
|
+
- [DSL reference](/reference/wizard/dsl): every macro and accessor.
|
|
452
|
+
- [Anchoring & resume](/reference/wizard/anchoring-resume): anchors, instance keys, resume.
|
|
453
|
+
- [Storage & config](/reference/wizard/storage-config): the table, config, encryption, the sweep.
|
|
454
|
+
- [Registration & launch](/reference/wizard/registration-launch): the `wizard` macro, `register_wizard`, routes.
|
|
455
|
+
- [One-time wizards](/reference/wizard/one-time): completion markers + the gate.
|
|
@@ -15,7 +15,7 @@ Plutonium's `pu:*` CLI generators. Discoverable via `rails g pu:<tab>`. Always p
|
|
|
15
15
|
| [`pu:rodauth:install`](#pu-rodauth-install) | Install Rodauth base |
|
|
16
16
|
| [`pu:rodauth:account`](#pu-rodauth-account) | Basic Rodauth account |
|
|
17
17
|
| [`pu:rodauth:admin`](#pu-rodauth-admin) | Hardened admin account (2FA, lockout, audit) |
|
|
18
|
-
| [`pu:saas:setup`](#pu-saas-setup) | **Meta
|
|
18
|
+
| [`pu:saas:setup`](#pu-saas-setup) | **Meta**: user + entity + membership + portal + profile + welcome + invites |
|
|
19
19
|
| [`pu:saas:user`](#individual-saas-generators) | Individual: SaaS user account |
|
|
20
20
|
| [`pu:saas:entity`](#individual-saas-generators) | Individual: entity model |
|
|
21
21
|
| [`pu:saas:membership`](#individual-saas-generators) | Individual: membership join model |
|
|
@@ -23,7 +23,7 @@ Plutonium's `pu:*` CLI generators. Discoverable via `rails g pu:<tab>`. Always p
|
|
|
23
23
|
| [`pu:saas:welcome`](#individual-saas-generators) | Individual: onboarding / select-entity flow |
|
|
24
24
|
| [`pu:saas:api_client`](#pu-saas-api-client) | API client for M2M auth |
|
|
25
25
|
| [`pu:profile:install`](#pu-profile-install) | Profile resource + security section |
|
|
26
|
-
| [`pu:profile:setup`](#pu-profile-setup) | Meta
|
|
26
|
+
| [`pu:profile:setup`](#pu-profile-setup) | Meta: `pu:profile:install` + `pu:profile:conn` |
|
|
27
27
|
| [`pu:profile:conn`](#pu-profile-conn) | Connect profile to a portal as a singular resource |
|
|
28
28
|
| [`pu:invites:install`](#pu-invites-install) | User invitations package |
|
|
29
29
|
| [`pu:invites:invitable`](#pu-invites-invitable) | Mark a model as invitable |
|
|
@@ -52,11 +52,11 @@ rails g pu:res:scaffold Post user:belongs_to title:string 'content:text?' --dest
|
|
|
52
52
|
|
|
53
53
|
| Option | Description |
|
|
54
54
|
|---|---|
|
|
55
|
-
| `--dest=NAME` | Destination package (`main_app` or `<package>`)
|
|
55
|
+
| `--dest=NAME` | Destination package (`main_app` or `<package>`), required for unattended runs |
|
|
56
56
|
| `--no-model` | Skip model file (for existing models) |
|
|
57
57
|
| `--no-migration` | Skip migration (use with `--no-model` for existing schema) |
|
|
58
58
|
|
|
59
|
-
Field type syntax
|
|
59
|
+
Field type syntax: full reference in [Resource › Model](/reference/resource/model). Quick recap:
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
62
|
'name:string' # required string
|
|
@@ -98,7 +98,7 @@ See [Portals › Connecting resources](./portals#connecting-resources-pu-res-con
|
|
|
98
98
|
|
|
99
99
|
### `pu:pkg:package`
|
|
100
100
|
|
|
101
|
-
Feature package
|
|
101
|
+
Feature package: models, policies, definitions, interactions.
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
104
|
rails g pu:pkg:package blogging
|
|
@@ -108,7 +108,7 @@ See [Packages › Feature packages](./packages#feature-packages).
|
|
|
108
108
|
|
|
109
109
|
### `pu:pkg:portal`
|
|
110
110
|
|
|
111
|
-
Portal package
|
|
111
|
+
Portal package: controllers, views, routes, auth.
|
|
112
112
|
|
|
113
113
|
```bash
|
|
114
114
|
rails g pu:pkg:portal admin --auth=user
|
|
@@ -122,7 +122,7 @@ rails g pu:pkg:portal admin --auth=admin --scope=Organization
|
|
|
122
122
|
| `--byo` | Bring your own auth |
|
|
123
123
|
| `--scope=CLASS` | Entity class for multi-tenancy |
|
|
124
124
|
|
|
125
|
-
See [Portals ›
|
|
125
|
+
The generator also mounts the engine at `/<name>` in `packages/<name>_portal/config/routes.rb`. Don't mount it again in `config/routes.rb`; edit `at:` there to change the path. See [Portals › Mounting](./portals#mounting).
|
|
126
126
|
|
|
127
127
|
---
|
|
128
128
|
|
|
@@ -130,7 +130,7 @@ See [Portals › Creating a portal](./portals#creating-a-portal).
|
|
|
130
130
|
|
|
131
131
|
### `pu:rodauth:install`
|
|
132
132
|
|
|
133
|
-
Install the Rodauth base
|
|
133
|
+
Install the Rodauth base: Roda app, base plugin, controller, layout, PostgreSQL extension migration.
|
|
134
134
|
|
|
135
135
|
```bash
|
|
136
136
|
rails g pu:rodauth:install
|
|
@@ -151,7 +151,7 @@ For full option tables (features, defaults, individual feature flags) see [Auth
|
|
|
151
151
|
|
|
152
152
|
### `pu:rodauth:admin`
|
|
153
153
|
|
|
154
|
-
Hardened admin account
|
|
154
|
+
Hardened admin account: pre-configured with multi-phase login, required TOTP, recovery codes, lockout, active sessions, audit logging, role-based access, invite interaction, and **no public signup**.
|
|
155
155
|
|
|
156
156
|
```bash
|
|
157
157
|
rails g pu:rodauth:admin admin
|
|
@@ -196,7 +196,7 @@ rails g pu:saas:setup --user Customer --entity Organization \
|
|
|
196
196
|
| `--user=NAME` | (required) | User account model name |
|
|
197
197
|
| `--entity=NAME` | (required) | Entity model name |
|
|
198
198
|
| `--allow-signup` | `true` | Allow public registration |
|
|
199
|
-
| `--roles` | `admin,member` | Additional roles
|
|
199
|
+
| `--roles` | `admin,member` | Additional roles: **`owner` always prepended as index 0** |
|
|
200
200
|
| `--skip-entity` | | Skip entity model generation |
|
|
201
201
|
| `--skip-membership` | | Skip membership model generation |
|
|
202
202
|
| `--user-attributes` | | Additional user model attributes |
|
|
@@ -271,7 +271,7 @@ rails g pu:profile:install AccountSettings bio:text --dest=main_app # custom r
|
|
|
271
271
|
|
|
272
272
|
### `pu:profile:setup`
|
|
273
273
|
|
|
274
|
-
Meta
|
|
274
|
+
Meta: runs `pu:profile:install` + `pu:profile:conn` in one shot.
|
|
275
275
|
|
|
276
276
|
```bash
|
|
277
277
|
rails g pu:profile:setup date_of_birth:date bio:text \
|
|
@@ -281,7 +281,7 @@ rails g pu:profile:setup date_of_birth:date bio:text \
|
|
|
281
281
|
|
|
282
282
|
### `pu:profile:conn`
|
|
283
283
|
|
|
284
|
-
Connect the profile resource to a portal as a **singular** resource (registers `/profile` and the `profile_url` helper).
|
|
284
|
+
Connect the profile resource to a portal as a **singular** resource (registers `/profile` and overrides the `profile_url` helper in the portal's controller concern). It also generates a user-scoped portal policy (`create? = user.profile.nil?`, `update? = true`, `destroy? = false`) and a `ShowPage` that renders the `SecuritySection`. See [Auth › Profile](/reference/auth/profile#what-pu-profile-conn-generates).
|
|
285
285
|
|
|
286
286
|
```bash
|
|
287
287
|
rails g pu:profile:conn --dest=customer_portal
|
|
@@ -310,7 +310,7 @@ rails g pu:invites:install --entity-model=Organization --user-model=Customer --i
|
|
|
310
310
|
| `--enforce-domain` | `false` | Require email domain to match entity |
|
|
311
311
|
| `--dest=PACKAGE` | `main_app` | Package where the entity model lives (controls where `invite_user_interaction.rb` is generated) |
|
|
312
312
|
|
|
313
|
-
Multiple invite flows are supported
|
|
313
|
+
Multiple invite flows are supported; run `pu:invites:install` once per flow.
|
|
314
314
|
|
|
315
315
|
### `pu:invites:invitable`
|
|
316
316
|
|
|
@@ -398,7 +398,7 @@ rails g pu:test:install
|
|
|
398
398
|
|
|
399
399
|
### `pu:test:scaffold`
|
|
400
400
|
|
|
401
|
-
Scaffold integration tests
|
|
401
|
+
Scaffold integration tests: one file per (resource × portal) pairing.
|
|
402
402
|
|
|
403
403
|
```bash
|
|
404
404
|
rails g pu:test:scaffold Blogging::Post --portals=admin,org
|
|
@@ -432,7 +432,7 @@ rails g pu:skills:sync
|
|
|
432
432
|
### Full app setup
|
|
433
433
|
|
|
434
434
|
```bash
|
|
435
|
-
# 1. Plutonium template (greenfield)
|
|
435
|
+
# 1. Plutonium template (greenfield): does all initial setup
|
|
436
436
|
rails new myapp -a propshaft -j esbuild -c tailwind \
|
|
437
437
|
-m https://radioactive-labs.github.io/plutonium-core/templates/plutonium.rb
|
|
438
438
|
|
|
@@ -505,13 +505,13 @@ Generators run from Rails root. Package names are case-sensitive.
|
|
|
505
505
|
|
|
506
506
|
### Migration already exists
|
|
507
507
|
|
|
508
|
-
If a migration with the same timestamp exists, wait a second and retry
|
|
508
|
+
If a migration with the same timestamp exists, wait a second and retry; Rails generates timestamps to one-second resolution.
|
|
509
509
|
|
|
510
510
|
## Related
|
|
511
511
|
|
|
512
|
-
- [Packages](./packages)
|
|
513
|
-
- [Portals](./portals)
|
|
514
|
-
- [Resource › Model](/reference/resource/model)
|
|
515
|
-
- [Auth](/reference/auth/)
|
|
516
|
-
- [Tenancy](/reference/tenancy/)
|
|
517
|
-
- [Testing](/reference/testing/)
|
|
512
|
+
- [Packages](./packages): feature vs portal package structure
|
|
513
|
+
- [Portals](./portals): portal configuration and resource connection
|
|
514
|
+
- [Resource › Model](/reference/resource/model): field-type syntax for `pu:res:scaffold`
|
|
515
|
+
- [Auth](/reference/auth/): account type configuration
|
|
516
|
+
- [Tenancy](/reference/tenancy/): multi-tenancy and invitations
|
|
517
|
+
- [Testing](/reference/testing/): test scaffolding
|