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.1] Allow drag and drop for collection elements #18699

Merged
merged 9 commits into from
Sep 16, 2024
3 changes: 3 additions & 0 deletions client/src/api/datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,6 @@ export async function fetchDatasetAttributes(datasetId: string) {

return data;
}

export type HistoryContentType = components["schemas"]["HistoryContentType"];
export type HistoryContentSource = components["schemas"]["HistoryContentSource"];
21 changes: 21 additions & 0 deletions client/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ export interface DCECollection extends DCESummary {
object: DCObject;
}

/**
* DatasetCollectionElement specific type for datasets.
*/
export interface DCEDataset extends DCESummary {
element_type: "hda";
object: HDAObject;
}

/**
* Contains summary information about a HDCA (HistoryDatasetCollectionAssociation).
*
Expand All @@ -148,6 +156,8 @@ export type HDCADetailed = components["schemas"]["HDCADetailed"];
*/
export type DCObject = components["schemas"]["DCObject"];

export type HDAObject = components["schemas"]["HDAObject"];

export type DatasetCollectionAttributes = components["schemas"]["DatasetCollectionAttributesResult"];

export type ConcreteObjectStoreModel = components["schemas"]["ConcreteObjectStoreModel"];
Expand Down Expand Up @@ -187,6 +197,13 @@ export function isCollectionElement(element: DCESummary): element is DCECollecti
return element.element_type === "dataset_collection";
}

/**
* Returns true if the given element of a collection is a Dataset.
*/
export function isDatasetElement(element: DCESummary): element is DCEDataset {
return element.element_type === "hda";
}

/**
* Returns true if the given dataset entry is an instance of DatasetDetails.
*/
Expand Down Expand Up @@ -215,6 +232,10 @@ export function isHistoryItem(item: object): item is HistoryItemSummary {
return item && "history_content_type" in item;
}

export function isDCE(item: object): item is DCESummary {
return item && "element_type" in item;
}

type QuotaUsageResponse = components["schemas"]["UserQuotaUsage"];

/** Represents a registered user.**/
Expand Down
15 changes: 13 additions & 2 deletions client/src/components/Form/Elements/FormData/FormData.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert, BButton, BButtonGroup, BCollapse, BFormCheckbox, BTooltip } from "bootstrap-vue";
import { computed, onMounted, type Ref, ref, watch } from "vue";

