dbviewer 0.7.1 → 0.7.2
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 +53 -49
- data/app/assets/javascripts/dbviewer/sidebar.js +210 -0
- data/app/views/dbviewer/shared/_tables_sidebar.html.erb +1 -17
- data/app/views/layouts/dbviewer/application.html.erb +1 -0
- data/app/views/layouts/dbviewer/shared/_sidebar.html.erb +0 -243
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +2 -0
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f1d51130bb3ed8d46ab3602679788ee98812bce9dbab988e32999c3b50f445fe
|
4
|
+
data.tar.gz: 53aa2f65c0dafce3d118de8801a69cd233b4c437eaccc9974b609dce12591148
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c1566aef41e200d6b1e61cdb665381480eb592e3e738cf3f74c2c8be2cdc9cf8f616ed63b2e9883a286fd41b1f0e8708bf05875e0a6beaea796e22bc40d9afeb
|
7
|
+
data.tar.gz: 1b84dfca22a4421f437e02f85d5c290d37f9a48dedd4e0f5f309c4e64a3d1f06d3de916a043cc448061f320fc31899d4c9346252e6ac907680ef7499c6e49c9f
|
@@ -100,37 +100,35 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
100
100
|
let relationshipsLoaded = false;
|
101
101
|
|
102
102
|
// Function to fetch relationships asynchronously
|
103
|
-
function fetchRelationships() {
|
103
|
+
async function fetchRelationships() {
|
104
104
|
const apiPath = document.getElementById("relationships_api_path").value;
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
.then((response) => {
|
112
|
-
if (!response.ok) {
|
113
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
114
|
-
}
|
115
|
-
return response.json();
|
116
|
-
})
|
117
|
-
.then((data) => {
|
118
|
-
relationships = data.relationships || [];
|
119
|
-
relationshipsLoaded = true;
|
120
|
-
updateRelationshipsStatus(true);
|
121
|
-
return relationships;
|
122
|
-
})
|
123
|
-
.catch((error) => {
|
124
|
-
console.error("Error fetching relationships:", error);
|
125
|
-
relationshipsLoaded = true; // Mark as loaded even on error to prevent infinite loading
|
126
|
-
updateRelationshipsStatus(true);
|
127
|
-
return [];
|
105
|
+
try {
|
106
|
+
const response = await fetch(apiPath, {
|
107
|
+
headers: {
|
108
|
+
Accept: "application/json",
|
109
|
+
"X-Requested-With": "XMLHttpRequest",
|
110
|
+
},
|
128
111
|
});
|
112
|
+
|
113
|
+
if (!response.ok) {
|
114
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
115
|
+
}
|
116
|
+
|
117
|
+
const data = await response.json();
|
118
|
+
relationships = data.relationships || [];
|
119
|
+
relationshipsLoaded = true;
|
120
|
+
updateRelationshipsStatus(true);
|
121
|
+
return relationships;
|
122
|
+
} catch (error) {
|
123
|
+
console.error("Error fetching relationships:", error);
|
124
|
+
relationshipsLoaded = true; // Mark as loaded even on error to prevent infinite loading
|
125
|
+
updateRelationshipsStatus(true);
|
126
|
+
return [];
|
127
|
+
}
|
129
128
|
}
|
130
129
|
|
131
130
|
// Function to update loading status
|
132
131
|
function updateLoadingStatus(message) {
|
133
|
-
const loadingElement = document.getElementById("erd-loading");
|
134
132
|
const loadingPhase = document.getElementById("loading-phase");
|
135
133
|
if (loadingPhase) {
|
136
134
|
loadingPhase.textContent = message;
|
@@ -196,6 +194,35 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
196
194
|
const tablePath = document.getElementById("tables_path").value;
|
197
195
|
|
198
196
|
// First pass: add all tables with minimal info and start loading columns
|
197
|
+
// Function to fetch column data for a table
|
198
|
+
async function fetchTableColumns(tableName) {
|
199
|
+
try {
|
200
|
+
const response = await fetch(`${tablePath}/${tableName}?format=json`, {
|
201
|
+
headers: {
|
202
|
+
Accept: "application/json",
|
203
|
+
"X-Requested-With": "XMLHttpRequest",
|
204
|
+
},
|
205
|
+
});
|
206
|
+
|
207
|
+
const data = await response.json();
|
208
|
+
|
209
|
+
if (data && data.columns) {
|
210
|
+
tableColumns[tableName] = data.columns;
|
211
|
+
columnsLoadedCount++;
|
212
|
+
|
213
|
+
// Update progress bar
|
214
|
+
updateTableProgress(columnsLoadedCount, totalTables);
|
215
|
+
|
216
|
+
checkIfReadyToUpdate();
|
217
|
+
}
|
218
|
+
} catch (error) {
|
219
|
+
console.error(`Error fetching columns for table ${tableName}:`, error);
|
220
|
+
columnsLoadedCount++;
|
221
|
+
updateTableProgress(columnsLoadedCount, totalTables);
|
222
|
+
checkIfReadyToUpdate();
|
223
|
+
}
|
224
|
+
}
|
225
|
+
|
199
226
|
tables.forEach(function (table) {
|
200
227
|
const tableName = table.name;
|
201
228
|
mermaidDefinition += ` ${tableName} {\n`;
|
@@ -203,30 +230,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|
203
230
|
mermaidDefinition += " }\n";
|
204
231
|
|
205
232
|
// Start loading column data asynchronously
|
206
|
-
|
207
|
-
headers: {
|
208
|
-
Accept: "application/json",
|
209
|
-
"X-Requested-With": "XMLHttpRequest",
|
210
|
-
},
|
211
|
-
})
|
212
|
-
.then((response) => response.json())
|
213
|
-
.then((data) => {
|
214
|
-
if (data && data.columns) {
|
215
|
-
tableColumns[tableName] = data.columns;
|
216
|
-
columnsLoadedCount++;
|
217
|
-
|
218
|
-
// Update progress bar
|
219
|
-
updateTableProgress(columnsLoadedCount, totalTables);
|
220
|
-
|
221
|
-
checkIfReadyToUpdate();
|
222
|
-
}
|
223
|
-
})
|
224
|
-
.catch((error) => {
|
225
|
-
console.error(`Error fetching columns for table ${tableName}:`, error);
|
226
|
-
columnsLoadedCount++;
|
227
|
-
updateTableProgress(columnsLoadedCount, totalTables);
|
228
|
-
checkIfReadyToUpdate();
|
229
|
-
});
|
233
|
+
fetchTableColumns(tableName);
|
230
234
|
});
|
231
235
|
|
232
236
|
// Function to check if we're ready to update the diagram with full data
|
@@ -0,0 +1,210 @@
|
|
1
|
+
document.addEventListener("DOMContentLoaded", function () {
|
2
|
+
const searchInput = document.getElementById("tableSearch");
|
3
|
+
const sidebarContent = document.querySelector(".dbviewer-sidebar-content");
|
4
|
+
|
5
|
+
// Storage keys for persistence
|
6
|
+
const STORAGE_KEYS = {
|
7
|
+
searchFilter: "dbviewer_sidebar_search_filter",
|
8
|
+
scrollPosition: "dbviewer_sidebar_scroll_position",
|
9
|
+
};
|
10
|
+
|
11
|
+
if (searchInput) {
|
12
|
+
// Debounce function to limit how often the filter runs
|
13
|
+
function debounce(func, wait) {
|
14
|
+
let timeout;
|
15
|
+
return function () {
|
16
|
+
const context = this;
|
17
|
+
const args = arguments;
|
18
|
+
clearTimeout(timeout);
|
19
|
+
timeout = setTimeout(function () {
|
20
|
+
func.apply(context, args);
|
21
|
+
}, wait);
|
22
|
+
};
|
23
|
+
}
|
24
|
+
|
25
|
+
// Filter function
|
26
|
+
const filterTables = debounce(function () {
|
27
|
+
const query = searchInput.value.toLowerCase();
|
28
|
+
const tableItems = document.querySelectorAll(
|
29
|
+
"#tablesList .list-group-item-action"
|
30
|
+
);
|
31
|
+
let visibleCount = 0;
|
32
|
+
|
33
|
+
// Save the current search filter to localStorage
|
34
|
+
localStorage.setItem(STORAGE_KEYS.searchFilter, searchInput.value);
|
35
|
+
|
36
|
+
tableItems.forEach(function (item) {
|
37
|
+
// Get the table name from the title attribute for more accurate matching
|
38
|
+
const tableName = (item.getAttribute("title") || item.textContent)
|
39
|
+
.trim()
|
40
|
+
.toLowerCase();
|
41
|
+
|
42
|
+
// Also get the displayed text content for a broader match
|
43
|
+
const displayedText = item.textContent.trim().toLowerCase();
|
44
|
+
|
45
|
+
if (tableName.includes(query) || displayedText.includes(query)) {
|
46
|
+
item.classList.remove("d-none");
|
47
|
+
visibleCount++;
|
48
|
+
} else {
|
49
|
+
item.classList.add("d-none");
|
50
|
+
}
|
51
|
+
});
|
52
|
+
|
53
|
+
// Update the tables count in the sidebar
|
54
|
+
const tableCountElement = document.getElementById("table-count");
|
55
|
+
if (tableCountElement) {
|
56
|
+
tableCountElement.textContent = visibleCount;
|
57
|
+
}
|
58
|
+
|
59
|
+
// Show/hide no results message
|
60
|
+
let noResultsEl = document.getElementById("dbviewer-no-filter-results");
|
61
|
+
if (visibleCount === 0 && query !== "") {
|
62
|
+
if (!noResultsEl) {
|
63
|
+
noResultsEl = document.createElement("div");
|
64
|
+
noResultsEl.id = "dbviewer-no-filter-results";
|
65
|
+
noResultsEl.className = "list-group-item text-muted text-center py-3";
|
66
|
+
noResultsEl.innerHTML =
|
67
|
+
'<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
|
68
|
+
document.getElementById("tablesList").appendChild(noResultsEl);
|
69
|
+
}
|
70
|
+
noResultsEl.querySelector(".fw-bold").textContent = query;
|
71
|
+
noResultsEl.style.display = "block";
|
72
|
+
} else if (noResultsEl) {
|
73
|
+
noResultsEl.style.display = "none";
|
74
|
+
}
|
75
|
+
}, 150); // Debounce for 150ms
|
76
|
+
|
77
|
+
// Set up clear button first
|
78
|
+
const clearButton = document.createElement("button");
|
79
|
+
clearButton.type = "button";
|
80
|
+
clearButton.className = "btn btn-sm btn-link position-absolute";
|
81
|
+
clearButton.style.right = "15px";
|
82
|
+
clearButton.style.top = "50%";
|
83
|
+
clearButton.style.transform = "translateY(-50%)";
|
84
|
+
clearButton.style.display = "none";
|
85
|
+
clearButton.style.color = "#6c757d";
|
86
|
+
clearButton.style.fontSize = "0.85rem";
|
87
|
+
clearButton.style.padding = "0.25rem";
|
88
|
+
clearButton.style.width = "1.5rem";
|
89
|
+
clearButton.style.textAlign = "center";
|
90
|
+
clearButton.innerHTML = '<i class="bi bi-x-circle"></i>';
|
91
|
+
clearButton.addEventListener("click", function () {
|
92
|
+
searchInput.value = "";
|
93
|
+
// Clear the saved filter from localStorage
|
94
|
+
localStorage.removeItem(STORAGE_KEYS.searchFilter);
|
95
|
+
// Call filter directly without debouncing for immediate feedback
|
96
|
+
filterTables();
|
97
|
+
this.style.display = "none";
|
98
|
+
});
|
99
|
+
|
100
|
+
const filterContainer = document.querySelector(
|
101
|
+
".dbviewer-table-filter-container"
|
102
|
+
);
|
103
|
+
if (filterContainer) {
|
104
|
+
filterContainer.style.position = "relative";
|
105
|
+
filterContainer.appendChild(clearButton);
|
106
|
+
|
107
|
+
searchInput.addEventListener("input", function () {
|
108
|
+
clearButton.style.display = this.value ? "block" : "none";
|
109
|
+
});
|
110
|
+
}
|
111
|
+
|
112
|
+
// Restore saved search filter on page load and apply it immediately
|
113
|
+
const savedFilter = localStorage.getItem(STORAGE_KEYS.searchFilter);
|
114
|
+
if (savedFilter) {
|
115
|
+
searchInput.value = savedFilter;
|
116
|
+
// Show clear button immediately when filter is restored
|
117
|
+
clearButton.style.display = "block";
|
118
|
+
// Apply filter immediately without debouncing to prevent blinking
|
119
|
+
const query = savedFilter.toLowerCase();
|
120
|
+
const tableItems = document.querySelectorAll(
|
121
|
+
"#tablesList .list-group-item-action"
|
122
|
+
);
|
123
|
+
let visibleCount = 0;
|
124
|
+
|
125
|
+
tableItems.forEach(function (item) {
|
126
|
+
const tableName = (item.getAttribute("title") || item.textContent)
|
127
|
+
.trim()
|
128
|
+
.toLowerCase();
|
129
|
+
const displayedText = item.textContent.trim().toLowerCase();
|
130
|
+
|
131
|
+
if (tableName.includes(query) || displayedText.includes(query)) {
|
132
|
+
item.classList.remove("d-none");
|
133
|
+
visibleCount++;
|
134
|
+
} else {
|
135
|
+
item.classList.add("d-none");
|
136
|
+
}
|
137
|
+
});
|
138
|
+
|
139
|
+
// Update the tables count immediately
|
140
|
+
const tableCountElement = document.getElementById("table-count");
|
141
|
+
if (tableCountElement) {
|
142
|
+
tableCountElement.textContent = visibleCount;
|
143
|
+
}
|
144
|
+
|
145
|
+
// Handle no results message immediately
|
146
|
+
let noResultsEl = document.getElementById("dbviewer-no-filter-results");
|
147
|
+
if (visibleCount === 0 && query !== "") {
|
148
|
+
if (!noResultsEl) {
|
149
|
+
noResultsEl = document.createElement("div");
|
150
|
+
noResultsEl.id = "dbviewer-no-filter-results";
|
151
|
+
noResultsEl.className = "list-group-item text-muted text-center py-3";
|
152
|
+
noResultsEl.innerHTML =
|
153
|
+
'<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
|
154
|
+
document.getElementById("tablesList").appendChild(noResultsEl);
|
155
|
+
}
|
156
|
+
noResultsEl.querySelector(".fw-bold").textContent = query;
|
157
|
+
noResultsEl.style.display = "block";
|
158
|
+
} else if (noResultsEl) {
|
159
|
+
noResultsEl.style.display = "none";
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
// Restore saved scroll position on page load
|
164
|
+
if (sidebarContent) {
|
165
|
+
const savedScrollPosition = localStorage.getItem(
|
166
|
+
STORAGE_KEYS.scrollPosition
|
167
|
+
);
|
168
|
+
if (savedScrollPosition) {
|
169
|
+
// Use requestAnimationFrame to ensure DOM is fully rendered
|
170
|
+
requestAnimationFrame(() => {
|
171
|
+
sidebarContent.scrollTop = parseInt(savedScrollPosition, 10);
|
172
|
+
});
|
173
|
+
}
|
174
|
+
|
175
|
+
// Save scroll position on scroll
|
176
|
+
const saveScrollPosition = debounce(function () {
|
177
|
+
localStorage.setItem(
|
178
|
+
STORAGE_KEYS.scrollPosition,
|
179
|
+
sidebarContent.scrollTop
|
180
|
+
);
|
181
|
+
}, 100);
|
182
|
+
|
183
|
+
sidebarContent.addEventListener("scroll", saveScrollPosition);
|
184
|
+
}
|
185
|
+
|
186
|
+
// Set up event listeners for the search input
|
187
|
+
searchInput.addEventListener("input", filterTables);
|
188
|
+
searchInput.addEventListener("keyup", function (e) {
|
189
|
+
filterTables();
|
190
|
+
|
191
|
+
// Add keyboard navigation for the filtered list
|
192
|
+
if (e.key === "Enter" || e.key === "ArrowDown") {
|
193
|
+
e.preventDefault();
|
194
|
+
// Focus the first visible table item (not having d-none class)
|
195
|
+
const firstVisibleItem = document.querySelector(
|
196
|
+
"#tablesList .list-group-item-action:not(.d-none)"
|
197
|
+
);
|
198
|
+
if (firstVisibleItem) {
|
199
|
+
firstVisibleItem.focus();
|
200
|
+
// Make sure the item is visible in the scrollable area
|
201
|
+
firstVisibleItem.scrollIntoView({
|
202
|
+
behavior: "smooth",
|
203
|
+
block: "nearest",
|
204
|
+
});
|
205
|
+
}
|
206
|
+
}
|
207
|
+
});
|
208
|
+
searchInput.addEventListener("search", filterTables); // For clearing via the "x" in some browsers
|
209
|
+
}
|
210
|
+
});
|
@@ -11,23 +11,7 @@
|
|
11
11
|
title: table[:name],
|
12
12
|
class: "list-group-item list-group-item-action d-flex align-items-center #{'active' if current_table?(table[:name])}",
|
13
13
|
tabindex: "0",
|
14
|
-
data: { table_name: table[:name] }
|
15
|
-
onkeydown: "
|
16
|
-
if(event.key === 'ArrowDown') {
|
17
|
-
event.preventDefault();
|
18
|
-
let next = this.nextElementSibling;
|
19
|
-
while(next && next.classList.contains('d-none')) {
|
20
|
-
next = next.nextElementSibling;
|
21
|
-
}
|
22
|
-
if(next) next.focus();
|
23
|
-
} else if(event.key === 'ArrowUp') {
|
24
|
-
event.preventDefault();
|
25
|
-
let prev = this.previousElementSibling;
|
26
|
-
while(prev && prev.classList.contains('d-none')) {
|
27
|
-
prev = prev.previousElementSibling;
|
28
|
-
}
|
29
|
-
if(prev) prev.focus();
|
30
|
-
}" do %>
|
14
|
+
data: { table_name: table[:name] } do %>
|
31
15
|
<div class="d-flex justify-content-between align-items-center w-100">
|
32
16
|
<div class="text-truncate">
|
33
17
|
<i class="bi bi-table me-2 small"></i>
|
@@ -38,6 +38,7 @@
|
|
38
38
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
39
39
|
|
40
40
|
<%= javascript_include_tag "dbviewer/layout", "data-turbo-track": "reload" %>
|
41
|
+
<%= javascript_include_tag "dbviewer/sidebar", "data-turbo-track": "reload" %>
|
41
42
|
<%= stylesheet_link_tag "dbviewer/application", "data-turbo-track": "reload" %>
|
42
43
|
|
43
44
|
<%= yield :head %>
|
@@ -3,58 +3,6 @@
|
|
3
3
|
<input type="text" class="form-control form-control-sm dbviewer-table-filter mb-0"
|
4
4
|
id="tableSearch" placeholder="Filter tables..." aria-label="Filter tables">
|
5
5
|
</div>
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
<!-- Add custom styling for datetime inputs -->
|
10
|
-
<style>
|
11
|
-
/* Better datetime input styling */
|
12
|
-
input[type="datetime-local"] {
|
13
|
-
padding-right: 0.5rem;
|
14
|
-
}
|
15
|
-
|
16
|
-
/* Dark mode support for datetime inputs */
|
17
|
-
[data-bs-theme="dark"] input[type="datetime-local"] {
|
18
|
-
background-color: rgba(255,255,255,0.1);
|
19
|
-
color: #fff;
|
20
|
-
border-color: rgba(255,255,255,0.15);
|
21
|
-
}
|
22
|
-
|
23
|
-
[data-bs-theme="dark"] input[type="datetime-local"]::-webkit-calendar-picker-indicator {
|
24
|
-
filter: invert(1);
|
25
|
-
}
|
26
|
-
</style>
|
27
|
-
|
28
|
-
<script>
|
29
|
-
// Set default values for datetime inputs when empty
|
30
|
-
document.addEventListener('DOMContentLoaded', function() {
|
31
|
-
const startInput = document.getElementById('creationFilterStart');
|
32
|
-
const endInput = document.getElementById('creationFilterEnd');
|
33
|
-
|
34
|
-
// When applying filter with empty start date, default to beginning of current month
|
35
|
-
if (startInput) {
|
36
|
-
startInput.addEventListener('click', function() {
|
37
|
-
if (!this.value) {
|
38
|
-
const now = new Date();
|
39
|
-
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
40
|
-
const formattedDate = firstDay.toISOString().slice(0, 16); // Format: YYYY-MM-DDTHH:MM
|
41
|
-
this.value = formattedDate;
|
42
|
-
}
|
43
|
-
});
|
44
|
-
}
|
45
|
-
|
46
|
-
// When applying filter with empty end date, default to current datetime
|
47
|
-
if (endInput) {
|
48
|
-
endInput.addEventListener('click', function() {
|
49
|
-
if (!this.value) {
|
50
|
-
const now = new Date();
|
51
|
-
const formattedDate = now.toISOString().slice(0, 16); // Format: YYYY-MM-DDTHH:MM
|
52
|
-
this.value = formattedDate;
|
53
|
-
}
|
54
|
-
});
|
55
|
-
}
|
56
|
-
});
|
57
|
-
</script>
|
58
6
|
</div>
|
59
7
|
|
60
8
|
<div class="dbviewer-sidebar-content">
|
@@ -115,194 +63,3 @@
|
|
115
63
|
<i class="bi bi-gem me-1"></i>
|
116
64
|
DBViewer v<%= Dbviewer::VERSION %>
|
117
65
|
</div>
|
118
|
-
|
119
|
-
<script>
|
120
|
-
document.addEventListener('DOMContentLoaded', function() {
|
121
|
-
const searchInput = document.getElementById('tableSearch');
|
122
|
-
const sidebarContent = document.querySelector('.dbviewer-sidebar-content');
|
123
|
-
|
124
|
-
// Storage keys for persistence
|
125
|
-
const STORAGE_KEYS = {
|
126
|
-
searchFilter: 'dbviewer_sidebar_search_filter',
|
127
|
-
scrollPosition: 'dbviewer_sidebar_scroll_position'
|
128
|
-
};
|
129
|
-
|
130
|
-
if (searchInput) {
|
131
|
-
// Debounce function to limit how often the filter runs
|
132
|
-
function debounce(func, wait) {
|
133
|
-
let timeout;
|
134
|
-
return function() {
|
135
|
-
const context = this;
|
136
|
-
const args = arguments;
|
137
|
-
clearTimeout(timeout);
|
138
|
-
timeout = setTimeout(function() {
|
139
|
-
func.apply(context, args);
|
140
|
-
}, wait);
|
141
|
-
};
|
142
|
-
}
|
143
|
-
|
144
|
-
// Filter function
|
145
|
-
const filterTables = debounce(function() {
|
146
|
-
const query = searchInput.value.toLowerCase();
|
147
|
-
const tableItems = document.querySelectorAll('#tablesList .list-group-item-action');
|
148
|
-
let visibleCount = 0;
|
149
|
-
|
150
|
-
// Save the current search filter to localStorage
|
151
|
-
localStorage.setItem(STORAGE_KEYS.searchFilter, searchInput.value);
|
152
|
-
|
153
|
-
tableItems.forEach(function(item) {
|
154
|
-
// Get the table name from the title attribute for more accurate matching
|
155
|
-
const tableName = (item.getAttribute('title') || item.textContent).trim().toLowerCase();
|
156
|
-
|
157
|
-
// Also get the displayed text content for a broader match
|
158
|
-
const displayedText = item.textContent.trim().toLowerCase();
|
159
|
-
|
160
|
-
if (tableName.includes(query) || displayedText.includes(query)) {
|
161
|
-
item.classList.remove('d-none');
|
162
|
-
visibleCount++;
|
163
|
-
} else {
|
164
|
-
item.classList.add('d-none');
|
165
|
-
}
|
166
|
-
});
|
167
|
-
|
168
|
-
// Update the tables count in the sidebar
|
169
|
-
const tableCountElement = document.getElementById('table-count');
|
170
|
-
if (tableCountElement) {
|
171
|
-
tableCountElement.textContent = visibleCount;
|
172
|
-
}
|
173
|
-
|
174
|
-
// Show/hide no results message
|
175
|
-
let noResultsEl = document.getElementById('dbviewer-no-filter-results');
|
176
|
-
if (visibleCount === 0 && query !== '') {
|
177
|
-
if (!noResultsEl) {
|
178
|
-
noResultsEl = document.createElement('div');
|
179
|
-
noResultsEl.id = 'dbviewer-no-filter-results';
|
180
|
-
noResultsEl.className = 'list-group-item text-muted text-center py-3';
|
181
|
-
noResultsEl.innerHTML = '<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
|
182
|
-
document.getElementById('tablesList').appendChild(noResultsEl);
|
183
|
-
}
|
184
|
-
noResultsEl.querySelector('.fw-bold').textContent = query;
|
185
|
-
noResultsEl.style.display = 'block';
|
186
|
-
} else if (noResultsEl) {
|
187
|
-
noResultsEl.style.display = 'none';
|
188
|
-
}
|
189
|
-
}, 150); // Debounce for 150ms
|
190
|
-
|
191
|
-
// Set up clear button first
|
192
|
-
const clearButton = document.createElement('button');
|
193
|
-
clearButton.type = 'button';
|
194
|
-
clearButton.className = 'btn btn-sm btn-link position-absolute';
|
195
|
-
clearButton.style.right = '15px';
|
196
|
-
clearButton.style.top = '50%';
|
197
|
-
clearButton.style.transform = 'translateY(-50%)';
|
198
|
-
clearButton.style.display = 'none';
|
199
|
-
clearButton.style.color = '#6c757d';
|
200
|
-
clearButton.style.fontSize = '0.85rem';
|
201
|
-
clearButton.style.padding = '0.25rem';
|
202
|
-
clearButton.style.width = '1.5rem';
|
203
|
-
clearButton.style.textAlign = 'center';
|
204
|
-
clearButton.innerHTML = '<i class="bi bi-x-circle"></i>';
|
205
|
-
clearButton.addEventListener('click', function() {
|
206
|
-
searchInput.value = '';
|
207
|
-
// Clear the saved filter from localStorage
|
208
|
-
localStorage.removeItem(STORAGE_KEYS.searchFilter);
|
209
|
-
// Call filter directly without debouncing for immediate feedback
|
210
|
-
filterTables();
|
211
|
-
this.style.display = 'none';
|
212
|
-
});
|
213
|
-
|
214
|
-
const filterContainer = document.querySelector('.dbviewer-table-filter-container');
|
215
|
-
if (filterContainer) {
|
216
|
-
filterContainer.style.position = 'relative';
|
217
|
-
filterContainer.appendChild(clearButton);
|
218
|
-
|
219
|
-
searchInput.addEventListener('input', function() {
|
220
|
-
clearButton.style.display = this.value ? 'block' : 'none';
|
221
|
-
});
|
222
|
-
}
|
223
|
-
|
224
|
-
// Restore saved search filter on page load and apply it immediately
|
225
|
-
const savedFilter = localStorage.getItem(STORAGE_KEYS.searchFilter);
|
226
|
-
if (savedFilter) {
|
227
|
-
searchInput.value = savedFilter;
|
228
|
-
// Show clear button immediately when filter is restored
|
229
|
-
clearButton.style.display = 'block';
|
230
|
-
// Apply filter immediately without debouncing to prevent blinking
|
231
|
-
const query = savedFilter.toLowerCase();
|
232
|
-
const tableItems = document.querySelectorAll('#tablesList .list-group-item-action');
|
233
|
-
let visibleCount = 0;
|
234
|
-
|
235
|
-
tableItems.forEach(function(item) {
|
236
|
-
const tableName = (item.getAttribute('title') || item.textContent).trim().toLowerCase();
|
237
|
-
const displayedText = item.textContent.trim().toLowerCase();
|
238
|
-
|
239
|
-
if (tableName.includes(query) || displayedText.includes(query)) {
|
240
|
-
item.classList.remove('d-none');
|
241
|
-
visibleCount++;
|
242
|
-
} else {
|
243
|
-
item.classList.add('d-none');
|
244
|
-
}
|
245
|
-
});
|
246
|
-
|
247
|
-
// Update the tables count immediately
|
248
|
-
const tableCountElement = document.getElementById('table-count');
|
249
|
-
if (tableCountElement) {
|
250
|
-
tableCountElement.textContent = visibleCount;
|
251
|
-
}
|
252
|
-
|
253
|
-
// Handle no results message immediately
|
254
|
-
let noResultsEl = document.getElementById('dbviewer-no-filter-results');
|
255
|
-
if (visibleCount === 0 && query !== '') {
|
256
|
-
if (!noResultsEl) {
|
257
|
-
noResultsEl = document.createElement('div');
|
258
|
-
noResultsEl.id = 'dbviewer-no-filter-results';
|
259
|
-
noResultsEl.className = 'list-group-item text-muted text-center py-3';
|
260
|
-
noResultsEl.innerHTML = '<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
|
261
|
-
document.getElementById('tablesList').appendChild(noResultsEl);
|
262
|
-
}
|
263
|
-
noResultsEl.querySelector('.fw-bold').textContent = query;
|
264
|
-
noResultsEl.style.display = 'block';
|
265
|
-
} else if (noResultsEl) {
|
266
|
-
noResultsEl.style.display = 'none';
|
267
|
-
}
|
268
|
-
}
|
269
|
-
|
270
|
-
// Restore saved scroll position on page load
|
271
|
-
if (sidebarContent) {
|
272
|
-
const savedScrollPosition = localStorage.getItem(STORAGE_KEYS.scrollPosition);
|
273
|
-
if (savedScrollPosition) {
|
274
|
-
// Use requestAnimationFrame to ensure DOM is fully rendered
|
275
|
-
requestAnimationFrame(() => {
|
276
|
-
sidebarContent.scrollTop = parseInt(savedScrollPosition, 10);
|
277
|
-
});
|
278
|
-
}
|
279
|
-
|
280
|
-
// Save scroll position on scroll
|
281
|
-
const saveScrollPosition = debounce(function() {
|
282
|
-
localStorage.setItem(STORAGE_KEYS.scrollPosition, sidebarContent.scrollTop);
|
283
|
-
}, 100);
|
284
|
-
|
285
|
-
sidebarContent.addEventListener('scroll', saveScrollPosition);
|
286
|
-
}
|
287
|
-
|
288
|
-
// Set up event listeners for the search input
|
289
|
-
searchInput.addEventListener('input', filterTables);
|
290
|
-
searchInput.addEventListener('keyup', function(e) {
|
291
|
-
filterTables();
|
292
|
-
|
293
|
-
// Add keyboard navigation for the filtered list
|
294
|
-
if (e.key === 'Enter' || e.key === 'ArrowDown') {
|
295
|
-
e.preventDefault();
|
296
|
-
// Focus the first visible table item (not having d-none class)
|
297
|
-
const firstVisibleItem = document.querySelector('#tablesList .list-group-item-action:not(.d-none)');
|
298
|
-
if (firstVisibleItem) {
|
299
|
-
firstVisibleItem.focus();
|
300
|
-
// Make sure the item is visible in the scrollable area
|
301
|
-
firstVisibleItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
302
|
-
}
|
303
|
-
}
|
304
|
-
});
|
305
|
-
searchInput.addEventListener('search', filterTables); // For clearing via the "x" in some browsers
|
306
|
-
}
|
307
|
-
});
|
308
|
-
</script>
|
data/lib/dbviewer/version.rb
CHANGED
data/lib/dbviewer.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dbviewer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wailan Tirajoh
|
@@ -67,6 +67,7 @@ files:
|
|
67
67
|
- app/assets/javascripts/dbviewer/home.js
|
68
68
|
- app/assets/javascripts/dbviewer/layout.js
|
69
69
|
- app/assets/javascripts/dbviewer/query.js
|
70
|
+
- app/assets/javascripts/dbviewer/sidebar.js
|
70
71
|
- app/assets/javascripts/dbviewer/table.js
|
71
72
|
- app/assets/stylesheets/dbviewer/application.css
|
72
73
|
- app/assets/stylesheets/dbviewer/entity_relationship_diagram.css
|