warped 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +104 -0
  5. data/app/assets/config/warped_manifest.js +2 -0
  6. data/app/assets/javascript/warped/controllers/filter_controller.js +76 -0
  7. data/app/assets/javascript/warped/controllers/filters_controller.js +21 -0
  8. data/app/assets/javascript/warped/index.js +2 -0
  9. data/app/assets/stylesheets/warped/application.css +15 -0
  10. data/app/assets/stylesheets/warped/base.css +23 -0
  11. data/app/assets/stylesheets/warped/filters.css +115 -0
  12. data/app/assets/stylesheets/warped/pagination.css +74 -0
  13. data/app/assets/stylesheets/warped/search.css +33 -0
  14. data/app/assets/stylesheets/warped/table.css +114 -0
  15. data/app/views/warped/_actions.html.erb +9 -0
  16. data/app/views/warped/_cell.html.erb +3 -0
  17. data/app/views/warped/_column.html.erb +35 -0
  18. data/app/views/warped/_filters.html.erb +21 -0
  19. data/app/views/warped/_hidden_fields.html.erb +19 -0
  20. data/app/views/warped/_pagination.html.erb +34 -0
  21. data/app/views/warped/_row.html.erb +19 -0
  22. data/app/views/warped/_search.html.erb +21 -0
  23. data/app/views/warped/_table.html.erb +52 -0
  24. data/app/views/warped/filters/_filter.html.erb +40 -0
  25. data/config/importmap.rb +3 -0
  26. data/docs/controllers/FILTERABLE.md +82 -3
  27. data/docs/controllers/views/PARTIALS.md +285 -0
  28. data/lib/warped/api/filter/base/value.rb +52 -0
  29. data/lib/warped/api/filter/base.rb +84 -0
  30. data/lib/warped/api/filter/boolean.rb +41 -0
  31. data/lib/warped/api/filter/date.rb +26 -0
  32. data/lib/warped/api/filter/date_time.rb +32 -0
  33. data/lib/warped/api/filter/decimal.rb +31 -0
  34. data/lib/warped/api/filter/factory.rb +38 -0
  35. data/lib/warped/api/filter/integer.rb +38 -0
  36. data/lib/warped/api/filter/string.rb +25 -0
  37. data/lib/warped/api/filter/time.rb +25 -0
  38. data/lib/warped/api/filter.rb +14 -0
  39. data/lib/warped/api/sort/value.rb +40 -0
  40. data/lib/warped/api/sort.rb +65 -0
  41. data/lib/warped/controllers/filterable/ui.rb +10 -32
  42. data/lib/warped/controllers/filterable.rb +75 -42
  43. data/lib/warped/controllers/pageable/ui.rb +13 -3
  44. data/lib/warped/controllers/pageable.rb +1 -1
  45. data/lib/warped/controllers/searchable/ui.rb +3 -1
  46. data/lib/warped/controllers/sortable/ui.rb +21 -26
  47. data/lib/warped/controllers/sortable.rb +53 -33
  48. data/lib/warped/controllers/tabulatable/ui.rb +4 -0
  49. data/lib/warped/controllers/tabulatable.rb +6 -9
  50. data/lib/warped/engine.rb +19 -0
  51. data/lib/warped/queries/filter.rb +3 -3
  52. data/lib/warped/table/action.rb +33 -0
  53. data/lib/warped/table/column.rb +34 -0
  54. data/lib/warped/version.rb +1 -1
  55. data/lib/warped.rb +1 -0
  56. data/warped.gemspec +1 -1
  57. metadata +44 -6
