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
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plutonium-controller
|
|
3
|
-
description: Use BEFORE overriding a controller action, adding a hook, or changing redirect logic in a Plutonium controller. Also when customizing resource_params or presentation hooks.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Plutonium Controllers
|
|
7
|
-
|
|
8
|
-
## 🚨 Critical (read first)
|
|
9
|
-
- **Use generators.** `pu:res:scaffold` and `pu:res:conn` create controllers — never hand-write them.
|
|
10
|
-
- **Don't override CRUD actions.** Customize via hooks (`resource_params`, `redirect_url_after_submit`, `preferred_action_after_submit`, presentation hooks). Overriding `create`/`update` usually breaks authorization, params filtering, or both.
|
|
11
|
-
- **Prefer definitions and interactions.** UI config belongs in definitions; business logic belongs in interactions. The controller is thin by design.
|
|
12
|
-
- **Named custom routes.** When adding custom member/collection routes, always use `as:` so `resource_url_for` can build URLs — especially for nested resources.
|
|
13
|
-
- **Related skills:** `plutonium-definition` (interactive actions instead of controller actions), `plutonium-policy` (authorization), `plutonium-nested-resources` (parent/child routing), `plutonium-views` (custom page classes).
|
|
14
|
-
|
|
15
|
-
**Controllers are generated automatically** - never create them manually:
|
|
16
|
-
- `rails g pu:res:scaffold` creates the base controller
|
|
17
|
-
- `rails g pu:res:conn` creates portal-specific controllers
|
|
18
|
-
|
|
19
|
-
Controllers in Plutonium provide full CRUD functionality out of the box. You rarely need to customize them - definitions handle most UI configuration and policies handle authorization.
|
|
20
|
-
|
|
21
|
-
## Base Classes
|
|
22
|
-
|
|
23
|
-
```ruby
|
|
24
|
-
# app/controllers/resource_controller.rb (generated during install)
|
|
25
|
-
class ResourceController < ApplicationController
|
|
26
|
-
include Plutonium::Resource::Controller
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# app/controllers/posts_controller.rb (generated per resource)
|
|
30
|
-
class PostsController < ::ResourceController
|
|
31
|
-
# Empty - all CRUD actions inherited
|
|
32
|
-
end
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## What You Get for Free
|
|
36
|
-
|
|
37
|
-
Every resource controller automatically provides:
|
|
38
|
-
|
|
39
|
-
| Action | Route | Purpose |
|
|
40
|
-
|--------|-------|---------|
|
|
41
|
-
| `index` | GET /posts | List with pagination, search, filters, sorting |
|
|
42
|
-
| `show` | GET /posts/:id | Display single record |
|
|
43
|
-
| `new` | GET /posts/new | New record form |
|
|
44
|
-
| `create` | POST /posts | Create record |
|
|
45
|
-
| `edit` | GET /posts/:id/edit | Edit record form |
|
|
46
|
-
| `update` | PATCH /posts/:id | Update record |
|
|
47
|
-
| `destroy` | DELETE /posts/:id | Delete record |
|
|
48
|
-
|
|
49
|
-
Plus interactive action routes for custom operations defined in definitions.
|
|
50
|
-
|
|
51
|
-
## When to Customize
|
|
52
|
-
|
|
53
|
-
**Use Definitions for:**
|
|
54
|
-
- Field configuration (inputs, displays, columns)
|
|
55
|
-
- Search, filters, scopes, sorting
|
|
56
|
-
- Actions (interactive operations)
|
|
57
|
-
- Form customization
|
|
58
|
-
|
|
59
|
-
**Customize Controller for:**
|
|
60
|
-
- Custom redirect logic
|
|
61
|
-
- Special parameter processing
|
|
62
|
-
- Non-standard authorization flows
|
|
63
|
-
- External integrations
|
|
64
|
-
- Response format changes
|
|
65
|
-
|
|
66
|
-
## Override Hooks
|
|
67
|
-
|
|
68
|
-
All customization is done by overriding private methods:
|
|
69
|
-
|
|
70
|
-
### Redirect Hooks
|
|
71
|
-
|
|
72
|
-
```ruby
|
|
73
|
-
class PostsController < ::ResourceController
|
|
74
|
-
private
|
|
75
|
-
|
|
76
|
-
# Where to go after create/update: "show" (default), "edit", "new", "index"
|
|
77
|
-
def preferred_action_after_submit
|
|
78
|
-
"edit"
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Custom URL after create/update (overrides preferred_action_after_submit)
|
|
82
|
-
def redirect_url_after_submit
|
|
83
|
-
posts_path
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Custom URL after destroy
|
|
87
|
-
def redirect_url_after_destroy
|
|
88
|
-
posts_path
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Parameter Hooks
|
|
94
|
-
|
|
95
|
-
```ruby
|
|
96
|
-
class PostsController < ::ResourceController
|
|
97
|
-
private
|
|
98
|
-
|
|
99
|
-
# Modify params before create/update
|
|
100
|
-
def resource_params
|
|
101
|
-
params = super
|
|
102
|
-
params[:tags] = params[:tags].split(",") if params[:tags].is_a?(String)
|
|
103
|
-
params
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Query Hooks
|
|
109
|
-
|
|
110
|
-
```ruby
|
|
111
|
-
class PostsController < ::ResourceController
|
|
112
|
-
private
|
|
113
|
-
|
|
114
|
-
# Customize the index query
|
|
115
|
-
def filtered_resource_collection
|
|
116
|
-
base = current_authorized_scope
|
|
117
|
-
base = base.featured if params[:featured]
|
|
118
|
-
current_query_object.apply(base, raw_resource_query_params)
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### Presentation Hooks
|
|
124
|
-
|
|
125
|
-
Control whether parent/entity fields appear in forms and displays:
|
|
126
|
-
|
|
127
|
-
```ruby
|
|
128
|
-
class PostsController < ::ResourceController
|
|
129
|
-
private
|
|
130
|
-
|
|
131
|
-
# Show parent field in displays (default: false)
|
|
132
|
-
def present_parent?
|
|
133
|
-
true
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Include parent field in forms (default: same as present_parent?)
|
|
137
|
-
def submit_parent?
|
|
138
|
-
true
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Show scoped entity in displays (default: false)
|
|
142
|
-
def present_scoped_entity?
|
|
143
|
-
true
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# Include scoped entity in forms (default: same as present_scoped_entity?)
|
|
147
|
-
def submit_scoped_entity?
|
|
148
|
-
true
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
## Custom Actions
|
|
154
|
-
|
|
155
|
-
```ruby
|
|
156
|
-
class PostsController < ::ResourceController
|
|
157
|
-
def publish
|
|
158
|
-
authorize_current!(resource_record!, to: :publish?)
|
|
159
|
-
resource_record!.update!(published: true)
|
|
160
|
-
redirect_to resource_url_for(resource_record!), notice: "Published!"
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
**Important:** When adding custom routes, always use the `as:` option to name them:
|
|
166
|
-
|
|
167
|
-
```ruby
|
|
168
|
-
# config/routes.rb or portal routes
|
|
169
|
-
resources :posts do
|
|
170
|
-
member do
|
|
171
|
-
post :publish, as: :publish # Named route required!
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
This ensures `resource_url_for` can generate correct URLs, especially for nested resources.
|
|
177
|
-
|
|
178
|
-
Note: For most custom operations, use Interactive Actions in definitions instead.
|
|
179
|
-
|
|
180
|
-
## Key Methods
|
|
181
|
-
|
|
182
|
-
### Resource Access
|
|
183
|
-
|
|
184
|
-
```ruby
|
|
185
|
-
resource_class # The model class (e.g., Post)
|
|
186
|
-
resource_record! # Current record (raises if not found)
|
|
187
|
-
resource_record? # Current record (nil if not found)
|
|
188
|
-
resource_params # Permitted params for create/update
|
|
189
|
-
current_parent # Parent record for nested routes
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### Authorization
|
|
193
|
-
|
|
194
|
-
```ruby
|
|
195
|
-
authorize_current!(record, to: :action?) # Check permission
|
|
196
|
-
current_policy # Policy for current resource
|
|
197
|
-
permitted_attributes # Allowed attributes for action
|
|
198
|
-
current_authorized_scope # Scoped records user can access
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### Definition Access
|
|
202
|
-
|
|
203
|
-
```ruby
|
|
204
|
-
current_definition # Definition for current resource
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### UI Building
|
|
208
|
-
|
|
209
|
-
```ruby
|
|
210
|
-
build_form # Build form component
|
|
211
|
-
build_detail # Build show/detail component
|
|
212
|
-
build_collection # Build table component
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### URL Generation
|
|
216
|
-
|
|
217
|
-
```ruby
|
|
218
|
-
resource_url_for(@post) # URL for record
|
|
219
|
-
resource_url_for(@post, action: :edit) # Edit URL
|
|
220
|
-
resource_url_for(Post) # Index URL
|
|
221
|
-
|
|
222
|
-
# With parent (nested resources)
|
|
223
|
-
resource_url_for(@comment, parent: @post) # Nested URL
|
|
224
|
-
resource_url_for(Comment, action: :new, parent: @post)
|
|
225
|
-
|
|
226
|
-
# Cross-package URLs
|
|
227
|
-
resource_url_for(@post, package: AdminPortal)
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
## Nested Resources
|
|
231
|
-
|
|
232
|
-
Parent records are automatically resolved from routes with the `nested_` prefix:
|
|
233
|
-
|
|
234
|
-
```ruby
|
|
235
|
-
# Route: /users/:user_id/nested_posts/:id
|
|
236
|
-
class PostsController < ::ResourceController
|
|
237
|
-
# current_parent returns the User
|
|
238
|
-
# current_nested_association returns :posts
|
|
239
|
-
# resource_record! returns the Post scoped to that User
|
|
240
|
-
end
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### Key Methods for Nested Resources
|
|
244
|
-
|
|
245
|
-
```ruby
|
|
246
|
-
current_parent # Parent record (e.g., User instance)
|
|
247
|
-
current_nested_association # Association name (e.g., :posts)
|
|
248
|
-
parent_route_param # URL param (e.g., :user_id)
|
|
249
|
-
parent_input_param # Form param (e.g., :user)
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
Parent fields are automatically excluded from forms/displays. Override with presentation hooks (see above).
|
|
253
|
-
|
|
254
|
-
### has_one Support
|
|
255
|
-
|
|
256
|
-
For `has_one` associations, routes are singular:
|
|
257
|
-
- `/users/:user_id/nested_profile` (no `:id` param)
|
|
258
|
-
- Index redirects to show (or new if no record exists)
|
|
259
|
-
|
|
260
|
-
## Entity Scoping (Multi-tenancy)
|
|
261
|
-
|
|
262
|
-
When a portal is scoped to an entity:
|
|
263
|
-
|
|
264
|
-
```ruby
|
|
265
|
-
# packages/admin_portal/lib/engine.rb
|
|
266
|
-
module AdminPortal
|
|
267
|
-
class Engine < Rails::Engine
|
|
268
|
-
include Plutonium::Portal::Engine
|
|
269
|
-
|
|
270
|
-
config.after_initialize do
|
|
271
|
-
scope_to_entity Organization, strategy: :path
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
end
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
Controllers automatically:
|
|
278
|
-
- Scope all queries to the entity
|
|
279
|
-
- Exclude entity field from forms (detected by association class)
|
|
280
|
-
- Inject entity value on create/update
|
|
281
|
-
- Provide `current_scoped_entity` method
|
|
282
|
-
|
|
283
|
-
### Association Detection
|
|
284
|
-
|
|
285
|
-
Plutonium auto-detects which `belongs_to` association points to the scoped entity class. This works even when `param_key` differs from the association name:
|
|
286
|
-
|
|
287
|
-
```ruby
|
|
288
|
-
# Portal config
|
|
289
|
-
scope_to_entity Competition::Team, param_key: :team
|
|
290
|
-
|
|
291
|
-
# Model (association name differs from param_key)
|
|
292
|
-
class Match < ApplicationRecord
|
|
293
|
-
belongs_to :competition_team # Plutonium finds this by class
|
|
294
|
-
end
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
### Multiple Associations to Same Class
|
|
298
|
-
|
|
299
|
-
If a model has multiple associations to the scoped entity class, Plutonium raises an error:
|
|
300
|
-
|
|
301
|
-
```
|
|
302
|
-
Match has multiple associations to Competition::Team: home_team, away_team.
|
|
303
|
-
Plutonium cannot auto-detect which one to use for entity scoping.
|
|
304
|
-
Override `scoped_entity_association` in your controller to specify the association.
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
Resolve by overriding `scoped_entity_association`:
|
|
308
|
-
|
|
309
|
-
```ruby
|
|
310
|
-
class MatchesController < ::ResourceController
|
|
311
|
-
private
|
|
312
|
-
|
|
313
|
-
def scoped_entity_association
|
|
314
|
-
:home_team # Return the association name as a symbol
|
|
315
|
-
end
|
|
316
|
-
end
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
## Authorization Verification
|
|
320
|
-
|
|
321
|
-
Controllers verify authorization was performed:
|
|
322
|
-
|
|
323
|
-
```ruby
|
|
324
|
-
# These run after every action
|
|
325
|
-
verify_authorize_current # Ensures authorize_current! was called
|
|
326
|
-
verify_current_authorized_scope # Ensures scope was loaded (except new/create)
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
To skip verification for custom actions:
|
|
330
|
-
|
|
331
|
-
```ruby
|
|
332
|
-
class PostsController < ::ResourceController
|
|
333
|
-
skip_verify_authorize_current only: [:custom_action]
|
|
334
|
-
|
|
335
|
-
def custom_action
|
|
336
|
-
# Handle authorization manually
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
## Response Formats
|
|
342
|
-
|
|
343
|
-
Controllers respond to multiple formats:
|
|
344
|
-
|
|
345
|
-
```ruby
|
|
346
|
-
def show
|
|
347
|
-
# Responds to:
|
|
348
|
-
# - HTML (default)
|
|
349
|
-
# - JSON (via RABL templates)
|
|
350
|
-
# - Turbo Stream (for Hotwire)
|
|
351
|
-
end
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
## Portal-Specific Controllers
|
|
355
|
-
|
|
356
|
-
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`.
|
|
357
|
-
|
|
358
|
-
```ruby
|
|
359
|
-
# With feature package controller:
|
|
360
|
-
class AdminPortal::PostsController < ::PostsController
|
|
361
|
-
include AdminPortal::Concerns::Controller
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
# Without feature package controller:
|
|
365
|
-
class AdminPortal::PostsController < AdminPortal::ResourceController
|
|
366
|
-
end
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
For non-resource portal pages (dashboard, settings), inherit from `PlutoniumController`:
|
|
370
|
-
|
|
371
|
-
```ruby
|
|
372
|
-
module AdminPortal
|
|
373
|
-
class DashboardController < PlutoniumController
|
|
374
|
-
def index
|
|
375
|
-
# Dashboard home
|
|
376
|
-
end
|
|
377
|
-
end
|
|
378
|
-
end
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
## Best Practices
|
|
382
|
-
|
|
383
|
-
1. **Keep controllers thin** - Use definitions for UI, policies for auth, interactions for logic
|
|
384
|
-
2. **Don't override CRUD actions** - Customize via hooks (`resource_params`, `redirect_url_after_submit`)
|
|
385
|
-
3. **Use interactive actions** - For custom operations, define in definition with interaction
|
|
386
|
-
4. **Let authorization work** - Don't skip verification without good reason
|
|
387
|
-
5. **Trust the framework** - Most customization belongs in definitions or policies
|
|
388
|
-
|
|
389
|
-
## Related Skills
|
|
390
|
-
|
|
391
|
-
- `plutonium` - How controllers fit in the resource architecture
|
|
392
|
-
- `plutonium-policy` - Authorization (used by controllers)
|
|
393
|
-
- `plutonium-definition` - Interactive actions (preferred over custom controller actions)
|
|
394
|
-
- `plutonium-views` - Custom page, form, display, and table classes
|
|
395
|
-
- `plutonium-nested-resources` - Parent/child routes and scoping
|
|
396
|
-
- `plutonium-model` - Resource models
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plutonium-create-resource
|
|
3
|
-
description: Use BEFORE running pu:res:scaffold or creating any new resource. Also when picking field types or generator options. Covers field syntax and scaffold options.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Create Resource Skill
|
|
7
|
-
|
|
8
|
-
## 🚨 Critical (read first)
|
|
9
|
-
- **Always use `pu:res:scaffold`.** Never hand-write models, migrations, policies, definitions, or controllers. Plutonium's conventions rely on files it generated.
|
|
10
|
-
- **Always pass `--dest`** (`--dest=main_app` or `--dest=package_name`) to skip the interactive destination prompt.
|
|
11
|
-
- **Quote fields with `?` or `{}`** to prevent shell expansion: `'field:type?'`, `'field:decimal{10,2}'`, `'field:decimal?{10,2}'`.
|
|
12
|
-
- **Run `pu:res:conn` next** to connect the resource to a portal — without it, the resource is invisible.
|
|
13
|
-
- **Related skills:** `plutonium-model` (model structure), `plutonium-definition` (UI config), `plutonium-policy` (authorization), `plutonium-portal` (connecting to portals).
|
|
14
|
-
|
|
15
|
-
Use the `pu:res:scaffold` generator to create complete resources in Plutonium applications.
|
|
16
|
-
|
|
17
|
-
## Quick checklist
|
|
18
|
-
|
|
19
|
-
Creating a new resource:
|
|
20
|
-
|
|
21
|
-
1. Pick a destination: `--dest=main_app` or `--dest=package_name`.
|
|
22
|
-
2. Identify field types (see field type syntax below). Quote fields with `?` or `{}`.
|
|
23
|
-
3. Run `rails g pu:res:scaffold ResourceName field:type ... --dest=<dest>`.
|
|
24
|
-
4. Review the generated migration — add cascade deletes, composite indexes, defaults.
|
|
25
|
-
5. Run `rails db:migrate`.
|
|
26
|
-
6. Run `rails g pu:res:conn ResourceName --dest=<portal_name>` to connect to a portal.
|
|
27
|
-
7. Verify routes: `bin/rails routes | grep resource_name`.
|
|
28
|
-
8. Customize the policy (`permitted_attributes_for_read`, `permitted_attributes_for_create`) as needed.
|
|
29
|
-
9. Open the portal route in the browser.
|
|
30
|
-
|
|
31
|
-
## Command Syntax
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
rails g pu:res:scaffold MODEL_NAME \
|
|
35
|
-
field1:type \
|
|
36
|
-
field2:type \
|
|
37
|
-
--dest=DESTINATION
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
**IMPORTANT**: Always specify `--dest` to avoid interactive prompts:
|
|
41
|
-
- `--dest=main_app` for resources in the main application
|
|
42
|
-
- `--dest=package_name` for resources in a feature package
|
|
43
|
-
|
|
44
|
-
**IMPORTANT**: Quote fields containing `?` or `{}` to prevent shell expansion:
|
|
45
|
-
```bash
|
|
46
|
-
'field:type?' # Nullable - must quote
|
|
47
|
-
'field:decimal{10,2}' # Options - must quote
|
|
48
|
-
'field:decimal?{10,2}' # Both - must quote
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## From Existing Models
|
|
52
|
-
|
|
53
|
-
For existing Rails projects with models you want to convert to Plutonium resources:
|
|
54
|
-
|
|
55
|
-
### Option 1: Model already includes Plutonium::Resource::Record
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
rails g pu:res:scaffold Post --no-migration --dest=main_app
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
This generates only the definition, policy, and controller - leaving your model unchanged.
|
|
62
|
-
|
|
63
|
-
### Option 2: Let the generator update the model
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
rails g pu:res:scaffold Post --dest=main_app
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Run without attributes to auto-import fields from `model.content_columns`. This regenerates the model file, so review changes carefully.
|
|
70
|
-
|
|
71
|
-
### Don't forget to include the module
|
|
72
|
-
|
|
73
|
-
Your model must include `Plutonium::Resource::Record` (directly or via inheritance):
|
|
74
|
-
|
|
75
|
-
```ruby
|
|
76
|
-
class Post < ApplicationRecord
|
|
77
|
-
include Plutonium::Resource::Record
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Or inherit from a base class
|
|
81
|
-
class Post < ResourceRecord
|
|
82
|
-
end
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## Field Type Syntax
|
|
86
|
-
|
|
87
|
-
Format: `name:type:index_type`
|
|
88
|
-
|
|
89
|
-
### Basic Types
|
|
90
|
-
|
|
91
|
-
| Syntax | Result |
|
|
92
|
-
|--------|--------|
|
|
93
|
-
| `name:string` | Required string |
|
|
94
|
-
| `'name:string?'` | Nullable string |
|
|
95
|
-
| `age:integer` | Required integer |
|
|
96
|
-
| `'age:integer?'` | Nullable integer |
|
|
97
|
-
| `active:boolean` | Required boolean |
|
|
98
|
-
| `'active:boolean?'` | Nullable boolean |
|
|
99
|
-
| `content:text` | Required text |
|
|
100
|
-
| `'content:text?'` | Nullable text |
|
|
101
|
-
| `birth_date:date` | Required date |
|
|
102
|
-
| `'anniversary:date?'` | Nullable date |
|
|
103
|
-
| `starts_at:datetime` | Required datetime |
|
|
104
|
-
| `'ends_at:datetime?'` | Nullable datetime |
|
|
105
|
-
| `alarm_time:time` | Required time |
|
|
106
|
-
| `'reminder_time:time?'` | Nullable time |
|
|
107
|
-
| `metadata:json` | JSON field |
|
|
108
|
-
| `settings:jsonb` | JSONB (PostgreSQL) |
|
|
109
|
-
| `external_id:uuid` | UUID field |
|
|
110
|
-
|
|
111
|
-
### PostgreSQL-Specific Types
|
|
112
|
-
|
|
113
|
-
These types work in both PostgreSQL and SQLite (automatically mapped):
|
|
114
|
-
|
|
115
|
-
| Type | PostgreSQL | SQLite |
|
|
116
|
-
|------|------------|--------|
|
|
117
|
-
| `jsonb` | `jsonb` | `json` |
|
|
118
|
-
| `hstore` | `hstore` | `json` |
|
|
119
|
-
| `uuid` | `uuid` | `string` |
|
|
120
|
-
| `inet` | `inet` | `string` |
|
|
121
|
-
| `cidr` | `cidr` | `string` |
|
|
122
|
-
| `macaddr` | `macaddr` | `string` |
|
|
123
|
-
| `ltree` | `ltree` | `string` |
|
|
124
|
-
|
|
125
|
-
### Default Values
|
|
126
|
-
|
|
127
|
-
Use `{default:value}` syntax for default values:
|
|
128
|
-
|
|
129
|
-
```bash
|
|
130
|
-
'status:string{default:draft}' # String default
|
|
131
|
-
'active:boolean{default:true}' # Boolean default (true/false/yes/1)
|
|
132
|
-
'priority:integer{default:0}' # Integer default
|
|
133
|
-
'rating:float{default:4.5}' # Float default
|
|
134
|
-
'status:string?{default:pending}' # Nullable with default
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### JSON/JSONB Default Values
|
|
138
|
-
|
|
139
|
-
Supports nested braces for structured defaults:
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
'metadata:jsonb{default:{}}' # Empty hash
|
|
143
|
-
'tags:jsonb{default:[]}' # Empty array
|
|
144
|
-
'settings:jsonb{default:{"theme":"dark"}}' # Object with values
|
|
145
|
-
'config:jsonb?{default:{}}' # Nullable with empty hash default
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Default values are parsed as JSON first. If JSON parsing fails, the value is treated as a string (or coerced based on column type for integers, floats, and booleans).
|
|
149
|
-
|
|
150
|
-
### Decimal with Precision and Default
|
|
151
|
-
|
|
152
|
-
```bash
|
|
153
|
-
'amount:decimal{10,2}' # precision: 10, scale: 2
|
|
154
|
-
'price:decimal{10,2,default:0}' # with default value
|
|
155
|
-
'balance:decimal?{15,2,default:0}' # nullable with precision and default
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
### References/Associations
|
|
159
|
-
|
|
160
|
-
```bash
|
|
161
|
-
company:belongs_to # Required foreign key
|
|
162
|
-
'parent:belongs_to?' # Nullable (null: true + optional: true)
|
|
163
|
-
user:references # Same as belongs_to
|
|
164
|
-
blogging/post:belongs_to # Cross-package reference
|
|
165
|
-
'author:belongs_to{class_name:User}' # Custom class_name (author_id -> User)
|
|
166
|
-
'reviewer:belongs_to?{class_name:User}' # Nullable with class_name
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
Nullable references generate:
|
|
170
|
-
- Migration: `null: true`
|
|
171
|
-
- Model: `belongs_to :parent, optional: true`
|
|
172
|
-
|
|
173
|
-
### Index Types (third segment)
|
|
174
|
-
|
|
175
|
-
```bash
|
|
176
|
-
email:string:index # Regular index
|
|
177
|
-
email:string:uniq # Unique index
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### Special Types
|
|
181
|
-
|
|
182
|
-
```bash
|
|
183
|
-
password_digest # has_secure_password
|
|
184
|
-
auth_token:token # has_secure_token (auto unique index)
|
|
185
|
-
content:rich_text # has_rich_text
|
|
186
|
-
avatar:attachment # has_one_attached
|
|
187
|
-
photos:attachments # has_many_attached
|
|
188
|
-
price_cents:integer # has_cents (money field)
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
Token fields automatically get a unique index in the migration.
|
|
192
|
-
|
|
193
|
-
## Generator Options
|
|
194
|
-
|
|
195
|
-
- `--dest=DESTINATION` - Target destination (**always required** to avoid prompts)
|
|
196
|
-
- `main_app` for main application resources
|
|
197
|
-
- `package_name` for feature package resources
|
|
198
|
-
- `--no-model` - Skip model generation (keeps existing model)
|
|
199
|
-
- `--no-migration` - Skip migration generation (use with `--no-model` for existing models)
|
|
200
|
-
|
|
201
|
-
## What Gets Generated
|
|
202
|
-
|
|
203
|
-
For **main_app** resources:
|
|
204
|
-
1. **Model** - `app/models/model_name.rb`
|
|
205
|
-
2. **Migration** - `db/migrate/xxx_create_model_names.rb`
|
|
206
|
-
3. **Controller** - `app/controllers/model_names_controller.rb`
|
|
207
|
-
4. **Policy** - `app/policies/model_name_policy.rb`
|
|
208
|
-
5. **Definition** - `app/definitions/model_name_definition.rb`
|
|
209
|
-
|
|
210
|
-
For **packaged** resources:
|
|
211
|
-
1. **Model** - `app/models/package_name/model_name.rb`
|
|
212
|
-
2. **Migration** - `db/migrate/xxx_create_package_name_model_names.rb`
|
|
213
|
-
3. **Controller** - `packages/package_name/app/controllers/package_name/model_names_controller.rb`
|
|
214
|
-
4. **Policy** - `packages/package_name/app/policies/package_name/model_name_policy.rb`
|
|
215
|
-
5. **Definition** - `packages/package_name/app/definitions/package_name/model_name_definition.rb`
|
|
216
|
-
|
|
217
|
-
## Migration Customizations
|
|
218
|
-
|
|
219
|
-
The generator creates basic migrations. **Always review and customize** the migration before running:
|
|
220
|
-
|
|
221
|
-
### Inline Indexes (preferred)
|
|
222
|
-
|
|
223
|
-
```ruby
|
|
224
|
-
create_table :model_names do |t|
|
|
225
|
-
t.belongs_to :parent, null: false, foreign_key: true
|
|
226
|
-
t.string :name, null: false
|
|
227
|
-
|
|
228
|
-
t.timestamps
|
|
229
|
-
|
|
230
|
-
t.index :name
|
|
231
|
-
t.index [:parent_id, :name], unique: true
|
|
232
|
-
end
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Cascade Delete
|
|
236
|
-
|
|
237
|
-
```ruby
|
|
238
|
-
t.belongs_to :parent, null: false, foreign_key: {on_delete: :cascade}
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Default Values
|
|
242
|
-
|
|
243
|
-
Default values can be set directly in the generator using `{default:value}` syntax (see Field Type Syntax above). For more complex defaults or expressions, edit the migration:
|
|
244
|
-
|
|
245
|
-
```ruby
|
|
246
|
-
t.boolean :is_active, default: true
|
|
247
|
-
t.integer :status, default: 0
|
|
248
|
-
t.datetime :published_at, default: -> { "CURRENT_TIMESTAMP" }
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
## Examples
|
|
252
|
-
|
|
253
|
-
### Main App Resource
|
|
254
|
-
|
|
255
|
-
```bash
|
|
256
|
-
rails g pu:res:scaffold Post \
|
|
257
|
-
user:belongs_to \
|
|
258
|
-
title:string \
|
|
259
|
-
'content:text?' \
|
|
260
|
-
'published_at:datetime?' \
|
|
261
|
-
--dest=main_app
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### Resource with Precision and Indexes
|
|
265
|
-
|
|
266
|
-
```bash
|
|
267
|
-
rails g pu:res:scaffold Property \
|
|
268
|
-
company:belongs_to \
|
|
269
|
-
code:string:uniq \
|
|
270
|
-
'latitude:decimal{11,8}' \
|
|
271
|
-
'longitude:decimal?{11,8}' \
|
|
272
|
-
'value:decimal?{15,2}' \
|
|
273
|
-
'notes:text?' \
|
|
274
|
-
--dest=main_app
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
### Optional Association
|
|
278
|
-
|
|
279
|
-
```bash
|
|
280
|
-
rails g pu:res:scaffold Comment \
|
|
281
|
-
user:belongs_to \
|
|
282
|
-
'parent:belongs_to?' \
|
|
283
|
-
body:text \
|
|
284
|
-
--dest=blogging
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
### Cross-Package Reference
|
|
288
|
-
|
|
289
|
-
```bash
|
|
290
|
-
rails g pu:res:scaffold Comment \
|
|
291
|
-
user:belongs_to \
|
|
292
|
-
blogging/post:belongs_to \
|
|
293
|
-
body:text \
|
|
294
|
-
--dest=comments
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
## After Generation
|
|
298
|
-
|
|
299
|
-
1. **Review and customize the migration** (add cascade delete, defaults, composite indexes)
|
|
300
|
-
2. Run `rails db:migrate`
|
|
301
|
-
3. Connect resource to portal: `rails g pu:res:conn Post --dest=admin_portal`
|
|
302
|
-
4. Customize policy permissions as needed
|
|
303
|
-
5. Add definition customizations for UI behavior
|