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,4 +1,4 @@
1
- # Chapter 7: Customizing the UI
1
+ # Chapter 8: Customizing the UI
2
2
 
3
3
  In this chapter, you'll customize forms, tables, and pages to create a polished interface.
4
4
 
@@ -12,7 +12,7 @@ Fields control how attributes appear in forms and displays. Plutonium auto-infer
12
12
  # packages/blogging/app/definitions/blogging/post_definition.rb
13
13
  class Blogging::PostDefinition < Blogging::ResourceDefinition
14
14
  # Rich text editor instead of plain textarea
15
- field :body, as: :rich_text
15
+ field :body, as: :markdown
16
16
 
17
17
  # Select with predefined options
18
18
  input :status, as: :select, choices: %w[draft review published]
@@ -38,8 +38,8 @@ Columns are auto-inferred from your model. Only declare columns when customizing
38
38
  ### Column Configuration
39
39
 
40
40
  ```ruby
41
- # Custom label and sortable
42
- column :user, label: "Author", sortable: true
41
+ # Custom label
42
+ column :user, label: "Author"
43
43
 
44
44
  # Computed column with block
45
45
  column :comment_count do |post|
@@ -73,7 +73,7 @@ end
73
73
 
74
74
  # Predefined scopes (reference model scopes)
75
75
  scope :published, default: true # Applied by default, uses Post.published
76
- scope :drafts # Uses Post.draft
76
+ scope :drafts # Uses Post.drafts
77
77
 
78
78
  # Inline scope with block
79
79
  scope(:recent) { |scope| scope.where('created_at > ?', 1.week.ago) }
@@ -146,7 +146,7 @@ Control form layout using wrapper options in definitions:
146
146
  class Blogging::PostDefinition < Blogging::ResourceDefinition
147
147
  # Full-width fields
148
148
  input :title, wrapper: {class: "col-span-full"}
149
- input :body, as: :rich_text, wrapper: {class: "col-span-full"}
149
+ input :body, as: :markdown, wrapper: {class: "col-span-full"}
150
150
 
151
151
  # Side-by-side fields (default is col-span-full)
152
152
  input :published_at, wrapper: {class: "col-span-1"}
@@ -244,11 +244,11 @@ Congratulations! You've built a complete blog application with:
244
244
  - Authorization with policies
245
245
  - Custom actions with Interactions
246
246
  - Nested resources
247
+ - Multiple portals (Admin and Author)
247
248
  - Customized UI
248
249
 
249
250
  Continue exploring:
250
251
  - [Guides](/guides/) - Deep dives on specific topics
251
252
  - [Reference](/reference/) - Complete API documentation
252
- - [Cookbook](/cookbook/) - Real-world recipes
253
253
 
254
254
  Happy building with Plutonium!
@@ -21,7 +21,7 @@ A blog application with:
21
21
  ## Prerequisites
22
22
 
23
23
  - Ruby 3.2+
24
- - Rails 8.0+ (or 7.1+)
24
+ - Rails 7.2+ (Rails 8 recommended)
25
25
  - Node.js 18+
26
26
  - PostgreSQL (or SQLite for development)
27
27
 
@@ -49,7 +49,10 @@ Create a "Publish" action using Interactions for business logic.
49
49
  ### [6. Nested Resources](./06-nested-resources)
50
50
  Add Comments as a nested resource under Posts.
51
51
 
52
- ### [7. Customizing the UI](./07-customizing-ui)
52
+ ### [7. Creating an Author Portal](./07-author-portal)
53
+ Create a second portal with different access levels for content authors.
54
+
55
+ ### [8. Customizing the UI](./08-customizing-ui)
53
56
  Customize forms, tables, and views to match your requirements.
54
57
 
55
58
  ## Getting Help
@@ -238,6 +238,39 @@ relation_scope do |relation|
238
238
  end
239
239
  ```
240
240
 
