recourse 7.8.2 → 7.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03ffa189dd77cb44309f61dc8a42e67fa818673f75daca4ee837aed53fb53a6b
4
- data.tar.gz: 32e06a42baacce634ec2a88bc54ee585a0f7e90b740afde1dea4453db1dc737d
3
+ metadata.gz: '024485f23e83a52580fa55b3d73f858bf737ab1a2fd5e53819f283aaf29a57e3'
4
+ data.tar.gz: f1aaa536fd9085f5baa7b54cf9590d0387aa79bc65173a9ab97a853ffacbdbe9
5
5
  SHA512:
6
- metadata.gz: faf5b85e985b30125c1ed657ba37a8912d236b6e1d35cd439b47fb793dd14397de66250325c75d86644b93dc122d4c8251809685cb218f2e3c438e75ec1144e0
7
- data.tar.gz: e5667f606d93ec6cb0016707e758ad5aa944cf6b50c3c9a4ade70614242838639813e08b73c4ec460abe71d00e09648eb9eb6476a0825511c44c2ac86e16a017
6
+ metadata.gz: bbad80f3cda8598fb3773e8caad5b887aa02354a8dd2ba56f289669ae456a4ebca178830d20900cb0ec95a73c641cf95d9d47698c85654a48abd5a5eaa78214e
7
+ data.tar.gz: 07daefa230e19c573044eb613716eafa6d347cdeceb19e1d72af511696a458abb3e4771fb2d91f5784adb5e8053ef8f172e6f08dab09b3011acab7f6336b933c
data/CHANGELOG.md CHANGED
@@ -7,6 +7,35 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## 7.9.0 - 2026-09-26
11
+
12
+ * [FEATURE] A pin or an area on a map leads to its row's page
13
+
14
+ Each point is sent as `[lat, lng, href, title]` and each place as `[id, href, title]`: the
15
+ record's own page wherever one is routed, and its label. bh's map controller makes a pin
16
+ or an area with an `href` clickable, following it through Turbo. Needs the bh that reads
17
+ these shapes: an older one still draws points, but takes a place's whole triple for its
18
+ ID and draws nothing, which is why this release requires bh 6.10.
19
+
20
+ * [FEATURE] A route named `enter` earns the sidebar a way in
21
+
22
+ The mirror of `exit`: `get 'session/new', to: 'sessions#new', as: :enter` draws a
23
+ `Sign in` link beside the toggle, to wherever the host signs people in. A host that
24
+ signs people in shows one or the other by answering `enter?` and `exit?` itself.
25
+
26
+ * [FEATURE] A bookmarks Proc answering nil draws no bookmark column
27
+
28
+ A visitor who never signed in has nowhere to keep a row, so `Recourse.bookmarks =
29
+ -> { Current.user&.bookmarks }` leaves them without the squares rather than raising.
30
+
31
+ ## 7.8.3 - 2026-09-21
32
+
33
+ * [FIX] A delete button in a table row is no taller than the row
34
+
35
+ The button kept `.btn`'s minimum height of 2rem, half again the line the other cells
36
+ read at, so every row of a table with a delete column stood taller than the rows of a
37
+ table without one.
38
+
10
39
  ## 7.8.2 - 2026-09-20
11
40
 
