recourse 5.5.0 → 5.6.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 +64 -0
- data/README.md +82 -1
- data/app/controllers/concerns/recourse/positioned.rb +40 -0
- data/app/controllers/recourse/base_controller.rb +2 -2
- data/app/controllers/recourse/positions_controller.rb +43 -0
- data/app/views/layouts/recourses.html.erb +9 -8
- data/app/views/recourses/_scheme.html.erb +1 -1
- data/app/views/recourses/_table.html.erb +12 -2
- data/config/locales/recourse.en.yml +9 -0
- data/lib/recourse/assets.rb +32 -0
- data/lib/recourse/helpers/cells.rb +6 -6
- data/lib/recourse/helpers/positions.rb +54 -0
- data/lib/recourse/helpers/searches.rb +5 -0
- data/lib/recourse/helpers/sorts.rb +10 -2
- data/lib/recourse/helpers/themes.rb +2 -2
- data/lib/recourse/helpers.rb +2 -1
- data/lib/recourse/positionable.rb +75 -0
- data/lib/recourse/positioning.rb +60 -0
- data/lib/recourse/positions.rb +50 -0
- data/lib/recourse/recoursive.rb +19 -3
- data/lib/recourse/routes/nested.rb +25 -10
- data/lib/recourse/routes.rb +24 -2
- data/lib/recourse/search.rb +24 -6
- data/lib/recourse/themes.rb +0 -5
- data/lib/recourse/titles.rb +6 -4
- data/lib/recourse/version.rb +1 -1
- data/lib/recourse.rb +18 -14
- 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: 8d2b5251cca5129bfd2c634cb82e8cb8951e9363507762ec1efab7263bce419f
|
|
4
|
+
data.tar.gz: 25630ad43f83b9a650e8a4d52fc01628a0032e087d23f7285f420d683db4895c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8fa002c794c334526bdc692ba5bcc5cc47f40521501c2dd945f9aa04638013a6af5d5fcae20b57960ab06fdb34adb77cf63dfb7bd8dae4e4908bf369b8d86d56
|
|
7
|
+
data.tar.gz: 8dc080edb46c2273cf4685a5a98c1bf82c58411acec408c1a9cf030ed9c09ff9219a55741df512028f9065f0b09b0f27983dcf58c7dd5dcf7b93e8f72546d5a2
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,70 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## 5.6.0 - 2026-09-18
|
|
11
|
+
|
|
12
|
+
* [Feature] A table can be dragged into an order
|
|
13
|
+
|
|
14
|
+
Back from the 4.0.0 development tree, and opting in the way the map and the calendar do:
|
|
15
|
+
a model keeping an integer `position` is one a reader positions by hand. A grip opens each
|
|
16
|
+
row, dragging it moves the row, and `resource :position, only: :update` is where the place
|
|
17
|
+
it landed is written. That route is drawn where the host asks for it —
|
|
18
|
+
`recourses :steps, positionable: true` — and nowhere else: a table is put in order from
|
|
19
|
+
its index, so the keyword is refused on a resource that draws none. The column still
|
|
20
|
+
decides everything else, `recourse_order` among it, because the order a table is read in
|
|
21
|
+
and the order somebody put it in are one fact.
|
|
22
|
+
|
|
23
|
+
The type is load-bearing rather than belt-and-braces, and asked through
|
|
24
|
+
`type_for_attribute` as a calendar asks about its two ends: `contacts.position` holding a
|
|
25
|
+
job title is a word about the row, not a place among rows. A map and a calendar only
|
|
26
|
+
read, so a column mistaken for one costs a link; this writes, and writes at the first
|
|
27
|
+
save.
|
|
28
|
+
|
|
29
|
+
Two things follow the column rather than the routes. `Recourse::Positionable` fills the
|
|
30
|
+
column on create and closes the gap on destroy, so a row made in a console is numbered
|
|
31
|
+
like one made behind a form — what a drop reports is a row's place on the *page*, which
|
|
32
|
+
is a position only while the numbers run 1, 2, 3 with no holes in them. A model pointing
|
|
33
|
+
several ways answers `recourse_siblings` with the rows one of its own is counted among;
|
|
34
|
+
one pointing a single way needs no answer, and one pointing nowhere is positioned among
|
|
35
|
+
the whole table.
|
|
36
|
+
|
|
37
|
+
Whether a page is positioned is asked per page, not per model: a place means something
|
|
38
|
+
under the parent it is counted within, so a flat model's own index is positioned and a
|
|
39
|
+
listing of every row across every parent — where the positions run 1, 2, 3, 1, 2, 3 side
|
|
40
|
+
by side — is not. And the search box and the sorted headings stand down while the grips
|
|
41
|
+
are drawn, a `q` typed by hand refused with them: a filter shortens the page, and a drop
|
|
42
|
+
on a shortened page reports a place among the rows that are left.
|
|
43
|
+
|
|
44
|
+
A host keeping the order itself writes `def recourse_position = nil`. A host with a
|
|
45
|
+
second listing of a positioned model names the second column on its own controller, which
|
|
46
|
+
then owns the write as well as the page — `Recourse::PositionsController` is public to
|
|
47
|
+
subclass, and `Recourse::Positioning` is public for the column such a host maintains.
|
|
48
|
+
|
|
49
|
+
* [Fix] A word that names a class which is not a model
|
|
50
|
+
|
|
51
|
+
`known_title` guarded against a name resolving to nothing — `pause` is a verb no app
|
|
52
|
+
makes a class for — but not against one resolving to a class with no `model_name`. A
|
|
53
|
+
host may keep a plain `Message` that gathers rows rather than a table of them, and a
|
|
54
|
+
breadcrumb over such a page raised instead of reading. It asks whether what it found
|
|
55
|
+
answers `model_name`, the way `known_icon` beside it already does.
|
|
56
|
+
|
|
57
|
+
* [Feature] `Recourse.assets` says where the house bundle is served from
|
|
58
|
+
|
|
59
|
+
The stylesheet, the script and the palettes were three URLs carrying one pinned version,
|
|
60
|
+
moved together by hand every release — the 5.5.0 entry below says as much. They are one
|
|
61
|
+
line now: `Recourse::BUNDLE` is which release, `Recourse.assets` is where it is read
|
|
62
|
+
from, and jsdelivr's copy of the published package stays the default.
|
|
63
|
+
|
|
64
|
+
A host that sets `Recourse.assets = ''` serves the bundle itself, out of the
|
|
65
|
+
`houseaccount` gem mounted by path: its engine answers `/css`, `/js` and `/theme` from
|
|
66
|
+
its own `public/`. That is how a stylesheet or a controller gets tried on a real page
|
|
67
|
+
before the version carrying it is published, which is what this release needed.
|
|
68
|
+
|
|
69
|
+
* [Change] The pinned bundle is `houseaccount@0.14.0`
|
|
70
|
+
|
|
71
|
+
What carries the `sortable` controller the feature above names, and the two rules beside
|
|
72
|
+
it: the grip's cursor, and the tint on the space a dragged row will land in.
|
|
73
|
+
|
|
10
74
|
## 5.5.0 - 2026-09-17
|
|
11
75
|
|
|
12
76
|
* [Fix] A sorted heading has a mark again
|
data/README.md
CHANGED
|
@@ -66,7 +66,8 @@ To change the content displayed in the `index` table of a model, override any of
|
|
|
66
66
|
| [`recourse_label`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_label-instance_method) | `:name` | the column a combobox shows and a foreign-key cell reads; typed rather than picked where it has a length validator |
|
|
67
67
|
| [`recourse_hidden`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_hidden-instance_method) | `[]` | columns kept off the table, the page, the form and the search |
|
|
68
68
|
| [`recourse_displayed`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_displayed-instance_method) | `[]` | columns a table draws that it would leave off: encrypted ones, the id, timestamps, JSON |
|
|
69
|
-
| [`recourse_order`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_order-instance_method) | `:id
|
|
69
|
+
| [`recourse_order`](https://rubydoc.info/gems/recourse/Recourse/Recoursive#recourse_order-instance_method) | `:id`, or the positioned column | the index's order, a Symbol or a Hash; rows with nothing in the column come last |
|
|
70
|
+
| [`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 |
|
|
70
71
|
| [`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 |
|
|
71
72
|
|
|
72
73
|
For instance, this would yield a more compact `index` than the default configuration:
|
|
@@ -239,6 +240,69 @@ Both columns have to be datetimes, asked through `type_for_attribute`, so an
|
|
|
239
240
|
`starts_at` earns nothing.
|
|
240
241
|
|
|
241
242
|
|
|
243
|
+
### Positionable tables
|
|
244
|
+
|
|
245
|
+
A table whose model keeps an integer `position` is one a reader puts in order by hand: a
|
|
246
|
+
grip opens each row, dragging it moves the row, and the place it lands in is written to a
|
|
247
|
+
route drawn under the index. Say where that route belongs, since a page nobody drags needs
|
|
248
|
+
none:
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
recourses :steps, positionable: true
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
A table is put in order from its index and from nowhere else, so the keyword is refused on
|
|
255
|
+
a resource that draws none. The column decides the rest — the grips, the callbacks, and
|
|
256
|
+
`recourse_order`, since the order a table is read in and the order somebody put it in are
|
|
257
|
+
one fact.
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
class Step < ActiveRecord::Base
|
|
261
|
+
belongs_to :team
|
|
262
|
+
belongs_to :person
|
|
263
|
+
|
|
264
|
+
# A step points two ways, so which of them its place is counted within is the model's
|
|
265
|
+
# to say. One `belongs_to` needs no answer, and a model pointing nowhere is positioned
|
|
266
|
+
# among the whole table.
|
|
267
|
+
def recourse_siblings = team.steps
|
|
268
|
+
end
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Two things follow the column rather than the routes. A new row lands last among its own
|
|
272
|
+
and the gap closes behind one that goes, whether it was made behind a form or in a
|
|
273
|
+
console: what a drop reports is a row's place on the *page*, which is a position only
|
|
274
|
+
while the numbers run 1, 2, 3 with no holes in them. And a table being positioned is asked
|
|
275
|
+
per page — a position means something under the parent it is counted within, so a flat
|
|
276
|
+
model's own index is positioned and a listing of every row across every parent is not.
|
|
277
|
+
|
|
278
|
+
The search box and the sorted headings stand down while the grips are drawn, and a `q`
|
|
279
|
+
typed by hand is refused for the same reason: a filter shortens the page, and a drop on a
|
|
280
|
+
shortened page reports a place among the rows that are left.
|
|
281
|
+
|
|
282
|
+
A host that keeps the order itself writes `def recourse_position = nil`, which takes the
|
|
283
|
+
grips off the table and the two callbacks off the model together. A host with a *second*
|
|
284
|
+
listing of a positioned model — a plan's place among its service's plans, and another
|
|
285
|
+
among every plan of its department — names the second column on the controller instead,
|
|
286
|
+
and owns the write as well as the page:
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
module Departmental
|
|
290
|
+
extend ActiveSupport::Concern
|
|
291
|
+
|
|
292
|
+
private
|
|
293
|
+
|
|
294
|
+
def recourse_position = 'ordering'
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
class Administered::Departments::Plans::PositionsController < Recourse::PositionsController
|
|
298
|
+
include Departmental
|
|
299
|
+
end
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`Recourse::Positioning` is public for exactly that case: `new(relation, column).close` is
|
|
303
|
+
what such a host calls to keep its own column contiguous.
|
|
304
|
+
|
|
305
|
+
|
|
242
306
|
## Development
|
|
243
307
|
|
|
244
308
|
```bash
|
|
@@ -249,6 +313,23 @@ cd test/dummy && bin/rails server
|
|
|
249
313
|
|
|
250
314
|
The dummy app under `test/dummy` runs on SQLite; the gem names no adapter.
|
|
251
315
|
|
|
316
|
+
### Reading an unpublished bundle
|
|
317
|
+
|
|
318
|
+
The pages link `houseaccount` from a CDN, pinned to `Recourse::BUNDLE`, so a stylesheet or
|
|
319
|
+
a Stimulus controller that is not published yet cannot be read on a page. Two lines change
|
|
320
|
+
that — one in this Gemfile, kept out of it because CI checks out this repo alone:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
echo "gem 'houseaccount', path: '../design'" >> Gemfile && bundle install
|
|
324
|
+
cd test/dummy && bin/rails db:migrate && HOUSE_ASSETS=1 bin/rails server
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`HOUSE_ASSETS` is what loads that gem and sets `Recourse.assets = ''`, so the page asks
|
|
328
|
+
this app for `/css`, `/js` and `/theme` and the gem's engine answers them out of its own
|
|
329
|
+
`public/`. Behind a variable because what a deployed app links is what the suite asserts,
|
|
330
|
+
and because the dummy is also the stand-in for a host with no such gem at all. The dummy's
|
|
331
|
+
development database is migrated on its own, which the line above is why.
|
|
332
|
+
|
|
252
333
|
## License
|
|
253
334
|
|
|
254
335
|
[MIT](MIT-LICENSE).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
# Which column a listing is dragged into order by, and so whether it is positioned at
|
|
3
|
+
# all. Two questions with one answer, asked by the table that draws the grips, by the
|
|
4
|
+
# search and the headings that stand down where they are drawn, and by the write a
|
|
5
|
+
# drop lands on.
|
|
6
|
+
module Positioned
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
# After the parent is found, which is what the default turns on.
|
|
11
|
+
before_action { @recourse_position = recourse_position }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# The model's own column by default — the `position` it keeps — and only on a page
|
|
17
|
+
# where a place among rows means something: the rows one is counted within are the
|
|
18
|
+
# rows the page lists, which the parent above it settles.
|
|
19
|
+
#
|
|
20
|
+
# A host overrides it where a listing is positioned by an order the model does not
|
|
21
|
+
# keep. A plan holds a place among its service's plans and another among every plan
|
|
22
|
+
# of its department, and which of the two a page is in is the page's answer rather
|
|
23
|
+
# than the model's: a model keeps one column, and a second listing is a second
|
|
24
|
+
# column that only its own controller knows about.
|
|
25
|
+
#
|
|
26
|
+
# Such a host owns the write as well as the listing. The gem draws a positions
|
|
27
|
+
# controller under every index, and that one has to be told the same two things —
|
|
28
|
+
# this column, and the rows to renumber — or a drop would move a row among rows the
|
|
29
|
+
# page never showed.
|
|
30
|
+
def recourse_position
|
|
31
|
+
return unless resource_model?
|
|
32
|
+
return unless Recourse.positioned? resource_class, @recourse_parent_association
|
|
33
|
+
|
|
34
|
+
Recourse.position_column resource_class
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Whether these rows are ones a reader positions, which is the same question again.
|
|
38
|
+
def positioned? = @recourse_position.present?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -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, AttachmentResolution, AttachmentWriting,
|
|
6
|
+
include Pagy::Method, Positioned, AttachmentResolution, AttachmentWriting,
|
|
7
7
|
Landing, Paging, ListResolution, ParentNaming,
|
|
8
8
|
ParentResolution, ReferenceResolution, ResourceResolution,
|
|
9
9
|
Weeks, Zoning
|
|
@@ -22,7 +22,7 @@ module Recourse
|
|
|
22
22
|
# Lists one page of the model the route is named after, or one week of it where the
|
|
23
23
|
# calendar was asked for — a week is how much that shape shows. `@q` is Ransack's own.
|
|
24
24
|
def index
|
|
25
|
-
search = Search.new recourse_relation, params[:q]
|
|
25
|
+
search = Search.new recourse_relation, params[:q], positioned: @recourse_position
|
|
26
26
|
@q = search.query
|
|
27
27
|
|
|
28
28
|
@pagy, @resources = week_or_page search.scope
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
# The place one row holds in a table somebody positioned: written by a drag, and never
|
|
3
|
+
# read back here. The path names the row and the route names the rows it is counted
|
|
4
|
+
# among, so a position is all there is to submit.
|
|
5
|
+
class PositionsController < ::RecoursesController
|
|
6
|
+
# `update` is a member action everywhere else, so the inherited callback would
|
|
7
|
+
# look for an `:id` a singular resource never carries.
|
|
8
|
+
skip_before_action :find_resource
|
|
9
|
+
|
|
10
|
+
# Puts the row the path names where the request says, among the rows the route
|
|
11
|
+
# named, and answers with as little as the caller can do with: a drag is answered
|
|
12
|
+
# with nothing at all, the row being already where it was dropped, and redrawing
|
|
13
|
+
# the table under the cursor that dropped it is what this avoids.
|
|
14
|
+
def update
|
|
15
|
+
positioning = Positioning.new recourse_relation, positioned_column
|
|
16
|
+
positioning.move moved_record, params.expect(:position)
|
|
17
|
+
|
|
18
|
+
head :no_content
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# The model this positions, read off the listing the route was nested under rather
|
|
24
|
+
# than off this controller's own name, which is always `positions`.
|
|
25
|
+
def recourse_model = Recourse.model(listing_path)
|
|
26
|
+
|
|
27
|
+
def moved_record = resource_class.find(params.expect(:"#{listing_name}_id"))
|
|
28
|
+
|
|
29
|
+
# A table nobody positions draws no handle, so the only way here is by hand — which
|
|
30
|
+
# earns a 404 rather than a 500 from somewhere below.
|
|
31
|
+
def positioned_column
|
|
32
|
+
raise ActiveRecord::RecordNotFound unless positioned?
|
|
33
|
+
|
|
34
|
+
@recourse_position
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# One segment up, which is where the routes drew it — the gem draws this path
|
|
38
|
+
# itself, so unlike a host's nesting there is nothing to look up.
|
|
39
|
+
def listing_path = controller_path.rpartition('/').first
|
|
40
|
+
|
|
41
|
+
def listing_name = listing_path.split('/').last.singularize
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -18,15 +18,16 @@
|
|
|
18
18
|
|
|
19
19
|
<%# Everything the page is styled by, from the one place every HouseAccount app draws
|
|
20
20
|
on: Bootstrap 6, its icons and the house's own rules for this gem's markup. Served
|
|
21
|
-
from the `houseaccount` package on a CDN, pinned to a version
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
from the `houseaccount` package on a CDN, pinned to a version — `Recourse.assets`
|
|
22
|
+
is where, and `Recourse::BUNDLE` is which — so a change there reaches these pages
|
|
23
|
+
when somebody moves that line and not before. Two connections opened before the
|
|
24
|
+
parser finds either tag: the stylesheet's plain one and the CORS one a module
|
|
25
|
+
script and a font are fetched over. They name the CDN outright, being a hint
|
|
26
|
+
about it and nothing more, so a host serving the bundle itself simply never uses
|
|
27
|
+
them. -%>
|
|
27
28
|
<link rel='preconnect' href='https://cdn.jsdelivr.net'>
|
|
28
29
|
<link rel='preconnect' href='https://cdn.jsdelivr.net' crossorigin>
|
|
29
|
-
<link rel='stylesheet' href='
|
|
30
|
+
<link rel='stylesheet' href='<%= Recourse.asset 'css/houseaccount.css' %>'>
|
|
30
31
|
<%# The one thing these screens set that the house's stylesheet does not: system faces
|
|
31
32
|
at a compact size, for pages dense with figures. 14px as rem, so a browser's own
|
|
32
33
|
text-size setting still scales it. After the stylesheet, since both are `:root`. -%>
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
<%# Turbo, Bootstrap and a Stimulus application with every controller this markup
|
|
51
52
|
names, started once under `window.Stimulus`. In the head, so a controller exists
|
|
52
53
|
before the body's first `data-controller`. -%>
|
|
53
|
-
<script type='module' src='
|
|
54
|
+
<script type='module' src='<%= Recourse.asset 'js/houseaccount.js' %>'></script>
|
|
54
55
|
</head>
|
|
55
56
|
|
|
56
57
|
<%# The reader's zone is told to the server here rather than from a page of its
|
|
@@ -17,11 +17,16 @@
|
|
|
17
17
|
bookmark_digest, Time.zone.name] -%>
|
|
18
18
|
<% cache_if cacheable_table?, key do %>
|
|
19
19
|
<% actions = resource_actions %>
|
|
20
|
+
<%# Read once for the whole table: every row of it is positioned or none is. -%>
|
|
21
|
+
<% positioned = positioned? %>
|
|
20
22
|
<div class='table-responsive'>
|
|
21
23
|
<table class='table caption-top table-hover align-middle sm:table-stacked'>
|
|
22
24
|
<thead>
|
|
23
25
|
<tr>
|
|
24
26
|
<% @recourse_headers = true %>
|
|
27
|
+
<%# The grip leads, because dragging it moves the whole row: what a reader
|
|
28
|
+
takes hold of belongs at the edge they reach for it from. -%>
|
|
29
|
+
<%= column header: position_header, class: 'recourse-actions' if positioned %>
|
|
25
30
|
<%# The square a row is kept by opens the table, ahead of the pages it
|
|
26
31
|
leads to: whether this row is one of mine, before what to do with it. -%>
|
|
27
32
|
<%= column header: bookmark_header, class: 'recourse-actions' if resource_bookmarks %>
|
|
@@ -31,10 +36,15 @@
|
|
|
31
36
|
<%= render 'row', resource_key => nil %>
|
|
32
37
|
</tr>
|
|
33
38
|
</thead>
|
|
34
|
-
<tbody
|
|
39
|
+
<tbody <%= tag.attributes data: position_data(pagy) if positioned %>>
|
|
35
40
|
<% @recourse_headers = false %>
|
|
36
41
|
<% recourses.each do |recourse| %>
|
|
37
|
-
<tr <%= tag.attributes id: Recourse.row_id(recourse),
|
|
42
|
+
<tr <%= tag.attributes id: Recourse.row_id(recourse),
|
|
43
|
+
class: bookmark_row_class(recourse),
|
|
44
|
+
data: ({ sortable_update_url: position_url(recourse) } if positioned) %>>
|
|
45
|
+
<% if positioned -%>
|
|
46
|
+
<%= column header: position_header, class: 'recourse-actions recourse-handle' do %><%= position_handle %><% end %>
|
|
47
|
+
<% end -%>
|
|
38
48
|
<% if resource_bookmarks -%>
|
|
39
49
|
<%= column header: bookmark_header, class: 'recourse-actions' do %><%= bookmark_button recourse %><% end %>
|
|
40
50
|
<% end -%>
|
|
@@ -18,6 +18,11 @@ en:
|
|
|
18
18
|
ago: "%{distance} ago"
|
|
19
19
|
all: All %{models}
|
|
20
20
|
all_values: All
|
|
21
|
+
# Why a model pointing several ways cannot be positioned without a word from it.
|
|
22
|
+
ambiguous_position: "%{model} points several ways, so which of them its position is
|
|
23
|
+
counted within is not the gem's to guess. Answer `recourse_siblings` with the rows
|
|
24
|
+
one of these is positioned among."
|
|
25
|
+
position: Reorder
|
|
21
26
|
as_calendar: Display as calendar
|
|
22
27
|
as_map: Display as map
|
|
23
28
|
as_table: Display as table
|
|
@@ -62,6 +67,9 @@ en:
|
|
|
62
67
|
has no %{model} model.
|
|
63
68
|
nested: You nested `recourses %{names}` inside `resources :%{parent}`, which does
|
|
64
69
|
not namespace what it holds. Nest it under `recourses :%{parent}` instead.
|
|
70
|
+
unindexed: "You asked for `recourses %{names}, positionable: true`, which draws no
|
|
71
|
+
index to put in order. A row is dragged from the table that lists it, so there is
|
|
72
|
+
nowhere for a drop to be reported from."
|
|
65
73
|
# What a menu with several picks reads as, the first named and the rest counted.
|
|
66
74
|
more: "%{first} + %{count} more"
|
|
67
75
|
new: New %{model}
|
|
@@ -72,6 +80,7 @@ en:
|
|
|
72
80
|
none: "No %{models}."
|
|
73
81
|
optional: Optional
|
|
74
82
|
per_page: "%{limit} per page"
|
|
83
|
+
position_updated: Position updated
|
|
75
84
|
preview: Preview
|
|
76
85
|
previous_week: Previous week
|
|
77
86
|
previous_weeks: Previous four weeks
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Reopened for where the house bundle these pages are drawn from is served from.
|
|
2
|
+
module Recourse
|
|
3
|
+
# The release of `houseaccount` they are drawn from, named once — the layout links its
|
|
4
|
+
# stylesheet and its script, and the sidebar's toggle swaps a palette out of the same
|
|
5
|
+
# release. A published version is never written again, so the bytes behind these URLs
|
|
6
|
+
# cannot move under us, and a change to the bundle reaches these pages when somebody
|
|
7
|
+
# moves this line and not before.
|
|
8
|
+
BUNDLE = '0.14.0'
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# Where that release is served from, with no trailing slash. jsdelivr's copy of the
|
|
12
|
+
# published package by default, which is what every deployed app reads and the one
|
|
13
|
+
# place all five of them draw from.
|
|
14
|
+
#
|
|
15
|
+
# A host that sets it to `''` serves the bundle itself, out of the `houseaccount`
|
|
16
|
+
# gem: its engine answers `/css`, `/js` and `/theme` from its own `public/`. That is
|
|
17
|
+
# how the bundle is worked on — a stylesheet or a controller can be tried on a real
|
|
18
|
+
# page before the version carrying it is published — and it is the whole of what
|
|
19
|
+
# such a host writes.
|
|
20
|
+
attr_accessor :assets
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@assets = "https://cdn.jsdelivr.net/npm/houseaccount@#{BUNDLE}/public"
|
|
24
|
+
|
|
25
|
+
# One file of the bundle.
|
|
26
|
+
def self.asset(path) = "#{assets}/#{path}"
|
|
27
|
+
|
|
28
|
+
# And the folder one palette is read from, which three places ask for: the layout
|
|
29
|
+
# links the one a host named, the sidebar's toggle rotates through the rest, and the
|
|
30
|
+
# layout's own script puts back whichever the reader chose last time.
|
|
31
|
+
def self.themes_path = asset('theme')
|
|
32
|
+
end
|
|
@@ -21,22 +21,22 @@ module Recourse
|
|
|
21
21
|
Recourse.ordered resource_model, resource_model.column_names - hidden_columns
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# What no table shows, less whatever the model asked to draw anyway. Each
|
|
25
|
-
#
|
|
26
|
-
# its own screens — so naming one overrules it.
|
|
24
|
+
# What no table shows, less whatever the model asked to draw anyway. Each below
|
|
25
|
+
# is a default the gem picks, and a host answers for its own screens.
|
|
27
26
|
def hidden_columns
|
|
28
27
|
columns_hidden_by_default - Array(resource_model.recourse_displayed).map(&:to_s)
|
|
29
28
|
end
|
|
30
29
|
|
|
31
30
|
# Ciphertext, the id that addresses the row, the parent a nested route already
|
|
32
31
|
# names, the timestamps and every JSON payload — what a machine keeps rather than
|
|
33
|
-
# what a row is about —
|
|
34
|
-
#
|
|
32
|
+
# what a row is about — whatever the model asked to hide, the one of these a host
|
|
33
|
+
# decides without the override above, and the column *this page* is dragged into
|
|
34
|
+
# order by, where a host positioned a second listing by one the model never keeps.
|
|
35
35
|
def columns_hidden_by_default
|
|
36
36
|
[
|
|
37
37
|
resource_model.recourse_encrypted_names, resource_model.primary_key, TIMESTAMPS,
|
|
38
38
|
resource_parent_association&.foreign_key, json_columns,
|
|
39
|
-
Recourse.hidden_columns(resource_model),
|
|
39
|
+
Recourse.hidden_columns(resource_model), position_column,
|
|
40
40
|
].flatten.compact
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
module Helpers
|
|
3
|
+
# What a table somebody positions by hand draws, and what it stops drawing.
|
|
4
|
+
module Positions
|
|
5
|
+
# The concept the grip beside each row is drawn as, named for an icon set to
|
|
6
|
+
# answer for rather than spelled as one set's own class: Bootstrap draws it
|
|
7
|
+
# `grip-vertical`, iOS as the three bars it has used for a reorder grabber
|
|
8
|
+
# since the first table view, and Material as `drag_indicator`.
|
|
9
|
+
HANDLE = :drag
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# The column these rows are dragged into order by, which the controller settled
|
|
14
|
+
# before the page began: a listing may be positioned by a column the model never
|
|
15
|
+
# nominated, and only the controller knows which.
|
|
16
|
+
def position_column = controller_assign('recourse_position')
|
|
17
|
+
|
|
18
|
+
# Whether these rows are ones a reader positions, which is the same question.
|
|
19
|
+
def positioned? = position_column.present?
|
|
20
|
+
|
|
21
|
+
# The grip's heading: the icon on the header row, since the column is as narrow
|
|
22
|
+
# as what sits in it, and the word in every other for the `data-cell` a stacked
|
|
23
|
+
# table labels itself with.
|
|
24
|
+
def position_header
|
|
25
|
+
label = t 'recourse.position'
|
|
26
|
+
return label unless @recourse_headers
|
|
27
|
+
|
|
28
|
+
icon_heading HANDLE, label
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The grip itself. A method rather than the constant, so a template reaches for
|
|
32
|
+
# nothing a view context cannot resolve.
|
|
33
|
+
def position_handle = icon_tag(HANDLE)
|
|
34
|
+
|
|
35
|
+
# What the body carries so a drop knows what it is counting from: the rows are a
|
|
36
|
+
# page rather than the table, so the index a drag reports is short by whatever
|
|
37
|
+
# the pages before it hold.
|
|
38
|
+
def position_data(pagy)
|
|
39
|
+
{
|
|
40
|
+
controller: 'sortable', sortable_offset_value: pagy&.offset.to_i,
|
|
41
|
+
sortable_message_value: t('recourse.position_updated'),
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# And what each row carries: where to write the place it ends up. One URL per
|
|
46
|
+
# row rather than one template on the body, so a host's own row partial and a
|
|
47
|
+
# nested table alike are addressed by the routes rather than by string-building.
|
|
48
|
+
def position_url(record)
|
|
49
|
+
url_for controller: "/#{controller.controller_path}/positions", action: :update,
|
|
50
|
+
"#{resource_key}_id": record
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -20,6 +20,11 @@ module Recourse
|
|
|
20
20
|
# earns it — a table with nothing worth searching may still be worth filtering,
|
|
21
21
|
# and a model whose only columns are an enum and a foreign key is the case.
|
|
22
22
|
def search_form
|
|
23
|
+
# Nor on a table somebody positions, for the reason a heading stands down: a
|
|
24
|
+
# search or a filter shortens the page, and a drop on a shortened one reports
|
|
25
|
+
# a place among the rows that are left rather than a position in the table.
|
|
26
|
+
return if positioned?
|
|
27
|
+
|
|
23
28
|
field = resource_search_field
|
|
24
29
|
filters = resource_filter_fields
|
|
25
30
|
return if field.blank? && filters.empty?
|
|
@@ -43,8 +43,16 @@ module Recourse
|
|
|
43
43
|
resource_column_title column.to_s
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
# Whether the model lets a heading sort by this column
|
|
47
|
-
|
|
46
|
+
# Whether the model lets a heading sort by this column — and whether this table
|
|
47
|
+
# is one a heading may re-sort at all. A positioned table is read in the order
|
|
48
|
+
# somebody put it in: a drop reports a row's place on the page, which is a
|
|
49
|
+
# position only while the page runs 1, 2, 3, so a heading that re-sorted it would
|
|
50
|
+
# leave the next drag renumbering by the wrong index.
|
|
51
|
+
def sortable_column?(column)
|
|
52
|
+
return false if positioned?
|
|
53
|
+
|
|
54
|
+
resource_model.ransortable_attributes.include? column.to_s
|
|
55
|
+
end
|
|
48
56
|
|
|
49
57
|
def sort_mark(column)
|
|
50
58
|
sort = resource_search.sorts.find { |one| one.name == column.to_s }
|
|
@@ -19,7 +19,7 @@ module Recourse
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# Where one palette is served from.
|
|
22
|
-
def theme_stylesheet_path(theme) = "#{Recourse
|
|
22
|
+
def theme_stylesheet_path(theme) = "#{Recourse.themes_path}/#{theme}.css"
|
|
23
23
|
|
|
24
24
|
# What the sidebar's toggle needs to rotate: every palette there is, where they
|
|
25
25
|
# are served from, and where to keep what the reader picked.
|
|
@@ -27,7 +27,7 @@ module Recourse
|
|
|
27
27
|
{
|
|
28
28
|
controller: 'scheme', action: 'scheme#rotate',
|
|
29
29
|
scheme_themes_value: Recourse::THEMES.keys.to_json,
|
|
30
|
-
scheme_path_value: Recourse
|
|
30
|
+
scheme_path_value: Recourse.themes_path,
|
|
31
31
|
scheme_storage_value: Recourse::SCHEME_STORAGE,
|
|
32
32
|
}
|
|
33
33
|
end
|
data/lib/recourse/helpers.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative 'helpers/actions'
|
|
2
|
+
require_relative 'helpers/positions'
|
|
2
3
|
require_relative 'helpers/attachments'
|
|
3
4
|
require_relative 'helpers/blobs'
|
|
4
5
|
require_relative 'helpers/bookmarks'
|
|
@@ -50,7 +51,7 @@ require_relative 'helpers/zones'
|
|
|
50
51
|
module Recourse
|
|
51
52
|
# View helpers for the pages the gem renders, and what the parts share.
|
|
52
53
|
module Helpers
|
|
53
|
-
include Actions, Attachments, Blobs, Bookmarks, Buttons, Calendars, Cards,
|
|
54
|
+
include Actions, Positions, Attachments, Blobs, Bookmarks, Buttons, Calendars, Cards,
|
|
54
55
|
Cells, Choices, Colors, Comboboxes, Constraints, Counters, Deletions, Densities,
|
|
55
56
|
Details,
|
|
56
57
|
Events, Examples, Fields, Filters, Flashes, Formats, Inputs, Kinds, Limits, Maps,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require 'active_support'
|
|
2
|
+
|
|
3
|
+
module Recourse
|
|
4
|
+
# Keeps a positioned table numbered 1, 2, 3: a new row lands last among its own, and
|
|
5
|
+
# the gap closes behind one that goes. Both are the gem's own screens' business. The
|
|
6
|
+
# form it draws never asks for a position — a reader sets one by dragging a row, and
|
|
7
|
+
# a box beside the handle would be a second way to say a thing already said — so a
|
|
8
|
+
# column the schema insists on has to be filled from somewhere. And what a drop
|
|
9
|
+
# reports is a row's place on the page, which means a position only while the two run
|
|
10
|
+
# together: leave a hole and every later drag lands beside where it was aimed.
|
|
11
|
+
#
|
|
12
|
+
# Every model carries this, and it acts on the ones that keep a `position`. Nothing
|
|
13
|
+
# to include and nothing to remember: the column is the declaration, so a table with
|
|
14
|
+
# one is numbered whether its rows are ever dragged or only ever made in a console.
|
|
15
|
+
# A host maintaining its own order says `def recourse_position = nil` and gets
|
|
16
|
+
# neither callback. What it writes otherwise is `recourse_siblings`, and only where
|
|
17
|
+
# more than one key could be the parent.
|
|
18
|
+
module Positionable
|
|
19
|
+
extend ActiveSupport::Concern
|
|
20
|
+
|
|
21
|
+
included do
|
|
22
|
+
before_validation :recourse_place_last, on: :create
|
|
23
|
+
after_destroy :recourse_close_gap
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The rows this one's position is counted among: those under the same parent, or
|
|
27
|
+
# the whole table where the model points nowhere. The parent is named rather than
|
|
28
|
+
# its key, so a key that names no one table is answered by the record it holds and
|
|
29
|
+
# the two halves are matched together.
|
|
30
|
+
#
|
|
31
|
+
# Override where two keys could be the parent and only one is: a picture belongs to
|
|
32
|
+
# a department and to the file it shows, and its place is among the department's.
|
|
33
|
+
def recourse_siblings
|
|
34
|
+
name = recourse_position_reference
|
|
35
|
+
return self.class.all unless name
|
|
36
|
+
|
|
37
|
+
self.class.where name => public_send(name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# A model pointing one way is positioned within what it points at, and one pointing
|
|
43
|
+
# nowhere is a whole table in one order. Which of several is the parent is the
|
|
44
|
+
# model's to say: guessing would number a picture among every picture there is,
|
|
45
|
+
# quietly and at the first write.
|
|
46
|
+
def recourse_position_reference
|
|
47
|
+
references = self.class.reflect_on_all_associations :belongs_to
|
|
48
|
+
return if references.empty?
|
|
49
|
+
raise Error, I18n.t('recourse.ambiguous_position', model: self.class.name) unless
|
|
50
|
+
references.one?
|
|
51
|
+
|
|
52
|
+
references.first.name
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def recourse_place_last
|
|
56
|
+
column = Recourse.position_column self.class
|
|
57
|
+
return unless column && self[column].nil?
|
|
58
|
+
|
|
59
|
+
self[column] = recourse_siblings.maximum(column).to_i + 1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def recourse_close_gap
|
|
63
|
+
column = Recourse.position_column self.class
|
|
64
|
+
return unless column
|
|
65
|
+
|
|
66
|
+
Positioning.new(recourse_siblings, column).close self[column]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Beside `Recoursive`'s own: a model is positioned by keeping the column, and there is no
|
|
72
|
+
# second place to say it again.
|
|
73
|
+
ActiveSupport.on_load :active_record do
|
|
74
|
+
include Recourse::Positionable
|
|
75
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Recourse
|
|
2
|
+
# Moves one row of a positioned table to a place in it, and closes the gap it leaves
|
|
3
|
+
# by shifting whatever it displaced one step the other way.
|
|
4
|
+
class Positioning
|
|
5
|
+
# The rows a position is counted within, and the column it is counted in. A
|
|
6
|
+
# relation rather than a model, because which rows those are is the route's answer
|
|
7
|
+
# on a drag and the record's on a delete, and each caller has already asked.
|
|
8
|
+
def initialize(relation, column)
|
|
9
|
+
@relation = relation
|
|
10
|
+
@column = column
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Puts the record at `position`, counting from one and never past the end — a drag
|
|
14
|
+
# reports where a row was dropped, and a page is not the whole table. That the two
|
|
15
|
+
# agree at all is what `Positionable` is for: a drop names a row's place on the page,
|
|
16
|
+
# which is a position only while the table runs 1, 2, 3 with no gaps in it.
|
|
17
|
+
def move(record, position)
|
|
18
|
+
target = position.to_i.clamp 1, @relation.count
|
|
19
|
+
current = record[@column]
|
|
20
|
+
return if target == current
|
|
21
|
+
|
|
22
|
+
@relation.transaction do
|
|
23
|
+
displace record, current, target
|
|
24
|
+
record.update! @column => target
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Closes the gap a row left behind it: whatever stood after it moves one step up.
|
|
29
|
+
# The row itself is already gone, so the block starts where it was standing.
|
|
30
|
+
def close(from)
|
|
31
|
+
@relation.where(@column => from..).update_all shift(-1)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Everything between where the row was and where it is going moves one step
|
|
37
|
+
# towards the space it left: moving up, the block beneath it shifts down; moving
|
|
38
|
+
# down, the block above it shifts up. That is what the two ranges say, the
|
|
39
|
+
# half-open one compensating for the row itself being left out of the count.
|
|
40
|
+
def displace(record, current, target)
|
|
41
|
+
delta = current <=> target
|
|
42
|
+
between = delta.positive? ? target...current : current..target
|
|
43
|
+
|
|
44
|
+
@relation.excluding(record).where(@column => between).update_all shift(delta)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# One statement however many rows it moves, and it touches them in the same
|
|
48
|
+
# breath: `update_all` runs no callbacks, so nothing else would tell the relation
|
|
49
|
+
# a table caches on that its version has changed, and the old order would be
|
|
50
|
+
# served straight back.
|
|
51
|
+
def shift(delta)
|
|
52
|
+
column = @relation.klass.connection.quote_column_name @column
|
|
53
|
+
moved = "#{column} = #{column} + ?"
|
|
54
|
+
|
|
55
|
+
return [moved, delta] unless @relation.klass.column_names.include? 'updated_at'
|
|
56
|
+
|
|
57
|
+
["#{moved}, updated_at = ?", delta, Time.current]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Reopened for the order somebody put a table's rows in, which is neither the order
|
|
2
|
+
# they were made in nor one a heading sorts them into.
|
|
3
|
+
module Recourse
|
|
4
|
+
# The column that order is kept in. The whole of the opt-in, the way a
|
|
5
|
+
# `google_place_id` is the whole of a map's: a table keeping a place in each row has
|
|
6
|
+
# said its rows are in an order, and there is nothing else for a model to declare.
|
|
7
|
+
POSITION_COLUMN = 'position'
|
|
8
|
+
|
|
9
|
+
# Whether a model's rows are positioned by hand. Asked of the type the model reports,
|
|
10
|
+
# as a calendar asks about the two ends of its events — a `position` holding a job
|
|
11
|
+
# title is a word about the row rather than a place among rows, and a table of those
|
|
12
|
+
# is one this must leave alone.
|
|
13
|
+
#
|
|
14
|
+
# The guard earns more here than it does there. A map and a calendar only read, so a
|
|
15
|
+
# column mistaken for one costs a link nobody follows; positioning writes, and it
|
|
16
|
+
# writes at the first save — a new row takes a number and a deleted one closes the
|
|
17
|
+
# gap behind it — so a wrong guess here is a column overwritten rather than a page
|
|
18
|
+
# drawn wrong.
|
|
19
|
+
def self.positionable?(model)
|
|
20
|
+
model.column_names.include?(POSITION_COLUMN) &&
|
|
21
|
+
model.type_for_attribute(POSITION_COLUMN).type == :integer
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The column a model is positioned by, or nil where nobody positions it. The model's
|
|
25
|
+
# own word rather than the convention, since that is what a host overrides — and nil
|
|
26
|
+
# for a page whose rows answer no model at all.
|
|
27
|
+
def self.position_column(model)
|
|
28
|
+
model.recourse_position&.to_s if model.respond_to? :recourse_position
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The same as a list, for a caller subtracting it from a set of column names: a
|
|
32
|
+
# position is nobody's to type and no table's to draw, since the row's own place in
|
|
33
|
+
# the table already says it.
|
|
34
|
+
def self.position_columns(model) = Array(position_column(model))
|
|
35
|
+
|
|
36
|
+
# Whether *this* page is one the positioning means anything on, which is the same
|
|
37
|
+
# question as whether the rows it lists are the rows a position is counted within.
|
|
38
|
+
#
|
|
39
|
+
# The association rather than the record it found: a key is what makes every row on
|
|
40
|
+
# the page share the parent, and `parent_columns` narrows the relation by the same
|
|
41
|
+
# one. A model nothing points away from — a flat list — is its own whole table, so
|
|
42
|
+
# its index is that level too. What is neither is a page listing every row across
|
|
43
|
+
# every parent, where the positions run 1, 2, 3, 1, 2, 3 and mean nothing side by
|
|
44
|
+
# side: that page sorts and searches like any other, and offers no handle.
|
|
45
|
+
def self.positioned?(model, association)
|
|
46
|
+
return false unless position_column model
|
|
47
|
+
|
|
48
|
+
association.present? || model.recourse_references.empty?
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/recourse/recoursive.rb
CHANGED
|
@@ -62,9 +62,25 @@ module Recourse
|
|
|
62
62
|
# belongs_to it can follow, since each cell naming one would be a query of its own.
|
|
63
63
|
def recourse_includes = recourse_references.map(&:name)
|
|
64
64
|
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
|
|
65
|
+
# The column this model's rows are positioned by — dragged into an order somebody
|
|
66
|
+
# chose rather than sorted into one — or nil for a table nobody positions. The
|
|
67
|
+
# column is the whole of the declaration, so a model keeping a `position` has said
|
|
68
|
+
# this already and says nothing here.
|
|
69
|
+
#
|
|
70
|
+
# Overridden two ways. `def recourse_position = :ordering` where the column is
|
|
71
|
+
# named otherwise, and `def recourse_position = nil` where a host keeps the order
|
|
72
|
+
# itself: that one is the way out of the convention, and it takes the handle off
|
|
73
|
+
# the table and both callbacks off the model together.
|
|
74
|
+
def recourse_position
|
|
75
|
+
Recourse::POSITION_COLUMN if Recourse.positionable? self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# How the index sorts its rows, in any shape `order` accepts. The column it is
|
|
79
|
+
# positioned by where there is one — the order a table is read in and the order
|
|
80
|
+
# somebody put it in are one fact, and a table read in another order would have a
|
|
81
|
+
# drop reporting a place that is no position at all — and by id otherwise, which is
|
|
82
|
+
# the one column every table has and the order rows were created in.
|
|
83
|
+
def recourse_order = recourse_position&.to_sym || :id
|
|
68
84
|
|
|
69
85
|
# What a column is for, said where the table itself documents it — a form draws it
|
|
70
86
|
# under the field that sets it. Read from the schema, which is the one thing here no
|
|
@@ -1,27 +1,42 @@
|
|
|
1
1
|
module Recourse
|
|
2
2
|
module Routes
|
|
3
3
|
# What a `recourses` block draws around whatever the host wrote inside it: the
|
|
4
|
-
# square that keeps one of its rows.
|
|
4
|
+
# square that keeps one of its rows, and the place one of them holds.
|
|
5
5
|
module Nested
|
|
6
6
|
private
|
|
7
7
|
|
|
8
|
-
# What a resource holds: the square that keeps one of its rows,
|
|
9
|
-
#
|
|
10
|
-
# every nested page relies on.
|
|
11
|
-
def draw_within(keepable, block)
|
|
8
|
+
# What a resource holds: the square that keeps one of its rows, the place a row
|
|
9
|
+
# holds in a table somebody positioned, and whatever the host's own block declared —
|
|
10
|
+
# each under the resource's own module, which is what every nested page relies on.
|
|
11
|
+
def draw_within(keepable, positionable, block)
|
|
12
|
+
addressable = addressable_rows?
|
|
13
|
+
|
|
12
14
|
scope module: parent_resource.name do
|
|
13
|
-
draw_bookmark if keepable &&
|
|
15
|
+
draw_bookmark if keepable && addressable
|
|
16
|
+
draw_position if positionable && addressable
|
|
14
17
|
instance_exec(&block) if block
|
|
15
18
|
end
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
# Whether this resource has rows to address one at a time. A bookmark names one
|
|
19
|
-
# row,
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
22
|
+
# row and a position names one row, so neither means anything for a name with no
|
|
23
|
+
# class behind it at all — and one was drawn anyway, at
|
|
24
|
+
# `/placeholders/:placeholder_id/bookmark`, where nothing linked to it and
|
|
25
|
+
# anything reaching it raised. Asked of the constant rather than the class:
|
|
26
|
+
# loading a model while the routes draw is what Rails 8.2 warns about.
|
|
23
27
|
def addressable_rows? = Object.const_defined? Recourse.model_name(parent_resource.name)
|
|
24
28
|
|
|
29
|
+
# The place a row of a positioned table holds, at `/teams/5/position`. Recorded
|
|
30
|
+
# nowhere, for the reason the bookmark gives: a tab and a bare-action button both
|
|
31
|
+
# look under a resource, and this is neither. Drawn wherever an index is —
|
|
32
|
+
# whether a model keeps a position column is a question for a request, and asking
|
|
33
|
+
# it here would reach for a database before the routes are even finished.
|
|
34
|
+
def draw_position
|
|
35
|
+
path = [current_module, 'positions'].compact.join '/'
|
|
36
|
+
Controllers.define_missing(path) { PositionsController }
|
|
37
|
+
resource :position, only: :update
|
|
38
|
+
end
|
|
39
|
+
|
|
25
40
|
# The row a table's bookmark square writes: one record kept by whoever is looking,
|
|
26
41
|
# at `/places/5/bookmark`. Deliberately not recorded through `Recourse.nest` — it
|
|
27
42
|
# would sit directly under the resource, where a tab and a bare-action button both
|
data/lib/recourse/routes.rb
CHANGED
|
@@ -24,10 +24,15 @@ module Recourse
|
|
|
24
24
|
keepable = Recourse.bookmarks? && parent_resource.nil?
|
|
25
25
|
|
|
26
26
|
names.each { |name| declare_resource name }
|
|
27
|
+
# The host's word, and taken out before Rails sees the rest: `resources` would
|
|
28
|
+
# refuse a keyword it does not know.
|
|
29
|
+
positionable = options.delete(:positionable) { false }
|
|
27
30
|
options = default_nested_actions options
|
|
28
|
-
|
|
31
|
+
refuse_unindexed_positioning names, positionable, options
|
|
29
32
|
|
|
30
|
-
resources(*names, **options)
|
|
33
|
+
return resources(*names, **options) unless block || keepable || positionable
|
|
34
|
+
|
|
35
|
+
resources(*names, **options) { draw_within keepable, positionable, block }
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
private
|
|
@@ -40,6 +45,23 @@ module Recourse
|
|
|
40
45
|
Controllers.define_missing path
|
|
41
46
|
end
|
|
42
47
|
|
|
48
|
+
# A table is put in order from its index and from nowhere else, so asking for the
|
|
49
|
+
# route without drawing one is a host saying two things that cannot both hold.
|
|
50
|
+
def refuse_unindexed_positioning(names, positionable, options)
|
|
51
|
+
return unless positionable && !indexed?(options)
|
|
52
|
+
|
|
53
|
+
raise Error, I18n.t('recourse.unindexed', names: names.map(&:inspect).join(', '))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Whether `index` survived the `only:` or `except:` the host wrote. Neither of
|
|
57
|
+
# them is the common case, and both name the action plainly.
|
|
58
|
+
def indexed?(options)
|
|
59
|
+
return Array(options[:except]).map(&:to_sym).exclude?(:index) if options.key? :except
|
|
60
|
+
return Array(options[:only]).map(&:to_sym).include?(:index) if options.key? :only
|
|
61
|
+
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
43
65
|
def default_nested_actions(options)
|
|
44
66
|
return options if !parent_resource || options.key?(:only) || options.key?(:except)
|
|
45
67
|
|
data/lib/recourse/search.rb
CHANGED
|
@@ -14,8 +14,14 @@ module Recourse
|
|
|
14
14
|
# it narrowed to rather than around it — the model is still what answers for the
|
|
15
15
|
# order, the eager loads and the allowlist, and a relation knows its own.
|
|
16
16
|
#
|
|
17
|
-
|
|
17
|
+
# `positioned` is the column this page is dragged into order by, or nil where it is
|
|
18
|
+
# no such page. The controller's word rather than a question asked here: whether a
|
|
19
|
+
# place among rows means anything depends on the level the page was reached at,
|
|
20
|
+
# which the route knows and a relation does not — and a listing may be positioned by
|
|
21
|
+
# a column the model never keeps, which only its own controller knows.
|
|
22
|
+
def initialize(relation, params, positioned: nil)
|
|
18
23
|
@model = relation.klass
|
|
24
|
+
@positioned = positioned
|
|
19
25
|
@query = relation.ransack conditions(params)
|
|
20
26
|
end
|
|
21
27
|
|
|
@@ -33,19 +39,27 @@ module Recourse
|
|
|
33
39
|
# A heading's sort or the model's own order, each column with its empty rows last.
|
|
34
40
|
# Ransack has ordered the relation already where a heading asked, but as the bare
|
|
35
41
|
# column; the same sort is written again here so it ends the way every order does.
|
|
42
|
+
#
|
|
43
|
+
# And on a page somebody positions, the column they positioned it by — which is the
|
|
44
|
+
# model's own order for a table positioned by the column it keeps, and is not for the
|
|
45
|
+
# second listing of one, where a host named another. The order a table is read in
|
|
46
|
+
# and the order somebody put it in have to be the same order, or a drop reports a
|
|
47
|
+
# place that is no position at all.
|
|
36
48
|
def ordering
|
|
37
49
|
sorts = @query.sorts.map { |sort| sort.attr.public_send(sort.dir).nulls_last }
|
|
38
50
|
return sorts if sorts.any?
|
|
39
51
|
|
|
40
|
-
kept_first + Recourse.nulls_last(@model, @model.recourse_order)
|
|
52
|
+
kept_first + Recourse.nulls_last(@model, @positioned&.to_sym || @model.recourse_order)
|
|
41
53
|
end
|
|
42
54
|
|
|
43
55
|
# The rows this viewer has kept, ahead of whatever the model orders by — but only
|
|
44
56
|
# where nobody clicked a heading, which is the same word `recourse_order` answers
|
|
45
57
|
# to. A semi-join rather than an outer one: it cannot multiply a row, and it
|
|
46
|
-
# leaves the count pagy runs over this relation well-formed.
|
|
58
|
+
# leaves the count pagy runs over this relation well-formed. Never on a positioned
|
|
59
|
+
# table, where it would put one reader's kept rows ahead of the order everybody
|
|
60
|
+
# else set by hand — and leave the numbers no longer running 1, 2, 3 down the page.
|
|
47
61
|
def kept_first
|
|
48
|
-
reflection = Recourse.bookmarks_for @model
|
|
62
|
+
reflection = Recourse.bookmarks_for @model unless @positioned
|
|
49
63
|
return [] unless reflection
|
|
50
64
|
|
|
51
65
|
kept = Recourse.bookmarks_of(reflection).select reflection.foreign_key
|
|
@@ -60,8 +74,12 @@ module Recourse
|
|
|
60
74
|
# is dropped, since `IN ()` would match no row rather than every one.
|
|
61
75
|
def conditions(params)
|
|
62
76
|
# `?q=anything` reaches here as a String rather than as parameters of its own,
|
|
63
|
-
# and a search nobody asked for reaches here as nil. Neither is a condition
|
|
64
|
-
|
|
77
|
+
# and a search nobody asked for reaches here as nil. Neither is a condition — and
|
|
78
|
+
# neither is anything at all where the table is positioned by hand, since a search
|
|
79
|
+
# or a filter shortens the page and a drop on a shortened one reports a place
|
|
80
|
+
# among the rows that are left. Refused here rather than by leaving the form off
|
|
81
|
+
# the page, because an address is typed as readily as it is clicked.
|
|
82
|
+
return {} if @positioned || !params.is_a?(ActionController::Parameters)
|
|
65
83
|
|
|
66
84
|
params.to_unsafe_h.filter_map do |key, value|
|
|
67
85
|
value = list_values value if key.match? LIST_PREDICATES
|
data/lib/recourse/themes.rb
CHANGED
|
@@ -22,11 +22,6 @@ module Recourse
|
|
|
22
22
|
tokyo_night: %i[blue brown cyan gray green orange pink purple red yellow],
|
|
23
23
|
}.freeze
|
|
24
24
|
|
|
25
|
-
# Where the palettes are served from, named once: the layout links one, the layout's
|
|
26
|
-
# own script puts back the one a reader chose, and the sidebar's controller swaps it.
|
|
27
|
-
# The version is the one the layout links its stylesheet and script at.
|
|
28
|
-
THEMES_PATH = 'https://cdn.jsdelivr.net/npm/houseaccount@0.13.0/public/theme'
|
|
29
|
-
|
|
30
25
|
# Where a reader's chosen palette and mode are kept in their browser. Named once for
|
|
31
26
|
# the same reason: the sidebar's controller writes it and the layout reads it back.
|
|
32
27
|
SCHEME_STORAGE = 'recourse-scheme'
|
data/lib/recourse/titles.rb
CHANGED
|
@@ -26,14 +26,16 @@ module Recourse
|
|
|
26
26
|
model_title model(name)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
# The same for a name that may
|
|
30
|
-
#
|
|
31
|
-
#
|
|
29
|
+
# The same for a name that may name no model at all: an action drawn under a record
|
|
30
|
+
# still needs a word for its button, and `pause` is a verb this app never made a
|
|
31
|
+
# class for. A word that does resolve is not a model either where what it found has
|
|
32
|
+
# no `model_name` — `Message` may be a host's own class for gathering rows rather
|
|
33
|
+
# than a table of them, and asking it would raise rather than read.
|
|
32
34
|
def known_title(name)
|
|
33
35
|
segment = name.to_s.split('/').last
|
|
34
36
|
model = segment.classify.safe_constantize
|
|
35
37
|
|
|
36
|
-
model ? model_title(model) : segment.humanize
|
|
38
|
+
model.respond_to?(:model_name) ? model_title(model) : segment.humanize
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
# And the singular of that, for the button a bare action earns: `Add sweep` is one
|
data/lib/recourse/version.rb
CHANGED
data/lib/recourse.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'ransack'
|
|
|
4
4
|
require 'unicon'
|
|
5
5
|
|
|
6
6
|
require_relative 'recourse/version'
|
|
7
|
+
require_relative 'recourse/assets'
|
|
7
8
|
require_relative 'recourse/attachments'
|
|
8
9
|
require_relative 'recourse/blobs'
|
|
9
10
|
require_relative 'recourse/bookmarks'
|
|
@@ -18,8 +19,11 @@ require_relative 'recourse/helpers'
|
|
|
18
19
|
require_relative 'recourse/limits'
|
|
19
20
|
require_relative 'recourse/maps'
|
|
20
21
|
require_relative 'recourse/orders'
|
|
22
|
+
require_relative 'recourse/positions'
|
|
23
|
+
require_relative 'recourse/positioning'
|
|
21
24
|
require_relative 'recourse/writes'
|
|
22
25
|
require_relative 'recourse/zones'
|
|
26
|
+
require_relative 'recourse/positionable'
|
|
23
27
|
require_relative 'recourse/broadcasting'
|
|
24
28
|
require_relative 'recourse/recoursive'
|
|
25
29
|
require_relative 'recourse/registry'
|
|
@@ -36,9 +40,8 @@ module Recourse
|
|
|
36
40
|
# them. Named once: three places ask which columns these are.
|
|
37
41
|
TIMESTAMPS = %w[created_at updated_at].freeze
|
|
38
42
|
|
|
39
|
-
# The shapes a page of rows takes besides the table, by the format each is asked
|
|
40
|
-
#
|
|
41
|
-
# a template of its own and all three answer to one controller action.
|
|
43
|
+
# The shapes a page of rows takes besides the table, by the format each is asked for.
|
|
44
|
+
# Both are the same HTML drawn another way: no template of their own, and one action.
|
|
42
45
|
SHAPES = %i[map cal].freeze
|
|
43
46
|
|
|
44
47
|
class << self
|
|
@@ -59,12 +62,14 @@ module Recourse
|
|
|
59
62
|
hidden_columns(model)
|
|
60
63
|
end
|
|
61
64
|
|
|
62
|
-
# Columns no screen shows: whatever the model asked to hide through
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
# to read out
|
|
65
|
+
# Columns no screen shows: whatever the model asked to hide through `recourse_hidden`
|
|
66
|
+
# — one name or a list, taken either way — the column Rails reserves for single table
|
|
67
|
+
# inheritance, and the place a row holds where somebody positioned the table. A class
|
|
68
|
+
# name is machinery rather than something to read out, and a position is set by
|
|
69
|
+
# dragging the row rather than typed beside it.
|
|
66
70
|
def self.hidden_columns(model)
|
|
67
|
-
Array(model.recourse_hidden).map(&:to_s) +
|
|
71
|
+
Array(model.recourse_hidden).map(&:to_s) +
|
|
72
|
+
[model.inheritance_column, *position_columns(model)]
|
|
68
73
|
end
|
|
69
74
|
|
|
70
75
|
# The names a column is validated under: its own, and — where it is a foreign key
|
|
@@ -75,17 +80,16 @@ module Recourse
|
|
|
75
80
|
[column, column.delete_suffix('_id')].uniq
|
|
76
81
|
end
|
|
77
82
|
|
|
78
|
-
# The model a resource is named after. A controller the gem defined has nothing
|
|
79
|
-
#
|
|
80
|
-
#
|
|
83
|
+
# The model a resource is named after. A controller the gem defined has nothing else
|
|
84
|
+
# to go on, so a name resolving to no model is a routes file to fix rather than a
|
|
85
|
+
# `NameError` from somewhere inside a view.
|
|
81
86
|
def self.model(name)
|
|
82
87
|
model?(name) || raise(Error, I18n.t('recourse.missing_model', name:, model: model_name(name)))
|
|
83
88
|
end
|
|
84
89
|
|
|
85
90
|
# The same, answering nil where there is no such model rather than raising. A bare
|
|
86
|
-
# action is a verb — `recourses :sweeps, only: :create` —
|
|
87
|
-
#
|
|
88
|
-
# and not an accusation.
|
|
91
|
+
# action is a verb — `recourses :sweeps, only: :create` — labelled from the path alone,
|
|
92
|
+
# so whether a name has a model behind it is a question and not an accusation.
|
|
89
93
|
def self.model?(name) = model_name(name).safe_constantize
|
|
90
94
|
|
|
91
95
|
# A namespaced resource is `admin/sources`, and the model it lists is a Source.
|
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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- claudiob
|
|
@@ -128,12 +128,14 @@ files:
|
|
|
128
128
|
- app/controllers/concerns/recourse/paging.rb
|
|
129
129
|
- app/controllers/concerns/recourse/parent_naming.rb
|
|
130
130
|
- app/controllers/concerns/recourse/parent_resolution.rb
|
|
131
|
+
- app/controllers/concerns/recourse/positioned.rb
|
|
131
132
|
- app/controllers/concerns/recourse/reference_resolution.rb
|
|
132
133
|
- app/controllers/concerns/recourse/resource_resolution.rb
|
|
133
134
|
- app/controllers/concerns/recourse/weeks.rb
|
|
134
135
|
- app/controllers/concerns/recourse/zoning.rb
|
|
135
136
|
- app/controllers/recourse/base_controller.rb
|
|
136
137
|
- app/controllers/recourse/bookmarks_controller.rb
|
|
138
|
+
- app/controllers/recourse/positions_controller.rb
|
|
137
139
|
- app/controllers/recourses_controller.rb
|
|
138
140
|
- app/views/layouts/recourses.html.erb
|
|
139
141
|
- app/views/recourses/_breadcrumb.html.erb
|
|
@@ -161,6 +163,7 @@ files:
|
|
|
161
163
|
- app/views/recourses/show.html.erb
|
|
162
164
|
- config/locales/recourse.en.yml
|
|
163
165
|
- lib/recourse.rb
|
|
166
|
+
- lib/recourse/assets.rb
|
|
164
167
|
- lib/recourse/attachments.rb
|
|
165
168
|
- lib/recourse/blobs.rb
|
|
166
169
|
- lib/recourse/bookmarks.rb
|
|
@@ -202,6 +205,7 @@ files:
|
|
|
202
205
|
- lib/recourse/helpers/navigation.rb
|
|
203
206
|
- lib/recourse/helpers/parents.rb
|
|
204
207
|
- lib/recourse/helpers/pictures.rb
|
|
208
|
+
- lib/recourse/helpers/positions.rb
|
|
205
209
|
- lib/recourse/helpers/previews.rb
|
|
206
210
|
- lib/recourse/helpers/references.rb
|
|
207
211
|
- lib/recourse/helpers/refreshes.rb
|
|
@@ -224,6 +228,9 @@ files:
|
|
|
224
228
|
- lib/recourse/limits.rb
|
|
225
229
|
- lib/recourse/maps.rb
|
|
226
230
|
- lib/recourse/orders.rb
|
|
231
|
+
- lib/recourse/positionable.rb
|
|
232
|
+
- lib/recourse/positioning.rb
|
|
233
|
+
- lib/recourse/positions.rb
|
|
227
234
|
- lib/recourse/recoursive.rb
|
|
228
235
|
- lib/recourse/registry.rb
|
|
229
236
|
- lib/recourse/routes.rb
|