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,146 @@
|
|
|
1
|
+
# Packages
|
|
2
|
+
|
|
3
|
+
Plutonium apps are organized into **packages** — Rails engines with stricter conventions. Two flavors, hard split:
|
|
4
|
+
|
|
5
|
+
| Type | Purpose | Generator | Examples |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| **Feature** | Business logic (models, policies, definitions, interactions, migrations) | `pu:pkg:package NAME` | `blogging`, `billing`, `inventory` |
|
|
8
|
+
| **Portal** | Web interface (controllers, views, routes, auth) | `pu:pkg:portal NAME` | `admin_portal`, `customer_portal`, `public_portal` |
|
|
9
|
+
|
|
10
|
+
## 🚨 Critical
|
|
11
|
+
|
|
12
|
+
- **Feature ↔ portal split is hard.** Feature packages hold models/policies/definitions/interactions. Portal packages hold controllers/views/routes/auth. Don't mix.
|
|
13
|
+
- **Package classes are auto-namespaced.** `packages/blogging/app/models/blogging/post.rb` resolves to `Blogging::Post`. Don't fight it.
|
|
14
|
+
- **Cross-package references use full namespace.** `rails g pu:res:conn Blogging::Post --dest=admin_portal`.
|
|
15
|
+
- **A resource is invisible until `pu:res:conn` registers it with a portal.**
|
|
16
|
+
|
|
17
|
+
## Feature packages
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
rails g pu:pkg:package blogging
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Structure
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
packages/blogging/
|
|
27
|
+
├── app/
|
|
28
|
+
│ ├── models/blogging/ # Blogging::Post
|
|
29
|
+
│ ├── definitions/blogging/ # Blogging::PostDefinition
|
|
30
|
+
│ ├── policies/blogging/ # Blogging::PostPolicy
|
|
31
|
+
│ └── interactions/blogging/ # Blogging::PublishPostInteraction
|
|
32
|
+
├── db/migrate/
|
|
33
|
+
└── lib/engine.rb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Engine
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
module Blogging
|
|
40
|
+
class Engine < Rails::Engine
|
|
41
|
+
include Plutonium::Package::Engine
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Auto-namespacing
|
|
47
|
+
|
|
48
|
+
Every file under `app/<kind>/blogging/` resolves to `Blogging::*`:
|
|
49
|
+
|
|
50
|
+
- `app/models/blogging/post.rb` → `Blogging::Post`
|
|
51
|
+
- `app/policies/blogging/post_policy.rb` → `Blogging::PostPolicy`
|
|
52
|
+
- `app/definitions/blogging/post_definition.rb` → `Blogging::PostDefinition`
|
|
53
|
+
- `app/interactions/blogging/publish_post_interaction.rb` → `Blogging::PublishPostInteraction`
|
|
54
|
+
|
|
55
|
+
Each feature package gets its own base classes:
|
|
56
|
+
|
|
57
|
+
- `Blogging::ApplicationRecord`
|
|
58
|
+
- `Blogging::ResourceRecord`
|
|
59
|
+
- `Blogging::ResourcePolicy`
|
|
60
|
+
- `Blogging::ResourceDefinition`
|
|
61
|
+
- `Blogging::ResourceInteraction`
|
|
62
|
+
|
|
63
|
+
These inherit from the main app's base classes — extend them for package-wide defaults.
|
|
64
|
+
|
|
65
|
+
### Creating resources inside a feature package
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
rails g pu:res:scaffold Blogging::Post title:string --dest=blogging
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Cross-package references use the full namespace:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
rails g pu:res:scaffold Comment user:belongs_to blogging/post:belongs_to body:text --dest=comments
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Portal packages
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
rails g pu:pkg:portal admin
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
See [Portals](./portals) for full details on portal generators, engine config, and routing. Key structural points here:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
packages/admin_portal/
|
|
87
|
+
├── app/
|
|
88
|
+
│ ├── controllers/admin_portal/
|
|
89
|
+
│ │ ├── concerns/controller.rb # auth + shared filters
|
|
90
|
+
│ │ ├── dashboard_controller.rb
|
|
91
|
+
│ │ ├── plutonium_controller.rb
|
|
92
|
+
│ │ └── resource_controller.rb
|
|
93
|
+
│ ├── definitions/admin_portal/ # per-portal overrides
|
|
94
|
+
│ ├── policies/admin_portal/ # per-portal overrides
|
|
95
|
+
│ └── views/layouts/admin_portal.html.erb
|
|
96
|
+
├── config/routes.rb
|
|
97
|
+
└── lib/engine.rb
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Package loading
|
|
101
|
+
|
|
102
|
+
`config/packages.rb` (created by `pu:core:install`):
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
Dir.glob(File.expand_path("../packages/**/lib/engine.rb", __dir__)) do |package|
|
|
106
|
+
load package
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This is loaded from `config/application.rb`. Migrations from all packages are picked up by `rails db:migrate` automatically.
|
|
111
|
+
|
|
112
|
+
## When to use which
|
|
113
|
+
|
|
114
|
+
**Feature packages** — domain logic that:
|
|
115
|
+
|
|
116
|
+
- Could be reused across multiple portals (admin and customer both edit `Blogging::Post`).
|
|
117
|
+
- Has no inherent UI / auth (it's just behavior).
|
|
118
|
+
- You want isolated from other domains (`billing` should not depend on `blogging`).
|
|
119
|
+
|
|
120
|
+
**Portal packages** — user-facing surfaces that:
|
|
121
|
+
|
|
122
|
+
- Have a specific auth flow (admin vs customer vs public).
|
|
123
|
+
- Render different views of the same underlying resources.
|
|
124
|
+
- Need different policies / definitions per audience.
|
|
125
|
+
|
|
126
|
+
## Typical architecture
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
packages/
|
|
130
|
+
├── blogging/ # Feature: blog functionality
|
|
131
|
+
│ └── models, definitions, policies, interactions
|
|
132
|
+
├── billing/ # Feature: payments/invoicing
|
|
133
|
+
│ └── models, definitions, policies, interactions
|
|
134
|
+
├── admin_portal/ # Portal: admin interface
|
|
135
|
+
│ └── controllers, views, routes
|
|
136
|
+
└── customer_portal/ # Portal: customer dashboard
|
|
137
|
+
└── controllers, views, routes
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The portals expose the features. A single feature can be exposed by multiple portals — usually with different policies and definitions per portal.
|
|
141
|
+
|
|
142
|
+
## Related
|
|
143
|
+
|
|
144
|
+
- [Portals](./portals) — portal-specific configuration (mounting, auth, route registration)
|
|
145
|
+
- [Generators](./generators) — `pu:pkg:package` and `pu:pkg:portal` flags
|
|
146
|
+
- [Guide: Creating Packages](/guides/creating-packages) — task-oriented walkthrough
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Portals
|
|
2
|
+
|
|
3
|
+
A portal is a Rails engine mixing in `Plutonium::Portal::Engine`. It defines its own routes, controller concern, and (optionally) entity scoping.
|
|
4
|
+
|
|
5
|
+
## 🚨 Critical
|
|
6
|
+
|
|
7
|
+
- **Use `pu:pkg:portal` for everything.** Never hand-write the engine file, controller concern, or layout.
|
|
8
|
+
- **Pass `--auth=<name>`, `--public`, or `--byo`** for unattended runs — without one of these flags, the generator prompts.
|
|
9
|
+
- **Always connect resources with `pu:res:conn`.** Until connected, a resource has no portal routes and is invisible.
|
|
10
|
+
- **For custom routes on a registered resource, pass `as:`.** Without it, `resource_url_for` can't build URLs.
|
|
11
|
+
|
|
12
|
+
## Creating a portal
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
rails g pu:pkg:portal <name>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Options
|
|
19
|
+
|
|
20
|
+
| Option | Description |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `--auth=NAME` | Rodauth account to authenticate with (e.g. `--auth=user`) |
|
|
23
|
+
| `--public` | Public access — no authentication |
|
|
24
|
+
| `--byo` | Bring your own authentication |
|
|
25
|
+
| `--scope=CLASS` | Entity class for multi-tenancy (e.g. `--scope=Organization`) |
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
rails g pu:pkg:portal admin --auth=admin
|
|
29
|
+
rails g pu:pkg:portal api --public
|
|
30
|
+
rails g pu:pkg:portal custom --byo
|
|
31
|
+
rails g pu:pkg:portal admin --auth=admin --scope=Organization
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Without flags, the generator prompts interactively.
|
|
35
|
+
|
|
36
|
+
## Engine file
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# packages/admin_portal/lib/engine.rb
|
|
40
|
+
module AdminPortal
|
|
41
|
+
class Engine < Rails::Engine
|
|
42
|
+
include Plutonium::Portal::Engine
|
|
43
|
+
|
|
44
|
+
config.after_initialize do
|
|
45
|
+
# Optional: multi-tenancy. See Tenancy › Entity scoping for strategies.
|
|
46
|
+
scope_to_entity Organization, strategy: :path
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Controller concern (auth)
|
|
53
|
+
|
|
54
|
+
Every portal has a `Concerns::Controller` mixed into its `ResourceController`. The generator wires this up; you customize for auth flow and shared before_action hooks.
|
|
55
|
+
|
|
56
|
+
### Rodauth
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
module AdminPortal::Concerns::Controller
|
|
60
|
+
extend ActiveSupport::Concern
|
|
61
|
+
include Plutonium::Portal::Controller
|
|
62
|
+
include Plutonium::Auth::Rodauth(:user)
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Public access
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
module AdminPortal::Concerns::Controller
|
|
70
|
+
extend ActiveSupport::Concern
|
|
71
|
+
include Plutonium::Portal::Controller
|
|
72
|
+
include Plutonium::Auth::Public
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### BYO auth
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
module AdminPortal::Concerns::Controller
|
|
80
|
+
extend ActiveSupport::Concern
|
|
81
|
+
include Plutonium::Portal::Controller
|
|
82
|
+
include Plutonium::Auth::Public # disables the Rodauth requirement
|
|
83
|
+
|
|
84
|
+
def current_user
|
|
85
|
+
@current_user ||= User.find_by(api_key: request.headers["X-API-Key"])
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Mounting
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# config/routes.rb
|
|
94
|
+
Rails.application.routes.draw do
|
|
95
|
+
# Authenticated mount
|
|
96
|
+
constraints Rodauth::Rails.authenticate(:user) do
|
|
97
|
+
mount AdminPortal::Engine, at: "/admin"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Unconstrained — the portal handles its own auth
|
|
101
|
+
mount PublicPortal::Engine, at: "/public"
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Routes & `register_resource`
|
|
106
|
+
|
|
107
|
+
Portal routes live in `packages/<name>_portal/config/routes.rb`:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
AdminPortal::Engine.routes.draw do
|
|
111
|
+
root to: "dashboard#index"
|
|
112
|
+
|
|
113
|
+
register_resource ::Post
|
|
114
|
+
register_resource Blogging::Comment
|
|
115
|
+
|
|
116
|
+
# Non-resource pages
|
|
117
|
+
get "settings", to: "settings#index"
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### What `register_resource` does
|
|
122
|
+
|
|
123
|
+
For each call, Plutonium auto-generates:
|
|
124
|
+
|
|
125
|
+
- Top-level CRUD routes (`/posts`, `/posts/:id`, etc.)
|
|
126
|
+
- Nested routes for every registered `has_many` / `has_one` parent (prefixed `nested_`)
|
|
127
|
+
- Route names that `resource_url_for` can resolve
|
|
128
|
+
|
|
129
|
+
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.
|
|
130
|
+
|
|
131
|
+
### Singular (singleton) resources
|
|
132
|
+
|
|
133
|
+
For resources with no collection — a single per-user `Profile`, app-wide `Settings`, etc.:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
register_resource ::Profile, singular: true
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Generates singular routes (no `:id`, no index):
|
|
140
|
+
|
|
141
|
+
- `GET /profile` → show
|
|
142
|
+
- `GET /profile/new` → new
|
|
143
|
+
- `GET /profile/edit` → edit
|
|
144
|
+
- `POST /profile` → create
|
|
145
|
+
- `PATCH /profile` → update
|
|
146
|
+
- `DELETE /profile` → destroy
|
|
147
|
+
|
|
148
|
+
Use the `--singular` flag on `pu:res:conn`:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
rails g pu:res:conn Profile --dest=customer_portal --singular
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Custom member / collection routes
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
register_resource ::Post do
|
|
158
|
+
member do
|
|
159
|
+
get :preview, as: :preview
|
|
160
|
+
get :analytics, as: :analytics
|
|
161
|
+
post :publish, as: :publish
|
|
162
|
+
end
|
|
163
|
+
collection do
|
|
164
|
+
get :archived, as: :archived
|
|
165
|
+
post :bulk_publish, as: :bulk_publish
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
::: warning Always pass `as:`
|
|
171
|
+
Without `as:`, `resource_url_for(@post, action: :preview)` fails because there's no named route — especially critical for nested resources.
|
|
172
|
+
:::
|
|
173
|
+
|
|
174
|
+
For most operations with business logic, prefer **interactive actions** (definition + interaction — see [Resource › Actions](/reference/resource/actions)) over custom controller routes. Action routes wire automatically with no `register_resource` block needed.
|
|
175
|
+
|
|
176
|
+
## Connecting resources — `pu:res:conn`
|
|
177
|
+
|
|
178
|
+
A resource is invisible until connected to at least one portal. The generator wires up the portal-specific controller, policy, definition, and route registration.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
rails g pu:res:conn RESOURCE [RESOURCE...] --dest=PORTAL_NAME [--singular]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Pass resources directly — avoids interactive prompts. No `--src` needed.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Main app resources
|
|
188
|
+
rails g pu:res:conn Post Comment Tag --dest=admin_portal
|
|
189
|
+
|
|
190
|
+
# Namespaced (from a feature package)
|
|
191
|
+
rails g pu:res:conn Blogging::Post Blogging::Comment --dest=admin_portal
|
|
192
|
+
|
|
193
|
+
# Singular
|
|
194
|
+
rails g pu:res:conn Profile --dest=customer_portal --singular
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
::: tip Run after migrations
|
|
198
|
+
The generator reads model columns to seed the policy's `permitted_attributes_for_*`. Run `rails db:migrate` first.
|
|
199
|
+
:::
|
|
200
|
+
|
|
201
|
+
### What gets generated
|
|
202
|
+
|
|
203
|
+
For `Post` connected to `admin_portal`:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
packages/admin_portal/app/
|
|
207
|
+
├── controllers/admin_portal/posts_controller.rb
|
|
208
|
+
├── policies/admin_portal/post_policy.rb
|
|
209
|
+
└── definitions/admin_portal/post_definition.rb
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Plus route registration appended to `packages/admin_portal/config/routes.rb`:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
register_resource ::Post
|
|
216
|
+
register_resource ::Profile, singular: true # if --singular
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### Generated controller
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
class AdminPortal::PostsController < ::PostsController
|
|
223
|
+
include AdminPortal::Concerns::Controller
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
#### Generated policy (seeded from model columns)
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
class AdminPortal::PostPolicy < ::PostPolicy
|
|
231
|
+
include AdminPortal::ResourcePolicy
|
|
232
|
+
|
|
233
|
+
def permitted_attributes_for_create
|
|
234
|
+
[:title, :content, :user_id]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def permitted_attributes_for_read
|
|
238
|
+
[:title, :content, :user_id, :created_at, :updated_at]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def permitted_associations
|
|
242
|
+
%i[]
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
::: warning Review the generated policy
|
|
248
|
+
The generator is liberal. Drop `_id` fields when the form uses the association name. Add `:price` (not `:price_cents`) for `has_cents` fields. See [Behavior › Policy](/reference/behavior/policies).
|
|
249
|
+
:::
|
|
250
|
+
|
|
251
|
+
## Controller hierarchy
|
|
252
|
+
|
|
253
|
+
Portal controllers inherit from the feature-package controller if one exists, OR from the portal's `ResourceController` otherwise.
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
# Feature controller exists → inherit from it AND include portal concern
|
|
257
|
+
class AdminPortal::PostsController < ::PostsController
|
|
258
|
+
include AdminPortal::Concerns::Controller
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# No feature controller → inherit from portal's ResourceController
|
|
262
|
+
class AdminPortal::PostsController < AdminPortal::ResourceController
|
|
263
|
+
end
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
For non-resource portal pages (dashboard, settings):
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
module AdminPortal
|
|
270
|
+
class DashboardController < PlutoniumController
|
|
271
|
+
def index; end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Per-portal overrides
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
# Definition — different fields per portal
|
|
280
|
+
class AdminPortal::PostDefinition < ::PostDefinition
|
|
281
|
+
input :internal_notes, as: :text # admins see this; customers don't
|
|
282
|
+
scope :pending_review
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Policy — different rules per portal
|
|
286
|
+
class AdminPortal::PostPolicy < ::PostPolicy
|
|
287
|
+
include AdminPortal::ResourcePolicy
|
|
288
|
+
|
|
289
|
+
def destroy? = true
|
|
290
|
+
def permitted_attributes_for_create = %i[title content featured internal_notes]
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Controller — different redirect after submit
|
|
294
|
+
module AdminPortal
|
|
295
|
+
class PostsController < ResourceController
|
|
296
|
+
private
|
|
297
|
+
def preferred_action_after_submit = "index"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Entity scoping
|
|
303
|
+
|
|
304
|
+
Portals can scope ALL their resources to a parent entity automatically:
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
config.after_initialize do
|
|
308
|
+
scope_to_entity Organization, strategy: :path
|
|
309
|
+
end
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Strategies: `:path` (entity id in URL — default) or a custom method name on the portal controller concern.
|
|
313
|
+
|
|
314
|
+
For the full multi-tenancy story, see [Tenancy › Entity scoping](/reference/tenancy/entity-scoping).
|
|
315
|
+
|
|
316
|
+
## Dashboard / non-resource pages
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
# config/routes.rb
|
|
320
|
+
AdminPortal::Engine.routes.draw do
|
|
321
|
+
root to: "dashboard#index"
|
|
322
|
+
get "settings", to: "settings#index"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Controller — inherit from PlutoniumController, NOT ResourceController
|
|
326
|
+
module AdminPortal
|
|
327
|
+
class DashboardController < PlutoniumController
|
|
328
|
+
def index
|
|
329
|
+
@stats = { posts: Post.count, users: User.count }
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
See [UI › Pages](/reference/ui/pages) for custom Phlex page classes.
|
|
336
|
+
|
|
337
|
+
## Multiple portals
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
# Admin — full access, entity-scoped
|
|
341
|
+
module AdminPortal
|
|
342
|
+
class Engine < Rails::Engine
|
|
343
|
+
include Plutonium::Portal::Engine
|
|
344
|
+
|
|
345
|
+
config.after_initialize do
|
|
346
|
+
scope_to_entity Organization, strategy: :path
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Customer dashboard — entity-scoped to the customer's organization
|
|
352
|
+
module DashboardPortal
|
|
353
|
+
class Engine < Rails::Engine
|
|
354
|
+
include Plutonium::Portal::Engine
|
|
355
|
+
|
|
356
|
+
config.after_initialize do
|
|
357
|
+
scope_to_entity Organization, strategy: :path
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Public — no auth, no entity scoping
|
|
363
|
+
module PublicPortal
|
|
364
|
+
class Engine < Rails::Engine
|
|
365
|
+
include Plutonium::Portal::Engine
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Related
|
|
371
|
+
|
|
372
|
+
- [Packages](./packages) — feature vs portal split, structure, namespacing
|
|
373
|
+
- [Generators](./generators) — full `pu:pkg:portal` / `pu:res:conn` option reference
|
|
374
|
+
- [Behavior › Controllers](/reference/behavior/controllers) — controller key methods, hooks, customizations
|
|
375
|
+
- [Tenancy › Entity scoping](/reference/tenancy/entity-scoping) — multi-tenancy mechanics
|
|
376
|
+
- [Auth](/reference/auth/) — Rodauth account types referenced by `--auth=`
|
|
377
|
+
- [UI › Layouts](/reference/ui/layouts) — customizing portal chrome
|