dbviewer 0.3.15 → 0.3.16
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 +34 -2
- data/app/controllers/concerns/dbviewer/database_operations.rb +23 -21
- data/app/controllers/concerns/dbviewer/pagination_concern.rb +8 -0
- data/app/controllers/dbviewer/tables_controller.rb +100 -21
- data/app/helpers/dbviewer/application_helper.rb +73 -0
- data/app/views/dbviewer/shared/_sidebar.html.erb +146 -1
- data/app/views/dbviewer/tables/index.html.erb +7 -1
- data/app/views/dbviewer/tables/show.html.erb +361 -28
- data/lib/dbviewer/database_manager.rb +31 -115
- data/lib/dbviewer/query_analyzer.rb +130 -0
- data/lib/dbviewer/table_query_operations.rb +621 -0
- data/lib/dbviewer/table_query_params.rb +39 -0
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +1 -0
- metadata +4 -9
- data/app/services/dbviewer/file_storage.rb +0 -0
- data/app/services/dbviewer/in_memory_storage.rb +0 -0
- data/app/services/dbviewer/query_analyzer.rb +0 -0
- data/app/services/dbviewer/query_collection.rb +0 -0
- data/app/services/dbviewer/query_logger.rb +0 -0
- data/app/services/dbviewer/query_parser.rb +0 -82
- data/app/services/dbviewer/query_storage.rb +0 -0
@@ -7,6 +7,92 @@
|
|
7
7
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
8
8
|
<!-- SVG-Pan-Zoom for interactive diagram navigation -->
|
9
9
|
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
|
10
|
+
<style>
|
11
|
+
/* Column sorting styles */
|
12
|
+
.sortable-column {
|
13
|
+
cursor: pointer;
|
14
|
+
position: relative;
|
15
|
+
transition: background-color 0.2s ease;
|
16
|
+
background-color: inherit;
|
17
|
+
}
|
18
|
+
|
19
|
+
.sortable-column:hover {
|
20
|
+
background-color: #f5f5f5;
|
21
|
+
}
|
22
|
+
|
23
|
+
.sortable-column.sorted {
|
24
|
+
background-color: #f0f0f0;
|
25
|
+
}
|
26
|
+
|
27
|
+
.sortable-column .column-sort-link {
|
28
|
+
display: flex;
|
29
|
+
align-items: center;
|
30
|
+
justify-content: space-between;
|
31
|
+
width: 100%;
|
32
|
+
height: 100%;
|
33
|
+
padding: 4px 0;
|
34
|
+
}
|
35
|
+
|
36
|
+
.sortable-column .column-name {
|
37
|
+
flex: 1;
|
38
|
+
overflow: hidden;
|
39
|
+
text-overflow: ellipsis;
|
40
|
+
white-space: nowrap;
|
41
|
+
}
|
42
|
+
|
43
|
+
.sortable-column .sort-icon-container {
|
44
|
+
flex: 0 0 auto;
|
45
|
+
width: 20px;
|
46
|
+
text-align: center;
|
47
|
+
margin-left: 4px;
|
48
|
+
}
|
49
|
+
|
50
|
+
.sortable-column .sort-icon {
|
51
|
+
font-size: 0.8em;
|
52
|
+
opacity: 0.7;
|
53
|
+
transition: opacity 0.2s ease, color 0.2s ease;
|
54
|
+
}
|
55
|
+
|
56
|
+
.sortable-column:hover .sort-icon.invisible {
|
57
|
+
visibility: visible !important;
|
58
|
+
opacity: 0.3;
|
59
|
+
}
|
60
|
+
|
61
|
+
/* Fix scrolling issues with sticky header */
|
62
|
+
.dbviewer-table-header {
|
63
|
+
position: sticky !important;
|
64
|
+
top: 0;
|
65
|
+
z-index: 10;
|
66
|
+
background-color: var(--bs-table-striped-bg, #f2f2f2) !important;
|
67
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
|
68
|
+
}
|
69
|
+
|
70
|
+
[data-bs-theme="dark"] .dbviewer-table-header {
|
71
|
+
background-color: var(--bs-dark-bg-subtle, #343a40) !important;
|
72
|
+
}
|
73
|
+
|
74
|
+
/* Improve mobile display for sort headers */
|
75
|
+
@media (max-width: 767.98px) {
|
76
|
+
.sortable-column .column-sort-link {
|
77
|
+
flex-direction: row;
|
78
|
+
align-items: center;
|
79
|
+
}
|
80
|
+
|
81
|
+
.sortable-column .sort-icon-container {
|
82
|
+
width: 16px;
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
/* Dark mode compatibility */
|
87
|
+
[data-bs-theme="dark"] .sortable-column:hover {
|
88
|
+
background-color: rgba(255, 255, 255, 0.05);
|
89
|
+
}
|
90
|
+
|
91
|
+
[data-bs-theme="dark"] .sortable-column.sorted {
|
92
|
+
background-color: rgba(255, 255, 255, 0.1);
|
93
|
+
}
|
94
|
+
</style>
|
95
|
+
|
10
96
|
<script>
|
11
97
|
// Initialize mermaid when document is ready
|
12
98
|
document.addEventListener('DOMContentLoaded', function() {
|
@@ -37,10 +123,11 @@
|
|
37
123
|
|
38
124
|
<% content_for :sidebar do %>
|
39
125
|
<%= render 'dbviewer/shared/sidebar' %>
|
40
|
-
<% end %>
|
41
|
-
|
126
|
+
<% end %>
|
42
127
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
43
|
-
<
|
128
|
+
<div>
|
129
|
+
<h1>Table: <%= @table_name %></h1>
|
130
|
+
</div>
|
44
131
|
<div class="d-flex gap-2">
|
45
132
|
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#miniErdModal">
|
46
133
|
<i class="bi bi-diagram-3 me-1"></i> View Relationships
|
@@ -119,16 +206,50 @@
|
|
119
206
|
<!-- Records Section -->
|
120
207
|
<div class="dbviewer-card card mb-4">
|
121
208
|
<div class="card-header d-flex justify-content-between align-items-center">
|
122
|
-
<h5 class="mb-0"
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
209
|
+
<h5 class="mb-0">
|
210
|
+
<%
|
211
|
+
# Build the URL query parameters including creation filters
|
212
|
+
url_params = "per_page=' + this.value + '&page=1&order_by=#{@order_by}&order_direction=#{@order_direction}"
|
213
|
+
|
214
|
+
# Add column filters
|
215
|
+
if @column_filters.reject { |_, v| v.blank? }.any?
|
216
|
+
url_params += "&" + @column_filters.reject { |_, v| v.blank? }.map { |k, v| "column_filters[#{k}]=#{CGI.escape(v.to_s)}" }.join("&")
|
217
|
+
end
|
218
|
+
|
219
|
+
# Add creation filters
|
220
|
+
url_params += "&creation_filter_start=#{@creation_filter_start}" if @creation_filter_start.present?
|
221
|
+
url_params += "&creation_filter_end=#{@creation_filter_end}" if @creation_filter_end.present?
|
222
|
+
%>
|
223
|
+
<select id="per-page-select" class="form-select form-select-sm" onchange="window.location.href='<%= table_path(@table_name) %>?<%= url_params %>'">
|
224
|
+
<% Dbviewer::TablesController.per_page_options.each do |option| %>
|
225
|
+
<option value="<%= option %>" <%= 'selected' if @per_page == option %>><%= option %></option>
|
226
|
+
<% end %>
|
227
|
+
</select>
|
228
|
+
</h5>
|
229
|
+
<div class="d-flex align-items-center table-actions">
|
230
|
+
<% if @order_by.present? %>
|
231
|
+
<span class="badge bg-primary me-2" title="Sort order">
|
232
|
+
<i class="bi bi-sort-<%= @order_direction == "ASC" ? "up" : "down" %> me-1"></i>
|
233
|
+
<%= @order_by %> (<%= @order_direction == "ASC" ? "ascending" : "descending" %>)
|
234
|
+
</span>
|
235
|
+
<% end %>
|
236
|
+
|
237
|
+
<%
|
238
|
+
clear_sort_params = {
|
239
|
+
page: 1,
|
240
|
+
per_page: @per_page,
|
241
|
+
column_filters: @column_filters
|
242
|
+
}
|
243
|
+
|
244
|
+
# Add creation filters if they exist
|
245
|
+
clear_sort_params[:creation_filter_start] = @creation_filter_start if @creation_filter_start.present?
|
246
|
+
clear_sort_params[:creation_filter_end] = @creation_filter_end if @creation_filter_end.present?
|
247
|
+
%>
|
248
|
+
<%= link_to "×", table_path(@table_name, clear_sort_params),
|
249
|
+
class: "text-light ms-2",
|
250
|
+
style: "text-decoration: none;",
|
251
|
+
title: "Clear sorting" %>
|
252
|
+
</span>
|
132
253
|
<span class="badge bg-secondary">Total: <%= @total_count %> records</span>
|
133
254
|
<% active_filters = @column_filters.reject { |_, v| v.blank? }.size %>
|
134
255
|
<% if active_filters > 0 %>
|
@@ -150,8 +271,9 @@
|
|
150
271
|
<tr>
|
151
272
|
<% if @records && @records.columns %>
|
152
273
|
<% @records.columns.each do |column_name| %>
|
153
|
-
|
154
|
-
|
274
|
+
<% is_sorted = @order_by == column_name %>
|
275
|
+
<th class="px-3 py-2 sortable-column <%= 'sorted' if is_sorted %>">
|
276
|
+
<%= sortable_column_header(column_name, @order_by, @order_direction, @table_name, @current_page, @per_page, @column_filters) %>
|
155
277
|
</th>
|
156
278
|
<% end %>
|
157
279
|
<% else %>
|
@@ -162,11 +284,87 @@
|
|
162
284
|
<% if @records && @records.columns %>
|
163
285
|
<% @records.columns.each do |column_name| %>
|
164
286
|
<th class="p-0">
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
287
|
+
<%
|
288
|
+
# Find column info to detect the type
|
289
|
+
column_info = @columns.find { |c| c[:name].to_s == column_name.to_s } if @columns
|
290
|
+
column_type = column_info ? column_info[:type].to_s.downcase : nil
|
291
|
+
%>
|
292
|
+
|
293
|
+
<div class="filter-input-group">
|
294
|
+
<%
|
295
|
+
# Define the default operator and available operators based on column type
|
296
|
+
default_operator = if column_type && column_type =~ /char|text|string|uuid|enum/i
|
297
|
+
"contains"
|
298
|
+
else
|
299
|
+
"eq"
|
300
|
+
end
|
301
|
+
|
302
|
+
# Get previously selected operator or default
|
303
|
+
selected_operator = @column_filters["#{column_name}_operator"]
|
304
|
+
selected_operator = default_operator if selected_operator.nil? || selected_operator == "default"
|
305
|
+
|
306
|
+
# Define operator options based on column type
|
307
|
+
if column_type && (column_type =~ /datetime/ || column_type =~ /^date$/ || column_type =~ /^time$/)
|
308
|
+
# Date/Time operators
|
309
|
+
operator_options = [
|
310
|
+
["=", "eq"],
|
311
|
+
["≠", "neq"],
|
312
|
+
["<", "lt"],
|
313
|
+
[">", "gt"],
|
314
|
+
["≤", "lte"],
|
315
|
+
["≥", "gte"]
|
316
|
+
]
|
317
|
+
elsif column_type && column_type =~ /int|float|decimal|double|number|numeric|real|money|bigint|smallint|tinyint|mediumint|bit/i
|
318
|
+
# Numeric operators
|
319
|
+
operator_options = [
|
320
|
+
["=", "eq"],
|
321
|
+
["≠", "neq"],
|
322
|
+
["<", "lt"],
|
323
|
+
[">", "gt"],
|
324
|
+
["≤", "lte"],
|
325
|
+
["≥", "gte"]
|
326
|
+
]
|
327
|
+
else
|
328
|
+
# Text operators
|
329
|
+
operator_options = [
|
330
|
+
["contains", "contains"],
|
331
|
+
["not contains", "not_contains"],
|
332
|
+
["=", "eq"],
|
333
|
+
["≠", "neq"],
|
334
|
+
["starts with", "starts_with"],
|
335
|
+
["ends with", "ends_with"]
|
336
|
+
]
|
337
|
+
end
|
338
|
+
%>
|
339
|
+
|
340
|
+
<%= form.select "column_filters[#{column_name}_operator]",
|
341
|
+
options_for_select(operator_options, selected_operator),
|
342
|
+
{ include_blank: false },
|
343
|
+
{ class: "form-select form-select-sm operator-select" } %>
|
344
|
+
|
345
|
+
<% if column_type && column_type =~ /datetime/ %>
|
346
|
+
<%= form.datetime_local_field "column_filters[#{column_name}]",
|
347
|
+
value: @column_filters[column_name],
|
348
|
+
class: "form-control form-control-sm column-filter rounded-0",
|
349
|
+
data: { column: column_name } %>
|
350
|
+
<% elsif column_type && column_type =~ /^date$/ %>
|
351
|
+
<%= form.date_field "column_filters[#{column_name}]",
|
352
|
+
value: @column_filters[column_name],
|
353
|
+
class: "form-control form-control-sm column-filter rounded-0",
|
354
|
+
data: { column: column_name } %>
|
355
|
+
<% elsif column_type && column_type =~ /^time$/ %>
|
356
|
+
<%= form.time_field "column_filters[#{column_name}]",
|
357
|
+
value: @column_filters[column_name],
|
358
|
+
class: "form-control form-control-sm column-filter rounded-0",
|
359
|
+
data: { column: column_name } %>
|
360
|
+
<% else %>
|
361
|
+
<%= form.text_field "column_filters[#{column_name}]",
|
362
|
+
value: @column_filters[column_name],
|
363
|
+
placeholder: "",
|
364
|
+
class: "form-control form-control-sm column-filter rounded-0",
|
365
|
+
data: { column: column_name } %>
|
366
|
+
<% end %>
|
367
|
+
</div>
|
170
368
|
</th>
|
171
369
|
<% end %>
|
172
370
|
<% else %>
|
@@ -194,10 +392,17 @@
|
|
194
392
|
nil
|
195
393
|
%>
|
196
394
|
<% if foreign_key && !cell.nil? %>
|
395
|
+
<%
|
396
|
+
# Build params for foreign key links with creation filters
|
397
|
+
fk_params = { column_filters: { foreign_key[:primary_key] => cell } }
|
398
|
+
|
399
|
+
# Add creation filters if they exist
|
400
|
+
fk_params[:creation_filter_start] = @creation_filter_start if @creation_filter_start.present?
|
401
|
+
fk_params[:creation_filter_end] = @creation_filter_end if @creation_filter_end.present?
|
402
|
+
%>
|
197
403
|
<td title="<%= cell_value %> (Click to view referenced record)">
|
198
404
|
<%= link_to cell_value,
|
199
|
-
table_path(foreign_key[:to_table],
|
200
|
-
column_filters: { foreign_key[:primary_key] => cell }),
|
405
|
+
table_path(foreign_key[:to_table], fk_params),
|
201
406
|
class: "text-decoration-none foreign-key-link" %>
|
202
407
|
<i class="bi bi-link-45deg text-muted small"></i>
|
203
408
|
</td>
|
@@ -214,10 +419,23 @@
|
|
214
419
|
</div>
|
215
420
|
|
216
421
|
<% if @total_pages && @total_pages > 1 %>
|
422
|
+
<%
|
423
|
+
# Prepare common URL params including creation filters
|
424
|
+
common_params = {
|
425
|
+
order_by: @order_by,
|
426
|
+
order_direction: @order_direction,
|
427
|
+
per_page: @per_page,
|
428
|
+
column_filters: @column_filters
|
429
|
+
}
|
430
|
+
|
431
|
+
# Add creation filters if they exist
|
432
|
+
common_params[:creation_filter_start] = @creation_filter_start if @creation_filter_start.present?
|
433
|
+
common_params[:creation_filter_end] = @creation_filter_end if @creation_filter_end.present?
|
434
|
+
%>
|
217
435
|
<nav aria-label="Page navigation">
|
218
436
|
<ul class="pagination justify-content-center">
|
219
437
|
<li class="page-item <%= 'disabled' if @current_page == 1 %>">
|
220
|
-
<%= link_to '«', table_path(@table_name, page: [@current_page - 1, 1].max
|
438
|
+
<%= link_to '«', table_path(@table_name, common_params.merge(page: [@current_page - 1, 1].max)), class: 'page-link' %>
|
221
439
|
</li>
|
222
440
|
|
223
441
|
<% start_page = [1, @current_page - 2].max %>
|
@@ -226,12 +444,12 @@
|
|
226
444
|
|
227
445
|
<% (start_page..end_page).each do |page_num| %>
|
228
446
|
<li class="page-item <%= 'active' if page_num == @current_page %>">
|
229
|
-
<%= link_to page_num, table_path(@table_name, page: page_num
|
447
|
+
<%= link_to page_num, table_path(@table_name, common_params.merge(page: page_num)), class: 'page-link' %>
|
230
448
|
</li>
|
231
449
|
<% end %>
|
232
450
|
|
233
451
|
<li class="page-item <%= 'disabled' if @current_page == @total_pages %>">
|
234
|
-
<%= link_to '»', table_path(@table_name, page: [@current_page + 1, @total_pages].min
|
452
|
+
<%= link_to '»', table_path(@table_name, common_params.merge(page: [@current_page + 1, @total_pages].min)), class: 'page-link' %>
|
235
453
|
</li>
|
236
454
|
</ul>
|
237
455
|
</nav>
|
@@ -249,10 +467,23 @@
|
|
249
467
|
<div class="card-header d-flex justify-content-between align-items-center">
|
250
468
|
<h5 class="mb-0"><i class="bi bi-graph-up me-2"></i>Record Creation Timeline</h5>
|
251
469
|
<div>
|
470
|
+
<%
|
471
|
+
# Build common params for time grouping links
|
472
|
+
time_params = {
|
473
|
+
page: @current_page,
|
474
|
+
order_by: @order_by,
|
475
|
+
order_direction: @order_direction,
|
476
|
+
per_page: @per_page
|
477
|
+
}
|
478
|
+
|
479
|
+
# Add creation filters if they exist
|
480
|
+
time_params[:creation_filter_start] = @creation_filter_start if @creation_filter_start.present?
|
481
|
+
time_params[:creation_filter_end] = @creation_filter_end if @creation_filter_end.present?
|
482
|
+
%>
|
252
483
|
<div class="btn-group btn-group-sm" role="group" aria-label="Time grouping">
|
253
|
-
<%= link_to "Hourly", table_path(@table_name, time_group: "hourly"
|
254
|
-
<%= link_to "Daily", table_path(@table_name, time_group: "daily"
|
255
|
-
<%= link_to "Weekly", table_path(@table_name, time_group: "weekly"
|
484
|
+
<%= link_to "Hourly", table_path(@table_name, time_params.merge(time_group: "hourly")), class: "btn btn-outline-primary #{@time_grouping == 'hourly' ? 'active' : ''}" %>
|
485
|
+
<%= link_to "Daily", table_path(@table_name, time_params.merge(time_group: "daily")), class: "btn btn-outline-primary #{@time_grouping == 'daily' ? 'active' : ''}" %>
|
486
|
+
<%= link_to "Weekly", table_path(@table_name, time_params.merge(time_group: "weekly")), class: "btn btn-outline-primary #{@time_grouping == 'weekly' ? 'active' : ''}" %>
|
256
487
|
</div>
|
257
488
|
</div>
|
258
489
|
</div>
|
@@ -301,6 +532,7 @@
|
|
301
532
|
document.addEventListener('DOMContentLoaded', function() {
|
302
533
|
// Column filter functionality
|
303
534
|
const columnFilters = document.querySelectorAll('.column-filter');
|
535
|
+
const operatorSelects = document.querySelectorAll('.operator-select');
|
304
536
|
const filterForm = document.getElementById('column-filters-form');
|
305
537
|
|
306
538
|
// Add debounce function to reduce form submissions
|
@@ -323,7 +555,18 @@
|
|
323
555
|
|
324
556
|
// Add event listeners to all filter inputs
|
325
557
|
columnFilters.forEach(function(filter) {
|
558
|
+
// For text fields use input event
|
326
559
|
filter.addEventListener('input', submitForm);
|
560
|
+
|
561
|
+
// For date/time fields also use change event since they have calendar/time pickers
|
562
|
+
if (filter.type === 'date' || filter.type === 'datetime-local' || filter.type === 'time') {
|
563
|
+
filter.addEventListener('change', submitForm);
|
564
|
+
}
|
565
|
+
});
|
566
|
+
|
567
|
+
// Add event listeners to operator selects
|
568
|
+
operatorSelects.forEach(function(select) {
|
569
|
+
select.addEventListener('change', submitForm);
|
327
570
|
});
|
328
571
|
|
329
572
|
// Add clear button functionality if there are any filters applied
|
@@ -343,7 +586,17 @@
|
|
343
586
|
paginationContainer.insertAdjacentHTML('afterend', clearButton.outerHTML);
|
344
587
|
|
345
588
|
document.getElementById('clear-filters').addEventListener('click', function() {
|
589
|
+
// Reset all input values
|
346
590
|
columnFilters.forEach(filter => filter.value = '');
|
591
|
+
|
592
|
+
// Reset operator selects to their default values
|
593
|
+
operatorSelects.forEach(select => {
|
594
|
+
// Find the first option of the select (usually the default)
|
595
|
+
if (select.options.length > 0) {
|
596
|
+
select.selectedIndex = 0;
|
597
|
+
}
|
598
|
+
});
|
599
|
+
|
347
600
|
submitForm();
|
348
601
|
});
|
349
602
|
}
|
@@ -785,6 +1038,46 @@
|
|
785
1038
|
font-size: 0.85rem;
|
786
1039
|
}
|
787
1040
|
|
1041
|
+
/* Filter input group styling */
|
1042
|
+
.filter-input-group {
|
1043
|
+
display: flex;
|
1044
|
+
flex-direction: column;
|
1045
|
+
width: 100%;
|
1046
|
+
}
|
1047
|
+
|
1048
|
+
.filter-input-group .operator-select {
|
1049
|
+
font-size: 0.7rem;
|
1050
|
+
padding: 0.1rem 0.5rem;
|
1051
|
+
border-bottom: none;
|
1052
|
+
border-radius: 0.2rem 0.2rem 0 0;
|
1053
|
+
background-color: rgba(0,0,0,0.03);
|
1054
|
+
font-weight: normal;
|
1055
|
+
min-height: 22px; /* Ensure consistent height */
|
1056
|
+
}
|
1057
|
+
|
1058
|
+
.filter-input-group .operator-select option {
|
1059
|
+
font-size: 0.8rem;
|
1060
|
+
padding: 2px;
|
1061
|
+
}
|
1062
|
+
|
1063
|
+
[data-bs-theme="dark"] .filter-input-group .operator-select {
|
1064
|
+
background-color: rgba(255,255,255,0.05);
|
1065
|
+
color: rgba(255,255,255,0.9);
|
1066
|
+
border-color: rgba(255,255,255,0.15);
|
1067
|
+
}
|
1068
|
+
|
1069
|
+
.filter-input-group .column-filter {
|
1070
|
+
border-radius: 0 0 0.2rem 0.2rem;
|
1071
|
+
border-top: none;
|
1072
|
+
}
|
1073
|
+
|
1074
|
+
/* Ensure consistent sizing for date/time inputs */
|
1075
|
+
input[type="date"].column-filter,
|
1076
|
+
input[type="datetime-local"].column-filter,
|
1077
|
+
input[type="time"].column-filter {
|
1078
|
+
padding: 0.2rem 0.5rem;
|
1079
|
+
}
|
1080
|
+
|
788
1081
|
[data-bs-theme="dark"] .column-filters td {
|
789
1082
|
background-color: rgba(255,255,255,0.05);
|
790
1083
|
}
|
@@ -795,6 +1088,13 @@
|
|
795
1088
|
border-color: rgba(255,255,255,0.15);
|
796
1089
|
}
|
797
1090
|
|
1091
|
+
/* Special styling for date/time inputs in dark mode */
|
1092
|
+
[data-bs-theme="dark"] input[type="datetime-local"].column-filter::-webkit-calendar-picker-indicator,
|
1093
|
+
[data-bs-theme="dark"] input[type="date"].column-filter::-webkit-calendar-picker-indicator,
|
1094
|
+
[data-bs-theme="dark"] input[type="time"].column-filter::-webkit-calendar-picker-indicator {
|
1095
|
+
filter: invert(0.8);
|
1096
|
+
}
|
1097
|
+
|
798
1098
|
/* Mini ERD modal styling */
|
799
1099
|
#miniErdModal .modal-dialog {
|
800
1100
|
max-width: 90%;
|
@@ -916,6 +1216,39 @@
|
|
916
1216
|
}
|
917
1217
|
}
|
918
1218
|
});
|
1219
|
+
|
1220
|
+
// Column sorting enhancement
|
1221
|
+
const sortableColumns = document.querySelectorAll('.sortable-column');
|
1222
|
+
sortableColumns.forEach(column => {
|
1223
|
+
const link = column.querySelector('.column-sort-link');
|
1224
|
+
|
1225
|
+
// Mouse over effects
|
1226
|
+
column.addEventListener('mouseenter', () => {
|
1227
|
+
const sortIcon = column.querySelector('.sort-icon');
|
1228
|
+
if (sortIcon && sortIcon.classList.contains('invisible')) {
|
1229
|
+
sortIcon.style.visibility = 'visible';
|
1230
|
+
sortIcon.style.opacity = '0.3';
|
1231
|
+
}
|
1232
|
+
});
|
1233
|
+
|
1234
|
+
column.addEventListener('mouseleave', () => {
|
1235
|
+
const sortIcon = column.querySelector('.sort-icon');
|
1236
|
+
if (sortIcon && sortIcon.classList.contains('invisible')) {
|
1237
|
+
sortIcon.style.visibility = 'hidden';
|
1238
|
+
sortIcon.style.opacity = '0';
|
1239
|
+
}
|
1240
|
+
});
|
1241
|
+
|
1242
|
+
// Keyboard accessibility
|
1243
|
+
if (link) {
|
1244
|
+
link.addEventListener('keydown', (e) => {
|
1245
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
1246
|
+
e.preventDefault();
|
1247
|
+
link.click();
|
1248
|
+
}
|
1249
|
+
});
|
1250
|
+
}
|
1251
|
+
});
|
919
1252
|
});
|
920
1253
|
</script>
|
921
1254
|
<% end %>
|