@@ -0,0 +1,35 @@
1
+ <%# locals: (path:, column:, turbo_action:) %>
2
+
3
+ <div class="warped-table--table--cell">
4
+ <% if controller.class.include?(Warped::Controllers::Sortable::Ui) && sorted? && sortable_field?(column.parameter_name) %>
5
+ <% html_form_options = {}.tap do |hash| %>
6
+ <% hash.merge!(filter_url_params) if try(:filtered?) %>
7
+ <% hash.merge!(paginate_url_params) if try(:sorted?) %>
8
+ <% hash.merge!(search_url_params) if try(:searched?) %>
9
+ <% end %>
10
+
11
+ <% column_sorted = sorted_field?(column.parameter_name) %>
12
+ <% sort_direction = column_sorted ? current_action_sort_value.opposite_direction : default_sort_direction %>
13
+
14
+ <%= button_to(path, { "data-turbo-action" => turbo_action, method: :get, params: html_form_options.merge(sort_key: column.parameter_name, sort_direction: sort_direction) }) do %>
15
+ <% if column_sorted %>
16
+ <% if current_action_sort_value.asc? %>
17
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
18
+ <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 15.75 7.5-7.5 7.5 7.5" />
19
+ </svg>
20
+ <% else %>
21
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
22
+ <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
23
+ </svg>
24
+ <% end %>
25
+ <% else %>
26
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
27
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
28
+ </svg>
29
+ <% end %>
30
+ <span> <%= column.display_name %> </span>
31
+ <% end %>
32
+ <% else %>
33
+ <%= column.display_name %>
34
+ <% end %>
35
+ </div>
@@ -0,0 +1,21 @@
1
+ <%# locals: (path:, turbo_action:, **options) %>
2
+
3
+ <% opts = options.deep_dup %>
4
+
5
+ <% data = opts.delete(:data) { {} } %>
6
+ <% data.merge!(controller: "filters #{data[:controller]}", filters_filter_outlet: ".warped-filters--filter", turbo_action:) %>
7
+
8
+ <% html = opts.extract!(:html) %>
9
+ <% html_class = opts.delete(:class) %>
10
+ <% html.merge!(class: "warped-filters #{html_class}") %>
11
+
12
+ <%= form_with url: path, method: :get, html:, data:, **opts do |f| %>
13
+ <%= f.submit "Filter", class: "warped-filters--submit" %>
14
+ <%= f.button "Clear", type: :reset, class: "warped-filters--clear", data: { action: "filters#clearAll" } %>
15
+
16
+ <% current_action_filters.each do |filter| %>
17
+ <%= render "warped/filters/filter", form: f, filter: filter %>
18
+ <% end %>
19
+
20
+ <%= render "warped/hidden_fields", form: f, modules: %i[sortable searchable] %>
21
+ <% end %>
@@ -0,0 +1,19 @@
1
+ <%# locals: (form:, modules:) %>
2
+
3
+ <% Array.wrap(modules).each do |module_name| %>
4
+ <% if module_name.to_sym == :searchable && try(:searched?) %>
5
+ <%= form.hidden_field search_param, value: search_term %>
6
+ <% elsif module_name.to_sym == :filterable && try(:filtered?) %>
7
+ <% filter_url_params.each do |key, value| %>
8
+ <%= form.hidden_field key, value: %>
9
+ <% end %>
10
+ <% elsif module_name.to_sym == :sortable && try(:sorted?) %>
11
+ <% sort_url_params.each do |key, value| %>
12
+ <%= form.hidden_field key, value: %>
13
+ <% end %>
14
+ <% elsif module_name.to_sym == :pageable && try(:paginated?) %>
15
+ <% paginate_url_params.each do |key, value| %>
16
+ <%= form.hidden_field key, value: %>
17
+ <% end %>
18
+ <% end %>
19
+ <% end %>
@@ -0,0 +1,34 @@
1
+ <%# locals: (path:, turbo_action:, **options) %>
2
+
3
+ <% opts = options.deep_dup %>
4
+ <% pagination_class = "warped--pagination #{opts.delete(:class)}" %>
5
+
6
+ <% uri = URI(path) %>
7
+
8
+ <% html_form_options = {}.tap do |hash| %>
9
+ <% hash.merge!(sort_url_params) if try(:sorted?) %>
10
+ <% hash.merge!(filter_url_params) if try(:filtered?) %>
11
+ <% hash.merge!(search_url_params) if try(:searched?) %>
12
+ <% end %>
13
+
14
+ <%= tag.nav(class: pagination_class, role: "navigation", **opts) do %>
15
+ <% if pagination[:prev_page] -%> <%= button_to("Previous", path, { "data-turbo-action" => turbo_action, method: :get, form_class: "warped--pagination--btn", params: html_form_options.merge(page: pagination[:prev_page]) }) %>
16
+ <% else -%> <span class="warped--pagination--btn warped--pagination--btn-disabled" disabled>Previous</span>
17
+ <% end -%>
18
+
19
+ <% pagination[:series].each do |item| -%>
20
+ <% if item.is_a?(Integer) -%> <%= button_to(item, path, { "data-turbo-action" => turbo_action, method: :get, form_class: "warped--pagination--btn warped--pagination--btn-inactive", params: html_form_options.merge(page: item) }) %>
21
+ <% elsif item.is_a?(String) -%> <span class="warped--pagination--btn warped--pagination--btn-active"><%= item %></span>
22
+ <% elsif item == :gap %>
23
+ <span class="warped--pagination--btn warped--pagination--btn-gap">
24
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
25
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
26
+ </svg>
27
+ </span>
28
+ <% end -%>
29
+ <% end -%>
30
+
31
+ <% if pagination[:next_page] -%> <%= button_to("Next", path, { "data-turbo-action" => turbo_action, method: :get, form_class: "warped--pagination--btn", params: html_form_options.merge(page: pagination[:next_page]) }) %>
32
+ <% else -%> <span class=" warped--pagination--btn warped--pagination--btn-disabled" disabled>Next</span>
33
+ <% end -%>
34
+ <% end %>
@@ -0,0 +1,19 @@
1
+ <%# locals: (resource:, columns:, path: nil, actions: nil) -%>
2
+
3
+ <% element_tag = path.present? ? :a : :div %>
4
+
5
+ <%= content_tag element_tag, **{ class: "warped-table--table--row" }.merge!(href: path).compact_blank do %>
6
+ <% if (block = yield).present? %>
7
+ <%= block %>
8
+ <% else %>
9
+ <% columns.each do |column| %>
10
+ <%= render "warped/cell" do %>
11
+ <%= column.content_for(resource) %>
12
+ <% end %>
13
+ <% end %>
14
+
15
+ <% if actions.present? %>
16
+ <%= render "warped/actions", actions:, resource: %>
17
+ <% end %>
18
+ <% end %>
19
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <%# locals: (path:, turbo_action:, **options) %>
2
+
3
+ <% opts = options.deep_dup %>
4
+
5
+ <% html = opts.extract!(:html) %>
6
+ <% html.merge!(class: "warped-searchbar #{opts.delete(:class)}") %>
7
+
8
+ <% data = opts.delete(:data) { {} } %>
9
+ <% data.merge!(turbo_action:) %>
10
+
11
+ <%= form_with url: path, method: :get, data:, html:, **opts do |f| %>
12
+ <%= f.button type: :submit, class: "warped-searchbar--button", name: nil do %>
13
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
14
+ <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
15
+ </svg>
16
+ <% end %>
17
+
18
+ <%= f.text_field search_param, autocomplete: "off", value: search_term, class: "warped-searchbar--input" %>
19
+
20
+ <%= render "warped/hidden_fields", form: f, modules: %i[pageable sortable filterable] %>
21
+ <% end %>
@@ -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__)
@@ -23,6 +23,82 @@ GET /users?email=john@example.com
23
23
  GET /users?created_at=2021-01-01
