Improve web UI: sort/search/context menu, fix security and UX bugs

This commit is contained in:
2026-02-15 23:30:26 +08:00
parent 67f057ca1c
commit bcad0cd3da
11 changed files with 372 additions and 19 deletions

View File

@@ -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 {

View File

@@ -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>';

View File

@@ -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"]');

View File

@@ -191,6 +191,10 @@ window.UICore = (function() {
}
});
window.addEventListener('beforeunload', function() {
pollingManager.stopAll();
});
return {
getCsrfToken: getCsrfToken,
formatBytes: formatBytes,