feat: AMPP folder picker UI — load folders, pick target, queue placement on upload

This commit is contained in:
Zac Gaetano 2026-04-30 17:57:00 -04:00
parent 34695f3caf
commit 4ee422a5e8

View file

@ -485,6 +485,21 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
</div> </div>
</div> </div>
<div style="margin-bottom:1.5rem" id="ampp-folder-section">
<div class="section-title" style="display:flex;align-items:center;gap:.5rem">
AMPP Destination Folder
<span id="ampp-folder-status" style="font-size:.7rem;font-weight:400;text-transform:none;letter-spacing:0;color:var(--text-dim)">— optional</span>
<button id="ampp-folder-refresh-btn" onclick="loadAmppFolders()" style="margin-left:auto;padding:.2rem .55rem;font-size:.72rem;background:transparent;border:1px solid var(--border);border-radius:6px;color:var(--text-secondary);cursor:pointer">↻ Refresh</button>
</div>
<input class="form-input" id="ampp-folder-search" placeholder="🔍 Search AMPP folders…" oninput="renderAmppFolderList()" style="margin-bottom:.4rem;font-size:.78rem;padding:.4rem .65rem"/>
<div class="folder-tree-box" id="ampp-folder-box" style="max-height:220px;overflow-y:auto">
<div style="color:var(--text-dim);font-size:.8rem;padding:.75rem;text-align:center">Click ↻ Refresh to load AMPP folders</div>
</div>
<div style="margin-top:.45rem;font-size:.73rem;color:var(--text-dim)">
AMPP folder: <code id="ampp-folder-display" style="color:var(--blue-bright);font-family:'JetBrains Mono',monospace">(none — skip placement)</code>
</div>
</div>
<div class="section-title">Files</div> <div class="section-title">Files</div>
<div class="drop-zone" id="drop-zone" onclick="document.getElementById('file-input').click()" ondragover="onDragOver(event)" ondragleave="onDragLeave(event)" ondrop="onDrop(event)"> <div class="drop-zone" id="drop-zone" onclick="document.getElementById('file-input').click()" ondragover="onDragOver(event)" ondragleave="onDragLeave(event)" ondrop="onDrop(event)">
<span class="drop-zone-icon">📂</span> <span class="drop-zone-icon">📂</span>
@ -816,6 +831,9 @@ let currentUser = localStorage.getItem('dw_user') || null;
let currentRole = localStorage.getItem('dw_role') || null; let currentRole = localStorage.getItem('dw_role') || null;
let uploadMode = 'http'; let uploadMode = 'http';
let selectedPrefix = ''; let selectedPrefix = '';
let selectedAmppFolderId = '';
let selectedAmppFolderName = '';
let amppFolderCache = [];
let folderTree = []; let folderTree = [];
let selectedFiles = []; let selectedFiles = [];
@ -861,6 +879,7 @@ function showApp() {
loadS3Config(); loadRelayConfig(); loadAmppConfig(); loadUsers(); loadShareLinks(); populateSlFolderSelect(); loadS3Config(); loadRelayConfig(); loadAmppConfig(); loadUsers(); loadShareLinks(); populateSlFolderSelect();
} }
loadFolders(); loadFolders();
loadAmppFolders();
loadAmppJobs(); loadAmppJobs();
} }
@ -1065,6 +1084,76 @@ async function deleteFolder(pathArr) {
} catch(e) { showToast(e.message,'error'); } } catch(e) { showToast(e.message,'error'); }
} }
// ============================================================
// AMPP FOLDER PICKER
// ============================================================
async function loadAmppFolders() {
const box = document.getElementById('ampp-folder-box');
const statusEl = document.getElementById('ampp-folder-status');
if (!box) return;
box.innerHTML = '<div style="color:var(--text-dim);font-size:.8rem;padding:.75rem;text-align:center">Loading AMPP folders…</div>';
if (statusEl) statusEl.textContent = '— loading…';
try {
const d = await api('GET', '/api/ampp/folders/list');
if (!d.success) {
if (d.error && d.error.toLowerCase().includes('not configured')) {
box.innerHTML = '<div style="color:var(--text-dim);font-size:.8rem;padding:.75rem;text-align:center">AMPP not configured — set up in Admin → AMPP</div>';
if (statusEl) statusEl.textContent = '— not configured';
} else {
box.innerHTML = `<div style="color:var(--red,#e55);font-size:.8rem;padding:.75rem;text-align:center">Error: ${esc(d.error||'Failed to load folders')}</div>`;
if (statusEl) statusEl.textContent = '— error';
}
return;
}
amppFolderCache = d.folders || [];
if (statusEl) statusEl.textContent = `— ${amppFolderCache.length} folder${amppFolderCache.length!==1?'s':''}`;
renderAmppFolderList();
} catch(e) {
box.innerHTML = `<div style="color:var(--red,#e55);font-size:.8rem;padding:.75rem;text-align:center">Error: ${esc(e.message)}</div>`;
if (statusEl) statusEl.textContent = '— error';
}
}
function renderAmppFolderList() {
const box = document.getElementById('ampp-folder-box');
if (!box) return;
box.innerHTML = '';
const searchEl = document.getElementById('ampp-folder-search');
const filter = (searchEl ? searchEl.value.trim().toLowerCase() : '');
const folders = amppFolderCache.filter(f => !filter || (f.path||f.name||'').toLowerCase().includes(filter));
// "None" row — always first
const noneRow = document.createElement('div');
noneRow.className = 'folder-tree-row' + (selectedAmppFolderId === '' ? ' active' : '');
noneRow.innerHTML = `<span class="ftr-icon">🚫</span><span class="ftr-name" style="font-style:italic;color:${selectedAmppFolderId===''?'':'var(--text-dim)'}">(no AMPP placement)</span>`;
noneRow.onclick = () => { selectedAmppFolderId = ''; selectedAmppFolderName = ''; updateAmppFolderDisplay(); renderAmppFolderList(); };
box.appendChild(noneRow);
if (!folders.length && amppFolderCache.length > 0) {
box.innerHTML += '<div style="color:var(--text-dim);font-size:.8rem;padding:.5rem .75rem">No folders match your search</div>';
return;
}
// Sort by path/name
const sorted = [...folders].sort((a, b) => (a.path||a.name||'').localeCompare(b.path||b.name||''));
sorted.forEach(f => {
const id = f.id || f['folder:id'] || '';
const displayName = f.path || f.name || f['name:text'] || id;
const depth = (displayName.match(/\//g)||[]).length;
const row = document.createElement('div');
row.className = 'folder-tree-row' + (selectedAmppFolderId === id ? ' active' : '');
row.style.paddingLeft = (0.75 + depth * 1.0) + 'rem';
row.innerHTML = `<span class="ftr-icon">📁</span><span class="ftr-name">${esc(displayName)}</span>`;
row.onclick = () => { selectedAmppFolderId = id; selectedAmppFolderName = displayName; updateAmppFolderDisplay(); renderAmppFolderList(); };
box.appendChild(row);
});
}
function updateAmppFolderDisplay() {
const el = document.getElementById('ampp-folder-display');
if (el) el.textContent = selectedAmppFolderName || '(none — skip placement)';
}
// ============================================================ // ============================================================
// FILE HANDLING // FILE HANDLING
// ============================================================ // ============================================================
@ -1223,6 +1312,7 @@ async function uploadFilePresigned(item, idx) {
setFileStatus(idx, 'uploading', 'Getting URL\u2026'); setFileStatus(idx, 'uploading', 'Getting URL\u2026');
const pre = await api('POST', '/api/presigned', { const pre = await api('POST', '/api/presigned', {
filename: item.name, prefix: selectedPrefix, contentType: mime, size: item.file.size, filename: item.name, prefix: selectedPrefix, contentType: mime, size: item.file.size,
amppFolderId: selectedAmppFolderId, amppFolderName: selectedAmppFolderName,
}); });
if (!pre.success) throw new Error(pre.error || 'Failed to get presigned URL'); if (!pre.success) throw new Error(pre.error || 'Failed to get presigned URL');
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -1237,7 +1327,7 @@ async function uploadFilePresigned(item, idx) {
}); });
xhr.addEventListener('load', async () => { xhr.addEventListener('load', async () => {
if (xhr.status >= 200 && xhr.status < 300) { if (xhr.status >= 200 && xhr.status < 300) {
try { await api('POST', '/api/presigned/complete', { key: pre.key, size: item.file.size }); } catch(_) {} try { await api('POST', '/api/presigned/complete', { key: pre.key, size: item.file.size, filename: item.name, amppFolderId: selectedAmppFolderId, amppFolderName: selectedAmppFolderName }); } catch(_) {}
resolve({ key: pre.key }); resolve({ key: pre.key });
} else { } else {
reject(new Error(`S3 returned ${xhr.status}: ${xhr.statusText}`)); reject(new Error(`S3 returned ${xhr.status}: ${xhr.statusText}`));
@ -1311,8 +1401,8 @@ async function uploadFilePresigned(item, idx) {
if (pb) pb.style.width = '100%'; if (pb) pb.style.width = '100%';
setFileStatus(idx, 'uploading', '100%'); setFileStatus(idx, 'uploading', '100%');
// Notify server for quota tracking // Notify server for quota tracking + queue AMPP placement
try { await api('POST', '/api/presigned/complete', { key, size: item.file.size }); } catch(_) {} try { await api('POST', '/api/presigned/complete', { key, size: item.file.size, filename: item.name, amppFolderId: selectedAmppFolderId, amppFolderName: selectedAmppFolderName }); } catch(_) {}
return { key }; return { key };
} }