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,4 +1,4 @@
|
|
|
1
|
-
# Chapter
|
|
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: :
|
|
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
|
|
42
|
-
column :user, label: "Author"
|
|
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.
|
|
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: :
|
|
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
|
|
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.
|
|
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
|
-
│ │
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
private
|
|
349
|
+
class AdminPortal::PostsController < ::PostsController
|
|
350
|
+
include AdminPortal::Concerns::Controller
|
|
354
351
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
::
|
|
366
|
-
↓
|
|
367
|
-
::ResourceController (resource handling)
|
|
368
|
-
↓
|
|
369
|
-
AdminPortal::ResourceController (portal base)
|
|
365
|
+
::PostsController (feature package controller)
|
|
370
366
|
↓
|
|
371
|
-
AdminPortal::PostsController (
|
|
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
|
-
|
|
71
|
+
Plutonium provides **6 built-in filter types**. Use shorthand symbols or full class names.
|
|
72
72
|
|
|
73
73
|
### Text Filter
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
String/text filtering with pattern matching:
|
|
76
76
|
|
|
77
77
|
```ruby
|
|
78
78
|
class PostDefinition < ResourceDefinition
|
|
79
|
-
#
|
|
80
|
-
filter :
|
|
79
|
+
# Shorthand (recommended)
|
|
80
|
+
filter :title, with: :text, predicate: :contains
|
|
81
|
+
filter :status, with: :text, predicate: :eq
|
|
81
82
|
|
|
82
|
-
#
|
|
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
|
-
|
|
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
|
-
###
|
|
101
|
+
### Boolean Filter
|
|
102
|
+
|
|
103
|
+
True/false filtering for boolean columns:
|
|
107
104
|
|
|
108
105
|
```ruby
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
filter :
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
126
|
-
def apply(scope,
|
|
127
|
-
scope = scope.where("
|
|
128
|
-
scope = scope.where("
|
|
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 :
|
|
134
|
-
input :
|
|
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 :
|
|
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[
|
|
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
|
|
312
|
+
class ProductDefinition < ResourceDefinition
|
|
236
313
|
# Full-text search
|
|
237
314
|
search do |scope, query|
|
|
238
315
|
scope.where(
|
|
239
|
-
"
|
|
316
|
+
"name ILIKE :q OR description ILIKE :q",
|
|
240
317
|
q: "%#{query}%"
|
|
241
318
|
)
|
|
242
319
|
end
|
|
243
320
|
|
|
244
321
|
# Filters
|
|
245
|
-
filter :
|
|
246
|
-
filter :
|
|
247
|
-
filter :
|
|
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 :
|
|
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 :
|
|
335
|
+
sorts :name, :price, :created_at
|
|
257
336
|
|
|
258
337
|
# Default: newest first
|
|
259
338
|
default_sort :created_at, :desc
|