diff --git a/client/src/components/Collections/ListCollectionCreator.vue b/client/src/components/Collections/ListCollectionCreator.vue index d33f98927d83..825c2c4a13b3 100644 --- a/client/src/components/Collections/ListCollectionCreator.vue +++ b/client/src/components/Collections/ListCollectionCreator.vue @@ -16,6 +16,7 @@ import { useDatatypesMapperStore } from "@/stores/datatypesMapperStore"; import localize from "@/utils/localization"; import FormSelectMany from "../Form/Elements/FormSelectMany/FormSelectMany.vue"; +import HelpText from "../Help/HelpText.vue"; import CollectionCreator from "@/components/Collections/common/CollectionCreator.vue"; import DatasetCollectionElementView from "@/components/Collections/ListDatasetCollectionElementView.vue"; @@ -79,6 +80,12 @@ const datatypesMapper = computed(() => datatypesMapperStore.datatypesMapper); /** Are we filtering by datatype? */ const filterExtensions = computed(() => !!datatypesMapper.value && !!props.extensions?.length); +/** Does `inListElements` have elements with different extensions? */ +const listHasMixedExtensions = computed(() => { + const extensions = new Set(inListElements.value.map((e) => e.extension)); + return extensions.size > 1; +}); + // ----------------------------------------------------------------------- process raw list /** set up main data */ function _elementsSetUp() { @@ -168,11 +175,26 @@ function _isElementInvalid(element: HistoryItemSummary): string | null { element.extension && !datatypesMapper.value?.isSubTypeOfAny(element.extension, props.extensions!) ) { - return localize(`has an invalid extension: ${element.extension}`); + return localize(`has an invalid format: ${element.extension}`); } return null; } +/** Show the element's extension next to its name: + * 1. If there are no required extensions, so users can avoid creating mixed extension lists. + * 2. If the extension is not in the list of required extensions but is a subtype of one of them, + * so users can see that those elements were still included as they are implicitly convertible. + */ +function showElementExtension(element: HDASummary) { + return ( + !props.extensions?.length || + (filterExtensions.value && + element.extension && + !props.extensions?.includes(element.extension) && + datatypesMapper.value?.isSubTypeOfAny(element.extension, props.extensions!)) + ); +} + // /** mangle duplicate names using a mac-like '(counter)' addition to any duplicates */ function _mangleDuplicateNames() { var counter = 1; @@ -523,6 +545,14 @@ function renameElement(element: any, name: string) { diff --git a/client/src/components/Collections/ListDatasetCollectionElementView.vue b/client/src/components/Collections/ListDatasetCollectionElementView.vue index 537b3e40f6cb..f242499581a1 100644 --- a/client/src/components/Collections/ListDatasetCollectionElementView.vue +++ b/client/src/components/Collections/ListDatasetCollectionElementView.vue @@ -13,6 +13,7 @@ interface Props { selected?: boolean; hasActions?: boolean; notEditable?: boolean; + hideExtension?: boolean; } const props = defineProps(); @@ -48,7 +49,7 @@ function clickDiscard() { {{ elementName }} - ({{ element.extension }}) + ({{ element.extension }})
diff --git a/client/src/components/Collections/PairCollectionCreator.vue b/client/src/components/Collections/PairCollectionCreator.vue index c00c2c0f9b1e..95997a5c00c0 100644 --- a/client/src/components/Collections/PairCollectionCreator.vue +++ b/client/src/components/Collections/PairCollectionCreator.vue @@ -5,6 +5,8 @@ import { BAlert, BButton } from "bootstrap-vue"; import { computed, ref, watch } from "vue"; import type { HDASummary, HistoryItemSummary } from "@/api"; +import { useAnimationFrameResizeObserver } from "@/composables/sensors/animationFrameResizeObserver"; +import { useAnimationFrameScroll } from "@/composables/sensors/animationFrameScroll"; import { Toast } from "@/composables/toast"; import STATES from "@/mvc/dataset/states"; import { useDatatypesMapperStore } from "@/stores/datatypesMapperStore"; @@ -12,6 +14,8 @@ import localize from "@/utils/localization"; import type { DatasetPair } from "../History/adapters/buildCollectionModal"; +import DelayedInput from "../Common/DelayedInput.vue"; +import HelpText from "../Help/HelpText.vue"; import DatasetCollectionElementView from "./ListDatasetCollectionElementView.vue"; import CollectionCreator from "@/components/Collections/common/CollectionCreator.vue"; @@ -42,6 +46,13 @@ const removeExtensions = ref(true); const initialSuggestedName = ref(""); const invalidElements = ref([]); const workingElements = ref([]); +const filterText = ref(""); + +const filteredElements = computed(() => { + return workingElements.value.filter((element) => { + return `${element.hid}: ${element.name}`.toLowerCase().includes(filterText.value.toLowerCase()); + }); +}); /** If not `fromSelection`, the manually added elements that will become the pair */ const inListElements = ref({ forward: undefined, reverse: undefined }); @@ -66,6 +77,13 @@ const pairElements = computed(() => { return inListElements.value; } }); +const pairHasMixedExtensions = computed(() => { + return ( + pairElements.value.forward?.extension && + pairElements.value.reverse?.extension && + pairElements.value.forward.extension !== pairElements.value.reverse.extension + ); +}); // variables for datatype mapping and then filtering const datatypesMapperStore = useDatatypesMapperStore(); @@ -74,6 +92,16 @@ const datatypesMapper = computed(() => datatypesMapperStore.datatypesMapper); /** Are we filtering by datatype? */ const filterExtensions = computed(() => !!datatypesMapper.value && !!props.extensions?.length); +// check if we have scrolled to the top or bottom of the scrollable div +const scrollableDiv = ref(null); +const { arrived } = useAnimationFrameScroll(scrollableDiv); +const isScrollable = ref(false); +useAnimationFrameResizeObserver(scrollableDiv, ({ clientSize, scrollSize }) => { + isScrollable.value = scrollSize.height >= clientSize.height + 1; +}); +const scrolledTop = computed(() => !isScrollable.value || arrived.top); +const scrolledBottom = computed(() => !isScrollable.value || arrived.bottom); + watch( () => props.initialElements, () => { @@ -187,7 +215,7 @@ function _isElementInvalid(element: HistoryItemSummary) { element.extension && !datatypesMapper.value?.isSubTypeOfAny(element.extension, props.extensions!) ) { - return localize(`has an invalid extension: ${element.extension}`); + return localize(`has an invalid format: ${element.extension}`); } return null; } @@ -355,17 +383,6 @@ function _naiveStartingAndEndingLCS(s1: string, s2: string) {
-
- - {{ localize("Exactly two elements are needed for the pair.") }} - - - {{ localize("Cancel") }} - - {{ localize("and reselect new elements.") }} - - -
- {{ localize("The following extensions are required for this pair: ") }} + {{ localize("The following formats are required for this pair: ") }}
  • {{ extension }} @@ -474,19 +491,48 @@ function _naiveStartingAndEndingLCS(s1: string, s2: string) {
    -
    - - - {{ localize("Swap") }} - +
    +
    + + + {{ localize("Swap") }} + +
    +
    + + {{ localize("Exactly two elements are needed for the pair.") }} + + + {{ localize("Cancel") }} + + {{ localize("and reselect new elements.") }} + + + + {{ localize("The selected datasets have mixed formats.") }} + {{ localize("You can still create the pair but generally") }} + {{ localize("dataset pairs should contain datasets of the same type.") }} + + + + {{ localize("The Dataset Pair is ready to be created.") }} + {{ localize("Provide a name and click the button below to create the pair.") }} + +
    -
    +
    {{ localize(dataset) }}:
    - {{ localize("Manually select a forward and reverse dataset to create a pair collection:") }} -
    - + + + {{ + localize("Manually select a forward and reverse dataset to create a dataset pair:") + }} + +
    +
    + +
    + + {{ localize(`No datasets found${filterText ? " matching '" + filterText + "'" : ""}`) }} +
    @@ -531,11 +590,19 @@ function _naiveStartingAndEndingLCS(s1: string, s2: string) { } .collection-elements-controls { - margin-bottom: 8px; + display: flex; + justify-content: space-between; + align-items: center; + + .alert { + padding: 0.25rem 0.5rem; + margin: 0; + text-align: center; + } } .collection-elements { - max-height: 400px; + max-height: 30vh; border: 0px solid lightgrey; overflow-y: auto; overflow-x: hidden; diff --git a/client/src/components/Collections/PairedListCollectionCreator.vue b/client/src/components/Collections/PairedListCollectionCreator.vue index 1f831e66a79c..4542ad324457 100644 --- a/client/src/components/Collections/PairedListCollectionCreator.vue +++ b/client/src/components/Collections/PairedListCollectionCreator.vue @@ -304,7 +304,7 @@ function _isElementInvalid(element: HistoryItemSummary) { element.extension && !datatypesMapper.value?.isSubTypeOfAny(element.extension, props.extensions!) ) { - return localize(`has an invalid extension: ${element.extension}`); + return localize(`has an invalid format: ${element.extension}`); } return null; } @@ -1036,7 +1036,7 @@ function _naiveStartingAndEndingLCS(s1: string, s2: string) { ) }} - {{ localize("The following extensions are required for this collection: ") }} + {{ localize("The following format(s) are required for this collection: ") }}
    • {{ extension }} diff --git a/client/src/components/Collections/common/CollectionCreator.vue b/client/src/components/Collections/common/CollectionCreator.vue index 6947a1d592fb..eb446332bfc9 100644 --- a/client/src/components/Collections/common/CollectionCreator.vue +++ b/client/src/components/Collections/common/CollectionCreator.vue @@ -1,7 +1,7 @@ diff --git a/client/src/components/DatasetInformation/DatasetInformation.vue b/client/src/components/DatasetInformation/DatasetInformation.vue index b311f6eda8ee..14cbf0616820 100644 --- a/client/src/components/DatasetInformation/DatasetInformation.vue +++ b/client/src/components/DatasetInformation/DatasetInformation.vue @@ -3,6 +3,7 @@ import { type HDADetailed } from "@/api"; import { withPrefix } from "@/utils/redirect"; import { bytesToString } from "@/utils/utils"; +import HelpText from "../Help/HelpText.vue"; import DatasetHashes from "@/components/DatasetInformation/DatasetHashes.vue"; import DatasetSources from "@/components/DatasetInformation/DatasetSources.vue"; import DecodedId from "@/components/DecodedId.vue"; @@ -60,7 +61,9 @@ defineProps(); - Format + + + {{ dataset.file_ext }} diff --git a/client/src/components/Help/terms.yml b/client/src/components/Help/terms.yml index 395fdf7ba708..6ebb0f0f07be 100644 --- a/client/src/components/Help/terms.yml +++ b/client/src/components/Help/terms.yml @@ -64,13 +64,22 @@ galaxy: Toggling this on means that the original history items that will become a part of the collection will be hidden from the history panel (they will still be searchable via the 'visible: false' filter). filteredExtensions: | - The history is filtered for the extensions that are required for this collection. You might see some - items with other extensions since those can still be valid inputs via implicit conversion. + The history is filtered for the formats that are required for this collection. You might see some + items with other formats since those can still be valid inputs via implicit conversion. requiredUploadExtensions: | - The extensions that are required for this collection. The files you upload will be assumed to have - these extensions. In the case of more than one extension, you can select a specific extension for - each individual file above. If there is only one extension, Galaxy will attempt to set that as the - extension for each file. + The formats that are required for this collection. The files you upload will be assumed to have + these formats. In the case of more than one format, you can select a specific format for + each individual file above. If there is only one format, Galaxy will attempt to set that as the + format for each file. + whyHomogenousCollections: | + Dataset collections are designed to streamline the analysis of large numbers of datasets by grouping + them together into a single, manageable entity. Unlike generic folders on your computer, which can hold + any mix of file types, dataset collections are specifically intended to be homogenous. This homogeneity + is crucial for consistency in processing. Homogeneous datasets ensure that each dataset in the collection + can be processed uniformly with the same tools and workflows. This eliminates the need for individual + adjustments, which can be time-consuming and prone to error. Most tools and workflows in Galaxy are designed + to operate on collections of similar data types. Homogeneous collections allow these tools to operate + uniformly over the collection. jobs: metrics: @@ -147,3 +156,19 @@ galaxy: to pass to the ``galaxy-upload`` command. ruleBased: | Galaxy can bulk import lists & tables of URLs into datasets or collections using reproducible rules. + + datasets: + formatVsDatatypeVsExtension: | + In Galaxy, the terms "datatype," "format," and "extension" are used to describe the nature of a dataset, + each with specific meanings. + + The ``datatype`` defines how a dataset is interpreted, validated, and processed, such as "fastqsanger", "bam", + or "vcf". Each datatype corresponds to a Python class that specifies how Galaxy should handle the file. + + The ``format`` is the user-facing label for the dataset's datatype, appearing in the Galaxy user interface + to indicate the structure and content expectations of the dataset. + + The ``extension`` is a string associated with each datatype, stored in Galaxy’s database to identify the + dataset’s datatype. This extension can match a file’s actual disk suffix (e.g., ".vcf") but may also be more + specialized (e.g., "fastqsanger"). Understanding these distinctions ensures that datasets are processed + correctly within Galaxy, maintaining the integrity and reproducibility of analyses.