241
+ ## Controller & View Helpers
242
+
243
+ These helpers are available in controllers and views for authorization checks.
244
+
245
+ ### authorized_resource_scope
246
+
247
+ Get an authorized scope for a resource other than the current controller's resource. Useful in dashboards and custom views:
248
+
249
+ ```ruby
250
+ # In a view or controller
251
+ authorized_resource_scope(Post) # => Post.where(...)
252
+ authorized_resource_scope(Post).count # => 42
253
+ authorized_resource_scope(Comment, relation: post.comments)
254
+ ```
255
+
256
+ ### policy_for
257
+
258
+ Get the policy instance for any record:
259
+
260
+ ```ruby
261
+ policy_for(@post) # => PostPolicy instance
262
+ policy_for(@post).update? # => true/false
263
+ ```
264
+
265
+ ### allowed_to?
266
+
267
+ Check if an action is permitted:
268
+
269
+ ```ruby
270
+ allowed_to?(:edit?, @post) # => true/false
271
+ allowed_to?(:create?, Post) # => true/false
272
+ ```
273
+
241
274
  ## Portal-Specific Policies
242
275
 
243
276
  Override policies for specific portals:
@@ -23,7 +23,6 @@ rails g pu:pkg:package blogging
23
23
  packages/blogging/
24
24
  ├── app/
25
25
  │ ├── controllers/blogging/
26
- │ │ └── resource_controller.rb
27
26
  │ ├── definitions/blogging/
28
27
  │ │ └── resource_definition.rb
29
28
  │ ├── interactions/blogging/
@@ -87,8 +86,7 @@ packages/admin_portal/
87
86
  │ ├── controllers/admin_portal/
88
87
  │ │ ├── concerns/controller.rb
89
88
  │ │ ├── dashboard_controller.rb
90
- │ │ ├── plutonium_controller.rb
91
- │ │ └── resource_controller.rb
89
+ │ │ └── plutonium_controller.rb
92
90
  │ ├── definitions/admin_portal/
93
91
  │ │ └── resource_definition.rb
94
92
  │ ├── policies/admin_portal/
@@ -348,30 +346,28 @@ end
348
346
 
349
347
  ```ruby
350
348
  # packages/admin_portal/app/controllers/admin_portal/posts_controller.rb
351
- module AdminPortal
352
- class PostsController < ResourceController
353
- private
349
+ class AdminPortal::PostsController < ::PostsController
350
+ include AdminPortal::Concerns::Controller
354
351
 
355
- def preferred_action_after_submit
356
- "index"
357
- end
352
+ private
353
+
354
+ def preferred_action_after_submit
355
+ "index"
358
356
  end
359
357
  end
360
358
  ```
361
359
 
362
360
  ## Controller Hierarchy
363
361
 
362
+ Portal controllers inherit from the feature package's controller:
363
+
364
364
  ```
365
- ::PlutoniumController (app-wide base)
366
-
367
- ::ResourceController (resource handling)
368
-
369
- AdminPortal::ResourceController (portal base)
365
+ ::PostsController (feature package controller)
370
366
 
371
- AdminPortal::PostsController (resource-specific)
367
+ AdminPortal::PostsController (portal-specific, includes Concerns::Controller)
372
368
  ```
373
369
 
374
- Controllers are auto-created if not defined.
370
+ Controllers are auto-created if not defined. When accessing a portal resource controller, Plutonium dynamically creates it by inheriting from the feature package's controller and including the portal's controller concern.
375
371
 
376
372
  ## Related
377
373
 
@@ -218,6 +218,10 @@ action :export, interaction: ExportInteraction, resource_action: true
218
218
 
219
219
  ## Bulk Action Interaction
220
220
 
