infotable 1.0.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -2
  3. data/app/assets/javascripts/infotable/controllers/drawer_controller.js +68 -42
  4. data/app/assets/javascripts/infotable/controllers/table_controller.js +276 -23
  5. data/app/controllers/infotable/tables_controller.rb +17 -7
  6. data/app/helpers/infotable/tables_helper.rb +64 -2
  7. data/app/views/infotable/tables/_active_filters.html.erb +24 -0
  8. data/app/views/infotable/tables/_column_visibility.html.erb +11 -11
  9. data/app/views/infotable/tables/_filter_drawer.html.erb +4 -4
  10. data/app/views/infotable/tables/_next_page_frame.html.erb +5 -5
  11. data/app/views/infotable/tables/_pagination.html.erb +12 -12
  12. data/app/views/infotable/tables/_rail.html.erb +56 -0
  13. data/app/views/infotable/tables/_side_panel.html.erb +22 -10
  14. data/app/views/infotable/tables/_table.html.erb +45 -28
  15. data/app/views/infotable/tables/_table_rows.html.erb +3 -3
  16. data/app/views/infotable/tables/filters/_boolean.html.erb +2 -2
  17. data/app/views/infotable/tables/filters/_date.html.erb +2 -30
  18. data/app/views/infotable/tables/filters/_number.html.erb +2 -32
  19. data/app/views/infotable/tables/filters/_range.html.erb +66 -0
  20. data/app/views/infotable/tables/filters/_select.html.erb +4 -4
  21. data/app/views/infotable/tables/filters/_text.html.erb +3 -3
  22. data/app/views/infotable/tables/show.html.erb +22 -13
  23. data/config/locales/en.yml +24 -0
  24. data/config/locales/pl.yml +24 -0
  25. data/config/routes.rb +1 -0
  26. data/lib/infotable/base.rb +80 -1
  27. data/lib/infotable/configuration.rb +3 -1
  28. data/lib/infotable/formatters/action_formatter.rb +7 -7
  29. data/lib/infotable/formatters/avatar_formatter.rb +2 -2
  30. data/lib/infotable/formatters/badge_formatter.rb +9 -9
  31. data/lib/infotable/formatters/boolean_formatter.rb +2 -2
  32. data/lib/infotable/formatters/link_formatter.rb +1 -1
  33. data/lib/infotable/formatters/percent_formatter.rb +1 -1
  34. data/lib/infotable/version.rb +1 -1
  35. metadata +4 -1
@@ -14,7 +14,7 @@
14
14
  id="<%= infotable_filter_id(table_name, column, 'value') %>"
15
15
  multiple
16
16
  size="6"
17
- class="block w-full appearance-none rounded-lg border-0 bg-white pl-3 py-2 pr-8 text-base sm:text-sm text-neutral-900 shadow-sm focus:outline-none focus:ring-2 focus:ring-neutral-600 focus:ring-offset-0 transition-all [box-shadow:inset_0_0_0_1px_rgb(212_212_212)] focus:[box-shadow:inset_0_0_0_2px_rgb(82_82_82)] cursor-pointer disabled:cursor-not-allowed disabled:bg-neutral-100 disabled:opacity-75"
17
+ class="block w-full appearance-none rounded-lg border-0 bg-white dark:bg-gray-900 pl-3 py-2 pr-8 text-base sm:text-sm text-neutral-900 dark:text-neutral-100 shadow-sm focus:outline-none focus:ring-2 focus:ring-neutral-600 focus:ring-offset-0 transition-all [box-shadow:inset_0_0_0_1px_rgb(212_212_212)] dark:[box-shadow:inset_0_0_0_1px_rgb(82_82_82)] focus:[box-shadow:inset_0_0_0_2px_rgb(82_82_82)] dark:focus:[box-shadow:inset_0_0_0_2px_rgb(163_163_163)] cursor-pointer disabled:cursor-not-allowed disabled:bg-neutral-100 disabled:opacity-75"
18
18
  style="min-height: 120px;">
19
19
  <% options.each do |option| %>
20
20
  <% if option.is_a?(Array) %>
@@ -25,7 +25,7 @@
25
25
  <% end %>
26
26
  </select>
27
27
  <button type="button"
28
- class="absolute right-2 top-2 text-gray-400 hover:text-gray-600 transition-colors"
28
+ class="absolute right-2 top-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
29
29
  data-action="click->infotable--drawer#clearSingleFilter"
30
30
  data-filter-name="<%= column[:name] %>"
31
31
  data-filter-type="select"
@@ -41,7 +41,7 @@
41
41
  <div class="relative">
42
42
  <select name="filters[<%= column[:name] %>][value]"
43
43
  id="<%= infotable_filter_id(table_name, column, 'value') %>"
