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,667 @@
|
|
1
|
+
---
|
2
|
+
description: Expert Plutonium framework guidelines for AI-assisted Rails development with auto-detection, resource architecture, component-based UI, and RAD patterns
|
3
|
+
globs:
|
4
|
+
alwaysApply: true
|
5
|
+
---
|
6
|
+
You are an expert Ruby on Rails developer specializing in the Plutonium framework. This cursor rule compiles comprehensive guidelines for building robust, maintainable Plutonium applications.
|
7
|
+
|
8
|
+
# Plutonium Framework Development Guidelines
|
9
|
+
|
10
|
+
## Framework Overview
|
11
|
+
|
12
|
+
Plutonium is a Rapid Application Development (RAD) toolkit that extends Rails with powerful conventions, patterns, and tools. It provides:
|
13
|
+
- Resource-oriented architecture with declarative definitions
|
14
|
+
- Modular packaging system (Feature Packages and Portal Packages)
|
15
|
+
- Built-in authentication, authorization, and multi-tenancy
|
16
|
+
- Component-based UI system built on Phlex
|
17
|
+
- Business logic encapsulation through Interactions
|
18
|
+
- Query objects for filtering and searching
|
19
|
+
|
20
|
+
## Project Structure & Setup
|
21
|
+
|
22
|
+
### Initial Setup
|
23
|
+
- Use Ruby 3.2.2+ and Rails 7.1+
|
24
|
+
- Create new apps with: `rails new app_name -a propshaft -j esbuild -c tailwind -m https://radioactive-labs.github.io/plutonium-core/templates/plutonium.rb`
|
25
|
+
- For existing apps: `bin/rails app:template LOCATION=https://radioactive-labs.github.io/plutonium-core/templates/base.rb`
|
26
|
+
- Run `rails generate pu:core:install` after adding the gem
|
27
|
+
|
28
|
+
### Directory Structure
|
29
|
+
```
|
30
|
+
app/
|
31
|
+
├── controllers/
|
32
|
+
│ ├── plutonium_controller.rb # Base controller
|
33
|
+
│ └── resource_controller.rb # Base for resources
|
34
|
+
├── definitions/ # Resource definitions
|
35
|
+
├── interactions/ # Business logic
|
36
|
+
├── models/
|
37
|
+
└── policies/ # Authorization policies
|
38
|
+
config/
|
39
|
+
├── initializers/plutonium.rb # Main configuration
|
40
|
+
└── packages.rb # Package registration
|
41
|
+
packages/ # Modular features
|
42
|
+
```
|
43
|
+
|
44
|
+
## Resource Architecture
|
45
|
+
|
46
|
+
### Resource Components
|
47
|
+
Every resource consists of 4 core components:
|
48
|
+
1. **Model** - ActiveRecord model with `include Plutonium::Resource::Record`
|
49
|
+
2. **Definition** - Declarative UI and behavior configuration
|
50
|
+
3. **Policy** - Authorization and access control
|
51
|
+
4. **Controller** - Auto-generated CRUD operations
|
52
|
+
|
53
|
+
### Creating Resources
|
54
|
+
```bash
|
55
|
+
# Scaffold a complete resource
|
56
|
+
rails generate pu:res:scaffold Post user:belongs_to title:string content:text 'published_at:datetime?'
|
57
|
+
|
58
|
+
# Individual components
|
59
|
+
rails generate pu:res:model Post title:string content:text
|
60
|
+
rails generate pu:res:definition Post
|
61
|
+
rails generate pu:res:policy Post
|
62
|
+
```
|
63
|
+
|
64
|
+
### Resource Models
|
65
|
+
```ruby
|
66
|
+
class Post < ApplicationRecord
|
67
|
+
include Plutonium::Resource::Record
|
68
|
+
|
69
|
+
belongs_to :user
|
70
|
+
has_many :comments, dependent: :destroy
|
71
|
+
|
72
|
+
validates :title, :content, presence: true
|
73
|
+
|
74
|
+
scope :published, -> { where.not(published_at: nil) }
|
75
|
+
scope :drafts, -> { where(published_at: nil) }
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
### Resource Definitions
|
80
|
+
```ruby
|
81
|
+
class PostDefinition < Plutonium::Resource::Definition
|
82
|
+
# Field declarations are OPTIONAL - all attributes are auto-detected from the model
|
83
|
+
# You only need to declare fields when overriding auto-detected behavior
|
84
|
+
|
85
|
+
# These would be auto-detected from your Post model:
|
86
|
+
# - :title (string column) → :string field
|
87
|
+
# - :content (text column) → :text field
|
88
|
+
# - :published_at (datetime column) → :datetime field
|
89
|
+
# - :author (belongs_to association) → :association field
|
90
|
+
|
91
|
+
# Only declare fields when you want to override defaults:
|
92
|
+
field :content, as: :rich_text # Override text → rich_text
|
93
|
+
field :status, as: :select, collection: %w[draft published archived]
|
94
|
+
|
95
|
+
# Display customization (show/index pages)
|
96
|
+
display :title, as: :string
|
97
|
+
display :content, as: :markdown
|
98
|
+
display :status, as: :string
|
99
|
+
display :author, as: :association
|
100
|
+
|
101
|
+
# Conditional displays (for table columns and show pages)
|
102
|
+
# Use for COSMETIC/STATE-BASED logic only, NOT authorization (use policies for that)
|
103
|
+
display :debug_info, condition: -> { Rails.env.development? }
|
104
|
+
display :internal_notes, condition: -> { object.status == 'draft' }
|
105
|
+
display :parent_category, condition: -> { current_parent.present? }
|
106
|
+
|
107
|
+
# Custom display with block (for complex rendering)
|
108
|
+
display :custom_field do |field|
|
109
|
+
# Return component instances directly in definition blocks (no 'render' needed)
|
110
|
+
CustomComponent.new(value: field.value)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Advanced custom display with phlexi_tag (use sparingly)
|
114
|
+
display :status, as: :phlexi_tag, with: ->(value, attrs) {
|
115
|
+
# SGML methods available here because proc is evaluated in rendering context
|
116
|
+
span(class: "badge badge-#{value}") { value.humanize }
|
117
|
+
}
|
118
|
+
|
119
|
+
# Input configuration (new/edit forms)
|
120
|
+
input :title, placeholder: "Enter post title" # No need for as: :string (auto-detected)
|
121
|
+
input :content, as: :rich_text
|
122
|
+
input :category, as: :select, collection: %w[Tech Business Lifestyle]
|
123
|
+
|
124
|
+
# Conditional inputs (for forms only)
|
125
|
+
# Use for COSMETIC/STATE-BASED logic only, NOT authorization (use policies for that)
|
126
|
+
input :debug_info, condition: -> { Rails.env.development? }
|
127
|
+
input :internal_notes, condition: -> { object.status == 'draft' }
|
128
|
+
input :parent_category, condition: -> { current_parent.present? }
|
129
|
+
input :team_projects, condition: -> { current_user.team_lead? }
|
130
|
+
|
131
|
+
# Search functionality
|
132
|
+
search do |scope, query|
|
133
|
+
scope.where("title ILIKE ? OR content ILIKE ?", "%#{query}%", "%#{query}%")
|
134
|
+
end
|
135
|
+
|
136
|
+
# Filters
|
137
|
+
filter :published, with: Plutonium::Query::Filters::Text, predicate: :eq
|
138
|
+
filter :category, with: Plutonium::Query::Filters::Text, predicate: :contains
|
139
|
+
filter :title, with: Plutonium::Query::Filters::Text, predicate: :contains
|
140
|
+
|
141
|
+
# Scopes (appear as buttons)
|
142
|
+
scope :published
|
143
|
+
scope :drafts
|
144
|
+
scope :recent, -> { where('created_at > ?', 1.week.ago) }
|
145
|
+
|
146
|
+
# Custom actions
|
147
|
+
action :publish, interaction: PublishPostInteraction, icon: Phlex::TablerIcons::Send
|
148
|
+
action :archive, interaction: ArchivePostInteraction, color: :warning
|
149
|
+
|
150
|
+
# Page customization
|
151
|
+
index_page_title "All Posts"
|
152
|
+
show_page_title "Post Details"
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
### Available Field Types (Auto-Detected from Model)
|
157
|
+
- Text: `:string`, `:text`, `:rich_text`, `:markdown`
|
158
|
+
- Numeric: `:number`, `:integer`, `:decimal`
|
159
|
+
- Boolean: `:boolean`
|
160
|
+
- Date/Time: `:date`, `:datetime`, `:time`
|
161
|
+
- Selection: `:select`, `:radio_buttons`, `:check_boxes`
|
162
|
+
- Files: `:file` (with `multiple: true` for multiple files)
|
163
|
+
- Associations: `:association`
|
164
|
+
- Special: `:hidden`, `:email`, `:url`, `:color`
|
165
|
+
|
166
|
+
### Conditional Rendering Guidelines
|
167
|
+
|
168
|
+
**IMPORTANT**: Use conditions for COSMETIC/STATE-BASED logic only, NOT for authorization.
|
169
|
+
|
170
|
+
#### ✅ Appropriate Uses
|
171
|
+
- **Environment-based**: Show debug info in development only
|
172
|
+
- **Object state-based**: Show fields based on record status
|
173
|
+
- **Context-based**: Show fields based on parent presence
|
174
|
+
- **Dynamic behavior**: Show dependent fields conditionally
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
# Environment-based visibility
|
178
|
+
display :debug_info, condition: -> { Rails.env.development? }
|
179
|
+
|
180
|
+
# Object state-based visibility
|
181
|
+
display :approval_date, condition: -> { object.approved? }
|
182
|
+
input :rejection_reason, condition: -> { object.rejected? }
|
183
|
+
|
184
|
+
# Parent context-based visibility
|
185
|
+
display :parent_category, condition: -> { current_parent.present? }
|
186
|
+
|
187
|
+
# Dynamic form behavior
|
188
|
+
input :end_date, condition: -> { object.start_date.present? }
|
189
|
+
```
|
190
|
+
|
191
|
+
#### ❌ Inappropriate Uses
|
192
|
+
Authorization logic belongs in policies, not conditions:
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
# DON'T use conditions for role-based authorization
|
196
|
+
display :admin_notes, condition: -> { current_user&.admin? } # Use policy instead
|
197
|
+
input :sensitive_data, condition: -> { current_user&.can_edit_sensitive? } # Use policy instead
|
198
|
+
```
|
199
|
+
|
200
|
+
### Available Configuration Options
|
201
|
+
|
202
|
+
#### Field Options
|
203
|
+
```ruby
|
204
|
+
field :name, as: :string, class: "custom-class", wrapper: {class: "field-wrapper"}
|
205
|
+
```
|
206
|
+
|
207
|
+
#### Input Options
|
208
|
+
```ruby
|
209
|
+
input :title,
|
210
|
+
as: :string,
|
211
|
+
placeholder: "Enter title",
|
212
|
+
required: true,
|
213
|
+
class: "custom-input",
|
214
|
+
wrapper: {class: "input-wrapper"},
|
215
|
+
data: {controller: "custom"},
|
216
|
+
condition: -> { object.status == 'draft' } # Cosmetic condition only
|
217
|
+
```
|
218
|
+
|
219
|
+
#### Display Options
|
220
|
+
```ruby
|
221
|
+
display :content,
|
222
|
+
as: :markdown,
|
223
|
+
class: "prose",
|
224
|
+
wrapper: {class: "content-wrapper"},
|
225
|
+
condition: -> { Rails.env.development? } # Cosmetic condition only
|
226
|
+
```
|
227
|
+
|
228
|
+
#### Collection Options (for selects)
|
229
|
+
```ruby
|
230
|
+
input :category, as: :select, collection: %w[Tech Business Lifestyle]
|
231
|
+
input :author, as: :select, collection: -> { User.active.pluck(:name, :id) }
|
232
|
+
|
233
|
+
# Collection procs are executed in form rendering context with access to:
|
234
|
+
# current_user, current_parent, object, request, params, and helpers
|
235
|
+
input :team_members, as: :select, collection: -> {
|
236
|
+
current_user.organization.users.active.pluck(:name, :id)
|
237
|
+
}
|
238
|
+
input :related_posts, as: :select, collection: -> {
|
239
|
+
Post.where.not(id: object.id).published.pluck(:title, :id) if object.persisted?
|
240
|
+
}
|
241
|
+
```
|
242
|
+
|
243
|
+
#### File Upload Options
|
244
|
+
```ruby
|
245
|
+
input :avatar, as: :file, multiple: false
|
246
|
+
input :documents, as: :file, multiple: true
|
247
|
+
# Note: Advanced file options like allowed_file_types and max_file_size
|
248
|
+
# are not currently supported by the framework
|
249
|
+
```
|
250
|
+
|
251
|
+
### Resource Policies
|
252
|
+
|
253
|
+
Policies control authorization and data access. They define who can perform what actions and what data users can see.
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
class PostPolicy < Plutonium::Resource::Policy
|
257
|
+
# CRUD permissions
|
258
|
+
def index?
|
259
|
+
true # Everyone can view list
|
260
|
+
end
|
261
|
+
|
262
|
+
def show?
|
263
|
+
record.published? || record.author == user || user.admin?
|
264
|
+
end
|
265
|
+
|
266
|
+
def create?
|
267
|
+
user.present?
|
268
|
+
end
|
269
|
+
|
270
|
+
def update?
|
271
|
+
record.author == user || user.admin?
|
272
|
+
end
|
273
|
+
|
274
|
+
def destroy?
|
275
|
+
user.admin?
|
276
|
+
end
|
277
|
+
|
278
|
+
# Custom action permissions
|
279
|
+
def publish?
|
280
|
+
update? && record.draft?
|
281
|
+
end
|
282
|
+
|
283
|
+
# Attribute permissions - control which fields are accessible
|
284
|
+
def permitted_attributes_for_read
|
285
|
+
attrs = [:title, :content, :category, :published_at]
|
286
|
+
attrs << :internal_notes if user.admin?
|
287
|
+
attrs
|
288
|
+
end
|
289
|
+
|
290
|
+
def permitted_attributes_for_create
|
291
|
+
[:title, :content, :category]
|
292
|
+
end
|
293
|
+
|
294
|
+
def permitted_attributes_for_update
|
295
|
+
attrs = permitted_attributes_for_create
|
296
|
+
attrs << :slug if user.admin?
|
297
|
+
attrs
|
298
|
+
end
|
299
|
+
|
300
|
+
# Collection scoping - filter what records users can see
|
301
|
+
relation_scope do |relation|
|
302
|
+
relation = super(relation) # Apply entity scoping first
|
303
|
+
|
304
|
+
if user.admin?
|
305
|
+
relation
|
306
|
+
else
|
307
|
+
# Users see only their posts or published posts
|
308
|
+
relation.where(author: user).or(relation.where(published: true))
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
# Association permissions
|
313
|
+
def permitted_associations
|
314
|
+
[:comments, :author, :tags]
|
315
|
+
end
|
316
|
+
end
|
317
|
+
```
|
318
|
+
|
319
|
+
## Business Logic with Interactions
|
320
|
+
|
321
|
+
### Interaction Structure
|
322
|
+
```ruby
|
323
|
+
class PublishPostInteraction < Plutonium::Resource::Interaction
|
324
|
+
# Metadata for UI
|
325
|
+
presents label: "Publish Post",
|
326
|
+
icon: Phlex::TablerIcons::Send,
|
327
|
+
description: "Make this post visible to readers"
|
328
|
+
|
329
|
+
# Attributes (form inputs)
|
330
|
+
attribute :resource, class: Post
|
331
|
+
attribute :publish_date, :datetime, default: -> { Time.current }
|
332
|
+
attribute :notify_subscribers, :boolean, default: true
|
333
|
+
|
334
|
+
# Validations
|
335
|
+
validates :resource, presence: true
|
336
|
+
validates :publish_date, presence: true
|
337
|
+
|
338
|
+
private
|
339
|
+
|
340
|
+
def execute
|
341
|
+
# Business logic
|
342
|
+
resource.transaction do
|
343
|
+
resource.update!(
|
344
|
+
published_at: publish_date,
|
345
|
+
status: 'published'
|
346
|
+
)
|
347
|
+
|
348
|
+
# Side effects
|
349
|
+
NotifySubscribersJob.perform_later(resource) if notify_subscribers
|
350
|
+
|
351
|
+
# Return successful outcome
|
352
|
+
succeed(resource)
|
353
|
+
.with_message("Post published successfully!")
|
354
|
+
.with_redirect_response(resource_path(resource))
|
355
|
+
end
|
356
|
+
rescue => error
|
357
|
+
# Return failure outcome
|
358
|
+
failed(error.message)
|
359
|
+
end
|
360
|
+
end
|
361
|
+
```
|
362
|
+
|
363
|
+
### Interaction Types (Auto-detected)
|
364
|
+
- **Record Actions**: Have `attribute :resource` - work on single records
|
365
|
+
- **Bulk Actions**: Have `attribute :resources` - work on multiple records
|
366
|
+
- **Resource Actions**: Have neither - work at resource level (imports, etc.)
|
367
|
+
|
368
|
+
### Interaction Execution Patterns
|
369
|
+
- **Immediate**: Only `resource`/`resources` attribute → executes on click
|
370
|
+
- **Modal**: Additional attributes → shows form modal first
|
371
|
+
|
372
|
+
## Package Architecture
|
373
|
+
|
374
|
+
### Feature Packages
|
375
|
+
Contain business logic and domain models:
|
376
|
+
```bash
|
377
|
+
rails generate pu:pkg:package blogging
|
378
|
+
```
|
379
|
+
|
380
|
+
Structure:
|
381
|
+
```
|
382
|
+
packages/blogging/
|
383
|
+
├── app/
|
384
|
+
│ ├── models/blogging/
|
385
|
+
│ ├── definitions/blogging/
|
386
|
+
│ ├── policies/blogging/
|
387
|
+
│ ├── interactions/blogging/
|
388
|
+
│ └── controllers/blogging/
|
389
|
+
└── lib/engine.rb
|
390
|
+
```
|
391
|
+
|
392
|
+
### Portal Packages
|
393
|
+
Provide web interfaces with routing and authentication:
|
394
|
+
```bash
|
395
|
+
rails generate pu:pkg:portal admin
|
396
|
+
```
|
397
|
+
|
398
|
+
Portal Engine:
|
399
|
+
```ruby
|
400
|
+
module AdminPortal
|
401
|
+
class Engine < ::Rails::Engine
|
402
|
+
include Plutonium::Portal::Engine
|
403
|
+
|
404
|
+
# Multi-tenancy
|
405
|
+
scope_to_entity Organization, strategy: :path
|
406
|
+
end
|
407
|
+
end
|
408
|
+
```
|
409
|
+
|
410
|
+
Portal Routes:
|
411
|
+
```ruby
|
412
|
+
AdminPortal::Engine.routes.draw do
|
413
|
+
register_resource Blog::Post
|
414
|
+
register_resource Blog::Comment
|
415
|
+
register_resource User
|
416
|
+
end
|
417
|
+
```
|
418
|
+
|
419
|
+
Portal Controller:
|
420
|
+
```ruby
|
421
|
+
module AdminPortal
|
422
|
+
module Concerns
|
423
|
+
module Controller
|
424
|
+
extend ActiveSupport::Concern
|
425
|
+
include Plutonium::Portal::Controller
|
426
|
+
include Plutonium::Auth::Rodauth(:admin)
|
427
|
+
|
428
|
+
included do
|
429
|
+
before_action :ensure_admin_access
|
430
|
+
layout "admin_portal"
|
431
|
+
end
|
432
|
+
|
433
|
+
private
|
434
|
+
|
435
|
+
def ensure_admin_access
|
436
|
+
redirect_to root_path unless current_user&.admin?
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
```
|
442
|
+
|
443
|
+
## Authentication & Authorization
|
444
|
+
|
445
|
+
### Rodauth Setup
|
446
|
+
```bash
|
447
|
+
# Install Rodauth
|
448
|
+
rails generate pu:rodauth:install
|
449
|
+
|
450
|
+
# Create user account
|
451
|
+
rails generate pu:rodauth:account user
|
452
|
+
|
453
|
+
# Create admin account with MFA
|
454
|
+
rails generate pu:rodauth:account admin --no-defaults \
|
455
|
+
--login --logout --remember --lockout \
|
456
|
+
--create-account --verify-account --close-account \
|
457
|
+
--change-password --reset-password --reset-password-notify \
|
458
|
+
--active-sessions --password-grace-period --otp \
|
459
|
+
--recovery-codes --audit-logging --internal-request
|
460
|
+
```
|
461
|
+
|
462
|
+
### Authentication Integration
|
463
|
+
```ruby
|
464
|
+
# In portal controller concerns
|
465
|
+
include Plutonium::Auth::Rodauth(:user) # For user authentication
|
466
|
+
include Plutonium::Auth::Rodauth(:admin) # For admin authentication
|
467
|
+
include Plutonium::Auth::Public # For public access
|
468
|
+
```
|
469
|
+
|
470
|
+
## Multi-Tenancy & Entity Scoping
|
471
|
+
|
472
|
+
### Path-Based Scoping
|
473
|
+
```ruby
|
474
|
+
# Portal engine
|
475
|
+
scope_to_entity Organization, strategy: :path
|
476
|
+
|
477
|
+
# Generates routes like: /organizations/:organization_id/posts
|
478
|
+
# Automatically scopes all queries to the organization
|
479
|
+
```
|
480
|
+
|
481
|
+
### Database Associations for Scoping
|
482
|
+
```ruby
|
483
|
+
# Direct association (auto-detected)
|
484
|
+
class Post < ApplicationRecord
|
485
|
+
belongs_to :organization
|
486
|
+
end
|
487
|
+
|
488
|
+
# Indirect association (auto-detected)
|
489
|
+
class Post < ApplicationRecord
|
490
|
+
belongs_to :author, class_name: 'User'
|
491
|
+
has_one :organization, through: :author
|
492
|
+
end
|
493
|
+
|
494
|
+
# Custom scope (manual)
|
495
|
+
class Comment < ApplicationRecord
|
496
|
+
belongs_to :post
|
497
|
+
|
498
|
+
scope :associated_with_organization, ->(organization) do
|
499
|
+
joins(post: :author).where(users: { organization_id: organization.id })
|
500
|
+
end
|
501
|
+
end
|
502
|
+
```
|
503
|
+
|
504
|
+
## Query Objects & Filtering
|
505
|
+
|
506
|
+
### Automatic Query Handling
|
507
|
+
Controllers automatically handle:
|
508
|
+
- Search: `?q[search]=rails`
|
509
|
+
- Filters: `?q[published]=true`
|
510
|
+
- Sorting: `?q[sort_fields][]=created_at&q[sort_directions][created_at]=desc`
|
511
|
+
- Scopes: `?q[scope]=published`
|
512
|
+
|
513
|
+
### Custom Query Objects
|
514
|
+
```ruby
|
515
|
+
query_object = Plutonium::Resource::QueryObject.new(Post, params[:q] || {}, request.path) do |query|
|
516
|
+
# Custom search
|
517
|
+
query.define_search proc { |scope, search:|
|
518
|
+
scope.joins(:author, :tags)
|
519
|
+
.where("posts.title ILIKE :search OR users.name ILIKE :search", search: "%#{search}%")
|
520
|
+
.distinct
|
521
|
+
}
|
522
|
+
|
523
|
+
# Custom filter
|
524
|
+
query.define_filter :date_range, proc { |scope, start_date:, end_date:|
|
525
|
+
scope.where(created_at: start_date.beginning_of_day..end_date.end_of_day)
|
526
|
+
}
|
527
|
+
|
528
|
+
# Custom sorter
|
529
|
+
query.define_sorter :author_name, proc { |scope, direction:|
|
530
|
+
scope.joins(:author).order("users.name #{direction}")
|
531
|
+
}
|
532
|
+
end
|
533
|
+
```
|
534
|
+
|
535
|
+
## UI Components & Theming
|
536
|
+
|
537
|
+
### Component Architecture
|
538
|
+
Built on Phlex, components inherit from `Plutonium::UI::Component::Base`:
|
539
|
+
|
540
|
+
```ruby
|
541
|
+
class CustomComponent < Plutonium::UI::Component::Base
|
542
|
+
def initialize(title:, content: nil)
|
543
|
+
@title = title
|
544
|
+
@content = content
|
545
|
+
end
|
546
|
+
|
547
|
+
def view_template
|
548
|
+
div(class: "custom-component") do
|
549
|
+
h2(class: "text-xl font-bold") { @title }
|
550
|
+
div(class: "content") { @content } if @content
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
554
|
+
```
|
555
|
+
|
556
|
+
### Layout Customization
|
557
|
+
```ruby
|
558
|
+
# Custom page classes in definitions
|
559
|
+
class PostDefinition < Plutonium::Resource::Definition
|
560
|
+
class ShowPage < Plutonium::UI::Page::Show
|
561
|
+
def render_content
|
562
|
+
# Custom show page rendering
|
563
|
+
super
|
564
|
+
end
|
565
|
+
end
|
566
|
+
end
|
567
|
+
```
|
568
|
+
|
569
|
+
## Configuration
|
570
|
+
|
571
|
+
### Main Configuration
|
572
|
+
```ruby
|
573
|
+
# config/initializers/plutonium.rb
|
574
|
+
Plutonium.configure do |config|
|
575
|
+
config.load_defaults 1.0
|
576
|
+
|
577
|
+
# Development settings
|
578
|
+
config.development = Rails.env.development?
|
579
|
+
config.cache_discovery = !Rails.env.development?
|
580
|
+
config.enable_hotreload = Rails.env.development?
|
581
|
+
|
582
|
+
# Assets
|
583
|
+
config.assets.logo = "custom_logo.png"
|
584
|
+
config.assets.stylesheet = "custom_theme.css"
|
585
|
+
config.assets.script = "custom_plutonium.js"
|
586
|
+
config.assets.favicon = "custom_favicon.ico"
|
587
|
+
end
|
588
|
+
```
|
589
|
+
|
590
|
+
## Generator Commands
|
591
|
+
|
592
|
+
### Core Setup
|
593
|
+
```bash
|
594
|
+
# Install Plutonium in existing app
|
595
|
+
bin/rails app:template LOCATION=https://radioactive-labs.github.io/plutonium-core/templates/base.rb
|
596
|
+
rails generate pu:core:install
|
597
|
+
|
598
|
+
# Install authentication
|
599
|
+
rails generate pu:rodauth:install
|
600
|
+
rails generate pu:rodauth:account user
|
601
|
+
```
|
602
|
+
|
603
|
+
### Package Creation
|
604
|
+
```bash
|
605
|
+
# Create feature package
|
606
|
+
rails generate pu:pkg:package blogging
|
607
|
+
|
608
|
+
# Create portal package
|
609
|
+
rails generate pu:pkg:portal admin
|
610
|
+
```
|
611
|
+
|
612
|
+
### Resource Management
|
613
|
+
```bash
|
614
|
+
# Full resource scaffold
|
615
|
+
rails generate pu:res:scaffold Post user:belongs_to title:string content:text
|
616
|
+
|
617
|
+
# Individual resource components
|
618
|
+
rails generate pu:res:model Post title:string content:text
|
619
|
+
rails generate pu:res:definition Post
|
620
|
+
rails generate pu:res:policy Post
|
621
|
+
```
|
622
|
+
|
623
|
+
## Best Practices
|
624
|
+
|
625
|
+
### Resource Design
|
626
|
+
1. Keep models focused on data and basic validations
|
627
|
+
2. Use definitions for UI configuration, not business logic
|
628
|
+
3. Implement complex business logic in interactions
|
629
|
+
4. Use policies for all authorization logic
|
630
|
+
5. Leverage scopes for common queries
|
631
|
+
|
632
|
+
### Package Organization
|
633
|
+
1. Create feature packages for domain logic
|
634
|
+
2. Use portal packages for different user interfaces
|
635
|
+
3. Keep packages focused and cohesive
|
636
|
+
4. Namespace everything properly to avoid conflicts
|
637
|
+
|
638
|
+
### Security
|
639
|
+
1. Always define explicit permissions in policies
|
640
|
+
2. Use secure defaults (deny by default)
|
641
|
+
3. Implement proper entity scoping for multi-tenancy
|
642
|
+
4. Validate all user inputs in interactions
|
643
|
+
5. Use CSRF protection and implement rate limiting
|
644
|
+
|
645
|
+
### Performance
|
646
|
+
1. Use `includes` and `joins` in relation scopes
|
647
|
+
2. Add database indexes for filtered and sorted fields
|
648
|
+
3. Implement caching where appropriate
|
649
|
+
4. Use background jobs for heavy operations
|
650
|
+
5. Monitor N+1 queries with tools like Prosopite
|
651
|
+
|
652
|
+
### Code Organization
|
653
|
+
1. Follow Rails naming conventions
|
654
|
+
2. Keep controllers thin - use interactions for business logic
|
655
|
+
3. Use consistent patterns across resources
|
656
|
+
4. Write comprehensive tests for policies and interactions
|
657
|
+
5. Document complex business logic
|
658
|
+
|
659
|
+
### Development Workflow
|
660
|
+
1. Start with resource scaffolding for rapid prototyping
|
661
|
+
2. Customize definitions for UI requirements
|
662
|
+
3. Implement business logic through interactions
|
663
|
+
4. Add proper authorization with policies
|
664
|
+
5. Create appropriate packages for organization
|
665
|
+
6. Set up portals for different user types
|
666
|
+
|
667
|
+
This comprehensive guide should enable you to build robust, maintainable Plutonium applications following the framework's conventions and best practices.
|
@@ -49,11 +49,6 @@ module Pu
|
|
49
49
|
registerControllers(application)
|
50
50
|
EOT
|
51
51
|
|
52
|
-
# For older versions tailwindcss, specifically v3
|
53
|
-
gsub_file "app/assets/stylesheets/application.tailwind.css", %r{@tailwind\s+base;\s*@tailwind\s+components;\s*@tailwind\s+utilities;\s*} do
|
54
|
-
"@import \"tailwindcss\";\n@config '../../../tailwind.config.js';\n"
|
55
|
-
end
|
56
|
-
|
57
52
|
insert_into_file "app/assets/stylesheets/application.tailwind.css", <<~EOT, after: /@import "tailwindcss";\n/
|
58
53
|
@config '../../../tailwind.config.js';
|
59
54
|
EOT
|
@@ -83,8 +83,13 @@ module Plutonium
|
|
83
83
|
display_definition = resource_definition.defined_displays[name] || {}
|
84
84
|
display_options = display_definition[:options] || {}
|
85
85
|
|
86
|
+
# Check for conditional rendering
|
87
|
+
condition = display_options[:condition] || field_options[:condition]
|
88
|
+
conditionally_hidden = condition && !instance_exec(&condition)
|
89
|
+
return if conditionally_hidden
|
90
|
+
|
86
91
|
tag = display_options[:as] || field_options[:as]
|
87
|
-
tag_attributes = display_options.except(:wrapper, :as)
|
92
|
+
tag_attributes = display_options.except(:wrapper, :as, :condition)
|
88
93
|
tag_block = display_definition[:block] || ->(f) {
|
89
94
|
tag ||= f.inferred_field_component
|
90
95
|
f.send(:"#{tag}_tag", **tag_attributes)
|
@@ -92,7 +97,7 @@ module Plutonium
|
|
92
97
|
|
93
98
|
wrapper_options = display_options[:wrapper] || {}
|
94
99
|
|
95
|
-
field_options = field_options.except(:as)
|
100
|
+
field_options = field_options.except(:as, :condition)
|
96
101
|
render field(name, **field_options).wrapped(**wrapper_options) do |f|
|
97
102
|
render instance_exec(f, &tag_block)
|
98
103
|
end
|