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.
- checksums.yaml +4 -4
- data/# Plutonium: The pre-alpha demo.md +4 -2
- data/.claude/skills/assets/SKILL.md +416 -0
- data/.claude/skills/connect-resource/SKILL.md +112 -0
- data/.claude/skills/controller/SKILL.md +302 -0
- data/.claude/skills/create-resource/SKILL.md +240 -0
- data/.claude/skills/definition/SKILL.md +218 -0
- data/.claude/skills/definition-actions/SKILL.md +386 -0
- data/.claude/skills/definition-fields/SKILL.md +474 -0
- data/.claude/skills/definition-query/SKILL.md +334 -0
- data/.claude/skills/forms/SKILL.md +439 -0
- data/.claude/skills/installation/SKILL.md +300 -0
- data/.claude/skills/interaction/SKILL.md +382 -0
- data/.claude/skills/model/SKILL.md +267 -0
- data/.claude/skills/model-features/SKILL.md +286 -0
- data/.claude/skills/nested-resources/SKILL.md +274 -0
- data/.claude/skills/package/SKILL.md +191 -0
- data/.claude/skills/policy/SKILL.md +352 -0
- data/.claude/skills/portal/SKILL.md +400 -0
- data/.claude/skills/resource/SKILL.md +281 -0
- data/.claude/skills/rodauth/SKILL.md +452 -0
- data/.claude/skills/views/SKILL.md +563 -0
- data/Appraisals +46 -4
- data/CHANGELOG.md +32 -1
- data/app/assets/plutonium.css +2 -2
- data/config/brakeman.ignore +239 -0
- data/config/initializers/action_policy.rb +1 -1
- data/docs/.vitepress/config.ts +132 -47
- data/docs/concepts/architecture.md +226 -0
- data/docs/concepts/auto-detection.md +254 -0
- data/docs/concepts/index.md +61 -0
- data/docs/concepts/packages-portals.md +304 -0
- data/docs/concepts/resources.md +224 -0
- data/docs/cookbook/blog.md +412 -0
- data/docs/cookbook/index.md +289 -0
- data/docs/cookbook/saas.md +481 -0
- data/docs/getting-started/index.md +56 -0
- data/docs/getting-started/installation.md +146 -0
- data/docs/getting-started/tutorial/01-setup.md +118 -0
- data/docs/getting-started/tutorial/02-first-resource.md +180 -0
- data/docs/getting-started/tutorial/03-authentication.md +246 -0
- data/docs/getting-started/tutorial/04-authorization.md +170 -0
- data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
- data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
- data/docs/getting-started/tutorial/index.md +64 -0
- data/docs/guides/adding-resources.md +420 -0
- data/docs/guides/authentication.md +551 -0
- data/docs/guides/authorization.md +468 -0
- data/docs/guides/creating-packages.md +380 -0
- data/docs/guides/custom-actions.md +523 -0
- data/docs/guides/index.md +45 -0
- data/docs/guides/multi-tenancy.md +302 -0
- data/docs/guides/nested-resources.md +411 -0
- data/docs/guides/search-filtering.md +266 -0
- data/docs/guides/theming.md +321 -0
- data/docs/index.md +67 -26
- data/docs/public/CLAUDE.md +64 -21
- data/docs/reference/assets/index.md +496 -0
- data/docs/reference/controller/index.md +363 -0
- data/docs/reference/definition/actions.md +400 -0
- data/docs/reference/definition/fields.md +350 -0
- data/docs/reference/definition/index.md +252 -0
- data/docs/reference/definition/query.md +342 -0
- data/docs/reference/generators/index.md +469 -0
- data/docs/reference/index.md +49 -0
- data/docs/reference/interaction/index.md +445 -0
- data/docs/reference/model/features.md +248 -0
- data/docs/reference/model/index.md +219 -0
- data/docs/reference/policy/index.md +385 -0
- data/docs/reference/portal/index.md +382 -0
- data/docs/reference/views/forms.md +396 -0
- data/docs/reference/views/index.md +479 -0
- data/gemfiles/rails_7.gemfile +9 -2
- data/gemfiles/rails_7.gemfile.lock +146 -111
- data/gemfiles/rails_8.0.gemfile +20 -0
- data/gemfiles/rails_8.0.gemfile.lock +417 -0
- data/gemfiles/rails_8.1.gemfile +20 -0
- data/gemfiles/rails_8.1.gemfile.lock +419 -0
- data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
- data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
- data/lib/generators/pu/pkg/portal/USAGE +65 -0
- data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
- data/lib/generators/pu/res/conn/USAGE +71 -0
- data/lib/generators/pu/res/model/USAGE +106 -110
- data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
- data/lib/generators/pu/res/scaffold/USAGE +85 -0
- data/lib/generators/pu/rodauth/install_generator.rb +2 -6
- data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
- data/lib/generators/pu/skills/sync/USAGE +14 -0
- data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
- data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
- data/lib/plutonium/core/controller.rb +2 -2
- data/lib/plutonium/interaction/base.rb +1 -0
- data/lib/plutonium/package/engine.rb +2 -2
- data/lib/plutonium/query/adhoc_block.rb +6 -2
- data/lib/plutonium/query/model_scope.rb +1 -1
- data/lib/plutonium/railtie.rb +4 -0
- data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
- data/lib/plutonium/resource/query_object.rb +38 -8
- data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
- data/lib/plutonium/version.rb +1 -1
- data/lib/tasks/release.rake +19 -4
- data/package.json +1 -1
- metadata +76 -39
- data/brakeman.ignore +0 -28
- data/docs/api-examples.md +0 -49
- data/docs/guide/claude-code-guide.md +0 -74
- data/docs/guide/deep-dive/authorization.md +0 -189
- data/docs/guide/deep-dive/multitenancy.md +0 -256
- data/docs/guide/deep-dive/resources.md +0 -390
- data/docs/guide/getting-started/01-installation.md +0 -165
- data/docs/guide/index.md +0 -28
- data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
- data/docs/guide/introduction/02-core-concepts.md +0 -440
- data/docs/guide/tutorial/01-project-setup.md +0 -75
- data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
- data/docs/guide/tutorial/03-defining-resources.md +0 -90
- data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
- data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
- data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
- data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
- data/docs/markdown-examples.md +0 -85
- data/docs/modules/action.md +0 -244
- data/docs/modules/authentication.md +0 -236
- data/docs/modules/configuration.md +0 -599
- data/docs/modules/controller.md +0 -443
- data/docs/modules/core.md +0 -316
- data/docs/modules/definition.md +0 -1308
- data/docs/modules/display.md +0 -759
- data/docs/modules/form.md +0 -495
- data/docs/modules/generator.md +0 -400
- data/docs/modules/index.md +0 -167
- data/docs/modules/interaction.md +0 -642
- data/docs/modules/package.md +0 -151
- data/docs/modules/policy.md +0 -176
- data/docs/modules/portal.md +0 -710
- data/docs/modules/query.md +0 -297
- data/docs/modules/resource_record.md +0 -618
- data/docs/modules/routing.md +0 -690
- data/docs/modules/table.md +0 -301
- data/docs/modules/ui.md +0 -631
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Chapter 5: Adding Custom Actions
|
|
2
|
+
|
|
3
|
+
In this chapter, you'll add a "Publish" action to posts using Interactions.
|
|
4
|
+
|
|
5
|
+
## What are Interactions?
|
|
6
|
+
|
|
7
|
+
Interactions are classes that encapsulate business logic. They're used for:
|
|
8
|
+
- Operations more complex than simple CRUD
|
|
9
|
+
- Actions that need validation beyond the model
|
|
10
|
+
- Operations involving multiple models
|
|
11
|
+
- Business logic you want to reuse
|
|
12
|
+
|
|
13
|
+
## Creating the Publish Interaction
|
|
14
|
+
|
|
15
|
+
Create an interaction to publish posts:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# packages/blogging/app/interactions/blogging/publish_post.rb
|
|
19
|
+
class Blogging::PublishPost < Blogging::ResourceInteraction
|
|
20
|
+
# Presentation
|
|
21
|
+
presents label: "Publish Post",
|
|
22
|
+
icon: Phlex::TablerIcons::Send
|
|
23
|
+
|
|
24
|
+
# Having `attribute :resource` makes this a record action
|
|
25
|
+
# (shows on individual records and table rows)
|
|
26
|
+
attribute :resource
|
|
27
|
+
|
|
28
|
+
# Validation
|
|
29
|
+
validate :post_not_already_published
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def execute
|
|
34
|
+
resource.update!(published: true, published_at: Time.current)
|
|
35
|
+
|
|
36
|
+
succeed(resource)
|
|
37
|
+
.with_message("Post published successfully!")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def post_not_already_published
|
|
41
|
+
if resource.published?
|
|
42
|
+
errors.add(:base, "Post is already published")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Registering the Action
|
|
49
|
+
|
|
50
|
+
Add the action to the Post definition:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# packages/blogging/app/definitions/blogging/post_definition.rb
|
|
54
|
+
class Blogging::PostDefinition < Blogging::ResourceDefinition
|
|
55
|
+
# Register the publish action
|
|
56
|
+
action :publish, interaction: Blogging::PublishPost
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Action placement is automatically determined:
|
|
61
|
+
- `attribute :resource` → shows on records and table rows
|
|
62
|
+
- `attribute :resources` → bulk action for selected records
|
|
63
|
+
- Neither → resource-level action (like "New" button)
|
|
64
|
+
|
|
65
|
+
## Authorizing the Action
|
|
66
|
+
|
|
67
|
+
Add permission for the action in the policy:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# packages/blogging/app/policies/blogging/post_policy.rb
|
|
71
|
+
class Blogging::PostPolicy < Blogging::ResourcePolicy
|
|
72
|
+
# ... existing permissions ...
|
|
73
|
+
|
|
74
|
+
def publish?
|
|
75
|
+
owner? && !record.published?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Testing the Action
|
|
81
|
+
|
|
82
|
+
1. Create an unpublished post
|
|
83
|
+
2. View the post details
|
|
84
|
+
3. Click the "Publish" action button
|
|
85
|
+
4. The post is now published
|
|
86
|
+
|
|
87
|
+
## Actions with User Input
|
|
88
|
+
|
|
89
|
+
Let's create a more complex action - scheduling publication:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# packages/blogging/app/interactions/blogging/schedule_post.rb
|
|
93
|
+
class Blogging::SchedulePost < Blogging::ResourceInteraction
|
|
94
|
+
presents label: "Schedule Publication",
|
|
95
|
+
icon: Phlex::TablerIcons::Calendar
|
|
96
|
+
|
|
97
|
+
attribute :resource
|
|
98
|
+
attribute :publish_at, :datetime
|
|
99
|
+
|
|
100
|
+
# Define form input
|
|
101
|
+
input :publish_at, as: :datetime
|
|
102
|
+
|
|
103
|
+
# Validations
|
|
104
|
+
validates :publish_at, presence: true
|
|
105
|
+
validate :publish_at_in_future
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def execute
|
|
110
|
+
resource.update!(
|
|
111
|
+
scheduled_at: publish_at,
|
|
112
|
+
published: false
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
succeed(resource)
|
|
116
|
+
.with_message("Post scheduled for #{publish_at.strftime('%B %d, %Y at %I:%M %p')}")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def publish_at_in_future
|
|
120
|
+
if publish_at.present? && publish_at <= Time.current
|
|
121
|
+
errors.add(:publish_at, "must be in the future")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Register it:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
# In PostDefinition
|
|
131
|
+
action :schedule, interaction: Blogging::SchedulePost
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Because the interaction defines an `input`, users see a form to select the publication date.
|
|
135
|
+
|
|
136
|
+
## Resource-Level Actions
|
|
137
|
+
|
|
138
|
+
Actions can operate at the resource level (not on a specific record):
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
# packages/blogging/app/interactions/blogging/import_posts.rb
|
|
142
|
+
class Blogging::ImportPosts < Blogging::ResourceInteraction
|
|
143
|
+
presents label: "Import Posts",
|
|
144
|
+
icon: Phlex::TablerIcons::Upload
|
|
145
|
+
|
|
146
|
+
attribute :file
|
|
147
|
+
|
|
148
|
+
input :file, as: :file
|
|
149
|
+
|
|
150
|
+
validates :file, presence: true
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def execute
|
|
155
|
+
# Process CSV file...
|
|
156
|
+
succeed(nil).with_message("Posts imported successfully")
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Register it:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
action :import, interaction: Blogging::ImportPosts
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Since `ImportPosts` has no `attribute :resource` or `attribute :resources`, it automatically becomes a resource-level action.
|
|
168
|
+
|
|
169
|
+
## Action Placement
|
|
170
|
+
|
|
171
|
+
For **interactive actions**, placement is auto-determined from attributes:
|
|
172
|
+
|
|
173
|
+
| Attribute | Placement |
|
|
174
|
+
|-----------|-----------|
|
|
175
|
+
| `attribute :resource` | Record show page + table rows |
|
|
176
|
+
| `attribute :resources` | Bulk action (selected records) |
|
|
177
|
+
| Neither | Resource-level (like "New" button) |
|
|
178
|
+
|
|
179
|
+
You can override with explicit options if needed:
|
|
180
|
+
|
|
181
|
+
| Option | Location |
|
|
182
|
+
|--------|----------|
|
|
183
|
+
| `record_action:` | Record show page |
|
|
184
|
+
| `collection_record_action:` | Table row actions |
|
|
185
|
+
| `resource_action:` | Above the table |
|
|
186
|
+
|
|
187
|
+
## Action Styling
|
|
188
|
+
|
|
189
|
+
Customize action appearance:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
action :archive,
|
|
193
|
+
interaction: ArchivePost,
|
|
194
|
+
category: :danger, # red styling
|
|
195
|
+
confirmation: "Are you sure?" # confirmation dialog
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## What's Next
|
|
199
|
+
|
|
200
|
+
We have posts with custom actions. In the next chapter, we'll add Comments as a nested resource.
|
|
201
|
+
|
|
202
|
+
[Continue to Chapter 6: Nested Resources →](./06-nested-resources)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Chapter 6: Nested Resources
|
|
2
|
+
|
|
3
|
+
In this chapter, you'll add Comments as a nested resource under Posts.
|
|
4
|
+
|
|
5
|
+
## What are Nested Resources?
|
|
6
|
+
|
|
7
|
+
Nested resources are resources that belong to a parent resource. In our blog:
|
|
8
|
+
- Comments belong to Posts
|
|
9
|
+
- The URL reflects this: `/admin/blogging/posts/1/blogging/comments`
|
|
10
|
+
- Comments are automatically scoped to their parent post
|
|
11
|
+
|
|
12
|
+
## Generating the Comment Resource
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
rails generate pu:res:scaffold Comment body:text user:belongs_to Blogging/Post:belongs_to --dest=blogging
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Setting Up the Association
|
|
19
|
+
|
|
20
|
+
Update the Post model to add the `has_many` association:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# packages/blogging/app/models/blogging/post.rb
|
|
24
|
+
class Blogging::Post < Blogging::ResourceRecord
|
|
25
|
+
belongs_to :user
|
|
26
|
+
has_many :comments, foreign_key: :post_id, dependent: :destroy
|
|
27
|
+
# ... existing code
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## The Comment Model
|
|
32
|
+
|
|
33
|
+
The generator creates:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# packages/blogging/app/models/blogging/comment.rb
|
|
37
|
+
class Blogging::Comment < Blogging::ResourceRecord
|
|
38
|
+
belongs_to :user
|
|
39
|
+
belongs_to :post, class_name: "Blogging::Post"
|
|
40
|
+
# ... generated code
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Note: When referencing resources within the same package, the generator uses the short name (`:post`) while setting the appropriate `class_name` and foreign key to the correct table.
|
|
45
|
+
|
|
46
|
+
## Connecting to the Portal
|
|
47
|
+
|
|
48
|
+
Connect comments to the admin portal:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
rails generate pu:res:conn Blogging::Comment --dest=admin_portal
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Because `Comment` has `belongs_to :post`, Plutonium automatically creates nested routes:
|
|
55
|
+
- `GET /admin/blogging/posts/:blogging_post_id/blogging/comments`
|
|
56
|
+
- `POST /admin/blogging/posts/:blogging_post_id/blogging/comments`
|
|
57
|
+
- `GET /admin/blogging/posts/:blogging_post_id/blogging/comments/:id`
|
|
58
|
+
|
|
59
|
+
## Showing Comments on Post Detail
|
|
60
|
+
|
|
61
|
+
To show a comments panel on the post detail page, add `comments` to `permitted_associations` in the policy:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# packages/blogging/app/policies/blogging/post_policy.rb
|
|
65
|
+
class Blogging::PostPolicy < Blogging::ResourcePolicy
|
|
66
|
+
def permitted_associations
|
|
67
|
+
%i[user comments]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The panel links to the nested comments route and shows "Add Comment" if the user has permission.
|
|
73
|
+
|
|
74
|
+
## Comment Policy
|
|
75
|
+
|
|
76
|
+
Add authorization for comments:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# packages/blogging/app/policies/blogging/comment_policy.rb
|
|
80
|
+
class Blogging::CommentPolicy < Blogging::ResourcePolicy
|
|
81
|
+
def read?
|
|
82
|
+
true # Anyone can read comments
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def create?
|
|
86
|
+
true # Anyone authenticated can comment
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def update?
|
|
90
|
+
owner?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def destroy?
|
|
94
|
+
owner? || post_owner?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Scope to comments on published posts (or user's own posts)
|
|
98
|
+
def relation_scope(relation)
|
|
99
|
+
relation.joins(:post).where(
|
|
100
|
+
blogging_posts: {published: true}
|
|
101
|
+
).or(
|
|
102
|
+
relation.where(user_id: user.id)
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def owner?
|
|
109
|
+
record.user_id == user.id
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def post_owner?
|
|
113
|
+
record.post.user_id == user.id
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Nested Forms
|
|
119
|
+
|
|
120
|
+
You can edit comments directly on the post form using nested attributes:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
# In Post model
|
|
124
|
+
accepts_nested_attributes_for :comments, allow_destroy: true
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
# In PostDefinition - option 1: inline block
|
|
129
|
+
nested_input :comments do |definition|
|
|
130
|
+
definition.input :body
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# In PostDefinition - option 2: use existing definition
|
|
134
|
+
nested_input :comments, using: Blogging::CommentDefinition, fields: %i[body]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This adds an inline comments editor to the post form.
|
|
138
|
+
|
|
139
|
+
## Nesting Limitations
|
|
140
|
+
|
|
141
|
+
Plutonium supports one level of nesting. For deeper hierarchies (e.g., Replies to Comments), keep URLs flat: `/blogging/comments/:blogging_comment_id/blogging/replies`
|
|
142
|
+
|
|
143
|
+
## What's Next
|
|
144
|
+
|
|
145
|
+
Our blog has posts and comments. In the final chapter, we'll customize the UI to make it look polished.
|
|
146
|
+
|
|
147
|
+
[Continue to Chapter 7: Customizing the UI →](./07-customizing-ui)
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Chapter 7: Customizing the UI
|
|
2
|
+
|
|
3
|
+
In this chapter, you'll customize forms, tables, and pages to create a polished interface.
|
|
4
|
+
|
|
5
|
+
## Customizing Fields
|
|
6
|
+
|
|
7
|
+
Fields control how attributes appear in forms and displays. Plutonium auto-infers fields from your model, so you only need to declare fields when customizing their behavior.
|
|
8
|
+
|
|
9
|
+
### Field Types
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# packages/blogging/app/definitions/blogging/post_definition.rb
|
|
13
|
+
class Blogging::PostDefinition < Blogging::ResourceDefinition
|
|
14
|
+
# Rich text editor instead of plain textarea
|
|
15
|
+
field :body, as: :rich_text
|
|
16
|
+
|
|
17
|
+
# Select with predefined options
|
|
18
|
+
input :status, as: :select, choices: %w[draft review published]
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Conditional Fields
|
|
23
|
+
|
|
24
|
+
Show fields based on conditions:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
# Only show published_at when published is true
|
|
28
|
+
field :published_at, condition: -> { object.published? }
|
|
29
|
+
|
|
30
|
+
# Show different fields for new vs existing records
|
|
31
|
+
field :author, condition: :new_record?
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Customizing Tables
|
|
35
|
+
|
|
36
|
+
Columns are auto-inferred from your model. Only declare columns when customizing their behavior.
|
|
37
|
+
|
|
38
|
+
### Column Configuration
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# Custom label and sortable
|
|
42
|
+
column :user, label: "Author", sortable: true
|
|
43
|
+
|
|
44
|
+
# Computed column with block
|
|
45
|
+
column :comment_count do |post|
|
|
46
|
+
post.comments.count
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Table Actions
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# Show page actions
|
|
54
|
+
action :publish, interaction: Blogging::PublishPost, record_action: true
|
|
55
|
+
|
|
56
|
+
# Table row actions
|
|
57
|
+
action :archive, interaction: Blogging::ArchivePost, collection_record_action: true
|
|
58
|
+
|
|
59
|
+
# Index page actions
|
|
60
|
+
action :import, interaction: Blogging::ImportPosts, resource_action: true
|
|
61
|
+
|
|
62
|
+
# Bulk actions (selected records)
|
|
63
|
+
action :bulk_publish, interaction: Blogging::BulkPublish, bulk_action: true
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Customizing Search and Filters
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# Search configuration
|
|
70
|
+
search do |scope, query|
|
|
71
|
+
scope.where("title ILIKE ? OR body ILIKE ?", "%#{query}%", "%#{query}%")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Predefined scopes (reference model scopes)
|
|
75
|
+
scope :published, default: true # Applied by default, uses Post.published
|
|
76
|
+
scope :drafts # Uses Post.draft
|
|
77
|
+
|
|
78
|
+
# Inline scope with block
|
|
79
|
+
scope(:recent) { |scope| scope.where('created_at > ?', 1.week.ago) }
|
|
80
|
+
|
|
81
|
+
# Inline scope with controller context
|
|
82
|
+
scope(:mine) { |scope| scope.where(user: current_user) }
|
|
83
|
+
|
|
84
|
+
# Filters
|
|
85
|
+
filter :title, with: Plutonium::Query::Filters::Text, predicate: :contains
|
|
86
|
+
filter :status, with: Plutonium::Query::Filters::Text, predicate: :eq
|
|
87
|
+
|
|
88
|
+
# Custom filter with lambda
|
|
89
|
+
filter :published, with: ->(scope, value) {
|
|
90
|
+
value == "true" ? scope.where.not(published_at: nil) : scope.where(published_at: nil)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Sorting options
|
|
94
|
+
sort :title
|
|
95
|
+
sort :created_at
|
|
96
|
+
sort :published
|
|
97
|
+
|
|
98
|
+
# Default sort
|
|
99
|
+
default_sort :created_at, :desc
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Custom Page Classes
|
|
103
|
+
|
|
104
|
+
Override page title and description in definitions:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
class Blogging::PostDefinition < Blogging::ResourceDefinition
|
|
108
|
+
# Custom page titles
|
|
109
|
+
index_page_title "Blog Posts"
|
|
110
|
+
index_page_description "Manage your blog content"
|
|
111
|
+
|
|
112
|
+
show_page_title { |record| record.title }
|
|
113
|
+
show_page_description "View post details"
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For more advanced customization, you can create custom page classes that inherit from Plutonium's page components:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# packages/admin_portal/app/views/admin_portal/blogging/posts/index_page.rb
|
|
121
|
+
class AdminPortal::Blogging::Posts::IndexPage < Blogging::PostDefinition::IndexPage
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def page_title
|
|
125
|
+
"Blog Posts"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def page_description
|
|
129
|
+
"Manage your blog content"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Add content after the page header
|
|
133
|
+
def render_after_page_header
|
|
134
|
+
div(class: "mb-4 p-4 bg-blue-50 rounded") do
|
|
135
|
+
p { "Custom content here" }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Custom Form Layout
|
|
142
|
+
|
|
143
|
+
Control form layout using wrapper options in definitions:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
class Blogging::PostDefinition < Blogging::ResourceDefinition
|
|
147
|
+
# Full-width fields
|
|
148
|
+
input :title, wrapper: {class: "col-span-full"}
|
|
149
|
+
input :body, as: :rich_text, wrapper: {class: "col-span-full"}
|
|
150
|
+
|
|
151
|
+
# Side-by-side fields (default is col-span-full)
|
|
152
|
+
input :published_at, wrapper: {class: "col-span-1"}
|
|
153
|
+
input :category, wrapper: {class: "col-span-1"}
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
For advanced form customization, use the block syntax:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
input :birth_date do |f|
|
|
161
|
+
f.date_tag(min: 18.years.ago.to_date)
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Theming with TailwindCSS
|
|
166
|
+
|
|
167
|
+
Plutonium uses TailwindCSS 4. Customize the theme:
|
|
168
|
+
|
|
169
|
+
```css
|
|
170
|
+
/* app/assets/stylesheets/application.css */
|
|
171
|
+
@import "tailwindcss";
|
|
172
|
+
@import "gem:plutonium/src/css/plutonium.css";
|
|
173
|
+
|
|
174
|
+
@theme {
|
|
175
|
+
--color-primary-500: #6366f1; /* Indigo */
|
|
176
|
+
--color-primary-600: #4f46e5;
|
|
177
|
+
--color-primary-700: #4338ca;
|
|
178
|
+
|
|
179
|
+
--radius-md: 0.5rem;
|
|
180
|
+
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Custom Components
|
|
185
|
+
|
|
186
|
+
Create reusable components with Phlex:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
# app/components/status_badge.rb
|
|
190
|
+
class StatusBadge < Plutonium::UI::Component::Base
|
|
191
|
+
def initialize(published:)
|
|
192
|
+
@published = published
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def view_template
|
|
196
|
+
if @published
|
|
197
|
+
span(class: "px-2 py-1 text-xs bg-green-100 text-green-800 rounded") { "Published" }
|
|
198
|
+
else
|
|
199
|
+
span(class: "px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded") { "Draft" }
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Use in definition
|
|
205
|
+
column :status do |post|
|
|
206
|
+
render StatusBadge.new(published: post.published?)
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Layout Customization
|
|
211
|
+
|
|
212
|
+
Layouts are Phlex components that wrap page content. The base layout provides hooks for customization:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
class CustomLayout < Plutonium::UI::Layout::ResourceLayout
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
# Customize body classes
|
|
219
|
+
def body_attributes
|
|
220
|
+
{class: "antialiased min-h-screen bg-white dark:bg-gray-900"}
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Add content before the main section
|
|
224
|
+
def render_before_main
|
|
225
|
+
super
|
|
226
|
+
# Add custom header content
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Add content after the main section
|
|
230
|
+
def render_after_main
|
|
231
|
+
super
|
|
232
|
+
# Add custom footer content
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
See the [Theming Guide](/guides/theming) for comprehensive customization options.
|
|
238
|
+
|
|
239
|
+
## What's Next
|
|
240
|
+
|
|
241
|
+
Congratulations! You've built a complete blog application with:
|
|
242
|
+
- Resource CRUD operations
|
|
243
|
+
- Authentication with Rodauth
|
|
244
|
+
- Authorization with policies
|
|
245
|
+
- Custom actions with Interactions
|
|
246
|
+
- Nested resources
|
|
247
|
+
- Customized UI
|
|
248
|
+
|
|
249
|
+
Continue exploring:
|
|
250
|
+
- [Guides](/guides/) - Deep dives on specific topics
|
|
251
|
+
- [Reference](/reference/) - Complete API documentation
|
|
252
|
+
- [Cookbook](/cookbook/) - Real-world recipes
|
|
253
|
+
|
|
254
|
+
Happy building with Plutonium!
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Tutorial: Building a Blog
|
|
2
|
+
|
|
3
|
+
In this tutorial, you'll build a complete blog application with Plutonium. You'll learn:
|
|
4
|
+
|
|
5
|
+
- How to structure a Plutonium application
|
|
6
|
+
- Creating resources with models, definitions, and policies
|
|
7
|
+
- Setting up authentication with Rodauth
|
|
8
|
+
- Implementing authorization rules
|
|
9
|
+
- Adding custom actions with Interactions
|
|
10
|
+
- Customizing the UI
|
|
11
|
+
|
|
12
|
+
## What We'll Build
|
|
13
|
+
|
|
14
|
+
A blog application with:
|
|
15
|
+
- **Posts** - Articles with title, body, and publication status
|
|
16
|
+
- **Comments** - Nested under posts
|
|
17
|
+
- **Users** - Authors who can manage their own posts
|
|
18
|
+
- **Admin Portal** - Full access for administrators
|
|
19
|
+
- **Author Portal** - Limited access for content authors
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
- Ruby 3.2+
|
|
24
|
+
- Rails 8.0+ (or 7.1+)
|
|
25
|
+
- Node.js 18+
|
|
26
|
+
- PostgreSQL (or SQLite for development)
|
|
27
|
+
|
|
28
|
+
## Time Required
|
|
29
|
+
|
|
30
|
+
This tutorial takes approximately 45-60 minutes to complete.
|
|
31
|
+
|
|
32
|
+
## Chapters
|
|
33
|
+
|
|
34
|
+
### [1. Project Setup](./01-setup)
|
|
35
|
+
Create a new Plutonium application and understand the project structure.
|
|
36
|
+
|
|
37
|
+
### [2. Creating Your First Resource](./02-first-resource)
|
|
38
|
+
Generate the Post resource with model, definition, policy, and controller.
|
|
39
|
+
|
|
40
|
+
### [3. Setting Up Authentication](./03-authentication)
|
|
41
|
+
Configure Rodauth for user authentication with multiple account types.
|
|
42
|
+
|
|
43
|
+
### [4. Implementing Authorization](./04-authorization)
|
|
44
|
+
Add policies to control who can view, create, edit, and delete posts.
|
|
45
|
+
|
|
46
|
+
### [5. Adding Custom Actions](./05-custom-actions)
|
|
47
|
+
Create a "Publish" action using Interactions for business logic.
|
|
48
|
+
|
|
49
|
+
### [6. Nested Resources](./06-nested-resources)
|
|
50
|
+
Add Comments as a nested resource under Posts.
|
|
51
|
+
|
|
52
|
+
### [7. Customizing the UI](./07-customizing-ui)
|
|
53
|
+
Customize forms, tables, and views to match your requirements.
|
|
54
|
+
|
|
55
|
+
## Getting Help
|
|
56
|
+
|
|
57
|
+
If you get stuck:
|
|
58
|
+
- Check the [Guides](/guides/) for detailed explanations
|
|
59
|
+
- Browse the [Reference Documentation](/reference/) for API details
|
|
60
|
+
- Visit our [GitHub Issues](https://github.com/radioactive-labs/plutonium-core/issues)
|
|
61
|
+
|
|
62
|
+
Let's get started!
|
|
63
|
+
|
|
64
|
+
[Begin Chapter 1: Project Setup →](./01-setup)
|