44
- class="block w-full appearance-none rounded-lg border-0 bg-white pr-10 pl-3 py-2 text-base sm:text-sm text-neutral-900 shadow-sm focus:outline-none focus:ring-2 focus:ring-neutral-600 focus:ring-offset-0 transition-all [box-shadow:inset_0_0_0_1px_rgb(212_212_212)] focus:[box-shadow:inset_0_0_0_2px_rgb(82_82_82)] cursor-pointer disabled:cursor-not-allowed disabled:bg-neutral-100 disabled:opacity-75 bg-[url('data:image/svg+xml,%3csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20fill=%27none%27%20viewBox=%270%200%2020%2020%27%3e%3cpath%20stroke=%27%23737373%27%20stroke-linecap=%27round%27%20stroke-linejoin=%27round%27%20stroke-width=%271.5%27%20d=%27M6%208l4%204%204-4%27/%3e%3c/svg%3e')] bg-[right_0.75rem_center] bg-no-repeat bg-[length:1.25em_1.25em]">
44
+ class="block w-full appearance-none rounded-lg border-0 bg-white dark:bg-gray-900 pr-10 pl-3 py-2 text-base sm:text-sm text-neutral-900 dark:text-neutral-100 shadow-sm focus:outline-none focus:ring-2 focus:ring-neutral-600 focus:ring-offset-0 transition-all [box-shadow:inset_0_0_0_1px_rgb(212_212_212)] dark:[box-shadow:inset_0_0_0_1px_rgb(82_82_82)] focus:[box-shadow:inset_0_0_0_2px_rgb(82_82_82)] dark:focus:[box-shadow:inset_0_0_0_2px_rgb(163_163_163)] cursor-pointer disabled:cursor-not-allowed disabled:bg-neutral-100 disabled:opacity-75 bg-[url('data:image/svg+xml,%3csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20fill=%27none%27%20viewBox=%270%200%2020%2020%27%3e%3cpath%20stroke=%27%23737373%27%20stroke-linecap=%27round%27%20stroke-linejoin=%27round%27%20stroke-width=%271.5%27%20d=%27M6%208l4%204%204-4%27/%3e%3c/svg%3e')] bg-[right_0.75rem_center] bg-no-repeat bg-[length:1.25em_1.25em]">
45
45
  <option value=""><%= t('infotable.filters.all') %></option>
46
46
  <% options.each do |option| %>
47
47
  <% if option.is_a?(Array) %>
@@ -52,7 +52,7 @@
52
52
  <% end %>
53
53
  </select>
54
54
  <button type="button"
55
- class="absolute right-8 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
55
+ class="absolute right-8 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
56
56
  data-action="click->infotable--drawer#clearSingleFilter"
57
57
  data-filter-name="<%= column[:name] %>"
58
58
  data-filter-type="select"
@@ -6,7 +6,7 @@
6
6
  data-filter-op
7
7
  data-action="change->infotable--drawer#syncFilterOps"
8
8
  id="<%= infotable_filter_id(table_name, column, 'op') %>"
9
- class="block w-full appearance-none rounded-lg border-0 bg-white pr-10 pl-3 py-2 text-base sm:text-sm text-neutral-900 shadow-sm focus:outline-none focus:ring-2 focus:ring-neutral-600 focus:ring-offset-0 transition-all [box-shadow:inset_0_0_0_1px_rgb(212_212_212)] focus:[box-shadow:inset_0_0_0_2px_rgb(82_82_82)] cursor-pointer disabled:cursor-not-allowed disabled:bg-neutral-100 disabled:opacity-75 bg-[url('data:image/svg+xml,%3csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20fill=%27none%27%20viewBox=%270%200%2020%2020%27%3e%3cpath%20stroke=%27%23737373%27%20stroke-linecap=%27round%27%20stroke-linejoin=%27round%27%20stroke-width=%271.5%27%20d=%27M6%208l4%204%204-4%27/%3e%3c/svg%3e')] bg-[right_0.75rem_center] bg-no-repeat bg-[length:1.25em_1.25em]">
9
+ class="block w-full appearance-none rounded-lg border-0 bg-white dark:bg-gray-900 pr-10 pl-3 py-2 text-base sm:text-sm text-neutral-900 dark:text-neutral-100 shadow-sm focus:outline-none focus:ring-2 focus:ring-neutral-600 focus:ring-offset-0 transition-all [box-shadow:inset_0_0_0_1px_rgb(212_212_212)] dark:[box-shadow:inset_0_0_0_1px_rgb(82_82_82)] focus:[box-shadow:inset_0_0_0_2px_rgb(82_82_82)] dark:focus:[box-shadow:inset_0_0_0_2px_rgb(163_163_163)] cursor-pointer disabled:cursor-not-allowed disabled:bg-neutral-100 disabled:opacity-75 bg-[url('data:image/svg+xml,%3csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20fill=%27none%27%20viewBox=%270%200%2020%2020%27%3e%3cpath%20stroke=%27%23737373%27%20stroke-linecap=%27round%27%20stroke-linejoin=%27round%27%20stroke-width=%271.5%27%20d=%27M6%208l4%204%204-4%27/%3e%3c/svg%3e')] bg-[right_0.75rem_center] bg-no-repeat bg-[length:1.25em_1.25em]">
10
10
  <option value="contains"><%= t('infotable.filters.text_ops.contains') %></option>
11
11
  <option value="exact"><%= t('infotable.filters.text_ops.exact') %></option>
12
12
  <option value="not_contain"><%= t('infotable.filters.text_ops.not_contain') %></option>
@@ -23,9 +23,9 @@
23
23
  name="filters[<%= column[:name] %>][value]"
