recourse 7.8.2 → 7.8.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03ffa189dd77cb44309f61dc8a42e67fa818673f75daca4ee837aed53fb53a6b
4
- data.tar.gz: 32e06a42baacce634ec2a88bc54ee585a0f7e90b740afde1dea4453db1dc737d
3
+ metadata.gz: 94653dc38ca1c788fce69b9406faddc48c189cefcd9fbe7dbe11e4d87ddc3faf
4
+ data.tar.gz: c1282266e1e70de6bc5438661f86321e52948dee028baa7128efe4fac4317719
5
5
  SHA512:
6
- metadata.gz: faf5b85e985b30125c1ed657ba37a8912d236b6e1d35cd439b47fb793dd14397de66250325c75d86644b93dc122d4c8251809685cb218f2e3c438e75ec1144e0
7
- data.tar.gz: e5667f606d93ec6cb0016707e758ad5aa944cf6b50c3c9a4ade70614242838639813e08b73c4ec460abe71d00e09648eb9eb6476a0825511c44c2ac86e16a017
6
+ metadata.gz: dbed84f6d5071f7be79cdb87c499c6a9c4ae23ceb74c70390606cfe33652236f38e782ecbae67347c4873b270c23cba660395d2451034202517af60fe9a0a8d6
7
+ data.tar.gz: e04c3108e911aba9d08555b8d696994cc56df42ee21ee207f96cc0f1cf326491fcd62d867f38953e22c3fdb76e14297a50d22e4444db0bae1c28746a20b546db
data/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## 7.8.3 - 2026-09-21
11
+
12
+ * [FIX] A delete button in a table row is no taller than the row
13
+
14
+ The button kept `.btn`'s minimum height of 2rem, half again the line the other cells
15
+ read at, so every row of a table with a delete column stood taller than the rows of a
16
+ table without one.
17
+
10
18
  ## 7.8.2 - 2026-09-20
11
19
 
12
20
  * [CHANGE] The retrieval button says what it does
data/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Swap two characters in your `config/routes.rb` and get awesome admin screens for your Rails app.
4
4
 
