From faf12b6e5c5a24225996d25a731cefa9f7cc3338 Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Mon, 18 Nov 2024 23:49:01 -0600 Subject: [PATCH] add uploaded files to collection directly In the case of `list`, they are now added directly to the final list. In the case of `list:paired` and `paired`, they are added to the `workingElements`; the user can then manually add them. This is all still done within the `CollectionCreator`. Next thing to look at is to try to only allow/wait for those uploaded files to be attempted as collection additions if they are of `ok` state. One way to do that is to maybe use a list to keep track of uploaded items in the creator, and if those uploaded items disappear from `workingElements` (meaning they are invalid) we have explicit Toasts mentioning this. --- client/src/api/datatypes.ts | 3 + .../Collections/ListCollectionCreator.vue | 75 +++++++++++++---- .../Collections/PairCollectionCreator.vue | 80 ++++++++++++++++--- .../PairedListCollectionCreator.vue | 39 ++++++++- .../Collections/common/CollectionCreator.vue | 78 +++++++++++++++--- client/src/components/Help/terms.yml | 6 ++ client/src/components/Upload/DefaultBox.vue | 39 +++++++-- 7 files changed, 272 insertions(+), 48 deletions(-) create mode 100644 client/src/api/datatypes.ts diff --git a/client/src/api/datatypes.ts b/client/src/api/datatypes.ts new file mode 100644 index 000000000000..0b6d30a71c60 --- /dev/null +++ b/client/src/api/datatypes.ts @@ -0,0 +1,3 @@ +import { type components } from "@/api"; + +export type CompositeFileInfo = components["schemas"]["CompositeFileInfo"]; diff --git a/client/src/components/Collections/ListCollectionCreator.vue b/client/src/components/Collections/ListCollectionCreator.vue index e743d2b4b33e..b1b94e8d70a5 100644 --- a/client/src/components/Collections/ListCollectionCreator.vue +++ b/client/src/components/Collections/ListCollectionCreator.vue @@ -5,10 +5,11 @@ import { faSquare } from "@fortawesome/free-regular-svg-icons"; import { faMinus, faSortAlphaDown, faTimes, faUndo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { BAlert, BButton, BButtonGroup } from "bootstrap-vue"; -import { computed, onMounted, ref, watch } from "vue"; +import { computed, ref, watch } from "vue"; import draggable from "vuedraggable"; import type { HDASummary, HistoryItemSummary } from "@/api"; +import { Toast } from "@/composables/toast"; import STATES from "@/mvc/dataset/states"; import { useDatatypesMapperStore } from "@/stores/datatypesMapperStore"; import localize from "@/utils/localization"; @@ -17,6 +18,8 @@ import FormSelectMany from "../Form/Elements/FormSelectMany/FormSelectMany.vue"; import CollectionCreator from "@/components/Collections/common/CollectionCreator.vue"; import DatasetCollectionElementView from "@/components/Collections/ListDatasetCollectionElementView.vue"; +const NOT_VALID_ELEMENT_MSG: string = localize("is not a valid element for this collection"); + interface Props { historyId: string; initialElements: HistoryItemSummary[]; @@ -75,15 +78,6 @@ const datatypesMapper = computed(() => datatypesMapperStore.datatypesMapper); /** Are we filtering by datatype? */ const filterExtensions = computed(() => !!datatypesMapper.value && !!props.extensions?.length); -// TODO: This needs to be done in the other creators as well -watch( - () => props.initialElements, - () => { - // for any new/removed elements, add them to working elements - _elementsSetUp(); - } -); - /** set up instance vars function */ function _instanceSetUp() { /** Ids of elements that have been selected by the user - to preserve over renders */ @@ -107,6 +101,26 @@ function _elementsSetUp() { // reverse the order of the elements to emulate what we have in the history panel workingElements.value.reverse(); + // for inListElements, reset their values (in order) to datasets from workingElements + const inListElementsPrev = inListElements.value; + inListElements.value = []; + inListElementsPrev.forEach((prevElem) => { + const element = workingElements.value.find((e) => e.id === prevElem.id); + const problem = _isElementInvalid(prevElem); + + if (element) { + inListElements.value.push(element); + } else if (problem) { + const invalidMsg = `${prevElem.hid}: ${prevElem.name} ${problem} and ${NOT_VALID_ELEMENT_MSG}`; + invalidElements.value.push(invalidMsg); + Toast.error(invalidMsg, localize("Invalid element")); + } else { + const invalidMsg = `${prevElem.hid}: ${prevElem.name} ${localize("has been removed from the collection")}`; + invalidElements.value.push(invalidMsg); + Toast.error(invalidMsg, localize("Invalid element")); + } + }); + // _ensureElementIds(); _validateElements(); _mangleDuplicateNames(); @@ -285,12 +299,17 @@ function onUpdateHideSourceItems(newHideSourceItems: boolean) { hideSourceItems.value = newHideSourceItems; } -// TODO: No need to use `onMounted` here, this can be done in the `setup` -onMounted(() => { - _instanceSetUp(); - _elementsSetUp(); - saveOriginalNames(); -}); +_instanceSetUp(); +saveOriginalNames(); + +watch( + () => props.initialElements, + () => { + // for any new/removed elements, add them to working elements + _elementsSetUp(); + }, + { immediate: true } +); watch( () => datatypesMapper.value, @@ -302,6 +321,29 @@ watch( { immediate: true } ); +function addUploadedFiles(files: HDASummary[]) { + const returnedElements = props.fromSelection ? workingElements : inListElements; + files.forEach((f) => { + const file = props.fromSelection ? f : workingElements.value.find((e) => e.id === f.id); + const problem = _isElementInvalid(f); + if (file && !returnedElements.value.find((e) => e.id === file.id)) { + returnedElements.value.push(file); + } else if (problem) { + invalidElements.value.push("Uploaded item: " + f.name + " " + problem); + Toast.error( + localize(`Dataset ${f.hid}: ${f.name} ${problem} and is an invalid element for this collection`), + localize("Uploaded item is invalid") + ); + } else { + invalidElements.value.push("Uploaded item: " + f.name + " could not be added to the collection"); + Toast.error( + localize(`Dataset ${f.hid}: ${f.name} could not be added to the collection`), + localize("Uploaded item is invalid") + ); + } + }); +} + /** find the element in the workingElements array and update its name */ function renameElement(element: any, name: string) { element = workingElements.value.find((e) => e.id === element.id); @@ -420,6 +462,7 @@ function renameElement(element: any, name: string) { :history-id="props.historyId" :hide-source-items="hideSourceItems" :extensions="extensions" + @add-uploaded-files="addUploadedFiles" @on-update-datatype-toggle="changeDatatypeFilter" @onUpdateHideSourceItems="onUpdateHideSourceItems" @clicked-create="clickedCreate"> diff --git a/client/src/components/Collections/PairCollectionCreator.vue b/client/src/components/Collections/PairCollectionCreator.vue index 979a6bdf1ead..f7a9ab5f6d5a 100644 --- a/client/src/components/Collections/PairCollectionCreator.vue +++ b/client/src/components/Collections/PairCollectionCreator.vue @@ -2,9 +2,10 @@ import { faArrowsAltV } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { BAlert, BButton } from "bootstrap-vue"; -import { computed, onMounted, ref } from "vue"; +import { computed, ref, watch } from "vue"; import type { HDASummary, HistoryItemSummary } from "@/api"; +import { Toast } from "@/composables/toast"; import STATES from "@/mvc/dataset/states"; import { useDatatypesMapperStore } from "@/stores/datatypesMapperStore"; import localize from "@/utils/localization"; @@ -14,6 +15,8 @@ import type { DatasetPair } from "../History/adapters/buildCollectionModal"; import DatasetCollectionElementView from "./ListDatasetCollectionElementView.vue"; import CollectionCreator from "@/components/Collections/common/CollectionCreator.vue"; +const NOT_VALID_ELEMENT_MSG: string = localize("is not a valid element for this collection"); + interface SelectedDatasetPair { forward: HDASummary | undefined; reverse: HDASummary | undefined; @@ -71,6 +74,24 @@ const datatypesMapper = computed(() => datatypesMapperStore.datatypesMapper); /** Are we filtering by datatype? */ const filterExtensions = computed(() => !!datatypesMapper.value && !!props.extensions?.length); +watch( + () => props.initialElements, + () => { + // initialize; and then for any new/removed elements, sync working elements + _elementsSetUp(); + + // TODO: fix this, now either of those will almost always be empty + if (props.fromSelection && !initialSuggestedName.value) { + initialSuggestedName.value = _guessNameForPair( + workingElements.value[0] as HDASummary, + workingElements.value[1] as HDASummary, + removeExtensions.value + ); + } + }, + { immediate: true } +); + function _elementsSetUp() { /** a list of invalid elements and the reasons they aren't valid */ invalidElements.value = []; @@ -85,6 +106,35 @@ function _elementsSetUp() { // reverse the order of the elements to emulate what we have in the history panel workingElements.value.reverse(); + // for inListElements, reset their values (in order) to datasets from workingElements + const inListElementsPrev = inListElements.value; + inListElements.value = { forward: undefined, reverse: undefined }; + + for (const key of ["forward", "reverse"]) { + const prevElem = inListElementsPrev[key as keyof SelectedDatasetPair]; + if (!prevElem) { + continue; + } + const element = workingElements.value.find( + (e) => e.id === inListElementsPrev[key as keyof SelectedDatasetPair]?.id + ); + const problem = _isElementInvalid(prevElem); + if (element) { + inListElements.value[key as keyof SelectedDatasetPair] = element; + } else if (problem) { + const invalidMsg = `${prevElem.hid}: ${prevElem.name} ${problem} and ${NOT_VALID_ELEMENT_MSG}`; + invalidElements.value.push(invalidMsg); + Toast.error(invalidMsg, localize("Invalid element")); + } else { + const invalidMsg = `${prevElem.hid}: ${prevElem.name} ${localize("has been removed from the collection")}`; + invalidElements.value.push(invalidMsg); + Toast.error(invalidMsg, localize("Invalid element")); + } + } + + // TODO: Next thing to add is: If the user adds an uploaded file, that is eventually nuked from workingElements + // because it is invalid, Toast an error for that file by keeping track of uploaded ids in a separate list + _ensureElementIds(); _validateElements(); } @@ -176,6 +226,21 @@ function swapButton() { } } +function addUploadedFiles(files: HDASummary[]) { + // Any added files are added to workingElements in _elementsSetUp + // The user will have to manually select the files to add them to the pair + + // Check for validity of uploads + files.forEach((file) => { + const problem = _isElementInvalid(file); + if (problem) { + const invalidMsg = `${file.hid}: ${file.name} ${problem} and ${NOT_VALID_ELEMENT_MSG}`; + invalidElements.value.push(invalidMsg); + Toast.error(invalidMsg, localize("Uploaded item invalid for pair")); + } + }); +} + function clickedCreate(collectionName: string) { if (state.value !== "error" && exactlyTwoValidElements.value) { const returnedPair = { @@ -270,17 +335,6 @@ function _naiveStartingAndEndingLCS(s1: string, s2: string) { return fwdLCS + revLCS; } - -onMounted(() => { - _elementsSetUp(); - - // TODO: fix this, now either of those will almost always be empty - initialSuggestedName.value = _guessNameForPair( - workingElements.value[0] as HDASummary, - workingElements.value[1] as HDASummary, - removeExtensions.value - ); -});