12
41
  * [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 `enter` and `exit`
82
+ routes that earn the sidebar a sign-in link and 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
 
@@ -202,151 +211,50 @@ Recourse.bookmarks = -> { Keepsake.where agent: Current.agent }
202
211
 
203
212
  A Proc rather than a relation, because at boot the viewer is nobody. Every model with a
204
213
  `has_many` at that class then opens its table with a square to keep a row by, and kept
205
- rows come first. A model that cannot hold a bookmark gets no column.
214
+ rows come first. A model that cannot hold a bookmark gets no column, and neither does a
215
+ visitor the Proc answers nil for — somebody who never signed in has nowhere to keep one.
206
216
 
207
217
  <img width="3824" height="1220" alt="Image" src="https://github.com/user-attachments/assets/d1db4adb-0b5c-4e41-8bb6-cf72a35288f0" />
208
218
 
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
219
+ ### Maps and calendars
232
220
 
233
221
  A table whose model keeps a `google_place_id`, or a `latitude` and a `longitude`, can be
234
222
  read as a Google map of the same page: the footer under it offers `Display as map`, and
235
223
  `/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.
224
+ search, sort and pages work the same on either shape. The key and the map ID are the
225
+ host's own credentials, under `google_maps` as `api_key` and `map_id`.
286
226
 
227
+ A table whose model keeps a `starts_at` and an `ends_at` can be read as a week of the
228
+ same rows: the footer offers `Display as calendar`, and `/shifts.cal` draws a column a
229
+ day with each row placed by the hours it runs between. `?week=2026-09-13` moves to the
230
+ week holding that day, and the search and the filters travel with it.
287
231
 
288
232
  ### Positionable tables
289
233
 
290
234
  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.
235
+ grip opens each row, dragging it moves the row, and the place it lands in is written to
236
+ the route `positionable: true` drew under the index. The column decides the rest — the
237
+ grips, the callbacks that keep the numbers running 1, 2, 3, and `recourse_order`, since
238
+ the order a table is read in and the order somebody put it in are one fact. A model
239
+ pointing two ways says which of them its place is counted within:
303
240
 
304
241
  ```ruby
305
242
  class Step < ActiveRecord::Base
306
243
  belongs_to :team
307
244
  belongs_to :person
308
245
 
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
246
  def recourse_siblings = team.steps
313
247
  end
314
248
  ```
315
249
 
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.
250
+ ### Retrievable tables
349
251
 
252
+ A table whose rows come from somewhere else — a CRM, a feed — offers to fetch them
253
+ again: `recourses :visits, only: :index, retrievable: true` draws
254
+ `POST /providers/5/visits/retrieval` and the `Retrieve` button that posts to it. Where
255
+ the rows come from is the host's to know, so `Providers::Visits::RetrievalsController`
256
+ is the host's to write, and `recourse_retrievable?` on the listing controller is how a
257
+ page says there is nothing to fetch from.
350
258
 
351
259
  ## Development
352
260
 
@@ -51,6 +51,14 @@
51
51
  </div>
52
52
  <%# Submitted by the browser rather than by Turbo: a log out ends with a redirect to
53
53
  wherever the host signs people in, which is another origin a fetch cannot follow. -%>
54
+ <%# The way in, where the host drew a route named `enter`: a link, since what it
55
+ leads to is a form rather than a change. -%>
56
+ <% if enter? %>
57
+ <%= link_to enter_path, class: 'nav-link border-0 bg-transparent' do %>
58
+ <i class='bi bi-box-arrow-in-right'></i>
59
+ <span class='recourse-foot-word'><%= t 'recourse.enter' %></span>
60
+ <% end %>
61
+ <% end %>
54
62
  <% if exit? %>
55
63
  <%= button_to exit_path, method: :delete, class: 'nav-link border-0 bg-transparent',
56
64
  form: { data: { turbo: false } } do %>
@@ -53,6 +53,7 @@ en:
53
53
  deleted_error: "%{model} could not be deleted."
54
54
  destroy: Delete
55
55
  exit: Exit
56
+ enter: Sign in
56
57
  displaying:
57
58
  one: Displaying 1 item
58
59
  other: Displaying %{count} items
@@ -17,7 +17,8 @@ module Recourse
17
17
  # would run a host's Proc long before there is a request for it to read.
18
18
  def self.bookmarks? = !@declared_bookmarks.nil?
19
19
 
20
- # The viewer's bookmarks, resolved for this request.
20
+ # The viewer's bookmarks, resolved for this request, or nil where the Proc answered
21
+ # nil: nobody is looking whose rows could be kept, a visitor who never signed in.
21
22
  def self.bookmarks
22
23
  bookmarks = @declared_bookmarks
23
24
  bookmarks = bookmarks.call if bookmarks.respond_to? :call
@@ -28,10 +29,11 @@ module Recourse
28
29
  # How a model's bookmarks point back at it: its own `has_many` at the bookmark
29
30
  # class, which is both the opt-in and everything needed to write one. A model that
30
31
  # cannot hold a bookmark has not declared one, so it gets no column.
32
+ # And none for a viewer the Proc named nobody, who has nowhere to keep a row.
31
33
  def self.bookmarks_for(model)
32
34
  return unless bookmarks?
33
35
 
34
- klass = bookmarks.klass
36
+ klass = bookmarks&.klass or return
35
37
  model.reflect_on_all_associations(:has_many).find { |one| one.klass == klass }
36
38
  end
37
39
 
@@ -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
@@ -15,23 +15,38 @@ module Recourse
15
15
  end
16
16
 
17
17
  # The places, with the layer they are areas on — pins where the model is no geography
18
- # — or the points, where a model keeps no place.
18
+ # — or the points, where a model keeps no place. Each carries where its row leads and
19
+ # what it is called, so a pin or an area is clicked through to the record the way the
20
+ # row's own link in the table is.
19
21
  def map_rows(recourses)
20
22
  return { map_points_value: map_points(recourses) } unless Recourse.placed? resource_model
21
23
 
22
24
  {
23
25
  map_boundary_value: Recourse.boundary(resource_model)&.to_s&.upcase,
24
- map_places_value: recourses.filter_map { |one| one.attributes[Recourse::PLACE_COLUMN] },
26
+ map_places_value: map_places(recourses),
25
27
  }
26
28
  end
27
29
 
28
- # Only a row with both halves is a point; one with either missing is nowhere.
30
+ # Each place as `[id, href, title]`, for a row that names one.
31
+ def map_places(recourses)
32
+ recourses.filter_map do |one|
33
+ place = one.attributes[Recourse::PLACE_COLUMN]
34
+ [place, *map_lead(one)] if place
35
+ end
36
+ end
37
+
38
+ # Each point as `[lat, lng, href, title]`. Only a row with both halves is a point; one
39
+ # with either missing is nowhere.
29
40
  def map_points(recourses)
30
41
  recourses.filter_map do |one|
31
42
  point = one.attributes.values_at(*Recourse::POINT_COLUMNS)
32
- point.map(&:to_f) if point.all?
43
+ [*point.map(&:to_f), *map_lead(one)] if point.all?
33
44
  end
34
45
  end
46
+
47
+ # Where a row's pin leads, the record's own page wherever one is routed, and what the
48
+ # pin says on hover: nothing to lead to where no page is.
49
+ def map_lead(record) = [resource_action_path(:show, record), Recourse.record_title(record)]
35
50
  end
36
51
  end
37
52
  end
@@ -10,6 +10,13 @@ module Recourse
10
10
  # convention, and its helper existing is what the sidebar asks.
11
11
  def exit? = respond_to? :exit_path
12
12
 
13
+ # And the way in, its mirror: a route named `enter` — `resource :session, only:
14
+ # :new, as: :enter`, or a `get` of the host's own — earns a link beside the toggle
15
+ # to wherever the host signs people in. A link rather than a button, since asking
16
+ # for a form to sign in with changes nothing. Whether a viewer who already signed
17
+ # in still sees it is the host's to say, by answering this for them.
18
+ def enter? = respond_to? :enter_path
19
+
13
20
  # Sidebar entries as [name, title, path, key], in the order routes.rb declares
14
21
  # them, `key` being where in the title the letter that reaches it sits.
15
22
  def sidebar_resources
@@ -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.9.0'
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.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - claudiob
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '6.7'
46
+ version: '6.10'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: '6.7'
53
+ version: '6.10'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: pagy
56
56
  requirement: !ruby/object:Gem::Requirement