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.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +553 -0
  3. data/app/assets/javascripts/dbviewer/home.js +287 -0
  4. data/app/assets/javascripts/dbviewer/layout.js +194 -0
  5. data/app/assets/javascripts/dbviewer/query.js +277 -0
  6. data/app/assets/javascripts/dbviewer/table.js +1563 -0
  7. data/app/assets/stylesheets/dbviewer/application.css +1460 -21
  8. data/app/assets/stylesheets/dbviewer/entity_relationship_diagram.css +181 -0
  9. data/app/assets/stylesheets/dbviewer/home.css +229 -0
  10. data/app/assets/stylesheets/dbviewer/logs.css +64 -0
  11. data/app/assets/stylesheets/dbviewer/query.css +171 -0
  12. data/app/assets/stylesheets/dbviewer/table.css +1144 -0
  13. data/app/views/dbviewer/connections/index.html.erb +0 -30
  14. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +14 -713
  15. data/app/views/dbviewer/home/index.html.erb +9 -499
  16. data/app/views/dbviewer/logs/index.html.erb +5 -220
  17. data/app/views/dbviewer/tables/index.html.erb +0 -65
  18. data/app/views/dbviewer/tables/query.html.erb +129 -565
  19. data/app/views/dbviewer/tables/show.html.erb +4 -2429
  20. data/app/views/layouts/dbviewer/application.html.erb +13 -1544
  21. data/lib/dbviewer/version.rb +1 -1
  22. metadata +12 -7
  23. data/app/assets/javascripts/dbviewer/connections.js +0 -70
  24. data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
  25. data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
  26. data/app/views/dbviewer/connections/new.html.erb +0 -79
  27. data/app/views/dbviewer/tables/mini_erd.html.erb +0 -517
