warped 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +25 -0
  3. data/Gemfile +0 -2
  4. data/Gemfile.lock +25 -19
  5. data/README.md +116 -270
  6. data/app/assets/config/warped_manifest.js +2 -0
  7. data/app/assets/javascript/warped/controllers/filter_controller.js +76 -0
  8. data/app/assets/javascript/warped/controllers/filters_controller.js +21 -0
  9. data/app/assets/javascript/warped/index.js +2 -0
  10. data/app/assets/stylesheets/warped/application.css +15 -0
  11. data/app/assets/stylesheets/warped/base.css +23 -0
  12. data/app/assets/stylesheets/warped/filters.css +115 -0
  13. data/app/assets/stylesheets/warped/pagination.css +74 -0
  14. data/app/assets/stylesheets/warped/search.css +33 -0
  15. data/app/assets/stylesheets/warped/table.css +114 -0
  16. data/app/views/warped/_actions.html.erb +9 -0
  17. data/app/views/warped/_cell.html.erb +3 -0
  18. data/app/views/warped/_column.html.erb +35 -0
  19. data/app/views/warped/_filters.html.erb +21 -0
  20. data/app/views/warped/_hidden_fields.html.erb +19 -0
  21. data/app/views/warped/_pagination.html.erb +34 -0
  22. data/app/views/warped/_row.html.erb +19 -0
  23. data/app/views/warped/_search.html.erb +21 -0
  24. data/app/views/warped/_table.html.erb +52 -0
  25. data/app/views/warped/filters/_filter.html.erb +40 -0
  26. data/config/importmap.rb +3 -0
  27. data/docs/controllers/FILTERABLE.md +193 -0
  28. data/docs/controllers/PAGEABLE.md +70 -0
  29. data/docs/controllers/README.md +8 -0
  30. data/docs/controllers/SEARCHABLE.md +95 -0
  31. data/docs/controllers/SORTABLE.md +94 -0
  32. data/docs/controllers/TABULATABLE.md +28 -0
  33. data/docs/controllers/views/PARTIALS.md +285 -0
  34. data/docs/jobs/README.md +22 -0
  35. data/docs/services/README.md +81 -0
  36. data/lib/generators/warped/install_generator.rb +1 -1
  37. data/lib/warped/api/filter/base/value.rb +52 -0
  38. data/lib/warped/api/filter/base.rb +84 -0
  39. data/lib/warped/api/filter/boolean.rb +41 -0
  40. data/lib/warped/api/filter/date.rb +26 -0
  41. data/lib/warped/api/filter/date_time.rb +32 -0
  42. data/lib/warped/api/filter/decimal.rb +31 -0
  43. data/lib/warped/api/filter/factory.rb +38 -0
  44. data/lib/warped/api/filter/integer.rb +38 -0
  45. data/lib/warped/api/filter/string.rb +25 -0
  46. data/lib/warped/api/filter/time.rb +25 -0
  47. data/lib/warped/api/filter.rb +14 -0
  48. data/lib/warped/api/sort/value.rb +40 -0
  49. data/lib/warped/api/sort.rb +65 -0
  50. data/lib/warped/controllers/filterable/ui.rb +46 -0
  51. data/lib/warped/controllers/filterable.rb +79 -42
  52. data/lib/warped/controllers/pageable/ui.rb +70 -0
  53. data/lib/warped/controllers/pageable.rb +11 -11
  54. data/lib/warped/controllers/searchable/ui.rb +37 -0
  55. data/lib/warped/controllers/searchable.rb +2 -0
  56. data/lib/warped/controllers/sortable/ui.rb +53 -0
  57. data/lib/warped/controllers/sortable.rb +53 -33
  58. data/lib/warped/controllers/tabulatable/ui.rb +54 -0
  59. data/lib/warped/controllers/tabulatable.rb +13 -27
  60. data/lib/warped/emails/components/align.rb +21 -0
  61. data/lib/warped/emails/components/base.rb +116 -0
  62. data/lib/warped/emails/components/button.rb +58 -0
  63. data/lib/warped/emails/components/divider.rb +15 -0
  64. data/lib/warped/emails/components/heading.rb +65 -0
  65. data/lib/warped/emails/components/layouts/columns.rb +36 -0
  66. data/lib/warped/emails/components/layouts/cta.rb +38 -0
  67. data/lib/warped/emails/components/layouts/main.rb +34 -0
  68. data/lib/warped/emails/components/link.rb +36 -0
  69. data/lib/warped/emails/components/spacer.rb +15 -0
  70. data/lib/warped/emails/components/stepper.rb +104 -0
  71. data/lib/warped/emails/components/table.rb +37 -0
  72. data/lib/warped/emails/components/text.rb +67 -0
  73. data/lib/warped/emails/helpers.rb +26 -0
  74. data/lib/warped/emails/slottable.rb +61 -0
  75. data/lib/warped/emails/styleable.rb +160 -0
  76. data/lib/warped/engine.rb +19 -0
  77. data/lib/warped/queries/filter.rb +32 -12
  78. data/lib/warped/table/action.rb +33 -0
  79. data/lib/warped/table/column.rb +34 -0
  80. data/lib/warped/version.rb +1 -1
  81. data/lib/warped.rb +2 -0
  82. data/warped.gemspec +1 -1
  83. metadata +73 -7
  84. data/lib/warped/emails/.keep +0 -0