import { isDatasetElement, isDCE } from "@/api";
import { getGalaxyInstance } from "@/app";
import { useDatatypesMapper } from "@/composables/datatypesMapper";
import { useUid } from "@/composables/utils/uid";
Expand Down Expand Up @@ -314,11 +315,21 @@ function handleIncoming(incoming: Record<string, unknown>, partial = true) {
const incomingValues: Array<DataOption> = [];
values.forEach((v) => {
// Map incoming objects to data option values
let newSrc;
if (isDCE(v)) {
if (isDatasetElement(v)) {
newSrc = SOURCE.DATASET;
v = v.object;
} else {
newSrc = SOURCE.COLLECTION_ELEMENT;
}
} else {
newSrc =
v.src || (v.history_content_type === "dataset_collection" ? SOURCE.COLLECTION : SOURCE.DATASET);
}
const newHid = v.hid;
const newId = v.id;
const newName = v.name ? v.name : newId;
const newSrc =
v.src || (v.history_content_type === "dataset_collection" ? SOURCE.COLLECTION : SOURCE.DATASET);
const newValue: DataOption = {
id: newId,
src: newSrc,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { canMutateHistory, isCollectionElement, isHDCA } from "@/api";
import ExpandedItems from "@/components/History/Content/ExpandedItems";
import { updateContentFields } from "@/components/History/model/queries";
import { useCollectionElementsStore } from "@/stores/collectionElementsStore";
import { setItemDragstart } from "@/utils/setDrag";
import { errorMessageAsString } from "@/utils/simple-error";

import CollectionDetails from "./CollectionDetails.vue";
Expand Down Expand Up @@ -164,6 +165,7 @@ watch(
:expand-dataset="isExpanded(item)"
:is-dataset="item.element_type == 'hda'"
:filterable="filterable"
@drag-start="setItemDragstart(item, $event)"
@update:expand-dataset="setExpanded(item, $event)"
@view-collection="onViewDatasetCollectionElement(item)" />
</template>
Expand Down
113 changes: 9 additions & 104 deletions client/src/components/History/CurrentHistory/HistoryPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,20 @@ import { BAlert } from "bootstrap-vue";
import { storeToRefs } from "pinia";
import { computed, onMounted, type Ref, ref, set as VueSet, unref, watch } from "vue";

import { type HistoryItemSummary, type HistorySummaryExtended, isHistoryItem, userOwnsHistory } from "@/api";
import { copyDataset } from "@/api/datasets";
import { type HistoryItemSummary, type HistorySummaryExtended, userOwnsHistory } from "@/api";
import ExpandedItems from "@/components/History/Content/ExpandedItems";
import SelectedItems from "@/components/History/Content/SelectedItems";
import { HistoryFilters } from "@/components/History/HistoryFilters";
import { deleteContent, updateContentFields } from "@/components/History/model/queries";
import { Toast } from "@/composables/toast";
import { useActiveElement } from "@/composables/useActiveElement";
import { startWatchingHistory } from "@/store/historyStore/model/watchHistory";
import { useEventStore } from "@/stores/eventStore";
import { useHistoryItemsStore } from "@/stores/historyItemsStore";
import { useHistoryStore } from "@/stores/historyStore";
import { useUserStore } from "@/stores/userStore";
import { type Alias, getOperatorForAlias } from "@/utils/filtering";
import { setDrag } from "@/utils/setDrag";
import { setItemDragstart } from "@/utils/setDrag";

import { useHistoryDragDrop } from "../../../composables/historyDragDrop";

import HistoryCounter from "./HistoryCounter.vue";
import HistoryDetails from "./HistoryDetails.vue";
Expand Down Expand Up @@ -70,11 +69,9 @@ const isLoading = ref(false);
const offsetQueryParam = ref(0);
const searchError = ref<BackendFilterError | undefined>(undefined);
const showAdvanced = ref(false);
const showDropZone = ref(false);
const operationRunning = ref<string | null>(null);
const operationError = ref(null);
const querySelectionBreak = ref(false);
const dragTarget = ref<EventTarget | null>(null);
const contentItemRefs = computed(() => {
return historyItems.value.reduce((acc: ContentItemRef, item) => {
acc[itemUniqueKey(item)] = ref(null);
Expand All @@ -91,6 +88,9 @@ const historyStore = useHistoryStore();
const historyItemsStore = useHistoryItemsStore();
const { currentUser } = storeToRefs(useUserStore());

const historyIdComputed = computed(() => props.history.id);
const { showDropZone, onDragEnter, onDragLeave, onDragOver, onDrop } = useHistoryDragDrop(historyIdComputed);

const currentUserOwnsHistory = computed(() => {
return userOwnsHistory(currentUser.value, props.history);
});
Expand Down Expand Up @@ -219,19 +219,6 @@ watch(
}
);

function dragSameHistory() {
return getDragData().sameHistory;
}

function getDragData() {
const eventStore = useEventStore();
const dragItems = eventStore.getDragItems();
// Filter out any non-history items
const historyItems = dragItems?.filter((item: any) => isHistoryItem(item)) as HistoryItemSummary[];
const historyId = historyItems?.[0]?.history_id;
return { data: historyItems, sameHistory: historyId === props.history.id, multiple: historyItems?.length > 1 };
}

function getHighlight(item: HistoryItemSummary) {
if (unref(isLoading)) {
return undefined;
Expand Down Expand Up @@ -345,70 +332,6 @@ function onOperationError(error: any) {
operationError.value = error;
}

function onDragEnter(e: DragEvent) {
if (dragSameHistory()) {
return;
}
dragTarget.value = e.target;
showDropZone.value = true;
}

function onDragOver(e: DragEvent) {
if (dragSameHistory()) {
return;
}
e.preventDefault();
}

function onDragLeave(e: DragEvent) {
if (dragSameHistory()) {
return;
}
if (dragTarget.value === e.target) {
showDropZone.value = false;
}
}

async function onDrop() {
showDropZone.value = false;
const { data, sameHistory, multiple } = getDragData();
if (!data || sameHistory) {
return;
}

let datasetCount = 0;
let collectionCount = 0;
try {
// iterate over the data array and copy each item to the current history
for (const item of data) {
const dataSource = item.history_content_type === "dataset" ? "hda" : "hdca";
await copyDataset(item.id, props.history.id, item.history_content_type, dataSource);

if (item.history_content_type === "dataset") {
datasetCount++;
if (!multiple) {
Toast.info("Dataset copied to history");
}
} else {
collectionCount++;
if (!multiple) {
Toast.info("Collection copied to history");
}
}
}

if (multiple && datasetCount > 0) {
Toast.info(`${datasetCount} dataset${datasetCount > 1 ? "s" : ""} copied to new history`);
}
if (multiple && collectionCount > 0) {
Toast.info(`${collectionCount} collection${collectionCount > 1 ? "s" : ""} copied to new history`);
}
historyStore.loadHistoryById(props.history.id);
} catch (error) {
Toast.error(`${error}`);
}
}

function updateFilterValue(filterKey: string, newValue: any) {
const currentFilterText = filterText.value;
filterText.value = filterClass.setFilterValue(currentFilterText, filterKey, newValue);
Expand Down Expand Up @@ -451,24 +374,6 @@ function arrowNavigate(item: HistoryItemSummary, eventKey: string) {
}
return nextItem;
}

function setItemDragstart(
item: HistoryItemSummary,
itemIsSelected: boolean,
selectedItems: Map<string, HistoryItemSummary>,
selectionSize: number,
event: DragEvent
) {
if (itemIsSelected && selectionSize > 1) {
const selectedItemsObj: any = {};
for (const [key, value] of selectedItems) {
selectedItemsObj[key] = value;
}
setDrag(event, selectedItemsObj, true);
} else {
setDrag(event, item as any);
}
}
</script>

<template>
Expand Down Expand Up @@ -629,10 +534,10 @@ function setItemDragstart(
@drag-start="
setItemDragstart(
item,
$event,
showSelection && isSelected(item),
selectedItems,
selectionSize,
$event
selectedItems
)
"
@hide-selection="setShowSelection(false)"
Expand Down
73 changes: 6 additions & 67 deletions client/src/components/History/Multiple/MultipleViewList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@ import { computed, type Ref, ref } from "vue";
//@ts-ignore missing typedefs
import VirtualList from "vue-virtual-scroll-list";

import { HistoryItemSummary, isHistoryItem } from "@/api";
import { copyDataset } from "@/api/datasets";
import { useAnimationFrameResizeObserver } from "@/composables/sensors/animationFrameResizeObserver";
import { useAnimationFrameScroll } from "@/composables/sensors/animationFrameScroll";
import { Toast } from "@/composables/toast";
import { useEventStore } from "@/stores/eventStore";
import { useHistoryStore } from "@/stores/historyStore";
import localize from "@/utils/localization";
import { errorMessageAsString } from "@/utils/simple-error";

import { useHistoryDragDrop } from "../../../composables/historyDragDrop";

import HistoryDropZone from "../CurrentHistory/HistoryDropZone.vue";
import MultipleViewItem from "./MultipleViewItem.vue";

Expand Down Expand Up @@ -65,67 +64,7 @@ async function createAndPin() {
}
}

const showDropZone = ref(false);
const processingDrop = ref(false);
async function onDrop(evt: any) {
const eventStore = useEventStore();
if (processingDrop.value) {
showDropZone.value = false;
return;
}
processingDrop.value = true;
showDropZone.value = false;
const dragItems = eventStore.getDragItems();
// Filter out any non-history items
const historyItems = dragItems?.filter((item: any) => isHistoryItem(item)) as HistoryItemSummary[];
const multiple = historyItems.length > 1;
const originalHistoryId = historyItems?.[0]?.history_id;

if (historyItems && originalHistoryId) {
await historyStore.createNewHistory();
const currentHistoryId = historyStore.currentHistoryId;

let datasetCount = 0;
let collectionCount = 0;
if (currentHistoryId) {
// iterate over the data array and copy each item to the new history
for (const item of historyItems) {
const dataSource = item.history_content_type === "dataset" ? "hda" : "hdca";
await copyDataset(item.id, currentHistoryId, item.history_content_type, dataSource)
.then(() => {
if (item.history_content_type === "dataset") {
datasetCount++;
if (!multiple) {
Toast.info(localize("Dataset copied to new history"));
}
} else {
collectionCount++;
if (!multiple) {
Toast.info(localize("Collection copied to new history"));
}
}
})
.catch((error) => {
Toast.error(errorMessageAsString(error));
});
}
if (multiple && datasetCount > 0) {
Toast.info(`${datasetCount} dataset${datasetCount > 1 ? "s" : ""} copied to new history`);
}
if (multiple && collectionCount > 0) {
Toast.info(`${collectionCount} collection${collectionCount > 1 ? "s" : ""} copied to new history`);
}

if (historyStore.pinnedHistories.length > 0) {
// pin the newly created history via the drop
historyStore.pinHistory(currentHistoryId);
// also pin the original history where the item came from
historyStore.pinHistory(originalHistoryId);
}
}
processingDrop.value = false;
}
}
const { showDropZone, onDragEnter, onDragLeave, onDragOver, onDrop } = useHistoryDragDrop(undefined, true, true);

async function onKeyDown(evt: KeyboardEvent) {
if (evt.key === "Enter" || evt.key === " ") {
Expand Down Expand Up @@ -159,9 +98,9 @@ async function onKeyDown(evt: KeyboardEvent) {
<div
class="history-picker"
@drop.prevent="onDrop"
@dragenter.prevent="showDropZone = true"
@dragover.prevent
@dragleave.prevent="showDropZone = false">
@dragenter.prevent="onDragEnter"
@dragover="onDragOver"
@dragleave.prevent="onDragLeave">
<span v-if="!showDropZone" class="d-flex flex-column h-100">
<div
class="history-picker-box top-picker text-primary"
Expand Down
Loading
Loading