@@ -0,0 +1,1563 @@
1
+ document.addEventListener("DOMContentLoaded", function () {
2
+ const tableName = document.getElementById("table_name").value;
3
+
4
+ // Record Detail Modal functionality
5
+ const recordDetailModal = document.getElementById("recordDetailModal");
6
+ if (recordDetailModal) {
7
+ recordDetailModal.addEventListener("show.bs.modal", function (event) {
8
+ // Button that triggered the modal
9
+ const button = event.relatedTarget;
10
+
11
+ // Extract record data from button's data attribute
12
+ let recordData;
13
+ let foreignKeys;
14
+ try {
15
+ recordData = JSON.parse(button.getAttribute("data-record-data"));
16
+ foreignKeys = JSON.parse(
17
+ button.getAttribute("data-foreign-keys") || "[]"
18
+ );
19
+ } catch (e) {
20
+ console.error("Error parsing record data:", e);
21
+ recordData = {};
22
+ foreignKeys = [];
23
+ }
24
+
25
+ // Update the modal's title with table name
26
+ const modalTitle = recordDetailModal.querySelector(".modal-title");
27
+ modalTitle.textContent = `${tableName} Record Details`;
28
+
29
+ // Populate the table with record data
30
+ const tableBody = document.getElementById("recordDetailTableBody");
31
+ tableBody.innerHTML = "";
32
+
33
+ // Get all columns
34
+ const columns = Object.keys(recordData);
35
+
36
+ // Create rows for each column
37
+ columns.forEach((column) => {
38
+ const row = document.createElement("tr");
39
+
40
+ // Create column name cell
41
+ const columnNameCell = document.createElement("td");
42
+ columnNameCell.className = "fw-bold";
43
+ columnNameCell.textContent = column;
44
+ row.appendChild(columnNameCell);
45
+
46
+ // Create value cell
47
+ const valueCell = document.createElement("td");
48
+ let cellValue = recordData[column];
49
+
50
+ // Format value differently based on type
51
+ if (cellValue === null) {
52
+ valueCell.innerHTML = '<span class="text-muted">NULL</span>';
53
+ } else if (
54
+ typeof cellValue === "string" &&
55
+ cellValue.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
56
+ ) {
57
+ // Handle datetime values
58
+ const date = new Date(cellValue);
59
+ if (!isNaN(date.getTime())) {
60
+ valueCell.textContent = date.toLocaleString();
61
+ } else {
62
+ valueCell.textContent = cellValue;
63
+ }
64
+ } else if (
65
+ typeof cellValue === "string" &&
66
+ (cellValue.startsWith("{") || cellValue.startsWith("["))
67
+ ) {
68
+ // Handle JSON values
69
+ try {
70
+ const jsonValue = JSON.parse(cellValue);
71
+ const formattedJSON = JSON.stringify(jsonValue, null, 2);
72
+ valueCell.innerHTML = `<pre class="mb-0 code-block">${formattedJSON}</pre>`;
73
+ } catch (e) {
74
+ valueCell.textContent = cellValue;
75
+ }
76
+ } else {
77
+ valueCell.textContent = cellValue;
78
+ }
79
+
80
+ row.appendChild(valueCell);
81
+ tableBody.appendChild(row);
82
+ });
83
+
84
+ // Populate relationships section
85
+ const relationshipsSection = document.getElementById(
86
+ "relationshipsSection"
87
+ );
88
+ const relationshipsContent = document.getElementById(
89
+ "relationshipsContent"
90
+ );
91
+ const reverseForeignKeys = JSON.parse(
92
+ button.dataset.reverseForeignKeys || "[]"
93
+ );
94
+
95
+ // Check if we have any relationships to show
96
+ const hasRelationships =
97
+ (foreignKeys && foreignKeys.length > 0) ||
98
+ (reverseForeignKeys && reverseForeignKeys.length > 0);
99
+
100
+ if (hasRelationships) {
101
+ relationshipsSection.style.display = "block";
102
+ relationshipsContent.innerHTML = "";
103
+
104
+ // Handle belongs_to relationships (foreign keys from this table)
105
+ if (foreignKeys && foreignKeys.length > 0) {
106
+ const activeRelationships = foreignKeys.filter((fk) => {
107
+ const columnValue = recordData[fk.column];
108
+ return (
109
+ columnValue !== null &&
110
+ columnValue !== undefined &&
111
+ columnValue !== ""
112
+ );
113
+ });
114
+
115
+ if (activeRelationships.length > 0) {
116
+ relationshipsContent.appendChild(
117
+ createRelationshipSection(
118
+ "Belongs To",
119
+ activeRelationships,
120
+ recordData,
121
+ "belongs_to"
122
+ )
123
+ );
124
+ }
125
+ }
126
+
127
+ // Handle has_many relationships (foreign keys from other tables pointing to this table)
128
+ if (reverseForeignKeys && reverseForeignKeys.length > 0) {
129
+ const primaryKeyValue =
130
+ recordData[
131
+ Object.keys(recordData).find((key) => key === "id") ||
132
+ Object.keys(recordData)[0]
133
+ ];
134
+
135
+ if (
136
+ primaryKeyValue !== null &&
137
+ primaryKeyValue !== undefined &&
138
+ primaryKeyValue !== ""
139
+ ) {
140
+ const hasManySection = createRelationshipSection(
141
+ "Has Many",
142
+ reverseForeignKeys,
143
+ recordData,
144
+ "has_many",
145
+ primaryKeyValue
146
+ );
147
+ relationshipsContent.appendChild(hasManySection);
148
+
149
+ // Fetch relationship counts asynchronously
150
+ fetchRelationshipCounts(
151
+ `${tableName}`,
152
+ primaryKeyValue,
153
+ reverseForeignKeys,
154
+ hasManySection
155
+ );
156
+ }
157
+ }
158
+
159
+ // Show message if no active relationships
160
+ if (relationshipsContent.children.length === 0) {
161
+ relationshipsContent.innerHTML = `
162
+ <div class="text-muted small">
163
+ <i class="bi bi-info-circle me-1"></i>
164
+ This record has no active relationships.
165
+ </div>
166
+ `;
167
+ }
168
+ } else {
169
+ relationshipsSection.style.display = "none";
170
+ }
171
+ });
172
+ }
173
+
174
+ // Column filter functionality
175
+ const columnFilters = document.querySelectorAll(".column-filter");
176
+ const operatorSelects = document.querySelectorAll(".operator-select");
177
+ const filterForm = document.getElementById("column-filters-form");
178
+
179
+ // Add debounce function to reduce form submissions
180
+ function debounce(func, wait) {
181
+ let timeout;
182
+ return function () {
183
+ const context = this;
184
+ const args = arguments;
185
+ clearTimeout(timeout);
186
+ timeout = setTimeout(function () {
187
+ func.apply(context, args);
188
+ }, wait);
189
+ };
190
+ }
191
+
192
+ // Function to handle operator changes for IS NULL and IS NOT NULL operators
193
+ function setupNullOperators() {
194
+ operatorSelects.forEach((select) => {
195
+ // Initial setup for existing null operators
196
+ if (select.value === "is_null" || select.value === "is_not_null") {
197
+ const columnName = select.name.match(/\[(.*?)_operator\]/)[1];
198
+ const inputContainer = select.closest(".filter-input-group");
199
+ // Check for display field (the visible disabled field)
200
+ const displayField = inputContainer.querySelector(
201
+ `[data-column="${columnName}_display"]`
202
+ );
203
+ if (displayField) {
204
+ displayField.classList.add("disabled-filter");
205
+ }
206
+
207
+ // Make sure the value field properly reflects the null operator
208
+ const valueField = inputContainer.querySelector(
209
+ `[data-column="${columnName}"]`
210
+ );
211
+ if (valueField) {
212
+ valueField.value = select.value;
213
+ }
214
+ }
215
+
216
+ // Handle operator changes
217
+ select.addEventListener("change", function () {
218
+ const columnName = this.name.match(/\[(.*?)_operator\]/)[1];
219
+ const filterForm = this.closest("form");
220
+ const inputContainer = this.closest(".filter-input-group");
221
+ const hiddenField = inputContainer.querySelector(
222
+ `[data-column="${columnName}"]`
223
+ );
224
+ const displayField = inputContainer.querySelector(
225
+ `[data-column="${columnName}_display"]`
226
+ );
227
+ const wasNullOperator =
228
+ hiddenField &&
229
+ (hiddenField.value === "is_null" ||
230
+ hiddenField.value === "is_not_null");
231
+ const isNullOperator =
232
+ this.value === "is_null" || this.value === "is_not_null";
233
+
234
+ if (isNullOperator) {
235
+ // Configure for null operator
236
+ if (hiddenField) {
237
+ hiddenField.value = this.value;
238
+ }
239
+ // Submit immediately
240
+ filterForm.submit();
241
+ } else if (wasNullOperator) {
242
+ // Clear value when switching from null operator to regular operator
243
+ if (hiddenField) {
244
+ hiddenField.value = "";
245
+ }
246
+ }
247
+ });
248
+ });
249
+ }
250
+
251
+ // Function to submit the form
252
+ const submitForm = debounce(function () {
253
+ filterForm.submit();
254
+ }, 500);
255
+
256
+ // Initialize the null operators handling
257
+ setupNullOperators();
258
+
259
+ // Add event listeners to all filter inputs
260
+ columnFilters.forEach(function (filter) {
261
+ // For text fields use input event
262
+ filter.addEventListener("input", submitForm);
263
+
264
+ // For date/time fields also use change event since they have calendar/time pickers
265
+ if (
266
+ filter.type === "date" ||
267
+ filter.type === "datetime-local" ||
268
+ filter.type === "time"
269
+ ) {
270
+ filter.addEventListener("change", submitForm);
271
+ }
272
+ });
273
+
274
+ // Add event listeners to operator selects
275
+ operatorSelects.forEach(function (select) {
276
+ select.addEventListener("change", submitForm);
277
+ });
278
+
279
+ // Add clear button functionality if there are any filters applied
280
+ const hasActiveFilters = Array.from(columnFilters).some(
281
+ (input) => input.value
282
+ );
283
+
284
+ if (hasActiveFilters) {
285
+ // Add a clear filters button
286
+ const paginationContainer =
287
+ document.querySelector('nav[aria-label="Page navigation"]') ||
288
+ document.querySelector(".table-responsive");
289
+
290
+ if (paginationContainer) {
291
+ const clearButton = document.createElement("div");
292
+ clearButton.className = "text-center mt-3";
293
+ clearButton.innerHTML =
294
+ '<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-filters">' +
295
+ '<i class="bi bi-x-circle me-1"></i>Clear All Filters</button>';
296
+
297
+ paginationContainer.insertAdjacentHTML("afterend", clearButton.outerHTML);
298
+
299
+ document
300
+ .getElementById("clear-filters")
301
+ .addEventListener("click", function () {
302
+ // Reset all input values
303
+ columnFilters.forEach((filter) => (filter.value = ""));
304
+
305
+ // Reset operator selects to their default values
306
+ operatorSelects.forEach((select) => {
307
+ // Find the first option of the select (usually the default)
308
+ if (select.options.length > 0) {
309
+ select.selectedIndex = 0;
310
+ }
311
+ });
312
+
313
+ submitForm();
314
+ });
315
+ }
316
+ }
317
+
318
+ // Load Mini ERD when modal is opened
319
+ const miniErdModal = document.getElementById("miniErdModal");
320
+ if (miniErdModal) {
321
+ let isModalLoaded = false;
322
+ let erdData = null;
323
+
324
+ miniErdModal.addEventListener("show.bs.modal", function (event) {
325
+ const modalContent = document.getElementById("miniErdModalContent");
326
+
327
+ // Set loading state
328
+ modalContent.innerHTML = `
329
+ <div class="modal-header">
330
+ <h5 class="modal-title">Relationships for ${tableName}</h5>
331
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
332
+ </div>
333
+ <div class="modal-body p-0">
334
+ <div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
335
+ <div class="text-center">
336
+ <div class="spinner-border text-primary mb-3" role="status">
337
+ <span class="visually-hidden">Loading...</span>
338
+ </div>
339
+ <p class="mt-2">Loading relationships diagram...</p>
340
+ <small class="text-muted">This may take a moment for tables with many relationships</small>
341
+ </div>
342
+ </div>
343
+ </div>
344
+ `;
345
+
346
+ // Always fetch fresh data when modal is opened
347
+ fetchErdData();
348
+ });
349
+
350
+ // Function to fetch ERD data
351
+ function fetchErdData() {
352
+ // Add cache-busting timestamp to prevent browser caching
353
+ const cacheBuster = new Date().getTime();
354
+ const pathElement = document.getElementById("mini_erd_table_path");
355
+ const fetchUrl = `${pathElement.value}?_=${cacheBuster}`;
356
+
357
+ fetch(fetchUrl)
358
+ .then((response) => {
359
+ if (!response.ok) {
360
+ throw new Error(
361
+ `Server returned ${response.status} ${response.statusText}`
362
+ );
363
+ }
364
+ return response.json(); // Parse as JSON instead of text
365
+ })
366
+ .then((data) => {
367
+ isModalLoaded = true;
368
+ erdData = data; // Store the data
369
+ renderMiniErd(data);
370
+ })
371
+ .catch((error) => {
372
+ console.error("Error loading mini ERD:", error);
373
+ showErdError(error);
374
+ });
375
+ }
376
+
377
+ // Function to show error modal
378
+ function showErdError(error) {
379
+ const modalContent = document.getElementById("miniErdModalContent");
380
+ modalContent.innerHTML = `
381
+ <div class="modal-header">
382
+ <h5 class="modal-title">Relationships for ${tableName}</h5>
383
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
384
+ </div>
385
+ <div class="modal-body p-0">
386
+ <div class="alert alert-danger m-3">
387
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
388
+ <strong>Error loading relationship diagram</strong>
389
+ <p class="mt-2 mb-0">${error.message}</p>
390
+ </div>
391
+ <div class="m-3">
392
+ <p><strong>Debug Information:</strong></p>
393
+ <p class="mt-3">
394
+ <button class="btn btn-sm btn-primary" onclick="retryLoadingMiniERD()">
395
+ <i class="bi bi-arrow-clockwise me-1"></i> Retry
396
+ </button>
397
+ </p>
398
+ </div>
399
+ </div>
400
+ <div class="modal-footer">
401
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
402
+ </div>
403
+ `;
404
+ }
405
+
406
+ // Function to render the ERD with Mermaid
407
+ function renderMiniErd(data) {
408
+ const modalContent = document.getElementById("miniErdModalContent");
409
+
410
+ // Set up the modal content with container for ERD
411
+ modalContent.innerHTML = `
412
+ <div class="modal-header">
413
+ <h5 class="modal-title">Relationships for ${tableName}</h5>
414
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
415
+ </div>
416
+ <div class="modal-body p-0"> <!-- Removed padding for full width -->
417
+ <div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;"> <!-- Increased height -->
418
+ <div id="mini-erd-loading" class="d-flex justify-content-center align-items-center" style="height: 100%; min-height: 450px;">
419
+ <div class="text-center">
420
+ <div class="spinner-border text-primary mb-3" role="status">
421
+ <span class="visually-hidden">Loading...</span>
422
+ </div>
423
+ <p>Generating Relationships Diagram...</p>
424
+ </div>
425
+ </div>
426
+ <div id="mini-erd-error" class="alert alert-danger m-3 d-none">
427
+ <h5>Error generating diagram</h5>
428
+ <p id="mini-erd-error-message">There was an error rendering the relationships diagram.</p>
429
+ <pre id="mini-erd-error-details" class="bg-light p-2 small mt-2"></pre>
430
+ </div>
431
+ </div>
432
+ <div id="debug-data" class="d-none m-3 border-top pt-3">
433
+ <details>
434
+ <summary>Debug Information</summary>
435
+ <div class="alert alert-info small">
436
+ <pre id="erd-data-debug" style="max-height: 100px; overflow: auto;">${JSON.stringify(
437
+ data,
438
+ null,
439
+ 2
440
+ )}</pre>
441
+ </div>
442
+ </details>
443
+ </div>
444
+ </div>
445
+ <div class="modal-footer">
446
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
447
+ </div>
448
+ `;
449
+
450
+ try {
451
+ const tables = data.tables || [];
452
+ const relationships = data.relationships || [];
453
+
454
+ // Validate data before proceeding
455
+ if (!Array.isArray(tables) || !Array.isArray(relationships)) {
456
+ showDiagramError(
457
+ "Invalid data format",
458
+ "The relationship data is not in the expected format."
459
+ );
460
+ console.error("Invalid data format received:", data);
461
+ return;
462
+ }
463
+
464
+ console.log(
465
+ `Found ${tables.length} tables and ${relationships.length} relationships`
466
+ );
467
+
468
+ // Create the ER diagram definition in Mermaid syntax
469
+ let mermaidDefinition = "erDiagram\n";
470
+
471
+ // Add tables to the diagram - ensure we have at least one table
472
+ if (tables.length === 0) {
473
+ mermaidDefinition += ` ${tableName.gsub(/[^\w]/, "_")} {\n`;
474
+ mermaidDefinition += ` string id PK\n`;
475
+ mermaidDefinition += ` }\n`;
476
+ } else {
477
+ tables.forEach(function (table) {
478
+ const tableName = table.name;
479
+
480
+ if (!tableName) {
481
+ console.warn("Table with no name found:", table);
482
+ return; // Skip this table
483
+ }
484
+
485
+ // Clean table name for mermaid (remove special characters)
486
+ const cleanTableName = tableName.replace(/[^\w]/g, "_");
487
+
488
+ // Make the current table stand out with a different visualization
489
+ if (tableName === `${tableName}`) {
490
+ mermaidDefinition += ` ${cleanTableName} {\n`;
491
+ mermaidDefinition += ` string id PK\n`;
492
+ mermaidDefinition += ` }\n`;
493
+ } else {
494
+ mermaidDefinition += ` ${cleanTableName} {\n`;
495
+ mermaidDefinition += ` string id\n`;
496
+ mermaidDefinition += ` }\n`;
497
+ }
498
+ });
499
+ }
500
+
501
+ // Add relationships
502
+ if (relationships && relationships.length > 0) {
503
+ relationships.forEach(function (rel) {
504
+ try {
505
+ // Ensure all required properties exist
506
+ if (!rel.from_table || !rel.to_table) {
507
+ console.error("Missing table in relationship:", rel);
508
+ return; // Skip this relationship
509
+ }
510
+
511
+ // Clean up table names for mermaid (remove special characters)
512
+ const fromTable = rel.from_table.replace(/[^\w]/g, "_");
513
+ const toTable = rel.to_table.replace(/[^\w]/g, "_");
514
+ const relationLabel = rel.from_column || "";
515
+
516
+ // Customize the display based on direction
517
+ mermaidDefinition += ` ${fromTable} }|--|| ${toTable} : "${relationLabel}"\n`;
518
+ } catch (err) {
519
+ console.error("Error processing relationship:", err, rel);
520
+ }
521
+ });
522
+ } else {
523
+ // Add a note if no relationships are found
524
+ mermaidDefinition += " %% No relationships found for this table\n";
525
+ }
526
+
527
+ // Log the generated mermaid definition for debugging
528
+ console.log("Mermaid Definition:", mermaidDefinition);
529
+
530
+ // Hide the loading indicator first since render might take time
531
+ document.getElementById("mini-erd-loading").style.display = "none";
532
+
533
+ // Render the diagram with Mermaid
534
+ mermaid
535
+ .render("mini-erd-graph", mermaidDefinition)
536
+ .then(function (result) {
537
+ console.log("Mermaid rendering successful");
538
+
539
+ // Get the container
540
+ const container = document.getElementById("mini-erd-container");
541
+
542
+ // Insert the rendered SVG
543
+ container.innerHTML = result.svg;
544
+
545
+ // Style the SVG element for better fit
546
+ const svgElement = container.querySelector("svg");
547
+ if (svgElement) {
548
+ // Set size attributes for the SVG
549
+ svgElement.setAttribute("width", "100%");
550
+ svgElement.setAttribute("height", "100%");
551
+ svgElement.style.minHeight = "450px";
552
+ svgElement.style.width = "100%";
553
+ svgElement.style.height = "100%";
554
+
555
+ // Set viewBox if not present to enable proper scaling
556
+ if (!svgElement.getAttribute("viewBox")) {
557
+ const width = svgElement.getAttribute("width") || "100%";
558
+ const height = svgElement.getAttribute("height") || "100%";
559
+ svgElement.setAttribute(
560
+ "viewBox",
561
+ `0 0 ${parseInt(width) || 1000} ${parseInt(height) || 800}`
562
+ );
563
+ }
564
+ }
565
+
566
+ // Apply SVG-Pan-Zoom to make the diagram interactive
567
+ try {
568
+ const svgElement = container.querySelector("svg");
569
+ if (svgElement && typeof svgPanZoom !== "undefined") {
570
+ // Make SVG take the full container width and ensure it has valid dimensions
571
+ svgElement.setAttribute("width", "100%");
572
+ svgElement.setAttribute("height", "100%");
573
+
574
+ // Wait for SVG to be fully rendered with proper dimensions
575
+ setTimeout(() => {
576
+ try {
577
+ // Get dimensions to ensure they're valid before initializing pan-zoom
578
+ const clientRect = svgElement.getBoundingClientRect();
579
+
580
+ // Only initialize if we have valid dimensions
581
+ if (clientRect.width > 0 && clientRect.height > 0) {
582
+ // Initialize SVG Pan-Zoom with more robust error handling
583
+ const panZoomInstance = svgPanZoom(svgElement, {
584
+ zoomEnabled: true,
585
+ controlIconsEnabled: true,
586
+ fit: false, // Don't automatically fit on init - can cause the matrix error
587
+ center: false, // Don't automatically center - can cause the matrix error
588
+ minZoom: 0.5,
589
+ maxZoom: 2.5,
590
+ beforeZoom: function () {
591
+ // Check if the SVG is valid for zooming
592
+ return (
593
+ svgElement.getBoundingClientRect().width > 0 &&
594
+ svgElement.getBoundingClientRect().height > 0
595
+ );
596
+ },
597
+ });
598
+
599
+ // Store the panZoom instance for resize handling
600
+ container.panZoomInstance = panZoomInstance;
601
+
602
+ // Manually fit and center after a slight delay
603
+ setTimeout(() => {
604
+ try {
605
+ panZoomInstance.resize();
606
+ panZoomInstance.fit();
607
+ panZoomInstance.center();
608
+ } catch (err) {
609
+ console.warn(
610
+ "Error during fit/center operation:",
611
+ err
612
+ );
613
+ }
614
+ }, 300);
615
+
616
+ // Setup resize observer to maintain full size
617
+ const resizeObserver = new ResizeObserver(() => {
618
+ if (container.panZoomInstance) {
619
+ try {
620
+ // Reset zoom and center when container is resized
621
+ container.panZoomInstance.resize();
622
+ // Only fit and center if the element is visible with valid dimensions
623
+ if (
624
+ svgElement.getBoundingClientRect().width > 0 &&
625
+ svgElement.getBoundingClientRect().height > 0
626
+ ) {
627
+ container.panZoomInstance.fit();
628
+ container.panZoomInstance.center();
629
+ }
630
+ } catch (err) {
631
+ console.warn(
632
+ "Error during resize observer callback:",
633
+ err
634
+ );
635
+ }
636
+ }
637
+ });
638
+
639
+ // Observe the container for size changes
640
+ resizeObserver.observe(container);
641
+
642
+ // Also handle manual resize on modal resize
643
+ miniErdModal.addEventListener(
644
+ "resize.bs.modal",
645
+ function () {
646
+ if (container.panZoomInstance) {
647
+ setTimeout(() => {
648
+ try {
649
+ container.panZoomInstance.resize();
650
+ // Only fit and center if the element is visible with valid dimensions
651
+ if (
652
+ svgElement.getBoundingClientRect().width >
653
+ 0 &&
654
+ svgElement.getBoundingClientRect().height > 0
655
+ ) {
656
+ container.panZoomInstance.fit();
657
+ container.panZoomInstance.center();
658
+ }
659
+ } catch (err) {
660
+ console.warn(
661
+ "Error during modal resize handler:",
662
+ err
663
+ );
664
+ }
665
+ }, 300);
666
+ }
667
+ }
668
+ );
669
+ } else {
670
+ console.warn(
671
+ "Cannot initialize SVG-Pan-Zoom: SVG has invalid dimensions",
672
+ clientRect
673
+ );
674
+ }
675
+ } catch (err) {
676
+ console.warn("Error initializing SVG-Pan-Zoom:", err);
677
+ }
678
+ }, 500); // Increased delay to ensure SVG is fully rendered with proper dimensions
679
+ }
680
+ } catch (e) {
681
+ console.warn("Failed to initialize svg-pan-zoom:", e);
682
+ // Not critical, continue without pan-zoom
683
+ }
684
+
685
+ // Add highlighting for the current table after a delay to ensure SVG is fully processed
686
+ setTimeout(function () {
687
+ try {
688
+ const cleanTableName = `${tableName}`.replace(/[^\w]/g, "_");
689
+ const currentTableElement = container.querySelector(
690
+ `[id*="${cleanTableName}"]`
691
+ );
692
+ if (currentTableElement) {
693
+ const rect = currentTableElement.querySelector("rect");
694
+ if (rect) {
695
+ // Highlight the current table
696
+ rect.setAttribute(
697
+ "fill",
698
+ document.documentElement.getAttribute("data-bs-theme") ===
699
+ "dark"
700
+ ? "#2c3034"
701
+ : "#e2f0ff"
702
+ );
703
+ rect.setAttribute(
704
+ "stroke",
705
+ document.documentElement.getAttribute("data-bs-theme") ===
706
+ "dark"
707
+ ? "#6ea8fe"
708
+ : "#0d6efd"
709
+ );
710
+ rect.setAttribute("stroke-width", "2");
711
+ }
712
+ }
713
+ } catch (e) {
714
+ console.error("Error highlighting current table:", e);
715
+ }
716
+ }, 100);
717
+ })
718
+ .catch(function (error) {
719
+ console.error("Error rendering mini ERD:", error);
720
+ showDiagramError(
721
+ "Error rendering diagram",
722
+ "There was an error rendering the relationships diagram.",
723
+ error.message || "Unknown error"
724
+ );
725
+
726
+ // Show debug data when there's an error
727
+ document.getElementById("debug-data").classList.remove("d-none");
728
+ });
729
+ } catch (error) {
730
+ console.error("Exception in renderMiniErd function:", error);
731
+ showDiagramError(
732
+ "Exception generating diagram",
733
+ "There was an exception processing the relationships diagram.",
734
+ error.message || "Unknown error"
735
+ );
736
+
737
+ // Show debug data when there's an error
738
+ document.getElementById("debug-data").classList.remove("d-none");
739
+ }
740
+ }
741
+
742
+ // Function to show diagram error
743
+ function showDiagramError(title, message, details = "") {
744
+ const errorContainer = document.getElementById("mini-erd-error");
745
+ const errorMessage = document.getElementById("mini-erd-error-message");
746
+ const errorDetails = document.getElementById("mini-erd-error-details");
747
+ const loadingIndicator = document.getElementById("mini-erd-loading");
748
+
749
+ if (loadingIndicator) {
750
+ loadingIndicator.style.display = "none";
751
+ }
752
+
753
+ if (errorContainer && errorMessage) {
754
+ // Set error message
755
+ errorMessage.textContent = message;
756
+
757
+ // Set error details if provided
758
+ if (details && errorDetails) {
759
+ errorDetails.textContent = details;
760
+ errorDetails.classList.remove("d-none");
761
+ } else if (errorDetails) {
762
+ errorDetails.classList.add("d-none");
763
+ }
764
+
765
+ // Show the error container
766
+ errorContainer.classList.remove("d-none");
767
+ }
768
+ }
769
+
770
+ // Handle modal shown event - adjust size after the modal is fully visible
771
+ miniErdModal.addEventListener("shown.bs.modal", function (event) {
772
+ // After modal is fully shown, resize the diagram to fit
773
+ const container = document.getElementById("mini-erd-container");
774
+ if (container && container.panZoomInstance) {
775
+ setTimeout(() => {
776
+ try {
777
+ // Check if the SVG still has valid dimensions before operating on it
778
+ const svgElement = container.querySelector("svg");
779
+ if (
780
+ svgElement &&
781
+ svgElement.getBoundingClientRect().width > 0 &&
782
+ svgElement.getBoundingClientRect().height > 0
783
+ ) {
784
+ container.panZoomInstance.resize();
785
+ container.panZoomInstance.fit();
786
+ container.panZoomInstance.center();
787
+ } else {
788
+ console.warn(
789
+ "Cannot perform pan-zoom operations: SVG has invalid dimensions"
790
+ );
791
+ }
792
+ } catch (err) {
793
+ console.warn("Error during modal shown handler:", err);
794
+ }
795
+ }, 500); // Increased delay to ensure modal is fully transitioned and SVG is rendered
796
+ }
797
+ });
798
+
799
+ // Handle modal close to reset state for future opens
800
+ miniErdModal.addEventListener("hidden.bs.modal", function (event) {
801
+ // Reset flags and cached data to ensure fresh fetch on next open
802
+ isModalLoaded = false;
803
+ erdData = null;
804
+ console.log("Modal closed, diagram data will be refetched on next open");
805
+ });
806
+ }
807
+
808
+ // Function to retry loading the Mini ERD
809
+ function retryLoadingMiniERD() {
810
+ console.log("Retrying loading of mini ERD");
811
+ const modalContent = document.getElementById("miniErdModalContent");
812
+
813
+ // Set loading state again
814
+ modalContent.innerHTML = `
815
+ <div class="modal-header">
816
+ <h5 class="modal-title">Relationships for ${tableName}</h5>
817
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
818
+ </div>
819
+ <div class="modal-body p-0">
820
+ <div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
821
+ <div class="text-center">
822
+ <div class="spinner-border text-primary mb-3" role="status">
823
+ <span class="visually-hidden">Loading...</span>
824
+ </div>
825
+ <p>Retrying to load relationships diagram...</p>
826
+ </div>
827
+ </div>
828
+ </div>
829
+ `;
830
+
831
+ // Reset state to ensure fresh fetch
832
+ isModalLoaded = false;
833
+ erdData = null;
834
+
835
+ // Retry fetching data
836
+ fetchErdData();
837
+ }
838
+
839
+ // Column sorting enhancement
840
+ const sortableColumns = document.querySelectorAll(".sortable-column");
841
+ sortableColumns.forEach((column) => {
842
+ const link = column.querySelector(".column-sort-link");
843
+
844
+ // Mouse over effects
845
+ column.addEventListener("mouseenter", () => {
846
+ const sortIcon = column.querySelector(".sort-icon");
847
+ if (sortIcon && sortIcon.classList.contains("invisible")) {
848
+ sortIcon.style.visibility = "visible";
849
+ sortIcon.style.opacity = "0.3";
850
+ }
851
+ });
852
+
853
+ column.addEventListener("mouseleave", () => {
854
+ const sortIcon = column.querySelector(".sort-icon");
855
+ if (sortIcon && sortIcon.classList.contains("invisible")) {
856
+ sortIcon.style.visibility = "hidden";
857
+ sortIcon.style.opacity = "0";
858
+ }
859
+ });
860
+
861
+ // Keyboard accessibility
862
+ if (link) {
863
+ link.addEventListener("keydown", (e) => {
864
+ if (e.key === "Enter" || e.key === " ") {
865
+ e.preventDefault();
866
+ link.click();
867
+ }
868
+ });
869
+ }
870
+ });
871
+
872
+ // Table fullscreen functionality
873
+ const fullscreenToggle = document.getElementById("fullscreen-toggle");
874
+ const fullscreenIcon = document.getElementById("fullscreen-icon");
875
+ const tableSection = document.getElementById("table-section");
876
+
877
+ if (fullscreenToggle && tableSection) {
878
+ // Key for storing fullscreen state in localStorage
879
+ const fullscreenStateKey = `dbviewer-table-fullscreen-${tableName}`;
880
+
881
+ // Function to apply fullscreen state
882
+ function applyFullscreenState(isFullscreen) {
883
+ if (isFullscreen) {
884
+ // Enter fullscreen
885
+ tableSection.classList.add("table-fullscreen");
886
+ document.body.classList.add("table-fullscreen-active");
887
+ fullscreenIcon.classList.remove("bi-fullscreen");
888
+ fullscreenIcon.classList.add("bi-fullscreen-exit");
889
+ fullscreenToggle.setAttribute("title", "Exit fullscreen");
890
+ } else {
891
+ // Exit fullscreen
892
+ tableSection.classList.remove("table-fullscreen");
893
+ document.body.classList.remove("table-fullscreen-active");
894
+ fullscreenIcon.classList.remove("bi-fullscreen-exit");
895
+ fullscreenIcon.classList.add("bi-fullscreen");
896
+ fullscreenToggle.setAttribute("title", "Toggle fullscreen");
897
+ }
898
+ }
899
+
900
+ // Restore fullscreen state from localStorage on page load
901
+ try {
902
+ const savedState = localStorage.getItem(fullscreenStateKey);
903
+ if (savedState === "true") {
904
+ applyFullscreenState(true);
905
+ }
906
+ } catch (e) {
907
+ // Handle localStorage not available (private browsing, etc.)
908
+ console.warn("Could not restore fullscreen state:", e);
909
+ }
910
+
911
+ fullscreenToggle.addEventListener("click", function () {
912
+ const isFullscreen = tableSection.classList.contains("table-fullscreen");
913
+ const newState = !isFullscreen;
914
+
915
+ // Apply the new state
916
+ applyFullscreenState(newState);
917
+
918
+ // Save state to localStorage
919
+ try {
920
+ localStorage.setItem(fullscreenStateKey, newState.toString());
921
+ } catch (e) {
922
+ // Handle localStorage not available (private browsing, etc.)
923
+ console.warn("Could not save fullscreen state:", e);
924
+ }
925
+ });
926
+
927
+ // Exit fullscreen with Escape key
928
+ document.addEventListener("keydown", function (e) {
929
+ if (
930
+ e.key === "Escape" &&
931
+ tableSection.classList.contains("table-fullscreen")
932
+ ) {
933
+ fullscreenToggle.click();
934
+ }
935
+ });
936
+ }
937
+
938
+ // Function to copy FactoryBot code
939
+ window.copyToJson = function (button) {
940
+ try {
941
+ // Get record data from data attribute
942
+ const recordData = JSON.parse(button.dataset.recordData);
943
+
944
+ // Generate formatted JSON string
945
+ const jsonString = JSON.stringify(recordData, null, 2);
946
+
947
+ // Copy to clipboard
948
+ navigator.clipboard
949
+ .writeText(jsonString)
950
+ .then(() => {
951
+ // Show a temporary success message on the button
952
+ const originalTitle = button.getAttribute("title");
953
+ button.setAttribute("title", "Copied!");
954
+ button.classList.remove("btn-outline-secondary");
955
+ button.classList.add("btn-success");
956
+
957
+ // Show a toast notification
958
+ if (typeof Toastify === "function") {
959
+ Toastify({
960
+ text: `<span class="toast-icon"><i class="bi bi-clipboard-check"></i></span> JSON data copied to clipboard!`,
961
+ className: "toast-factory-bot",
962
+ duration: 3000,
963
+ gravity: "bottom",
964
+ position: "right",
965
+ escapeMarkup: false,
966
+ style: {
967
+ animation:
968
+ "slideInRight 0.3s ease-out, slideOutRight 0.3s ease-out 2.7s",
969
+ },
970
+ onClick: function () {
971
+ /* Dismiss toast on click */
972
+ },
973
+ }).showToast();
974
+ }
975
+
976
+ setTimeout(() => {
977
+ button.setAttribute("title", originalTitle);
978
+ button.classList.remove("btn-success");
979
+ button.classList.add("btn-outline-secondary");
980
+ }, 2000);
981
+ })
982
+ .catch((err) => {
983
+ console.error("Failed to copy text: ", err);
984
+
985
+ // Show error toast
986
+ if (typeof Toastify === "function") {
987
+ Toastify({
988
+ text: '<span class="toast-icon"><i class="bi bi-exclamation-triangle"></i></span> Failed to copy to clipboard',
989
+ className: "bg-danger",
990
+ duration: 3000,
991
+ gravity: "bottom",
992
+ position: "right",
993
+ escapeMarkup: false,
994
+ style: {
995
+ background: "linear-gradient(135deg, #dc3545, #c82333)",
996
+ animation: "slideInRight 0.3s ease-out",
997
+ },
998
+ }).showToast();
999
+ } else {
1000
+ alert("Failed to copy to clipboard. See console for details.");
1001
+ }
1002
+ });
1003
+ } catch (error) {
1004
+ console.error("Error generating JSON:", error);
1005
+ alert("Error generating JSON. See console for details.");
1006
+ }
1007
+ };
1008
+
1009
+ // Helper function to create relationship sections
1010
+ // Function to fetch relationship counts from API
1011
+ async function fetchRelationshipCounts(
1012
+ tableName,
1013
+ recordId,
1014
+ relationships,
1015
+ hasManySection
1016
+ ) {
1017
+ try {
1018
+ const response = await fetch(
1019
+ `/dbviewer/api/tables/${tableName}/relationship_counts?record_id=${recordId}`
1020
+ );
1021
+ if (!response.ok) {
1022
+ throw new Error(`HTTP error! status: ${response.status}`);
1023
+ }
1024
+
1025
+ const data = await response.json();
1026
+
1027
+ // Update each count in the UI
1028
+ const countSpans = hasManySection.querySelectorAll(".relationship-count");
1029
+
1030
+ relationships.forEach((relationship, index) => {
1031
+ const countSpan = countSpans[index];
1032
+ if (countSpan) {
1033
+ const relationshipData = data.relationships.find(
1034
+ (r) =>
1035
+ r.table === relationship.from_table &&
1036
+ r.foreign_key === relationship.column
1037
+ );
1038
+
1039
+ if (relationshipData) {
1040
+ const count = relationshipData.count;
1041
+ let badgeClass = "bg-secondary";
1042
+ let badgeText = `${count} record${count !== 1 ? "s" : ""}`;
1043
+
1044
+ // Use different colors based on count
1045
+ if (count > 0) {
1046
+ badgeClass = count > 10 ? "bg-warning" : "bg-success";
1047
+ }
1048
+
1049
+ countSpan.innerHTML = `<span class="badge ${badgeClass}">${badgeText}</span>`;
1050
+ } else {
1051
+ // Fallback if no data found
1052
+ countSpan.innerHTML = '<span class="badge bg-danger">Error</span>';
1053
+ }
1054
+ }
1055
+ });
1056
+ } catch (error) {
1057
+ console.error("Error fetching relationship counts:", error);
1058
+
1059
+ // Show error state in UI
1060
+ const countSpans = hasManySection.querySelectorAll(".relationship-count");
1061
+ countSpans.forEach((span) => {
1062
+ span.innerHTML = '<span class="badge bg-danger">Error</span>';
1063
+ });
1064
+ }
1065
+ }
1066
+
1067
+ function createRelationshipSection(
1068
+ title,
1069
+ relationships,
1070
+ recordData,
1071
+ type,
1072
+ primaryKeyValue = null
1073
+ ) {
1074
+ const section = document.createElement("div");
1075
+ section.className = "relationship-section mb-4";
1076
+
1077
+ // Create section header
1078
+ const header = document.createElement("h6");
1079
+ header.className = "mb-3";
1080
+ const icon =
1081
+ type === "belongs_to" ? "bi-arrow-up-right" : "bi-arrow-down-left";
1082
+ header.innerHTML = `<i class="bi ${icon} me-2"></i>${title}`;
1083
+ section.appendChild(header);
1084
+
1085
+ const tableContainer = document.createElement("div");
1086
+ tableContainer.className = "table-responsive";
1087
+
1088
+ const table = document.createElement("table");
1089
+ table.className = "table table-sm table-bordered";
1090
+
1091
+ // Create header based on relationship type
1092
+ const thead = document.createElement("thead");
1093
+ if (type === "belongs_to") {
1094
+ thead.innerHTML = `
1095
+ <tr>
1096
+ <th width="25%">Column</th>
1097
+ <th width="25%">Value</th>
1098
+ <th width="25%">References</th>
1099
+ <th width="25%">Action</th>
1100
+ </tr>
1101
+ `;
1102
+ } else {
1103
+ thead.innerHTML = `
1104
+ <tr>
1105
+ <th width="30%">Related Table</th>
1106
+ <th width="25%">Foreign Key</th>
1107
+ <th width="20%">Count</th>
1108
+ <th width="25%">Action</th>
1109
+ </tr>
1110
+ `;
1111
+ }
1112
+ table.appendChild(thead);
1113
+
1114
+ // Create body
1115
+ const tbody = document.createElement("tbody");
1116
+
1117
+ relationships.forEach((fk) => {
1118
+ const row = document.createElement("tr");
1119
+
1120
+ if (type === "belongs_to") {
1121
+ const columnValue = recordData[fk.column];
1122
+ row.innerHTML = `
1123
+ <td class="fw-medium">${fk.column}</td>
1124
+ <td><code>${columnValue}</code></td>
1125
+ <td>
1126
+ <span class="text-muted">${fk.to_table}.</span><strong>${
1127
+ fk.primary_key
1128
+ }</strong>
1129
+ </td>
1130
+ <td>
1131
+ <a href="/dbviewer/tables/${fk.to_table}?column_filters[${
1132
+ fk.primary_key
1133
+ }]=${encodeURIComponent(columnValue)}"
1134
+ class="btn btn-sm btn-outline-primary"
1135
+ title="View referenced record in ${fk.to_table}">
1136
+ <i class="bi bi-arrow-right me-1"></i>View
1137
+ </a>
1138
+ </td>
1139
+ `;
1140
+ } else {
1141
+ // For has_many relationships
1142
+ row.innerHTML = `
1143
+ <td class="fw-medium">${fk.from_table}</td>
1144
+ <td>
1145
+ <span class="text-muted">${fk.from_table}.</span><strong>${
1146
+ fk.column
1147
+ }</strong>
1148
+ </td>
1149
+ <td>
1150
+ <span class="relationship-count">
1151
+ <span class="badge bg-secondary">
1152
+ <span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
1153
+ Loading...
1154
+ </span>
1155
+ </span>
1156
+ </td>
1157
+ <td>
1158
+ <a href="/dbviewer/tables/${fk.from_table}?column_filters[${
1159
+ fk.column
1160
+ }]=${encodeURIComponent(primaryKeyValue)}"
1161
+ class="btn btn-sm btn-outline-success"
1162
+ title="View all ${
1163
+ fk.from_table
1164
+ } records that reference this record">
1165
+ <i class="bi bi-list me-1"></i>View Related
1166
+ </a>
1167
+ </td>
1168
+ `;
1169
+ }
1170
+
1171
+ tbody.appendChild(row);
1172
+ });
1173
+
1174
+ table.appendChild(tbody);
1175
+ tableContainer.appendChild(table);
1176
+ section.appendChild(tableContainer);
1177
+
1178
+ return section;
1179
+ }
1180
+
1181
+ // Configure Mermaid for better ERD diagrams
1182
+ mermaid.initialize({
1183
+ startOnLoad: false,
1184
+ theme:
1185
+ document.documentElement.getAttribute("data-bs-theme") === "dark"
1186
+ ? "dark"
1187
+ : "default",
1188
+ securityLevel: "loose",
1189
+ er: {
1190
+ diagramPadding: 20,
1191
+ layoutDirection: "TB",
1192
+ minEntityWidth: 100,
1193
+ minEntityHeight: 75,
1194
+ entityPadding: 15,
1195
+ stroke: "gray",
1196
+ fill:
1197
+ document.documentElement.getAttribute("data-bs-theme") === "dark"
1198
+ ? "#2D3748"
1199
+ : "#f5f5f5",
1200
+ fontSize: 14,
1201
+ useMaxWidth: true,
1202
+ wrappingLength: 30,
1203
+ },
1204
+ });
1205
+
1206
+ // Initialize Flatpickr date range picker
1207
+ const dateRangeInput = document.getElementById("floatingCreationFilterRange");
1208
+ const startHidden = document.getElementById("creation_filter_start");
1209
+ const endHidden = document.getElementById("creation_filter_end");
1210
+
1211
+ if (dateRangeInput && typeof flatpickr !== "undefined") {
1212
+ console.log("Flatpickr library loaded, initializing date range picker");
1213
+ // Store the Flatpickr instance in a variable accessible to all handlers
1214
+ let fp;
1215
+
1216
+ // Function to initialize Flatpickr
1217
+ function initializeFlatpickr(theme) {
1218
+ // Determine theme based on current document theme or passed parameter
1219
+ const currentTheme =
1220
+ theme ||
1221
+ (document.documentElement.getAttribute("data-bs-theme") === "dark"
1222
+ ? "dark"
1223
+ : "light");
1224
+
1225
+ const config = {
1226
+ mode: "range",
1227
+ enableTime: true,
1228
+ dateFormat: "Y-m-d H:i",
1229
+ time_24hr: true,
1230
+ allowInput: false,
1231
+ clickOpens: true,
1232
+ theme: currentTheme,
1233
+ animate: true,
1234
+ position: "auto",
1235
+ static: false,
1236
+ appendTo: document.body, // Ensure it renders above other elements
1237
+ locale: {
1238
+ rangeSeparator: " to ",
1239
+ weekdays: {
1240
+ shorthand: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
1241
+ longhand: [
1242
+ "Sunday",
1243
+ "Monday",
1244
+ "Tuesday",
1245
+ "Wednesday",
1246
+ "Thursday",
1247
+ "Friday",
1248
+ "Saturday",
1249
+ ],
1250
+ },
1251
+ months: {
1252
+ shorthand: [
1253
+ "Jan",
1254
+ "Feb",
1255
+ "Mar",
1256
+ "Apr",
1257
+ "May",
1258
+ "Jun",
1259
+ "Jul",
1260
+ "Aug",
1261
+ "Sep",
1262
+ "Oct",
1263
+ "Nov",
1264
+ "Dec",
1265
+ ],
1266
+ longhand: [
1267
+ "January",
1268
+ "February",
1269
+ "March",
1270
+ "April",
1271
+ "May",
1272
+ "June",
1273
+ "July",
1274
+ "August",
1275
+ "September",
1276
+ "October",
1277
+ "November",
1278
+ "December",
1279
+ ],
1280
+ },
1281
+ },
1282
+ onOpen: function (selectedDates, dateStr, instance) {
1283
+ // Add a slight delay to apply theme-specific styling after calendar opens
1284
+ setTimeout(() => {
1285
+ const calendar = instance.calendarContainer;
1286
+ if (calendar) {
1287
+ // Apply theme-specific class for additional styling control
1288
+ calendar.classList.add(`flatpickr-${currentTheme}`);
1289
+
1290
+ // Ensure proper z-index for offcanvas overlay
1291
+ calendar.style.zIndex = "1070";
1292
+
1293
+ // Add elegant entrance animation
1294
+ calendar.classList.add("open");
1295
+ }
1296
+ }, 10);
1297
+ },
1298
+ onClose: function (selectedDates, dateStr, instance) {
1299
+ const calendar = instance.calendarContainer;
1300
+ if (calendar) {
1301
+ calendar.classList.remove("open");
1302
+ }
1303
+ },
1304
+ onChange: function (selectedDates, dateStr, instance) {
1305
+ console.log("Date range changed:", selectedDates);
1306
+
1307
+ if (selectedDates.length === 2) {
1308
+ // Format dates for hidden inputs (Rails expects ISO format)
1309
+ startHidden.value = selectedDates[0].toISOString().slice(0, 16);
1310
+ endHidden.value = selectedDates[1].toISOString().slice(0, 16);
1311
+
1312
+ // Update display with elegant formatting
1313
+ const formatOptions = {
1314
+ year: "numeric",
1315
+ month: "short",
1316
+ day: "numeric",
1317
+ hour: "2-digit",
1318
+ minute: "2-digit",
1319
+ hour12: false,
1320
+ };
1321
+
1322
+ const startFormatted = selectedDates[0].toLocaleDateString(
1323
+ "en-US",
1324
+ formatOptions
1325
+ );
1326
+ const endFormatted = selectedDates[1].toLocaleDateString(
1327
+ "en-US",
1328
+ formatOptions
1329
+ );
1330
+ dateRangeInput.value = `${startFormatted} to ${endFormatted}`;
1331
+ } else if (selectedDates.length === 1) {
1332
+ startHidden.value = selectedDates[0].toISOString().slice(0, 16);
1333
+ endHidden.value = "";
1334
+
1335
+ const formatOptions = {
1336
+ year: "numeric",
1337
+ month: "short",
1338
+ day: "numeric",
1339
+ hour: "2-digit",
1340
+ minute: "2-digit",
1341
+ hour12: false,
1342
+ };
1343
+
1344
+ const startFormatted = selectedDates[0].toLocaleDateString(
1345
+ "en-US",
1346
+ formatOptions
1347
+ );
1348
+ dateRangeInput.value = `${startFormatted} (select end date)`;
1349
+ } else {
1350
+ startHidden.value = "";
1351
+ endHidden.value = "";
1352
+ dateRangeInput.value = "";
1353
+ }
1354
+ },
1355
+ };
1356
+
1357
+ return flatpickr(dateRangeInput, config);
1358
+ }
1359
+
1360
+ // Initialize date range picker
1361
+ fp = initializeFlatpickr();
1362
+
1363
+ // Set initial values if they exist
1364
+ if (startHidden.value || endHidden.value) {
1365
+ const dates = [];
1366
+ if (startHidden.value) {
1367
+ dates.push(new Date(startHidden.value));
1368
+ }
1369
+ if (endHidden.value) {
1370
+ dates.push(new Date(endHidden.value));
1371
+ }
1372
+ fp.setDate(dates);
1373
+ }
1374
+
1375
+ // Preset button functionality
1376
+ const presetButtons = document.querySelectorAll(".preset-btn");
1377
+ presetButtons.forEach((button) => {
1378
+ button.addEventListener("click", function (event) {
1379
+ event.preventDefault(); // Prevent any form submission
1380
+
1381
+ const preset = this.getAttribute("data-preset");
1382
+ const now = new Date();
1383
+ let startDate, endDate;
1384
+
1385
+ console.log("Preset button clicked:", preset); // Debug log
1386
+
1387
+ switch (preset) {
1388
+ case "lastminute":
1389
+ startDate = new Date(now);
1390
+ startDate.setMinutes(startDate.getMinutes() - 1);
1391
+ endDate = new Date(now);
1392
+ break;
1393
+ case "last5minutes":
1394
+ startDate = new Date(now);
1395
+ startDate.setMinutes(startDate.getMinutes() - 5);
1396
+ endDate = new Date(now);
1397
+ break;
1398
+ case "today":
1399
+ startDate = new Date(
1400
+ now.getFullYear(),
1401
+ now.getMonth(),
1402
+ now.getDate(),
1403
+ 0,
1404
+ 0,
1405
+ 0
1406
+ );
1407
+ endDate = new Date(
1408
+ now.getFullYear(),
1409
+ now.getMonth(),
1410
+ now.getDate(),
1411
+ 23,
1412
+ 59,
1413
+ 59
1414
+ );
1415
+ break;
1416
+ case "yesterday":
1417
+ const yesterday = new Date(now);
1418
+ yesterday.setDate(yesterday.getDate() - 1);
1419
+ startDate = new Date(
1420
+ yesterday.getFullYear(),
1421
+ yesterday.getMonth(),
1422
+ yesterday.getDate(),
1423
+ 0,
1424
+ 0,
1425
+ 0
1426
+ );
1427
+ endDate = new Date(
1428
+ yesterday.getFullYear(),
1429
+ yesterday.getMonth(),
1430
+ yesterday.getDate(),
1431
+ 23,
1432
+ 59,
1433
+ 59
1434
+ );
1435
+ break;
1436
+ case "last7days":
1437
+ startDate = new Date(now);
1438
+ startDate.setDate(startDate.getDate() - 7);
1439
+ startDate.setHours(0, 0, 0, 0);
1440
+ endDate = new Date(now);
1441
+ endDate.setHours(23, 59, 59, 999);
1442
+ break;
1443
+ case "last30days":
1444
+ startDate = new Date(now);
1445
+ startDate.setDate(startDate.getDate() - 30);
1446
+ startDate.setHours(0, 0, 0, 0);
1447
+ endDate = new Date(now);
1448
+ endDate.setHours(23, 59, 59, 999);
1449
+ break;
1450
+ case "thismonth":
1451
+ startDate = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0);
1452
+ endDate = new Date(
1453
+ now.getFullYear(),
1454
+ now.getMonth() + 1,
1455
+ 0,
1456
+ 23,
1457
+ 59,
1458
+ 59
1459
+ );
1460
+ break;
1461
+ }
1462
+
1463
+ if (startDate && endDate && fp) {
1464
+ console.log("Setting dates:", startDate, endDate); // Debug log
1465
+ fp.setDate([startDate, endDate]);
1466
+
1467
+ // Also update the hidden inputs directly as a fallback
1468
+ startHidden.value = startDate.toISOString().slice(0, 16);
1469
+ endHidden.value = endDate.toISOString().slice(0, 16);
1470
+
1471
+ // Update the display value
1472
+ const formattedStart =
1473
+ startDate.toLocaleDateString() +
1474
+ " " +
1475
+ startDate.toLocaleTimeString();
1476
+ const formattedEnd =
1477
+ endDate.toLocaleDateString() + " " + endDate.toLocaleTimeString();
1478
+ dateRangeInput.value = formattedStart + " to " + formattedEnd;
1479
+ } else {
1480
+ console.error(
1481
+ "Failed to set dates - startDate:",
1482
+ startDate,
1483
+ "endDate:",
1484
+ endDate,
1485
+ "fp:",
1486
+ fp
1487
+ );
1488
+ }
1489
+ });
1490
+ });
1491
+
1492
+ // Listen for theme changes and update Flatpickr theme
1493
+ document.addEventListener("dbviewerThemeChanged", function (e) {
1494
+ const newTheme = e.detail.theme === "dark" ? "dark" : "light";
1495
+ console.log("Theme changed to:", newTheme);
1496
+
1497
+ // Destroy and recreate with new theme
1498
+ if (fp) {
1499
+ const currentDates = fp.selectedDates;
1500
+ fp.destroy();
1501
+ fp = initializeFlatpickr(newTheme);
1502
+
1503
+ // Restore previous values if they existed
1504
+ if (currentDates && currentDates.length > 0) {
1505
+ fp.setDate(currentDates);
1506
+ }
1507
+ }
1508
+ });
1509
+
1510
+ // Also listen for direct data-bs-theme attribute changes using MutationObserver
1511
+ const themeObserver = new MutationObserver(function (mutations) {
1512
+ mutations.forEach(function (mutation) {
1513
+ if (
1514
+ mutation.type === "attributes" &&
1515
+ mutation.attributeName === "data-bs-theme"
1516
+ ) {
1517
+ const newTheme =
1518
+ document.documentElement.getAttribute("data-bs-theme") === "dark"
1519
+ ? "dark"
1520
+ : "light";
1521
+ console.log("Theme attribute changed to:", newTheme);
1522
+
1523
+ if (fp) {
1524
+ const currentDates = fp.selectedDates;
1525
+ fp.destroy();
1526
+ fp = initializeFlatpickr(newTheme);
1527
+
1528
+ // Restore previous values if they existed
1529
+ if (currentDates && currentDates.length > 0) {
1530
+ fp.setDate(currentDates);
1531
+ }
1532
+ }
1533
+ }
1534
+ });
1535
+ });
1536
+
1537
+ // Start observing theme changes
1538
+ themeObserver.observe(document.documentElement, {
1539
+ attributes: true,
1540
+ attributeFilter: ["data-bs-theme"],
1541
+ });
1542
+ } else {
1543
+ console.error("Date range picker initialization failed:", {
1544
+ dateRangeInput: !!dateRangeInput,
1545
+ flatpickr: typeof flatpickr !== "undefined",
1546
+ });
1547
+ }
1548
+
1549
+ // Close offcanvas after form submission
1550
+ const form = document.getElementById("floatingCreationFilterForm");
1551
+ if (form) {
1552
+ form.addEventListener("submit", function () {
1553
+ const offcanvas = bootstrap.Offcanvas.getInstance(
1554
+ document.getElementById("creationFilterOffcanvas")
1555
+ );
1556
+ if (offcanvas) {
1557
+ setTimeout(() => {
1558
+ offcanvas.hide();
1559
+ }, 100);
1560
+ }
1561
+ });
1562
+ }
1563
+ });