24
24
  id="<%= infotable_filter_id(table_name, column, 'value') %>"
25
25
  placeholder="<%= t('infotable.filters.enter_value') %>"
26
- class="block w-full rounded-lg border-0 bg-white pr-8 pl-3 py-2 text-base sm:text-sm text-neutral-900 placeholder:text-neutral-500 shadow-sm focus:outline-none focus:ring-2 focus:ring-neutral-600 focus:ring-offset-0 transition-all [box-shadow:inset_0_0_0_1px_rgb(212_212_212)] focus:[box-shadow:inset_0_0_0_2px_rgb(82_82_82)] disabled:cursor-not-allowed disabled:bg-neutral-100">
26
+ class="block w-full rounded-lg border-0 bg-white dark:bg-gray-900 pr-8 pl-3 py-2 text-base sm:text-sm text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-500 shadow-sm focus:outline-none focus:ring-2 focus:ring-neutral-600 focus:ring-offset-0 transition-all [box-shadow:inset_0_0_0_1px_rgb(212_212_212)] dark:[box-shadow:inset_0_0_0_1px_rgb(82_82_82)] focus:[box-shadow:inset_0_0_0_2px_rgb(82_82_82)] dark:focus:[box-shadow:inset_0_0_0_2px_rgb(163_163_163)] disabled:cursor-not-allowed disabled:bg-neutral-100">
27
27
  <button type="button"
28
- class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
28
+ class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
29
29
  data-action="click->infotable--drawer#clearSingleFilter"
30
30
  data-filter-name="<%= column[:name] %>"
31
31
  data-filter-type="text"
@@ -1,13 +1,15 @@
1
1
  <%# This view renders inside the turbo frame %>
2
2
  <%# Turbo requires the frame element to be present in the response %>
3
3
  <turbo-frame id="<%= params[:table_name] %>_content">
4
- <div class="relative overflow-y-auto overflow-x-auto scroll-smooth [scrollbar-width:thin] [scrollbar-color:#cbd5e1_#f1f5f9] [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar-track]:bg-slate-100 [&::-webkit-scrollbar-thumb]:bg-slate-300 [&::-webkit-scrollbar-thumb]:rounded [&::-webkit-scrollbar-thumb:hover]:bg-slate-400"
4
+ <% active_filters = infotable_active_filters(@table_class, @filters, @search) %>
5
+ <%= render "infotable/tables/active_filters", chips: active_filters, table_name: params[:table_name] %>
6
+ <div<% if @pagy && !@table_class.countless? %> data-total-count="<%= @pagy.count %>"<% end %> class="relative overflow-y-auto overflow-x-auto scroll-smooth [scrollbar-width:thin] [scrollbar-color:#cbd5e1_#f1f5f9] [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar-track]:bg-slate-100 [&::-webkit-scrollbar-thumb]:bg-slate-300 [&::-webkit-scrollbar-thumb]:rounded [&::-webkit-scrollbar-thumb:hover]:bg-slate-400 dark:[scrollbar-color:#475569_#1e293b] dark:[&::-webkit-scrollbar-track]:bg-slate-800 dark:[&::-webkit-scrollbar-thumb]:bg-slate-600"
5
7
  style="min-height: <%= @table_class.min_height %>; max-height: <%= @table_class.max_height %>;">
6
8
 
7
9
  <% if @records.any? %>
8
10
  <%# Table %>
9
- <table class="min-w-full divide-y divide-gray-200">
10
- <thead class="bg-gray-50 border-b-2 border-gray-200">
11
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
12
+ <thead class="bg-gray-50 dark:bg-gray-800 border-b-2 border-gray-200 dark:border-gray-700">
11
13
  <tr>
12
14
  <%
13
15
  row_size = @table_class.row_size_value
@@ -18,12 +20,12 @@
18
20
  end
19
21
  %>
20
22
  <% if @table_class.checkbox_enabled? %>
21
- <th scope="col" class="<%= header_padding %> w-10 bg-gray-50 sticky top-0 z-20">
23
+ <th scope="col" class="<%= header_padding %> w-10 bg-gray-50 dark:bg-gray-800 sticky top-0 z-20">
22
24
  <input type="checkbox"
23
25
  aria-label="<%= t('infotable.batch.select_all') %>"
24
26
  data-infotable--table-target="selectAll"
25
27
  data-action="change->infotable--table#toggleAll"
26
- class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
28
+ class="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-indigo-600 dark:text-indigo-400 focus:ring-indigo-500">
27
29
  </th>
28
30
  <% end %>
29
31
  <% @table_class.columns.each do |column| %>
@@ -32,7 +34,7 @@
32
34
  data-default-visible="<%= column[:visible] %>"
33
35
  <%= tag.attributes(data: { default_pinned: column[:pinned] }) if column[:pinned] %>
34
36
  style="width: <%= column[:width] %>; max-width: <%= column[:width] %>; overflow: hidden;"
35
- class="<%= header_padding %> text-left text-xs font-semibold text-gray-900 tracking-wider select-none bg-gray-50 sticky top-0 z-20">
37
+ class="<%= header_padding %> text-left text-xs font-semibold text-gray-900 dark:text-gray-100 tracking-wider select-none bg-gray-50 dark:bg-gray-800 sticky top-0 z-20">
36
38
  <% if column[:sortable] %>
