plutonium 0.50.0 → 0.52.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 +85 -102
- data/.claude/skills/plutonium-app/SKILL.md +574 -0
- data/.claude/skills/plutonium-auth/SKILL.md +167 -302
- data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
- data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +674 -0
- data/.claude/skills/plutonium-testing/SKILL.md +9 -6
- data/.claude/skills/plutonium-ui/SKILL.md +900 -0
- data/CHANGELOG.md +44 -2
- data/Rakefile +2 -1
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1010 -1214
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +52 -51
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/.vitepress/config.ts +38 -29
- data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
- data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
- data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
- data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
- data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
- data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
- data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
- data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
- data/docs/.vitepress/theme/custom.css +144 -0
- data/docs/.vitepress/theme/index.ts +58 -1
- data/docs/getting-started/index.md +33 -57
- data/docs/getting-started/installation.md +37 -80
- data/docs/getting-started/tutorial/02-first-resource.md +17 -8
- data/docs/getting-started/tutorial/03-authentication.md +31 -23
- data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
- data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
- data/docs/getting-started/tutorial/07-author-portal.md +8 -0
- data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
- data/docs/getting-started/tutorial/index.md +4 -5
- data/docs/guides/adding-resources.md +66 -377
- data/docs/guides/authentication.md +98 -462
- data/docs/guides/authorization.md +124 -370
- data/docs/guides/creating-packages.md +93 -298
- data/docs/guides/custom-actions.md +126 -441
- data/docs/guides/customizing-ui.md +258 -0
- data/docs/guides/index.md +49 -52
- data/docs/guides/multi-tenancy.md +123 -186
- data/docs/guides/nested-resources.md +137 -396
- data/docs/guides/search-filtering.md +127 -238
- data/docs/guides/testing.md +10 -5
- data/docs/guides/theming.md +168 -405
- data/docs/guides/troubleshooting.md +5 -3
- data/docs/guides/user-invites.md +112 -425
- data/docs/guides/user-profile.md +82 -241
- data/docs/index.md +10 -219
- data/docs/public/asciinema/home-scaffold.cast +305 -0
- data/docs/public/images/guides/custom-actions-bulk.png +0 -0
- data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
- data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
- data/docs/public/images/guides/nested-inputs.png +0 -0
- data/docs/public/images/guides/nested-resources-tab.png +0 -0
- data/docs/public/images/guides/search-filtering-index.png +0 -0
- data/docs/public/images/guides/search-filtering-panel.png +0 -0
- data/docs/public/images/guides/theming-after.png +0 -0
- data/docs/public/images/guides/theming-before.png +0 -0
- data/docs/public/images/guides/user-invites-landing.png +0 -0
- data/docs/public/images/guides/user-profile-edit.png +0 -0
- data/docs/public/images/guides/user-profile-show.png +0 -0
- data/docs/public/images/home-index.png +0 -0
- data/docs/public/images/home-new.png +0 -0
- data/docs/public/images/home-show.png +0 -0
- data/docs/public/images/tutorial/02-empty-index.png +0 -0
- data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
- data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
- data/docs/public/images/tutorial/02-new-form.png +0 -0
- data/docs/public/images/tutorial/03-create-account.png +0 -0
- data/docs/public/images/tutorial/03-login.png +0 -0
- data/docs/public/images/tutorial/04-admin-index.png +0 -0
- data/docs/public/images/tutorial/05-actions-menu.png +0 -0
- data/docs/public/images/tutorial/05-row-actions.png +0 -0
- data/docs/public/images/tutorial/06-comments-tab.png +0 -0
- data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
- data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
- data/docs/public/images/tutorial/07-author-portal.png +0 -0
- data/docs/public/images/tutorial/08-customized-index.png +0 -0
- data/docs/reference/app/generators.md +517 -0
- data/docs/reference/app/index.md +158 -0
- data/docs/reference/app/packages.md +146 -0
- data/docs/reference/app/portals.md +377 -0
- data/docs/reference/auth/accounts.md +229 -0
- data/docs/reference/auth/index.md +88 -0
- data/docs/reference/auth/profile.md +185 -0
- data/docs/reference/behavior/controllers.md +395 -0
- data/docs/reference/behavior/index.md +22 -0
- data/docs/reference/behavior/interactions.md +341 -0
- data/docs/reference/behavior/policies.md +417 -0
- data/docs/reference/index.md +67 -48
- data/docs/reference/resource/actions.md +423 -0
- data/docs/reference/resource/definition.md +508 -0
- data/docs/reference/resource/index.md +50 -0
- data/docs/reference/resource/model.md +348 -0
- data/docs/reference/resource/query.md +305 -0
- data/docs/reference/tenancy/entity-scoping.md +368 -0
- data/docs/reference/tenancy/index.md +36 -0
- data/docs/reference/tenancy/invites.md +400 -0
- data/docs/reference/tenancy/nested-resources.md +267 -0
- data/docs/reference/testing/index.md +287 -0
- data/docs/reference/ui/assets.md +400 -0
- data/docs/reference/ui/components.md +165 -0
- data/docs/reference/ui/displays.md +104 -0
- data/docs/reference/ui/forms.md +284 -0
- data/docs/reference/ui/index.md +30 -0
- data/docs/reference/ui/layouts.md +106 -0
- data/docs/reference/ui/pages.md +189 -0
- data/docs/reference/ui/tables.md +121 -0
- data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
- data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
- data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
- data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
- data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
- data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
- 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/core/assets/assets_generator.rb +10 -0
- data/lib/generators/pu/core/update/update_generator.rb +0 -20
- data/lib/generators/pu/invites/install_generator.rb +45 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
- data/lib/generators/pu/profile/conn_generator.rb +2 -2
- data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
- data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
- data/lib/generators/pu/rodauth/account_generator.rb +2 -1
- data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
- data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
- data/lib/generators/pu/rodauth/views_generator.rb +0 -2
- data/lib/generators/pu/saas/membership/USAGE +4 -1
- data/lib/generators/pu/saas/setup_generator.rb +16 -4
- data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
- data/lib/plutonium/definition/base.rb +1 -1
- data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
- data/lib/plutonium/helpers/turbo_helper.rb +30 -0
- data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
- data/lib/plutonium/resource/controller.rb +1 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +23 -5
- data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
- data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
- data/lib/plutonium/resource/policy.rb +7 -0
- data/lib/plutonium/routing/mapper_extensions.rb +15 -0
- data/lib/plutonium/ui/component/methods.rb +5 -0
- data/lib/plutonium/ui/form/base.rb +23 -3
- data/lib/plutonium/ui/form/components/json.rb +58 -0
- data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
- data/lib/plutonium/ui/form/components/secure_association.rb +103 -22
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
- data/lib/plutonium/ui/form/interaction.rb +1 -1
- data/lib/plutonium/ui/form/resource.rb +0 -4
- data/lib/plutonium/ui/form/theme.rb +1 -1
- data/lib/plutonium/ui/grid/resource.rb +1 -1
- data/lib/plutonium/ui/layout/base.rb +1 -0
- data/lib/plutonium/ui/page/base.rb +0 -7
- data/lib/plutonium/ui/page/edit.rb +1 -1
- data/lib/plutonium/ui/page/index.rb +4 -4
- data/lib/plutonium/ui/page/new.rb +1 -1
- data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium.rb +8 -0
- data/lib/tasks/release.rake +15 -1
- data/package.json +13 -10
- data/src/css/slim_select.css +4 -0
- data/src/js/controllers/form_controller.js +5 -4
- data/src/js/controllers/slim_select_controller.js +61 -0
- data/src/js/turbo/turbo_actions.js +33 -0
- data/yarn.lock +661 -544
- metadata +86 -33
- data/.claude/skills/plutonium-assets/SKILL.md +0 -512
- data/.claude/skills/plutonium-controller/SKILL.md +0 -396
- data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
- data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
- data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
- data/.claude/skills/plutonium-forms/SKILL.md +0 -465
- data/.claude/skills/plutonium-installation/SKILL.md +0 -331
- data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
- data/.claude/skills/plutonium-invites/SKILL.md +0 -408
- data/.claude/skills/plutonium-model/SKILL.md +0 -440
- data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
- data/.claude/skills/plutonium-package/SKILL.md +0 -198
- data/.claude/skills/plutonium-policy/SKILL.md +0 -456
- data/.claude/skills/plutonium-portal/SKILL.md +0 -410
- data/.claude/skills/plutonium-views/SKILL.md +0 -651
- data/docs/reference/assets/index.md +0 -496
- data/docs/reference/controller/index.md +0 -412
- data/docs/reference/definition/actions.md +0 -462
- data/docs/reference/definition/fields.md +0 -383
- data/docs/reference/definition/index.md +0 -326
- data/docs/reference/definition/query.md +0 -351
- data/docs/reference/generators/index.md +0 -648
- data/docs/reference/interaction/index.md +0 -449
- data/docs/reference/model/features.md +0 -248
- data/docs/reference/model/index.md +0 -218
- data/docs/reference/policy/index.md +0 -456
- data/docs/reference/portal/index.md +0 -379
- data/docs/reference/views/forms.md +0 -411
- data/docs/reference/views/index.md +0 -544
|
@@ -1,198 +1,130 @@
|
|
|
1
1
|
# Search and Filtering
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Add a search box, sidebar filters, quick-scope buttons, and sortable columns to a resource's index page. All in the definition.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Goal
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **Search** - Full-text search across fields
|
|
9
|
-
- **Filters** - Input filters for specific fields (dropdown panel)
|
|
10
|
-
- **Scopes** - Predefined query shortcuts (quick filter buttons)
|
|
11
|
-
- **Sorting** - Column-based ordering
|
|
7
|
+
Users can:
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
- Type into a search box to narrow the index list.
|
|
10
|
+
- Click sidebar filters to narrow by status / category / date / etc.
|
|
11
|
+
- Click scope buttons (top-of-list quick filters) for common queries like "Published" or "My posts".
|
|
12
|
+
- Click column headers to sort.
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
## The four pieces
|
|
17
|
+
|
|
18
|
+
| DSL | Purpose |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `search` | The top-level search box. ONE block, queries you define. |
|
|
21
|
+
| `filter` | Sidebar filter inputs. One per filterable attribute. |
|
|
22
|
+
| `scope` | Quick-filter buttons across the top. References model scopes (or inline blocks). |
|
|
23
|
+
| `sort` / `default_sort` | Sortable columns. |
|
|
14
24
|
|
|
15
|
-
|
|
25
|
+
All declared in the definition.
|
|
26
|
+
|
|
27
|
+
## Quick recipe
|
|
16
28
|
|
|
17
29
|
```ruby
|
|
18
30
|
class PostDefinition < ResourceDefinition
|
|
31
|
+
# Search box — searches title and body
|
|
19
32
|
search do |scope, query|
|
|
20
|
-
scope.where("title ILIKE
|
|
33
|
+
scope.where("title ILIKE :q OR body ILIKE :q", q: "%#{query}%")
|
|
21
34
|
end
|
|
22
|
-
end
|
|
23
|
-
```
|
|
24
35
|
|
|
25
|
-
|
|
36
|
+
# Sidebar filters
|
|
37
|
+
filter :status, with: :select, choices: %w[draft published archived]
|
|
38
|
+
filter :title, with: :text, predicate: :contains
|
|
39
|
+
filter :created_at, with: :date_range
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
scope
|
|
30
|
-
"title ILIKE :q OR content ILIKE :q OR author_name ILIKE :q",
|
|
31
|
-
q: "%#{query}%"
|
|
32
|
-
)
|
|
33
|
-
end
|
|
34
|
-
```
|
|
41
|
+
# Quick-filter buttons
|
|
42
|
+
scope :published # uses Post.published
|
|
43
|
+
scope :draft
|
|
35
44
|
|
|
36
|
-
|
|
45
|
+
# Default scope (the "Published" button is highlighted on initial load)
|
|
46
|
+
default_scope :published
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
q: "%#{query}%"
|
|
43
|
-
).distinct
|
|
48
|
+
# Sortable columns
|
|
49
|
+
sort :title
|
|
50
|
+
sort :created_at
|
|
51
|
+
default_sort :created_at, :desc
|
|
44
52
|
end
|
|
45
53
|
```
|
|
46
54
|
|
|
47
|
-
|
|
55
|
+
## Search
|
|
48
56
|
|
|
49
57
|
```ruby
|
|
58
|
+
# Single field
|
|
50
59
|
search do |scope, query|
|
|
51
|
-
|
|
52
|
-
terms.reduce(scope) do |current_scope, term|
|
|
53
|
-
current_scope.where("title ILIKE ?", "%#{term}%")
|
|
54
|
-
end
|
|
60
|
+
scope.where("title ILIKE ?", "%#{query}%")
|
|
55
61
|
end
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Full-Text Search (PostgreSQL)
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
# Multiple fields
|
|
61
64
|
search do |scope, query|
|
|
62
65
|
scope.where(
|
|
63
|
-
"
|
|
64
|
-
query
|
|
66
|
+
"title ILIKE :q OR content ILIKE :q OR author_name ILIKE :q",
|
|
67
|
+
q: "%#{query}%"
|
|
65
68
|
)
|
|
66
69
|
end
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Filters
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
```ruby
|
|
78
|
-
class PostDefinition < ResourceDefinition
|
|
79
|
-
# Shorthand (recommended)
|
|
80
|
-
filter :title, with: :text, predicate: :contains
|
|
81
|
-
filter :status, with: :text, predicate: :eq
|
|
82
|
-
|
|
83
|
-
# Full class name also works
|
|
84
|
-
filter :slug, with: Plutonium::Query::Filters::Text, predicate: :starts_with
|
|
71
|
+
# Across associations
|
|
72
|
+
search do |scope, query|
|
|
73
|
+
scope.joins(:author).where(
|
|
74
|
+
"posts.title ILIKE :q OR users.name ILIKE :q",
|
|
75
|
+
q: "%#{query}%"
|
|
76
|
+
).distinct
|
|
85
77
|
end
|
|
86
78
|
```
|
|
87
79
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
| Predicate | SQL | Description |
|
|
91
|
-
|-----------|-----|-------------|
|
|
92
|
-
| `:eq` | `= value` | Exact match (default) |
|
|
93
|
-
| `:not_eq` | `!= value` | Not equal |
|
|
94
|
-
| `:contains` | `LIKE %value%` | Contains text |
|
|
95
|
-
| `:not_contains` | `NOT LIKE %value%` | Does not contain |
|
|
96
|
-
| `:starts_with` | `LIKE value%` | Starts with |
|
|
97
|
-
| `:ends_with` | `LIKE %value` | Ends with |
|
|
98
|
-
| `:matches` | `LIKE value` | Pattern match (`*` becomes `%`) |
|
|
99
|
-
| `:not_matches` | `NOT LIKE value` | Does not match pattern |
|
|
100
|
-
|
|
101
|
-
### Boolean Filter
|
|
102
|
-
|
|
103
|
-
True/false filtering for boolean columns:
|
|
104
|
-
|
|
105
|
-
```ruby
|
|
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
|
|
80
|
+
### The `search` block also powers typeahead
|
|
126
81
|
|
|
127
|
-
|
|
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 |
|
|
82
|
+
When an association input targets this resource, the dropdown's autocomplete calls the resource's `search` block. Same code, two surfaces.
|
|
135
83
|
|
|
136
|
-
###
|
|
84
|
+
### Without a `search` block — typeahead fallback
|
|
137
85
|
|
|
138
|
-
|
|
86
|
+
The framework falls back to a case-insensitive `LIKE` on the first column it finds, in priority order:
|
|
139
87
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
88
|
+
1. The input's `label_method:` option, if it's a real column.
|
|
89
|
+
2. Otherwise the first match from `[name, title, label, slug, display_name, email]`.
|
|
90
|
+
3. Otherwise the relation is returned unfiltered (capped).
|
|
143
91
|
|
|
144
|
-
|
|
145
|
-
filter :published_at, with: :date_range,
|
|
146
|
-
from_label: "Published from",
|
|
147
|
-
to_label: "Published to"
|
|
148
|
-
```
|
|
92
|
+
For large tables, write an explicit `search` block — the leading-wildcard `LIKE` can't use a b-tree index. See [Reference › Resource › Query › Search](/reference/resource/query#search).
|
|
149
93
|
|
|
150
|
-
|
|
94
|
+
## Filters
|
|
151
95
|
|
|
152
|
-
|
|
96
|
+
Six built-in types. Use shorthand symbols:
|
|
153
97
|
|
|
154
|
-
|
|
98
|
+
| Type | Symbol | URL params | Options |
|
|
99
|
+
|---|---|---|---|
|
|
100
|
+
| Text | `:text` | `query` | `predicate:` |
|
|
101
|
+
| Boolean | `:boolean` | `value` | `true_label:`, `false_label:` |
|
|
102
|
+
| Date | `:date` | `value` | `predicate:` |
|
|
103
|
+
| Date range | `:date_range` | `from`, `to` | `from_label:`, `to_label:` |
|
|
104
|
+
| Select | `:select` | `value` | `choices:`, `multiple:` |
|
|
105
|
+
| Association | `:association` | `value` | `class_name:`, `multiple:` |
|
|
155
106
|
|
|
156
107
|
```ruby
|
|
157
|
-
|
|
158
|
-
filter :
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
filter :
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
filter :tags, with: :select, choices: %w[ruby rails js], multiple: true
|
|
108
|
+
filter :title, with: :text, predicate: :contains
|
|
109
|
+
filter :active, with: :boolean
|
|
110
|
+
filter :due_date, with: :date, predicate: :lt
|
|
111
|
+
filter :created_at, with: :date_range
|
|
112
|
+
filter :status, with: :select, choices: %w[draft published]
|
|
113
|
+
filter :category, with: :select, choices: -> { Category.pluck(:name) }
|
|
114
|
+
filter :author, with: :association, class_name: User
|
|
165
115
|
```
|
|
166
116
|
|
|
167
|
-
###
|
|
168
|
-
|
|
169
|
-
Filter by associated record:
|
|
117
|
+
### Custom filter (lambda)
|
|
170
118
|
|
|
171
119
|
```ruby
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
120
|
+
filter :published, with: ->(scope, value) {
|
|
121
|
+
value == "true" ? scope.where.not(published_at: nil) : scope.where(published_at: nil)
|
|
122
|
+
}
|
|
180
123
|
```
|
|
181
124
|
|
|
182
|
-
|
|
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:` |
|
|
125
|
+
### Custom filter class
|
|
194
126
|
|
|
195
|
-
|
|
127
|
+
For reusable filters with multiple inputs:
|
|
196
128
|
|
|
197
129
|
```ruby
|
|
198
130
|
class PriceRangeFilter < Plutonium::Query::Filter
|
|
@@ -205,99 +137,62 @@ class PriceRangeFilter < Plutonium::Query::Filter
|
|
|
205
137
|
def customize_inputs
|
|
206
138
|
input :min, as: :number
|
|
207
139
|
input :max, as: :number
|
|
208
|
-
field :min, placeholder: "Min price..."
|
|
209
|
-
field :max, placeholder: "Max price..."
|
|
210
140
|
end
|
|
211
141
|
end
|
|
212
142
|
|
|
213
|
-
# Use in definition
|
|
214
143
|
filter :price, with: PriceRangeFilter
|
|
215
144
|
```
|
|
216
145
|
|
|
217
|
-
|
|
146
|
+
Clicking **Filter** opens a slideover with one input per declared filter:
|
|
218
147
|
|
|
219
|
-
|
|
148
|
+