221
+ Bulk actions operate on multiple selected records. When a definition has bulk actions, the resource table automatically shows:
222
+ - **Selection checkboxes** in each row
223
+ - **Bulk actions toolbar** that appears when records are selected
224
+
221
225
  ```ruby
222
226
  class BulkPublishInteraction < ResourceInteraction
223
227
  presents label: "Publish Selected",
@@ -240,6 +244,34 @@ class BulkPublishInteraction < ResourceInteraction
240
244
  end
241
245
  ```
242
246
 
247
+ Register in your definition:
248
+
249
+ ```ruby
250
+ class PostDefinition < ResourceDefinition
251
+ action :bulk_publish, interaction: BulkPublishInteraction
252
+ # bulk_action: true is automatically inferred from `resources` attribute
253
+ end
254
+ ```
255
+
256
+ Add the policy method (checked per-record):
257
+
258
+ ```ruby
259
+ class PostPolicy < ResourcePolicy
260
+ def bulk_publish?
261
+ # Can use record attributes - checked for each selected record
262
+ user.admin? || record.author == user
263
+ end
264
+ end
265
+ ```
266
+
267
+ ::: tip Bulk Action Authorization
268
+ Bulk actions use **per-record authorization**:
269
+ - The policy method (e.g., `bulk_publish?`) is checked for **each selected record** - you can use `record` attributes
270
+ - Backend rejects the entire request if any record fails authorization
271
+ - UI only shows actions that **all** selected records support (buttons hide dynamically as you select)
272
+ - Records are fetched from `current_authorized_scope` - only accessible records can be selected
273
+ :::
274
+
243
275
  ## Resource Action (No Record)
244
276
 
245
277
  ```ruby
@@ -307,6 +339,10 @@ action :bulk_delete,
307
339
 
308
340
  ### Success
309
341
 
342
+ ::: tip Automatic Redirect
343
+ On success, the controller automatically redirects to the resource. You can use `with_redirect_response` if you want a **different** destination.
344
+ :::
345
+
310
346
  ```ruby
311
347
  def execute
312
348
  # ... do work ...
@@ -6,7 +6,7 @@ This guide covers implementing search, filters, scopes, and sorting.
6
6
 
7
7
  Plutonium provides built-in support for:
8
8
  - **Search** - Full-text search across fields
9
- - **Filters** - Input filters for specific fields
9
+ - **Filters** - Input filters for specific fields (dropdown panel)
10
10
  - **Scopes** - Predefined query shortcuts (quick filter buttons)
11
11
  - **Sorting** - Column-based ordering
12
12
 
@@ -68,33 +68,28 @@ end
68
68
 
69
69
  ## Filters
70
70
 
71
- Filters provide UI controls for narrowing results.
71
+ Plutonium provides **6 built-in filter types**. Use shorthand symbols or full class names.
72
72
 
73
73
  ### Text Filter
74
74
 
75
- The built-in `Text` filter supports various predicates:
75
+ String/text filtering with pattern matching:
76
76
 
77
77
  ```ruby
78
78
  class PostDefinition < ResourceDefinition
79
- # Exact match
80
- filter :status, with: Plutonium::Query::Filters::Text, predicate: :eq
79
+ # Shorthand (recommended)
80
+ filter :title, with: :text, predicate: :contains
81
+ filter :status, with: :text, predicate: :eq
81
82
 
82
- # Contains (LIKE %value%)
83
- filter :title, with: Plutonium::Query::Filters::Text, predicate: :contains
84
-
85
- # Starts with
83
+ # Full class name also works
86
84
  filter :slug, with: Plutonium::Query::Filters::Text, predicate: :starts_with
87
-
88
- # Ends with
89
- filter :email, with: Plutonium::Query::Filters::Text, predicate: :ends_with
90
85
  end
91
86
  ```
92
87
 
93
- ### Available Predicates
88
+ #### Available Predicates
94
89
 
95
90
  | Predicate | SQL | Description |
96
91
  |-----------|-----|-------------|