@@ -0,0 +1,52 @@
1
+ <%# locals: (collection:, path:, columns:, turbo_action: :replace, actions: [], **options) %>
2
+
3
+
4
+ <%= tag.div class: 'warped-table' do %>
5
+ <div class="warped-table--controls">
6
+ <% if controller.class.include?(Warped::Controllers::Searchable::Ui) %>
7
+ <%= render "warped/search", path:, turbo_action:, **(options[:search].presence || {}) %>
8
+ <% end %>
9
+
10
+ <% if controller.class.include?(Warped::Controllers::Filterable::Ui) %>
11
+ <%= render "warped/filters", path:, turbo_action:, **(options[:filters].presence || {}) %>
12
+ <% end %>
13
+ </div>
14
+
15
+
16
+
17
+ <% table_opts = options[:table]&.deep_dup.presence || {} %>
18
+ <% table_class = "warped-table--table #{table_opts.delete(:class)}" %>
19
+
20
+ <div class="warped-table--container">
21
+ <%= tag.div class: table_class, **table_opts do %>
22
+ <div class="warped-table--table--header">
23
+ <div class="warped-table--table--row">
24
+ <% columns.each do |column| %>
25
+ <%= render "warped/column", column:, path:, turbo_action: %>
26
+ <% end %>
27
+
28
+ <% if actions.any? %>
29
+ <div class="warped-table--table--cell">
30
+ Actions
31
+ </div>
32
+ <% end %>
33
+ </div>
34
+ </div>
35
+ <div class="warped-table--table--row-group">
36
+ <div class="warped-table--table--empty-row">
37
+ <div class="warped-table--table--cell">
38
+ Whoops! Nothing over here!
39
+ </div>
40
+ </div>
41
+ <% if (block = yield).present? %>
42
+ <%= block %>
43
+ <% else %>
44
+ <% collection.each do |resource| %>
45
+ <%= render "warped/row", resource:, columns:, actions: %>
46
+ <% end %>
47
+ <% end %>
48
+ </div>
49
+ <% end %>
50
+ </div>
51
+ <%= render("warped/pagination", path:, turbo_action:, **(options[:pagination].presence || {})) if controller.class.include?(Warped::Controllers::Pageable::Ui) %>
52
+ <% end %>
@@ -0,0 +1,40 @@
1
+ <%# locals: (form:, filter:) %>
2
+
3
+ <% filter_value = current_action_filter_values.find { |v| v.filter.name == filter.name } %>
4
+
5
+ <div class="warped-filters--filter <%= 'warped-filters--filter-inactive' if filter_value.blank? %>" data-controller="filter" data-filter-filter-outlet=".warped-filters--filter" data-filter-empty-class="warped-filters--filter-inactive" data-filter-collapsed-class="warped-filters--filter--panel-collapsed" data-action="keydown.esc@window->filter#close click@window->filter#clickOutside">
6
+ <div class="warped-filters--filter--icon" data-action="click->filter#toggle">
7
+ <% if !filter_value.nil? %>
8
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
9
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
10
+ </svg>
11
+ <% else %>
12
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
13
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
14
+ </svg>
15
+ <% end %>
16
+ </div>
17
+
18
+ <%= form.label filter.parameter_name, class: "warped-filters--filter--label" %>
19
+
20
+ <div data-filter-target="badgeValue" class="warped-filters--filter--value">
21
+ <span class="warped-filters--filter--value--values" data-filter-target="relation">
22
+ <%= filter_value&.relation if !filter_value&.empty? %>
23
+ </span>
24
+
25
+ <span class="warped-filters--filter--value--values" data-filter-target="value">
26
+ <%= ": #{filter_value&.value}" if !filter_value&.html_value.nil? %>
27
+ </span>
28
+ </div>
29
+
30
+ <div class="warped-filters--filter--panel-collapsed warped-filters--filter--panel" data-filter-target="panel">
31
+ <%= form.select "#{filter.parameter_name}.rel", options_for_select(filter.relations, include_blank: true, selected: filter_value&.relation), {}, class: "warped-filters--filter--panel--select", data: { filter_target: "relationInput", action: "change->filter#changeRelation" } %>
32
+ <%= tag :input, type: filter.html_type, name: filter.parameter_name, value: filter_value&.html_value, class: "warped-filters--filter--panel--input", data: { filter_target: "valueInput", action: "keyup->filter#changeValue" } %>
33
+
34
+ <span class="warped-filters--filter--panel--remove" data-action="click->filter#clear click->filter#close">
35
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
36
+ <path stroke-linecap="round" stroke-linejoin="round" d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
37
+ </svg>
38
+ </span>
39
+ </div>
40
+ </div>
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ pin_all_from File.expand_path("../app/assets/javascript", __dir__)
@@ -0,0 +1,193 @@
1
+ # Warped::Controllers::Filterable
2
+
3
+ The `Filterable` concern provides a method to filter the records in a controller's action.
4
+ The method `filterable_by` is used to define the filterable fields and the filter method to use.
5
+
6
+ ```ruby
7
+ class UsersController < ApplicationController
8
+ include Warped::Controllers::Filterable
9
+
10
+ filterable_by :name, :email, :created_at
11
+
12
+ def index
13
+ users = filter(User.all)
14
+ render json: users
15
+ end
16
+ end
17
+ ```
18
+ The `filter` method will use the query parameters to filter the records. For example, to filter the users by name, email, and created_at, the following query parameters can be used:
19
+
20
+ ```
21
+ GET /users?name=John
22
+ GET /users?email=john@example.com
23
+ GET /users?created_at=2021-01-01
24
+ ```
25
+
26
+ ## Adding type-safety to the fields
27
+
28
+ The `filterable_by` method, accepts keyword arguments to be passed. The keyword arguments are the field names and the type to cast the query parameter to. This is useful to prevent invalid queries from being executed on the database.
29
+
30
+ ```ruby
31
+ class UsersController < ApplicationController
32
+ include Warped::Controllers::Filterable
33
+
34
+ filterable_by name: { kind: :string }, email: { kind: :string }, created_at: { kind: :date }
35
+
36
+ def index
37
+ users = filter(User.all)
38
+ render json: users
39
+ end
40
+ end
41
+ ```
42
+
43
+ When passing a value to the query parameter, the value will be cast to the specified type. If casting fails, the query parameter will be ignored.
44
+
45
+ If the strict flag is passed to the filterable_by method, then it will raise a `Filter::ValueError`.
46
+
47
+ ```ruby
48
+ class UsersController < ApplicationController
49
+ include Warped::Controllers::Filterable
50
+
51
+ filterable_by created_at: { kind: :date }, strict: true
52
+
53
+ def index
54
+ users = filter(User.all)
55
+ render json: users
56
+ end
57
+ end
58
+ ```
59
+
60
+ Request examples:
61
+ ```
62
+ GET /users?created_at=2021-01-01 # returns users created at 2021-01-01
63
+ GET /users?created_at=not_a_date # Raises a Filter::ValueError, with the message 'not_a_date' cannot be casted to date
64
+ ```
65
+
66
+ ## Handling invalid filter values in strict mode
67
+
68
+ ```ruby
69
+ class UsersController < ApplicationController
70
+ include Warped::Controllers::Filterable
71
+
72
+ rescue_from Filter::ValueError, with: :render_invalid_filter_value
73
+ rescue_from Filter::RelationError, with: :render_invalid_filter_relation
74
+
75
+ filterable_by age: { kind: :integer }, strict: true
76
+
77
+ def index
78
+ users = filter(User.all)
79
+ render json: users
80
+ end
81
+
82
+ private
83
+
84
+ # default handler for invalid filter values
85
+ def render_invalid_filter_value(exception)
86
+ render json: { error: exception.message }, status: :bad_request
87
+ end
88
+
89
+ def render_invalid_filter_relation(exception)
90
+ render json: { error: exception.message }, status: :bad_request
91
+ end
92
+ end
93
+ ```
94
+
95
+ Request examples:
96
+ ```
97
+ GET /users?age=18 # returns users with age 18
98
+ GET /users?age=not_an_integer
99
+ # returns a 400 (Bad Request), with the message "'not_an_integer' cannot be casted to integer"
100
+ ```
101
+
102
+ ## Referencing tables in the filterable fields
103
+
104
+ The `filterable_by` method can also be used to reference fields in associated tables. For example, to filter the users by the name of the company they work for, the following can be done:
105
+
106
+ ```ruby
107
+ class UsersController < ApplicationController
108
+ include Warped::Controllers::Filterable
109
+
110
+ filterable_by :name, 'companies.name', "companies.created_at" => { kind: :date_time }
111
+
112
+ def index
113
+ users = filter(User.joins(:company))
114
+ render json: users
115
+ end
116
+ end
117
+ ```
118
+
119
+ Request examples:
120
+ ```
121
+ GET /users?name=John
122
+ GET /users?companies.name=Acme
123
+ GET /users?companies.created_at=2021-01-01T00:00:00
124
+ ```
125
+
126
+ ## Renaming the filter query parameters
127
+
128
+ If you don't want to use the field name as the query parameter (as to not expose the database schema, or when joining the same table multiple times),
129
+ you can specify the query parameter to use for each field:
130
+
131
+ ```ruby
132
+ class UsersController < ApplicationController
133
+ include Warped::Controllers::Filterable
134
+
135
+ filterable_by 'companies.name' => { kind: :string, alias_name: :company_name },
136
+ 'users.name' => { kind: :string, alias_name: :user_name }
137
+
138
+
139
+ def index
140
+ users = filter(User.join(:company))
141
+ render json: users
142
+ end
143
+ end
144
+ ```
145
+
146
+ Request examples:
147
+ ```
148
+ GET /users?user_name=John
149
+ GET /users?company_name=Acme
150
+ ```
151
+
152
+ ## Using filters other than `eq`
153
+
154
+ By default, the `filter` method will use the `eq` filter method to filter the records. If you want to use a different filter method, you can specify the filter "relation" in the query parameter:
155
+
156
+ ```ruby
157
+ class UsersController < ApplicationController
158
+ include Warped::Controllers::Filterable
159
+
160
+ filterable_by name: { kind: :string }, age: { kind: :integer }
161
+
162
+ def index
163
+ users = filter(User.all)
164
+ render json: users
165
+ end
166
+ end
167
+ ```
168
+
169
+ Request examples:
170
+ ```
171
+ GET /users?name=John # returns users with name John
172
+ GET /users?name[]=John&name[]=Jane # returns users where the name is in ('John', 'Jane')
173
+ GET /users?age.rel=is_null # returns users where the age is null
174
+ GET /users?age.rel=is_not_null # returns users where the age is not null
175
+ GET /users?age.rel=between&age[]=18&age[]=30 # returns users with age between 18 and 30
176
+ GET /users?age.rel=gt&age=18 # returns users with age greater than 18
177
+ ```
178
+
179
+ The full list of filter relations is:
180
+ - `eq` (default) - equals
181
+ - `neq` - not equals
182
+ - `gt` - greater than
183
+ - `gte` - greater than or equals
184
+ - `lt` - less than
185
+ - `lte` - less than or equals
186
+ - `between` - between (requires two values)
187
+ - `in` - in (default when multiple values are provided)
188
+ - `not_in` - not in (requires multiple values)
189
+ - `starts_with` - starts with
190
+ - `ends_with` - ends with
191
+ - `contains` - contains
192
+ - `is_null` - is null (does not require a value)
193
+ - `is_not_null` - is not null (does not require a value)
@@ -0,0 +1,70 @@
1
+ # Warped::Controllers::Pageable
2
+
3
+ The `Pageable` concern provides a method to paginate the records in a controller's action.
4
+
5
+ The method `paginate` is used to paginate the records.
6
+ It will use the query parameters `page` and `per_page` to paginate the records.
7
+
8
+ ```ruby
9
+ class UsersController < ApplicationController
10
+ include Warped::Controllers::Pageable
11
+
12
+ def index
13
+ users = paginate(User.all)
14
+ render json: users, meta: pagination
15
+ end
16
+ end
17
+ ```
18
+
19
+ Request examples:
20
+ ```
21
+ GET /users?page=1&per_page=10 # returns the first page of users with 10 records per page
22
+ GET /users?per_page=25 # returns the first page of users with 25 records per page
23
+ GET /users?page=2&per_page=25 # returns the second page of users with 25 records per page
24
+ ```
25
+
26
+ ## Accessing the pagination information
27
+
28
+ The `pagination` method can be used to access the pagination information.
29
+
30
+ ```ruby
31
+ class UsersController < ApplicationController
32
+ include Warped::Controllers::Pageable
33
+
34
+ def index
35
+ users = paginate(User.all)
36
+ render json: users, meta: pagination
37
+ end
38
+ end
39
+ ```
40
+
41
+ `pagination` returns a hash with
42
+ - `page` - the current page
43
+ - `per_page` - the number of records per page
44
+ - `total_pages` - the total number of pages
45
+ - `total_count` - the number of records in the scope
46
+ - `next-page` - the next page number
47
+ - `prev-page` - the previous page number
48
+
49
+
50
+ ## Customizing the pagination behavior
51
+
52
+ By default, the `paginate` method will paginate the scope in pages of size 10, and will return the first page if the `page` query parameter is not provided.
53
+
54
+ Additionally, there's a limit of `100` records per page. So, if the `per_page` query parameter is greater than `100`, the pagination will use `100` as the page size.
55
+
56
+ You can customize the default page size and the default page number by overriding the `default_per_page` value in the controller.
57
+
58
+ ```ruby
59
+ class UsersController < ApplicationController
60
+ include Warped::Controllers::Pageable
61
+
62
+ # This will set the default page size to 25 when the `per_page` query parameter is not provided
63
+ self.default_per_page = 25
64
+
65
+ def index
66
+ users = paginate(User.all)
67
+ render json: users, meta: pagination
68
+ end
69
+ end
70
+ ```
@@ -0,0 +1,8 @@
1
+ # Warped::Controllers
2
+
3
+ The `Warped::Controllers` module defines five concerns that can be included in a controller to provide additional functionality:
4
+ - [Warped::Controllers::Filterable](FILTERABLE.md)
5
+ - [Warped::Controllers::Searchable](SEARCHABLE.md)
6
+ - [Warped::Controllers::Sortable](SORTABLE.md)
7
+ - [Warped::Controllers::Pageable](PAGEABLE.md)
8
+ - [Warped::Controllers::Tabulatable](TABULATABLE.md)
@@ -0,0 +1,95 @@
1
+ # Warped::Controllers::Searchable
2
+
3
+ The `Searchable` concern provides a method to search the records in a controller's action.
4
+
5
+ By default it calls the scope `search` on the model, and uses the query parameter `q` to search the records.
6
+
7
+ ```ruby
8
+ # app/models/user.rb
9
+ class User < ApplicationRecord
10
+ # You can define your own search scope and use standard sql
11
+ # scope :search, ->(query) { where('name LIKE ?', "%#{query}%") }
12
+
13
+ # Or use pg_search
14
+ include PgSearch::Model
15
+ pg_search_scope :search, against: [:name, :email]
16
+ end
17
+
18
+ # app/controllers/users_controller.rb
19
+ class UsersController < ApplicationController
20
+ include Warped::Controllers::Searchable
21
+
22
+ def index
23
+ users = search(User.all)
24
+ render json: users
25
+ end
26
+ end
27
+ ```
28
+
29
+ Request examples:
30
+ ```
31
+ GET /users?q=John
32
+ # calls #search(User.all, search_term: 'John', model_search_scope: :search) in the controller
33
+ ```
34
+
35
+ ## Customizing the search query parameter
36
+
37
+ You can customize the default query parameter by:
38
+ 1. Passing fetching the fetch term from the params hash, and passing it directly to the search method:
39
+
40
+ ```ruby
41
+ class UsersController < ApplicationController
42
+ include Warped::Controllers::Searchable
43
+
44
+ def index
45
+ # This will use the query parameter `term` instead of `q`
46
+ users = search(User.all, search_term: params[:term])
47
+ render json: users
48
+ end
49
+ end
50
+ ```
51
+
52
+ 2. Overriding the `search_param` method in the controller
53
+
54
+ ```ruby
55
+ class UsersController < ApplicationController
56
+ include Warped::Controllers::Searchable
57
+
58
+ def index
59
+ # This will use the query parameter `term` instead of `q`
60
+ users = search(User.all)
61
+ render json: users
62
+ end
63
+
64
+ private
65
+
66
+ def search_param
67
+ :term
68
+ end
69
+ end
70
+ ```
71
+
72
+ 3. Calling #searchable_by in the controller and overriding the default query parameter
73
+
74
+ ```ruby
75
+ # app/models/user.rb
76
+ class User < ApplicationRecord
77
+ include PgSearch::Model
78
+ pg_search_scope :search_by_word, against: :name, using: { tsearch: { any_word: true } }
79
+ end
80
+
81
+
82
+ # app/controllers/users_controller.rb
83
+ class UsersController < ApplicationController
84
+ include Warped::Controllers::Searchable
85
+
86
+ # This will use the query parameter `term` instead of `q`
87
+ # and the search scope `search_by_word` instead of the default
88
+ searchable_by :search_by_word, param: :term
89
+
90
+ def index
91
+ users = search(User.all)
92
+ render json: users
93
+ end
94
+ end
95
+ ```
@@ -0,0 +1,94 @@
1
+ # Warped::Controllers::Sortable
2
+
3
+ The `Sortable` concern provides a method to sort the records in a controller's action.
4
+
5
+ The method `sortable_by` is used to define the sortable fields.
6
+
7
+ ```ruby
8
+ class UsersController < ApplicationController
9
+ include Warped::Controllers::Sortable
10
+
11
+ sortable_by :name, :created_at
12
+
13
+ def index
14
+ users = sort(User.all)
15
+ render json: users
16
+ end
17
+ end
18
+ ```
19
+
20
+ This will use the query parameter `sort_key` and `sort_direction` to sort the records.
21
+ - The default sort direction is `desc`.
22
+ - The default sort key is `:id`.
23
+
24
+ Example requests:
25
+ ```
26
+ GET /users?sort_key=name # sort by name in descending order
27
+ GET /users?sort_key=created_at&sort_direction=asc # sort by created_at in ascending order
28
+ ```
29
+
30
+ When calling sort in a controller action, and the sort parameters are not provided, the default sort key and direction will be used.
31
+
32
+ ```ruby
33
+ class UsersController < ApplicationController
34
+ include Warped::Controllers::Sortable
35
+
36
+ sortable_by :name, :created_at
37
+
38
+ def index
39
+ users = sort(User.all)
40
+ render json: users
41
+ end
42
+ end
43
+ ```
44
+
45
+ Request examples:
46
+ ```
47
+ GET /users # sort by id in descending order
48
+ ```
49
+ ## Referencing tables in the sortable fields
50
+
51
+ Like the `filterable_by` method, the `sortable_by` method can also be used to reference fields in associated tables.
52
+
53
+ ```ruby
54
+ class UsersController < ApplicationController
55
+ include Warped::Controllers::Sortable
56
+
57
+ sortable_by :name, 'companies.name'
58
+
59
+ def index
60
+ users = sort(User.joins(:company))
61
+ render json: users
62
+ end
63
+ end
64
+ ```
65
+
66
+ Request examples:
67
+ ```
68
+ GET /users?sort_key=name # sort by name in descending order
69
+ GET /users?sort_key=companies.name&sort_direction=asc # sort by company name in ascending order
70
+ ```
71
+
72
+ ## Renaming the sort query parameters
73
+
74
+ If you don't want to use the field name as the query parameter (as to not expose the database schema, or when joining the same table multiple times),
75
+ you can specify the query parameter to use for each field:
76
+
77
+ ```ruby
78
+ class UsersController < ApplicationController
79
+ include Warped::Controllers::Sortable
80
+
81
+ sortable_by 'companies.name' => :company_name, 'users.name' => :user_name
82
+
83
+ def index
84
+ users = sort(User.join(:company))
85
+ render json: users
86
+ end
87
+ end
88
+ ```
89
+
90
+ Request examples:
91
+ ```
92
+ GET /users?sort_key=user_name # sort by name in descending order
93
+ GET /users?sort_key=company_name&sort_direction=asc # sort by company name in ascending order
94
+ ```
@@ -0,0 +1,28 @@
1
+ # Warped::Controllers::Tabulatable
2
+
3
+ The `Tabulatable` concern provides a method to filter, sort, search, and paginate the records in a controller's action.
4
+
5
+ The method `tabulate` is used to filter, sort, search, and paginate the records. So, in the case that the controller action needs to filter, sort, search, and paginate the records, the `tabulate` method can be used.
6
+
7
+ The tabulatable concern provides the `tabulatable_by` method, which passes the values to `filterable_by` and `sortable_by`.
8
+
9
+ ```ruby
10
+ class UsersController < ApplicationController
11
+ include Warped::Controllers::Tabulatable
12
+
13
+ tabulatable_by :name, :email, :created_at
14
+
15
+ def index
16
+ users = tabulate(User.all)
17
+ render json: users, meta: pagination
18
+ end
19
+ end
20
+ ```
21
+
22
+ Request examples:
23
+ ```
24
+ GET /users?age[]=18&age[]=30&age.rel=between&sort_key=name&sort_direction=asc&q=John&page=2&per_page=10
25
+ # returns the second page of users with 10 records per page, where the age is between 18 and 30, sorted by name in ascending order, and searched by the term John
26
+ ```
27
+
28
+ Just like `paginate`, when calling the `tabulate` method in the controller action, the `pagination` method can be used to access the pagination information.