recourse 5.1.0 → 5.2.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: bc89ed83bcbaae940eaed20f48be082dc774898d05f74482b356b4eaf6ccdaa2
4
- data.tar.gz: 6d12db3c10f106031e448badb96ae965e8ec4135d7f6bd85b78dd151ac039743
3
+ metadata.gz: 70333646b284ff615c653da393325f5ea94bb7bd04f73f46b9bff27e5d693a1c
4
+ data.tar.gz: 1b386bf8262585fa5d3d5493e4297ea3521bac6105f8f9fffba64ecbdc7bd12d
5
5
  SHA512:
6
- metadata.gz: c7ef41021c4e0abd0211de0b2cceb10fd731ef1193b52f816417243263b51865c46135a6104a0daa7d17f6e57e57dfdeecf64b457f0229f3f2d66ea07bd90dbc
7
- data.tar.gz: 30722d3d332f26c81bfe6327bfe51f5952fd2af2bb5dace8419f3f39daa3a8b72ea9f4f3000e7b0e15b33b79067f591a430a19ed3ef1707d61f64b5ab43f477e
6
+ metadata.gz: 88b168334941dfe01535414620b28dffea6ceedf1583e023f586b71f33c5f6eda669c900a45bd293b28c0d2b2f262c89bfd276f6bc396f08f05bc1c9a2e062c9
7
+ data.tar.gz: b890abc1502680ad0fcbf1ba27bfcbca86743bbb051d2e30bc9ea171cc87f495885baa8158c3b672efb7549b64370fc2e9068883a9ecbc2c2f522d8d2ffe6c86
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## 5.2.0 - 2026-09-14
11
+
12
+ * [Feature] A table of places can be read as a map
13
+
14
+ A model keeping a `google_place_id`, or a `latitude` and a `longitude`, earns a second
15
+ shape for its index: `/counties.map` draws the page on a Google map, in the frame and
16
+ over the footer the table has, so the search, the sort and the pages work the same on
17
+ either. The footer under the table offers `Display as map` and the one under the map
18
+ `Display as table`, each keeping the page the other was on. A place ID is filled in as
19
+ an area where the model is a geography Google draws boundaries for — a `State`, a `County`,
20
+ a `City` or a `ZIP`, by name — and pinned at the place for any other model; a point is
21
+ pinned as it is. The key and the map style, with those
22
+ layers turned on, are read from the host's credentials under `google_maps` as `api_key`
23
+ and `map_id`. `.map` is registered as a name for HTML, and the index renders its HTML
24
+ templates for it. The design bundle is pinned at v0.5.0, which carries the `map`
25
+ controller.
26
+
10
27
  ## 5.1.0 - 2026-09-14
11
28
 
12
29
  * [Feature] The head past the gem's own tags is the host's, through a `recourses/head` partial
data/README.md CHANGED
@@ -144,6 +144,24 @@ Recourse.bookmarks = -> { Keepsake.where agent: Current.agent }
144
144
 
145
145
  <img width="3824" height="1220" alt="Image" src="https://github.com/user-attachments/assets/d1db4adb-0b5c-4e41-8bb6-cf72a35288f0" />
146
146
 
147
+ A table whose model keeps a `google_place_id`, or a `latitude` and a `longitude`, can be
148
+ read as a Google map of the same page: the footer under it offers `Display as map`, and
149
+ `/counties.map` draws this page's rows in the frame and over the footer the table has, so
150
+ search, sort and pages work the same on either shape. A place ID is filled in as an area
151
+ where the model is a geography Google draws boundaries for — a `State`, a `County`, a `City`
152
+ or a `ZIP`, by name — and pinned at the place for any other model; a point is a pin. The key
153
+ and the map ID are the host's credentials, and the map's style has the matching **Feature
154
+ layers** turned on in the Cloud console — Postal code for ZIPs, Administrative area level 2
155
+ for counties. A host with a Content Security Policy allows `maps.googleapis.com` for scripts
156
+ and connections and Google's tile hosts for images.
157
+
158
+ ```yaml
159
+ # config/credentials.yml.enc
160
+ google_maps:
161
+ api_key: AIza…
162
+ map_id: 4f2a…
163
+ ```
164
+
147
165
 