37
39
  <% sort_index = current_sorts.index { |name, _| name == column[:name].to_s } %>
38
40
  <% sort_dir = sort_index ? current_sorts[sort_index].last : nil %>
@@ -56,16 +58,16 @@
56
58
  <span class="flex-none rounded">
57
59
  <% if sort_index %>
58
60
  <% if sort_dir == 'asc' %>
59
- <svg class="h-4 w-4 text-indigo-600" fill="currentColor" viewBox="0 0 20 20">
61
+ <svg class="h-4 w-4 text-indigo-600 dark:text-indigo-400" fill="currentColor" viewBox="0 0 20 20">
60
62
  <path fill-rule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clip-rule="evenodd"/>
61
63
  </svg>
62
64
  <% else %>
63
- <svg class="h-4 w-4 text-indigo-600" fill="currentColor" viewBox="0 0 20 20">
65
+ <svg class="h-4 w-4 text-indigo-600 dark:text-indigo-400" fill="currentColor" viewBox="0 0 20 20">
64
66
  <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
65
67
  </svg>
66
68
  <% end %>
67
69
  <% if current_sorts.size > 1 %>
68
- <span class="text-[10px] font-bold text-indigo-600"><%= sort_index + 1 %></span>
70
+ <span class="text-[10px] font-bold text-indigo-600 dark:text-indigo-400"><%= sort_index + 1 %></span>
69
71
  <% end %>
70
72
  <% else %>
71
73
  <svg class="h-4 w-4 text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity" fill="currentColor" viewBox="0 0 20 20">
@@ -81,7 +83,7 @@
81
83
  <% end %>
82
84
  </tr>
83
85
  </thead>
84
- <tbody id="<%= params[:table_name] %>_tbody" class="bg-white">
86
+ <tbody id="<%= params[:table_name] %>_tbody" class="bg-white dark:bg-gray-900">
85
87
  <%= render partial: "infotable/tables/table_rows", locals: { records: @records, table_class: @table_class } %>
86
88
  </tbody>
87
89
  </table>
@@ -96,8 +98,15 @@
96
98
  <svg class="mx-auto h-16 w-16 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
97
99
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/>
98
100
  </svg>
99
- <h3 class="mt-4 text-lg font-medium text-gray-900"><%= t('infotable.table.empty_state.title') %></h3>
100
- <p class="mt-2 text-sm text-gray-500"><%= t('infotable.table.empty_state.description') %></p>
101
+ <% if active_filters.any? %>
102
+ <h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-gray-100"><%= t('infotable.table.empty_state.filtered_title') %></h3>
103
+ <p class="mt-2 text-sm text-gray-500 dark:text-gray-400"><%= t('infotable.table.empty_state.filtered_description') %></p>
104
+ <button type="button" data-action="click->infotable--table#clearAllFilters" data-empty-clear-filters
105
+ class="mt-4 inline-flex items-center rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500"><%= t('infotable.filters.active.clear_all') %></button>
106
+ <% else %>
107
+ <h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-gray-100"><%= t('infotable.table.empty_state.title') %></h3>
108
+ <p class="mt-2 text-sm text-gray-500 dark:text-gray-400"><%= t('infotable.table.empty_state.description') %></p>
109
+ <% end %>
101
110
  </div>
102
111
  <% end %>
103
112
 
@@ -105,7 +114,7 @@
105
114
 
106
115
  <%# Pagination - Only visible when infinite scroll is disabled %>
107
116
  <% unless @infinite_scroll_enabled %>
108
- <div class="border-t border-gray-200 bg-white px-6 py-4">
117
+ <div class="border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 sm:px-6 py-4">
109
118
  <%= render "infotable/tables/pagination", pagy: @pagy, table_name: params[:table_name] %>
110
119
  </div>
111
120
  <% end %>
@@ -5,14 +5,19 @@ en:
5
5
  export_csv: "Export CSV"
6
6
  filters: "Filters"
7
7
  exporting: "Exporting..."
8
+ refresh: "Refresh"
9
+ columns: "Columns"
8
10
  sort_hint: "Click to sort, Shift+click to add a column to the sort"
9
11
  loading:
10
12
  initial: "Loading data..."
11
13
  more_items: "Loading more items..."
12
14
  all_loaded: "All items loaded"
15
+ resize_column: "resize column"
13
16
  empty_state:
14
17
  title: "No data found"
15
18
  description: "Get started by adding some records to see them here."
19
+ filtered_title: "No rows match your filters"
20
+ filtered_description: "Remove a filter or search for something else."
16
21
 
17
22
  drawer:
18
23
  label: "Filters and columns"
@@ -39,6 +44,10 @@ en:
39
44
  clear_all: "Clear All"
40
45
  apply_filters: "Apply Filters"
41
46
  clear_filter: "Clear this filter"
47
+ active:
48
+ search: "Search: \u201c%{query}\u201d"
49
+ remove: "Remove filter %{filter}"
50
+ clear_all: "Clear all filters"
42
51
  from: "From"
