infotable 1.4.1 → 1.6.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.
- checksums.yaml +4 -4
- data/README.md +179 -11
- data/app/assets/javascripts/infotable/controllers/drawer_controller.js +74 -378
- data/app/assets/javascripts/infotable/controllers/table_controller.js +112 -120
- data/app/assets/javascripts/infotable/vendor/sortable.esm.js +3363 -0
- data/app/controllers/infotable/tables_controller.rb +16 -5
- data/app/helpers/infotable/tables_helper.rb +10 -48
- data/app/views/infotable/tables/_column_visibility.html.erb +4 -4
- data/app/views/infotable/tables/_end_indicator.html.erb +8 -0
- data/app/views/infotable/tables/_filter_drawer.html.erb +6 -6
- data/app/views/infotable/tables/_next_page_frame.html.erb +1 -9
- data/app/views/infotable/tables/_pagination.html.erb +4 -7
- data/app/views/infotable/tables/_table.html.erb +1 -1
- data/app/views/infotable/tables/_table_rows.html.erb +9 -2
- data/app/views/infotable/tables/filters/_boolean.html.erb +1 -1
- data/app/views/infotable/tables/filters/_range.html.erb +1 -1
- data/app/views/infotable/tables/filters/_select.html.erb +2 -2
- data/app/views/infotable/tables/filters/_text.html.erb +2 -2
- data/app/views/infotable/tables/show.html.erb +2 -2
- data/config/importmap.rb +2 -2
- data/config/locales/de.yml +126 -0
- data/config/locales/el.yml +126 -0
- data/config/locales/en.yml +18 -4
- data/config/locales/es.yml +126 -0
- data/config/locales/fr.yml +126 -0
- data/config/locales/it.yml +126 -0
- data/config/locales/pl.yml +18 -4
- data/exe/infotable-mcp +13 -0
- data/lib/generators/infotable/install/templates/initializer.rb +5 -3
- data/lib/generators/infotable/table/table_generator.rb +23 -1
- data/lib/generators/infotable/table/templates/table.rb.tt +8 -1
- data/lib/infotable/base.rb +62 -4
- data/lib/infotable/configuration.rb +1 -1
- data/lib/infotable/engine.rb +1 -8
- data/lib/infotable/filter_processor.rb +10 -1
- data/lib/infotable/formatters/boolean_formatter.rb +4 -4
- data/lib/infotable/formatters/currency_formatter.rb +4 -12
- data/lib/infotable/formatters/date_formatter.rb +3 -9
- data/lib/infotable/formatters/duration_formatter.rb +51 -0
- data/lib/infotable/formatters/filesize_formatter.rb +14 -0
- data/lib/infotable/formatters/image_formatter.rb +31 -0
- data/lib/infotable/formatters/number_formatter.rb +46 -0
- data/lib/infotable/formatters/tags_formatter.rb +44 -0
- data/lib/infotable/formatters/truncate_formatter.rb +19 -0
- data/lib/infotable/mcp/introspection.rb +94 -0
- data/lib/infotable/mcp/server.rb +26 -0
- data/lib/infotable/mcp/table_checker.rb +80 -0
- data/lib/infotable/mcp/table_writer.rb +66 -0
- data/lib/infotable/mcp/tools.rb +122 -0
- data/lib/infotable/mcp.rb +30 -0
- data/lib/infotable/table_resolver.rb +9 -0
- data/lib/infotable/version.rb +1 -1
- data/lib/infotable.rb +22 -0
- metadata +53 -4
|
@@ -27,6 +27,10 @@ module Infotable
|
|
|
27
27
|
format.turbo_stream {
|
|
28
28
|
# For infinite scroll - return turbo stream to append rows and replace pagination frame
|
|
29
29
|
# Following the guide's "Frames + Streams" pattern
|
|
30
|
+
if @page_out_of_range # nothing left: appending the first page again would repeat its rows
|
|
31
|
+
return render turbo_stream: turbo_stream.replace("#{params[:table_name]}_pagination", partial: "infotable/tables/end_indicator")
|
|
32
|
+
end
|
|
33
|
+
|
|
30
34
|
streams = [
|
|
31
35
|
turbo_stream.append(
|
|
32
36
|
"#{params[:table_name]}_tbody",
|
|
@@ -48,7 +52,7 @@ module Infotable
|
|
|
48
52
|
# No more pages - replace with end indicator
|
|
49
53
|
streams << turbo_stream.replace(
|
|
50
54
|
"#{params[:table_name]}_pagination",
|
|
51
|
-
|
|
55
|
+
partial: "infotable/tables/end_indicator"
|
|
52
56
|
)
|
|
53
57
|
end
|
|
54
58
|
|
|
@@ -139,6 +143,7 @@ module Infotable
|
|
|
139
143
|
# Apply pagination
|
|
140
144
|
@pagy, @records = paginate(@page)
|
|
141
145
|
rescue Pagy::RangeError # page past the end: show the first page
|
|
146
|
+
@page_out_of_range = true
|
|
142
147
|
@pagy, @records = paginate(1)
|
|
143
148
|
end
|
|
144
149
|
|
|
@@ -207,7 +212,7 @@ module Infotable
|
|
|
207
212
|
def add_sort_tiebreaker(query, sort_columns)
|
|
208
213
|
model = query.model
|
|
209
214
|
pk = model.primary_key
|
|
210
|
-
return query if pk.blank? || sort_columns.map(&:to_s).include?(pk) || query.distinct_value
|
|
215
|
+
return query if pk.blank? || sort_columns.map(&:to_s).include?(pk) || query.distinct_value || query.group_values.any?
|
|
211
216
|
|
|
212
217
|
query.order(Arel.sql("#{model.table_name}.#{pk} ASC"))
|
|
213
218
|
end
|
|
@@ -228,9 +233,15 @@ module Infotable
|
|
|
228
233
|
requested.positive? ? [ requested, MAX_PER_PAGE ].min : default
|
|
229
234
|
end
|
|
230
235
|
|
|
231
|
-
|
|
236
|
+
PLAIN_NUMBER = /\A[+-]?\d+([.,]\d+)?\z/
|
|
237
|
+
|
|
238
|
+
# Guard against spreadsheet formula injection. Plain numbers ("-5", "+48123456789") are
|
|
239
|
+
# not formulas and are left alone.
|
|
232
240
|
def csv_safe(value)
|
|
233
|
-
value.is_a?(String) && value.match?(/\A[=+\-@\t\r]/)
|
|
241
|
+
return value unless value.is_a?(String) && value.match?(/\A[=+\-@\t\r]/)
|
|
242
|
+
return value if value.match?(PLAIN_NUMBER)
|
|
243
|
+
|
|
244
|
+
"'#{value}"
|
|
234
245
|
end
|
|
235
246
|
|
|
236
247
|
def render_table_frame
|
|
@@ -302,7 +313,7 @@ module Infotable
|
|
|
302
313
|
when :boolean
|
|
303
314
|
value ? "true" : "false"
|
|
304
315
|
else
|
|
305
|
-
value.to_s
|
|
316
|
+
value.is_a?(Array) ? value.join(", ") : value.to_s
|
|
306
317
|
end
|
|
307
318
|
end
|
|
308
319
|
end
|
|
@@ -89,29 +89,6 @@ module Infotable
|
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
# Build export params with current filters, sort, and visible columns
|
|
93
|
-
def build_export_params(table_name, table_class)
|
|
94
|
-
params_hash = {
|
|
95
|
-
table_name: table_name,
|
|
96
|
-
sort_column: scalar_request_param(:sort_column),
|
|
97
|
-
sort_direction: scalar_request_param(:sort_direction),
|
|
98
|
-
sort: scalar_request_param(:sort)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
# Add filters if present
|
|
102
|
-
if params[:filters].present?
|
|
103
|
-
params_hash[:filters] = params[:filters]
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Get visible columns from localStorage via JavaScript or use defaults
|
|
107
|
-
# For now, we'll pass all visible columns and let the controller handle it
|
|
108
|
-
# The JavaScript will need to update the link dynamically
|
|
109
|
-
visible_column_names = table_class.visible_columns.map { |c| c[:name].to_s }
|
|
110
|
-
params_hash[:visible_columns] = visible_column_names.join(",")
|
|
111
|
-
|
|
112
|
-
params_hash.compact
|
|
113
|
-
end
|
|
114
|
-
|
|
115
92
|
private
|
|
116
93
|
|
|
117
94
|
def resolve_table_class(table_name)
|
|
@@ -143,29 +120,11 @@ module Infotable
|
|
|
143
120
|
formatter_name = column[:formatter]
|
|
144
121
|
formatter_options = column[:formatter_options] || {}
|
|
145
122
|
|
|
146
|
-
formatter_class =
|
|
147
|
-
|
|
148
|
-
Infotable::Formatters::DateFormatter
|
|
149
|
-
when :currency
|
|
150
|
-
Infotable::Formatters::CurrencyFormatter
|
|
151
|
-
when :boolean
|
|
152
|
-
Infotable::Formatters::BooleanFormatter
|
|
153
|
-
when :badge, :status
|
|
154
|
-
Infotable::Formatters::BadgeFormatter
|
|
155
|
-
when :action, :actions
|
|
156
|
-
Infotable::Formatters::ActionFormatter
|
|
157
|
-
when :link
|
|
158
|
-
Infotable::Formatters::LinkFormatter
|
|
159
|
-
when :avatar
|
|
160
|
-
Infotable::Formatters::AvatarFormatter
|
|
161
|
-
when :percent
|
|
162
|
-
Infotable::Formatters::PercentFormatter
|
|
163
|
-
else
|
|
164
|
-
# Custom formatter: :avatar => AvatarFormatter (or Namespaced::AvatarFormatter)
|
|
123
|
+
formatter_class = Infotable::Formatters.built_in[formatter_name.to_sym] ||
|
|
124
|
+
# Custom formatter: :stars => StarsFormatter (or Namespaced::StarsFormatter)
|
|
165
125
|
"#{formatter_name.to_s.camelize}Formatter".safe_constantize ||
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
end
|
|
126
|
+
raise(Infotable::Error, "Unknown formatter :#{formatter_name} for column :#{column[:name]}. " \
|
|
127
|
+
"Built-ins: #{Infotable::Formatters.built_in.keys.join(', ')}; custom: define #{formatter_name.to_s.camelize}Formatter.")
|
|
169
128
|
|
|
170
129
|
formatter_class.new(self).format(value, record, formatter_options)
|
|
171
130
|
end
|
|
@@ -262,14 +221,17 @@ module Infotable
|
|
|
262
221
|
DEFAULT_DRAWER_WIDTH = "26rem".freeze
|
|
263
222
|
|
|
264
223
|
# HTML attributes for the slide-in drawer (side and width from config)
|
|
265
|
-
|
|
224
|
+
# `rail`: the drawer sits flush against the rail, so no rounded corners or heavy shadow
|
|
225
|
+
def infotable_drawer_attributes(config, rail: false)
|
|
266
226
|
side = config.drawer_position.to_s == "left" ? "left" : "right"
|
|
267
227
|
width = config.drawer_width.to_s.match?(DRAWER_WIDTH_FORMAT) ? config.drawer_width.to_s : DEFAULT_DRAWER_WIDTH
|
|
268
228
|
hidden = side == "left" ? "-100%" : "100%"
|
|
269
229
|
|
|
230
|
+
# With a rail the drawer must line up with the card's inner edge: inset it by the card's 1px border
|
|
231
|
+
inset = rail ? "1px" : "0"
|
|
270
232
|
{
|
|
271
|
-
style: "#{side}:
|
|
272
|
-
class: "absolute top-0 bottom-0 bg-white dark:bg-gray-900 #{side == 'left' ? 'border-r rounded-r-xl' : '
|
|
233
|
+
style: "#{side}: #{inset}; #{"top: 1px; bottom: 1px; " if rail}width: #{width}; max-width: 90%; transform: translateX(#{hidden});",
|
|
234
|
+
class: "absolute top-0 bottom-0 bg-white dark:bg-gray-900 #{side == 'left' ? 'border-r' : 'border-l'}#{side == 'left' ? (rail ? ' rounded-l-[11px]' : ' rounded-r-xl') : (rail ? ' rounded-r-[11px]' : ' rounded-l-xl')} border-gray-200 dark:border-gray-700 #{rail ? 'shadow-none' : 'shadow-xl'} transition-all duration-300 ease-in-out z-50",
|
|
273
235
|
role: "dialog",
|
|
274
236
|
inert: true, # closed drawer is off-screen: keep it out of the tab order (JS toggles it)
|
|
275
237
|
aria: { label: I18n.t("infotable.drawer.label"), hidden: true },
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
data-pin-direction="left"
|
|
51
51
|
title="<%= t('infotable.columns.pin.left') %>"
|
|
52
52
|
class="pin-btn pin-left w-7 h-7 rounded flex items-center justify-center <%= column[:pinned] == :left ? 'bg-indigo-100 dark:bg-indigo-900/40 text-indigo-600 dark:text-indigo-400' : 'text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300' %> transition-colors">
|
|
53
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
54
|
-
<path
|
|
53
|
+
<svg class="w-4 h-4 -rotate-45" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
|
54
|
+
<path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/>
|
|
55
55
|
</svg>
|
|
56
56
|
</button>
|
|
57
57
|
<%# Pin Right %>
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
data-pin-direction="right"
|
|
62
62
|
title="<%= t('infotable.columns.pin.right') %>"
|
|
63
63
|
class="pin-btn pin-right w-7 h-7 rounded flex items-center justify-center <%= column[:pinned] == :right ? 'bg-indigo-100 dark:bg-indigo-900/40 text-indigo-600 dark:text-indigo-400' : 'text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300' %> transition-colors">
|
|
64
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
65
|
-
<path
|
|
64
|
+
<svg class="w-4 h-4 rotate-45" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
|
65
|
+
<path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/>
|
|
66
66
|
</svg>
|
|
67
67
|
</button>
|
|
68
68
|
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div class="border-t border-gray-100 dark:border-gray-800 bg-gradient-to-b from-gray-50/60 to-white/60 px-6 py-8 text-center">
|
|
2
|
+
<div class="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
|
|
3
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
4
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
5
|
+
</svg>
|
|
6
|
+
<span class="font-medium"><%= t('infotable.table.loading.all_loaded') %></span>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
end %>
|
|
7
7
|
<div class="flex flex-col h-full">
|
|
8
8
|
<%# Drawer Content - Scrollable %>
|
|
9
|
-
<div class="flex-1 overflow-y-auto px-
|
|
9
|
+
<div class="flex-1 overflow-y-auto px-5 py-1 bg-white dark:bg-gray-900">
|
|
10
10
|
<form id="filter-form-<%= table_name %>"
|
|
11
11
|
action="<%= infotable_table_data_path(table_name: table_name) %>"
|
|
12
12
|
method="get"
|
|
13
13
|
data-table-frame="<%= table_name %>_content"
|
|
14
14
|
data-action="submit->infotable--drawer#submitFilters"
|
|
15
15
|
data-autofilter="<%= autofilter.to_s %>"
|
|
16
|
-
class="
|
|
16
|
+
class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
17
17
|
|
|
18
18
|
<%# Preserve sort and per_page params %>
|
|
19
19
|
<% if params[:sort].is_a?(String) && params[:sort].present? %>
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
|
|
30
30
|
<%# Render filter for each filterable column %>
|
|
31
31
|
<% table_class.filterable_columns.each do |column| %>
|
|
32
|
-
<div class="
|
|
33
|
-
<label class="block text-
|
|
32
|
+
<div class="py-4">
|
|
33
|
+
<label class="block text-[13px] font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
34
34
|
<%= column[:label] %>
|
|
35
35
|
</label>
|
|
36
36
|
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
</div>
|
|
46
46
|
|
|
47
47
|
<%# Drawer Footer with actions %>
|
|
48
|
-
<div class="px-
|
|
48
|
+
<div class="px-5 py-3 border-t border-gray-100 dark:border-gray-800 bg-white dark:bg-gray-900">
|
|
49
49
|
<div class="flex gap-2">
|
|
50
50
|
<button type="button"
|
|
51
51
|
data-action="click->infotable--drawer#clearFilters"
|
|
52
52
|
data-table-name="<%= table_name %>"
|
|
53
|
-
class="<%= autofilter ? 'w-full' : 'flex-1' %> px-4 py-2
|
|
53
|
+
class="<%= autofilter ? 'w-full' : 'flex-1' %> px-4 py-2 rounded-lg text-sm font-medium text-gray-600 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-indigo-500 transition-all">
|
|
54
54
|
<%= t('infotable.filters.actions.clear_all') %>
|
|
55
55
|
</button>
|
|
56
56
|
<% unless autofilter %>
|
|
@@ -27,13 +27,5 @@
|
|
|
27
27
|
</div>
|
|
28
28
|
<% end %>
|
|
29
29
|
<% else %>
|
|
30
|
-
|
|
31
|
-
<div class="border-t border-gray-100 dark:border-gray-800 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 dark:text-gray-400">
|
|
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>
|
|
30
|
+
<%= render partial: "infotable/tables/end_indicator" %>
|
|
39
31
|
<% end %>
|
|
@@ -3,14 +3,11 @@
|
|
|
3
3
|
<%# Results info and per-page selector %>
|
|
4
4
|
<div class="flex-1 flex items-center gap-4">
|
|
5
5
|
<p class="text-sm text-gray-700 dark:text-gray-300">
|
|
6
|
-
|
|
7
|
-
<span class="font-semibold text-gray-900 dark:text-gray-100"><%= pagy.from %></span>
|
|
8
|
-
<%= t('infotable.pagination.to') %>
|
|
9
|
-
<span class="font-semibold text-gray-900 dark:text-gray-100"><%= pagy.to %></span>
|
|
6
|
+
<% bold = ->(value) { tag.span(value, class: "font-semibold text-gray-900 dark:text-gray-100") } %>
|
|
10
7
|
<% if pagy.count %>
|
|
11
|
-
<%= t('infotable.pagination.
|
|
12
|
-
|
|
13
|
-
<%= t('infotable.pagination.
|
|
8
|
+
<%= t('infotable.pagination.summary_html', from: bold.(pagy.from), to: bold.(pagy.to), total: bold.(pagy.count)) %>
|
|
9
|
+
<% else %>
|
|
10
|
+
<%= t('infotable.pagination.summary_no_total_html', from: bold.(pagy.from), to: bold.(pagy.to)) %>
|
|
14
11
|
<% end %>
|
|
15
12
|
</p>
|
|
16
13
|
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
|
|
144
144
|
<%# Side panel drawer - always available for column visibility, filters if available %>
|
|
145
145
|
<%# Drawer - positioned within table bounds, starts completely hidden off-screen %>
|
|
146
|
-
<div <%= tag.attributes(infotable_drawer_attributes(config)) %>>
|
|
146
|
+
<div <%= tag.attributes(infotable_drawer_attributes(config, rail: rail)) %>>
|
|
147
147
|
<div data-infotable--drawer-target="content" class="h-full flex flex-col overflow-hidden">
|
|
148
148
|
<%= render "infotable/tables/side_panel", table_name: table_name, table_class: table_class, config: config %>
|
|
149
149
|
</div>
|
|
@@ -14,8 +14,15 @@
|
|
|
14
14
|
else "text-sm" # default
|
|
15
15
|
end
|
|
16
16
|
%>
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
<%
|
|
18
|
+
clickable = table_class.row_clickable?
|
|
19
|
+
row_dom_id = dom_id(record) if record.respond_to?(:to_key) && record.to_key.present?
|
|
20
|
+
row_data = table_class.row_data_for(record).transform_keys { |key| key.to_s.dasherize }
|
|
21
|
+
row_data["row-link"] = row_url if row_url
|
|
22
|
+
row_data["action"] = "click->infotable--table#handleRowClick" if clickable
|
|
23
|
+
%>
|
|
24
|
+
<tr <%= tag.attributes(id: row_dom_id, data: row_data) %>
|
|
25
|
+
class="infotable-row group relative border-t border-gray-100 dark:border-gray-800 first:border-t-0 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors duration-150 <%= 'cursor-pointer' if clickable %> <%= table_class.row_classes_for(record) %>">
|
|
19
26
|
<% if table_class.checkbox_enabled? %>
|
|
20
27
|
<td class="<%= cell_padding %> w-10">
|
|
21
28
|
<input type="checkbox"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div class="relative">
|
|
3
3
|
<select name="filters[<%= column[:name] %>][value]"
|
|
4
4
|
id="<%= infotable_filter_id(table_name, column, 'value') %>"
|
|
5
|
-
class="block w-full appearance-none rounded-lg border-0 bg-
|
|
5
|
+
class="block w-full appearance-none rounded-lg border-0 bg-gray-50 dark:bg-gray-800/60 hover:bg-gray-100 dark:hover:bg-gray-800 pr-10 pl-3 py-2 text-base sm:text-sm text-neutral-900 dark:text-neutral-100 ring-1 ring-inset ring-gray-200 dark:ring-gray-700 focus:outline-none focus:bg-white dark:focus:bg-gray-900 focus:ring-2 focus:ring-indigo-500 transition-colors cursor-pointer disabled:cursor-not-allowed disabled:bg-gray-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
6
|
<option value=""><%= t('infotable.filters.all') %></option>
|
|
7
7
|
<option value="true"><%= t('infotable.filters.boolean.true') %></option>
|
|
8
8
|
<option value="false"><%= t('infotable.filters.boolean.false') %></option>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
kind = kind.to_sym
|
|
9
9
|
input_type = kind == :date ? "date" : "number"
|
|
10
10
|
ops = kind == :date ? %w[on before after] : %w[eq neq gt gte lt lte]
|
|
11
|
-
input_class = "block w-full rounded-lg border-0 bg-
|
|
11
|
+
input_class = "block w-full rounded-lg border-0 bg-gray-50 dark:bg-gray-800/60 hover:bg-gray-100 dark:hover:bg-gray-800 pr-3 pl-3 py-2 text-base sm:text-sm text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-500 ring-1 ring-inset ring-gray-200 dark:ring-gray-700 focus:outline-none focus:bg-white dark:focus:bg-gray-900 focus:ring-2 focus:ring-indigo-500 transition-colors disabled:cursor-not-allowed disabled:bg-gray-100"
|
|
12
12
|
select_class = "#{input_class} appearance-none pr-10 cursor-pointer 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]"
|
|
13
13
|
step = kind == :number ? "any" : nil
|
|
14
14
|
%>
|
|
@@ -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-
|
|
17
|
+
class="block w-full appearance-none rounded-lg border-0 bg-gray-50 dark:bg-gray-800/60 hover:bg-gray-100 dark:hover:bg-gray-800 pl-3 py-2 pr-8 text-base sm:text-sm text-neutral-900 dark:text-neutral-100 ring-1 ring-inset ring-gray-200 dark:ring-gray-700 focus:outline-none focus:bg-white dark:focus:bg-gray-900 focus:ring-2 focus:ring-indigo-500 transition-colors cursor-pointer disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-75"
|
|
18
18
|
style="min-height: 120px;">
|
|
19
19
|
<% options.each do |option| %>
|
|
20
20
|
<% if option.is_a?(Array) %>
|
|
@@ -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-
|
|
44
|
+
class="block w-full appearance-none rounded-lg border-0 bg-gray-50 dark:bg-gray-800/60 hover:bg-gray-100 dark:hover:bg-gray-800 pr-10 pl-3 py-2 text-base sm:text-sm text-neutral-900 dark:text-neutral-100 ring-1 ring-inset ring-gray-200 dark:ring-gray-700 focus:outline-none focus:bg-white dark:focus:bg-gray-900 focus:ring-2 focus:ring-indigo-500 transition-colors cursor-pointer disabled:cursor-not-allowed disabled:bg-gray-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) %>
|
|
@@ -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-
|
|
9
|
+
class="block w-full appearance-none rounded-lg border-0 bg-gray-50 dark:bg-gray-800/60 hover:bg-gray-100 dark:hover:bg-gray-800 pr-10 pl-3 py-2 text-base sm:text-sm text-neutral-900 dark:text-neutral-100 ring-1 ring-inset ring-gray-200 dark:ring-gray-700 focus:outline-none focus:bg-white dark:focus:bg-gray-900 focus:ring-2 focus:ring-indigo-500 transition-colors cursor-pointer disabled:cursor-not-allowed disabled:bg-gray-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,7 +23,7 @@
|
|
|
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-
|
|
26
|
+
class="block w-full rounded-lg border-0 bg-gray-50 dark:bg-gray-800/60 hover:bg-gray-100 dark:hover:bg-gray-800 pr-8 pl-3 py-2 text-base sm:text-sm text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-500 ring-1 ring-inset ring-gray-200 dark:ring-gray-700 focus:outline-none focus:bg-white dark:focus:bg-gray-900 focus:ring-2 focus:ring-indigo-500 transition-colors disabled:cursor-not-allowed disabled:bg-gray-100">
|
|
27
27
|
<button type="button"
|
|
28
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"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
data-default-visible="<%= column[:visible] %>"
|
|
36
36
|
<%= tag.attributes(data: { default_pinned: column[:pinned] }) if column[:pinned] %>
|
|
37
37
|
style="<%= infotable_cell_width_style(column) %>"
|
|
38
|
-
class="<%= header_padding %> text-left text-xs font-
|
|
38
|
+
class="<%= header_padding %> text-left whitespace-nowrap text-xs font-medium tracking-wide text-gray-500 dark:text-gray-400 select-none bg-gray-50 dark:bg-gray-800 sticky top-0 z-20">
|
|
39
39
|
<% if column[:sortable] %>
|
|
40
40
|
<% sort_index = current_sorts.index { |name, _| name == column[:name].to_s } %>
|
|
41
41
|
<% sort_dir = sort_index ? current_sorts[sort_index].last : nil %>
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
</table>
|
|
91
91
|
|
|
92
92
|
<%# Next page frame for infinite scroll - rendered after table %>
|
|
93
|
-
<% if @infinite_scroll_enabled
|
|
93
|
+
<% if @infinite_scroll_enabled %>
|
|
94
94
|
<%= render partial: "infotable/tables/next_page_frame" %>
|
|
95
95
|
<% end %>
|
|
96
96
|
<% else %>
|
data/config/importmap.rb
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
pin "infotable", to: "infotable/index.js"
|
|
3
3
|
pin_all_from Infotable::Engine.root.join("app/assets/javascripts/infotable/controllers"), under: "controllers/infotable", to: "infotable/controllers"
|
|
4
4
|
|
|
5
|
-
# External dependencies
|
|
6
|
-
pin "sortablejs", to: "
|
|
5
|
+
# External dependencies, served with the gem (no third-party CDN at runtime)
|
|
6
|
+
pin "sortablejs", to: "infotable/vendor/sortable.esm.js"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
de:
|
|
2
|
+
infotable:
|
|
3
|
+
table:
|
|
4
|
+
buttons:
|
|
5
|
+
export_csv: "CSV exportieren"
|
|
6
|
+
filters: "Filter"
|
|
7
|
+
exporting: "Exportiere..."
|
|
8
|
+
refresh: "Aktualisieren"
|
|
9
|
+
columns: "Spalten"
|
|
10
|
+
sort_hint: "Klicken zum Sortieren, Shift+Klick fügt eine Spalte zur Sortierung hinzu"
|
|
11
|
+
loading:
|
|
12
|
+
initial: "Daten werden geladen..."
|
|
13
|
+
more_items: "Weitere Einträge werden geladen..."
|
|
14
|
+
all_loaded: "Alle Einträge geladen"
|
|
15
|
+
resize_column: "Spaltenbreite ändern"
|
|
16
|
+
empty_state:
|
|
17
|
+
title: "Keine Daten gefunden"
|
|
18
|
+
description: "Fügen Sie Datensätze hinzu, um sie hier zu sehen."
|
|
19
|
+
filtered_title: "Keine Zeilen entsprechen Ihren Filtern"
|
|
20
|
+
filtered_description: "Entfernen Sie einen Filter oder suchen Sie nach etwas anderem."
|
|
21
|
+
|
|
22
|
+
drawer:
|
|
23
|
+
label: "Filter und Spalten"
|
|
24
|
+
panel_title:
|
|
25
|
+
customize_columns: "Spalten anpassen"
|
|
26
|
+
filters: "Filter"
|
|
27
|
+
columns: "Spalten"
|
|
28
|
+
tabs:
|
|
29
|
+
filters: "Filter"
|
|
30
|
+
columns: "Spalten"
|
|
31
|
+
|
|
32
|
+
columns:
|
|
33
|
+
instructions: "Zum Umsortieren ziehen, Spalten anheften und ein-/ausblenden:"
|
|
34
|
+
pin:
|
|
35
|
+
left: "Links anheften"
|
|
36
|
+
right: "Rechts anheften"
|
|
37
|
+
actions:
|
|
38
|
+
select_all: "Alle auswählen"
|
|
39
|
+
deselect_all: "Alle abwählen"
|
|
40
|
+
reset_to_defaults: "🔄 Standard wiederherstellen"
|
|
41
|
+
|
|
42
|
+
filters:
|
|
43
|
+
actions:
|
|
44
|
+
clear_all: "Alle löschen"
|
|
45
|
+
apply_filters: "Filter anwenden"
|
|
46
|
+
clear_filter: "Diesen Filter löschen"
|
|
47
|
+
active:
|
|
48
|
+
search: "Suche: „%{query}“"
|
|
49
|
+
remove: "Filter %{filter} entfernen"
|
|
50
|
+
clear_all: "Alle Filter löschen"
|
|
51
|
+
from: "Von"
|
|
52
|
+
to: "Bis"
|
|
53
|
+
min: "Min"
|
|
54
|
+
max: "Max"
|
|
55
|
+
enter_value: "Wert eingeben..."
|
|
56
|
+
all: "Alle"
|
|
57
|
+
multi_select_hint: "Strg/Cmd gedrückt halten, um mehrere auszuwählen"
|
|
58
|
+
options_truncated: "Die ersten %{shown} von %{total} Optionen werden angezeigt"
|
|
59
|
+
boolean:
|
|
60
|
+
"true": "Wahr"
|
|
61
|
+
"false": "Falsch"
|
|
62
|
+
empty: "Leer"
|
|
63
|
+
not_empty: "Nicht leer"
|
|
64
|
+
text_ops:
|
|
65
|
+
contains: "Enthält"
|
|
66
|
+
exact: "Exakte Übereinstimmung"
|
|
67
|
+
not_contain: "Enthält nicht"
|
|
68
|
+
starts_with: "Beginnt mit"
|
|
69
|
+
ends_with: "Endet mit"
|
|
70
|
+
is_empty: "Ist leer"
|
|
71
|
+
is_not_empty: "Ist nicht leer"
|
|
72
|
+
range_ops:
|
|
73
|
+
between: "Zwischen"
|
|
74
|
+
number_ops:
|
|
75
|
+
eq: "Gleich"
|
|
76
|
+
neq: "Ungleich"
|
|
77
|
+
gt: "Größer als"
|
|
78
|
+
gte: "Größer oder gleich"
|
|
79
|
+
lt: "Kleiner als"
|
|
80
|
+
lte: "Kleiner oder gleich"
|
|
81
|
+
date_ops:
|
|
82
|
+
"on": "Am"
|
|
83
|
+
before: "Vor"
|
|
84
|
+
after: "Nach"
|
|
85
|
+
|
|
86
|
+
formatters:
|
|
87
|
+
ago: "vor %{time}"
|
|
88
|
+
date:
|
|
89
|
+
short: "%d. %b %Y"
|
|
90
|
+
long: "%d. %B %Y"
|
|
91
|
+
time: "%H:%M"
|
|
92
|
+
datetime: "%d. %b %Y %H:%M"
|
|
93
|
+
action: "Aktion"
|
|
94
|
+
boolean:
|
|
95
|
+
"true": "Ja"
|
|
96
|
+
"false": "Nein"
|
|
97
|
+
duration:
|
|
98
|
+
short:
|
|
99
|
+
d: "%{count} T"
|
|
100
|
+
h: "%{count} Std."
|
|
101
|
+
m: "%{count} Min."
|
|
102
|
+
s: "%{count} Sek."
|
|
103
|
+
long:
|
|
104
|
+
d: { one: "%{count} Tag", few: "%{count} Tage", many: "%{count} Tage", other: "%{count} Tage" }
|
|
105
|
+
h: { one: "%{count} Stunde", few: "%{count} Stunden", many: "%{count} Stunden", other: "%{count} Stunden" }
|
|
106
|
+
m: { one: "%{count} Minute", few: "%{count} Minuten", many: "%{count} Minuten", other: "%{count} Minuten" }
|
|
107
|
+
s: { one: "%{count} Sekunde", few: "%{count} Sekunden", many: "%{count} Sekunden", other: "%{count} Sekunden" }
|
|
108
|
+
|
|
109
|
+
batch:
|
|
110
|
+
selected: "%{count} ausgewählt"
|
|
111
|
+
clear: "Auswahl aufheben"
|
|
112
|
+
select_all: "Alle Zeilen auswählen"
|
|
113
|
+
select_row: "Zeile auswählen"
|
|
114
|
+
select_all_matching: "Alle %{count} Treffer auswählen"
|
|
115
|
+
truncated: "Nur die ersten %{max} Zeilen sind ausgewählt"
|
|
116
|
+
|
|
117
|
+
search:
|
|
118
|
+
placeholder: "Suchen..."
|
|
119
|
+
label: "Tabelle durchsuchen"
|
|
120
|
+
|
|
121
|
+
pagination:
|
|
122
|
+
summary_html: "Zeige %{from} bis %{to} von %{total} Ergebnissen"
|
|
123
|
+
summary_no_total_html: "Zeige %{from} bis %{to}"
|
|
124
|
+
rows_per_page: "Zeilen pro Seite:"
|
|
125
|
+
previous: "Zurück"
|
|
126
|
+
next: "Weiter"
|