97
- | `:eq` | `= value` | Exact match |
92
+ | `:eq` | `= value` | Exact match (default) |
98
93
  | `:not_eq` | `!= value` | Not equal |
99
94
  | `:contains` | `LIKE %value%` | Contains text |
100
95
  | `:not_contains` | `NOT LIKE %value%` | Does not contain |
@@ -103,40 +98,120 @@ end
103
98
  | `:matches` | `LIKE value` | Pattern match (`*` becomes `%`) |
104
99
  | `:not_matches` | `NOT LIKE value` | Does not match pattern |
105
100
 
106
- ### Custom Filter with Lambda
101
+ ### Boolean Filter
102
+
103
+ True/false filtering for boolean columns:
107
104
 
108
105
  ```ruby
109
- filter :published, with: ->(scope, value) {
110
- value == "true" ? scope.where.not(published_at: nil) : scope.where(published_at: nil)
111
- }
112
-
113
- filter :has_comments, with: ->(scope, value) {
114
- if value == "true"
115
- scope.joins(:comments).distinct
116
- else
117
- scope.left_joins(:comments).where(comments: { id: nil })
118
- end
119
- }
106
+ # Basic
107
+ filter :active, with: :boolean
108
+
109
+ # Custom labels
110
+ filter :published, with: :boolean, true_label: "Published", false_label: "Draft"
111
+ ```
112
+
113
+ Renders a select dropdown with "All", true label ("Yes"), and false label ("No").
114
+
115
+ ### Date Filter
116
+
117
+ Single date filtering with comparison predicates:
118
+
119
+ ```ruby
120
+ filter :created_at, with: :date, predicate: :gteq # On or after
121
+ filter :due_date, with: :date, predicate: :lt # Before
122
+ filter :published_at, with: :date, predicate: :eq # On exact date
123
+ ```
124
+
125
+ #### Available Predicates
126
+
127
+ | Predicate | Description |
128
+ |-----------|-------------|
129
+ | `:eq` | On this date (default) |
130
+ | `:not_eq` | Not on this date |
131
+ | `:lt` | Before date |
132
+ | `:lteq` | On or before date |
133
+ | `:gt` | After date |
134
+ | `:gteq` | On or after date |
135
+
136
+ ### Date Range Filter
137
+
138
+ Filter between two dates (from/to):
139
+
140
+ ```ruby
141
+ # Basic
142
+ filter :created_at, with: :date_range
143
+
144
+ # Custom labels
145
+ filter :published_at, with: :date_range,
146
+ from_label: "Published from",
147
+ to_label: "Published to"
120
148
  ```
121
149
 
150
+ Renders two date pickers. Both are optional - users can filter with just "from" or just "to".
151
+
152
+ ### Select Filter
153
+
154
+ Filter from predefined choices:
155
+
156
+ ```ruby
157
+ # Static choices (array)
158
+ filter :status, with: :select, choices: %w[draft published archived]
159
+
160
+ # Dynamic choices (proc)
161
+ filter :category, with: :select, choices: -> { Category.pluck(:name) }
162
+
163
+ # Multiple selection
164
+ filter :tags, with: :select, choices: %w[ruby rails js], multiple: true
165
+ ```
166
+
167
+ ### Association Filter
168
+
169
+ Filter by associated record:
170
+
171
+ ```ruby
172
+ # Basic - infers Category class from :category key
173
+ filter :category, with: :association
174
+
175
+ # Explicit class
176
+ filter :author, with: :association, class_name: User
177
+
178
+ # Multiple selection
179
+ filter :tags, with: :association, class_name: Tag, multiple: true
180
+ ```
181
+
182
+ Renders a resource select dropdown. Converts filter key to foreign key (`:category` -> `:category_id`).
183
+
184
+ ### Filter Summary Table
185
+
186
+ | Type | Symbol | Input Params | Options |
187
+ |------|--------|--------------|---------|
188
+ | Text | `:text` | `query` | `predicate:` |
189
+ | Boolean | `:boolean` | `value` | `true_label:`, `false_label:` |
190
+ | Date | `:date` | `value` | `predicate:` |
191
+ | Date Range | `:date_range` | `from`, `to` | `from_label:`, `to_label:` |
192
+ | Select | `:select` | `value` | `choices:`, `multiple:` |
193
+ | Association | `:association` | `value` | `class_name:`, `multiple:` |
194
+
122
195
  ### Custom Filter Class
