plutonium 0.37.0 → 0.38.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-controller/SKILL.md +25 -2
- data/.claude/skills/plutonium-definition-fields/SKILL.md +33 -0
- data/.claude/skills/plutonium-nested-resources/SKILL.md +79 -19
- data/.claude/skills/plutonium-policy/SKILL.md +93 -6
- data/CHANGELOG.md +36 -0
- data/CLAUDE.md +8 -10
- data/CONTRIBUTING.md +6 -8
- data/Rakefile +16 -1
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +9371 -11492
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +55 -55
- data/app/assets/plutonium.min.js.map +4 -4
- data/docs/guides/index.md +5 -0
- data/docs/guides/nested-resources.md +132 -29
- data/docs/guides/troubleshooting.md +82 -0
- data/docs/reference/controller/index.md +1 -1
- data/docs/reference/definition/fields.md +33 -0
- data/docs/reference/model/index.md +1 -1
- data/docs/reference/policy/index.md +77 -6
- data/gemfiles/rails_7.gemfile.lock +3 -3
- data/gemfiles/rails_8.0.gemfile.lock +3 -3
- data/gemfiles/rails_8.1.gemfile.lock +3 -3
- data/lib/plutonium/core/controller.rb +144 -19
- data/lib/plutonium/core/controllers/association_resolver.rb +86 -0
- data/lib/plutonium/helpers/display_helper.rb +12 -0
- data/lib/plutonium/query/filters/association.rb +25 -3
- data/lib/plutonium/resource/controller.rb +90 -9
- data/lib/plutonium/resource/controllers/authorizable.rb +17 -4
- data/lib/plutonium/resource/controllers/crud_actions.rb +7 -5
- data/lib/plutonium/resource/controllers/interactive_actions.rb +9 -0
- data/lib/plutonium/resource/controllers/presentable.rb +13 -11
- data/lib/plutonium/resource/policy.rb +85 -2
- data/lib/plutonium/resource/record/routes.rb +31 -1
- data/lib/plutonium/routing/mapper_extensions.rb +40 -4
- data/lib/plutonium/routing/route_set_extensions.rb +3 -0
- data/lib/plutonium/ui/breadcrumbs.rb +1 -1
- data/lib/plutonium/ui/display/resource.rb +5 -2
- data/lib/plutonium/ui/form/components/key_value_store.rb +17 -5
- data/lib/plutonium/ui/page/index.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/lib/tasks/release.rake +1 -1
- data/package.json +6 -5
- data/plutonium.gemspec +1 -1
- data/src/js/controllers/key_value_store_controller.js +6 -0
- data/src/js/controllers/resource_drop_down_controller.js +3 -3
- data/yarn.lock +1465 -693
- metadata +6 -5
- data/app/javascript/controllers/key_value_store_controller.js +0 -119
data/docs/guides/index.md
CHANGED
|
@@ -27,6 +27,10 @@ These guides show you how to accomplish specific tasks, with complete examples.
|
|
|
27
27
|
|
|
28
28
|
- [Theming](./theming) - Customize colors, styles, and branding
|
|
29
29
|
|
|
30
|
+
### Help
|
|
31
|
+
|
|
32
|
+
- [Troubleshooting](./troubleshooting) - Common issues and solutions
|
|
33
|
+
|
|
30
34
|
## Finding What You Need
|
|
31
35
|
|
|
32
36
|
| I want to... | Guide |
|
|
@@ -39,6 +43,7 @@ These guides show you how to accomplish specific tasks, with complete examples.
|
|
|
39
43
|
| Separate data by company | [Multi-tenancy](./multi-tenancy) |
|
|
40
44
|
| Add search to a list | [Search and Filtering](./search-filtering) |
|
|
41
45
|
| Change the color scheme | [Theming](./theming) |
|
|
46
|
+
| Fix a confusing error | [Troubleshooting](./troubleshooting) |
|
|
42
47
|
|
|
43
48
|
## Looking for Reference Docs?
|
|
44
49
|
|
|
@@ -4,7 +4,7 @@ This guide covers setting up parent/child resource relationships.
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
Nested resources create URLs like `/posts/1/
|
|
7
|
+
Nested resources create URLs like `/posts/1/nested_comments` where comments belong to a specific post. Plutonium automatically handles:
|
|
8
8
|
|
|
9
9
|
- Scoping queries to the parent
|
|
10
10
|
- Assigning parent to new records
|
|
@@ -12,6 +12,8 @@ Nested resources create URLs like `/posts/1/comments` where comments belong to a
|
|
|
12
12
|
- URL generation with parent context
|
|
13
13
|
- Breadcrumb navigation
|
|
14
14
|
|
|
15
|
+
Plutonium supports both `has_many` (plural routes) and `has_one` (singular routes) associations.
|
|
16
|
+
|
|
15
17
|
## Setting Up Nested Resources
|
|
16
18
|
|
|
17
19
|
### 1. Define the Association
|
|
@@ -20,12 +22,17 @@ Nested resources create URLs like `/posts/1/comments` where comments belong to a
|
|
|
20
22
|
# Parent model
|
|
21
23
|
class Post < ResourceRecord
|
|
22
24
|
has_many :comments, dependent: :destroy
|
|
25
|
+
has_one :post_metadata, dependent: :destroy
|
|
23
26
|
end
|
|
24
27
|
|
|
25
|
-
# Child
|
|
28
|
+
# Child models
|
|
26
29
|
class Comment < ResourceRecord
|
|
27
30
|
belongs_to :post
|
|
28
31
|
end
|
|
32
|
+
|
|
33
|
+
class PostMetadata < ResourceRecord
|
|
34
|
+
belongs_to :post
|
|
35
|
+
end
|
|
29
36
|
```
|
|
30
37
|
|
|
31
38
|
### 2. Register Both Resources
|
|
@@ -35,15 +42,25 @@ end
|
|
|
35
42
|
AdminPortal::Engine.routes.draw do
|
|
36
43
|
register_resource ::Post
|
|
37
44
|
register_resource ::Comment
|
|
45
|
+
register_resource ::PostMetadata
|
|
38
46
|
end
|
|
39
47
|
```
|
|
40
48
|
|
|
41
|
-
Plutonium automatically creates nested routes based on the `belongs_to` association:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- `GET /posts/:post_id/
|
|
49
|
+
Plutonium automatically creates nested routes with a `nested_` prefix based on the `belongs_to` association:
|
|
50
|
+
|
|
51
|
+
**has_many routes (plural):**
|
|
52
|
+
- `GET /posts/:post_id/nested_comments`
|
|
53
|
+
- `GET /posts/:post_id/nested_comments/new`
|
|
54
|
+
- `GET /posts/:post_id/nested_comments/:id`
|
|
45
55
|
- etc.
|
|
46
56
|
|
|
57
|
+
**has_one routes (singular):**
|
|
58
|
+
- `GET /posts/:post_id/nested_post_metadata`
|
|
59
|
+
- `GET /posts/:post_id/nested_post_metadata/new`
|
|
60
|
+
- `GET /posts/:post_id/nested_post_metadata/edit`
|
|
61
|
+
|
|
62
|
+
The `nested_` prefix prevents route conflicts when the same resource is registered both as a top-level and nested resource.
|
|
63
|
+
|
|
47
64
|
### 3. Enable Association Panel
|
|
48
65
|
|
|
49
66
|
Show comments on the post detail page:
|
|
@@ -112,30 +129,46 @@ parent_input_param # => :post
|
|
|
112
129
|
Use `resource_url_for` with the `parent:` option:
|
|
113
130
|
|
|
114
131
|
```ruby
|
|
115
|
-
# Child collection
|
|
132
|
+
# Child collection (has_many)
|
|
116
133
|
resource_url_for(Comment, parent: @post)
|
|
117
|
-
# => /posts/123/
|
|
134
|
+
# => /posts/123/nested_comments
|
|
118
135
|
|
|
119
136
|
# Child record
|
|
120
137
|
resource_url_for(@comment, parent: @post)
|
|
121
|
-
# => /posts/123/
|
|
138
|
+
# => /posts/123/nested_comments/456
|
|
122
139
|
|
|
123
140
|
# New child form
|
|
124
141
|
resource_url_for(Comment, action: :new, parent: @post)
|
|
125
|
-
# => /posts/123/
|
|
142
|
+
# => /posts/123/nested_comments/new
|
|
126
143
|
|
|
127
144
|
# Edit child
|
|
128
145
|
resource_url_for(@comment, action: :edit, parent: @post)
|
|
129
|
-
# => /posts/123/
|
|
146
|
+
# => /posts/123/nested_comments/456/edit
|
|
147
|
+
|
|
148
|
+
# Singular resource (has_one)
|
|
149
|
+
resource_url_for(@post_metadata, parent: @post)
|
|
150
|
+
# => /posts/123/nested_post_metadata
|
|
151
|
+
|
|
152
|
+
resource_url_for(PostMetadata, action: :new, parent: @post)
|
|
153
|
+
# => /posts/123/nested_post_metadata/new
|
|
130
154
|
```
|
|
131
155
|
|
|
132
156
|
Within a nested context, `parent:` defaults to `current_parent`:
|
|
133
157
|
|
|
134
158
|
```ruby
|
|
135
|
-
# In CommentsController under /posts/:post_id/
|
|
159
|
+
# In CommentsController under /posts/:post_id/nested_comments
|
|
136
160
|
resource_url_for(@comment) # parent: current_parent is automatic
|
|
137
161
|
```
|
|
138
162
|
|
|
163
|
+
### Cross-Package URL Generation
|
|
164
|
+
|
|
165
|
+
Generate URLs for resources in a different package:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
# From AdminPortal, generate URL to CustomerPortal resource
|
|
169
|
+
resource_url_for(@comment, parent: @post, package: CustomerPortal)
|
|
170
|
+
```
|
|
171
|
+
|
|
139
172
|
## Presentation Hooks
|
|
140
173
|
|
|
141
174
|
Control whether the parent field appears:
|
|
@@ -167,23 +200,47 @@ The parent is authorized for `:read?` before being returned:
|
|
|
167
200
|
authorize! parent, to: :read?
|
|
168
201
|
```
|
|
169
202
|
|
|
170
|
-
###
|
|
203
|
+
### Parent Scoping Context
|
|
171
204
|
|
|
172
|
-
|
|
205
|
+
For nested resources, policies receive `parent` and `parent_association` context. This is used for automatic query scoping:
|
|
173
206
|
|
|
174
207
|
```ruby
|
|
175
208
|
class CommentPolicy < ResourcePolicy
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
209
|
+
# Available context:
|
|
210
|
+
# - parent: the parent record (e.g., Post instance)
|
|
211
|
+
# - parent_association: the association name (e.g., :comments)
|
|
212
|
+
# - entity_scope: the scoped entity (for multi-tenancy)
|
|
180
213
|
|
|
181
|
-
|
|
182
|
-
|
|
214
|
+
relation_scope do |relation|
|
|
215
|
+
relation = super(relation) # Applies parent scoping automatically
|
|
216
|
+
relation
|
|
183
217
|
end
|
|
218
|
+
end
|
|
219
|
+
```
|
|
184
220
|
|
|
185
|
-
|
|
186
|
-
|
|
221
|
+
**Parent scoping takes precedence over entity scoping** - when a parent is present, the policy scopes via the parent association rather than the entity scope. This prevents double-scoping since the parent was already authorized and entity-scoped.
|
|
222
|
+
|
|
223
|
+
### has_many vs has_one Scoping
|
|
224
|
+
|
|
225
|
+
For **has_many** associations, scoping uses the association directly:
|
|
226
|
+
```ruby
|
|
227
|
+
parent.send(parent_association) # e.g., post.comments
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
For **has_one** associations, scoping uses a where clause:
|
|
231
|
+
```ruby
|
|
232
|
+
relation.where(foreign_key => parent.id) # e.g., where(post_id: post.id)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Entity Scope Fallback
|
|
236
|
+
|
|
237
|
+
When no parent is present (top-level resource access), entity_scope is used:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
class CommentPolicy < ResourcePolicy
|
|
241
|
+
def create?
|
|
242
|
+
# entity_scope is available for multi-tenancy
|
|
243
|
+
entity_scope.present? && user.can_comment_on?(entity_scope)
|
|
187
244
|
end
|
|
188
245
|
end
|
|
189
246
|
```
|
|
@@ -195,7 +252,7 @@ Add role-based filtering on top of parent scoping:
|
|
|
195
252
|
```ruby
|
|
196
253
|
class CommentPolicy < ResourcePolicy
|
|
197
254
|
relation_scope do |relation|
|
|
198
|
-
relation = super(relation) # Applies
|
|
255
|
+
relation = super(relation) # Applies parent scoping first
|
|
199
256
|
|
|
200
257
|
if user.moderator?
|
|
201
258
|
relation
|
|
@@ -206,6 +263,33 @@ class CommentPolicy < ResourcePolicy
|
|
|
206
263
|
end
|
|
207
264
|
```
|
|
208
265
|
|
|
266
|
+
### default_relation_scope is Required
|
|
267
|
+
|
|
268
|
+
Plutonium verifies that `default_relation_scope` is called in every `relation_scope` to prevent multi-tenancy leaks:
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
# ❌ This will raise an error
|
|
272
|
+
relation_scope do |relation|
|
|
273
|
+
relation.where(approved: true) # Missing default_relation_scope!
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# ✅ Correct
|
|
277
|
+
relation_scope do |relation|
|
|
278
|
+
default_relation_scope(relation).where(approved: true)
|
|
279
|
+
end
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
When overriding an inherited scope but still wanting parent scoping:
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
class AdminCommentPolicy < CommentPolicy
|
|
286
|
+
relation_scope do |relation|
|
|
287
|
+
# Replace inherited scope but keep parent scoping
|
|
288
|
+
default_relation_scope(relation)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
```
|
|
292
|
+
|
|
209
293
|
## Association Panels
|
|
210
294
|
|
|
211
295
|
Associations listed in `permitted_associations` appear on the parent's show page:
|
|
@@ -257,15 +341,34 @@ class PostPolicy < ResourcePolicy
|
|
|
257
341
|
end
|
|
258
342
|
```
|
|
259
343
|
|
|
344
|
+
## has_one Associations
|
|
345
|
+
|
|
346
|
+
Plutonium supports `has_one` associations with singular routes:
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
class Post < ResourceRecord
|
|
350
|
+
has_one :post_metadata, dependent: :destroy
|
|
351
|
+
end
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Routes generated:
|
|
355
|
+
- `GET /posts/:post_id/nested_post_metadata` - Show metadata
|
|
356
|
+
- `GET /posts/:post_id/nested_post_metadata/new` - New metadata form
|
|
357
|
+
- `GET /posts/:post_id/nested_post_metadata/edit` - Edit metadata form
|
|
358
|
+
- `PATCH /posts/:post_id/nested_post_metadata` - Update metadata
|
|
359
|
+
- `DELETE /posts/:post_id/nested_post_metadata` - Delete metadata
|
|
360
|
+
|
|
361
|
+
Note: No `:id` parameter in singular routes - only one record can exist per parent.
|
|
362
|
+
|
|
260
363
|
## Nesting Depth
|
|
261
364
|
|
|
262
365
|
Plutonium supports **one level of nesting**:
|
|
263
366
|
|
|
264
|
-
- `/posts/:post_id/
|
|
265
|
-
- `/comments/:comment_id/
|
|
367
|
+
- `/posts/:post_id/nested_comments` (parent → child)
|
|
368
|
+
- `/comments/:comment_id/nested_replies` (parent → child)
|
|
266
369
|
|
|
267
370
|
Not supported:
|
|
268
|
-
- `/posts/:post_id/
|
|
371
|
+
- `/posts/:post_id/nested_comments/:comment_id/nested_replies` (grandparent → parent → child)
|
|
269
372
|
|
|
270
373
|
### Working with Deep Hierarchies
|
|
271
374
|
|
|
@@ -295,9 +398,9 @@ end
|
|
|
295
398
|
```
|
|
296
399
|
|
|
297
400
|
Generates nested routes:
|
|
298
|
-
- `POST /posts/:post_id/
|
|
299
|
-
- `POST /posts/:post_id/
|
|
300
|
-
- `GET /posts/:post_id/
|
|
401
|
+
- `POST /posts/:post_id/nested_comments/:id/approve`
|
|
402
|
+
- `POST /posts/:post_id/nested_comments/:id/flag`
|
|
403
|
+
- `GET /posts/:post_id/nested_comments/pending`
|
|
301
404
|
|
|
302
405
|
## Breadcrumbs
|
|
303
406
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Common issues and their solutions when working with Plutonium.
|
|
4
|
+
|
|
5
|
+
## Resource Class Detection
|
|
6
|
+
|
|
7
|
+
### "Failed to determine the resource class" Error
|
|
8
|
+
|
|
9
|
+
**Error messages:**
|
|
10
|
+
```
|
|
11
|
+
NameError: Failed to determine the resource class for MyPortal::PostMetadataController.
|
|
12
|
+
Rails singularized "PostMetadata" to "PostMetadatum", but "PostMetadata" exists.
|
|
13
|
+
Add an inflection rule to config/initializers/inflections.rb.
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
or:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
NameError: Failed to determine the resource class.
|
|
20
|
+
Please call `controller_for(MyResource)` in MyPortal::MyResourceController.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Cause:** Plutonium infers the resource class from the controller name by singularizing it. For resources with names that don't follow standard Rails pluralization (like `PostMetadata`), Rails may singularize incorrectly (`PostMetadata` → `PostMetadatum`). Plutonium detects this and provides a helpful error message.
|
|
24
|
+
|
|
25
|
+
**Solution:** Add a custom inflection rule in `config/initializers/inflections.rb`:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
29
|
+
# Preserve "Metadata" when singularizing (e.g., PostMetadata stays PostMetadata)
|
|
30
|
+
inflect.singular(/(M)etadata$/i, '\1etadata')
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This ensures Rails correctly handles the singularization throughout your application, including:
|
|
35
|
+
- Controller to model resolution
|
|
36
|
+
- Route generation
|
|
37
|
+
- Association lookups
|
|
38
|
+
|
|
39
|
+
**Alternative:** If you can't modify inflections, explicitly set the resource class in your controller:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
class MyPortal::PostMetadataController < MyPortal::ResourceController
|
|
43
|
+
controller_for Blogging::PostMetadata
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Common Words Needing Inflection Rules
|
|
48
|
+
|
|
49
|
+
| Word | Without Rule | With Rule | Inflection |
|
|
50
|
+
|------|--------------|-----------|------------|
|
|
51
|
+
| Metadata | `PostMetadatum` | `PostMetadata` | `inflect.singular(/(M)etadata$/i, '\1etadata')` |
|
|
52
|
+
| Media | `PostMedium` | `PostMedia` | `inflect.singular(/(M)edia$/i, '\1edia')` |
|
|
53
|
+
| Data | `PostDatum` | `PostData` | `inflect.singular(/(D)ata$/i, '\1ata')` |
|
|
54
|
+
| Criteria | `SearchCriterium` | `SearchCriteria` | `inflect.singular(/(C)riteria$/i, '\1riteria')` |
|
|
55
|
+
|
|
56
|
+
## URL Generation
|
|
57
|
+
|
|
58
|
+
### Wrong URLs for Nested Resources
|
|
59
|
+
|
|
60
|
+
**Symptom:** URLs for nested resources include unexpected IDs or route to the wrong path.
|
|
61
|
+
|
|
62
|
+
**Cause:** Rails "param recall" fills in missing parameters from the current request when generating URLs. This can cause issues when both top-level and nested routes exist for the same resource.
|
|
63
|
+
|
|
64
|
+
**Solution:** Plutonium handles this automatically by using named route helpers for nested resources. Ensure you're using `resource_url_for` instead of `url_for`:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# Good - uses Plutonium's smart URL generation
|
|
68
|
+
resource_url_for(@comment, parent: @post)
|
|
69
|
+
|
|
70
|
+
# May have issues with param recall
|
|
71
|
+
url_for(controller: 'comments', action: 'show', id: @comment.id)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Need More Help?
|
|
75
|
+
|
|
76
|
+
If you encounter an issue not covered here, please [open an issue](https://github.com/radioactive-labs/plutonium-core/issues) on GitHub.
|
|
77
|
+
|
|
78
|
+
## Related
|
|
79
|
+
|
|
80
|
+
- [Nested Resources Guide](./nested-resources)
|
|
81
|
+
- [Adding Resources Guide](./adding-resources)
|
|
82
|
+
- [Rails Inflections Documentation](https://api.rubyonrails.org/classes/ActiveSupport/Inflector/Inflections.html)
|
|
@@ -258,7 +258,7 @@ end
|
|
|
258
258
|
Parent records are automatically resolved:
|
|
259
259
|
|
|
260
260
|
```ruby
|
|
261
|
-
# Route: /users/:user_id/
|
|
261
|
+
# Route: /users/:user_id/nested_posts/:id
|
|
262
262
|
class PostsController < ::ResourceController
|
|
263
263
|
# current_parent returns the User
|
|
264
264
|
# resource_record! returns the Post scoped to that User
|
|
@@ -232,6 +232,39 @@ column :status, align: :center # Center
|
|
|
232
232
|
column :amount, align: :end # Right
|
|
233
233
|
```
|
|
234
234
|
|
|
235
|
+
## Value Formatting
|
|
236
|
+
|
|
237
|
+
Use `formatter` for simple value transformations without a full block:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
# Truncate long text
|
|
241
|
+
column :description, formatter: ->(value) { value&.truncate(30) }
|
|
242
|
+
|
|
243
|
+
# Format numbers
|
|
244
|
+
column :price, formatter: ->(value) { "$%.2f" % value if value }
|
|
245
|
+
|
|
246
|
+
# Transform values
|
|
247
|
+
column :status, formatter: ->(value) { value&.humanize&.upcase }
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The `formatter` option:
|
|
251
|
+
- Receives the field value as its argument
|
|
252
|
+
- Returns the transformed value for display
|
|
253
|
+
- Works with `column` and `display` declarations
|
|
254
|
+
- Is simpler than block syntax when you only need to transform the value
|
|
255
|
+
|
|
256
|
+
**formatter vs block:** Use `formatter` when you only need the value. Use a block when you need access to the full record:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
# formatter - receives just the value
|
|
260
|
+
column :name, formatter: ->(value) { value&.titleize }
|
|
261
|
+
|
|
262
|
+
# block - receives the full record
|
|
263
|
+
column :full_name do |record|
|
|
264
|
+
"#{record.first_name} #{record.last_name}"
|
|
265
|
+
end
|
|
266
|
+
```
|
|
267
|
+
|
|
235
268
|
## Nested Inputs
|
|
236
269
|
|
|
237
270
|
Render inline forms for associated records. Requires `accepts_nested_attributes_for` on the model.
|
|
@@ -67,7 +67,7 @@ class Comment < ResourceRecord
|
|
|
67
67
|
end
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
When both `Post` and `Comment` are registered in a portal, Plutonium automatically creates nested routes (`/posts/:post_id/
|
|
70
|
+
When both `Post` and `Comment` are registered in a portal, Plutonium automatically creates nested routes (`/posts/:post_id/nested_comments`). Queries are automatically scoped to the parent via the association.
|
|
71
71
|
|
|
72
72
|
See the [Nested Resources Guide](/guides/nested-resources) for details.
|
|
73
73
|
|
|
@@ -36,12 +36,16 @@ Inside a policy, you have access to:
|
|
|
36
36
|
| `user` | Current authenticated user (required) |
|
|
37
37
|
| `record` | Resource being authorized |
|
|
38
38
|
| `entity_scope` | Current scoped entity (for multi-tenancy) |
|
|
39
|
+
| `parent` | Parent record for nested resources (nil if not nested) |
|
|
40
|
+
| `parent_association` | Association name on parent (e.g., `:comments`) |
|
|
39
41
|
|
|
40
42
|
```ruby
|
|
41
43
|
def update?
|
|
42
|
-
user
|
|
43
|
-
record
|
|
44
|
-
entity_scope
|
|
44
|
+
user # => Current user
|
|
45
|
+
record # => The Post instance
|
|
46
|
+
entity_scope # => Organization for multi-tenant portals
|
|
47
|
+
parent # => Parent record (for nested routes)
|
|
48
|
+
parent_association # => :comments (association name)
|
|
45
49
|
end
|
|
46
50
|
```
|
|
47
51
|
|
|
@@ -218,9 +222,29 @@ class PostPolicy < Plutonium::Resource::Policy
|
|
|
218
222
|
end
|
|
219
223
|
```
|
|
220
224
|
|
|
221
|
-
### With
|
|
225
|
+
### With Parent Scoping (Nested Resources)
|
|
222
226
|
|
|
223
|
-
Call `super` to
|
|
227
|
+
Call `super` to apply automatic parent scoping for nested resources:
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
relation_scope do |relation|
|
|
231
|
+
relation = super(relation) # Applies parent scoping automatically
|
|
232
|
+
|
|
233
|
+
if user.admin?
|
|
234
|
+
relation
|
|
235
|
+
else
|
|
236
|
+
relation.where(approved: true)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Parent scoping takes precedence over entity scoping.** When a parent is present:
|
|
242
|
+
- For `has_many` associations: scopes via `parent.association_name`
|
|
243
|
+
- For `has_one` associations: scopes via `where(foreign_key: parent.id)`
|
|
244
|
+
|
|
245
|
+
### With Entity Scoping (Multi-tenancy)
|
|
246
|
+
|
|
247
|
+
When no parent is present, `super` applies entity scoping:
|
|
224
248
|
|
|
225
249
|
```ruby
|
|
226
250
|
relation_scope do |relation|
|
|
@@ -234,7 +258,54 @@ relation_scope do |relation|
|
|
|
234
258
|
end
|
|
235
259
|
```
|
|
236
260
|
|
|
237
|
-
The default `relation_scope` automatically applies `relation.associated_with(entity_scope)` when an entity scope is present.
|
|
261
|
+
The default `relation_scope` automatically applies `relation.associated_with(entity_scope)` when an entity scope is present and no parent is set.
|
|
262
|
+
|
|
263
|
+
### default_relation_scope is Required
|
|
264
|
+
|
|
265
|
+
Plutonium verifies that `default_relation_scope` is called in every `relation_scope`. This prevents accidental multi-tenancy leaks when overriding scopes.
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
# ❌ This will raise an error
|
|
269
|
+
relation_scope do |relation|
|
|
270
|
+
relation.where(published: true) # Missing default_relation_scope!
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# ✅ Correct - call default_relation_scope
|
|
274
|
+
relation_scope do |relation|
|
|
275
|
+
default_relation_scope(relation).where(published: true)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# ✅ Also correct - super calls default_relation_scope
|
|
279
|
+
relation_scope do |relation|
|
|
280
|
+
super(relation).where(published: true)
|
|
281
|
+
end
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
When overriding an inherited scope:
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
class AdminPostPolicy < PostPolicy
|
|
288
|
+
relation_scope do |relation|
|
|
289
|
+
# Replace inherited scope but keep Plutonium's parent/entity scoping
|
|
290
|
+
default_relation_scope(relation)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
This method applies parent scoping (for nested resources) or entity scoping (for multi-tenancy) directly, bypassing any inherited scope customizations.
|
|
296
|
+
|
|
297
|
+
### Skipping Default Scoping
|
|
298
|
+
|
|
299
|
+
If you intentionally need to bypass scoping, call `skip_default_relation_scope!`:
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
relation_scope do |relation|
|
|
303
|
+
skip_default_relation_scope!
|
|
304
|
+
relation # No parent/entity scoping applied
|
|
305
|
+
end
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
This should be rare - consider using a separate portal with different scoping rules instead.
|
|
238
309
|
|
|
239
310
|
## Portal-Specific Policies
|
|
240
311
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
plutonium (0.
|
|
4
|
+
plutonium (0.37.0)
|
|
5
5
|
action_policy (~> 0.7.0)
|
|
6
6
|
listen (~> 3.8)
|
|
7
7
|
pagy (~> 9.0)
|
|
@@ -11,7 +11,7 @@ PATH
|
|
|
11
11
|
phlex-tabler_icons
|
|
12
12
|
phlexi-display (>= 0.2.0)
|
|
13
13
|
phlexi-field (>= 0.2.0)
|
|
14
|
-
phlexi-form (>= 0.
|
|
14
|
+
phlexi-form (>= 0.14.0)
|
|
15
15
|
phlexi-menu (>= 0.4.0)
|
|
16
16
|
phlexi-table (>= 0.2.0)
|
|
17
17
|
rabl (~> 0.16.1)
|
|
@@ -230,7 +230,7 @@ GEM
|
|
|
230
230
|
fiber-local
|
|
231
231
|
phlex (~> 2.0)
|
|
232
232
|
zeitwerk
|
|
233
|
-
phlexi-form (0.
|
|
233
|
+
phlexi-form (0.14.0)
|
|
234
234
|
activesupport
|
|
235
235
|
phlex (~> 2.0)
|
|
236
236
|
phlexi-field (~> 0.2.0)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
plutonium (0.
|
|
4
|
+
plutonium (0.37.0)
|
|
5
5
|
action_policy (~> 0.7.0)
|
|
6
6
|
listen (~> 3.8)
|
|
7
7
|
pagy (~> 9.0)
|
|
@@ -11,7 +11,7 @@ PATH
|
|
|
11
11
|
phlex-tabler_icons
|
|
12
12
|
phlexi-display (>= 0.2.0)
|
|
13
13
|
phlexi-field (>= 0.2.0)
|
|
14
|
-
phlexi-form (>= 0.
|
|
14
|
+
phlexi-form (>= 0.14.0)
|
|
15
15
|
phlexi-menu (>= 0.4.0)
|
|
16
16
|
phlexi-table (>= 0.2.0)
|
|
17
17
|
rabl (~> 0.16.1)
|
|
@@ -209,7 +209,7 @@ GEM
|
|
|
209
209
|
fiber-local
|
|
210
210
|
phlex (~> 2.0)
|
|
211
211
|
zeitwerk
|
|
212
|
-
phlexi-form (0.
|
|
212
|
+
phlexi-form (0.14.0)
|
|
213
213
|
activesupport
|
|
214
214
|
phlex (~> 2.0)
|
|
215
215
|
phlexi-field (~> 0.2.0)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
plutonium (0.
|
|
4
|
+
plutonium (0.37.0)
|
|
5
5
|
action_policy (~> 0.7.0)
|
|
6
6
|
listen (~> 3.8)
|
|
7
7
|
pagy (~> 9.0)
|
|
@@ -11,7 +11,7 @@ PATH
|
|
|
11
11
|
phlex-tabler_icons
|
|
12
12
|
phlexi-display (>= 0.2.0)
|
|
13
13
|
phlexi-field (>= 0.2.0)
|
|
14
|
-
phlexi-form (>= 0.
|
|
14
|
+
phlexi-form (>= 0.14.0)
|
|
15
15
|
phlexi-menu (>= 0.4.0)
|
|
16
16
|
phlexi-table (>= 0.2.0)
|
|
17
17
|
rabl (~> 0.16.1)
|
|
@@ -211,7 +211,7 @@ GEM
|
|
|
211
211
|
fiber-local
|
|
212
212
|
phlex (~> 2.0)
|
|
213
213
|
zeitwerk
|
|
214
|
-
phlexi-form (0.
|
|
214
|
+
phlexi-form (0.14.0)
|
|
215
215
|
activesupport
|
|
216
216
|
phlex (~> 2.0)
|
|
217
217
|
phlexi-field (~> 0.2.0)
|