pagy 8.0.0 → 9.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/apps/calendar.ru +745 -0
- data/{lib/apps → apps}/demo.ru +35 -52
- data/apps/keyset_ar.ru +236 -0
- data/apps/keyset_s.ru +238 -0
- data/{lib/apps → apps}/rails.ru +40 -33
- data/{lib/apps → apps}/repro.ru +33 -24
- data/apps/tmp/calendar.sqlite3 +0 -0
- data/apps/tmp/calendar.sqlite3-shm +0 -0
- data/apps/tmp/calendar.sqlite3-wal +0 -0
- data/apps/tmp/local_secret.txt +1 -0
- data/apps/tmp/pagy-keyset-ar.sqlite3 +0 -0
- data/apps/tmp/pagy-keyset-ar.sqlite3-shm +0 -0
- data/apps/tmp/pagy-keyset-ar.sqlite3-wal +0 -0
- data/apps/tmp/pagy-keyset-s.sqlite3 +0 -0
- data/{lib/bin → bin}/pagy +36 -17
- data/{lib/config → config}/pagy.rb +37 -68
- data/javascripts/pagy-module.js +100 -0
- data/javascripts/pagy.js +4 -0
- data/javascripts/pagy.min.js +4 -0
- data/javascripts/pagy.min.js.map +10 -0
- data/javascripts/pagy.mjs +100 -0
- data/lib/optimist.rb +1 -1
- data/lib/pagy/b64.rb +33 -0
- data/lib/pagy/backend.rb +24 -15
- data/lib/pagy/calendar/day.rb +5 -4
- data/lib/pagy/calendar/month.rb +5 -4
- data/lib/pagy/calendar/quarter.rb +5 -4
- data/lib/pagy/calendar/unit.rb +103 -0
- data/lib/pagy/calendar/week.rb +4 -4
- data/lib/pagy/calendar/year.rb +5 -4
- data/lib/pagy/calendar.rb +55 -99
- data/lib/pagy/console.rb +2 -2
- data/lib/pagy/countless.rb +17 -16
- data/lib/pagy/extras/arel.rb +8 -10
- data/lib/pagy/extras/array.rb +4 -6
- data/lib/pagy/extras/bootstrap.rb +7 -7
- data/lib/pagy/extras/bulma.rb +13 -9
- data/lib/pagy/extras/calendar.rb +35 -6
- data/lib/pagy/extras/countless.rb +7 -14
- data/lib/pagy/extras/elasticsearch_rails.rb +15 -15
- data/lib/pagy/extras/gearbox.rb +36 -35
- data/lib/pagy/extras/headers.rb +26 -25
- data/lib/pagy/extras/i18n.rb +1 -1
- data/lib/pagy/extras/js_tools.rb +12 -9
- data/lib/pagy/extras/jsonapi.rb +27 -17
- data/lib/pagy/extras/keyset.rb +26 -0
- data/lib/pagy/extras/limit.rb +63 -0
- data/lib/pagy/extras/meilisearch.rb +11 -11
- data/lib/pagy/extras/metadata.rb +7 -3
- data/lib/pagy/extras/overflow.rb +9 -8
- data/lib/pagy/extras/pagy.rb +18 -18
- data/lib/pagy/extras/searchkick.rb +11 -11
- data/lib/pagy/extras/size.rb +40 -0
- data/lib/pagy/extras/standalone.rb +8 -8
- data/lib/pagy/extras/trim.rb +3 -3
- data/lib/pagy/frontend.rb +39 -37
- data/lib/pagy/i18n.rb +1 -1
- data/lib/pagy/keyset/active_record.rb +38 -0
- data/lib/pagy/keyset/sequel.rb +51 -0
- data/lib/pagy/keyset.rb +99 -0
- data/lib/pagy/url_helpers.rb +11 -11
- data/lib/pagy.rb +96 -120
- data/{lib/locales → locales}/ar.yml +9 -10
- data/{lib/locales → locales}/be.yml +2 -2
- data/{lib/locales → locales}/bg.yml +2 -2
- data/{lib/locales → locales}/bs.yml +2 -2
- data/{lib/locales → locales}/ca.yml +5 -7
- data/{lib/locales → locales}/ckb.yml +2 -2
- data/{lib/locales → locales}/cs.yml +2 -2
- data/{lib/locales → locales}/da.yml +5 -7
- data/{lib/locales → locales}/de.yml +2 -2
- data/{lib/locales → locales}/en.yml +2 -2
- data/{lib/locales → locales}/es.yml +2 -2
- data/{lib/locales → locales}/fr.yml +2 -2
- data/{lib/locales → locales}/hr.yml +2 -2
- data/{lib/locales → locales}/id.yml +2 -2
- data/{lib/locales → locales}/it.yml +2 -2
- data/{lib/locales → locales}/ja.yml +2 -2
- data/{lib/locales → locales}/km.yml +2 -2
- data/{lib/locales → locales}/ko.yml +3 -5
- data/{lib/locales → locales}/nb.yml +2 -2
- data/{lib/locales → locales}/nl.yml +2 -2
- data/{lib/locales → locales}/nn.yml +2 -2
- data/{lib/locales → locales}/pl.yml +2 -2
- data/{lib/locales → locales}/pt-BR.yml +2 -2
- data/{lib/locales → locales}/pt.yml +2 -2
- data/{lib/locales → locales}/ru.yml +7 -9
- data/{lib/locales → locales}/sr.yml +2 -2
- data/{lib/locales → locales}/sv-SE.yml +2 -2
- data/{lib/locales → locales}/sv.yml +2 -2
- data/{lib/locales → locales}/sw.yml +2 -2
- data/{lib/locales → locales}/ta.yml +2 -2
- data/{lib/locales → locales}/tr.yml +2 -2
- data/{lib/locales → locales}/uk.yml +2 -2
- data/{lib/locales → locales}/vi.yml +2 -2
- data/{lib/locales → locales}/zh-CN.yml +2 -2
- data/{lib/locales → locales}/zh-HK.yml +2 -2
- data/{lib/locales → locales}/zh-TW.yml +2 -2
- data/pkg/pagy-9.0.0.gem +0 -0
- metadata +75 -70
- data/lib/apps/calendar.ru +0 -2196
- data/lib/javascripts/pagy-dev.js +0 -112
- data/lib/javascripts/pagy-module.js +0 -111
- data/lib/javascripts/pagy.js +0 -1
- data/lib/pagy/calendar/helper.rb +0 -65
- data/lib/pagy/extras/foundation.rb +0 -93
- data/lib/pagy/extras/items.rb +0 -64
- data/lib/pagy/extras/materialize.rb +0 -97
- data/lib/pagy/extras/semantic.rb +0 -91
- data/lib/pagy/extras/uikit.rb +0 -96
- /data/{lib/javascripts/pagy-module.d.ts → javascripts/pagy.d.ts} +0 -0
- /data/{lib/stylesheets → stylesheets}/pagy.css +0 -0
- /data/{lib/stylesheets → stylesheets}/pagy.scss +0 -0
- /data/{lib/stylesheets → stylesheets}/pagy.tailwind.css +0 -0
@@ -23,11 +23,11 @@ class Pagy # :nodoc:
|
|
23
23
|
# Additions for the Pagy class
|
24
24
|
module PagyExtension
|
25
25
|
# Create a Pagy object from a Searchkick::Results object
|
26
|
-
def new_from_searchkick(results, vars
|
27
|
-
vars[:
|
26
|
+
def new_from_searchkick(results, **vars)
|
27
|
+
vars[:limit] = results.options[:per_page]
|
28
28
|
vars[:page] = results.options[:page]
|
29
29
|
vars[:count] = results.total_count
|
30
|
-
new(vars)
|
30
|
+
new(**vars)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
Pagy.extend PagyExtension
|
@@ -37,17 +37,17 @@ class Pagy # :nodoc:
|
|
37
37
|
private
|
38
38
|
|
39
39
|
# Return Pagy object and results
|
40
|
-
def pagy_searchkick(pagy_search_args, vars
|
40
|
+
def pagy_searchkick(pagy_search_args, **vars)
|
41
41
|
model, term, options, block, *called = pagy_search_args
|
42
42
|
vars = pagy_searchkick_get_vars(nil, vars)
|
43
|
-
options[:per_page] = vars[:
|
43
|
+
options[:per_page] = vars[:limit]
|
44
44
|
options[:page] = vars[:page]
|
45
45
|
results = model.send(DEFAULT[:searchkick_search], term, **options, &block)
|
46
46
|
vars[:count] = results.total_count
|
47
47
|
|
48
|
-
pagy = ::Pagy.new(vars)
|
48
|
+
pagy = ::Pagy.new(**vars)
|
49
49
|
# with :last_page overflow we need to re-run the method in order to get the hits
|
50
|
-
return pagy_searchkick(pagy_search_args, vars
|
50
|
+
return pagy_searchkick(pagy_search_args, **vars, page: pagy.page) \
|
51
51
|
if defined?(::Pagy::OverflowExtra) && pagy.overflow? && pagy.vars[:overflow] == :last_page
|
52
52
|
|
53
53
|
[pagy, called.empty? ? results : results.send(*called)]
|
@@ -56,10 +56,10 @@ class Pagy # :nodoc:
|
|
56
56
|
# Sub-method called only by #pagy_searchkick: here for easy customization of variables by overriding
|
57
57
|
# the _collection argument is not available when the method is called
|
58
58
|
def pagy_searchkick_get_vars(_collection, vars)
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
59
|
+
vars.tap do |v|
|
60
|
+
v[:page] ||= pagy_get_page(v)
|
61
|
+
v[:limit] ||= pagy_get_limit(v) || DEFAULT[:limit]
|
62
|
+
end
|
63
63
|
end
|
64
64
|
end
|
65
65
|
Backend.prepend BackendAddOn
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# See the Pagy documentation: https://ddnexus.github.io/pagy/docs/extras/size
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class Pagy # :nodoc:
|
5
|
+
# Implement the legacy bar using the array size.
|
6
|
+
# Unless you have very specific requirements, use the faster and better looking default bar.
|
7
|
+
module SizeExtra
|
8
|
+
# Implements the old series algorithm
|
9
|
+
def series(size: @vars[:size], **_)
|
10
|
+
return super unless size.is_a?(Array)
|
11
|
+
return [] if size == []
|
12
|
+
raise VariableError.new(self, :size, 'to be an Array of 4 Integers or []', size) \
|
13
|
+
unless size.is_a?(Array) && size.size == 4 && size.all? { |num| !num.negative? rescue false } # rubocop:disable Style/RescueModifier
|
14
|
+
|
15
|
+
[].tap do |series|
|
16
|
+
# This algorithm is up to ~5x faster and ~2.3x lighter than the previous one (pagy < 4.3)
|
17
|
+
# However the behavior of the legacy nav bar was taken straight from WillPaginate and Kaminari:
|
18
|
+
# it's ill-concieved and complicates the experience of devs and users.
|
19
|
+
left_gap_start = 1 + size[0]
|
20
|
+
left_gap_end = @page - size[1] - 1
|
21
|
+
right_gap_start = @page + size[2] + 1
|
22
|
+
right_gap_end = @last - size[3]
|
23
|
+
left_gap_end = right_gap_end if left_gap_end > right_gap_end
|
24
|
+
right_gap_start = left_gap_start if left_gap_start > right_gap_start
|
25
|
+
start = 1
|
26
|
+
if (left_gap_end - left_gap_start).positive?
|
27
|
+
series.push(*start...left_gap_start, :gap)
|
28
|
+
start = left_gap_end + 1
|
29
|
+
end
|
30
|
+
if (right_gap_end - right_gap_start).positive?
|
31
|
+
series.push(*start...right_gap_start, :gap)
|
32
|
+
start = right_gap_end + 1
|
33
|
+
end
|
34
|
+
series.push(*start..@last)
|
35
|
+
series[series.index(@page)] = @page.to_s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
prepend SizeExtra
|
40
|
+
end
|
@@ -12,10 +12,6 @@ class Pagy # :nodoc:
|
|
12
12
|
module QueryUtils
|
13
13
|
module_function
|
14
14
|
|
15
|
-
def escape(str)
|
16
|
-
URI.encode_www_form_component(str)
|
17
|
-
end
|
18
|
-
|
19
15
|
def build_nested_query(value, prefix = nil)
|
20
16
|
case value
|
21
17
|
when Array
|
@@ -32,21 +28,25 @@ class Pagy # :nodoc:
|
|
32
28
|
"#{escape(prefix)}=#{escape(value)}"
|
33
29
|
end
|
34
30
|
end
|
31
|
+
|
32
|
+
def escape(str)
|
33
|
+
URI.encode_www_form_component(str)
|
34
|
+
end
|
35
35
|
end
|
36
36
|
# :nocov:
|
37
37
|
|
38
38
|
# Return the URL for the page. If there is no pagy.vars[:url]
|
39
39
|
# it works exactly as the regular #pagy_url_for, relying on the params method and Rack.
|
40
40
|
# If there is a defined pagy.vars[:url] variable it does not need the params method nor Rack.
|
41
|
-
def pagy_url_for(pagy, page,
|
41
|
+
def pagy_url_for(pagy, page, fragment: nil, **_)
|
42
42
|
return super unless pagy.vars[:url]
|
43
43
|
|
44
44
|
vars = pagy.vars
|
45
|
-
params =
|
45
|
+
params = vars[:params].is_a?(Hash) ? vars[:params].clone : {} # safe when it gets reused
|
46
46
|
pagy_set_query_params(page, vars, params)
|
47
|
-
params =
|
47
|
+
params = vars[:params].(params) if vars[:params].is_a?(Proc)
|
48
48
|
query_string = "?#{QueryUtils.build_nested_query(params)}"
|
49
|
-
"#{vars[:url]}#{query_string}#{
|
49
|
+
"#{vars[:url]}#{query_string}#{fragment}"
|
50
50
|
end
|
51
51
|
end
|
52
52
|
UrlHelpers.prepend StandaloneExtra
|
data/lib/pagy/extras/trim.rb
CHANGED
@@ -6,10 +6,10 @@ class Pagy # :nodoc:
|
|
6
6
|
|
7
7
|
# Remove the page=1 param from the first page link
|
8
8
|
module TrimExtra
|
9
|
-
# Override the original
|
9
|
+
# Override the original pagy_anchor.
|
10
10
|
# Call the pagy_trim method for page 1 if the trim_extra is enabled
|
11
|
-
def pagy_anchor(pagy)
|
12
|
-
a_proc = super
|
11
|
+
def pagy_anchor(pagy, **_)
|
12
|
+
a_proc = super
|
13
13
|
return a_proc unless pagy.vars[:trim_extra]
|
14
14
|
|
15
15
|
lambda do |page, text = pagy.label_for(page), **opts|
|
data/lib/pagy/frontend.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# See Pagy::Frontend API documentation: https://ddnexus.github.io/pagy/docs/api/frontend
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
require_relative 'i18n'
|
5
|
+
require_relative 'url_helpers'
|
6
6
|
|
7
7
|
class Pagy
|
8
8
|
# Used for search and replace, hardcoded also in the pagy.js file
|
@@ -14,13 +14,44 @@ class Pagy
|
|
14
14
|
module Frontend
|
15
15
|
include UrlHelpers
|
16
16
|
|
17
|
+
# Return a performance optimized lambda to generate the HTML anchor element (a tag)
|
18
|
+
# Benchmarked on a 20 link nav: it is ~22x faster and uses ~18x less memory than rails' link_to
|
19
|
+
def pagy_anchor(pagy, anchor_string: nil, **vars)
|
20
|
+
anchor_string &&= %( #{anchor_string})
|
21
|
+
left, right = %(<a#{anchor_string} href="#{pagy_url_for(pagy, PAGE_TOKEN, **vars)}").split(PAGE_TOKEN, 2)
|
22
|
+
# lambda used by all the helpers
|
23
|
+
lambda do |page, text = pagy.label_for(page), classes: nil, aria_label: nil|
|
24
|
+
classes = %( class="#{classes}") if classes
|
25
|
+
aria_label = %( aria-label="#{aria_label}") if aria_label
|
26
|
+
%(#{left}#{page}#{right}#{classes}#{aria_label}>#{text}</a>)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Return examples: "Displaying items 41-60 of 324 in total" or "Displaying Products 41-60 of 324 in total"
|
31
|
+
def pagy_info(pagy, id: nil, item_name: nil)
|
32
|
+
id = %( id="#{id}") if id
|
33
|
+
p_count = pagy.count
|
34
|
+
key = if p_count.zero?
|
35
|
+
'pagy.info.no_items'
|
36
|
+
elsif pagy.pages == 1
|
37
|
+
'pagy.info.single_page'
|
38
|
+
else
|
39
|
+
'pagy.info.multiple_pages'
|
40
|
+
end
|
41
|
+
|
42
|
+
%(<span#{id} class="pagy info">#{
|
43
|
+
pagy_t key, item_name: item_name || pagy_t('pagy.item_name', count: p_count),
|
44
|
+
count: p_count, from: pagy.from, to: pagy.to
|
45
|
+
}</span>)
|
46
|
+
end
|
47
|
+
|
17
48
|
# Generic pagination: it returns the html with the series of links to the pages
|
18
|
-
def pagy_nav(pagy, id: nil, aria_label: nil, **vars)
|
49
|
+
def pagy_nav(pagy, id: nil, aria_label: nil, anchor_string: nil, **vars)
|
19
50
|
id = %( id="#{id}") if id
|
20
|
-
a = pagy_anchor(pagy)
|
51
|
+
a = pagy_anchor(pagy, anchor_string:)
|
21
52
|
|
22
53
|
html = %(<nav#{id} class="pagy nav" #{nav_aria_label(pagy, aria_label:)}>#{
|
23
|
-
|
54
|
+
prev_a(pagy, a)})
|
24
55
|
pagy.series(**vars).each do |item| # series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]
|
25
56
|
html << case item
|
26
57
|
when Integer
|
@@ -36,39 +67,10 @@ class Pagy
|
|
36
67
|
html << %(#{next_a(pagy, a)}</nav>)
|
37
68
|
end
|
38
69
|
|
39
|
-
# Return examples: "Displaying items 41-60 of 324 in total" or "Displaying Products 41-60 of 324 in total"
|
40
|
-
def pagy_info(pagy, id: nil, item_name: nil)
|
41
|
-
id = %( id="#{id}") if id
|
42
|
-
p_count = pagy.count
|
43
|
-
key = if p_count.zero? then 'pagy.info.no_items'
|
44
|
-
elsif pagy.pages == 1 then 'pagy.info.single_page'
|
45
|
-
else 'pagy.info.multiple_pages' # rubocop:disable Lint/ElseLayout
|
46
|
-
end
|
47
|
-
|
48
|
-
%(<span#{id} class="pagy info">#{
|
49
|
-
pagy_t key, item_name: item_name || pagy_t('pagy.item_name', count: p_count),
|
50
|
-
count: p_count, from: pagy.from, to: pagy.to
|
51
|
-
}</span>)
|
52
|
-
end
|
53
|
-
|
54
|
-
# Return a performance optimized lambda to generate the HtML anchor element (a tag)
|
55
|
-
# Benchmarked on a 20 link nav: it is ~22x faster and uses ~18x less memory than rails' link_to
|
56
|
-
def pagy_anchor(pagy)
|
57
|
-
a_string = pagy.vars[:anchor_string]
|
58
|
-
a_string = %( #{a_string}) if a_string
|
59
|
-
left, right = %(<a#{a_string} href="#{pagy_url_for(pagy, PAGE_TOKEN)}").split(PAGE_TOKEN, 2)
|
60
|
-
# lambda used by all the helpers
|
61
|
-
lambda do |page, text = pagy.label_for(page), classes: nil, aria_label: nil|
|
62
|
-
classes = %( class="#{classes}") if classes
|
63
|
-
aria_label = %( aria-label="#{aria_label}") if aria_label
|
64
|
-
%(#{left}#{page}#{right}#{classes}#{aria_label}>#{text}</a>)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
70
|
# Similar to I18n.t: just ~18x faster using ~10x less memory
|
69
71
|
# (@pagy_locale explicitly initialized in order to avoid warning)
|
70
|
-
def pagy_t(key, opts
|
71
|
-
Pagy::I18n.translate(@pagy_locale ||= nil, key, opts)
|
72
|
+
def pagy_t(key, **opts)
|
73
|
+
Pagy::I18n.translate(@pagy_locale ||= nil, key, **opts)
|
72
74
|
end
|
73
75
|
|
74
76
|
private
|
@@ -90,7 +92,7 @@ class Pagy
|
|
90
92
|
if (p_next = pagy.next)
|
91
93
|
a.(p_next, text, aria_label:)
|
92
94
|
else
|
93
|
-
%(<a role="link" aria-disabled="true" aria-label
|
95
|
+
%(<a role="link" aria-disabled="true" aria-label="#{aria_label}">#{text}</a>)
|
94
96
|
end
|
95
97
|
end
|
96
98
|
end
|
data/lib/pagy/i18n.rb
CHANGED
@@ -154,7 +154,7 @@ class Pagy
|
|
154
154
|
end
|
155
155
|
|
156
156
|
# Translate and pluralize the key with the locale DATA
|
157
|
-
def translate(locale, key, opts
|
157
|
+
def translate(locale, key, **opts)
|
158
158
|
data, pluralize = DATA[locale]
|
159
159
|
translation = data[key] || (opts[:count] && data[key += ".#{pluralize.call(opts[:count])}"]) \
|
160
160
|
or return %([translation missing: "#{key}"])
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# See Pagy API documentation: https://ddnexus.github.io/pagy/docs/api/keyset
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class Pagy
|
5
|
+
class Keyset
|
6
|
+
# Keyset adapter for ActiveRecord
|
7
|
+
class ActiveRecord < Keyset
|
8
|
+
protected
|
9
|
+
|
10
|
+
# Get the keyset attributes of the record
|
11
|
+
def latest_from(latest_record) = latest_record.slice(*@keyset.keys)
|
12
|
+
|
13
|
+
# Extract the keyset from the set
|
14
|
+
def extract_keyset
|
15
|
+
@set.order_values.each_with_object({}) do |node, keyset|
|
16
|
+
keyset[node.value.name.to_sym] = node.direction
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Filter out the already retrieved records
|
21
|
+
def after_latest = @set.where(after_latest_query, **@latest)
|
22
|
+
|
23
|
+
# Append the missing keyset keys if the set is restricted by select
|
24
|
+
def apply_select
|
25
|
+
@set.select(*@keyset.keys)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Set with selected columns?
|
29
|
+
def select? = !@set.select_values.empty?
|
30
|
+
|
31
|
+
# Typecast the latest attributes
|
32
|
+
def typecast_latest(latest)
|
33
|
+
@set.model.new(latest).slice(latest.keys)
|
34
|
+
.to_hash.transform_keys(&:to_sym)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# See Pagy API documentation: https://ddnexus.github.io/pagy/docs/api/keyset
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class Pagy
|
5
|
+
class Keyset
|
6
|
+
# Keyset adapter for sequel
|
7
|
+
class Sequel < Keyset
|
8
|
+
protected
|
9
|
+
|
10
|
+
# Get the keyset attributes of the latest record
|
11
|
+
def latest_from(latest_record) = latest_record.to_hash.slice(*@keyset.keys)
|
12
|
+
|
13
|
+
# Extract the keyset from the set
|
14
|
+
def extract_keyset
|
15
|
+
return {} unless @set.opts[:order]
|
16
|
+
|
17
|
+
@set.opts[:order].each_with_object({}) do |item, keyset|
|
18
|
+
case item
|
19
|
+
when Symbol
|
20
|
+
keyset[item] = :asc
|
21
|
+
when ::Sequel::SQL::OrderedExpression
|
22
|
+
keyset[item.expression] = item.descending ? :desc : :asc
|
23
|
+
else
|
24
|
+
raise TypeError, "#{item.class.inspect} is not a supported Sequel::SQL::OrderedExpression"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Filter out the already retrieved records
|
30
|
+
def after_latest = @set.where(::Sequel.lit(after_latest_query, **@latest))
|
31
|
+
|
32
|
+
# Append the missing keyset keys if the set is restricted by select
|
33
|
+
def apply_select
|
34
|
+
selected = @set.opts[:select]
|
35
|
+
@set.select_append(*@keyset.keys.reject { |c| selected.include?(c) })
|
36
|
+
end
|
37
|
+
|
38
|
+
# Set with selected columns?
|
39
|
+
def select? = !@set.opts[:select].nil?
|
40
|
+
|
41
|
+
# Typecast the latest attributes
|
42
|
+
def typecast_latest(latest)
|
43
|
+
model = @set.opts[:model]
|
44
|
+
model.unrestrict_primary_key if (restricted_pk = model.restrict_primary_key?)
|
45
|
+
latest = model.new(latest).to_hash.slice(*latest.keys.map(&:to_sym))
|
46
|
+
model.restrict_primary_key if restricted_pk
|
47
|
+
latest
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/pagy/keyset.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# See Pagy API documentation: https://ddnexus.github.io/pagy/docs/api/keyset
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require_relative 'b64'
|
6
|
+
|
7
|
+
class Pagy
|
8
|
+
# Implement wicked-fast keyset pagination for big data
|
9
|
+
class Keyset
|
10
|
+
class TypeError < ::TypeError; end
|
11
|
+
|
12
|
+
include SharedMethods
|
13
|
+
|
14
|
+
# Pick the right adapter for the set
|
15
|
+
def self.new(set, **vars)
|
16
|
+
if self == Pagy::Keyset
|
17
|
+
if defined?(::ActiveRecord) && set.is_a?(::ActiveRecord::Relation)
|
18
|
+
ActiveRecord
|
19
|
+
elsif defined?(::Sequel) && set.is_a?(::Sequel::Dataset)
|
20
|
+
Sequel
|
21
|
+
else
|
22
|
+
raise TypeError, "expected set to be an instance of ActiveRecord::Relation or Sequel::Dataset; got #{set.class}"
|
23
|
+
end.new(set, **vars)
|
24
|
+
else
|
25
|
+
allocate.tap { |instance| instance.send(:initialize, set, **vars) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :latest # Other readers from SharedMethods
|
30
|
+
|
31
|
+
def initialize(set, **vars)
|
32
|
+
default = DEFAULT.slice(:limit, :page_param, # from pagy
|
33
|
+
:headers, # from headers extra
|
34
|
+
:jsonapi, # from jsonapi extra
|
35
|
+
:limit_param, :limit_max, :limit_extra) # from limit_extra
|
36
|
+
assign_vars({ **default, page: nil }, vars)
|
37
|
+
assign_limit
|
38
|
+
@set = set
|
39
|
+
@page = @vars[:page]
|
40
|
+
@keyset = extract_keyset
|
41
|
+
raise InternalError, 'the set must be ordered' if @keyset.empty?
|
42
|
+
return unless @page
|
43
|
+
|
44
|
+
latest = JSON.parse(B64.urlsafe_decode(@page)).transform_keys(&:to_sym)
|
45
|
+
@latest = @vars[:typecast_latest]&.(latest) || typecast_latest(latest)
|
46
|
+
raise InternalError, 'page and keyset are not consistent' \
|
47
|
+
unless @latest.keys == @keyset.keys
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return the next page
|
51
|
+
def next
|
52
|
+
records
|
53
|
+
return unless @more
|
54
|
+
|
55
|
+
@next ||= B64.urlsafe_encode(latest_from(@records.last).to_json)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Retrieve the array of records for the current page
|
59
|
+
def records
|
60
|
+
@records ||= begin
|
61
|
+
@set = apply_select if select?
|
62
|
+
@set = @vars[:after_latest]&.(@set, @latest) || after_latest if @latest
|
63
|
+
records = @set.limit(@limit + 1).to_a
|
64
|
+
@more = records.size > @limit && !records.pop.nil?
|
65
|
+
records
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
protected
|
70
|
+
|
71
|
+
# Prepare the literal query to filter out the already retrieved records
|
72
|
+
def after_latest_query
|
73
|
+
operator = { asc: '>', desc: '<' }
|
74
|
+
directions = @keyset.values
|
75
|
+
if @vars[:row_comparison]
|
76
|
+
# Row comparison: check if your DB supports it (especially if you have mixed order rows)
|
77
|
+
columns = @keyset.keys
|
78
|
+
placeholders = columns.map { |column| ":#{column}" }.join(', ')
|
79
|
+
"( #{columns.join(', ')} ) #{operator[directions.first]} ( #{placeholders} )"
|
80
|
+
else
|
81
|
+
# Generic comparison: works for keysets ordered in mixed or same directions
|
82
|
+
keyset = @keyset.to_a
|
83
|
+
where = []
|
84
|
+
until keyset.empty?
|
85
|
+
last_column, last_direction = keyset.pop
|
86
|
+
query = +'( '
|
87
|
+
query << (keyset.map { |column, _d| "#{column} = :#{column}" } \
|
88
|
+
<< "#{last_column} #{operator[last_direction]} :#{last_column}").join(' AND ')
|
89
|
+
query << ' )'
|
90
|
+
where << query
|
91
|
+
end
|
92
|
+
where.join(' OR ')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
require_relative 'keyset/active_record'
|
99
|
+
require_relative 'keyset/sequel'
|
data/lib/pagy/url_helpers.rb
CHANGED
@@ -6,21 +6,21 @@ class Pagy
|
|
6
6
|
# Return the URL for the page, relying on the params method and Rack by default.
|
7
7
|
# It supports all Rack-based frameworks (Sinatra, Padrino, Rails, ...).
|
8
8
|
# For non-rack environments you can use the standalone extra
|
9
|
-
def pagy_url_for(pagy, page, absolute: false, **_)
|
9
|
+
def pagy_url_for(pagy, page, absolute: false, fragment: nil, **_)
|
10
10
|
vars = pagy.vars
|
11
|
-
|
12
|
-
params
|
13
|
-
pagy_set_query_params(page, vars,
|
14
|
-
|
15
|
-
query_string = "?#{Rack::Utils.build_nested_query(
|
16
|
-
"#{request.base_url if absolute}#{vars[:request_path] || request.path}#{query_string}#{
|
11
|
+
query_params = request.GET.clone
|
12
|
+
query_params.merge!(vars[:params].transform_keys(&:to_s)) if vars[:params].is_a?(Hash)
|
13
|
+
pagy_set_query_params(page, vars, query_params)
|
14
|
+
query_params = vars[:params].(query_params) if vars[:params].is_a?(Proc)
|
15
|
+
query_string = "?#{Rack::Utils.build_nested_query(query_params)}"
|
16
|
+
"#{request.base_url if absolute}#{vars[:request_path] || request.path}#{query_string}#{fragment}"
|
17
17
|
end
|
18
18
|
|
19
|
-
# Add the page and
|
19
|
+
# Add the page and limit params
|
20
20
|
# Overridable by the jsonapi extra
|
21
|
-
def pagy_set_query_params(page, vars,
|
22
|
-
|
23
|
-
|
21
|
+
def pagy_set_query_params(page, vars, query_params)
|
22
|
+
query_params[vars[:page_param].to_s] = page if page
|
23
|
+
query_params[vars[:limit_param].to_s] = vars[:limit] if vars[:limit_extra]
|
24
24
|
end
|
25
25
|
end
|
26
26
|
end
|