recourse 5.2.0 → 5.3.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 +36 -0
- data/README.md +64 -19
- data/app/controllers/concerns/recourse/attachment_resolution.rb +44 -0
- data/app/controllers/concerns/recourse/attachment_writing.rb +70 -0
- data/app/controllers/concerns/recourse/parent_resolution.rb +2 -2
- data/app/controllers/concerns/recourse/reference_resolution.rb +4 -0
- data/app/controllers/concerns/recourse/resource_resolution.rb +16 -5
- data/app/controllers/recourse/base_controller.rb +4 -2
- data/app/views/recourses/_fields.html.erb +3 -0
- data/app/views/recourses/_row.html.erb +4 -0
- data/app/views/recourses/_values.html.erb +3 -0
- data/config/locales/recourse.en.yml +9 -0
- data/lib/recourse/attachments.rb +28 -0
- data/lib/recourse/blobs.rb +22 -0
- data/lib/recourse/helpers/actions.rb +42 -10
- data/lib/recourse/helpers/attachments.rb +66 -0
- data/lib/recourse/helpers/blobs.rb +80 -0
- data/lib/recourse/helpers/cells.rb +2 -0
- data/lib/recourse/helpers/previews.rb +40 -0
- data/lib/recourse/helpers/resources.rb +16 -6
- data/lib/recourse/helpers.rb +5 -2
- data/lib/recourse/search.rb +11 -3
- data/lib/recourse/version.rb +1 -1
- data/lib/recourse.rb +2 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c224b5ff48a88afa37e70da5d2157d9b5229eccdc02d7d9f8ce1b85e4df31d8
|
|
4
|
+
data.tar.gz: 50da170bddd967baba9c46c276e61f750694465a2c2e11c5fe09c5e59cc84eff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91bc98cadd2186f29b342da8804c0926f523656aec2149e8025393c4c3ba3ec034e1ae359732cb81368192ac00b70f484731640227692c18b4c5d1ed09adac1f
|
|
7
|
+
data.tar.gz: a0565fb334f552e1a403af4db94f8d878f25ae5473e6af167304785fc017d42e392e9276ece5b2fcd4475fe48511cac8216a670adc23f8269f9ee8b931573c4f
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,42 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## 5.3.0 - 2026-09-16
|
|
11
|
+
|
|
12
|
+
* [Feature] A record's files are a field, a value and a page
|
|
13
|
+
|
|
14
|
+
Back from the 4.0.0 development tree: a `has_one_attached` is a file field on the form
|
|
15
|
+
and a value on the record's page, a `has_many_attached :photos` beside
|
|
16
|
+
`recourses :photos, only: %i[index destroy]` under the record is a table of Active
|
|
17
|
+
Storage's blobs — searched, sorted and paged like any other — and a submitted file is
|
|
18
|
+
attached after the save rather than assigned, so an edit that touched only a name never
|
|
19
|
+
purges what the record had. New this time: a file a browser can be shown is a picture
|
|
20
|
+
first, a `<picture>` 100 pixels tall of a representation made 200 tall — WebP at quality
|
|
21
|
+
80 with its metadata stripped where the browser takes it, the file's own kind where not
|
|
22
|
+
— in the link that opens the whole file inline — the image scaled, or the frame a
|
|
23
|
+
previewer takes of a video or a PDF where the host has `ffmpeg` or `poppler` — on the
|
|
24
|
+
table and on the record's page alike. And a Delete on each row of the table, which
|
|
25
|
+
takes the file off the record and leaves the blob to Active Storage. Files are still
|
|
26
|
+
added on the record's form, and a shelf is still not counted on its tab: there is no
|
|
27
|
+
`belongs_to` to hang a counter cache on.
|
|
28
|
+
|
|
29
|
+
* [Feature] A table routed `destroy` without `edit` offers Delete on each row
|
|
30
|
+
|
|
31
|
+
The Delete button stood on the edit page and nowhere else, so a resource with a
|
|
32
|
+
`destroy` and no `edit` — a file attached to a record, a row joining two — had no way to
|
|
33
|
+
be deleted from the gem's own screens. Its table now draws a third action column, the
|
|
34
|
+
trash icon in `fg-danger`, a `button_to` carrying the same confirmation the edit page's
|
|
35
|
+
button does. A table whose rows have an edit page is unchanged.
|
|
36
|
+
|
|
37
|
+
* [Fix] A filter narrows a table again
|
|
38
|
+
|
|
39
|
+
A combobox became a `<select>` in 5.0.0 and its picks are submitted one value each, but the
|
|
40
|
+
search read them the way it read the comma-joined string before it — so `q[team_id_in][]=1`
|
|
41
|
+
arrived as the array `["1"]`, was written out as the one value `["1"]`, and was cast to the
|
|
42
|
+
id 0 no row holds. Every filter menu in the gem came to an empty table, whatever was ticked.
|
|
43
|
+
The picks are now taken as the values they arrive as, and `All …`, which submits one empty
|
|
44
|
+
value, is no filter rather than a filter nothing answers.
|
|
45
|
+
|
|
10
46
|
## 5.2.0 - 2026-09-14
|
|
11
47
|
|
|
12
48
|
* [Feature] A table of places can be read as a map
|
data/README.md
CHANGED
|
@@ -47,7 +47,8 @@ Every recourse with `new` or `edit` gets a form with appropriate browser formatt
|
|
|
47
47
|
|
|
48
48
|
<img width="3824" height="1220" alt="Image" src="https://github.com/user-attachments/assets/ebe5108b-bbd1-4cd1-a635-89eef0e6fc50" />
|
|
49
49
|
|
|
50
|
-
Every recourse with `destroy` gets a button with a detailed confirmation message
|
|
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:
|
|
51
52
|
|
|
52
53
|
[image]
|
|
53
54
|
|
|
@@ -123,37 +124,76 @@ A controller the app already defines is left alone.
|
|
|
123
124
|
|
|
124
125
|
## Step 5. Enjoy the extras
|
|
125
126
|
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
The rest of what the gem offers, in four parts. The first two are a line each in
|
|
128
|
+
`config/initializers/recourse.rb`; the last two need nothing beyond what the model
|
|
129
|
+
already declares.
|
|
128
130
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
a Proc answering the viewer's bookmark rows. Every table of a model with a `has_many`
|
|
137
|
-
at that class then opens with a square to keep a row by, kept rows first.
|
|
131
|
+
### Color and theme
|
|
132
|
+
|
|
133
|
+
[`Recourse.color=`](https://rubydoc.info/gems/recourse/Recourse#color%3D-class_method)
|
|
134
|
+
says which Bootstrap family is primary on every page, and
|
|
135
|
+
[`Recourse.theme=`](https://rubydoc.info/gems/recourse/Recourse#theme%3D-class_method)
|
|
136
|
+
says which color scheme the pages are drawn in — one of `Recourse::THEMES.keys`: eight
|
|
137
|
+
palettes taken from code editors, plus Bootstrap's own.
|
|
138
138
|
|
|
139
139
|
```ruby
|
|
140
140
|
Recourse.color = :orange
|
|
141
141
|
Recourse.theme = :nord
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The theme is where a reader starts, not where they stay. The moon at the foot of the
|
|
145
|
+
sidebar rotates through every palette, light and dark, and the one they pick stays in
|
|
146
|
+
their browser.
|
|
147
|
+
|
|
148
|
+
### Bookmarks
|
|
149
|
+
|
|
150
|
+
[`Recourse.bookmarks=`](https://rubydoc.info/gems/recourse/Recourse#bookmarks%3D-class_method)
|
|
151
|
+
takes a Proc answering the rows the viewer has kept:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
142
154
|
Recourse.bookmarks = -> { Keepsake.where agent: Current.agent }
|
|
143
155
|
```
|
|
144
156
|
|
|
157
|
+
A Proc rather than a relation, because at boot the viewer is nobody. Every model with a
|
|
158
|
+
`has_many` at that class then opens its table with a square to keep a row by, and kept
|
|
159
|
+
rows come first. A model that cannot hold a bookmark gets no column.
|
|
160
|
+
|
|
145
161
|
<img width="3824" height="1220" alt="Image" src="https://github.com/user-attachments/assets/d1db4adb-0b5c-4e41-8bb6-cf72a35288f0" />
|
|
146
162
|
|
|
163
|
+
### Attachments
|
|
164
|
+
|
|
165
|
+
What a model keeps as files needs nothing declared either. A `has_one_attached :logo` is
|
|
166
|
+
a file field on the form and, on the record's page, the picture Active Storage makes of
|
|
167
|
+
the file — 100 pixels tall, WebP where the browser takes it, opening the whole file in a
|
|
168
|
+
new tab — or the file's name where nothing can be drawn of it.
|
|
169
|
+
|
|
170
|
+
A `has_many_attached :photos` earns a page of its own, nested under the record:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
recourses :photos, only: %i[index destroy]
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
That page is a table of the files with their pictures, and a Delete on each row that
|
|
177
|
+
takes the file off the record. Files are added on the record's own form, where a chosen
|
|
178
|
+
file joins a shelf and replaces a single one, and a field nobody touched leaves
|
|
179
|
+
everything as it was. `recourse_hidden :photos` keeps a file off every screen the way it
|
|
180
|
+
keeps a column off.
|
|
181
|
+
|
|
182
|
+
Drawing a picture of an image needs `image_processing`; video and PDF also need the
|
|
183
|
+
host's `ffmpeg` and `poppler`.
|
|
184
|
+
|
|
185
|
+
### Maps
|
|
186
|
+
|
|
147
187
|
A table whose model keeps a `google_place_id`, or a `latitude` and a `longitude`, can be
|
|
148
188
|
read as a Google map of the same page: the footer under it offers `Display as map`, and
|
|
149
189
|
`/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.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
190
|
+
search, sort and pages work the same on either shape.
|
|
191
|
+
|
|
192
|
+
A point is a pin. A place ID is filled in as an area where the model is a geography
|
|
193
|
+
Google draws boundaries for — a `State`, a `County`, a `City` or a `ZIP`, by name — and
|
|
194
|
+
pinned at the place for any other model.
|
|
195
|
+
|
|
196
|
+
The key and the map ID are the host's own credentials:
|
|
157
197
|
|
|
158
198
|
```yaml
|
|
159
199
|
# config/credentials.yml.enc
|
|
@@ -162,6 +202,11 @@ google_maps:
|
|
|
162
202
|
map_id: 4f2a…
|
|
163
203
|
```
|
|
164
204
|
|
|
205
|
+
The map's style has the matching **Feature layers** turned on in the Cloud console —
|
|
206
|
+
Postal code for ZIPs, Administrative area level 2 for counties. A host with a Content
|
|
207
|
+
Security Policy allows `maps.googleapis.com` for scripts and connections, and Google's
|
|
208
|
+
tile hosts for images.
|
|
209
|
+
|
|
165
210
|
|
|
166
211
|
## Development
|
|
167
212
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
# An index over what a record has attached rather than over a model of its own.
|
|
3
|
+
# `has_many_attached :photos` and `recourses :photos, only: :index` under the same
|
|
4
|
+
# record is the whole declaration: Rails already generates the association this
|
|
5
|
+
# reads, and Active Storage already has the model it lists.
|
|
6
|
+
module AttachmentResolution
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# The attachment this page lists, or nil where the name is a model like any
|
|
12
|
+
# other. `has_one_attached` is not one: a single file is a value on the record's
|
|
13
|
+
# own page, and a table of one row says less than the field it replaced.
|
|
14
|
+
def attachment_reflection
|
|
15
|
+
return unless defined?(ActiveStorage::Reflection) && attachment_parent
|
|
16
|
+
|
|
17
|
+
reflection = attachment_parent.class.attachment_reflections[controller_name]
|
|
18
|
+
|
|
19
|
+
reflection if reflection.is_a? ActiveStorage::Reflection::HasManyAttachedReflection
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The record the path names, asked before the model is settled: a blob holds no key
|
|
23
|
+
# pointing back at what it hangs off, and what this page is about turns on which
|
|
24
|
+
# record that is. The same lookup `find_parent` falls back on, kept for the request.
|
|
25
|
+
def attachment_parent
|
|
26
|
+
return @attachment_parent if defined? @attachment_parent
|
|
27
|
+
|
|
28
|
+
@attachment_parent = path_parent
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The blobs themselves, through the association `has_many_attached` generated —
|
|
32
|
+
# a real relation, so the search, the sort and the page all still apply.
|
|
33
|
+
def attachment_relation
|
|
34
|
+
attachment_parent.association(:"#{controller_name}_blobs").reader
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The row joining the record to one of its files, which is what a delete on this
|
|
38
|
+
# page removes: the file stays as long as anything else still points at it, and
|
|
39
|
+
# Active Storage purges it once nothing does.
|
|
40
|
+
def attachment_of(blob)
|
|
41
|
+
attachment_parent.association(:"#{controller_name}_attachments").reader.find_by! blob: blob
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
# The files a form submits, which are written after the record rather than with it.
|
|
3
|
+
# An attachment row points at an id, and a record being created has none until it is
|
|
4
|
+
# saved — so a write here is always two steps where a column's is one.
|
|
5
|
+
module AttachmentWriting
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# A new record, then the files that came with it, in one transaction: a record
|
|
11
|
+
# half-saved is worse than one not saved.
|
|
12
|
+
def create_resource(record)
|
|
13
|
+
record.transaction { super && attach_submitted_files(record) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The same for one that already exists.
|
|
17
|
+
def update_resource(record)
|
|
18
|
+
super && attach_submitted_files(record)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# On a page of what a record has attached, the row between the two goes and the
|
|
22
|
+
# file stays for whatever else points at it; everywhere else the record itself goes.
|
|
23
|
+
def destroy_resource(record)
|
|
24
|
+
return super unless attachment_reflection
|
|
25
|
+
|
|
26
|
+
attachment_of(record).destroy!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Attached rather than assigned, which is the whole reason this exists. Rails'
|
|
30
|
+
# generated writer replaces every attachment rather than adding to them, and reads
|
|
31
|
+
# a blank field as an instruction to delete the lot — so an edit that touched only
|
|
32
|
+
# a name would purge what the record had. `Attached::Many#attach` appends and
|
|
33
|
+
# `Attached::One#attach` replaces the one, which is what each means on a form.
|
|
34
|
+
#
|
|
35
|
+
# `each` answers the list it was given, which is truthy even when it is empty — so
|
|
36
|
+
# a model with nothing attached never turns a save that worked into a failure.
|
|
37
|
+
def attach_submitted_files(record)
|
|
38
|
+
Recourse.attachment_names(resource_class).each do |name|
|
|
39
|
+
files = submitted_files name
|
|
40
|
+
attached(record, name).attach(*files) if files.present?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Built rather than read off the record: the reader Active Storage generates is
|
|
45
|
+
# reachable only by its own name, and this is the object that reader returns.
|
|
46
|
+
def attached(record, name)
|
|
47
|
+
return ActiveStorage::Attached::Many.new name, record if
|
|
48
|
+
Recourse.attachment_many? resource_class, name
|
|
49
|
+
|
|
50
|
+
ActiveStorage::Attached::One.new name, record
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# What the form sent for one attachment, as a list either way. Blanks are dropped
|
|
54
|
+
# rather than trusted: the browser sends an empty file input as an empty string,
|
|
55
|
+
# and Active Storage would read a list of those as `delete everything`.
|
|
56
|
+
def submitted_files(name)
|
|
57
|
+
Array(params.dig(controller_name.singularize.to_sym, name)).compact_blank
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# What a form may submit for one: a list of files where the model keeps several,
|
|
61
|
+
# and one file where it keeps one. Permitted alongside the columns, so a host
|
|
62
|
+
# running `action_on_unpermitted_parameters` at `:raise` is not tripped by a field
|
|
63
|
+
# the gem itself drew — and dropped again before anything reaches the record.
|
|
64
|
+
def attachment_filters
|
|
65
|
+
Recourse.attachment_names(resource_class).map do |name|
|
|
66
|
+
Recourse.attachment_many?(resource_class, name) ? { name => [] } : name
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -12,7 +12,7 @@ module Recourse
|
|
|
12
12
|
|
|
13
13
|
# The record a key points at where the resource has one, and otherwise the record
|
|
14
14
|
# the path names: a page nested by the path alone — the memos of a team no memo
|
|
15
|
-
# belongs to — still sits under something.
|
|
15
|
+
# belongs to, the blobs a record has attached — still sits under something.
|
|
16
16
|
def find_parent
|
|
17
17
|
@recourse_parent_association = parent_association
|
|
18
18
|
@recourse_parent = @recourse_parent_association ? parent_model.find(parent_id) : path_parent
|
|
@@ -34,7 +34,7 @@ module Recourse
|
|
|
34
34
|
# What the route settled and every action honours: the index lists rows carrying
|
|
35
35
|
# these columns, and `new` and `create` build records that do.
|
|
36
36
|
def parent_columns
|
|
37
|
-
return {} if @recourse_parent_association.nil?
|
|
37
|
+
return {} if attachment_reflection || @recourse_parent_association.nil?
|
|
38
38
|
|
|
39
39
|
{ @recourse_parent_association.foreign_key => @recourse_parent.id }
|
|
40
40
|
end
|
|
@@ -22,6 +22,10 @@ module Recourse
|
|
|
22
22
|
record.update attributes
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# And the record gone. `destroy!`, so a callback that stops one says so rather than
|
|
26
|
+
# leaving the page claiming it worked.
|
|
27
|
+
def destroy_resource(record) = record.destroy!
|
|
28
|
+
|
|
25
29
|
# A foreign key whose label is typed arrives as that label, so it is looked up
|
|
26
30
|
# here. Nothing found leaves the key nil, and `belongs_to` reports it missing.
|
|
27
31
|
def resolve_references(attributes)
|
|
@@ -25,8 +25,13 @@ module Recourse
|
|
|
25
25
|
false
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# The model the route is named after
|
|
29
|
-
|
|
28
|
+
# The model the route is named after — or Active Storage's, where the name is
|
|
29
|
+
# something the parent has attached rather than a model of this app's own.
|
|
30
|
+
def resource_class
|
|
31
|
+
return ActiveStorage::Blob if attachment_reflection
|
|
32
|
+
|
|
33
|
+
recourse_model
|
|
34
|
+
end
|
|
30
35
|
|
|
31
36
|
# The model this screen is about, and the second thing a host overrides to put a
|
|
32
37
|
# page of its own behind a screen the gem otherwise draws whole:
|
|
@@ -39,7 +44,11 @@ module Recourse
|
|
|
39
44
|
Recourse.model controller_name
|
|
40
45
|
end
|
|
41
46
|
|
|
47
|
+
# What the page calls one of its rows: the model's own word, or the route's for a
|
|
48
|
+
# page of files — `Photo was deleted.`, never `Blob`.
|
|
42
49
|
def human_name
|
|
50
|
+
return controller_name.singularize.humanize if attachment_reflection
|
|
51
|
+
|
|
43
52
|
resource_class.model_name.human
|
|
44
53
|
end
|
|
45
54
|
|
|
@@ -50,15 +59,17 @@ module Recourse
|
|
|
50
59
|
# `def resource_params = params.expect(provider: %i[name cid])`.
|
|
51
60
|
def resource_params
|
|
52
61
|
# The parent is merged after resolving, so the one the route names is never
|
|
53
|
-
# mistaken for a label
|
|
54
|
-
|
|
62
|
+
# mistaken for a label; the files go no further than the permit that let them
|
|
63
|
+
# through, being attached rather than assigned.
|
|
64
|
+
submitted_attributes.except(*Recourse.attachment_names(resource_class))
|
|
65
|
+
.merge parent_columns
|
|
55
66
|
end
|
|
56
67
|
|
|
57
68
|
# What the form sent, with a typed reference read back as the id it names. A bare
|
|
58
69
|
# `Create` submits no attributes at all, so the key may be absent: the parent a
|
|
59
70
|
# nested route names is everything such a record starts from.
|
|
60
71
|
def submitted_attributes
|
|
61
|
-
permitted = Recourse.editable_columns
|
|
72
|
+
permitted = Recourse.editable_columns(resource_class) + attachment_filters
|
|
62
73
|
key = controller_name.singularize.to_sym
|
|
63
74
|
return {} unless params.key? key
|
|
64
75
|
|
|
@@ -3,7 +3,7 @@ module Recourse
|
|
|
3
3
|
# its own behavior above it — `class RecoursesController < Recourse::BaseController`
|
|
4
4
|
# with a `before_action :authenticate!` guards every screen the gem serves.
|
|
5
5
|
class BaseController < ApplicationController
|
|
6
|
-
include Pagy::Method,
|
|
6
|
+
include Pagy::Method, AttachmentResolution, AttachmentWriting,
|
|
7
7
|
Landing, Paging, ListResolution, ParentNaming,
|
|
8
8
|
ParentResolution, ReferenceResolution, ResourceResolution,
|
|
9
9
|
Zoning
|
|
@@ -64,7 +64,7 @@ module Recourse
|
|
|
64
64
|
# Deletes the record and shows the index without it. `destroy!`, so a callback that
|
|
65
65
|
# stops one says so rather than leaving the page claiming it worked.
|
|
66
66
|
def destroy
|
|
67
|
-
@recourse
|
|
67
|
+
destroy_resource @recourse
|
|
68
68
|
wrote t('recourse.deleted', model: human_name)
|
|
69
69
|
end
|
|
70
70
|
|
|
@@ -80,6 +80,8 @@ module Recourse
|
|
|
80
80
|
# `def recourse_relation = County.with_boosts_for(@recourse_parent)`. Private, so
|
|
81
81
|
# overriding it adds a query and never an action.
|
|
82
82
|
def recourse_relation
|
|
83
|
+
return attachment_relation if attachment_reflection
|
|
84
|
+
|
|
83
85
|
resource_class.where parent_columns
|
|
84
86
|
end
|
|
85
87
|
end
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
<% record = local_assigns[resource_key] -%>
|
|
2
|
+
<%# A file is a picture before it is a row, where there is a picture to draw. -%>
|
|
3
|
+
<% if blob_resource? -%>
|
|
4
|
+
<%= column header: t('recourse.preview') do %><%= blob_preview record %><% end %>
|
|
5
|
+
<% end -%>
|
|
2
6
|
<% resource_columns.each do |name| -%>
|
|
3
7
|
<%= column header: sort_header(name), class: counter_class(name) do %><%= resource_cell record, name %><% end %>
|
|
4
8
|
<% end -%>
|
|
@@ -13,6 +13,13 @@ en:
|
|
|
13
13
|
all_values: All
|
|
14
14
|
as_map: Display as map
|
|
15
15
|
as_table: Display as table
|
|
16
|
+
# One message in four spellings: what a record is holding, under the field that
|
|
17
|
+
# adds to it.
|
|
18
|
+
attached:
|
|
19
|
+
many: "%{count} files attached (%{files})"
|
|
20
|
+
none: No file attached
|
|
21
|
+
none_many: No files attached
|
|
22
|
+
one: 1 file attached (%{files})
|
|
16
23
|
blank: "—"
|
|
17
24
|
bookmark: Bookmark
|
|
18
25
|
bookmark_added: Bookmark added
|
|
@@ -27,6 +34,7 @@ en:
|
|
|
27
34
|
darken: Dark
|
|
28
35
|
delete: Delete %{model}
|
|
29
36
|
deleted: "%{model} was deleted."
|
|
37
|
+
destroy: Delete
|
|
30
38
|
exit: Exit
|
|
31
39
|
displaying:
|
|
32
40
|
one: Displaying 1 item
|
|
@@ -54,6 +62,7 @@ en:
|
|
|
54
62
|
none: "No %{models}."
|
|
55
63
|
optional: Optional
|
|
56
64
|
per_page: "%{limit} per page"
|
|
65
|
+
preview: Preview
|
|
57
66
|
ambiguous: "matches more than one record, so it does not say which: %{label}"
|
|
58
67
|
reference: "%{model} %{attribute}"
|
|
59
68
|
reveal: Show
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Reopened for what a record keeps beside its columns, which a form, a show page and
|
|
2
|
+
# the params a write permits each have to ask a model about.
|
|
3
|
+
module Recourse
|
|
4
|
+
# What Active Storage was told a model has attached. Extended onto `Recourse`, so
|
|
5
|
+
# every one of these is `Recourse.something` wherever it is called from.
|
|
6
|
+
module Attachments
|
|
7
|
+
# The attachments a screen offers, in the order the model declared them, less
|
|
8
|
+
# whatever it asked to keep off one: `recourse_hidden :photos` hides a file exactly
|
|
9
|
+
# as it hides a column. Empty for a class Active Storage never reached — a host
|
|
10
|
+
# that installed none of it — which is the same guard `AttachmentResolution`
|
|
11
|
+
# already carries as `defined?(ActiveStorage::Reflection)`.
|
|
12
|
+
def attachment_names(model)
|
|
13
|
+
return [] unless model.respond_to? :attachment_reflections
|
|
14
|
+
|
|
15
|
+
model.attachment_reflections.keys - hidden_columns(model)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Whether that name holds several files or one, which is the only thing a field and
|
|
19
|
+
# a write each need to know about it: a field offers `multiple` for the one and not
|
|
20
|
+
# the other, and a write appends to the one where it replaces the other. Read off
|
|
21
|
+
# the reflection's own `macro`, so neither of Active Storage's classes is named.
|
|
22
|
+
def attachment_many?(model, name)
|
|
23
|
+
model.attachment_reflections[name]&.macro == :has_many_attached
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
extend Attachments
|
|
28
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
# What a table of attachments reads off Active Storage's own model, which was never
|
|
3
|
+
# asked how it wanted drawing. Extended onto the blob through the load hook Rails
|
|
4
|
+
# publishes for it, so nothing here is a patch.
|
|
5
|
+
module Blobs
|
|
6
|
+
# A blob is known by the name it was uploaded under. Every other model answers
|
|
7
|
+
# `:name`, and Active Storage has no such column — point a combobox at one
|
|
8
|
+
# without this and the select raises.
|
|
9
|
+
def recourse_label = :filename
|
|
10
|
+
|
|
11
|
+
# The service's business rather than the reader's: where the file sits, what it
|
|
12
|
+
# is called there, what it hashes to, and whatever the analyzer wrote down.
|
|
13
|
+
def recourse_hidden = %i[key checksum service_name metadata]
|
|
14
|
+
|
|
15
|
+
# Newest first, which is the order somebody looking at what was attached wants.
|
|
16
|
+
def recourse_order = { created_at: :desc }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
ActiveSupport.on_load :active_storage_blob do
|
|
21
|
+
extend Recourse::Blobs
|
|
22
|
+
end
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
module Recourse
|
|
2
2
|
module Helpers
|
|
3
|
-
# The action columns a row opens with: a look at a record,
|
|
3
|
+
# The action columns a row opens with: a look at a record, a change to it, and —
|
|
4
|
+
# where the routes drew nowhere else to put it — the end of it.
|
|
4
5
|
module Actions
|
|
5
|
-
# The
|
|
6
|
-
# than as one set's own word for them, so
|
|
7
|
-
# here as everywhere else. A row's links and
|
|
8
|
-
# so the two cannot drift apart — and its order
|
|
9
|
-
|
|
6
|
+
# The pages a record has and the one thing done to it without a page, named as
|
|
7
|
+
# the concepts an icon set knows rather than as one set's own word for them, so
|
|
8
|
+
# what draws them is Unicon's business here as everywhere else. A row's links and
|
|
9
|
+
# a card's tabs read the same map, so the two cannot drift apart — and its order
|
|
10
|
+
# is the order a row opens with.
|
|
11
|
+
ICONS = { show: :view, edit: :edit, destroy: :delete }.freeze
|
|
10
12
|
|
|
11
13
|
private
|
|
12
14
|
|
|
13
|
-
# Which of those
|
|
15
|
+
# Which of those this table draws a column for. Read into a local by the
|
|
14
16
|
# table, so the routes are asked once per render rather than twice for the
|
|
15
17
|
# heading and twice more for every row.
|
|
16
18
|
def resource_actions
|
|
@@ -29,14 +31,33 @@ module Recourse
|
|
|
29
31
|
|
|
30
32
|
# Whether a record's own page is there to be linked to, wherever its routes
|
|
31
33
|
# were drawn: a nested table's rows lead to the resource's own pages, the ones
|
|
32
|
-
# a nested route leaves to it, so the columns are the same either way.
|
|
34
|
+
# a nested route leaves to it, so the columns are the same either way. A delete
|
|
35
|
+
# is the exception, standing on the table only where no page would carry it.
|
|
33
36
|
def resource_action?(action)
|
|
37
|
+
return destroy_action_path.present? if action == :destroy
|
|
38
|
+
|
|
34
39
|
routed_action? action.to_s, resource_controller_path
|
|
35
40
|
end
|
|
36
41
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
42
|
+
# Where a row is deleted from, or nil. The Delete button stands on the edit page
|
|
43
|
+
# where the routes drew one; a resource routed `destroy` and no `edit` — a file
|
|
44
|
+
# attached to a record, a row joining two — has no page of its own to carry it,
|
|
45
|
+
# so the table does, at whichever of the two paths drew the route: the nesting's
|
|
46
|
+
# own, or the resource's. Remembered per render, since the table asks per row.
|
|
47
|
+
def destroy_action_path
|
|
48
|
+
return @recourse_destroy_path if defined? @recourse_destroy_path
|
|
49
|
+
|
|
50
|
+
paths = [controller.controller_path, resource_controller_path].uniq
|
|
51
|
+
@recourse_destroy_path = if paths.none? { |path| routed_action? 'edit', path }
|
|
52
|
+
paths.find { |path| routed_action? 'destroy', path }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The icon linking to one of those pages, or the button that deletes the row, or
|
|
57
|
+
# nothing where the page is not routed.
|
|
39
58
|
def resource_action_link(action, record)
|
|
59
|
+
return destroy_button record if action == :destroy
|
|
60
|
+
|
|
40
61
|
path = resource_action_path action, record
|
|
41
62
|
return unless path
|
|
42
63
|
|
|
@@ -51,6 +72,17 @@ module Recourse
|
|
|
51
72
|
|
|
52
73
|
url_for controller: "/#{resource_controller_path}", action: action, id: record
|
|
53
74
|
end
|
|
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.
|
|
78
|
+
def destroy_button(record)
|
|
79
|
+
path = url_for controller: "/#{destroy_action_path}", action: :destroy, id: record
|
|
80
|
+
|
|
81
|
+
button_to icon_tag(ICONS[:destroy], class: 'fg-danger'), path,
|
|
82
|
+
method: :delete, class: 'btn btn-sm btn-link btn-icon p-0',
|
|
83
|
+
aria: { label: t('recourse.delete', model: resource_name) },
|
|
84
|
+
form: { data: { turbo_confirm: destroy_warning(record) } }
|
|
85
|
+
end
|
|
54
86
|
end
|
|
55
87
|
end
|
|
56
88
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
module Helpers
|
|
3
|
+
# The field a form offers for putting a file on a record, and the note under it
|
|
4
|
+
# saying what the record is holding already.
|
|
5
|
+
module Attachments
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# The attachments this screen's model keeps, which the form draws after every
|
|
9
|
+
# column it has: a file input is the widest control on the page.
|
|
10
|
+
def attachment_names
|
|
11
|
+
Recourse.attachment_names resource_model
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# One labelled file input, in the grid a column's field sits in. `multiple` where
|
|
15
|
+
# the model keeps several, and no hidden blank beside it: nothing here is ever
|
|
16
|
+
# assigned, so a field nobody touched is one the write passes over.
|
|
17
|
+
def attachment_field(name)
|
|
18
|
+
label = resource_model.human_attribute_name name
|
|
19
|
+
|
|
20
|
+
tag.div class: ROW do
|
|
21
|
+
safe_join [
|
|
22
|
+
@recourse_form.label(name, label, class: 'form-label'),
|
|
23
|
+
@recourse_form.file_field(name, **attachment_options(name)),
|
|
24
|
+
attached_note(name),
|
|
25
|
+
].compact
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# A file input carries none of a column's constraints, so its options are the
|
|
30
|
+
# class, the note under it, and whether it takes several files at once.
|
|
31
|
+
def attachment_options(name)
|
|
32
|
+
described = field_note_id name if resource_record&.persisted?
|
|
33
|
+
options = { class: 'form-control', aria: { describedby: described } }
|
|
34
|
+
return options unless Recourse.attachment_many? resource_model, name
|
|
35
|
+
|
|
36
|
+
options.merge multiple: true, include_hidden: false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# What the record is holding, under the field that adds to it: choosing a file
|
|
40
|
+
# joins what is there or replaces it, and which of the two it is depends on what
|
|
41
|
+
# is there. Only where there is a record to ask — a form making one has nothing
|
|
42
|
+
# attached yet, so it has nothing to report and says nothing.
|
|
43
|
+
def attached_note(name)
|
|
44
|
+
return unless resource_record&.persisted?
|
|
45
|
+
|
|
46
|
+
field_note attached_reading(name), field_note_id(name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def attached_reading(name)
|
|
50
|
+
files = attached_filenames name
|
|
51
|
+
return attached_nothing name if files.empty?
|
|
52
|
+
return t 'recourse.attached.one', files: files.first if files.one?
|
|
53
|
+
|
|
54
|
+
t 'recourse.attached.many', count: files.size, files: files.to_sentence
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# A field for one file reads `No file attached` where a field for several reads
|
|
58
|
+
# `No files attached`, since which it is decides what choosing one will do.
|
|
59
|
+
def attached_nothing(name)
|
|
60
|
+
many = Recourse.attachment_many? resource_model, name
|
|
61
|
+
|
|
62
|
+
t "recourse.attached.#{many ? 'none_many' : 'none'}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
module Helpers
|
|
3
|
+
# What a page reads off Active Storage's own objects: the row behind a file, the way
|
|
4
|
+
# to open the file itself, and the picture a browser can make of it. Paired with
|
|
5
|
+
# `Recourse::Blobs`, which is what tells the blob model how it wants drawing.
|
|
6
|
+
module Blobs
|
|
7
|
+
# How tall a preview stands on the page. The picture behind it is made twice as
|
|
8
|
+
# tall, so a screen with two pixels to the point has every one of them.
|
|
9
|
+
PREVIEW_HEIGHT = 100
|
|
10
|
+
|
|
11
|
+
# How a picture is saved for the trip: lossy at a quality the eye forgives at this
|
|
12
|
+
# size, and stripped of whatever the camera wrote — neither is worth the bytes.
|
|
13
|
+
PREVIEW_SAVER = { quality: 80, strip: true }.freeze
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# True for the filename of a blob, and for nothing else — a host model with a
|
|
18
|
+
# column of that name is drawing its own value, not Active Storage's.
|
|
19
|
+
def blob_filename?(column) = column == 'filename' && blob_resource?
|
|
20
|
+
|
|
21
|
+
# By name, so an app with no Active Storage never mentions the constant.
|
|
22
|
+
def blob_resource? = resource_model.name == 'ActiveStorage::Blob'
|
|
23
|
+
|
|
24
|
+
# The file itself, in a tab of its own: an admin opening one is leaving the page
|
|
25
|
+
# they were reading, and a file that replaced it would lose their place. Inline,
|
|
26
|
+
# so the browser shows a picture and plays a video rather than saving either.
|
|
27
|
+
# Whatever the caller hands over is what the link reads as.
|
|
28
|
+
def blob_link(blob, read)
|
|
29
|
+
link_to read, main_app.rails_blob_path(blob, disposition: :inline),
|
|
30
|
+
target: '_blank', rel: 'noopener'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A picture of the file where Active Storage can make one — the image itself
|
|
34
|
+
# scaled down, or the frame a previewer takes of a video or a PDF — inside the link
|
|
35
|
+
# that opens the whole file. Nothing for a file nothing can draw. A `<picture>`
|
|
36
|
+
# offering WebP first, the smallest a browser can be sent, and the file's own kind
|
|
37
|
+
# under it for one that cannot take WebP; either at half the pixels it was made
|
|
38
|
+
# with, and the height is the attribute's, so no class here may say `height: auto`,
|
|
39
|
+
# as `img-thumbnail` does.
|
|
40
|
+
def blob_preview(blob)
|
|
41
|
+
return unless blob.representable?
|
|
42
|
+
|
|
43
|
+
blob_link blob, tag.picture(safe_join([preview_source(blob), preview_image(blob)]))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def preview_source(blob)
|
|
47
|
+
tag.source srcset: preview_path(blob, format: :webp, saver: PREVIEW_SAVER),
|
|
48
|
+
type: 'image/webp'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def preview_image(blob)
|
|
52
|
+
image_tag preview_path(blob), alt: blob.filename.to_s, height: PREVIEW_HEIGHT,
|
|
53
|
+
class: 'border rounded', loading: 'lazy'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The address of one scaling of the file, made on first request and kept.
|
|
57
|
+
def preview_path(blob, **saving)
|
|
58
|
+
representation = blob.representation resize_to_limit: [nil, PREVIEW_HEIGHT * 2], **saving
|
|
59
|
+
|
|
60
|
+
main_app.rails_representation_path representation
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The blobs one attachment holds, as a list whichever kind it is. Read through
|
|
64
|
+
# the associations Active Storage generated rather than through the reader it
|
|
65
|
+
# named, which is the trade `AttachmentResolution` already makes next door.
|
|
66
|
+
def attached_blobs(name)
|
|
67
|
+
return resource_record.association(:"#{name}_blobs").reader.to_a if
|
|
68
|
+
Recourse.attachment_many? resource_model, name
|
|
69
|
+
|
|
70
|
+
Array resource_record.association(:"#{name}_blob").reader
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# And the names they were uploaded under, which is what a reader is told a
|
|
74
|
+
# record is holding.
|
|
75
|
+
def attached_filenames(name)
|
|
76
|
+
attached_blobs(name).map { |blob| blob.filename.to_s }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -79,6 +79,8 @@ module Recourse
|
|
|
79
79
|
return search_highlight named_cell(resource, association), column if association
|
|
80
80
|
|
|
81
81
|
value = resource.attributes[column]
|
|
82
|
+
return blob_link resource, value if blob_filename? column
|
|
83
|
+
|
|
82
84
|
counted = resource_model.recourse_counters[column]
|
|
83
85
|
|
|
84
86
|
# A count is the bare number — the icon in the heading already says what it
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
module Helpers
|
|
3
|
+
# An attached file on a record's own page: shown where a browser can make a picture
|
|
4
|
+
# of it, and named where it cannot, either opening the file.
|
|
5
|
+
module Previews
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# The attachments a record's own page reads out, which are the ones it keeps a
|
|
9
|
+
# single file under. A `has_many_attached` is a table of its own — a page a host
|
|
10
|
+
# nests under the record — and a row of pictures says more there than a list
|
|
11
|
+
# squeezed into a value's row would.
|
|
12
|
+
def shown_attachments
|
|
13
|
+
attachment_names.reject { |name| Recourse.attachment_many? resource_model, name }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# One labelled file, in the grid a column's value sits in.
|
|
17
|
+
def attachment_value(name)
|
|
18
|
+
label = resource_model.human_attribute_name name
|
|
19
|
+
|
|
20
|
+
tag.div class: ROW do
|
|
21
|
+
safe_join [tag.div(label, class: 'form-label'), attachment_control(name)]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Nothing attached reads as the dash every other empty value reads as.
|
|
26
|
+
def attachment_control(name)
|
|
27
|
+
blob = attached_blobs(name).first
|
|
28
|
+
read = blob ? attached_file(blob) : t('recourse.blank')
|
|
29
|
+
|
|
30
|
+
tag.div read, class: 'form-control-plaintext'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The picture where there is one to draw, and the name where there is not — a
|
|
34
|
+
# spreadsheet is a download and nothing else. Either opens the file.
|
|
35
|
+
def attached_file(blob)
|
|
36
|
+
blob_preview(blob) || blob_link(blob, blob.filename.to_s)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -3,13 +3,22 @@ module Recourse
|
|
|
3
3
|
# What the page is about and what it is called: the model behind it, the record
|
|
4
4
|
# on it, and the words both are read out under.
|
|
5
5
|
module Resources
|
|
6
|
-
# Human, plural name of the resource on the page, e.g. `Contacts`.
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
|
|
6
|
+
# Human, plural name of the resource on the page, e.g. `Contacts`. A page of
|
|
7
|
+
# attachments is named after what the record calls them rather than after Active
|
|
8
|
+
# Storage's own word for the row: `Photos`, never `Blobs`. `known_title`, since a
|
|
9
|
+
# host page wearing this layout may be named after no model at all — a contact's
|
|
10
|
+
# home is a `Location`, and the path is the only word for it.
|
|
11
|
+
def resources_name
|
|
12
|
+
return controller.controller_name.humanize if blob_resource?
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
Recourse.known_title controller.controller_name
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Singular, lowercase name of the resource, e.g. 'contact' — and 'photo' on a page
|
|
18
|
+
# of files, for the same reason as above.
|
|
12
19
|
def resource_name
|
|
20
|
+
return Recourse.downcase controller.controller_name.singularize.humanize if blob_resource?
|
|
21
|
+
|
|
13
22
|
Recourse.downcase resource_model.model_name.human
|
|
14
23
|
end
|
|
15
24
|
|
|
@@ -38,7 +47,8 @@ module Recourse
|
|
|
38
47
|
|
|
39
48
|
private
|
|
40
49
|
|
|
41
|
-
# Resolved by the controller,
|
|
50
|
+
# Resolved by the controller, which is the one that knows whether the name is a
|
|
51
|
+
# model of this app's, something a record has attached, or a model a host named.
|
|
42
52
|
def resource_model
|
|
43
53
|
controller_assign('recourse_model') || Recourse.model(controller.controller_name)
|
|
44
54
|
end
|
data/lib/recourse/helpers.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require_relative 'helpers/actions'
|
|
2
|
+
require_relative 'helpers/attachments'
|
|
3
|
+
require_relative 'helpers/blobs'
|
|
2
4
|
require_relative 'helpers/bookmarks'
|
|
3
5
|
require_relative 'helpers/buttons'
|
|
4
6
|
require_relative 'helpers/cards'
|
|
@@ -24,6 +26,7 @@ require_relative 'helpers/names'
|
|
|
24
26
|
require_relative 'helpers/navigation'
|
|
25
27
|
require_relative 'helpers/parents'
|
|
26
28
|
require_relative 'helpers/pictures'
|
|
29
|
+
require_relative 'helpers/previews'
|
|
27
30
|
require_relative 'helpers/references'
|
|
28
31
|
require_relative 'helpers/resources'
|
|
29
32
|
require_relative 'helpers/refreshes'
|
|
@@ -42,11 +45,11 @@ require_relative 'helpers/zones'
|
|
|
42
45
|
module Recourse
|
|
43
46
|
# View helpers for the pages the gem renders, and what the parts share.
|
|
44
47
|
module Helpers
|
|
45
|
-
include Actions, Bookmarks, Buttons, Cards,
|
|
48
|
+
include Actions, Attachments, Blobs, Bookmarks, Buttons, Cards,
|
|
46
49
|
Cells, Choices, Colors, Comboboxes, Constraints, Counters, Deletions, Densities,
|
|
47
50
|
Details,
|
|
48
51
|
Examples, Fields, Filters, Flashes, Formats, Inputs, Kinds, Limits, Maps,
|
|
49
|
-
Names, Navigation, Parents, Pictures, References, Refreshes,
|
|
52
|
+
Names, Navigation, Parents, Pictures, Previews, References, Refreshes,
|
|
50
53
|
Routing,
|
|
51
54
|
Resources, Rows, Searches, Shortcuts, Sidebars, Sorts, Tabs, Themes,
|
|
52
55
|
Times, Values, Zones
|
data/lib/recourse/search.rb
CHANGED
|
@@ -56,18 +56,26 @@ module Recourse
|
|
|
56
56
|
|
|
57
57
|
# Ransack reads nothing it has not been shown — `ransackable_attributes` is the
|
|
58
58
|
# allowlist — so what arrives here needs no permitting, only untangling: a list
|
|
59
|
-
# predicate is
|
|
60
|
-
# since `IN ()` would match no row rather than every one.
|
|
59
|
+
# predicate is gathered into the values it was picked as, and a filter nobody set
|
|
60
|
+
# is dropped, since `IN ()` would match no row rather than every one.
|
|
61
61
|
def conditions(params)
|
|
62
62
|
# `?q=anything` reaches here as a String rather than as parameters of its own,
|
|
63
63
|
# and a search nobody asked for reaches here as nil. Neither is a condition.
|
|
64
64
|
return {} unless params.is_a? ActionController::Parameters
|
|
65
65
|
|
|
66
66
|
params.to_unsafe_h.filter_map do |key, value|
|
|
67
|
+
value = list_values value if key.match? LIST_PREDICATES
|
|
67
68
|
next if value.blank?
|
|
68
69
|
|
|
69
|
-
[key,
|
|
70
|
+
[key, value]
|
|
70
71
|
end.to_h
|
|
71
72
|
end
|
|
73
|
+
|
|
74
|
+
# A multiple select submits one value per pick, so a list predicate arrives as an
|
|
75
|
+
# array — and as a lone value where a link carried one. Never split on a comma:
|
|
76
|
+
# that was the shape before a combobox became a `<select>`, and it read `["48"]`,
|
|
77
|
+
# the array written out, as one value, which Ransack cast to the 0 no row holds.
|
|
78
|
+
# Emptied of its blanks after, since the way back to no filter submits one.
|
|
79
|
+
def list_values(value) = Array(value).compact_blank
|
|
72
80
|
end
|
|
73
81
|
end
|
data/lib/recourse/version.rb
CHANGED
data/lib/recourse.rb
CHANGED
|
@@ -4,6 +4,8 @@ require 'ransack'
|
|
|
4
4
|
require 'unicon'
|
|
5
5
|
|
|
6
6
|
require_relative 'recourse/version'
|
|
7
|
+
require_relative 'recourse/attachments'
|
|
8
|
+
require_relative 'recourse/blobs'
|
|
7
9
|
require_relative 'recourse/bookmarks'
|
|
8
10
|
require_relative 'recourse/colors'
|
|
9
11
|
require_relative 'recourse/columns'
|
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.
|
|
4
|
+
version: 5.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- claudiob
|
|
@@ -121,6 +121,8 @@ files:
|
|
|
121
121
|
- MIT-LICENSE
|
|
122
122
|
- README.md
|
|
123
123
|
- app/controllers/bookmarks_controller.rb
|
|
124
|
+
- app/controllers/concerns/recourse/attachment_resolution.rb
|
|
125
|
+
- app/controllers/concerns/recourse/attachment_writing.rb
|
|
124
126
|
- app/controllers/concerns/recourse/landing.rb
|
|
125
127
|
- app/controllers/concerns/recourse/list_resolution.rb
|
|
126
128
|
- app/controllers/concerns/recourse/paging.rb
|
|
@@ -156,6 +158,8 @@ files:
|
|
|
156
158
|
- app/views/recourses/show.html.erb
|
|
157
159
|
- config/locales/recourse.en.yml
|
|
158
160
|
- lib/recourse.rb
|
|
161
|
+
- lib/recourse/attachments.rb
|
|
162
|
+
- lib/recourse/blobs.rb
|
|
159
163
|
- lib/recourse/bookmarks.rb
|
|
160
164
|
- lib/recourse/broadcasting.rb
|
|
161
165
|
- lib/recourse/colors.rb
|
|
@@ -165,6 +169,8 @@ files:
|
|
|
165
169
|
- lib/recourse/engine.rb
|
|
166
170
|
- lib/recourse/helpers.rb
|
|
167
171
|
- lib/recourse/helpers/actions.rb
|
|
172
|
+
- lib/recourse/helpers/attachments.rb
|
|
173
|
+
- lib/recourse/helpers/blobs.rb
|
|
168
174
|
- lib/recourse/helpers/bookmarks.rb
|
|
169
175
|
- lib/recourse/helpers/buttons.rb
|
|
170
176
|
- lib/recourse/helpers/cards.rb
|
|
@@ -190,6 +196,7 @@ files:
|
|
|
190
196
|
- lib/recourse/helpers/navigation.rb
|
|
191
197
|
- lib/recourse/helpers/parents.rb
|
|
192
198
|
- lib/recourse/helpers/pictures.rb
|
|
199
|
+
- lib/recourse/helpers/previews.rb
|
|
193
200
|
- lib/recourse/helpers/references.rb
|
|
194
201
|
- lib/recourse/helpers/refreshes.rb
|
|
195
202
|
- lib/recourse/helpers/resources.rb
|