123
196
 
124
197
  ```ruby
125
- class DateRangeFilter < Plutonium::Query::Filter
126
- def apply(scope, start_date: nil, end_date: nil)
127
- scope = scope.where("#{key} >= ?", start_date.beginning_of_day) if start_date.present?
128
- scope = scope.where("#{key} <= ?", end_date.end_of_day) if end_date.present?
198
+ class PriceRangeFilter < Plutonium::Query::Filter
199
+ def apply(scope, min: nil, max: nil)
200
+ scope = scope.where("price >= ?", min) if min.present?
201
+ scope = scope.where("price <= ?", max) if max.present?
129
202
  scope
130
203
  end
131
204
 
132
205
  def customize_inputs
133
- input :start_date, as: :date
134
- input :end_date, as: :date
206
+ input :min, as: :number
207
+ input :max, as: :number
208
+ field :min, placeholder: "Min price..."
209
+ field :max, placeholder: "Max price..."
135
210
  end
136
211
  end
137
212
 
138
213
  # Use in definition
139
- filter :created_at, with: DateRangeFilter
214
+ filter :price, with: PriceRangeFilter
140
215
  ```
141
216
 
142
217
  ## Scopes
@@ -224,7 +299,9 @@ Query parameters are structured under `q`:
224
299
 
225
300
  ```
226
301
  /posts?q[search]=rails
227
- /posts?q[status][query]=published
302
+ /posts?q[title][query]=widget
303
+ /posts?q[status][value]=published
304
+ /posts?q[created_at][from]=2024-01-01&q[created_at][to]=2024-12-31
228
305
  /posts?q[scope]=recent
229
306
  /posts?q[sort_fields][]=created_at&q[sort_directions][created_at]=desc
230
307
  ```
@@ -232,28 +309,30 @@ Query parameters are structured under `q`:
232
309
  ## Complete Example
233
310
 
234
311
  ```ruby
235
- class PostDefinition < ResourceDefinition
312
+ class ProductDefinition < ResourceDefinition
236
313
  # Full-text search
237
314
  search do |scope, query|
238
315
  scope.where(
239
- "title ILIKE :q OR content ILIKE :q",
316
+ "name ILIKE :q OR description ILIKE :q",
240
317
  q: "%#{query}%"
241
318
  )
242
319
  end
243
320
 
244
321
  # Filters
245
- filter :status, with: Plutonium::Query::Filters::Text, predicate: :eq
246
- filter :category, with: Plutonium::Query::Filters::Text, predicate: :eq
247
- filter :title, with: Plutonium::Query::Filters::Text, predicate: :contains
322
+ filter :name, with: :text, predicate: :contains
323
+ filter :status, with: :select, choices: %w[draft active discontinued]
324
+ filter :featured, with: :boolean
325
+ filter :created_at, with: :date_range
326
+ filter :price, with: :date, predicate: :gteq
327
+ filter :category, with: :association
248
328
 
249
329
  # Quick scopes (reference model scopes)
250
- scope :published
251
- scope :draft
330
+ scope :active, default: true
252
331
  scope :featured
253
332
  scope(:recent) { |scope| scope.where("created_at > ?", 1.week.ago) }
254
333
 
255
334
  # Sortable columns
256
- sorts :title, :created_at, :view_count, :published_at
335
+ sorts :name, :price, :created_at
257
336
 
258
337
  # Default: newest first
259
338
  default_sort :created_at, :desc