|
|
220
149
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
Reference existing model scopes:
|
|
150
|
+
## Scopes (quick-filter buttons)
|
|
224
151
|
|
|
225
152
|
```ruby
|
|
226
153
|
class PostDefinition < ResourceDefinition
|
|
227
|
-
scope :published #
|
|
228
|
-
scope :draft #
|
|
229
|
-
scope :featured # Uses Post.featured
|
|
230
|
-
end
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
### Inline Scopes
|
|
234
|
-
|
|
235
|
-
Use block syntax with the scope passed as an argument:
|
|
236
|
-
|
|
237
|
-
```ruby
|
|
238
|
-
scope(:recent) { |scope| scope.where("created_at > ?", 1.week.ago) }
|
|
239
|
-
scope(:this_month) { |scope| scope.where(created_at: Time.current.all_month) }
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### With Controller Context
|
|
154
|
+
scope :published # uses Post.published
|
|
155
|
+
scope :draft # uses Post.draft
|
|
243
156
|
|
|
244
|
-
Inline
|
|
157
|
+
# Inline scope — block runs with scope as argument
|
|
158
|
+
scope(:recent) { |s| s.where('created_at > ?', 1.week.ago) }
|
|
245
159
|
|
|
246
|
-
|
|
247
|
-
scope(:mine) { |
|
|
248
|
-
|
|
160
|
+
# Scope with controller context
|
|
161
|
+
scope(:mine) { |s| s.where(author: current_user) }
|
|
162
|
+
end
|
|
249
163
|
```
|
|
250
164
|
|
|
251
|
-
|
|
165
|
+
Named scopes reference a model scope. Inline scopes have access to `current_user`, `current_parent`, `current_scoped_entity`.
|
|
252
166
|
|
|
253
|
-
|
|
167
|
+
### Default scope
|
|
254
168
|
|
|
255
169
|
```ruby
|
|
256
|
-
|
|
257
|
-
scope :published
|
|
258
|
-
scope :draft
|
|
259
|
-
scope :archived
|
|
260
|
-
|
|
261
|
-
default_scope :published
|
|
262
|
-
end
|
|
170
|
+
default_scope :published
|
|
263
171
|
```
|
|
264
172
|
|
|
265
|
-
|
|
266
|
-
- The default scope is
|
|
267
|
-
-
|
|
268
|
-
- Clicking "All" shows all records without any scope filter
|
|
173
|
+
- Applied on initial page load.
|
|
174
|
+
- The default scope button is highlighted (not "All").
|
|
175
|
+
- Clicking "All" shows the unscoped collection.
|
|
269
176
|
|
|
270
177
|
## Sorting
|
|
271
178
|
|
|
272
|
-
### Define Sortable Fields
|
|
273
|
-
|
|
274
179
|
```ruby
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
sort :created_at
|
|
278
|
-
sort :view_count
|
|
279
|
-
|
|
280
|
-
# Multiple at once
|
|
281
|
-
sorts :title, :created_at, :view_count
|
|
282
|
-
end
|
|
283
|
-
```
|
|
180
|
+
sort :title
|
|
181
|
+
sort :created_at
|
|
284
182
|
|
|
285
|
-
|
|
183
|
+
sorts :title, :created_at, :view_count # shorthand
|
|
286
184
|
|
|
287
|
-
```ruby
|
|
288
|
-
# Field and direction
|
|
289
185
|
default_sort :created_at, :desc
|
|
290
|
-
default_sort :title, :asc
|
|
291
186
|
|
|
292
|
-
# Complex
|
|
187
|
+
# Complex with a block
|
|
293
188
|
default_sort { |scope| scope.order(featured: :desc, created_at: :desc) }
|
|
294
189
|
```
|
|
295
190
|
|
|
296
|
-
|
|
191
|
+
Framework default (nothing declared, no user sort): `id DESC`.
|
|
297
192
|
|
|
298
|
-
## URL
|
|
193
|
+
## URL parameters
|
|
299
194
|
|
|
300
|
-
Query
|
|
195
|
+
Query params are namespaced under `q`:
|
|
301
196
|
|
|
302
197
|
```
|
|
303
198
|
/posts?q[search]=rails
|
|
@@ -308,43 +203,37 @@ Query parameters are structured under `q`:
|
|
|
308
203
|
/posts?q[sort_fields][]=created_at&q[sort_directions][created_at]=desc
|
|
309
204
|
```
|
|
310
205
|
|
|
311
|
-
##
|
|
312
|
-
|
|
313
|
-
```ruby
|
|
314
|
-
class ProductDefinition < ResourceDefinition
|
|
315
|
-
# Full-text search
|
|
316
|
-
search do |scope, query|
|
|
317
|
-
scope.where(
|
|
318
|
-
"name ILIKE :q OR description ILIKE :q",
|
|
319
|
-
q: "%#{query}%"
|
|
320
|
-
)
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
# Filters
|
|
324
|
-
filter :name, with: :text, predicate: :contains
|
|
325
|
-
filter :status, with: :select, choices: %w[draft active discontinued]
|
|
326
|
-
filter :featured, with: :boolean
|
|
327
|
-
filter :created_at, with: :date_range
|
|
328
|
-
filter :price, with: :date, predicate: :gteq
|
|
329
|
-
filter :category, with: :association
|
|
206
|
+
## Performance tips
|
|
330
207
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
208
|
+
- **Add indexes** for filtered and sorted columns.
|
|
209
|
+
- **Use `.distinct`** when joining associations in search — duplicate rows otherwise.
|
|
210
|
+
- **Prefer scopes over filters** for queries used often (no input parsing).
|
|
211
|
+
- **`LIKE '%q%'` can't use a b-tree index** — for large tables, use `pg_search` or a trigram/GIN/full-text index.
|
|
335
212
|
|
|
336
|
-
|
|
337
|
-
default_scope :active
|
|
213
|
+
## Full-text search with `pg_search`
|
|
338
214
|
|
|
339
|
-
|
|
340
|
-
|
|
215
|
+
```ruby
|
|
216
|
+
# Model
|
|
217
|
+
class Post < ResourceRecord
|
|
218
|
+
include PgSearch::Model
|
|
219
|
+
pg_search_scope :search_content, against: %i[title content]
|
|
220
|
+
end
|
|
341
221
|
|
|
342
|
-
|
|
343
|
-
|
|
222
|
+
# Definition
|
|
223
|
+
search do |scope, query|
|
|
224
|
+
scope.search_content(query)
|
|
344
225
|
end
|
|
345
226
|
```
|
|
346
227
|
|
|
228
|
+
## Common issues
|
|
229
|
+
|
|
230
|
+
- **Filter not showing up** — make sure the attribute is in `permitted_attributes_for_index` on the policy.
|
|
231
|
+
- **Slow search on large tables** — `LIKE '%q%'` can't be indexed by a b-tree. Switch to FTS or trigram.
|
|
232
|
+
- **Duplicate rows in results** — add `.distinct` when joining associations.
|
|
233
|
+
- **Typeahead works on small dev tables but slows in production** — same b-tree issue. Write an explicit `search` block backed by a proper index.
|
|
234
|
+
|
|
347
235
|
## Related
|
|
348
236
|
|
|
349
|
-
- [
|
|
350
|
-
- [
|
|
237
|
+
- [Reference › Resource › Query](/reference/resource/query) — full surface
|
|
238
|
+
- [Adding resources](./adding-resources) — basic resource setup
|
|
239
|
+
- [Authorization](./authorization) — `permitted_attributes_for_index` gates which fields can be filtered
|
data/docs/guides/testing.md
CHANGED
|
@@ -101,7 +101,11 @@ current_account # uses portal from DSL
|
|
|
101
101
|
with_portal(:org) { ... } # scoped portal switch
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
::: info Default Rodauth login expects password `"password123"`
|
|
105
|
+
`login_as` posts to `/<account_table>/login` with `password: "password123"` by default. Either create test accounts with that password (e.g. in fixtures or factories), or override the auth flow via `sign_in_for_tests` below.
|
|
106
|
+
:::
|
|
107
|
+
|
|
108
|
+
### Non-Rodauth auth (or skipping Rodauth in tests)
|
|
105
109
|
|
|
106
110
|
Define `sign_in_for_tests(account, portal:)` in your test class (or in `test/support/plutonium_testing.rb` for project-wide use):
|
|
107
111
|
|
|
@@ -147,8 +151,9 @@ Output: `test/integration/<portal>_portal/<resource>_test.rb`.
|
|
|
147
151
|
- **Nested resources need `parent:` in the DSL AND a parent record** from `parent_record!`. Both are required for path interpolation.
|
|
148
152
|
- **`PortalAccess` uses `portal_access_for`**, not `resource_tests_for`. Don't mix them on the same class.
|
|
149
153
|
|
|
150
|
-
##
|
|
154
|
+
## Related
|
|
151
155
|
|
|
152
|
-
- [
|
|
153
|
-
- [
|
|
154
|
-
- [
|
|
156
|
+
- [Reference › Testing](/reference/testing/) — full DSL reference, all concern stubs, override hooks
|
|
157
|
+
- [Authorization](./authorization) — write the policy this concern verifies
|
|
158
|
+
- [Multi-tenancy](./multi-tenancy) — entity scoping that drives nested-resource tests
|
|
159
|
+
- [Authentication](./authentication) — Rodauth setup behind the default login flow
|