Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[24.2] Guide users to collection builders #18857

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7cd2147
This really stablizes these tests - they sort of runaway without this.
jmchilton Nov 19, 2024
abe326a
fix pesky warning
ElectronicBlueberry Nov 21, 2024
d44f037
add button for creating a list from run form field
ahmedhamidawan Sep 19, 2024
6d90374
fully implement `list` collection creator in `FormData`
ahmedhamidawan Sep 26, 2024
4b20276
add a `maintain-selection-order` prop to `FormSelectMany`
ahmedhamidawan Sep 26, 2024
0eaccd5
fix reactivity of `ClickToEdit` and add some styling
ahmedhamidawan Oct 1, 2024
f53170f
`ListCollectionCreator`: add more types
ahmedhamidawan Oct 1, 2024
35ff19d
modernize/refactor `PairedListCollectionCreator` for input forms
ahmedhamidawan Oct 1, 2024
6a504c0
remove the extensions toggle, only include `isSubTypeOfAny` items
ahmedhamidawan Oct 1, 2024
74234e0
improve styling of collection create button in `FormData`
ahmedhamidawan Oct 1, 2024
6862f64
change create new collection `ButtonSpinner` variant
ahmedhamidawan Oct 3, 2024
297a529
change create new collection `ButtonSpinner` title; fix icon imports
ahmedhamidawan Oct 3, 2024
67f3105
add history name to modal header
ahmedhamidawan Oct 4, 2024
e844241
restrict the `list:paired` creator to required extensions
ahmedhamidawan Oct 7, 2024
16c4eea
make the "pairing" section in `list:paired` builder expand/collapseable
ahmedhamidawan Oct 7, 2024
8a9d9ac
add `CollectionCreatorModal` that replaces `buildCollectionModal`
ahmedhamidawan Oct 18, 2024
b9f8f3c
slight optimization
ahmedhamidawan Oct 18, 2024
3a827cc
(incomplete/WIP) add uploader to collection creator
ahmedhamidawan Oct 21, 2024
60a5fb7
`paired` collection items are now also reversed like history
ahmedhamidawan Nov 19, 2024
fa572ad
add uploaded files to collection directly
ahmedhamidawan Nov 19, 2024
b70f90c
Fix jest collection modal test to be more flexible, allowing title co…
dannon Nov 19, 2024
99447d6
remove legacy JQuery collection creator modal files
ahmedhamidawan Nov 19, 2024
c2ab795
fix `PairedListCollectionCreator` jest
ahmedhamidawan Nov 19, 2024
29796d4
fix selectors for the changes to the collection builder
ahmedhamidawan Nov 22, 2024
03527bd
do not show create another collection link if from selection
ahmedhamidawan Dec 2, 2024
56ccef4
fix selection for collection creator in libraries
ahmedhamidawan Dec 3, 2024
b9adc1b
fix reset function when creating collection from a selection
ahmedhamidawan Dec 3, 2024
e09a364
fix `selection` object for rule collection builder
ahmedhamidawan Dec 3, 2024
9deac09
ensure library import to collection modal closes
ahmedhamidawan Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions client/src/api/datatypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { type components } from "@/api";

export type CompositeFileInfo = components["schemas"]["CompositeFileInfo"];
3 changes: 3 additions & 0 deletions client/src/api/histories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { type components } from "@/api";

export type HistoryContentsResult = components["schemas"]["HistoryContentsResult"];
329 changes: 329 additions & 0 deletions client/src/components/Collections/CollectionCreatorModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
<script setup lang="ts">
import { faCheckCircle, faUndo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert, BLink, BModal } from "bootstrap-vue";
import { computed, ref, watch } from "vue";

import type { HDASummary, HistoryItemSummary, HistorySummary } from "@/api";
import { createDatasetCollection } from "@/components/History/model/queries";
import { useCollectionBuilderItemsStore } from "@/stores/collectionBuilderItemsStore";
import { useHistoryStore } from "@/stores/historyStore";
import localize from "@/utils/localization";
import { orList } from "@/utils/strings";

import type { CollectionType, DatasetPair } from "../History/adapters/buildCollectionModal";

import ListCollectionCreator from "./ListCollectionCreator.vue";
import PairCollectionCreator from "./PairCollectionCreator.vue";
import PairedListCollectionCreator from "./PairedListCollectionCreator.vue";
import Heading from "@/components/Common/Heading.vue";
import GenericItem from "@/components/History/Content/GenericItem.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";

interface Props {
historyId: string;
showModal: boolean;
collectionType: CollectionType;
selectedItems?: HistoryItemSummary[];
defaultHideSourceItems?: boolean;
extensions?: string[];
fromRulesInput?: boolean;
hideModalOnCreate?: boolean;
filterText?: string;
}
const props = defineProps<Props>();

