12312
This commit is contained in:
167
index.html
167
index.html
@@ -162,6 +162,10 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-3" id="recordControls">
|
||||
<input id="recordSearch" type="text" placeholder="Поиск по записям..." class="w-56 border border-slate-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
<button onclick="app.showFiltersModal()" class="flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors text-sm">
|
||||
<i data-lucide="filter" class="w-4 h-4"></i>
|
||||
Фильтры
|
||||
</button>
|
||||
<div class="text-sm text-slate-600" id="recordCount">
|
||||
<!-- Record count will be shown here -->
|
||||
</div>
|
||||
@@ -389,49 +393,32 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Column Modal -->
|
||||
<div id="columnModal" class="hidden fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md fade-in">
|
||||
<div class="p-6 border-b border-slate-200">
|
||||
<h3 id="columnModalTitle" class="text-xl font-bold text-slate-800">Добавить колонку</h3>
|
||||
<!-- Filters Modal -->
|
||||
<div id="filtersModal" class="hidden fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col fade-in">
|
||||
<div class="p-6 border-b border-slate-200 flex items-center justify-between">
|
||||
<h3 class="text-xl font-bold text-slate-800">Фильтры</h3>
|
||||
<button onclick="app.closeModal('filtersModal')" class="p-2 hover:bg-slate-100 rounded-lg transition-colors">
|
||||
<i data-lucide="x" class="w-5 h-5 text-slate-500"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Название колонки</label>
|
||||
<input type="text" id="columnName" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none" placeholder="column_name">
|
||||
<div class="p-6 overflow-y-auto flex-1">
|
||||
<div id="filtersContainer" class="space-y-4">
|
||||
<!-- Filters will be added here -->
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Тип данных</label>
|
||||
<select id="columnType" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<option value="VARCHAR(255)">VARCHAR(255)</option>
|
||||
<option value="TEXT">TEXT</option>
|
||||
<option value="INTEGER">INTEGER</option>
|
||||
<option value="BIGINT">BIGINT</option>
|
||||
<option value="DECIMAL">DECIMAL</option>
|
||||
<option value="BOOLEAN">BOOLEAN</option>
|
||||
<option value="DATE">DATE</option>
|
||||
<option value="TIMESTAMP">TIMESTAMP</option>
|
||||
<option value="UUID">UUID</option>
|
||||
<option value="JSON">JSON</option>
|
||||
<option value="JSONB">JSONB</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="checkbox" id="columnNullable" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-500">
|
||||
<label for="columnNullable" class="text-sm text-slate-700">Разрешить NULL</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Значение по умолчанию</label>
|
||||
<input type="text" id="columnDefault" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none" placeholder="NULL">
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="checkbox" id="columnPrimary" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-500">
|
||||
<label for="columnPrimary" class="text-sm text-slate-700">Первичный ключ</label>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<button onclick="app.addFilter()" class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
Добавить фильтр
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 border-t border-slate-200 bg-slate-50 flex justify-end gap-3">
|
||||
<button onclick="app.closeModal('columnModal')" class="px-4 py-2 text-slate-600 hover:bg-slate-200 rounded-lg">Отмена</button>
|
||||
<button onclick="app.saveColumn()" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">Сохранить</button>
|
||||
<div class="p-6 border-t border-slate-200 bg-slate-50 flex justify-between">
|
||||
<button onclick="app.clearFilters()" class="px-4 py-2 text-slate-600 hover:bg-slate-200 rounded-lg">Очистить</button>
|
||||
<div class="flex gap-3">
|
||||
<button onclick="app.closeModal('filtersModal')" class="px-4 py-2 text-slate-600 hover:bg-slate-200 rounded-lg">Отмена</button>
|
||||
<button onclick="app.applyFilters()" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">Применить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -452,6 +439,9 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
||||
this.editingColumn = null;
|
||||
this.primaryKey = null;
|
||||
this.tableStructure = [];
|
||||
this.filters = [];
|
||||
this.sortColumn = '';
|
||||
this.sortDirection = 'ASC';
|
||||
this.init();
|
||||
}
|
||||
|
||||
@@ -598,7 +588,10 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
||||
const params = new URLSearchParams({
|
||||
page: this.currentPage,
|
||||
limit: this.limit,
|
||||
search: searchQuery
|
||||
search: searchQuery,
|
||||
filters: JSON.stringify(this.filters),
|
||||
sortColumn: this.sortColumn,
|
||||
sortDirection: this.sortDirection
|
||||
});
|
||||
|
||||
const response = await fetch(`/api/tables/${this.currentTable}/data?${params}`);
|
||||
@@ -623,7 +616,11 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
||||
|
||||
// Generate headers
|
||||
const columns = Object.keys(records[0]);
|
||||
headers.innerHTML = columns.map(col => `<th class="text-left p-3">${col}</th>`).join('') + '<th class="text-left p-3">Действия</th>';
|
||||
headers.innerHTML = columns.map(col => {
|
||||
const isSorted = this.sortColumn === col;
|
||||
const arrow = isSorted ? (this.sortDirection === 'ASC' ? '↑' : '↓') : '';
|
||||
return `<th class="text-left p-3 cursor-pointer hover:bg-slate-100 select-none" onclick="app.sortBy('${col}')">${col} ${arrow}</th>`;
|
||||
}).join('') + '<th class="text-left p-3">Действия</th>';
|
||||
|
||||
// Generate rows
|
||||
body.innerHTML = records.map(record => {
|
||||
@@ -647,6 +644,23 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
||||
}).join('');
|
||||
}
|
||||
|
||||
sortBy(column) {
|
||||
if (this.sortColumn === column) {
|
||||
if (this.sortDirection === 'ASC') {
|
||||
this.sortDirection = 'DESC';
|
||||
} else {
|
||||
// Third click: remove sorting
|
||||
this.sortColumn = '';
|
||||
this.sortDirection = 'ASC';
|
||||
}
|
||||
} else {
|
||||
this.sortColumn = column;
|
||||
this.sortDirection = 'ASC';
|
||||
}
|
||||
this.currentPage = 1;
|
||||
this.loadTableData();
|
||||
}
|
||||
|
||||
editRecord(pkValue) {
|
||||
// Find the record by primary key
|
||||
// Since we have the data, we can find it
|
||||
@@ -1176,6 +1190,75 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
||||
}
|
||||
}
|
||||
|
||||
showFiltersModal() {
|
||||
this.renderFilters();
|
||||
document.getElementById('filtersModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
renderFilters() {
|
||||
const container = document.getElementById('filtersContainer');
|
||||
container.innerHTML = this.filters.map((filter, index) => `
|
||||
<div class="flex items-center gap-2 p-3 bg-slate-50 rounded-lg">
|
||||
<select class="px-3 py-2 border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none" onchange="app.updateFilterColumn(${index}, this.value)">
|
||||
<option value="">Выберите колонку</option>
|
||||
${this.tableStructure.map(col => `<option value="${col.name}" ${filter.column === col.name ? 'selected' : ''}>${col.name}</option>`).join('')}
|
||||
</select>
|
||||
<select class="px-3 py-2 border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none" onchange="app.updateFilterOperator(${index}, this.value)">
|
||||
<option value="=" ${filter.operator === '=' ? 'selected' : ''}>=</option>
|
||||
<option value="!=" ${filter.operator === '!=' ? 'selected' : ''}>!=</option>
|
||||
<option value=">" ${filter.operator === '>' ? 'selected' : ''}>></option>
|
||||
<option value="<" ${filter.operator === '<' ? 'selected' : ''}><</option>
|
||||
<option value=">=" ${filter.operator === '>=' ? 'selected' : ''}>>=</option>
|
||||
<option value="<=" ${filter.operator === '<=' ? 'selected' : ''}><=</option>
|
||||
<option value="LIKE" ${filter.operator === 'LIKE' ? 'selected' : ''}>Содержит</option>
|
||||
<option value="ILIKE" ${filter.operator === 'ILIKE' ? 'selected' : ''}>Содержит (регистронезависимо)</option>
|
||||
<option value="IS NULL" ${filter.operator === 'IS NULL' ? 'selected' : ''}>Пустое</option>
|
||||
<option value="IS NOT NULL" ${filter.operator === 'IS NOT NULL' ? 'selected' : ''}>Не пустое</option>
|
||||
</select>
|
||||
<input type="text" value="${filter.value || ''}" class="flex-1 px-3 py-2 border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none" placeholder="Значение" oninput="app.updateFilterValue(${index}, this.value)" ${filter.operator === 'IS NULL' || filter.operator === 'IS NOT NULL' ? 'disabled' : ''}>
|
||||
<button onclick="app.removeFilter(${index})" class="p-2 text-red-600 hover:bg-red-50 rounded">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
addFilter() {
|
||||
this.filters.push({ column: '', operator: '=', value: '' });
|
||||
this.renderFilters();
|
||||
}
|
||||
|
||||
updateFilterColumn(index, column) {
|
||||
this.filters[index].column = column;
|
||||
this.renderFilters(); // Re-render to update disabled state
|
||||
}
|
||||
|
||||
updateFilterOperator(index, operator) {
|
||||
this.filters[index].operator = operator;
|
||||
this.renderFilters(); // Re-render to update disabled state
|
||||
}
|
||||
|
||||
updateFilterValue(index, value) {
|
||||
this.filters[index].value = value;
|
||||
}
|
||||
|
||||
removeFilter(index) {
|
||||
this.filters.splice(index, 1);
|
||||
this.renderFilters();
|
||||
}
|
||||
|
||||
clearFilters() {
|
||||
this.filters = [];
|
||||
this.renderFilters();
|
||||
}
|
||||
|
||||
applyFilters() {
|
||||
this.currentPage = 1;
|
||||
this.closeModal('filtersModal');
|
||||
this.loadTableData();
|
||||
}
|
||||
|
||||
async loadTableStructure() {
|
||||
try {
|
||||
const response = await fetch(`/api/tables/${this.currentTable}/structure`);
|
||||
@@ -1246,7 +1329,7 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
||||
|
||||
// Close modals on outside click
|
||||
window.onclick = function(event) {
|
||||
if (event.target.classList.contains('fixed')) {
|
||||
if (event.target.classList.contains('fixed') && event.target.id !== 'loginScreen') {
|
||||
event.target.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user