Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/inputs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ JSON format).
When uploading through the Web UI, navigate to your project and use the upload
interface in the "Inputs" panel.

.. note::

Uploading directories or folders directly is not supported. Please compress
directories into an archive (such as ``.zip`` or ``.tar.gz``) before uploading.

For REST API uploads, refer to the :ref:`rest_api` documentation for endpoint details.

.. _inputs_download_url:
Expand Down
209 changes: 197 additions & 12 deletions scancodeio/static/js/add_inputs.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@

const fileInput = document.querySelector("#id_input_files");
let selectedFiles = []; // Store selected files
fileInput.onchange = updateFiles;
let folderAlertTimeout = null;
if (fileInput) {
fileInput.onchange = updateFiles;
}

// Update the list of files to be uploaded in the UI
function updateFiles() {
if (fileInput.files.length > 0) {
if (fileInput && fileInput.files.length > 0) {
const fileName = document.querySelector("#inputs_file_name");
fileName.replaceChildren();

Expand Down Expand Up @@ -100,24 +103,206 @@ function removeFile(fileName) {
dataTransfer.items.add(file);
}

fileInput.files = dataTransfer.files;
if (fileInput) {
fileInput.files = dataTransfer.files;
}
}

function inspectDataTransfer(dataTransfer) {
const validFiles = [];
const rejectedFolders = [];

const items = dataTransfer.items;
if (items && items.length > 0) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind !== "file") continue;

const entry = item.webkitGetAsEntry ? item.webkitGetAsEntry() : null;
if (entry) {
if (entry.isDirectory) {
rejectedFolders.push(entry.name || "folder");
continue;
}
}

const file = item.getAsFile ? item.getAsFile() : null;
if (file) {
validFiles.push(file);
}
}
} else if (dataTransfer.files && dataTransfer.files.length > 0) {
for (const file of dataTransfer.files) {
validFiles.push(file);
}
}

return { validFiles, rejectedFolders };
}

function showFolderUploadWarning(count) {
const inputFilesBox = document.querySelector("#input_files_box");
if (!inputFilesBox) return;

const fileCta = inputFilesBox.querySelector(".file-cta");
if (fileCta) {
fileCta.classList.remove("has-folder-error");
void fileCta.offsetWidth;
fileCta.classList.add("has-folder-error");
setTimeout(() => {
fileCta.classList.remove("has-folder-error");
}, 600);
}

if (folderAlertTimeout) {
clearTimeout(folderAlertTimeout);
folderAlertTimeout = null;
}

let tray = inputFilesBox.querySelector(".folder-notice-tray");
let isNew = false;

if (!tray) {
isNew = true;
tray = document.createElement("div");
tray.className = "folder-notice-tray";
tray.setAttribute("role", "alert");
tray.setAttribute("aria-live", "polite");

const inner = document.createElement("div");
inner.className = "folder-notice-inner";

const bar = document.createElement("div");
bar.className = "folder-notice-bar";

const main = document.createElement("div");
main.className = "folder-notice-main";

const iconSpan = document.createElement("span");
iconSpan.className = "folder-notice-icon";
const icon = document.createElement("i");
icon.className = "fa-solid fa-folder-minus";
iconSpan.appendChild(icon);
main.appendChild(iconSpan);

const msg = document.createElement("span");
msg.className = "folder-notice-msg";
msg.append("Folders cannot be uploaded directly. Please upload a ");

const zipKbd = document.createElement("kbd");
zipKbd.className = "folder-notice-kbd";
zipKbd.textContent = ".zip";
msg.appendChild(zipKbd);

msg.append(" or ");

const tarKbd = document.createElement("kbd");
tarKbd.className = "folder-notice-kbd";
tarKbd.textContent = ".tar.gz";
msg.appendChild(tarKbd);

msg.append(" archive instead.");

const countSpan = document.createElement("span");
countSpan.className = "folder-notice-count";
countSpan.textContent = `(Folders skipped: ${count || 1})`;
msg.appendChild(countSpan);

main.appendChild(msg);

const dismissBtn = document.createElement("button");
dismissBtn.type = "button";
dismissBtn.className = "folder-notice-dismiss";
dismissBtn.setAttribute("aria-label", "Dismiss notice");
dismissBtn.setAttribute("title", "Dismiss");
const dismissIcon = document.createElement("i");
dismissIcon.className = "fa-solid fa-xmark";
dismissBtn.appendChild(dismissIcon);

bar.appendChild(main);
bar.appendChild(dismissBtn);
inner.appendChild(bar);
tray.appendChild(inner);

inputFilesBox.appendChild(tray);
inputFilesBox.classList.add("has-folder-notice");
} else {
const countSpan = tray.querySelector(".folder-notice-count");
if (countSpan && count) {
countSpan.textContent = `(Folders skipped: ${count})`;
}
}

function dismiss() {
if (folderAlertTimeout) {
clearTimeout(folderAlertTimeout);
folderAlertTimeout = null;
}
if (tray && tray.parentNode) {
tray.classList.remove("is-open");
setTimeout(() => {
if (tray && tray.parentNode) {
tray.remove();
}
if (inputFilesBox && !inputFilesBox.querySelector(".folder-notice-tray")) {
inputFilesBox.classList.remove("has-folder-notice");
}
}, 230);
}
}

const dismissBtn = tray.querySelector(".folder-notice-dismiss");
if (dismissBtn) {
dismissBtn.onclick = dismiss;
}

if (isNew) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
tray.classList.add("is-open");
});
});
}

folderAlertTimeout = setTimeout(dismiss, 8000);
tray.onmouseenter = () => {
if (folderAlertTimeout) {
clearTimeout(folderAlertTimeout);
folderAlertTimeout = null;
}
};
tray.onmouseleave = () => {
folderAlertTimeout = setTimeout(dismiss, 3000);
};
}

function dropHandler(event) {
disableEvent(event);

// Merge existing files and dropped files, let updateFiles handle dedup
const dataTransfer = new DataTransfer();
for (const file of [...fileInput.files, ...event.dataTransfer.files]) {
dataTransfer.items.add(file);
const { validFiles, rejectedFolders } = inspectDataTransfer(event.dataTransfer);

if (rejectedFolders.length > 0) {
showFolderUploadWarning(rejectedFolders.length);
}

fileInput.files = dataTransfer.files;
updateFiles();
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
const existing = fileInput ? fileInput.files : [];
for (const file of [...existing, ...validFiles]) {
dataTransfer.items.add(file);
}

if (fileInput) {
fileInput.files = dataTransfer.files;
updateFiles();
}
}
}

// Handle drag and drop events
const inputFilesBox = document.querySelector("#input_files_box");
inputFilesBox.addEventListener("dragenter", disableEvent);
inputFilesBox.addEventListener("dragover", disableEvent);
inputFilesBox.addEventListener("drop", dropHandler);
if (inputFilesBox) {
inputFilesBox.addEventListener("dragenter", disableEvent);
inputFilesBox.addEventListener("dragover", disableEvent);
inputFilesBox.addEventListener("drop", dropHandler);
}
Loading