Improve web UI: sort/search/context menu, fix security and UX bugs
This commit is contained in:
@@ -2819,6 +2819,112 @@ body:has(.login-card) .main-wrapper {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
z-index: 1060;
|
||||
min-width: 180px;
|
||||
background: var(--myfsio-card-bg);
|
||||
border: 1px solid var(--myfsio-card-border);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
||||
padding: 0.25rem 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .context-menu {
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.5rem 0.875rem;
|
||||
color: var(--myfsio-text);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background-color: var(--myfsio-hover-bg);
|
||||
}
|
||||
|
||||
.context-menu-item.text-danger:hover {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.context-menu-divider {
|
||||
height: 1px;
|
||||
background: var(--myfsio-card-border);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.context-menu-shortcut {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
color: var(--myfsio-muted);
|
||||
}
|
||||
|
||||
.kbd-shortcuts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.kbd-shortcuts-list .shortcut-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.375rem 0;
|
||||
}
|
||||
|
||||
.kbd-shortcuts-list kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.75rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
font-family: inherit;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
background: var(--myfsio-preview-bg);
|
||||
border: 1px solid var(--myfsio-card-border);
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 1px 0 1px rgba(0, 0, 0, 0.05);
|
||||
color: var(--myfsio-text);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .kbd-shortcuts-list kbd {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 1px 0 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.sort-dropdown .dropdown-item.active,
|
||||
.sort-dropdown .dropdown-item:active {
|
||||
background-color: var(--myfsio-hover-bg);
|
||||
color: var(--myfsio-text);
|
||||
}
|
||||
|
||||
.sort-dropdown .dropdown-item {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.375rem 1rem;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.sidebar,
|
||||
.mobile-header {
|
||||
|
||||
@@ -162,6 +162,8 @@
|
||||
let isLoadingObjects = false;
|
||||
let hasMoreObjects = false;
|
||||
let currentFilterTerm = '';
|
||||
let currentSortField = 'name';
|
||||
let currentSortDir = 'asc';
|
||||
let pageSize = 5000;
|
||||
let currentPrefix = '';
|
||||
let allObjects = [];
|
||||
@@ -348,14 +350,18 @@
|
||||
const currentInputs = {
|
||||
objectCount: allObjects.length,
|
||||
prefix: currentPrefix,
|
||||
filterTerm: currentFilterTerm
|
||||
filterTerm: currentFilterTerm,
|
||||
sortField: currentSortField,
|
||||
sortDir: currentSortDir
|
||||
};
|
||||
|
||||
if (!forceRecompute &&
|
||||
memoizedVisibleItems !== null &&
|
||||
memoizedInputs.objectCount === currentInputs.objectCount &&
|
||||
memoizedInputs.prefix === currentInputs.prefix &&
|
||||
memoizedInputs.filterTerm === currentInputs.filterTerm) {
|
||||
memoizedInputs.filterTerm === currentInputs.filterTerm &&
|
||||
memoizedInputs.sortField === currentInputs.sortField &&
|
||||
memoizedInputs.sortDir === currentInputs.sortDir) {
|
||||
return memoizedVisibleItems;
|
||||
}
|
||||
|
||||
@@ -394,9 +400,19 @@
|
||||
items.sort((a, b) => {
|
||||
if (a.type === 'folder' && b.type === 'file') return -1;
|
||||
if (a.type === 'file' && b.type === 'folder') return 1;
|
||||
const aKey = a.type === 'folder' ? a.path : a.data.key;
|
||||
const bKey = b.type === 'folder' ? b.path : b.data.key;
|
||||
return aKey.localeCompare(bKey);
|
||||
if (a.type === 'folder' && b.type === 'folder') {
|
||||
return a.path.localeCompare(b.path);
|
||||
}
|
||||
const dir = currentSortDir === 'asc' ? 1 : -1;
|
||||
if (currentSortField === 'size') {
|
||||
return (a.data.size - b.data.size) * dir;
|
||||
}
|
||||
if (currentSortField === 'date') {
|
||||
const aTime = new Date(a.data.lastModified || a.data.last_modified || 0).getTime();
|
||||
const bTime = new Date(b.data.lastModified || b.data.last_modified || 0).getTime();
|
||||
return (aTime - bTime) * dir;
|
||||
}
|
||||
return a.data.key.localeCompare(b.data.key) * dir;
|
||||
});
|
||||
|
||||
memoizedVisibleItems = items;
|
||||
@@ -2034,6 +2050,128 @@
|
||||
refreshVirtualList();
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-sort-field]').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const field = el.dataset.sortField;
|
||||
const dir = el.dataset.sortDir || 'asc';
|
||||
currentSortField = field;
|
||||
currentSortDir = dir;
|
||||
document.querySelectorAll('[data-sort-field]').forEach(s => s.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
var label = document.getElementById('sort-dropdown-label');
|
||||
if (label) label.textContent = el.textContent.trim();
|
||||
refreshVirtualList();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT' || e.target.isContentEditable) return;
|
||||
|
||||
if (e.key === '/' && !e.ctrlKey && !e.metaKey) {
|
||||
e.preventDefault();
|
||||
document.getElementById('object-search')?.focus();
|
||||
}
|
||||
|
||||
if (e.key === '?' && !e.ctrlKey && !e.metaKey) {
|
||||
e.preventDefault();
|
||||
var kbModal = document.getElementById('keyboardShortcutsModal');
|
||||
if (kbModal) {
|
||||
var instance = bootstrap.Modal.getOrCreateInstance(kbModal);
|
||||
instance.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
var searchInput = document.getElementById('object-search');
|
||||
if (searchInput && document.activeElement === searchInput) {
|
||||
searchInput.value = '';
|
||||
currentFilterTerm = '';
|
||||
refreshVirtualList();
|
||||
searchInput.blur();
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'Delete' && !e.ctrlKey && !e.metaKey) {
|
||||
if (selectedRows.size > 0 && bulkDeleteButton && !bulkDeleteButton.disabled) {
|
||||
bulkDeleteButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'a' && (e.ctrlKey || e.metaKey)) {
|
||||
if (visibleItems.length > 0 && selectAllCheckbox) {
|
||||
e.preventDefault();
|
||||
selectAllCheckbox.checked = true;
|
||||
selectAllCheckbox.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const ctxMenu = document.getElementById('objectContextMenu');
|
||||
let ctxTargetRow = null;
|
||||
|
||||
const hideContextMenu = () => {
|
||||
if (ctxMenu) ctxMenu.classList.add('d-none');
|
||||
ctxTargetRow = null;
|
||||
};
|
||||
|
||||
if (ctxMenu) {
|
||||
document.addEventListener('click', hideContextMenu);
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
const row = e.target.closest('[data-object-row]');
|
||||
if (!row) { hideContextMenu(); return; }
|
||||
e.preventDefault();
|
||||
ctxTargetRow = row;
|
||||
|
||||
const x = Math.min(e.clientX, window.innerWidth - 200);
|
||||
const y = Math.min(e.clientY, window.innerHeight - 200);
|
||||
ctxMenu.style.left = x + 'px';
|
||||
ctxMenu.style.top = y + 'px';
|
||||
ctxMenu.classList.remove('d-none');
|
||||
});
|
||||
|
||||
ctxMenu.querySelectorAll('[data-ctx-action]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
if (!ctxTargetRow) return;
|
||||
const action = btn.dataset.ctxAction;
|
||||
const key = ctxTargetRow.dataset.key;
|
||||
const bucket = objectsContainer?.dataset.bucket || '';
|
||||
|
||||
if (action === 'download') {
|
||||
const url = ctxTargetRow.dataset.downloadUrl;
|
||||
if (url) window.open(url, '_blank');
|
||||
} else if (action === 'copy-path') {
|
||||
const s3Path = 's3://' + bucket + '/' + key;
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(s3Path).then(() => {
|
||||
if (window.showToast) window.showToast('Copied: ' + s3Path, 'Copied', 'success');
|
||||
});
|
||||
}
|
||||
} else if (action === 'presign') {
|
||||
selectRow(ctxTargetRow);
|
||||
presignLink.value = '';
|
||||
presignModal?.show();
|
||||
requestPresignedUrl();
|
||||
} else if (action === 'delete') {
|
||||
const deleteEndpoint = ctxTargetRow.dataset.deleteEndpoint;
|
||||
if (deleteEndpoint) {
|
||||
selectRow(ctxTargetRow);
|
||||
const deleteModalEl = document.getElementById('deleteObjectModal');
|
||||
const deleteModal = deleteModalEl ? bootstrap.Modal.getOrCreateInstance(deleteModalEl) : null;
|
||||
const deleteObjectForm = document.getElementById('deleteObjectForm');
|
||||
const deleteObjectKey = document.getElementById('deleteObjectKey');
|
||||
if (deleteModal && deleteObjectForm) {
|
||||
deleteObjectForm.setAttribute('action', deleteEndpoint);
|
||||
if (deleteObjectKey) deleteObjectKey.textContent = key;
|
||||
deleteModal.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
hideContextMenu();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
refreshVersionsButton?.addEventListener('click', () => {
|
||||
if (!activeRow) {
|
||||
versionList.innerHTML = '<p class="text-muted small mb-0">Select an object to view versions.</p>';
|
||||
|
||||
@@ -78,7 +78,7 @@ window.ConnectionsManagement = (function() {
|
||||
|
||||
try {
|
||||
var controller = new AbortController();
|
||||
var timeoutId = setTimeout(function() { controller.abort(); }, 15000);
|
||||
var timeoutId = setTimeout(function() { controller.abort(); }, 10000);
|
||||
|
||||
var response = await fetch(endpoints.healthTemplate.replace('CONNECTION_ID', connectionId), {
|
||||
signal: controller.signal
|
||||
@@ -147,7 +147,7 @@ window.ConnectionsManagement = (function() {
|
||||
'<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#editConnectionModal" ' +
|
||||
'data-id="' + window.UICore.escapeHtml(conn.id) + '" data-name="' + window.UICore.escapeHtml(conn.name) + '" ' +
|
||||
'data-endpoint="' + window.UICore.escapeHtml(conn.endpoint_url) + '" data-region="' + window.UICore.escapeHtml(conn.region) + '" ' +
|
||||
'data-access="' + window.UICore.escapeHtml(conn.access_key) + '" data-secret="' + window.UICore.escapeHtml(conn.secret_key || '') + '" title="Edit connection">' +
|
||||
'data-access="' + window.UICore.escapeHtml(conn.access_key) + '" title="Edit connection">' +
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">' +
|
||||
'<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5z"/></svg></button>' +
|
||||
'<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteConnectionModal" ' +
|
||||
@@ -185,7 +185,9 @@ window.ConnectionsManagement = (function() {
|
||||
document.getElementById('edit_endpoint_url').value = button.getAttribute('data-endpoint') || '';
|
||||
document.getElementById('edit_region').value = button.getAttribute('data-region') || '';
|
||||
document.getElementById('edit_access_key').value = button.getAttribute('data-access') || '';
|
||||
document.getElementById('edit_secret_key').value = button.getAttribute('data-secret') || '';
|
||||
document.getElementById('edit_secret_key').value = '';
|
||||
document.getElementById('edit_secret_key').placeholder = '(unchanged — leave blank to keep current)';
|
||||
document.getElementById('edit_secret_key').required = false;
|
||||
document.getElementById('editTestResult').innerHTML = '';
|
||||
|
||||
var form = document.getElementById('editConnectionForm');
|
||||
@@ -288,9 +290,6 @@ window.ConnectionsManagement = (function() {
|
||||
editBtn.setAttribute('data-endpoint', data.connection.endpoint_url);
|
||||
editBtn.setAttribute('data-region', data.connection.region);
|
||||
editBtn.setAttribute('data-access', data.connection.access_key);
|
||||
if (data.connection.secret_key) {
|
||||
editBtn.setAttribute('data-secret', data.connection.secret_key);
|
||||
}
|
||||
}
|
||||
|
||||
var deleteBtn = row.querySelector('[data-bs-target="#deleteConnectionModal"]');
|
||||
|
||||
@@ -191,6 +191,10 @@ window.UICore = (function() {
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', function() {
|
||||
pollingManager.stopAll();
|
||||
});
|
||||
|
||||
return {
|
||||
getCsrfToken: getCsrfToken,
|
||||
formatBytes: formatBytes,
|
||||
|
||||
Reference in New Issue
Block a user