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,224 +0,0 @@
1
- # Resources
2
-
3
- In Plutonium, a **resource** is a complete unit that represents a domain concept. Unlike plain Rails models, a Plutonium resource combines data, presentation, and authorization.
4
-
5
- ## What Makes a Resource?
6
-
7
- A resource consists of four parts:
8
-
9
- | Component | Purpose | Example |
10
- |-----------|---------|---------|
11
- | **Model** | Data structure and validation | `Post` |
12
- | **Definition** | How it renders | `PostDefinition` |
13
- | **Policy** | Who can do what | `PostPolicy` |
14
- | **Controller** | HTTP handling | `PostsController` |
15
-
16
- ## Resource Models
17
-
18
- Resource models inherit from `ResourceRecord`:
19
-
20
- ```ruby
21
- class Post < ResourceRecord
22
- belongs_to :user
23
- has_many :comments
24
-
25
- validates :title, presence: true
26
- end
27
- ```
28
-
29
- This base class adds:
30
- - Automatic field introspection
31
- - Association detection
32
- - Integration with definitions and policies
33
-
34
- ## Creating Resources
35
-
36
- ### Using the Generator
37
-
38
- The fastest way to create a resource:
39
-
40
- ```bash
41
- rails generate pu:res:scaffold Post title:string body:text published:boolean
42
- ```
43
-
44
- This generates:
45
- - Model with attributes and validations
46
- - Definition with default configuration
47
- - Policy with standard permissions
48
- - Migration
49
-
50
- ### Manual Creation
51
-
52
- You can also create resources manually:
53
-
54
- ```ruby
55
- # app/models/post.rb
56
- class Post < ResourceRecord
57
- validates :title, presence: true
58
- end
59
-
60
- # app/definitions/post_definition.rb
61
- class PostDefinition < Plutonium::Resource::Definition
62
- end
63
-
64
- # app/policies/post_policy.rb
65
- class PostPolicy < Plutonium::Resource::Policy
66
- end
67
- ```
68
-
69
- ## Resource vs Model
70
-
71
- | Aspect | Plain Model | Resource |
72
- |--------|-------------|----------|
73
- | Inheritance | `ApplicationRecord` | `ResourceRecord` |
74
- | Fields | Manual configuration | Auto-detected |
75
- | Authorization | Separate concern | Integrated via Policy |
76
- | UI | Manual forms/views | Auto-generated |
77
- | CRUD | Write manually | Generated |
78
-
79
- ## Resource Discovery
80
-
81
- Plutonium automatically discovers resources based on naming conventions:
82
-
83
- ```
84
- Post → PostDefinition, PostPolicy, PostsController
85
- Blogging::Post → Blogging::PostDefinition, Blogging::PostPolicy
86
- ```
87
-
88
- ## Field Introspection
89
-
90
- Resources automatically detect their fields from the database schema:
91
-
92
- ```ruby
93
- # Given this schema:
94
- create_table :posts do |t|
95
- t.string :title, null: false
96
- t.text :body
97
- t.boolean :published, default: false
98
- t.belongs_to :user
99
- t.timestamps
100
- end
101
-
102
- # Plutonium auto-detects:
103
- # - title: string input, required
104
- # - body: textarea
105
- # - published: checkbox
106
- # - user: association select
107
- # - created_at, updated_at: datetime displays
108
- ```
109
-
110
- ## Resource Registration
111
-
112
- Resources must be registered with a portal to be accessible:
113
-
114
- ```bash
115
- rails generate pu:res:conn Post --dest=admin_portal
116
- ```
117
-
118
- Or manually in routes:
119
-
120
- ```ruby
121
- # packages/admin_portal/config/routes.rb
122
- AdminPortal::Engine.routes.draw do
123
- register_resource ::Post
124
- end
125
- ```
126
-
127
- ## Nested Resources
128
-
129
- Resources are automatically nested via `belongs_to` associations:
130
-
131
- ```ruby
132
- class Comment < ResourceRecord
133
- belongs_to :post
134
- end
135
- ```
136
-
137
- When both resources are registered in a portal, Plutonium creates nested URLs like `/posts/:post_id/comments`.
138
-
139
- ## Resource Features
140
-
141
- ### Entity Scoping (Multi-tenancy)
142
-
143
- Entity scoping is configured on the portal engine, not the model:
144
-
145
- ```ruby
146
- # packages/customer_portal/lib/engine.rb
147
- module CustomerPortal
148
- class Engine < Rails::Engine
149
- include Plutonium::Portal::Engine
150
-
151
- config.after_initialize do
152
- scope_to_entity Organization
153
- end
154
- end
155
- end
156
- ```
157
-
158
- ### Monetary Fields
159
-
160
- ```ruby
161
- class Product < ResourceRecord
162
- # Store as cents, expose as decimal
163
- has_cents :price_cents
164
- end
165
- ```
166
-
167
- ## Resource Lifecycle
168
-
169
- ```
170
- 1. User requests /posts/new
171
- 2. Controller builds new Post instance
172
- 3. Policy checks create? permission
173
- 4. Definition provides form fields
174
- 5. Form rendered to user
175
-
176
- 6. User submits form
177
- 7. Controller receives params
178
- 8. Policy filters permitted attributes
179
- 9. Model validates and saves
180
- 10. Controller redirects or re-renders
181
- ```
182
-
183
- ## Best Practices
184
-
185
- ### Keep Models Thin
186
- Put business logic in Interactions, not models.
187
-
188
- ```ruby
189
- # Good: Model handles data
190
- class Post < ResourceRecord
191
- validates :title, presence: true
192
- end
193
-
194
- # Interaction handles logic
195
- class PublishPost < Plutonium::Interaction::Base
196
- def execute
197
- resource.update!(published: true, published_at: Time.current)
198
- notify_subscribers
199
- succeed(resource)
200
- end
201
- end
202
- ```
203
-
204
- ### Use Meaningful Scopes
205
-
206
- ```ruby
207
- class Post < ResourceRecord
208
- scope :published, -> { where(published: true) }
209
- scope :recent, -> { order(created_at: :desc) }
210
- scope :by_author, ->(user) { where(user: user) }
211
- end
212
- ```
213
-
214
- ### Validate at the Right Level
215
-
216
- - **Model**: Data integrity (presence, format, uniqueness)
217
- - **Interaction**: Business rules (can only publish once)
218
- - **Policy**: Authorization (user must own post)
219
-
220
- ## Related Topics
221
-
222
- - [Architecture](./architecture) - How layers work together
223
- - [Model Reference](/reference/model/) - Complete model documentation
224
- - [Definition Reference](/reference/definition/) - Field configuration
@@ -1,411 +0,0 @@
1
- # Recipe: Blog Application
2
-
3
- Build a full-featured blog with posts, comments, categories, and multi-user support.
4
-
5
- ## Overview
6
-
7
- This recipe covers:
8
- - Post and comment management
9
- - Categories and tags
10
- - User roles (admin, author, reader)
11
- - Publication workflow
12
- - SEO features
13
-
14
- ## Architecture
15
-
16
- ```
17
- packages/
18
- ├── blogging/ # Feature package
19
- │ ├── models/
20
- │ │ ├── post.rb
21
- │ │ ├── comment.rb
22
- │ │ ├── category.rb
23
- │ │ └── tag.rb
24
- │ ├── definitions/
25
- │ ├── policies/
26
- │ └── interactions/
27
- ├── admin_portal/ # Admin interface
28
- └── public_portal/ # Public blog
29
- ```
30
-
31
- ## Models
32
-
33
- ### Post
34
-
35
- ```ruby
36
- module Blogging
37
- class Post < Blogging::ResourceRecord
38
- belongs_to :author, class_name: 'User'
39
- belongs_to :category
40
- has_many :comments, dependent: :destroy
41
- has_many :taggings, dependent: :destroy
42
- has_many :tags, through: :taggings
43
-
44
- validates :title, presence: true, length: { maximum: 200 }
45
- validates :body, presence: true
46
- validates :slug, presence: true, uniqueness: true
47
-
48
- scope :published, -> { where(published: true) }
49
- scope :draft, -> { where(published: false) }
50
- scope :featured, -> { where(featured: true) }
51
- scope :recent, -> { order(published_at: :desc) }
52
-
53
- before_validation :generate_slug, on: :create
54
-
55
- def publish!
56
- update!(published: true, published_at: Time.current)
57
- end
58
-
59
- def reading_time
60
- words_per_minute = 200
61
- (body.to_plain_text.split.size / words_per_minute.to_f).ceil
62
- end
63
-
64
- private
65
-
66
- def generate_slug
67
- self.slug ||= title&.parameterize
68
- end
69
- end
70
- end
71
- ```
72
-
73
- ### Comment
74
-
75
- ```ruby
76
- module Blogging
77
- class Comment < Blogging::ResourceRecord
78
- belongs_to :post
79
- belongs_to :author, class_name: 'User'
80
- belongs_to :parent, class_name: 'Comment', optional: true
81
- has_many :replies, class_name: 'Comment', foreign_key: :parent_id
82
-
83
- validates :body, presence: true
84
-
85
- scope :approved, -> { where(approved: true) }
86
- scope :pending, -> { where(approved: false) }
87
- scope :root, -> { where(parent_id: nil) }
88
- end
89
- end
90
- ```
91
-
92
- ### Category
93
-
94
- ```ruby
95
- module Blogging
96
- class Category < Blogging::ResourceRecord
97
- has_many :posts
98
-
99
- validates :name, presence: true, uniqueness: true
100
- validates :slug, presence: true, uniqueness: true
101
-
102
- before_validation :generate_slug
103
-
104
- private
105
-
106
- def generate_slug
107
- self.slug ||= name&.parameterize
108
- end
109
- end
110
- end
111
- ```
112
-
113
- ## Definitions
114
-
115
- ### Post Definition
116
-
117
- ```ruby
118
- module Blogging
119
- class PostDefinition < Plutonium::Resource::Definition
120
- # Form fields
121
- field :title
122
- field :slug, hint: "URL-friendly version (auto-generated if blank)"
123
- field :body, as: :rich_text
124
- field :excerpt, as: :text
125
- field :category
126
- field :tags, as: :select, multiple: true, collection: -> { Tag.pluck(:name, :id) }
127
- field :featured_image, as: :file, accept: "image/*"
128
- field :published, as: :switch
129
- field :featured, as: :switch
130
- field :meta_title
131
- field :meta_description, as: :text
132
-
133
- # Table columns
134
- column :title, sortable: true
135
- column :category
136
- column :author
137
- column :published
138
- column :featured
139
- column :published_at, sortable: true
140
-
141
- # Search
142
- search do |scope, query|
143
- scope.where("title ILIKE :q OR body ILIKE :q", q: "%#{query}%")
144
- end
145
-
146
- # Scopes
147
- scope :all, default: true
148
- scope :published, -> { where(published: true) }, badge: true
149
- scope :drafts, -> { where(published: false) }, badge: true
150
- scope :featured, -> { where(featured: true) }
151
-
152
- # Filters
153
- filter :category, as: :select, collection: -> { Category.pluck(:name, :id) }
154
- filter :author, as: :select, collection: -> { User.pluck(:name, :id) }
155
- filter :published, as: :boolean
156
- filter :created_at, as: :date_range
157
-
158
- # Actions
159
- action :publish, interaction: PublishPost, condition: ->(p) { !p.published? }
160
- action :unpublish, interaction: UnpublishPost, condition: ->(p) { p.published? }
161
- action :feature, interaction: FeaturePost, condition: ->(p) { !p.featured? }
162
-
163
- # Associations
164
- association :comments, fields: [:body, :author, :approved, :created_at]
165
-
166
- # Eager loading
167
- includes :author, :category, :tags
168
- end
169
- end
170
- ```
171
-
172
- ### Comment Definition
173
-
174
- ```ruby
175
- module Blogging
176
- class CommentDefinition < Plutonium::Resource::Definition
177
- field :body, as: :text
178
- field :post, as: :hidden
179
- field :author, as: :hidden
180
- field :approved, as: :switch
181
-
182
- column :body
183
- column :author
184
- column :approved
185
- column :created_at
186
-
187
- scope :all, default: true
188
- scope :approved, -> { where(approved: true) }
189
- scope :pending, -> { where(approved: false) }, badge: true
190
-
191
- action :approve, interaction: ApproveComment, condition: ->(c) { !c.approved? }
192
- action :reject, interaction: RejectComment, condition: ->(c) { c.approved? }
193
- end
194
- end
195
- ```
196
-
197
- ## Interactions
198
-
199
- ### Publish Post
200
-
201
- ```ruby
202
- module Blogging
203
- class PublishPost < Plutonium::Interaction::Base
204
- presents model_class: Post
205
- presents label: "Publish"
206
- presents icon: Phlex::TablerIcons::Send
207
-
208
- validate :has_content
209
-
210
- def execute
211
- resource.update!(
212
- published: true,
213
- published_at: Time.current
214
- )
215
-
216
- # Notify subscribers
217
- NotifySubscribersJob.perform_later(resource.id)
218
-
219
- succeed(resource).with_message("Post published!")
220
- end
221
-
222
- private
223
-
224
- def has_content
225
- errors.add(:base, "Post must have content") if resource.body.blank?
226
- end
227
- end
228
- end
229
- ```
230
-
231
- ### Approve Comment
232
-
233
- ```ruby
234
- module Blogging
235
- class ApproveComment < Plutonium::Interaction::Base
236
- presents model_class: Comment
237
- presents label: "Approve"
238
-
239
- def execute
240
- resource.update!(approved: true)
241
-
242
- # Notify comment author
243
- CommentApprovedMailer.notify(resource).deliver_later
244
-
245
- succeed(resource).with_message("Comment approved")
246
- end
247
- end
248
- end
249
- ```
250
-
251
- ## Policies
252
-
253
- ### Post Policy
254
-
255
- ```ruby
256
- module Blogging
257
- class PostPolicy < Plutonium::Resource::Policy
258
- def read?
259
- record.published? || author? || admin?
260
- end
261
-
262
- def create?
263
- user.present? && (user.author? || user.admin?)
264
- end
265
-
266
- def update?
267
- author? || admin?
268
- end
269
-
270
- def destroy?
271
- author? || admin?
272
- end
273
-
274
- def publish?
275
- (author? || admin?) && !record.published?
276
- end
277
-
278
- def relation_scope(relation)
279
- if admin?
280
- relation
281
- elsif user&.author?
282
- relation.where(author: user).or(relation.where(published: true))
283
- else
284
- relation.where(published: true)
285
- end
286
- end
287
-
288
- private
289
-
290
- def author?
291
- record.author_id == user&.id
292
- end
293
-
294
- def admin?
295
- user&.admin?
296
- end
297
- end
298
- end
299
- ```
300
-
301
- ## Portal Configuration
302
-
303
- ### Admin Portal
304
-
305
- Engine:
306
-
307
- ```ruby
308
- module AdminPortal
309
- class Engine < Rails::Engine
310
- include Plutonium::Portal::Engine
311
- end
312
- end
313
- ```
314
-
315
- Authentication (in controller concern):
316
-
317
- ```ruby
318
- # packages/admin_portal/app/controllers/admin_portal/concerns/controller.rb
319
- include Plutonium::Auth::Rodauth(:admin)
320
- ```
321
-
322
- Admin policy override:
323
-
324
- ```ruby
325
- # packages/admin_portal/app/policies/admin_portal/blogging/post_policy.rb
326
- module AdminPortal
327
- module Blogging
328
- class PostPolicy < ::Blogging::PostPolicy
329
- def read?
330
- true
331
- end
332
-
333
- def destroy?
334
- true
335
- end
336
-
337
- def relation_scope(relation)
338
- relation
339
- end
340
- end
341
- end
342
- end
343
- ```
344
-
345
- ### Public Portal
346
-
347
- Engine:
348
-
349
- ```ruby
350
- module PublicPortal
351
- class Engine < Rails::Engine
352
- include Plutonium::Portal::Engine
353
- end
354
- end
355
- ```
356
-
357
- Authentication (in controller concern):
358
-
359
- ```ruby
360
- # packages/public_portal/app/controllers/public_portal/concerns/controller.rb
361
- include Plutonium::Auth::Rodauth(:user)
362
- ```
363
-
364
- Public policy override:
365
-
366
- ```ruby
367
- # packages/public_portal/app/policies/public_portal/blogging/post_policy.rb
368
- module PublicPortal
369
- module Blogging
370
- class PostPolicy < ::Blogging::PostPolicy
371
- def create?
372
- false
373
- end
374
-
375
- def update?
376
- false
377
- end
378
-
379
- def relation_scope(relation)
380
- relation.published
381
- end
382
- end
383
- end
384
- end
385
- ```
386
-
387
- ## Usage
388
-
389
- ```bash
390
- # Generate the structure
391
- rails generate pu:pkg:package blogging
392
- rails generate pu:res:scaffold Post title:string slug:string body:text published:boolean --dest=blogging
393
- rails generate pu:res:scaffold Comment body:text approved:boolean post:belongs_to --dest=blogging
394
- rails generate pu:res:scaffold Category name:string slug:string --dest=blogging
395
-
396
- # Create portals
397
- rails generate pu:pkg:portal admin
398
- rails generate pu:pkg:portal public
399
-
400
- # Connect resources to portal
401
- rails generate pu:res:conn Blogging::Post Blogging::Comment Blogging::Category --dest=admin_portal
402
-
403
- rails db:migrate
404
- ```
405
-
406
- ## Next Steps
407
-
408
- - Add image uploads with Active Storage
409
- - Implement RSS feeds
410
- - Add social sharing
411
- - Set up full-text search with PostgreSQL