5
+ This README is the tour. The [guides](https://claudiob.github.io/recourse/guides/) are
6
+ where every screen is explained in full, one page each, and the
7
+ [reference](https://rubydoc.info/gems/recourse) is built from the source.
8
+
5
9
  ## How to install
6
10
 
7
11
  ```bash
@@ -10,10 +14,10 @@ gem install recourse
10
14
 
11
15
  ```ruby
12
16
  # Gemfile
13
- gem 'recourse', '~> 6.0'
17
+ gem 'recourse', '~> 7.0'
14
18
  ```
15
19
 
16
- `~> 6.0` follows Semantic Versioning: `bundle update` takes every 6.x and never a
20
+ `~> 7.0` follows Semantic Versioning: `bundle update` takes every 7.x and never a
17
21
  breaking change. Rails 8.1 and Ruby 3.2 are the minimum; the pages need Turbo, which
18
22
  `turbo-rails` brings, and nothing else in the host. Everything a page is styled and
19
23
  scripted by — Bootstrap 6, its icons, Turbo, Stimulus and the controllers behind these
@@ -39,7 +43,8 @@ first letter, and a paginated, searchable, sortable, filterable index table:
39
43
  <img width="3824" height="2274" alt="Image" src="https://github.com/user-attachments/assets/f952d3e5-c320-464b-b166-77c7ab8f9071" />
40
44
 
41
45
  Every recourse with `show` gets a detail page where attributes are displayed with the appropriate
42
- formatting, or masked if sensitive:
46
+ formatting — a boolean as `Yes` or `No`, a date by its name, a key as the label of what it
47
+ points at — or masked if sensitive:
43
48
 
44
49
  <img width="3824" height="2274" alt="Image" src="https://github.com/user-attachments/assets/515f63c4-8a9b-422e-95c4-88d6647911d8" />
45
50
 
@@ -47,73 +52,48 @@ Every recourse with `new` or `edit` gets a form with appropriate browser formatt
47
52
 
48
53
  <img width="3824" height="1220" alt="Image" src="https://github.com/user-attachments/assets/ebe5108b-bbd1-4cd1-a635-89eef0e6fc50" />
49
54
 
50
- Every recourse with `destroy` gets a button with a detailed confirmation message — on its
51
- edit page, or on each row of its table where no `edit` is routed:
55
+ Every recourse with `destroy` gets a button with a detailed confirmation message — on the
56
+ record's own page, whichever of `show` and `edit` is open, and on each row of its table
57
+ where no `edit` page would carry it. A row a model will not give up stays, and the page says so
58
+ rather than raising. A model may word its own deletion — `Disconnect` rather than
59
+ `Delete` — by writing `recourse.models.<model>.delete` in its locale:
52
60
 
53
61
  [image]
54
62
 
55
- `recourse` draws what Rails' `resource` draws, and the gem records it the same way: one
56
- record reached with no id of its own, at `/locations/5/property`. Rails routes a singular
57
- resource to a plural controller, so `Locations::PropertiesController` answers it, and the
58
- gem reads the record off the parent under the name the route gives — `location.property`,
59
- whether the parent keeps it with a `has_one` or points at one with a `belongs_to`. Where
60
- the parent has no association of that name the record is still yours to find, with a
61
- `find_resource` of your own.
62
-
63
- What such a nesting earns on the parent's card depends only on what it routes. Routed
64
- `show`, it earns a tab reading the model's own word in the singular, pointing at that one
65
- page. Routed `create` or `destroy` and neither `index` nor `new`, it earns a button beside
66
- the breadcrumb instead — `Add property`, `Delete property` — and which of the two it
67
- offers is the record's to say: nothing to add where there is already one, nothing to
68
- delete where there is none. A write has no index to return to, so it lands on the
69
- record's own page: a singular resource is the collection of one.
70
-
71
- Filters are drawn per enum, per boolean and per foreign key. A model adds one the schema
72
- says nothing about — the type behind a `has_one`, a word reached through another table —
73
- by naming the words itself, and naming the menu too, since there is no column to head it:
74
-
75
- ```ruby
76
- def self.filter_fields
77
- super.merge 'integration_type_in' => { label: 'CRM', values: Integration.selectable }
78
- end
79
- ```
80
-
81
- Naming the words is not leave to query through a table, so `ransackable_associations`
82
- still says how far Ransack may reach.
83
-
84
- Every nested recourse gets namespaced after the parent:
63
+ Every nested recourse gets namespaced after the parent, and a tab on the parent's card:
85
64
 
86
65
  <img width="3824" height="2274" alt="Image" src="https://github.com/user-attachments/assets/9b242335-a25b-4fa8-8023-422538d235b0" />
87
66
 
67
+ `recourse` draws what Rails' `resource` draws: one record reached with no id of its own,
68
+ at `/locations/5/property`, read off the parent under the name the route gives —
69
+ `location.property`, whether the parent keeps it with a `has_one` or points at one with
70
+ a `belongs_to`. What such a nesting earns on the parent's card depends only on what it
71
+ routes: a tab where `show` is, and a button beside the breadcrumb — `Add property`,
72
+ `Delete property` — where `create` or `destroy` is routed with no page of its own. A
73
+ singular resource holds at most one, so `new` sends a reader to the record where there
74
+ already is one, and `show` sends them to the form where there is none yet.
75
+
76
+ Two keywords say what a table may do beyond the seven actions. `positionable: true`
77
+ draws the route a dragged row reports its place to, and `retrievable: true` draws the
78
+ one a `Retrieve` button on the table posts to, for rows that came from somewhere else.
79
+ Both are refused on a resource with no `index`, since there is nowhere for either to
80
+ stand. The [routes guide](https://claudiob.github.io/recourse/guides/routes.html) has
81
+ the rest: namespaces, the defaults a nested resource takes, and the `exit` route that
82
+ earns the sidebar a log-out button.
88
83
 
89
84
  ## Step 2. Enhance your models
90
85
 
91
- To change the content displayed in the `index` table of a model, override any of these class methods:
86
+ To change what a model's screens show, override any of these class methods:
92
87
 
93
88
  | Hook | Default | Decides |
94
89
  | --- | --- | --- |
95
90
  | [`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 |
96
91
  | [`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 |
97
- | [`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 |
98
92
  | [`recourse_order`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_order-instance_method) | `:id`, or the positioned column | the index's order, a Symbol or a Hash; rows with nothing in the column come last |
99
93
  | [`recourse_position`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_position-instance_method) | `'position'` where the model keeps an integer one | the column a reader drags the rows into order by, or `nil` for a table nobody positions |
100
94
  | [`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 |
101
-
102
- A number is drawn as a count, headed with what it counts and linking to the rows behind
103
- it, when its column holds a counter cache or is named `<association>_count` for an
104
- association the model has. So a count the app keeps itself — a `has_many through`, which
105
- Rails will not cache — needs nothing declared:
106
-
107
- ```ruby
108
- class Technician < ActiveRecord::Base
109
- has_many :visits, through: :dispatches # and a visits_count column beside it
110
- end
111
- ```
112
-
113
- Which means a `*_count` column whose prefix names an association must hold the count of
114
- that association. One naming nothing — `word_count`, `retry_count` — is an ordinary
115
- number, and one naming an association it does not count wants another name or a place in
116
- `recourse_hidden`.
95
+ | [`recourse_includes`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_includes-instance_method) | every `belongs_to` | what the index eager-loads, in any shape `includes` takes |
96
+ | [`recourse_broadcasts?`](https://rubydoc.info/gems/recourse/Recourse/Broadcasting#recourse_broadcasts%3F-instance_method) | `true` | whether saving a record redraws every open index listing it |
117
97
 
118
98
  For instance, this would yield a more compact `index` than the default configuration:
119
99
 
@@ -121,14 +101,35 @@ For instance, this would yield a more compact `index` than the default configura
121
101
  class Post < ActiveRecord::Base
122
102
  def self.recourse_label = :title # Label a post with its title
123
103
  def self.recourse_hidden = %i[ip_address] # Hide the IP address from the views
124
- def self.recourse_displayed = :created_at # Display the created_at in the views
125
104
  def self.recourse_order = { published_at: :desc } # Sort posts by last published first
126
105
  def self.recourse_icon = :question # Represent Posts with a question icon
127
106
  end
128
- ````
107
+ ```
129
108
 
130
109
  [image]
131
110
 
111
+ What the schema already says needs nothing declared. A number is drawn as a count,
112
+ headed with what it counts and linking to the rows behind it, when its column holds a
113
+ counter cache or is named `<association>_count` for an association the model has. An
114
+ enum is a badge on a page, a menu on a form and a filter beside the search box, and so
115
+ is a boolean and every foreign key whose table is short enough to list. An encrypted
116
+ column stays off every table, arrives masked on the record's page and is offered in the
117
+ clear on its form. A `has_one_attached` is a file field and a picture; a
118
+ `has_many_attached :photos` is a page of its own once `recourses :photos` is routed
119
+ under the record.
120
+
121
+ Filters are drawn per enum, per boolean and per foreign key. A model adds one the schema
122
+ says nothing about — a word reached through another table — by naming the predicate,
123
+ and the gem reads the words off the model behind it:
124
+
125
+ ```ruby
126
+ def self.filter_fields = super + %i[team_name_in]
127
+ ```
128
+
129
+ Naming a filter is not leave to query through a table, so `ransackable_associations`
130
+ still says how far Ransack may reach — and a filter Ransack will not answer is refused
131
+ while the page draws rather than left to narrow nothing.
132
+
132
133
  ## Step 3. Delete your views
133
134
 
134
135
  If you are happy with the generated HTML files, then be happy to skip this step!
@@ -153,6 +154,12 @@ to define the columns you want:
153
154
 
154
155
  [image]
155
156
 
157
+ A `_fields.html.erb` replaces the fields of a form the same way, and a `show`, `edit` or
158
+ nested `index` template of your own replaces the body of that page and nothing else:
159
+ the card, its tabs, the buttons beside the breadcrumb and the tab's title are the
160
+ layout's, so a page you write is its content alone. A page that wants the width to
161
+ itself assigns `@recourse_card = false`.
162
+
156
163
  ## Step 4. Secure your controllers
157
164
 
158
165
  Every controller the gem defines inherits
@@ -165,14 +172,15 @@ class RecoursesController < Recourse::BaseController
165
172
  end
166
173
  ```
167
174
 
168
- A controller the app already defines is left alone.
169
-
175
+ A controller the app already defines is left alone, and one line of it is usually
176
+ enough: `recourse_relation` puts a scope of your own behind a table the gem draws
177
+ whole, and `find_resource` finds a singular record the parent names no association for.
170
178
 
171
179
  ## Step 5. Enjoy the extras
172
180
 
173
- The rest of what the gem offers, in four parts. The first two are a line each in
174
- `config/initializers/recourse.rb`; the last two need nothing beyond what the model
175
- already declares.
181
+ The rest of what the gem offers. The first two are a line each in
182
+ `config/initializers/recourse.rb`; the others need nothing beyond what the model
183
+ already declares, or one keyword in the routes.
176
184
 
177
185
  ### Color and theme
178
186
 
@@ -189,7 +197,8 @@ Recourse.theme = :nord
189
197
 
190
198
  The theme is where a reader starts, not where they stay. The moon at the foot of the
191
199
  sidebar rotates through every palette, light and dark, and the one they pick stays in
192
- their browser.
200
+ their browser. Times are drawn in the reader's own zone the same way, reported by their
201
+ browser and never written to the host.
193
202
 
194
203
  ### Bookmarks
195
204
 
@@ -206,147 +215,45 @@ rows come first. A model that cannot hold a bookmark gets no column.
206
215
 
207
216
  <img width="3824" height="1220" alt="Image" src="https://github.com/user-attachments/assets/d1db4adb-0b5c-4e41-8bb6-cf72a35288f0" />
208
217
 
209
- ### Attachments
210
-
211
- What a model keeps as files needs nothing declared either. A `has_one_attached :logo` is
212
- a file field on the form and, on the record's page, the picture Active Storage makes of
213
- the file — 100 pixels tall, WebP where the browser takes it, opening the whole file in a
214
- new tab — or the file's name where nothing can be drawn of it.
215
-
216
- A `has_many_attached :photos` earns a page of its own, nested under the record:
217
-
218
- ```ruby
219
- recourses :photos, only: %i[index destroy]
220
- ```
221
-
222
- That page is a table of the files with their pictures, and a Delete on each row that
223
- takes the file off the record. Files are added on the record's own form, where a chosen
224
- file joins a shelf and replaces a single one, and a field nobody touched leaves
225
- everything as it was. `recourse_hidden :photos` keeps a file off every screen the way it
226
- keeps a column off.
227
-
228
- Drawing a picture of an image needs `image_processing`; video and PDF also need the
229
- host's `ffmpeg` and `poppler`.
230
-
231
- ### Maps
218
+ ### Maps and calendars
232
219
 
233
220
  A table whose model keeps a `google_place_id`, or a `latitude` and a `longitude`, can be
234
221
  read as a Google map of the same page: the footer under it offers `Display as map`, and
235
222
  `/counties.map` draws this page's rows in the frame and over the footer the table has, so
236
- search, sort and pages work the same on either shape.
237
-
238
- A point is a pin. A place ID is filled in as an area where the model is a geography
239
- Google draws boundaries for — a `State`, a `County`, a `City` or a `ZIP`, by name — and
240
- pinned at the place for any other model.
241
-
242
- The key and the map ID are the host's own credentials:
243
-
244
- ```yaml
245
- # config/credentials.yml.enc
246
- google_maps:
247
- api_key: AIza…
248
- map_id: 4f2a…
249
- ```
250
-
251
- The map's style has the matching **Feature layers** turned on in the Cloud console —
252
- Postal code for ZIPs, Administrative area level 2 for counties. A host with a Content
253
- Security Policy allows `maps.googleapis.com` for scripts and connections, and Google's
254
- tile hosts for images.
255
-
256
-
257
- ### Calendars
258
-
259
- A table whose model keeps a `starts_at` and an `ends_at` can be read as a week of the same
260
- rows: the footer offers `Display as calendar`, and `/shifts.cal` draws a column a day with
261
- each row placed by the hours it runs between — rows that overlap in lanes of their own.
262
- A row says what it is, whoever it points at, and the hours themselves, each led to its own
263
- page where the routes drew one.
264
-
265
- ```ruby
266
- recourses :shifts
267
- ```
268
-
269
- A week rather than a page is what a calendar shows, so there is nothing to paginate:
270
- `?week=2026-09-13` moves to the week holding that day, and `<<`, `<`, `>` and `>>` stand
271
- where the pages stand under a table — a week either side and four weeks beyond each —
272
- with the week named between them. The search and the
273
- filters travel with it — and answer in the shape they were picked in — so a calendar
274
- narrowed to one person stays a narrowed calendar as the weeks move.
275
-
276
- The grid stands as tall as a table's own first page, so the weeks under a calendar sit
277
- where the pages under a table do. The week opens on Sunday and the hours are the reader's
278
- own — the same cookie every other
279
- time on these pages is drawn against — and the scale runs only over the hours that week's
280
- rows cover, or a working day where it holds none. A row running past midnight is drawn on
281
- the day it opens, down to the foot of its column.
282
-
283
- Both columns have to be datetimes, asked through `type_for_attribute`, so an
284
- `attribute :starts_at, :datetime` override counts and a column of another kind named
285
- `starts_at` earns nothing.
223
+ search, sort and pages work the same on either shape. The key and the map ID are the
224
+ host's own credentials, under `google_maps` as `api_key` and `map_id`.
286
225
 
226
+ A table whose model keeps a `starts_at` and an `ends_at` can be read as a week of the
227
+ same rows: the footer offers `Display as calendar`, and `/shifts.cal` draws a column a
228
+ day with each row placed by the hours it runs between. `?week=2026-09-13` moves to the
229
+ week holding that day, and the search and the filters travel with it.
287
230
 
288
231
  ### Positionable tables
289
232
 
290
233
  A table whose model keeps an integer `position` is one a reader puts in order by hand: a
291
- grip opens each row, dragging it moves the row, and the place it lands in is written to a
292
- route drawn under the index. Say where that route belongs, since a page nobody drags needs
293
- none:
294
-
295
- ```ruby
296
- recourses :steps, positionable: true
297
- ```
298
-
299
- A table is put in order from its index and from nowhere else, so the keyword is refused on
300
- a resource that draws none. The column decides the rest — the grips, the callbacks, and
301
- `recourse_order`, since the order a table is read in and the order somebody put it in are
302
- one fact.
234
+ grip opens each row, dragging it moves the row, and the place it lands in is written to
235
+ the route `positionable: true` drew under the index. The column decides the rest the
236
+ grips, the callbacks that keep the numbers running 1, 2, 3, and `recourse_order`, since
237
+ the order a table is read in and the order somebody put it in are one fact. A model
238
+ pointing two ways says which of them its place is counted within:
303
239
 
304
240
  ```ruby
305
241
  class Step < ActiveRecord::Base
306
242
  belongs_to :team
307
243
  belongs_to :person
308
244
 
309
- # A step points two ways, so which of them its place is counted within is the model's
310
- # to say. One `belongs_to` needs no answer, and a model pointing nowhere is positioned
311
- # among the whole table.
312
245
  def recourse_siblings = team.steps
313
246
  end
314
247
  ```
315
248
 
316
- Two things follow the column rather than the routes. A new row lands last among its own
317
- and the gap closes behind one that goes, whether it was made behind a form or in a
318
- console: what a drop reports is a row's place on the *page*, which is a position only
319
- while the numbers run 1, 2, 3 with no holes in them. And a table being positioned is asked
320
- per page — a position means something under the parent it is counted within, so a flat
321
- model's own index is positioned and a listing of every row across every parent is not.
322
-
323
- The search box and the sorted headings stand down while the grips are drawn, and a `q`
324
- typed by hand is refused for the same reason: a filter shortens the page, and a drop on a
325
- shortened page reports a place among the rows that are left.
326
-
327
- A host that keeps the order itself writes `def recourse_position = nil`, which takes the
328
- grips off the table and the two callbacks off the model together. A host with a *second*
329
- listing of a positioned model — a plan's place among its service's plans, and another
330
- among every plan of its department — names the second column on the controller instead,
331
- and owns the write as well as the page:
332
-
333
- ```ruby
334
- module Departmental
335
- extend ActiveSupport::Concern
336
-
337
- private
338
-
339
- def recourse_position = 'ordering'
340
- end
341
-
342
- class Administered::Departments::Plans::PositionsController < Recourse::PositionsController
343
- include Departmental
344
- end
345
- ```
346
-
347
- `Recourse::Positioning` is public for exactly that case: `new(relation, column).close` is
348
- what such a host calls to keep its own column contiguous.
249
+ ### Retrievable tables
349
250
 
251
+ A table whose rows come from somewhere else — a CRM, a feed — offers to fetch them
252
+ again: `recourses :visits, only: :index, retrievable: true` draws
253
+ `POST /providers/5/visits/retrieval` and the `Retrieve` button that posts to it. Where
254
+ the rows come from is the host's to know, so `Providers::Visits::RetrievalsController`
255
+ is the host's to write, and `recourse_retrievable?` on the listing controller is how a
256
+ page says there is nothing to fetch from.
350
257
 
351
258
  ## Development
352
259
 
@@ -73,17 +73,18 @@ module Recourse
73
73
  url_for controller: "/#{resource_controller_path}", action: action, id: record
74
74
  end
75
75
 
76
- # The icon alone, red for what it does, and the same warning the edit page's button
77
- # carries in front of it, which the bundle draws as its dialog. Out of the frame
78
- # the table is drawn in, like every other action in the row: what a delete lands on
79
- # is a whole page with a message over it, and answering inside the frame would keep
80
- # the table and throw the message away.
76
+ # The icon alone, red for what it does, no taller than the line the row reads at,
77
+ # and the same warning the edit page's button carries in front of it, which the
78
+ # bundle draws as its dialog. Out of the frame the table is drawn in, like every
79
+ # other action in the row: what a delete lands on is a whole page with a message
80
+ # over it, and answering inside the frame would keep the table and throw the
81
+ # message away.
81
82
  def destroy_button(record)
82
83
  path = url_for controller: "/#{destroy_action_path}", action: :destroy, id: record
83
84
 
84
85
  confirm_button_to icon_tag(ICONS[:destroy], class: 'fg-danger'), path,
85
86
  confirm: destroy_warning(record), method: :delete,
86
- class: 'btn btn-sm btn-link btn-icon p-0',
87
+ class: 'btn btn-sm btn-link btn-icon p-0 min-h-0',
87
88
  aria: { label: t('recourse.delete', model: resource_name) },
88
89
  form: { data: { turbo_frame: '_top' } }
89
90
  end
@@ -1,4 +1,4 @@
1
1
  module Recourse
2
2
  # Version of the gem, read by the gemspec and by hosts checking compatibility.
3
- VERSION = '7.8.2'
3
+ VERSION = '7.8.3'
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recourse
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.8.2
4
+ version: 7.8.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - claudiob