148
166
  ## Development
149
167
 
@@ -24,6 +24,8 @@ module Recourse
24
24
  search = Search.new recourse_relation, params[:q]
25
25
  @q = search.query
26
26
  @pagy, @resources = pagy search.scope, limit: recourse_limit
27
+ # A map is the same page in another shape, so the HTML templates draw it.
28
+ render :index, formats: :html if request.format.map?
27
29
  end
28
30
 
29
31
  # Builds a blank record under the name Rails would use: @contact for contacts, with
@@ -25,7 +25,7 @@
25
25
  over. -%>
26
26
  <link rel='preconnect' href='https://design.houseaccount.com'>
27
27
  <link rel='preconnect' href='https://design.houseaccount.com' crossorigin>
28
- <link rel='stylesheet' href='https://design.houseaccount.com/v0.3.1/css/houseaccount.css'>
28
+ <link rel='stylesheet' href='https://design.houseaccount.com/v0.5.0/css/houseaccount.css'>
29
29
  <%# The one thing these screens set that the house's stylesheet does not: system faces
30
30
  at a compact size, for pages dense with figures. 14px as rem, so a browser's own
31
31
  text-size setting still scales it. After the stylesheet, since both are `:root`. -%>
@@ -49,7 +49,7 @@
49
49
  <%# Turbo, Bootstrap and a Stimulus application with every controller this markup
50
50
  names, started once under `window.Stimulus`. In the head, so a controller exists
51
51
  before the body's first `data-controller`. -%>
52
- <script type='module' src='https://design.houseaccount.com/v0.3.1/js/houseaccount.js'></script>
52
+ <script type='module' src='https://design.houseaccount.com/v0.5.0/js/houseaccount.js'></script>
53
53
  </head>
54
54
 
55
55
  <%# The reader's zone is told to the server here rather than from a page of its
@@ -18,6 +18,12 @@
18
18
  <% else %>
19
19
  <%= t 'recourse.displaying', count: pagy.count %>
20
20
  <% end %>
21
+ <%# And the other shape the same rows can take, where the model keeps a place or
22
+ a point: a table becomes a map of this page's rows, and a map a table again. -%>
23
+ <% if (other = view_link) %>
24
+ <span class='fg-2'>&middot;</span>
25
+ <%= other %>
26
+ <% end %>
21
27
  </div>
22
28
  <%# Centered on the row of their own a phone gives them, and at the end of the shared row from `sm` up. -%>
23
29
  <div class='px-2 mt-2 sm:mt-0 d-flex justify-content-center sm:justify-content-end'><%== pagy.series_nav(:bootstrap, classes: 'pagination mb-0') if pagy.last > 1 %></div>
@@ -0,0 +1,13 @@
1
+ <%# locals: (recourses:, pagy:) %>
2
+ <%# This page of the rows on a Google map, each place's area filled in or pinned, in the
3
+ frame and over the footer the table has — so the search, the sort and the pages all
4
+ work the same on either shape. Never cached: the map is drawn by the browser. In a
5
+ card, which gives it the border and the corners the page's other card has —
6
+ `overflow` since the map is a canvas that would otherwise square the corners off —
7
+ and under it the margin a table keeps, so the footer stands as far from the map as
8
+ it does from the rows. -%>
9
+ <div class='card overflow-hidden mb-3'>
10
+ <div class='recourse-map' <%= tag.attributes data: map_data(recourses) %>></div>
11
+ </div>
12
+
13
+ <%= render 'footer', pagy: pagy %>
@@ -5,6 +5,6 @@
5
5
  <% if resources.blank? %>
6
6
  <%= render 'none' %>
7
7
  <% else %>
8
- <%= render 'table', recourses: resources, pagy: pagy %>
8
+ <%= render map_view? ? 'map' : 'table', recourses: resources, pagy: pagy %>
9
9
  <% end %>
10
10
  </turbo-frame>
@@ -11,6 +11,8 @@ en:
11
11
  ago: "%{distance} ago"
