release(v1.6.7): Folder Move feature, stable DnD persistence, safer uploads, and ACL/UI polish

This commit is contained in:
Ryan
2025-10-25 02:16:01 -04:00
committed by GitHub
parent b5610cf156
commit 2739925f0b
15 changed files with 915 additions and 224 deletions

View File

@@ -103,6 +103,7 @@ async function applyFolderCapabilities(folder) {
const isRoot = (folder === 'root');
setControlEnabled(document.getElementById('createFolderBtn'), !!caps.canCreate);
setControlEnabled(document.getElementById('moveFolderBtn'), !!caps.canMoveFolder);
setControlEnabled(document.getElementById('renameFolderBtn'), !isRoot && !!caps.canRename);
setControlEnabled(document.getElementById('deleteFolderBtn'), !isRoot && !!caps.canDelete);
setControlEnabled(document.getElementById('shareFolderBtn'), !isRoot && !!caps.canShareFolder);
@@ -180,6 +181,49 @@ function breadcrumbDropHandler(e) {
console.error("Invalid drag data on breadcrumb:", err);
return;
}
/* FOLDER MOVE FALLBACK */
if (!dragData) {
const plain = (event.dataTransfer && event.dataTransfer.getData("application/x-filerise-folder")) ||
(event.dataTransfer && event.dataTransfer.getData("text/plain")) || "";
if (plain) {
const sourceFolder = String(plain).trim();
if (sourceFolder && sourceFolder !== "root") {
if (dropFolder === sourceFolder || (dropFolder + "/").startsWith(sourceFolder + "/")) {
showToast("Invalid destination.", 4000);
return;
}
fetchWithCsrf("/api/folder/moveFolder.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ source: sourceFolder, destination: dropFolder })
})
.then(safeJson)
.then(data => {
if (data && !data.error) {
showToast(`Folder moved to ${dropFolder}!`);
if (window.currentFolder && (window.currentFolder === sourceFolder || window.currentFolder.startsWith(sourceFolder + "/"))) {
const base = sourceFolder.split("/").pop();
const newPath = (dropFolder === "root" ? "" : dropFolder + "/") + base;
window.currentFolder = newPath;
}
return loadFolderTree().then(() => {
try { expandTreePath(window.currentFolder || "root"); } catch (_) {}
loadFileList(window.currentFolder || "root");
});
} else {
showToast("Error: " + (data && data.error || "Could not move folder"), 5000);
}
})
.catch(err => {
console.error("Error moving folder:", err);
showToast("Error moving folder", 5000);
});
}
}
return;
}
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
if (filesToMove.length === 0) return;
@@ -262,7 +306,7 @@ function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") {
} else {
html += `<span class="folder-indent-placeholder"></span>`;
}
html += `<span class="folder-option" data-folder="${fullPath}">${escapeHTML(folder)}</span>`;
html += `<span class="folder-option" draggable="true" data-folder="${fullPath}">${escapeHTML(folder)}</span>`;
if (hasChildren) {
html += renderFolderTree(tree[folder], fullPath, displayState);
}
@@ -312,13 +356,58 @@ function folderDropHandler(event) {
event.preventDefault();
event.currentTarget.classList.remove("drop-hover");
const dropFolder = event.currentTarget.getAttribute("data-folder");
let dragData;
let dragData = null;
try {
dragData = JSON.parse(event.dataTransfer.getData("application/json"));
} catch (e) {
const jsonStr = event.dataTransfer.getData("application/json") || "";
if (jsonStr) dragData = JSON.parse(jsonStr);
}
catch (e) {
console.error("Invalid drag data", e);
return;
}
/* FOLDER MOVE FALLBACK */
if (!dragData) {
const plain = (event.dataTransfer && event.dataTransfer.getData("application/x-filerise-folder")) ||
(event.dataTransfer && event.dataTransfer.getData("text/plain")) || "";
if (plain) {
const sourceFolder = String(plain).trim();
if (sourceFolder && sourceFolder !== "root") {
if (dropFolder === sourceFolder || (dropFolder + "/").startsWith(sourceFolder + "/")) {
showToast("Invalid destination.", 4000);
return;
}
fetchWithCsrf("/api/folder/moveFolder.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ source: sourceFolder, destination: dropFolder })
})
.then(safeJson)
.then(data => {
if (data && !data.error) {
showToast(`Folder moved to ${dropFolder}!`);
if (window.currentFolder && (window.currentFolder === sourceFolder || window.currentFolder.startsWith(sourceFolder + "/"))) {
const base = sourceFolder.split("/").pop();
const newPath = (dropFolder === "root" ? "" : dropFolder + "/") + base;
window.currentFolder = newPath;
}
return loadFolderTree().then(() => {
try { expandTreePath(window.currentFolder || "root"); } catch (_) {}
loadFileList(window.currentFolder || "root");
});
} else {
showToast("Error: " + (data && data.error || "Could not move folder"), 5000);
}
})
.catch(err => {
console.error("Error moving folder:", err);
showToast("Error moving folder", 5000);
});
}
}
return;
}
const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []);
if (filesToMove.length === 0) return;
@@ -459,6 +548,14 @@ export async function loadFolderTree(selectedFolder) {
// Attach drag/drop event listeners.
container.querySelectorAll(".folder-option").forEach(el => {
// Provide folder path payload for folder->folder DnD
el.addEventListener("dragstart", (ev) => {
const src = el.getAttribute("data-folder");
try { ev.dataTransfer.setData("application/x-filerise-folder", src); } catch (e) {}
try { ev.dataTransfer.setData("text/plain", src); } catch (e) {}
ev.dataTransfer.effectAllowed = "move";
});
el.addEventListener("dragover", folderDragOverHandler);
el.addEventListener("dragleave", folderDragLeaveHandler);
el.addEventListener("drop", folderDropHandler);
@@ -487,6 +584,14 @@ export async function loadFolderTree(selectedFolder) {
// Folder-option click: update selection, breadcrumbs, and file list
container.querySelectorAll(".folder-option").forEach(el => {
// Provide folder path payload for folder->folder DnD
el.addEventListener("dragstart", (ev) => {
const src = el.getAttribute("data-folder");
try { ev.dataTransfer.setData("application/x-filerise-folder", src); } catch (e) {}
try { ev.dataTransfer.setData("text/plain", src); } catch (e) {}
ev.dataTransfer.effectAllowed = "move";
});
el.addEventListener("click", function (e) {
e.stopPropagation();
container.querySelectorAll(".folder-option").forEach(item => item.classList.remove("selected"));
@@ -642,6 +747,44 @@ if (submitRename) {
});
}
// === Move Folder Modal helper (shared by button + context menu) ===
function openMoveFolderUI(sourceFolder) {
const modal = document.getElementById('moveFolderModal');
const targetSel = document.getElementById('moveFolderTarget');
// If you right-clicked a different folder than currently selected, use that
if (sourceFolder && sourceFolder !== 'root') {
window.currentFolder = sourceFolder;
}
// Fill target dropdown
if (targetSel) {
targetSel.innerHTML = '';
fetch('/api/folder/getFolderList.php', { credentials: 'include' })
.then(r => r.json())
.then(list => {
if (Array.isArray(list) && list.length && typeof list[0] === 'object' && list[0].folder) {
list = list.map(it => it.folder);
}
// Root option
const rootOpt = document.createElement('option');
rootOpt.value = 'root'; rootOpt.textContent = '(Root)';
targetSel.appendChild(rootOpt);
(list || [])
.filter(f => f && f !== 'trash' && f !== (window.currentFolder || ''))
.forEach(f => {
const o = document.createElement('option');
o.value = f; o.textContent = f;
targetSel.appendChild(o);
});
})
.catch(()=>{ /* no-op */ });
}
if (modal) modal.style.display = 'block';
}
export function openDeleteFolderModal() {
const selectedFolder = window.currentFolder || "root";
if (!selectedFolder || selectedFolder === "root") {
@@ -841,6 +984,10 @@ function folderManagerContextMenuHandler(e) {
if (input) input.focus();
}
},
{
label: t("move_folder"),
action: () => { openMoveFolderUI(folder); }
},
{
label: t("rename_folder"),
action: () => { openRenameFolderModal(); }
@@ -923,4 +1070,53 @@ document.addEventListener("DOMContentLoaded", function () {
});
// Initial context menu delegation bind
bindFolderManagerContextMenu();
bindFolderManagerContextMenu();
document.addEventListener("DOMContentLoaded", () => {
const moveBtn = document.getElementById('moveFolderBtn');
const modal = document.getElementById('moveFolderModal');
const targetSel = document.getElementById('moveFolderTarget');
const cancelBtn = document.getElementById('cancelMoveFolder');
const confirmBtn= document.getElementById('confirmMoveFolder');
if (moveBtn) {
moveBtn.addEventListener('click', () => {
const cf = window.currentFolder || 'root';
if (!cf || cf === 'root') { showToast('Select a non-root folder to move.'); return; }
openMoveFolderUI(cf);
});
}
if (cancelBtn) cancelBtn.addEventListener('click', () => { if (modal) modal.style.display = 'none'; });
if (confirmBtn) confirmBtn.addEventListener('click', async () => {
if (!targetSel) return;
const destination = targetSel.value;
const source = window.currentFolder;
if (!destination) { showToast('Pick a destination'); return; }
if (destination === source || (destination + '/').startsWith(source + '/')) {
showToast('Invalid destination'); return;
}
try {
const res = await fetch('/api/folder/moveFolder.php', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.csrfToken },
body: JSON.stringify({ source, destination })
});
const data = await safeJson(res);
if (res.ok && data && !data.error) {
showToast('Folder moved');
if (modal) modal.style.display='none';
await loadFolderTree();
const base = source.split('/').pop();
const newPath = (destination === 'root' ? '' : destination + '/') + base;
window.currentFolder = newPath;
loadFileList(window.currentFolder || 'root');
} else {
showToast('Error: ' + (data && data.error || 'Move failed'));
}
} catch (e) { console.error(e); showToast('Move failed'); }
});
});