plutonium 0.23.4 → 0.23.5
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/app/assets/plutonium.css +2 -2
- data/config/initializers/sqlite_json_alias.rb +1 -1
- data/docs/.vitepress/config.ts +60 -19
- data/docs/guide/cursor-rules.md +75 -0
- data/docs/guide/deep-dive/authorization.md +189 -0
- data/docs/guide/{getting-started → deep-dive}/resources.md +137 -0
- data/docs/guide/getting-started/{installation.md → 01-installation.md} +0 -105
- data/docs/guide/index.md +28 -0
- data/docs/guide/introduction/02-core-concepts.md +440 -0
- data/docs/guide/tutorial/01-project-setup.md +75 -0
- data/docs/guide/tutorial/02-creating-a-feature-package.md +45 -0
- data/docs/guide/tutorial/03-defining-resources.md +90 -0
- data/docs/guide/tutorial/04-creating-a-portal.md +101 -0
- data/docs/guide/tutorial/05-customizing-the-ui.md +128 -0
- data/docs/guide/tutorial/06-adding-custom-actions.md +101 -0
- data/docs/guide/tutorial/07-implementing-authorization.md +90 -0
- data/docs/index.md +24 -31
- data/docs/modules/action.md +190 -0
- data/docs/modules/authentication.md +236 -0
- data/docs/modules/configuration.md +599 -0
- data/docs/modules/controller.md +398 -0
- data/docs/modules/core.md +316 -0
- data/docs/modules/definition.md +876 -0
- data/docs/modules/display.md +759 -0
- data/docs/modules/form.md +605 -0
- data/docs/modules/generator.md +288 -0
- data/docs/modules/index.md +167 -0
- data/docs/modules/interaction.md +470 -0
- data/docs/modules/package.md +151 -0
- data/docs/modules/policy.md +176 -0
- data/docs/modules/portal.md +710 -0
- data/docs/modules/query.md +287 -0
- data/docs/modules/resource_record.md +618 -0
- data/docs/modules/routing.md +641 -0
- data/docs/modules/table.md +293 -0
- data/docs/modules/ui.md +631 -0
- data/docs/public/plutonium.mdc +667 -0
- data/lib/generators/pu/core/assets/assets_generator.rb +0 -5
- data/lib/plutonium/ui/display/resource.rb +7 -2
- data/lib/plutonium/ui/table/resource.rb +8 -3
- data/lib/plutonium/version.rb +1 -1
- metadata +36 -9
- data/docs/guide/getting-started/authorization.md +0 -296
- data/docs/guide/getting-started/core-concepts.md +0 -432
- data/docs/guide/getting-started/index.md +0 -21
- data/docs/guide/tutorial.md +0 -401
- /data/docs/guide/{what-is-plutonium.md → introduction/01-what-is-plutonium.md} +0 -0
@@ -0,0 +1,876 @@
|
|
1
|
+
---
|
2
|
+
title: Definition Module
|
3
|
+
---
|
4
|
+
|
5
|
+
# Definition Module
|
6
|
+
|
7
|
+
The Definition module provides a powerful DSL for declaratively configuring how resources are displayed, edited, filtered, and interacted with. It serves as the central configuration point for resource behavior in Plutonium applications.
|
8
|
+
|
9
|
+
::: tip
|
10
|
+
The Definition module is located in `lib/plutonium/definition/`. Resource definitions are typically placed in `app/definitions/`.
|
11
|
+
:::
|
12
|
+
|
13
|
+
## Overview
|
14
|
+
|
15
|
+
- **Field Configuration**: Define how fields are displayed and edited.
|
16
|
+
- **Display Customization**: Configure field presentation and rendering.
|
17
|
+
- **Input Management**: Control form inputs and validation.
|
18
|
+
- **Filter & Search**: Set up filtering and search capabilities.
|
19
|
+
- **Action Definitions**: Define custom actions and operations.
|
20
|
+
- **Conditional Logic**: Dynamic configuration based on context.
|
21
|
+
|
22
|
+
## Core DSL Methods
|
23
|
+
|
24
|
+
### Field, Display, and Input
|
25
|
+
|
26
|
+
The three core methods for defining a resource's attributes are `field`, `display`, and `input`. **All model attributes are automatically detected** - you only need to declare them when you want to override defaults or add custom options.
|
27
|
+
|
28
|
+
::: code-group
|
29
|
+
```ruby [field]
|
30
|
+
# Field declarations are OPTIONAL - all attributes are auto-detected
|
31
|
+
# You only need to declare fields when overriding auto-detected behavior
|
32
|
+
class PostDefinition < Plutonium::Resource::Definition
|
33
|
+
# These are all auto-detected from your Post model:
|
34
|
+
# - :title (string column)
|
35
|
+
# - :content (text column)
|
36
|
+
# - :published_at (datetime column)
|
37
|
+
# - :published (boolean column)
|
38
|
+
# - :author (belongs_to association)
|
39
|
+
# - :tags (has_many association)
|
40
|
+
# - :featured_image (has_one_attached)
|
41
|
+
|
42
|
+
# Only declare fields when you want to override:
|
43
|
+
field :content, as: :rich_text # Override text -> rich_text
|
44
|
+
field :author_id, as: :hidden # Override integer -> hidden
|
45
|
+
field :internal_notes, as: :text # Add custom field options
|
46
|
+
end
|
47
|
+
```
|
48
|
+
```ruby [display]
|
49
|
+
# Display declarations are also OPTIONAL for auto-detected fields
|
50
|
+
# Only declare when you want custom display behavior
|
51
|
+
class PostDefinition < Plutonium::Resource::Definition
|
52
|
+
# All model attributes auto-detected and displayed appropriately
|
53
|
+
|
54
|
+
# Only override when you need custom display:
|
55
|
+
display :content, as: :markdown # Override text -> markdown
|
56
|
+
display :published_at, as: :date # Override datetime -> date only
|
57
|
+
display :view_count, class: "font-bold" # Add custom styling
|
58
|
+
|
59
|
+
# Custom display with block for complex rendering
|
60
|
+
display :status do |field|
|
61
|
+
StatusBadgeComponent.new(value: field.value, class: field.dom.css_class)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
```
|
65
|
+
```ruby [input]
|
66
|
+
# Input declarations are also OPTIONAL for auto-detected fields
|
67
|
+
# Only declare when you need custom input behavior
|
68
|
+
class PostDefinition < Plutonium::Resource::Definition
|
69
|
+
# All editable attributes auto-detected with appropriate inputs
|
70
|
+
|
71
|
+
# Only override when you need custom input behavior:
|
72
|
+
input :content, as: :rich_text # Override text -> rich_text
|
73
|
+
input :title, placeholder: "Enter title" # Add placeholder
|
74
|
+
input :category, as: :select, collection: %w[Tech Business] # Add options
|
75
|
+
input :published_at, as: :date # Override datetime -> date only
|
76
|
+
end
|
77
|
+
```
|
78
|
+
:::
|
79
|
+
|
80
|
+
## Field Type Auto-Detection
|
81
|
+
|
82
|
+
**Plutonium automatically detects ALL model attributes** and creates appropriate field, display, and input configurations. The system inspects your ActiveRecord model to discover:
|
83
|
+
|
84
|
+
- **Database columns** (string, text, integer, boolean, datetime, etc.)
|
85
|
+
- **Associations** (belongs_to, has_many, has_one, etc.)
|
86
|
+
- **Active Storage attachments** (has_one_attached, has_many_attached)
|
87
|
+
- **Enum attributes**
|
88
|
+
- **Virtual attributes** (with proper accessor methods)
|
89
|
+
|
90
|
+
::: details Complete Auto-Detection Logic
|
91
|
+
```ruby
|
92
|
+
# Database columns are automatically detected:
|
93
|
+
# CREATE TABLE posts (
|
94
|
+
# id bigint PRIMARY KEY,
|
95
|
+
# title varchar(255), # → field :title, as: :string
|
96
|
+
# content text, # → field :content, as: :text
|
97
|
+
# published_at timestamp, # → field :published_at, as: :datetime
|
98
|
+
# published boolean, # → field :published, as: :boolean
|
99
|
+
# view_count integer, # → field :view_count, as: :number
|
100
|
+
# rating decimal(3,2), # → field :rating, as: :decimal
|
101
|
+
# created_at timestamp, # → field :created_at, as: :datetime
|
102
|
+
# updated_at timestamp # → field :updated_at, as: :datetime
|
103
|
+
# );
|
104
|
+
|
105
|
+
# Associations are automatically detected:
|
106
|
+
class Post < ApplicationRecord
|
107
|
+
belongs_to :author, class_name: 'User' # → field :author, as: :association
|
108
|
+
has_many :comments # → field :comments, as: :association
|
109
|
+
has_many :tags, through: :post_tags # → field :tags, as: :association
|
110
|
+
end
|
111
|
+
|
112
|
+
# Active Storage attachments are automatically detected:
|
113
|
+
class Post < ApplicationRecord
|
114
|
+
has_one_attached :featured_image # → field :featured_image, as: :attachment
|
115
|
+
has_many_attached :documents # → field :documents, as: :attachment
|
116
|
+
end
|
117
|
+
|
118
|
+
# Enums are automatically detected:
|
119
|
+
class Post < ApplicationRecord
|
120
|
+
enum status: { draft: 0, published: 1, archived: 2 } # → field :status, as: :select
|
121
|
+
end
|
122
|
+
```
|
123
|
+
:::
|
124
|
+
|
125
|
+
## When to Declare Fields
|
126
|
+
|
127
|
+
You only need to explicitly declare fields, displays, or inputs in these scenarios:
|
128
|
+
|
129
|
+
### 1. **Override Auto-Detected Type**
|
130
|
+
```ruby
|
131
|
+
class PostDefinition < Plutonium::Resource::Definition
|
132
|
+
# Change text column to rich text editor
|
133
|
+
input :content, as: :rich_text
|
134
|
+
|
135
|
+
# Change datetime to date-only picker
|
136
|
+
input :published_at, as: :date
|
137
|
+
|
138
|
+
# Change text display to markdown rendering
|
139
|
+
display :content, as: :markdown
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
### 2. **Add Custom Options**
|
144
|
+
```ruby
|
145
|
+
class PostDefinition < Plutonium::Resource::Definition
|
146
|
+
# Add placeholder text
|
147
|
+
input :title, placeholder: "Enter an engaging title"
|
148
|
+
|
149
|
+
# Add custom CSS classes
|
150
|
+
display :title, class: "text-2xl font-bold"
|
151
|
+
|
152
|
+
# Add wrapper styling
|
153
|
+
display :content, wrapper: {class: "prose max-w-none"}
|
154
|
+
end
|
155
|
+
```
|
156
|
+
|
157
|
+
### 3. **Configure Select Options**
|
158
|
+
```ruby
|
159
|
+
class PostDefinition < Plutonium::Resource::Definition
|
160
|
+
# Provide options for select inputs
|
161
|
+
input :category, as: :select, collection: %w[Tech Business Lifestyle]
|
162
|
+
input :author, as: :select, collection: -> { User.active.pluck(:name, :id) }
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
### 4. **Add Conditional Logic**
|
167
|
+
```ruby
|
168
|
+
class PostDefinition < Plutonium::Resource::Definition
|
169
|
+
# Conditional logic is for showing/hiding fields based on the application's
|
170
|
+
# state or other field values. It is not for authorization. Use policies
|
171
|
+
# to control access to data.
|
172
|
+
|
173
|
+
# Conditional fields based on the object's state
|
174
|
+
display :published_at, condition: -> { object.published? }
|
175
|
+
display :reason_for_rejection, condition: -> { object.rejected? }
|
176
|
+
column :published_at, condition: -> { object.published? }
|
177
|
+
|
178
|
+
# Use `pre_submit` to create dynamic forms where inputs appear based on other inputs.
|
179
|
+
input :send_notifications, as: :boolean, pre_submit: true
|
180
|
+
input :notification_channel, as: :select, collection: %w[Email SMS],
|
181
|
+
condition: -> { object.send_notifications? }
|
182
|
+
|
183
|
+
# Show debug fields only in development
|
184
|
+
field :debug_info, as: :string, condition: -> { Rails.env.development? }
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
::: danger Authorization with Policies
|
189
|
+
While the rendering context may provide access to `current_user`, it is strongly recommended to use **policies** for authorization logic (i.e., controlling who can see what data). The `condition` option is intended for cosmetic or state-based logic, such as hiding a field based on another field's value or the record's status. JSON requests for example are not affected by this.
|
190
|
+
:::
|
191
|
+
|
192
|
+
::: tip Condition Context & Dynamic Forms
|
193
|
+
`condition` procs are evaluated in their respective rendering contexts and have access to contextual data.
|
194
|
+
|
195
|
+
**For `input` fields** (form rendering context):
|
196
|
+
- `object` - The record being edited
|
197
|
+
- `current_parent` - Parent record for nested resources
|
198
|
+
- `request` and `params` - Request information
|
199
|
+
- All helper methods available in the form context
|
200
|
+
|
201
|
+
**For `display` fields** (display rendering context):
|
202
|
+
- `object` - The record being displayed
|
203
|
+
- `current_parent` - Parent record for nested resources
|
204
|
+
- All helper methods available in the display context
|
205
|
+
|
206
|
+
**For `column` fields** (table rendering context):
|
207
|
+
- `current_parent` - Parent record for nested resources
|
208
|
+
- All helper methods available in the table context
|
209
|
+
|
210
|
+
To create forms that dynamically show/hide inputs based on other form values, pair a `condition` option with `pre_submit: true` on the "trigger" input. This will cause the form to re-render whenever that input's value changes, re-evaluating any conditions that depend on it.
|
211
|
+
:::
|
212
|
+
|
213
|
+
### 5. Custom Field Rendering
|
214
|
+
|
215
|
+
Plutonium offers three main approaches for rendering fields in a definition. Choose the one that best fits your needs for clarity, flexibility, and control.
|
216
|
+
|
217
|
+
#### 1. The `as:` Option (Recommended)
|
218
|
+
|
219
|
+
The `as:` option is the simplest and most common way to specify a rendering component for an `input` or `display` declaration. It's ideal for using standard built-in components or overriding auto-detected types.
|
220
|
+
|
221
|
+
**Use When:**
|
222
|
+
- Using standard or enhanced built-in components.
|
223
|
+
- You want clean, readable code with minimal boilerplate.
|
224
|
+
- Overriding an auto-detected type (e.g., `text` to `rich_text`).
|
225
|
+
|
226
|
+
::: code-group
|
227
|
+
```ruby [Input Fields]
|
228
|
+
# Simple and concise overrides
|
229
|
+
class PostDefinition < Plutonium::Resource::Definition
|
230
|
+
input :content, as: :rich_text
|
231
|
+
input :published_at, as: :date
|
232
|
+
input :avatar, as: :uppy
|
233
|
+
|
234
|
+
# With options
|
235
|
+
input :email, as: :email, placeholder: "Enter email"
|
236
|
+
end
|
237
|
+
```
|
238
|
+
```ruby [Display Fields]
|
239
|
+
# Simple and concise overrides
|
240
|
+
class PostDefinition < Plutonium::Resource::Definition
|
241
|
+
display :content, as: :markdown
|
242
|
+
display :author, as: :association
|
243
|
+
display :documents, as: :attachment
|
244
|
+
|
245
|
+
# With styling options
|
246
|
+
display :status, as: :string, class: "badge badge-success"
|
247
|
+
end
|
248
|
+
```
|
249
|
+
:::
|
250
|
+
|
251
|
+
#### 2. The Block Syntax
|
252
|
+
|
253
|
+
The block syntax offers more control over rendering, allowing for custom components, complex layouts, and conditional logic. The block receives a `field` object that you can use to render custom output.
|
254
|
+
|
255
|
+
**Use When:**
|
256
|
+
- Integrating custom-built Phlex or ViewComponent components.
|
257
|
+
- Building complex layouts with multiple components or custom HTML.
|
258
|
+
- You need conditional logic to determine which component to render.
|
259
|
+
|
260
|
+
::: code-group
|
261
|
+
```ruby [Custom Display Components]
|
262
|
+
# Custom display component
|
263
|
+
display :chart_data do |field|
|
264
|
+
ChartComponent.new(data: field.value, type: :bar)
|
265
|
+
end
|
266
|
+
```
|
267
|
+
```ruby [Custom Input Components]
|
268
|
+
# Custom input component
|
269
|
+
input :color do |field|
|
270
|
+
ColorPickerComponent.new(field)
|
271
|
+
end
|
272
|
+
```
|
273
|
+
|
274
|
+
```
|
275
|
+
```ruby [Conditional Rendering]
|
276
|
+
# Conditional display based on value
|
277
|
+
display :metrics do |field|
|
278
|
+
if field.value.present?
|
279
|
+
MetricsChartComponent.new(data: field.value)
|
280
|
+
else
|
281
|
+
EmptyStateComponent.new(message: "No metrics available")
|
282
|
+
end
|
283
|
+
end
|
284
|
+
```
|
285
|
+
:::
|
286
|
+
|
287
|
+
#### 3. `as: :phlexi_tag` (Advanced)
|
288
|
+
|
289
|
+
`phlexi_tag` provides maximum rendering flexibility for `display` declarations. It's a powerful tool for building reusable component libraries and handling highly dynamic or polymorphic data.
|
290
|
+
|
291
|
+
**Use When:**
|
292
|
+
- Building reusable component libraries that need to be highly configurable.
|
293
|
+
- Working with polymorphic data that requires specialized renderers.
|
294
|
+
- You need complex rendering logic but want to keep it inline in the definition.
|
295
|
+
|
296
|
+
::: code-group
|
297
|
+
```ruby [With a Component Class]
|
298
|
+
# Pass a component class for rendering.
|
299
|
+
# The component's #initialize will receive (value, **attrs).
|
300
|
+
display :status, as: :phlexi_tag, with: StatusBadgeComponent
|
301
|
+
```
|
302
|
+
```ruby [With an Inline Proc]
|
303
|
+
# Use a proc for complex inline logic.
|
304
|
+
# The proc receives (value, attrs).
|
305
|
+
display :priority, as: :phlexi_tag, with: ->(value, attrs) {
|
306
|
+
case value
|
307
|
+
when 'high'
|
308
|
+
span(class: tokens("badge badge-danger", attrs[:class])) { "High" }
|
309
|
+
when 'medium'
|
310
|
+
span(class: tokens("badge badge-warning", attrs[:class])) { "Medium" }
|
311
|
+
else
|
312
|
+
span(class: tokens("badge badge-info", attrs[:class])) { "Low" }
|
313
|
+
end
|
314
|
+
}
|
315
|
+
```
|
316
|
+
```ruby [Handling Polymorphic Content]
|
317
|
+
# Dynamically render different components based on content type.
|
318
|
+
display :rich_content, as: :phlexi_tag, with: ->(value, attrs) {
|
319
|
+
# `value` is the rich_content object itself
|
320
|
+
case value&.content_type
|
321
|
+
when 'markdown'
|
322
|
+
MarkdownComponent.new(content: value.body, **attrs)
|
323
|
+
when 'image'
|
324
|
+
# Must return a proc for inline HTML rendering with Phlex
|
325
|
+
proc { img(src: value.url, alt: value.caption, **attrs) }
|
326
|
+
else
|
327
|
+
nil # Fallback to default rendering: <p>#{value}</p>
|
328
|
+
end
|
329
|
+
}
|
330
|
+
```
|
331
|
+
:::
|
332
|
+
|
333
|
+
## Minimal Definition Example
|
334
|
+
|
335
|
+
Here's what a typical definition looks like when leveraging auto-detection:
|
336
|
+
|
337
|
+
```ruby
|
338
|
+
class PostDefinition < Plutonium::Resource::Definition
|
339
|
+
# No field declarations needed! All attributes auto-detected.
|
340
|
+
# Post model columns, associations, and attachments are automatically available.
|
341
|
+
|
342
|
+
# Only customize what you need to override:
|
343
|
+
input :content, as: :rich_text
|
344
|
+
display :content, as: :markdown
|
345
|
+
|
346
|
+
# Add search and filtering:
|
347
|
+
search do |scope, query|
|
348
|
+
scope.where("title ILIKE ? OR content ILIKE ?", "%#{query}%", "%#{query}%")
|
349
|
+
end
|
350
|
+
|
351
|
+
filter :status, with: Plutonium::Query::Filters::Text, predicate: :eq
|
352
|
+
|
353
|
+
# Add custom actions:
|
354
|
+
action :publish, interaction: PublishPostInteraction
|
355
|
+
end
|
356
|
+
```
|
357
|
+
|
358
|
+
This approach means you can create a fully functional admin interface with just a few lines of configuration, while still having the flexibility to customize anything you need.
|
359
|
+
|
360
|
+
## Search, Filters, and Scopes
|
361
|
+
|
362
|
+
Configure how users can query the resource index.
|
363
|
+
|
364
|
+
::: code-group
|
365
|
+
```ruby [Search]
|
366
|
+
# Defines the global search logic for the resource.
|
367
|
+
class PostDefinition < Plutonium::Resource::Definition
|
368
|
+
search do |scope, query|
|
369
|
+
scope.where("title ILIKE ? OR content ILIKE ?", "%#{query}%", "%#{query}%")
|
370
|
+
end
|
371
|
+
end
|
372
|
+
```
|
373
|
+
```ruby [Filters]
|
374
|
+
# Currently, only Text filter is implemented
|
375
|
+
class PostDefinition < Plutonium::Resource::Definition
|
376
|
+
filter :title, with: Plutonium::Query::Filters::Text, predicate: :contains
|
377
|
+
filter :status, with: Plutonium::Query::Filters::Text, predicate: :eq
|
378
|
+
filter :category, with: Plutonium::Query::Filters::Text, predicate: :eq
|
379
|
+
|
380
|
+
# Available predicates: :eq, :not_eq, :contains, :not_contains,
|
381
|
+
# :starts_with, :ends_with, :matches, :not_matches
|
382
|
+
end
|
383
|
+
```
|
384
|
+
```ruby [Scopes]
|
385
|
+
# Defines named scopes that appear as buttons.
|
386
|
+
class PostDefinition < Plutonium::Resource::Definition
|
387
|
+
scope :published
|
388
|
+
scope :featured
|
389
|
+
scope :recent, -> { where('created_at > ?', 1.week.ago) }
|
390
|
+
end
|
391
|
+
```
|
392
|
+
:::
|
393
|
+
|
394
|
+
## Actions
|
395
|
+
|
396
|
+
Define custom operations that can be performed on a resource.
|
397
|
+
|
398
|
+
```ruby
|
399
|
+
class PostDefinition < Plutonium::Resource::Definition
|
400
|
+
# Each `action` call defines ONE action.
|
401
|
+
action :publish, interaction: PublishPostInteraction
|
402
|
+
action :archive, interaction: ArchivePostInteraction, color: :warning
|
403
|
+
|
404
|
+
# Use an icon from Phlex::TablerIcons
|
405
|
+
action :feature, interaction: FeaturePostInteraction,
|
406
|
+
icon: Phlex::TablerIcons::Star
|
407
|
+
|
408
|
+
# Add a confirmation dialog
|
409
|
+
action :delete_permanently, interaction: DeletePostInteraction,
|
410
|
+
color: :danger, confirm: "Are you sure?"
|
411
|
+
end
|
412
|
+
```
|
413
|
+
|
414
|
+
## UI Customization
|
415
|
+
|
416
|
+
### Page Titles and Descriptions
|
417
|
+
|
418
|
+
Page titles and descriptions are rendered using `phlexi_render`, which means they can be **strings**, **procs**, or **component instances**:
|
419
|
+
|
420
|
+
```ruby
|
421
|
+
class PostDefinition < Plutonium::Resource::Definition
|
422
|
+
# Static strings
|
423
|
+
index_page_title "All Posts"
|
424
|
+
index_page_description "Manage your blog posts"
|
425
|
+
|
426
|
+
# Dynamic procs (have access to context)
|
427
|
+
show_page_title -> { h1 { "#{current_record!.title} - Post Details" } }
|
428
|
+
show_page_description -> { h2 { "Created by #{current_record!.author.name} on #{current_record!.created_at.strftime('%B %d, %Y')}" } }
|
429
|
+
|
430
|
+
# Component instances for complex rendering
|
431
|
+
new_page_title -> { PageTitleComponent.new(text: "Create New Post", icon: :plus) }
|
432
|
+
edit_page_title -> { PageTitleComponent.new(text: "Edit: #{current_record!.title}", icon: :edit) }
|
433
|
+
|
434
|
+
# Conditional titles based on state
|
435
|
+
index_page_title -> {
|
436
|
+
case params[:status]
|
437
|
+
when 'published' then "Published Posts"
|
438
|
+
when 'draft' then "Draft Posts"
|
439
|
+
else "All Posts"
|
440
|
+
end
|
441
|
+
}
|
442
|
+
end
|
443
|
+
```
|
444
|
+
|
445
|
+
::: tip phlexi_render Context
|
446
|
+
Title and description procs are evaluated in the page rendering context, giving you access to:
|
447
|
+
- `current_record!` - The current record (for show/edit pages)
|
448
|
+
- `params` - Request parameters
|
449
|
+
- `current_user` - The authenticated user
|
450
|
+
- All helper methods available in the view context
|
451
|
+
:::
|
452
|
+
|
453
|
+
### Custom Page Classes
|
454
|
+
|
455
|
+
Override page classes for complete control over page rendering:
|
456
|
+
|
457
|
+
```ruby
|
458
|
+
class PostDefinition < Plutonium::Resource::Definition
|
459
|
+
class IndexPage < Plutonium::UI::Page::Resource::Index
|
460
|
+
def view_template(&block)
|
461
|
+
# Custom page header
|
462
|
+
div(class: "mb-8") do
|
463
|
+
h1(class: "text-3xl font-bold") { "Content Management" }
|
464
|
+
p(class: "text-gray-600") { "Manage your blog posts and articles" }
|
465
|
+
|
466
|
+
# Custom stats dashboard
|
467
|
+
div(class: "grid grid-cols-1 md:grid-cols-4 gap-4 mt-6") do
|
468
|
+
render_stat_card("Total Posts", Post.count)
|
469
|
+
render_stat_card("Published", Post.published.count)
|
470
|
+
render_stat_card("Drafts", Post.draft.count)
|
471
|
+
render_stat_card("This Month", Post.where(created_at: 1.month.ago..Time.current).count)
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
# Standard table rendering
|
476
|
+
super(&block)
|
477
|
+
end
|
478
|
+
|
479
|
+
private
|
480
|
+
|
481
|
+
def render_stat_card(title, value)
|
482
|
+
div(class: "bg-white p-4 rounded-lg shadow") do
|
483
|
+
div(class: "text-sm text-gray-500") { title }
|
484
|
+
div(class: "text-2xl font-bold") { value }
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
class ShowPage < Plutonium::UI::Page::Resource::Show
|
490
|
+
def view_template(&block)
|
491
|
+
div(class: "max-w-4xl mx-auto") do
|
492
|
+
# Custom breadcrumbs
|
493
|
+
nav(class: "mb-6") do
|
494
|
+
ol(class: "flex space-x-2 text-sm") do
|
495
|
+
li { link_to("Posts", posts_path, class: "text-blue-600") }
|
496
|
+
li { span(class: "text-gray-500") { "/" } }
|
497
|
+
li { span(class: "text-gray-900") { current_record.title.truncate(50) } }
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
# Two-column layout
|
502
|
+
div(class: "grid grid-cols-1 lg:grid-cols-3 gap-8") do
|
503
|
+
# Main content
|
504
|
+
div(class: "lg:col-span-2") do
|
505
|
+
super(&block)
|
506
|
+
end
|
507
|
+
|
508
|
+
# Sidebar with metadata
|
509
|
+
div(class: "lg:col-span-1") do
|
510
|
+
render_metadata_sidebar
|
511
|
+
end
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
private
|
517
|
+
|
518
|
+
def render_metadata_sidebar
|
519
|
+
div(class: "bg-gray-50 p-6 rounded-lg") do
|
520
|
+
h3(class: "text-lg font-medium mb-4") { "Post Metadata" }
|
521
|
+
|
522
|
+
dl(class: "space-y-3") do
|
523
|
+
render_metadata_item("Status", current_record.status.humanize)
|
524
|
+
render_metadata_item("Created", time_ago_in_words(current_record.created_at))
|
525
|
+
render_metadata_item("Updated", time_ago_in_words(current_record.updated_at))
|
526
|
+
render_metadata_item("Views", current_record.view_count)
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
def render_metadata_item(label, value)
|
532
|
+
div do
|
533
|
+
dt(class: "text-sm text-gray-500") { label }
|
534
|
+
dd(class: "text-sm font-medium") { value }
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
class NewPage < Plutonium::UI::Page::Resource::New
|
540
|
+
def page_title
|
541
|
+
"Create New #{current_record.class.model_name.human}"
|
542
|
+
end
|
543
|
+
|
544
|
+
def page_description
|
545
|
+
"Fill out the form below to create a new post. All fields marked with * are required."
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
class EditPage < Plutonium::UI::Page::Resource::Edit
|
550
|
+
def page_title
|
551
|
+
"Edit: #{current_record.title}"
|
552
|
+
end
|
553
|
+
|
554
|
+
def page_description
|
555
|
+
"Last updated #{time_ago_in_words(current_record.updated_at)} ago"
|
556
|
+
end
|
557
|
+
end
|
558
|
+
end
|
559
|
+
```
|
560
|
+
|
561
|
+
### Custom Form Classes
|
562
|
+
|
563
|
+
Override form classes for complete control over form rendering:
|
564
|
+
|
565
|
+
```ruby
|
566
|
+
class PostDefinition < Plutonium::Resource::Definition
|
567
|
+
class Form < Plutonium::UI::Form::Resource
|
568
|
+
def form_template
|
569
|
+
# Custom form layout
|
570
|
+
div(class: "grid grid-cols-1 lg:grid-cols-3 gap-8") do
|
571
|
+
# Main content area
|
572
|
+
div(class: "lg:col-span-2") do
|
573
|
+
render_main_fields
|
574
|
+
end
|
575
|
+
|
576
|
+
# Sidebar
|
577
|
+
div(class: "lg:col-span-1") do
|
578
|
+
render_sidebar_fields
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
582
|
+
render_actions
|
583
|
+
end
|
584
|
+
|
585
|
+
private
|
586
|
+
|
587
|
+
def render_main_fields
|
588
|
+
# Group related fields
|
589
|
+
fieldset(class: "space-y-6") do
|
590
|
+
legend(class: "text-lg font-medium") { "Content" }
|
591
|
+
|
592
|
+
render field(:title).input_tag(placeholder: "Enter a compelling title")
|
593
|
+
render field(:content).easymde_tag
|
594
|
+
render field(:excerpt).input_tag(as: :textarea, rows: 3)
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
def render_sidebar_fields
|
599
|
+
# Publishing controls
|
600
|
+
fieldset(class: "space-y-4") do
|
601
|
+
legend(class: "text-lg font-medium") { "Publishing" }
|
602
|
+
|
603
|
+
render field(:status).input_tag(as: :select)
|
604
|
+
render field(:published_at).flatpickr_tag
|
605
|
+
render field(:featured).input_tag(as: :boolean)
|
606
|
+
end
|
607
|
+
|
608
|
+
# Categorization
|
609
|
+
fieldset(class: "space-y-4 mt-8") do
|
610
|
+
legend(class: "text-lg font-medium") { "Categorization" }
|
611
|
+
|
612
|
+
render field(:category).belongs_to_tag
|
613
|
+
render field(:tags).has_many_tag
|
614
|
+
end
|
615
|
+
end
|
616
|
+
end
|
617
|
+
end
|
618
|
+
```
|
619
|
+
|
620
|
+
## Policy Integration
|
621
|
+
|
622
|
+
**Field visibility is controlled by policies, not definitions:**
|
623
|
+
|
624
|
+
```ruby
|
625
|
+
# app/policies/post_policy.rb
|
626
|
+
class PostPolicy < Plutonium::Resource::Policy
|
627
|
+
def permitted_attributes_for_show
|
628
|
+
if user.admin?
|
629
|
+
[:title, :content, :admin_notes] # Admin sees admin_notes
|
630
|
+
else
|
631
|
+
[:title, :content] # Regular users don't
|
632
|
+
end
|
633
|
+
end
|
634
|
+
|
635
|
+
def permitted_attributes_for_create
|
636
|
+
if user.admin?
|
637
|
+
[:title, :content, :published, :featured, :admin_notes]
|
638
|
+
else
|
639
|
+
[:title, :content]
|
640
|
+
end
|
641
|
+
end
|
642
|
+
|
643
|
+
def permitted_attributes_for_update
|
644
|
+
attrs = permitted_attributes_for_create
|
645
|
+
|
646
|
+
# Authors can edit their own posts
|
647
|
+
if user == record.author
|
648
|
+
attrs + [:draft_notes]
|
649
|
+
else
|
650
|
+
attrs
|
651
|
+
end
|
652
|
+
end
|
653
|
+
|
654
|
+
def permitted_associations
|
655
|
+
[:author, :tags, :comments]
|
656
|
+
end
|
657
|
+
end
|
658
|
+
```
|
659
|
+
|
660
|
+
## Integration Points
|
661
|
+
|
662
|
+
### Resource Integration
|
663
|
+
|
664
|
+
Definitions are automatically discovered and used by resource controllers:
|
665
|
+
|
666
|
+
```ruby
|
667
|
+
# app/definitions/post_definition.rb
|
668
|
+
class PostDefinition < Plutonium::Resource::Definition
|
669
|
+
# All model attributes are auto-detected!
|
670
|
+
# No field declarations needed unless overriding
|
671
|
+
|
672
|
+
# Only customize what you need:
|
673
|
+
input :content, as: :rich_text # Override text -> rich_text
|
674
|
+
display :content, as: :markdown # Override text -> markdown
|
675
|
+
|
676
|
+
search { |scope, search| scope.where("title ILIKE ?", "%#{search}%") }
|
677
|
+
end
|
678
|
+
|
679
|
+
# app/controllers/posts_controller.rb
|
680
|
+
class PostsController < ApplicationController
|
681
|
+
include Plutonium::Resource::Controller
|
682
|
+
# PostDefinition is automatically used
|
683
|
+
end
|
684
|
+
```
|
685
|
+
|
686
|
+
### Interaction Integration
|
687
|
+
|
688
|
+
Actions integrate with the Interaction system:
|
689
|
+
|
690
|
+
```ruby
|
691
|
+
class PostDefinition < Plutonium::Resource::Definition
|
692
|
+
action :publish, interaction: PublishPostInteraction
|
693
|
+
end
|
694
|
+
|
695
|
+
class PublishPostInteraction < Plutonium::Interaction::Base
|
696
|
+
attribute :resource
|
697
|
+
attribute :publish_date, :date
|
698
|
+
|
699
|
+
def execute
|
700
|
+
resource.update!(published: true, published_at: publish_date || Time.current)
|
701
|
+
succeed(resource).with_redirect_response(resource_url_for(resource))
|
702
|
+
end
|
703
|
+
end
|
704
|
+
```
|
705
|
+
|
706
|
+
## Best Practices
|
707
|
+
|
708
|
+
### Field Type Philosophy
|
709
|
+
- **Let auto-detection work**: Don't declare fields unless you need to override
|
710
|
+
- **Override when needed**: Use declarations to change text to rich_text, datetime to date, etc.
|
711
|
+
- **Use conditions sparingly**: Prefer policy-based visibility over conditional fields
|
712
|
+
|
713
|
+
### Separation of Concerns
|
714
|
+
- **Definitions**: Configure HOW fields are rendered and processed
|
715
|
+
- **Policies**: Control WHAT fields are visible and editable
|
716
|
+
- **Interactions**: Handle business logic and operations
|
717
|
+
|
718
|
+
### Minimal Configuration Approach
|
719
|
+
```ruby
|
720
|
+
# Preferred: Let auto-detection work, only override what you need
|
721
|
+
class PostDefinition < Plutonium::Resource::Definition
|
722
|
+
# All fields auto-detected from Post model
|
723
|
+
|
724
|
+
# Only declare overrides:
|
725
|
+
input :content, as: :rich_text
|
726
|
+
display :content, as: :markdown
|
727
|
+
|
728
|
+
search { |scope, search| scope.where("title ILIKE ?", "%#{search}%") }
|
729
|
+
end
|
730
|
+
|
731
|
+
# Avoid: Over-declaring fields that would be auto-detected anyway
|
732
|
+
class PostDefinition < Plutonium::Resource::Definition
|
733
|
+
field :title, as: :string # Unnecessary - auto-detected
|
734
|
+
field :content, as: :text # Unnecessary - auto-detected
|
735
|
+
field :author, as: :association # Unnecessary - auto-detected
|
736
|
+
|
737
|
+
# This creates extra maintenance burden
|
738
|
+
end
|
739
|
+
```
|
740
|
+
|
741
|
+
The Definition module provides a clean, declarative way to configure resource behavior while maintaining clear separation between configuration (definitions), authorization (policies), and business logic (interactions).
|
742
|
+
|
743
|
+
## Related Modules
|
744
|
+
|
745
|
+
- **[Resource Record](./resource_record.md)** - Resource controllers and CRUD operations
|
746
|
+
- **[UI](./ui.md)** - User interface components
|
747
|
+
- **[Query](./query.md)** - Query objects and filtering
|
748
|
+
- **[Action](./action.md)** - Custom actions and operations
|
749
|
+
- **[Interaction](./interaction.md)** - Business logic encapsulation
|
750
|
+
|
751
|
+
## Available Field Types
|
752
|
+
|
753
|
+
### Input Types (Form Components)
|
754
|
+
- **Text**: `:string`, `:text`, `:email`, `:url`, `:tel`, `:password`
|
755
|
+
- **Rich Text**: `:rich_text`, `:markdown` (uses EasyMDE)
|
756
|
+
- **Numeric**: `:number`, `:integer`, `:decimal`, `:range`
|
757
|
+
- **Boolean**: `:boolean`
|
758
|
+
- **Date/Time**: `:date`, `:time`, `:datetime` (uses Flatpickr)
|
759
|
+
- **Selection**: `:select`, `:slim_select`, `:radio_buttons`, `:check_boxes`
|
760
|
+
- **Files**: `:file`, `:uppy`, `:attachment` (uses Uppy)
|
761
|
+
- **Associations**: `:association`, `:secure_association`, `:belongs_to`, `:has_many`, `:has_one`
|
762
|
+
- **Special**: `:hidden`, `:color`, `:phone` (uses IntlTelInput)
|
763
|
+
|
764
|
+
### Display Types (Show/Index Components)
|
765
|
+
- **Text**: `:string`, `:text`, `:email`, `:url`, `:phone`
|
766
|
+
- **Rich Content**: `:markdown` (renders with Redcarpet)
|
767
|
+
- **Numeric**: `:number`, `:integer`, `:decimal`
|
768
|
+
- **Boolean**: `:boolean`
|
769
|
+
- **Date/Time**: `:date`, `:time`, `:datetime`
|
770
|
+
- **Associations**: `:association` (auto-links to show page)
|
771
|
+
- **Files**: `:attachment` (shows previews/downloads)
|
772
|
+
- **Custom**: `:phlexi_render` (for custom components)
|
773
|
+
|
774
|
+
## Available Configuration Options
|
775
|
+
|
776
|
+
### Field Options
|
777
|
+
```ruby
|
778
|
+
field :name, as: :string, class: "custom-class", wrapper: {class: "field-wrapper"}
|
779
|
+
```
|
780
|
+
|
781
|
+
### Input Options
|
782
|
+
```ruby
|
783
|
+
input :title,
|
784
|
+
as: :string,
|
785
|
+
placeholder: "Enter title",
|
786
|
+
required: true,
|
787
|
+
class: "custom-input",
|
788
|
+
wrapper: {class: "input-wrapper"},
|
789
|
+
data: {controller: "custom"},
|
790
|
+
condition: -> { current_user.admin? }
|
791
|
+
```
|
792
|
+
|
793
|
+
### Display Options
|
794
|
+
```ruby
|
795
|
+
display :content,
|
796
|
+
as: :markdown,
|
797
|
+
class: "prose",
|
798
|
+
wrapper: {class: "content-wrapper"},
|
799
|
+
condition: -> { current_user.can_see_content? }
|
800
|
+
```
|
801
|
+
|
802
|
+
### Collection Options (for selects)
|
803
|
+
```ruby
|
804
|
+
input :category, as: :select, collection: %w[Tech Business Lifestyle]
|
805
|
+
input :author, as: :select, collection: -> { User.active.pluck(:name, :id) }
|
806
|
+
|
807
|
+
# Collection procs are executed in the form rendering context
|
808
|
+
# and have access to current_user and other helpers:
|
809
|
+
input :team_members, as: :select, collection: -> {
|
810
|
+
current_user.organization.users.active.pluck(:name, :id)
|
811
|
+
}
|
812
|
+
|
813
|
+
# You can also access the form object being edited:
|
814
|
+
input :related_posts, as: :select, collection: -> {
|
815
|
+
Post.where.not(id: object.id).published.pluck(:title, :id) if object.persisted?
|
816
|
+
}
|
817
|
+
```
|
818
|
+
|
819
|
+
::: tip Collection Context
|
820
|
+
Collection procs are evaluated in the form rendering context, which means they have access to:
|
821
|
+
- `current_user` - The authenticated user
|
822
|
+
- `current_parent` - Parent record for nested resources
|
823
|
+
- `object` - The record being edited (in edit forms)
|
824
|
+
- `request` and `params` - Request information
|
825
|
+
- All helper methods available in the form context
|
826
|
+
|
827
|
+
This is the same context as `condition` procs, allowing for dynamic, user-specific collections.
|
828
|
+
:::
|
829
|
+
|
830
|
+
### File Upload Options
|
831
|
+
```ruby
|
832
|
+
input :avatar, as: :file, multiple: false
|
833
|
+
input :documents, as: :file, multiple: true,
|
834
|
+
allowed_file_types: ['.pdf', '.doc', '.docx'],
|
835
|
+
max_file_size: 5.megabytes
|
836
|
+
```
|
837
|
+
|
838
|
+
## Dynamic Configuration & Policies
|
839
|
+
|
840
|
+
::: danger IMPORTANT
|
841
|
+
Definitions are instantiated outside the controller context, which means **`current_user` and other controller methods are NOT available** within the definition file itself. However, `condition` and `collection` procs ARE evaluated in the rendering context where `current_user` and the record (`object`) are available.
|
842
|
+
:::
|
843
|
+
|
844
|
+
The `condition` option configures **if an input is rendered**. It does not control if a field's *value* is accessible. For that, you must use policies.
|
845
|
+
|
846
|
+
::: code-group
|
847
|
+
```ruby [Static Definition]
|
848
|
+
# app/definitions/post_definition.rb
|
849
|
+
class PostDefinition < Plutonium::Resource::Definition
|
850
|
+
# This configuration is static.
|
851
|
+
# The :admin_notes field is always defined here.
|
852
|
+
def customize_fields
|
853
|
+
field :admin_notes, as: :text
|
854
|
+
end
|
855
|
+
|
856
|
+
def customize_displays
|
857
|
+
display :admin_notes, as: :text
|
858
|
+
end
|
859
|
+
|
860
|
+
def customize_inputs
|
861
|
+
input :admin_notes, as: :text
|
862
|
+
end
|
863
|
+
end
|
864
|
+
```
|
865
|
+
```ruby [Dynamic Policy]
|
866
|
+
# app/policies/post_policy.rb
|
867
|
+
class PostPolicy < Plutonium::Resource::Policy
|
868
|
+
# The policy determines if the user can SEE the field.
|
869
|
+
def permitted_attributes_for_show
|
870
|
+
if user.admin?
|
871
|
+
[:title, :content, :admin_notes] # Admin sees admin_notes
|
872
|
+
else
|
873
|
+
[:title, :content] # Regular users do not
|
874
|
+
end
|
875
|
+
end
|
876
|
+
end
|