infotable 0.1.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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1043 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascripts/infotable/controllers/drawer_controller.js +1412 -0
  6. data/app/assets/javascripts/infotable/controllers/table_controller.js +921 -0
  7. data/app/assets/javascripts/infotable/index.js +14 -0
  8. data/app/controllers/infotable/application_controller.rb +4 -0
  9. data/app/controllers/infotable/tables_controller.rb +299 -0
  10. data/app/helpers/infotable/application_helper.rb +5 -0
  11. data/app/helpers/infotable/tables_helper.rb +354 -0
  12. data/app/jobs/infotable/application_job.rb +4 -0
  13. data/app/mailers/infotable/application_mailer.rb +6 -0
  14. data/app/models/infotable/application_record.rb +5 -0
  15. data/app/views/infotable/tables/_column_visibility.html.erb +100 -0
  16. data/app/views/infotable/tables/_filter_drawer.html.erb +65 -0
  17. data/app/views/infotable/tables/_next_page_frame.html.erb +39 -0
  18. data/app/views/infotable/tables/_pagination.html.erb +101 -0
  19. data/app/views/infotable/tables/_side_panel.html.erb +63 -0
  20. data/app/views/infotable/tables/_table.html.erb +135 -0
  21. data/app/views/infotable/tables/_table_rows.html.erb +38 -0
  22. data/app/views/infotable/tables/filters/_boolean.html.erb +22 -0
  23. data/app/views/infotable/tables/filters/_date.html.erb +30 -0
  24. data/app/views/infotable/tables/filters/_number.html.erb +32 -0
  25. data/app/views/infotable/tables/filters/_select.html.erb +68 -0
  26. data/app/views/infotable/tables/filters/_text.html.erb +38 -0
  27. data/app/views/infotable/tables/show.html.erb +113 -0
  28. data/app/views/layouts/infotable/application.html.erb +15 -0
  29. data/config/importmap.rb +6 -0
  30. data/config/locales/en.yml +88 -0
  31. data/config/locales/pl.yml +88 -0
  32. data/config/routes.rb +6 -0
  33. data/lib/generators/infotable/install/install_generator.rb +565 -0
  34. data/lib/generators/infotable/install/templates/TAILWIND_CONFIG.md +131 -0
  35. data/lib/generators/infotable/install/templates/initializer.rb +26 -0
  36. data/lib/generators/infotable/install/templates/tailwind.config.js +284 -0
  37. data/lib/generators/infotable/table/table_generator.rb +95 -0
  38. data/lib/generators/infotable/table/templates/table.rb.tt +26 -0
  39. data/lib/infotable/base.rb +463 -0
  40. data/lib/infotable/batch_actions.rb +34 -0
  41. data/lib/infotable/configuration.rb +53 -0
  42. data/lib/infotable/engine.rb +51 -0
  43. data/lib/infotable/filter_processor.rb +326 -0
  44. data/lib/infotable/formatters/action_formatter.rb +141 -0
  45. data/lib/infotable/formatters/avatar_formatter.rb +49 -0
  46. data/lib/infotable/formatters/badge_formatter.rb +80 -0
  47. data/lib/infotable/formatters/base_formatter.rb +47 -0
  48. data/lib/infotable/formatters/boolean_formatter.rb +38 -0
  49. data/lib/infotable/formatters/currency_formatter.rb +22 -0
  50. data/lib/infotable/formatters/date_formatter.rb +58 -0
  51. data/lib/infotable/formatters/link_formatter.rb +48 -0
  52. data/lib/infotable/formatters/percent_formatter.rb +27 -0
  53. data/lib/infotable/search_processor.rb +42 -0
  54. data/lib/infotable/table_resolver.rb +45 -0
  55. data/lib/infotable/version.rb +3 -0
  56. data/lib/infotable.rb +54 -0
  57. data/lib/tasks/infotable.rake +55 -0
  58. metadata +216 -0
