pagy 43.0.0 → 43.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/LICENSE.txt +1 -1
- data/apps/calendar.ru +11 -12
- data/apps/demo.ru +5 -5
- data/apps/enable_rails_page_segment.rb +54 -0
- data/apps/index.rb +1 -1
- data/apps/keynav+root_key.ru +316 -0
- data/apps/keynav.ru +10 -13
- data/apps/keyset.ru +5 -11
- data/apps/keyset_sequel.ru +10 -12
- data/apps/rails.ru +8 -12
- data/apps/repro.ru +11 -11
- data/bin/pagy +2 -94
- data/config/pagy.rb +8 -7
- data/javascripts/ai_widget.js +65 -51
- data/javascripts/pagy.js +20 -17
- data/javascripts/pagy.js.map +3 -3
- data/javascripts/pagy.min.js +2 -1
- data/javascripts/pagy.mjs +19 -16
- data/javascripts/wand.js +15 -9
- data/lib/pagy/classes/calendar/calendar.rb +36 -31
- data/lib/pagy/classes/calendar/day.rb +1 -1
- data/lib/pagy/classes/calendar/month.rb +1 -1
- data/lib/pagy/classes/calendar/quarter.rb +1 -1
- data/lib/pagy/classes/calendar/unit.rb +12 -13
- data/lib/pagy/classes/calendar/year.rb +1 -1
- data/lib/pagy/classes/exceptions.rb +1 -8
- data/lib/pagy/classes/keyset/adapters/active_record.rb +3 -1
- data/lib/pagy/classes/keyset/adapters/sequel.rb +3 -1
- data/lib/pagy/classes/keyset/keynav.rb +9 -4
- data/lib/pagy/classes/keyset/keyset.rb +57 -32
- data/lib/pagy/classes/offset/countish.rb +17 -0
- data/lib/pagy/classes/offset/countless.rb +26 -14
- data/lib/pagy/classes/offset/offset.rb +8 -2
- data/lib/pagy/classes/offset/search.rb +6 -10
- data/lib/pagy/classes/request.rb +29 -20
- data/lib/pagy/cli.rb +135 -0
- data/lib/pagy/console.rb +6 -0
- data/lib/pagy/modules/abilities/configurable.rb +2 -2
- data/lib/pagy/modules/abilities/countable.rb +24 -0
- data/lib/pagy/modules/abilities/linkable.rb +35 -24
- data/lib/pagy/modules/abilities/rangeable.rb +3 -3
- data/lib/pagy/modules/b64.rb +9 -3
- data/lib/pagy/modules/console.rb +15 -20
- data/lib/pagy/modules/i18n/i18n.rb +38 -14
- data/lib/pagy/modules/i18n/p11n/arabic.rb +1 -0
- data/lib/pagy/modules/i18n/p11n/east_slavic.rb +1 -0
- data/lib/pagy/modules/i18n/p11n/polish.rb +1 -0
- data/lib/pagy/modules/searcher.rb +9 -8
- data/lib/pagy/toolbox/helpers/anchor_tags.rb +11 -15
- data/lib/pagy/toolbox/helpers/bootstrap/input_nav_js.rb +3 -0
- data/lib/pagy/toolbox/helpers/bootstrap/series_nav.rb +3 -1
- data/lib/pagy/toolbox/helpers/bootstrap/series_nav_js.rb +2 -0
- data/lib/pagy/toolbox/helpers/bulma/input_nav_js.rb +3 -0
- data/lib/pagy/toolbox/helpers/bulma/previous_next_html.rb +1 -1
- data/lib/pagy/toolbox/helpers/bulma/series_nav.rb +3 -1
- data/lib/pagy/toolbox/helpers/bulma/series_nav_js.rb +2 -0
- data/lib/pagy/toolbox/helpers/data_hash.rb +19 -17
- data/lib/pagy/toolbox/helpers/headers_hash.rb +15 -9
- data/lib/pagy/toolbox/helpers/info_tag.rb +2 -0
- data/lib/pagy/toolbox/helpers/input_nav_js.rb +9 -6
- data/lib/pagy/toolbox/helpers/limit_tag_js.rb +4 -3
- data/lib/pagy/toolbox/helpers/loader.rb +3 -0
- data/lib/pagy/toolbox/helpers/page_url.rb +10 -16
- data/lib/pagy/toolbox/helpers/series_nav.rb +5 -4
- data/lib/pagy/toolbox/helpers/series_nav_js.rb +2 -1
- data/lib/pagy/toolbox/helpers/support/a_lambda.rb +8 -6
- data/lib/pagy/toolbox/helpers/support/data_pagy_attribute.rb +6 -1
- data/lib/pagy/toolbox/helpers/support/series.rb +1 -2
- data/lib/pagy/toolbox/helpers/support/wrap_input_nav_js.rb +1 -1
- data/lib/pagy/toolbox/helpers/support/wrap_series_nav.rb +2 -1
- data/lib/pagy/toolbox/helpers/support/wrap_series_nav_js.rb +10 -4
- data/lib/pagy/toolbox/helpers/urls_hash.rb +7 -7
- data/lib/pagy/toolbox/paginators/calendar.rb +13 -9
- data/lib/pagy/toolbox/paginators/countish.rb +39 -0
- data/lib/pagy/toolbox/paginators/countless.rb +13 -15
- data/lib/pagy/toolbox/paginators/elasticsearch_rails.rb +43 -18
- data/lib/pagy/toolbox/paginators/keynav_js.rb +14 -15
- data/lib/pagy/toolbox/paginators/keyset.rb +7 -9
- data/lib/pagy/toolbox/paginators/meilisearch.rb +21 -18
- data/lib/pagy/toolbox/paginators/method.rb +15 -3
- data/lib/pagy/toolbox/paginators/offset.rb +14 -22
- data/lib/pagy/toolbox/paginators/searchkick.rb +21 -18
- data/lib/pagy/toolbox/paginators/typesense_rails.rb +35 -0
- data/lib/pagy.rb +23 -10
- data/locales/id.yml +1 -3
- data/locales/ja.yml +1 -3
- data/locales/km.yml +1 -3
- data/locales/sw.yml +2 -2
- data/locales/tr.yml +10 -8
- data/stylesheets/pagy-tailwind.css +1 -1
- data/stylesheets/pagy.css +1 -6
- metadata +25 -8
- data/lib/optimist.rb +0 -1022
- data/lib/pagy/classes/keyset/active_record.rb +0 -11
- data/lib/pagy/classes/keyset/keynav/active_record.rb +0 -13
- data/lib/pagy/classes/keyset/keynav/sequel.rb +0 -13
- data/lib/pagy/classes/keyset/sequel.rb +0 -11
|
@@ -6,37 +6,57 @@ require_relative '../../modules/b64'
|
|
|
6
6
|
class Pagy
|
|
7
7
|
# Implement wicked-fast keyset pagination for big data
|
|
8
8
|
class Keyset < Pagy
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
# Autoload adapters: files are loaded only when const_get accesses them
|
|
10
|
+
module Adapters
|
|
11
|
+
path = Pathname.new(__dir__)
|
|
12
|
+
autoload :ActiveRecord, path.join('adapters/active_record')
|
|
13
|
+
autoload :Sequel, path.join('adapters/sequel')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
autoload :Keynav, Pathname.new(__dir__).join('keynav')
|
|
17
|
+
|
|
18
|
+
# Define empty subclasses to allow specific typing without triggering autoload
|
|
19
|
+
class ActiveRecord < self; end
|
|
20
|
+
class Sequel < self; end
|
|
13
21
|
|
|
14
22
|
class TypeError < ::TypeError; end
|
|
15
23
|
|
|
16
|
-
#
|
|
24
|
+
# Factory method: detects the set type, configures the subclass, and instantiates
|
|
17
25
|
def self.new(set, **)
|
|
18
|
-
#
|
|
19
|
-
if /::(?:ActiveRecord|Sequel)$/.match?(name)
|
|
20
|
-
|
|
26
|
+
# 1. Handle direct subclass usage (e.g. Pagy::Keyset::ActiveRecord.new)
|
|
27
|
+
if /::(?:ActiveRecord|Sequel)$/.match?(name)
|
|
28
|
+
# Ensure the adapter is mixed in (lazy load)
|
|
29
|
+
mix_in_adapter(name.split('::').last)
|
|
30
|
+
return allocate.tap { _1.send(:initialize, set, **) }
|
|
21
31
|
end
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
# 2. Handle Factory usage (Pagy::Keyset.new)
|
|
34
|
+
adapter = if defined?(::ActiveRecord) && set.is_a?(::ActiveRecord::Relation)
|
|
35
|
+
:ActiveRecord
|
|
36
|
+
elsif defined?(::Sequel) && set.is_a?(::Sequel::Dataset)
|
|
37
|
+
:Sequel
|
|
38
|
+
else
|
|
39
|
+
raise TypeError, "expected an ActiveRecord::Relation or Sequel::Dataset; got #{set.class}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
const_get(adapter).tap { _1.mix_in_adapter(adapter) }.new(set, **)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Helper to lazy-include the adapter module
|
|
46
|
+
def self.mix_in_adapter(adapter)
|
|
47
|
+
adapter_module = Adapters.const_get(adapter)
|
|
48
|
+
include(adapter_module) unless self < adapter_module
|
|
31
49
|
end
|
|
32
50
|
|
|
33
|
-
def initialize(set, **)
|
|
51
|
+
def initialize(set, **)
|
|
34
52
|
assign_options(**)
|
|
35
53
|
assign_and_check(limit: 1)
|
|
36
54
|
@set = set
|
|
37
55
|
@keyset = @options[:keyset] || extract_keyset
|
|
38
56
|
raise InternalError, 'the set must be ordered' if @keyset.empty?
|
|
39
57
|
|
|
58
|
+
@identifiers = quoted_identifiers(@set.model.table_name)
|
|
59
|
+
|
|
40
60
|
assign_page
|
|
41
61
|
self.next
|
|
42
62
|
end
|
|
@@ -91,23 +111,28 @@ class Pagy
|
|
|
91
111
|
def compose_predicate(prefix = nil)
|
|
92
112
|
operator = { asc: '>', desc: '<' }
|
|
93
113
|
directions = @keyset.values
|
|
94
|
-
identifier =
|
|
114
|
+
identifier = @identifiers
|
|
95
115
|
placeholder = @keyset.to_h { |column| [column, ":#{prefix}#{column}"] }
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
union << intersection
|
|
108
|
-
end
|
|
109
|
-
union.join(' OR ')
|
|
116
|
+
|
|
117
|
+
return "(#{identifier.values.join(', ')}) #{operator[directions.first]} (#{placeholder.values.join(', ')})" \
|
|
118
|
+
if @options[:tuple_comparison] && (directions.all?(:asc) || directions.all?(:desc))
|
|
119
|
+
|
|
120
|
+
keyset = @keyset.to_a
|
|
121
|
+
ors = []
|
|
122
|
+
until keyset.empty?
|
|
123
|
+
column, direction = keyset.pop
|
|
124
|
+
ands = keyset.map { |k, _| "#{identifier[k]} = #{placeholder[k]}" }
|
|
125
|
+
ands << "#{identifier[column]} #{operator[direction]} #{placeholder[column]}"
|
|
126
|
+
ors << "(#{ands.join(' AND ')})"
|
|
110
127
|
end
|
|
128
|
+
query = ors.join(' OR ')
|
|
129
|
+
|
|
130
|
+
return query unless @keyset.size > 1
|
|
131
|
+
|
|
132
|
+
# Add hint predicate for DB optimizers that struggle with ORs
|
|
133
|
+
column, direction = @keyset.first
|
|
134
|
+
hint = "#{identifier[column]} #{operator[direction]}= #{placeholder[column]}"
|
|
135
|
+
"#{hint} AND (#{query})"
|
|
111
136
|
end
|
|
112
137
|
|
|
113
138
|
# Return the prefixed arguments from a cutoff
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Pagy
|
|
4
|
+
class Offset
|
|
5
|
+
# Offset pagination with memoized count
|
|
6
|
+
class Countish < Offset
|
|
7
|
+
protected
|
|
8
|
+
|
|
9
|
+
# Return page+count or page+count+epoch
|
|
10
|
+
def compose_page_param(page)
|
|
11
|
+
value = "#{page || 1}+#{@count}"
|
|
12
|
+
value << "+#{@options[:epoch]}" if @options[:epoch]
|
|
13
|
+
EscapedValue.new(value)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -4,11 +4,11 @@ class Pagy
|
|
|
4
4
|
class Offset
|
|
5
5
|
# Offset pagination without a count
|
|
6
6
|
class Countless < Offset
|
|
7
|
-
def initialize(**)
|
|
7
|
+
def initialize(**)
|
|
8
8
|
assign_options(**)
|
|
9
9
|
assign_and_check(limit: 1, page: 1)
|
|
10
10
|
@page = upto_max_pages(@page)
|
|
11
|
-
|
|
11
|
+
@last = upto_max_pages(@options[:last]) unless @options[:headless]
|
|
12
12
|
assign_offset
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -25,32 +25,44 @@ class Pagy
|
|
|
25
25
|
def countless? = true
|
|
26
26
|
|
|
27
27
|
def upto_max_pages(value)
|
|
28
|
-
return value unless @options[:max_pages]
|
|
28
|
+
return value unless value && @options[:max_pages]
|
|
29
29
|
|
|
30
30
|
[value, @options[:max_pages]].min
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def assign_last
|
|
34
|
-
return unless @options[:last]
|
|
35
|
-
|
|
36
|
-
@last = upto_max_pages(@options[:last].to_i)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
33
|
# Finalize the instance variables based on the fetched size
|
|
40
34
|
def finalize(fetched_size)
|
|
41
|
-
|
|
35
|
+
# empty records (trigger the right info message for known 0 count)
|
|
36
|
+
@count = 0 if fetched_size.zero? && @page == 1
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@last = upto_max_pages(fetched_size > @limit ? @page + 1 : @page)
|
|
38
|
+
unless in_range? { fetched_size.positive? || @page == 1 }
|
|
39
|
+
assign_empty_page_variables
|
|
40
|
+
return self
|
|
47
41
|
end
|
|
42
|
+
|
|
43
|
+
past = @last && @page < @last # current page is before the known last page
|
|
44
|
+
more = fetched_size > @limit # more pages after this one
|
|
45
|
+
@last = upto_max_pages(more ? @page + 1 : @page) unless past && more
|
|
48
46
|
@in = [fetched_size, @limit].min
|
|
49
47
|
@from = @in.zero? ? 0 : @offset + 1
|
|
50
48
|
@to = @offset + @in
|
|
51
49
|
assign_previous_and_next
|
|
50
|
+
|
|
52
51
|
self
|
|
53
52
|
end
|
|
53
|
+
|
|
54
|
+
# Called by false in_range?
|
|
55
|
+
def assign_empty_page_variables
|
|
56
|
+
@in = @from = @to = 0
|
|
57
|
+
target_last = [@page - 1, 1].max
|
|
58
|
+
@last = [@last || target_last, target_last].min
|
|
59
|
+
@previous = @last
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Support easy countless page param overriding (for legacy param and behavior)
|
|
63
|
+
def compose_page_param(page)
|
|
64
|
+
EscapedValue.new("#{page || 1}+#{@last}")
|
|
65
|
+
end
|
|
54
66
|
end
|
|
55
67
|
end
|
|
56
68
|
end
|
|
@@ -9,20 +9,26 @@ class Pagy
|
|
|
9
9
|
DEFAULT = { page: 1 }.freeze
|
|
10
10
|
|
|
11
11
|
autoload :Countless, Pathname.new(__dir__).join('countless')
|
|
12
|
+
autoload :Countish, Pathname.new(__dir__).join('countish')
|
|
12
13
|
|
|
13
14
|
include Rangeable
|
|
14
15
|
include Shiftable
|
|
15
16
|
|
|
16
|
-
def initialize(**)
|
|
17
|
+
def initialize(**)
|
|
17
18
|
assign_options(**)
|
|
18
19
|
assign_and_check(limit: 1, count: 0, page: 1)
|
|
19
20
|
assign_last
|
|
20
21
|
assign_offset
|
|
21
|
-
|
|
22
|
+
|
|
23
|
+
unless in_range? { @page <= @last }
|
|
24
|
+
assign_empty_page_variables
|
|
25
|
+
return
|
|
26
|
+
end
|
|
22
27
|
|
|
23
28
|
@from = [@offset + 1, @count].min
|
|
24
29
|
@to = [@offset + @limit, @count].min
|
|
25
30
|
@in = [@to - @from + 1, @count].min
|
|
31
|
+
|
|
26
32
|
assign_previous_and_next
|
|
27
33
|
end
|
|
28
34
|
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
class Pagy
|
|
4
4
|
module Search
|
|
5
5
|
class Arguments < Array
|
|
6
|
-
def respond_to_missing? = true
|
|
6
|
+
def respond_to_missing?(*) = true
|
|
7
7
|
|
|
8
8
|
def method_missing(*) = push(*)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
# Collect the search arguments to pass to the actual search
|
|
12
|
-
def pagy_search(
|
|
13
|
-
Arguments.new([self,
|
|
12
|
+
def pagy_search(*arguments, **options, &block)
|
|
13
|
+
Arguments.new([self, arguments, options, block])
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -22,17 +22,13 @@ class Pagy
|
|
|
22
22
|
def search? = true
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
class ElasticsearchRails < SearchBase
|
|
26
|
-
# Get the count from different versions of ElasticsearchRails
|
|
27
|
-
def self.total_count(results)
|
|
28
|
-
total = results.instance_eval { respond_to?(:raw_response) ? raw_response['hits']['total'] : response['hits']['total'] }
|
|
29
|
-
total.is_a?(Hash) ? total['value'] : total
|
|
30
|
-
end
|
|
31
|
-
end
|
|
25
|
+
class ElasticsearchRails < SearchBase; end
|
|
32
26
|
|
|
33
27
|
class Meilisearch < SearchBase
|
|
34
28
|
DEFAULT = { search_method: :ms_search }.freeze
|
|
35
29
|
end
|
|
36
30
|
|
|
37
31
|
class Searchkick < SearchBase; end
|
|
32
|
+
|
|
33
|
+
class TypesenseRails < SearchBase; end
|
|
38
34
|
end
|
data/lib/pagy/classes/request.rb
CHANGED
|
@@ -1,36 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Pagy
|
|
4
|
-
# Decouple the
|
|
5
|
-
# Resolve :page and :limit
|
|
4
|
+
# Decouple the request from the env, allowing non-rack apps to use pagy by passing a hash.
|
|
5
|
+
# Resolve the :page and :limit options from params.
|
|
6
6
|
class Request
|
|
7
|
-
def initialize(
|
|
8
|
-
@
|
|
7
|
+
def initialize(options)
|
|
8
|
+
@options = options
|
|
9
|
+
request = @options[:request]
|
|
10
|
+
@base_url, @path, @params, @cookie =
|
|
9
11
|
if request.is_a?(Hash)
|
|
10
|
-
request.values_at(:base_url, :path, :
|
|
12
|
+
request.values_at(:base_url, :path, :params, :cookie)
|
|
11
13
|
else
|
|
12
|
-
[request.base_url, request.path, request
|
|
14
|
+
[request.base_url, request.path, get_params(request), request.cookies['pagy']]
|
|
13
15
|
end
|
|
14
|
-
|
|
15
|
-
raise JsonapiReservedParamError, @query['page'] if @jsonapi && !@query['page'].respond_to?(:fetch)
|
|
16
|
+
freeze
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
attr_reader :base_url, :path, :
|
|
19
|
+
attr_reader :base_url, :path, :params, :cookie
|
|
19
20
|
|
|
20
|
-
def resolve_page(
|
|
21
|
-
page_key = options[:page_key] || DEFAULT[:page_key]
|
|
22
|
-
page = @
|
|
23
|
-
page
|
|
24
|
-
|
|
21
|
+
def resolve_page(force_integer: true)
|
|
22
|
+
page_key = @options[:page_key] || DEFAULT[:page_key]
|
|
23
|
+
page = @params.dig(@options[:root_key], page_key) || @params[page_key]
|
|
24
|
+
return [page.to_s.to_i, 1].max if force_integer
|
|
25
|
+
|
|
26
|
+
page unless page == ''
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def resolve_limit
|
|
30
|
+
default = @options[:limit] || DEFAULT[:limit]
|
|
31
|
+
max_limit = @options[:client_max_limit]
|
|
32
|
+
return default unless max_limit
|
|
33
|
+
|
|
34
|
+
limit_key = @options[:limit_key] || DEFAULT[:limit_key]
|
|
35
|
+
limit = (@params.dig(@options[:root_key], limit_key) || @params[limit_key]).to_s.to_i
|
|
36
|
+
limit.zero? ? default : [limit, max_limit].min
|
|
25
37
|
end
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
limit_key = options[:limit_key] || DEFAULT[:limit_key]
|
|
29
|
-
return options[:limit] || DEFAULT[:limit] \
|
|
30
|
-
unless options[:client_max_limit] &&
|
|
31
|
-
(requested_limit = @jsonapi ? @query['page'][limit_key] : @query[limit_key])
|
|
39
|
+
private
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
def get_params(request)
|
|
42
|
+
request.GET.merge(request.POST).to_h.freeze
|
|
34
43
|
end
|
|
35
44
|
end
|
|
36
45
|
end
|
data/lib/pagy/cli.rb
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'rbconfig'
|
|
6
|
+
require_relative '../pagy'
|
|
7
|
+
require_relative '../../apps/index'
|
|
8
|
+
|
|
9
|
+
class Pagy
|
|
10
|
+
class CLI
|
|
11
|
+
HOST = 'localhost'
|
|
12
|
+
PORT = '8000'
|
|
13
|
+
|
|
14
|
+
def start(args = ARGV)
|
|
15
|
+
options = parse_options(args)
|
|
16
|
+
run_command(args, options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def parse_options(args)
|
|
22
|
+
options = { env: 'development', host: HOST, port: PORT, quiet: false }
|
|
23
|
+
|
|
24
|
+
parser = OptionParser.new do |opts|
|
|
25
|
+
opts.banner = <<~BANNER
|
|
26
|
+
Pagy #{VERSION} (https://ddnexus.github.io/pagy/playground)
|
|
27
|
+
Playground to showcase, clone and develop Pagy APPs
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
pagy APP [opts] Showcase APP from the installed gem
|
|
31
|
+
pagy clone APP Clone APP to the current dir
|
|
32
|
+
pagy FILE [opts] Develop app FILE from local path
|
|
33
|
+
BANNER
|
|
34
|
+
|
|
35
|
+
opts.summary_indent = ' '
|
|
36
|
+
opts.summary_width = 18
|
|
37
|
+
|
|
38
|
+
opts.separator "\nAPPs"
|
|
39
|
+
PagyApps::INDEX.each do |name, path|
|
|
40
|
+
desc = File.readlines(path)[3].sub('# ', '').strip
|
|
41
|
+
opts.separator " #{name.ljust(18)}#{desc}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
opts.separator "\nRackup options"
|
|
45
|
+
opts.on('-e', '--env ENV', 'Environment') { |v| options[:env] = v }
|
|
46
|
+
opts.on('-o', '--host HOST', 'Host') { |v| options[:host] = v }
|
|
47
|
+
opts.on('-p', '--port PORT', 'Port') { |v| options[:port] = v }
|
|
48
|
+
opts.on('-t', '--threads THREADS', 'Threads') { |v| options[:threads] = v }
|
|
49
|
+
|
|
50
|
+
opts.separator "\nOther options"
|
|
51
|
+
opts.on('-q', '--quiet', 'Quiet mode for development') { options[:quiet] = true }
|
|
52
|
+
opts.on('-v', '--version', 'Show version') do
|
|
53
|
+
puts VERSION
|
|
54
|
+
exit
|
|
55
|
+
end
|
|
56
|
+
opts.on('-h', '--help', 'Show this help') do
|
|
57
|
+
puts opts
|
|
58
|
+
exit
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
opts.separator "\nExamples"
|
|
62
|
+
opts.separator " pagy demo Showcase demo at http://#{HOST}:#{PORT}"
|
|
63
|
+
opts.separator ' pagy clone repro Clone repro to ./repro.ru (rename it)'
|
|
64
|
+
opts.separator " pagy ~/myapp.ru Develop ~/myapp.ru at #{HOST}:#{PORT}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
begin
|
|
68
|
+
parser.parse!(args)
|
|
69
|
+
rescue OptionParser::InvalidOption => e
|
|
70
|
+
abort e.message
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if args.empty?
|
|
74
|
+
puts parser
|
|
75
|
+
exit
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
options
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def run_command(args, options)
|
|
82
|
+
run_from_repo = Pagy::ROOT.join('pagy.gemspec').exist?
|
|
83
|
+
setup_gems(run_from_repo)
|
|
84
|
+
|
|
85
|
+
arg = args.shift
|
|
86
|
+
|
|
87
|
+
if arg.eql?('clone')
|
|
88
|
+
clone_app(args.shift)
|
|
89
|
+
else
|
|
90
|
+
serve_app(arg, options)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def clone_app(name)
|
|
95
|
+
abort "Expected APP to be in [#{PagyApps::INDEX.keys.join(', ')}]; got #{name.inspect}" unless PagyApps::INDEX.key?(name)
|
|
96
|
+
|
|
97
|
+
if File.exist?(name)
|
|
98
|
+
print "Do you want to overwrite the #{name.inspect} file? (y/n)> "
|
|
99
|
+
answer = gets.chomp
|
|
100
|
+
abort "#{name.inspect} file already present" unless answer.start_with?(/y/i)
|
|
101
|
+
end
|
|
102
|
+
FileUtils.cp(PagyApps::INDEX[name], '.', verbose: true)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def serve_app(arg, options)
|
|
106
|
+
if PagyApps::INDEX.key?(arg)
|
|
107
|
+
options[:env] = 'showcase'
|
|
108
|
+
options[:quiet] = true
|
|
109
|
+
# Avoid the creation of './tmp/local_secret.txt' for showcase env
|
|
110
|
+
ENV['SECRET_KEY_BASE'] = 'absolute secret!' if arg.eql?('rails')
|
|
111
|
+
file = PagyApps::INDEX[arg]
|
|
112
|
+
else
|
|
113
|
+
file = arg
|
|
114
|
+
end
|
|
115
|
+
abort "#{file.inspect} app not found" unless File.exist?(file)
|
|
116
|
+
|
|
117
|
+
gem_dir = File.expand_path('../..', __dir__)
|
|
118
|
+
rackup = "rackup -I #{gem_dir}/lib -r pagy -o #{options[:host]} -p #{options[:port]} -E #{options[:env]} #{file}"
|
|
119
|
+
rackup << " -O Threads=#{options[:threads]}" if options[:threads]
|
|
120
|
+
rackup << ' -q' if options[:quiet]
|
|
121
|
+
|
|
122
|
+
exec(rackup)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Kept as a separate method because mocking 'gemfile' (dsl) is complex otherwise
|
|
126
|
+
def setup_gems(run_from_repo)
|
|
127
|
+
require 'bundler/inline'
|
|
128
|
+
gemfile(!run_from_repo) do
|
|
129
|
+
source 'https://rubygems.org'
|
|
130
|
+
gem 'logger'
|
|
131
|
+
gem 'rackup'
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
data/lib/pagy/console.rb
ADDED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Pagy
|
|
4
|
-
# Add
|
|
4
|
+
# Add configuration methods
|
|
5
5
|
module Configurable
|
|
6
6
|
# Sync the pagy javascript targets
|
|
7
7
|
def sync_javascript(destination, *targets)
|
|
@@ -15,7 +15,7 @@ class Pagy
|
|
|
15
15
|
def dev_tools(wand_scale: 1)
|
|
16
16
|
<<~HTML
|
|
17
17
|
<script id="pagy-ai-widget">
|
|
18
|
-
|
|
18
|
+
#{ROOT.join('javascripts/ai_widget.js').read}
|
|
19
19
|
</script>
|
|
20
20
|
<script id="pagy-wand" data-scale="#{wand_scale}">
|
|
21
21
|
#{ROOT.join('javascripts/wand.js').read}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Pagy
|
|
4
|
+
# Provide the helpers to count a collection
|
|
5
|
+
module Countable
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Get the collection count
|
|
9
|
+
def get_count(collection, options)
|
|
10
|
+
return collection.size if collection.instance_of?(Array)
|
|
11
|
+
return collection.count unless defined?(::ActiveRecord) && collection.is_a?(::ActiveRecord::Relation)
|
|
12
|
+
|
|
13
|
+
count = if options[:count_over] && !collection.group_values.empty?
|
|
14
|
+
# COUNT(*) OVER ()
|
|
15
|
+
sql = Arel.star.count.over(Arel::Nodes::Grouping.new([]))
|
|
16
|
+
collection.unscope(:order).pick(sql).to_i
|
|
17
|
+
else
|
|
18
|
+
collection.count(:all)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
count.is_a?(Hash) ? count.size : count
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -3,31 +3,31 @@
|
|
|
3
3
|
require 'uri'
|
|
4
4
|
|
|
5
5
|
class Pagy
|
|
6
|
+
# Support spaces in placeholder params
|
|
7
|
+
class EscapedValue < String; end
|
|
8
|
+
|
|
6
9
|
# Provide the helpers to handle the url and anchor
|
|
7
10
|
module Linkable
|
|
8
11
|
module QueryUtils
|
|
9
12
|
module_function
|
|
10
13
|
|
|
11
14
|
# Extracted from Rack::Utils and reformatted for rubocop
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
def build_nested_query(value, prefix = nil, unescaped = [])
|
|
15
|
+
# Allow unescaped Pagy::RawQueryValue
|
|
16
|
+
def build_nested_query(value, prefix = nil)
|
|
15
17
|
case value
|
|
16
18
|
when Array
|
|
17
|
-
value.map { |v| build_nested_query(v, "#{prefix}[]"
|
|
19
|
+
value.map { |v| build_nested_query(v, "#{prefix}[]") }.join('&')
|
|
18
20
|
when Hash
|
|
19
21
|
value.map do |k, v|
|
|
20
|
-
|
|
21
|
-
unescaped[unescaped.find_index(k)] = new_k if unescaped.size.positive? && new_k != k && unescaped.include?(k)
|
|
22
|
-
build_nested_query(v, new_k, unescaped)
|
|
22
|
+
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
|
|
23
23
|
end.delete_if(&:empty?).join('&')
|
|
24
24
|
when nil
|
|
25
25
|
escape(prefix)
|
|
26
26
|
else
|
|
27
27
|
raise ArgumentError, 'value must be a Hash' if prefix.nil?
|
|
28
|
-
return "#{escape(prefix)}=#{value}" if unescaped.include?(prefix)
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
escaped_value = value.is_a?(EscapedValue) ? value : escape(value)
|
|
30
|
+
"#{escape(prefix)}=#{escaped_value}"
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -38,22 +38,33 @@ class Pagy
|
|
|
38
38
|
|
|
39
39
|
protected
|
|
40
40
|
|
|
41
|
+
# Overridable by classes with composite page param
|
|
42
|
+
def compose_page_param(page) = page
|
|
43
|
+
|
|
41
44
|
# Return the URL for the page, relying on the Pagy::Request
|
|
42
|
-
def compose_page_url(page,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
def compose_page_url(page, **options)
|
|
46
|
+
opts = @options.merge(options)
|
|
47
|
+
params = @request.params.clone(freeze: false)
|
|
48
|
+
root_key = opts[:root_key]
|
|
49
|
+
container = if root_key
|
|
50
|
+
params[root_key] = params[root_key]&.clone(freeze: false) || {}
|
|
51
|
+
else
|
|
52
|
+
params
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
{ opts[:page_key] => compose_page_param(page),
|
|
56
|
+
opts[:limit_key] => opts[:client_max_limit] && opts[:limit] }.each do |k, v|
|
|
57
|
+
v ? container[k] = v : container.delete(k)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
opts[:querify]&.(params) # Must modify the params: the returned value is ignored
|
|
61
|
+
fragment = opts[:fragment].to_s.sub(/\A(?=[^#])/, '#') # conditionally prepend '#'
|
|
62
|
+
|
|
63
|
+
compose_url(opts[:absolute], opts[:path], params, fragment)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def compose_url(absolute, path, params, fragment)
|
|
67
|
+
query_string = QueryUtils.build_nested_query(params).sub(/\A(?=.)/, '?') # conditionally prepend '?'
|
|
57
68
|
"#{@request.base_url if absolute}#{path || @request.path}#{query_string}#{fragment}"
|
|
58
69
|
end
|
|
59
70
|
end
|
|
@@ -5,11 +5,11 @@ class Pagy
|
|
|
5
5
|
module Rangeable
|
|
6
6
|
# Check if in range
|
|
7
7
|
def in_range?
|
|
8
|
-
return @in_range if defined?(@in_range)
|
|
8
|
+
return @in_range if defined?(@in_range)
|
|
9
|
+
return true if (@in_range = yield)
|
|
9
10
|
raise RangeError.new(self, :page, "in 1..#{@last}", @page) if @options[:raise_range_error]
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
false
|
|
12
|
+
@in_range = false
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
end
|
data/lib/pagy/modules/b64.rb
CHANGED
|
@@ -5,14 +5,19 @@ class Pagy
|
|
|
5
5
|
module B64
|
|
6
6
|
module_function
|
|
7
7
|
|
|
8
|
-
def encode(bin)
|
|
8
|
+
def encode(bin)
|
|
9
|
+
[bin].pack('m0')
|
|
10
|
+
end
|
|
9
11
|
|
|
10
|
-
def decode(str)
|
|
12
|
+
def decode(str)
|
|
13
|
+
str.unpack1('m0')
|
|
14
|
+
end
|
|
11
15
|
|
|
12
16
|
def urlsafe_encode(bin)
|
|
13
17
|
str = encode(bin)
|
|
14
|
-
str.chomp!('==') or str.chomp!('=')
|
|
15
18
|
str.tr!('+/', '-_')
|
|
19
|
+
str.delete!('=')
|
|
20
|
+
|
|
16
21
|
str
|
|
17
22
|
end
|
|
18
23
|
|
|
@@ -23,6 +28,7 @@ class Pagy
|
|
|
23
28
|
else
|
|
24
29
|
str = str.tr('-_', '+/')
|
|
25
30
|
end
|
|
31
|
+
|
|
26
32
|
decode(str)
|
|
27
33
|
end
|
|
28
34
|
end
|