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,508 @@
|
|
|
1
|
+
# Definition
|
|
2
|
+
|
|
3
|
+
Definitions configure **how** a resource is rendered and interacted with — which fields appear, how they render, what page chrome looks like. Auto-detection from the model handles the defaults; declare only what you're overriding.
|
|
4
|
+
|
|
5
|
+
For search/filters/scopes/sorting see [Query](./query). For custom actions see [Actions](./actions).
|
|
6
|
+
|
|
7
|
+
## 🚨 Critical
|
|
8
|
+
|
|
9
|
+
- **Don't declare for completeness.** A `field :title` matching what Plutonium auto-detects is dead code. Declare ONLY when you need a different type, an option (`hint:`, `placeholder:`, `wrapper:`, `class:`), a `condition:`, a block, or a custom component.
|
|
10
|
+
- **Use `condition:` for UI state, the policy for authorization.** `condition: -> { object.published? }` is fine. "Only admins see this field" belongs in `permitted_attributes_for_*`.
|
|
11
|
+
- **Custom action ⇒ policy method.** `action :publish` needs `def publish?` on the policy (see [Behavior › Policy](/reference/behavior/policies)).
|
|
12
|
+
- **`has_cents` fields use the virtual name** (`field :price`), never `:price_cents`.
|
|
13
|
+
- **Nested inputs need `accepts_nested_attributes_for` AND `inverse_of:` on the child's `belongs_to`** — without `inverse_of:`, validation fails with "Parent must exist" because the parent isn't saved yet.
|
|
14
|
+
|
|
15
|
+
## File location
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
app/definitions/post_definition.rb
|
|
19
|
+
packages/blogging/app/definitions/blogging/post_definition.rb
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| Model | Definition |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `Post` | `PostDefinition` |
|
|
25
|
+
| `Blogging::Post` | `Blogging::PostDefinition` |
|
|
26
|
+
|
|
27
|
+
## Hierarchy
|
|
28
|
+
|
|
29
|
+
Definitions inherit from each other so portals can override:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
# app/definitions/resource_definition.rb (installed once)
|
|
33
|
+
class ResourceDefinition < Plutonium::Resource::Definition
|
|
34
|
+
action :archive, interaction: ArchiveInteraction, color: :danger, position: 1000
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# app/definitions/post_definition.rb (scaffolded)
|
|
38
|
+
class PostDefinition < ResourceDefinition
|
|
39
|
+
scope :published
|
|
40
|
+
input :content, as: :markdown
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# packages/admin_portal/app/definitions/admin_portal/post_definition.rb (per-portal)
|
|
44
|
+
class AdminPortal::PostDefinition < ::PostDefinition
|
|
45
|
+
input :internal_notes, as: :text # admins see this; customers don't
|
|
46
|
+
scope :pending_review
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Auto-detection
|
|
51
|
+
|
|
52
|
+
Empty definition = everything auto-detected from the model:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
class PostDefinition < Plutonium::Resource::Definition
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Plutonium detects, from the model:
|
|
60
|
+
|
|
61
|
+
- Database columns (string, text, integer, boolean, datetime, etc.)
|
|
62
|
+
- Associations (`belongs_to`, `has_many`, `has_one`)
|
|
63
|
+
- ActiveStorage attachments (`has_one_attached`, `has_many_attached`)
|
|
64
|
+
- Enums
|
|
65
|
+
- Virtual attributes (when they have accessor methods)
|
|
66
|
+
|
|
67
|
+
| Database type | Detected as |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `string`, `text` | `:string` / `:text` |
|
|
70
|
+
| `integer`, `bigint` | `:integer` |
|
|
71
|
+
| `float`, `decimal` | `:float` / `:decimal` |
|
|
72
|
+
| `boolean` | `:boolean` |
|
|
73
|
+
| `date`, `datetime`, `time` | `:date` / `:datetime` / `:time` |
|
|
74
|
+
| `json`, `jsonb` | `:json` |
|
|
75
|
+
|
|
76
|
+
Validations on the model inform the UI too: `validates :title, presence: true` → required field; `validates :role, inclusion: { in: [...] }` → select choices.
|
|
77
|
+
|
|
78
|
+
## Core methods
|
|
79
|
+
|
|
80
|
+
| Method | Applies to | Use when |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `field` | Forms + Show + Table | Universal type override |
|
|
83
|
+
| `input` | Forms only | Form-specific options |
|
|
84
|
+
| `display` | Show page only | Display-specific options |
|
|
85
|
+
| `column` | Table only | Table-specific options |
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
class PostDefinition < Plutonium::Resource::Definition
|
|
89
|
+
field :content, as: :markdown # everywhere
|
|
90
|
+
input :title, hint: "Be descriptive"
|
|
91
|
+
display :content, wrapper: {class: "col-span-full"}
|
|
92
|
+
column :view_count, align: :end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Available field types
|
|
97
|
+
|
|
98
|
+
### Input types (forms)
|
|
99
|
+
|
|
100
|
+
| Category | Types |
|
|
101
|
+
|---|---|
|
|
102
|
+
| Text | `:string`, `:text`, `:email`, `:url`, `:tel`, `:password` |
|
|
103
|
+
| Rich text | `:markdown` (EasyMDE editor) |
|
|
104
|
+
| Numeric | `:number`, `:integer`, `:decimal`, `:range` |
|
|
105
|
+
| Boolean | `:boolean` |
|
|
106
|
+
| Date/Time | `:date`, `:time`, `:datetime` |
|
|
107
|
+
| Selection | `:select`, `:slim_select`, `:radio_buttons`, `:check_boxes` |
|
|
108
|
+
| Files | `:file`, `:uppy`, `:attachment` |
|
|
109
|
+
| Associations | `:association`, `:secure_association`, `:belongs_to`, `:has_many`, `:has_one` |
|
|
110
|
+
| Special | `:hidden`, `:color`, `:phone` |
|
|
111
|
+
|
|
112
|
+
### Display types (show / index)
|
|
113
|
+
|
|
114
|
+
`:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`
|
|
115
|
+
|
|
116
|
+
## Field options
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
input :title,
|
|
120
|
+
# Wrapper-level (label, hint, placeholder, description)
|
|
121
|
+
label: "Custom Label",
|
|
122
|
+
hint: "Help text",
|
|
123
|
+
placeholder: "Enter value",
|
|
124
|
+
description: "Shown on the show page",
|
|
125
|
+
|
|
126
|
+
# Tag-level (HTML attributes)
|
|
127
|
+
class: "custom-class",
|
|
128
|
+
data: {controller: "custom"},
|
|
129
|
+
required: true,
|
|
130
|
+
readonly: true,
|
|
131
|
+
disabled: true,
|
|
132
|
+
|
|
133
|
+
# Layout
|
|
134
|
+
wrapper: {class: "col-span-full"}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Select / choices
|
|
138
|
+
|
|
139
|
+
### Static
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
input :category, as: :select, choices: %w[Tech Business Lifestyle]
|
|
143
|
+
input :status, as: :select, choices: Post.statuses.keys
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Dynamic (block required)
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
input :author do |f|
|
|
150
|
+
f.select_tag choices: User.active.pluck(:name, :id)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# With context: current_user, current_parent, object, request, params all available
|
|
154
|
+
input :team_members do |f|
|
|
155
|
+
f.select_tag choices: current_user.organization.users.pluck(:name, :id)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Based on object state
|
|
159
|
+
input :related_posts do |f|
|
|
160
|
+
choices = object.persisted? ?
|
|
161
|
+
Post.where.not(id: object.id).published.pluck(:title, :id) : []
|
|
162
|
+
f.select_tag choices: choices
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Conditional rendering
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
display :published_at, condition: -> { object.published? }
|
|
170
|
+
display :rejection_reason, condition: -> { object.rejected? }
|
|
171
|
+
field :debug_info, condition: -> { Rails.env.development? }
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
::: warning UI state, not authorization
|
|
175
|
+
`condition:` is for UI logic ("show this when published"). For "who can see this", use the policy's `permitted_attributes_for_*` — see [Behavior › Policy](/reference/behavior/policies).
|
|
176
|
+
:::
|
|
177
|
+
|
|
178
|
+
## Dynamic forms (`pre_submit`)
|
|
179
|
+
|
|
180
|
+
A field with `pre_submit: true` triggers a server re-render on change, re-evaluating `condition:` procs. Use for cascading or context-dependent forms.
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
class QuestionDefinition < ResourceDefinition
|
|
184
|
+
# Trigger field
|
|
185
|
+
input :question_type, as: :select,
|
|
186
|
+
choices: %w[text choice scale],
|
|
187
|
+
pre_submit: true
|
|
188
|
+
|
|
189
|
+
# Dependents — no `as:` needed when the model column type matches
|
|
190
|
+
input :max_length, condition: -> { object.question_type == "text" }
|
|
191
|
+
input :choices, condition: -> { object.question_type == "choice" }
|
|
192
|
+
input :min_value, condition: -> { object.question_type == "scale" }
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
How it works:
|
|
197
|
+
|
|
198
|
+
1. User changes a `pre_submit: true` field.
|
|
199
|
+
2. Form submits via Turbo (no page reload).
|
|
200
|
+
3. Server re-renders the form with updated `object` state.
|
|
201
|
+
4. `condition:` procs are re-evaluated. Newly visible fields appear; newly hidden ones disappear.
|
|
202
|
+
|
|
203
|
+
Tips:
|
|
204
|
+
|
|
205
|
+
- Only add `pre_submit:` to fields that gate visibility of others.
|
|
206
|
+
- Avoid on frequently-changed fields (every keystroke = submit).
|
|
207
|
+
|
|
208
|
+
## Custom rendering
|
|
209
|
+
|
|
210
|
+
### Block syntax
|
|
211
|
+
|
|
212
|
+
**Display (any return value, can be a component):**
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
display :status do |field|
|
|
216
|
+
StatusBadgeComponent.new(value: field.value, class: field.dom.css_class)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
display :metrics do |field|
|
|
220
|
+
field.value.present? ?
|
|
221
|
+
MetricsChartComponent.new(data: field.value) :
|
|
222
|
+
EmptyStateComponent.new(message: "No metrics")
|
|
223
|
+
end
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Input (must call form builder methods):**
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
input :birth_date do |f|
|
|
230
|
+
case object.age_category
|
|
231
|
+
when 'adult' then f.date_tag(min: 18.years.ago.to_date)
|
|
232
|
+
when 'minor' then f.date_tag(max: 18.years.ago.to_date)
|
|
233
|
+
else f.date_tag
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### `phlexi_tag` for declarative custom display
|
|
239
|
+
|
|
240
|
+
`with:` takes either a Phlex component class OR a proc whose body is **rendered inside a Phlex context** — HTML tag methods (`span`, `div`, `a`) and Tailwind classes are first-class. The proc receives `(value, attrs)`.
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
# Component — preferred for anything reusable
|
|
244
|
+
display :status, as: :phlexi_tag, with: StatusBadgeComponent
|
|
245
|
+
|
|
246
|
+
# Inline proc — `span` here is a Phlex tag method, not a Rails helper
|
|
247
|
+
display :priority, as: :phlexi_tag, with: ->(value, attrs) {
|
|
248
|
+
case value
|
|
249
|
+
when 'high' then span(class: "badge badge-danger") { "High" }
|
|
250
|
+
when 'medium' then span(class: "badge badge-warning") { "Medium" }
|
|
251
|
+
else span(class: "badge badge-info") { "Low" }
|
|
252
|
+
end
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
See [UI › Components](/reference/ui/components) for writing reusable Phlex components.
|
|
257
|
+
|
|
258
|
+
### Custom component class
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
input :color_picker, as: ColorPickerComponent
|
|
262
|
+
display :chart, as: ChartComponent
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Column options
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
column :title, align: :start # default
|
|
269
|
+
column :status, align: :center
|
|
270
|
+
column :amount, align: :end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Value formatting
|
|
274
|
+
|
|
275
|
+
`formatter:` receives just the value. Use a block when you need the full record.
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
column :description, formatter: ->(value) { value&.truncate(30) }
|
|
279
|
+
column :price, formatter: ->(value) { "$%.2f" % value if value }
|
|
280
|
+
column :status, formatter: ->(value) { value&.humanize&.upcase }
|
|
281
|
+
|
|
282
|
+
# Block — full record access
|
|
283
|
+
column :full_name do |record|
|
|
284
|
+
"#{record.first_name} #{record.last_name}"
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Nested inputs
|
|
289
|
+
|
|
290
|
+
Inline forms for associated records. Requires `accepts_nested_attributes_for` on the model.
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
class Post < ResourceRecord
|
|
294
|
+
has_many :comments
|
|
295
|
+
has_one :metadata
|
|
296
|
+
|
|
297
|
+
accepts_nested_attributes_for :comments, allow_destroy: true, limit: 10
|
|
298
|
+
accepts_nested_attributes_for :metadata, update_only: true
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
class PostDefinition < ResourceDefinition
|
|
302
|
+
nested_input :comments do |n|
|
|
303
|
+
n.input :body, as: :text
|
|
304
|
+
n.input :author_name
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Or use another definition
|
|
308
|
+
nested_input :metadata, using: PostMetadataDefinition, fields: %i[seo_title seo_description]
|
|
309
|
+
end
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Options
|
|
313
|
+
|
|
314
|
+
| Option | Description |
|
|
315
|
+
|---|---|
|
|
316
|
+
| `limit` | Max records (auto-detected from model; default 10) |
|
|
317
|
+
| `allow_destroy` | Show delete checkbox (auto-detected) |
|
|
318
|
+
| `update_only` | Hide "Add" button — only edit existing |
|
|
319
|
+
| `description` | Help text above the section |
|
|
320
|
+
| `condition` | Proc to show/hide |
|
|
321
|
+
| `using` | Another Definition class |
|
|
322
|
+
| `fields` | Subset of fields from the referenced definition |
|
|
323
|
+
|
|
324
|
+
### Gotchas
|
|
325
|
+
|
|
326
|
+
- **`inverse_of:` is required** on the child's `belongs_to`:
|
|
327
|
+
```ruby
|
|
328
|
+
class Comment < ResourceRecord
|
|
329
|
+
belongs_to :post, inverse_of: :comments # ← without this, validation fails with "Parent must exist"
|
|
330
|
+
end
|
|
331
|
+
```
|
|
332
|
+
- **Don't put `*_attributes` hashes in the policy.** Plutonium extracts nested params from the form definition, not the policy. The policy permits just the association name (`:variants`); `nested_input :variants` handles the rest. Adding `{variants_attributes: [...]}` to `permitted_attributes_for_create` renders as a literal text input. See [Behavior › Policy](/reference/behavior/policies).
|
|
333
|
+
- **`update_only: true` hides the Add button** — for `has_one` and "settings"-style associations.
|
|
334
|
+
- **Custom class names** — use `class_name:` in the model AND `using:` in the definition.
|
|
335
|
+
|
|
336
|
+
## File uploads
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
input :avatar, as: :file
|
|
340
|
+
input :avatar, as: :uppy
|
|
341
|
+
|
|
342
|
+
input :documents, as: :file, multiple: true
|
|
343
|
+
input :documents, as: :uppy,
|
|
344
|
+
allowed_file_types: %w[.pdf .doc],
|
|
345
|
+
max_file_size: 5.megabytes
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Context in blocks
|
|
349
|
+
|
|
350
|
+
Inside `condition:` procs and block-form `input`/`display`:
|
|
351
|
+
|
|
352
|
+
- `object` — the record being edited or displayed
|
|
353
|
+
- `current_user`
|
|
354
|
+
- `current_parent` — parent record for nested resources
|
|
355
|
+
- `request`, `params`
|
|
356
|
+
- All view helpers (via the same context as controllers)
|
|
357
|
+
|
|
358
|
+
## Runtime customization hooks
|
|
359
|
+
|
|
360
|
+
Override these methods for dynamic per-request configuration:
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
class PostDefinition < ResourceDefinition
|
|
364
|
+
def customize_fields # add/modify fields
|
|
365
|
+
def customize_inputs
|
|
366
|
+
def customize_displays
|
|
367
|
+
def customize_columns
|
|
368
|
+
def customize_filters
|
|
369
|
+
def customize_scopes
|
|
370
|
+
def customize_sorts
|
|
371
|
+
def customize_actions
|
|
372
|
+
end
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Useful when configuration depends on `current_user`, the environment, or feature flags.
|
|
376
|
+
|
|
377
|
+
## Page configuration
|
|
378
|
+
|
|
379
|
+
### Titles and descriptions
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
class PostDefinition < ResourceDefinition
|
|
383
|
+
index_page_title "All Posts"
|
|
384
|
+
index_page_description "Manage your blog posts"
|
|
385
|
+
|
|
386
|
+
new_page_title "Create Post"
|
|
387
|
+
show_page_title -> { current_record!.title } # dynamic
|
|
388
|
+
edit_page_title -> { "Edit: #{current_record!.title}" }
|
|
389
|
+
end
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Breadcrumbs
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
breadcrumbs true # global default
|
|
396
|
+
index_page_breadcrumbs false # per-page override
|
|
397
|
+
show_page_breadcrumbs true
|
|
398
|
+
new_page_breadcrumbs true
|
|
399
|
+
edit_page_breadcrumbs true
|
|
400
|
+
interactive_action_page_breadcrumbs true
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Form configuration
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
class PostDefinition < ResourceDefinition
|
|
407
|
+
# "Save and add another" / "Update and continue editing"
|
|
408
|
+
# nil (default) — auto: hidden for singular resources, shown for plural
|
|
409
|
+
# true — always show
|
|
410
|
+
# false — always hide
|
|
411
|
+
submit_and_continue false
|
|
412
|
+
|
|
413
|
+
# How :new / :edit render
|
|
414
|
+
# :slideover (default) — slide-in panel from the right
|
|
415
|
+
# :centered — centered dialog
|
|
416
|
+
# false — full standalone pages (no modal)
|
|
417
|
+
modal :centered
|
|
418
|
+
end
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
`modal:` only affects framework `:new`/`:edit` actions. Custom interactive actions have their own per-action `modal:` option — see [Actions](./actions).
|
|
422
|
+
|
|
423
|
+
## Metadata panel (show page)
|
|
424
|
+
|
|
425
|
+
A right-side aside on the show page rendering label/value rows. Keeps the main card focused on substance; chrome (timestamps, ownership, system flags) lives in the aside.
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
class PostDefinition < ResourceDefinition
|
|
429
|
+
metadata :author, :state, :created_at, :updated_at
|
|
430
|
+
end
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Behavior:
|
|
434
|
+
|
|
435
|
+
- **Opt-in.** No `metadata` call → show page renders full-width.
|
|
436
|
+
- **Policy-aware.** Fields intersect with the policy's permitted attributes. The panel auto-hides when nothing is permitted.
|
|
437
|
+
- **Deduplicated.** Fields listed in `metadata` are removed from the main card so values aren't shown twice.
|
|
438
|
+
- **Responsive.** Side-by-side at `lg+`, stacked below.
|
|
439
|
+
- **Formatting inherits.** Field labels and `as:` declarations propagate — the metadata panel uses the same field-rendering machinery as the main card.
|
|
440
|
+
|
|
441
|
+
## Index views (Table & Grid)
|
|
442
|
+
|
|
443
|
+
Resources can offer both Table and Grid views. The user switches via the toolbar; the choice persists per-resource via cookie.
|
|
444
|
+
|
|
445
|
+
```ruby
|
|
446
|
+
class UserDefinition < ResourceDefinition
|
|
447
|
+
# No `index_views :table, :grid` needed — declaring grid_fields auto-enables :grid.
|
|
448
|
+
grid_fields(
|
|
449
|
+
image: :avatar, # ActiveStorage attachment, Shrine, or URL
|
|
450
|
+
header: :name, # falls back to to_label
|
|
451
|
+
subheader: :email,
|
|
452
|
+
body: :bio,
|
|
453
|
+
meta: [:role, :status], # rendered as small pills
|
|
454
|
+
footer: :last_seen_at # falls back to :created_at
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
default_index_view :grid # optional — initial view when no cookie
|
|
458
|
+
grid_layout :media # :compact (default) or :media
|
|
459
|
+
grid_columns 3 # pin lg+ cols; default is 1/2/3/4 responsive
|
|
460
|
+
end
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
| Method | Purpose |
|
|
464
|
+
|---|---|
|
|
465
|
+
| `index_views :table, :grid` | Which views are available. Default `[:table]`. Usually unnecessary. |
|
|
466
|
+
| `default_index_view :grid` | Initial view when no cookie. Falls back to first available view. |
|
|
467
|
+
| `grid_fields(...)` | Map card slots to fields. **Implicitly enables `:grid`**. |
|
|
468
|
+
| `grid_layout :compact \| :media` | `:compact` puts image left of content; `:media` stacks image full-width on top. |
|
|
469
|
+
| `grid_columns N` | Override responsive column count on `lg+`. Default is 1/2/3/4 at sm/md/lg/xl. |
|
|
470
|
+
|
|
471
|
+
Grid slots — `:image`, `:header`, `:subheader`, `:body`, `:meta`, `:footer` — are all optional. `:meta` accepts an array; the rest are single fields. Slots pointing at policy-blocked fields collapse silently.
|
|
472
|
+
|
|
473
|
+
Only declare `index_views` explicitly to **disable** one (e.g. `index_views :grid` to drop the table view).
|
|
474
|
+
|
|
475
|
+
## Custom page classes
|
|
476
|
+
|
|
477
|
+
Override the rendered page entirely — full control via Phlex:
|
|
478
|
+
|
|
479
|
+
```ruby
|
|
480
|
+
class PostDefinition < ResourceDefinition
|
|
481
|
+
class IndexPage < IndexPage # inherits the parent's nested class
|
|
482
|
+
def view_template(&block)
|
|
483
|
+
div(class: "custom-header") { h1 { "Custom" } }
|
|
484
|
+
super(&block)
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
class Form < Form
|
|
489
|
+
def form_template
|
|
490
|
+
div(class: "grid grid-cols-2") do
|
|
491
|
+
render field(:title).input_tag
|
|
492
|
+
render field(:content).easymde_tag
|
|
493
|
+
end
|
|
494
|
+
render_actions
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
See [UI › Pages](/reference/ui/pages) and [UI › Forms](/reference/ui/forms) for the full page-class surface.
|
|
501
|
+
|
|
502
|
+
## Related
|
|
503
|
+
|
|
504
|
+
- [Query](./query) — search, filters, scopes, sorting
|
|
505
|
+
- [Actions](./actions) — custom + bulk actions
|
|
506
|
+
- [Behavior › Policy](/reference/behavior/policies) — `permitted_attributes_for_*`, authorization
|
|
507
|
+
- [UI › Forms](/reference/ui/forms) — field builder, association inputs, theming
|
|
508
|
+
- [UI › Pages](/reference/ui/pages) — custom page classes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Resource Reference
|
|
2
|
+
|
|
3
|
+
A **resource** is the unit Plutonium gives you full CRUD for — list, show, create, edit, delete — automatically. It's four cooperating layers, plus an optional fifth for business logic.
|
|
4
|
+
|
|
5
|
+
| Layer | File | What it controls |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| [Model](./model) | `app/models/post.rb` | Data, validations, associations |
|
|
8
|
+
| [Definition](./definition) | `app/definitions/post_definition.rb` | UI — which fields, how they render, what actions exist |
|
|
9
|
+
| Policy | `app/policies/post_policy.rb` | Authorization — see [Behavior › Policy](/reference/behavior/policies) |
|
|
10
|
+
| Controller | `app/controllers/posts_controller.rb` | Request handling — see [Behavior › Controller](/reference/behavior/controllers) |
|
|
11
|
+
| Interaction *(optional)* | `app/interactions/publish_post_interaction.rb` | Business logic for custom actions — see [Behavior › Interaction](/reference/behavior/interactions) |
|
|
12
|
+
|
|
13
|
+
## How a resource is born
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
rails g pu:res:scaffold Post user:belongs_to title:string 'content:text?' --dest=main_app
|
|
17
|
+
rails db:migrate
|
|
18
|
+
rails g pu:res:conn Post --dest=admin_portal
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
That single scaffold gives you a working model + migration + controller + policy + definition. `pu:res:conn` adds it to a portal. See [App › Generators](/reference/app/generators) for the full generator catalog.
|
|
22
|
+
|
|
23
|
+
## Auto-detection is the default
|
|
24
|
+
|
|
25
|
+
Plutonium reads your model and renders every attribute automatically — type, label, form widget, display formatter, table column. You only declare overrides:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
class PostDefinition < Plutonium::Resource::Definition
|
|
29
|
+
# No field/input/display/column needed unless you're overriding the default.
|
|
30
|
+
field :content, as: :markdown # override: render as markdown editor + viewer
|
|
31
|
+
input :title, hint: "Be descriptive"
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
::: warning Don't declare for completeness
|
|
36
|
+
A `field :title` with no options that matches what Plutonium would auto-detect is **dead code** — it does nothing and clutters the file. Declare ONLY when you need a different type, an option, a `condition:`, a block, or a custom component.
|
|
37
|
+
:::
|
|
38
|
+
|
|
39
|
+
## Sub-pages
|
|
40
|
+
|
|
41
|
+
- [Model](./model) — `Plutonium::Resource::Record`, `has_cents`, SGID, custom routing, labeling
|
|
42
|
+
- [Definition](./definition) — fields, inputs, displays, columns, page chrome, metadata panel, index views
|
|
43
|
+
- [Query](./query) — search, filters, scopes, sorting
|
|
44
|
+
- [Actions](./actions) — custom actions, bulk actions, interaction integration
|
|
45
|
+
|
|
46
|
+
## Related
|
|
47
|
+
|
|
48
|
+
- [Guides › Adding Resources](/guides/adding-resources) — task recipe
|
|
49
|
+
- [App › Generators](/reference/app/generators) — `pu:res:scaffold` / `pu:res:conn` reference
|
|
50
|
+
- [Tenancy](/reference/tenancy/) — multi-tenant scoping
|