plutonium 0.34.1 → 0.35.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/.claude/skills/plutonium/skill.md +53 -0
- data/.claude/skills/{assets → plutonium-assets}/SKILL.md +13 -8
- data/.claude/skills/{connect-resource → plutonium-connect-resource}/SKILL.md +1 -1
- data/.claude/skills/{controller → plutonium-controller}/SKILL.md +27 -13
- data/.claude/skills/{create-resource → plutonium-create-resource}/SKILL.md +1 -1
- data/.claude/skills/{definition → plutonium-definition}/SKILL.md +10 -10
- data/.claude/skills/{definition-actions → plutonium-definition-actions}/SKILL.md +34 -9
- data/.claude/skills/{definition-fields → plutonium-definition-fields}/SKILL.md +38 -10
- data/.claude/skills/plutonium-definition-query/SKILL.md +356 -0
- data/.claude/skills/{forms → plutonium-forms}/SKILL.md +6 -6
- data/.claude/skills/{installation → plutonium-installation}/SKILL.md +9 -9
- data/.claude/skills/{interaction → plutonium-interaction}/SKILL.md +20 -19
- data/.claude/skills/{model → plutonium-model}/SKILL.md +3 -3
- data/.claude/skills/{model-features → plutonium-model-features}/SKILL.md +3 -3
- data/.claude/skills/{nested-resources → plutonium-nested-resources}/SKILL.md +5 -5
- data/.claude/skills/{package → plutonium-package}/SKILL.md +7 -8
- data/.claude/skills/{policy → plutonium-policy}/SKILL.md +26 -4
- data/.claude/skills/{portal → plutonium-portal}/SKILL.md +33 -31
- data/.claude/skills/{resource → plutonium-resource}/SKILL.md +27 -27
- data/.claude/skills/{rodauth → plutonium-rodauth}/SKILL.md +5 -5
- data/.claude/skills/plutonium-theming/SKILL.md +424 -0
- data/.claude/skills/{views → plutonium-views}/SKILL.md +7 -7
- data/CHANGELOG.md +52 -0
- data/CLAUDE.md +215 -0
- data/CONTRIBUTING.md +72 -18
- data/README.md +100 -19
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1685 -1146
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +70 -70
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/resource/interactive_bulk_action.html.erb +1 -5
- data/app/views/rodauth/_email_auth_request_form.html.erb +1 -1
- data/app/views/rodauth/_login_form.html.erb +15 -55
- data/app/views/rodauth/_login_form_footer.html.erb +2 -2
- data/app/views/rodauth/_password_visibility.html.erb +2 -8
- data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
- data/app/views/rodauth/change_login.html.erb +36 -19
- data/app/views/rodauth/change_password.html.erb +34 -10
- data/app/views/rodauth/close_account.html.erb +12 -4
- data/app/views/rodauth/confirm_password.html.erb +19 -17
- data/app/views/rodauth/create_account.html.erb +30 -109
- data/app/views/rodauth/email_auth.html.erb +1 -1
- data/app/views/rodauth/logout.html.erb +4 -4
- data/app/views/rodauth/otp_auth.html.erb +13 -4
- data/app/views/rodauth/otp_disable.html.erb +12 -4
- data/app/views/rodauth/otp_setup.html.erb +29 -12
- data/app/views/rodauth/otp_unlock.html.erb +19 -10
- data/app/views/rodauth/otp_unlock_not_available.html.erb +7 -7
- data/app/views/rodauth/recovery_auth.html.erb +12 -4
- data/app/views/rodauth/recovery_codes.html.erb +12 -4
- data/app/views/rodauth/remember.html.erb +7 -7
- data/app/views/rodauth/reset_password.html.erb +23 -7
- data/app/views/rodauth/reset_password_request.html.erb +14 -10
- data/app/views/rodauth/sms_auth.html.erb +13 -4
- data/app/views/rodauth/sms_confirm.html.erb +13 -4
- data/app/views/rodauth/sms_disable.html.erb +12 -4
- data/app/views/rodauth/sms_request.html.erb +1 -1
- data/app/views/rodauth/sms_setup.html.erb +23 -7
- data/app/views/rodauth/two_factor_auth.html.erb +2 -2
- data/app/views/rodauth/two_factor_disable.html.erb +12 -4
- data/app/views/rodauth/two_factor_manage.html.erb +7 -7
- data/app/views/rodauth/unlock_account.html.erb +13 -5
- data/app/views/rodauth/unlock_account_request.html.erb +2 -2
- data/app/views/rodauth/verify_account.html.erb +25 -7
- data/app/views/rodauth/verify_account_resend.html.erb +14 -10
- data/app/views/rodauth/verify_login_change.html.erb +1 -1
- data/app/views/rodauth/webauthn_auth.html.erb +1 -1
- data/app/views/rodauth/webauthn_remove.html.erb +18 -8
- data/app/views/rodauth/webauthn_setup.html.erb +12 -4
- data/docs/.vitepress/config.ts +15 -26
- data/docs/.vitepress/theme/custom.css +388 -29
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/tutorial/02-first-resource.md +9 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +2 -2
- data/docs/getting-started/tutorial/07-author-portal.md +191 -0
- data/docs/getting-started/tutorial/{07-customizing-ui.md → 08-customizing-ui.md} +7 -7
- data/docs/getting-started/tutorial/index.md +5 -2
- data/docs/guides/authorization.md +33 -0
- data/docs/guides/creating-packages.md +12 -16
- data/docs/guides/custom-actions.md +36 -0
- data/docs/guides/search-filtering.md +121 -42
- data/docs/guides/theming.md +232 -36
- data/docs/index.md +203 -57
- data/docs/public/og-image.png +0 -0
- data/docs/reference/controller/index.md +14 -16
- data/docs/reference/definition/actions.md +38 -3
- data/docs/reference/definition/fields.md +3 -3
- data/docs/reference/definition/index.md +2 -2
- data/docs/reference/generators/index.md +0 -1
- data/docs/reference/interaction/index.md +14 -10
- data/docs/reference/model/index.md +0 -1
- data/docs/reference/portal/index.md +13 -27
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/pkg/portal/portal_generator.rb +0 -2
- data/lib/generators/pu/pkg/portal/templates/app/views/package/dashboard/index.html.erb +28 -72
- data/lib/plutonium/action/interactive.rb +2 -2
- data/lib/plutonium/core/controller.rb +2 -1
- data/lib/plutonium/definition/actions.rb +2 -2
- data/lib/plutonium/lib/deep_freezer.rb +3 -7
- data/lib/plutonium/query/filter.rb +14 -0
- data/lib/plutonium/query/filters/association.rb +49 -0
- data/lib/plutonium/query/filters/boolean.rb +35 -0
- data/lib/plutonium/query/filters/date.rb +97 -0
- data/lib/plutonium/query/filters/date_range.rb +58 -0
- data/lib/plutonium/query/filters/select.rb +55 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +24 -6
- data/lib/plutonium/resource/controllers/interactive_actions.rb +76 -58
- data/lib/plutonium/resource/controllers/queryable.rb +4 -2
- data/lib/plutonium/resource/query_object.rb +1 -1
- data/lib/plutonium/ui/action_button.rb +23 -65
- data/lib/plutonium/ui/actions_dropdown.rb +103 -0
- data/lib/plutonium/ui/block.rb +1 -1
- data/lib/plutonium/ui/breadcrumbs.rb +12 -19
- data/lib/plutonium/ui/color_mode_selector.rb +1 -1
- data/lib/plutonium/ui/component/kit.rb +6 -0
- data/lib/plutonium/ui/component_classes.rb +102 -0
- data/lib/plutonium/ui/display/base.rb +15 -0
- data/lib/plutonium/ui/display/components/attachment.rb +6 -5
- data/lib/plutonium/ui/display/components/boolean.rb +23 -0
- data/lib/plutonium/ui/display/components/color.rb +23 -0
- data/lib/plutonium/ui/display/resource.rb +1 -1
- data/lib/plutonium/ui/display/theme.rb +29 -15
- data/lib/plutonium/ui/empty_card.rb +3 -3
- data/lib/plutonium/ui/form/base.rb +20 -0
- data/lib/plutonium/ui/form/components/key_value_store.rb +11 -11
- data/lib/plutonium/ui/form/components/resource_select.rb +31 -0
- data/lib/plutonium/ui/form/components/secure_association.rb +1 -2
- data/lib/plutonium/ui/form/components/uppy.rb +5 -4
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +4 -4
- data/lib/plutonium/ui/form/interaction.rb +17 -1
- data/lib/plutonium/ui/form/query.rb +133 -80
- data/lib/plutonium/ui/form/theme.rb +50 -35
- data/lib/plutonium/ui/frame_navigator_panel.rb +2 -2
- data/lib/plutonium/ui/layout/base.rb +1 -1
- data/lib/plutonium/ui/layout/header.rb +4 -7
- data/lib/plutonium/ui/layout/rodauth_layout.rb +7 -7
- data/lib/plutonium/ui/layout/sidebar.rb +1 -1
- data/lib/plutonium/ui/nav_grid_menu.rb +7 -6
- data/lib/plutonium/ui/nav_user.rb +9 -8
- data/lib/plutonium/ui/page/interactive_action.rb +5 -5
- data/lib/plutonium/ui/page_header.rb +29 -10
- data/lib/plutonium/ui/panel.rb +4 -4
- data/lib/plutonium/ui/sidebar_menu.rb +8 -8
- data/lib/plutonium/ui/skeleton_table.rb +7 -8
- data/lib/plutonium/ui/tab_list.rb +5 -5
- data/lib/plutonium/ui/table/base.rb +3 -0
- data/lib/plutonium/ui/table/components/attachment.rb +4 -3
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +82 -0
- data/lib/plutonium/ui/table/components/pagy_info.rb +2 -2
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +13 -8
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +101 -0
- data/lib/plutonium/ui/table/components/scopes_bar.rb +2 -2
- data/lib/plutonium/ui/table/components/selection_column.rb +100 -0
- data/lib/plutonium/ui/table/display_theme.rb +6 -6
- data/lib/plutonium/ui/table/resource.rb +93 -52
- data/lib/plutonium/ui/table/theme.rb +28 -15
- data/lib/plutonium/version.rb +1 -1
- data/package.json +2 -2
- data/plutonium.gemspec +5 -4
- data/src/css/components.css +471 -0
- data/src/css/intl_tel_input.css +2 -2
- data/src/css/plutonium.css +2 -0
- data/src/css/tokens.css +149 -0
- data/src/js/controllers/bulk_actions_controller.js +109 -0
- data/src/js/controllers/filter_panel_controller.js +35 -0
- data/src/js/controllers/register_controllers.js +5 -1
- data/src/js/controllers/resource_drop_down_controller.js +25 -1
- data/src/js/controllers/slim_select_controller.js +6 -2
- data/src/js/turbo/turbo_actions.js +1 -1
- metadata +52 -39
- data/.claude/skills/definition-query/SKILL.md +0 -334
- data/docs/concepts/architecture.md +0 -226
- data/docs/concepts/auto-detection.md +0 -254
- data/docs/concepts/index.md +0 -61
- data/docs/concepts/packages-portals.md +0 -304
- data/docs/concepts/resources.md +0 -224
- data/docs/cookbook/blog.md +0 -411
- data/docs/cookbook/index.md +0 -289
- data/docs/cookbook/saas.md +0 -481
- data/docs/public/CLAUDE.md +0 -578
- data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +0 -5
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: definition-query
|
|
3
|
-
description: Configure search, filters, scopes, and sorting for Plutonium resources
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Definition Query
|
|
7
|
-
|
|
8
|
-
Configure how users can search, filter, and sort resource collections.
|
|
9
|
-
|
|
10
|
-
## Overview
|
|
11
|
-
|
|
12
|
-
```ruby
|
|
13
|
-
class PostDefinition < ResourceDefinition
|
|
14
|
-
# Search - global text search
|
|
15
|
-
search do |scope, query|
|
|
16
|
-
scope.where("title ILIKE ?", "%#{query}%")
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Filters - sidebar filter inputs
|
|
20
|
-
filter :status, with: Plutonium::Query::Filters::Text, predicate: :eq
|
|
21
|
-
|
|
22
|
-
# Scopes - quick filter buttons
|
|
23
|
-
scope :published
|
|
24
|
-
scope :draft
|
|
25
|
-
|
|
26
|
-
# Sorting - sortable columns
|
|
27
|
-
sort :title
|
|
28
|
-
sort :created_at
|
|
29
|
-
|
|
30
|
-
# Default sort
|
|
31
|
-
default_sort :created_at, :desc
|
|
32
|
-
end
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Search
|
|
36
|
-
|
|
37
|
-
Define global search across fields:
|
|
38
|
-
|
|
39
|
-
```ruby
|
|
40
|
-
# Single field
|
|
41
|
-
search do |scope, query|
|
|
42
|
-
scope.where("title ILIKE ?", "%#{query}%")
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Multiple fields
|
|
46
|
-
search do |scope, query|
|
|
47
|
-
scope.where(
|
|
48
|
-
"title ILIKE :q OR content ILIKE :q OR author_name ILIKE :q",
|
|
49
|
-
q: "%#{query}%"
|
|
50
|
-
)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# With associations
|
|
54
|
-
search do |scope, query|
|
|
55
|
-
scope.joins(:author).where(
|
|
56
|
-
"posts.title ILIKE :q OR users.name ILIKE :q",
|
|
57
|
-
q: "%#{query}%"
|
|
58
|
-
).distinct
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Split search terms
|
|
62
|
-
search do |scope, query|
|
|
63
|
-
terms = query.split(/\s+/)
|
|
64
|
-
terms.reduce(scope) do |current_scope, term|
|
|
65
|
-
current_scope.where("title ILIKE ?", "%#{term}%")
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## Filters
|
|
71
|
-
|
|
72
|
-
Currently Plutonium provides the **Text filter** with various predicates.
|
|
73
|
-
|
|
74
|
-
### Text Filter Predicates
|
|
75
|
-
|
|
76
|
-
```ruby
|
|
77
|
-
# Exact match
|
|
78
|
-
filter :status, with: Plutonium::Query::Filters::Text, predicate: :eq
|
|
79
|
-
|
|
80
|
-
# Not equal
|
|
81
|
-
filter :status, with: Plutonium::Query::Filters::Text, predicate: :not_eq
|
|
82
|
-
|
|
83
|
-
# Contains (LIKE %value%)
|
|
84
|
-
filter :title, with: Plutonium::Query::Filters::Text, predicate: :contains
|
|
85
|
-
|
|
86
|
-
# Not contains
|
|
87
|
-
filter :title, with: Plutonium::Query::Filters::Text, predicate: :not_contains
|
|
88
|
-
|
|
89
|
-
# Starts with (LIKE value%)
|
|
90
|
-
filter :slug, with: Plutonium::Query::Filters::Text, predicate: :starts_with
|
|
91
|
-
|
|
92
|
-
# Ends with (LIKE %value)
|
|
93
|
-
filter :email, with: Plutonium::Query::Filters::Text, predicate: :ends_with
|
|
94
|
-
|
|
95
|
-
# Pattern match (* becomes %)
|
|
96
|
-
filter :title, with: Plutonium::Query::Filters::Text, predicate: :matches
|
|
97
|
-
|
|
98
|
-
# Not matching pattern
|
|
99
|
-
filter :title, with: Plutonium::Query::Filters::Text, predicate: :not_matches
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Custom Filter with Lambda
|
|
103
|
-
|
|
104
|
-
```ruby
|
|
105
|
-
filter :published, with: ->(scope, value) {
|
|
106
|
-
value == "true" ? scope.where.not(published_at: nil) : scope.where(published_at: nil)
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### Custom Filter Class
|
|
111
|
-
|
|
112
|
-
```ruby
|
|
113
|
-
# Define custom filter
|
|
114
|
-
class DateRangeFilter < Plutonium::Query::Filter
|
|
115
|
-
def apply(scope, start_date: nil, end_date: nil)
|
|
116
|
-
scope = scope.where("#{key} >= ?", start_date.beginning_of_day) if start_date.present?
|
|
117
|
-
scope = scope.where("#{key} <= ?", end_date.end_of_day) if end_date.present?
|
|
118
|
-
scope
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def customize_inputs
|
|
122
|
-
input :start_date, as: :date
|
|
123
|
-
input :end_date, as: :date
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Use in definition
|
|
128
|
-
filter :created_at, with: DateRangeFilter
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Scopes
|
|
132
|
-
|
|
133
|
-
Scopes appear as quick filter buttons. They reference model scopes.
|
|
134
|
-
|
|
135
|
-
### Basic Usage
|
|
136
|
-
|
|
137
|
-
```ruby
|
|
138
|
-
class PostDefinition < ResourceDefinition
|
|
139
|
-
scope :published # Uses Post.published
|
|
140
|
-
scope :draft # Uses Post.draft
|
|
141
|
-
scope :featured # Uses Post.featured
|
|
142
|
-
end
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### Inline Scope
|
|
146
|
-
|
|
147
|
-
Use block syntax with the scope passed as an argument:
|
|
148
|
-
|
|
149
|
-
```ruby
|
|
150
|
-
scope(:recent) { |scope| scope.where('created_at > ?', 1.week.ago) }
|
|
151
|
-
scope(:this_month) { |scope| scope.where(created_at: Time.current.all_month) }
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### With Controller Context
|
|
155
|
-
|
|
156
|
-
Inline scopes have access to controller context like `current_user`:
|
|
157
|
-
|
|
158
|
-
```ruby
|
|
159
|
-
scope(:mine) { |scope| scope.where(author: current_user) }
|
|
160
|
-
scope(:my_team) { |scope| scope.where(team: current_user.team) }
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
### Default Scope
|
|
164
|
-
|
|
165
|
-
Set a scope as default to apply it when no scope is explicitly selected:
|
|
166
|
-
|
|
167
|
-
```ruby
|
|
168
|
-
class PostDefinition < ResourceDefinition
|
|
169
|
-
scope :published, default: true # Applied by default
|
|
170
|
-
scope :draft
|
|
171
|
-
scope :archived
|
|
172
|
-
end
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
When a default scope is set:
|
|
176
|
-
- The default scope is applied on initial page load
|
|
177
|
-
- The default scope button is highlighted (not "All")
|
|
178
|
-
- Clicking "All" shows all records without any scope filter
|
|
179
|
-
- URL without scope param uses the default; URL with `?q[scope]=` uses "All"
|
|
180
|
-
|
|
181
|
-
## Sorting
|
|
182
|
-
|
|
183
|
-
### Basic Sorting
|
|
184
|
-
|
|
185
|
-
```ruby
|
|
186
|
-
sort :title
|
|
187
|
-
sort :created_at
|
|
188
|
-
sort :view_count
|
|
189
|
-
|
|
190
|
-
# Multiple at once
|
|
191
|
-
sorts :title, :created_at, :view_count
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### Default Sort
|
|
195
|
-
|
|
196
|
-
```ruby
|
|
197
|
-
# Field and direction
|
|
198
|
-
default_sort :created_at, :desc
|
|
199
|
-
default_sort :title, :asc
|
|
200
|
-
|
|
201
|
-
# Complex sorting with block
|
|
202
|
-
default_sort { |scope| scope.order(featured: :desc, created_at: :desc) }
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
**Note:** Default sort only applies when no sort params are provided.
|
|
206
|
-
|
|
207
|
-
## URL Parameters
|
|
208
|
-
|
|
209
|
-
Query parameters are structured under `q`:
|
|
210
|
-
|
|
211
|
-
```
|
|
212
|
-
/posts?q[search]=rails
|
|
213
|
-
/posts?q[status][query]=published
|
|
214
|
-
/posts?q[scope]=recent
|
|
215
|
-
/posts?q[sort_fields][]=created_at&q[sort_directions][created_at]=desc
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
Combined:
|
|
219
|
-
```
|
|
220
|
-
/posts?q[search]=rails&q[scope]=published&q[sort_fields][]=created_at&q[sort_directions][created_at]=desc
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## Common Patterns
|
|
224
|
-
|
|
225
|
-
### Status Filter
|
|
226
|
-
|
|
227
|
-
```ruby
|
|
228
|
-
class PostDefinition < ResourceDefinition
|
|
229
|
-
filter :status, with: Plutonium::Query::Filters::Text, predicate: :eq
|
|
230
|
-
|
|
231
|
-
scope :draft
|
|
232
|
-
scope :published
|
|
233
|
-
scope :archived
|
|
234
|
-
end
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### Date-Based Scopes
|
|
238
|
-
|
|
239
|
-
```ruby
|
|
240
|
-
class PostDefinition < ResourceDefinition
|
|
241
|
-
scope(:today) { |scope| scope.where(created_at: Time.current.all_day) }
|
|
242
|
-
scope(:this_week) { |scope| scope.where(created_at: Time.current.all_week) }
|
|
243
|
-
scope(:this_month) { |scope| scope.where(created_at: Time.current.all_month) }
|
|
244
|
-
end
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
### Archive State Scopes
|
|
248
|
-
|
|
249
|
-
```ruby
|
|
250
|
-
class PostDefinition < ResourceDefinition
|
|
251
|
-
scope :active
|
|
252
|
-
scope :archived
|
|
253
|
-
|
|
254
|
-
# Default to showing only active
|
|
255
|
-
default_sort { |scope| scope.active.order(created_at: :desc) }
|
|
256
|
-
end
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
### Full-Text Search with pg_search
|
|
260
|
-
|
|
261
|
-
```ruby
|
|
262
|
-
# Model
|
|
263
|
-
class Post < ApplicationRecord
|
|
264
|
-
include PgSearch::Model
|
|
265
|
-
pg_search_scope :search_content, against: [:title, :content]
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
# Definition
|
|
269
|
-
class PostDefinition < ResourceDefinition
|
|
270
|
-
search do |scope, query|
|
|
271
|
-
scope.search_content(query)
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
### Association Filtering
|
|
277
|
-
|
|
278
|
-
```ruby
|
|
279
|
-
class PostDefinition < ResourceDefinition
|
|
280
|
-
filter :author_name, with: Plutonium::Query::Filters::Text, predicate: :contains
|
|
281
|
-
|
|
282
|
-
search do |scope, query|
|
|
283
|
-
scope.joins(:author).where(
|
|
284
|
-
"posts.title ILIKE :q OR users.name ILIKE :q",
|
|
285
|
-
q: "%#{query}%"
|
|
286
|
-
).distinct
|
|
287
|
-
end
|
|
288
|
-
end
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
## Complete Example
|
|
292
|
-
|
|
293
|
-
```ruby
|
|
294
|
-
class PostDefinition < ResourceDefinition
|
|
295
|
-
# Full-text search
|
|
296
|
-
search do |scope, query|
|
|
297
|
-
scope.where(
|
|
298
|
-
"title ILIKE :q OR content ILIKE :q",
|
|
299
|
-
q: "%#{query}%"
|
|
300
|
-
)
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
# Filters
|
|
304
|
-
filter :status, with: Plutonium::Query::Filters::Text, predicate: :eq
|
|
305
|
-
filter :category, with: Plutonium::Query::Filters::Text, predicate: :eq
|
|
306
|
-
filter :title, with: Plutonium::Query::Filters::Text, predicate: :contains
|
|
307
|
-
|
|
308
|
-
# Quick scopes (reference model scopes)
|
|
309
|
-
scope :published
|
|
310
|
-
scope :draft
|
|
311
|
-
scope :featured
|
|
312
|
-
scope(:recent) { |scope| scope.where('created_at > ?', 1.week.ago) }
|
|
313
|
-
|
|
314
|
-
# Sortable columns
|
|
315
|
-
sorts :title, :created_at, :view_count, :published_at
|
|
316
|
-
|
|
317
|
-
# Default: newest first
|
|
318
|
-
default_sort :created_at, :desc
|
|
319
|
-
end
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
## Performance Tips
|
|
323
|
-
|
|
324
|
-
1. **Add indexes** for filtered/sorted columns
|
|
325
|
-
2. **Use `.distinct`** when joining associations in search
|
|
326
|
-
3. **Consider `pg_search`** for complex full-text search
|
|
327
|
-
4. **Limit search fields** to indexed columns
|
|
328
|
-
5. **Use scopes** instead of filters for common queries
|
|
329
|
-
|
|
330
|
-
## Related Skills
|
|
331
|
-
|
|
332
|
-
- `definition` - Overview and structure
|
|
333
|
-
- `definition-fields` - Fields, inputs, displays
|
|
334
|
-
- `definition-actions` - Actions and interactions
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
# Architecture
|
|
2
|
-
|
|
3
|
-
Plutonium follows a layered architecture where each layer has a specific responsibility. This separation makes applications easier to understand, test, and maintain.
|
|
4
|
-
|
|
5
|
-
## The Four Layers
|
|
6
|
-
|
|
7
|
-
### 1. Model Layer
|
|
8
|
-
|
|
9
|
-
The Model layer handles **data and business rules**.
|
|
10
|
-
|
|
11
|
-
```ruby
|
|
12
|
-
class Post < ResourceRecord
|
|
13
|
-
belongs_to :user
|
|
14
|
-
has_many :comments
|
|
15
|
-
|
|
16
|
-
validates :title, presence: true
|
|
17
|
-
validates :body, presence: true
|
|
18
|
-
|
|
19
|
-
scope :published, -> { where(published: true) }
|
|
20
|
-
end
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Responsibilities:
|
|
24
|
-
- Database schema and migrations
|
|
25
|
-
- Validations
|
|
26
|
-
- Associations
|
|
27
|
-
- Scopes and queries
|
|
28
|
-
- Core business logic
|
|
29
|
-
|
|
30
|
-
### 2. Definition Layer
|
|
31
|
-
|
|
32
|
-
The Definition layer controls **how resources render**.
|
|
33
|
-
|
|
34
|
-
```ruby
|
|
35
|
-
class PostDefinition < Plutonium::Resource::Definition
|
|
36
|
-
# Fields shown in forms
|
|
37
|
-
field :title
|
|
38
|
-
field :body, as: :rich_text
|
|
39
|
-
field :published, as: :switch
|
|
40
|
-
|
|
41
|
-
# Columns shown in tables
|
|
42
|
-
column :title, sortable: true
|
|
43
|
-
column :published
|
|
44
|
-
column :created_at
|
|
45
|
-
|
|
46
|
-
# Custom actions
|
|
47
|
-
action :publish, interaction: PublishPost
|
|
48
|
-
end
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Responsibilities:
|
|
52
|
-
- Which fields appear in forms
|
|
53
|
-
- How fields are rendered (input types)
|
|
54
|
-
- Table column configuration
|
|
55
|
-
- Search and filtering
|
|
56
|
-
- Custom actions
|
|
57
|
-
- Field groups and layout
|
|
58
|
-
|
|
59
|
-
### 3. Policy Layer
|
|
60
|
-
|
|
61
|
-
The Policy layer controls **authorization**.
|
|
62
|
-
|
|
63
|
-
```ruby
|
|
64
|
-
class PostPolicy < Plutonium::Resource::Policy
|
|
65
|
-
def read?
|
|
66
|
-
record.published? || owner?
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def update?
|
|
70
|
-
owner?
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def permitted_attributes_for_update
|
|
74
|
-
[:title, :body, :published]
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def relation_scope(relation)
|
|
78
|
-
relation.where(published: true).or(relation.where(user: user))
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Responsibilities:
|
|
84
|
-
- Action permissions (can user perform action?)
|
|
85
|
-
- Attribute permissions (which fields can user see/modify?)
|
|
86
|
-
- Collection scoping (which records can user access?)
|
|
87
|
-
- Multi-tenancy isolation
|
|
88
|
-
|
|
89
|
-
### 4. Controller Layer
|
|
90
|
-
|
|
91
|
-
The Controller layer handles **HTTP requests**.
|
|
92
|
-
|
|
93
|
-
```ruby
|
|
94
|
-
class PostsController < Plutonium::Resource::Controller
|
|
95
|
-
private
|
|
96
|
-
|
|
97
|
-
def build_resource
|
|
98
|
-
super.tap do |post|
|
|
99
|
-
post.user = current_user
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def after_create_success
|
|
104
|
-
notify_subscribers(@resource)
|
|
105
|
-
super
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
Responsibilities:
|
|
111
|
-
- CRUD actions
|
|
112
|
-
- Request/response handling
|
|
113
|
-
- Resource building hooks
|
|
114
|
-
- Redirects and rendering
|
|
115
|
-
- Format handling (HTML, JSON, etc.)
|
|
116
|
-
|
|
117
|
-
## Request Flow
|
|
118
|
-
|
|
119
|
-
When a request comes in, it flows through the layers:
|
|
120
|
-
|
|
121
|
-
```
|
|
122
|
-
1. REQUEST arrives at Portal
|
|
123
|
-
↓
|
|
124
|
-
2. CONTROLLER receives request
|
|
125
|
-
↓
|
|
126
|
-
3. POLICY checks authorization
|
|
127
|
-
↓
|
|
128
|
-
4. DEFINITION determines rendering
|
|
129
|
-
↓
|
|
130
|
-
5. MODEL provides data
|
|
131
|
-
↓
|
|
132
|
-
6. RESPONSE rendered and returned
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### Example: Viewing a Post
|
|
136
|
-
|
|
137
|
-
```
|
|
138
|
-
GET /admin/posts/1
|
|
139
|
-
|
|
140
|
-
1. AdminPortal routes to PostsController#show
|
|
141
|
-
2. PostsController loads Post.find(1)
|
|
142
|
-
3. PostPolicy#read? checks if user can view
|
|
143
|
-
4. PostDefinition provides field configuration
|
|
144
|
-
5. UI renders the post display
|
|
145
|
-
6. HTML response returned
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### Example: Creating a Post
|
|
149
|
-
|
|
150
|
-
```
|
|
151
|
-
POST /admin/posts
|
|
152
|
-
|
|
153
|
-
1. AdminPortal routes to PostsController#create
|
|
154
|
-
2. PostsController builds new Post
|
|
155
|
-
3. PostPolicy#create? checks if user can create
|
|
156
|
-
4. PostPolicy#permitted_attributes_for_create filters params
|
|
157
|
-
5. Post validates and saves
|
|
158
|
-
6. Redirect to show page
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
## Layer Interaction
|
|
162
|
-
|
|
163
|
-
Layers communicate through well-defined interfaces:
|
|
164
|
-
|
|
165
|
-
```ruby
|
|
166
|
-
# Controller asks Policy
|
|
167
|
-
policy = policy_for(resource)
|
|
168
|
-
policy.authorize!(:update)
|
|
169
|
-
|
|
170
|
-
# Controller uses Definition
|
|
171
|
-
definition = definition_for(resource)
|
|
172
|
-
definition.fields_for(:form)
|
|
173
|
-
|
|
174
|
-
# Policy uses Model
|
|
175
|
-
def owner?
|
|
176
|
-
record.user_id == user.id
|
|
177
|
-
end
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
## Customization Points
|
|
181
|
-
|
|
182
|
-
Each layer provides hooks for customization:
|
|
183
|
-
|
|
184
|
-
### Model Hooks
|
|
185
|
-
- Callbacks (before_save, after_create, etc.)
|
|
186
|
-
- Custom methods
|
|
187
|
-
- Scopes
|
|
188
|
-
|
|
189
|
-
### Definition Hooks
|
|
190
|
-
- Field configuration
|
|
191
|
-
- Custom renderers
|
|
192
|
-
- Conditional display
|
|
193
|
-
|
|
194
|
-
### Policy Hooks
|
|
195
|
-
- Custom permission methods
|
|
196
|
-
- Attribute filtering
|
|
197
|
-
- Scope customization
|
|
198
|
-
|
|
199
|
-
### Controller Hooks
|
|
200
|
-
- `build_resource` - Customize resource initialization
|
|
201
|
-
- `before_action` - Standard Rails callbacks
|
|
202
|
-
- `after_*_success/failure` - Action result hooks
|
|
203
|
-
|
|
204
|
-
## Why This Architecture?
|
|
205
|
-
|
|
206
|
-
### 1. Separation of Concerns
|
|
207
|
-
Each layer has one job. Forms don't know about authorization. Policies don't know about rendering.
|
|
208
|
-
|
|
209
|
-
### 2. Testability
|
|
210
|
-
Each layer can be tested in isolation:
|
|
211
|
-
- Model specs test validations and queries
|
|
212
|
-
- Policy specs test authorization
|
|
213
|
-
- Definition specs test field configuration
|
|
214
|
-
- Controller specs test request handling
|
|
215
|
-
|
|
216
|
-
### 3. Reusability
|
|
217
|
-
Definitions and policies can be shared across portals. Models are independent of the UI.
|
|
218
|
-
|
|
219
|
-
### 4. Maintainability
|
|
220
|
-
Changes to authorization don't affect forms. UI changes don't affect data logic.
|
|
221
|
-
|
|
222
|
-
## Related Topics
|
|
223
|
-
|
|
224
|
-
- [Resources](./resources) - Understanding resource classes
|
|
225
|
-
- [Packages and Portals](./packages-portals) - Organizing your application
|
|
226
|
-
- [Auto-Detection](./auto-detection) - How defaults are determined
|