recourse 7.2.1 → 7.4.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 +25 -0
- data/app/controllers/concerns/recourse/parameter_resolution.rb +31 -0
- data/app/controllers/concerns/recourse/resource_resolution.rb +12 -24
- data/app/controllers/recourse/base_controller.rb +5 -1
- data/lib/recourse/bands.rb +80 -0
- data/lib/recourse/columns.rb +25 -75
- data/lib/recourse/helpers/breadcrumbs.rb +58 -0
- data/lib/recourse/helpers/cells.rb +1 -1
- data/lib/recourse/helpers/counters.rb +18 -0
- data/lib/recourse/helpers/formats.rb +11 -25
- data/lib/recourse/helpers/links.rb +33 -0
- data/lib/recourse/helpers/navigation.rb +1 -50
- data/lib/recourse/helpers/tabs.rb +0 -18
- data/lib/recourse/helpers/values.rb +1 -1
- data/lib/recourse/helpers.rb +9 -8
- data/lib/recourse/version.rb +1 -1
- data/lib/recourse.rb +1 -27
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da821317fa8c02a37911dd03b62aaeb072bb3cacbc40cfb15c29eb51c3ffbd8b
|
|
4
|
+
data.tar.gz: 276e8d9570b080d81dbefc6b304ba6d4a169e783191587cbe534059a2ccd3c0b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 172bd06e4fc69264846c29ed4c12123290aab2532c98033076c1fce4f0ab7829931cfeac6d16e16765c7d25117eec969f624b2fd3a66120878d85fc3f56f57ee
|
|
7
|
+
data.tar.gz: f7ab6fd22033642867259dc840ec3da125c9914c08af2a9617a72ebf67fa4cf7e70c7b5930f928ef8bac45855449111621ef4565b323da2631958e5a9af70392
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## 7.4.0 - 2026-09-20
|
|
11
|
+
|
|
12
|
+
* [CHANGE] A singular resource that has its record reads it rather than offering a form
|
|
13
|
+
|
|
14
|
+
`new` drew a form for a second record where the parent already kept one. At most one is
|
|
15
|
+
what singular means, so the reader is sent to the page that reads it — the mirror of the
|
|
16
|
+
redirect that already sent them to `new` when there was none yet. Before the action
|
|
17
|
+
rather than inside it, so a host writing its own `new` gets it too.
|
|
18
|
+
|
|
19
|
+
* [FIX] Every file in the gem is under a hundred lines again
|
|
20
|
+
|
|
21
|
+
Seven of them had grown past it. Each split along a seam it already had: web addresses
|
|
22
|
+
out of `Formats`, the trail out of `Navigation`, the counter lookups into `Counters`,
|
|
23
|
+
what a form sent into `ParameterResolution`, and which columns a screen uses apart from
|
|
24
|
+
what order they read in.
|
|
25
|
+
|
|
26
|
+
## 7.3.0 - 2026-09-20
|
|
27
|
+
|
|
28
|
+
* [CHANGE] A boolean reads as the word a reader answers with
|
|
29
|
+
|
|
30
|
+
A cell and a value read `true` and `false`, which is what the database keeps and not
|
|
31
|
+
what anybody asked. Both now read `Yes` and `No` — the two the filter menu beside the
|
|
32
|
+
column already offered — and a nullable column nobody answered reads as the dash every
|
|
33
|
+
other empty value reads as, rather than as a no.
|
|
34
|
+
|
|
10
35
|
## 7.2.1 - 2026-09-20
|
|
11
36
|
|
|
12
37
|
* [FIX] A refused change keeps the card it was made in
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
# Resolves what a form sent: the attributes a record may be written with, read back
|
|
3
|
+
# from what the route names and what the model lets a screen set.
|
|
4
|
+
module ParameterResolution
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
# What a form may submit: every column a user may set, less the ones the model
|
|
8
|
+
# keeps off its screens. The other thing a host overrides, and for the same
|
|
9
|
+
# reason as `recourse_relation` — a form of its own asks for what it asks for,
|
|
10
|
+
# which may be a hidden column or an attribute that is no column at all:
|
|
11
|
+
# `def resource_params = params.expect(provider: %i[name cid])`.
|
|
12
|
+
def resource_params
|
|
13
|
+
# The parent is merged after resolving, so the one the route names is never
|
|
14
|
+
# mistaken for a label; the files go no further than the permit that let them
|
|
15
|
+
# through, being attached rather than assigned.
|
|
16
|
+
submitted_attributes.except(*Recourse.attachment_names(resource_class))
|
|
17
|
+
.merge parent_columns
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# What the form sent, with a typed reference read back as the id it names. A bare
|
|
21
|
+
# `Create` submits no attributes at all, so the key may be absent: the parent a
|
|
22
|
+
# nested route names is everything such a record starts from.
|
|
23
|
+
def submitted_attributes
|
|
24
|
+
permitted = Recourse.editable_columns(resource_class) + attachment_filters
|
|
25
|
+
key = controller_name.singularize.to_sym
|
|
26
|
+
return {} unless params.key? key
|
|
27
|
+
|
|
28
|
+
resolve_lists resolve_references(params.expect(key => permitted))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -33,6 +33,18 @@ module Recourse
|
|
|
33
33
|
redirect_to url_for(action: :new) if Recourse.routed? controller_path, 'new'
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# And the other way, before a form is drawn: a singular resource holds at most one,
|
|
37
|
+
# so where the parent already keeps it there is nothing for `new` to make and the
|
|
38
|
+
# page that reads it answers instead. The mirror of `missing_record`, which sends a
|
|
39
|
+
# reader here when there is none yet. Where no `show` is drawn the form stands, that
|
|
40
|
+
# being the only page the routes gave this record.
|
|
41
|
+
def redirect_to_existing_record
|
|
42
|
+
return unless singular_reflection && Recourse.routed?(controller_path, 'show')
|
|
43
|
+
return unless @recourse_parent.association(singular_reflection.name).reader
|
|
44
|
+
|
|
45
|
+
redirect_to url_for(action: :show)
|
|
46
|
+
end
|
|
47
|
+
|
|
36
48
|
def assign(record)
|
|
37
49
|
@recourse = record
|
|
38
50
|
instance_variable_set "@#{controller_name.singularize}", record
|
|
@@ -76,29 +88,5 @@ module Recourse
|
|
|
76
88
|
|
|
77
89
|
resource_class.model_name.human
|
|
78
90
|
end
|
|
79
|
-
|
|
80
|
-
# What a form may submit: every column a user may set, less the ones the model
|
|
81
|
-
# keeps off its screens. The other thing a host overrides, and for the same
|
|
82
|
-
# reason as `recourse_relation` — a form of its own asks for what it asks for,
|
|
83
|
-
# which may be a hidden column or an attribute that is no column at all:
|
|
84
|
-
# `def resource_params = params.expect(provider: %i[name cid])`.
|
|
85
|
-
def resource_params
|
|
86
|
-
# The parent is merged after resolving, so the one the route names is never
|
|
87
|
-
# mistaken for a label; the files go no further than the permit that let them
|
|
88
|
-
# through, being attached rather than assigned.
|
|
89
|
-
submitted_attributes.except(*Recourse.attachment_names(resource_class))
|
|
90
|
-
.merge parent_columns
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# What the form sent, with a typed reference read back as the id it names. A bare
|
|
94
|
-
# `Create` submits no attributes at all, so the key may be absent: the parent a
|
|
95
|
-
# nested route names is everything such a record starts from.
|
|
96
|
-
def submitted_attributes
|
|
97
|
-
permitted = Recourse.editable_columns(resource_class) + attachment_filters
|
|
98
|
-
key = controller_name.singularize.to_sym
|
|
99
|
-
return {} unless params.key? key
|
|
100
|
-
|
|
101
|
-
resolve_lists resolve_references(params.expect(key => permitted))
|
|
102
|
-
end
|
|
103
91
|
end
|
|
104
92
|
end
|
|
@@ -4,7 +4,7 @@ module Recourse
|
|
|
4
4
|
# with a `before_action :authenticate!` guards every screen the gem serves.
|
|
5
5
|
class BaseController < ApplicationController
|
|
6
6
|
include Pagy::Method, Positioned, AttachmentResolution, AttachmentWriting,
|
|
7
|
-
Landing, Paging, ListResolution, ParentNaming,
|
|
7
|
+
Landing, Paging, ListResolution, ParameterResolution, ParentNaming,
|
|
8
8
|
ParentResolution, ReferenceResolution, ResourceResolution,
|
|
9
9
|
Weeks, Zoning
|
|
10
10
|
|
|
@@ -13,6 +13,10 @@ module Recourse
|
|
|
13
13
|
# `find` raises RecordNotFound, so an id that names nothing answers 404.
|
|
14
14
|
before_action :find_resource, only: %i[show edit update destroy]
|
|
15
15
|
|
|
16
|
+
# Before the form rather than inside `new`, so a host that writes its own still
|
|
17
|
+
# sends a reader to the record a singular resource already has.
|
|
18
|
+
before_action :redirect_to_existing_record, only: :new
|
|
19
|
+
|
|
16
20
|
# The model behind the page, assigned once, and only where the route names one.
|
|
17
21
|
before_action { @recourse_model = resource_class if resource_model? }
|
|
18
22
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Reopened for the order a row reads in, which a table, a show page and a form all ask
|
|
2
|
+
# for the same way.
|
|
3
|
+
module Recourse
|
|
4
|
+
# Which part of a row a column belongs to. What a column holds is the gem's to know
|
|
5
|
+
# and where the schema put it is the host's, so both have a say: the kind picks the
|
|
6
|
+
# band, and the order inside the band is the one the table already has. Extended onto
|
|
7
|
+
# `Recourse`, so this is `Recourse.ordered` wherever it is called from.
|
|
8
|
+
module Bands
|
|
9
|
+
# The bands, in the order a row reads. What kind of row this is and what state it is
|
|
10
|
+
# in, whose it is, what it says, its flags, the long values a narrow column suits
|
|
11
|
+
# least, when it happened, the two Rails keeps — and last of all the counts, which
|
|
12
|
+
# say nothing about the row itself, only how much hangs off it. So they close the
|
|
13
|
+
# row at the far edge rather than opening it beside the buttons they resemble.
|
|
14
|
+
#
|
|
15
|
+
# A flag follows what the row says rather than leading it. A yes or a no is narrow
|
|
16
|
+
# enough to have led on width alone, but it reads as a note *about* the row, and a
|
|
17
|
+
# reader scanning a table is looking for the name it belongs to first.
|
|
18
|
+
BANDS = %i[state reference scalar boolean long date timestamp counter].freeze
|
|
19
|
+
|
|
20
|
+
# Values that are paragraphs rather than words, under every name an adapter has for
|
|
21
|
+
# them: PostgreSQL reports `jsonb` where SQLite and MySQL report `json`.
|
|
22
|
+
LONG_KINDS = %i[text json jsonb].freeze
|
|
23
|
+
|
|
24
|
+
# And the ones that are a point in time, whichever part of one they keep.
|
|
25
|
+
DATE_KINDS = %i[date datetime time].freeze
|
|
26
|
+
|
|
27
|
+
# Whether a column holds a list of values rather than one. A PostgreSQL array
|
|
28
|
+
# reports a type wrapping a subtype, and so does a `serialize` of an Array; asked
|
|
29
|
+
# that way rather than by an adapter's own class, which only exists where that
|
|
30
|
+
# adapter is loaded. But wrapping a subtype is not enough on its own: an enum and a
|
|
31
|
+
# range always answered the same way, and since Rails 8.2 so does every decorator
|
|
32
|
+
# a column may wear — time zone conversion on a timestamp, `normalizes`, a lock. What
|
|
33
|
+
# sets a list apart is that its value is changed in place, which `Mutable` marks and
|
|
34
|
+
# none of those are.
|
|
35
|
+
def list_column?(model, column)
|
|
36
|
+
type = model.type_for_attribute column
|
|
37
|
+
|
|
38
|
+
type.respond_to?(:subtype) && type.is_a?(ActiveModel::Type::Helpers::Mutable)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The columns of a model in the order a row reads them, given whichever of them the
|
|
42
|
+
# caller is drawing. Grouped rather than sorted: `group_by` keeps the order it was
|
|
43
|
+
# given inside each group, which is what leaves an order the schema already carries
|
|
44
|
+
# standing, and `sort_by` would not — Ruby's sort is not stable.
|
|
45
|
+
def ordered(model, names)
|
|
46
|
+
keys = reference_keys model
|
|
47
|
+
|
|
48
|
+
names.group_by { |name| BANDS.index band(model, name, keys) }.sort.flat_map(&:last)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Asked in the order that settles it. A counter is one whatever it is stored as; the
|
|
54
|
+
# column Rails keeps a subclass in says what kind of row this is, as an enum says
|
|
55
|
+
# what state it is in; and a key is an integer, so it has to be recognised as a key
|
|
56
|
+
# before its type is asked about at all.
|
|
57
|
+
def band(model, name, keys)
|
|
58
|
+
return :counter if Recourse.counters(model).key? name
|
|
59
|
+
return :state if name == model.inheritance_column || model.defined_enums.key?(name)
|
|
60
|
+
return :timestamp if TIMESTAMPS.include? name
|
|
61
|
+
return :reference if keys.include? name
|
|
62
|
+
|
|
63
|
+
band_of model.type_for_attribute(name).type
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def band_of(kind)
|
|
67
|
+
return :boolean if kind == :boolean
|
|
68
|
+
return :long if LONG_KINDS.include? kind
|
|
69
|
+
return :date if DATE_KINDS.include? kind
|
|
70
|
+
|
|
71
|
+
:scalar
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def reference_keys(model)
|
|
75
|
+
model.recourse_references.map { |reference| reference.foreign_key.to_s }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
extend Bands
|
|
80
|
+
end
|
data/lib/recourse/columns.rb
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# Reopened for
|
|
2
|
-
#
|
|
1
|
+
# Reopened for which columns a screen uses at all, which the table, the show page and
|
|
2
|
+
# the form each ask before they ask what order to read them in.
|
|
3
3
|
module Recourse
|
|
4
4
|
# Columns the database writes itself out of the others, which a form never offers: a
|
|
5
5
|
# stored generated column takes no value, and Postgres refuses the one a form would send.
|
|
@@ -9,80 +9,30 @@ module Recourse
|
|
|
9
9
|
model.columns.select(&:virtual?).map(&:name)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# say nothing about the row itself, only how much hangs off it. So they close the
|
|
21
|
-
# row at the far edge rather than opening it beside the buttons they resemble.
|
|
22
|
-
#
|
|
23
|
-
# A flag follows what the row says rather than leading it. A yes or a no is narrow
|
|
24
|
-
# enough to have led on width alone, but it reads as a note *about* the row, and a
|
|
25
|
-
# reader scanning a table is looking for the name it belongs to first.
|
|
26
|
-
BANDS = %i[state reference scalar boolean long date timestamp counter].freeze
|
|
27
|
-
|
|
28
|
-
# Values that are paragraphs rather than words, under every name an adapter has for
|
|
29
|
-
# them: PostgreSQL reports `jsonb` where SQLite and MySQL report `json`.
|
|
30
|
-
LONG_KINDS = %i[text json jsonb].freeze
|
|
31
|
-
|
|
32
|
-
# And the ones that are a point in time, whichever part of one they keep.
|
|
33
|
-
DATE_KINDS = %i[date datetime time].freeze
|
|
34
|
-
|
|
35
|
-
# Whether a column holds a list of values rather than one. A PostgreSQL array
|
|
36
|
-
# reports a type wrapping a subtype, and so does a `serialize` of an Array; asked
|
|
37
|
-
# that way rather than by an adapter's own class, which only exists where that
|
|
38
|
-
# adapter is loaded. But wrapping a subtype is not enough on its own: an enum and a
|
|
39
|
-
# range always answered the same way, and since Rails 8.2 so does every decorator
|
|
40
|
-
# a column may wear — time zone conversion on a timestamp, `normalizes`, a lock. What
|
|
41
|
-
# sets a list apart is that its value is changed in place, which `Mutable` marks and
|
|
42
|
-
# none of those are.
|
|
43
|
-
def list_column?(model, column)
|
|
44
|
-
type = model.type_for_attribute column
|
|
45
|
-
|
|
46
|
-
type.respond_to?(:subtype) && type.is_a?(ActiveModel::Type::Helpers::Mutable)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# The columns of a model in the order a row reads them, given whichever of them the
|
|
50
|
-
# caller is drawing. Grouped rather than sorted: `group_by` keeps the order it was
|
|
51
|
-
# given inside each group, which is what leaves an order the schema already carries
|
|
52
|
-
# standing, and `sort_by` would not — Ruby's sort is not stable.
|
|
53
|
-
def ordered(model, names)
|
|
54
|
-
keys = reference_keys model
|
|
55
|
-
|
|
56
|
-
names.group_by { |name| BANDS.index band(model, name, keys) }.sort.flat_map(&:last)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
private
|
|
60
|
-
|
|
61
|
-
# Asked in the order that settles it. A counter is one whatever it is stored as; the
|
|
62
|
-
# column Rails keeps a subclass in says what kind of row this is, as an enum says
|
|
63
|
-
# what state it is in; and a key is an integer, so it has to be recognised as a key
|
|
64
|
-
# before its type is asked about at all.
|
|
65
|
-
def band(model, name, keys)
|
|
66
|
-
return :counter if Recourse.counters(model).key? name
|
|
67
|
-
return :state if name == model.inheritance_column || model.defined_enums.key?(name)
|
|
68
|
-
return :timestamp if TIMESTAMPS.include? name
|
|
69
|
-
return :reference if keys.include? name
|
|
70
|
-
|
|
71
|
-
band_of model.type_for_attribute(name).type
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def band_of(kind)
|
|
75
|
-
return :boolean if kind == :boolean
|
|
76
|
-
return :long if LONG_KINDS.include? kind
|
|
77
|
-
return :date if DATE_KINDS.include? kind
|
|
78
|
-
|
|
79
|
-
:scalar
|
|
80
|
-
end
|
|
12
|
+
# Columns a user may set: the form offers these, the show page reads these out, and
|
|
13
|
+
# `create` permits these. A counter cache is none of a user's business — Rails keeps
|
|
14
|
+
# it, so a form that offered one would let it be typed over, and neither is a column
|
|
15
|
+
# the database generates: it takes no value at all.
|
|
16
|
+
def self.editable_columns(model)
|
|
17
|
+
ordered model, model.column_names - ['id', *TIMESTAMPS] - counters(model).keys -
|
|
18
|
+
hidden_columns(model) - virtual_columns(model)
|
|
19
|
+
end
|
|
81
20
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
21
|
+
# Columns no screen shows: whatever the model asked to hide through `recourse_hidden`
|
|
22
|
+
# — one name or a list, taken either way — the column Rails reserves for single table
|
|
23
|
+
# inheritance, and the place a row holds where somebody positioned the table. A class
|
|
24
|
+
# name is machinery rather than something to read out, and a position is set by
|
|
25
|
+
# dragging the row rather than typed beside it.
|
|
26
|
+
def self.hidden_columns(model)
|
|
27
|
+
Array(model.recourse_hidden).map(&:to_s) +
|
|
28
|
+
[model.inheritance_column, *position_columns(model)]
|
|
85
29
|
end
|
|
86
30
|
|
|
87
|
-
|
|
31
|
+
# The names a column is validated under: its own, and — where it is a foreign key
|
|
32
|
+
# — the association's, since `belongs_to` validates the record it points at rather
|
|
33
|
+
# than the number pointing there. Two questions where a column is a key, one
|
|
34
|
+
# everywhere else.
|
|
35
|
+
def self.validated_names(column)
|
|
36
|
+
[column, column.delete_suffix('_id')].uniq
|
|
37
|
+
end
|
|
88
38
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
module Helpers
|
|
3
|
+
# The trail across the navbar: what each crumb reads, and which of them lead
|
|
4
|
+
# anywhere.
|
|
5
|
+
module Breadcrumbs
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# Trail to the current page as [resource, title, path] triples, opening with
|
|
9
|
+
# the parent a nested page sits under; a nil path is not a link.
|
|
10
|
+
def resource_breadcrumbs
|
|
11
|
+
crumbs = parent_breadcrumbs
|
|
12
|
+
leaf = breadcrumb_leaf
|
|
13
|
+
here = controller.controller_path
|
|
14
|
+
return crumbs << [here, breadcrumb_name, nil] unless leaf
|
|
15
|
+
|
|
16
|
+
crumbs << [here, breadcrumb_name, index_url] << [nil, leaf, nil]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# What the crumb naming this resource reads: its plural, or its singular where
|
|
20
|
+
# the routes drew one record rather than a list. Rails routes a singular resource
|
|
21
|
+
# to a plural controller, so the path says `properties` for the one property a
|
|
22
|
+
# location keeps -- and the crumb over it would read `HouseCanaries` for a page
|
|
23
|
+
# there is only ever one of. The same word the tab leading here took, from the
|
|
24
|
+
# same place, since the two stand for one page.
|
|
25
|
+
def breadcrumb_name
|
|
26
|
+
return resources_name unless idless_route? controller.controller_path, 'show'
|
|
27
|
+
|
|
28
|
+
Recourse.known_singular controller.controller_name
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Where this resource's index is, or nil where it has none: a singular resource
|
|
32
|
+
# is one record reached with no id, so there is no list of it to go back to and
|
|
33
|
+
# the crumb naming it is read out rather than linked.
|
|
34
|
+
def index_url
|
|
35
|
+
url_for action: :index if routed_action? 'index'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A crumb's words, which a phone drops where the icon stands for them: the row at
|
|
39
|
+
# the top has a search box and a button to fit beside the trail.
|
|
40
|
+
def crumb_label(resource, title)
|
|
41
|
+
icon = Recourse.known_icon resource
|
|
42
|
+
return title unless icon
|
|
43
|
+
|
|
44
|
+
word = tag.span title, class: 'recourse-crumb-word'
|
|
45
|
+
|
|
46
|
+
safe_join [tag.i(class: "bi bi-#{icon}"), word], ' '
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Only a page beneath the index names itself, and names what it is showing.
|
|
50
|
+
def breadcrumb_leaf
|
|
51
|
+
case controller.action_name
|
|
52
|
+
when 'new', 'create' then t 'recourse.new', model: resource_name
|
|
53
|
+
when 'show', 'edit', 'update' then resource_record_label
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -68,7 +68,7 @@ module Recourse
|
|
|
68
68
|
# `Created`, not `Created at`, since when it happened is what the column holds.
|
|
69
69
|
def dated_title(column)
|
|
70
70
|
title = resource_model.human_attribute_name column
|
|
71
|
-
dated = column.end_with?('_at', '_on') &&
|
|
71
|
+
dated = column.end_with?('_at', '_on') && Bands::DATE_KINDS.include?(attribute_type(column))
|
|
72
72
|
|
|
73
73
|
dated ? title.sub(/ (at|on)\z/, '') : title
|
|
74
74
|
end
|
|
@@ -74,6 +74,24 @@ module Recourse
|
|
|
74
74
|
|
|
75
75
|
safe_join [icon, tag.span(title, class: 'recourse-counter-word')]
|
|
76
76
|
end
|
|
77
|
+
|
|
78
|
+
# The counter a tab reads. A `has_many through:` keeps none of its own, and the
|
|
79
|
+
# join it goes through usually does: a market's ZIPs are reached through its
|
|
80
|
+
# territories, and `territories_count` is the number of them it holds. The figure
|
|
81
|
+
# is of the join rows, which is what the record actually keeps a count of.
|
|
82
|
+
def counter_column_of(model, association)
|
|
83
|
+
counter_of(model, association) || counter_of(model, join_of(model, association))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def counter_of(model, association)
|
|
87
|
+
Recourse.counters(model).find { |_, one| one == association }&.first if association
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def join_of(model, association)
|
|
91
|
+
name = association.options[:through]
|
|
92
|
+
|
|
93
|
+
model.reflect_on_association name if name
|
|
94
|
+
end
|
|
77
95
|
end
|
|
78
96
|
end
|
|
79
97
|
end
|
|
@@ -2,11 +2,6 @@ module Recourse
|
|
|
2
2
|
module Helpers
|
|
3
3
|
# How one attribute reads on a page that only reads it.
|
|
4
4
|
module Formats
|
|
5
|
-
# One absolute web address and nothing else: a value to follow, not to read.
|
|
6
|
-
# Anything around it — words, a second address — reads as text instead. What it
|
|
7
|
-
# captures is what a link says: the host, less any `www.`, and whatever follows.
|
|
8
|
-
WEB_URL = %r{\Ahttps?://(?:www\.)?([^/?#\s]+)(\S*)\z}
|
|
9
|
-
|
|
10
5
|
private
|
|
11
6
|
|
|
12
7
|
# What the record says for one column, formatted by what the column holds.
|
|
@@ -18,12 +13,13 @@ module Recourse
|
|
|
18
13
|
end
|
|
19
14
|
|
|
20
15
|
# One value, formatted by the kind its column holds — the one ladder a table
|
|
21
|
-
# cell and a show page's value both come down, so a boolean is the same
|
|
16
|
+
# cell and a show page's value both come down, so a boolean is the same word
|
|
22
17
|
# and an enum the same badge on either. A block, where the caller has one,
|
|
23
18
|
# marks the search terms inside whichever arm ends up as words.
|
|
24
19
|
def formatted_attribute(column, value, &)
|
|
25
20
|
kind = attribute_kind column
|
|
26
21
|
return listed Array(value) if kind == :list
|
|
22
|
+
return affirmed value if kind == :boolean
|
|
27
23
|
return formatted_number kind, column, value if numeric_kind? kind
|
|
28
24
|
|
|
29
25
|
formatted_text kind, value, &
|
|
@@ -51,6 +47,15 @@ module Recourse
|
|
|
51
47
|
end
|
|
52
48
|
end
|
|
53
49
|
|
|
50
|
+
# The word a reader answers with, which is the word the filter beside the column
|
|
51
|
+
# already offers. A column that was never answered holds neither, so it reads as
|
|
52
|
+
# the dash every other empty value does rather than as a no.
|
|
53
|
+
def affirmed(value)
|
|
54
|
+
return if value.nil?
|
|
55
|
+
|
|
56
|
+
t value ? 'recourse.affirmative' : 'recourse.negative'
|
|
57
|
+
end
|
|
58
|
+
|
|
54
59
|
# One whole web address is a value to follow rather than to read, and anything
|
|
55
60
|
# else is words — which the caller may have marking of its own for.
|
|
56
61
|
def linked_or_marked(value, &)
|
|
@@ -75,25 +80,6 @@ module Recourse
|
|
|
75
80
|
def phone_span(value) = value && tag.span(value, data: { controller: 'phone' })
|
|
76
81
|
|
|
77
82
|
def enum_badge(value) = tag.span(value, class: 'badge')
|
|
78
|
-
|
|
79
|
-
def web_url?(value) = value.is_a?(String) && value.match?(WEB_URL)
|
|
80
|
-
|
|
81
|
-
# The words lead where the value points, in this tab, the way any link does; the
|
|
82
|
-
# arrow after them — Bootstrap's icon link, stepping under the cursor — opens the
|
|
83
|
-
# same address in a new tab. Either reads as the host, and an ellipsis where the
|
|
84
|
-
# address goes further: a path is how a machine finds the page, and the href has it.
|
|
85
|
-
def url_link(value)
|
|
86
|
-
host, rest = value.match(WEB_URL).captures
|
|
87
|
-
said = rest.delete_suffix('/').empty? ? host : "#{host}/…"
|
|
88
|
-
|
|
89
|
-
safe_join [tag.a(said, href: value), new_tab_arrow(value)], ' '
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def new_tab_arrow(value)
|
|
93
|
-
tag.a icon_tag(:point_right), href: value, target: '_blank', rel: 'noopener',
|
|
94
|
-
class: 'icon-link icon-link-hover',
|
|
95
|
-
aria: { label: t('recourse.new_tab') }
|
|
96
|
-
end
|
|
97
83
|
end
|
|
98
84
|
end
|
|
99
85
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
module Helpers
|
|
3
|
+
# A value that is a web address rather than words: how one is recognised, and how
|
|
4
|
+
# it reads once it is.
|
|
5
|
+
module Links
|
|
6
|
+
# One absolute web address and nothing else: a value to follow, not to read.
|
|
7
|
+
# Anything around it — words, a second address — reads as text instead. What it
|
|
8
|
+
# captures is what a link says: the host, less any `www.`, and whatever follows.
|
|
9
|
+
WEB_URL = %r{\Ahttps?://(?:www\.)?([^/?#\s]+)(\S*)\z}
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def web_url?(value) = value.is_a?(String) && value.match?(WEB_URL)
|
|
14
|
+
|
|
15
|
+
# The words lead where the value points, in this tab, the way any link does; the
|
|
16
|
+
# arrow after them — Bootstrap's icon link, stepping under the cursor — opens the
|
|
17
|
+
# same address in a new tab. Either reads as the host, and an ellipsis where the
|
|
18
|
+
# address goes further: a path is how a machine finds the page, and the href has it.
|
|
19
|
+
def url_link(value)
|
|
20
|
+
host, rest = value.match(WEB_URL).captures
|
|
21
|
+
said = rest.delete_suffix('/').empty? ? host : "#{host}/…"
|
|
22
|
+
|
|
23
|
+
safe_join [tag.a(said, href: value), new_tab_arrow(value)], ' '
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def new_tab_arrow(value)
|
|
27
|
+
tag.a icon_tag(:point_right), href: value, target: '_blank', rel: 'noopener',
|
|
28
|
+
class: 'icon-link icon-link-hover',
|
|
29
|
+
aria: { label: t('recourse.new_tab') }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -1,39 +1,9 @@
|
|
|
1
1
|
module Recourse
|
|
2
2
|
module Helpers
|
|
3
|
-
#
|
|
3
|
+
# The links and forms the navbar draws, and where each of them goes.
|
|
4
4
|
module Navigation
|
|
5
5
|
private
|
|
6
6
|
|
|
7
|
-
# Trail to the current page as [resource, title, path] triples, opening with
|
|
8
|
-
# the parent a nested page sits under; a nil path is not a link.
|
|
9
|
-
def resource_breadcrumbs
|
|
10
|
-
crumbs = parent_breadcrumbs
|
|
11
|
-
leaf = breadcrumb_leaf
|
|
12
|
-
here = controller.controller_path
|
|
13
|
-
return crumbs << [here, breadcrumb_name, nil] unless leaf
|
|
14
|
-
|
|
15
|
-
crumbs << [here, breadcrumb_name, index_url] << [nil, leaf, nil]
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# What the crumb naming this resource reads: its plural, or its singular where
|
|
19
|
-
# the routes drew one record rather than a list. Rails routes a singular resource
|
|
20
|
-
# to a plural controller, so the path says `properties` for the one property a
|
|
21
|
-
# location keeps -- and the crumb over it would read `HouseCanaries` for a page
|
|
22
|
-
# there is only ever one of. The same word the tab leading here took, from the
|
|
23
|
-
# same place, since the two stand for one page.
|
|
24
|
-
def breadcrumb_name
|
|
25
|
-
return resources_name unless idless_route? controller.controller_path, 'show'
|
|
26
|
-
|
|
27
|
-
Recourse.known_singular controller.controller_name
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Where this resource's index is, or nil where it has none: a singular resource
|
|
31
|
-
# is one record reached with no id, so there is no list of it to go back to and
|
|
32
|
-
# the crumb naming it is read out rather than linked.
|
|
33
|
-
def index_url
|
|
34
|
-
url_for action: :index if routed_action? 'index'
|
|
35
|
-
end
|
|
36
|
-
|
|
37
7
|
# A link out of a table. Every cell is inside the results frame, and the page a
|
|
38
8
|
# cell links to has no frame of that name, so Turbo would replace the table with
|
|
39
9
|
# `Content missing` rather than leaving the page. `_top` is what leaves it.
|
|
@@ -77,25 +47,6 @@ module Recourse
|
|
|
77
47
|
|
|
78
48
|
safe_join [tag.i(class: "bi bi-#{icon}"), tag.span(name, class: 'recourse-nav-word')], ' '
|
|
79
49
|
end
|
|
80
|
-
|
|
81
|
-
# The same for a crumb, whose words a phone drops where the icon stands for them:
|
|
82
|
-
# the row at the top has a search box and a button to fit beside the trail.
|
|
83
|
-
def crumb_label(resource, title)
|
|
84
|
-
icon = Recourse.known_icon resource
|
|
85
|
-
return title unless icon
|
|
86
|
-
|
|
87
|
-
word = tag.span title, class: 'recourse-crumb-word'
|
|
88
|
-
|
|
89
|
-
safe_join [tag.i(class: "bi bi-#{icon}"), word], ' '
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Only a page beneath the index names itself, and names what it is showing.
|
|
93
|
-
def breadcrumb_leaf
|
|
94
|
-
case controller.action_name
|
|
95
|
-
when 'new', 'create' then t 'recourse.new', model: resource_name
|
|
96
|
-
when 'show', 'edit', 'update' then resource_record_label
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
50
|
end
|
|
100
51
|
end
|
|
101
52
|
end
|
|
@@ -90,24 +90,6 @@ module Recourse
|
|
|
90
90
|
def namespace_words(namespace)
|
|
91
91
|
namespace.map { |segment| Recourse.downcase segment.humanize }.join ' '
|
|
92
92
|
end
|
|
93
|
-
|
|
94
|
-
# The counter a tab reads. A `has_many through:` keeps none of its own, and the
|
|
95
|
-
# join it goes through usually does: a market's ZIPs are reached through its
|
|
96
|
-
# territories, and `territories_count` is the number of them it holds. The figure
|
|
97
|
-
# is of the join rows, which is what the record actually keeps a count of.
|
|
98
|
-
def counter_column_of(model, association)
|
|
99
|
-
counter_of(model, association) || counter_of(model, join_of(model, association))
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def counter_of(model, association)
|
|
103
|
-
Recourse.counters(model).find { |_, one| one == association }&.first if association
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def join_of(model, association)
|
|
107
|
-
name = association.options[:through]
|
|
108
|
-
|
|
109
|
-
model.reflect_on_association name if name
|
|
110
|
-
end
|
|
111
93
|
end
|
|
112
94
|
end
|
|
113
95
|
end
|
|
@@ -18,7 +18,7 @@ module Recourse
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
# What the record says for one column, or a dash where it says nothing. A
|
|
21
|
-
# boolean says something either way, and
|
|
21
|
+
# boolean says something either way, and a word says it, so only a value that
|
|
22
22
|
# formats to nothing at all reads as nothing.
|
|
23
23
|
def resource_value(column)
|
|
24
24
|
value = formatted_value column
|
data/lib/recourse/helpers.rb
CHANGED
|
@@ -3,6 +3,7 @@ require_relative 'helpers/positions'
|
|
|
3
3
|
require_relative 'helpers/attachments'
|
|
4
4
|
require_relative 'helpers/blobs'
|
|
5
5
|
require_relative 'helpers/bookmarks'
|
|
6
|
+
require_relative 'helpers/breadcrumbs'
|
|
6
7
|
require_relative 'helpers/buttons'
|
|
7
8
|
require_relative 'helpers/calendars'
|
|
8
9
|
require_relative 'helpers/cards'
|
|
@@ -24,6 +25,7 @@ require_relative 'helpers/formats'
|
|
|
24
25
|
require_relative 'helpers/inputs'
|
|
25
26
|
require_relative 'helpers/kinds'
|
|
26
27
|
require_relative 'helpers/limits'
|
|
28
|
+
require_relative 'helpers/links'
|
|
27
29
|
require_relative 'helpers/maps'
|
|
28
30
|
require_relative 'helpers/names'
|
|
29
31
|
require_relative 'helpers/navigation'
|
|
@@ -51,14 +53,13 @@ require_relative 'helpers/zones'
|
|
|
51
53
|
module Recourse
|
|
52
54
|
# View helpers for the pages the gem renders, and what the parts share.
|
|
53
55
|
module Helpers
|
|
54
|
-
include Actions, Positions, Attachments, Blobs, Bookmarks,
|
|
55
|
-
Cells, Choices, Colors, Comboboxes, Constraints, Counters,
|
|
56
|
-
Details,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Themes, Times, Values, Weeks, Zones
|
|
56
|
+
include Actions, Positions, Attachments, Blobs, Bookmarks, Breadcrumbs, Buttons,
|
|
57
|
+
Calendars, Cards, Cells, Choices, Colors, Comboboxes, Constraints, Counters,
|
|
58
|
+
Deletions, Densities, Details, Events, Examples, Fields, Filters, Flashes,
|
|
59
|
+
Formats, Inputs, Kinds, Limits, Links, Maps, Names, Navigation, Parents,
|
|
60
|
+
Pictures, Previews, References, Refreshes, Routing, Resources, Rows, Searches,
|
|
61
|
+
Shapes, Shortcuts, Sidebars, Sorts, Spans, Tabs, Themes, Times, Values, Weeks,
|
|
62
|
+
Zones
|
|
62
63
|
|
|
63
64
|
# The grid a record's own two pages lay an attribute out in: two columns on a large
|
|
64
65
|
# viewport, and the same padding on both, so a value and the field that edits it sit
|
data/lib/recourse/version.rb
CHANGED
data/lib/recourse.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative 'recourse/blobs'
|
|
|
11
11
|
require_relative 'recourse/bookmarks'
|
|
12
12
|
require_relative 'recourse/calendars'
|
|
13
13
|
require_relative 'recourse/colors'
|
|
14
|
+
require_relative 'recourse/bands'
|
|
14
15
|
require_relative 'recourse/columns'
|
|
15
16
|
require_relative 'recourse/counters'
|
|
16
17
|
require_relative 'recourse/searches'
|
|
@@ -57,33 +58,6 @@ module Recourse
|
|
|
57
58
|
@parents = {}
|
|
58
59
|
@declared_bookmarks = nil
|
|
59
60
|
|
|
60
|
-
# Columns a user may set: the form offers these, the show page reads these out, and
|
|
61
|
-
# `create` permits these. A counter cache is none of a user's business — Rails keeps
|
|
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.
|
|
64
|
-
def self.editable_columns(model)
|
|
65
|
-
ordered model, model.column_names - ['id', *TIMESTAMPS] - counters(model).keys -
|
|
66
|
-
hidden_columns(model) - virtual_columns(model)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Columns no screen shows: whatever the model asked to hide through `recourse_hidden`
|
|
70
|
-
# — one name or a list, taken either way — the column Rails reserves for single table
|
|
71
|
-
# inheritance, and the place a row holds where somebody positioned the table. A class
|
|
72
|
-
# name is machinery rather than something to read out, and a position is set by
|
|
73
|
-
# dragging the row rather than typed beside it.
|
|
74
|
-
def self.hidden_columns(model)
|
|
75
|
-
Array(model.recourse_hidden).map(&:to_s) +
|
|
76
|
-
[model.inheritance_column, *position_columns(model)]
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# The names a column is validated under: its own, and — where it is a foreign key
|
|
80
|
-
# — the association's, since `belongs_to` validates the record it points at rather
|
|
81
|
-
# than the number pointing there. Two questions where a column is a key, one
|
|
82
|
-
# everywhere else.
|
|
83
|
-
def self.validated_names(column)
|
|
84
|
-
[column, column.delete_suffix('_id')].uniq
|
|
85
|
-
end
|
|
86
|
-
|
|
87
61
|
# The model a resource is named after. A controller the gem defined has nothing else
|
|
88
62
|
# to go on, so a name resolving to no model is a routes file to fix rather than a
|
|
89
63
|
# `NameError` from somewhere inside a view.
|
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.
|
|
4
|
+
version: 7.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- claudiob
|
|
@@ -140,6 +140,7 @@ files:
|
|
|
140
140
|
- app/controllers/concerns/recourse/landing.rb
|
|
141
141
|
- app/controllers/concerns/recourse/list_resolution.rb
|
|
142
142
|
- app/controllers/concerns/recourse/paging.rb
|
|
143
|
+
- app/controllers/concerns/recourse/parameter_resolution.rb
|
|
143
144
|
- app/controllers/concerns/recourse/parent_naming.rb
|
|
144
145
|
- app/controllers/concerns/recourse/parent_resolution.rb
|
|
145
146
|
- app/controllers/concerns/recourse/positioned.rb
|
|
@@ -178,6 +179,7 @@ files:
|
|
|
178
179
|
- lib/recourse.rb
|
|
179
180
|
- lib/recourse/assets.rb
|
|
180
181
|
- lib/recourse/attachments.rb
|
|
182
|
+
- lib/recourse/bands.rb
|
|
181
183
|
- lib/recourse/blobs.rb
|
|
182
184
|
- lib/recourse/bookmarks.rb
|
|
183
185
|
- lib/recourse/broadcasting.rb
|
|
@@ -193,6 +195,7 @@ files:
|
|
|
193
195
|
- lib/recourse/helpers/attachments.rb
|
|
194
196
|
- lib/recourse/helpers/blobs.rb
|
|
195
197
|
- lib/recourse/helpers/bookmarks.rb
|
|
198
|
+
- lib/recourse/helpers/breadcrumbs.rb
|
|
196
199
|
- lib/recourse/helpers/buttons.rb
|
|
197
200
|
- lib/recourse/helpers/calendars.rb
|
|
198
201
|
- lib/recourse/helpers/cards.rb
|
|
@@ -214,6 +217,7 @@ files:
|
|
|
214
217
|
- lib/recourse/helpers/inputs.rb
|
|
215
218
|
- lib/recourse/helpers/kinds.rb
|
|
216
219
|
- lib/recourse/helpers/limits.rb
|
|
220
|
+
- lib/recourse/helpers/links.rb
|
|
217
221
|
- lib/recourse/helpers/maps.rb
|
|
218
222
|
- lib/recourse/helpers/names.rb
|
|
219
223
|
- lib/recourse/helpers/navigation.rb
|