feat: AMPP folder picker UI — load folders, pick target, queue placement on upload
This commit is contained in:
parent
34695f3caf
commit
4ee422a5e8
1 changed files with 93 additions and 3 deletions
|
|
@ -485,6 +485,21 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
|
|||
</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="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>
|
||||
|
|
@ -816,6 +831,9 @@ let currentUser = localStorage.getItem('dw_user') || null;
|
|||
let currentRole = localStorage.getItem('dw_role') || null;
|
||||
let uploadMode = 'http';
|
||||
let selectedPrefix = '';
|
||||
let selectedAmppFolderId = '';
|
||||
let selectedAmppFolderName = '';
|
||||
let amppFolderCache = [];
|
||||
let folderTree = [];
|
||||
let selectedFiles = [];
|
||||
|
||||
|
|
@ -861,6 +879,7 @@ function showApp() {
|
|||
loadS3Config(); loadRelayConfig(); loadAmppConfig(); loadUsers(); loadShareLinks(); populateSlFolderSelect();
|
||||
}
|
||||
loadFolders();
|
||||
loadAmppFolders();
|
||||
loadAmppJobs();
|
||||
}
|
||||
|
||||
|
|
@ -1065,6 +1084,76 @@ async function deleteFolder(pathArr) {
|
|||
} 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
|
||||
// ============================================================
|
||||
|
|
@ -1223,6 +1312,7 @@ async function uploadFilePresigned(item, idx) {
|
|||
setFileStatus(idx, 'uploading', 'Getting URL\u2026');
|
||||
const pre = await api('POST', '/api/presigned', {
|
||||
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');
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -1237,7 +1327,7 @@ async function uploadFilePresigned(item, idx) {
|
|||
});
|
||||
xhr.addEventListener('load', async () => {
|
||||
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 });
|
||||
} else {
|
||||
reject(new Error(`S3 returned ${xhr.status}: ${xhr.statusText}`));
|
||||
|
|
@ -1311,8 +1401,8 @@ async function uploadFilePresigned(item, idx) {
|
|||
if (pb) pb.style.width = '100%';
|
||||
setFileStatus(idx, 'uploading', '100%');
|
||||
|
||||
// Notify server for quota tracking
|
||||
try { await api('POST', '/api/presigned/complete', { key, size: item.file.size }); } catch(_) {}
|
||||
// Notify server for quota tracking + queue AMPP placement
|
||||
try { await api('POST', '/api/presigned/complete', { key, size: item.file.size, filename: item.name, amppFolderId: selectedAmppFolderId, amppFolderName: selectedAmppFolderName }); } catch(_) {}
|
||||
return { key };
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue