plutonium 0.50.0 → 0.51.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 +572 -0
- data/.claude/skills/plutonium-auth/SKILL.md +163 -300
- 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 +655 -0
- data/.claude/skills/plutonium-testing/SKILL.md +6 -5
- data/.claude/skills/plutonium-ui/SKILL.md +900 -0
- data/CHANGELOG.md +27 -2
- data/Rakefile +2 -1
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1009 -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 +37 -27
- data/docs/getting-started/index.md +22 -29
- data/docs/getting-started/installation.md +37 -80
- data/docs/getting-started/tutorial/index.md +4 -5
- data/docs/guides/adding-resources.md +66 -377
- data/docs/guides/authentication.md +94 -463
- data/docs/guides/authorization.md +124 -370
- data/docs/guides/creating-packages.md +94 -296
- data/docs/guides/custom-actions.md +121 -441
- data/docs/guides/index.md +22 -42
- data/docs/guides/multi-tenancy.md +116 -187
- data/docs/guides/nested-resources.md +103 -431
- data/docs/guides/search-filtering.md +123 -240
- data/docs/guides/testing.md +5 -4
- data/docs/guides/theming.md +157 -407
- data/docs/guides/troubleshooting.md +5 -3
- data/docs/guides/user-invites.md +106 -425
- data/docs/guides/user-profile.md +76 -243
- data/docs/index.md +1 -1
- 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 +230 -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 +56 -49
- 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 +361 -0
- data/docs/reference/tenancy/index.md +36 -0
- data/docs/reference/tenancy/invites.md +393 -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 +117 -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/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/update/update_generator.rb +0 -20
- data/lib/generators/pu/invites/install_generator.rb +1 -0
- 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 +11 -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 +19 -1
- 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 +4 -0
- data/lib/plutonium/ui/form/base.rb +6 -2
- 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 +98 -22
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
- data/lib/plutonium/ui/form/resource.rb +0 -4
- 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/index.rb +4 -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 +10 -10
- data/src/css/slim_select.css +4 -0
- data/src/js/controllers/slim_select_controller.js +61 -0
- data/src/js/turbo/turbo_actions.js +33 -0
- data/yarn.lock +553 -543
- metadata +44 -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,128 @@
|
|
|
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
|
+
## The four pieces
|
|
15
|
+
|
|
16
|
+
| DSL | Purpose |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `search` | The top-level search box. ONE block, queries you define. |
|
|
19
|
+
| `filter` | Sidebar filter inputs. One per filterable attribute. |
|
|
20
|
+
| `scope` | Quick-filter buttons across the top. References model scopes (or inline blocks). |
|
|
21
|
+
| `sort` / `default_sort` | Sortable columns. |
|
|
22
|
+
|
|
23
|
+
All declared in the definition.
|
|
14
24
|
|
|
15
|
-
|
|
25
|
+
## Quick recipe
|
|
16
26
|
|
|
17
27
|
```ruby
|
|
18
28
|
class PostDefinition < ResourceDefinition
|
|
29
|
+
# Search box — searches title and body
|
|
19
30
|
search do |scope, query|
|
|
20
|
-
scope.where("title ILIKE
|
|
31
|
+
scope.where("title ILIKE :q OR body ILIKE :q", q: "%#{query}%")
|
|
21
32
|
end
|
|
22
|
-
end
|
|
23
|
-
```
|
|
24
33
|
|
|
25
|
-
|
|
34
|
+
# Sidebar filters
|
|
35
|
+
filter :status, with: :select, choices: %w[draft published archived]
|
|
36
|
+
filter :title, with: :text, predicate: :contains
|
|
37
|
+
filter :created_at, with: :date_range
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
scope
|
|
30
|
-
"title ILIKE :q OR content ILIKE :q OR author_name ILIKE :q",
|
|
31
|
-
q: "%#{query}%"
|
|
32
|
-
)
|
|
33
|
-
end
|
|
34
|
-
```
|
|
39
|
+
# Quick-filter buttons
|
|
40
|
+
scope :published # uses Post.published
|
|
41
|
+
scope :draft
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
# Default scope (the "Published" button is highlighted on initial load)
|
|
44
|
+
default_scope :published
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
q: "%#{query}%"
|
|
43
|
-
).distinct
|
|
46
|
+
# Sortable columns
|
|
47
|
+
sort :title
|
|
48
|
+
sort :created_at
|
|
49
|
+
default_sort :created_at, :desc
|
|
44
50
|
end
|
|
45
51
|
```
|
|
46
52
|
|
|
47
|
-
|
|
53
|
+
## Search
|
|
48
54
|
|
|
49
55
|
```ruby
|
|
56
|
+
# Single field
|
|
50
57
|
search do |scope, query|
|
|
51
|
-
|
|
52
|
-
terms.reduce(scope) do |current_scope, term|
|
|
53
|
-
current_scope.where("title ILIKE ?", "%#{term}%")
|
|
54
|
-
end
|
|
58
|
+
scope.where("title ILIKE ?", "%#{query}%")
|
|
55
59
|
end
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Full-Text Search (PostgreSQL)
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
# Multiple fields
|
|
61
62
|
search do |scope, query|
|
|
62
63
|
scope.where(
|
|
63
|
-
"
|
|
64
|
-
query
|
|
64
|
+
"title ILIKE :q OR content ILIKE :q OR author_name ILIKE :q",
|
|
65
|
+
q: "%#{query}%"
|
|
65
66
|
)
|
|
66
67
|
end
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Filters
|
|
70
|
-
|
|
71
|
-
Plutonium provides **6 built-in filter types**. Use shorthand symbols or full class names.
|
|
72
|
-
|
|
73
|
-
### Text Filter
|
|
74
68
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
69
|
+
# Across associations
|
|
70
|
+
search do |scope, query|
|
|
71
|
+
scope.joins(:author).where(
|
|
72
|
+
"posts.title ILIKE :q OR users.name ILIKE :q",
|
|
73
|
+
q: "%#{query}%"
|
|
74
|
+
).distinct
|
|
85
75
|
end
|
|
86
76
|
```
|
|
87
77
|
|
|
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
|
|
78
|
+
### The `search` block also powers typeahead
|
|
102
79
|
|
|
103
|
-
|
|
80
|
+
When an association input targets this resource, the dropdown's autocomplete calls the resource's `search` block. Same code, two surfaces.
|
|
104
81
|
|
|
105
|
-
|
|
106
|
-
# Basic
|
|
107
|
-
filter :active, with: :boolean
|
|
108
|
-
|
|
109
|
-
# Custom labels
|
|
110
|
-
filter :published, with: :boolean, true_label: "Published", false_label: "Draft"
|
|
111
|
-
```
|
|
82
|
+
### Without a `search` block — typeahead fallback
|
|
112
83
|
|
|
113
|
-
|
|
84
|
+
The framework falls back to a case-insensitive `LIKE` on the first column it finds, in priority order:
|
|
114
85
|
|
|
115
|
-
|
|
86
|
+
1. The input's `label_method:` option, if it's a real column.
|
|
87
|
+
2. Otherwise the first match from `[name, title, label, slug, display_name, email]`.
|
|
88
|
+
3. Otherwise the relation is returned unfiltered (capped).
|
|
116
89
|
|
|
117
|
-
|
|
90
|
+
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).
|
|
118
91
|
|
|
119
|
-
|
|
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"
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
Renders two date pickers. Both are optional - users can filter with just "from" or just "to".
|
|
92
|
+
## Filters
|
|
151
93
|
|
|
152
|
-
|
|
94
|
+
Six built-in types. Use shorthand symbols:
|
|
153
95
|
|
|
154
|
-
|
|
96
|
+
| Type | Symbol | URL params | Options |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| Text | `:text` | `query` | `predicate:` |
|
|
99
|
+
| Boolean | `:boolean` | `value` | `true_label:`, `false_label:` |
|
|
100
|
+
| Date | `:date` | `value` | `predicate:` |
|
|
101
|
+
| Date range | `:date_range` | `from`, `to` | `from_label:`, `to_label:` |
|
|
102
|
+
| Select | `:select` | `value` | `choices:`, `multiple:` |
|
|
103
|
+
| Association | `:association` | `value` | `class_name:`, `multiple:` |
|
|
155
104
|
|
|
156
105
|
```ruby
|
|
157
|
-
|
|
158
|
-
filter :
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
filter :
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
filter :tags, with: :select, choices: %w[ruby rails js], multiple: true
|
|
106
|
+
filter :title, with: :text, predicate: :contains
|
|
107
|
+
filter :active, with: :boolean
|
|
108
|
+
filter :due_date, with: :date, predicate: :lt
|
|
109
|
+
filter :created_at, with: :date_range
|
|
110
|
+
filter :status, with: :select, choices: %w[draft published]
|
|
111
|
+
filter :category, with: :select, choices: -> { Category.pluck(:name) }
|
|
112
|
+
filter :author, with: :association, class_name: User
|
|
165
113
|
```
|
|
166
114
|
|
|
167
|
-
###
|
|
168
|
-
|
|
169
|
-
Filter by associated record:
|
|
115
|
+
### Custom filter (lambda)
|
|
170
116
|
|
|
171
117
|
```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
|
|
118
|
+
filter :published, with: ->(scope, value) {
|
|
119
|
+
value == "true" ? scope.where.not(published_at: nil) : scope.where(published_at: nil)
|
|
120
|
+
}
|
|
180
121
|
```
|
|
181
122
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
### Filter Summary Table
|
|
123
|
+
### Custom filter class
|
|
185
124
|
|
|
186
|
-
|
|
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
|
-
|
|
195
|
-
### Custom Filter Class
|
|
125
|
+
For reusable filters with multiple inputs:
|
|
196
126
|
|
|
197
127
|
```ruby
|
|
198
128
|
class PriceRangeFilter < Plutonium::Query::Filter
|
|
@@ -205,99 +135,58 @@ class PriceRangeFilter < Plutonium::Query::Filter
|
|
|
205
135
|
def customize_inputs
|
|
206
136
|
input :min, as: :number
|
|
207
137
|
input :max, as: :number
|
|
208
|
-
field :min, placeholder: "Min price..."
|
|
209
|
-
field :max, placeholder: "Max price..."
|
|
210
138
|
end
|
|
211
139
|
end
|
|
212
140
|
|
|
213
|
-
# Use in definition
|
|
214
141
|
filter :price, with: PriceRangeFilter
|
|
215
142
|
```
|
|
216
143
|
|
|
217
|
-
## Scopes
|
|
218
|
-
|
|
219
|
-
Scopes appear as quick filter buttons. They reference model scopes or use inline blocks.
|
|
220
|
-
|
|
221
|
-
### Basic Scopes
|
|
222
|
-
|
|
223
|
-
Reference existing model scopes:
|
|
144
|
+
## Scopes (quick-filter buttons)
|
|
224
145
|
|
|
225
146
|
```ruby
|
|
226
147
|
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
|
|
148
|
+
scope :published # uses Post.published
|
|
149
|
+
scope :draft # uses Post.draft
|
|
243
150
|
|
|
244
|
-
Inline
|
|
151
|
+
# Inline scope — block runs with scope as argument
|
|
152
|
+
scope(:recent) { |s| s.where('created_at > ?', 1.week.ago) }
|
|
245
153
|
|
|
246
|
-
|
|
247
|
-
scope(:mine) { |
|
|
248
|
-
|
|
154
|
+
# Scope with controller context
|
|
155
|
+
scope(:mine) { |s| s.where(author: current_user) }
|
|
156
|
+
end
|
|
249
157
|
```
|
|
250
158
|
|
|
251
|
-
|
|
159
|
+
Named scopes reference a model scope. Inline scopes have access to `current_user`, `current_parent`, `current_scoped_entity`.
|
|
252
160
|
|
|
253
|
-
|
|
161
|
+
### Default scope
|
|
254
162
|
|
|
255
163
|
```ruby
|
|
256
|
-
|
|
257
|
-
scope :published
|
|
258
|
-
scope :draft
|
|
259
|
-
scope :archived
|
|
260
|
-
|
|
261
|
-
default_scope :published
|
|
262
|
-
end
|
|
164
|
+
default_scope :published
|
|
263
165
|
```
|
|
264
166
|
|
|
265
|
-
|
|
266
|
-
- The default scope is
|
|
267
|
-
-
|
|
268
|
-
- Clicking "All" shows all records without any scope filter
|
|
167
|
+
- Applied on initial page load.
|
|
168
|
+
- The default scope button is highlighted (not "All").
|
|
169
|
+
- Clicking "All" shows the unscoped collection.
|
|
269
170
|
|
|
270
171
|
## Sorting
|
|
271
172
|
|
|
272
|
-
### Define Sortable Fields
|
|
273
|
-
|
|
274
173
|
```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
|
-
```
|
|
174
|
+
sort :title
|
|
175
|
+
sort :created_at
|
|
284
176
|
|
|
285
|
-
|
|
177
|
+
sorts :title, :created_at, :view_count # shorthand
|
|
286
178
|
|
|
287
|
-
```ruby
|
|
288
|
-
# Field and direction
|
|
289
179
|
default_sort :created_at, :desc
|
|
290
|
-
default_sort :title, :asc
|
|
291
180
|
|
|
292
|
-
# Complex
|
|
181
|
+
# Complex with a block
|
|
293
182
|
default_sort { |scope| scope.order(featured: :desc, created_at: :desc) }
|
|
294
183
|
```
|
|
295
184
|
|
|
296
|
-
|
|
185
|
+
Framework default (nothing declared, no user sort): `id DESC`.
|
|
297
186
|
|
|
298
|
-
## URL
|
|
187
|
+
## URL parameters
|
|
299
188
|
|
|
300
|
-
Query
|
|
189
|
+
Query params are namespaced under `q`:
|
|
301
190
|
|
|
302
191
|
```
|
|
303
192
|
/posts?q[search]=rails
|
|
@@ -308,43 +197,37 @@ Query parameters are structured under `q`:
|
|
|
308
197
|
/posts?q[sort_fields][]=created_at&q[sort_directions][created_at]=desc
|
|
309
198
|
```
|
|
310
199
|
|
|
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
|
|
200
|
+
## Performance tips
|
|
330
201
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
202
|
+
- **Add indexes** for filtered and sorted columns.
|
|
203
|
+
- **Use `.distinct`** when joining associations in search — duplicate rows otherwise.
|
|
204
|
+
- **Prefer scopes over filters** for queries used often (no input parsing).
|
|
205
|
+
- **`LIKE '%q%'` can't use a b-tree index** — for large tables, use `pg_search` or a trigram/GIN/full-text index.
|
|
335
206
|
|
|
336
|
-
|
|
337
|
-
default_scope :active
|
|
207
|
+
## Full-text search with `pg_search`
|
|
338
208
|
|
|
339
|
-
|
|
340
|
-
|
|
209
|
+
```ruby
|
|
210
|
+
# Model
|
|
211
|
+
class Post < ResourceRecord
|
|
212
|
+
include PgSearch::Model
|
|
213
|
+
pg_search_scope :search_content, against: %i[title content]
|
|
214
|
+
end
|
|
341
215
|
|
|
342
|
-
|
|
343
|
-
|
|
216
|
+
# Definition
|
|
217
|
+
search do |scope, query|
|
|
218
|
+
scope.search_content(query)
|
|
344
219
|
end
|
|
345
220
|
```
|
|
346
221
|
|
|
222
|
+
## Common issues
|
|
223
|
+
|
|
224
|
+
- **Filter not showing up** — make sure the attribute is in `permitted_attributes_for_index` on the policy.
|
|
225
|
+
- **Slow search on large tables** — `LIKE '%q%'` can't be indexed by a b-tree. Switch to FTS or trigram.
|
|
226
|
+
- **Duplicate rows in results** — add `.distinct` when joining associations.
|
|
227
|
+
- **Typeahead works on small dev tables but slows in production** — same b-tree issue. Write an explicit `search` block backed by a proper index.
|
|
228
|
+
|
|
347
229
|
## Related
|
|
348
230
|
|
|
349
|
-
- [
|
|
350
|
-
- [
|
|
231
|
+
- [Reference › Resource › Query](/reference/resource/query) — full surface
|
|
232
|
+
- [Adding resources](./adding-resources) — basic resource setup
|
|
233
|
+
- [Authorization](./authorization) — `permitted_attributes_for_index` gates which fields can be filtered
|
data/docs/guides/testing.md
CHANGED
|
@@ -147,8 +147,9 @@ Output: `test/integration/<portal>_portal/<resource>_test.rb`.
|
|
|
147
147
|
- **Nested resources need `parent:` in the DSL AND a parent record** from `parent_record!`. Both are required for path interpolation.
|
|
148
148
|
- **`PortalAccess` uses `portal_access_for`**, not `resource_tests_for`. Don't mix them on the same class.
|
|
149
149
|
|
|
150
|
-
##
|
|
150
|
+
## Related
|
|
151
151
|
|
|
152
|
-
- [
|
|
153
|
-
- [
|
|
154
|
-
- [
|
|
152
|
+
- [Reference › Testing](/reference/testing/) — full DSL reference, all concern stubs, override hooks
|
|
153
|
+
- [Authorization](./authorization) — write the policy this concern verifies
|
|
154
|
+
- [Multi-tenancy](./multi-tenancy) — entity scoping that drives nested-resource tests
|
|
155
|
+
- [Authentication](./authentication) — Rodauth setup behind the default login flow
|