const emit = defineEmits<{
(e: "created-collection", collection: any): void;
(e: "update:show-modal", showModal: boolean): void;
}>();

/** Computed toggle that handles opening and closing the modal */
const localShowToggle = computed({
get: () => props.showModal,
set: (value: boolean) => {
emit("update:show-modal", value);
},
});

// Create Collection refs
const creatingCollection = ref(false);
const createCollectionError = ref<string | null>(null);
const createdCollection = ref<any>(null);

// History items variables
const historyItemsError = ref<string | null>(null);
const collectionItemsStore = useCollectionBuilderItemsStore();
const historyStore = useHistoryStore();
const history = computed(() => historyStore.getHistoryById(props.historyId));
const historyId = computed(() => props.historyId);
const localFilterText = computed(() => props.filterText || "");
const historyUpdateTime = computed(() => history.value?.update_time);
const isFetchingItems = computed(() => collectionItemsStore.isFetching[localFilterText.value]);
const historyDatasets = computed(() => {
if (collectionItemsStore.cachedDatasetsForFilterText) {
return collectionItemsStore.cachedDatasetsForFilterText[localFilterText.value] || [];
} else {
return [];
}
});

/** Flag for the initial fetch of history items */
const initialFetch = ref(false);

/** Whether a list of items was selected to create a collection from */
const fromSelection = computed(() => !!props.selectedItems?.length);

/** Items to create the collection from */
const creatorItems = computed(() => (fromSelection.value ? props.selectedItems : historyDatasets.value));

watch(
() => localShowToggle.value,
async (show) => {
if (show) {
await fetchHistoryDatasets();
if (!initialFetch.value) {
initialFetch.value = true;
}
}
},
{ immediate: true }
);

// Fetch items when history ID or update time changes, only if localShowToggle is true
watch([historyId, historyUpdateTime, localFilterText], async () => {
if (localShowToggle.value) {
await fetchHistoryDatasets();
}
});

// If there is a change in `historyDatasets`, but we have selected items, we should update the selected items
watch(
() => historyDatasets.value,
(newDatasets) => {
if (fromSelection.value) {
// find each selected item in the new datasets, and update it
props.selectedItems?.forEach((selectedItem) => {
const newDataset = newDatasets.find((dataset) => dataset.id === selectedItem.id);
if (newDataset) {
Object.assign(selectedItem, newDataset);
}
});
}
}
);

const modalTitle = computed(() => {
if (props.collectionType === "list") {
return localize(
`Create a collection from a list of ${fromSelection.value ? "selected" : ""} ${
props.extensions?.length ? orList(props.extensions) : ""
} datasets`
);
} else if (props.collectionType === "list:paired") {
return localize(
`Create a collection of ${fromSelection.value ? "selected" : ""} ${
props.extensions?.length ? orList(props.extensions) : ""
} dataset pairs`
);
} else if (props.collectionType === "paired") {
return localize(
`Create a ${props.extensions?.length ? orList(props.extensions) : ""} dataset pair collection ${
fromSelection.value ? "from selected items" : ""
}`
);
} else {
return localize("Create a collection");
}
});

// Methods
function createListCollection(elements: HDASummary[], name: string, hideSourceItems: boolean) {
const returnedElems = elements.map((element) => ({
id: element.id,
name: element.name,
//TODO: this allows for list:list even if the implementation does not - reconcile
src: "src" in element ? element.src : element.history_content_type == "dataset" ? "hda" : "hdca",
}));
return createHDCA(returnedElems, "list", name, hideSourceItems);
}

function createListPairedCollection(elements: DatasetPair[], name: string, hideSourceItems: boolean) {
const returnedElems = elements.map((pair) => ({
collection_type: "paired",
src: "new_collection",
name: pair.name,
element_identifiers: [
{
name: "forward",
id: pair.forward.id,
src: "src" in pair.forward ? pair.forward.src : "hda",
},
{
name: "reverse",
id: pair.reverse.id,
src: "src" in pair.reverse ? pair.reverse.src : "hda",
},
],
}));
return createHDCA(returnedElems, "list:paired", name, hideSourceItems);
}

function createPairedCollection(elements: DatasetPair, name: string, hideSourceItems: boolean) {
const { forward, reverse } = elements;
const returnedElems = [
{ name: "forward", src: "src" in forward ? forward.src : "hda", id: forward.id },
{ name: "reverse", src: "src" in reverse ? reverse.src : "hda", id: reverse.id },
];
return createHDCA(returnedElems, "paired", name, hideSourceItems);
}

