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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/plutonium.css +2 -2
  3. data/config/initializers/sqlite_json_alias.rb +1 -1
  4. data/docs/.vitepress/config.ts +60 -19
  5. data/docs/guide/cursor-rules.md +75 -0
  6. data/docs/guide/deep-dive/authorization.md +189 -0
  7. data/docs/guide/{getting-started → deep-dive}/resources.md +137 -0
  8. data/docs/guide/getting-started/{installation.md → 01-installation.md} +0 -105
  9. data/docs/guide/index.md +28 -0
  10. data/docs/guide/introduction/02-core-concepts.md +440 -0
  11. data/docs/guide/tutorial/01-project-setup.md +75 -0
  12. data/docs/guide/tutorial/02-creating-a-feature-package.md +45 -0
  13. data/docs/guide/tutorial/03-defining-resources.md +90 -0
  14. data/docs/guide/tutorial/04-creating-a-portal.md +101 -0
  15. data/docs/guide/tutorial/05-customizing-the-ui.md +128 -0
  16. data/docs/guide/tutorial/06-adding-custom-actions.md +101 -0
  17. data/docs/guide/tutorial/07-implementing-authorization.md +90 -0
  18. data/docs/index.md +24 -31
  19. data/docs/modules/action.md +190 -0
  20. data/docs/modules/authentication.md +236 -0
  21. data/docs/modules/configuration.md +599 -0
  22. data/docs/modules/controller.md +398 -0
  23. data/docs/modules/core.md +316 -0
  24. data/docs/modules/definition.md +876 -0
  25. data/docs/modules/display.md +759 -0
  26. data/docs/modules/form.md +605 -0
  27. data/docs/modules/generator.md +288 -0
  28. data/docs/modules/index.md +167 -0
  29. data/docs/modules/interaction.md +470 -0
  30. data/docs/modules/package.md +151 -0
  31. data/docs/modules/policy.md +176 -0
  32. data/docs/modules/portal.md +710 -0
  33. data/docs/modules/query.md +287 -0
  34. data/docs/modules/resource_record.md +618 -0
  35. data/docs/modules/routing.md +641 -0
  36. data/docs/modules/table.md +293 -0
  37. data/docs/modules/ui.md +631 -0
  38. data/docs/public/plutonium.mdc +667 -0
  39. data/lib/generators/pu/core/assets/assets_generator.rb +0 -5
  40. data/lib/plutonium/ui/display/resource.rb +7 -2
  41. data/lib/plutonium/ui/table/resource.rb +8 -3
  42. data/lib/plutonium/version.rb +1 -1
  43. metadata +36 -9
  44. data/docs/guide/getting-started/authorization.md +0 -296
  45. data/docs/guide/getting-started/core-concepts.md +0 -432
  46. data/docs/guide/getting-started/index.md +0 -21
  47. data/docs/guide/tutorial.md +0 -401
  48. /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