diff --git a/client/src/components/Panels/Upload/methods/PasteContentUpload.vue b/client/src/components/Panels/Upload/methods/PasteContentUpload.vue index 6748713e539..7534d62c8ad 100644 --- a/client/src/components/Panels/Upload/methods/PasteContentUpload.vue +++ b/client/src/components/Panels/Upload/methods/PasteContentUpload.vue @@ -58,7 +58,7 @@ interface PasteItem { id: number; name: string; content: string; - ext: string; + extension: string; dbkey: string; spaceToTab: boolean; toPosixLines: boolean; @@ -72,7 +72,7 @@ const pasteItems = ref([ id: nextId++, name: "Pasted Dataset 1", content: "", - ext: configOptions.value?.defaultExtension || "auto", + extension: configOptions.value?.defaultExtension || "auto", dbkey: configOptions.value?.defaultDbKey || "?", spaceToTab: false, toPosixLines: false, @@ -101,7 +101,7 @@ function addPasteItem() { id: newId, name: `Pasted Dataset ${pasteItems.value.length + 1}`, content: "", - ext: configOptions.value?.defaultExtension || "auto", + extension: configOptions.value?.defaultExtension || "auto", dbkey: configOptions.value?.defaultDbKey || "?", spaceToTab: false, toPosixLines: false, @@ -180,7 +180,7 @@ const bulkExtensionWarning = computed(() => { function setAllExtensions(extension: string | null) { if (extension) { pasteItems.value.forEach((item) => { - item.ext = extension; + item.extension = extension; }); } } @@ -285,10 +285,10 @@ function startUpload() { const uploads = validItems.map((item) => ({ uploadMode: "paste-content" as const, name: item.name, - size: item.content.length, + size: new Blob([item.content]).size, targetHistoryId: props.targetHistoryId, dbkey: item.dbkey, - extension: item.ext, + extension: item.extension, spaceToTab: item.spaceToTab, toPosixLines: item.toPosixLines, deferred: false, @@ -313,7 +313,7 @@ function startUpload() { id: newId, name: "Pasted Dataset 1", content: "", - ext: configOptions.value?.defaultExtension || "auto", + extension: configOptions.value?.defaultExtension || "auto", dbkey: configOptions.value?.defaultDbKey || "?", spaceToTab: false, toPosixLines: false, @@ -445,7 +445,7 @@ defineExpose({ startUpload }); diff --git a/client/src/components/Panels/Upload/methods/PasteLinksUpload.vue b/client/src/components/Panels/Upload/methods/PasteLinksUpload.vue index fd8040bd63c..d38c6bdb276 100644 --- a/client/src/components/Panels/Upload/methods/PasteLinksUpload.vue +++ b/client/src/components/Panels/Upload/methods/PasteLinksUpload.vue @@ -4,15 +4,21 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { BFormCheckbox, BFormInput, BFormSelect, BTable } from "bootstrap-vue"; import { computed, nextTick, ref, watch } from "vue"; -import { findExtension } from "@/components/Upload/utils"; +import { useBulkUploadOperations } from "@/composables/upload/bulkUploadOperations"; +import { useUploadItemValidation } from "@/composables/upload/uploadItemValidation"; +import { useUploadReadyState } from "@/composables/upload/uploadReadyState"; import { useUploadConfigurations } from "@/composables/uploadConfigurations"; import type { CollectionConfig } from "@/composables/uploadQueue"; import { useUploadQueue } from "@/composables/uploadQueue"; +import { extractNameFromUrl, isValidUrl, validateUrl } from "@/utils/url"; import type { UploadMethodComponent, UploadMethodConfig } from "../types"; import type { CollectionCreationState } from "../types/collectionCreation"; import CollectionCreationConfig from "../CollectionCreationConfig.vue"; +import UploadTableBulkDbKeyHeader from "../shared/UploadTableBulkDbKeyHeader.vue"; +import UploadTableBulkExtensionHeader from "../shared/UploadTableBulkExtensionHeader.vue"; +import UploadTableOptionsHeader from "../shared/UploadTableOptionsHeader.vue"; import GButton from "@/components/BaseComponents/GButton.vue"; interface Props { @@ -51,7 +57,7 @@ interface UrlItem { id: number; url: string; name: string; - ext: string; + extension: string; dbkey: string; spaceToTab: boolean; toPosixLines: boolean; @@ -68,81 +74,27 @@ const placeholder = "https://example.org/data1.txt\nhttps://example.org/data2.tx const hasItems = computed(() => urlItems.value.length > 0); -const isReadyToUpload = computed(() => { - if (!hasItems.value) { - return false; - } - // Check if any URL is invalid - const hasInvalidUrls = urlItems.value.some((item) => isValidUrl(item.url) === false); - if (hasInvalidUrls) { - return false; - } - // If collection creation is active, it must be valid to proceed - if (collectionState.value.validation.isActive && !collectionState.value.validation.isValid) { - return false; - } - return true; -}); +const { isNameValid, restoreOriginalName: restoreOriginalNameBase } = useUploadItemValidation(); + +const bulk = useBulkUploadOperations(urlItems, effectiveExtensions); + +// Additional validation for URL items +const hasInvalidUrls = computed(() => urlItems.value.some((item) => !isValidUrl(item.url))); + +const { isReadyToUpload } = useUploadReadyState( + hasItems, + collectionState, + computed(() => !hasInvalidUrls.value), +); watch(isReadyToUpload, (ready) => emit("ready", ready), { immediate: true }); -function extractNameFromUrl(url: string): string { - try { - const urlObj = new URL(url); - const pathname = urlObj.pathname; - const segments = pathname.split("/").filter((s) => s.length > 0); - if (segments.length > 0) { - const lastSegment = segments[segments.length - 1]; - if (lastSegment) { - // Decode URI component to handle encoded characters - return decodeURIComponent(lastSegment); - } - } - } catch { - // If URL parsing fails, fall through to return the full URL - } - return url; -} - -function validateUrl(url: string): { isValid: boolean; message: string | null } { - if (!url.trim()) { - return { isValid: false, message: "URL is required" }; - } - - try { - const urlObj = new URL(url); - - if (!urlObj.host) { - return { isValid: false, message: "URL must include a valid hostname" }; - } - - const hasContent = urlObj.pathname !== "/" || urlObj.search || urlObj.hash; - if (!hasContent) { - return { isValid: false, message: "URL should point to a specific file or resource" }; - } - - return { isValid: true, message: null }; - } catch { - return { isValid: false, message: "URL format is invalid or missing protocol" }; - } -} - -function isValidUrl(url: string): boolean { - return validateUrl(url).isValid; -} - function getUrlValidationMessage(url: string): string { return validateUrl(url).message || url; } -function isNameValid(name: string): boolean | null { - return name.trim().length > 0 ? null : false; -} - function restoreOriginalName(item: UrlItem) { - if (!item.name.trim()) { - item.name = extractNameFromUrl(item.url); - } + restoreOriginalNameBase(item, extractNameFromUrl(item.url)); } function addUrlsFromText() { @@ -163,7 +115,7 @@ function addUrlsFromText() { id: nextId++, url, name: extractNameFromUrl(url), - ext: defaultExtension, + extension: defaultExtension, dbkey: defaultDbKey, spaceToTab: false, toPosixLines: false, @@ -197,80 +149,6 @@ function handleCollectionStateChange(state: CollectionCreationState) { collectionState.value = state; } -function getExtensionWarning(extensionId: string): string | null { - const ext = findExtension(effectiveExtensions.value, extensionId); - return ext?.upload_warning || null; -} - -// Bulk operations -const bulkExtension = ref(null); -const bulkDbKey = ref(null); - -const bulkExtensionWarning = computed(() => { - if (!bulkExtension.value) { - return null; - } - const ext = findExtension(effectiveExtensions.value, bulkExtension.value); - return ext?.upload_warning || null; -}); - -function setAllExtensions(extension: string | null) { - if (extension) { - urlItems.value.forEach((item) => { - item.ext = extension; - }); - } -} - -function setAllDbKeys(dbKey: string | null) { - if (dbKey) { - urlItems.value.forEach((item) => { - item.dbkey = dbKey; - }); - } -} - -const allSpaceToTab = computed(() => urlItems.value.length > 0 && urlItems.value.every((f) => f.spaceToTab)); - -const allToPosixLines = computed(() => urlItems.value.length > 0 && urlItems.value.every((f) => f.toPosixLines)); - -const allDeferred = computed(() => urlItems.value.length > 0 && urlItems.value.every((f) => f.deferred)); - -const spaceToTabIndeterminate = computed( - () => - urlItems.value.length > 0 && - urlItems.value.some((f) => f.spaceToTab) && - !urlItems.value.every((f) => f.spaceToTab), -); - -const toPosixLinesIndeterminate = computed( - () => - urlItems.value.length > 0 && - urlItems.value.some((f) => f.toPosixLines) && - !urlItems.value.every((f) => f.toPosixLines), -); - -const deferredIndeterminate = computed( - () => - urlItems.value.length > 0 && urlItems.value.some((f) => f.deferred) && !urlItems.value.every((f) => f.deferred), -); - -function toggleAllSpaceToTab() { - const newValue = !allSpaceToTab.value; - urlItems.value.forEach((f) => (f.spaceToTab = newValue)); -} - -function toggleAllToPosixLines() { - const newValue = !allToPosixLines.value; - urlItems.value.forEach((f) => (f.toPosixLines = newValue)); -} - -function toggleAllDeferred() { - const newValue = !allDeferred.value; - urlItems.value.forEach((f) => (f.deferred = newValue)); -} - -// Table configuration const tableFields = [ { key: "name", @@ -329,7 +207,7 @@ function startUpload() { size: 0, targetHistoryId: props.targetHistoryId, dbkey: item.dbkey, - extension: item.ext, + extension: item.extension, spaceToTab: item.spaceToTab, toPosixLines: item.toPosixLines, deferred: item.deferred, @@ -420,33 +298,19 @@ defineExpose({ startUpload });