uchi 0.1.2 → 0.1.4
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/README.md +222 -0
- data/app/assets/config/uchi_manifest.js +2 -0
- data/app/assets/javascripts/uchi/application.js +6095 -0
- data/app/assets/javascripts/uchi.js +4 -0
- data/app/assets/stylesheets/uchi/application.css +3971 -0
- data/app/assets/stylesheets/uchi/uchi.css +17 -0
- data/app/assets/tailwind/uchi.css +21 -0
- data/app/components/flowbite/breadcrumb.rb +33 -0
- data/app/components/flowbite/breadcrumb_home.rb +26 -0
- data/app/components/flowbite/breadcrumb_item/current.rb +33 -0
- data/app/components/flowbite/breadcrumb_item/first.rb +35 -0
- data/app/components/flowbite/breadcrumb_item.rb +48 -0
- data/app/components/flowbite/breadcrumb_separator.rb +30 -0
- data/app/components/flowbite/button/outline.rb +22 -0
- data/app/components/flowbite/button/pill.rb +40 -0
- data/app/components/flowbite/button.rb +92 -0
- data/app/components/flowbite/card.rb +45 -0
- data/app/components/flowbite/input/checkbox.rb +73 -0
- data/app/components/flowbite/input/date.rb +11 -0
- data/app/components/flowbite/input/date_time.rb +11 -0
- data/app/components/flowbite/input/email.rb +12 -0
- data/app/components/flowbite/input/field.rb +117 -0
- data/app/components/flowbite/input/file.rb +30 -0
- data/app/components/flowbite/input/hint.rb +57 -0
- data/app/components/flowbite/input/label.rb +82 -0
- data/app/components/flowbite/input/number.rb +11 -0
- data/app/components/flowbite/input/password.rb +11 -0
- data/app/components/flowbite/input/phone.rb +11 -0
- data/app/components/flowbite/input/radio_button.rb +50 -0
- data/app/components/flowbite/input/select.rb +49 -0
- data/app/components/flowbite/input/textarea.rb +42 -0
- data/app/components/flowbite/input/url.rb +12 -0
- data/app/components/flowbite/input/validation_error.rb +11 -0
- data/app/components/flowbite/input_field/checkbox.html.erb +14 -0
- data/app/components/flowbite/input_field/checkbox.rb +54 -0
- data/app/components/flowbite/input_field/date.rb +13 -0
- data/app/components/flowbite/input_field/date_time.rb +13 -0
- data/app/components/flowbite/input_field/email.rb +13 -0
- data/app/components/flowbite/input_field/file.rb +13 -0
- data/app/components/flowbite/input_field/input_field.html.erb +8 -0
- data/app/components/flowbite/input_field/number.rb +13 -0
- data/app/components/flowbite/input_field/password.rb +13 -0
- data/app/components/flowbite/input_field/phone.rb +13 -0
- data/app/components/flowbite/input_field/radio_button.html.erb +14 -0
- data/app/components/flowbite/input_field/radio_button.rb +86 -0
- data/app/components/flowbite/input_field/select.rb +31 -0
- data/app/components/flowbite/input_field/text.rb +8 -0
- data/app/components/flowbite/input_field/textarea.rb +13 -0
- data/app/components/flowbite/input_field/url.rb +13 -0
- data/app/components/flowbite/input_field.rb +192 -0
- data/app/components/flowbite/link.rb +21 -0
- data/app/components/flowbite/style.rb +13 -0
- data/app/components/flowbite/toast/icon.html.erb +5 -0
- data/app/components/flowbite/toast/icon.rb +57 -0
- data/app/components/flowbite/toast/toast.html.erb +11 -0
- data/app/components/flowbite/toast.rb +34 -0
- data/app/components/uchi/field/base.rb +57 -0
- data/app/components/uchi/field/belongs_to/edit.html.erb +1 -0
- data/app/components/uchi/field/belongs_to/index.html.erb +1 -0
- data/app/components/uchi/field/belongs_to/show.html.erb +3 -0
- data/app/components/uchi/field/belongs_to.rb +105 -0
- data/app/components/uchi/field/blank/edit.html.erb +1 -0
- data/app/components/uchi/field/blank/index.html.erb +1 -0
- data/app/components/uchi/field/blank/show.html.erb +1 -0
- data/app/components/uchi/field/blank.rb +16 -0
- data/app/components/uchi/field/boolean/edit.html.erb +1 -0
- data/app/components/uchi/field/boolean/index.html.erb +9 -0
- data/app/components/uchi/field/boolean/show.html.erb +9 -0
- data/app/components/uchi/field/boolean.rb +27 -0
- data/app/components/uchi/field/date/edit.html.erb +1 -0
- data/app/components/uchi/field/date/index.html.erb +1 -0
- data/app/components/uchi/field/date/show.html.erb +1 -0
- data/app/components/uchi/field/date.rb +27 -0
- data/app/components/uchi/field/date_time/edit.html.erb +1 -0
- data/app/components/uchi/field/date_time/index.html.erb +1 -0
- data/app/components/uchi/field/date_time/show.html.erb +1 -0
- data/app/components/uchi/field/date_time.rb +27 -0
- data/app/components/uchi/field/file/edit.html.erb +1 -0
- data/app/components/uchi/field/file/index.html.erb +6 -0
- data/app/components/uchi/field/file/show.html.erb +8 -0
- data/app/components/uchi/field/file.rb +37 -0
- data/app/components/uchi/field/has_and_belongs_to_many/edit.html.erb +9 -0
- data/app/components/uchi/field/has_and_belongs_to_many/index.html.erb +1 -0
- data/app/components/uchi/field/has_and_belongs_to_many/show.html.erb +28 -0
- data/app/components/uchi/field/has_and_belongs_to_many.rb +131 -0
- data/app/components/uchi/field/has_many/edit.html.erb +1 -0
- data/app/components/uchi/field/has_many/index.html.erb +1 -0
- data/app/components/uchi/field/has_many/show.html.erb +28 -0
- data/app/components/uchi/field/has_many.rb +107 -0
- data/app/components/uchi/field/id/index.html.erb +4 -0
- data/app/components/uchi/field/id/show.html.erb +4 -0
- data/app/components/uchi/field/id.rb +26 -0
- data/app/components/uchi/field/image/edit.html.erb +1 -0
- data/app/components/uchi/field/image/index.html.erb +6 -0
- data/app/components/uchi/field/image/show.html.erb +6 -0
- data/app/components/uchi/field/image.rb +38 -0
- data/app/components/uchi/field/number/edit.html.erb +1 -0
- data/app/components/uchi/field/number/index.html.erb +1 -0
- data/app/components/uchi/field/number/show.html.erb +1 -0
- data/app/components/uchi/field/number.rb +32 -0
- data/app/components/uchi/field/string/edit.html.erb +1 -0
- data/app/components/uchi/field/string/index.html.erb +1 -0
- data/app/components/uchi/field/string/show.html.erb +1 -0
- data/app/components/uchi/field/string.rb +33 -0
- data/app/components/uchi/field/text/edit.html.erb +1 -0
- data/app/components/uchi/field/text/index.html.erb +1 -0
- data/app/components/uchi/field/text/show.html.erb +1 -0
- data/app/components/uchi/field/text.rb +38 -0
- data/app/components/uchi/ui/breadcrumb/breadcrumb.html.erb +13 -0
- data/app/components/uchi/ui/breadcrumb.rb +14 -0
- data/app/components/uchi/ui/form/footer/footer.html.erb +5 -0
- data/app/components/uchi/ui/form/footer.rb +15 -0
- data/app/components/uchi/ui/form/input/collection_checkboxes.html.erb +32 -0
- data/app/components/uchi/ui/form/input/collection_checkboxes.rb +125 -0
- data/app/components/uchi/ui/frame/frame.html.erb +3 -0
- data/app/components/uchi/ui/frame.rb +10 -0
- data/app/components/uchi/ui/index/records_table/records_table.html.erb +67 -0
- data/app/components/uchi/ui/index/records_table/search_form/search_form.html.erb +21 -0
- data/app/components/uchi/ui/index/records_table/search_form.rb +49 -0
- data/app/components/uchi/ui/index/records_table.rb +29 -0
- data/app/components/uchi/ui/index/turbo_frame.rb +50 -0
- data/app/components/uchi/ui/page_header/page_header.html.erb +24 -0
- data/app/components/uchi/ui/page_header.rb +18 -0
- data/app/components/uchi/ui/pagination/current_link.html.erb +3 -0
- data/app/components/uchi/ui/pagination/current_link.rb +10 -0
- data/app/components/uchi/ui/pagination/gap.html.erb +3 -0
- data/app/components/uchi/ui/pagination/gap.rb +10 -0
- data/app/components/uchi/ui/pagination/item.rb +24 -0
- data/app/components/uchi/ui/pagination/link.html.erb +3 -0
- data/app/components/uchi/ui/pagination/link.rb +10 -0
- data/app/components/uchi/ui/pagination/next_link.html.erb +8 -0
- data/app/components/uchi/ui/pagination/next_link.rb +22 -0
- data/app/components/uchi/ui/pagination/pagination.html.erb +15 -0
- data/app/components/uchi/ui/pagination/previous_link.html.erb +8 -0
- data/app/components/uchi/ui/pagination/previous_link.rb +22 -0
- data/app/components/uchi/ui/pagination.rb +48 -0
- data/app/components/uchi/ui/show/attribute_fields/attribute_fields.html.erb +14 -0
- data/app/components/uchi/ui/show/attribute_fields.rb +18 -0
- data/app/components/uchi/ui/spinner/spinner.html.erb +7 -0
- data/app/components/uchi/ui/spinner.rb +15 -0
- data/app/controllers/uchi/application_controller.rb +4 -0
- data/app/controllers/uchi/controller.rb +13 -0
- data/app/controllers/uchi/repository_controller.rb +166 -0
- data/app/helpers/uchi/application_helper.rb +17 -0
- data/app/jobs/uchi/application_job.rb +4 -0
- data/app/mailers/uchi/application_mailer.rb +6 -0
- data/app/views/layouts/uchi/_flash_messages.html.erb +10 -0
- data/app/views/layouts/uchi/application.html.erb +33 -0
- data/app/views/uchi/repository/edit.html.erb +40 -0
- data/app/views/uchi/repository/index.html.erb +49 -0
- data/app/views/uchi/repository/new.html.erb +37 -0
- data/app/views/uchi/repository/show.html.erb +41 -0
- data/lib/generators/uchi/controller/controller_generator.rb +16 -0
- data/lib/generators/uchi/controller/templates/controller.rb.tt +11 -0
- data/lib/generators/uchi/install/install_generator.rb +13 -0
- data/lib/generators/uchi/repository/repository_generator.rb +16 -0
- data/lib/generators/uchi/repository/templates/repository.rb.tt +11 -0
- data/lib/tasks/uchi_tasks.rake +4 -0
- data/lib/uchi/application_record.rb +5 -0
- data/lib/uchi/engine.rb +24 -0
- data/lib/uchi/field/configuration.rb +142 -0
- data/lib/uchi/field.rb +80 -0
- data/lib/uchi/i18n.rb +13 -0
- data/lib/uchi/pagination/controller.rb +26 -0
- data/lib/uchi/pagination/page.rb +20 -0
- data/lib/uchi/pagy/LICENSE.txt +21 -0
- data/lib/uchi/pagy/classes/exceptions.rb +35 -0
- data/lib/uchi/pagy/classes/offset/offset.rb +56 -0
- data/lib/uchi/pagy/classes/request.rb +38 -0
- data/lib/uchi/pagy/modules/abilities/configurable.rb +38 -0
- data/lib/uchi/pagy/modules/abilities/linkable.rb +62 -0
- data/lib/uchi/pagy/modules/abilities/rangeable.rb +17 -0
- data/lib/uchi/pagy/modules/abilities/shiftable.rb +14 -0
- data/lib/uchi/pagy/modules/console.rb +40 -0
- data/lib/uchi/pagy/toolbox/helpers/loader.rb +19 -0
- data/lib/uchi/pagy/toolbox/helpers/page_url.rb +25 -0
- data/lib/uchi/pagy/toolbox/paginators/method.rb +21 -0
- data/lib/uchi/pagy/toolbox/paginators/offset.rb +35 -0
- data/lib/uchi/pagy.rb +60 -0
- data/lib/uchi/repository/routes.rb +62 -0
- data/lib/uchi/repository/translate.rb +284 -0
- data/lib/uchi/repository.rb +156 -0
- data/lib/uchi/sort_order.rb +35 -0
- data/lib/uchi/version.rb +5 -0
- data/lib/uchi.rb +18 -0
- data/uchi.gemspec +35 -0
- metadata +197 -107
- data/.github/dependabot.yml +0 -17
- data/.github/workflows/build.yml +0 -23
- data/.github/workflows/lint.yml +0 -30
- data/package.json +0 -31
- data/sig/uchi.rbs +0 -4
- data/test/components/uchi/field/belongs_to_test.rb +0 -134
- data/test/components/uchi/field/blank_test.rb +0 -119
- data/test/components/uchi/field/boolean_test.rb +0 -163
- data/test/components/uchi/field/date_test.rb +0 -163
- data/test/components/uchi/field/date_time_test.rb +0 -152
- data/test/components/uchi/field/has_many_test.rb +0 -138
- data/test/components/uchi/field/id_test.rb +0 -113
- data/test/components/uchi/field/number_test.rb +0 -163
- data/test/components/uchi/field/string_test.rb +0 -159
- data/test/controllers/uchi/authors_controller_test.rb +0 -119
- data/test/controllers/uchi/repository_controller_test.rb +0 -93
- data/test/controllers/uchi/scoped_repository_controller_test.rb +0 -73
- data/test/dummy/Rakefile +0 -6
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/stylesheets/application.css +0 -15
- data/test/dummy/app/controllers/application_controller.rb +0 -4
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/controllers/uchi/authors_controller.rb +0 -7
- data/test/dummy/app/controllers/uchi/books_controller.rb +0 -7
- data/test/dummy/app/controllers/uchi/titles_controller.rb +0 -7
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/jobs/application_job.rb +0 -7
- data/test/dummy/app/mailers/application_mailer.rb +0 -4
- data/test/dummy/app/models/application_record.rb +0 -3
- data/test/dummy/app/models/author.rb +0 -3
- data/test/dummy/app/models/book.rb +0 -3
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/models/title.rb +0 -3
- data/test/dummy/app/uchi/repositories/author.rb +0 -20
- data/test/dummy/app/uchi/repositories/book.rb +0 -16
- data/test/dummy/app/uchi/repositories/title.rb +0 -17
- data/test/dummy/app/views/layouts/application.html.erb +0 -27
- data/test/dummy/app/views/layouts/mailer.html.erb +0 -13
- data/test/dummy/app/views/layouts/mailer.text.erb +0 -1
- data/test/dummy/app/views/pwa/manifest.json.erb +0 -22
- data/test/dummy/app/views/pwa/service-worker.js +0 -26
- data/test/dummy/bin/dev +0 -2
- data/test/dummy/bin/rails +0 -4
- data/test/dummy/bin/rake +0 -4
- data/test/dummy/bin/setup +0 -34
- data/test/dummy/config/application.rb +0 -29
- data/test/dummy/config/boot.rb +0 -5
- data/test/dummy/config/cable.yml +0 -10
- data/test/dummy/config/database.yml +0 -32
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -69
- data/test/dummy/config/environments/production.rb +0 -89
- data/test/dummy/config/environments/test.rb +0 -53
- data/test/dummy/config/initializers/content_security_policy.rb +0 -25
- data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -8
- data/test/dummy/config/initializers/inflections.rb +0 -16
- data/test/dummy/config/locales/da.yml +0 -51
- data/test/dummy/config/locales/en.yml +0 -31
- data/test/dummy/config/puma.rb +0 -38
- data/test/dummy/config/routes.rb +0 -9
- data/test/dummy/config/storage.yml +0 -34
- data/test/dummy/config.ru +0 -6
- data/test/dummy/db/migrate/20251002183635_create_authors.rb +0 -11
- data/test/dummy/db/migrate/20251005131726_create_books.rb +0 -9
- data/test/dummy/db/migrate/20251005131811_create_titles.rb +0 -11
- data/test/dummy/db/schema.rb +0 -38
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/public/400.html +0 -114
- data/test/dummy/public/404.html +0 -114
- data/test/dummy/public/406-unsupported-browser.html +0 -114
- data/test/dummy/public/422.html +0 -114
- data/test/dummy/public/500.html +0 -114
- data/test/dummy/public/icon.png +0 -0
- data/test/dummy/public/icon.svg +0 -3
- data/test/dummy/storage/.keep +0 -0
- data/test/dummy/test/fixtures/authors.yml +0 -11
- data/test/dummy/test/models/author_test.rb +0 -7
- data/test/dummy/tmp/.keep +0 -0
- data/test/dummy/tmp/pids/.keep +0 -0
- data/test/dummy/tmp/storage/.keep +0 -0
- data/test/test_helper.rb +0 -15
- data/test/uchi/field_test.rb +0 -63
- data/test/uchi/i18n_test.rb +0 -18
- data/test/uchi/repository/routes_test.rb +0 -49
- data/test/uchi/repository/translate_test.rb +0 -263
- data/test/uchi/repository_test.rb +0 -137
- data/test/uchi/sort_order_test.rb +0 -47
- data/test/uchi_test.rb +0 -7
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
class RepositoryGenerator < Rails::Generators::NamedBase
|
|
5
|
+
source_root File.expand_path("templates", __dir__)
|
|
6
|
+
|
|
7
|
+
def create_repository_file
|
|
8
|
+
destination = File.join("app/uchi/repositories")
|
|
9
|
+
template "repository.rb", File.join(destination, "#{file_name}.rb")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def generate_controller
|
|
13
|
+
generate("uchi:controller", name)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/uchi/engine.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Uchi
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace Uchi
|
|
4
|
+
|
|
5
|
+
initializer "uchi.assets" do |app|
|
|
6
|
+
if app.config.respond_to?(:assets)
|
|
7
|
+
app.config.assets.paths << root.join("app/assets/stylesheets")
|
|
8
|
+
app.config.assets.paths << root.join("app/assets/javascripts")
|
|
9
|
+
app.config.assets.precompile += %w[uchi_manifest]
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer "uchi.autoload" do |app|
|
|
14
|
+
# Preserve Uchi as a namespace for autoloading from app/uchi
|
|
15
|
+
# See https://github.com/fxn/zeitwerk/issues/250 for details
|
|
16
|
+
ActiveSupport::Dependencies.autoload_paths.delete("#{Rails.root}/app/uchi")
|
|
17
|
+
|
|
18
|
+
uchi_directory = Rails.root.join("app/uchi")
|
|
19
|
+
if uchi_directory.exist?
|
|
20
|
+
Rails.autoloaders.main.push_dir(uchi_directory, namespace: Uchi)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
class Field
|
|
5
|
+
module Configuration
|
|
6
|
+
class Unset; end
|
|
7
|
+
|
|
8
|
+
DEFAULT_READER = ->(record, field_name) { record.public_send(field_name) }
|
|
9
|
+
|
|
10
|
+
def initialize(*args)
|
|
11
|
+
super
|
|
12
|
+
@on = default_on
|
|
13
|
+
@reader = DEFAULT_READER
|
|
14
|
+
@searchable = default_searchable?
|
|
15
|
+
@sortable = default_sortable?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Sets or gets which actions this field should appear on.
|
|
19
|
+
#
|
|
20
|
+
# When called with arguments, sets the actions and returns self for chaining.
|
|
21
|
+
# When called without arguments, returns the current actions.
|
|
22
|
+
#
|
|
23
|
+
# @param actions [Array<Symbol>] The actions where this field should appear
|
|
24
|
+
# (e.g., :index, :show, :new, :edit)
|
|
25
|
+
# @return [self, Array<Symbol>] Returns self for method chaining when setting,
|
|
26
|
+
# or the actions array when getting
|
|
27
|
+
#
|
|
28
|
+
# @example Setting
|
|
29
|
+
# Field::Number.new(:id).on(:index, :show)
|
|
30
|
+
#
|
|
31
|
+
# @example Getting
|
|
32
|
+
# field.on # => [:index, :show]
|
|
33
|
+
def on(*actions)
|
|
34
|
+
return @on if actions.empty?
|
|
35
|
+
|
|
36
|
+
@on = actions.flatten
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Sets or gets a custom reader for this field.
|
|
41
|
+
#
|
|
42
|
+
# When called with an argument, sets the reader and returns self for chaining.
|
|
43
|
+
# When called without arguments, returns the current reader.
|
|
44
|
+
#
|
|
45
|
+
# @param reader_proc [Proc, nil] A callable that reads the value from a record.
|
|
46
|
+
# The proc receives the model and field name, and should return the value.
|
|
47
|
+
# @return [self, Proc] Returns self for method chaining when setting,
|
|
48
|
+
# or the reader proc when getting
|
|
49
|
+
#
|
|
50
|
+
# @example Setting
|
|
51
|
+
# Field::String.new(:full_name).reader(->(record, field_name) {
|
|
52
|
+
# "#{record.first_name} #{record.last_name}"
|
|
53
|
+
# })
|
|
54
|
+
#
|
|
55
|
+
# @example Getting
|
|
56
|
+
# field.reader # => #<Proc...>
|
|
57
|
+
def reader(reader_proc = nil)
|
|
58
|
+
return @reader if reader_proc.nil? && !block_given?
|
|
59
|
+
|
|
60
|
+
@reader = reader_proc || Proc.new
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Sets or gets whether this field is searchable.
|
|
65
|
+
#
|
|
66
|
+
# When called with an argument, sets searchable and returns self for chaining.
|
|
67
|
+
# When called without arguments, returns whether the field is searchable.
|
|
68
|
+
#
|
|
69
|
+
# @param value [Boolean, nil] Whether the field is searchable in index views.
|
|
70
|
+
# Defaults to false for most fields, except text-based fields.
|
|
71
|
+
# @return [self, Boolean] Returns self for method chaining when setting,
|
|
72
|
+
# or boolean when getting
|
|
73
|
+
#
|
|
74
|
+
# @example Setting
|
|
75
|
+
# Field::String.new(:password).searchable(false)
|
|
76
|
+
# Field::Number.new(:id).searchable(true)
|
|
77
|
+
def searchable(value = Configuration::Unset)
|
|
78
|
+
return @searchable if value == Configuration::Unset
|
|
79
|
+
|
|
80
|
+
@searchable = value
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns true if the field is searchable and should be included in the
|
|
85
|
+
# query when a search term has been entered.
|
|
86
|
+
def searchable?
|
|
87
|
+
return default_searchable? if @searchable.nil?
|
|
88
|
+
|
|
89
|
+
!!@searchable
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Sets or gets whether and how this field is sortable.
|
|
93
|
+
#
|
|
94
|
+
# When called with an argument, sets sortable and returns self for chaining.
|
|
95
|
+
# When called without arguments, returns the sortable value.
|
|
96
|
+
#
|
|
97
|
+
# @param value [Boolean, Proc, Symbol] Whether the field is sortable. Pass true/false
|
|
98
|
+
# for simple sorting, or a lambda that receives the query and direction
|
|
99
|
+
# and returns an ActiveRecord::Relation for custom sorting.
|
|
100
|
+
# @return [self, Boolean, Proc] Returns self for method chaining when setting,
|
|
101
|
+
# or the sortable value when getting
|
|
102
|
+
#
|
|
103
|
+
# @example Setting with boolean
|
|
104
|
+
# Field::Number.new(:calculated_sum).sortable(false)
|
|
105
|
+
#
|
|
106
|
+
# @example Setting with lambda
|
|
107
|
+
# Field::Number.new(:users_count).sortable(lambda { |query, direction|
|
|
108
|
+
# query.joins(:users).group(:id).order("COUNT(users.id) #{direction}")
|
|
109
|
+
# })
|
|
110
|
+
#
|
|
111
|
+
# @example Getting
|
|
112
|
+
# field.sortable # => true
|
|
113
|
+
def sortable(value = Configuration::Unset)
|
|
114
|
+
return @sortable if value == Configuration::Unset
|
|
115
|
+
|
|
116
|
+
@sortable = value
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Returns true if the field is sortable
|
|
121
|
+
def sortable?
|
|
122
|
+
return default_sortable? if @sortable.nil?
|
|
123
|
+
|
|
124
|
+
!!@sortable
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
protected
|
|
128
|
+
|
|
129
|
+
def default_on
|
|
130
|
+
[:edit, :index, :show]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def default_searchable?
|
|
134
|
+
false
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def default_sortable?
|
|
138
|
+
true
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
data/lib/uchi/field.rb
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "field/configuration"
|
|
4
|
+
|
|
5
|
+
module Uchi
|
|
6
|
+
class Field
|
|
7
|
+
include Configuration
|
|
8
|
+
|
|
9
|
+
attr_reader :name
|
|
10
|
+
|
|
11
|
+
# The repository this field is associated with.
|
|
12
|
+
attr_accessor :repository
|
|
13
|
+
|
|
14
|
+
def column_name
|
|
15
|
+
name.to_s.humanize
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def group_as(_action)
|
|
19
|
+
:attributes
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def edit_component(form:, repository:, label: nil, hint: nil)
|
|
23
|
+
edit_component_class.new(
|
|
24
|
+
field: self,
|
|
25
|
+
form: form,
|
|
26
|
+
repository: repository,
|
|
27
|
+
label: label,
|
|
28
|
+
hint: hint
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def edit_component_class
|
|
33
|
+
self.class.const_get(:Edit)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def index_component(record:, repository:)
|
|
37
|
+
index_component_class.new(
|
|
38
|
+
field: self,
|
|
39
|
+
record: record,
|
|
40
|
+
repository: repository
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def index_component_class
|
|
45
|
+
self.class.const_get(:Index)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param name [String, Symbol] The name of the field.
|
|
49
|
+
def initialize(name)
|
|
50
|
+
super()
|
|
51
|
+
@name = name.to_sym
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns the key that this field is expected to use in params
|
|
55
|
+
def param_key
|
|
56
|
+
name.to_sym
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns the values to use for permitting this field in strong parameters
|
|
60
|
+
def permitted_param
|
|
61
|
+
param_key
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def show_component(record:, repository:)
|
|
65
|
+
show_component_class.new(
|
|
66
|
+
field: self,
|
|
67
|
+
record: record,
|
|
68
|
+
repository: repository
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def show_component_class
|
|
73
|
+
self.class.const_get(:Show)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def value(record)
|
|
77
|
+
reader.call(record, name)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/uchi/i18n.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
module I18n
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def translate(key, **options)
|
|
8
|
+
scope = options.delete(:scope)
|
|
9
|
+
scope ||= "uchi" unless key.to_s.start_with?("uchi.")
|
|
10
|
+
::I18n.translate(key, **options, scope: scope)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uchi/pagy"
|
|
4
|
+
|
|
5
|
+
require "uchi/pagination/page"
|
|
6
|
+
|
|
7
|
+
module Uchi
|
|
8
|
+
module Pagination
|
|
9
|
+
module Controller
|
|
10
|
+
include Pagy::Method
|
|
11
|
+
|
|
12
|
+
# Set up pagination for the given records. Returns a Page object and the
|
|
13
|
+
# paginated records for that page.
|
|
14
|
+
#
|
|
15
|
+
# @param records [ActiveRecord::Relation] The records to paginate
|
|
16
|
+
#
|
|
17
|
+
# @param records_per_page [Integer] The number of records per page
|
|
18
|
+
#
|
|
19
|
+
# @return [Array<(Uchi::Pagination::Page, ActiveRecord::Relation)>]
|
|
20
|
+
def paginate(records, records_per_page:)
|
|
21
|
+
page, records = pagy(:offset, records, limit: records_per_page)
|
|
22
|
+
[Uchi::Pagination::Page.new(page), records]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
module Pagination
|
|
5
|
+
class Page
|
|
6
|
+
delegate \
|
|
7
|
+
:last,
|
|
8
|
+
:next,
|
|
9
|
+
:page,
|
|
10
|
+
:page_url,
|
|
11
|
+
to: :pagy
|
|
12
|
+
|
|
13
|
+
private attr_reader :pagy
|
|
14
|
+
|
|
15
|
+
def initialize(pagy)
|
|
16
|
+
@pagy = pagy
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017-2025 Domizio Demichelis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
class Pagy
|
|
5
|
+
# Generic option error
|
|
6
|
+
class OptionError < ArgumentError
|
|
7
|
+
attr_reader :pagy, :option, :value
|
|
8
|
+
|
|
9
|
+
# Set the options and prepare the message
|
|
10
|
+
def initialize(pagy, option, description, value)
|
|
11
|
+
@pagy = pagy
|
|
12
|
+
@option = option
|
|
13
|
+
@value = value
|
|
14
|
+
super("expected :#{@option} #{description}; got #{@value.inspect}")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Specific range error
|
|
19
|
+
class RangeError < OptionError; end
|
|
20
|
+
|
|
21
|
+
# I18n localization error
|
|
22
|
+
class RailsI18nLoadError < LoadError; end
|
|
23
|
+
|
|
24
|
+
# Generic internal error
|
|
25
|
+
class InternalError < StandardError; end
|
|
26
|
+
|
|
27
|
+
# JsonApi :page param error
|
|
28
|
+
class JsonapiReservedParamError < StandardError
|
|
29
|
+
# Inform about the actual value
|
|
30
|
+
def initialize(value)
|
|
31
|
+
super("expected reserved :page param to be nil or Hash-like; got #{value.inspect}")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../modules/abilities/shiftable'
|
|
4
|
+
require_relative '../../modules/abilities/rangeable'
|
|
5
|
+
|
|
6
|
+
module Uchi
|
|
7
|
+
class Pagy
|
|
8
|
+
# Implements Offset Pagination
|
|
9
|
+
class Offset < Pagy
|
|
10
|
+
DEFAULT = { page: 1 }.freeze
|
|
11
|
+
|
|
12
|
+
autoload :Countless, Pathname.new(__dir__).join('countless')
|
|
13
|
+
|
|
14
|
+
include Rangeable
|
|
15
|
+
include Shiftable
|
|
16
|
+
|
|
17
|
+
def initialize(**) # rubocop:disable Lint/MissingSuper
|
|
18
|
+
assign_options(**)
|
|
19
|
+
assign_and_check(limit: 1, count: 0, page: 1)
|
|
20
|
+
assign_last
|
|
21
|
+
assign_offset
|
|
22
|
+
return unless in_range? { @page <= @last }
|
|
23
|
+
|
|
24
|
+
@from = [@offset + 1, @count].min
|
|
25
|
+
@to = [@offset + @limit, @count].min
|
|
26
|
+
@in = [@to - @from + 1, @count].min
|
|
27
|
+
assign_previous_and_next
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr_reader :offset, :count, :from, :to, :in, :previous, :last
|
|
31
|
+
|
|
32
|
+
def records(collection)
|
|
33
|
+
collection.offset(@offset).limit(@limit)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
protected
|
|
37
|
+
|
|
38
|
+
def offset? = true
|
|
39
|
+
|
|
40
|
+
def assign_last
|
|
41
|
+
@last = [(@count.to_f / @limit).ceil, 1].max
|
|
42
|
+
@last = @options[:max_pages] if @options[:max_pages] && @last > @options[:max_pages]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def assign_offset
|
|
46
|
+
@offset = (@limit * (@page - 1))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Called by false in_range?
|
|
50
|
+
def assign_empty_page_variables
|
|
51
|
+
@in = @from = @to = 0 # options relative to the actual page
|
|
52
|
+
@previous = @last # @previous relative to the actual page
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
class Pagy
|
|
5
|
+
# Decouple the reuest from the env, allowing non-rack apps to use pagy by passing a hash.
|
|
6
|
+
# Resolve :page and :limit, supporting the :jsonapi option. Support for URL composition.
|
|
7
|
+
class Request
|
|
8
|
+
def initialize(request, options = {})
|
|
9
|
+
@base_url, @path, @query, @cookie =
|
|
10
|
+
if request.is_a?(Hash)
|
|
11
|
+
request.values_at(:base_url, :path, :query, :cookie)
|
|
12
|
+
else
|
|
13
|
+
[request.base_url, request.path, request.GET, request.cookies['pagy']]
|
|
14
|
+
end
|
|
15
|
+
@jsonapi = @query['page'] && options[:jsonapi]
|
|
16
|
+
raise JsonapiReservedParamError, @query['page'] if @jsonapi && !@query['page'].respond_to?(:fetch)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :base_url, :path, :query, :cookie
|
|
20
|
+
|
|
21
|
+
def resolve_page(options, force_integer: true)
|
|
22
|
+
page_key = options[:page_key] || DEFAULT[:page_key]
|
|
23
|
+
page = @jsonapi ? @query['page'][page_key] : @query[page_key]
|
|
24
|
+
page = nil if page == '' # fix for app-generated queries like ?page=
|
|
25
|
+
force_integer ? (page || 1).to_i : page
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def resolve_limit(options)
|
|
29
|
+
limit_key = options[:limit_key] || DEFAULT[:limit_key]
|
|
30
|
+
return options[:limit] || DEFAULT[:limit] \
|
|
31
|
+
unless options[:client_max_limit] &&
|
|
32
|
+
(requested_limit = @jsonapi ? @query['page'][limit_key] : @query[limit_key])
|
|
33
|
+
|
|
34
|
+
[requested_limit.to_i, options[:client_max_limit]].min
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
class Pagy
|
|
5
|
+
# Add configurstion methods
|
|
6
|
+
module Configurable
|
|
7
|
+
# Sync the pagy javascript targets
|
|
8
|
+
def sync_javascript(destination, *targets)
|
|
9
|
+
names = %w[pagy.mjs pagy.js pagy.js.map pagy.min.js]
|
|
10
|
+
targets = names if targets.empty?
|
|
11
|
+
targets.each { |filename| FileUtils.cp(ROOT.join('javascripts', filename), destination) }
|
|
12
|
+
(names - targets).each { |filename| FileUtils.rm_f(File.join(destination, filename)) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Generate the script and style tags to help development
|
|
16
|
+
def dev_tools(wand_scale: 1)
|
|
17
|
+
<<~HTML
|
|
18
|
+
<script id="pagy-ai-widget">
|
|
19
|
+
document.addEventListener('wand-positioned', #{ROOT.join('javascripts/ai_widget.js').read});
|
|
20
|
+
</script>
|
|
21
|
+
<script id="pagy-wand" data-scale="#{wand_scale}">
|
|
22
|
+
#{ROOT.join('javascripts/wand.js').read}
|
|
23
|
+
</script>
|
|
24
|
+
<style id="pagy-wand-default">
|
|
25
|
+
#{ROOT.join('stylesheets/pagy.css').read}
|
|
26
|
+
</style>
|
|
27
|
+
HTML
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Setup pagy for using the i18n gem
|
|
31
|
+
def translate_with_the_slower_i18n_gem!
|
|
32
|
+
send(:remove_const, :I18n)
|
|
33
|
+
send(:const_set, :I18n, ::I18n)
|
|
34
|
+
::I18n.load_path += Dir[ROOT.join('locales/*.yml')]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module Uchi
|
|
6
|
+
class Pagy
|
|
7
|
+
# Provide the helpers to handle the url and anchor
|
|
8
|
+
module Linkable
|
|
9
|
+
module QueryUtils
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Extracted from Rack::Utils and reformatted for rubocop
|
|
13
|
+
# Add the 'unescaped' param, and use it for simple and safe url-templating.
|
|
14
|
+
# All string keyed hashes
|
|
15
|
+
def build_nested_query(value, prefix = nil, unescaped = [])
|
|
16
|
+
case value
|
|
17
|
+
when Array
|
|
18
|
+
value.map { |v| build_nested_query(v, "#{prefix}[]", unescaped) }.join('&')
|
|
19
|
+
when Hash
|
|
20
|
+
value.map do |k, v|
|
|
21
|
+
new_k = prefix ? "#{prefix}[#{escape(k)}]" : escape(k)
|
|
22
|
+
unescaped[unescaped.find_index(k)] = new_k if unescaped.size.positive? && new_k != k && unescaped.include?(k)
|
|
23
|
+
build_nested_query(v, new_k, unescaped)
|
|
24
|
+
end.delete_if(&:empty?).join('&')
|
|
25
|
+
when nil
|
|
26
|
+
escape(prefix)
|
|
27
|
+
else
|
|
28
|
+
raise ArgumentError, 'value must be a Hash' if prefix.nil?
|
|
29
|
+
return "#{escape(prefix)}=#{value}" if unescaped.include?(prefix)
|
|
30
|
+
|
|
31
|
+
"#{escape(prefix)}=#{escape(value)}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def escape(str)
|
|
36
|
+
URI.encode_www_form_component(str)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
protected
|
|
41
|
+
|
|
42
|
+
# Return the URL for the page, relying on the Pagy::Request
|
|
43
|
+
def compose_page_url(page, limit_token: nil, **options)
|
|
44
|
+
jsonapi, page_key, limit_key, limit, client_max_limit, querify, absolute, path, fragment =
|
|
45
|
+
@options.merge(options)
|
|
46
|
+
.values_at(:jsonapi, :page_key, :limit_key, :limit, :client_max_limit, :querify, :absolute, :path, :fragment)
|
|
47
|
+
query = @request.query.clone(freeze: false)
|
|
48
|
+
query.delete(jsonapi ? 'page' : page_key)
|
|
49
|
+
factors = {}.tap do |h|
|
|
50
|
+
h[page_key] = countless? ? "#{page || 1}+#{@last}" : page
|
|
51
|
+
h[limit_key] = limit_token || limit if client_max_limit
|
|
52
|
+
end.compact # No empty params
|
|
53
|
+
query.merge!(jsonapi ? { 'page' => factors } : factors) if factors.size.positive?
|
|
54
|
+
querify&.(query) # Must modify the query: the returned value is ignored
|
|
55
|
+
query_string = QueryUtils.build_nested_query(query, nil, [page_key, limit_key])
|
|
56
|
+
query_string = "?#{query_string}" unless query_string.empty?
|
|
57
|
+
fragment &&= %(##{fragment}) unless fragment&.start_with?('#')
|
|
58
|
+
"#{@request.base_url if absolute}#{path || @request.path}#{query_string}#{fragment}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
class Pagy
|
|
5
|
+
# Add method supporting range checking, range error and rescue
|
|
6
|
+
module Rangeable
|
|
7
|
+
# Check if in range
|
|
8
|
+
def in_range?
|
|
9
|
+
return @in_range if defined?(@in_range) || (@in_range = yield)
|
|
10
|
+
raise RangeError.new(self, :page, "in 1..#{@last}", @page) if @options[:raise_range_error]
|
|
11
|
+
|
|
12
|
+
assign_empty_page_variables
|
|
13
|
+
false
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
class Pagy
|
|
5
|
+
# Provide a ready to use pagy environment when included in irb/rails console
|
|
6
|
+
module Console
|
|
7
|
+
class Request
|
|
8
|
+
attr_accessor :base_url, :path, :params
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@base_url = 'http://www.example.com'
|
|
12
|
+
@path = '/path'
|
|
13
|
+
@params = { example: '123' }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def GET = @params # rubocop:disable Naming/MethodName
|
|
17
|
+
|
|
18
|
+
def cookies = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Collection < Array
|
|
22
|
+
def initialize(arr = Array(1..1000))
|
|
23
|
+
super
|
|
24
|
+
@collection = clone
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def offset(value) = tap { @collection = self[value..] }
|
|
28
|
+
def limit(value) = @collection[0, value]
|
|
29
|
+
def count(*) = size
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
include Method
|
|
33
|
+
|
|
34
|
+
# Direct reference to request.params via a method
|
|
35
|
+
def params = request.params
|
|
36
|
+
def request = @request ||= Request.new
|
|
37
|
+
def collection = Collection
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|