warped 0.2.0 → 1.0.1
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/.rubocop.yml +6 -0
- data/Gemfile.lock +2 -2
- data/README.md +104 -0
- data/app/assets/config/warped_manifest.js +2 -0
- data/app/assets/javascript/warped/controllers/filter_controller.js +76 -0
- data/app/assets/javascript/warped/controllers/filters_controller.js +21 -0
- data/app/assets/javascript/warped/index.js +2 -0
- data/app/assets/stylesheets/warped/application.css +15 -0
- data/app/assets/stylesheets/warped/base.css +23 -0
- data/app/assets/stylesheets/warped/filters.css +115 -0
- data/app/assets/stylesheets/warped/pagination.css +74 -0
- data/app/assets/stylesheets/warped/search.css +33 -0
- data/app/assets/stylesheets/warped/table.css +114 -0
- data/app/views/warped/_actions.html.erb +9 -0
- data/app/views/warped/_cell.html.erb +3 -0
- data/app/views/warped/_column.html.erb +35 -0
- data/app/views/warped/_filters.html.erb +21 -0
- data/app/views/warped/_hidden_fields.html.erb +19 -0
- data/app/views/warped/_pagination.html.erb +34 -0
- data/app/views/warped/_row.html.erb +19 -0
- data/app/views/warped/_search.html.erb +21 -0
- data/app/views/warped/_table.html.erb +52 -0
- data/app/views/warped/filters/_filter.html.erb +40 -0
- data/config/importmap.rb +3 -0
- data/docs/controllers/FILTERABLE.md +82 -3
- data/docs/controllers/views/PARTIALS.md +285 -0
- data/lib/warped/api/filter/base/value.rb +52 -0
- data/lib/warped/api/filter/base.rb +84 -0
- data/lib/warped/api/filter/boolean.rb +41 -0
- data/lib/warped/api/filter/date.rb +26 -0
- data/lib/warped/api/filter/date_time.rb +32 -0
- data/lib/warped/api/filter/decimal.rb +31 -0
- data/lib/warped/api/filter/factory.rb +38 -0
- data/lib/warped/api/filter/integer.rb +38 -0
- data/lib/warped/api/filter/string.rb +25 -0
- data/lib/warped/api/filter/time.rb +25 -0
- data/lib/warped/api/filter.rb +14 -0
- data/lib/warped/api/sort/value.rb +40 -0
- data/lib/warped/api/sort.rb +65 -0
- data/lib/warped/controllers/filterable/ui.rb +10 -32
- data/lib/warped/controllers/filterable.rb +75 -42
- data/lib/warped/controllers/pageable/ui.rb +13 -3
- data/lib/warped/controllers/pageable.rb +1 -1
- data/lib/warped/controllers/searchable/ui.rb +3 -1
- data/lib/warped/controllers/sortable/ui.rb +21 -26
- data/lib/warped/controllers/sortable.rb +53 -33
- data/lib/warped/controllers/tabulatable/ui.rb +4 -0
- data/lib/warped/controllers/tabulatable.rb +6 -9
- data/lib/warped/engine.rb +19 -0
- data/lib/warped/queries/filter.rb +3 -3
- data/lib/warped/table/action.rb +33 -0
- data/lib/warped/table/column.rb +34 -0
- data/lib/warped/version.rb +1 -1
- data/lib/warped.rb +1 -0
- data/warped.gemspec +1 -1
- metadata +43 -11
@@ -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>
|
data/config/importmap.rb
ADDED
@@ -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,
|
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' => :
|
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 :
|
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
|
+
```
|