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,759 @@
|
|
1
|
+
---
|
2
|
+
title: Display Module
|
3
|
+
---
|
4
|
+
|
5
|
+
# Display Module
|
6
|
+
|
7
|
+
The Display module provides a comprehensive system for rendering and displaying data values in Plutonium applications. Built on top of `Phlexi::Display`, it offers specialized components for different data types, consistent theming, and intelligent value rendering.
|
8
|
+
|
9
|
+
::: tip
|
10
|
+
The Display module is located in `lib/plutonium/ui/display/`.
|
11
|
+
:::
|
12
|
+
|
13
|
+
## Overview
|
14
|
+
|
15
|
+
- **Value Rendering**: Intelligent rendering of different data types.
|
16
|
+
- **Specialized Components**: Purpose-built components for associations, attachments, markdown, etc.
|
17
|
+
- **Theme System**: Consistent styling across all display components.
|
18
|
+
- **Type Inference**: Automatic component selection based on data types.
|
19
|
+
- **Resource Integration**: Seamless integration with resource definitions.
|
20
|
+
- **Responsive Design**: Mobile-first responsive display layouts.
|
21
|
+
|
22
|
+
## Core Components
|
23
|
+
|
24
|
+
### Base Display (`lib/plutonium/ui/display/base.rb`)
|
25
|
+
|
26
|
+
This is the foundation that all display components inherit from. It extends `Phlexi::Display::Base` with Plutonium's specific behaviors and custom display components.
|
27
|
+
|
28
|
+
::: details Base Display Implementation
|
29
|
+
```ruby
|
30
|
+
class Plutonium::UI::Display::Base < Phlexi::Display::Base
|
31
|
+
include Plutonium::UI::Component::Behaviour
|
32
|
+
|
33
|
+
# Enhanced builder with Plutonium-specific components
|
34
|
+
class Builder < Builder
|
35
|
+
include Plutonium::UI::Display::Options::InferredTypes
|
36
|
+
|
37
|
+
def association_tag(**options, &block)
|
38
|
+
create_component(Plutonium::UI::Display::Components::Association, :association, **options, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def markdown_tag(**options, &block)
|
42
|
+
create_component(Plutonium::UI::Display::Components::Markdown, :markdown, **options, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def attachment_tag(**options, &block)
|
46
|
+
create_component(Plutonium::UI::Display::Components::Attachment, :attachment, **options, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
def phlexi_render_tag(**options, &block)
|
50
|
+
create_component(Plutonium::UI::Display::Components::PhlexiRender, :phlexi_render, **options, &block)
|
51
|
+
end
|
52
|
+
alias_method :phlexi_tag, :phlexi_render_tag
|
53
|
+
end
|
54
|
+
end
|
55
|
+
```
|
56
|
+
:::
|
57
|
+
|
58
|
+
### Resource Display (`lib/plutonium/ui/display/resource.rb`)
|
59
|
+
|
60
|
+
This is a specialized component for displaying resource objects, automatically rendering fields and associations based on the resource's definition.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class PostDisplay < Plutonium::UI::Display::Resource
|
64
|
+
def initialize(post, resource_fields:, resource_associations:, resource_definition:)
|
65
|
+
super(
|
66
|
+
post,
|
67
|
+
resource_fields: resource_fields,
|
68
|
+
resource_associations: resource_associations,
|
69
|
+
resource_definition: resource_definition
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
def display_template
|
74
|
+
render_fields # Render configured fields
|
75
|
+
render_associations if present_associations? # Render associations
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
## Display Components
|
81
|
+
|
82
|
+
### Association Component
|
83
|
+
|
84
|
+
Renders associated objects with automatic linking to the resource's show page if it's a registered resource.
|
85
|
+
|
86
|
+
::: code-group
|
87
|
+
```ruby [Usage]
|
88
|
+
# Automatically used for association fields
|
89
|
+
render field(:author).association_tag
|
90
|
+
```
|
91
|
+
```ruby [Implementation]
|
92
|
+
class Plutonium::UI::Display::Components::Association
|
93
|
+
def render_value(value)
|
94
|
+
if registered_resources.include?(value.class)
|
95
|
+
# Create link to resource
|
96
|
+
href = resource_url_for(value, parent: appropriate_parent)
|
97
|
+
a(class: themed(:link), href: href) { display_name_of(value) }
|
98
|
+
else
|
99
|
+
# Plain text display
|
100
|
+
display_name_of(value)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
```
|
105
|
+
:::
|
106
|
+
|
107
|
+
### Attachment Component
|
108
|
+
|
109
|
+
Provides a rich display for file attachments with thumbnails for images and icons for other file types.
|
110
|
+
|
111
|
+
::: code-group
|
112
|
+
```ruby [Basic Usage]
|
113
|
+
# Automatically used for attachment fields
|
114
|
+
render field(:featured_image).attachment_tag
|
115
|
+
```
|
116
|
+
```ruby [With Options]
|
117
|
+
render field(:documents).attachment_tag(caption: false)
|
118
|
+
render field(:gallery).attachment_tag(
|
119
|
+
caption: ->(attachment) { attachment.description }
|
120
|
+
)
|
121
|
+
```
|
122
|
+
:::
|
123
|
+
|
124
|
+
::: details Attachment Component Implementation
|
125
|
+
```ruby
|
126
|
+
class Plutonium::UI::Display::Components::Attachment
|
127
|
+
def render_value(attachment)
|
128
|
+
div(
|
129
|
+
class: "attachment-preview",
|
130
|
+
data: {
|
131
|
+
controller: "attachment-preview",
|
132
|
+
attachment_preview_mime_type_value: attachment.content_type,
|
133
|
+
attachment_preview_thumbnail_url_value: attachment_thumbnail_url(attachment)
|
134
|
+
}
|
135
|
+
) do
|
136
|
+
render_thumbnail(attachment) # Image or file type icon
|
137
|
+
render_caption(attachment) # Filename or custom caption
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def render_thumbnail(attachment)
|
144
|
+
if attachment.representable?
|
145
|
+
img(src: attachment_thumbnail_url(attachment), class: "w-full h-full object-cover")
|
146
|
+
else
|
147
|
+
# File type icon
|
148
|
+
div(class: "file-icon") { ".#{attachment_extension(attachment)}" }
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
:::
|
154
|
+
|
155
|
+
### Markdown Component
|
156
|
+
|
157
|
+
Securely renders markdown content with syntax highlighting for code blocks.
|
158
|
+
|
159
|
+
::: code-group
|
160
|
+
```ruby [Usage]
|
161
|
+
# Automatically used for :markdown fields
|
162
|
+
render field(:description).markdown_tag
|
163
|
+
```
|
164
|
+
```ruby [Implementation]
|
165
|
+
class Plutonium::UI::Display::Components::Markdown
|
166
|
+
RENDERER = Redcarpet::Markdown.new(
|
167
|
+
Redcarpet::Render::HTML.new(
|
168
|
+
safe_links_only: true,
|
169
|
+
with_toc_data: true,
|
170
|
+
hard_wrap: true,
|
171
|
+
link_attributes: { rel: :nofollow, target: :_blank }
|
172
|
+
),
|
173
|
+
autolink: true,
|
174
|
+
tables: true,
|
175
|
+
fenced_code_blocks: true,
|
176
|
+
strikethrough: true,
|
177
|
+
footnotes: true
|
178
|
+
)
|
179
|
+
|
180
|
+
def render_value(value)
|
181
|
+
article(class: themed(:markdown)) do
|
182
|
+
raw(safe(render_markdown(value)))
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
```
|
187
|
+
:::
|
188
|
+
|
189
|
+
### PhlexiRender Component
|
190
|
+
|
191
|
+
Renders a given value using a custom Phlex component, allowing for complex, specialized displays.
|
192
|
+
|
193
|
+
::: code-group
|
194
|
+
```ruby [Block Syntax (Recommended)]
|
195
|
+
# Render with conditional logic
|
196
|
+
render field(:chart_data) do |f|
|
197
|
+
if f.value.present?
|
198
|
+
render ChartComponent.new(data: f.value, class: f.dom.css_class)
|
199
|
+
else
|
200
|
+
span(class: "text-gray-500") { "No chart data" }
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Simple component rendering
|
205
|
+
render field(:status_badge) do |f|
|
206
|
+
render StatusBadgeComponent.new(status: f.value, class: f.dom.css_class)
|
207
|
+
end
|
208
|
+
```
|
209
|
+
```ruby [Implementation]
|
210
|
+
class Plutonium::UI::Display::Components::PhlexiRender
|
211
|
+
def render_value(value)
|
212
|
+
phlexi_render(build_phlexi_component(value)) do
|
213
|
+
# Fallback rendering if component fails
|
214
|
+
p(class: themed(:string)) { value }
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
def build_phlexi_component(value)
|
221
|
+
@builder.call(value, attributes)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
```
|
225
|
+
:::
|
226
|
+
|
227
|
+
## Type Inference
|
228
|
+
|
229
|
+
The display system automatically selects the appropriate component based on the field's type, but you can always override it manually.
|
230
|
+
|
231
|
+
::: code-group
|
232
|
+
```ruby [Automatic Inference]
|
233
|
+
# Based on Active Record column types or Active Storage attachments
|
234
|
+
render field(:title).string_tag # -> :string
|
235
|
+
render field(:content).text_tag # -> :text
|
236
|
+
render field(:published_at).datetime_tag # -> :datetime
|
237
|
+
render field(:author).association_tag # -> :association
|
238
|
+
render field(:featured_image).attachment_tag # -> :attachment
|
239
|
+
render field(:description).markdown_tag # -> :markdown (if configured in definition)
|
240
|
+
```
|
241
|
+
```ruby [Manual Override]
|
242
|
+
render field(:title).string_tag
|
243
|
+
render field(:content).markdown_tag
|
244
|
+
render field(:author).association_tag
|
245
|
+
```
|
246
|
+
:::
|
247
|
+
|
248
|
+
::: details Type Mapping Implementation
|
249
|
+
```ruby
|
250
|
+
module Plutonium::UI::Display::Options::InferredTypes
|
251
|
+
private
|
252
|
+
|
253
|
+
def infer_field_component
|
254
|
+
case inferred_field_type
|
255
|
+
when :attachment
|
256
|
+
:attachment
|
257
|
+
when :association
|
258
|
+
:association
|
259
|
+
when :boolean
|
260
|
+
:boolean
|
261
|
+
# ... and so on for all standard types
|
262
|
+
else
|
263
|
+
:string
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
```
|
268
|
+
:::
|
269
|
+
|
270
|
+
## Theme System
|
271
|
+
|
272
|
+
### Display Theme (`lib/plutonium/ui/display/theme.rb`)
|
273
|
+
|
274
|
+
Comprehensive theming for consistent visual appearance:
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
class Plutonium::UI::Display::Theme < Phlexi::Display::Theme
|
278
|
+
def self.theme
|
279
|
+
super.merge({
|
280
|
+
# Layout
|
281
|
+
fields_wrapper: "p-6 grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-4 gap-6 gap-y-10 grid-flow-row-dense",
|
282
|
+
value_wrapper: "max-h-[300px] overflow-y-auto",
|
283
|
+
|
284
|
+
# Typography
|
285
|
+
label: "text-base font-bold text-gray-500 dark:text-gray-400 mb-1",
|
286
|
+
string: "text-md text-gray-900 dark:text-white mb-1 whitespace-pre-line",
|
287
|
+
text: "text-md text-gray-900 dark:text-white mb-1 whitespace-pre-line",
|
288
|
+
|
289
|
+
# Interactive elements
|
290
|
+
link: "text-primary-600 dark:text-primary-500 whitespace-pre-line",
|
291
|
+
email: "flex items-center text-md text-primary-600 dark:text-primary-500 mb-1",
|
292
|
+
phone: "flex items-center text-md text-primary-600 dark:text-primary-500 mb-1",
|
293
|
+
|
294
|
+
# Special content
|
295
|
+
markdown: "format dark:format-invert format-primary",
|
296
|
+
json: "text-sm text-gray-900 dark:text-white mb-1 whitespace-pre font-mono shadow-inner p-4",
|
297
|
+
|
298
|
+
# Attachments
|
299
|
+
attachment_value_wrapper: "grid grid-cols-[repeat(auto-fill,minmax(0,180px))]",
|
300
|
+
|
301
|
+
# Colors
|
302
|
+
color: "flex items-center text-md text-gray-900 dark:text-white mb-1",
|
303
|
+
color_indicator: "w-10 h-10 rounded-full mr-2"
|
304
|
+
})
|
305
|
+
end
|
306
|
+
end
|
307
|
+
```
|
308
|
+
|
309
|
+
### Table Display Theme (`lib/plutonium/ui/table/display_theme.rb`)
|
310
|
+
|
311
|
+
Specialized theming for table contexts:
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
class Plutonium::UI::Table::DisplayTheme < Phlexi::Table::DisplayTheme
|
315
|
+
def self.theme
|
316
|
+
super.merge({
|
317
|
+
# Compact display for tables
|
318
|
+
value_wrapper: "max-h-[150px] overflow-y-auto",
|
319
|
+
prefixed_icon: "w-4 h-4 mr-1",
|
320
|
+
|
321
|
+
# Table-specific styles
|
322
|
+
email: "flex items-center text-primary-600 dark:text-primary-500 whitespace-nowrap",
|
323
|
+
phone: "flex items-center text-primary-600 dark:text-primary-500 whitespace-nowrap",
|
324
|
+
attachment_value_wrapper: "flex flex-wrap gap-1"
|
325
|
+
})
|
326
|
+
end
|
327
|
+
end
|
328
|
+
```
|
329
|
+
|
330
|
+
## Usage Patterns
|
331
|
+
|
332
|
+
### Basic Display
|
333
|
+
|
334
|
+
```ruby
|
335
|
+
# Simple field display
|
336
|
+
class PostDisplay < Plutonium::UI::Display::Base
|
337
|
+
def display_template
|
338
|
+
render field(:title).string_tag
|
339
|
+
render field(:content).text_tag
|
340
|
+
render field(:published_at).datetime_tag
|
341
|
+
render field(:author).association_tag
|
342
|
+
end
|
343
|
+
end
|
344
|
+
```
|
345
|
+
|
346
|
+
### Field Rendering and Wrappers
|
347
|
+
|
348
|
+
Fields must be explicitly rendered using the `render` method. You can also use wrappers to control the layout and styling:
|
349
|
+
|
350
|
+
```ruby
|
351
|
+
class PostDisplay < Plutonium::UI::Display::Base
|
352
|
+
def display_template
|
353
|
+
# Basic field rendering
|
354
|
+
render field(:title).string_tag
|
355
|
+
|
356
|
+
# Field with wrapper options
|
357
|
+
render field(:content).wrapped(class: "col-span-full prose") do |f|
|
358
|
+
render f.markdown_tag
|
359
|
+
end
|
360
|
+
|
361
|
+
# Field with custom wrapper and styling
|
362
|
+
render field(:author).wrapped(
|
363
|
+
class: "border rounded-lg p-4",
|
364
|
+
data: { controller: "tooltip" }
|
365
|
+
) do |f|
|
366
|
+
render f.association_tag
|
367
|
+
end
|
368
|
+
|
369
|
+
# Multiple fields with consistent wrapper
|
370
|
+
[:created_at, :updated_at].each do |field_name|
|
371
|
+
render field(field_name).wrapped(class: "text-sm text-gray-500") do |f|
|
372
|
+
render f.datetime_tag
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
```
|
378
|
+
|
379
|
+
### Resource Display
|
380
|
+
|
381
|
+
```ruby
|
382
|
+
# Automatic resource display based on definition
|
383
|
+
class PostsController < ApplicationController
|
384
|
+
def show
|
385
|
+
@post = Post.find(params[:id])
|
386
|
+
@display = Plutonium::UI::Display::Resource.new(
|
387
|
+
@post,
|
388
|
+
resource_fields: current_definition.defined_displays.keys,
|
389
|
+
resource_associations: [],
|
390
|
+
resource_definition: current_definition
|
391
|
+
)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# In view
|
396
|
+
<%= render @display %>
|
397
|
+
```
|
398
|
+
|
399
|
+
### Custom Display Components
|
400
|
+
|
401
|
+
```ruby
|
402
|
+
# Create custom display component
|
403
|
+
class StatusBadgeComponent < Plutonium::UI::Component::Base
|
404
|
+
def initialize(status, **options)
|
405
|
+
@status = status
|
406
|
+
@options = options
|
407
|
+
end
|
408
|
+
|
409
|
+
def view_template
|
410
|
+
span(class: badge_classes) { @status.humanize }
|
411
|
+
end
|
412
|
+
|
413
|
+
private
|
414
|
+
|
415
|
+
def badge_classes
|
416
|
+
base_classes = "px-2 py-1 text-xs font-medium rounded-full"
|
417
|
+
case @status
|
418
|
+
when 'active'
|
419
|
+
"#{base_classes} bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"
|
420
|
+
when 'inactive'
|
421
|
+
"#{base_classes} bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
|
422
|
+
else
|
423
|
+
"#{base_classes} bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300"
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
# Use in display
|
429
|
+
render field(:status) do |f|
|
430
|
+
render StatusBadgeComponent.new(status: f.value)
|
431
|
+
end
|
432
|
+
```
|
433
|
+
|
434
|
+
### Conditional Display
|
435
|
+
|
436
|
+
You can conditionally show or hide display fields using the `:condition` option in your resource definition. This is useful for creating dynamic views that adapt to the state of your data.
|
437
|
+
|
438
|
+
**Note:** Conditional display is for cosmetic or state-based logic. For controlling data visibility based on user roles or permissions, use **policies**.
|
439
|
+
|
440
|
+
```ruby
|
441
|
+
# app/definitions/post_definition.rb
|
442
|
+
class PostDefinition < Plutonium::Resource::Definition
|
443
|
+
# Show a field only when the object is in a certain state.
|
444
|
+
display :published_at, condition: -> { object.published? }
|
445
|
+
display :reason_for_rejection, condition: -> { object.rejected? }
|
446
|
+
display :scheduled_for, condition: -> { object.scheduled? }
|
447
|
+
|
448
|
+
# Show a field based on the object's attributes.
|
449
|
+
display :comments, condition: -> { object.comments_enabled? }
|
450
|
+
|
451
|
+
# Show debug information only in development.
|
452
|
+
display :debug_info, condition: -> { Rails.env.development? }
|
453
|
+
end
|
454
|
+
```
|
455
|
+
|
456
|
+
::: tip Condition Context
|
457
|
+
`condition` procs for `display` fields are evaluated in the display rendering context, which means they have access to:
|
458
|
+
- `object` - The record being displayed
|
459
|
+
- All helper methods available in the display context
|
460
|
+
|
461
|
+
This allows for dynamic field visibility based on the record's state or other contextual information.
|
462
|
+
:::
|
463
|
+
|
464
|
+
You can also implement custom conditional logic by overriding the rendering methods:
|
465
|
+
|
466
|
+
```ruby
|
467
|
+
class PostDisplay < Plutonium::UI::Display::Resource
|
468
|
+
private
|
469
|
+
|
470
|
+
def render_resource_field(name)
|
471
|
+
# Only render if user has permission
|
472
|
+
when_permitted(name) do
|
473
|
+
# Get field and display options from definition
|
474
|
+
field_options = resource_definition.defined_fields[name]&.dig(:options) || {}
|
475
|
+
display_definition = resource_definition.defined_displays[name] || {}
|
476
|
+
display_options = display_definition[:options] || {}
|
477
|
+
|
478
|
+
# Render field with appropriate component
|
479
|
+
field(name, **field_options).wrapped(**wrapper_options) do |f|
|
480
|
+
render_field_component(f, display_options)
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def when_permitted(name, &block)
|
486
|
+
return unless @resource_fields.include?(name)
|
487
|
+
return unless policy_allows_field?(name)
|
488
|
+
|
489
|
+
yield
|
490
|
+
end
|
491
|
+
end
|
492
|
+
```
|
493
|
+
|
494
|
+
### Responsive Layouts
|
495
|
+
|
496
|
+
```ruby
|
497
|
+
# Grid layout with responsive columns
|
498
|
+
class PostDisplay < Plutonium::UI::Display::Base
|
499
|
+
private
|
500
|
+
|
501
|
+
def fields_wrapper(&block)
|
502
|
+
div(class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6") do
|
503
|
+
yield
|
504
|
+
end
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
# Full-width fields
|
509
|
+
render field(:description).wrapped(class: "col-span-full") do |f|
|
510
|
+
render f.markdown_tag
|
511
|
+
end
|
512
|
+
|
513
|
+
# Compact display
|
514
|
+
render field(:tags).wrapped(class: "col-span-1") do |f|
|
515
|
+
render f.collection_tag
|
516
|
+
end
|
517
|
+
```
|
518
|
+
|
519
|
+
## Helper Integration
|
520
|
+
|
521
|
+
### Display Helpers (`lib/plutonium/helpers/display_helper.rb`)
|
522
|
+
|
523
|
+
Rich helper methods for value formatting:
|
524
|
+
|
525
|
+
```ruby
|
526
|
+
module Plutonium::Helpers::DisplayHelper
|
527
|
+
# Generic field display with helper support
|
528
|
+
def display_field(value:, helper: nil, **options)
|
529
|
+
return "-" unless value.present?
|
530
|
+
|
531
|
+
if value.respond_to?(:each) && stack_multiple
|
532
|
+
# Handle collections
|
533
|
+
tag.ul(class: "list-unstyled") do
|
534
|
+
value.each do |val|
|
535
|
+
concat tag.li(display_field_value(value: val, helper: helper))
|
536
|
+
end
|
537
|
+
end
|
538
|
+
else
|
539
|
+
display_field_value(value: value, helper: helper, **options)
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
# Specialized display methods
|
544
|
+
def display_association_value(association)
|
545
|
+
display_name = display_name_of(association)
|
546
|
+
if registered_resources.include?(association.class)
|
547
|
+
link_to display_name, resource_url_for(association),
|
548
|
+
class: "font-medium text-primary-600 dark:text-primary-500"
|
549
|
+
else
|
550
|
+
display_name
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
def display_datetime_value(value)
|
555
|
+
timeago(value)
|
556
|
+
end
|
557
|
+
|
558
|
+
def display_boolean_value(value)
|
559
|
+
tag.input(type: :checkbox, checked: value, disabled: true)
|
560
|
+
end
|
561
|
+
|
562
|
+
def display_name_of(obj, separator: ", ")
|
563
|
+
return unless obj.present?
|
564
|
+
return obj.map { |i| display_name_of(i) }.join(separator) if obj.is_a?(Array)
|
565
|
+
|
566
|
+
# Try common display methods
|
567
|
+
%i[to_label name title].each do |method|
|
568
|
+
name = obj.public_send(method) if obj.respond_to?(method)
|
569
|
+
return name if name.present?
|
570
|
+
end
|
571
|
+
|
572
|
+
# Fallback for Active Record objects
|
573
|
+
return "#{resource_name(obj.class)} ##{obj.id}" if obj.respond_to?(:id)
|
574
|
+
|
575
|
+
obj.to_s
|
576
|
+
end
|
577
|
+
end
|
578
|
+
```
|
579
|
+
|
580
|
+
## Advanced Features
|
581
|
+
|
582
|
+
### Attachment Previews
|
583
|
+
|
584
|
+
```ruby
|
585
|
+
# Automatic attachment preview with JavaScript enhancement
|
586
|
+
field(:documents).attachment_tag
|
587
|
+
|
588
|
+
# Generates:
|
589
|
+
# - Thumbnail images for representable files
|
590
|
+
# - File type indicators for non-representable files
|
591
|
+
# - Click-to-preview functionality
|
592
|
+
# - Download links
|
593
|
+
# - Responsive grid layout
|
594
|
+
|
595
|
+
# JavaScript controller provides:
|
596
|
+
# - Preview modal/lightbox
|
597
|
+
# - Keyboard navigation
|
598
|
+
# - Touch/swipe support
|
599
|
+
# - Loading states
|
600
|
+
```
|
601
|
+
|
602
|
+
### Markdown Processing
|
603
|
+
|
604
|
+
```ruby
|
605
|
+
# Secure markdown with custom renderer
|
606
|
+
class CustomMarkdownRenderer < Redcarpet::Render::HTML
|
607
|
+
def initialize(options = {})
|
608
|
+
super(options.merge(
|
609
|
+
safe_links_only: true,
|
610
|
+
with_toc_data: true,
|
611
|
+
hard_wrap: true,
|
612
|
+
link_attributes: { rel: :nofollow, target: :_blank }
|
613
|
+
))
|
614
|
+
end
|
615
|
+
|
616
|
+
def block_code(code, language)
|
617
|
+
# Custom syntax highlighting
|
618
|
+
"<pre><code class=\"language-#{language}\">#{highlight_code(code, language)}</code></pre>"
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
# Use custom renderer
|
623
|
+
CUSTOM_RENDERER = Redcarpet::Markdown.new(
|
624
|
+
CustomMarkdownRenderer.new,
|
625
|
+
autolink: true,
|
626
|
+
tables: true,
|
627
|
+
fenced_code_blocks: true
|
628
|
+
)
|
629
|
+
```
|
630
|
+
|
631
|
+
### Performance Optimizations
|
632
|
+
|
633
|
+
```ruby
|
634
|
+
# Lazy loading for expensive displays
|
635
|
+
class PostDisplay < Plutonium::UI::Display::Base
|
636
|
+
def display_template
|
637
|
+
render field(:title).string_tag
|
638
|
+
|
639
|
+
# Only render associations if not in turbo frame
|
640
|
+
if current_turbo_frame.nil?
|
641
|
+
render field(:comments_count).number_tag
|
642
|
+
render field(:recent_comments).collection_tag
|
643
|
+
end
|
644
|
+
end
|
645
|
+
end
|
646
|
+
|
647
|
+
# Conditional rendering based on permissions
|
648
|
+
def render_resource_field(name)
|
649
|
+
return unless authorized_to_view_field?(name)
|
650
|
+
|
651
|
+
# Cache expensive field computations
|
652
|
+
@field_cache ||= {}
|
653
|
+
@field_cache[name] ||= compute_field_display(name)
|
654
|
+
|
655
|
+
render @field_cache[name]
|
656
|
+
end
|
657
|
+
```
|
658
|
+
|
659
|
+
## Testing
|
660
|
+
|
661
|
+
### Component Testing
|
662
|
+
|
663
|
+
```ruby
|
664
|
+
RSpec.describe Plutonium::UI::Display::Components::Association do
|
665
|
+
let(:user) { create(:user, name: "John Doe") }
|
666
|
+
let(:component) { described_class.new(field_for(user, :author)) }
|
667
|
+
|
668
|
+
context "when association is a registered resource" do
|
669
|
+
before { allow(component).to receive(:registered_resources).and_return([User]) }
|
670
|
+
|
671
|
+
it "renders a link to the resource" do
|
672
|
+
html = render(component)
|
673
|
+
expect(html).to include('href="/users/')
|
674
|
+
expect(html).to include("John Doe")
|
675
|
+
end
|
676
|
+
end
|
677
|
+
|
678
|
+
context "when association is not registered" do
|
679
|
+
before { allow(component).to receive(:registered_resources).and_return([]) }
|
680
|
+
|
681
|
+
it "renders plain text" do
|
682
|
+
html = render(component)
|
683
|
+
expect(html).not_to include('href=')
|
684
|
+
expect(html).to include("John Doe")
|
685
|
+
end
|
686
|
+
end
|
687
|
+
end
|
688
|
+
```
|
689
|
+
|
690
|
+
### Integration Testing
|
691
|
+
|
692
|
+
```ruby
|
693
|
+
RSpec.describe "Display Integration", type: :system do
|
694
|
+
let(:post) { create(:post, :with_attachments, :with_author) }
|
695
|
+
|
696
|
+
it "displays all field types correctly" do
|
697
|
+
visit post_path(post)
|
698
|
+
|
699
|
+
# Text fields
|
700
|
+
expect(page).to have_content(post.title)
|
701
|
+
expect(page).to have_content(post.content)
|
702
|
+
|
703
|
+
# Associations
|
704
|
+
expect(page).to have_link(post.author.name, href: user_path(post.author))
|
705
|
+
|
706
|
+
# Attachments
|
707
|
+
expect(page).to have_css(".attachment-preview")
|
708
|
+
expect(page).to have_link(href: rails_blob_path(post.featured_image))
|
709
|
+
|
710
|
+
# Timestamps
|
711
|
+
expect(page).to have_content("ago") # timeago formatting
|
712
|
+
end
|
713
|
+
|
714
|
+
it "handles responsive layout" do
|
715
|
+
visit post_path(post)
|
716
|
+
|
717
|
+
# Desktop layout
|
718
|
+
expect(page).to have_css(".md\\:grid-cols-2")
|
719
|
+
|
720
|
+
# Mobile layout (resize viewport)
|
721
|
+
page.driver.browser.manage.window.resize_to(375, 667)
|
722
|
+
expect(page).to have_css(".grid-cols-1")
|
723
|
+
end
|
724
|
+
end
|
725
|
+
```
|
726
|
+
|
727
|
+
## Best Practices
|
728
|
+
|
729
|
+
### Component Design
|
730
|
+
|
731
|
+
1. **Single Responsibility**: Each component should handle one display type
|
732
|
+
2. **Consistent API**: Follow the same patterns for all display components
|
733
|
+
3. **Theme Integration**: Use themed classes for consistent styling
|
734
|
+
4. **Accessibility**: Include proper ARIA attributes and semantic HTML
|
735
|
+
5. **Performance**: Avoid expensive operations in render methods
|
736
|
+
|
737
|
+
### Value Processing
|
738
|
+
|
739
|
+
1. **Null Safety**: Always handle nil/empty values gracefully
|
740
|
+
2. **Type Checking**: Verify value types before processing
|
741
|
+
3. **Sanitization**: Sanitize user-generated content (especially HTML/markdown)
|
742
|
+
4. **Formatting**: Use consistent formatting for dates, numbers, etc.
|
743
|
+
5. **Localization**: Support internationalization for display text
|
744
|
+
|
745
|
+
### Responsive Design
|
746
|
+
|
747
|
+
1. **Mobile First**: Design for mobile, enhance for desktop
|
748
|
+
2. **Flexible Layouts**: Use CSS Grid/Flexbox for adaptive layouts
|
749
|
+
3. **Content Priority**: Show most important content first on small screens
|
750
|
+
4. **Touch Friendly**: Ensure interactive elements are touch-accessible
|
751
|
+
5. **Performance**: Optimize images and assets for different screen sizes
|
752
|
+
|
753
|
+
### Security
|
754
|
+
|
755
|
+
1. **Input Sanitization**: Always sanitize user-generated content
|
756
|
+
2. **XSS Prevention**: Use safe HTML rendering methods
|
757
|
+
3. **Link Security**: Add `rel="nofollow"` to user-generated links
|
758
|
+
4. **File Security**: Validate file types and sizes for attachments
|
759
|
+
5. **Permission Checks**: Verify user permissions before displaying sensitive data
|