plutonium 0.33.1 → 0.34.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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/# Plutonium: The pre-alpha demo.md +4 -2
  3. data/.claude/skills/assets/SKILL.md +416 -0
  4. data/.claude/skills/connect-resource/SKILL.md +112 -0
  5. data/.claude/skills/controller/SKILL.md +302 -0
  6. data/.claude/skills/create-resource/SKILL.md +240 -0
  7. data/.claude/skills/definition/SKILL.md +218 -0
  8. data/.claude/skills/definition-actions/SKILL.md +386 -0
  9. data/.claude/skills/definition-fields/SKILL.md +474 -0
  10. data/.claude/skills/definition-query/SKILL.md +334 -0
  11. data/.claude/skills/forms/SKILL.md +439 -0
  12. data/.claude/skills/installation/SKILL.md +300 -0
  13. data/.claude/skills/interaction/SKILL.md +382 -0
  14. data/.claude/skills/model/SKILL.md +267 -0
  15. data/.claude/skills/model-features/SKILL.md +286 -0
  16. data/.claude/skills/nested-resources/SKILL.md +274 -0
  17. data/.claude/skills/package/SKILL.md +191 -0
  18. data/.claude/skills/policy/SKILL.md +352 -0
  19. data/.claude/skills/portal/SKILL.md +400 -0
  20. data/.claude/skills/resource/SKILL.md +281 -0
  21. data/.claude/skills/rodauth/SKILL.md +452 -0
  22. data/.claude/skills/views/SKILL.md +563 -0
  23. data/Appraisals +46 -4
  24. data/CHANGELOG.md +32 -1
  25. data/app/assets/plutonium.css +2 -2
  26. data/config/brakeman.ignore +239 -0
  27. data/config/initializers/action_policy.rb +1 -1
  28. data/docs/.vitepress/config.ts +132 -47
  29. data/docs/concepts/architecture.md +226 -0
  30. data/docs/concepts/auto-detection.md +254 -0
  31. data/docs/concepts/index.md +61 -0
  32. data/docs/concepts/packages-portals.md +304 -0
  33. data/docs/concepts/resources.md +224 -0
  34. data/docs/cookbook/blog.md +412 -0
  35. data/docs/cookbook/index.md +289 -0
  36. data/docs/cookbook/saas.md +481 -0
  37. data/docs/getting-started/index.md +56 -0
  38. data/docs/getting-started/installation.md +146 -0
  39. data/docs/getting-started/tutorial/01-setup.md +118 -0
  40. data/docs/getting-started/tutorial/02-first-resource.md +180 -0
  41. data/docs/getting-started/tutorial/03-authentication.md +246 -0
  42. data/docs/getting-started/tutorial/04-authorization.md +170 -0
  43. data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
  44. data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
  45. data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
  46. data/docs/getting-started/tutorial/index.md +64 -0
  47. data/docs/guides/adding-resources.md +420 -0
  48. data/docs/guides/authentication.md +551 -0
  49. data/docs/guides/authorization.md +468 -0
  50. data/docs/guides/creating-packages.md +380 -0
  51. data/docs/guides/custom-actions.md +523 -0
  52. data/docs/guides/index.md +45 -0
  53. data/docs/guides/multi-tenancy.md +302 -0
  54. data/docs/guides/nested-resources.md +411 -0
  55. data/docs/guides/search-filtering.md +266 -0
  56. data/docs/guides/theming.md +321 -0
  57. data/docs/index.md +67 -26
  58. data/docs/public/CLAUDE.md +64 -21
  59. data/docs/reference/assets/index.md +496 -0
  60. data/docs/reference/controller/index.md +363 -0
  61. data/docs/reference/definition/actions.md +400 -0
  62. data/docs/reference/definition/fields.md +350 -0
  63. data/docs/reference/definition/index.md +252 -0
  64. data/docs/reference/definition/query.md +342 -0
  65. data/docs/reference/generators/index.md +469 -0
  66. data/docs/reference/index.md +49 -0
  67. data/docs/reference/interaction/index.md +445 -0
  68. data/docs/reference/model/features.md +248 -0
  69. data/docs/reference/model/index.md +219 -0
  70. data/docs/reference/policy/index.md +385 -0
  71. data/docs/reference/portal/index.md +382 -0
  72. data/docs/reference/views/forms.md +396 -0
  73. data/docs/reference/views/index.md +479 -0
  74. data/gemfiles/rails_7.gemfile +9 -2
  75. data/gemfiles/rails_7.gemfile.lock +146 -111
  76. data/gemfiles/rails_8.0.gemfile +20 -0
  77. data/gemfiles/rails_8.0.gemfile.lock +417 -0
  78. data/gemfiles/rails_8.1.gemfile +20 -0
  79. data/gemfiles/rails_8.1.gemfile.lock +419 -0
  80. data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
  81. data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
  82. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
  83. data/lib/generators/pu/pkg/portal/USAGE +65 -0
  84. data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
  85. data/lib/generators/pu/res/conn/USAGE +71 -0
  86. data/lib/generators/pu/res/model/USAGE +106 -110
  87. data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
  88. data/lib/generators/pu/res/scaffold/USAGE +85 -0
  89. data/lib/generators/pu/rodauth/install_generator.rb +2 -6
  90. data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
  91. data/lib/generators/pu/skills/sync/USAGE +14 -0
  92. data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
  93. data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
  94. data/lib/plutonium/core/controller.rb +2 -2
  95. data/lib/plutonium/interaction/base.rb +1 -0
  96. data/lib/plutonium/package/engine.rb +2 -2
  97. data/lib/plutonium/query/adhoc_block.rb +6 -2
  98. data/lib/plutonium/query/model_scope.rb +1 -1
  99. data/lib/plutonium/railtie.rb +4 -0
  100. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
  101. data/lib/plutonium/resource/query_object.rb +38 -8
  102. data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
  103. data/lib/plutonium/version.rb +1 -1
  104. data/lib/tasks/release.rake +19 -4
  105. data/package.json +1 -1
  106. metadata +76 -39
  107. data/brakeman.ignore +0 -28
  108. data/docs/api-examples.md +0 -49
  109. data/docs/guide/claude-code-guide.md +0 -74
  110. data/docs/guide/deep-dive/authorization.md +0 -189
  111. data/docs/guide/deep-dive/multitenancy.md +0 -256
  112. data/docs/guide/deep-dive/resources.md +0 -390
  113. data/docs/guide/getting-started/01-installation.md +0 -165
  114. data/docs/guide/index.md +0 -28
  115. data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
  116. data/docs/guide/introduction/02-core-concepts.md +0 -440
  117. data/docs/guide/tutorial/01-project-setup.md +0 -75
  118. data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
  119. data/docs/guide/tutorial/03-defining-resources.md +0 -90
  120. data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
  121. data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
  122. data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
  123. data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
  124. data/docs/markdown-examples.md +0 -85
  125. data/docs/modules/action.md +0 -244
  126. data/docs/modules/authentication.md +0 -236
  127. data/docs/modules/configuration.md +0 -599
  128. data/docs/modules/controller.md +0 -443
  129. data/docs/modules/core.md +0 -316
  130. data/docs/modules/definition.md +0 -1308
  131. data/docs/modules/display.md +0 -759
  132. data/docs/modules/form.md +0 -495
  133. data/docs/modules/generator.md +0 -400
  134. data/docs/modules/index.md +0 -167
  135. data/docs/modules/interaction.md +0 -642
  136. data/docs/modules/package.md +0 -151
  137. data/docs/modules/policy.md +0 -176
  138. data/docs/modules/portal.md +0 -710
  139. data/docs/modules/query.md +0 -297
  140. data/docs/modules/resource_record.md +0 -618
  141. data/docs/modules/routing.md +0 -690
  142. data/docs/modules/table.md +0 -301
  143. data/docs/modules/ui.md +0 -631
