diff --git a/docs/inputs.rst b/docs/inputs.rst index d87acd2203..3a3c7f14bb 100644 --- a/docs/inputs.rst +++ b/docs/inputs.rst @@ -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: diff --git a/scancodeio/static/js/add_inputs.js b/scancodeio/static/js/add_inputs.js index 1850759c38..b0a79489dc 100644 --- a/scancodeio/static/js/add_inputs.js +++ b/scancodeio/static/js/add_inputs.js @@ -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(); @@ -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); +} diff --git a/scancodeio/static/main.css b/scancodeio/static/main.css index f85577d7d1..0c302166d5 100644 --- a/scancodeio/static/main.css +++ b/scancodeio/static/main.css @@ -327,9 +327,218 @@ button.as-link { .input.is-smaller { font-size: 0.9rem; } +:root { + --folder-notice-bg: #fffdf7; + --folder-notice-border: #fcd34d; + --folder-notice-divider: rgba(217, 119, 6, 0.2); + --folder-notice-accent: #d97706; + --folder-notice-text: #78350f; + --folder-notice-kbd-bg: rgba(217, 119, 6, 0.08); + --folder-notice-kbd-border: rgba(217, 119, 6, 0.22); + --folder-notice-kbd-text: #92400e; + --folder-notice-hover: rgba(120, 53, 15, 0.08); + --folder-notice-glow: rgba(217, 119, 6, 0.18); + --folder-notice-cta-tint: rgba(245, 158, 11, 0.04); +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --folder-notice-bg: #1c1f26; + --folder-notice-border: #374151; + --folder-notice-divider: rgba(245, 158, 11, 0.25); + --folder-notice-accent: #fbbf24; + --folder-notice-text: #f3f4f6; + --folder-notice-kbd-bg: rgba(255, 255, 255, 0.08); + --folder-notice-kbd-border: rgba(255, 255, 255, 0.16); + --folder-notice-kbd-text: #fde68a; + --folder-notice-hover: rgba(255, 255, 255, 0.1); + --folder-notice-glow: rgba(245, 158, 11, 0.2); + --folder-notice-cta-tint: rgba(245, 158, 11, 0.06); + } +} + +[data-theme="dark"], +.theme-dark { + --folder-notice-bg: #1c1f26; + --folder-notice-border: #374151; + --folder-notice-divider: rgba(245, 158, 11, 0.25); + --folder-notice-accent: #fbbf24; + --folder-notice-text: #f3f4f6; + --folder-notice-kbd-bg: rgba(255, 255, 255, 0.08); + --folder-notice-kbd-border: rgba(255, 255, 255, 0.16); + --folder-notice-kbd-text: #fde68a; + --folder-notice-hover: rgba(255, 255, 255, 0.1); + --folder-notice-glow: rgba(245, 158, 11, 0.2); + --folder-notice-cta-tint: rgba(245, 158, 11, 0.06); +} + +[data-theme="light"], +.theme-light { + --folder-notice-bg: #fffdf7; + --folder-notice-border: #fcd34d; + --folder-notice-divider: rgba(217, 119, 6, 0.2); + --folder-notice-accent: #d97706; + --folder-notice-text: #78350f; + --folder-notice-kbd-bg: rgba(217, 119, 6, 0.08); + --folder-notice-kbd-border: rgba(217, 119, 6, 0.22); + --folder-notice-kbd-text: #92400e; + --folder-notice-hover: rgba(120, 53, 15, 0.08); + --folder-notice-glow: rgba(217, 119, 6, 0.18); + --folder-notice-cta-tint: rgba(245, 158, 11, 0.04); +} + +#input_files_box { + flex-direction: column; +} + .file.is-boxed.has-name .file-cta { border-width: medium; border-style: dashed; + transition: border-color 220ms cubic-bezier(0.25, 1, 0.5, 1), + background-color 220ms cubic-bezier(0.25, 1, 0.5, 1), + box-shadow 220ms cubic-bezier(0.25, 1, 0.5, 1); +} + +.file.is-boxed.has-name .file-cta.has-folder-error { + border-color: var(--folder-notice-accent) !important; + background-color: var(--folder-notice-cta-tint) !important; + box-shadow: 0 0 0 3px var(--folder-notice-glow) !important; +} + +#input_files_box.has-folder-notice #inputs_file_name { + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; + border-bottom-color: var(--folder-notice-divider) !important; + transition: border-radius 220ms cubic-bezier(0.25, 1, 0.5, 1), + border-bottom-color 220ms ease; +} + +.folder-notice-tray { + display: grid; + grid-template-rows: 0fr; + opacity: 0; + width: 100%; + transition: grid-template-rows 220ms cubic-bezier(0.25, 1, 0.5, 1), + opacity 180ms ease; + will-change: grid-template-rows, opacity; +} + +.folder-notice-tray.is-open { + grid-template-rows: 1fr; + opacity: 1; + transition: grid-template-rows 280ms cubic-bezier(0.16, 1, 0.3, 1), + opacity 200ms ease; +} + +.folder-notice-inner { + overflow: hidden; + min-height: 0; +} + +.folder-notice-bar { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.85rem; + padding: 0.55rem 0.85rem; + font-size: 0.8125rem; + line-height: 1.45; + background-color: var(--folder-notice-bg); + border: 1px solid var(--folder-notice-border); + border-top: none; + border-bottom-left-radius: var(--bulma-file-radius, var(--bulma-radius, 6px)); + border-bottom-right-radius: var(--bulma-file-radius, var(--bulma-radius, 6px)); + color: var(--folder-notice-text); + box-shadow: inset 0 1px 0 var(--folder-notice-divider); +} + +.folder-notice-main { + display: flex; + align-items: flex-start; + gap: 0.75rem; + flex: 1; + min-width: 0; +} + +.folder-notice-icon { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--folder-notice-accent); + font-size: 0.95rem; + line-height: 1; + margin-top: 0.125rem; +} + +.folder-notice-msg { + color: var(--folder-notice-text); +} + +.folder-notice-msg strong { + color: inherit; + font-weight: 600; +} + +.folder-notice-kbd { + display: inline-block; + padding: 0.08em 0.35em; + font-family: var(--bulma-family-monospace, monospace); + font-size: 0.75rem; + font-weight: 600; + line-height: 1.2; + border-radius: 3px; + background-color: var(--folder-notice-kbd-bg); + border: 1px solid var(--folder-notice-kbd-border); + color: var(--folder-notice-kbd-text); + vertical-align: baseline; +} + +.folder-notice-count { + display: inline-block; + margin-left: 0.4rem; + font-size: 0.775rem; + opacity: 0.85; + white-space: nowrap; +} + +.folder-notice-dismiss { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.35rem; + height: 1.35rem; + padding: 0; + border: none; + background: transparent; + color: var(--folder-notice-text); + opacity: 0.6; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + line-height: 1; + margin-top: -0.05rem; + transition: opacity 140ms ease, background-color 140ms ease, transform 80ms ease; +} + +.folder-notice-dismiss:hover { + opacity: 1; + background-color: var(--folder-notice-hover); +} + +.folder-notice-dismiss:active { + transform: scale(0.9); +} + +@media (prefers-reduced-motion: reduce) { + .folder-notice-tray, + .folder-notice-bar, + .file.is-boxed.has-name .file-cta, + #input_files_box.has-folder-notice #inputs_file_name { + transition: none !important; + transform: none !important; + } } progress.file-upload::before { content: "Files upload: " attr(value) "%";