async function createHDCA(
element_identifiers: any[],
collection_type: CollectionType,
name: string,
hide_source_items: boolean,
options = {}
) {
try {
creatingCollection.value = true;
const collection = await createDatasetCollection(history.value as HistorySummary, {
collection_type,
name,
hide_source_items,
element_identifiers,
options,
});

emit("created-collection", collection);
createdCollection.value = collection;

if (props.hideModalOnCreate) {
hideModal();
}
} catch (error) {
createCollectionError.value = error as string;
} finally {
creatingCollection.value = false;
}
}

async function fetchHistoryDatasets() {
const { error } = await collectionItemsStore.fetchDatasetsForFiltertext(
historyId.value,
historyUpdateTime.value,
localFilterText.value
);
if (error) {
historyItemsError.value = error;
console.error("Error fetching history items:", historyItemsError.value);
} else {
historyItemsError.value = null;
}
}

function hideModal() {
localShowToggle.value = false;
}

function resetModal() {
createCollectionError.value = null;
createdCollection.value = null;
}
</script>

<template>
<BModal
id="collection-creator-modal"
v-model="localShowToggle"
:busy="(fromSelection && isFetchingItems) || creatingCollection"
modal-class="ui-modal collection-creator-modal"
:hide-footer="!createdCollection && !createCollectionError"
:ok-disabled="!!createdCollection || !!createCollectionError"
:cancel-title="localize('Exit')"
footer-class="d-flex justify-content-between"
:ok-title="localize('Create Collection')"
@hidden="resetModal">
<template v-slot:modal-header>
<Heading class="w-100" size="sm">
<div class="d-flex justify-content-between unselectable w-100">
<div>{{ modalTitle }}</div>
<div v-if="!!history">
From history: <b>{{ history.name }}</b>
</div>
</div>
</Heading>
</template>
<BAlert v-if="isFetchingItems && !initialFetch" variant="info" show>
<LoadingSpan :message="localize('Loading items')" />
</BAlert>
<BAlert v-else-if="!fromSelection && historyItemsError" variant="danger" show>
{{ historyItemsError }}
</BAlert>
<BAlert v-else-if="!creatorItems?.length" variant="info" show>
{{ localize("No items available to create a collection.") }}
</BAlert>
<BAlert v-else-if="creatingCollection" variant="info" show>
<LoadingSpan :message="localize('Creating collection')" />
</BAlert>
<BAlert v-else-if="createCollectionError" variant="danger" show>
{{ createCollectionError }}
<BLink class="text-decoration-none" @click.stop.prevent="resetModal">
<FontAwesomeIcon :icon="faUndo" fixed-width />
{{ localize("Try again") }}
</BLink>
</BAlert>
<div v-else-if="createdCollection">
<BAlert variant="success" show>
<FontAwesomeIcon :icon="faCheckCircle" class="text-success" fixed-width />
{{ localize("Collection created successfully.") }}
<BLink v-if="!fromSelection" class="text-decoration-none" @click.stop.prevent="resetModal">
<FontAwesomeIcon :icon="faUndo" fixed-width />
{{ localize("Create another collection") }}
</BLink>
</BAlert>

<!-- TODO: This is a bit shady, better if we confirm it is a collection type -->
<GenericItem :item-id="createdCollection.id" item-src="hdca" />
</div>
<ListCollectionCreator
v-else-if="props.collectionType === 'list'"
:history-id="props.historyId"
:initial-elements="creatorItems"
:default-hide-source-items="props.defaultHideSourceItems"
:from-selection="fromSelection"
:extensions="props.extensions"
@clicked-create="createListCollection"
@on-cancel="hideModal" />
<PairedListCollectionCreator
v-else-if="props.collectionType === 'list:paired'"
:history-id="props.historyId"
:initial-elements="creatorItems"
:default-hide-source-items="props.defaultHideSourceItems"
:from-selection="fromSelection"
:extensions="props.extensions"
@clicked-create="createListPairedCollection"
@on-cancel="hideModal" />
<PairCollectionCreator
v-else-if="props.collectionType === 'paired'"
:history-id="props.historyId"
:initial-elements="creatorItems"
:default-hide-source-items="props.defaultHideSourceItems"
:from-selection="fromSelection"
:extensions="props.extensions"
@clicked-create="createPairedCollection"
@on-cancel="hideModal" />
</BModal>
</template>

<style lang="scss">
/** NOTE: Not using `<style scoped> here because these classes are
`BModal` `body-class` and `content-class` and don't seem to work
with scoped */
.collection-creator-modal {
.modal-dialog {
width: 85%;
max-width: 100%;
}
}
</style>
Loading
Loading