@@ -1,1308 +0,0 @@
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, choices: %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
- # Field-level options for inputs (affect the field wrapper)
147
- input :title,
148
- label: "Article Title",
149
- hint: "Enter a descriptive title",
150
- placeholder: "e.g. My First Post"
151
-
152
- # Field-level options for displays (affect the field wrapper)
153
- display :content,
154
- label: "Article Content",
155
- description: "Published article content",
156
- placeholder: "No content available"
157
-
158
- # Tag-level options (passed to the HTML element)
159
- input :slug, class: "font-mono"
160
- display :title, class: "text-2xl font-bold"
161
-
162
- # Wrapper styling
163
- display :content, wrapper: {class: "prose max-w-none"}
164
- end
165
- ```
166
-
167
- ::: tip Field-level vs Tag-level Options
168
- Plutonium distinguishes between two types of options:
169
- - **Field-level options** (`label`, `hint`, `description`, `placeholder`) - Processed by the field wrapper, affect labels and help text
170
- - **Tag-level options** (`class`, `data`, `required`, etc.) - Passed directly to the HTML element
171
-
172
- The framework automatically routes each option to the correct destination.
173
- :::
174
-
175
- ### 3. **Configure Select Options**
176
- ```ruby
177
- class PostDefinition < Plutonium::Resource::Definition
178
- # Provide options for select inputs
179
- input :category, as: :select, choices: %w[Tech Business Lifestyle]
180
- input :author, as: :select, choices: -> { User.active.pluck(:name, :id) }
181
- end
182
- ```
183
-
184
- ### 4. **Add Conditional Logic**
185
- ```ruby
186
- class PostDefinition < Plutonium::Resource::Definition
187
- # Conditional logic is for showing/hiding fields based on the application's
188
- # state or other field values. It is not for authorization. Use policies
189
- # to control access to data.
190
-
191
- # Conditional fields based on the object's state
192
- display :published_at, condition: -> { object.published? }
193
- display :reason_for_rejection, condition: -> { object.rejected? }
194
- column :published_at, condition: -> { object.published? }
195
-
196
- # Use `pre_submit` to create dynamic forms where inputs appear based on other inputs.
197
- input :send_notifications, as: :boolean, pre_submit: true
198
- input :notification_channel, as: :select, choices: %w[Email SMS],
199
- condition: -> { object.send_notifications? }
200
-
201
- # Show debug fields only in development
202
- field :debug_info, as: :string, condition: -> { Rails.env.development? }
203
- end
204
- ```
205
-
206
- ::: danger Authorization with Policies
207
- 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.
208
- :::
209
-
210
- ::: tip Condition Context & Dynamic Forms
211
- `condition` procs are evaluated in their respective rendering contexts and have access to contextual data.
212
-
213
- **For `input` fields** (form rendering context):
214
- - `object` - The record being edited
215
- - `current_parent` - Parent record for nested resources
216
- - `request` and `params` - Request information
217
- - All helper methods available in the form context
218
-
219
- **For `display` fields** (display rendering context):
220
- - `object` - The record being displayed
221
- - `current_parent` - Parent record for nested resources
222
- - All helper methods available in the display context
223
-
224
- **For `column` fields** (table rendering context):
225
- - `current_parent` - Parent record for nested resources
226
- - All helper methods available in the table context
227
-
228
- 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.
229
- :::
230
-
231
- ### 5. Custom Field Rendering
232
-
233
- Plutonium offers three main approaches for rendering fields in a definition. Choose the one that best fits your needs for clarity, flexibility, and control.
234
-
235
- **Quick Reference:**
236
- - **`as: :symbol`** - Use built-in components for input and display (e.g., `:rich_text`, `:date`, `:markdown`)
237
- - **`as: ComponentClass`** - Use custom component classes for input and display (new feature)
238
- - **Block syntax** - Use for conditional logic and custom builder method calls
239
- - **`as: :phlexi_tag`** - Advanced inline rendering with maximum flexibility (display only)
240
-
241
- #### 1. The `as:` Option (Recommended)
242
-
243
- The `as:` option is the simplest and most common way to specify a rendering component for both `input` and `display` declarations. It's ideal for using standard built-in components or overriding auto-detected types.
244
-
245
- **New Feature**: You can now pass a component class directly to the `as:` option for custom rendering in both input and display contexts.
246
-
247
- **Use When:**
248
- - Using standard or enhanced built-in components for input or display.
249
- - You want clean, readable code with minimal boilerplate.
250
- - Overriding an auto-detected type (e.g., `text` to `rich_text` for input, or `text` to `markdown` for display).
251
- - Using custom component classes for rendering.
252
-
253
- ::: code-group
254
- ```ruby [Standard Input Fields]
255
- # Simple and concise overrides
256
- class PostDefinition < Plutonium::Resource::Definition
257
- input :content, as: :rich_text
258
- input :published_at, as: :date
259
- input :avatar, as: :uppy
260
-
261
- # With options
262
- input :email, as: :email, placeholder: "Enter email"
263
- end
264
- ```
265
- ```ruby [Custom Component Classes]
266
- # Using custom component classes
267
- class PostDefinition < Plutonium::Resource::Definition
268
- # Pass component class to input
269
- input :color_picker, as: ColorPickerComponent
270
- input :custom_widget, as: MyCustomInputComponent
271
-
272
- # Pass component class to display
273
- display :status_badge, as: StatusBadgeComponent
274
- display :chart, as: ChartComponent
275
- end
276
- ```
277
- ```ruby [Standard Display Fields]
278
- # Simple and concise overrides
279
- class PostDefinition < Plutonium::Resource::Definition
280
- display :content, as: :markdown
281
- display :author, as: :association
282
- display :documents, as: :attachment
283
-
284
- # With styling options
285
- display :status, as: :string, class: "badge badge-success"
286
- end
287
- ```
288
- :::
289
-
290
- #### 2. The Block Syntax
291
-
292
- 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.
293
-
294
- **Important for Input Blocks**: When using blocks with `input` declarations, you can only use existing form builder methods (like `f.date_tag`, `f.text_tag`, etc.). You cannot return arbitrary components because the form system requires inputs to be registered internally.
295
-
296
- **Use When:**
297
- - Integrating custom-built Phlex or ViewComponent components (for `display` only).
298
- - Building complex layouts with multiple components or custom HTML (for `display` only).
299
- - You need conditional logic to determine which component to render.
300
- - You need to call specific form builder methods with custom logic (for `input`).
301
- - **You need dynamic choices for select inputs** (since `choices:` option only accepts static arrays).
302
-
303
- ::: code-group
304
- ```ruby [Custom Display Components]
305
- # Custom display component - can return any component
306
- display :chart_data do |field|
307
- ChartComponent.new(data: field.value, type: :bar)
308
- end
309
- ```
310
- ```ruby [Custom Input with Builder Methods]
311
- # Custom input - can only use form builder methods
312
- input :birth_date do |f|
313
- # Can use builder methods with custom logic
314
- case object.age_category
315
- when 'adult'
316
- f.date_tag(min: 18.years.ago.to_date)
317
- when 'minor'
318
- f.date_tag(max: 18.years.ago.to_date)
319
- else
320
- f.date_tag
321
- end
322
- end
323
- ```
324
- ```ruby [Dynamic Choices for Select Inputs]
325
- # Dynamic choices based on object state
326
- input :widget_type do |f|
327
- choices = case object.question_type
328
- when nil, ""
329
- []
330
- when "text"
331
- ["input", "textarea"]
332
- when "choice"
333
- ["radio", "select", "checkbox"]
334
- when "scale"
335
- ["slider", "radio_scale", "select_scale"]
336
- when "date"
337
- ["date_picker", "datetime_picker"]
338
- when "boolean"
339
- ["checkbox", "toggle", "yes_no"]
340
- else
341
- Question::WIDGET_TYPES
342
- end
343
-
344
- f.select_tag choices: choices
345
- end
346
- ```
347
- ```ruby [Conditional Rendering]
348
- # Conditional display based on value
349
- display :metrics do |field|
350
- if field.value.present?
351
- MetricsChartComponent.new(data: field.value)
352
- else
353
- EmptyStateComponent.new(message: "No metrics available")
354
- end
355
- end
356
- ```
357
- :::
358
-
359
- #### 3. `as: :phlexi_tag` (Advanced)
360
-
361
- `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.
362
-
363
- **Use When:**
364
- - Building reusable component libraries that need to be highly configurable.
365
- - Working with polymorphic data that requires specialized renderers.
366
- - You need complex rendering logic but want to keep it inline in the definition.
367
-
368
- ::: code-group
369
- ```ruby [With a Component Class]
370
- # Pass a component class for rendering.
371
- # The component's #initialize will receive (value, **attrs).
372
- display :status, as: :phlexi_tag, with: StatusBadgeComponent
373
- ```
374
- ```ruby [With an Inline Proc]
375
- # Use a proc for complex inline logic.
376
- # The proc receives (value, attrs).
377
- display :priority, as: :phlexi_tag, with: ->(value, attrs) {
378
- case value
379
- when 'high'
380
- span(class: tokens("badge badge-danger", attrs[:class])) { "High" }
381
- when 'medium'
382
- span(class: tokens("badge badge-warning", attrs[:class])) { "Medium" }
383
- else
384
- span(class: tokens("badge badge-info", attrs[:class])) { "Low" }
385
- end
386
- }
387
- ```
388
- ```ruby [Handling Polymorphic Content]
389
- # Dynamically render different components based on content type.
390
- display :rich_content, as: :phlexi_tag, with: ->(value, attrs) {
391
- # `value` is the rich_content object itself
392
- case value&.content_type
393
- when 'markdown'
394
- MarkdownComponent.new(content: value.body, **attrs)
395
- when 'image'
396
- # Must return a proc for inline HTML rendering with Phlex
397
- proc { img(src: value.url, alt: value.caption, **attrs) }
398
- else
399
- nil # Fallback to default rendering: <p>#{value}</p>
400
- end
401
- }
402
- ```
403
- :::
404
-
405
- ## Minimal Definition Example
406
-
407
- Here's what a typical definition looks like when leveraging auto-detection:
408
-
409
- ```ruby
410
- class PostDefinition < Plutonium::Resource::Definition
411
- # No field declarations needed! All attributes auto-detected.
412
- # Post model columns, associations, and attachments are automatically available.
413
-
414
- # Only customize what you need to override:
415
- input :content, as: :rich_text
416
- display :content, as: :markdown
417
-
418
- # Add search and filtering:
419
- search do |scope, query|
420
- scope.where("title ILIKE ? OR content ILIKE ?", "%#{query}%", "%#{query}%")
421
- end
422
-
423
- filter :status, with: Plutonium::Query::Filters::Text, predicate: :eq
424
-
425
- # Add custom actions:
426
- action :publish, interaction: PublishPostInteraction
427
- end
428
- ```
429
-
430
- 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.
431
-
432
- ## Search, Filters, and Scopes
433
-
434
- Configure how users can query the resource index.
435
-
436
- ::: code-group
437
- ```ruby [Search]
438
- # Defines the global search logic for the resource.
439
- class PostDefinition < Plutonium::Resource::Definition
440
- search do |scope, query|
441
- scope.where("title ILIKE ? OR content ILIKE ?", "%#{query}%", "%#{query}%")
442
- end
443
- end
444
- ```
445
- ```ruby [Filters]
446
- # Currently, only Text filter is implemented
447
- class PostDefinition < Plutonium::Resource::Definition
448
- filter :title, with: Plutonium::Query::Filters::Text, predicate: :contains
449
- filter :status, with: Plutonium::Query::Filters::Text, predicate: :eq
450
- filter :category, with: Plutonium::Query::Filters::Text, predicate: :eq
451
-
452
- # Available predicates: :eq, :not_eq, :contains, :not_contains,
453
- # :starts_with, :ends_with, :matches, :not_matches
454
- end
455
- ```
456
- ```ruby [Scopes]
457
- # Defines named scopes that appear as buttons.
458
- class PostDefinition < Plutonium::Resource::Definition
459
- scope :published
460
- scope :featured
461
- scope :recent, -> { where('created_at > ?', 1.week.ago) }
462
- end
463
- ```
464
- :::
465
-
466
- ## Actions
467
-
468
- Define custom operations that can be performed on a resource.
469
-
470
- ```ruby
471
- class PostDefinition < Plutonium::Resource::Definition
472
- # Each `action` call defines ONE action.
473
- action :publish, interaction: PublishPostInteraction
474
- action :archive, interaction: ArchivePostInteraction, color: :warning
475
-
476
- # Use an icon from Phlex::TablerIcons
477
- action :feature, interaction: FeaturePostInteraction,
478
- icon: Phlex::TablerIcons::Star
479
-
480
- # Add a confirmation dialog
481
- action :delete_permanently, interaction: DeletePostInteraction,
482
- color: :danger, confirm: "Are you sure?"
483
- end
484
- ```
485
-
486
- ## UI Customization
487
-
488
- ### Page Titles and Descriptions
489
-
490
- Page titles and descriptions are rendered using `phlexi_render`, which means they can be **strings**, **procs**, or **component instances**:
491
-
492
- ```ruby
493
- class PostDefinition < Plutonium::Resource::Definition
494
- # Static strings
495
- index_page_title "All Posts"
496
- index_page_description "Manage your blog posts"
497
-
498
- # Dynamic procs (have access to context)
499
- show_page_title -> { h1 { "#{current_record!.title} - Post Details" } }
500
- show_page_description -> { h2 { "Created by #{current_record!.author.name} on #{current_record!.created_at.strftime('%B %d, %Y')}" } }
501
-
502
- # Component instances for complex rendering
503
- new_page_title -> { PageTitleComponent.new(text: "Create New Post", icon: :plus) }
504
- edit_page_title -> { PageTitleComponent.new(text: "Edit: #{current_record!.title}", icon: :edit) }
505
-
506
- # Conditional titles based on state
507
- index_page_title -> {
508
- case params[:status]
509
- when 'published' then "Published Posts"
510
- when 'draft' then "Draft Posts"
511
- else "All Posts"
512
- end
513
- }
514
- end
515
- ```
516
-
517
- ::: tip phlexi_render Context
518
- Title and description procs are evaluated in the page rendering context, giving you access to:
519
- - `current_record!` - The current record (for show/edit pages)
520
- - `params` - Request parameters
521
- - `current_user` - The authenticated user
522
- - All helper methods available in the view context
523
- :::
524
-
525
- ### Custom Page Classes
526
-
527
- Override page classes for complete control over page rendering:
528
-
529
- ```ruby
530
- class PostDefinition < Plutonium::Resource::Definition
531
- class IndexPage < Plutonium::UI::Page::Resource::Index
532
- def view_template(&block)
533
- # Custom page header
534
- div(class: "mb-8") do
535
- h1(class: "text-3xl font-bold") { "Content Management" }
536
- p(class: "text-gray-600") { "Manage your blog posts and articles" }
537
-
538
- # Custom stats dashboard
539
- div(class: "grid grid-cols-1 md:grid-cols-4 gap-4 mt-6") do
540
- render_stat_card("Total Posts", Post.count)
541
- render_stat_card("Published", Post.published.count)
542
- render_stat_card("Drafts", Post.draft.count)
543
- render_stat_card("This Month", Post.where(created_at: 1.month.ago..Time.current).count)
544
- end
545
- end
546
-
547
- # Standard table rendering
548
- super(&block)
549
- end
550
-
551
- private
552
-
553
- def render_stat_card(title, value)
554
- div(class: "bg-white p-4 rounded-lg shadow") do
555
- div(class: "text-sm text-gray-500") { title }
556
- div(class: "text-2xl font-bold") { value }
557
- end
558
- end
559
- end
560
-
561
- class ShowPage < Plutonium::UI::Page::Resource::Show
562
- def view_template(&block)
563
- div(class: "max-w-4xl mx-auto") do
564
- # Custom breadcrumbs
565
- nav(class: "mb-6") do
566
- ol(class: "flex space-x-2 text-sm") do
567
- li { link_to("Posts", posts_path, class: "text-blue-600") }
568
- li { span(class: "text-gray-500") { "/" } }
569
- li { span(class: "text-gray-900") { current_record.title.truncate(50) } }
570
- end
571
- end
572
-
573
- # Two-column layout
574
- div(class: "grid grid-cols-1 lg:grid-cols-3 gap-8") do
575
- # Main content
576
- div(class: "lg:col-span-2") do
577
- super(&block)
578
- end
579
-
580
- # Sidebar with metadata
581
- div(class: "lg:col-span-1") do
582
- render_metadata_sidebar
583
- end
584
- end
585
- end
586
- end
587
-
588
- private
589
-
590
- def render_metadata_sidebar
591
- div(class: "bg-gray-50 p-6 rounded-lg") do
592
- h3(class: "text-lg font-medium mb-4") { "Post Metadata" }
593
-
594
- dl(class: "space-y-3") do
595
- render_metadata_item("Status", current_record.status.humanize)
596
- render_metadata_item("Created", time_ago_in_words(current_record.created_at))
597
- render_metadata_item("Updated", time_ago_in_words(current_record.updated_at))
598
- render_metadata_item("Views", current_record.view_count)
599
- end
600
- end
601
- end
602
-
603
- def render_metadata_item(label, value)
604
- div do
605
- dt(class: "text-sm text-gray-500") { label }
606
- dd(class: "text-sm font-medium") { value }
607
- end
608
- end
609
- end
610
-
611
- class NewPage < Plutonium::UI::Page::Resource::New
612
- def page_title
613
- "Create New #{current_record.class.model_name.human}"
614
- end
615
-
616
- def page_description
617
- "Fill out the form below to create a new post. All fields marked with * are required."
618
- end
619
- end
620
-
621
- class EditPage < Plutonium::UI::Page::Resource::Edit
622
- def page_title
623
- "Edit: #{current_record.title}"
624
- end
625
-
626
- def page_description
627
- "Last updated #{time_ago_in_words(current_record.updated_at)} ago"
628
- end
629
- end
630
- end
631
- ```
632
-
633
- ### Custom Form Classes
634
-
635
- Override form classes for complete control over form rendering:
636
-
637
- ```ruby
638
- class PostDefinition < Plutonium::Resource::Definition
639
- class Form < Plutonium::UI::Form::Resource
640
- def form_template
641
- # Custom form layout
642
- div(class: "grid grid-cols-1 lg:grid-cols-3 gap-8") do
643
- # Main content area
644
- div(class: "lg:col-span-2") do
645
- render_main_fields
646
- end
647
-
648
- # Sidebar
649
- div(class: "lg:col-span-1") do
650
- render_sidebar_fields
651
- end
652
- end
653
-
654
- render_actions
655
- end
656
-
657
- private
658
-
659
- def render_main_fields
660
- # Group related fields
661
- fieldset(class: "space-y-6") do
662
- legend(class: "text-lg font-medium") { "Content" }
663
-
664
- render field(:title).input_tag(placeholder: "Enter a compelling title")
665
- render field(:content).easymde_tag
666
- render field(:excerpt).input_tag(as: :textarea, rows: 3)
667
- end
668
- end
669
-
670
- def render_sidebar_fields
671
- # Publishing controls
672
- fieldset(class: "space-y-4") do
673
- legend(class: "text-lg font-medium") { "Publishing" }
674
-
675
- render field(:status).input_tag(as: :select)
676
- render field(:published_at).flatpickr_tag
677
- render field(:featured).input_tag(as: :boolean)
678
- end
679
-
680
- # Categorization
681
- fieldset(class: "space-y-4 mt-8") do
682
- legend(class: "text-lg font-medium") { "Categorization" }
683
-
684
- render field(:category).belongs_to_tag
685
- render field(:tags).has_many_tag
686
- end
687
- end
688
- end
689
- end
690
- ```
691
-
692
- ## Policy Integration
693
-
694
- **Field visibility is controlled by policies, not definitions:**
695
-
696
- ```ruby
697
- # app/policies/post_policy.rb
698
- class PostPolicy < Plutonium::Resource::Policy
699
- def permitted_attributes_for_show
700
- if user.admin?
701
- [:title, :content, :admin_notes] # Admin sees admin_notes
702
- else
703
- [:title, :content] # Regular users don't
704
- end
705
- end
706
-
707
- def permitted_attributes_for_create
708
- if user.admin?
709
- [:title, :content, :published, :featured, :admin_notes]
710
- else
711
- [:title, :content]
712
- end
713
- end
714
-
715
- def permitted_attributes_for_update
716
- attrs = permitted_attributes_for_create
717
-
718
- # Authors can edit their own posts
719
- if user == record.author
720
- attrs + [:draft_notes]
721
- else
722
- attrs
723
- end
724
- end
725
-
726
- def permitted_associations
727
- [:author, :tags, :comments]
728
- end
729
- end
730
- ```
731
-
732
- ## Integration Points
733
-
734
- ### Resource Integration
735
-
736
- Definitions are automatically discovered and used by resource controllers:
737
-
738
- ```ruby
739
- # app/definitions/post_definition.rb
740
- class PostDefinition < Plutonium::Resource::Definition
741
- # All model attributes are auto-detected!
742
- # No field declarations needed unless overriding
743
-
744
- # Only customize what you need:
745
- input :content, as: :rich_text # Override text -> rich_text
746
- display :content, as: :markdown # Override text -> markdown
747
-
748
- search { |scope, search| scope.where("title ILIKE ?", "%#{search}%") }
749
- end
750
-
751
- # app/controllers/posts_controller.rb
752
- class PostsController < ApplicationController
753
- include Plutonium::Resource::Controller
754
- # PostDefinition is automatically used
755
- end
756
- ```
757
-
758
- ### Interaction Integration
759
-
760
- Actions integrate with the Interaction system:
761
-
762
- ```ruby
763
- class PostDefinition < Plutonium::Resource::Definition
764
- action :publish, interaction: PublishPostInteraction
765
- end
766
-
767
- class PublishPostInteraction < Plutonium::Interaction::Base
768
- attribute :resource
769
- attribute :publish_date, :date
770
-
771
- def execute
772
- resource.update!(published: true, published_at: publish_date || Time.current)
773
- succeed(resource).with_redirect_response(resource_url_for(resource))
774
- end
775
- end
776
- ```
777
-
778
- ## Best Practices
779
-
780
- ### Field Type Philosophy
781
- - **Let auto-detection work**: Don't declare fields unless you need to override
782
- - **Override when needed**: Use declarations to change text to rich_text, datetime to date, etc.
783
- - **Use conditions sparingly**: Prefer policy-based visibility over conditional fields
784
-
785
- ### Separation of Concerns
786
- - **Definitions**: Configure HOW fields are rendered and processed
787
- - **Policies**: Control WHAT fields are visible and editable
788
- - **Interactions**: Handle business logic and operations
789
-
790
- ### Minimal Configuration Approach
791
- ```ruby
792
- # Preferred: Let auto-detection work, only override what you need
793
- class PostDefinition < Plutonium::Resource::Definition
794
- # All fields auto-detected from Post model
795
-
796
- # Only declare overrides:
797
- input :content, as: :rich_text
798
- display :content, as: :markdown
799
-
800
- search { |scope, search| scope.where("title ILIKE ?", "%#{search}%") }
801
- end
802
-
803
- # Avoid: Over-declaring fields that would be auto-detected anyway
804
- class PostDefinition < Plutonium::Resource::Definition
805
- field :title, as: :string # Unnecessary - auto-detected
806
- field :content, as: :text # Unnecessary - auto-detected
807
- field :author, as: :association # Unnecessary - auto-detected
808
-
809
- # This creates extra maintenance burden
810
- end
811
- ```
812
-
813
- 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).
814
-
815
- ## Structuring Your Definition File
816
-
817
- A well-structured definition file is easy to read, maintain, and understand. Here's the recommended organization:
818
-
819
- ### 1. Logical Grouping
820
-
821
- Group related declarations together for better readability:
822
-
823
- ```ruby
824
- class PostDefinition < Plutonium::Resource::Definition
825
- # === Field Type Overrides ===
826
- # Group all basic field type changes together
827
- field :content, as: :rich_text
828
- field :author_id, as: :hidden
829
- field :metadata, as: :json
830
-
831
- # === Input Customizations ===
832
- # Group form-specific configurations
833
- input :title,
834
- label: "Post Title",
835
- hint: "Enter a descriptive title",
836
- placeholder: "e.g. Getting Started with Plutonium"
837
-
838
- input :slug,
839
- label: "URL Slug",
840
- hint: "Leave blank to auto-generate from title",
841
- class: "font-mono"
842
-
843
- input :category, as: :select, choices: %w[Tech Business Lifestyle]
844
-
845
- # === Display Customizations ===
846
- # Group show page configurations
847
- display :content, as: :markdown
848
- display :view_count, label: "Views", class: "font-bold"
849
- display :metadata, wrapper: {class: "col-span-full"}
850
-
851
- # === Column Customizations ===
852
- # Group table-specific configurations
853
- column :title, label: "Article Title"
854
- column :view_count, align: :end, label: "Views"
855
- column :published_at, align: :end
856
-
857
- # === Search & Filtering ===
858
- # Group query-related configurations
859
- search do |scope, query|
860
- scope.where("title ILIKE ? OR content ILIKE ?", "%#{query}%", "%#{query}%")
861
- end
862
-
863
- filter :status, as: :select, choices: %w[draft published archived]
864
- filter :category, as: :select, choices: %w[Tech Business Lifestyle]
865
-
866
- # === Scopes ===
867
- scope :published
868
- scope :draft
869
- scope :archived
870
-
871
- # === Sorting ===
872
- sort :title
873
- sort :created_at
874
- sort :view_count
875
-
876
- # === Actions ===
877
- action :publish, interaction: Posts::Publish
878
- action :archive, interaction: Posts::Archive
879
- end
880
- ```
881
-
882
- ### 2. Use Comments to Delineate Sections
883
-
884
- Add section headers with comments to make the file scannable:
885
-
886
- ```ruby
887
- class PostDefinition < Plutonium::Resource::Definition
888
- # ========================================
889
- # Field Configuration
890
- # ========================================
891
-
892
- field :content, as: :rich_text
893
-
894
- # ========================================
895
- # Form Inputs
896
- # ========================================
897
-
898
- input :title, hint: "Enter a descriptive title"
899
-
900
- # ========================================
901
- # Display Fields
902
- # ========================================
903
-
904
- display :content, as: :markdown
905
-
906
- # ========================================
907
- # Table Columns
908
- # ========================================
909
-
910
- column :view_count, align: :end
911
-
912
- # ========================================
913
- # Search, Filters & Scopes
914
- # ========================================
915
-
916
- search { |scope, query| scope.where("title ILIKE ?", "%#{query}%") }
917
- filter :status, as: :select, choices: %w[draft published]
918
-
919
- # ========================================
920
- # Actions
921
- # ========================================
922
-
923
- action :publish, interaction: Posts::Publish
924
- end
925
- ```
926
-
927
- ### 3. Order Within Each Section
928
-
929
- Within each section, order declarations logically:
930
-
931
- **For fields/inputs/displays:**
932
- 1. Basic field type overrides first
933
- 2. Fields with simple options next
934
- 3. Fields with complex options or blocks last
935
-
936
- **For filters and scopes:**
937
- 1. Most commonly used first
938
- 2. Grouped by related functionality
939
-
940
- **For actions:**
941
- 1. Primary actions first (create, publish)
942
- 2. Secondary actions next (archive, duplicate)
943
- 3. Destructive actions last (delete)
944
-
945
- ### 4. Use `field` vs `input` vs `display` Appropriately
946
-
947
- Understanding when to use each declaration:
948
-
949
- ```ruby
950
- class PostDefinition < Plutonium::Resource::Definition
951
- # Use `field` when the configuration applies to ALL contexts
952
- # (form inputs, show page displays, and table columns)
953
- field :content, as: :rich_text # Changes type everywhere
954
-
955
- # Use `input` for form-specific options
956
- input :title, hint: "Shown only in forms", placeholder: "Form placeholder"
957
-
958
- # Use `display` for show-page-specific options
959
- display :content,
960
- description: "Shown only on show pages",
961
- wrapper: {class: "col-span-full"}
962
-
963
- # Use `column` for table-specific options
964
- column :title, label: "Custom header", align: :center
965
- end
966
- ```
967
-
968
- ### 5. Avoid Redundant Declarations
969
-
970
- Remember that **all model attributes are auto-detected**. Only declare overrides:
971
-
972
- ::: code-group
973
- ```ruby [✅ Good - Only Overrides]
974
- class PostDefinition < Plutonium::Resource::Definition
975
- # Only override what needs customization
976
- field :content, as: :rich_text
977
- input :title, hint: "Enter a descriptive title"
978
- display :metadata, wrapper: {class: "col-span-full"}
979
- end
980
- ```
981
-
982
- ```ruby [❌ Bad - Redundant]
983
- class PostDefinition < Plutonium::Resource::Definition
984
- # Unnecessary - these are auto-detected correctly
985
- field :title, as: :string
986
- field :content, as: :text
987
- field :published_at, as: :datetime
988
- field :author, as: :association
989
-
990
- # Only these override defaults
991
- field :content, as: :rich_text # This actually works, overrides the earlier :text
992
- input :title, hint: "Enter a descriptive title"
993
- end
994
- ```
995
- :::
996
-
997
- ### 6. Complex Customizations at the End
998
-
999
- Place complex blocks and custom components at the end of their sections:
1000
-
1001
- ```ruby
1002
- class PostDefinition < Plutonium::Resource::Definition
1003
- # Simple overrides first
1004
- input :title, hint: "Enter title"
1005
- input :category, as: :select, choices: %w[Tech Business]
1006
-
1007
- # Complex blocks last
1008
- input :related_posts do |f|
1009
- choices = if object.persisted?
1010
- Post.where.not(id: object.id).published.pluck(:title, :id)
1011
- else
1012
- []
1013
- end
1014
- f.select_tag choices: choices
1015
- end
1016
-
1017
- display :status do |f|
1018
- StatusBadgeComponent.new(
1019
- status: f.value,
1020
- class: "inline-flex items-center"
1021
- )
1022
- end
1023
- end
1024
- ```
1025
-
1026
- ### 7. Example: Well-Structured Definition
1027
-
1028
- Here's a complete example following all best practices:
1029
-
1030
- ```ruby
1031
- class PostDefinition < Plutonium::Resource::Definition
1032
- # ========================================
1033
- # Field Type Overrides
1034
- # ========================================
1035
-
1036
- field :content, as: :rich_text
1037
- field :author_id, as: :hidden
1038
-
1039
- # ========================================
1040
- # Form Inputs
1041
- # ========================================
1042
-
1043
- # Basic fields with simple options
1044
- input :title,
1045
- label: "Post Title",
1046
- hint: "Enter a descriptive title",
1047
- placeholder: "e.g. Getting Started"
1048
-
1049
- input :slug,
1050
- hint: "Leave blank to auto-generate",
1051
- class: "font-mono"
1052
-
1053
- # Select inputs
1054
- input :category, as: :select, choices: %w[Tech Business Lifestyle]
1055
- input :author, as: :association
1056
-
1057
- # Conditional inputs
1058
- input :published_at,
1059
- condition: -> { object.published? },
1060
- hint: "When this post was published"
1061
-
1062
- # ========================================
1063
- # Display Fields
1064
- # ========================================
1065
-
1066
- display :content, as: :markdown
1067
- display :view_count, label: "Total Views", class: "font-bold text-lg"
1068
- display :metadata, wrapper: {class: "col-span-full"}
1069
-
1070
- # ========================================
1071
- # Table Columns
1072
- # ========================================
1073
-
1074
- column :title, label: "Article Title"
1075
- column :category, align: :center
1076
- column :view_count, align: :end, label: "Views"
1077
- column :published_at, align: :end
1078
-
1079
- # ========================================
1080
- # Search & Filtering
1081
- # ========================================
1082
-
1083
- search do |scope, query|
1084
- scope.where("title ILIKE ? OR content ILIKE ?", "%#{query}%", "%#{query}%")
1085
- end
1086
-
1087
- filter :status, as: :select, choices: %w[draft published archived]
1088
- filter :category, as: :select, choices: %w[Tech Business Lifestyle]
1089
- filter :published_after, as: :date
1090
-
1091
- # ========================================
1092
- # Scopes
1093
- # ========================================
1094
-
1095
- scope :published
1096
- scope :draft
1097
- scope :scheduled
1098
-
1099
- # ========================================
1100
- # Sorting
1101
- # ========================================
1102
-
1103
- sort :title
1104
- sort :published_at
1105
- sort :view_count
1106
-
1107
- # ========================================
1108
- # Actions
1109
- # ========================================
1110
-
1111
- action :publish, interaction: Posts::Publish
1112
- action :schedule, interaction: Posts::Schedule
1113
- action :archive, interaction: Posts::Archive
1114
- end
1115
- ```
1116
-
1117
- ## Related Modules
1118
-
1119
- - **[Resource Record](./resource_record.md)** - Resource controllers and CRUD operations
1120
- - **[UI](./ui.md)** - User interface components
1121
- - **[Query](./query.md)** - Query objects and filtering
1122
- - **[Action](./action.md)** - Custom actions and operations
1123
- - **[Interaction](./interaction.md)** - Business logic encapsulation
1124
-
1125
- ## Available Field Types
1126
-
1127
- ### Input Types (Form Components)
1128
- - **Text**: `:string`, `:text`, `:email`, `:url`, `:tel`, `:password`
1129
- - **Rich Text**: `:rich_text`, `:markdown` (uses EasyMDE)
1130
- - **Numeric**: `:number`, `:integer`, `:decimal`, `:range`
1131
- - **Boolean**: `:boolean`
1132
- - **Date/Time**: `:date`, `:time`, `:datetime` (uses Flatpickr)
1133
- - **Selection**: `:select`, `:slim_select`, `:radio_buttons`, `:check_boxes`
1134
- - **Files**: `:file`, `:uppy`, `:attachment` (uses Uppy)
1135
- - **Associations**: `:association`, `:secure_association`, `:belongs_to`, `:has_many`, `:has_one`
1136
- - **Special**: `:hidden`, `:color`, `:phone` (uses IntlTelInput)
1137
-
1138
- ### Display Types (Show/Index Components)
1139
- - **Text**: `:string`, `:text`, `:email`, `:url`, `:phone`
1140
- - **Rich Content**: `:markdown` (renders with Redcarpet)
1141
- - **Numeric**: `:number`, `:integer`, `:decimal`
1142
- - **Boolean**: `:boolean`
1143
- - **Date/Time**: `:date`, `:time`, `:datetime`
1144
- - **Associations**: `:association` (auto-links to show page)
1145
- - **Files**: `:attachment` (shows previews/downloads)
1146
- - **Custom**: `:phlexi_render` (for custom components)
1147
-
1148
- ## Available Configuration Options
1149
-
1150
- ### Field Options
1151
- ```ruby
1152
- field :name, as: :string, class: "custom-class", wrapper: {class: "field-wrapper"}
1153
- ```
1154
-
1155
- ### Input Options
1156
- ```ruby
1157
- input :title,
1158
- as: :string,
1159
- # Field-level options (passed to the field wrapper):
1160
- label: "Article Title", # Custom field label
1161
- hint: "Enter a descriptive title", # Help text shown below input
1162
- placeholder: "e.g. My First Post", # Input placeholder text
1163
- # Tag-level options (passed to the input tag):
1164
- required: true,
1165
- class: "custom-input",
1166
- data: {controller: "custom"},
1167
- # Wrapper options:
1168
- wrapper: {class: "input-wrapper"},
1169
- # Conditional rendering:
1170
- condition: -> { current_user.admin? },
1171
- # Pre-submit for dynamic forms:
1172
- pre_submit: false
1173
- ```
1174
-
1175
- ::: tip Field-level vs Tag-level Options
1176
- - **Field-level options** (`label`, `hint`, `placeholder`) are processed by the field wrapper and affect the field's label and help text display
1177
- - **Tag-level options** (like `class`, `data`, `required`) are passed directly to the HTML input element
1178
- - The system automatically routes options to the correct destination
1179
- :::
1180
-
1181
- ### Display Options
1182
- ```ruby
1183
- display :content,
1184
- as: :markdown,
1185
- # Field-level options (passed to the field wrapper):
1186
- label: "Article Content", # Custom field label
1187
- description: "Published content", # Description text shown below value
1188
- placeholder: "No content yet", # Shown when value is empty
1189
- # Tag-level options (passed to the display tag):
1190
- class: "prose",
1191
- # Wrapper options:
1192
- wrapper: {class: "content-wrapper"},
1193
- # Conditional rendering:
1194
- condition: -> { current_user.can_see_content? }
1195
- ```
1196
-
1197
- ::: tip Display Uses Description, Forms Use Hint
1198
- - **Display fields** use `:description` for explanatory text below the value
1199
- - **Form fields** use `:hint` for help text below the input
1200
- - Both support `:label` and `:placeholder` options
1201
- :::
1202
-
1203
- ### Column Options
1204
- ```ruby
1205
- column :title,
1206
- as: :string,
1207
- # Column-level options:
1208
- label: "Article Title", # Custom column header
1209
- align: :start, # Alignment: :start, :center, :end
1210
- # Tag-level options (passed to the cell renderer):
1211
- class: "font-bold",
1212
- # Conditional rendering:
1213
- condition: -> { current_user.admin? }
1214
- ```
1215
-
1216
- ::: tip Table Columns
1217
- Table columns support a minimal set of field-level options since they render in a compact table format:
1218
- - `:label` - Column header text
1219
- - `:align` - Column alignment
1220
- - Options like `:description` and `:placeholder` are filtered out as they don't make sense in table cells
1221
- :::
1222
-
1223
- ### Choices Options (for selects)
1224
- ```ruby
1225
- # Static choices
1226
- input :category, as: :select, choices: %w[Tech Business Lifestyle]
1227
-
1228
- # Dynamic choices require block form
1229
- input :author do |f|
1230
- choices = User.active.pluck(:name, :id)
1231
- f.select_tag choices: choices
1232
- end
1233
-
1234
- # Dynamic choices with access to context
1235
- input :team_members do |f|
1236
- choices = current_user.organization.users.active.pluck(:name, :id)
1237
- f.select_tag choices: choices
1238
- end
1239
-
1240
- # Dynamic choices based on object state
1241
- input :related_posts do |f|
1242
- choices = if object.persisted?
1243
- Post.where.not(id: object.id).published.pluck(:title, :id)
1244
- else
1245
- []
1246
- end
1247
- f.select_tag choices: choices
1248
- end
1249
- ```
1250
-
1251
- ::: tip Dynamic Choices Context
1252
- When using block forms for dynamic choices, you have access to:
1253
- - `current_user` - The authenticated user
1254
- - `current_parent` - Parent record for nested resources
1255
- - `object` - The record being edited (in edit forms)
1256
- - `request` and `params` - Request information
1257
- - All helper methods available in the form context
1258
-
1259
- Block forms are required for dynamic choices since the `choices:` option only accepts static arrays.
1260
- :::
1261
-
1262
- ### File Upload Options
1263
- ```ruby
1264
- input :avatar, as: :file, multiple: false
1265
- input :documents, as: :file, multiple: true,
1266
- allowed_file_types: ['.pdf', '.doc', '.docx'],
1267
- max_file_size: 5.megabytes
1268
- ```
1269
-
1270
- ## Dynamic Configuration & Policies
1271
-
1272
- ::: danger IMPORTANT
1273
- 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` procs and input blocks ARE evaluated in the rendering context where `current_user` and the record (`object`) are available.
1274
- :::
1275
-
1276
- 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.
1277
-
1278
- ::: code-group
1279
- ```ruby [Static Definition]
1280
- # app/definitions/post_definition.rb
1281
- class PostDefinition < Plutonium::Resource::Definition
1282
- # This configuration is static.
1283
- # The :admin_notes field is always defined here.
1284
- def customize_fields
1285
- field :admin_notes, as: :text
1286
- end
1287
-
1288
- def customize_displays
1289
- display :admin_notes, as: :text
1290
- end
1291
-
1292
- def customize_inputs
1293
- input :admin_notes, as: :text
1294
- end
1295
- end
1296
- ```
1297
- ```ruby [Dynamic Policy]
1298
- # app/policies/post_policy.rb
1299
- class PostPolicy < Plutonium::Resource::Policy
1300
- # The policy determines if the user can SEE the field.
1301
- def permitted_attributes_for_show
1302
- if user.admin?
1303
- [:title, :content, :admin_notes] # Admin sees admin_notes
1304
- else
1305
- [:title, :content] # Regular users do not
1306
- end
1307
- end
1308
- end