recourse 6.0.0 → 7.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 +47 -0
- data/README.md +16 -0
- data/app/views/layouts/recourses.html.erb +15 -1
- data/app/views/recourses/_card.html.erb +3 -0
- data/app/views/recourses/edit.html.erb +1 -8
- data/app/views/recourses/index.html.erb +1 -9
- data/app/views/recourses/show.html.erb +9 -14
- data/lib/recourse/columns.rb +9 -1
- data/lib/recourse/counters.rb +40 -0
- data/lib/recourse/engine.rb +4 -0
- data/lib/recourse/helpers/cards.rb +7 -0
- data/lib/recourse/helpers/cells.rb +2 -2
- data/lib/recourse/helpers/counters.rb +1 -1
- data/lib/recourse/helpers/deletions.rb +10 -1
- data/lib/recourse/helpers/filters.rb +1 -1
- data/lib/recourse/helpers/kinds.rb +1 -1
- data/lib/recourse/helpers/rows.rb +1 -1
- data/lib/recourse/helpers/searches.rb +13 -3
- data/lib/recourse/helpers/sorts.rb +1 -1
- data/lib/recourse/helpers/tabs.rb +1 -1
- data/lib/recourse/recoursive.rb +3 -12
- data/lib/recourse/searchable/columns.rb +1 -0
- data/lib/recourse/searchable/terms.rb +12 -21
- data/lib/recourse/searchable.rb +3 -1
- data/lib/recourse/searches.rb +31 -0
- data/lib/recourse/version.rb +1 -1
- data/lib/recourse.rb +6 -3
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 80390029ce0bfed87fcf8247faceb992a392422044f0b0641bbfadb9bd3660f0
|
|
4
|
+
data.tar.gz: 81f226e7f69b2fa9846e2bc80e610d227e182d680b8c1d563b095a26f26fee1c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f87b1572b17070744c3c03c2f64b6d699e53a738270b4bf2d88f0040264992a050709e891fde967e2c21f64508e41191a56cab0ba3b7e3dda1fa04bb3b25f9dd
|
|
7
|
+
data.tar.gz: bcf92ebc75b14486038c81b558580ff958e49d28a17259d895a97888e5a350dd3afe610f72b959b343a9a47b9c41a09b203518b1f28532d61fada8df683b5f2c
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,53 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## 7.1.0 - 2026-09-20
|
|
11
|
+
|
|
12
|
+
* [CHANGE] The card draws the button that deletes what the page is about
|
|
13
|
+
|
|
14
|
+
It was drawn by the gem's own `edit` template, so a host writing a template of its own
|
|
15
|
+
lost it and wrote `destroy_resource_button` back by hand. The card draws it now, the way
|
|
16
|
+
it draws the tabs and the buttons beside the trail, and a host's page is its body alone.
|
|
17
|
+
|
|
18
|
+
A record's own page carries it, whichever of the two is open, so a resource routed to be
|
|
19
|
+
read and deleted but never changed is deleted from the page that reads it. Nothing gains
|
|
20
|
+
the ability to delete what it could not: a route without `destroy` draws no button.
|
|
21
|
+
|
|
22
|
+
* [CHANGE] A column the database generates is never offered on a form
|
|
23
|
+
|
|
24
|
+
Postgres writes a stored generated column out of the others and takes no value for one,
|
|
25
|
+
so a field for it would be refused whatever was typed. Such a column is read on every
|
|
26
|
+
other screen as before.
|
|
27
|
+
|
|
28
|
+
## 7.0.0 - 2026-09-19
|
|
29
|
+
|
|
30
|
+
* [BREAKING] What a page counts is the gem's to work out
|
|
31
|
+
|
|
32
|
+
`recourse_counters` is gone from every model. A column is counted where it holds a
|
|
33
|
+
counter cache, as before, and now also where it is named `<association>_count` for an
|
|
34
|
+
association the model has -- so a figure the app keeps itself, a `has_many through`
|
|
35
|
+
Rails will not cache among them, is drawn as a count without being declared.
|
|
36
|
+
|
|
37
|
+
The rule a host signs up for: a `*_count` column whose prefix names an association must
|
|
38
|
+
hold the count of that association. One naming nothing is an ordinary number, and one
|
|
39
|
+
naming an association it does not count wants another name -- the heading and the link
|
|
40
|
+
a counter draws would both say the wrong thing. `recourse_hidden` takes such a column
|
|
41
|
+
off the screens.
|
|
42
|
+
|
|
43
|
+
* [BREAKING] A record's card is the layout's, not the template's
|
|
44
|
+
|
|
45
|
+
A host writing its own `show`, `edit` or nested `index` for a recoursed model used to
|
|
46
|
+
replace the whole page, card and all, and wrote `render layout: 'recourses/card'` back by
|
|
47
|
+
hand to get the tabs and the buttons beside the trail. The card is drawn around whatever
|
|
48
|
+
template answers now, so a host's page is its body and nothing else.
|
|
49
|
+
|
|
50
|
+
Every host view still wrapping itself draws a card inside a card: drop the wrapper. A page
|
|
51
|
+
that wants the width to itself assigns `@recourse_card = false`.
|
|
52
|
+
|
|
53
|
+
`card_record` is what the card is about -- the parent on a page nested under one, the
|
|
54
|
+
record itself on its look and its change -- which is the rule every hand-written wrapper
|
|
55
|
+
was already passing.
|
|
56
|
+
|
|
10
57
|
## 6.0.0 - 2026-09-19
|
|
11
58
|
|
|
12
59
|
* [Change] These pages are drawn from bh
|
data/README.md
CHANGED
|
@@ -99,6 +99,22 @@ To change the content displayed in the `index` table of a model, override any of
|
|
|
99
99
|
| [`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
100
|
| [`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
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`.
|
|
117
|
+
|
|
102
118
|
For instance, this would yield a more compact `index` than the default configuration:
|
|
103
119
|
|
|
104
120
|
```ruby
|
|
@@ -59,6 +59,20 @@
|
|
|
59
59
|
<%= render 'recourses/sidebar' %>
|
|
60
60
|
</aside>
|
|
61
61
|
|
|
62
|
+
<%# A record's pages are drawn in its card whoever wrote the template, so the tabs and
|
|
63
|
+
the buttons beside the trail stay the gem's and a host's own page is its body. Drawn
|
|
64
|
+
here rather than where it is written, since the card is where the bare actions are
|
|
65
|
+
said and the trail below is where they are read. -%>
|
|
66
|
+
<% page = capture do %>
|
|
67
|
+
<% if (record = card_record) %>
|
|
68
|
+
<%= render layout: 'recourses/card', locals: { record: record } do %>
|
|
69
|
+
<%= yield %>
|
|
70
|
+
<% end %>
|
|
71
|
+
<% else %>
|
|
72
|
+
<%= yield %>
|
|
73
|
+
<% end %>
|
|
74
|
+
<% end %>
|
|
75
|
+
|
|
62
76
|
<nav class='navbar border-bottom'>
|
|
63
77
|
<%# `row-gap` rather than a margin on the search: a margin is there whether the
|
|
64
78
|
form wrapped or not, and on the line it shares with the breadcrumb it drops
|
|
@@ -74,7 +88,7 @@
|
|
|
74
88
|
</nav>
|
|
75
89
|
|
|
76
90
|
<main class='py-4 px-3'>
|
|
77
|
-
<%=
|
|
91
|
+
<%= page %>
|
|
78
92
|
</main>
|
|
79
93
|
|
|
80
94
|
<%= toasts(data: written_data) { |key, message| written_message key, message } %>
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
<%# Every bare action the routes drew under this record: a nested resource with no
|
|
5
5
|
index of its own is reached from nowhere, so its button sits beside the
|
|
6
6
|
breadcrumbs wherever the record is shown, on whichever of its pages that is. -%>
|
|
7
|
+
<%# The button that deletes what the page is about, drawn here rather than in a template
|
|
8
|
+
so a host writing its own show or edit keeps it without asking. -%>
|
|
7
9
|
<% content_for :actions do %>
|
|
10
|
+
<%= destroy_resource_button destroyable_record %>
|
|
8
11
|
<% bare_action_buttons(record).each do |label, path, method| %>
|
|
9
12
|
<%= button_to label, path, method:, form_class: 'ms-3',
|
|
10
13
|
class: 'btn theme-primary btn-sm btn-solid' %>
|
|
@@ -1,10 +1,3 @@
|
|
|
1
1
|
<% content_for :title, resource_record_label %>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
<%= destroy_resource_button resource_record %>
|
|
5
|
-
<% end %>
|
|
6
|
-
|
|
7
|
-
<%# The card is the parent's on a page nested under one, as it is for a nested index. -%>
|
|
8
|
-
<%= render layout: 'card', locals: { record: resource_parent || resource_record } do %>
|
|
9
|
-
<%= render 'form', resource_key => resource_record %>
|
|
10
|
-
<% end %>
|
|
3
|
+
<%= render 'form', resource_key => resource_record %>
|
|
@@ -22,12 +22,4 @@
|
|
|
22
22
|
cable connection down and reopens it. -%>
|
|
23
23
|
<%= refresh_subscription %>
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
card, beside the record's other pages — its tab, the count, the current one. -%>
|
|
27
|
-
<% if resource_parent %>
|
|
28
|
-
<%= render layout: 'card', locals: { record: resource_parent } do %>
|
|
29
|
-
<%= render 'results', resources: @resources, pagy: @pagy, week: @recourse_week %>
|
|
30
|
-
<% end %>
|
|
31
|
-
<% else %>
|
|
32
|
-
<%= render 'results', resources: @resources, pagy: @pagy, week: @recourse_week %>
|
|
33
|
-
<% end %>
|
|
25
|
+
<%= render 'results', resources: @resources, pagy: @pagy, week: @recourse_week %>
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
<% content_for :title, resource_record_label %>
|
|
2
2
|
|
|
3
|
-
<%#
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<%= render 'values', resource_key => resource_record %>
|
|
13
|
-
</div>
|
|
14
|
-
<% else %>
|
|
15
|
-
<%= render 'none', models: resource_name %>
|
|
16
|
-
<% end %>
|
|
3
|
+
<%# Nothing to read out where the host found no record: a singular resource is reached
|
|
4
|
+
with no id, so the one it stands for is one it may not have yet, and the page says so
|
|
5
|
+
rather than reading attributes off nothing. -%>
|
|
6
|
+
<% if resource_record %>
|
|
7
|
+
<div class='row recourse-values'>
|
|
8
|
+
<%= render 'values', resource_key => resource_record %>
|
|
9
|
+
</div>
|
|
10
|
+
<% else %>
|
|
11
|
+
<%= render 'none', models: resource_name %>
|
|
17
12
|
<% end %>
|
data/lib/recourse/columns.rb
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# Reopened for the order a row reads in, which a table, a show page and a form all ask
|
|
2
2
|
# for the same way.
|
|
3
3
|
module Recourse
|
|
4
|
+
# Columns the database writes itself out of the others, which a form never offers: a
|
|
5
|
+
# stored generated column takes no value, and Postgres refuses the one a form would send.
|
|
6
|
+
# @param model [Class] the model the page is about.
|
|
7
|
+
# @return [Array<String>] the names of its generated columns.
|
|
8
|
+
def self.virtual_columns(model)
|
|
9
|
+
model.columns.select(&:virtual?).map(&:name)
|
|
10
|
+
end
|
|
11
|
+
|
|
4
12
|
# Which part of a row a column belongs to. What a column holds is the gem's to know
|
|
5
13
|
# and where the schema put it is the host's, so both have a say: the kind picks the
|
|
6
14
|
# band, and the order inside the band is the one the table already has. Extended onto
|
|
@@ -55,7 +63,7 @@ module Recourse
|
|
|
55
63
|
# what state it is in; and a key is an integer, so it has to be recognised as a key
|
|
56
64
|
# before its type is asked about at all.
|
|
57
65
|
def band(model, name, keys)
|
|
58
|
-
return :counter if model.
|
|
66
|
+
return :counter if Recourse.counters(model).key? name
|
|
59
67
|
return :state if name == model.inheritance_column || model.defined_enums.key?(name)
|
|
60
68
|
return :timestamp if TIMESTAMPS.include? name
|
|
61
69
|
return :reference if keys.include? name
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Reopened for what a page counts, which a table, a heading and a tab all read here.
|
|
2
|
+
module Recourse
|
|
3
|
+
# What a page counts, worked out once per class: a reload replaces the classes the
|
|
4
|
+
# reflections belong to, so the engine empties this where it does.
|
|
5
|
+
@counters = {}
|
|
6
|
+
|
|
7
|
+
# The counts a model's pages draw, as `{ column => association }`. A column holding a
|
|
8
|
+
# counter cache answers, read from the `belongs_to` where `counter_cache` is declared;
|
|
9
|
+
# so does any `*_count` column named after an association the model has, which is how a
|
|
10
|
+
# figure the app keeps itself -- a `has_many through`, which Rails will not cache -- is
|
|
11
|
+
# counted like any other. A `*_count` column naming no association is a number and
|
|
12
|
+
# nothing more, and one naming an association it does not count belongs under another
|
|
13
|
+
# name: the heading and the link a counter draws would both say the wrong thing.
|
|
14
|
+
# @param model [Class] the model the page is about.
|
|
15
|
+
# @return [Hash{String => ActiveRecord::Reflection::AbstractReflection}] the counts.
|
|
16
|
+
def self.counters(model)
|
|
17
|
+
@counters[model.name] ||= cached_counters(model).merge counted_columns(model)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Empties what was read off the reflections of classes a reload has replaced.
|
|
21
|
+
# @return [Hash] the empty table.
|
|
22
|
+
def self.forget_counters = @counters.clear
|
|
23
|
+
|
|
24
|
+
private_class_method def self.cached_counters(model)
|
|
25
|
+
model.reflect_on_all_associations(:has_many).filter_map do |association|
|
|
26
|
+
column = association.inverse_of&.counter_cache_column
|
|
27
|
+
[column, association] if column
|
|
28
|
+
end.to_h
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private_class_method def self.counted_columns(model)
|
|
32
|
+
model.column_names.filter_map do |column|
|
|
33
|
+
name = column.delete_suffix '_count'
|
|
34
|
+
next if name == column
|
|
35
|
+
|
|
36
|
+
association = model.reflect_on_association name
|
|
37
|
+
[column, association] if association
|
|
38
|
+
end.to_h
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/recourse/engine.rb
CHANGED
|
@@ -15,6 +15,10 @@ module Recourse
|
|
|
15
15
|
ActionDispatch::Routing::Mapper.include Scopes, Routes
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# Reflections belong to the classes a reload replaces, so what was read off them
|
|
19
|
+
# goes with the reload.
|
|
20
|
+
config.to_prepare { Recourse.forget_counters }
|
|
21
|
+
|
|
18
22
|
# `/counties.map` and `/shifts.cal` are the same page in another shape, so each
|
|
19
23
|
# format is a name for HTML: without one Rails has no type for the extension and
|
|
20
24
|
# answers 406.
|
|
@@ -5,6 +5,13 @@ module Recourse
|
|
|
5
5
|
module Cards
|
|
6
6
|
private
|
|
7
7
|
|
|
8
|
+
# The record whose card this page is drawn in: the parent, on a page nested under
|
|
9
|
+
# one, and the record itself where the page is its look or its change. An index of
|
|
10
|
+
# its own, and a form for a record that is not there yet, stand outside a card.
|
|
11
|
+
def card_record
|
|
12
|
+
resource_parent || (resource_record if controller.action_name.in? %w[show edit])
|
|
13
|
+
end
|
|
14
|
+
|
|
8
15
|
# The pages of the record the card is about, as `[label, path, current]` — a
|
|
9
16
|
# look first, a change second, then one tab per nested index: `8 ZIPs` where
|
|
10
17
|
# a counter cache answers, the bare `Settings` where none does. On a nested
|
|
@@ -58,7 +58,7 @@ module Recourse
|
|
|
58
58
|
# counter is headed with what it counts — `ZIPs`, not `ZIPs count` — since the
|
|
59
59
|
# column holds a number and the heading says what the number is of.
|
|
60
60
|
def resource_column_title(column)
|
|
61
|
-
counted = resource_model
|
|
61
|
+
counted = Recourse.counters(resource_model)[column]
|
|
62
62
|
return dated_title column unless counted
|
|
63
63
|
|
|
64
64
|
Recourse.model_title counted.klass
|
|
@@ -81,7 +81,7 @@ module Recourse
|
|
|
81
81
|
value = resource.attributes[column]
|
|
82
82
|
return blob_link resource, value if blob_filename? column
|
|
83
83
|
|
|
84
|
-
counted = resource_model
|
|
84
|
+
counted = Recourse.counters(resource_model)[column]
|
|
85
85
|
|
|
86
86
|
# A count is the bare number — the icon in the heading already says what it
|
|
87
87
|
# counts — linking to the counted rows where a block nested their index here.
|
|
@@ -8,7 +8,7 @@ module Recourse
|
|
|
8
8
|
# The class a counter's cells carry, which is what sizes the column like the
|
|
9
9
|
# action columns beside it rather than like the columns carrying text.
|
|
10
10
|
def counter_class(column)
|
|
11
|
-
'recourse-counter' if resource_model.
|
|
11
|
+
'recourse-counter' if Recourse.counters(resource_model).key? column
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
# A figure, and the word saying what it counts. The `aria-label` carries both,
|
|
@@ -12,10 +12,18 @@ module Recourse
|
|
|
12
12
|
|
|
13
13
|
private
|
|
14
14
|
|
|
15
|
+
# The record a page may delete: the one it is about, on its own look or its own
|
|
16
|
+
# change page. A nested index wears the parent's card and deletes nothing from it.
|
|
17
|
+
# @api private
|
|
18
|
+
def destroyable_record
|
|
19
|
+
resource_record if controller.action_name.in? %w[show edit]
|
|
20
|
+
end
|
|
21
|
+
|
|
15
22
|
# Deletes the record on the page, or nothing at all where no action is routed
|
|
16
23
|
# to delete it with — the same two guards the edit link answers to.
|
|
24
|
+
# @api private
|
|
17
25
|
def destroy_resource_button(record)
|
|
18
|
-
path = destroy_resource_path
|
|
26
|
+
path = record && destroy_resource_path(record)
|
|
19
27
|
return unless path
|
|
20
28
|
|
|
21
29
|
confirm_button_to t('recourse.delete', model: resource_name), path,
|
|
@@ -24,6 +32,7 @@ module Recourse
|
|
|
24
32
|
form_class: 'd-inline-block'
|
|
25
33
|
end
|
|
26
34
|
|
|
35
|
+
# @api private
|
|
27
36
|
# What deleting this record takes with it, counted a level down and no further:
|
|
28
37
|
# a state reaches counties, then ZIPs, then locations, and counting that far
|
|
29
38
|
# would join 40,965 rows to draw one page.
|
|
@@ -78,7 +78,7 @@ module Recourse
|
|
|
78
78
|
def filter_counter(klass)
|
|
79
79
|
return if resource_parent
|
|
80
80
|
|
|
81
|
-
klass.
|
|
81
|
+
Recourse.counters(klass).find { |_, one| one.klass == resource_model }&.first
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
# Never invalid and never required: a filter narrows rather than sets.
|
|
@@ -43,7 +43,7 @@ module Recourse
|
|
|
43
43
|
# already follow; and everything else is the type the attribute itself reports —
|
|
44
44
|
# `:monetary` included, where a host has registered a type that says so.
|
|
45
45
|
def attribute_kind(column)
|
|
46
|
-
return :counter if resource_model.
|
|
46
|
+
return :counter if Recourse.counters(resource_model).key? column
|
|
47
47
|
return :enum if resource_model.defined_enums.key? column
|
|
48
48
|
return :list if Recourse.list_column? resource_model, column
|
|
49
49
|
return :phone if column == 'phone'
|
|
@@ -39,7 +39,7 @@ module Recourse
|
|
|
39
39
|
# showing the count it was cached with. Read off the rows in memory like the
|
|
40
40
|
# version is, so this costs no query either.
|
|
41
41
|
def counters_version(rows)
|
|
42
|
-
columns = resource_model.
|
|
42
|
+
columns = Recourse.counters(resource_model).keys
|
|
43
43
|
return if columns.empty?
|
|
44
44
|
|
|
45
45
|
rows.map { |row| row.attributes.values_at(*columns) }
|
|
@@ -44,13 +44,23 @@ module Recourse
|
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
# The model's search field, less the reach-through a nested route already
|
|
47
|
-
# answered: a page pinned to one provider offers no box to search them all.
|
|
47
|
+
# answered: a page pinned to one provider offers no box to search them all. A
|
|
48
|
+
# host naming a predicate of its own has said what it wants searched, and what
|
|
49
|
+
# it said is read on every page — which is what answering differently from the
|
|
50
|
+
# gem means here.
|
|
48
51
|
def resource_search_field
|
|
49
|
-
resource_model.search_field
|
|
52
|
+
chosen = resource_model.search_field
|
|
53
|
+
return chosen unless chosen == Recourse.search_field(resource_model)
|
|
54
|
+
|
|
55
|
+
Recourse.search_field resource_model, except: resource_parent_association
|
|
50
56
|
end
|
|
51
57
|
|
|
58
|
+
# And the words above it, narrowed the same way and for the same reason.
|
|
52
59
|
def resource_search_prompt
|
|
53
|
-
resource_model.search_prompt
|
|
60
|
+
chosen = resource_model.search_prompt
|
|
61
|
+
return chosen unless chosen == Recourse.search_prompt(resource_model)
|
|
62
|
+
|
|
63
|
+
Recourse.search_prompt resource_model, except: resource_parent_association
|
|
54
64
|
end
|
|
55
65
|
|
|
56
66
|
# A foreign key's cell shows a label from the other table, so what decides is
|
|
@@ -37,7 +37,7 @@ module Recourse
|
|
|
37
37
|
# and nobody types anything under it — and `Location address line 1` over a column
|
|
38
38
|
# of addresses reads as a form's question asked where there is no form.
|
|
39
39
|
def sort_title(column)
|
|
40
|
-
counted = resource_model
|
|
40
|
+
counted = Recourse.counters(resource_model)[column.to_s]
|
|
41
41
|
return counter_title counted if counted && @recourse_headers
|
|
42
42
|
|
|
43
43
|
resource_column_title column.to_s
|
|
@@ -100,7 +100,7 @@ module Recourse
|
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
def counter_of(model, association)
|
|
103
|
-
model.
|
|
103
|
+
Recourse.counters(model).find { |_, one| one == association }&.first if association
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
def join_of(model, association)
|
data/lib/recourse/recoursive.rb
CHANGED
|
@@ -25,22 +25,12 @@ module Recourse
|
|
|
25
25
|
# `%i[created_at updated_at]` asks for the two Rails keeps rather than the two
|
|
26
26
|
# a record is about. A timestamp named here still lands last, and in that order,
|
|
27
27
|
# whichever way round it was written.
|
|
28
|
+
# @api private
|
|
28
29
|
def recourse_displayed = []
|
|
29
30
|
|
|
30
|
-
# Columns holding a counter cache, each mapped to the association it counts. Read
|
|
31
|
-
# from the `belongs_to` on the other side, which is where `counter_cache` is
|
|
32
|
-
# declared: a column merely named `quote_count` is not one of these. Worked out
|
|
33
|
-
# once per class, the way `recourse_listable?` is: associations are declared at
|
|
34
|
-
# load and a reloaded class is a new one, with a memo of its own.
|
|
35
|
-
def recourse_counters
|
|
36
|
-
@recourse_counters ||= reflect_on_all_associations(:has_many).filter_map do |association|
|
|
37
|
-
column = association.inverse_of&.counter_cache_column
|
|
38
|
-
[column, association] if column
|
|
39
|
-
end.to_h
|
|
40
|
-
end
|
|
41
|
-
|
|
42
31
|
# `ZIP code`: what to call a foreign key pointing here. A form's label, a table's
|
|
43
32
|
# heading and a search prompt all name the same thing, so they name it once.
|
|
33
|
+
# @api private
|
|
44
34
|
def recourse_reference_name
|
|
45
35
|
attribute = Recourse.downcase human_attribute_name(recourse_label)
|
|
46
36
|
|
|
@@ -49,6 +39,7 @@ module Recourse
|
|
|
49
39
|
|
|
50
40
|
# True when the label has a length, so it is short enough to be typed and a
|
|
51
41
|
# form can ask for the value instead of listing every record to pick from.
|
|
42
|
+
# @api private
|
|
52
43
|
def recourse_typed_label?
|
|
53
44
|
validators_on(recourse_label).any? ActiveModel::Validations::LengthValidator
|
|
54
45
|
end
|
|
@@ -2,6 +2,7 @@ module Recourse
|
|
|
2
2
|
module Searchable
|
|
3
3
|
# What a model's own schema offers a search: which of its columns are worth
|
|
4
4
|
# looking through, and which of its foreign keys are worth looking past.
|
|
5
|
+
# @api private
|
|
5
6
|
module Columns
|
|
6
7
|
# Column types a search box can match on containment. An enum is a Postgres
|
|
7
8
|
# type of its own, and what it holds is a word, so it reads as a string too.
|
|
@@ -2,33 +2,20 @@ module Recourse
|
|
|
2
2
|
module Searchable
|
|
3
3
|
# What the search box above a table submits, and what it says while it is empty.
|
|
4
4
|
module Terms
|
|
5
|
-
# The predicate a search box submits: everything
|
|
6
|
-
# joined by `or`.
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
# pinned to one provider offers no box to search them all.
|
|
10
|
-
def search_field(except: nil)
|
|
11
|
-
fields, predicate = recourse_search_terms(except:)
|
|
12
|
-
return if fields.empty?
|
|
13
|
-
|
|
14
|
-
"#{fields.join '_or_'}_#{predicate}"
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# What the search box says while it is empty, naming what it looks through.
|
|
18
|
-
def search_prompt(except: nil)
|
|
19
|
-
fields, predicate = recourse_search_terms(except:)
|
|
20
|
-
return if fields.empty?
|
|
5
|
+
# The predicate a search box submits: everything the model looks through at once,
|
|
6
|
+
# joined by `or`. A host naming its own instead is taken whole, on every page the
|
|
7
|
+
# model is read on.
|
|
8
|
+
def search_field = Recourse.search_field(self)
|
|
21
9
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
private
|
|
10
|
+
# What the search box says while it is empty, naming what it looks through. A
|
|
11
|
+
# host's own words are taken whole here too.
|
|
12
|
+
def search_prompt = Recourse.search_prompt(self)
|
|
27
13
|
|
|
28
14
|
# What a search box looks through, and how it matches: the plaintext columns
|
|
29
15
|
# and the labels behind foreign keys — less the one `except:` names — on
|
|
30
16
|
# containment; or, for a model that keeps nothing in plaintext worth
|
|
31
17
|
# searching, its encrypted columns, whole.
|
|
18
|
+
# @api private
|
|
32
19
|
def recourse_search_terms(except: nil)
|
|
33
20
|
plain = recourse_searchable_columns + recourse_searchable_references(except:)
|
|
34
21
|
return [plain, 'cont'] if plain.any?
|
|
@@ -37,14 +24,18 @@ module Recourse
|
|
|
37
24
|
end
|
|
38
25
|
|
|
39
26
|
# Those same terms as words, lower case but for the acronyms among them.
|
|
27
|
+
# @api private
|
|
40
28
|
def recourse_search_names(except: nil)
|
|
41
29
|
fields, = recourse_search_terms(except:)
|
|
42
30
|
|
|
43
31
|
fields.map { |field| Recourse.downcase recourse_term_name(field) }
|
|
44
32
|
end
|
|
45
33
|
|
|
34
|
+
private
|
|
35
|
+
|
|
46
36
|
# A term is a column of this model, or a `zip_code` reaching through one of its
|
|
47
37
|
# foreign keys — which a form and a table already have a name for.
|
|
38
|
+
# @api private
|
|
48
39
|
def recourse_term_name(field)
|
|
49
40
|
reached = recourse_searchable_associations.find { |one| field.start_with? "#{one.name}_" }
|
|
50
41
|
return reached.klass.recourse_reference_name if reached
|
data/lib/recourse/searchable.rb
CHANGED
|
@@ -37,7 +37,7 @@ module Recourse
|
|
|
37
37
|
# for leaving those out too: hidden from every screen means hidden here.
|
|
38
38
|
def ransortable_attributes(_auth_object = nil)
|
|
39
39
|
readable = ransackable_attributes - recourse_encrypted_names
|
|
40
|
-
indexed = recourse_indexed_columns +
|
|
40
|
+
indexed = recourse_indexed_columns + Recourse.counters(self).keys + Recourse::TIMESTAMPS
|
|
41
41
|
|
|
42
42
|
keys = recourse_references.map { |one| one.foreign_key.to_s }
|
|
43
43
|
|
|
@@ -47,11 +47,13 @@ module Recourse
|
|
|
47
47
|
# True where a foreign key pointing here is typed rather than picked, which a
|
|
48
48
|
# form and the controller reading its parameters back have to agree on: the label
|
|
49
49
|
# is bounded, or the table is too long to list.
|
|
50
|
+
# @api private
|
|
50
51
|
def recourse_typed_reference? = recourse_typed_label? || !recourse_listable?
|
|
51
52
|
|
|
52
53
|
# True where every row of this model could be listed in one menu. The count is
|
|
53
54
|
# bounded, so the question costs the same on ten rows as on ten million, and it
|
|
54
55
|
# is asked once per class — a table that crosses the line is noticed at boot.
|
|
56
|
+
# @api private
|
|
55
57
|
def recourse_listable?
|
|
56
58
|
return @recourse_listable unless @recourse_listable.nil?
|
|
57
59
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Reopened for what a search box submits and what it says, which a page narrows by the
|
|
2
|
+
# association its route has already answered.
|
|
3
|
+
module Recourse
|
|
4
|
+
# The predicate a search box submits: every term the model looks through, joined by
|
|
5
|
+
# `or`. Nil where a model has nothing worth looking through, which is what leaves its
|
|
6
|
+
# index without the form -- filters and all. `except:` drops the association a nested
|
|
7
|
+
# route already answered, so a page pinned to one provider offers no box to search
|
|
8
|
+
# them all.
|
|
9
|
+
# @param model [Class] the model the page is about.
|
|
10
|
+
# @param except [ActiveRecord::Reflection::AbstractReflection, nil] what the route said.
|
|
11
|
+
# @return [String, nil] the predicate, or nothing where there is none.
|
|
12
|
+
def self.search_field(model, except: nil)
|
|
13
|
+
fields, predicate = model.recourse_search_terms(except:)
|
|
14
|
+
return if fields.empty?
|
|
15
|
+
|
|
16
|
+
"#{fields.join '_or_'}_#{predicate}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# What the search box says while it is empty, naming what it looks through.
|
|
20
|
+
# @param model [Class] the model the page is about.
|
|
21
|
+
# @param except [ActiveRecord::Reflection::AbstractReflection, nil] what the route said.
|
|
22
|
+
# @return [String, nil] the words, or nothing where there is nothing to look through.
|
|
23
|
+
def self.search_prompt(model, except: nil)
|
|
24
|
+
fields, predicate = model.recourse_search_terms(except:)
|
|
25
|
+
return if fields.empty?
|
|
26
|
+
|
|
27
|
+
list = model.recourse_search_names(except:).join ' or '
|
|
28
|
+
|
|
29
|
+
I18n.t "recourse.searched_#{predicate}", list: list
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/recourse/version.rb
CHANGED
data/lib/recourse.rb
CHANGED
|
@@ -12,6 +12,8 @@ require_relative 'recourse/bookmarks'
|
|
|
12
12
|
require_relative 'recourse/calendars'
|
|
13
13
|
require_relative 'recourse/colors'
|
|
14
14
|
require_relative 'recourse/columns'
|
|
15
|
+
require_relative 'recourse/counters'
|
|
16
|
+
require_relative 'recourse/searches'
|
|
15
17
|
require_relative 'recourse/themes'
|
|
16
18
|
require_relative 'recourse/icons'
|
|
17
19
|
require_relative 'recourse/controllers'
|
|
@@ -57,10 +59,11 @@ module Recourse
|
|
|
57
59
|
|
|
58
60
|
# Columns a user may set: the form offers these, the show page reads these out, and
|
|
59
61
|
# `create` permits these. A counter cache is none of a user's business — Rails keeps
|
|
60
|
-
# it, so a form that offered one would let it be typed over
|
|
62
|
+
# it, so a form that offered one would let it be typed over, and neither is a column
|
|
63
|
+
# the database generates: it takes no value at all.
|
|
61
64
|
def self.editable_columns(model)
|
|
62
|
-
ordered model, model.column_names - ['id', *TIMESTAMPS] - model.
|
|
63
|
-
hidden_columns(model)
|
|
65
|
+
ordered model, model.column_names - ['id', *TIMESTAMPS] - counters(model).keys -
|
|
66
|
+
hidden_columns(model) - virtual_columns(model)
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
# Columns no screen shows: whatever the model asked to hide through `recourse_hidden`
|
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:
|
|
4
|
+
version: 7.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- claudiob
|
|
@@ -185,6 +185,7 @@ files:
|
|
|
185
185
|
- lib/recourse/colors.rb
|
|
186
186
|
- lib/recourse/columns.rb
|
|
187
187
|
- lib/recourse/controllers.rb
|
|
188
|
+
- lib/recourse/counters.rb
|
|
188
189
|
- lib/recourse/densities.rb
|
|
189
190
|
- lib/recourse/engine.rb
|
|
190
191
|
- lib/recourse/helpers.rb
|
|
@@ -255,6 +256,7 @@ files:
|
|
|
255
256
|
- lib/recourse/searchable/columns.rb
|
|
256
257
|
- lib/recourse/searchable/filters.rb
|
|
257
258
|
- lib/recourse/searchable/terms.rb
|
|
259
|
+
- lib/recourse/searches.rb
|
|
258
260
|
- lib/recourse/themes.rb
|
|
259
261
|
- lib/recourse/titles.rb
|
|
260
262
|
- lib/recourse/version.rb
|