recourse 3.0.4 → 4.1.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/CHANGELOG.md +245 -0
- data/README.md +150 -20
- data/app/controllers/bookmarks_controller.rb +7 -0
- data/app/controllers/concerns/recourse/landing.rb +49 -0
- data/app/controllers/concerns/recourse/list_resolution.rb +28 -0
- data/app/controllers/concerns/recourse/paging.rb +17 -0
- data/app/controllers/concerns/recourse/parent_naming.rb +24 -0
- data/app/controllers/concerns/recourse/parent_resolution.rb +63 -0
- data/app/controllers/concerns/recourse/reference_resolution.rb +70 -0
- data/app/controllers/concerns/recourse/resource_resolution.rb +68 -0
- data/app/controllers/concerns/recourse/zoning.rb +29 -0
- data/app/controllers/recourse/base_controller.rb +85 -0
- data/app/controllers/recourse/bookmarks_controller.rb +76 -0
- data/app/controllers/recourses_controller.rb +6 -25
- data/app/javascript/recourse/bookmark_controller.js +100 -0
- data/app/javascript/recourse/clear_controller.js +22 -0
- data/app/javascript/recourse/combobox_controller.js +68 -0
- data/app/javascript/recourse/confirm.js +46 -0
- data/app/javascript/recourse/deselect_controller.js +37 -0
- data/app/javascript/recourse/flash.js +35 -0
- data/app/javascript/recourse/limit_controller.js +23 -0
- data/app/javascript/recourse/phone_controller.js +33 -0
- data/app/javascript/recourse/relative_time_controller.js +45 -0
- data/app/javascript/recourse/reveal_controller.js +16 -0
- data/app/javascript/recourse/scheme_controller.js +51 -0
- data/app/javascript/recourse/search_controller.js +78 -0
- data/app/javascript/recourse/shortcuts_controller.js +42 -0
- data/app/javascript/recourse/timezone_controller.js +27 -0
- data/app/javascript/recourse/toast_controller.js +35 -0
- data/app/javascript/recourse/tooltip_controller.js +16 -0
- data/app/javascript/recourse/written.js +29 -0
- data/app/javascript/recourse/written_controller.js +28 -0
- data/app/views/layouts/recourses.html.erb +442 -0
- data/app/views/recourses/_breadcrumb.html.erb +35 -0
- data/app/views/recourses/_card.html.erb +33 -0
- data/app/views/recourses/_color.html.erb +20 -0
- data/app/views/recourses/_combobox.html.erb +68 -0
- data/app/views/recourses/_confirm.html.erb +14 -0
- data/app/views/recourses/_fields.html.erb +3 -0
- data/app/views/recourses/_flash.html.erb +26 -0
- data/app/views/recourses/_footer.html.erb +22 -0
- data/app/views/recourses/_form.html.erb +11 -0
- data/app/views/recourses/_none.html.erb +1 -0
- data/app/views/recourses/_results.html.erb +10 -0
- data/app/views/recourses/_row.html.erb +4 -10
- data/app/views/recourses/_scheme.html.erb +14 -0
- data/app/views/recourses/_search.html.erb +24 -0
- data/app/views/recourses/_sidebar.html.erb +40 -0
- data/app/views/recourses/_table.html.erb +47 -42
- data/app/views/recourses/_values.html.erb +3 -0
- data/app/views/recourses/edit.html.erb +10 -0
- data/app/views/recourses/index.html.erb +27 -24
- data/app/views/recourses/new.html.erb +3 -0
- data/app/views/recourses/show.html.erb +10 -0
- data/config/locales/recourse.en.yml +72 -0
- data/lib/recourse/bookmarks.rb +51 -0
- data/lib/recourse/broadcasting.rb +33 -0
- data/lib/recourse/colors.rb +32 -0
- data/lib/recourse/columns.rb +80 -0
- data/lib/recourse/controllers.rb +26 -0
- data/lib/recourse/engine.rb +33 -13
- data/lib/recourse/helpers/actions.rb +56 -0
- data/lib/recourse/helpers/bookmarks.rb +88 -0
- data/lib/recourse/helpers/buttons.rb +65 -0
- data/lib/recourse/helpers/cards.rb +67 -0
- data/lib/recourse/helpers/cells.rb +89 -0
- data/lib/recourse/helpers/choices.rb +49 -0
- data/lib/recourse/helpers/colors.rb +20 -0
- data/lib/recourse/helpers/comboboxes.rb +87 -0
- data/lib/recourse/helpers/constraints.rb +92 -0
- data/lib/recourse/helpers/counters.rb +75 -0
- data/lib/recourse/helpers/deletions.rb +87 -0
- data/lib/recourse/helpers/details.rb +41 -0
- data/lib/recourse/helpers/examples.rb +35 -0
- data/lib/recourse/helpers/fields.rb +78 -0
- data/lib/recourse/helpers/filters.rb +91 -0
- data/lib/recourse/helpers/formats.rb +94 -0
- data/lib/recourse/helpers/inputs.rb +92 -0
- data/lib/recourse/helpers/kinds.rb +87 -0
- data/lib/recourse/helpers/limits.rb +37 -0
- data/lib/recourse/helpers/names.rb +44 -0
- data/lib/recourse/helpers/navigation.rb +75 -0
- data/lib/recourse/helpers/parents.rb +82 -0
- data/lib/recourse/helpers/pictures.rb +33 -0
- data/lib/recourse/helpers/references.rb +99 -0
- data/lib/recourse/helpers/refreshes.rb +27 -0
- data/lib/recourse/helpers/resources.rb +47 -0
- data/lib/recourse/helpers/routing.rb +28 -0
- data/lib/recourse/helpers/rows.rb +33 -0
- data/lib/recourse/helpers/schemes.rb +14 -0
- data/lib/recourse/helpers/searches.rb +57 -0
- data/lib/recourse/helpers/shortcuts.rb +30 -0
- data/lib/recourse/helpers/sidebars.rb +39 -0
- data/lib/recourse/helpers/sorts.rb +62 -0
- data/lib/recourse/helpers/tabs.rb +82 -0
- data/lib/recourse/helpers/times.rb +43 -0
- data/lib/recourse/helpers/values.rb +64 -0
- data/lib/recourse/helpers/zones.rb +38 -0
- data/lib/recourse/helpers.rb +81 -0
- data/lib/recourse/icons.rb +21 -0
- data/lib/recourse/limits.rb +12 -0
- data/lib/recourse/orders.rb +21 -0
- data/lib/recourse/recoursive.rb +66 -19
- data/lib/recourse/registry.rb +36 -0
- data/lib/recourse/routes/nested.rb +36 -0
- data/lib/recourse/routes.rb +57 -0
- data/lib/recourse/routing.rb +11 -32
- data/lib/recourse/schemes.rb +6 -0
- data/lib/recourse/scopes.rb +37 -0
- data/lib/recourse/search.rb +73 -0
- data/lib/recourse/searchable/columns.rb +99 -0
- data/lib/recourse/searchable/filters.rb +58 -0
- data/lib/recourse/searchable/terms.rb +56 -0
- data/lib/recourse/searchable.rb +47 -22
- data/lib/recourse/titles.rb +56 -0
- data/lib/recourse/version.rb +2 -1
- data/lib/recourse/writes.rb +12 -0
- data/lib/recourse/zones.rb +6 -0
- data/lib/recourse.rb +80 -7
- data/vendor/recourse/bootstrap-icons.min.css +5 -0
- data/vendor/recourse/bootstrap.bundle.min.js +9 -0
- data/vendor/recourse/bootstrap.min.css +2 -0
- data/vendor/recourse/fonts/bootstrap-icons.woff +0 -0
- data/vendor/recourse/fonts/bootstrap-icons.woff2 +0 -0
- data/vendor/recourse/stimulus.js +2563 -0
- metadata +170 -30
- data/Rakefile +0 -6
- data/app/helpers/navigable_helper.rb +0 -36
- data/app/helpers/recoursive_helper.rb +0 -10
- data/app/helpers/searchable_helper.rb +0 -54
- data/db/migrate/20260323234318_add_baby_to_posts.rb +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4258e3870b7a65f742563e24f02f45e2c2716bcc22666fbbb63867f70b656e71
|
|
4
|
+
data.tar.gz: 5d2ae92ccde9995457f78060fd9f22d74fd3971bae0c5de68ec7aa022e290603
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 343ca9a2eba9663618236339649c017f00934d3d01cf8741e6b152a8cc6c6bf17036eff91ea63f46acf5cf804e65ae7f59b68ffdf541ae1bd06e9c5dd1332793
|
|
7
|
+
data.tar.gz: 5f5946a02e7d397bec49ac1089912933f730758215ea4e18559f003a5a13522e5de587db0abb9c75d00a9fab5ba7978a73858a634bfda05956d0267c70617df0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
For more information about changelogs, check [Keep a Changelog](http://keepachangelog.com) and
|
|
6
|
+
[Vandamme](http://tech-angels.github.io/vandamme).
|
|
7
|
+
|
|
8
|
+
## 4.1.0 - 2026-09-08
|
|
9
|
+
|
|
10
|
+
* [Feature] Rows with nothing in the sorted column come last
|
|
11
|
+
|
|
12
|
+
Whichever way a table is ordered — by the model's own `recourse_order`, or by the
|
|
13
|
+
heading a reader clicked — the rows with nothing in that column now come after every
|
|
14
|
+
row that has something, where the database's default put them first on a descending
|
|
15
|
+
sort. A `recourse_order` given as a Symbol or a Hash earns this; a SQL string a host
|
|
16
|
+
wrote is still taken as written, so `'size desc nulls last'` and `{ size: :desc }` now
|
|
17
|
+
say the same thing.
|
|
18
|
+
|
|
19
|
+
## 4.0.0 - 2026-09-08
|
|
20
|
+
|
|
21
|
+
Version 4 is a rewrite, developed under the working name `drive` and released here
|
|
22
|
+
because it is the same library: the module is still `Recourse` and the entry point is
|
|
23
|
+
still one word in `config/routes.rb`.
|
|
24
|
+
|
|
25
|
+
Version 3 drew the routes and served one screen — a paginated, searchable index — and
|
|
26
|
+
left the controller, the other six actions and every form to the host app. Version 4
|
|
27
|
+
serves all seven, defines the controllers itself, and works out what each screen should
|
|
28
|
+
look like by reading the model: its validators, its associations, its column types and
|
|
29
|
+
its indexes. Most of what a host used to override is now something it no longer writes.
|
|
30
|
+
|
|
31
|
+
* [BREAKING CHANGE] Rails 8.1 and Ruby 3.2 are the minimum; so are Pagy 43 and Ransack 4.4
|
|
32
|
+
* [BREAKING CHANGE] `recourses` defines the controller as well as the routes
|
|
33
|
+
|
|
34
|
+
A host no longer writes `class PostsController < RecoursesController` for each
|
|
35
|
+
resource. The controller is defined as the route is drawn, and a host that wants one
|
|
36
|
+
of its own still writes it — the gem only fills the gap. `RecoursesController`
|
|
37
|
+
changes meaning with that: it is no longer the class each resource subclasses but the
|
|
38
|
+
one they all inherit, and a host defines it — `class RecoursesController <
|
|
39
|
+
Recourse::BaseController` — to put a `before_action` above every screen at once.
|
|
40
|
+
|
|
41
|
+
* [BREAKING CHANGE] `search_field`, `search_prompt` and `searchable_fields` are no
|
|
42
|
+
longer yours to define
|
|
43
|
+
|
|
44
|
+
The search box looks through every indexed string column the table shows, plus the
|
|
45
|
+
label behind a foreign key too long to list, and says so in its own placeholder.
|
|
46
|
+
|
|
47
|
+
* [BREAKING CHANGE] The Ransack hooks default to something instead of to nothing
|
|
48
|
+
|
|
49
|
+
`ransackable_attributes`, `ransackable_associations`, `ransortable_attributes` and
|
|
50
|
+
`filter_fields` are still yours to override, but a model that says nothing is now
|
|
51
|
+
fully searchable, sortable and filterable: a column is sortable when an index covers
|
|
52
|
+
it, an enum and a boolean earn a filter, and so does each `belongs_to`.
|
|
53
|
+
|
|
54
|
+
* [BREAKING CHANGE] A `filter_fields` entry is keyed by the predicate and carries its
|
|
55
|
+
options: `{ 'state_id_in' => { label: 'Home state' } }`
|
|
56
|
+
* [BREAKING CHANGE] `recourse_searchable?`, `recourse_sortable?`, `recourse_cachable?`
|
|
57
|
+
and `recourse_timestamps` are gone
|
|
58
|
+
|
|
59
|
+
The first two follow from the indexes. The table is a fragment keyed on the
|
|
60
|
+
relation, so caching needs no switch. `recourse_displayed` names a timestamp back.
|
|
61
|
+
|
|
62
|
+
* [BREAKING CHANGE] `Recourse.resources`, `navigation_links` and `NavigableHelper` are gone
|
|
63
|
+
|
|
64
|
+
The gem draws its own sidebar from the resources `recourses` declared. An icon comes
|
|
65
|
+
from the `unicon` gem, named by `recourse_icon`, which defaults to the model's name.
|
|
66
|
+
|
|
67
|
+
* [BREAKING CHANGE] `search_highlight` takes the column, not the model:
|
|
68
|
+
`search_highlight(post.content, :content)`
|
|
69
|
+
* [BREAKING CHANGE] A row partial sorts with `sort_header`, not Ransack's `sort_link`
|
|
70
|
+
* [BREAKING CHANGE] A row reads in the order its columns' kinds earn — state, keys,
|
|
71
|
+
words, flags, long values, dates, timestamps, counts — and the schema's order inside each
|
|
72
|
+
* [BREAKING CHANGE] A link reads as its host rather than as the whole address
|
|
73
|
+
* [Feature] Every action is served: index, show, new, create, edit, update and destroy
|
|
74
|
+
* [Feature] A form field is chosen to suit each column, and carries the rules the
|
|
75
|
+
model's validators state — a length becomes a `maxlength`, a format becomes a
|
|
76
|
+
`pattern`, a numericality becomes a numeric keyboard
|
|
77
|
+
* [Feature] An enum becomes a badge on a page and a menu in a form; a foreign key
|
|
78
|
+
becomes a menu of the records it points at, or a field to type into where there are
|
|
79
|
+
too many to list — and a typed label naming two rows is refused rather than guessed at
|
|
80
|
+
* [Feature] Values are formatted by what the column holds: delimited integers, money,
|
|
81
|
+
phone numbers, a month by its name, a URL as a link, JSON as JSON, a list as a count
|
|
82
|
+
that opens, and a timestamp in the reader's own time zone saying how long ago
|
|
83
|
+
* [Feature] Nested resources are drawn as tabs on the parent's card, counted where a
|
|
84
|
+
counter cache exists; a nested `create` with no index is a button on the parent
|
|
85
|
+
* [Feature] Active Record Encryption is respected throughout: an encrypted column never
|
|
86
|
+
reaches a table, arrives masked on a record's own page behind a `Show`, and is offered
|
|
87
|
+
in the clear on the form that edits it
|
|
88
|
+
* [Feature] A viewer keeps a row: `Recourse.bookmarks` names the viewer's rows, and every
|
|
89
|
+
table that can hold one opens with a square, kept rows first
|
|
90
|
+
* [Feature] A route named `exit` gives the sidebar a log-out button
|
|
91
|
+
* [Feature] Turbo drives the screens — frames, live refreshes of an open index when a row
|
|
92
|
+
changes, a delete that names what goes with it before it goes, and the row a write
|
|
93
|
+
landed on marked
|
|
94
|
+
* [Feature] A reader picks how many rows a page shows, and whether the page is light or dark
|
|
95
|
+
* [Feature] Bootstrap 6 and Bootstrap Icons are vendored and served by the engine, so a
|
|
96
|
+
host with no asset pipeline and no CDN still gets styled screens
|
|
97
|
+
* [Feature] `Recourse.color = :purple` restyles every screen at once
|
|
98
|
+
* [Feature] Model hooks: `recourse_label`, `recourse_hidden`, `recourse_displayed`,
|
|
99
|
+
`recourse_order`, `recourse_icon`, `recourse_includes`, `recourse_comment` and
|
|
100
|
+
`recourse_broadcasts?`
|
|
101
|
+
* [Feature] Every string the gem shows is a key in `config/locales/recourse.en.yml`
|
|
102
|
+
|
|
103
|
+
## 3.0.4 - 2026-07-24
|
|
104
|
+
|
|
105
|
+
* [Feature] Add "Inquiries" icon
|
|
106
|
+
|
|
107
|
+
## 3.0.3 - 2026-07-24
|
|
108
|
+
|
|
109
|
+
* [Feature] Add "Agents" icon
|
|
110
|
+
|
|
111
|
+
## 3.0.2 - 2026-07-23
|
|
112
|
+
|
|
113
|
+
* [Feature] Add "Contacts" icon
|
|
114
|
+
|
|
115
|
+
## 3.0.1 - 2026-07-22
|
|
116
|
+
|
|
117
|
+
* [Feature] Add searchable_fields to models
|
|
118
|
+
|
|
119
|
+
## 3.0.0 - 2026-07-22
|
|
120
|
+
|
|
121
|
+
* [BREAKING CHANGE] Replace single filter_field with multiple filter_fields
|
|
122
|
+
|
|
123
|
+
## 2.0.2 - 2026-07-20
|
|
124
|
+
|
|
125
|
+
* [Feature] Add "Contract", "Profile", "CRM" icons
|
|
126
|
+
|
|
127
|
+
## 2.0.1 - 2026-07-13
|
|
128
|
+
|
|
129
|
+
* [Fix] Improve search bar responsiveness
|
|
130
|
+
|
|
131
|
+
## 2.0.0 - 2026-07-13
|
|
132
|
+
|
|
133
|
+
* [BREAKING CHANGE] Restyle table and search field to take advantage of Bootstrap 6
|
|
134
|
+
|
|
135
|
+
## 1.4.6 - 2026-07-10
|
|
136
|
+
|
|
137
|
+
* [Feature] Replace "Benches" with "Markets" icons
|
|
138
|
+
|
|
139
|
+
## 1.4.5 - 2026-06-24
|
|
140
|
+
|
|
141
|
+
* [Feature] Add "Benches" icons
|
|
142
|
+
|
|
143
|
+
## 1.4.4 - 2026-06-24
|
|
144
|
+
|
|
145
|
+
* [Feature] Add "Platforms" icons
|
|
146
|
+
|
|
147
|
+
## 1.4.3 - 2026-06-24
|
|
148
|
+
|
|
149
|
+
* [Feature] Add "Brands" icons
|
|
150
|
+
|
|
151
|
+
## 1.4.2 - 2026-06-24
|
|
152
|
+
|
|
153
|
+
* [Feature] Add common recourse icons
|
|
154
|
+
* [Feature] Add common acronyms, e.g.: API, CRM, ZIP
|
|
155
|
+
|
|
156
|
+
## 1.4.1 - 2026-06-23
|
|
157
|
+
|
|
158
|
+
* [Fix] Remove deprecated LookupContext.find_template!
|
|
159
|
+
|
|
160
|
+
## 1.4.0 - 2026-06-23
|
|
161
|
+
|
|
162
|
+
* [Feature] Add navigation_links method
|
|
163
|
+
|
|
164
|
+
## 1.3.5 - 2026-05-15
|
|
165
|
+
|
|
166
|
+
* [Feature] Display pagy info with number delimiters
|
|
167
|
+
|
|
168
|
+
## 1.3.3 - 2026-04-09
|
|
169
|
+
|
|
170
|
+
* [Fix] Allow for nested resources not defined at the root level
|
|
171
|
+
|
|
172
|
+
## 1.3.2 - 2026-04-09
|
|
173
|
+
|
|
174
|
+
* Temporarily disable caching
|
|
175
|
+
|
|
176
|
+
## 1.3.1 - 2026-04-06
|
|
177
|
+
|
|
178
|
+
* [Fix] Use a different caching key based on the controller path
|
|
179
|
+
|
|
180
|
+
Posts could be displayed differently under /users/:id/posts or under /topics/:id/posts
|
|
181
|
+
so they should be cached separately.
|
|
182
|
+
|
|
183
|
+
## 1.3.0 - 2026-04-03
|
|
184
|
+
|
|
185
|
+
* [BREAKING CHANGE] Rename `search_placeholder` to `search_prompt`
|
|
186
|
+
|
|
187
|
+
## 1.2.0 - 2026-03-31
|
|
188
|
+
|
|
189
|
+
* [BREAKING CHANGE] `RecourseController` is now `RecoursesController`
|
|
190
|
+
* [BREAKING CHANGE] "Add" button is now yield in the `content_for :actions`
|
|
191
|
+
* [BREAKING CHANGE] `recourse_positionable?` is no longer supported
|
|
192
|
+
* [Deprecation] `header:` parameter is no longer required in `column`.
|
|
193
|
+
* [Feature] support for nested resources
|
|
194
|
+
* [Feature] support for ransack searches
|
|
195
|
+
|
|
196
|
+
## 1.1.0 - 2026-03-24
|
|
197
|
+
|
|
198
|
+
* [BREAKING CHANGE] `recourses` only accepts one resource if a block is provided
|
|
199
|
+
* [BREAKING CHANGE] `recourses` automatically sets the module for nested resources
|
|
200
|
+
|
|
201
|
+
Before this change this config/routes.rb was valid:
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
recourses(:users, :posts) { resources :comments }
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
and followed Rails `resources` behavior of creating **two** nested resources: `users/comments` and
|
|
208
|
+
`posts/comments`. After this change, each base resource needs to be defined separately:
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
recourses(:users) { resources :comments }
|
|
212
|
+
recourses(:posts) { resources :comments }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
This syntax is more explicit and allows nested resources to be defined under the parent's module.
|
|
216
|
+
In other words, the previous code is equivalent to:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
resources(:users) { resources :comments, module: :users }
|
|
220
|
+
resources(:posts) { resources :comments, module: :posts }
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
which allows developers to have two different controllers/actions to display a user comments
|
|
224
|
+
(/users/:id/comments) or to display a post comments (/posts/:id/comments)
|
|
225
|
+
|
|
226
|
+
## 1.0.2 - 2026-03-23
|
|
227
|
+
|
|
228
|
+
* [BUG] Avoid Zeitweirk conflict when loading Active Record
|
|
229
|
+
|
|
230
|
+
## 1.0.1 - 2026-03-10
|
|
231
|
+
|
|
232
|
+
* [BUG] Only show search form when search attributes are present
|
|
233
|
+
|
|
234
|
+
## 1.0.0 - 2026-03-10
|
|
235
|
+
|
|
236
|
+
* [FEATURE] New `recourses` method that can be invoked inside config/routes.rb
|
|
237
|
+
|
|
238
|
+
`recourses` is like `resources` on steroids for admin-only routes:
|
|
239
|
+
|
|
240
|
+
- All the routes are included in `Recourse.resources` to easily display in a navbar
|
|
241
|
+
- Their controllers do not need to define the `index` action: they inherit a predefined one
|
|
242
|
+
- There is also a predefined `index.html` view which displays the resources paginated/searchable.
|
|
243
|
+
- The content of each row can be customized defining a new `_row.html.erb` partial
|
|
244
|
+
|
|
245
|
+
|
data/README.md
CHANGED
|
@@ -1,33 +1,163 @@
|
|
|
1
|
-
Recourse
|
|
2
|
-
========
|
|
1
|
+
# Recourse
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
Admin screens for a Rails app, drawn from its models. One word in `config/routes.rb`
|
|
4
|
+
serves a resource's seven actions — a paginated, searchable, sortable, filterable index
|
|
5
|
+
table, a record's page, its form and its delete — with no controller, view or partial
|
|
6
|
+
added to the app. What each screen shows is read off the model: its columns, its
|
|
7
|
+
validators, its associations and its indexes.
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
Everything a host may call is listed below with a link to its reference on
|
|
10
|
+
[RubyDoc](https://rubydoc.info/gems/recourse). Every default can be overridden by
|
|
11
|
+
defining the same thing in the app.
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
- Their controllers do not need to define the `index` action: they inherit a predefined one
|
|
10
|
-
- There is also a predefined `index.html` view which displays the resources paginated/searchable.
|
|
11
|
-
- The content of each row can be customized defining a new `_row.html.erb` partial
|
|
13
|
+
## How to install
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
```bash
|
|
16
|
+
gem install recourse
|
|
17
|
+
```
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
```ruby
|
|
20
|
+
# Gemfile
|
|
21
|
+
gem 'recourse', '~> 4.0'
|
|
22
|
+
```
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
`~> 4.0` follows Semantic Versioning: `bundle update` takes every 4.x and never a
|
|
25
|
+
breaking change. Rails 8.1 and Ruby 3.2 are the minimum; the pages need Turbo, which
|
|
26
|
+
`turbo-rails` brings, and nothing else — Bootstrap and its icons are vendored and served
|
|
27
|
+
by the engine, so a host without an asset pipeline gets the same screens.
|
|
20
28
|
|
|
21
|
-
In config/routes.rb
|
|
29
|
+
## In `config/routes.rb`
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
[`recourses`](https://rubydoc.info/gems/recourse/Recourse/Routes#recourses-instance_method)
|
|
32
|
+
is `resources` with a controller and views supplied. It takes the same `only:` and
|
|
33
|
+
`except:`, and a block nests children under the parent:
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
```ruby
|
|
36
|
+
scope module: :admin do
|
|
37
|
+
recourses :providers do
|
|
38
|
+
recourses :markets, only: :index # a counted tab on each provider
|
|
39
|
+
recourses :retrievals, only: :create # a button on each provider
|
|
40
|
+
end
|
|
41
|
+
recourses :searches, only: %i[index show new create]
|
|
42
|
+
end
|
|
43
|
+
```
|
|
26
44
|
|
|
27
|
-
- `
|
|
28
|
-
|
|
45
|
+
- Every resource with an `index` gets a sidebar entry, in routes order, with a keyboard
|
|
46
|
+
shortcut on its first letter. `only: []` declares a resource with no page of its own.
|
|
47
|
+
- A nested `index` is a tab on the parent's card, counted where a `counter_cache` exists.
|
|
48
|
+
- A nested `create` with no index is a button on the parent's page, named after the
|
|
49
|
+
route. The host answers it in a controller of its own, or the gem creates the record.
|
|
50
|
+
- Nested controllers are namespaced after the parent (`Admin::Providers::MarketsController`),
|
|
51
|
+
and a nested index lists only the parent's rows.
|
|
52
|
+
- Do you name a route `exit`? The sidebar ends with a log-out button submitting a
|
|
53
|
+
`DELETE` to it:
|
|
29
54
|
|
|
30
|
-
|
|
55
|
+
```ruby
|
|
56
|
+
resource :session, only: :destroy, as: :exit
|
|
57
|
+
```
|
|
31
58
|
|
|
32
|
-
|
|
59
|
+
## In `app/controllers/recourses_controller.rb`
|
|
33
60
|
|
|
61
|
+
Every controller the gem defines inherits
|
|
62
|
+
[`RecoursesController`](https://rubydoc.info/gems/recourse/Recourse/BaseController),
|
|
63
|
+
which the host defines to put behavior above every screen at once:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
class RecoursesController < Recourse::BaseController
|
|
67
|
+
before_action :authenticate!
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A controller the app already defines is left alone.
|
|
72
|
+
|
|
73
|
+
## In `config/initializers/recourse.rb`
|
|
74
|
+
|
|
75
|
+
- [`Recourse.color=`](https://rubydoc.info/gems/recourse/Recourse#color%3D-class_method):
|
|
76
|
+
which Bootstrap family is primary on every page. Readers switch light and dark themselves.
|
|
77
|
+
- [`Recourse.bookmarks=`](https://rubydoc.info/gems/recourse/Recourse#bookmarks%3D-class_method):
|
|
78
|
+
a Proc answering the viewer's bookmark rows. Every table of a model with a `has_many`
|
|
79
|
+
at that class then opens with a square to keep a row by, kept rows first.
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
Recourse.color = :orange
|
|
83
|
+
Recourse.bookmarks = -> { Keepsake.where agent: Current.agent }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## In a model
|
|
87
|
+
|
|
88
|
+
Every Active Record model answers these; a model overrides one in a
|
|
89
|
+
[`Recoursive`](https://rubydoc.info/gems/recourse/Recourse/Recoursive) concern of its own.
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# app/models/market/recoursive.rb
|
|
93
|
+
module Market::Recoursive extend ActiveSupport::Concern
|
|
94
|
+
class_methods do
|
|
95
|
+
def recourse_label = :slug # what a menu and a cell call one
|
|
96
|
+
def recourse_hidden = %i[callback_url payload] # off every screen
|
|
97
|
+
def recourse_displayed = :created_at # back on the table
|
|
98
|
+
def recourse_order = { size: :desc } # how the index sorts
|
|
99
|
+
def recourse_icon = :question # a Unicon concept
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| Hook | Default | Decides |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| [`recourse_label`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_label-instance_method) | `:name` | the column a combobox shows and a foreign-key cell reads; typed rather than picked where it has a length validator |
|
|
107
|
+
| [`recourse_hidden`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_hidden-instance_method) | `[]` | columns kept off the table, the page, the form and the search |
|
|
108
|
+
| [`recourse_displayed`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_displayed-instance_method) | `[]` | columns a table draws that it would leave off: encrypted ones, the id, timestamps, JSON |
|
|
109
|
+
| [`recourse_order`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_order-instance_method) | `:id` | the index's order, a Symbol or a Hash; rows with nothing in the column come last |
|
|
110
|
+
| [`recourse_icon`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_icon-instance_method) | the model's name | the icon on the sidebar, the crumbs and the tabs |
|
|
111
|
+
|
|
112
|
+
What needs no hook, being read off the model:
|
|
113
|
+
|
|
114
|
+
- The index eager-loads every `belongs_to` the table names, sorts by whatever an index
|
|
115
|
+
covers, searches through its indexed string columns and the labels behind its foreign
|
|
116
|
+
keys, marks what it matched, and offers a filter menu per enum, boolean and `belongs_to`.
|
|
117
|
+
- Saving a row refreshes every open index of it.
|
|
118
|
+
- A form field per column, carrying the rules the validators state: a length is a
|
|
119
|
+
`maxlength`, a format a `pattern`, a numericality a numeric keyboard.
|
|
120
|
+
- An enum is a badge on a page and a menu on a form; a foreign key is a menu of the
|
|
121
|
+
records it points at, or a field to type the label into where there are too many.
|
|
122
|
+
- Values formatted by kind: delimited integers, money, phone numbers, dates and times in
|
|
123
|
+
the reader's own time zone, a `<time>` saying how long ago, a URL as a link, a list as
|
|
124
|
+
a count that opens.
|
|
125
|
+
- An encrypted column stays off the table, arrives masked on the record's page behind a
|
|
126
|
+
`Show`, and is edited in the clear.
|
|
127
|
+
- The delete button names what goes with the record, counted one association down.
|
|
128
|
+
|
|
129
|
+
## In `app/views`
|
|
130
|
+
|
|
131
|
+
Do you want your own cells? Add `app/views/<resources>/_row.html.erb` and build it from
|
|
132
|
+
[`column`](https://rubydoc.info/gems/recourse/Recourse/Helpers/Cells#column-instance_method),
|
|
133
|
+
[`sort_header`](https://rubydoc.info/gems/recourse/Recourse/Helpers/Sorts#sort_header-instance_method)
|
|
134
|
+
and [`search_highlight`](https://rubydoc.info/gems/recourse/Recourse/Helpers/Searches#search_highlight-instance_method):
|
|
135
|
+
|
|
136
|
+
```erb
|
|
137
|
+
<%# app/views/admin/providers/_row.html.erb %>
|
|
138
|
+
<%# locals: (provider:) -%>
|
|
139
|
+
<%= column header: sort_header(:name) do %>
|
|
140
|
+
<%= search_highlight provider.name, :name %>
|
|
141
|
+
<% end %>
|
|
142
|
+
<%= column header: 'Phone' do %><%= number_to_phone provider.phone %><% end %>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Any template or partial of the gem's is replaced by defining it in the app, under the
|
|
146
|
+
resource (`app/views/admin/providers/index.html.erb`) or for every resource at once
|
|
147
|
+
(`app/views/recourses/_sidebar.html.erb`). Every string the gem shows is a key under
|
|
148
|
+
`recourse` in `config/locales/recourse.en.yml`, so a host rewords `Add provider` in a
|
|
149
|
+
locale file of its own.
|
|
150
|
+
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
bin/setup # installs the bundle
|
|
155
|
+
bundle exec rake # the suite at 100% coverage, RuboCop, and the file-length ceiling
|
|
156
|
+
cd test/dummy && bin/rails server
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The dummy app under `test/dummy` runs on SQLite; the gem names no adapter.
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
[MIT](MIT-LICENSE).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# The superclass every bookmark controller gets. A host defines this class itself —
|
|
2
|
+
# its `app/controllers` comes first — to write the row its own way, the same way it
|
|
3
|
+
# redefines `RecoursesController` to guard every screen.
|
|
4
|
+
class BookmarksController < Recourse::BookmarksController
|
|
5
|
+
# Empty on purpose: the behavior is the base class, so a host redefining this one
|
|
6
|
+
# loses none of it.
|
|
7
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
# Where a write goes once it has landed, and what the page it lands on is told about
|
|
3
|
+
# it. Apart from the actions that write, because the answer is the routes' rather than
|
|
4
|
+
# any one action's: three of them ask it, and none of them decides it.
|
|
5
|
+
module Landing
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# What a write says once it has landed: the message, and the row it landed on where
|
|
9
|
+
# one survives, for the page to mark while that message stands.
|
|
10
|
+
def wrote(message, record = nil)
|
|
11
|
+
flash.notice = message
|
|
12
|
+
flash[Recourse::WRITTEN] = Recourse.row_id record if record
|
|
13
|
+
redirect_to written_url, status: :see_other
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# What a rejected write does instead: the form again, with the message over it and
|
|
17
|
+
# the errors beside the fields that earned them.
|
|
18
|
+
def rejected(record, page, message)
|
|
19
|
+
return refused record, message unless Recourse.routed? controller_path, page.to_s
|
|
20
|
+
|
|
21
|
+
flash.now.alert = message
|
|
22
|
+
render page, status: :unprocessable_entity
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# And where a rejection goes when there is no form to send it back to. A bare
|
|
26
|
+
# action's button stands on a page about something else, so what turned the write
|
|
27
|
+
# down is the whole of what there is to say, said where the button was.
|
|
28
|
+
def refused(record, message)
|
|
29
|
+
flash.alert = record.errors.full_messages.to_sentence.presence || message
|
|
30
|
+
redirect_to written_url, status: :see_other
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# And where a write goes: the index, or — where the routes drew none — back to the
|
|
34
|
+
# record it hangs off, which for a bare action is the page its button stood on.
|
|
35
|
+
def written_url
|
|
36
|
+
return url_for action: :index if Recourse.routed? controller_path, 'index'
|
|
37
|
+
|
|
38
|
+
parent_url
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# That record's own page. The path is read back from the routes rather than chopped
|
|
42
|
+
# off this controller's own: how many segments the nesting added — a `namespace`
|
|
43
|
+
# among them — is something the routes knew and a path no longer says.
|
|
44
|
+
def parent_url
|
|
45
|
+
url_for controller: "/#{Recourse.parent_of controller_path}", action: :show,
|
|
46
|
+
id: @recourse_parent
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
# Resolves a submitted list back to the values it holds, for a column that keeps
|
|
3
|
+
# several of them where a form can only send one string.
|
|
4
|
+
module ListResolution
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
# A list is typed one value to a line and arrives as those lines, so it is split
|
|
8
|
+
# back here — the same place a typed reference is looked up, and for the same
|
|
9
|
+
# reason: no host model needs a virtual attribute and no host needs a strong
|
|
10
|
+
# parameter of its own. A line that is only spaces is not a value, and neither is
|
|
11
|
+
# an empty box: both leave the list empty rather than holding one blank string.
|
|
12
|
+
def resolve_lists(attributes)
|
|
13
|
+
list_columns.each do |column|
|
|
14
|
+
next unless attributes.key? column
|
|
15
|
+
|
|
16
|
+
attributes[column] = attributes[column].to_s.lines.filter_map do |line|
|
|
17
|
+
line.strip.presence
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attributes
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def list_columns
|
|
25
|
+
resource_class.column_names.select { |column| Recourse.list_column? resource_class, column }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
# How much of a table one page shows: pagy's twenty by default, or whatever the
|
|
3
|
+
# reader picked from the menu under the table, kept in their own browser.
|
|
4
|
+
module Paging
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
# Checked against the two we offer rather than taken as read — a cookie is a value
|
|
8
|
+
# a stranger can write, and an unchecked one is `?limit=100000` by another route.
|
|
9
|
+
# Which is also why pagy's `max_limit` stays unset: the query string is still not
|
|
10
|
+
# asked, and this is the one thing that is.
|
|
11
|
+
def recourse_limit
|
|
12
|
+
limit = cookies[Recourse::LIMIT_STORAGE].to_i
|
|
13
|
+
|
|
14
|
+
Recourse::LIMITS.include?(limit) ? limit : Recourse::LIMITS.first
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
# Names the record a nested route sits under, so a host's own view can read it.
|
|
3
|
+
module ParentNaming
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
# `/providers/5/openings` sets `@provider`, the way a hand-written controller would
|
|
7
|
+
# -- the contract `ResourceResolution` already keeps for the record a page is about,
|
|
8
|
+
# kept here for the one above it, and the reason a host needs no scoping concern of
|
|
9
|
+
# its own to write a form or a row that names the parent.
|
|
10
|
+
#
|
|
11
|
+
# Named from the route rather than from the association, because the two need not
|
|
12
|
+
# meet through a key: a page listing every vertical under a provider is nested by
|
|
13
|
+
# the path alone, and no `belongs_to` joins the two. What `find_parent` resolved is
|
|
14
|
+
# what gets the name, and only where it is the record the path names: a bare action
|
|
15
|
+
# has no model, and a path may name a class that answers no queries.
|
|
16
|
+
def name_parent
|
|
17
|
+
path = Recourse.parent_of controller_path
|
|
18
|
+
model = path && Recourse.model?(path)
|
|
19
|
+
return unless model && @recourse_parent.is_a?(model)
|
|
20
|
+
|
|
21
|
+
instance_variable_set "@#{model.model_name.singular}", @recourse_parent
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
# Finds the record a nested route names, so `/counties/5/zips` lists county 5's
|
|
3
|
+
# ZIPs, builds ZIPs inside it, and answers 404 when no county 5 exists.
|
|
4
|
+
module ParentResolution
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
before_action :find_parent
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# The record a key points at where the resource has one, and otherwise the record
|
|
14
|
+
# the path names: a page nested by the path alone — the memos of a team no memo
|
|
15
|
+
# belongs to — still sits under something.
|
|
16
|
+
def find_parent
|
|
17
|
+
@recourse_parent_association = parent_association
|
|
18
|
+
@recourse_parent = @recourse_parent_association ? parent_model.find(parent_id) : path_parent
|
|
19
|
+
|
|
20
|
+
name_parent
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The record the segment above this one names, read off the routes rather than off
|
|
24
|
+
# a key, or nil where the path names nothing or the name is no model's.
|
|
25
|
+
def path_parent
|
|
26
|
+
parent = Recourse.parent_of controller_path
|
|
27
|
+
model = parent && Recourse.model?(parent)
|
|
28
|
+
return unless model
|
|
29
|
+
|
|
30
|
+
id = request.path_parameters[:"#{model.model_name.singular}_id"]
|
|
31
|
+
model.find_by id: id if id
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# What the route settled and every action honours: the index lists rows carrying
|
|
35
|
+
# these columns, and `new` and `create` build records that do.
|
|
36
|
+
def parent_columns
|
|
37
|
+
return {} if @recourse_parent_association.nil?
|
|
38
|
+
|
|
39
|
+
{ @recourse_parent_association.foreign_key => @recourse_parent.id }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The belongs_to whose record the path names, or nil at the top level. Path
|
|
43
|
+
# parameters rather than `params`, so a stray `?county_id=` nests nothing.
|
|
44
|
+
def parent_association
|
|
45
|
+
own_references.find { |one| path_names? one.name }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# A host may serve a page over a verb with no class behind it, whose action the
|
|
49
|
+
# host answers itself, and a verb answers no question about keys. The routes still
|
|
50
|
+
# named a parent, and the host still finds it.
|
|
51
|
+
def own_references
|
|
52
|
+
resource_model? ? resource_class.recourse_references : []
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def parent_id
|
|
56
|
+
request.path_parameters[:"#{@recourse_parent_association.name}_id"]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parent_model = @recourse_parent_association.klass
|
|
60
|
+
|
|
61
|
+
def path_names?(name) = request.path_parameters.key?(:"#{name}_id")
|
|
62
|
+
end
|
|
63
|
+
end
|