From f49fb4c48664dedd4929096d2bb7ea532bf0bcc1 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:29:18 +0100 Subject: [PATCH] Refactors upload panels for reusable bulk/batch logic Introduces composable utilities and shared components for bulk operations and validation across upload panels, reducing duplication and improving maintainability. Moves extension, dbkey, and upload settings header UI into reusable components, centralizes item validation and upload readiness checks, and cleans up code logic. Enhances consistency and simplifies future enhancements to upload features. --- .../Upload/methods/PasteContentUpload.vue | 20 +- .../Upload/methods/PasteLinksUpload.vue | 336 +++--------------- .../shared/UploadTableBulkDbKeyHeader.vue | 54 +++ .../shared/UploadTableBulkExtensionHeader.vue | 62 ++++ .../shared/UploadTableOptionsHeader.vue | 86 +++++ .../Upload/shared/upload-table-shared.scss | 88 +++++ .../upload/bulkUploadOperations.ts | 257 ++++++++++++++ .../upload/uploadItemValidation.ts | 42 +++ .../composables/upload/uploadReadyState.ts | 63 ++++ client/src/utils/url.ts | 97 +++++ 10 files changed, 814 insertions(+), 291 deletions(-) create mode 100644 client/src/components/Panels/Upload/shared/UploadTableBulkDbKeyHeader.vue create mode 100644 client/src/components/Panels/Upload/shared/UploadTableBulkExtensionHeader.vue create mode 100644 client/src/components/Panels/Upload/shared/UploadTableOptionsHeader.vue create mode 100644 client/src/components/Panels/Upload/shared/upload-table-shared.scss create mode 100644 client/src/composables/upload/bulkUploadOperations.ts create mode 100644 client/src/composables/upload/uploadItemValidation.ts create mode 100644 client/src/composables/upload/uploadReadyState.ts 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 });