dbviewer 0.6.7 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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
@@ -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
|
+
});
|