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.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/skill.md +53 -0
  3. data/.claude/skills/{assets → plutonium-assets}/SKILL.md +13 -8
  4. data/.claude/skills/{connect-resource → plutonium-connect-resource}/SKILL.md +1 -1
  5. data/.claude/skills/{controller → plutonium-controller}/SKILL.md +27 -13
  6. data/.claude/skills/{create-resource → plutonium-create-resource}/SKILL.md +1 -1
  7. data/.claude/skills/{definition → plutonium-definition}/SKILL.md +10 -10
  8. data/.claude/skills/{definition-actions → plutonium-definition-actions}/SKILL.md +34 -9
  9. data/.claude/skills/{definition-fields → plutonium-definition-fields}/SKILL.md +38 -10
  10. data/.claude/skills/plutonium-definition-query/SKILL.md +356 -0
  11. data/.claude/skills/{forms → plutonium-forms}/SKILL.md +6 -6
  12. data/.claude/skills/{installation → plutonium-installation}/SKILL.md +9 -9
  13. data/.claude/skills/{interaction → plutonium-interaction}/SKILL.md +20 -19
  14. data/.claude/skills/{model → plutonium-model}/SKILL.md +3 -3
  15. data/.claude/skills/{model-features → plutonium-model-features}/SKILL.md +3 -3
  16. data/.claude/skills/{nested-resources → plutonium-nested-resources}/SKILL.md +5 -5
  17. data/.claude/skills/{package → plutonium-package}/SKILL.md +7 -8
  18. data/.claude/skills/{policy → plutonium-policy}/SKILL.md +26 -4
  19. data/.claude/skills/{portal → plutonium-portal}/SKILL.md +33 -31
  20. data/.claude/skills/{resource → plutonium-resource}/SKILL.md +27 -27
  21. data/.claude/skills/{rodauth → plutonium-rodauth}/SKILL.md +5 -5
  22. data/.claude/skills/plutonium-theming/SKILL.md +424 -0
  23. data/.claude/skills/{views → plutonium-views}/SKILL.md +7 -7
  24. data/CHANGELOG.md +52 -0
  25. data/CLAUDE.md +215 -0
  26. data/CONTRIBUTING.md +72 -18
  27. data/README.md +100 -19
  28. data/app/assets/plutonium.css +1 -11
  29. data/app/assets/plutonium.js +1685 -1146
  30. data/app/assets/plutonium.js.map +4 -4
  31. data/app/assets/plutonium.min.js +70 -70
  32. data/app/assets/plutonium.min.js.map +4 -4
  33. data/app/views/resource/interactive_bulk_action.html.erb +1 -5
  34. data/app/views/rodauth/_email_auth_request_form.html.erb +1 -1
  35. data/app/views/rodauth/_login_form.html.erb +15 -55
  36. data/app/views/rodauth/_login_form_footer.html.erb +2 -2
  37. data/app/views/rodauth/_password_visibility.html.erb +2 -8
  38. data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
  39. data/app/views/rodauth/change_login.html.erb +36 -19
  40. data/app/views/rodauth/change_password.html.erb +34 -10
  41. data/app/views/rodauth/close_account.html.erb +12 -4
  42. data/app/views/rodauth/confirm_password.html.erb +19 -17
  43. data/app/views/rodauth/create_account.html.erb +30 -109
  44. data/app/views/rodauth/email_auth.html.erb +1 -1
  45. data/app/views/rodauth/logout.html.erb +4 -4
  46. data/app/views/rodauth/otp_auth.html.erb +13 -4
  47. data/app/views/rodauth/otp_disable.html.erb +12 -4
  48. data/app/views/rodauth/otp_setup.html.erb +29 -12
  49. data/app/views/rodauth/otp_unlock.html.erb +19 -10
  50. data/app/views/rodauth/otp_unlock_not_available.html.erb +7 -7
  51. data/app/views/rodauth/recovery_auth.html.erb +12 -4
  52. data/app/views/rodauth/recovery_codes.html.erb +12 -4
  53. data/app/views/rodauth/remember.html.erb +7 -7
  54. data/app/views/rodauth/reset_password.html.erb +23 -7
  55. data/app/views/rodauth/reset_password_request.html.erb +14 -10
  56. data/app/views/rodauth/sms_auth.html.erb +13 -4
  57. data/app/views/rodauth/sms_confirm.html.erb +13 -4
  58. data/app/views/rodauth/sms_disable.html.erb +12 -4
  59. data/app/views/rodauth/sms_request.html.erb +1 -1
  60. data/app/views/rodauth/sms_setup.html.erb +23 -7
  61. data/app/views/rodauth/two_factor_auth.html.erb +2 -2
  62. data/app/views/rodauth/two_factor_disable.html.erb +12 -4
  63. data/app/views/rodauth/two_factor_manage.html.erb +7 -7
  64. data/app/views/rodauth/unlock_account.html.erb +13 -5
  65. data/app/views/rodauth/unlock_account_request.html.erb +2 -2
  66. data/app/views/rodauth/verify_account.html.erb +25 -7
  67. data/app/views/rodauth/verify_account_resend.html.erb +14 -10
  68. data/app/views/rodauth/verify_login_change.html.erb +1 -1
  69. data/app/views/rodauth/webauthn_auth.html.erb +1 -1
  70. data/app/views/rodauth/webauthn_remove.html.erb +18 -8
  71. data/app/views/rodauth/webauthn_setup.html.erb +12 -4
  72. data/docs/.vitepress/config.ts +15 -26
  73. data/docs/.vitepress/theme/custom.css +388 -29
  74. data/docs/getting-started/index.md +1 -1
  75. data/docs/getting-started/tutorial/02-first-resource.md +9 -0
  76. data/docs/getting-started/tutorial/06-nested-resources.md +2 -2
  77. data/docs/getting-started/tutorial/07-author-portal.md +191 -0
  78. data/docs/getting-started/tutorial/{07-customizing-ui.md → 08-customizing-ui.md} +7 -7
  79. data/docs/getting-started/tutorial/index.md +5 -2
  80. data/docs/guides/authorization.md +33 -0
  81. data/docs/guides/creating-packages.md +12 -16
  82. data/docs/guides/custom-actions.md +36 -0
  83. data/docs/guides/search-filtering.md +121 -42
  84. data/docs/guides/theming.md +232 -36
  85. data/docs/index.md +203 -57
  86. data/docs/public/og-image.png +0 -0
  87. data/docs/reference/controller/index.md +14 -16
  88. data/docs/reference/definition/actions.md +38 -3
  89. data/docs/reference/definition/fields.md +3 -3
  90. data/docs/reference/definition/index.md +2 -2
  91. data/docs/reference/generators/index.md +0 -1
  92. data/docs/reference/interaction/index.md +14 -10
  93. data/docs/reference/model/index.md +0 -1
  94. data/docs/reference/portal/index.md +13 -27
  95. data/gemfiles/rails_7.gemfile.lock +1 -1
  96. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  97. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  98. data/lib/generators/pu/pkg/portal/portal_generator.rb +0 -2
  99. data/lib/generators/pu/pkg/portal/templates/app/views/package/dashboard/index.html.erb +28 -72
  100. data/lib/plutonium/action/interactive.rb +2 -2
  101. data/lib/plutonium/core/controller.rb +2 -1
  102. data/lib/plutonium/definition/actions.rb +2 -2
  103. data/lib/plutonium/lib/deep_freezer.rb +3 -7
  104. data/lib/plutonium/query/filter.rb +14 -0
  105. data/lib/plutonium/query/filters/association.rb +49 -0
  106. data/lib/plutonium/query/filters/boolean.rb +35 -0
  107. data/lib/plutonium/query/filters/date.rb +97 -0
  108. data/lib/plutonium/query/filters/date_range.rb +58 -0
  109. data/lib/plutonium/query/filters/select.rb +55 -0
  110. data/lib/plutonium/resource/controllers/crud_actions.rb +24 -6
  111. data/lib/plutonium/resource/controllers/interactive_actions.rb +76 -58
  112. data/lib/plutonium/resource/controllers/queryable.rb +4 -2
  113. data/lib/plutonium/resource/query_object.rb +1 -1
  114. data/lib/plutonium/ui/action_button.rb +23 -65
  115. data/lib/plutonium/ui/actions_dropdown.rb +103 -0
  116. data/lib/plutonium/ui/block.rb +1 -1
  117. data/lib/plutonium/ui/breadcrumbs.rb +12 -19
  118. data/lib/plutonium/ui/color_mode_selector.rb +1 -1
  119. data/lib/plutonium/ui/component/kit.rb +6 -0
  120. data/lib/plutonium/ui/component_classes.rb +102 -0
  121. data/lib/plutonium/ui/display/base.rb +15 -0
  122. data/lib/plutonium/ui/display/components/attachment.rb +6 -5
  123. data/lib/plutonium/ui/display/components/boolean.rb +23 -0
  124. data/lib/plutonium/ui/display/components/color.rb +23 -0
  125. data/lib/plutonium/ui/display/resource.rb +1 -1
  126. data/lib/plutonium/ui/display/theme.rb +29 -15
  127. data/lib/plutonium/ui/empty_card.rb +3 -3
  128. data/lib/plutonium/ui/form/base.rb +20 -0
  129. data/lib/plutonium/ui/form/components/key_value_store.rb +11 -11
  130. data/lib/plutonium/ui/form/components/resource_select.rb +31 -0
  131. data/lib/plutonium/ui/form/components/secure_association.rb +1 -2
  132. data/lib/plutonium/ui/form/components/uppy.rb +5 -4
  133. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +4 -4
  134. data/lib/plutonium/ui/form/interaction.rb +17 -1
  135. data/lib/plutonium/ui/form/query.rb +133 -80
  136. data/lib/plutonium/ui/form/theme.rb +50 -35
  137. data/lib/plutonium/ui/frame_navigator_panel.rb +2 -2
  138. data/lib/plutonium/ui/layout/base.rb +1 -1
  139. data/lib/plutonium/ui/layout/header.rb +4 -7
  140. data/lib/plutonium/ui/layout/rodauth_layout.rb +7 -7
  141. data/lib/plutonium/ui/layout/sidebar.rb +1 -1
  142. data/lib/plutonium/ui/nav_grid_menu.rb +7 -6
  143. data/lib/plutonium/ui/nav_user.rb +9 -8
  144. data/lib/plutonium/ui/page/interactive_action.rb +5 -5
  145. data/lib/plutonium/ui/page_header.rb +29 -10
  146. data/lib/plutonium/ui/panel.rb +4 -4
  147. data/lib/plutonium/ui/sidebar_menu.rb +8 -8
  148. data/lib/plutonium/ui/skeleton_table.rb +7 -8
  149. data/lib/plutonium/ui/tab_list.rb +5 -5
  150. data/lib/plutonium/ui/table/base.rb +3 -0
  151. data/lib/plutonium/ui/table/components/attachment.rb +4 -3
  152. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +82 -0
  153. data/lib/plutonium/ui/table/components/pagy_info.rb +2 -2
  154. data/lib/plutonium/ui/table/components/pagy_pagination.rb +13 -8
  155. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +101 -0
  156. data/lib/plutonium/ui/table/components/scopes_bar.rb +2 -2
  157. data/lib/plutonium/ui/table/components/selection_column.rb +100 -0
  158. data/lib/plutonium/ui/table/display_theme.rb +6 -6
  159. data/lib/plutonium/ui/table/resource.rb +93 -52
  160. data/lib/plutonium/ui/table/theme.rb +28 -15
  161. data/lib/plutonium/version.rb +1 -1
  162. data/package.json +2 -2
  163. data/plutonium.gemspec +5 -4
  164. data/src/css/components.css +471 -0
  165. data/src/css/intl_tel_input.css +2 -2
  166. data/src/css/plutonium.css +2 -0
  167. data/src/css/tokens.css +149 -0
  168. data/src/js/controllers/bulk_actions_controller.js +109 -0
  169. data/src/js/controllers/filter_panel_controller.js +35 -0
  170. data/src/js/controllers/register_controllers.js +5 -1
  171. data/src/js/controllers/resource_drop_down_controller.js +25 -1
  172. data/src/js/controllers/slim_select_controller.js +6 -2
  173. data/src/js/turbo/turbo_actions.js +1 -1
  174. metadata +52 -39
  175. data/.claude/skills/definition-query/SKILL.md +0 -334
  176. data/docs/concepts/architecture.md +0 -226
  177. data/docs/concepts/auto-detection.md +0 -254
  178. data/docs/concepts/index.md +0 -61
  179. data/docs/concepts/packages-portals.md +0 -304
  180. data/docs/concepts/resources.md +0 -224
  181. data/docs/cookbook/blog.md +0 -411
  182. data/docs/cookbook/index.md +0 -289
  183. data/docs/cookbook/saas.md +0 -481
  184. data/docs/public/CLAUDE.md +0 -578
  185. 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