24
24
  ```
25
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
+
26
102
  ## Referencing tables in the filterable fields
27
103
 
28
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:
@@ -31,7 +107,7 @@ The `filterable_by` method can also be used to reference fields in associated ta
31
107
  class UsersController < ApplicationController
32
108
  include Warped::Controllers::Filterable
33
109
 
34
- filterable_by :name, :email, :created_at, 'companies.name'
110
+ filterable_by :name, 'companies.name', "companies.created_at" => { kind: :date_time }
35
111
 
36
112
  def index
37
113
  users = filter(User.joins(:company))
@@ -44,6 +120,7 @@ Request examples:
44
120
  ```
45
121
  GET /users?name=John
46
122
  GET /users?companies.name=Acme
123
+ GET /users?companies.created_at=2021-01-01T00:00:00
47
124
  ```
48
125
 
49
126
  ## Renaming the filter query parameters
@@ -55,7 +132,9 @@ you can specify the query parameter to use for each field:
55
132
  class UsersController < ApplicationController
56
133
  include Warped::Controllers::Filterable
57
134
 
58
- filterable_by 'companies.name' => :company_name, 'users.name' => :user_name
135
+ filterable_by 'companies.name' => { kind: :string, alias_name: :company_name },
136
+ 'users.name' => { kind: :string, alias_name: :user_name }
137
+
59
138
 
60
139
  def index
61
140
  users = filter(User.join(:company))
@@ -78,7 +157,7 @@ By default, the `filter` method will use the `eq` filter method to filter the re
78
157
  class UsersController < ApplicationController
79
158
  include Warped::Controllers::Filterable
80
159
 
81
- filterable_by :name, :age
160
+ filterable_by name: { kind: :string }, age: { kind: :integer }
82
161
 
83
162
  def index
84
163
  users = filter(User.all)
@@ -0,0 +1,285 @@
1
+ # Using Warped built-in view partials
2
+
3
+ In order to use the built in Warped view partials, you need to include the `Warped::Controllers::<WarpedConcern>::Ui` module in your controller.
4
+
5
+ ## Warped::Controllers::Filterable::Ui
6
+ ```ruby
7
+ # app/controllers/users_controller.rb
8
+ class UsersController < ApplicationController
9
+ include Warped::Controllers::Filterable::Ui
10
+
11
+ def index
12
+ @users = filter(User.all)
13
+ end
14
+ end
15
+ ```
16
+
17
+ ```erb
18
+ <!-- app/views/users/index.html.erb -->
19
+
20
+ <%= render "warped/filters", path: users_path, turbo_action: "replace" %>
21
+
22
+ <% @users.each do |user| %>
23
+ <p><%= user.first_name %>, <%= user.last_name %></p>
24
+ <% end %>
25
+ ```
26
+
27
+ The `warped/_filters` partial uses [strict locals](https://edgeguides.rubyonrails.org/action_view_overview.html#strict-locals), and it accepts:
28
+ - `path` - the path to the controller action to which the filters will be applied
29
+ - `turbo_action` - the Turbo action to be used when submitting the form (replace/advance)
30
+
31
+ The partial also accepts the following optional locals:
32
+ - `class`: the class to be applied to the form
33
+ - `data`: a hash of data attributes to be applied to the form
34
+ Any other locals that are passed to the partial will be passed to the `form_with` helper.
35
+
36
+ The `warped/_filters` partial will also render a set of hidden fields to store the sortable and searchable values, if the controller action called `#search` or `#sort`
37
+
38
+ ## Warped::Controllers::Pageable::Ui
39
+ ```ruby
40
+ # app/controllers/users_controller.rb
41
+ class UsersController < ApplicationController
42
+ include Warped::Controllers::Pageable::Ui
43
+
44
+ def index
45
+ @users = paginate(User.all)
46
+ end
47
+ end
48
+ ```
49
+
50
+ ```erb
51
+ <!-- app/views/users/index.html.erb -->
52
+
53
+ <% @users.each do |user| %>
54
+ <p><%= user.first_name %>, <%= user.last_name %></p>
55
+ <% end %>
56
+
57
+ <%= render "warped/pagination", path: users_path, turbo_action: "replace" %>
58
+ ```
59
+
60
+ The `warped/_pagination` partial uses [strict locals](https://edgeguides.rubyonrails.org/action_view_overview.html#strict-locals), and it accepts:
61
+ - `path` - the path to the controller action to which the pagination will be applied
62
+ - `turbo_action` - the Turbo action to be used when submitting the form (replace/advance)
63
+
64
+ Any other locals that are passed to the partial will be passed to the pagination <nav> tag.
65
+
66
+ The `warped/_pagination` partial will also render a set of hidden fields for each button, to store the sortable, searchable and filterable values, if the controller action called `#search`, `#sort` or `#filter`.
67
+
68
+ ## Warped::Controllers::Searchable::Ui
69
+ ```ruby
70
+ # app/models/user.rb
71
+
72
+ class User < ApplicationRecord
73
+
74
+ scope :search, ->(query) {
75
+ where("first_name ILIKE :query OR last_name ILIKE :query", query: "%#{query}%")
76
+ }
77
+ end
78
+ ```
79
+
80
+ ```ruby
81
+ # app/controllers/users_controller.rb
82
+ class UsersController < ApplicationController
83
+ include Warped::Controllers::Searchable::Ui
84
+
85
+ def index
86
+ @users = search(User.all)
87
+ end
88
+ end
89
+ ```
90
+
91
+ ```erb
92
+ <!-- app/views/users/index.html.erb -->
93
+
94
+ <%= render "warped/search", path: users_path, turbo_action: "replace" %>
95
+
96
+ <% @users.each do |user| %>
97
+ <p><%= user.first_name %>, <%= user.last_name %></p>
98
+ <% end %>
99
+ ```
100
+
101
+ The `warped/_search` partial uses [strict locals](https://edgeguides.rubyonrails.org/action_view_overview.html#strict-locals), and it accepts:
102
+ - `path` - the path to the controller action to which the search will be applied
103
+ - `turbo_action` - the Turbo action to be used when submitting the form (replace/advance)
104
+ Any other locals that are passed to the partial will be passed to the search `form_with` helper.
105
+
106
+ The `warped/_search` partial will also render a set of hidden fields to store the sortable, filterable and pagination values, if the controller action called `#sort`, `#filter` or `#paginate`.
107
+
108
+ ## Warped::Controllers::Tabulatable::Ui
109
+ ```ruby
110
+ # app/controllers/users_controller.rb
111
+ class UsersController < ApplicationController
112
+ include Warped::Controllers::Tabulatable::Ui
113
+
114
+ def index
115
+ @users = tabulate(User.all)
116
+ end
117
+
118
+ def show
119
+ @user = User.find(params[:id])
120
+ end
121
+
122
+ def destroy
123
+ User.find(params[:id]).destroy
124
+ redirect_to users_path
125
+ end
126
+ end
127
+ ```
128
+
129
+ ```erb
130
+ <!-- app/views/users/index.html.erb -->
131
+
132
+ <%= render "warped/table", collection: @users, path: users_path, turbo_action: "replace",
133
+ columns: [
134
+ Warped::Table::Column.new(:first_name, "First Name"),
135
+ Warped::Table::Column.new(:last_name, "Last Name")
136
+ ],
137
+ actions: [
138
+ Warped::Table::Action.new(:show, ->(user) { user_path(user) }),
139
+ Warped::Table::Action.new(:destroy, ->(user) { user_path(user) }, data: { turbo_method: "delete", turbo_confirm: "Are you sure?" })
140
+ ] %>
141
+ ```
142
+
143
+ The `warped/_table` partial uses [strict locals](https://edgeguides.rubyonrails.org/action_view_overview.html#strict-locals), and it accepts:
144
+ - `collection` - the collection of records to be displayed
145
+ - `path` - the path to the controller action to which the table filtering/sorting/searching/pagination will be applied
146
+ - `turbo_action` - the Turbo action to be used when submitting the form (replace/advance)
147
+ - `columns` - an array of `Warped::Table::Column` objects that define the columns to be displayed
148
+ - `actions` - an array of `Warped::Table::Action` objects that define the actions to be displayed for each record
149
+ Any other locals under the keys `:table`, `:filters`, `:search` or `:pagination` are passed to the respective partials.
150
+ Example:
151
+ ```erb
152
+ <%= render "warped/table", collection: @users, path: users_path, turbo_action: "replace",
153
+ columns: [
154
+ Warped::Table::Column.new(:first_name, "First Name"),
155
+ Warped::Table::Column.new(:last_name, "Last Name")
156
+ ],
157
+ actions: [
158
+ Warped::Table::Action.new(:show, ->(user) { user_path(user) }),
159
+ Warped::Table::Action.new(:destroy, ->(user) { user_path(user) }, data: { turbo_method: "delete", turbo_confirm: "Are you sure?" })
160
+ ],
161
+ table: { class: "table" },
162
+ filters: { class: "filters" },
163
+ search: { class: "search" },
164
+ pagination: { class: "pagination" } %>
165
+ ```
166
+ This will render the table with the class `table`, and it will forward the locals:
167
+ - `class: "filters"` to the `warped/_filters` partial
168
+ - `class: "search"` to the `warped/_search` partial
169
+ - `class: "pagination"` to the `warped/_pagination` partial
170
+
171
+ ### Using the `Warped::Table::Column` class
172
+ The `Warped::Table::Column` class is used to define the columns to be displayed in the table. The initializer accepts the following arguments:
173
+ - `parameter_name` - the name of the parameter/alias_name passed to the `filterable_by`/`sortable_by`/`tabulatable_by` methods
174
+ - `display_name`(optional) - the name to be displayed in the table header
175
+ - `method`(optional) - this can be a symbol or a lambda. If the method is a symbol, it will be called on the record to get the value. If the method is a lambda, it will be called with the record as an argument.
176
+
177
+ ### Using the `Warped::Table::Action` class
178
+ The `Warped::Table::Action` class is used to define the actions to be displayed for each record in the table. The initializer accepts the following arguments:
179
+ - `name` - the name of the action. If the name is a symbol or a string, it will be used as the action name. If the name is a lambda, it will be called with the record as an argument.
180
+ - `path` - the path to the controller action to which the action will be applied. If the path is a lambda, it will be called with the record as an argument.
181
+
182
+
183
+ ## Using turbo-frames with the partials
184
+ The partials can be used with turbo-frames to provide a seamless experience, regardless of the action used in the controller.
185
+
186
+ ```ruby
187
+ # app/controllers/user_controller.rb
188
+ class UserController < ApplicationController
189
+ include Warped::Controllers::Tabulatable::Ui
190
+
191
+ tabulatable_by title: { kind: :string }, published_at: { kind: :date_time },
192
+
193
+ def show
194
+ @user = current_user
195
+ @posts = tabulate(@user.posts)
196
+ end
197
+ end
198
+ ```
199
+
200
+ ```erb
201
+ <!-- app/views/users/show.html.erb -->
202
+ <h1><%= @user.name %></h1>
203
+
204
+ <%= turbo_frame_tag dom_id(@posts) do %>
205
+ <%= render "warped/table", collection: @posts, path: user_path, turbo_action: "advance",
206
+ columns: [
207
+ Warped::Table::Column.new(:title, "Title"),
208
+ Warped::Table::Column.new(:published_at, "Published At")
209
+ ],
210
+ actions: [
211
+ <%# pass target: "_top" to the action, to escape from the turbo_frame navigation %>
212
+ Warped::Table::Action.new(:show, ->(post) { post_path(post) }, target: "_top")
213
+ ] %>
214
+ <% end %>
215
+ ```
216
+
217
+ ## Styling the partials
218
+ The partials are designed to be as unobtrusive as possible, and they can be styled using the classes passed as locals to the partials, or by modifying the partials css classes.
219
+
220
+ The css classes are in the following files:
221
+ – `app/assets/stylesheets/warped/base.css` - the base css variables
222
+ - `app/assets/stylesheets/warped/filters.css` - the css classes for the filters partial
223
+ - `app/assets/stylesheets/warped/pagination.css` - the css classes for the pagination partial
224
+ - `app/assets/stylesheets/warped/search.css` - the css classes for the search partial
225
+ - `app/assets/stylesheets/warped/table.css` - the css classes for the table partial
226
+
227
+ You can override the css classes by adding the following to your css file:
228
+ ```css
229
+ @import "warped/base.css";
230
+ @import "warped/filters.css";
231
+ @import "warped/pagination.css";
232
+ @import "warped/search.css";
233
+ @import "warped/table.css";
234
+ ```
235
+
236
+ ## Moving logic from the view to the controller
237
+ Using the "warped/_table" can introduce a lot of logic in the view. To move the logic to the controller, you can instantiate the Columns and Actions in the controller and use them in the view as instance variables.
238
+
239
+ ```ruby
240
+ # app/controllers/users_controller.rb
241
+ class UsersController < ApplicationController
242
+ include Warped::Controllers::Tabulatable::Ui
243
+
244
+ helper_method :columns_for_index, :actions_for_index
245
+
246
+ tabulatable_by first_name: { kind: :string },
247
+ last_name: { kind: :string },
248
+ email: { kind: :string },
249
+ created_at: { kind: :date_time, alias_name: :registered_at },
250
+ status: { kind: :date_time }
251
+
252
+ def index
253
+ users_query = User.all.select("users.*, CASE WHEN confirmed_at IS NULL THEN 'Inactive' ELSE 'Active' END AS status")
254
+ @users = tabulate(users_query)
255
+ end
256
+
257
+ ...
258
+
259
+ private
260
+
261
+ def columns_for_index
262
+ [
263
+ Warped::Table::Column.new(:first_name, "First Name"),
264
+ Warped::Table::Column.new(:last_name, "Last Name"),
265
+ Warped::Table::Column.new(:email, "Email"),
266
+ Warped::Table::Column.new(:registered_at, "Registered At"),
267
+ Warped::Table::Column.new(:status, "Status")
268
+ ]
269
+ end
270
+
271
+ def actions_for_index
272
+ [
273
+ Warped::Table::Action.new(:show, ->(user) { user_path(user) }),
274
+ Warped::Table::Action.new(:edit, ->(user) { edit_user_path(user) }),
275
+ Warped::Table::Action.new(:destroy, ->(user) { user_path(user) }, data: { turbo_method: "delete", turbo_confirm: "Are you sure?" })
276
+ ]
277
+ end
278
+ ```
279
+
280
+ ```erb
281
+ <!-- app/views/users/index.html.erb -->
282
+ <%= render "warped/table", collection: @users, path: users_path, turbo_action: "replace",
283
+ columns: columns_for_index,
284
+ actions: actions_for_index %>
285
+ ```