plutonium 0.50.0 → 0.52.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 +574 -0
- data/.claude/skills/plutonium-auth/SKILL.md +167 -302
- 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 +674 -0
- data/.claude/skills/plutonium-testing/SKILL.md +9 -6
- data/.claude/skills/plutonium-ui/SKILL.md +900 -0
- data/CHANGELOG.md +44 -2
- data/Rakefile +2 -1
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1010 -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 +38 -29
- data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
- data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
- data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
- data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
- data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
- data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
- data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
- data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
- data/docs/.vitepress/theme/custom.css +144 -0
- data/docs/.vitepress/theme/index.ts +58 -1
- data/docs/getting-started/index.md +33 -57
- data/docs/getting-started/installation.md +37 -80
- data/docs/getting-started/tutorial/02-first-resource.md +17 -8
- data/docs/getting-started/tutorial/03-authentication.md +31 -23
- data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
- data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
- data/docs/getting-started/tutorial/07-author-portal.md +8 -0
- data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
- data/docs/getting-started/tutorial/index.md +4 -5
- data/docs/guides/adding-resources.md +66 -377
- data/docs/guides/authentication.md +98 -462
- data/docs/guides/authorization.md +124 -370
- data/docs/guides/creating-packages.md +93 -298
- data/docs/guides/custom-actions.md +126 -441
- data/docs/guides/customizing-ui.md +258 -0
- data/docs/guides/index.md +49 -52
- data/docs/guides/multi-tenancy.md +123 -186
- data/docs/guides/nested-resources.md +137 -396
- data/docs/guides/search-filtering.md +127 -238
- data/docs/guides/testing.md +10 -5
- data/docs/guides/theming.md +168 -405
- data/docs/guides/troubleshooting.md +5 -3
- data/docs/guides/user-invites.md +112 -425
- data/docs/guides/user-profile.md +82 -241
- data/docs/index.md +10 -219
- data/docs/public/asciinema/home-scaffold.cast +305 -0
- data/docs/public/images/guides/custom-actions-bulk.png +0 -0
- data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
- data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
- data/docs/public/images/guides/nested-inputs.png +0 -0
- data/docs/public/images/guides/nested-resources-tab.png +0 -0
- data/docs/public/images/guides/search-filtering-index.png +0 -0
- data/docs/public/images/guides/search-filtering-panel.png +0 -0
- data/docs/public/images/guides/theming-after.png +0 -0
- data/docs/public/images/guides/theming-before.png +0 -0
- data/docs/public/images/guides/user-invites-landing.png +0 -0
- data/docs/public/images/guides/user-profile-edit.png +0 -0
- data/docs/public/images/guides/user-profile-show.png +0 -0
- data/docs/public/images/home-index.png +0 -0
- data/docs/public/images/home-new.png +0 -0
- data/docs/public/images/home-show.png +0 -0
- data/docs/public/images/tutorial/02-empty-index.png +0 -0
- data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
- data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
- data/docs/public/images/tutorial/02-new-form.png +0 -0
- data/docs/public/images/tutorial/03-create-account.png +0 -0
- data/docs/public/images/tutorial/03-login.png +0 -0
- data/docs/public/images/tutorial/04-admin-index.png +0 -0
- data/docs/public/images/tutorial/05-actions-menu.png +0 -0
- data/docs/public/images/tutorial/05-row-actions.png +0 -0
- data/docs/public/images/tutorial/06-comments-tab.png +0 -0
- data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
- data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
- data/docs/public/images/tutorial/07-author-portal.png +0 -0
- data/docs/public/images/tutorial/08-customized-index.png +0 -0
- 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 +229 -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 +67 -48
- 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 +368 -0
- data/docs/reference/tenancy/index.md +36 -0
- data/docs/reference/tenancy/invites.md +400 -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 +121 -0
- data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
- data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -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/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -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/assets/assets_generator.rb +10 -0
- data/lib/generators/pu/core/update/update_generator.rb +0 -20
- data/lib/generators/pu/invites/install_generator.rb +45 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
- data/lib/generators/pu/profile/conn_generator.rb +2 -2
- data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
- data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
- data/lib/generators/pu/rodauth/account_generator.rb +2 -1
- data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
- data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
- data/lib/generators/pu/rodauth/views_generator.rb +0 -2
- data/lib/generators/pu/saas/membership/USAGE +4 -1
- data/lib/generators/pu/saas/setup_generator.rb +16 -4
- data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
- 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 +30 -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 +23 -5
- data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
- 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 +5 -0
- data/lib/plutonium/ui/form/base.rb +23 -3
- 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 +103 -22
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
- data/lib/plutonium/ui/form/interaction.rb +1 -1
- data/lib/plutonium/ui/form/resource.rb +0 -4
- data/lib/plutonium/ui/form/theme.rb +1 -1
- 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/edit.rb +1 -1
- data/lib/plutonium/ui/page/index.rb +4 -4
- data/lib/plutonium/ui/page/new.rb +1 -1
- data/lib/plutonium/ui/table/components/filter_form.rb +12 -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 +13 -10
- data/src/css/slim_select.css +4 -0
- data/src/js/controllers/form_controller.js +5 -4
- data/src/js/controllers/slim_select_controller.js +61 -0
- data/src/js/turbo/turbo_actions.js +33 -0
- data/yarn.lock +661 -544
- metadata +86 -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
|
@@ -1,381 +1,176 @@
|
|
|
1
1
|
# Creating Packages
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Organize your app into feature and portal packages.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Goal
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|------|---------|-----------|
|
|
9
|
-
| **Feature Package** | Business logic (models, definitions, policies) | `rails g pu:pkg:package NAME` |
|
|
10
|
-
| **Portal Package** | Web interface (routes, auth, UI) | `rails g pu:pkg:portal NAME` |
|
|
7
|
+
Domain code (models, policies, definitions, interactions) lives in **feature packages**. Web interfaces (controllers, views, routes, auth) live in **portal packages**. Both are Rails engines with Plutonium conventions on top.
|
|
11
8
|
|
|
12
|
-
##
|
|
9
|
+
## Two types
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
| Type | Purpose | Generator | Examples |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| **Feature** | Business logic | `pu:pkg:package NAME` | `blogging`, `billing`, `inventory` |
|
|
14
|
+
| **Portal** | Web interface | `pu:pkg:portal NAME` | `admin_portal`, `customer_portal`, `public_portal` |
|
|
15
|
+
|
|
16
|
+
🚨 Don't mix the two. Feature packages own the **domain code** — models, interactions, policies/definitions for resources owned by that feature. Portal packages own the **web surface** — controllers, routes, auth, and portal-specific policy/definition *overrides* for resources they expose.
|
|
17
|
+
|
|
18
|
+
## Feature package
|
|
19
|
+
|
|
20
|
+
### 1. Generate
|
|
15
21
|
|
|
16
22
|
```bash
|
|
17
23
|
rails g pu:pkg:package blogging
|
|
18
24
|
```
|
|
19
25
|
|
|
20
|
-
###
|
|
26
|
+
### 2. Structure
|
|
21
27
|
|
|
22
28
|
```
|
|
23
29
|
packages/blogging/
|
|
24
30
|
├── app/
|
|
25
|
-
│ ├──
|
|
26
|
-
│
|
|
27
|
-
│ ├──
|
|
28
|
-
│
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
│ ├── models/blogging/
|
|
32
|
-
│ │ └── resource_record.rb
|
|
33
|
-
│ ├── policies/blogging/
|
|
34
|
-
│ │ └── resource_policy.rb
|
|
35
|
-
│ └── views/blogging/
|
|
36
|
-
└── lib/
|
|
37
|
-
└── engine.rb
|
|
31
|
+
│ ├── models/blogging/ # Blogging::Post
|
|
32
|
+
│ ├── definitions/blogging/ # Blogging::PostDefinition
|
|
33
|
+
│ ├── policies/blogging/ # Blogging::PostPolicy
|
|
34
|
+
│ └── interactions/blogging/ # Blogging::PublishPostInteraction
|
|
35
|
+
├── db/migrate/
|
|
36
|
+
└── lib/engine.rb
|
|
38
37
|
```
|
|
39
38
|
|
|
40
|
-
###
|
|
41
|
-
|
|
42
|
-
```ruby
|
|
43
|
-
# packages/blogging/lib/engine.rb
|
|
44
|
-
module Blogging
|
|
45
|
-
class Engine < Rails::Engine
|
|
46
|
-
include Plutonium::Package::Engine
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Namespacing
|
|
52
|
-
|
|
53
|
-
All classes are auto-namespaced:
|
|
54
|
-
- `app/models/blogging/post.rb` → `Blogging::Post`
|
|
55
|
-
- `app/policies/blogging/post_policy.rb` → `Blogging::PostPolicy`
|
|
56
|
-
|
|
57
|
-
## Creating a Portal Package
|
|
58
|
-
|
|
59
|
-
### Using the Generator
|
|
39
|
+
### 3. Create resources inside it
|
|
60
40
|
|
|
61
41
|
```bash
|
|
62
|
-
rails g pu:
|
|
42
|
+
rails g pu:res:scaffold Blogging::Post title:string --dest=blogging
|
|
43
|
+
rails db:migrate
|
|
63
44
|
```
|
|
64
45
|
|
|
65
|
-
###
|
|
66
|
-
|
|
67
|
-
| Option | Description |
|
|
68
|
-
|--------|-------------|
|
|
69
|
-
| `--auth=NAME` | Rodauth account to authenticate with |
|
|
70
|
-
| `--public` | Grant public access (no authentication) |
|
|
71
|
-
| `--byo` | Bring your own authentication |
|
|
46
|
+
### 4. Expose it via a portal
|
|
72
47
|
|
|
73
48
|
```bash
|
|
74
|
-
|
|
75
|
-
rails g pu:pkg:portal admin --auth=admin
|
|
76
|
-
rails g pu:pkg:portal api --public
|
|
77
|
-
rails g pu:pkg:portal custom --byo
|
|
49
|
+
rails g pu:res:conn Blogging::Post --dest=admin_portal
|
|
78
50
|
```
|
|
79
51
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
### Generated Structure
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
packages/admin_portal/
|
|
86
|
-
├── app/
|
|
87
|
-
│ ├── controllers/admin_portal/
|
|
88
|
-
│ │ ├── concerns/controller.rb
|
|
89
|
-
│ │ ├── dashboard_controller.rb
|
|
90
|
-
│ │ ├── plutonium_controller.rb
|
|
91
|
-
│ │ └── resource_controller.rb
|
|
92
|
-
│ ├── definitions/admin_portal/
|
|
93
|
-
│ │ └── resource_definition.rb
|
|
94
|
-
│ ├── policies/admin_portal/
|
|
95
|
-
│ │ └── resource_policy.rb
|
|
96
|
-
│ └── views/admin_portal/
|
|
97
|
-
│ └── dashboard/index.html.erb
|
|
98
|
-
├── config/
|
|
99
|
-
│ └── routes.rb
|
|
100
|
-
└── lib/
|
|
101
|
-
└── engine.rb
|
|
102
|
-
```
|
|
52
|
+
## Portal package
|
|
103
53
|
|
|
104
|
-
###
|
|
54
|
+
### 1. Generate
|
|
105
55
|
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
module AdminPortal
|
|
109
|
-
class Engine < Rails::Engine
|
|
110
|
-
include Plutonium::Portal::Engine
|
|
111
|
-
|
|
112
|
-
config.after_initialize do
|
|
113
|
-
# Multi-tenancy (optional)
|
|
114
|
-
scope_to_entity Organization, strategy: :path
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
56
|
+
```bash
|
|
57
|
+
rails g pu:pkg:portal admin --auth=user
|
|
118
58
|
```
|
|
119
59
|
|
|
120
|
-
|
|
60
|
+
Options:
|
|
121
61
|
|
|
122
|
-
|
|
62
|
+
- `--auth=NAME` — Rodauth account to authenticate with.
|
|
63
|
+
- `--public` — public access, no auth.
|
|
64
|
+
- `--byo` — bring your own auth.
|
|
65
|
+
- `--scope=CLASS` — entity class for multi-tenancy.
|
|
123
66
|
|
|
124
|
-
|
|
125
|
-
# packages/admin_portal/app/controllers/admin_portal/concerns/controller.rb
|
|
126
|
-
module AdminPortal
|
|
127
|
-
module Concerns
|
|
128
|
-
module Controller
|
|
129
|
-
extend ActiveSupport::Concern
|
|
130
|
-
include Plutonium::Portal::Controller
|
|
131
|
-
include Plutonium::Auth::Rodauth(:admin)
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
```
|
|
67
|
+
The generator mounts the engine for you — at `/admin` in this case, wrapped in `constraints Rodauth::Rails.authenticate(:user)` because you passed `--auth=user`. Open `packages/admin_portal/config/routes.rb` to see the generated mount.
|
|
136
68
|
|
|
137
|
-
|
|
69
|
+
### 2. Connect resources
|
|
138
70
|
|
|
139
|
-
```
|
|
140
|
-
|
|
71
|
+
```bash
|
|
72
|
+
rails g pu:res:conn Blogging::Post --dest=admin_portal
|
|
141
73
|
```
|
|
142
74
|
|
|
143
|
-
|
|
75
|
+
You can connect multiple resources in one command:
|
|
144
76
|
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
helper_method :current_user
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def current_user
|
|
151
|
-
# Your authentication logic
|
|
152
|
-
@current_user ||= User.find_by(api_key: request.headers["X-API-Key"])
|
|
153
|
-
end
|
|
77
|
+
```bash
|
|
78
|
+
rails g pu:res:conn Blogging::Post Blogging::Comment --dest=admin_portal
|
|
154
79
|
```
|
|
155
80
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
The portal generator creates routes and auto-mounts to the main app:
|
|
81
|
+
See [Reference › App › Portals](/reference/app/portals) for the full portal surface.
|
|
159
82
|
|
|
160
|
-
|
|
161
|
-
# packages/admin_portal/config/routes.rb
|
|
162
|
-
AdminPortal::Engine.routes.draw do
|
|
163
|
-
root to: "dashboard#index"
|
|
83
|
+
## Auto-namespacing
|
|
164
84
|
|
|
165
|
-
|
|
166
|
-
register_resource ::Post
|
|
167
|
-
register_resource Blogging::Comment
|
|
168
|
-
end
|
|
85
|
+
Every file under `app/<kind>/blogging/` resolves to `Blogging::*`:
|
|
169
86
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
Rails.application.routes.draw do
|
|
173
|
-
constraints Rodauth::Rails.authenticate(:admin) do
|
|
174
|
-
mount AdminPortal::Engine, at: "/admin"
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
```
|
|
87
|
+
- `app/models/blogging/post.rb` → `Blogging::Post`
|
|
88
|
+
- `app/policies/blogging/post_policy.rb` → `Blogging::PostPolicy`
|
|
178
89
|
|
|
179
|
-
|
|
90
|
+
Each feature package gets base classes — `Blogging::ApplicationRecord`, `Blogging::ResourceRecord`, `Blogging::ResourcePolicy`, `Blogging::ResourceDefinition`, `Blogging::ResourceInteraction` — that inherit from the main app's.
|
|
180
91
|
|
|
181
|
-
|
|
92
|
+
## Cross-package references
|
|
182
93
|
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
member do
|
|
186
|
-
get :preview
|
|
187
|
-
post :publish
|
|
188
|
-
end
|
|
189
|
-
collection do
|
|
190
|
-
get :archived
|
|
191
|
-
end
|
|
192
|
-
end
|
|
94
|
+
```bash
|
|
95
|
+
rails g pu:res:scaffold Comment user:belongs_to blogging/post:belongs_to body:text --dest=comments
|
|
193
96
|
```
|
|
194
97
|
|
|
195
|
-
|
|
98
|
+
The `blogging/post` syntax expands to `Blogging::Post`.
|
|
196
99
|
|
|
197
|
-
|
|
100
|
+
## When to use which
|
|
198
101
|
|
|
199
|
-
|
|
200
|
-
# config/packages.rb
|
|
201
|
-
Dir.glob(File.expand_path("../packages/**/lib/engine.rb", __dir__)) do |package|
|
|
202
|
-
load package
|
|
203
|
-
end
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
This is automatically required in `config/application.rb`.
|
|
102
|
+
### Feature package
|
|
207
103
|
|
|
208
|
-
|
|
104
|
+
When the code:
|
|
209
105
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
106
|
+
- Could be reused across multiple portals (admin and customer both edit `Blogging::Post`).
|
|
107
|
+
- Has no inherent UI / auth.
|
|
108
|
+
- You want isolated from other domains (`billing` shouldn't depend on `blogging`).
|
|
213
109
|
|
|
214
|
-
|
|
215
|
-
rails g pu:res:scaffold Post title:string --dest=blogging
|
|
216
|
-
```
|
|
110
|
+
### Portal package
|
|
217
111
|
|
|
218
|
-
|
|
112
|
+
When the code:
|
|
219
113
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
class Post < Blogging::ResourceRecord
|
|
224
|
-
# Model code
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
```
|
|
114
|
+
- Has a specific auth flow (admin vs customer vs public).
|
|
115
|
+
- Renders different views of the same underlying resources.
|
|
116
|
+
- Needs different policies / definitions per audience.
|
|
228
117
|
|
|
229
|
-
|
|
118
|
+
### When NOT to make a package
|
|
230
119
|
|
|
231
|
-
|
|
120
|
+
For an app that doesn't need cross-portal sharing, just put resources in `--dest=main_app`. Packages add organization, not power.
|
|
232
121
|
|
|
233
|
-
|
|
234
|
-
# Connect main app resource
|
|
235
|
-
rails g pu:res:conn Post --dest=admin_portal
|
|
122
|
+
## Typical architecture
|
|
236
123
|
|
|
237
|
-
# Connect namespaced resource
|
|
238
|
-
rails g pu:res:conn Blogging::Post --dest=admin_portal
|
|
239
124
|
```
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
### Path Strategy
|
|
246
|
-
|
|
247
|
-
Entity ID in URL path:
|
|
248
|
-
|
|
249
|
-
```ruby
|
|
250
|
-
# packages/admin_portal/lib/engine.rb
|
|
251
|
-
config.after_initialize do
|
|
252
|
-
scope_to_entity Organization, strategy: :path
|
|
253
|
-
end
|
|
125
|
+
packages/
|
|
126
|
+
├── blogging/ # Feature: blog functionality
|
|
127
|
+
├── billing/ # Feature: payments/invoicing
|
|
128
|
+
├── admin_portal/ # Portal: admin interface
|
|
129
|
+
└── customer_portal/ # Portal: customer dashboard
|
|
254
130
|
```
|
|
255
131
|
|
|
256
|
-
|
|
132
|
+
The portals expose the features. A single feature can be exposed by multiple portals — usually with different policies and definitions per portal.
|
|
257
133
|
|
|
258
|
-
|
|
134
|
+
## Package loading
|
|
259
135
|
|
|
260
|
-
|
|
136
|
+
Generated by `pu:core:install`:
|
|
261
137
|
|
|
262
138
|
```ruby
|
|
263
|
-
config.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
# In controller concern
|
|
268
|
-
def current_organization
|
|
269
|
-
@current_organization ||= Organization.find_by!(subdomain: request.subdomain)
|
|
139
|
+
# config/packages.rb
|
|
140
|
+
Dir.glob(File.expand_path("../packages/**/lib/engine.rb", __dir__)) do |package|
|
|
141
|
+
load package
|
|
270
142
|
end
|
|
271
143
|
```
|
|
272
144
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
### 1. Single Responsibility
|
|
276
|
-
Each feature package should handle one domain:
|
|
277
|
-
- `blogging` - Posts, comments, categories
|
|
278
|
-
- `inventory` - Products, stock, warehouses
|
|
279
|
-
- `billing` - Invoices, payments, subscriptions
|
|
280
|
-
|
|
281
|
-
### 2. Clear Naming
|
|
282
|
-
- Feature packages: domain nouns (`blogging`, `billing`)
|
|
283
|
-
- Portal packages: role + portal (`admin_portal`, `api_portal`)
|
|
284
|
-
|
|
285
|
-
### 3. Minimal Cross-Dependencies
|
|
286
|
-
Limit dependencies between feature packages. If two packages are tightly coupled, consider merging them.
|
|
287
|
-
|
|
288
|
-
### 4. Portal Customization
|
|
289
|
-
Put UI customizations in portal packages, not feature packages:
|
|
290
|
-
|
|
291
|
-
```ruby
|
|
292
|
-
# Good: Portal-specific definition
|
|
293
|
-
# packages/admin_portal/app/definitions/admin_portal/post_definition.rb
|
|
294
|
-
|
|
295
|
-
# Bad: Feature package with portal-specific code
|
|
296
|
-
# packages/blogging/app/definitions/blogging/admin_post_definition.rb
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
## Multiple Portals Pattern
|
|
300
|
-
|
|
301
|
-
Common pattern for different user types:
|
|
302
|
-
|
|
303
|
-
```
|
|
304
|
-
packages/
|
|
305
|
-
├── blogging/ # Feature: blog functionality
|
|
306
|
-
├── billing/ # Feature: payment/invoicing
|
|
307
|
-
├── admin_portal/ # Portal: admin interface
|
|
308
|
-
├── dashboard_portal/ # Portal: user dashboard
|
|
309
|
-
└── public_portal/ # Portal: public read-only
|
|
310
|
-
```
|
|
145
|
+
Loaded from `config/application.rb`. Migrations from all packages are picked up by `rails db:migrate` automatically.
|
|
311
146
|
|
|
312
|
-
|
|
313
|
-
- Have different authentication
|
|
314
|
-
- Show different fields
|
|
315
|
-
- Allow different actions
|
|
316
|
-
- Use different layouts
|
|
317
|
-
|
|
318
|
-
## Portal-Specific Overrides
|
|
319
|
-
|
|
320
|
-
### Override Definition
|
|
147
|
+
## Per-portal overrides
|
|
321
148
|
|
|
322
149
|
```ruby
|
|
323
|
-
#
|
|
150
|
+
# Definition — different fields per portal
|
|
324
151
|
class AdminPortal::PostDefinition < ::PostDefinition
|
|
325
|
-
#
|
|
326
|
-
scope :
|
|
152
|
+
input :internal_notes, as: :text # admins see this; customers don't
|
|
153
|
+
scope :pending_review
|
|
327
154
|
end
|
|
328
|
-
```
|
|
329
155
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
```ruby
|
|
333
|
-
# packages/admin_portal/app/policies/admin_portal/post_policy.rb
|
|
156
|
+
# Policy — different rules per portal
|
|
334
157
|
class AdminPortal::PostPolicy < ::PostPolicy
|
|
335
158
|
include AdminPortal::ResourcePolicy
|
|
336
159
|
|
|
337
|
-
def destroy?
|
|
338
|
-
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
def permitted_attributes_for_create
|
|
342
|
-
%i[title content featured internal_notes] # More fields
|
|
343
|
-
end
|
|
160
|
+
def destroy? = true
|
|
161
|
+
def permitted_attributes_for_create = %i[title content featured internal_notes]
|
|
344
162
|
end
|
|
345
163
|
```
|
|
346
164
|
|
|
347
|
-
|
|
165
|
+
## Common issues
|
|
348
166
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
include AdminPortal::Concerns::Controller
|
|
353
|
-
|
|
354
|
-
private
|
|
355
|
-
|
|
356
|
-
def preferred_action_after_submit
|
|
357
|
-
"index"
|
|
358
|
-
end
|
|
359
|
-
end
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
## Controller Hierarchy
|
|
363
|
-
|
|
364
|
-
Portal controllers inherit from the feature package's controller if one exists (and include the portal's `Concerns::Controller`). If no feature package controller exists, they inherit from the portal's `ResourceController`.
|
|
365
|
-
|
|
366
|
-
```ruby
|
|
367
|
-
# With feature package controller:
|
|
368
|
-
class AdminPortal::PostsController < ::PostsController
|
|
369
|
-
include AdminPortal::Concerns::Controller
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
# Without feature package controller:
|
|
373
|
-
class AdminPortal::PostsController < AdminPortal::ResourceController
|
|
374
|
-
end
|
|
375
|
-
```
|
|
167
|
+
- **Class not loading** — namespace must match the directory: `app/models/blogging/post.rb` MUST be `Blogging::Post`.
|
|
168
|
+
- **Migration not running** — package migrations are auto-included. If they aren't running, check `config/packages.rb` is loaded from `application.rb`.
|
|
169
|
+
- **Cross-package association fails** — use `blogging/post:belongs_to` in `pu:res:scaffold`, OR manually set `class_name: "Blogging::Post"` on the `belongs_to`.
|
|
376
170
|
|
|
377
171
|
## Related
|
|
378
172
|
|
|
379
|
-
- [
|
|
380
|
-
- [
|
|
381
|
-
- [
|
|
173
|
+
- [Reference › App › Packages](/reference/app/packages) — full package surface
|
|
174
|
+
- [Reference › App › Portals](/reference/app/portals) — portal-specific configuration
|
|
175
|
+
- [Adding resources](./adding-resources) — `pu:res:scaffold` and `pu:res:conn`
|
|
176
|
+
- [Authentication](./authentication) — portal auth setup
|