feat: rename and move in file manager (#630)
## Summary * **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) This adds renaming and moving files to the File Manager * **What changes are included?** New `/move` and `/rename` endpoints, and corresponding modals and icons added. Uses the `file.rename()` function, after sanity checking. ## Additional Context * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). Fixes #559, #661, #663. Only touches the File Manager, so low risk of affecting other systems. Simpler than #619, at the cost of not migrating the cache of renamed books. <img width="870" height="437" alt="image" src="https://github.com/user-attachments/assets/73e0e750-dfc8-48e0-a7a6-9694470b7ded" /> <img width="575" height="318" alt="image" src="https://github.com/user-attachments/assets/38c5fb19-c38a-436b-b3ad-75c1be7375ab" /> <img width="574" height="293" alt="image" src="https://github.com/user-attachments/assets/1d2a2403-765d-473f-8c4f-c6968e9bbfeb" /> --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_ I used Codex for the implementation itself, and then carefully reviewed the code myself. As this is a simple change and only to the webserver, it is low risk.
This commit is contained in:
committed by
GitHub
parent
d762325035
commit
d35bda8023
@@ -322,25 +322,47 @@
|
||||
.folder-btn:hover {
|
||||
background-color: #d68910;
|
||||
}
|
||||
/* Delete button styles */
|
||||
.delete-btn {
|
||||
/* Action button styles */
|
||||
.delete-btn,
|
||||
.rename-btn,
|
||||
.move-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.1em;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
color: #95a5a6;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.delete-btn {
|
||||
color: #95a5a6;
|
||||
}
|
||||
.delete-btn:hover {
|
||||
background-color: #fee;
|
||||
color: #e74c3c;
|
||||
}
|
||||
.rename-btn {
|
||||
color: #2980b9;
|
||||
}
|
||||
.rename-btn:hover {
|
||||
background-color: #e8f4fd;
|
||||
}
|
||||
.move-btn {
|
||||
color: #16a085;
|
||||
}
|
||||
.move-btn:hover {
|
||||
background-color: #e6f7f4;
|
||||
}
|
||||
.actions-col {
|
||||
width: 60px;
|
||||
width: 140px;
|
||||
text-align: center;
|
||||
}
|
||||
.action-icon-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
/* Failed uploads banner */
|
||||
.failed-uploads-banner {
|
||||
background-color: #fff3cd;
|
||||
@@ -463,6 +485,32 @@
|
||||
.delete-btn-cancel:hover {
|
||||
background-color: #7f8c8d;
|
||||
}
|
||||
.rename-btn-confirm {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
width: 100%;
|
||||
}
|
||||
.rename-btn-confirm:hover {
|
||||
background-color: #2e86c1;
|
||||
}
|
||||
.move-btn-confirm {
|
||||
background-color: #16a085;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
width: 100%;
|
||||
}
|
||||
.move-btn-confirm:hover {
|
||||
background-color: #138d75;
|
||||
}
|
||||
.loader-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -558,12 +606,17 @@
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.actions-col {
|
||||
width: 40px;
|
||||
width: 120px;
|
||||
}
|
||||
.delete-btn {
|
||||
.delete-btn,
|
||||
.rename-btn,
|
||||
.move-btn {
|
||||
font-size: 1em;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
.action-icon-group {
|
||||
gap: 4px;
|
||||
}
|
||||
.no-files {
|
||||
padding: 20px;
|
||||
font-size: 0.9em;
|
||||
@@ -665,6 +718,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rename Modal -->
|
||||
<div class="modal-overlay" id="renameModal">
|
||||
<div class="modal">
|
||||
<button class="modal-close" onclick="closeRenameModal()">×</button>
|
||||
<h3>✏️ Rename File</h3>
|
||||
<div class="folder-form">
|
||||
<p class="file-info">Renaming <strong id="renameItemName"></strong></p>
|
||||
<input type="text" id="renameNewName" class="folder-input" placeholder="New file name...">
|
||||
<input type="hidden" id="renameItemPath">
|
||||
<button class="rename-btn-confirm" onclick="confirmRename()">Rename</button>
|
||||
<button class="delete-btn-cancel" onclick="closeRenameModal()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Move Modal -->
|
||||
<div class="modal-overlay" id="moveModal">
|
||||
<div class="modal">
|
||||
<button class="modal-close" onclick="closeMoveModal()">×</button>
|
||||
<h3>📂 Move File</h3>
|
||||
<div class="folder-form">
|
||||
<p class="file-info">Moving <strong id="moveItemName"></strong></p>
|
||||
<input type="text" id="moveDestPath" class="folder-input" list="moveFolderOptions" placeholder="/Destination/Folder">
|
||||
<datalist id="moveFolderOptions"></datalist>
|
||||
<input type="hidden" id="moveItemPath">
|
||||
<button class="move-btn-confirm" onclick="confirmMove()">Move</button>
|
||||
<button class="delete-btn-cancel" onclick="closeMoveModal()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// get current path from query parameter
|
||||
const currentPath = decodeURIComponent(new URLSearchParams(window.location.search).get('path') || '/');
|
||||
@@ -760,7 +844,7 @@
|
||||
fileTableContent += `<td><span class="file-icon">📁</span><a href="/files?path=${encodeURIComponent(folderPath)}" class="folder-link">${escapeHtml(file.name)}</a><span class="folder-badge">FOLDER</span></td>`;
|
||||
fileTableContent += '<td>Folder</td>';
|
||||
fileTableContent += '<td>-</td>';
|
||||
fileTableContent += `<td class="actions-col"><button class="delete-btn" onclick="openDeleteModal('${file.name.replaceAll("'", "\\'")}', '${folderPath.replaceAll("'", "\\'")}', true)" title="Delete folder">🗑️</button></td>`;
|
||||
fileTableContent += `<td class="actions-col"><div class="action-icon-group"><button class="delete-btn" onclick="openDeleteModal('${file.name.replaceAll("'", "\\'")}', '${folderPath.replaceAll("'", "\\'")}', true)" title="Delete folder">🗑️</button></div></td>`;
|
||||
fileTableContent += '</tr>';
|
||||
} else {
|
||||
let filePath = currentPath;
|
||||
@@ -773,7 +857,11 @@
|
||||
fileTableContent += '</td>';
|
||||
fileTableContent += `<td>${file.name.split('.').pop().toUpperCase()}</td>`;
|
||||
fileTableContent += `<td>${formatFileSize(file.size)}</td>`;
|
||||
fileTableContent += `<td class="actions-col"><button class="delete-btn" onclick="openDeleteModal('${file.name.replaceAll("'", "\\'")}', '${filePath.replaceAll("'", "\\'")}', false)" title="Delete file">🗑️</button></td>`;
|
||||
fileTableContent += `<td class="actions-col"><div class="action-icon-group">`;
|
||||
fileTableContent += `<button class="move-btn" onclick="openMoveModal('${file.name.replaceAll("'", "\\'")}', '${filePath.replaceAll("'", "\\'")}' )" title="Move file">📂</button>`;
|
||||
fileTableContent += `<button class="rename-btn" onclick="openRenameModal('${file.name.replaceAll("'", "\\'")}', '${filePath.replaceAll("'", "\\'")}' )" title="Rename file">✏️</button>`;
|
||||
fileTableContent += `<button class="delete-btn" onclick="openDeleteModal('${file.name.replaceAll("'", "\\'")}', '${filePath.replaceAll("'", "\\'")}', false)" title="Delete file">🗑️</button>`;
|
||||
fileTableContent += `</div></td>`;
|
||||
fileTableContent += '</tr>';
|
||||
}
|
||||
});
|
||||
@@ -1175,6 +1263,170 @@ function retryAllFailedUploads() {
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
// Rename functions
|
||||
function openRenameModal(name, path) {
|
||||
document.getElementById('renameItemName').textContent = '📄 ' + name;
|
||||
document.getElementById('renameItemPath').value = path;
|
||||
document.getElementById('renameNewName').value = name;
|
||||
document.getElementById('renameModal').classList.add('open');
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById('renameNewName');
|
||||
input.focus();
|
||||
input.select();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function closeRenameModal() {
|
||||
document.getElementById('renameModal').classList.remove('open');
|
||||
}
|
||||
|
||||
function confirmRename() {
|
||||
const path = document.getElementById('renameItemPath').value;
|
||||
const newName = document.getElementById('renameNewName').value.trim();
|
||||
|
||||
if (!newName) {
|
||||
alert('Please enter a new name.');
|
||||
return;
|
||||
}
|
||||
if (newName.includes('/') || newName.includes('\\')) {
|
||||
alert('File name cannot include slashes.');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('path', path);
|
||||
formData.append('name', newName);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/rename', true);
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Failed to rename: ' + xhr.responseText);
|
||||
}
|
||||
closeRenameModal();
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
alert('Failed to rename - network error');
|
||||
closeRenameModal();
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
// Move functions
|
||||
function normalizePath(path) {
|
||||
if (!path) return '/';
|
||||
let normalized = path.trim();
|
||||
if (!normalized.startsWith('/')) normalized = '/' + normalized;
|
||||
if (normalized.length > 1 && normalized.endsWith('/')) {
|
||||
normalized = normalized.slice(0, -1);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getParentPath(path) {
|
||||
const normalized = normalizePath(path);
|
||||
if (normalized === '/') return '/';
|
||||
const idx = normalized.lastIndexOf('/');
|
||||
return idx <= 0 ? '/' : normalized.slice(0, idx);
|
||||
}
|
||||
|
||||
async function loadMoveFolderOptions() {
|
||||
const options = new Set();
|
||||
options.add('/');
|
||||
const parent = getParentPath(currentPath);
|
||||
if (parent) options.add(parent);
|
||||
|
||||
async function fetchFolders(path) {
|
||||
try {
|
||||
const response = await fetch('/api/files?path=' + encodeURIComponent(path));
|
||||
if (!response.ok) return [];
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const rootFiles = await fetchFolders('/');
|
||||
rootFiles.forEach(file => {
|
||||
if (file.isDirectory) {
|
||||
options.add('/' + file.name);
|
||||
}
|
||||
});
|
||||
|
||||
if (currentPath !== '/') {
|
||||
const currentFiles = await fetchFolders(currentPath);
|
||||
currentFiles.forEach(file => {
|
||||
if (file.isDirectory) {
|
||||
let folderPath = currentPath;
|
||||
if (!folderPath.endsWith('/')) folderPath += '/';
|
||||
folderPath += file.name;
|
||||
options.add(folderPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const dataList = document.getElementById('moveFolderOptions');
|
||||
dataList.innerHTML = '';
|
||||
Array.from(options).sort().forEach(path => {
|
||||
const option = document.createElement('option');
|
||||
option.value = path;
|
||||
dataList.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function openMoveModal(name, path) {
|
||||
document.getElementById('moveItemName').textContent = '📄 ' + name;
|
||||
document.getElementById('moveItemPath').value = path;
|
||||
document.getElementById('moveDestPath').value = currentPath === '/' ? '/' : currentPath;
|
||||
document.getElementById('moveModal').classList.add('open');
|
||||
loadMoveFolderOptions();
|
||||
setTimeout(() => {
|
||||
document.getElementById('moveDestPath').focus();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function closeMoveModal() {
|
||||
document.getElementById('moveModal').classList.remove('open');
|
||||
}
|
||||
|
||||
function confirmMove() {
|
||||
const path = document.getElementById('moveItemPath').value;
|
||||
const destPath = normalizePath(document.getElementById('moveDestPath').value);
|
||||
|
||||
if (!destPath) {
|
||||
alert('Please enter a destination folder.');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('path', path);
|
||||
formData.append('dest', destPath);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/move', true);
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Failed to move: ' + xhr.responseText);
|
||||
}
|
||||
closeMoveModal();
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
alert('Failed to move - network error');
|
||||
closeMoveModal();
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
// Delete functions
|
||||
function openDeleteModal(name, path, isFolder) {
|
||||
document.getElementById('deleteItemName').textContent = (isFolder ? '📁 ' : '📄 ') + name;
|
||||
|
||||
Reference in New Issue
Block a user