43
52
  to: "To"
44
53
  min: "Min"
@@ -60,6 +69,19 @@ en:
60
69
  ends_with: "Ends with"
61
70
  is_empty: "Is empty"
62
71
  is_not_empty: "Is not empty"
72
+ range_ops:
73
+ between: "Between"
74
+ number_ops:
75
+ eq: "Equals"
76
+ neq: "Does not equal"
77
+ gt: "Greater than"
78
+ gte: "Greater than or equal"
79
+ lt: "Less than"
80
+ lte: "Less than or equal"
81
+ date_ops:
82
+ "on": "On"
83
+ before: "Before"
84
+ after: "After"
63
85
 
64
86
  formatters:
65
87
  ago: "%{time} ago"
@@ -73,6 +95,8 @@ en:
73
95
  clear: "Clear selection"
74
96
  select_all: "Select all rows"
75
97
  select_row: "Select row"
98
+ select_all_matching: "Select all %{count} matching"
99
+ truncated: "Only the first %{max} rows are selected"
76
100
 
77
101
  search:
78
102
  placeholder: "Search..."
@@ -5,14 +5,19 @@ pl:
5
5
  export_csv: "Eksportuj CSV"
6
6
  filters: "Filtry"
7
7
  exporting: "Eksportowanie..."
8
+ refresh: "Odśwież"
9
+ columns: "Kolumny"
8
10
  sort_hint: "Kliknij, aby sortować; Shift+klik dodaje kolumnę do sortowania"
9
11
  loading:
10
12
  initial: "Ładowanie danych..."
11
13
  more_items: "Ładowanie kolejnych elementów..."
12
14
  all_loaded: "Wszystkie elementy załadowane"
15
+ resize_column: "zmień szerokość kolumny"
13
16
  empty_state:
14
17
  title: "Nie znaleziono danych"
15
18
  description: "Rozpocznij od dodania rekordów, aby zobaczyć je tutaj."
19
+ filtered_title: "Żaden wiersz nie pasuje do filtrów"
20
+ filtered_description: "Usuń filtr lub wyszukaj coś innego."
16
21
 
17
22
  drawer:
18
23
  label: "Filtry i kolumny"
@@ -39,6 +44,10 @@ pl:
39
44
  clear_all: "Wyczyść wszystkie"
40
45
  apply_filters: "Zastosuj filtry"
41
46
  clear_filter: "Wyczyść ten filtr"
47
+ active:
48
+ search: "Szukaj: \u201e%{query}\u201d"
49
+ remove: "Usuń filtr %{filter}"
50
+ clear_all: "Wyczyść wszystkie filtry"
42
51
  from: "Od"
43
52
  to: "Do"
44
53
  min: "Min"
@@ -60,6 +69,19 @@ pl:
60
69
  ends_with: "Kończy się na"
61
70
  is_empty: "Jest puste"
62
71
  is_not_empty: "Nie jest puste"
72
+ range_ops:
73
+ between: "Pomiędzy"
74
+ number_ops:
75
+ eq: "Równa się"
76
+ neq: "Różne od"
77
+ gt: "Większe niż"
78
+ gte: "Większe lub równe"
79
+ lt: "Mniejsze niż"
80
+ lte: "Mniejsze lub równe"
81
+ date_ops:
82
+ "on": "W dniu"
83
+ before: "Przed"
84
+ after: "Po"
63
85
 
64
86
  formatters:
65
87
  ago: "%{time} temu"
@@ -72,6 +94,8 @@ pl:
72
94
  selected: "Zaznaczono: %{count}"
73
95
  clear: "Wyczyść zaznaczenie"
74
96
  select_all: "Zaznacz wszystkie wiersze"
97
+ select_all_matching: "Zaznacz wszystkie pasujące (%{count})"
98
+ truncated: "Zaznaczono tylko pierwsze %{max} wierszy"
75
99
  select_row: "Zaznacz wiersz"
76
100
 
77
101
  search:
data/config/routes.rb CHANGED
@@ -3,4 +3,5 @@ Infotable::Engine.routes.draw do
3
3
  get "tables/:table_name", to: "tables#show", as: :table
4
4
  get "tables/:table_name/data", to: "tables#data", as: :table_data
5
5
  get "tables/:table_name/export", to: "tables#export", as: :table_export
6
+ get "tables/:table_name/ids", to: "tables#ids", as: :table_ids
6
7
  end
@@ -1,7 +1,7 @@
1
1
  module Infotable
2
2
  class Base
3
3
  class << self
4
- attr_accessor :table_columns, :table_query, :table_query_sql, :pagination_settings, :sort_settings, :height_settings, :infinite_scroll_settings, :row_link_settings, :slim_rows_settings, :row_size_settings, :filters_button_settings, :display_settings, :nil_value_settings, :empty_value_settings, :table_authorize, :checkbox_settings, :batch_action_list, :countless_setting, :url_state_setting
4
+ attr_accessor :table_columns, :table_query, :table_query_sql, :pagination_settings, :sort_settings, :height_settings, :infinite_scroll_settings, :row_link_settings, :slim_rows_settings, :row_size_settings, :filters_button_settings, :display_settings, :nil_value_settings, :empty_value_settings, :table_authorize, :checkbox_settings, :batch_action_list, :countless_setting, :url_state_setting, :resizable_columns_setting
5
5
 
