plutonium 0.50.0 → 0.51.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 +85 -102
- data/.claude/skills/plutonium-app/SKILL.md +572 -0
- data/.claude/skills/plutonium-auth/SKILL.md +163 -300
- data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
- data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
- data/.claude/skills/plutonium-testing/SKILL.md +6 -5
- data/.claude/skills/plutonium-ui/SKILL.md +900 -0
- data/CHANGELOG.md +27 -2
- data/Rakefile +2 -1
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1009 -1214
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +52 -51
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/.vitepress/config.ts +37 -27
- data/docs/getting-started/index.md +22 -29
- data/docs/getting-started/installation.md +37 -80
- data/docs/getting-started/tutorial/index.md +4 -5
- data/docs/guides/adding-resources.md +66 -377
- data/docs/guides/authentication.md +94 -463
- data/docs/guides/authorization.md +124 -370
- data/docs/guides/creating-packages.md +94 -296
- data/docs/guides/custom-actions.md +121 -441
- data/docs/guides/index.md +22 -42
- data/docs/guides/multi-tenancy.md +116 -187
- data/docs/guides/nested-resources.md +103 -431
- data/docs/guides/search-filtering.md +123 -240
- data/docs/guides/testing.md +5 -4
- data/docs/guides/theming.md +157 -407
- data/docs/guides/troubleshooting.md +5 -3
- data/docs/guides/user-invites.md +106 -425
- data/docs/guides/user-profile.md +76 -243
- data/docs/index.md +1 -1
- data/docs/reference/app/generators.md +517 -0
- data/docs/reference/app/index.md +158 -0
- data/docs/reference/app/packages.md +146 -0
- data/docs/reference/app/portals.md +377 -0
- data/docs/reference/auth/accounts.md +230 -0
- data/docs/reference/auth/index.md +88 -0
- data/docs/reference/auth/profile.md +185 -0
- data/docs/reference/behavior/controllers.md +395 -0
- data/docs/reference/behavior/index.md +22 -0
- data/docs/reference/behavior/interactions.md +341 -0
- data/docs/reference/behavior/policies.md +417 -0
- data/docs/reference/index.md +56 -49
- data/docs/reference/resource/actions.md +423 -0
- data/docs/reference/resource/definition.md +508 -0
- data/docs/reference/resource/index.md +50 -0
- data/docs/reference/resource/model.md +348 -0
- data/docs/reference/resource/query.md +305 -0
- data/docs/reference/tenancy/entity-scoping.md +361 -0
- data/docs/reference/tenancy/index.md +36 -0
- data/docs/reference/tenancy/invites.md +393 -0
- data/docs/reference/tenancy/nested-resources.md +267 -0
- data/docs/reference/testing/index.md +287 -0
- data/docs/reference/ui/assets.md +400 -0
- data/docs/reference/ui/components.md +165 -0
- data/docs/reference/ui/displays.md +104 -0
- data/docs/reference/ui/forms.md +284 -0
- data/docs/reference/ui/index.md +30 -0
- data/docs/reference/ui/layouts.md +106 -0
- data/docs/reference/ui/pages.md +189 -0
- data/docs/reference/ui/tables.md +117 -0
- data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
- data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
- data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +0 -20
- data/lib/generators/pu/invites/install_generator.rb +1 -0
- data/lib/plutonium/definition/base.rb +1 -1
- data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
- data/lib/plutonium/helpers/turbo_helper.rb +11 -0
- data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
- data/lib/plutonium/resource/controller.rb +1 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
- data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
- data/lib/plutonium/resource/policy.rb +7 -0
- data/lib/plutonium/routing/mapper_extensions.rb +15 -0
- data/lib/plutonium/ui/component/methods.rb +4 -0
- data/lib/plutonium/ui/form/base.rb +6 -2
- data/lib/plutonium/ui/form/components/json.rb +58 -0
- data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
- data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
- data/lib/plutonium/ui/form/resource.rb +0 -4
- data/lib/plutonium/ui/grid/resource.rb +1 -1
- data/lib/plutonium/ui/layout/base.rb +1 -0
- data/lib/plutonium/ui/page/base.rb +0 -7
- data/lib/plutonium/ui/page/index.rb +4 -4
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium.rb +8 -0
- data/lib/tasks/release.rake +15 -1
- data/package.json +10 -10
- data/src/css/slim_select.css +4 -0
- data/src/js/controllers/slim_select_controller.js +61 -0
- data/src/js/turbo/turbo_actions.js +33 -0
- data/yarn.lock +553 -543
- metadata +44 -33
- data/.claude/skills/plutonium-assets/SKILL.md +0 -512
- data/.claude/skills/plutonium-controller/SKILL.md +0 -396
- data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
- data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
- data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
- data/.claude/skills/plutonium-forms/SKILL.md +0 -465
- data/.claude/skills/plutonium-installation/SKILL.md +0 -331
- data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
- data/.claude/skills/plutonium-invites/SKILL.md +0 -408
- data/.claude/skills/plutonium-model/SKILL.md +0 -440
- data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
- data/.claude/skills/plutonium-package/SKILL.md +0 -198
- data/.claude/skills/plutonium-policy/SKILL.md +0 -456
- data/.claude/skills/plutonium-portal/SKILL.md +0 -410
- data/.claude/skills/plutonium-views/SKILL.md +0 -651
- data/docs/reference/assets/index.md +0 -496
- data/docs/reference/controller/index.md +0 -412
- data/docs/reference/definition/actions.md +0 -462
- data/docs/reference/definition/fields.md +0 -383
- data/docs/reference/definition/index.md +0 -326
- data/docs/reference/definition/query.md +0 -351
- data/docs/reference/generators/index.md +0 -648
- data/docs/reference/interaction/index.md +0 -449
- data/docs/reference/model/features.md +0 -248
- data/docs/reference/model/index.md +0 -218
- data/docs/reference/policy/index.md +0 -456
- data/docs/reference/portal/index.md +0 -379
- data/docs/reference/views/forms.md +0 -411
- data/docs/reference/views/index.md +0 -544
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plutonium-app
|
|
3
|
+
description: Use BEFORE installing Plutonium, creating a portal or feature package, mounting an engine, or registering resources/routes. Covers initial setup, the package system, portal engines, route registration (including singular and custom routes), and resource-to-portal wiring.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Plutonium App — Installation, Packages, Portals, Routes
|
|
7
|
+
|
|
8
|
+
How a Plutonium app is assembled: the install bootstrap, the package system (feature vs portal), portal engines, and the routing surface that exposes resources to the web.
|
|
9
|
+
|
|
10
|
+
For the resources themselves (model + definition + scaffold options), see [[plutonium-resource]]. For controllers/policies/interactions, see [[plutonium-behavior]]. For multi-tenancy, see [[plutonium-tenancy]].
|
|
11
|
+
|
|
12
|
+
## 🚨 Critical (read first)
|
|
13
|
+
|
|
14
|
+
- **Use the generators for everything.** `pu:core:install`, `pu:rodauth:install`, `pu:pkg:portal`, `pu:pkg:package`, `pu:res:scaffold`, `pu:res:conn`. Never hand-write base controllers, engine files, layouts, or portal route registration.
|
|
15
|
+
- **Existing app → `base.rb`. New app → `plutonium.rb`.** The `plutonium.rb` template re-runs full bootstrap (dotenv, annotate, solid_*, asset config) and creates generic "initial commit" commits that clobber history. For any pre-existing app use `base.rb`.
|
|
16
|
+
- **Pass `--dest`, `--auth`, `--force`, `--skip-bundle`** etc. for unattended runs so generators don't block on prompts.
|
|
17
|
+
- **Feature vs portal is a hard split.** Feature packages hold models/policies/definitions/interactions. Portal packages hold controllers/views/routes/auth. Don't mix.
|
|
18
|
+
- **Package classes are auto-namespaced** — `packages/blogging/app/models/blogging/post.rb` → `Blogging::Post`. Don't fight it.
|
|
19
|
+
- **Always connect resources with `pu:res:conn`** — until connected, a resource has no portal routes and is invisible.
|
|
20
|
+
- **For custom routes on a registered resource, pass `as:`** — otherwise `resource_url_for` can't build URLs.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# Part 1 — Installation
|
|
25
|
+
|
|
26
|
+
## Fresh Rails app (recommended)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
rails new myapp -a propshaft -j esbuild -c tailwind \
|
|
30
|
+
-m https://radioactive-labs.github.io/plutonium-core/templates/plutonium.rb
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Configures Rails + Propshaft + esbuild + TailwindCSS + Plutonium in one shot.
|
|
34
|
+
|
|
35
|
+
## Existing Rails app
|
|
36
|
+
|
|
37
|
+
⚠️ Use `base.rb`, **not** `plutonium.rb`.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Option 1 — template
|
|
41
|
+
bin/rails app:template \
|
|
42
|
+
LOCATION=https://radioactive-labs.github.io/plutonium-core/templates/base.rb
|
|
43
|
+
|
|
44
|
+
# Option 2 — manual
|
|
45
|
+
# Add `gem "plutonium"` to Gemfile, then:
|
|
46
|
+
bundle install
|
|
47
|
+
rails generate pu:core:install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Full setup workflow
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# 1. Core install (base controllers/policies/definitions/layouts)
|
|
54
|
+
rails generate pu:core:install
|
|
55
|
+
|
|
56
|
+
# 2. Auth (if needed)
|
|
57
|
+
rails generate pu:rodauth:install
|
|
58
|
+
rails generate pu:rodauth:account user
|
|
59
|
+
|
|
60
|
+
# 3. Portal
|
|
61
|
+
rails generate pu:pkg:portal admin --auth=user
|
|
62
|
+
|
|
63
|
+
# 4. First resource
|
|
64
|
+
rails generate pu:res:scaffold Post user:belongs_to title:string 'content:text?' --dest=main_app
|
|
65
|
+
rails db:migrate
|
|
66
|
+
|
|
67
|
+
# 5. Connect resource to portal
|
|
68
|
+
rails generate pu:res:conn Post --dest=admin_portal
|
|
69
|
+
|
|
70
|
+
# 6. Mount portal in config/routes.rb
|
|
71
|
+
# mount AdminPortal::Engine, at: "/admin"
|
|
72
|
+
|
|
73
|
+
# 7. Start
|
|
74
|
+
rails server
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## What `pu:core:install` creates
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
app/
|
|
81
|
+
├── controllers/
|
|
82
|
+
│ ├── plutonium_controller.rb # non-resource base
|
|
83
|
+
│ └── resource_controller.rb # CRUD base — see plutonium-behavior
|
|
84
|
+
├── definitions/resource_definition.rb
|
|
85
|
+
├── interactions/resource_interaction.rb
|
|
86
|
+
├── models/resource_record.rb # abstract model — includes Plutonium::Resource::Record
|
|
87
|
+
├── policies/resource_policy.rb
|
|
88
|
+
└── views/layouts/resource.html.erb
|
|
89
|
+
|
|
90
|
+
config/
|
|
91
|
+
├── initializers/plutonium.rb
|
|
92
|
+
└── packages.rb # auto-loads packages/**/lib/engine.rb
|
|
93
|
+
|
|
94
|
+
packages/.keep
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The base classes (`ResourceController`, `ResourcePolicy`, `ResourceDefinition`, `ResourceRecord`, `ResourceInteraction`) are where you put app-wide defaults; resource-specific subclasses come from `pu:res:scaffold`.
|
|
98
|
+
|
|
99
|
+
## Converting an existing model to a resource
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
# 1. Include the module on your model
|
|
103
|
+
class Post < ApplicationRecord
|
|
104
|
+
include Plutonium::Resource::Record
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# 2. Generate supporting files (skips model + migration)
|
|
110
|
+
rails g pu:res:scaffold Post --no-migration --dest=main_app
|
|
111
|
+
|
|
112
|
+
# 3. Connect to portal
|
|
113
|
+
rails g pu:res:conn Post --dest=admin_portal
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# config/initializers/plutonium.rb
|
|
120
|
+
Plutonium.configure do |config|
|
|
121
|
+
config.load_defaults 1.0
|
|
122
|
+
|
|
123
|
+
# Page chrome. Default :modern (topbar + icon rail).
|
|
124
|
+
# :classic preserves the legacy header + sidebar (only when upgrading).
|
|
125
|
+
# config.shell = :classic
|
|
126
|
+
|
|
127
|
+
# Custom assets
|
|
128
|
+
# config.assets.stylesheet = "custom_stylesheet"
|
|
129
|
+
# config.assets.script = "custom_script"
|
|
130
|
+
# config.assets.logo = "custom_logo.png"
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
# Part 2 — The Package System
|
|
137
|
+
|
|
138
|
+
Two kinds, hard split:
|
|
139
|
+
|
|
140
|
+
| Type | Purpose | Generator | Examples |
|
|
141
|
+
|---|---|---|---|
|
|
142
|
+
| **Feature** | Business logic (models, policies, definitions, interactions, migrations) | `pu:pkg:package NAME` | `blogging`, `billing`, `inventory` |
|
|
143
|
+
| **Portal** | Web interface (controllers, views, routes, auth) | `pu:pkg:portal NAME` | `admin_portal`, `customer_portal`, `public_portal` |
|
|
144
|
+
|
|
145
|
+
## Feature packages
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
rails g pu:pkg:package blogging
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Structure:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
packages/blogging/
|
|
155
|
+
├── app/
|
|
156
|
+
│ ├── models/blogging/ # Blogging::Post
|
|
157
|
+
│ ├── definitions/blogging/ # Blogging::PostDefinition
|
|
158
|
+
│ ├── policies/blogging/ # Blogging::PostPolicy
|
|
159
|
+
│ └── interactions/blogging/ # Blogging::PublishPostInteraction
|
|
160
|
+
├── db/migrate/
|
|
161
|
+
└── lib/engine.rb
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Engine:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
module Blogging
|
|
168
|
+
class Engine < Rails::Engine
|
|
169
|
+
include Plutonium::Package::Engine
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Auto-namespacing: every file under `app/<kind>/blogging/` resolves to `Blogging::*`.
|
|
175
|
+
|
|
176
|
+
### Creating resources in a feature package
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
rails g pu:res:scaffold Blogging::Post title:string --dest=blogging
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`--dest=<package_name>` puts model/migration in the package. Cross-package references use the full namespace:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
rails g pu:res:scaffold Comment user:belongs_to blogging/post:belongs_to body:text --dest=comments
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Portal packages
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
rails g pu:pkg:portal admin
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Structure:
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
packages/admin_portal/
|
|
198
|
+
├── app/
|
|
199
|
+
│ ├── controllers/admin_portal/
|
|
200
|
+
│ │ ├── concerns/controller.rb # auth + shared filters
|
|
201
|
+
│ │ ├── dashboard_controller.rb
|
|
202
|
+
│ │ ├── plutonium_controller.rb
|
|
203
|
+
│ │ └── resource_controller.rb
|
|
204
|
+
│ ├── definitions/admin_portal/ # per-portal overrides
|
|
205
|
+
│ ├── policies/admin_portal/ # per-portal overrides
|
|
206
|
+
│ └── views/layouts/admin_portal.html.erb
|
|
207
|
+
├── config/routes.rb
|
|
208
|
+
└── lib/engine.rb
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
See Part 3 for engine configuration and Part 5 for resource connection.
|
|
212
|
+
|
|
213
|
+
## Package loading
|
|
214
|
+
|
|
215
|
+
`config/packages.rb` (created by `pu:core:install`):
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
Dir.glob(File.expand_path("../packages/**/lib/engine.rb", __dir__)) do |package|
|
|
219
|
+
load package
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This is loaded from `config/application.rb`. Migrations from all packages are picked up by `rails db:migrate` automatically.
|
|
224
|
+
|
|
225
|
+
## When to use which
|
|
226
|
+
|
|
227
|
+
**Feature packages** — domain logic that:
|
|
228
|
+
- Could be reused across multiple portals (admin and customer both edit `Blogging::Post`)
|
|
229
|
+
- Has no inherent UI / auth (it's just behavior)
|
|
230
|
+
- You want to keep isolated from other domains (`billing` should not depend on `blogging`)
|
|
231
|
+
|
|
232
|
+
**Portal packages** — user-facing surfaces that:
|
|
233
|
+
- Have a specific auth flow (admin vs customer vs public)
|
|
234
|
+
- Render different views of the same underlying resources
|
|
235
|
+
- Need different policies / definitions per audience
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
# Part 3 — Portal Engines
|
|
240
|
+
|
|
241
|
+
A portal is a Rails engine mixing in `Plutonium::Portal::Engine`. It defines its own routes, controller concern, and (optionally) entity scoping.
|
|
242
|
+
|
|
243
|
+
## Generator
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
rails g pu:pkg:portal <name>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Options
|
|
250
|
+
|
|
251
|
+
| Option | Description |
|
|
252
|
+
|---|---|
|
|
253
|
+
| `--auth=NAME` | Rodauth account to use (e.g. `--auth=user`) |
|
|
254
|
+
| `--public` | Public access — no auth |
|
|
255
|
+
| `--byo` | Bring your own auth |
|
|
256
|
+
| `--scope=CLASS` | Entity class for multi-tenancy (e.g. `--scope=Organization`) |
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
rails g pu:pkg:portal admin --auth=admin
|
|
260
|
+
rails g pu:pkg:portal api --public
|
|
261
|
+
rails g pu:pkg:portal custom --byo
|
|
262
|
+
rails g pu:pkg:portal admin --auth=admin --scope=Organization
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Without flags, the generator prompts interactively.
|
|
266
|
+
|
|
267
|
+
## Engine file
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
# packages/admin_portal/lib/engine.rb
|
|
271
|
+
module AdminPortal
|
|
272
|
+
class Engine < Rails::Engine
|
|
273
|
+
include Plutonium::Portal::Engine
|
|
274
|
+
|
|
275
|
+
config.after_initialize do
|
|
276
|
+
# Optional: multi-tenancy. See plutonium-tenancy for strategies.
|
|
277
|
+
scope_to_entity Organization, strategy: :path
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Controller concern (auth)
|
|
284
|
+
|
|
285
|
+
Every portal has a `Concerns::Controller` mixed into its `ResourceController`. The generator wires this up; you customize it for auth / before_action hooks.
|
|
286
|
+
|
|
287
|
+
### Rodauth
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
module AdminPortal::Concerns::Controller
|
|
291
|
+
extend ActiveSupport::Concern
|
|
292
|
+
include Plutonium::Portal::Controller
|
|
293
|
+
include Plutonium::Auth::Rodauth(:user)
|
|
294
|
+
end
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Public access
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
module AdminPortal::Concerns::Controller
|
|
301
|
+
extend ActiveSupport::Concern
|
|
302
|
+
include Plutonium::Portal::Controller
|
|
303
|
+
include Plutonium::Auth::Public
|
|
304
|
+
end
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### BYO auth
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
module AdminPortal::Concerns::Controller
|
|
311
|
+
extend ActiveSupport::Concern
|
|
312
|
+
include Plutonium::Portal::Controller
|
|
313
|
+
include Plutonium::Auth::Public # disables Rodauth requirement
|
|
314
|
+
|
|
315
|
+
def current_user
|
|
316
|
+
@current_user ||= User.find_by(api_key: request.headers["X-API-Key"])
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Mounting
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
# config/routes.rb
|
|
325
|
+
Rails.application.routes.draw do
|
|
326
|
+
# Authenticated mount
|
|
327
|
+
constraints Rodauth::Rails.authenticate(:user) do
|
|
328
|
+
mount AdminPortal::Engine, at: "/admin"
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Unconstrained (portal handles its own auth)
|
|
332
|
+
mount PublicPortal::Engine, at: "/public"
|
|
333
|
+
end
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Controller hierarchy
|
|
337
|
+
|
|
338
|
+
Portal controllers inherit from the feature-package controller if one exists, OR from the portal's `ResourceController` otherwise.
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
# Feature controller exists → inherit from it AND include portal concern
|
|
342
|
+
class AdminPortal::PostsController < ::PostsController
|
|
343
|
+
include AdminPortal::Concerns::Controller
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# No feature controller → inherit from portal's ResourceController
|
|
347
|
+
class AdminPortal::PostsController < AdminPortal::ResourceController
|
|
348
|
+
end
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
For non-resource portal pages (dashboard, settings):
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
module AdminPortal
|
|
355
|
+
class DashboardController < PlutoniumController
|
|
356
|
+
def index; end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Per-portal overrides
|
|
362
|
+
|
|
363
|
+
```ruby
|
|
364
|
+
# Definition
|
|
365
|
+
class AdminPortal::PostDefinition < ::PostDefinition
|
|
366
|
+
input :internal_notes, as: :text # admins see this; customers don't
|
|
367
|
+
scope :pending_review
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Policy
|
|
371
|
+
class AdminPortal::PostPolicy < ::PostPolicy
|
|
372
|
+
include AdminPortal::ResourcePolicy
|
|
373
|
+
def destroy? = true
|
|
374
|
+
def permitted_attributes_for_create = %i[title content featured internal_notes]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Controller
|
|
378
|
+
module AdminPortal
|
|
379
|
+
class PostsController < ResourceController
|
|
380
|
+
private
|
|
381
|
+
def preferred_action_after_submit = "index"
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
# Part 4 — Routes & `register_resource`
|
|
389
|
+
|
|
390
|
+
Portal routes live in `packages/<name>_portal/config/routes.rb`:
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
AdminPortal::Engine.routes.draw do
|
|
394
|
+
root to: "dashboard#index"
|
|
395
|
+
|
|
396
|
+
register_resource ::Post
|
|
397
|
+
register_resource Blogging::Comment
|
|
398
|
+
|
|
399
|
+
# Non-resource pages
|
|
400
|
+
get "settings", to: "settings#index"
|
|
401
|
+
end
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## `register_resource` — what it does
|
|
405
|
+
|
|
406
|
+
For each call, Plutonium auto-generates:
|
|
407
|
+
|
|
408
|
+
- Top-level CRUD routes (`/posts`, `/posts/:id`, etc.)
|
|
409
|
+
- Nested routes for every registered `has_many` / `has_one` parent (prefixed `nested_`)
|
|
410
|
+
- Route names that `resource_url_for` can resolve
|
|
411
|
+
|
|
412
|
+
You list every resource the portal exposes. If a resource isn't registered, it has no URLs in that portal — `resource_url_for` will fail.
|
|
413
|
+
|
|
414
|
+
## Singular (singleton) resources
|
|
415
|
+
|
|
416
|
+
For resources with no collection — a single per-user `Profile`, app-wide `Settings`, etc.:
|
|
417
|
+
|
|
418
|
+
```ruby
|
|
419
|
+
register_resource ::Profile, singular: true
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Generates singular routes (no `:id`, no index):
|
|
423
|
+
|
|
424
|
+
- `GET /profile` → show
|
|
425
|
+
- `GET /profile/new` → new
|
|
426
|
+
- `GET /profile/edit` → edit
|
|
427
|
+
- `POST /profile` → create
|
|
428
|
+
- `PATCH /profile` → update
|
|
429
|
+
- `DELETE /profile` → destroy
|
|
430
|
+
|
|
431
|
+
Use the `--singular` flag on `pu:res:conn`:
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
rails g pu:res:conn Profile --dest=customer_portal --singular
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## Custom member / collection routes
|
|
438
|
+
|
|
439
|
+
```ruby
|
|
440
|
+
register_resource ::Post do
|
|
441
|
+
member do
|
|
442
|
+
get :preview, as: :preview
|
|
443
|
+
get :analytics, as: :analytics
|
|
444
|
+
post :publish, as: :publish
|
|
445
|
+
end
|
|
446
|
+
collection do
|
|
447
|
+
get :archived, as: :archived
|
|
448
|
+
post :bulk_publish, as: :bulk_publish
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**Always pass `as:`.** Without it, `resource_url_for(@post, action: :preview)` fails because there's no named route to look up — especially critical for nested resources.
|
|
454
|
+
|
|
455
|
+
For most operations with business logic, prefer **interactive actions** (definition + interaction — see [[plutonium-resource]] › Actions) over custom controller routes. The action routes are wired automatically with no `register_resource` block needed.
|
|
456
|
+
|
|
457
|
+
## Cross-package and nested URLs
|
|
458
|
+
|
|
459
|
+
See [[plutonium-behavior]] for full `resource_url_for` signature and [[plutonium-tenancy]] for nested routing semantics.
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
# Part 5 — Connecting Resources to Portals (`pu:res:conn`)
|
|
464
|
+
|
|
465
|
+
A resource is invisible until connected to at least one portal. The generator wires up the portal-specific controller, policy, definition, and route registration.
|
|
466
|
+
|
|
467
|
+
## Command syntax
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
rails g pu:res:conn RESOURCE [RESOURCE...] --dest=PORTAL_NAME [--singular]
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Pass resources directly — avoids interactive prompts. No `--src` needed.
|
|
474
|
+
|
|
475
|
+
## Usage
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
# Main app resources
|
|
479
|
+
rails g pu:res:conn Post Comment Tag --dest=admin_portal
|
|
480
|
+
|
|
481
|
+
# Namespaced (from a feature package)
|
|
482
|
+
rails g pu:res:conn Blogging::Post Blogging::Comment --dest=admin_portal
|
|
483
|
+
|
|
484
|
+
# Singular (profile, settings, dashboard)
|
|
485
|
+
rails g pu:res:conn Profile --dest=customer_portal --singular
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**Run after migrations** — the generator reads model columns to seed the policy's `permitted_attributes_for_*`.
|
|
489
|
+
|
|
490
|
+
## What gets generated
|
|
491
|
+
|
|
492
|
+
For `Post` connected to `admin_portal`:
|
|
493
|
+
|
|
494
|
+
```
|
|
495
|
+
packages/admin_portal/app/
|
|
496
|
+
├── controllers/admin_portal/posts_controller.rb
|
|
497
|
+
├── policies/admin_portal/post_policy.rb
|
|
498
|
+
└── definitions/admin_portal/post_definition.rb
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Plus route registration appended to `packages/admin_portal/config/routes.rb`:
|
|
502
|
+
|
|
503
|
+
```ruby
|
|
504
|
+
register_resource ::Post
|
|
505
|
+
register_resource ::Profile, singular: true # if --singular
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Generated controller
|
|
509
|
+
|
|
510
|
+
```ruby
|
|
511
|
+
class AdminPortal::PostsController < ::PostsController
|
|
512
|
+
include AdminPortal::Concerns::Controller
|
|
513
|
+
end
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Generated policy (seeded from model columns)
|
|
517
|
+
|
|
518
|
+
```ruby
|
|
519
|
+
class AdminPortal::PostPolicy < ::PostPolicy
|
|
520
|
+
include AdminPortal::ResourcePolicy
|
|
521
|
+
|
|
522
|
+
def permitted_attributes_for_create
|
|
523
|
+
[:title, :content, :user_id]
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def permitted_attributes_for_read
|
|
527
|
+
[:title, :content, :user_id, :created_at, :updated_at]
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def permitted_associations
|
|
531
|
+
%i[]
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Review and trim — the generator is liberal. Especially: drop `_id` fields when the form uses the association name, and add `:price` (not `:price_cents`) for `has_cents` fields.
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## Generator reference
|
|
541
|
+
|
|
542
|
+
| Generator | Purpose |
|
|
543
|
+
|---|---|
|
|
544
|
+
| `pu:core:install` | Initial Plutonium setup (base classes, config, layouts) |
|
|
545
|
+
| `pu:rodauth:install` | Set up Rodauth auth |
|
|
546
|
+
| `pu:rodauth:account NAME` | Create user account type |
|
|
547
|
+
| `pu:rodauth:admin NAME` | Admin account with 2FA, lockout, audit |
|
|
548
|
+
| `pu:saas:setup` | User + entity + membership in one shot |
|
|
549
|
+
| `pu:saas:user NAME` | SaaS user account |
|
|
550
|
+
| `pu:saas:entity NAME` | Entity model |
|
|
551
|
+
| `pu:saas:membership` | Membership join table |
|
|
552
|
+
| `pu:pkg:package NAME` | Feature package |
|
|
553
|
+
| `pu:pkg:portal NAME` | Portal package |
|
|
554
|
+
| `pu:res:scaffold NAME` | Resource (model, migration, controller, policy, definition) |
|
|
555
|
+
| `pu:res:conn NAME` | Connect resource to portal |
|
|
556
|
+
| `pu:invites:install` | Invite system (see [[plutonium-tenancy]]) |
|
|
557
|
+
| `pu:invites:invitable NAME` | Mark a model as invitable |
|
|
558
|
+
| `pu:eject:layout` | Eject layouts for customization |
|
|
559
|
+
| `pu:eject:shell` | Eject topbar/sidebar partials |
|
|
560
|
+
| `pu:core:update` | Update plutonium gem + npm |
|
|
561
|
+
| `pu:skills:sync` | Sync Claude Code skills to project |
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## Related skills
|
|
566
|
+
|
|
567
|
+
- [[plutonium-resource]] — what a resource IS (model + definition + scaffold options)
|
|
568
|
+
- [[plutonium-behavior]] — controllers, policies, interactions
|
|
569
|
+
- [[plutonium-tenancy]] — entity scoping, nested resources, invites
|
|
570
|
+
- [[plutonium-auth]] — Rodauth account configuration
|
|
571
|
+
- [[plutonium-ui]] — layouts, page classes, custom Phlex components, assets
|
|
572
|
+
- [[plutonium-testing]] — testing portals, packages, controllers
|