12
12
  all: All %{models}
13
13
  all_values: All
14
+ as_map: Display as map
15
+ as_table: Display as table
14
16
  blank: "—"
15
17
  bookmark: Bookmark
16
18
  bookmark_added: Bookmark added
@@ -14,5 +14,11 @@ module Recourse
14
14
  initializer 'recourse.routes' do
15
15
  ActionDispatch::Routing::Mapper.include Scopes, Routes
16
16
  end
17
+
18
+ # `/counties.map` is the same page in another shape, so the format is a name for
19
+ # HTML: without it Rails has no type for the extension and answers 406.
20
+ initializer 'recourse.formats' do
21
+ Mime::Type.register_alias 'text/html', :map
22
+ end
17
23
  end
18
24
  end
@@ -0,0 +1,52 @@
1
+ module Recourse
2
+ module Helpers
3
+ # The map a table of places is read as, and the link under either that leads to
4
+ # the other.
5
+ module Maps
6
+ private
7
+
8
+ # Whether this page is the map rather than the table.
9
+ def map_view? = request.format.map?
10
+
11
+ # The link under the rows to the other shape of the page, or nothing where the
12
+ # model keeps neither a place nor a point to draw one by. The query goes with it —
13
+ # the search, the sort and the page — so the map shows the rows the table did.
14
+ def view_link
15
+ return unless Recourse.mappable? resource_model
16
+
17
+ params = request.query_parameters
18
+ return link_to t('recourse.as_table'), url_for(params.merge(format: nil)) if map_view?
19
+
20
+ link_to t('recourse.as_map'), url_for(params.merge(format: :map))
21
+ end
22
+
23
+ # What the map controller needs: the key and the map from the host's credentials,
24
+ # and this page's rows as the places or the points they keep.
25
+ def map_data(recourses)
26
+ keys = Recourse.google_maps
27
+
28
+ { controller: 'map', map_key_value: keys[:api_key], map_id_value: keys[:map_id] }
29
+ .merge map_rows(recourses)
30
+ end
31
+
32
+ # The places, with the layer they are areas on — pins where the model is no geography
33
+ # — or the points, where a model keeps no place.
34
+ def map_rows(recourses)
35
+ return { map_points_value: map_points(recourses) } unless Recourse.placed? resource_model
36
+
37
+ {
38
+ map_boundary_value: Recourse.boundary(resource_model)&.to_s&.upcase,
39
+ map_places_value: recourses.filter_map { |one| one.attributes[Recourse::PLACE_COLUMN] },
40
+ }
41
+ end
42
+
43
+ # Only a row with both halves is a point; one with either missing is nowhere.
44
+ def map_points(recourses)
45
+ recourses.filter_map do |one|
46
+ point = one.attributes.values_at(*Recourse::POINT_COLUMNS)
47
+ point.map(&:to_f) if point.all?
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -19,6 +19,7 @@ require_relative 'helpers/formats'
19
19
  require_relative 'helpers/inputs'
20
20
  require_relative 'helpers/kinds'
21
21
  require_relative 'helpers/limits'
22
+ require_relative 'helpers/maps'
22
23
  require_relative 'helpers/names'
23
24
  require_relative 'helpers/navigation'
24
25
  require_relative 'helpers/parents'
@@ -44,7 +45,7 @@ module Recourse
44
45
  include Actions, Bookmarks, Buttons, Cards,
45
46
  Cells, Choices, Colors, Comboboxes, Constraints, Counters, Deletions, Densities,
46
47
  Details,
47
- Examples, Fields, Filters, Flashes, Formats, Inputs, Kinds, Limits,
48
+ Examples, Fields, Filters, Flashes, Formats, Inputs, Kinds, Limits, Maps,
48
49
  Names, Navigation, Parents, Pictures, References, Refreshes,
49
50
  Routing,
50
51
  Resources, Rows, Searches, Shortcuts, Sidebars, Sorts, Tabs, Themes,