6
6
  def inherited(subclass)
7
7
  super
@@ -45,6 +45,7 @@ module Infotable
45
45
  subclass.batch_action_list = []
46
46
  subclass.countless_setting = false
47
47
  subclass.url_state_setting = nil
48
+ subclass.resizable_columns_setting = nil
48
49
 
49
50
  inherit_from_parent(subclass)
50
51
  end
@@ -62,6 +63,7 @@ module Infotable
62
63
  subclass.batch_action_list = parent.batch_action_list.map(&:dup)
63
64
  subclass.countless_setting = parent.countless_setting
64
65
  subclass.url_state_setting = parent.url_state_setting
66
+ subclass.resizable_columns_setting = parent.resizable_columns_setting
65
67
  %i[pagination_settings sort_settings height_settings infinite_scroll_settings row_link_settings
66
68
  slim_rows_settings row_size_settings filters_button_settings display_settings
67
69
  nil_value_settings empty_value_settings].each do |attr|
@@ -88,6 +90,17 @@ module Infotable
88
90
  @url_state_setting = enabled
89
91
  end
90
92
 
93
+ # Drag handles on the column headers (and arrow keys on the focused handle) to change
94
+ # column widths; widths are remembered per browser. Double-click resets one column.
95
+ # resizable_columns false
96
+ def resizable_columns(enabled = true)
97
+ @resizable_columns_setting = enabled
98
+ end
99
+
100
+ def resizable_columns?
101
+ @resizable_columns_setting.nil? ? true : @resizable_columns_setting ? true : false
102
+ end
103
+
91
104
  def url_state?
92
105
  @url_state_setting.nil? ? Infotable.configuration.url_state : @url_state_setting ? true : false
93
106
  end
@@ -116,6 +129,21 @@ module Infotable
116
129
  record.public_send(@checkbox_settings[:id_method])
117
130
  end
118
131
 
132
+ # Ids of every row the query matches (the "select all matching" button), at most `limit`.
133
+ # Plucks when the id method is a plain column of an ungrouped query, loads records otherwise.
134
+ def checkbox_ids(query, limit = nil)
135
+ method = @checkbox_settings[:id_method]
136
+ scope = query.reorder(nil)
137
+ scope = scope.limit(limit) if limit
138
+ model = query.model
139
+
140
+ if model.column_names.include?(method.to_s) && scope.group_values.empty? && scope.having_clause.empty?
141
+ scope.pluck(model.arel_table[method])
142
+ else
143
+ scope.map { |record| checkbox_value(record) }
144
+ end
145
+ end
146
+
119
147
  # Per-table access check. Block receives the controller; falsy => 403.
120
148
  # authorize { |controller| controller.current_user&.admin? }
121
149
  def authorize(&block)
@@ -157,6 +185,7 @@ module Infotable
157
185
  searchable: options.fetch(:searchable, false),
158
186
  filterable: options.fetch(:filterable, false),
159
187
  visible: options.fetch(:visible, true),
188
+ exportable: options.fetch(:exportable, !%i[action actions].include?(type)), # false keeps the column out of CSV exports
160
189
  pinned: options[:pinned], # :left, :right, or nil
161
190
  virtual: options.fetch(:virtual, false), # Virtual/computed columns (aggregates, subqueries)
162
191
  virtual_sql: options[:virtual_sql], # SQL expression for aggregate columns (used in HAVING clause)
@@ -322,6 +351,26 @@ module Infotable
322
351
  @filters_button_settings[:visible] = visible
323
352
  end
324
353
 
354
+ # Where the controls live. :header (default) is the bar above the table with title, search,
355
+ # export and filters buttons. :rail hides that bar and puts filters, columns, export and
356
+ # refresh on a slim vertical rail at the table's right edge; search moves into the drawer.
357
+ # @example
358
+ # toolbar :rail
359
+ def toolbar(mode)
360
+ unless [ :header, :rail ].include?(mode)
361
+ raise ArgumentError, "Invalid toolbar: #{mode}. Must be one of header, rail"
362
+ end
363
+ @toolbar_setting = mode
364
+ end
365
+
366
+ def toolbar_mode
367
+ @toolbar_setting || :header
368
+ end
369
+
370
+ def rail_toolbar?
371
+ toolbar_mode == :rail
372
+ end
373
+
325
374
  # Check if filters button is visible
326
375
  def filters_button_visible?
327
376
  @filters_button_settings ||= { visible: true }
@@ -444,6 +493,36 @@ module Infotable
444
493
  columns.select { |col| col[:visible] }
445
494
  end
446
495
 
