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.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +574 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +167 -302
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +674 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +9 -6
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +44 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1010 -1214
  14. data/app/assets/plutonium.js.map +3 -3
  15. data/app/assets/plutonium.min.js +52 -51
  16. data/app/assets/plutonium.min.js.map +3 -3
  17. data/docs/.vitepress/config.ts +38 -29
  18. data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
  19. data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
  20. data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
  21. data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
  22. data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
  23. data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
  24. data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
  25. data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
  26. data/docs/.vitepress/theme/custom.css +144 -0
  27. data/docs/.vitepress/theme/index.ts +58 -1
  28. data/docs/getting-started/index.md +33 -57
  29. data/docs/getting-started/installation.md +37 -80
  30. data/docs/getting-started/tutorial/02-first-resource.md +17 -8
  31. data/docs/getting-started/tutorial/03-authentication.md +31 -23
  32. data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
  33. data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
  34. data/docs/getting-started/tutorial/07-author-portal.md +8 -0
  35. data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
  36. data/docs/getting-started/tutorial/index.md +4 -5
  37. data/docs/guides/adding-resources.md +66 -377
  38. data/docs/guides/authentication.md +98 -462
  39. data/docs/guides/authorization.md +124 -370
  40. data/docs/guides/creating-packages.md +93 -298
  41. data/docs/guides/custom-actions.md +126 -441
  42. data/docs/guides/customizing-ui.md +258 -0
  43. data/docs/guides/index.md +49 -52
  44. data/docs/guides/multi-tenancy.md +123 -186
  45. data/docs/guides/nested-resources.md +137 -396
  46. data/docs/guides/search-filtering.md +127 -238
  47. data/docs/guides/testing.md +10 -5
  48. data/docs/guides/theming.md +168 -405
  49. data/docs/guides/troubleshooting.md +5 -3
  50. data/docs/guides/user-invites.md +112 -425
  51. data/docs/guides/user-profile.md +82 -241
  52. data/docs/index.md +10 -219
  53. data/docs/public/asciinema/home-scaffold.cast +305 -0
  54. data/docs/public/images/guides/custom-actions-bulk.png +0 -0
  55. data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
  56. data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
  57. data/docs/public/images/guides/nested-inputs.png +0 -0
  58. data/docs/public/images/guides/nested-resources-tab.png +0 -0
  59. data/docs/public/images/guides/search-filtering-index.png +0 -0
  60. data/docs/public/images/guides/search-filtering-panel.png +0 -0
  61. data/docs/public/images/guides/theming-after.png +0 -0
  62. data/docs/public/images/guides/theming-before.png +0 -0
  63. data/docs/public/images/guides/user-invites-landing.png +0 -0
  64. data/docs/public/images/guides/user-profile-edit.png +0 -0
  65. data/docs/public/images/guides/user-profile-show.png +0 -0
  66. data/docs/public/images/home-index.png +0 -0
  67. data/docs/public/images/home-new.png +0 -0
  68. data/docs/public/images/home-show.png +0 -0
  69. data/docs/public/images/tutorial/02-empty-index.png +0 -0
  70. data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
  71. data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
  72. data/docs/public/images/tutorial/02-new-form.png +0 -0
  73. data/docs/public/images/tutorial/03-create-account.png +0 -0
  74. data/docs/public/images/tutorial/03-login.png +0 -0
  75. data/docs/public/images/tutorial/04-admin-index.png +0 -0
  76. data/docs/public/images/tutorial/05-actions-menu.png +0 -0
  77. data/docs/public/images/tutorial/05-row-actions.png +0 -0
  78. data/docs/public/images/tutorial/06-comments-tab.png +0 -0
  79. data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
  80. data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
  81. data/docs/public/images/tutorial/07-author-portal.png +0 -0
  82. data/docs/public/images/tutorial/08-customized-index.png +0 -0
  83. data/docs/reference/app/generators.md +517 -0
  84. data/docs/reference/app/index.md +158 -0
  85. data/docs/reference/app/packages.md +146 -0
  86. data/docs/reference/app/portals.md +377 -0
  87. data/docs/reference/auth/accounts.md +229 -0
  88. data/docs/reference/auth/index.md +88 -0
  89. data/docs/reference/auth/profile.md +185 -0
  90. data/docs/reference/behavior/controllers.md +395 -0
  91. data/docs/reference/behavior/index.md +22 -0
  92. data/docs/reference/behavior/interactions.md +341 -0
  93. data/docs/reference/behavior/policies.md +417 -0
  94. data/docs/reference/index.md +67 -48
  95. data/docs/reference/resource/actions.md +423 -0
  96. data/docs/reference/resource/definition.md +508 -0
  97. data/docs/reference/resource/index.md +50 -0
  98. data/docs/reference/resource/model.md +348 -0
  99. data/docs/reference/resource/query.md +305 -0
  100. data/docs/reference/tenancy/entity-scoping.md +368 -0
  101. data/docs/reference/tenancy/index.md +36 -0
  102. data/docs/reference/tenancy/invites.md +400 -0
  103. data/docs/reference/tenancy/nested-resources.md +267 -0
  104. data/docs/reference/testing/index.md +287 -0
  105. data/docs/reference/ui/assets.md +400 -0
  106. data/docs/reference/ui/components.md +165 -0
  107. data/docs/reference/ui/displays.md +104 -0
  108. data/docs/reference/ui/forms.md +284 -0
  109. data/docs/reference/ui/index.md +30 -0
  110. data/docs/reference/ui/layouts.md +106 -0
  111. data/docs/reference/ui/pages.md +189 -0
  112. data/docs/reference/ui/tables.md +121 -0
  113. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
  114. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
  115. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  116. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  117. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  118. data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
  119. data/gemfiles/rails_7.gemfile.lock +1 -1
  120. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  121. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  122. data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
  123. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  124. data/lib/generators/pu/invites/install_generator.rb +45 -0
  125. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
  126. data/lib/generators/pu/profile/conn_generator.rb +2 -2
  127. data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
  128. data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
  129. data/lib/generators/pu/rodauth/account_generator.rb +2 -1
  130. data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
  131. data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
  132. data/lib/generators/pu/rodauth/views_generator.rb +0 -2
  133. data/lib/generators/pu/saas/membership/USAGE +4 -1
  134. data/lib/generators/pu/saas/setup_generator.rb +16 -4
  135. data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
  136. data/lib/plutonium/definition/base.rb +1 -1
  137. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  138. data/lib/plutonium/helpers/turbo_helper.rb +30 -0
  139. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  140. data/lib/plutonium/resource/controller.rb +1 -0
  141. data/lib/plutonium/resource/controllers/crud_actions.rb +23 -5
  142. data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
  143. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  144. data/lib/plutonium/resource/policy.rb +7 -0
  145. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  146. data/lib/plutonium/ui/component/methods.rb +5 -0
  147. data/lib/plutonium/ui/form/base.rb +23 -3
  148. data/lib/plutonium/ui/form/components/json.rb +58 -0
  149. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  150. data/lib/plutonium/ui/form/components/secure_association.rb +103 -22
  151. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  152. data/lib/plutonium/ui/form/interaction.rb +1 -1
  153. data/lib/plutonium/ui/form/resource.rb +0 -4
  154. data/lib/plutonium/ui/form/theme.rb +1 -1
  155. data/lib/plutonium/ui/grid/resource.rb +1 -1
  156. data/lib/plutonium/ui/layout/base.rb +1 -0
  157. data/lib/plutonium/ui/page/base.rb +0 -7
  158. data/lib/plutonium/ui/page/edit.rb +1 -1
  159. data/lib/plutonium/ui/page/index.rb +4 -4
  160. data/lib/plutonium/ui/page/new.rb +1 -1
  161. data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
  162. data/lib/plutonium/ui/table/resource.rb +1 -1
  163. data/lib/plutonium/version.rb +1 -1
  164. data/lib/plutonium.rb +8 -0
  165. data/lib/tasks/release.rake +15 -1
  166. data/package.json +13 -10
  167. data/src/css/slim_select.css +4 -0
  168. data/src/js/controllers/form_controller.js +5 -4
  169. data/src/js/controllers/slim_select_controller.js +61 -0
  170. data/src/js/turbo/turbo_actions.js +33 -0
  171. data/yarn.lock +661 -544
  172. metadata +86 -33
  173. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  174. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  175. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  176. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  177. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  178. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  179. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  180. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  181. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  182. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  183. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  184. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  185. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  186. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  187. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  188. data/docs/reference/assets/index.md +0 -496
  189. data/docs/reference/controller/index.md +0 -412
  190. data/docs/reference/definition/actions.md +0 -462
  191. data/docs/reference/definition/fields.md +0 -383
  192. data/docs/reference/definition/index.md +0 -326
  193. data/docs/reference/definition/query.md +0 -351
  194. data/docs/reference/generators/index.md +0 -648
  195. data/docs/reference/interaction/index.md +0 -449
  196. data/docs/reference/model/features.md +0 -248
  197. data/docs/reference/model/index.md +0 -218
  198. data/docs/reference/policy/index.md +0 -456
  199. data/docs/reference/portal/index.md +0 -379
  200. data/docs/reference/views/forms.md +0 -411
  201. data/docs/reference/views/index.md +0 -544
