Add IMAP webmail tag, admin/opensource subpage tiers, store/cart, htmleditor, uploads
New __imap*/__smtpsend/__eval Jet tags give templates real IMAP (list/message/flag/delete/move/search/folders/accounts) and SMTP-send capability, backing the new /email webmail client (email.jet/ email.menu, assets/images/email.js + newemail.js) and its mainpagetemplate.jet-based page shell. Most of this tag family's source was already being built into the live site (Tag.cpp already registered __imap/__imaplist/__imapmessage/__imapflag/__imapdelete/ __eval) but had never actually been committed; this also registers and commits the remaining five (__imapmove/__imapfolders/ __imapsearch/__smtpsend/__imapaccounts). admin.page/admin.menu gain a real subpage tier (admin-email, admin-newemail, admin-invoices, admin-products, admin-users) replacing several previously top-level pages, mirrored by a similar opensource.page subpage tier (opensource-overview/asm1802/barantmail/ jet/serverframework/synthmax) - old top-level pages retired via git rename (asm1802.page, jet.page, serverframework.page, admin_invoices.page); barantmail.page is dropped outright, folded into opensource-barantmail.subpage. New storefront (store.page, store-createstore.subpage, barantstore.page) and cart flow (cart.jet, updatecartqty.proc, removecartitem.proc, editproduct.page/selectproduct.proc/ update_product.proc for inventory maintenance against the barant database). New document upload/download manager (documents.jet, documents_api.jet, documents_upload.proc, documents_delete.proc, assets/images/documents.js + documents-page.js) with a persistent floating panel, and a rich-text page editor (htmleditor.jet/.page, createhtmleditor.proc, savehtmleditor.proc, assets/images/htmleditor.js + its icon/logo assets). SESSIONCTL.tag: login now redirects back to the originally-requested page instead of always landing on /account ($[start:0] vs $[start] variable-name mismatch). main.css/menu.bar/invoices.menu/jet_overview.section/login.menu: styling and navigation updates for the above; new site assets (fonts, images, audio) referenced by them. jetc/build-assets.sh, list_jet_files.sh, lowercase_jet.py, strip_fragments.py: small dev tooling used while building the above.
This commit is contained in:
1 parent
b0cadc3f08
commit
c1491b044b
115 files changed
+6174
-137
No files matched your search
@@ -0,0 +1,178 @@
|
||||
// Site-wide file transfer manager - loaded once via mainpagetemplate.jet
|
||||
// (see its own script tag, outside #content) so the floating panel and
|
||||
// any in-progress transfer survive spa.js's own #content-only swaps
|
||||
// when navigating to a different page mid-upload/download.
|
||||
(function() {
|
||||
var panel = document.getElementById('filemgr-panel');
|
||||
if (!panel)
|
||||
return;
|
||||
|
||||
var toggle = document.getElementById('filemgr-toggle');
|
||||
var badge = document.getElementById('filemgr-badge');
|
||||
var list = document.getElementById('filemgr-list');
|
||||
var clearBtn = document.getElementById('filemgr-clear');
|
||||
|
||||
var transfers = {};
|
||||
var nextId = 1;
|
||||
|
||||
function escapeHtml(s) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = s == null ? '' : String(s);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function updateBadge() {
|
||||
var activeCount = 0;
|
||||
for (var k in transfers) {
|
||||
if (transfers[k].status === 'active')
|
||||
activeCount++;
|
||||
}
|
||||
badge.textContent = activeCount > 0 ? String(activeCount) : '';
|
||||
}
|
||||
|
||||
function removeEmptyPlaceholder() {
|
||||
var empty = document.getElementById('filemgr-empty');
|
||||
if (empty)
|
||||
empty.remove();
|
||||
}
|
||||
|
||||
function showEmptyPlaceholderIfNeeded() {
|
||||
if (Object.keys(transfers).length === 0 && !document.getElementById('filemgr-empty'))
|
||||
list.innerHTML = '<div id="filemgr-empty">No transfers yet.</div>';
|
||||
}
|
||||
|
||||
var UPLOAD_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V6M5 12l7-7 7 7"/></svg>';
|
||||
var DOWNLOAD_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v13M5 12l7 7 7-7"/></svg>';
|
||||
|
||||
function addTransfer(name, direction) {
|
||||
var id = 'xfer' + (nextId++);
|
||||
removeEmptyPlaceholder();
|
||||
|
||||
var el = document.createElement('div');
|
||||
el.className = 'filemgr-item';
|
||||
el.innerHTML =
|
||||
'<div class="filemgr-item-row">' +
|
||||
'<span class="filemgr-item-icon">' + (direction === 'upload' ? UPLOAD_ICON : DOWNLOAD_ICON) + '</span>' +
|
||||
'<span class="filemgr-item-name">' + escapeHtml(name) + '</span>' +
|
||||
'<span class="filemgr-item-status">0%</span>' +
|
||||
'</div>' +
|
||||
'<div class="filemgr-progress-track"><div class="filemgr-progress-fill"></div></div>';
|
||||
list.insertBefore(el, list.firstChild);
|
||||
|
||||
transfers[id] = { el: el, status: 'active' };
|
||||
panel.classList.add('filemgr-expanded');
|
||||
updateBadge();
|
||||
return id;
|
||||
}
|
||||
|
||||
function updateProgress(id, percent) {
|
||||
var t = transfers[id];
|
||||
if (!t)
|
||||
return;
|
||||
t.el.querySelector('.filemgr-progress-fill').style.width = percent + '%';
|
||||
t.el.querySelector('.filemgr-item-status').textContent = percent + '%';
|
||||
}
|
||||
|
||||
function markDone(id, statusText) {
|
||||
var t = transfers[id];
|
||||
if (!t)
|
||||
return;
|
||||
t.status = 'done';
|
||||
t.el.classList.add('filemgr-done');
|
||||
t.el.querySelector('.filemgr-progress-fill').style.width = '100%';
|
||||
t.el.querySelector('.filemgr-item-status').textContent = statusText || 'Done';
|
||||
updateBadge();
|
||||
}
|
||||
|
||||
function markFailed(id, statusText) {
|
||||
var t = transfers[id];
|
||||
if (!t)
|
||||
return;
|
||||
t.status = 'failed';
|
||||
t.el.classList.add('filemgr-failed');
|
||||
t.el.querySelector('.filemgr-item-status').textContent = statusText || 'Failed';
|
||||
updateBadge();
|
||||
}
|
||||
|
||||
// Uploads a FileList/array of File objects, each as its own
|
||||
// concurrent XHR request - the browser's own HTTP connection pooling
|
||||
// runs several at once, tracked here as separate panel entries with
|
||||
// independent progress via xhr.upload's own progress event (fetch()
|
||||
// has no equivalent for the request/upload side, only the response).
|
||||
window.fileManagerUpload = function(files) {
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
(function(file) {
|
||||
var id = addTransfer(file.name, 'upload');
|
||||
var xhr = new XMLHttpRequest();
|
||||
transfers[id].xhr = xhr;
|
||||
xhr.open('POST', '/documents/api/upload');
|
||||
xhr.upload.addEventListener('progress', function(e) {
|
||||
if (e.lengthComputable)
|
||||
updateProgress(id, Math.round((e.loaded / e.total) * 100));
|
||||
});
|
||||
xhr.addEventListener('load', function() {
|
||||
var res = null;
|
||||
try { res = JSON.parse(xhr.responseText); } catch (e) {}
|
||||
if (xhr.status === 200 && res && res.ok) {
|
||||
markDone(id);
|
||||
document.dispatchEvent(new CustomEvent('documents:changed'));
|
||||
} else {
|
||||
markFailed(id, (res && res.error) || 'Failed');
|
||||
}
|
||||
});
|
||||
xhr.addEventListener('error', function() { markFailed(id); });
|
||||
var fd = new FormData();
|
||||
fd.append('name', file.name);
|
||||
fd.append('filedata', file);
|
||||
xhr.send(fd);
|
||||
})(files[i]);
|
||||
}
|
||||
};
|
||||
|
||||
// Downloads a document by id via XHR (responseType 'blob') instead
|
||||
// of a plain <a href> so it gets real tracked progress in the same
|
||||
// panel as uploads, then hands the fetched bytes to the browser's
|
||||
// normal save flow via a Blob URL + synthetic <a download> click.
|
||||
window.fileManagerDownload = function(id, name) {
|
||||
var xferId = addTransfer(name, 'download');
|
||||
var xhr = new XMLHttpRequest();
|
||||
transfers[xferId].xhr = xhr;
|
||||
xhr.open('GET', '/documents/download?id=' + encodeURIComponent(id));
|
||||
xhr.responseType = 'blob';
|
||||
xhr.addEventListener('progress', function(e) {
|
||||
if (e.lengthComputable)
|
||||
updateProgress(xferId, Math.round((e.loaded / e.total) * 100));
|
||||
});
|
||||
xhr.addEventListener('load', function() {
|
||||
if (xhr.status !== 200) {
|
||||
markFailed(xferId);
|
||||
return;
|
||||
}
|
||||
var blobUrl = URL.createObjectURL(xhr.response);
|
||||
var a = document.createElement('a');
|
||||
a.href = blobUrl;
|
||||
a.download = name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(function() { URL.revokeObjectURL(blobUrl); }, 30000);
|
||||
markDone(xferId);
|
||||
});
|
||||
xhr.addEventListener('error', function() { markFailed(xferId); });
|
||||
xhr.send();
|
||||
};
|
||||
|
||||
toggle.addEventListener('click', function() {
|
||||
panel.classList.toggle('filemgr-expanded');
|
||||
});
|
||||
|
||||
clearBtn.addEventListener('click', function() {
|
||||
for (var k in transfers) {
|
||||
if (transfers[k].status !== 'active') {
|
||||
transfers[k].el.remove();
|
||||
delete transfers[k];
|
||||
}
|
||||
}
|
||||
showEmptyPlaceholderIfNeeded();
|
||||
});
|
||||
})();
|
||||
Reference in new issue
Block a user