496
+ # Columns a CSV export may contain (action columns and `exportable: false` are left out)
497
+ def exportable_columns
498
+ columns.select { |col| col[:exportable] }
499
+ end
500
+
501
+ # Which columns the CSV export contains and in which order, when the user has not chosen:
502
+ # export_columns :name, :email, :created_at
503
+ # Without it the export uses the visible, exportable columns in table order. With it, the
504
+ # list is also the pool the user can pick from (visible_columns can only narrow and reorder it).
505
+ def export_columns(*names)
506
+ @export_column_names = names.flatten.map(&:to_sym)
507
+ end
508
+
509
+ def export_column_names
510
+ @export_column_names
511
+ end
512
+
513
+ # The columns to export, as column definitions in export order. `requested` is the list of
514
+ # names the browser sends (the user's visible columns in their on-screen order).
515
+ def columns_for_export(requested = nil)
516
+ pool = exportable_columns
517
+ pool = export_column_names.map { |name| pool.find { |col| col[:name] == name } || raise(ArgumentError, "export_columns: unknown or non-exportable column #{name.inspect} in #{self.name}") } if export_column_names
518
+
519
+ by_name = pool.index_by { |col| col[:name].to_s }
520
+ picked = Array(requested).map { |name| by_name[name.to_s] }.compact.uniq
521
+ return picked if picked.any?
522
+
523
+ export_column_names ? pool : pool.select { |col| col[:visible] }
524
+ end
525
+
447
526
  # Get sortable columns
448
527
  def sortable_columns
449
528
  columns.select { |col| col[:sortable] }
@@ -14,7 +14,8 @@ module Infotable
14
14
  :empty_value,
15
15
  :parent_controller,
16
16
  :max_filter_options,
17
- :url_state
17
+ :url_state,
18
+ :max_select_all
18
19
 
19
20
  def initialize
20
21
  @default_per_page = 25
@@ -30,6 +31,7 @@ module Infotable
30
31
  @nil_value = "-" # Default placeholder for nil values
31
32
  @empty_value = "-" # Default placeholder for empty strings
32
33
  @url_state = true # mirror table state in the address bar
34
+ @max_select_all = 10_000 # "select all matching" collects at most this many ids (nil = no limit)
33
35
  @max_filter_options = 500 # select filters render at most this many <option>s (nil = no limit)
34
36
  @parent_controller = "ActionController::Base" # Set to "ApplicationController" to inherit host auth/filters
35
37
  end
@@ -72,7 +72,7 @@ module Infotable
72
72
  action[:label] || action[:icon] || view_context.t("infotable.formatters.action"),
73
73
  resolve_url(action[:url], record),
74
74
  role: "menuitem",
75
- class: "block px-3 py-1.5 text-sm whitespace-nowrap #{link_classes(action[:color])} hover:bg-gray-50",
75
+ class: "block px-3 py-1.5 text-sm whitespace-nowrap #{link_classes(action[:color])} hover:bg-gray-50 dark:hover:bg-gray-800",
76
76
  data: link_data(action[:data] || {}, action[:method] || :get, action[:confirm])
77
77
  )
78
78
  end
@@ -81,10 +81,10 @@ module Infotable
81
81
  # <details> gives open/close and keyboard support without JS
82
82
  content_tag(:details, class: "relative inline-block text-left") do
83
83
  summary = content_tag(:summary, "\u22EF".html_safe,
84
- class: "list-none cursor-pointer inline-flex items-center px-2 py-1 rounded-md border border-gray-300 bg-white text-gray-600 hover:bg-gray-50",
84
+ class: "list-none cursor-pointer inline-flex items-center px-2 py-1 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800",
85
85
  "aria-label": view_context.t("infotable.formatters.action"), "aria-haspopup": "menu")
86
86
  menu = content_tag(:div, role: "menu",
87
- class: "absolute right-0 z-30 mt-1 min-w-36 rounded-md border border-gray-200 bg-white py-1 shadow-lg") do
87
+ class: "absolute right-0 z-30 mt-1 min-w-36 rounded-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 py-1 shadow-lg") do
88
88
  safe_join(actions.map { |action| render_menu_item(action, record) })
89
89
  end
90
90
  summary + menu
@@ -110,7 +110,7 @@ module Infotable
110
110
  when "success"
111
111
  "border-green-600 bg-green-600 text-white hover:bg-green-700"
112
112
  else
113
- "border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
113
+ "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
114
114
  end
115
115
 
116
116
  "#{base} #{color_classes}"
@@ -119,11 +119,11 @@ module Infotable
119
119
  def link_classes(color = nil)
120
120
  case color&.to_s
121
121
  when "danger"
122
- "text-red-600 hover:text-red-900"
122
+ "text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
123
123
  when "success"
124
- "text-green-600 hover:text-green-900"
124
+ "text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300"
125
125
  else
126
- "text-indigo-600 hover:text-indigo-900"
126
+ "text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-200"
127
127
  end
128
128
  end
129
129
 
@@ -10,8 +10,8 @@ module Infotable
10
10
  class AvatarFormatter < BaseFormatter
11
11
  SIZES = { sm: "h-6 w-6 text-xs", md: "h-8 w-8 text-sm", lg: "h-10 w-10 text-base" }.freeze
12
12
  COLORS = [
13
- "bg-indigo-100 text-indigo-700", "bg-green-100 text-green-700", "bg-yellow-100 text-yellow-700",
14
- "bg-pink-100 text-pink-700", "bg-blue-100 text-blue-700", "bg-purple-100 text-purple-700"
13
+ "bg-indigo-100 dark:bg-indigo-900/40 text-indigo-700 dark:text-indigo-300", "bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300", "bg-yellow-100 dark:bg-yellow-900/40 text-yellow-700 dark:text-yellow-300",
14
+ "bg-pink-100 dark:bg-pink-900/40 text-pink-700 dark:text-pink-300", "bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300", "bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300"
15
15
  ].freeze