@@ -1,198 +1,130 @@
1
1
  # Search and Filtering
2
2
 
3
- This guide covers implementing search, filters, scopes, and sorting.
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
- ## Overview
5
+ ## Goal
6
6
 
7
- Plutonium provides built-in support for:
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
- ## Search
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
+ ![Search box, scope tabs, filter button, sortable columns](/images/guides/search-filtering-index.png)
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
- Define global search in the definition:
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 ?", "%#{query}%")
33
+ scope.where("title ILIKE :q OR body ILIKE :q", q: "%#{query}%")
21
34
  end
22
- end
23
- ```
24
35
 
25
- ### Multi-Field Search
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
- ```ruby
28
- search do |scope, query|
29
- scope.where(
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
- ### Search with Associations
45
+ # Default scope (the "Published" button is highlighted on initial load)
46
+ default_scope :published
37
47
 
38
- ```ruby
39
- search do |scope, query|
40
- scope.joins(:author).where(
41
- "posts.title ILIKE :q OR users.name ILIKE :q",
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
- ### Split Search Terms
55
+ ## Search
48
56
 
49
57
  ```ruby
58
+ # Single field
50
59
  search do |scope, query|
51
- terms = query.split(/\s+/)
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
- ```ruby
63
+ # Multiple fields
61
64
  search do |scope, query|
62
65
  scope.where(
63
- "to_tsvector('english', title || ' ' || body) @@ plainto_tsquery('english', ?)",
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
- Plutonium provides **6 built-in filter types**. Use shorthand symbols or full class names.
72
-
73
- ### Text Filter
74
-
75
- String/text filtering with pattern matching:
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
- #### Available Predicates
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
- | 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 |
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
- ### Date Range Filter
84
+ ### Without a `search` block — typeahead fallback
137
85
 
138
- Filter between two dates (from/to):
86
+ The framework falls back to a case-insensitive `LIKE` on the first column it finds, in priority order:
139
87
 
140
- ```ruby
141
- # Basic
142
- filter :created_at, with: :date_range
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
- # Custom labels
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
- Renders two date pickers. Both are optional - users can filter with just "from" or just "to".
94
+ ## Filters
151
95
 
152
- ### Select Filter
96
+ Six built-in types. Use shorthand symbols:
153
97
 
154
- Filter from predefined choices:
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
- # 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
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
- ### Association Filter
168
-
169
- Filter by associated record:
117
+ ### Custom filter (lambda)
170
118
 
171
119
  ```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
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
- 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:` |
125
+ ### Custom filter class
194
126
 
195
- ### Custom Filter Class
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
- ## Scopes
146
+ Clicking **Filter** opens a slideover with one input per declared filter:
218
147
 
219
- Scopes appear as quick filter buttons. They reference model scopes or use inline blocks.
148
+ ![Filter slideover](/images/guides/search-filtering-panel.png)
220
149
 
221
- ### Basic Scopes
222
-
223
- Reference existing model scopes:
150
+ ## Scopes (quick-filter buttons)
224
151
 
225
152
  ```ruby
226
153
  class PostDefinition < ResourceDefinition
227
- scope :published # Uses Post.published
228
- scope :draft # Uses Post.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 scopes have access to controller context like `current_user`:
157
+ # Inline scope block runs with scope as argument
158
+ scope(:recent) { |s| s.where('created_at > ?', 1.week.ago) }
245
159
 
246
- ```ruby
247
- scope(:mine) { |scope| scope.where(author: current_user) }
248
- scope(:my_team) { |scope| scope.where(team: current_user.team) }
160
+ # Scope with controller context
161
+ scope(:mine) { |s| s.where(author: current_user) }
162
+ end
249
163
  ```
250
164
 
251
- ### Default Scope
165
+ Named scopes reference a model scope. Inline scopes have access to `current_user`, `current_parent`, `current_scoped_entity`.
252
166
 
253
- Set a scope as default:
167
+ ### Default scope
254
168
 
255
169
  ```ruby
256
- class PostDefinition < ResourceDefinition
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
- When a default scope is set:
266
- - The default scope is applied on initial page load
267
- - The default scope button is highlighted (not "All")
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
- class PostDefinition < ResourceDefinition
276
- sort :title
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
- ### Default Sort Order
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 sorting with block
187
+ # Complex with a block
293
188
  default_sort { |scope| scope.order(featured: :desc, created_at: :desc) }
294
189
  ```
295
190
 
296
- **Note:** Default sort only applies when no sort params are provided. The system default is `:id, :desc`.
191
+ Framework default (nothing declared, no user sort): `id DESC`.
297
192
 
298
- ## URL Parameters
193
+ ## URL parameters
299
194
 
300
- Query parameters are structured under `q`:
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
- ## Complete Example
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
- # Quick scopes (reference model scopes)
332
- scope :active
333
- scope :featured
334
- scope(:recent) { |scope| scope.where("created_at > ?", 1.week.ago) }
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
- # Default scope
337
- default_scope :active
213
+ ## Full-text search with `pg_search`
338
214
 
339
- # Sortable columns
340
- sorts :name, :price, :created_at
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
- # Default sort: newest first
343
- default_sort :created_at, :desc
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
- - [Custom Actions](./custom-actions)
350
- - [Authorization](./authorization)
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
@@ -101,7 +101,11 @@ current_account # uses portal from DSL
101
101
  with_portal(:org) { ... } # scoped portal switch
102
102
  ```
103
103
 
104
- ### Non-Rodauth auth
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
- ## See also
154
+ ## Related
151
155
 
152
- - [Authorization](/guides/authorization) — write the policy this concern verifies
153
- - [Multi-tenancy](/guides/multi-tenancy) — entity scoping that drives nested-resource tests
154
- - [Authentication](/guides/authentication) — Rodauth setup behind the default login flow
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