Skip to content

Commit

Permalink
Merge pull request #17502 from ahmedhamidawan/keyboard_select_in_history
Browse files Browse the repository at this point in the history
Add multiple drag/drop and keyboard accessible selection to `HistoryPanel` items
  • Loading branch information
jdavcs authored Mar 1, 2024
2 parents ba418af + 970e7b2 commit ff66f47
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 64 deletions.
2 changes: 1 addition & 1 deletion client/src/components/ActivityBar/ActivityBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function panelActivityIsActive(activity: Activity) {
*/
function onDragEnter(evt: MouseEvent) {
const eventData = eventStore.getDragData();
if (eventData) {
if (eventData && !eventStore.multipleDragData) {
dragTarget.value = evt.target;
dragItem.value = convertDropData(eventData);
emit("dragstart", dragItem.value);
Expand Down
6 changes: 6 additions & 0 deletions client/src/components/DragGhost.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { library } from "@fortawesome/fontawesome-svg-core";
import { faPaperPlane } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { storeToRefs } from "pinia";
import { useEventStore } from "stores/eventStore";
import { computed } from "vue";
Expand All @@ -10,9 +11,14 @@ import TextShort from "@/components/Common/TextShort.vue";
library.add(faPaperPlane);
const eventStore = useEventStore();
const { multipleDragData } = storeToRefs(eventStore);
const name = computed(() => {
const dragData = eventStore.getDragData();
if (multipleDragData.value) {
const count = Object.keys(dragData).length;
return `${count} items`;
}
return dragData?.name ?? "Draggable";
});
</script>
Expand Down
6 changes: 5 additions & 1 deletion client/src/components/Form/Elements/FormData/FormData.vue
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,11 @@ function onDragOver() {
function onDrop() {
if (dragData.value) {
handleIncoming(dragData.value);
if (eventStore.multipleDragData) {
handleIncoming(Object.values(dragData.value) as any, false);
} else {
handleIncoming(dragData.value);
}
currentHighlighting.value = "success";
dragData.value = null;
clearHighlighting();
Expand Down
32 changes: 24 additions & 8 deletions client/src/components/History/Content/ContentItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
:is-visible="item.visible"
:state="state"
:item-urls="itemUrls"
:keyboard-selectable="expandDataset"
:keyboard-selectable="isCollection || expandDataset"
@delete="onDelete"
@display="onDisplay"
@showCollectionInfo="onShowCollectionInfo"
Expand Down Expand Up @@ -113,7 +113,7 @@ import { updateContentFields } from "components/History/model/queries";
import StatelessTags from "components/TagsMultiselect/StatelessTags";
import { useEntryPointStore } from "stores/entryPointStore";
import { clearDrag, setDrag } from "@/utils/setDrag.js";
import { clearDrag } from "@/utils/setDrag.ts";
import CollectionDescription from "./Collection/CollectionDescription";
import { JobStateSummary } from "./Collection/JobStateSummary";
Expand Down Expand Up @@ -237,13 +237,29 @@ export default {
if (event.key === "Enter" || event.key === " ") {
this.onClick();
} else if ((event.key === "ArrowUp" || event.key === "ArrowDown") && event.shiftKey) {
event.preventDefault();
this.$emit("shift-select", event.key);
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
event.preventDefault();
this.$emit("arrow-navigate", event.key);
} else if (event.key === "Delete" && !this.selected && !this.item.deleted) {
event.preventDefault();
this.onDelete(event.shiftKey);
} else if (event.key === "Escape") {
event.preventDefault();
this.$emit("hide-selection");
} else if (event.key === "a" && event.ctrlKey) {
event.preventDefault();
this.$emit("select-all");
}
},
onClick() {
if (this.isPlaceholder) {
onClick(event) {
if (event && event.ctrlKey) {
this.$emit("update:selected", !this.selected);
} else if (this.isPlaceholder) {
return;
}
if (this.isDataset) {
} else if (this.isDataset) {
this.$emit("update:expand-dataset", !this.expandDataset);
} else {
this.$emit("view-collection", this.item, this.name);
Expand Down Expand Up @@ -271,9 +287,9 @@ export default {
this.$emit("delete", this.item, recursive);
},
onDragStart(evt) {
setDrag(evt, this.item);
this.$emit("drag-start", evt);
},
onDragEnd: function () {
onDragEnd() {
clearDrag();
},
onEdit() {
Expand Down
32 changes: 32 additions & 0 deletions client/src/components/History/Content/SelectedItems.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export default {
items: new Map(),
showSelection: false,
allSelected: false,
initSelectedKey: null,
initDirection: null,
};
},
computed: {
Expand Down Expand Up @@ -52,6 +54,33 @@ export default {
this.items = newSelected;
this.breakQuerySelection();
},
shiftSelect({ item, nextItem, eventKey }) {
const currentItemKey = this.getItemKey(item);
if (!this.initDirection) {
this.initSelectedKey = currentItemKey;
this.initDirection = eventKey;
this.setSelected(item, true);
}
// got back to the initial selected item
else if (this.initSelectedKey === currentItemKey) {
this.initDirection = eventKey;
}
// same direction
else if (this.initDirection === eventKey) {
this.setSelected(item, true);
}
// different direction
else {
this.setSelected(item, false);
}
if (nextItem) {
this.setSelected(nextItem, true);
}
},
initKeySelection() {
this.initSelectedKey = null;
this.initDirection = null;
},
selectItems(items = []) {
const newItems = [...this.items.values(), ...items];
const newEntries = newItems.map((item) => {
Expand All @@ -70,6 +99,7 @@ export default {
reset() {
this.items = new Map();
this.allSelected = false;
this.initKeySelection();
},
cancelSelection() {
this.showSelection = false;
Expand Down Expand Up @@ -110,6 +140,8 @@ export default {
isSelected: this.isSelected,
setSelected: this.setSelected,
resetSelection: this.reset,
shiftSelect: this.shiftSelect,
initKeySelection: this.initKeySelection,
});
},
};
121 changes: 111 additions & 10 deletions client/src/components/History/CurrentHistory/HistoryPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import { HistoryFilters } from "@/components/History/HistoryFilters";
import { deleteContent, updateContentFields } from "@/components/History/model/queries";
import { Toast } from "@/composables/toast";
import { startWatchingHistory } from "@/store/historyStore/model/watchHistory";
import { useEventStore } from "@/stores/eventStore";
import { type HistoryItem, useHistoryItemsStore } from "@/stores/historyItemsStore";
import { useHistoryStore } from "@/stores/historyStore";
import { type Alias, getOperatorForAlias } from "@/utils/filtering";
import { setDrag } from "@/utils/setDrag";
import HistoryCounter from "./HistoryCounter.vue";
import HistoryDetails from "./HistoryDetails.vue";
Expand Down Expand Up @@ -73,6 +75,13 @@ 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: any, item) => {
// TODO: type `any` properly
acc[`item-${item.hid}`] = ref(null);
return acc;
}, {});
});
const { currentFilterText, currentHistoryId } = storeToRefs(useHistoryStore());
const { lastCheckedTime, totalMatchesCount, isWatching } = storeToRefs(useHistoryItemsStore());
Expand Down Expand Up @@ -302,28 +311,61 @@ function onDragLeave(e: DragEvent) {
}
async function onDrop(evt: any) {
const eventStore = useEventStore();
showDropZone.value = false;
let data;
let data: HistoryItem[] | undefined;
let historyId: string | undefined;
const multiple = eventStore.multipleDragData;
try {
data = JSON.parse(evt.dataTransfer.getData("text"))[0];
if (multiple) {
const dragData = eventStore.getDragData() as Record<string, HistoryItem>;
// set historyId to the first history_id in the multiple drag data
const firstItem = Object.values(dragData)[0];
if (firstItem) {
historyId = firstItem.history_id;
}
data = Object.values(dragData);
} else {
data = [eventStore.getDragData() as HistoryItem];
if (data[0]) {
historyId = data[0].history_id;
}
}
} catch (error) {
// this was not a valid object for this dropzone, ignore
}
if (!data || data.history_id === props.history.id) {
if (!data || historyId === props.history.id) {
return;
}
let datasetCount = 0;
let collectionCount = 0;
try {
const dataSource = data.history_content_type === "dataset" ? "hda" : "hdca";
await copyDataset(data.id, props.history.id, data.history_content_type, dataSource);
if (data.history_content_type === "dataset") {
Toast.info("Dataset copied to history");
} else {
Toast.info("Collection copied to history");
// 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} datasets copied to history`);
}
if (multiple && collectionCount > 0) {
Toast.info(`${collectionCount} collections copied to history`);
}
historyStore.loadHistoryById(props.history.id);
} catch (error) {
Toast.error(`${error}`);
Expand All @@ -346,6 +388,46 @@ onMounted(async () => {
}
await loadHistoryItems();
});
function nextSelections(item: HistoryItem, eventKey: string) {
const nextItem = arrowNavigate(item, eventKey);
return {
item,
nextItem,
eventKey,
};
}
function arrowNavigate(item: HistoryItem, eventKey: string) {
let nextItem = null;
if (eventKey === "ArrowDown") {
nextItem = historyItems.value[historyItems.value.indexOf(item) + 1];
} else if (eventKey === "ArrowUp") {
nextItem = historyItems.value[historyItems.value.indexOf(item) - 1];
}
if (nextItem) {
contentItemRefs.value[`item-${nextItem.hid}`].value.$el.focus();
}
return nextItem;
}
function setItemDragstart(
item: HistoryItem,
itemIsSelected: boolean,
selectedItems: Map<string, HistoryItem>,
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 All @@ -363,6 +445,8 @@ onMounted(async () => {
selectAllInCurrentQuery,
isSelected,
setSelected,
shiftSelect,
initKeySelection,
resetSelection,
}"
:scope-key="queryKey"
Expand Down Expand Up @@ -479,6 +563,7 @@ onMounted(async () => {
<template v-slot:item="{ item, currentOffset }">
<ContentItem
:id="item.hid"
:ref="contentItemRefs[`item-${item.hid}`]"
is-history-item
:item="item"
:name="item.name"
Expand All @@ -489,6 +574,22 @@ onMounted(async () => {
:selected="isSelected(item)"
:selectable="showSelection"
:filterable="filterable"
@arrow-navigate="
arrowNavigate(item, $event);
initKeySelection();
"
@drag-start="
setItemDragstart(
item,
showSelection && isSelected(item),
selectedItems,
selectionSize,
$event
)
"
@hide-selection="setShowSelection(false)"
@shift-select="(eventKey) => shiftSelect(nextSelections(item, eventKey))"
@select-all="selectAllInCurrentQuery(historyItems)"
@tag-click="updateFilterValue('tag', $event)"
@tag-change="onTagChange"
@toggleHighlights="updateFilterValue('related', item.hid)"
Expand Down
1 change: 1 addition & 0 deletions client/src/components/History/Multiple/MultipleView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const selectedHistories = computed<PinnedHistory[]>(() => {
} else {
// get the latest four histories
return [...histories.value]
.filter((h) => !h.user_id || (!currentUser.value?.isAnonymous && h.user_id === currentUser.value?.id))
.sort((a, b) => {
if (a.update_time < b.update_time) {
return 1;
Expand Down
Loading

0 comments on commit ff66f47

Please sign in to comment.