16
16
 
17
17
  def format(value, record, options = {})
@@ -54,23 +54,23 @@ module Infotable
54
54
 
55
55
  color_classes = case color.to_s
56
56
  when "green"
57
- "bg-green-100 text-green-800"
57
+ "bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300"
58
58
  when "red"
59
- "bg-red-100 text-red-800"
59
+ "bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-300"
60
60
  when "yellow"
61
- "bg-yellow-100 text-yellow-800"
61
+ "bg-yellow-100 dark:bg-yellow-900/40 text-yellow-800 dark:text-yellow-300"
62
62
  when "blue"
63
- "bg-blue-100 text-blue-800"
63
+ "bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300"
64
64
  when "indigo"
65
- "bg-indigo-100 text-indigo-800"
65
+ "bg-indigo-100 dark:bg-indigo-900/40 text-indigo-800 dark:text-indigo-300"
66
66
  when "purple"
67
- "bg-purple-100 text-purple-800"
67
+ "bg-purple-100 dark:bg-purple-900/40 text-purple-800 dark:text-purple-300"
68
68
  when "pink"
69
- "bg-pink-100 text-pink-800"
69
+ "bg-pink-100 dark:bg-pink-900/40 text-pink-800 dark:text-pink-300"
70
70
  when "cyan"
71
- "bg-cyan-100 text-cyan-800"
71
+ "bg-cyan-100 dark:bg-cyan-900/40 text-cyan-800 dark:text-cyan-300"
72
72
  else # gray
73
- "bg-gray-100 text-gray-800"
73
+ "bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200"
74
74
  end
75
75
 
76
76
  "#{base} #{size_classes} #{color_classes}"
@@ -20,9 +20,9 @@ module Infotable
20
20
 
21
21
  def format_as_badge(value, true_text, false_text)
22
22
  if value
23
- content_tag(:span, true_text, class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800")
23
+ content_tag(:span, true_text, class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300")
24
24
  else
25
- content_tag(:span, false_text, class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800")
25
+ content_tag(:span, false_text, class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200")
26
26
  end
27
27
  end
28
28
 
@@ -10,7 +10,7 @@ module Infotable
10
10
  # Without url: the value itself is the URL (handy for website columns); mailto:/tel: work too.
11
11
  class LinkFormatter < BaseFormatter
12
12
  ALLOWED_SCHEMES = %w[http https mailto tel].freeze
13
- DEFAULT_CLASSES = "text-indigo-600 hover:text-indigo-800 hover:underline".freeze
13
+ DEFAULT_CLASSES = "text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-200 hover:underline".freeze
14
14
 
15
15
  def format(value, record, options = {})
16
16
  label = resolve(options[:label], record) || value
@@ -19,7 +19,7 @@ module Infotable
19
19
 
20
20
  width = number.clamp(0, 100).round
21
21
  bar = content_tag(:div, content_tag(:div, "", class: "h-full rounded-full bg-indigo-500", style: "width: #{width}%"),
22
- class: "h-1.5 w-16 rounded-full bg-gray-200 flex-none", role: "presentation")
22
+ class: "h-1.5 w-16 rounded-full bg-gray-200 dark:bg-gray-700 flex-none", role: "presentation")
23
23
  content_tag(:div, safe_join([ content_tag(:span, text, class: "tabular-nums"), bar ]), class: "flex items-center gap-2")
24
24
  end
25
25
  end
@@ -1,3 +1,3 @@
1
1
  module Infotable
2
- VERSION = "1.0.1"
2
+ VERSION = "1.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: infotable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Cieślar
@@ -145,16 +145,19 @@ files:
145
145
  - app/jobs/infotable/application_job.rb
146
146
  - app/mailers/infotable/application_mailer.rb
147
147
  - app/models/infotable/application_record.rb
148
+ - app/views/infotable/tables/_active_filters.html.erb
148
149
  - app/views/infotable/tables/_column_visibility.html.erb
149
150
  - app/views/infotable/tables/_filter_drawer.html.erb
150
151
  - app/views/infotable/tables/_next_page_frame.html.erb
151
152
  - app/views/infotable/tables/_pagination.html.erb
153
+ - app/views/infotable/tables/_rail.html.erb
152
154
  - app/views/infotable/tables/_side_panel.html.erb
153
155
  - app/views/infotable/tables/_table.html.erb
154
156
  - app/views/infotable/tables/_table_rows.html.erb
155
157
  - app/views/infotable/tables/filters/_boolean.html.erb
156
158
  - app/views/infotable/tables/filters/_date.html.erb
157
159
  - app/views/infotable/tables/filters/_number.html.erb
160
+ - app/views/infotable/tables/filters/_range.html.erb
158
161
  - app/views/infotable/tables/filters/_select.html.erb
159
162
  - app/views/infotable/tables/filters/_text.html.erb
160
163
  - app/views/infotable/tables/show.html.erb