@@ -0,0 +1,5 @@
1
+ module Infotable
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,100 @@
1
+ <%# Column Visibility Panel %>
2
+ <div class="px-4 py-4">
3
+ <div class="mb-3">
4
+ <p class="text-sm text-gray-600"><%= t('infotable.columns.instructions') %></p>
5
+ </div>
6
+
7
+ <form id="column-visibility-form-<%= table_name %>"
8
+ data-table-name="<%= table_name %>"
9
+ data-infotable--drawer-target="columnList"
10
+ data-columns-schema="<%= table_class.columns.map { |c| { name: c[:name], pinned: c[:pinned] } }.to_json %>"
11
+ class="space-y-2">
12
+
13
+ <% table_class.columns.each do |column| %>
14
+ <div class="column-item group p-3 bg-white rounded-lg border border-gray-100 hover:border-indigo-200 transition-all duration-150"
15
+ data-column-name="<%= column[:name] %>"
16
+ data-default-visible="<%= column[:visible] %>"
17
+ data-default-pinned="<%= column[:pinned] %>">
18
+ <div class="flex items-center gap-3">
19
+ <%# Drag Handle %>
20
+ <div class="drag-handle cursor-move flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors">
21
+ <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
22
+ <path d="M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z"></path>
23
+ </svg>
24
+ </div>
25
+
26
+ <%# Checkbox and Label %>
27
+ <label class="flex items-center gap-3 cursor-pointer flex-1">
28
+ <input type="checkbox"
29
+ name="visible_columns[]"
30
+ value="<%= column[:name] %>"
31
+ data-column-name="<%= column[:name] %>"
32
+ data-default-visible="<%= column[:visible] %>"
33
+ data-action="change->infotable--drawer#updateColumnVisibility"
34
+ <%= 'checked' if column[:visible] %>
35
+ class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500 focus:ring-1 transition-colors">
36
+ <div class="flex-1">
37
+ <span class="text-sm font-medium text-gray-700"><%= column[:label] %></span>
38
+ <% if column[:type] %>
39
+ <span class="ml-2 text-xs text-gray-400">(<%= column[:type] %>)</span>
40
+ <% end %>
41
+ </div>
42
+ </label>
43
+
44
+ <%# Pin Controls %>
45
+ <div class="flex gap-1 flex-shrink-0">
46
+ <%# Pin Left %>
47
+ <button type="button"
48
+ data-action="click->infotable--drawer#pinColumn"
49
+ data-column-name="<%= column[:name] %>"
50
+ data-pin-direction="left"
51
+ title="<%= t('infotable.columns.pin.left') %>"
52
+ class="pin-btn pin-left w-7 h-7 rounded flex items-center justify-center <%= column[:pinned] == :left ? 'bg-indigo-100 text-indigo-600' : 'text-gray-400 hover:bg-gray-100 hover:text-gray-600' %> transition-colors">
53
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
54
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
55
+ </svg>
56
+ </button>
57
+ <%# Pin Right %>
58
+ <button type="button"
59
+ data-action="click->infotable--drawer#pinColumn"
60
+ data-column-name="<%= column[:name] %>"
61
+ data-pin-direction="right"
62
+ title="<%= t('infotable.columns.pin.right') %>"
63
+ class="pin-btn pin-right w-7 h-7 rounded flex items-center justify-center <%= column[:pinned] == :right ? 'bg-indigo-100 text-indigo-600' : 'text-gray-400 hover:bg-gray-100 hover:text-gray-600' %> transition-colors">
64
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
65
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
66
+ </svg>
67
+ </button>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ <% end %>
72
+
73
+ </form>
74
+
75
+ <%# Footer Actions %>
76
+ <div class="mt-4 pt-4 border-t border-gray-100 space-y-2">
77
+ <div class="flex gap-2">
78
+ <button type="button"
79
+ data-action="click->infotable--drawer#selectAllColumns"
80
+ data-table-name="<%= table_name %>"
81
+ class="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-indigo-500 transition-all">
82
+ <%= t('infotable.columns.actions.select_all') %>
83
+ </button>
84
+ <button type="button"
85
+ data-action="click->infotable--drawer#deselectAllColumns"
86
+ data-table-name="<%= table_name %>"
87
+ class="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-indigo-500 transition-all">
88
+ <%= t('infotable.columns.actions.deselect_all') %>
89
+ </button>
90
+ </div>
91
+
92
+ <%# Reset to Defaults Button %>
93
+ <button type="button"
94
+ data-action="click->infotable--drawer#resetToDefaults"
95
+ data-table-name="<%= table_name %>"
96
+ class="w-full px-3 py-2 border border-red-200 rounded-lg text-sm font-medium text-red-700 bg-red-50 hover:bg-red-100 hover:border-red-300 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-red-500 transition-all">
97
+ <%= t('infotable.columns.actions.reset_to_defaults') %>
98
+ </button>
99
+ </div>
100
+ </div>
@@ -0,0 +1,65 @@
1
+ <%# Filter Drawer - Right side sliding panel %>
2
+ <% autofilter = if config&.autofilter.nil?
3
+ Infotable.configuration.autofilter
4
+ else
5
+ config.autofilter
6
+ end %>
7
+ <div class="flex flex-col h-full">
8
+ <%# Drawer Content - Scrollable %>
9
+ <div class="flex-1 overflow-y-auto px-4 py-4">
10
+ <form id="filter-form-<%= table_name %>"
11
+ action="<%= infotable_table_data_path(table_name: table_name) %>"
12
+ method="get"
13
+ data-table-frame="<%= table_name %>_content"
14
+ data-action="submit->infotable--drawer#submitFilters"
15
+ data-autofilter="<%= autofilter.to_s %>"
16
+ class="space-y-4">
17
+
18
+ <%# Preserve sort and per_page params %>
19
+ <% if params[:sort].is_a?(String) && params[:sort].present? %>
20
+ <input type="hidden" name="sort" value="<%= params[:sort] %>">
21
+ <% end %>
22
+ <% if params[:sort_column].present? %>
23
+ <input type="hidden" name="sort_column" value="<%= params[:sort_column] %>">
24
+ <input type="hidden" name="sort_direction" value="<%= params[:sort_direction] %>">
25
+ <% end %>
26
+ <% if params[:per_page].present? %>
27
+ <input type="hidden" name="per_page" value="<%= params[:per_page] %>">
28
+ <% end %>
29
+
30
+ <%# Render filter for each filterable column %>
31
+ <% table_class.filterable_columns.each do |column| %>
32
+ <div class="group p-4 bg-white rounded-lg border border-gray-100 hover:border-indigo-200 transition-all duration-150">
33
+ <label class="block text-sm font-semibold text-gray-700 mb-3">
34
+ <%= column[:label] %>
35
+ </label>
36
+
37
+ <%= render "infotable/tables/filters/#{filter_type_for_column(column)}",
38
+ column: column,
39
+ table_name: table_name,
40
+ table_class: table_class %>
41
+ </div>
42
+ <% end %>
43
+
44
+ </form>
45
+ </div>
46
+
47
+ <%# Drawer Footer with actions %>
48
+ <div class="px-4 py-3 border-t border-gray-100 bg-white">
49
+ <div class="flex gap-2">
50
+ <button type="button"
51
+ data-action="click->infotable--drawer#clearFilters"
52
+ data-table-name="<%= table_name %>"
53
+ class="<%= autofilter ? 'w-full' : 'flex-1' %> px-4 py-2 border border-gray-200 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-indigo-500 transition-all">
54
+ <%= t('infotable.filters.actions.clear_all') %>
55
+ </button>
56
+ <% unless autofilter %>
57
+ <button type="submit"
58
+ form="filter-form-<%= table_name %>"
59
+ class="flex-1 px-4 py-2 border border-transparent rounded-lg text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-indigo-500 transition-all">
60
+ <%= t('infotable.filters.actions.apply_filters') %>
61
+ </button>
62
+ <% end %>
63
+ </div>
64
+ </div>
65
+ </div>
@@ -0,0 +1,39 @@
1
+ <%# Next page frame for infinite scroll - lazy loads when visible %>
2
+ <%# Frame src points to turbo_stream URL - Turbo handles it automatically %>
3
+ <% if @pagy.next %>
4
+ <%= turbo_frame_tag(
5
+ "#{params[:table_name]}_pagination",
6
+ loading: :lazy,
7
+ src: infotable_table_data_path(
8
+ table_url_params(page: @pagy.next, format: :turbo_stream)
9
+ )
10
+ ) do %>
11
+ <%# Loading placeholder - modern spinner design %>
12
+ <div class="flex items-center justify-center py-12 border-t border-gray-100 bg-gradient-to-b from-white/[0.98] to-gray-50/[0.98] backdrop-blur-sm">
13
+ <div class="text-center space-y-3">
14
+ <div class="mx-auto animate-spin">
15
+ <div class="relative">
16
+ <svg class="h-10 w-10 text-indigo-500 drop-shadow-[0_4px_12px_rgba(99,102,241,0.2)]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
17
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3"></circle>
18
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
19
+ </svg>
20
+ <div class="absolute inset-0 flex items-center justify-center">
21
+ <div class="h-4 w-4 bg-indigo-500 rounded-full animate-pulse"></div>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ <p class="text-sm font-medium text-gray-600 tracking-wide animate-pulse"><%= t('infotable.table.loading.more_items') %></p>
26
+ </div>
27
+ </div>
28
+ <% end %>
29
+ <% else %>
30
+ <%# No more pages - show end indicator %>
31
+ <div class="border-t border-gray-100 bg-gradient-to-b from-gray-50/60 to-white/60 px-6 py-8 text-center">
32
+ <div class="inline-flex items-center gap-2 text-sm text-gray-500">
33
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
34
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
35
+ </svg>
36
+ <span class="font-medium"><%= t('infotable.table.loading.all_loaded') %></span>
37
+ </div>
38
+ </div>
39
+ <% end %>
@@ -0,0 +1,101 @@
1
+ <%# Modern pagination using Pagy %>
2
+ <div class="flex flex-col sm:flex-row items-center justify-between gap-4">
3
+ <%# Results info and per-page selector %>
4
+ <div class="flex-1 flex items-center gap-4">
5
+ <p class="text-sm text-gray-700">
6
+ <%= t('infotable.pagination.showing') %>
7
+ <span class="font-semibold text-gray-900"><%= pagy.from %></span>
8
+ <%= t('infotable.pagination.to') %>
9
+ <span class="font-semibold text-gray-900"><%= pagy.to %></span>
10
+ <% if pagy.count %>
11
+ <%= t('infotable.pagination.of') %>
12
+ <span class="font-semibold text-gray-900"><%= pagy.count %></span>
13
+ <%= t('infotable.pagination.results') %>
14
+ <% end %>
15
+ </p>
16
+
17
+ <%# Per-page selector %>
18
+ <div class="flex items-center gap-2">
19
+ <label for="per_page_<%= table_name %>" class="text-sm text-gray-700">
20
+ <%= t('infotable.pagination.rows_per_page') %>
21
+ </label>
22
+ <select id="per_page_<%= table_name %>"
23
+ class="text-sm border border-gray-300 rounded-lg px-2 py-1 bg-white hover:border-gray-400 transition-colors cursor-pointer"
24
+ data-action="change->infotable--table#changePerPage"
25
+ data-table-name="<%= table_name %>"
26
+ data-current-query="<%= table_url_params.except(:table_name).to_query %>">
27
+ <% per_page_choices(@table_class, pagy.limit).each do |size| %>
28
+ <option value="<%= size %>" <%= 'selected' if pagy.limit == size %>>
29
+ <%= size %>
30
+ </option>
31
+ <% end %>
32
+ </select>
33
+ </div>
34
+ </div>
35
+
36
+ <%# Pagination controls %>
37
+ <div class="flex items-center gap-2">
38
+ <%# Previous button %>
39
+ <% if pagy.previous %>
40
+ <%= link_to infotable_table_data_path(table_url_params(page: pagy.previous, per_page: pagy.limit)),
41
+ class: "inline-flex items-center gap-1 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:border-gray-400 transition-all",
42
+ data: { turbo_frame: "#{table_name}_content" } do %>
43
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
44
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
45
+ </svg>
46
+ <span class="hidden sm:inline"><%= t('infotable.pagination.previous') %></span>
47
+ <% end %>
48
+ <% else %>
49
+ <span class="inline-flex items-center gap-1 px-3 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-200 rounded-lg cursor-not-allowed">
50
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
51
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
52
+ </svg>
53
+ <span class="hidden sm:inline"><%= t('infotable.pagination.previous') %></span>
54
+ </span>
55
+ <% end %>
56
+
57
+ <%# Page numbers - Show in ascending order with current page gray/disabled %>
58
+ <div class="flex items-center gap-1">
59
+ <%
60
+ # Get all pages from series, ensure current page is included, and sort numerically
61
+ pages = pagy.count ? infotable_page_window(pagy) : []
62
+ pages << pagy.page unless pages.include?(pagy.page)
63
+ pages = pages.uniq.sort
64
+ %>
65
+
66
+ <% pages.each do |page_num| %>
67
+ <% if page_num == pagy.page %>
68
+ <%# Current page - disabled/not clickable (gray) %>
69
+ <span aria-current="page" class="inline-flex items-center justify-center w-10 h-10 text-sm font-semibold rounded-lg bg-gray-200 text-gray-600 border border-gray-300 cursor-not-allowed">
70
+ <%= page_num %>
71
+ </span>
72
+ <% else %>
73
+ <%# Other pages - clickable %>
74
+ <%= link_to page_num,
75
+ infotable_table_data_path(table_url_params(page: page_num, per_page: pagy.limit)),
76
+ class: "inline-flex items-center justify-center w-10 h-10 text-sm font-medium rounded-lg transition-all text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 hover:border-gray-400",
77
+ data: { turbo_frame: "#{table_name}_content" } %>
78
+ <% end %>
79
+ <% end %>
80
+ </div>
81
+
82
+ <%# Next button %>
83
+ <% if pagy.next %>
84
+ <%= link_to infotable_table_data_path(table_url_params(page: pagy.next, per_page: pagy.limit)),
85
+ class: "inline-flex items-center gap-1 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:border-gray-400 transition-all",
86
+ data: { turbo_frame: "#{table_name}_content" } do %>
87
+ <span class="hidden sm:inline"><%= t('infotable.pagination.next') %></span>
88
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
89
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
90
+ </svg>
91
+ <% end %>
92
+ <% else %>
93
+ <span class="inline-flex items-center gap-1 px-3 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-200 rounded-lg cursor-not-allowed">
94
+ <span class="hidden sm:inline"><%= t('infotable.pagination.next') %></span>
95
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
96
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
97
+ </svg>
98
+ </span>
99
+ <% end %>
100
+ </div>
101
+ </div>
@@ -0,0 +1,63 @@
1
+ <%# Side Panel with Tabs for Filters and Columns %>
2
+ <div class="flex flex-col h-full bg-white">
3
+ <%# Panel Header %>
4
+ <div class="flex items-center justify-between px-4 py-3 bg-white border-b border-gray-100">
5
+ <div class="flex items-center gap-2.5">
6
+ <div class="p-2 bg-indigo-50 rounded-lg">
7
+ <svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
8
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
9
+ </svg>
10
+ </div>
11
+ <h3 class="text-base font-semibold text-gray-900" data-infotable--drawer-target="panelTitle" data-columns-title="<%= t('infotable.drawer.panel_title.customize_columns') %>" data-filters-title="<%= t('infotable.drawer.panel_title.filters') %>"><%= table_class.filterable_columns.any? ? t('infotable.drawer.panel_title.filters') : t('infotable.drawer.panel_title.customize_columns') %></h3>
12
+ </div>
13
+ <button type="button"
14
+ data-action="click->infotable--drawer#close"
15
+ class="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-50 rounded-md transition-colors">
16
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
17
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
18
+ </svg>
19
+ </button>
20
+ </div>
21
+
22
+ <%# Tabs Navigation %>
23
+ <div class="flex border-b border-gray-100 bg-white px-4">
24
+ <% if table_class.filterable_columns.any? %>
25
+ <button type="button"
26
+ data-action="click->infotable--drawer#switchTab"
27
+ data-tab="filters"
28
+ data-infotable--drawer-target="tabButton"
29
+ class="flex-1 px-3 py-2.5 text-sm font-medium text-indigo-600 border-b-2 border-indigo-600 transition-colors">
30
+ <%= t('infotable.drawer.tabs.filters') %>
31
+ </button>
32
+ <% end %>
33
+ <button type="button"
34
+ data-action="click->infotable--drawer#switchTab"
35
+ data-tab="columns"
36
+ data-infotable--drawer-target="tabButton"
37
+ data-table-name="<%= table_name %>"
38
+ class="flex-1 px-3 py-2.5 text-sm font-medium <%= table_class.filterable_columns.any? ? 'text-gray-500 border-b-2 border-transparent hover:text-gray-700' : 'text-indigo-600 border-b-2 border-indigo-600' %> transition-colors flex items-center justify-center gap-2">
39
+ <span><%= t('infotable.drawer.tabs.columns') %></span>
40
+ <span data-infotable--drawer-target="columnBadge"
41
+ data-badge-table-name="<%= table_name %>"
42
+ class="px-2 py-0.5 text-xs font-medium bg-indigo-100 text-indigo-700 rounded-full">
43
+ <%= table_class.columns.count { |c| c[:visible] } %> / <%= table_class.columns.count %>
44
+ </span>
45
+ </button>
46
+ </div>
47
+
48
+ <%# Tab Content - Filters (shown by default when available) %>
49
+ <% if table_class.filterable_columns.any? %>
50
+ <div data-infotable--drawer-target="tabContent"
51
+ data-tab="filters"
52
+ class="flex-1 overflow-y-auto">
53
+ <%= render "infotable/tables/filter_drawer", table_name: table_name, table_class: table_class, config: config %>
54
+ </div>
55
+ <% end %>
56
+
57
+ <%# Tab Content - Columns (shown by default when filters are not available) %>
58
+ <div data-infotable--drawer-target="tabContent"
59
+ data-tab="columns"
60
+ class="<%= table_class.filterable_columns.any? ? 'hidden' : '' %> flex-1 overflow-y-auto">
61
+ <%= render "infotable/tables/column_visibility", table_name: table_name, table_class: table_class %>
62
+ </div>
63
+ </div>
@@ -0,0 +1,135 @@
1
+ <%# Main table wrapper %>
2
+ <%# Build config object if not provided (when partial is called directly) %>
3
+ <% if local_assigns.key?(:config) && !local_assigns[:config].nil? %>
4
+ <%# Config provided - use it, but allow autofilter override %>
5
+ <% config = local_assigns[:config] %>
6
+ <% if local_assigns.key?(:autofilter) %>
7
+ <% config.autofilter = local_assigns[:autofilter] %>
8
+ <% end %>
9
+ <% else %>
10
+ <%# No config provided or config is nil - build it from local_assigns or defaults %>
11
+ <% config = ActiveSupport::OrderedOptions.new.merge!({
12
+ autofilter: local_assigns.fetch(:autofilter, Infotable.configuration.autofilter),
13
+ per_page: table_class.pagination_settings[:per_page],
14
+ per_page_options: table_class.pagination_settings[:per_page_options],
15
+ drawer_position: Infotable.configuration.drawer_position,
16
+ drawer_width: Infotable.configuration.drawer_width,
17
+ theme: Infotable.configuration.theme
18
+ }) %>
19
+ <% end %>
20
+ <%# Set title and subtitle with priority: local_assigns > table_class settings > defaults %>
21
+ <% title = local_assigns.fetch(:title, table_class.title || table_class.name.demodulize.titleize) %>
22
+ <% subtitle = local_assigns.fetch(:subtitle, table_class.subtitle) %>
23
+ <% initial_state = table_class.url_state? ? infotable_initial_state(table_name) : {} %>
24
+ <div class="w-full relative overflow-hidden" data-controller="infotable--table infotable--drawer" data-infotable--table-name-value="<%= table_name %>" data-infotable--table-url-state-value="<%= table_class.url_state? %>">
25
+ <div class="bg-white rounded-xl shadow-sm border border-gray-200 relative" style="overflow: hidden;">
26
+ <%# Header section with controls %>
27
+ <div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
28
+ <div class="flex justify-between items-center">
29
+ <div>
30
+ <h2 class="text-xl font-semibold text-gray-900"><%= title %></h2>
31
+ <% if subtitle.present? %>
32
+ <p class="mt-1 text-sm text-gray-500"><%= subtitle %></p>
33
+ <% end %>
34
+ </div>
35
+ <div class="flex gap-3">
36
+ <%# Global search over `searchable: true` columns %>
37
+ <% if table_class.searchable_columns.any? %>
38
+ <input type="search"
39
+ name="search"
40
+ value="<%= initial_state[:search] %>"
41
+ placeholder="<%= t('infotable.search.placeholder') %>"
42
+ aria-label="<%= t('infotable.search.label') %>"
43
+ data-action="input->infotable--table#search"
44
+ class="w-56 px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-700 bg-white focus:outline-none focus:ring-2 focus:ring-indigo-500">
45
+ <% end %>
46
+ <%# Export CSV button %>
47
+ <%
48
+ export_params = {
49
+ table_name: table_name,
50
+ sort_column: initial_state[:sort_column],
51
+ sort_direction: initial_state[:sort_direction],
52
+ sort: initial_state[:sort],
53
+ visible_columns: table_class.visible_columns.map { |c| c[:name].to_s }.join(',')
54
+ }
55
+ export_params[:filters] = initial_state[:filters] if initial_state[:filters].present?
56
+ export_params[:search] = initial_state[:search] if initial_state[:search].present?
57
+ export_url = Infotable::Engine.routes.url_helpers.table_export_path(export_params.compact)
58
+ %>
59
+ <a href="<%= export_url %>"
60
+ data-action="click->infotable--table#exportCsv"
61
+ data-infotable--table-target="exportButton"
62
+ data-exporting-text="<%= t('infotable.table.buttons.exporting') %>"
63
+ data-turbo="false"
64
+ download
65
+ class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors">
66
+ <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
67
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
68
+ </svg>
69
+ <span data-infotable--table-target="exportButtonText"><%= t('infotable.table.buttons.export_csv') %></span>
70
+ </a>
71
+ <%# Drawer toggle button - conditionally shown %>
72
+ <% if table_class.filters_button_visible? %>
73
+ <button type="button"
74
+ data-action="click->infotable--drawer#open"
75
+ class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors">
76
+ <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
77
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
78
+ </svg>
79
+ <%= t('infotable.table.buttons.filters') %>
80
+ </button>
81
+ <% end %>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ <%# Batch action bar: shown while rows are selected %>
87
+ <% if table_class.batch_action_list.any? %>
88
+ <div class="hidden items-center justify-between gap-3 px-6 py-3 border-b border-indigo-100 bg-indigo-50"
89
+ data-infotable--table-target="batchBar">
90
+ <div class="flex items-center gap-3 text-sm text-indigo-900">
91
+ <span data-infotable--table-target="batchCount"
92
+ data-template="<%= t('infotable.batch.selected', count: '__COUNT__') %>"></span>
93
+ <button type="button" data-action="click->infotable--table#clearSelection"
94
+ class="text-indigo-600 hover:text-indigo-800 underline"><%= t('infotable.batch.clear') %></button>
95
+ </div>
96
+ <div class="flex gap-2">
97
+ <% table_class.batch_action_list.each do |action| %>
98
+ <button type="button"
99
+ data-action="click->infotable--table#runBatchAction"
100
+ data-batch-name="<%= action[:name] %>"
101
+ data-batch-url="<%= action[:url] %>"
102
+ data-batch-method="<%= action[:method] %>"
103
+ data-batch-confirm="<%= action[:confirm] %>"
104
+ class="<%= infotable_batch_button_classes(action[:color]) %>"><%= action[:label] %></button>
105
+ <% end %>
106
+ </div>
107
+ </div>
108
+ <% end %>
109
+
110
+ <%# Main content frame %>
111
+ <turbo-frame id="<%= table_name %>_content"
112
+ src="<%= infotable_table_path(initial_state.merge(table_name: table_name)) %>"
113
+ data-action="turbo:frame-load->infotable--table#handleFrameLoad">
114
+ <%# Loading state %>
115
+ <div class="flex items-center justify-center py-16">
116
+ <div class="text-center">
117
+ <svg class="animate-spin h-8 w-8 text-indigo-600 mx-auto mb-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
118
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
119
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
120
+ </svg>
121
+ <p class="text-sm text-gray-500"><%= t('infotable.table.loading.initial') %></p>
122
+ </div>
123
+ </div>
124
+ </turbo-frame>
125
+ </div>
126
+
127
+ <%# Side panel drawer - always available for column visibility, filters if available %>
128
+ <%# Drawer - positioned within table bounds, starts completely hidden off-screen %>
129
+ <div <%= tag.attributes(infotable_drawer_attributes(config)) %>>
130
+ <div data-infotable--drawer-target="content" class="h-full flex flex-col overflow-hidden">
131
+ <%= render "infotable/tables/side_panel", table_name: table_name, table_class: table_class, config: config %>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
@@ -0,0 +1,38 @@
1
+ <% records.each do |record| %>
2
+ <%
3
+ row_link_enabled = table_class.row_link_enabled?
4
+ row_url = row_link_enabled ? table_class.row_link_url(record) : nil
5
+ row_size = table_class.row_size_value
6
+ cell_padding = case row_size
7
+ when :slim then "px-2 py-1"
8
+ when :mid then "px-4 py-2.5"
9
+ else "px-6 py-4" # default
10
+ end
11
+ text_size = case row_size
12
+ when :slim then "text-xs"
13
+ when :mid then "text-sm"
14
+ else "text-sm" # default
15
+ end
16
+ %>
17
+ <tr class="infotable-row relative border-t border-gray-50 first:border-t-0 hover:shadow-sm hover:translate-x-px transition-all duration-200 ease-out <%= 'cursor-pointer' if row_link_enabled %>"
18
+ <%= tag.attributes(data: { row_link: row_url, action: "click->infotable--table#handleRowClick" }) if row_link_enabled && row_url %>>
19
+ <% if table_class.checkbox_enabled? %>
20
+ <td class="<%= cell_padding %> w-10">
21
+ <input type="checkbox"
22
+ value="<%= table_class.checkbox_value(record) %>"
23
+ aria-label="<%= t('infotable.batch.select_row') %>"
24
+ data-infotable--table-target="rowCheckbox"
25
+ data-action="change->infotable--table#toggleRow"
26
+ class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
27
+ </td>
28
+ <% end %>
29
+ <% table_class.columns.each do |column| %>
30
+ <td data-column-name="<%= column[:name] %>"
31
+ data-default-visible="<%= column[:visible] %>"
32
+ style="width: <%= column[:width] %>; max-width: <%= column[:width] %>; overflow: hidden; text-overflow: ellipsis;"
33
+ class="<%= cell_padding %> whitespace-nowrap <%= text_size %> text-gray-700">
34
+ <%= format_cell_value(record, column, table_class) %>
35
+ </td>
36
+ <% end %>
37
+ </tr>
38
+ <% end %>
@@ -0,0 +1,22 @@
1
+ <%# Boolean filter - true/false/empty/not empty %>
2
+ <div class="relative">
3
+ <select name="filters[<%= column[:name] %>][value]"
4
+ id="<%= infotable_filter_id(table_name, column, 'value') %>"
5
+ 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]">
6
+ <option value=""><%= t('infotable.filters.all') %></option>
7
+ <option value="true"><%= t('infotable.filters.boolean.true') %></option>
8
+ <option value="false"><%= t('infotable.filters.boolean.false') %></option>
9
+ <option value="is_empty"><%= t('infotable.filters.boolean.empty') %></option>
10
+ <option value="is_not_empty"><%= t('infotable.filters.boolean.not_empty') %></option>
11
+ </select>
12
+ <button type="button"
13
+ class="absolute right-8 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
14
+ data-action="click->infotable--drawer#clearSingleFilter"
15
+ data-filter-name="<%= column[:name] %>"
16
+ data-filter-type="boolean"
17
+ title="<%= t('infotable.filters.clear_filter') %>">
18
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
19
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
20
+ </svg>
21
+ </button>
22
+ </div>
@@ -0,0 +1,30 @@
1
+ <%# Date filter - from/to range with date pickers %>
2
+ <div class="grid grid-cols-2 gap-2">
3
+ <%# From date %>
4
+ <div class="relative">
5
+ <input type="date"
6
+ name="filters[<%= column[:name] %>][from]"
7
+ id="<%= infotable_filter_id(table_name, column, 'from') %>"
8
+ placeholder="<%= t('infotable.filters.from') %>"
9
+ 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">
10
+ <button type="button"
11
+ class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
12
+ data-action="click->infotable--drawer#clearSingleFilter"
13
+ data-filter-name="<%= column[:name] %>"
14
+ data-filter-type="date"
15
+ title="<%= t('infotable.filters.clear_filter') %>">
16
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
17
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
18
+ </svg>
19
+ </button>
20
+ </div>
21
+
22
+ <%# To date %>
23
+ <div class="relative">
24
+ <input type="date"
25
+ name="filters[<%= column[:name] %>][to]"
26
+ id="<%= infotable_filter_id(table_name, column, 'to') %>"
27
+ placeholder="<%= t('infotable.filters.to') %>"
28
+ class="block w-full rounded-lg border-0 bg-white pr-3 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">
29
+ </div>
30
+ </div>