@@ -0,0 +1,31 @@
1
+ # Reopened for the map a table of places can be read as.
2
+ module Recourse
3
+ # The column a row is drawn on a Google map by: the ID Google gives a place.
4
+ PLACE_COLUMN = 'google_place_id'
5
+
6
+ # The two a row is pinned by where it keeps no place: a point needs no lookup.
7
+ POINT_COLUMNS = %w[latitude longitude].freeze
8
+
9
+ # The boundary layer a model's places are areas on, by the name the model goes by —
10
+ # the four geographies a host is likely to keep, in Google's words for each. A model
11
+ # named otherwise has its places pinned.
12
+ BOUNDARIES = {
13
+ state: :administrative_area_level_1, county: :administrative_area_level_2,
14
+ city: :locality, zip: :postal_code,
15
+ }.freeze
16
+
17
+ # Whether a model's rows can be drawn on a map: by the place each keeps, or its point.
18
+ def self.mappable?(model) = placed?(model) || (POINT_COLUMNS - model.column_names).empty?
19
+
20
+ # The layer a model's places are drawn on, or nil for a model that is no geography.
21
+ def self.boundary(model) = BOUNDARIES[model.model_name.singular.to_sym]
22
+
23
+ # Whether a model's rows keep a place ID, which an area or a pin at a place is drawn from.
24
+ def self.placed?(model) = model.column_names.include? PLACE_COLUMN
25
+
26
+ # The key the Maps API is called with and the map the rows are drawn on, read from the
27
+ # host's own credentials under `google_maps` as `api_key` and `map_id`.
28
+ def self.google_maps
29
+ Rails.application.credentials[:google_maps] || {}
30
+ end
31
+ end
@@ -25,7 +25,7 @@ module Recourse
25
25
  # Where the palettes are served from, named once: the layout links one, the layout's
26
26
  # own script puts back the one a reader chose, and the sidebar's controller swaps it.
27
27
  # The version is the one the layout links its stylesheet and script at.
28
- THEMES_PATH = 'https://design.houseaccount.com/v0.3.1/theme'
28
+ THEMES_PATH = 'https://design.houseaccount.com/v0.5.0/theme'
29
29
 
30
30
  # Where a reader's chosen palette and mode are kept in their browser. Named once for
31
31
  # the same reason: the sidebar's controller writes it and the layout reads it back.
@@ -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 = '5.1.0'
3
+ VERSION = '5.2.0'
4
4
  end
data/lib/recourse.rb CHANGED
@@ -13,6 +13,7 @@ require_relative 'recourse/controllers'
13
13
  require_relative 'recourse/densities'
14
14
  require_relative 'recourse/helpers'
15
15
  require_relative 'recourse/limits'
16
+ require_relative 'recourse/maps'
16
17
  require_relative 'recourse/orders'
17
18
  require_relative 'recourse/writes'
18
19
  require_relative 'recourse/zones'
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: 5.1.0
4
+ version: 5.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - claudiob
@@ -141,6 +141,7 @@ files:
141
141
  - app/views/recourses/_flash.html.erb
142
142
  - app/views/recourses/_footer.html.erb
143
143
  - app/views/recourses/_form.html.erb
144
+ - app/views/recourses/_map.html.erb
144
145
  - app/views/recourses/_none.html.erb
145
146
  - app/views/recourses/_results.html.erb
146
147
  - app/views/recourses/_row.html.erb
@@ -184,6 +185,7 @@ files:
184
185
  - lib/recourse/helpers/inputs.rb
185
186
  - lib/recourse/helpers/kinds.rb
186
187
  - lib/recourse/helpers/limits.rb
188
+ - lib/recourse/helpers/maps.rb
187
189
  - lib/recourse/helpers/names.rb
188
190
  - lib/recourse/helpers/navigation.rb
189
191
  - lib/recourse/helpers/parents.rb
@@ -204,6 +206,7 @@ files:
204
206
  - lib/recourse/helpers/zones.rb
205
207
  - lib/recourse/icons.rb
206
208
  - lib/recourse/limits.rb
209
+ - lib/recourse/maps.rb
207
210
  - lib/recourse/orders.rb
208
211
  - lib/recourse/recoursive.rb
209
212
  - lib/recourse/registry.rb