dbviewer 0.6.7 → 0.6.8
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/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +553 -0
- data/app/assets/javascripts/dbviewer/home.js +287 -0
- data/app/assets/javascripts/dbviewer/layout.js +194 -0
- data/app/assets/javascripts/dbviewer/query.js +277 -0
- data/app/assets/javascripts/dbviewer/table.js +1563 -0
- data/app/assets/stylesheets/dbviewer/application.css +1460 -21
- data/app/assets/stylesheets/dbviewer/entity_relationship_diagram.css +181 -0
- data/app/assets/stylesheets/dbviewer/home.css +229 -0
- data/app/assets/stylesheets/dbviewer/logs.css +64 -0
- data/app/assets/stylesheets/dbviewer/query.css +171 -0
- data/app/assets/stylesheets/dbviewer/table.css +1144 -0
- data/app/views/dbviewer/connections/index.html.erb +0 -30
- data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +14 -713
- data/app/views/dbviewer/home/index.html.erb +9 -499
- data/app/views/dbviewer/logs/index.html.erb +5 -220
- data/app/views/dbviewer/tables/index.html.erb +0 -65
- data/app/views/dbviewer/tables/query.html.erb +129 -565
- data/app/views/dbviewer/tables/show.html.erb +4 -2429
- data/app/views/layouts/dbviewer/application.html.erb +13 -1544
- data/lib/dbviewer/version.rb +1 -1
- metadata +12 -7
- data/app/assets/javascripts/dbviewer/connections.js +0 -70
- data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
- data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
- data/app/views/dbviewer/connections/new.html.erb +0 -79
- data/app/views/dbviewer/tables/mini_erd.html.erb +0 -517
@@ -3,331 +3,10 @@
|
|
3
3
|
<% end %>
|
4
4
|
|
5
5
|
<% content_for :head do %>
|
6
|
-
<!-- Mermaid.js library for ERD diagrams -->
|
7
6
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
8
|
-
<!-- SVG-Pan-Zoom for interactive diagram navigation -->
|
9
7
|
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
|
10
|
-
|
11
|
-
|
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
|
-
/* Ensure proper layering for sticky elements */
|
75
|
-
.dbviewer-table-header th {
|
76
|
-
position: sticky;
|
77
|
-
top: 0;
|
78
|
-
z-index: 20;
|
79
|
-
}
|
80
|
-
|
81
|
-
/* Increase z-index for the intersection point of sticky header and sticky column */
|
82
|
-
.dbviewer-table-header th.action-column {
|
83
|
-
z-index: 40 !important;
|
84
|
-
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
85
|
-
}
|
86
|
-
|
87
|
-
/* Ensure thead has higher z-index than tbody */
|
88
|
-
thead tr th.action-column {
|
89
|
-
z-index: 40 !important;
|
90
|
-
}
|
91
|
-
|
92
|
-
tbody tr td.action-column {
|
93
|
-
z-index: 30 !important;
|
94
|
-
}
|
95
|
-
|
96
|
-
/* Improve mobile display for sort headers */
|
97
|
-
@media (max-width: 767.98px) {
|
98
|
-
.sortable-column .column-sort-link {
|
99
|
-
flex-direction: row;
|
100
|
-
align-items: center;
|
101
|
-
}
|
102
|
-
|
103
|
-
.sortable-column .sort-icon-container {
|
104
|
-
width: 16px;
|
105
|
-
}
|
106
|
-
}
|
107
|
-
|
108
|
-
/* Dark mode compatibility */
|
109
|
-
[data-bs-theme="dark"] .sortable-column:hover {
|
110
|
-
background-color: rgba(255, 255, 255, 0.05);
|
111
|
-
}
|
112
|
-
|
113
|
-
[data-bs-theme="dark"] .sortable-column.sorted {
|
114
|
-
background-color: rgba(255, 255, 255, 0.1);
|
115
|
-
}
|
116
|
-
|
117
|
-
/* Column filter styling */
|
118
|
-
.column-filters td {
|
119
|
-
padding: 0.5rem;
|
120
|
-
background-color: var(--bs-tertiary-bg, #f8f9fa);
|
121
|
-
}
|
122
|
-
|
123
|
-
/* Styling for disabled input fields (IS NULL, IS NOT NULL) */
|
124
|
-
.column-filter:disabled, .disabled-filter {
|
125
|
-
background-color: var(--bs-tertiary-bg, #f0f0f0);
|
126
|
-
border-color: var(--bs-border-color, #dee2e6);
|
127
|
-
color: var(--bs-secondary-color, #6c757d);
|
128
|
-
opacity: 0.6;
|
129
|
-
cursor: not-allowed;
|
130
|
-
}
|
131
|
-
|
132
|
-
/* Action column styling */
|
133
|
-
.action-column {
|
134
|
-
width: 100px; /* Increased from 60px to accommodate two buttons */
|
135
|
-
min-width: 100px; /* Ensure minimum width */
|
136
|
-
white-space: nowrap;
|
137
|
-
position: sticky;
|
138
|
-
left: 0;
|
139
|
-
z-index: 15;
|
140
|
-
background-color: var(--bs-table-striped-bg, #f2f2f2);
|
141
|
-
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
|
142
|
-
}
|
143
|
-
|
144
|
-
.copy-factory-btn {
|
145
|
-
padding: 0.1rem 0.4rem;
|
146
|
-
width: 32px;
|
147
|
-
}
|
148
|
-
|
149
|
-
.copy-factory-btn:hover {
|
150
|
-
opacity: 0.85;
|
151
|
-
transform: translateY(-1px);
|
152
|
-
}
|
153
|
-
|
154
|
-
/* Ensure proper background color for actions column in dark mode */
|
155
|
-
[data-bs-theme="dark"] .action-column {
|
156
|
-
background-color: var(--bs-dark-bg-subtle, #343a40);
|
157
|
-
}
|
158
|
-
|
159
|
-
/* Maintain zebra striping with sticky action column */
|
160
|
-
.table-striped > tbody > tr:nth-of-type(odd) > .action-column {
|
161
|
-
background-color: var(--bs-table-striped-bg, #f8f9fa);
|
162
|
-
}
|
163
|
-
|
164
|
-
[data-bs-theme="dark"] .table-striped > tbody > tr:nth-of-type(odd) > .action-column {
|
165
|
-
background-color: var(--bs-dark-bg-subtle, #343a40);
|
166
|
-
}
|
167
|
-
|
168
|
-
.view-record-btn {
|
169
|
-
padding: 0.1rem 0.4rem;
|
170
|
-
width: 32px;
|
171
|
-
}
|
172
|
-
|
173
|
-
.view-record-btn:hover {
|
174
|
-
opacity: 0.85;
|
175
|
-
transform: translateY(-1px);
|
176
|
-
}
|
177
|
-
|
178
|
-
/* Make action column header sticky as well */
|
179
|
-
.action-column-header {
|
180
|
-
position: sticky;
|
181
|
-
left: 0;
|
182
|
-
z-index: 40 !important; /* Even higher z-index to stay on top of everything */
|
183
|
-
background-color: var(--bs-tertiary-bg, #f8f9fa) !important;
|
184
|
-
box-shadow: 2px 0 6px rgba(0, 0, 0, 0.04) !important;
|
185
|
-
}
|
186
|
-
|
187
|
-
[data-bs-theme="dark"] .action-column-header {
|
188
|
-
background-color: var(--bs-dark-bg-subtle, #343a40) !important;
|
189
|
-
}
|
190
|
-
|
191
|
-
[data-bs-theme="dark"] .action-column-header {
|
192
|
-
background-color: var(--bs-dark-bg-subtle, #343a40) !important;
|
193
|
-
}
|
194
|
-
|
195
|
-
/* Make action column filter cell sticky as well */
|
196
|
-
.action-column-filter {
|
197
|
-
position: sticky;
|
198
|
-
left: 0;
|
199
|
-
z-index: 40 !important;
|
200
|
-
background-color: var(--bs-tertiary-bg, #f8f9fa) !important;
|
201
|
-
}
|
202
|
-
|
203
|
-
[data-bs-theme="dark"] .action-column-filter {
|
204
|
-
background-color: var(--bs-tertiary-bg, #2b3035) !important;
|
205
|
-
}
|
206
|
-
|
207
|
-
/* Fix action column for entire table */
|
208
|
-
.action-column {
|
209
|
-
box-shadow: 2px 0 6px rgba(0, 0, 0, 0.04);
|
210
|
-
}
|
211
|
-
|
212
|
-
/* Ensure equal padding for all cells */
|
213
|
-
.action-column-header, .action-column-filter {
|
214
|
-
padding-left: 8px !important;
|
215
|
-
padding-right: 8px !important;
|
216
|
-
}
|
217
|
-
|
218
|
-
/* Relationship section styles */
|
219
|
-
#relationshipsSection {
|
220
|
-
border-top: 1px solid var(--bs-border-color, #dee2e6);
|
221
|
-
margin-top: 1.5rem;
|
222
|
-
padding-top: 1.5rem;
|
223
|
-
}
|
224
|
-
|
225
|
-
#relationshipsSection h6 {
|
226
|
-
color: var(--bs-primary, #0d6efd);
|
227
|
-
font-weight: 600;
|
228
|
-
border-bottom: 2px solid var(--bs-primary, #0d6efd);
|
229
|
-
padding-bottom: 0.5rem;
|
230
|
-
margin-bottom: 1rem;
|
231
|
-
}
|
232
|
-
|
233
|
-
.relationship-section h6 {
|
234
|
-
font-size: 0.95rem;
|
235
|
-
margin-bottom: 0.75rem;
|
236
|
-
padding: 0.5rem 0.75rem;
|
237
|
-
background: linear-gradient(135deg, var(--bs-primary-bg-subtle, #cfe2ff), transparent);
|
238
|
-
border-left: 3px solid var(--bs-primary, #0d6efd);
|
239
|
-
border-radius: 0.25rem;
|
240
|
-
}
|
241
|
-
|
242
|
-
.relationship-section .table {
|
243
|
-
margin-bottom: 0;
|
244
|
-
border: 1px solid var(--bs-border-color, #dee2e6);
|
245
|
-
}
|
246
|
-
|
247
|
-
.relationship-section .table th {
|
248
|
-
background-color: var(--bs-light, #f8f9fa);
|
249
|
-
font-weight: 600;
|
250
|
-
font-size: 0.875rem;
|
251
|
-
border-bottom: 2px solid var(--bs-border-color, #dee2e6);
|
252
|
-
}
|
253
|
-
|
254
|
-
.relationship-section .table td {
|
255
|
-
vertical-align: middle;
|
256
|
-
font-size: 0.875rem;
|
257
|
-
}
|
258
|
-
|
259
|
-
.relationship-section .btn {
|
260
|
-
font-size: 0.8rem;
|
261
|
-
padding: 0.375rem 0.75rem;
|
262
|
-
}
|
263
|
-
|
264
|
-
.relationship-section .btn-outline-primary:hover {
|
265
|
-
transform: translateX(2px);
|
266
|
-
transition: transform 0.2s ease;
|
267
|
-
}
|
268
|
-
|
269
|
-
.relationship-section .btn-outline-success:hover {
|
270
|
-
transform: translateX(2px);
|
271
|
-
transition: transform 0.2s ease;
|
272
|
-
}
|
273
|
-
|
274
|
-
/* Dark mode relationship styles */
|
275
|
-
[data-bs-theme="dark"] #relationshipsSection {
|
276
|
-
border-top-color: var(--bs-border-color, #495057);
|
277
|
-
}
|
278
|
-
|
279
|
-
[data-bs-theme="dark"] .relationship-section h6 {
|
280
|
-
background: linear-gradient(135deg, var(--bs-primary-bg-subtle, #031633), transparent);
|
281
|
-
}
|
282
|
-
|
283
|
-
[data-bs-theme="dark"] .relationship-section .table th {
|
284
|
-
background-color: var(--bs-dark-bg-subtle, #343a40);
|
285
|
-
color: var(--bs-light, #f8f9fa);
|
286
|
-
}
|
287
|
-
|
288
|
-
[data-bs-theme="dark"] .relationship-section .table {
|
289
|
-
border-color: var(--bs-border-color, #495057);
|
290
|
-
}
|
291
|
-
|
292
|
-
/* Responsive relationship tables */
|
293
|
-
@media (max-width: 767.98px) {
|
294
|
-
.relationship-section .table th,
|
295
|
-
.relationship-section .table td {
|
296
|
-
font-size: 0.8rem;
|
297
|
-
padding: 0.5rem 0.25rem;
|
298
|
-
}
|
299
|
-
|
300
|
-
.relationship-section .btn {
|
301
|
-
font-size: 0.75rem;
|
302
|
-
padding: 0.25rem 0.5rem;
|
303
|
-
}
|
304
|
-
}
|
305
|
-
</style>
|
306
|
-
|
307
|
-
<script>
|
308
|
-
// Initialize mermaid when document is ready
|
309
|
-
document.addEventListener('DOMContentLoaded', function() {
|
310
|
-
// Configure Mermaid for better ERD diagrams
|
311
|
-
mermaid.initialize({
|
312
|
-
startOnLoad: false,
|
313
|
-
theme: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'default',
|
314
|
-
securityLevel: 'loose',
|
315
|
-
er: {
|
316
|
-
diagramPadding: 20,
|
317
|
-
layoutDirection: 'TB',
|
318
|
-
minEntityWidth: 100,
|
319
|
-
minEntityHeight: 75,
|
320
|
-
entityPadding: 15,
|
321
|
-
stroke: 'gray',
|
322
|
-
fill: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#2D3748' : '#f5f5f5',
|
323
|
-
fontSize: 14,
|
324
|
-
useMaxWidth: true,
|
325
|
-
wrapiength: 30
|
326
|
-
}
|
327
|
-
});
|
328
|
-
console.log('Mermaid initialized with theme:', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'default');
|
329
|
-
});
|
330
|
-
</script>
|
8
|
+
<%= stylesheet_link_tag "dbviewer/table", "data-turbo-track": "reload" %>
|
9
|
+
<%= javascript_include_tag "dbviewer/table", "data-turbo-track": "reload" %>
|
331
10
|
<% end %>
|
332
11
|
|
333
12
|
<% content_for :sidebar_active do %>active<% end %>
|
@@ -525,1247 +204,6 @@
|
|
525
204
|
</div>
|
526
205
|
</div>
|
527
206
|
|
528
|
-
<style>
|
529
|
-
/* Borderless table styling */
|
530
|
-
.table {
|
531
|
-
border-collapse: separate;
|
532
|
-
border-spacing: 0;
|
533
|
-
}
|
534
|
-
|
535
|
-
.table th,
|
536
|
-
.table td {
|
537
|
-
border: none;
|
538
|
-
border-bottom: 1px solid var(--bs-border-subtle, rgba(0, 0, 0, 0.05));
|
539
|
-
}
|
540
|
-
|
541
|
-
.table thead th {
|
542
|
-
border-bottom: 2px solid var(--bs-border-subtle, rgba(0, 0, 0, 0.08));
|
543
|
-
font-weight: 500;
|
544
|
-
}
|
545
|
-
|
546
|
-
/* Add a subtle hover effect on table rows */
|
547
|
-
.table tbody tr:hover {
|
548
|
-
background-color: var(--bs-tertiary-bg, rgba(0, 0, 0, 0.02));
|
549
|
-
}
|
550
|
-
|
551
|
-
/* Dark mode compatibility */
|
552
|
-
[data-bs-theme="dark"] .table th,
|
553
|
-
[data-bs-theme="dark"] .table td {
|
554
|
-
border-bottom: 1px solid var(--bs-border-subtle, rgba(255, 255, 255, 0.05));
|
555
|
-
}
|
556
|
-
|
557
|
-
[data-bs-theme="dark"] .table thead th {
|
558
|
-
border-bottom: 2px solid var(--bs-border-subtle, rgba(255, 255, 255, 0.08));
|
559
|
-
}
|
560
|
-
|
561
|
-
[data-bs-theme="dark"] .table tbody tr:hover {
|
562
|
-
background-color: var(--bs-tertiary-bg, rgba(255, 255, 255, 0.03));
|
563
|
-
}
|
564
|
-
|
565
|
-
/* Column filter styling */
|
566
|
-
.column-filters td {
|
567
|
-
padding: 0.5rem;
|
568
|
-
background-color: var(--bs-tertiary-bg, #f8f9fa);
|
569
|
-
}
|
570
|
-
|
571
|
-
/* Action column styling */
|
572
|
-
.action-column {
|
573
|
-
width: 100px; /* Increased from 60px to accommodate two buttons */
|
574
|
-
min-width: 100px; /* Ensure minimum width */
|
575
|
-
white-space: nowrap;
|
576
|
-
position: sticky;
|
577
|
-
left: 0;
|
578
|
-
z-index: 30; /* Increased z-index to ensure it stays on top */
|
579
|
-
background-color: var(--bs-body-bg, #fff); /* Use body background color */
|
580
|
-
box-shadow: 2px 0 6px rgba(0, 0, 0, 0.04);
|
581
|
-
}
|
582
|
-
|
583
|
-
.copy-factory-btn {
|
584
|
-
padding: 0.1rem 0.4rem;
|
585
|
-
width: 32px;
|
586
|
-
}
|
587
|
-
|
588
|
-
.copy-factory-btn:hover {
|
589
|
-
opacity: 0.85;
|
590
|
-
transform: translateY(-1px);
|
591
|
-
}
|
592
|
-
|
593
|
-
/* Ensure proper background color for actions column in dark mode */
|
594
|
-
[data-bs-theme="dark"] .action-column {
|
595
|
-
background-color: var(--bs-body-bg, #212529); /* Use body background in dark mode */
|
596
|
-
}
|
597
|
-
|
598
|
-
/* Maintain zebra striping with sticky action column */
|
599
|
-
.table-striped > tbody > tr:nth-of-type(odd) > .action-column {
|
600
|
-
background-color: var(--bs-tertiary-bg, #f8f9fa);
|
601
|
-
}
|
602
|
-
|
603
|
-
.table-striped > tbody > tr:nth-of-type(even) > .action-column {
|
604
|
-
background-color: var(--bs-body-bg, #fff);
|
605
|
-
}
|
606
|
-
|
607
|
-
[data-bs-theme="dark"] .table-striped > tbody > tr:nth-of-type(odd) > .action-column {
|
608
|
-
background-color: var(--bs-tertiary-bg, #2b3035);
|
609
|
-
}
|
610
|
-
|
611
|
-
[data-bs-theme="dark"] .table-striped > tbody > tr:nth-of-type(even) > .action-column {
|
612
|
-
background-color: var(--bs-body-bg, #212529);
|
613
|
-
}
|
614
|
-
|
615
|
-
.view-record-btn {
|
616
|
-
padding: 0.1rem 0.4rem;
|
617
|
-
width: 32px;
|
618
|
-
}
|
619
|
-
|
620
|
-
.view-record-btn:hover {
|
621
|
-
opacity: 0.85;
|
622
|
-
transform: translateY(-1px);
|
623
|
-
}
|
624
|
-
|
625
|
-
/* Record detail modal styling */
|
626
|
-
.record-detail-table tr:first-child th,
|
627
|
-
.record-detail-table tr:first-child td {
|
628
|
-
border-top: none;
|
629
|
-
}
|
630
|
-
|
631
|
-
.record-detail-table .code-block {
|
632
|
-
background-color: var(--bs-light);
|
633
|
-
padding: 0.5rem;
|
634
|
-
border-radius: 0.25rem;
|
635
|
-
overflow-x: auto;
|
636
|
-
max-height: 200px;
|
637
|
-
}
|
638
|
-
|
639
|
-
/* Relationships section styling */
|
640
|
-
#relationshipsSection {
|
641
|
-
border-top: 1px solid var(--bs-border-color);
|
642
|
-
padding-top: 1rem;
|
643
|
-
}
|
644
|
-
|
645
|
-
#relationshipsSection h6 {
|
646
|
-
color: var(--bs-emphasis-color);
|
647
|
-
margin-bottom: 1rem;
|
648
|
-
}
|
649
|
-
|
650
|
-
[data-bs-theme="dark"] #relationshipsSection {
|
651
|
-
border-top-color: #495057;
|
652
|
-
}
|
653
|
-
|
654
|
-
.relationships-table .btn-outline-primary {
|
655
|
-
font-size: 0.75rem;
|
656
|
-
padding: 0.25rem 0.5rem;
|
657
|
-
}
|
658
|
-
|
659
|
-
.relationships-table code {
|
660
|
-
background-color: var(--bs-gray-100);
|
661
|
-
padding: 0.125rem 0.25rem;
|
662
|
-
border-radius: 0.125rem;
|
663
|
-
font-size: 0.875rem;
|
664
|
-
}
|
665
|
-
|
666
|
-
[data-bs-theme="dark"] .relationships-table code {
|
667
|
-
background-color: var(--bs-gray-800);
|
668
|
-
color: var(--bs-gray-100);
|
669
|
-
}
|
670
|
-
margin-bottom: 0;
|
671
|
-
}
|
672
|
-
|
673
|
-
[data-bs-theme="dark"] .record-detail-table .code-block {
|
674
|
-
background-color: var(--bs-dark);
|
675
|
-
}
|
676
|
-
|
677
|
-
/* Fullscreen table styles */
|
678
|
-
.table-fullscreen {
|
679
|
-
position: fixed !important;
|
680
|
-
top: 0 !important;
|
681
|
-
left: 0 !important;
|
682
|
-
width: 100vw !important;
|
683
|
-
height: 100vh !important;
|
684
|
-
z-index: 9999 !important;
|
685
|
-
background: var(--bs-body-bg) !important;
|
686
|
-
margin: 0 !important;
|
687
|
-
border-radius: 0 !important;
|
688
|
-
overflow: hidden !important;
|
689
|
-
display: flex !important;
|
690
|
-
flex-direction: column !important;
|
691
|
-
}
|
692
|
-
|
693
|
-
.table-fullscreen .card-body {
|
694
|
-
flex: 1 !important;
|
695
|
-
overflow: hidden !important;
|
696
|
-
display: flex !important;
|
697
|
-
flex-direction: column !important;
|
698
|
-
}
|
699
|
-
|
700
|
-
.table-fullscreen .table-responsive {
|
701
|
-
flex: 1 !important;
|
702
|
-
overflow: auto !important;
|
703
|
-
}
|
704
|
-
|
705
|
-
.table-fullscreen .card-header {
|
706
|
-
flex-shrink: 0 !important;
|
707
|
-
position: sticky !important;
|
708
|
-
top: 0 !important;
|
709
|
-
z-index: 10000 !important;
|
710
|
-
background: var(--bs-body-bg) !important;
|
711
|
-
border-bottom: 1px solid var(--bs-border-color) !important;
|
712
|
-
}
|
713
|
-
|
714
|
-
/* Hide pagination in fullscreen mode */
|
715
|
-
.table-fullscreen .pagination-container {
|
716
|
-
display: none !important;
|
717
|
-
}
|
718
|
-
|
719
|
-
/* Adjust table header in fullscreen */
|
720
|
-
.table-fullscreen .dbviewer-table-header {
|
721
|
-
position: sticky !important;
|
722
|
-
top: 0 !important;
|
723
|
-
z-index: 100 !important;
|
724
|
-
}
|
725
|
-
|
726
|
-
/* Ensure body doesn't scroll when table is fullscreen */
|
727
|
-
body.table-fullscreen-active {
|
728
|
-
overflow: hidden !important;
|
729
|
-
}
|
730
|
-
|
731
|
-
/* Fullscreen button hover effect */
|
732
|
-
#fullscreen-toggle:hover {
|
733
|
-
background-color: var(--bs-secondary-bg) !important;
|
734
|
-
border-color: var(--bs-secondary-border-subtle) !important;
|
735
|
-
}
|
736
|
-
|
737
|
-
/* Smooth transitions */
|
738
|
-
#table-section {
|
739
|
-
transition: all 0.3s ease-in-out;
|
740
|
-
}
|
741
|
-
</style>
|
742
|
-
|
743
|
-
<script>
|
744
|
-
document.addEventListener('DOMContentLoaded', function() {
|
745
|
-
// Record Detail Modal functionality
|
746
|
-
const recordDetailModal = document.getElementById('recordDetailModal');
|
747
|
-
if (recordDetailModal) {
|
748
|
-
recordDetailModal.addEventListener('show.bs.modal', function (event) {
|
749
|
-
// Button that triggered the modal
|
750
|
-
const button = event.relatedTarget;
|
751
|
-
|
752
|
-
// Extract record data from button's data attribute
|
753
|
-
let recordData;
|
754
|
-
let foreignKeys;
|
755
|
-
try {
|
756
|
-
recordData = JSON.parse(button.getAttribute('data-record-data'));
|
757
|
-
foreignKeys = JSON.parse(button.getAttribute('data-foreign-keys') || '[]');
|
758
|
-
} catch (e) {
|
759
|
-
console.error('Error parsing record data:', e);
|
760
|
-
recordData = {};
|
761
|
-
foreignKeys = [];
|
762
|
-
}
|
763
|
-
|
764
|
-
// Update the modal's title with table name
|
765
|
-
const modalTitle = recordDetailModal.querySelector('.modal-title');
|
766
|
-
modalTitle.textContent = '<%= @table_name %> Record Details';
|
767
|
-
|
768
|
-
// Populate the table with record data
|
769
|
-
const tableBody = document.getElementById('recordDetailTableBody');
|
770
|
-
tableBody.innerHTML = '';
|
771
|
-
|
772
|
-
// Get all columns
|
773
|
-
const columns = Object.keys(recordData);
|
774
|
-
|
775
|
-
// Create rows for each column
|
776
|
-
columns.forEach(column => {
|
777
|
-
const row = document.createElement('tr');
|
778
|
-
|
779
|
-
// Create column name cell
|
780
|
-
const columnNameCell = document.createElement('td');
|
781
|
-
columnNameCell.className = 'fw-bold';
|
782
|
-
columnNameCell.textContent = column;
|
783
|
-
row.appendChild(columnNameCell);
|
784
|
-
|
785
|
-
// Create value cell
|
786
|
-
const valueCell = document.createElement('td');
|
787
|
-
let cellValue = recordData[column];
|
788
|
-
|
789
|
-
// Format value differently based on type
|
790
|
-
if (cellValue === null) {
|
791
|
-
valueCell.innerHTML = '<span class="text-muted">NULL</span>';
|
792
|
-
} else if (typeof cellValue === 'string' && cellValue.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)) {
|
793
|
-
// Handle datetime values
|
794
|
-
const date = new Date(cellValue);
|
795
|
-
if (!isNaN(date.getTime())) {
|
796
|
-
valueCell.textContent = date.toLocaleString();
|
797
|
-
} else {
|
798
|
-
valueCell.textContent = cellValue;
|
799
|
-
}
|
800
|
-
} else if (typeof cellValue === 'string' && (cellValue.startsWith('{') || cellValue.startsWith('['))) {
|
801
|
-
// Handle JSON values
|
802
|
-
try {
|
803
|
-
const jsonValue = JSON.parse(cellValue);
|
804
|
-
const formattedJSON = JSON.stringify(jsonValue, null, 2);
|
805
|
-
valueCell.innerHTML = `<pre class="mb-0 code-block">${formattedJSON}</pre>`;
|
806
|
-
} catch (e) {
|
807
|
-
valueCell.textContent = cellValue;
|
808
|
-
}
|
809
|
-
} else {
|
810
|
-
valueCell.textContent = cellValue;
|
811
|
-
}
|
812
|
-
|
813
|
-
row.appendChild(valueCell);
|
814
|
-
tableBody.appendChild(row);
|
815
|
-
});
|
816
|
-
|
817
|
-
// Populate relationships section
|
818
|
-
const relationshipsSection = document.getElementById('relationshipsSection');
|
819
|
-
const relationshipsContent = document.getElementById('relationshipsContent');
|
820
|
-
const reverseForeignKeys = JSON.parse(button.dataset.reverseForeignKeys || '[]');
|
821
|
-
|
822
|
-
// Check if we have any relationships to show
|
823
|
-
const hasRelationships = (foreignKeys && foreignKeys.length > 0) || (reverseForeignKeys && reverseForeignKeys.length > 0);
|
824
|
-
|
825
|
-
if (hasRelationships) {
|
826
|
-
relationshipsSection.style.display = 'block';
|
827
|
-
relationshipsContent.innerHTML = '';
|
828
|
-
|
829
|
-
// Handle belongs_to relationships (foreign keys from this table)
|
830
|
-
if (foreignKeys && foreignKeys.length > 0) {
|
831
|
-
const activeRelationships = foreignKeys.filter(fk => {
|
832
|
-
const columnValue = recordData[fk.column];
|
833
|
-
return columnValue !== null && columnValue !== undefined && columnValue !== '';
|
834
|
-
});
|
835
|
-
|
836
|
-
if (activeRelationships.length > 0) {
|
837
|
-
relationshipsContent.appendChild(createRelationshipSection('Belongs To', activeRelationships, recordData, 'belongs_to'));
|
838
|
-
}
|
839
|
-
}
|
840
|
-
|
841
|
-
// Handle has_many relationships (foreign keys from other tables pointing to this table)
|
842
|
-
if (reverseForeignKeys && reverseForeignKeys.length > 0) {
|
843
|
-
const primaryKeyValue = recordData[Object.keys(recordData).find(key => key === 'id') || Object.keys(recordData)[0]];
|
844
|
-
|
845
|
-
if (primaryKeyValue !== null && primaryKeyValue !== undefined && primaryKeyValue !== '') {
|
846
|
-
const hasManySection = createRelationshipSection('Has Many', reverseForeignKeys, recordData, 'has_many', primaryKeyValue);
|
847
|
-
relationshipsContent.appendChild(hasManySection);
|
848
|
-
|
849
|
-
// Fetch relationship counts asynchronously
|
850
|
-
fetchRelationshipCounts('<%= @table_name %>', primaryKeyValue, reverseForeignKeys, hasManySection);
|
851
|
-
}
|
852
|
-
}
|
853
|
-
|
854
|
-
// Show message if no active relationships
|
855
|
-
if (relationshipsContent.children.length === 0) {
|
856
|
-
relationshipsContent.innerHTML = `
|
857
|
-
<div class="text-muted small">
|
858
|
-
<i class="bi bi-info-circle me-1"></i>
|
859
|
-
This record has no active relationships.
|
860
|
-
</div>
|
861
|
-
`;
|
862
|
-
}
|
863
|
-
} else {
|
864
|
-
relationshipsSection.style.display = 'none';
|
865
|
-
}
|
866
|
-
});
|
867
|
-
}
|
868
|
-
|
869
|
-
// Column filter functionality
|
870
|
-
const columnFilters = document.querySelectorAll('.column-filter');
|
871
|
-
const operatorSelects = document.querySelectorAll('.operator-select');
|
872
|
-
const filterForm = document.getElementById('column-filters-form');
|
873
|
-
|
874
|
-
// Add debounce function to reduce form submissions
|
875
|
-
function debounce(func, wait) {
|
876
|
-
let timeout;
|
877
|
-
return function() {
|
878
|
-
const context = this;
|
879
|
-
const args = arguments;
|
880
|
-
clearTimeout(timeout);
|
881
|
-
timeout = setTimeout(function() {
|
882
|
-
func.apply(context, args);
|
883
|
-
}, wait);
|
884
|
-
};
|
885
|
-
}
|
886
|
-
|
887
|
-
// Function to handle operator changes for IS NULL and IS NOT NULL operators
|
888
|
-
function setupNullOperators() {
|
889
|
-
operatorSelects.forEach(select => {
|
890
|
-
// Initial setup for existing null operators
|
891
|
-
if (select.value === 'is_null' || select.value === 'is_not_null') {
|
892
|
-
const columnName = select.name.match(/\[(.*?)_operator\]/)[1];
|
893
|
-
const inputContainer = select.closest('.filter-input-group');
|
894
|
-
// Check for display field (the visible disabled field)
|
895
|
-
const displayField = inputContainer.querySelector(`[data-column="${columnName}_display"]`);
|
896
|
-
if (displayField) {
|
897
|
-
displayField.classList.add('disabled-filter');
|
898
|
-
}
|
899
|
-
|
900
|
-
// Make sure the value field properly reflects the null operator
|
901
|
-
const valueField = inputContainer.querySelector(`[data-column="${columnName}"]`);
|
902
|
-
if (valueField) {
|
903
|
-
valueField.value = select.value;
|
904
|
-
}
|
905
|
-
}
|
906
|
-
|
907
|
-
// Handle operator changes
|
908
|
-
select.addEventListener('change', function() {
|
909
|
-
const columnName = this.name.match(/\[(.*?)_operator\]/)[1];
|
910
|
-
const filterForm = this.closest('form');
|
911
|
-
const inputContainer = this.closest('.filter-input-group');
|
912
|
-
const hiddenField = inputContainer.querySelector(`[data-column="${columnName}"]`);
|
913
|
-
const displayField = inputContainer.querySelector(`[data-column="${columnName}_display"]`);
|
914
|
-
const wasNullOperator = hiddenField && (hiddenField.value === 'is_null' || hiddenField.value === 'is_not_null');
|
915
|
-
const isNullOperator = this.value === 'is_null' || this.value === 'is_not_null';
|
916
|
-
|
917
|
-
if (isNullOperator) {
|
918
|
-
// Configure for null operator
|
919
|
-
if (hiddenField) {
|
920
|
-
hiddenField.value = this.value;
|
921
|
-
}
|
922
|
-
// Submit immediately
|
923
|
-
filterForm.submit();
|
924
|
-
} else if (wasNullOperator) {
|
925
|
-
// Clear value when switching from null operator to regular operator
|
926
|
-
if (hiddenField) {
|
927
|
-
hiddenField.value = '';
|
928
|
-
}
|
929
|
-
}
|
930
|
-
});
|
931
|
-
});
|
932
|
-
}
|
933
|
-
|
934
|
-
// Function to submit the form
|
935
|
-
const submitForm = debounce(function() {
|
936
|
-
filterForm.submit();
|
937
|
-
}, 500);
|
938
|
-
|
939
|
-
// Initialize the null operators handling
|
940
|
-
setupNullOperators();
|
941
|
-
|
942
|
-
// Add event listeners to all filter inputs
|
943
|
-
columnFilters.forEach(function(filter) {
|
944
|
-
// For text fields use input event
|
945
|
-
filter.addEventListener('input', submitForm);
|
946
|
-
|
947
|
-
// For date/time fields also use change event since they have calendar/time pickers
|
948
|
-
if (filter.type === 'date' || filter.type === 'datetime-local' || filter.type === 'time') {
|
949
|
-
filter.addEventListener('change', submitForm);
|
950
|
-
}
|
951
|
-
});
|
952
|
-
|
953
|
-
// Add event listeners to operator selects
|
954
|
-
operatorSelects.forEach(function(select) {
|
955
|
-
select.addEventListener('change', submitForm);
|
956
|
-
});
|
957
|
-
|
958
|
-
// Add clear button functionality if there are any filters applied
|
959
|
-
const hasActiveFilters = Array.from(columnFilters).some(input => input.value);
|
960
|
-
|
961
|
-
if (hasActiveFilters) {
|
962
|
-
// Add a clear filters button
|
963
|
-
const paginationContainer = document.querySelector('nav[aria-label="Page navigation"]') ||
|
964
|
-
document.querySelector('.table-responsive');
|
965
|
-
|
966
|
-
if (paginationContainer) {
|
967
|
-
const clearButton = document.createElement('div');
|
968
|
-
clearButton.className = 'text-center mt-3';
|
969
|
-
clearButton.innerHTML = '<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-filters">' +
|
970
|
-
'<i class="bi bi-x-circle me-1"></i>Clear All Filters</button>';
|
971
|
-
|
972
|
-
paginationContainer.insertAdjacentHTML('afterend', clearButton.outerHTML);
|
973
|
-
|
974
|
-
document.getElementById('clear-filters').addEventListener('click', function() {
|
975
|
-
// Reset all input values
|
976
|
-
columnFilters.forEach(filter => filter.value = '');
|
977
|
-
|
978
|
-
// Reset operator selects to their default values
|
979
|
-
operatorSelects.forEach(select => {
|
980
|
-
// Find the first option of the select (usually the default)
|
981
|
-
if (select.options.length > 0) {
|
982
|
-
select.selectedIndex = 0;
|
983
|
-
}
|
984
|
-
});
|
985
|
-
|
986
|
-
submitForm();
|
987
|
-
});
|
988
|
-
}
|
989
|
-
}
|
990
|
-
|
991
|
-
// Load Mini ERD when modal is opened
|
992
|
-
const miniErdModal = document.getElementById('miniErdModal');
|
993
|
-
if (miniErdModal) {
|
994
|
-
let isModalLoaded = false;
|
995
|
-
let erdData = null;
|
996
|
-
|
997
|
-
miniErdModal.addEventListener('show.bs.modal', function(event) {
|
998
|
-
const modalContent = document.getElementById('miniErdModalContent');
|
999
|
-
|
1000
|
-
// Set loading state
|
1001
|
-
modalContent.innerHTML = `
|
1002
|
-
<div class="modal-header">
|
1003
|
-
<h5 class="modal-title">Relationships for <%= @table_name %></h5>
|
1004
|
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
1005
|
-
</div>
|
1006
|
-
<div class="modal-body p-0">
|
1007
|
-
<div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
|
1008
|
-
<div class="text-center">
|
1009
|
-
<div class="spinner-border text-primary mb-3" role="status">
|
1010
|
-
<span class="visually-hidden">Loading...</span>
|
1011
|
-
</div>
|
1012
|
-
<p class="mt-2">Loading relationships diagram...</p>
|
1013
|
-
<small class="text-muted">This may take a moment for tables with many relationships</small>
|
1014
|
-
</div>
|
1015
|
-
</div>
|
1016
|
-
</div>
|
1017
|
-
`;
|
1018
|
-
|
1019
|
-
// Always fetch fresh data when modal is opened
|
1020
|
-
fetchErdData();
|
1021
|
-
});
|
1022
|
-
|
1023
|
-
// Function to fetch ERD data
|
1024
|
-
function fetchErdData() {
|
1025
|
-
// Add cache-busting timestamp to prevent browser caching
|
1026
|
-
const cacheBuster = new Date().getTime();
|
1027
|
-
const fetchUrl = `<%= dbviewer.mini_erd_table_path(@table_name, format: :json) %>?_=${cacheBuster}`;
|
1028
|
-
|
1029
|
-
fetch(fetchUrl)
|
1030
|
-
.then(response => {
|
1031
|
-
if (!response.ok) {
|
1032
|
-
throw new Error(`Server returned ${response.status} ${response.statusText}`);
|
1033
|
-
}
|
1034
|
-
return response.json(); // Parse as JSON instead of text
|
1035
|
-
})
|
1036
|
-
.then(data => {
|
1037
|
-
isModalLoaded = true;
|
1038
|
-
erdData = data; // Store the data
|
1039
|
-
renderMiniErd(data);
|
1040
|
-
})
|
1041
|
-
.catch(error => {
|
1042
|
-
console.error('Error loading mini ERD:', error);
|
1043
|
-
showErdError(error);
|
1044
|
-
});
|
1045
|
-
}
|
1046
|
-
|
1047
|
-
// Function to show error modal
|
1048
|
-
function showErdError(error) {
|
1049
|
-
const modalContent = document.getElementById('miniErdModalContent');
|
1050
|
-
modalContent.innerHTML = `
|
1051
|
-
<div class="modal-header">
|
1052
|
-
<h5 class="modal-title">Relationships for <%= @table_name %></h5>
|
1053
|
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
1054
|
-
</div>
|
1055
|
-
<div class="modal-body p-0">
|
1056
|
-
<div class="alert alert-danger m-3">
|
1057
|
-
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
1058
|
-
<strong>Error loading relationship diagram</strong>
|
1059
|
-
<p class="mt-2 mb-0">${error.message}</p>
|
1060
|
-
</div>
|
1061
|
-
<div class="m-3">
|
1062
|
-
<p><strong>Debug Information:</strong></p>
|
1063
|
-
<code>GET <%= dbviewer.mini_erd_table_path(@table_name, format: :json) %></code> failed
|
1064
|
-
<p class="mt-3">
|
1065
|
-
<button class="btn btn-sm btn-primary" onclick="retryLoadingMiniERD()">
|
1066
|
-
<i class="bi bi-arrow-clockwise me-1"></i> Retry
|
1067
|
-
</button>
|
1068
|
-
</p>
|
1069
|
-
</div>
|
1070
|
-
</div>
|
1071
|
-
<div class="modal-footer">
|
1072
|
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
1073
|
-
</div>
|
1074
|
-
`;
|
1075
|
-
}
|
1076
|
-
|
1077
|
-
// Function to render the ERD with Mermaid
|
1078
|
-
function renderMiniErd(data) {
|
1079
|
-
const modalContent = document.getElementById('miniErdModalContent');
|
1080
|
-
|
1081
|
-
// Set up the modal content with container for ERD
|
1082
|
-
modalContent.innerHTML = `
|
1083
|
-
<div class="modal-header">
|
1084
|
-
<h5 class="modal-title">Relationships for <%= @table_name %></h5>
|
1085
|
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
1086
|
-
</div>
|
1087
|
-
<div class="modal-body p-0"> <!-- Removed padding for full width -->
|
1088
|
-
<div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;"> <!-- Increased height -->
|
1089
|
-
<div id="mini-erd-loading" class="d-flex justify-content-center align-items-center" style="height: 100%; min-height: 450px;">
|
1090
|
-
<div class="text-center">
|
1091
|
-
<div class="spinner-border text-primary mb-3" role="status">
|
1092
|
-
<span class="visually-hidden">Loading...</span>
|
1093
|
-
</div>
|
1094
|
-
<p>Generating Relationships Diagram...</p>
|
1095
|
-
</div>
|
1096
|
-
</div>
|
1097
|
-
<div id="mini-erd-error" class="alert alert-danger m-3 d-none">
|
1098
|
-
<h5>Error generating diagram</h5>
|
1099
|
-
<p id="mini-erd-error-message">There was an error rendering the relationships diagram.</p>
|
1100
|
-
<pre id="mini-erd-error-details" class="bg-light p-2 small mt-2"></pre>
|
1101
|
-
</div>
|
1102
|
-
</div>
|
1103
|
-
<div id="debug-data" class="d-none m-3 border-top pt-3">
|
1104
|
-
<details>
|
1105
|
-
<summary>Debug Information</summary>
|
1106
|
-
<div class="alert alert-info small">
|
1107
|
-
<pre id="erd-data-debug" style="max-height: 100px; overflow: auto;">${JSON.stringify(data, null, 2)}</pre>
|
1108
|
-
</div>
|
1109
|
-
</details>
|
1110
|
-
</div>
|
1111
|
-
</div>
|
1112
|
-
<div class="modal-footer">
|
1113
|
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
1114
|
-
<a href="<%= dbviewer.entity_relationship_diagrams_path %>" class="btn btn-primary">View Full ERD</a>
|
1115
|
-
</div>
|
1116
|
-
`;
|
1117
|
-
|
1118
|
-
try {
|
1119
|
-
const tables = data.tables || [];
|
1120
|
-
const relationships = data.relationships || [];
|
1121
|
-
|
1122
|
-
// Validate data before proceeding
|
1123
|
-
if (!Array.isArray(tables) || !Array.isArray(relationships)) {
|
1124
|
-
showDiagramError('Invalid data format', 'The relationship data is not in the expected format.');
|
1125
|
-
console.error('Invalid data format received:', data);
|
1126
|
-
return;
|
1127
|
-
}
|
1128
|
-
|
1129
|
-
console.log(`Found ${tables.length} tables and ${relationships.length} relationships`);
|
1130
|
-
|
1131
|
-
// Create the ER diagram definition in Mermaid syntax
|
1132
|
-
let mermaidDefinition = 'erDiagram\n';
|
1133
|
-
|
1134
|
-
// Add tables to the diagram - ensure we have at least one table
|
1135
|
-
if (tables.length === 0) {
|
1136
|
-
mermaidDefinition += ` <%= @table_name.gsub(/[^\w]/, '_') %> {\n`;
|
1137
|
-
mermaidDefinition += ` string id PK\n`;
|
1138
|
-
mermaidDefinition += ` }\n`;
|
1139
|
-
} else {
|
1140
|
-
tables.forEach(function(table) {
|
1141
|
-
const tableName = table.name;
|
1142
|
-
|
1143
|
-
if (!tableName) {
|
1144
|
-
console.warn('Table with no name found:', table);
|
1145
|
-
return; // Skip this table
|
1146
|
-
}
|
1147
|
-
|
1148
|
-
// Clean table name for mermaid (remove special characters)
|
1149
|
-
const cleanTableName = tableName.replace(/[^\w]/g, '_');
|
1150
|
-
|
1151
|
-
// Make the current table stand out with a different visualization
|
1152
|
-
if (tableName === '<%= @table_name %>') {
|
1153
|
-
mermaidDefinition += ` ${cleanTableName} {\n`;
|
1154
|
-
mermaidDefinition += ` string id PK\n`;
|
1155
|
-
mermaidDefinition += ` }\n`;
|
1156
|
-
} else {
|
1157
|
-
mermaidDefinition += ` ${cleanTableName} {\n`;
|
1158
|
-
mermaidDefinition += ` string id\n`;
|
1159
|
-
mermaidDefinition += ` }\n`;
|
1160
|
-
}
|
1161
|
-
});
|
1162
|
-
}
|
1163
|
-
|
1164
|
-
// Add relationships
|
1165
|
-
if (relationships && relationships.length > 0) {
|
1166
|
-
relationships.forEach(function(rel) {
|
1167
|
-
try {
|
1168
|
-
// Ensure all required properties exist
|
1169
|
-
if (!rel.from_table || !rel.to_table) {
|
1170
|
-
console.error('Missing table in relationship:', rel);
|
1171
|
-
return; // Skip this relationship
|
1172
|
-
}
|
1173
|
-
|
1174
|
-
// Clean up table names for mermaid (remove special characters)
|
1175
|
-
const fromTable = rel.from_table.replace(/[^\w]/g, '_');
|
1176
|
-
const toTable = rel.to_table.replace(/[^\w]/g, '_');
|
1177
|
-
const relationLabel = rel.from_column || '';
|
1178
|
-
|
1179
|
-
// Customize the display based on direction
|
1180
|
-
mermaidDefinition += ` ${fromTable} }|--|| ${toTable} : "${relationLabel}"\n`;
|
1181
|
-
} catch (err) {
|
1182
|
-
console.error('Error processing relationship:', err, rel);
|
1183
|
-
}
|
1184
|
-
});
|
1185
|
-
} else {
|
1186
|
-
// Add a note if no relationships are found
|
1187
|
-
mermaidDefinition += ' %% No relationships found for this table\n';
|
1188
|
-
}
|
1189
|
-
|
1190
|
-
// Log the generated mermaid definition for debugging
|
1191
|
-
console.log('Mermaid Definition:', mermaidDefinition);
|
1192
|
-
|
1193
|
-
// Hide the loading indicator first since render might take time
|
1194
|
-
document.getElementById('mini-erd-loading').style.display = 'none';
|
1195
|
-
|
1196
|
-
// Render the diagram with Mermaid
|
1197
|
-
mermaid.render('mini-erd-graph', mermaidDefinition)
|
1198
|
-
.then(function(result) {
|
1199
|
-
console.log('Mermaid rendering successful');
|
1200
|
-
|
1201
|
-
// Get the container
|
1202
|
-
const container = document.getElementById('mini-erd-container');
|
1203
|
-
|
1204
|
-
// Insert the rendered SVG
|
1205
|
-
container.innerHTML = result.svg;
|
1206
|
-
|
1207
|
-
// Style the SVG element for better fit
|
1208
|
-
const svgElement = container.querySelector('svg');
|
1209
|
-
if (svgElement) {
|
1210
|
-
// Set size attributes for the SVG
|
1211
|
-
svgElement.setAttribute('width', '100%');
|
1212
|
-
svgElement.setAttribute('height', '100%');
|
1213
|
-
svgElement.style.minHeight = '450px';
|
1214
|
-
svgElement.style.width = '100%';
|
1215
|
-
svgElement.style.height = '100%';
|
1216
|
-
|
1217
|
-
// Set viewBox if not present to enable proper scaling
|
1218
|
-
if (!svgElement.getAttribute('viewBox')) {
|
1219
|
-
const width = svgElement.getAttribute('width') || '100%';
|
1220
|
-
const height = svgElement.getAttribute('height') || '100%';
|
1221
|
-
svgElement.setAttribute('viewBox', `0 0 ${parseInt(width) || 1000} ${parseInt(height) || 800}`);
|
1222
|
-
}
|
1223
|
-
}
|
1224
|
-
|
1225
|
-
// Apply SVG-Pan-Zoom to make the diagram interactive
|
1226
|
-
try {
|
1227
|
-
const svgElement = container.querySelector('svg');
|
1228
|
-
if (svgElement && typeof svgPanZoom !== 'undefined') {
|
1229
|
-
// Make SVG take the full container width and ensure it has valid dimensions
|
1230
|
-
svgElement.setAttribute('width', '100%');
|
1231
|
-
svgElement.setAttribute('height', '100%');
|
1232
|
-
|
1233
|
-
// Wait for SVG to be fully rendered with proper dimensions
|
1234
|
-
setTimeout(() => {
|
1235
|
-
try {
|
1236
|
-
// Get dimensions to ensure they're valid before initializing pan-zoom
|
1237
|
-
const clientRect = svgElement.getBoundingClientRect();
|
1238
|
-
|
1239
|
-
// Only initialize if we have valid dimensions
|
1240
|
-
if (clientRect.width > 0 && clientRect.height > 0) {
|
1241
|
-
// Initialize SVG Pan-Zoom with more robust error handling
|
1242
|
-
const panZoomInstance = svgPanZoom(svgElement, {
|
1243
|
-
zoomEnabled: true,
|
1244
|
-
controlIconsEnabled: true,
|
1245
|
-
fit: false, // Don't automatically fit on init - can cause the matrix error
|
1246
|
-
center: false, // Don't automatically center - can cause the matrix error
|
1247
|
-
minZoom: 0.5,
|
1248
|
-
maxZoom: 2.5,
|
1249
|
-
beforeZoom: function() {
|
1250
|
-
// Check if the SVG is valid for zooming
|
1251
|
-
return svgElement.getBoundingClientRect().width > 0 &&
|
1252
|
-
svgElement.getBoundingClientRect().height > 0;
|
1253
|
-
}
|
1254
|
-
});
|
1255
|
-
|
1256
|
-
// Store the panZoom instance for resize handling
|
1257
|
-
container.panZoomInstance = panZoomInstance;
|
1258
|
-
|
1259
|
-
// Manually fit and center after a slight delay
|
1260
|
-
setTimeout(() => {
|
1261
|
-
try {
|
1262
|
-
panZoomInstance.resize();
|
1263
|
-
panZoomInstance.fit();
|
1264
|
-
panZoomInstance.center();
|
1265
|
-
} catch(err) {
|
1266
|
-
console.warn("Error during fit/center operation:", err);
|
1267
|
-
}
|
1268
|
-
}, 300);
|
1269
|
-
|
1270
|
-
// Setup resize observer to maintain full size
|
1271
|
-
const resizeObserver = new ResizeObserver(() => {
|
1272
|
-
if (container.panZoomInstance) {
|
1273
|
-
try {
|
1274
|
-
// Reset zoom and center when container is resized
|
1275
|
-
container.panZoomInstance.resize();
|
1276
|
-
// Only fit and center if the element is visible with valid dimensions
|
1277
|
-
if (svgElement.getBoundingClientRect().width > 0 &&
|
1278
|
-
svgElement.getBoundingClientRect().height > 0) {
|
1279
|
-
container.panZoomInstance.fit();
|
1280
|
-
container.panZoomInstance.center();
|
1281
|
-
}
|
1282
|
-
} catch(err) {
|
1283
|
-
console.warn("Error during resize observer callback:", err);
|
1284
|
-
}
|
1285
|
-
}
|
1286
|
-
});
|
1287
|
-
|
1288
|
-
// Observe the container for size changes
|
1289
|
-
resizeObserver.observe(container);
|
1290
|
-
|
1291
|
-
// Also handle manual resize on modal resize
|
1292
|
-
miniErdModal.addEventListener('resize.bs.modal', function() {
|
1293
|
-
if (container.panZoomInstance) {
|
1294
|
-
setTimeout(() => {
|
1295
|
-
try {
|
1296
|
-
container.panZoomInstance.resize();
|
1297
|
-
// Only fit and center if the element is visible with valid dimensions
|
1298
|
-
if (svgElement.getBoundingClientRect().width > 0 &&
|
1299
|
-
svgElement.getBoundingClientRect().height > 0) {
|
1300
|
-
container.panZoomInstance.fit();
|
1301
|
-
container.panZoomInstance.center();
|
1302
|
-
}
|
1303
|
-
} catch(err) {
|
1304
|
-
console.warn("Error during modal resize handler:", err);
|
1305
|
-
}
|
1306
|
-
}, 300);
|
1307
|
-
}
|
1308
|
-
});
|
1309
|
-
} else {
|
1310
|
-
console.warn("Cannot initialize SVG-Pan-Zoom: SVG has invalid dimensions", clientRect);
|
1311
|
-
}
|
1312
|
-
} catch(err) {
|
1313
|
-
console.warn("Error initializing SVG-Pan-Zoom:", err);
|
1314
|
-
}
|
1315
|
-
}, 500); // Increased delay to ensure SVG is fully rendered with proper dimensions
|
1316
|
-
}
|
1317
|
-
} catch (e) {
|
1318
|
-
console.warn('Failed to initialize svg-pan-zoom:', e);
|
1319
|
-
// Not critical, continue without pan-zoom
|
1320
|
-
}
|
1321
|
-
|
1322
|
-
// Add highlighting for the current table after a delay to ensure SVG is fully processed
|
1323
|
-
setTimeout(function() {
|
1324
|
-
try {
|
1325
|
-
const cleanTableName = '<%= @table_name %>'.replace(/[^\w]/g, '_');
|
1326
|
-
const currentTableElement = container.querySelector(`[id*="${cleanTableName}"]`);
|
1327
|
-
if (currentTableElement) {
|
1328
|
-
const rect = currentTableElement.querySelector('rect');
|
1329
|
-
if (rect) {
|
1330
|
-
// Highlight the current table
|
1331
|
-
rect.setAttribute('fill', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#2c3034' : '#e2f0ff');
|
1332
|
-
rect.setAttribute('stroke', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#6ea8fe' : '#0d6efd');
|
1333
|
-
rect.setAttribute('stroke-width', '2');
|
1334
|
-
}
|
1335
|
-
}
|
1336
|
-
} catch (e) {
|
1337
|
-
console.error('Error highlighting current table:', e);
|
1338
|
-
}
|
1339
|
-
}, 100);
|
1340
|
-
})
|
1341
|
-
.catch(function(error) {
|
1342
|
-
console.error('Error rendering mini ERD:', error);
|
1343
|
-
showDiagramError(
|
1344
|
-
'Error rendering diagram',
|
1345
|
-
'There was an error rendering the relationships diagram.',
|
1346
|
-
error.message || 'Unknown error'
|
1347
|
-
);
|
1348
|
-
|
1349
|
-
// Show debug data when there's an error
|
1350
|
-
document.getElementById('debug-data').classList.remove('d-none');
|
1351
|
-
});
|
1352
|
-
} catch (error) {
|
1353
|
-
console.error('Exception in renderMiniErd function:', error);
|
1354
|
-
showDiagramError(
|
1355
|
-
'Exception generating diagram',
|
1356
|
-
'There was an exception processing the relationships diagram.',
|
1357
|
-
error.message || 'Unknown error'
|
1358
|
-
);
|
1359
|
-
|
1360
|
-
// Show debug data when there's an error
|
1361
|
-
document.getElementById('debug-data').classList.remove('d-none');
|
1362
|
-
}
|
1363
|
-
}
|
1364
|
-
|
1365
|
-
// Function to show diagram error
|
1366
|
-
function showDiagramError(title, message, details = '') {
|
1367
|
-
const errorContainer = document.getElementById('mini-erd-error');
|
1368
|
-
const errorMessage = document.getElementById('mini-erd-error-message');
|
1369
|
-
const errorDetails = document.getElementById('mini-erd-error-details');
|
1370
|
-
const loadingIndicator = document.getElementById('mini-erd-loading');
|
1371
|
-
|
1372
|
-
if (loadingIndicator) {
|
1373
|
-
loadingIndicator.style.display = 'none';
|
1374
|
-
}
|
1375
|
-
|
1376
|
-
if (errorContainer && errorMessage) {
|
1377
|
-
// Set error message
|
1378
|
-
errorMessage.textContent = message;
|
1379
|
-
|
1380
|
-
// Set error details if provided
|
1381
|
-
if (details && errorDetails) {
|
1382
|
-
errorDetails.textContent = details;
|
1383
|
-
errorDetails.classList.remove('d-none');
|
1384
|
-
} else if (errorDetails) {
|
1385
|
-
errorDetails.classList.add('d-none');
|
1386
|
-
}
|
1387
|
-
|
1388
|
-
// Show the error container
|
1389
|
-
errorContainer.classList.remove('d-none');
|
1390
|
-
}
|
1391
|
-
}
|
1392
|
-
|
1393
|
-
// Handle modal shown event - adjust size after the modal is fully visible
|
1394
|
-
miniErdModal.addEventListener('shown.bs.modal', function(event) {
|
1395
|
-
// After modal is fully shown, resize the diagram to fit
|
1396
|
-
const container = document.getElementById('mini-erd-container');
|
1397
|
-
if (container && container.panZoomInstance) {
|
1398
|
-
setTimeout(() => {
|
1399
|
-
try {
|
1400
|
-
// Check if the SVG still has valid dimensions before operating on it
|
1401
|
-
const svgElement = container.querySelector('svg');
|
1402
|
-
if (svgElement &&
|
1403
|
-
svgElement.getBoundingClientRect().width > 0 &&
|
1404
|
-
svgElement.getBoundingClientRect().height > 0) {
|
1405
|
-
container.panZoomInstance.resize();
|
1406
|
-
container.panZoomInstance.fit();
|
1407
|
-
container.panZoomInstance.center();
|
1408
|
-
} else {
|
1409
|
-
console.warn("Cannot perform pan-zoom operations: SVG has invalid dimensions");
|
1410
|
-
}
|
1411
|
-
} catch(err) {
|
1412
|
-
console.warn("Error during modal shown handler:", err);
|
1413
|
-
}
|
1414
|
-
}, 500); // Increased delay to ensure modal is fully transitioned and SVG is rendered
|
1415
|
-
}
|
1416
|
-
});
|
1417
|
-
|
1418
|
-
// Handle modal close to reset state for future opens
|
1419
|
-
miniErdModal.addEventListener('hidden.bs.modal', function(event) {
|
1420
|
-
// Reset flags and cached data to ensure fresh fetch on next open
|
1421
|
-
isModalLoaded = false;
|
1422
|
-
erdData = null;
|
1423
|
-
console.log('Modal closed, diagram data will be refetched on next open');
|
1424
|
-
});
|
1425
|
-
}
|
1426
|
-
|
1427
|
-
// Function to retry loading the Mini ERD
|
1428
|
-
function retryLoadingMiniERD() {
|
1429
|
-
console.log('Retrying loading of mini ERD');
|
1430
|
-
const modalContent = document.getElementById('miniErdModalContent');
|
1431
|
-
|
1432
|
-
// Set loading state again
|
1433
|
-
modalContent.innerHTML = `
|
1434
|
-
<div class="modal-header">
|
1435
|
-
<h5 class="modal-title">Relationships for <%= @table_name %></h5>
|
1436
|
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
1437
|
-
</div>
|
1438
|
-
<div class="modal-body p-0">
|
1439
|
-
<div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
|
1440
|
-
<div class="text-center">
|
1441
|
-
<div class="spinner-border text-primary mb-3" role="status">
|
1442
|
-
<span class="visually-hidden">Loading...</span>
|
1443
|
-
</div>
|
1444
|
-
<p>Retrying to load relationships diagram...</p>
|
1445
|
-
</div>
|
1446
|
-
</div>
|
1447
|
-
</div>
|
1448
|
-
`;
|
1449
|
-
|
1450
|
-
// Reset state to ensure fresh fetch
|
1451
|
-
isModalLoaded = false;
|
1452
|
-
erdData = null;
|
1453
|
-
|
1454
|
-
// Retry fetching data
|
1455
|
-
fetchErdData();
|
1456
|
-
}
|
1457
|
-
|
1458
|
-
// Column sorting enhancement
|
1459
|
-
const sortableColumns = document.querySelectorAll('.sortable-column');
|
1460
|
-
sortableColumns.forEach(column => {
|
1461
|
-
const link = column.querySelector('.column-sort-link');
|
1462
|
-
|
1463
|
-
// Mouse over effects
|
1464
|
-
column.addEventListener('mouseenter', () => {
|
1465
|
-
const sortIcon = column.querySelector('.sort-icon');
|
1466
|
-
if (sortIcon && sortIcon.classList.contains('invisible')) {
|
1467
|
-
sortIcon.style.visibility = 'visible';
|
1468
|
-
sortIcon.style.opacity = '0.3';
|
1469
|
-
}
|
1470
|
-
});
|
1471
|
-
|
1472
|
-
column.addEventListener('mouseleave', () => {
|
1473
|
-
const sortIcon = column.querySelector('.sort-icon');
|
1474
|
-
if (sortIcon && sortIcon.classList.contains('invisible')) {
|
1475
|
-
sortIcon.style.visibility = 'hidden';
|
1476
|
-
sortIcon.style.opacity = '0';
|
1477
|
-
}
|
1478
|
-
});
|
1479
|
-
|
1480
|
-
// Keyboard accessibility
|
1481
|
-
if (link) {
|
1482
|
-
link.addEventListener('keydown', (e) => {
|
1483
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
1484
|
-
e.preventDefault();
|
1485
|
-
link.click();
|
1486
|
-
}
|
1487
|
-
});
|
1488
|
-
}
|
1489
|
-
});
|
1490
|
-
|
1491
|
-
// Table fullscreen functionality
|
1492
|
-
const fullscreenToggle = document.getElementById('fullscreen-toggle');
|
1493
|
-
const fullscreenIcon = document.getElementById('fullscreen-icon');
|
1494
|
-
const tableSection = document.getElementById('table-section');
|
1495
|
-
|
1496
|
-
if (fullscreenToggle && tableSection) {
|
1497
|
-
// Key for storing fullscreen state in localStorage
|
1498
|
-
const fullscreenStateKey = 'dbviewer-table-fullscreen-<%= @table_name %>';
|
1499
|
-
|
1500
|
-
// Function to apply fullscreen state
|
1501
|
-
function applyFullscreenState(isFullscreen) {
|
1502
|
-
if (isFullscreen) {
|
1503
|
-
// Enter fullscreen
|
1504
|
-
tableSection.classList.add('table-fullscreen');
|
1505
|
-
document.body.classList.add('table-fullscreen-active');
|
1506
|
-
fullscreenIcon.classList.remove('bi-fullscreen');
|
1507
|
-
fullscreenIcon.classList.add('bi-fullscreen-exit');
|
1508
|
-
fullscreenToggle.setAttribute('title', 'Exit fullscreen');
|
1509
|
-
} else {
|
1510
|
-
// Exit fullscreen
|
1511
|
-
tableSection.classList.remove('table-fullscreen');
|
1512
|
-
document.body.classList.remove('table-fullscreen-active');
|
1513
|
-
fullscreenIcon.classList.remove('bi-fullscreen-exit');
|
1514
|
-
fullscreenIcon.classList.add('bi-fullscreen');
|
1515
|
-
fullscreenToggle.setAttribute('title', 'Toggle fullscreen');
|
1516
|
-
}
|
1517
|
-
}
|
1518
|
-
|
1519
|
-
// Restore fullscreen state from localStorage on page load
|
1520
|
-
try {
|
1521
|
-
const savedState = localStorage.getItem(fullscreenStateKey);
|
1522
|
-
if (savedState === 'true') {
|
1523
|
-
applyFullscreenState(true);
|
1524
|
-
}
|
1525
|
-
} catch (e) {
|
1526
|
-
// Handle localStorage not available (private browsing, etc.)
|
1527
|
-
console.warn('Could not restore fullscreen state:', e);
|
1528
|
-
}
|
1529
|
-
|
1530
|
-
fullscreenToggle.addEventListener('click', function() {
|
1531
|
-
const isFullscreen = tableSection.classList.contains('table-fullscreen');
|
1532
|
-
const newState = !isFullscreen;
|
1533
|
-
|
1534
|
-
// Apply the new state
|
1535
|
-
applyFullscreenState(newState);
|
1536
|
-
|
1537
|
-
// Save state to localStorage
|
1538
|
-
try {
|
1539
|
-
localStorage.setItem(fullscreenStateKey, newState.toString());
|
1540
|
-
} catch (e) {
|
1541
|
-
// Handle localStorage not available (private browsing, etc.)
|
1542
|
-
console.warn('Could not save fullscreen state:', e);
|
1543
|
-
}
|
1544
|
-
});
|
1545
|
-
|
1546
|
-
// Exit fullscreen with Escape key
|
1547
|
-
document.addEventListener('keydown', function(e) {
|
1548
|
-
if (e.key === 'Escape' && tableSection.classList.contains('table-fullscreen')) {
|
1549
|
-
fullscreenToggle.click();
|
1550
|
-
}
|
1551
|
-
});
|
1552
|
-
}
|
1553
|
-
|
1554
|
-
// Function to copy FactoryBot code
|
1555
|
-
window.copyToJson = function(button) {
|
1556
|
-
try {
|
1557
|
-
// Get record data from data attribute
|
1558
|
-
const recordData = JSON.parse(button.dataset.recordData);
|
1559
|
-
|
1560
|
-
// Generate formatted JSON string
|
1561
|
-
const jsonString = JSON.stringify(recordData, null, 2);
|
1562
|
-
|
1563
|
-
// Copy to clipboard
|
1564
|
-
navigator.clipboard.writeText(jsonString).then(() => {
|
1565
|
-
// Show a temporary success message on the button
|
1566
|
-
const originalTitle = button.getAttribute('title');
|
1567
|
-
button.setAttribute('title', 'Copied!');
|
1568
|
-
button.classList.remove('btn-outline-secondary');
|
1569
|
-
button.classList.add('btn-success');
|
1570
|
-
|
1571
|
-
// Show a toast notification
|
1572
|
-
if (typeof Toastify === 'function') {
|
1573
|
-
Toastify({
|
1574
|
-
text: `<span class="toast-icon"><i class="bi bi-clipboard-check"></i></span> JSON data copied to clipboard!`,
|
1575
|
-
className: "toast-factory-bot",
|
1576
|
-
duration: 3000,
|
1577
|
-
gravity: "bottom",
|
1578
|
-
position: "right",
|
1579
|
-
escapeMarkup: false,
|
1580
|
-
style: {
|
1581
|
-
animation: "slideInRight 0.3s ease-out, slideOutRight 0.3s ease-out 2.7s"
|
1582
|
-
},
|
1583
|
-
onClick: function() { /* Dismiss toast on click */ }
|
1584
|
-
}).showToast();
|
1585
|
-
}
|
1586
|
-
|
1587
|
-
setTimeout(() => {
|
1588
|
-
button.setAttribute('title', originalTitle);
|
1589
|
-
button.classList.remove('btn-success');
|
1590
|
-
button.classList.add('btn-outline-secondary');
|
1591
|
-
}, 2000);
|
1592
|
-
}).catch(err => {
|
1593
|
-
console.error('Failed to copy text: ', err);
|
1594
|
-
|
1595
|
-
// Show error toast
|
1596
|
-
if (typeof Toastify === 'function') {
|
1597
|
-
Toastify({
|
1598
|
-
text: '<span class="toast-icon"><i class="bi bi-exclamation-triangle"></i></span> Failed to copy to clipboard',
|
1599
|
-
className: "bg-danger",
|
1600
|
-
duration: 3000,
|
1601
|
-
gravity: "bottom",
|
1602
|
-
position: "right",
|
1603
|
-
escapeMarkup: false,
|
1604
|
-
style: {
|
1605
|
-
background: "linear-gradient(135deg, #dc3545, #c82333)",
|
1606
|
-
animation: "slideInRight 0.3s ease-out"
|
1607
|
-
}
|
1608
|
-
}).showToast();
|
1609
|
-
} else {
|
1610
|
-
alert('Failed to copy to clipboard. See console for details.');
|
1611
|
-
}
|
1612
|
-
});
|
1613
|
-
} catch (error) {
|
1614
|
-
console.error('Error generating JSON:', error);
|
1615
|
-
alert('Error generating JSON. See console for details.');
|
1616
|
-
}
|
1617
|
-
};
|
1618
|
-
|
1619
|
-
// Helper function to create relationship sections
|
1620
|
-
// Function to fetch relationship counts from API
|
1621
|
-
async function fetchRelationshipCounts(tableName, recordId, relationships, hasManySection) {
|
1622
|
-
try {
|
1623
|
-
const response = await fetch(`/dbviewer/api/tables/${tableName}/relationship_counts?record_id=${recordId}`);
|
1624
|
-
if (!response.ok) {
|
1625
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
1626
|
-
}
|
1627
|
-
|
1628
|
-
const data = await response.json();
|
1629
|
-
|
1630
|
-
// Update each count in the UI
|
1631
|
-
const countSpans = hasManySection.querySelectorAll('.relationship-count');
|
1632
|
-
|
1633
|
-
relationships.forEach((relationship, index) => {
|
1634
|
-
const countSpan = countSpans[index];
|
1635
|
-
if (countSpan) {
|
1636
|
-
const relationshipData = data.relationships.find(r =>
|
1637
|
-
r.table === relationship.from_table && r.foreign_key === relationship.column
|
1638
|
-
);
|
1639
|
-
|
1640
|
-
if (relationshipData) {
|
1641
|
-
const count = relationshipData.count;
|
1642
|
-
let badgeClass = 'bg-secondary';
|
1643
|
-
let badgeText = `${count} record${count !== 1 ? 's' : ''}`;
|
1644
|
-
|
1645
|
-
// Use different colors based on count
|
1646
|
-
if (count > 0) {
|
1647
|
-
badgeClass = count > 10 ? 'bg-warning' : 'bg-success';
|
1648
|
-
}
|
1649
|
-
|
1650
|
-
countSpan.innerHTML = `<span class="badge ${badgeClass}">${badgeText}</span>`;
|
1651
|
-
} else {
|
1652
|
-
// Fallback if no data found
|
1653
|
-
countSpan.innerHTML = '<span class="badge bg-danger">Error</span>';
|
1654
|
-
|
1655
|
-
}
|
1656
|
-
}
|
1657
|
-
});
|
1658
|
-
|
1659
|
-
} catch (error) {
|
1660
|
-
console.error('Error fetching relationship counts:', error);
|
1661
|
-
|
1662
|
-
// Show error state in UI
|
1663
|
-
const countSpans = hasManySection.querySelectorAll('.relationship-count');
|
1664
|
-
countSpans.forEach(span => {
|
1665
|
-
span.innerHTML = '<span class="badge bg-danger">Error</span>';
|
1666
|
-
});
|
1667
|
-
}
|
1668
|
-
}
|
1669
|
-
|
1670
|
-
function createRelationshipSection(title, relationships, recordData, type, primaryKeyValue = null) {
|
1671
|
-
const section = document.createElement('div');
|
1672
|
-
section.className = 'relationship-section mb-4';
|
1673
|
-
|
1674
|
-
// Create section header
|
1675
|
-
const header = document.createElement('h6');
|
1676
|
-
header.className = 'mb-3';
|
1677
|
-
const icon = type === 'belongs_to' ? 'bi-arrow-up-right' : 'bi-arrow-down-left';
|
1678
|
-
header.innerHTML = `<i class="bi ${icon} me-2"></i>${title}`;
|
1679
|
-
section.appendChild(header);
|
1680
|
-
|
1681
|
-
const tableContainer = document.createElement('div');
|
1682
|
-
tableContainer.className = 'table-responsive';
|
1683
|
-
|
1684
|
-
const table = document.createElement('table');
|
1685
|
-
table.className = 'table table-sm table-bordered';
|
1686
|
-
|
1687
|
-
// Create header based on relationship type
|
1688
|
-
const thead = document.createElement('thead');
|
1689
|
-
if (type === 'belongs_to') {
|
1690
|
-
thead.innerHTML = `
|
1691
|
-
<tr>
|
1692
|
-
<th width="25%">Column</th>
|
1693
|
-
<th width="25%">Value</th>
|
1694
|
-
<th width="25%">References</th>
|
1695
|
-
<th width="25%">Action</th>
|
1696
|
-
</tr>
|
1697
|
-
`;
|
1698
|
-
} else {
|
1699
|
-
thead.innerHTML = `
|
1700
|
-
<tr>
|
1701
|
-
<th width="30%">Related Table</th>
|
1702
|
-
<th width="25%">Foreign Key</th>
|
1703
|
-
<th width="20%">Count</th>
|
1704
|
-
<th width="25%">Action</th>
|
1705
|
-
</tr>
|
1706
|
-
`;
|
1707
|
-
}
|
1708
|
-
table.appendChild(thead);
|
1709
|
-
|
1710
|
-
// Create body
|
1711
|
-
const tbody = document.createElement('tbody');
|
1712
|
-
|
1713
|
-
relationships.forEach(fk => {
|
1714
|
-
const row = document.createElement('tr');
|
1715
|
-
|
1716
|
-
if (type === 'belongs_to') {
|
1717
|
-
const columnValue = recordData[fk.column];
|
1718
|
-
row.innerHTML = `
|
1719
|
-
<td class="fw-medium">${fk.column}</td>
|
1720
|
-
<td><code>${columnValue}</code></td>
|
1721
|
-
<td>
|
1722
|
-
<span class="text-muted">${fk.to_table}.</span><strong>${fk.primary_key}</strong>
|
1723
|
-
</td>
|
1724
|
-
<td>
|
1725
|
-
<a href="/dbviewer/tables/${fk.to_table}?column_filters[${fk.primary_key}]=${encodeURIComponent(columnValue)}"
|
1726
|
-
class="btn btn-sm btn-outline-primary"
|
1727
|
-
title="View referenced record in ${fk.to_table}">
|
1728
|
-
<i class="bi bi-arrow-right me-1"></i>View
|
1729
|
-
</a>
|
1730
|
-
</td>
|
1731
|
-
`;
|
1732
|
-
} else {
|
1733
|
-
// For has_many relationships
|
1734
|
-
row.innerHTML = `
|
1735
|
-
<td class="fw-medium">${fk.from_table}</td>
|
1736
|
-
<td>
|
1737
|
-
<span class="text-muted">${fk.from_table}.</span><strong>${fk.column}</strong>
|
1738
|
-
</td>
|
1739
|
-
<td>
|
1740
|
-
<span class="relationship-count">
|
1741
|
-
<span class="badge bg-secondary">
|
1742
|
-
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
1743
|
-
Loading...
|
1744
|
-
</span>
|
1745
|
-
</span>
|
1746
|
-
</td>
|
1747
|
-
<td>
|
1748
|
-
<a href="/dbviewer/tables/${fk.from_table}?column_filters[${fk.column}]=${encodeURIComponent(primaryKeyValue)}"
|
1749
|
-
class="btn btn-sm btn-outline-success"
|
1750
|
-
title="View all ${fk.from_table} records that reference this record">
|
1751
|
-
<i class="bi bi-list me-1"></i>View Related
|
1752
|
-
</a>
|
1753
|
-
</td>
|
1754
|
-
`;
|
1755
|
-
}
|
1756
|
-
|
1757
|
-
tbody.appendChild(row);
|
1758
|
-
});
|
1759
|
-
|
1760
|
-
table.appendChild(tbody);
|
1761
|
-
tableContainer.appendChild(table);
|
1762
|
-
section.appendChild(tableContainer);
|
1763
|
-
|
1764
|
-
return section;
|
1765
|
-
}
|
1766
|
-
});
|
1767
|
-
</script>
|
1768
|
-
|
1769
207
|
<!-- Floating Creation Filter - Only visible on desktop and on table details page -->
|
1770
208
|
<% if has_timestamp_column?(@table_name) %>
|
1771
209
|
<div class="floating-creation-filter d-none d-lg-block">
|
@@ -1909,869 +347,6 @@
|
|
1909
347
|
</form>
|
1910
348
|
</div>
|
1911
349
|
</div>
|
1912
|
-
|
1913
|
-
<style>
|
1914
|
-
/* Floating creation filter button */
|
1915
|
-
.floating-creation-filter {
|
1916
|
-
position: fixed;
|
1917
|
-
bottom: 30px;
|
1918
|
-
right: 30px;
|
1919
|
-
z-index: 1050;
|
1920
|
-
}
|
1921
|
-
|
1922
|
-
.floating-filter-btn {
|
1923
|
-
width: 60px;
|
1924
|
-
height: 60px;
|
1925
|
-
border-radius: 50%;
|
1926
|
-
display: flex;
|
1927
|
-
align-items: center;
|
1928
|
-
justify-content: center;
|
1929
|
-
font-size: 1.2rem;
|
1930
|
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
1931
|
-
border: none;
|
1932
|
-
position: relative;
|
1933
|
-
background: var(--bs-primary);
|
1934
|
-
color: white;
|
1935
|
-
box-shadow: 0 4px 12px rgba(var(--bs-primary-rgb), 0.4);
|
1936
|
-
}
|
1937
|
-
|
1938
|
-
.floating-filter-btn:hover {
|
1939
|
-
transform: translateY(-3px) scale(1.05);
|
1940
|
-
box-shadow: 0 8px 25px rgba(var(--bs-primary-rgb), 0.5) !important;
|
1941
|
-
background: var(--bs-primary) !important;
|
1942
|
-
color: white !important;
|
1943
|
-
}
|
1944
|
-
|
1945
|
-
.floating-filter-btn:active {
|
1946
|
-
transform: translateY(-1px) scale(1.02);
|
1947
|
-
transition: all 0.1s ease;
|
1948
|
-
}
|
1949
|
-
|
1950
|
-
.floating-filter-btn:focus {
|
1951
|
-
outline: 2px solid rgba(var(--bs-primary-rgb), 0.5);
|
1952
|
-
outline-offset: 2px;
|
1953
|
-
}
|
1954
|
-
|
1955
|
-
/* Badge for active filter indicator */
|
1956
|
-
.floating-filter-btn .badge {
|
1957
|
-
font-size: 0.6rem;
|
1958
|
-
width: 18px;
|
1959
|
-
height: 18px;
|
1960
|
-
display: flex;
|
1961
|
-
align-items: center;
|
1962
|
-
justify-content: center;
|
1963
|
-
background: var(--bs-success) !important;
|
1964
|
-
animation: pulse 2s infinite;
|
1965
|
-
}
|
1966
|
-
|
1967
|
-
@keyframes pulse {
|
1968
|
-
0% { transform: scale(1); }
|
1969
|
-
50% { transform: scale(1.1); }
|
1970
|
-
100% { transform: scale(1); }
|
1971
|
-
}
|
1972
|
-
|
1973
|
-
/* Better datetime input styling for the floating filter */
|
1974
|
-
#creationFilterOffcanvas .form-control {
|
1975
|
-
border-radius: 6px;
|
1976
|
-
border: 1px solid var(--bs-border-color);
|
1977
|
-
background-color: var(--bs-body-bg);
|
1978
|
-
color: var(--bs-body-color);
|
1979
|
-
transition: all 0.15s ease-in-out;
|
1980
|
-
}
|
1981
|
-
|
1982
|
-
#creationFilterOffcanvas .form-control:focus {
|
1983
|
-
border-color: var(--bs-primary);
|
1984
|
-
box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
|
1985
|
-
background-color: var(--bs-body-bg);
|
1986
|
-
}
|
1987
|
-
|
1988
|
-
/* Offcanvas enhancements */
|
1989
|
-
#creationFilterOffcanvas {
|
1990
|
-
backdrop-filter: blur(10px);
|
1991
|
-
}
|
1992
|
-
|
1993
|
-
#creationFilterOffcanvas .offcanvas-header {
|
1994
|
-
background: var(--bs-body-bg);
|
1995
|
-
border-bottom: 1px solid var(--bs-border-color);
|
1996
|
-
padding: 1.25rem;
|
1997
|
-
}
|
1998
|
-
|
1999
|
-
#creationFilterOffcanvas .offcanvas-body {
|
2000
|
-
background: var(--bs-body-bg);
|
2001
|
-
padding: 1.25rem;
|
2002
|
-
}
|
2003
|
-
|
2004
|
-
#creationFilterOffcanvas .offcanvas-title {
|
2005
|
-
color: var(--bs-body-color);
|
2006
|
-
font-weight: 600;
|
2007
|
-
}
|
2008
|
-
|
2009
|
-
/* Dark mode specific enhancements */
|
2010
|
-
[data-bs-theme="dark"] .floating-filter-btn {
|
2011
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(var(--bs-primary-rgb), 0.2);
|
2012
|
-
}
|
2013
|
-
|
2014
|
-
[data-bs-theme="dark"] .floating-filter-btn:hover {
|
2015
|
-
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(var(--bs-primary-rgb), 0.3) !important;
|
2016
|
-
}
|
2017
|
-
|
2018
|
-
[data-bs-theme="dark"] #creationFilterOffcanvas .offcanvas-header {
|
2019
|
-
background: var(--bs-dark);
|
2020
|
-
border-bottom-color: var(--bs-border-color-translucent);
|
2021
|
-
}
|
2022
|
-
|
2023
|
-
[data-bs-theme="dark"] #creationFilterOffcanvas .offcanvas-body {
|
2024
|
-
background: var(--bs-dark);
|
2025
|
-
}
|
2026
|
-
|
2027
|
-
[data-bs-theme="dark"] #creationFilterOffcanvas .form-control {
|
2028
|
-
background-color: var(--bs-body-bg);
|
2029
|
-
border-color: var(--bs-border-color-translucent);
|
2030
|
-
}
|
2031
|
-
|
2032
|
-
[data-bs-theme="dark"] #creationFilterOffcanvas .form-control:focus {
|
2033
|
-
background-color: var(--bs-body-bg);
|
2034
|
-
border-color: var(--bs-primary);
|
2035
|
-
}
|
2036
|
-
|
2037
|
-
/* Date range picker styling */
|
2038
|
-
#floatingCreationFilterRange {
|
2039
|
-
cursor: pointer;
|
2040
|
-
background-color: var(--bs-body-bg);
|
2041
|
-
color: var(--bs-body-color);
|
2042
|
-
border: 1px solid var(--bs-border-color);
|
2043
|
-
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, background-color 0.15s ease-in-out;
|
2044
|
-
}
|
2045
|
-
|
2046
|
-
#floatingCreationFilterRange:focus {
|
2047
|
-
border-color: var(--bs-primary);
|
2048
|
-
box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
|
2049
|
-
background-color: var(--bs-body-bg);
|
2050
|
-
}
|
2051
|
-
|
2052
|
-
#floatingCreationFilterRange::placeholder {
|
2053
|
-
color: var(--bs-secondary-color);
|
2054
|
-
opacity: 0.7;
|
2055
|
-
}
|
2056
|
-
|
2057
|
-
/* Enhanced dark mode support for input */
|
2058
|
-
[data-bs-theme="dark"] #floatingCreationFilterRange {
|
2059
|
-
background-color: var(--bs-body-bg);
|
2060
|
-
border-color: var(--bs-border-color-translucent);
|
2061
|
-
color: var(--bs-body-color);
|
2062
|
-
}
|
2063
|
-
|
2064
|
-
[data-bs-theme="dark"] #floatingCreationFilterRange:focus {
|
2065
|
-
background-color: var(--bs-body-bg);
|
2066
|
-
border-color: var(--bs-primary);
|
2067
|
-
box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
|
2068
|
-
}
|
2069
|
-
|
2070
|
-
/* Preset buttons styling */
|
2071
|
-
.preset-btn {
|
2072
|
-
font-size: 0.8rem;
|
2073
|
-
padding: 0.35rem 0.75rem;
|
2074
|
-
text-align: left;
|
2075
|
-
justify-content: flex-start;
|
2076
|
-
border: 1px solid var(--bs-border-color);
|
2077
|
-
background-color: var(--bs-body-bg);
|
2078
|
-
color: var(--bs-body-color);
|
2079
|
-
transition: all 0.2s ease;
|
2080
|
-
position: relative;
|
2081
|
-
overflow: hidden;
|
2082
|
-
}
|
2083
|
-
|
2084
|
-
.preset-btn::before {
|
2085
|
-
content: '';
|
2086
|
-
position: absolute;
|
2087
|
-
top: 0;
|
2088
|
-
left: -100%;
|
2089
|
-
width: 100%;
|
2090
|
-
height: 100%;
|
2091
|
-
background: linear-gradient(90deg, transparent, rgba(var(--bs-primary-rgb), 0.1), transparent);
|
2092
|
-
transition: left 0.3s ease;
|
2093
|
-
}
|
2094
|
-
|
2095
|
-
.preset-btn:hover {
|
2096
|
-
background-color: var(--bs-primary);
|
2097
|
-
color: white;
|
2098
|
-
border-color: var(--bs-primary);
|
2099
|
-
transform: translateY(-1px);
|
2100
|
-
box-shadow: 0 2px 4px rgba(var(--bs-primary-rgb), 0.2);
|
2101
|
-
}
|
2102
|
-
|
2103
|
-
.preset-btn:hover::before {
|
2104
|
-
left: 100%;
|
2105
|
-
}
|
2106
|
-
|
2107
|
-
.preset-btn:active {
|
2108
|
-
transform: translateY(0);
|
2109
|
-
box-shadow: 0 1px 2px rgba(var(--bs-primary-rgb), 0.2);
|
2110
|
-
}
|
2111
|
-
|
2112
|
-
.preset-btn i {
|
2113
|
-
opacity: 0.8;
|
2114
|
-
transition: opacity 0.2s ease;
|
2115
|
-
}
|
2116
|
-
|
2117
|
-
.preset-btn:hover i {
|
2118
|
-
opacity: 1;
|
2119
|
-
}
|
2120
|
-
|
2121
|
-
/* Dark mode enhancements for preset buttons */
|
2122
|
-
[data-bs-theme="dark"] .preset-btn {
|
2123
|
-
background-color: var(--bs-dark);
|
2124
|
-
border-color: var(--bs-border-color-translucent);
|
2125
|
-
color: var(--bs-body-color);
|
2126
|
-
}
|
2127
|
-
|
2128
|
-
[data-bs-theme="dark"] .preset-btn:hover {
|
2129
|
-
background-color: var(--bs-primary);
|
2130
|
-
color: white;
|
2131
|
-
border-color: var(--bs-primary);
|
2132
|
-
box-shadow: 0 2px 8px rgba(var(--bs-primary-rgb), 0.3);
|
2133
|
-
}
|
2134
|
-
|
2135
|
-
/* Flatpickr theme adjustments */
|
2136
|
-
.flatpickr-calendar {
|
2137
|
-
border-radius: 8px;
|
2138
|
-
box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.2);
|
2139
|
-
border: 1px solid var(--bs-border-color);
|
2140
|
-
background: var(--bs-body-bg);
|
2141
|
-
font-family: var(--bs-body-font-family);
|
2142
|
-
}
|
2143
|
-
|
2144
|
-
.flatpickr-months {
|
2145
|
-
background: var(--bs-body-bg);
|
2146
|
-
border-bottom: 1px solid var(--bs-border-color);
|
2147
|
-
border-radius: 8px 8px 0 0;
|
2148
|
-
}
|
2149
|
-
|
2150
|
-
.flatpickr-month {
|
2151
|
-
color: var(--bs-body-color);
|
2152
|
-
fill: var(--bs-body-color);
|
2153
|
-
}
|
2154
|
-
|
2155
|
-
.flatpickr-current-month {
|
2156
|
-
color: var(--bs-body-color);
|
2157
|
-
}
|
2158
|
-
|
2159
|
-
.flatpickr-current-month .flatpickr-monthDropdown-month {
|
2160
|
-
background: var(--bs-body-bg);
|
2161
|
-
color: var(--bs-body-color);
|
2162
|
-
}
|
2163
|
-
|
2164
|
-
.flatpickr-weekdays {
|
2165
|
-
background: var(--bs-body-bg);
|
2166
|
-
}
|
2167
|
-
|
2168
|
-
.flatpickr-weekday {
|
2169
|
-
color: var(--bs-secondary-color);
|
2170
|
-
font-weight: 600;
|
2171
|
-
font-size: 0.75rem;
|
2172
|
-
}
|
2173
|
-
|
2174
|
-
.flatpickr-day {
|
2175
|
-
color: var(--bs-body-color);
|
2176
|
-
border-radius: 4px;
|
2177
|
-
transition: all 0.2s ease;
|
2178
|
-
}
|
2179
|
-
|
2180
|
-
.flatpickr-day:hover {
|
2181
|
-
background: var(--bs-primary-bg-subtle);
|
2182
|
-
color: var(--bs-primary);
|
2183
|
-
border-color: var(--bs-primary-border-subtle);
|
2184
|
-
}
|
2185
|
-
|
2186
|
-
.flatpickr-day.selected {
|
2187
|
-
background: var(--bs-primary);
|
2188
|
-
color: white;
|
2189
|
-
border-color: var(--bs-primary);
|
2190
|
-
box-shadow: 0 2px 4px rgba(var(--bs-primary-rgb), 0.3);
|
2191
|
-
}
|
2192
|
-
|
2193
|
-
.flatpickr-day.selected:hover {
|
2194
|
-
background: var(--bs-primary);
|
2195
|
-
color: white;
|
2196
|
-
}
|
2197
|
-
|
2198
|
-
.flatpickr-day.inRange {
|
2199
|
-
background: var(--bs-primary-bg-subtle);
|
2200
|
-
color: var(--bs-primary);
|
2201
|
-
border-color: transparent;
|
2202
|
-
}
|
2203
|
-
|
2204
|
-
.flatpickr-day.startRange {
|
2205
|
-
background: var(--bs-primary);
|
2206
|
-
color: white;
|
2207
|
-
border-radius: 4px 0 0 4px;
|
2208
|
-
}
|
2209
|
-
|
2210
|
-
.flatpickr-day.endRange {
|
2211
|
-
background: var(--bs-primary);
|
2212
|
-
color: white;
|
2213
|
-
border-radius: 0 4px 4px 0;
|
2214
|
-
}
|
2215
|
-
|
2216
|
-
.flatpickr-day.startRange.endRange {
|
2217
|
-
border-radius: 4px;
|
2218
|
-
}
|
2219
|
-
|
2220
|
-
.flatpickr-day.today {
|
2221
|
-
border-color: var(--bs-primary);
|
2222
|
-
color: var(--bs-primary);
|
2223
|
-
font-weight: 600;
|
2224
|
-
}
|
2225
|
-
|
2226
|
-
.flatpickr-day.today:hover {
|
2227
|
-
background: var(--bs-primary);
|
2228
|
-
color: white;
|
2229
|
-
}
|
2230
|
-
|
2231
|
-
.flatpickr-day.disabled {
|
2232
|
-
color: var(--bs-secondary-color);
|
2233
|
-
opacity: 0.5;
|
2234
|
-
}
|
2235
|
-
|
2236
|
-
.flatpickr-time {
|
2237
|
-
background: var(--bs-body-bg);
|
2238
|
-
border-radius: 0 0 8px 8px;
|
2239
|
-
}
|
2240
|
-
|
2241
|
-
.flatpickr-time input {
|
2242
|
-
background: var(--bs-body-bg);
|
2243
|
-
color: var(--bs-body-color);
|
2244
|
-
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
2245
|
-
}
|
2246
|
-
|
2247
|
-
.flatpickr-time input:focus {
|
2248
|
-
border-color: var(--bs-primary);
|
2249
|
-
box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
|
2250
|
-
outline: 0;
|
2251
|
-
}
|
2252
|
-
|
2253
|
-
.flatpickr-time .flatpickr-time-separator {
|
2254
|
-
color: var(--bs-body-color);
|
2255
|
-
}
|
2256
|
-
|
2257
|
-
.flatpickr-prev-month,
|
2258
|
-
.flatpickr-next-month {
|
2259
|
-
color: var(--bs-body-color);
|
2260
|
-
fill: var(--bs-body-color);
|
2261
|
-
transition: color 0.2s ease;
|
2262
|
-
}
|
2263
|
-
|
2264
|
-
.flatpickr-prev-month:hover,
|
2265
|
-
.flatpickr-next-month:hover {
|
2266
|
-
color: var(--bs-primary);
|
2267
|
-
fill: var(--bs-primary);
|
2268
|
-
}
|
2269
|
-
|
2270
|
-
/* Dark mode specific enhancements */
|
2271
|
-
[data-bs-theme="dark"] .flatpickr-calendar {
|
2272
|
-
background: var(--bs-dark);
|
2273
|
-
border-color: var(--bs-border-color-translucent);
|
2274
|
-
box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.4);
|
2275
|
-
}
|
2276
|
-
|
2277
|
-
[data-bs-theme="dark"] .flatpickr-months {
|
2278
|
-
background: var(--bs-dark);
|
2279
|
-
border-bottom-color: var(--bs-border-color-translucent);
|
2280
|
-
}
|
2281
|
-
|
2282
|
-
[data-bs-theme="dark"] .flatpickr-weekdays {
|
2283
|
-
background: var(--bs-dark);
|
2284
|
-
}
|
2285
|
-
|
2286
|
-
/* Enhanced dark mode day styling for better contrast */
|
2287
|
-
[data-bs-theme="dark"] .flatpickr-day {
|
2288
|
-
color: #e9ecef !important;
|
2289
|
-
background: transparent;
|
2290
|
-
border: 1px solid transparent;
|
2291
|
-
}
|
2292
|
-
|
2293
|
-
[data-bs-theme="dark"] .flatpickr-day:hover {
|
2294
|
-
background: rgba(var(--bs-primary-rgb), 0.25) !important;
|
2295
|
-
color: #ffffff !important;
|
2296
|
-
border-color: rgba(var(--bs-primary-rgb), 0.4);
|
2297
|
-
}
|
2298
|
-
|
2299
|
-
[data-bs-theme="dark"] .flatpickr-day.inRange {
|
2300
|
-
background: rgba(var(--bs-primary-rgb), 0.2) !important;
|
2301
|
-
color: #ffffff !important;
|
2302
|
-
border-color: transparent;
|
2303
|
-
}
|
2304
|
-
|
2305
|
-
[data-bs-theme="dark"] .flatpickr-day.selected {
|
2306
|
-
background: var(--bs-primary) !important;
|
2307
|
-
color: #ffffff !important;
|
2308
|
-
border-color: var(--bs-primary);
|
2309
|
-
box-shadow: 0 2px 6px rgba(var(--bs-primary-rgb), 0.4);
|
2310
|
-
}
|
2311
|
-
|
2312
|
-
[data-bs-theme="dark"] .flatpickr-day.selected:hover {
|
2313
|
-
background: var(--bs-primary) !important;
|
2314
|
-
color: #ffffff !important;
|
2315
|
-
}
|
2316
|
-
|
2317
|
-
[data-bs-theme="dark"] .flatpickr-day.startRange {
|
2318
|
-
background: var(--bs-primary) !important;
|
2319
|
-
color: #ffffff !important;
|
2320
|
-
border-radius: 4px 0 0 4px;
|
2321
|
-
}
|
2322
|
-
|
2323
|
-
[data-bs-theme="dark"] .flatpickr-day.endRange {
|
2324
|
-
background: var(--bs-primary) !important;
|
2325
|
-
color: #ffffff !important;
|
2326
|
-
border-radius: 0 4px 4px 0;
|
2327
|
-
}
|
2328
|
-
|
2329
|
-
[data-bs-theme="dark"] .flatpickr-day.startRange.endRange {
|
2330
|
-
border-radius: 4px;
|
2331
|
-
}
|
2332
|
-
|
2333
|
-
[data-bs-theme="dark"] .flatpickr-day.today {
|
2334
|
-
border-color: var(--bs-primary) !important;
|
2335
|
-
color: var(--bs-primary) !important;
|
2336
|
-
font-weight: 600;
|
2337
|
-
background: rgba(var(--bs-primary-rgb), 0.1);
|
2338
|
-
}
|
2339
|
-
|
2340
|
-
[data-bs-theme="dark"] .flatpickr-day.today:hover {
|
2341
|
-
background: var(--bs-primary) !important;
|
2342
|
-
color: #ffffff !important;
|
2343
|
-
}
|
2344
|
-
|
2345
|
-
[data-bs-theme="dark"] .flatpickr-day.disabled {
|
2346
|
-
color: #6c757d !important;
|
2347
|
-
opacity: 0.4;
|
2348
|
-
background: transparent !important;
|
2349
|
-
}
|
2350
|
-
|
2351
|
-
/* Dark mode other day states */
|
2352
|
-
[data-bs-theme="dark"] .flatpickr-day.nextMonthDay,
|
2353
|
-
[data-bs-theme="dark"] .flatpickr-day.prevMonthDay {
|
2354
|
-
color: #6c757d !important;
|
2355
|
-
opacity: 0.6;
|
2356
|
-
}
|
2357
|
-
|
2358
|
-
[data-bs-theme="dark"] .flatpickr-day.nextMonthDay:hover,
|
2359
|
-
[data-bs-theme="dark"] .flatpickr-day.prevMonthDay:hover {
|
2360
|
-
background: rgba(var(--bs-primary-rgb), 0.15) !important;
|
2361
|
-
color: #adb5bd !important;
|
2362
|
-
}
|
2363
|
-
|
2364
|
-
[data-bs-theme="dark"] .flatpickr-time {
|
2365
|
-
background: var(--bs-dark);
|
2366
|
-
border-top-color: var(--bs-border-color-translucent);
|
2367
|
-
}
|
2368
|
-
|
2369
|
-
[data-bs-theme="dark"] .flatpickr-time input {
|
2370
|
-
background: var(--bs-body-bg);
|
2371
|
-
border-color: var(--bs-border-color-translucent);
|
2372
|
-
color: #e9ecef !important;
|
2373
|
-
}
|
2374
|
-
|
2375
|
-
.flatpickr-calendar.hasTime .flatpickr-time {
|
2376
|
-
border-top: var(--bs-border-color-translucent);
|
2377
|
-
}
|
2378
|
-
|
2379
|
-
.flatpickr-next-month {
|
2380
|
-
color: var(--bs-body-color);
|
2381
|
-
fill: var(--bs-body-color);
|
2382
|
-
}
|
2383
|
-
|
2384
|
-
span.flatpickr-weekday {
|
2385
|
-
color: var(--bs-secondary-color);
|
2386
|
-
font-weight: 600;
|
2387
|
-
font-size: 0.75rem;
|
2388
|
-
}
|
2389
|
-
|
2390
|
-
[data-bs-theme="dark"] .flatpickr-time input:focus {
|
2391
|
-
border-color: var(--bs-primary);
|
2392
|
-
background: var(--bs-body-bg);
|
2393
|
-
color: #ffffff !important;
|
2394
|
-
}
|
2395
|
-
|
2396
|
-
[data-bs-theme="dark"] .flatpickr-time .flatpickr-time-separator {
|
2397
|
-
color: #e9ecef !important;
|
2398
|
-
}
|
2399
|
-
|
2400
|
-
/* Better focus states */
|
2401
|
-
.flatpickr-day:focus {
|
2402
|
-
outline: 2px solid var(--bs-primary);
|
2403
|
-
outline-offset: -2px;
|
2404
|
-
z-index: 10;
|
2405
|
-
}
|
2406
|
-
|
2407
|
-
/* Animation for calendar appearance */
|
2408
|
-
.flatpickr-calendar.open {
|
2409
|
-
animation: flatpickr-slideDown 0.2s ease-out;
|
2410
|
-
}
|
2411
|
-
|
2412
|
-
@keyframes flatpickr-slideDown {
|
2413
|
-
from {
|
2414
|
-
opacity: 0;
|
2415
|
-
transform: translateY(-10px) scale(0.98);
|
2416
|
-
}
|
2417
|
-
to {
|
2418
|
-
opacity: 1;
|
2419
|
-
transform: translateY(0) scale(1);
|
2420
|
-
}
|
2421
|
-
}
|
2422
|
-
|
2423
|
-
/* Small alert styling */
|
2424
|
-
.alert-sm {
|
2425
|
-
padding: 0.5rem;
|
2426
|
-
font-size: 0.875rem;
|
2427
|
-
}
|
2428
|
-
|
2429
|
-
/* Responsive adjustments - hide on smaller screens */
|
2430
|
-
@media (max-width: 991.98px) {
|
2431
|
-
.floating-creation-filter {
|
2432
|
-
display: none !important;
|
2433
|
-
}
|
2434
|
-
}
|
2435
|
-
|
2436
|
-
/* Flatpickr positioning and responsive adjustments */
|
2437
|
-
.flatpickr-calendar.arrowTop:before,
|
2438
|
-
.flatpickr-calendar.arrowTop:after {
|
2439
|
-
border-bottom-color: var(--bs-border-color);
|
2440
|
-
}
|
2441
|
-
|
2442
|
-
.flatpickr-calendar.arrowBottom:before,
|
2443
|
-
.flatpickr-calendar.arrowBottom:after {
|
2444
|
-
border-top-color: var(--bs-border-color);
|
2445
|
-
}
|
2446
|
-
|
2447
|
-
[data-bs-theme="dark"] .flatpickr-calendar.arrowTop:before,
|
2448
|
-
[data-bs-theme="dark"] .flatpickr-calendar.arrowTop:after {
|
2449
|
-
border-bottom-color: var(--bs-border-color-translucent);
|
2450
|
-
}
|
2451
|
-
|
2452
|
-
[data-bs-theme="dark"] .flatpickr-calendar.arrowBottom:before,
|
2453
|
-
[data-bs-theme="dark"] .flatpickr-calendar.arrowBottom:after {
|
2454
|
-
border-top-color: var(--bs-border-color-translucent);
|
2455
|
-
}
|
2456
|
-
|
2457
|
-
/* Ensure calendar stays within viewport on mobile */
|
2458
|
-
@media (max-width: 576px) {
|
2459
|
-
.flatpickr-calendar {
|
2460
|
-
max-width: calc(100vw - 20px);
|
2461
|
-
font-size: 14px;
|
2462
|
-
}
|
2463
|
-
|
2464
|
-
.flatpickr-day {
|
2465
|
-
height: 35px;
|
2466
|
-
line-height: 35px;
|
2467
|
-
}
|
2468
|
-
|
2469
|
-
.flatpickr-time input {
|
2470
|
-
font-size: 14px;
|
2471
|
-
}
|
2472
|
-
}
|
2473
|
-
|
2474
|
-
/* Improved accessibility */
|
2475
|
-
.flatpickr-calendar {
|
2476
|
-
font-family: inherit;
|
2477
|
-
}
|
2478
|
-
|
2479
|
-
.flatpickr-day[aria-label] {
|
2480
|
-
position: relative;
|
2481
|
-
}
|
2482
|
-
|
2483
|
-
/* Smooth scroll behavior for time inputs */
|
2484
|
-
.flatpickr-time input {
|
2485
|
-
scroll-behavior: smooth;
|
2486
|
-
}
|
2487
|
-
|
2488
|
-
/* Enhanced visual feedback for interactive elements */
|
2489
|
-
.flatpickr-prev-month,
|
2490
|
-
.flatpickr-next-month {
|
2491
|
-
border-radius: 4px;
|
2492
|
-
padding: 4px;
|
2493
|
-
margin: 2px;
|
2494
|
-
}
|
2495
|
-
|
2496
|
-
.flatpickr-prev-month:hover,
|
2497
|
-
.flatpickr-next-month:hover {
|
2498
|
-
background: rgba(var(--bs-primary-rgb), 0.1);
|
2499
|
-
}
|
2500
|
-
|
2501
|
-
/* Relationship count styling */
|
2502
|
-
.relationship-count .badge {
|
2503
|
-
min-width: 80px;
|
2504
|
-
display: inline-flex;
|
2505
|
-
align-items: center;
|
2506
|
-
justify-content: center;
|
2507
|
-
}
|
2508
|
-
|
2509
|
-
.relationship-count .spinner-border-sm {
|
2510
|
-
width: 0.875rem;
|
2511
|
-
height: 0.875rem;
|
2512
|
-
}
|
2513
|
-
</style>
|
2514
|
-
|
2515
|
-
<script>
|
2516
|
-
document.addEventListener('DOMContentLoaded', function() {
|
2517
|
-
// Initialize Flatpickr date range picker
|
2518
|
-
const dateRangeInput = document.getElementById('floatingCreationFilterRange');
|
2519
|
-
const startHidden = document.getElementById('creation_filter_start');
|
2520
|
-
const endHidden = document.getElementById('creation_filter_end');
|
2521
|
-
|
2522
|
-
if (dateRangeInput && typeof flatpickr !== 'undefined') {
|
2523
|
-
console.log('Flatpickr library loaded, initializing date range picker');
|
2524
|
-
// Store the Flatpickr instance in a variable accessible to all handlers
|
2525
|
-
let fp;
|
2526
|
-
|
2527
|
-
// Function to initialize Flatpickr
|
2528
|
-
function initializeFlatpickr(theme) {
|
2529
|
-
// Determine theme based on current document theme or passed parameter
|
2530
|
-
const currentTheme = theme || (document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light');
|
2531
|
-
|
2532
|
-
const config = {
|
2533
|
-
mode: 'range',
|
2534
|
-
enableTime: true,
|
2535
|
-
dateFormat: 'Y-m-d H:i',
|
2536
|
-
time_24hr: true,
|
2537
|
-
allowInput: false,
|
2538
|
-
clickOpens: true,
|
2539
|
-
theme: currentTheme,
|
2540
|
-
animate: true,
|
2541
|
-
position: 'auto',
|
2542
|
-
static: false,
|
2543
|
-
appendTo: document.body, // Ensure it renders above other elements
|
2544
|
-
locale: {
|
2545
|
-
rangeSeparator: ' to ',
|
2546
|
-
weekdays: {
|
2547
|
-
shorthand: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
|
2548
|
-
longhand: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
2549
|
-
},
|
2550
|
-
months: {
|
2551
|
-
shorthand: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
2552
|
-
longhand: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
|
2553
|
-
}
|
2554
|
-
},
|
2555
|
-
onOpen: function(selectedDates, dateStr, instance) {
|
2556
|
-
// Add a slight delay to apply theme-specific styling after calendar opens
|
2557
|
-
setTimeout(() => {
|
2558
|
-
const calendar = instance.calendarContainer;
|
2559
|
-
if (calendar) {
|
2560
|
-
// Apply theme-specific class for additional styling control
|
2561
|
-
calendar.classList.add(`flatpickr-${currentTheme}`);
|
2562
|
-
|
2563
|
-
// Ensure proper z-index for offcanvas overlay
|
2564
|
-
calendar.style.zIndex = '1070';
|
2565
|
-
|
2566
|
-
// Add elegant entrance animation
|
2567
|
-
calendar.classList.add('open');
|
2568
|
-
}
|
2569
|
-
}, 10);
|
2570
|
-
},
|
2571
|
-
onClose: function(selectedDates, dateStr, instance) {
|
2572
|
-
const calendar = instance.calendarContainer;
|
2573
|
-
if (calendar) {
|
2574
|
-
calendar.classList.remove('open');
|
2575
|
-
}
|
2576
|
-
},
|
2577
|
-
onChange: function(selectedDates, dateStr, instance) {
|
2578
|
-
console.log('Date range changed:', selectedDates);
|
2579
|
-
|
2580
|
-
if (selectedDates.length === 2) {
|
2581
|
-
// Format dates for hidden inputs (Rails expects ISO format)
|
2582
|
-
startHidden.value = selectedDates[0].toISOString().slice(0, 16);
|
2583
|
-
endHidden.value = selectedDates[1].toISOString().slice(0, 16);
|
2584
|
-
|
2585
|
-
// Update display with elegant formatting
|
2586
|
-
const formatOptions = {
|
2587
|
-
year: 'numeric',
|
2588
|
-
month: 'short',
|
2589
|
-
day: 'numeric',
|
2590
|
-
hour: '2-digit',
|
2591
|
-
minute: '2-digit',
|
2592
|
-
hour12: false
|
2593
|
-
};
|
2594
|
-
|
2595
|
-
const startFormatted = selectedDates[0].toLocaleDateString('en-US', formatOptions);
|
2596
|
-
const endFormatted = selectedDates[1].toLocaleDateString('en-US', formatOptions);
|
2597
|
-
dateRangeInput.value = `${startFormatted} to ${endFormatted}`;
|
2598
|
-
|
2599
|
-
} else if (selectedDates.length === 1) {
|
2600
|
-
startHidden.value = selectedDates[0].toISOString().slice(0, 16);
|
2601
|
-
endHidden.value = '';
|
2602
|
-
|
2603
|
-
const formatOptions = {
|
2604
|
-
year: 'numeric',
|
2605
|
-
month: 'short',
|
2606
|
-
day: 'numeric',
|
2607
|
-
hour: '2-digit',
|
2608
|
-
minute: '2-digit',
|
2609
|
-
hour12: false
|
2610
|
-
};
|
2611
|
-
|
2612
|
-
const startFormatted = selectedDates[0].toLocaleDateString('en-US', formatOptions);
|
2613
|
-
dateRangeInput.value = `${startFormatted} (select end date)`;
|
2614
|
-
|
2615
|
-
} else {
|
2616
|
-
startHidden.value = '';
|
2617
|
-
endHidden.value = '';
|
2618
|
-
dateRangeInput.value = '';
|
2619
|
-
}
|
2620
|
-
}
|
2621
|
-
};
|
2622
|
-
|
2623
|
-
return flatpickr(dateRangeInput, config);
|
2624
|
-
}
|
2625
|
-
|
2626
|
-
// Initialize date range picker
|
2627
|
-
fp = initializeFlatpickr();
|
2628
|
-
|
2629
|
-
// Set initial values if they exist
|
2630
|
-
if (startHidden.value || endHidden.value) {
|
2631
|
-
const dates = [];
|
2632
|
-
if (startHidden.value) {
|
2633
|
-
dates.push(new Date(startHidden.value));
|
2634
|
-
}
|
2635
|
-
if (endHidden.value) {
|
2636
|
-
dates.push(new Date(endHidden.value));
|
2637
|
-
}
|
2638
|
-
fp.setDate(dates);
|
2639
|
-
}
|
2640
|
-
|
2641
|
-
// Preset button functionality
|
2642
|
-
const presetButtons = document.querySelectorAll('.preset-btn');
|
2643
|
-
presetButtons.forEach(button => {
|
2644
|
-
button.addEventListener('click', function(event) {
|
2645
|
-
event.preventDefault(); // Prevent any form submission
|
2646
|
-
|
2647
|
-
const preset = this.getAttribute('data-preset');
|
2648
|
-
const now = new Date();
|
2649
|
-
let startDate, endDate;
|
2650
|
-
|
2651
|
-
console.log('Preset button clicked:', preset); // Debug log
|
2652
|
-
|
2653
|
-
switch (preset) {
|
2654
|
-
case 'lastminute':
|
2655
|
-
startDate = new Date(now);
|
2656
|
-
startDate.setMinutes(startDate.getMinutes() - 1);
|
2657
|
-
endDate = new Date(now);
|
2658
|
-
break;
|
2659
|
-
case 'last5minutes':
|
2660
|
-
startDate = new Date(now);
|
2661
|
-
startDate.setMinutes(startDate.getMinutes() - 5);
|
2662
|
-
endDate = new Date(now);
|
2663
|
-
break;
|
2664
|
-
case 'today':
|
2665
|
-
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
|
2666
|
-
endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
|
2667
|
-
break;
|
2668
|
-
case 'yesterday':
|
2669
|
-
const yesterday = new Date(now);
|
2670
|
-
yesterday.setDate(yesterday.getDate() - 1);
|
2671
|
-
startDate = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate(), 0, 0, 0);
|
2672
|
-
endDate = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate(), 23, 59, 59);
|
2673
|
-
break;
|
2674
|
-
case 'last7days':
|
2675
|
-
startDate = new Date(now);
|
2676
|
-
startDate.setDate(startDate.getDate() - 7);
|
2677
|
-
startDate.setHours(0, 0, 0, 0);
|
2678
|
-
endDate = new Date(now);
|
2679
|
-
endDate.setHours(23, 59, 59, 999);
|
2680
|
-
break;
|
2681
|
-
case 'last30days':
|
2682
|
-
startDate = new Date(now);
|
2683
|
-
startDate.setDate(startDate.getDate() - 30);
|
2684
|
-
startDate.setHours(0, 0, 0, 0);
|
2685
|
-
endDate = new Date(now);
|
2686
|
-
endDate.setHours(23, 59, 59, 999);
|
2687
|
-
break;
|
2688
|
-
case 'thismonth':
|
2689
|
-
startDate = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0);
|
2690
|
-
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
|
2691
|
-
break;
|
2692
|
-
}
|
2693
|
-
|
2694
|
-
if (startDate && endDate && fp) {
|
2695
|
-
console.log('Setting dates:', startDate, endDate); // Debug log
|
2696
|
-
fp.setDate([startDate, endDate]);
|
2697
|
-
|
2698
|
-
// Also update the hidden inputs directly as a fallback
|
2699
|
-
startHidden.value = startDate.toISOString().slice(0, 16);
|
2700
|
-
endHidden.value = endDate.toISOString().slice(0, 16);
|
2701
|
-
|
2702
|
-
// Update the display value
|
2703
|
-
const formattedStart = startDate.toLocaleDateString() + ' ' + startDate.toLocaleTimeString();
|
2704
|
-
const formattedEnd = endDate.toLocaleDateString() + ' ' + endDate.toLocaleTimeString();
|
2705
|
-
dateRangeInput.value = formattedStart + ' to ' + formattedEnd;
|
2706
|
-
} else {
|
2707
|
-
console.error('Failed to set dates - startDate:', startDate, 'endDate:', endDate, 'fp:', fp);
|
2708
|
-
}
|
2709
|
-
});
|
2710
|
-
});
|
2711
|
-
|
2712
|
-
// Listen for theme changes and update Flatpickr theme
|
2713
|
-
document.addEventListener('dbviewerThemeChanged', function(e) {
|
2714
|
-
const newTheme = e.detail.theme === 'dark' ? 'dark' : 'light';
|
2715
|
-
console.log('Theme changed to:', newTheme);
|
2716
|
-
|
2717
|
-
// Destroy and recreate with new theme
|
2718
|
-
if (fp) {
|
2719
|
-
const currentDates = fp.selectedDates;
|
2720
|
-
fp.destroy();
|
2721
|
-
fp = initializeFlatpickr(newTheme);
|
2722
|
-
|
2723
|
-
// Restore previous values if they existed
|
2724
|
-
if (currentDates && currentDates.length > 0) {
|
2725
|
-
fp.setDate(currentDates);
|
2726
|
-
}
|
2727
|
-
}
|
2728
|
-
});
|
2729
|
-
|
2730
|
-
// Also listen for direct data-bs-theme attribute changes using MutationObserver
|
2731
|
-
const themeObserver = new MutationObserver(function(mutations) {
|
2732
|
-
mutations.forEach(function(mutation) {
|
2733
|
-
if (mutation.type === 'attributes' && mutation.attributeName === 'data-bs-theme') {
|
2734
|
-
const newTheme = document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light';
|
2735
|
-
console.log('Theme attribute changed to:', newTheme);
|
2736
|
-
|
2737
|
-
if (fp) {
|
2738
|
-
const currentDates = fp.selectedDates;
|
2739
|
-
fp.destroy();
|
2740
|
-
fp = initializeFlatpickr(newTheme);
|
2741
|
-
|
2742
|
-
// Restore previous values if they existed
|
2743
|
-
if (currentDates && currentDates.length > 0) {
|
2744
|
-
fp.setDate(currentDates);
|
2745
|
-
}
|
2746
|
-
}
|
2747
|
-
}
|
2748
|
-
});
|
2749
|
-
});
|
2750
|
-
|
2751
|
-
// Start observing theme changes
|
2752
|
-
themeObserver.observe(document.documentElement, {
|
2753
|
-
attributes: true,
|
2754
|
-
attributeFilter: ['data-bs-theme']
|
2755
|
-
});
|
2756
|
-
} else {
|
2757
|
-
console.error('Date range picker initialization failed:', {
|
2758
|
-
dateRangeInput: !!dateRangeInput,
|
2759
|
-
flatpickr: typeof flatpickr !== 'undefined'
|
2760
|
-
});
|
2761
|
-
}
|
2762
|
-
|
2763
|
-
// Close offcanvas after form submission
|
2764
|
-
const form = document.getElementById('floatingCreationFilterForm');
|
2765
|
-
if (form) {
|
2766
|
-
form.addEventListener('submit', function() {
|
2767
|
-
const offcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('creationFilterOffcanvas'));
|
2768
|
-
if (offcanvas) {
|
2769
|
-
setTimeout(() => {
|
2770
|
-
offcanvas.hide();
|
2771
|
-
}, 100);
|
2772
|
-
}
|
2773
|
-
});
|
2774
|
-
}
|
2775
|
-
});
|
2776
|
-
</script>
|
2777
350
|
<% end %>
|
351
|
+
<input type="hidden" id="mini_erd_table_path" name="mini_erd_table_path" value="<%= dbviewer.mini_erd_table_path(@table_name, format: :json) %>">
|
352
|
+
<input type="hidden" id="table_name" name="table_name" value="<%= @table_name %>">
|