mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
add History Multiview activity panel, create HistoryScrollList
This commit is contained in:
@@ -16,6 +16,7 @@ import InteractiveItem from "./Items/InteractiveItem.vue";
|
||||
import NotificationItem from "./Items/NotificationItem.vue";
|
||||
import UploadItem from "./Items/UploadItem.vue";
|
||||
import FlexPanel from "@/components/Panels/FlexPanel.vue";
|
||||
import MultiviewPanel from "@/components/Panels/MultiviewPanel.vue";
|
||||
import NotificationsPanel from "@/components/Panels/NotificationsPanel.vue";
|
||||
import SettingsPanel from "@/components/Panels/SettingsPanel.vue";
|
||||
import ToolPanel from "@/components/Panels/ToolPanel.vue";
|
||||
@@ -150,7 +151,7 @@ function onToggleSidebar(toggle: string) {
|
||||
:to="activity.to"
|
||||
@click="onToggleSidebar()" />
|
||||
<ActivityItem
|
||||
v-else-if="['tools', 'visualizations'].includes(activity.id)"
|
||||
v-else-if="['tools', 'visualizations', 'multiview'].includes(activity.id)"
|
||||
:id="`activity-${activity.id}`"
|
||||
:key="activity.id"
|
||||
:icon="activity.icon"
|
||||
@@ -193,6 +194,7 @@ function onToggleSidebar(toggle: string) {
|
||||
<FlexPanel v-if="isSideBarOpen" side="left" :collapsible="false">
|
||||
<ToolPanel v-if="isActiveSideBar('tools')" />
|
||||
<VisualizationPanel v-else-if="isActiveSideBar('visualizations')" />
|
||||
<MultiviewPanel v-else-if="isActiveSideBar('multiview')" />
|
||||
<NotificationsPanel v-else-if="isActiveSideBar('notifications')" />
|
||||
<SettingsPanel v-else-if="isActiveSideBar('settings')" />
|
||||
</FlexPanel>
|
||||
|
||||
@@ -154,14 +154,14 @@ describe("FilterMenu", () => {
|
||||
// `has_help` filter should have help modal button
|
||||
expect(wrapper.find("[title='Value Help']").classes().includes("btn")).toBe(true);
|
||||
// ranged time field (has 2 datepickers)
|
||||
const createdGtInput = wrapper.find("[placeholder='creation time after']");
|
||||
const createdLtInput = wrapper.find("[placeholder='creation time before']");
|
||||
const createdGtInput = wrapper.find("[placeholder='after creation time']");
|
||||
const createdLtInput = wrapper.find("[placeholder='before creation time']");
|
||||
createdGtInput.setValue("January 1, 2022");
|
||||
createdLtInput.setValue("January 1, 2023");
|
||||
expect(wrapper.findAll(".b-form-datepicker").length).toBe(2);
|
||||
// ranged number field (has different placeholder: greater instead of after...)
|
||||
const indexGtInput = wrapper.find("[placeholder='index greater']");
|
||||
const indexLtInput = wrapper.find("[placeholder='index lower']");
|
||||
const indexGtInput = wrapper.find("[placeholder='greater than index']");
|
||||
const indexLtInput = wrapper.find("[placeholder='lower than index']");
|
||||
indexGtInput.setValue("1234");
|
||||
indexLtInput.setValue("5678");
|
||||
// default bool filter
|
||||
|
||||
@@ -49,11 +49,11 @@ function hasError(field: string) {
|
||||
|
||||
function localPlaceholder(comp: "gt" | "lt") {
|
||||
if (comp == "gt") {
|
||||
const field = isDateType.value ? "after" : "greater";
|
||||
return `${props.filter.placeholder} ${field}`;
|
||||
const field = isDateType.value ? "after" : "greater than";
|
||||
return `${field} ${props.filter.placeholder}`;
|
||||
} else {
|
||||
const field = isDateType.value ? "before" : "lower";
|
||||
return `${props.filter.placeholder} ${field}`;
|
||||
const field = isDateType.value ? "before" : "lower than";
|
||||
return `${field} ${props.filter.placeholder}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import Filtering, { compare, contains, expandNameTag, toDate } from "utils/filtering";
|
||||
|
||||
const validFilters = {
|
||||
name: { placeholder: "name", type: String, handler: contains("name"), menuItem: true },
|
||||
tag: { placeholder: "tag", type: String, handler: contains("tags", "tag", expandNameTag), menuItem: true },
|
||||
annotation: { placeholder: "annotation", type: String, handler: contains("annotation"), menuItem: true },
|
||||
update_time: {
|
||||
placeholder: "updated time",
|
||||
type: Date,
|
||||
handler: compare("update_time", "le", toDate),
|
||||
isRangeInput: true,
|
||||
menuItem: true,
|
||||
},
|
||||
update_time_ge: { handler: compare("update_time", "ge", toDate), menuItem: false },
|
||||
update_time_le: { handler: compare("update_time", "le", toDate), menuItem: false },
|
||||
};
|
||||
export const HistoriesFilters = new Filtering(validFilters);
|
||||
@@ -0,0 +1,428 @@
|
||||
<script setup lang="ts">
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faListAlt } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faArrowDown, faCheckSquare, faColumns, faSignInAlt, faSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { useInfiniteScroll } from "@vueuse/core";
|
||||
import { BBadge, BButton, BButtonGroup, BListGroup, BListGroupItem } from "bootstrap-vue";
|
||||
import { orderBy } from "lodash";
|
||||
import isEqual from "lodash.isequal";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, onMounted, onUnmounted, type PropType, type Ref, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router/composables";
|
||||
|
||||
import type { HistoryDetailed, HistorySummary } from "@/api";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
import localize from "@/utils/localization";
|
||||
|
||||
import { HistoriesFilters } from "./HistoriesFilters";
|
||||
|
||||
import TextSummary from "../Common/TextSummary.vue";
|
||||
import Heading from "@/components/Common/Heading.vue";
|
||||
import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue";
|
||||
import UtcDate from "@/components/UtcDate.vue";
|
||||
|
||||
type AdditionalOptions = "set-current" | "multi" | "center";
|
||||
|
||||
const props = defineProps({
|
||||
multiple: { type: Boolean, default: false },
|
||||
histories: { type: Array as PropType<HistorySummary[]>, default: () => [] },
|
||||
additionalOptions: { type: Array as PropType<AdditionalOptions[]>, default: () => [] },
|
||||
showModal: { type: Boolean, default: false },
|
||||
inModal: { type: Boolean, default: false },
|
||||
filter: { type: String, default: "" },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "selectHistory", history: HistorySummary): void;
|
||||
(e: "selectHistories", histories: { id: string }[]): void;
|
||||
(e: "setFilter", filter: string, value: string): void;
|
||||
(e: "update:loading", loading: boolean): void;
|
||||
}>();
|
||||
|
||||
library.add(faColumns, faSignInAlt, faListAlt, faArrowDown, faCheckSquare, faSquare);
|
||||
|
||||
const selectedHistories: Ref<{ id: string }[]> = ref([]);
|
||||
const busy = ref(false);
|
||||
const showAdvanced = ref(false);
|
||||
const scrollableDiv: Ref<HTMLElement | null> = ref(null);
|
||||
|
||||
const historyStore = useHistoryStore();
|
||||
const { currentHistoryId, totalHistoryCount, pinnedHistories } = storeToRefs(useHistoryStore());
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const hasNoResults = computed(() => props.filter && filtered.value.length == 0);
|
||||
const validFilter = computed(() => props.filter && props.filter.length > 2);
|
||||
const allLoaded = computed(() => totalHistoryCount.value <= filtered.value.length);
|
||||
|
||||
onMounted(async () => {
|
||||
useInfiniteScroll(scrollableDiv.value, () => loadMore());
|
||||
if (props.filter !== "" && validFilter.value) {
|
||||
await loadMore(true);
|
||||
}
|
||||
});
|
||||
|
||||
// retain previously selected histories when you reopen the modal in multi view
|
||||
watch(
|
||||
() => props.showModal,
|
||||
(show: boolean) => {
|
||||
if (props.multiple && show) {
|
||||
selectedHistories.value = [...pinnedHistories.value];
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
// Remove the infinite scrolling behavior
|
||||
useInfiniteScroll(scrollableDiv.value, () => {});
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.filter,
|
||||
async (newVal: string, oldVal: string) => {
|
||||
if (newVal !== "" && validFilter.value && newVal !== oldVal) {
|
||||
await loadMore(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => busy.value,
|
||||
(loading: boolean) => {
|
||||
emit("update:loading", loading);
|
||||
}
|
||||
);
|
||||
|
||||
// reactive proxy for props.histories, as the prop is not
|
||||
// always guaranteed to be reactive for some strange reason.
|
||||
// TODO: Re investigate when upgrading to vue3
|
||||
/** `historyStore` histories for current user */
|
||||
const historiesProxy: Ref<HistorySummary[]> = ref([]);
|
||||
watch(
|
||||
() => props.histories as HistoryDetailed[],
|
||||
(h: HistoryDetailed[]) => {
|
||||
historiesProxy.value = h.filter(
|
||||
(h) => !h.user_id || (!currentUser.value?.isAnonymous && h.user_id === currentUser.value?.id)
|
||||
);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const filtered: Ref<HistorySummary[]> = computed(() => {
|
||||
let filteredHistories: HistorySummary[] = [];
|
||||
if (!validFilter.value) {
|
||||
filteredHistories = historiesProxy.value;
|
||||
} else {
|
||||
const filters = HistoriesFilters.getFiltersForText(props.filter);
|
||||
filteredHistories = historiesProxy.value.filter((history: HistorySummary) => {
|
||||
if (!HistoriesFilters.testFilters(filters, history)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return filteredHistories.sort((a, b) => {
|
||||
if (!isMultiviewPanel.value && a.id == currentHistoryId.value) {
|
||||
return -1;
|
||||
} else if (!isMultiviewPanel.value && b.id == currentHistoryId.value) {
|
||||
return 1;
|
||||
} else if (a.update_time < b.update_time) {
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/** this is the selector list that shows up in the left panel */
|
||||
const isMultiviewPanel = computed(() => !props.inModal && props.multiple);
|
||||
|
||||
/** if pinned histories and selected histories are equal */
|
||||
const pinnedSelectedEqual = computed(() => {
|
||||
// uses `orderBy` to ensure same ids are found in both `{ id: string }[]` arrays
|
||||
return isEqual(orderBy(pinnedHistories.value, ["id"], ["asc"]), orderBy(selectedHistories.value, ["id"], ["asc"]));
|
||||
});
|
||||
|
||||
function isActiveItem(history: HistorySummary) {
|
||||
if (isMultiviewPanel.value) {
|
||||
return pinnedHistories.value.some((item) => item.id == history.id);
|
||||
} else {
|
||||
return selectedHistories.value.some((item) => item.id == history.id);
|
||||
}
|
||||
}
|
||||
|
||||
function historyClicked(history: HistorySummary) {
|
||||
if (props.multiple && props.inModal) {
|
||||
const index = selectedHistories.value.findIndex((item) => item.id == history.id);
|
||||
if (index !== -1) {
|
||||
selectedHistories.value.splice(index, 1);
|
||||
} else {
|
||||
selectedHistories.value.push({ id: history.id });
|
||||
}
|
||||
} else if (isMultiviewPanel.value) {
|
||||
if (pinnedHistories.value.some((item) => item.id == history.id)) {
|
||||
historyStore.unpinHistories([history.id]);
|
||||
} else {
|
||||
openInMulti(history);
|
||||
}
|
||||
} else {
|
||||
emit("selectHistory", history);
|
||||
// modal.value?.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function selectHistories() {
|
||||
// set value of pinned histories in store
|
||||
emit("selectHistories", selectedHistories.value);
|
||||
// set local value equal to updated value from store
|
||||
selectedHistories.value = pinnedHistories.value;
|
||||
// modal.value?.hide();
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function setCurrentHistory(history: HistorySummary) {
|
||||
historyStore.setCurrentHistory(history.id);
|
||||
// modal.value?.hide();
|
||||
}
|
||||
|
||||
function setCenterPanelHistory(history: HistorySummary) {
|
||||
router.push(`/histories/view?id=${history.id}`);
|
||||
// modal.value?.hide();
|
||||
}
|
||||
|
||||
function setFilterValue(newFilter: string, newValue: string) {
|
||||
emit("setFilter", newFilter, newValue);
|
||||
}
|
||||
|
||||
function openInMulti(history: HistorySummary) {
|
||||
router.push("/histories/view_multiple");
|
||||
historyStore.pinHistory(history.id);
|
||||
// modal.value?.hide();
|
||||
}
|
||||
|
||||
/** Loads (paginates) for more histories
|
||||
* @param noScroll If true, we are not scrolling and will load _all_ items for current filter
|
||||
*/
|
||||
async function loadMore(noScroll = false) {
|
||||
if (!busy.value && (noScroll || (!noScroll && !props.filter && !allLoaded.value))) {
|
||||
busy.value = true;
|
||||
const queryString = props.filter && HistoriesFilters.getQueryString(props.filter);
|
||||
await historyStore.loadHistories(true, queryString);
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="unified-panel">
|
||||
<div class="unified-panel-controls">
|
||||
<BBadge v-if="props.filter && !validFilter" class="alert-danger w-100 mb-2">
|
||||
Search string too short!
|
||||
</BBadge>
|
||||
<b-alert v-else-if="!busy && hasNoResults" class="mb-2" variant="danger" show>No histories found.</b-alert>
|
||||
</div>
|
||||
|
||||
<div class="position-relative flex-grow-1">
|
||||
<div
|
||||
v-show="!showAdvanced"
|
||||
ref="scrollableDiv"
|
||||
:class="{ 'history-scroll-list': !hasNoResults, toolMenuContainer: isMultiviewPanel }"
|
||||
role="list">
|
||||
<BListGroup>
|
||||
<BListGroupItem
|
||||
v-for="history in filtered"
|
||||
:key="history.id"
|
||||
:data-pk="history.id"
|
||||
button
|
||||
:class="{
|
||||
current: !isMultiviewPanel && history.id === currentHistoryId,
|
||||
'panel-item': isMultiviewPanel,
|
||||
}"
|
||||
:active="isActiveItem(history)"
|
||||
@click="() => historyClicked(history)">
|
||||
<div class="overflow-auto">
|
||||
<div :class="!isMultiviewPanel ? 'd-flex justify-content-between align-items-center' : ''">
|
||||
<div v-if="!isMultiviewPanel">
|
||||
<Heading h3 inline bold size="text">
|
||||
<span>{{ history.name }}</span>
|
||||
<i v-if="history.id === currentHistoryId">(Current)</i>
|
||||
</Heading>
|
||||
<i
|
||||
v-if="props.multiple && pinnedHistories.some((h) => h.id === history.id)"
|
||||
v-b-tooltip.noninteractive.hover
|
||||
title="This history is currently pinned in the multi-history view">
|
||||
(currently pinned)
|
||||
</i>
|
||||
</div>
|
||||
<TextSummary v-else component="h4" :description="history.name" one-line-summary />
|
||||
<div class="d-flex align-items-center flex-gapx-1">
|
||||
<BBadge
|
||||
v-b-tooltip.noninteractive.hover
|
||||
pill
|
||||
:title="localize('Amount of items in history')">
|
||||
{{ history.count }} {{ localize("items") }}
|
||||
</BBadge>
|
||||
<BBadge
|
||||
v-if="history.update_time"
|
||||
v-b-tooltip.noninteractive.hover
|
||||
pill
|
||||
:title="localize('Last edited')">
|
||||
<UtcDate :date="history.update_time" mode="elapsed" />
|
||||
</BBadge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="!isMultiviewPanel && history.annotation" class="my-1">{{ history.annotation }}</p>
|
||||
|
||||
<StatelessTags
|
||||
v-if="history.tags.length > 0"
|
||||
class="my-1"
|
||||
:value="history.tags"
|
||||
:disabled="true"
|
||||
:max-visible-tags="isMultiviewPanel ? 1 : 10"
|
||||
@tag-click="setFilterValue('tag', $event)" />
|
||||
|
||||
<div
|
||||
v-if="props.additionalOptions.length > 0"
|
||||
class="d-flex justify-content-end align-items-center mt-1">
|
||||
<BButtonGroup>
|
||||
<BButton
|
||||
v-if="props.additionalOptions.includes('set-current')"
|
||||
v-b-tooltip.noninteractive.hover
|
||||
:title="localize('Set as current history')"
|
||||
variant="link"
|
||||
class="p-0 px-1"
|
||||
@click.stop="() => setCurrentHistory(history)">
|
||||
<FontAwesomeIcon icon="fa-sign-in-alt" />
|
||||
</BButton>
|
||||
|
||||
<BButton
|
||||
v-if="props.additionalOptions.includes('multi')"
|
||||
v-b-tooltip.noninteractive.hover
|
||||
:title="localize('Open in multi-view')"
|
||||
variant="link"
|
||||
class="p-0 px-1"
|
||||
@click.stop="() => openInMulti(history)">
|
||||
<FontAwesomeIcon icon="fa-columns" />
|
||||
</BButton>
|
||||
|
||||
<BButton
|
||||
v-if="props.additionalOptions.includes('center')"
|
||||
v-b-tooltip.noninteractive.hover
|
||||
:title="localize('Open in center panel')"
|
||||
variant="link"
|
||||
class="p-0 px-1"
|
||||
@click.stop="() => setCenterPanelHistory(history)">
|
||||
<FontAwesomeIcon icon="far fa-list-alt" />
|
||||
</BButton>
|
||||
</BButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isMultiviewPanel" class="d-flex align-items-center">
|
||||
<FontAwesomeIcon v-if="isActiveItem(history)" :icon="['far', 'check-square']" size="lg" />
|
||||
<FontAwesomeIcon v-else :icon="['far', 'square']" size="lg" />
|
||||
</div>
|
||||
</BListGroupItem>
|
||||
<div>
|
||||
<div v-if="allLoaded || props.filter !== ''" class="list-end my-2">
|
||||
<span v-if="filtered.length == 1">- {{ filtered.length }} history loaded -</span>
|
||||
<span v-else-if="filtered.length > 1">- All {{ filtered.length }} histories loaded -</span>
|
||||
</div>
|
||||
<b-overlay :show="busy" opacity="0.5" />
|
||||
</div>
|
||||
</BListGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO: This stuff goes in the BModal footer -->
|
||||
<div>
|
||||
<div
|
||||
v-if="!allLoaded"
|
||||
class="mr-auto"
|
||||
:class="isMultiviewPanel && 'mt-1 d-flex justify-content-center align-items-center'">
|
||||
<i>Loaded {{ filtered.length }} out of {{ totalHistoryCount }} histories</i>
|
||||
<BButton
|
||||
v-if="!props.filter"
|
||||
v-b-tooltip.noninteractive.hover
|
||||
class="load-more-hist-button"
|
||||
size="sm"
|
||||
title="Load More"
|
||||
variant="link"
|
||||
@click="loadMore()">
|
||||
<FontAwesomeIcon icon="fa-arrow-down" />
|
||||
</BButton>
|
||||
</div>
|
||||
|
||||
<div v-if="props.inModal">
|
||||
<i v-if="multiple">{{ selectedHistories.length }} histories selected</i>
|
||||
<BButton
|
||||
v-if="multiple"
|
||||
v-localize
|
||||
:disabled="pinnedSelectedEqual || showAdvanced"
|
||||
variant="primary"
|
||||
@click="selectHistories">
|
||||
Change Selected
|
||||
</BButton>
|
||||
<span v-else v-localize> Click a history to switch to it </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "theme/blue.scss";
|
||||
|
||||
.history-scroll-list {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
scroll-behavior: smooth;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
// max-height: 65vh; /** TODO: should only apply to modal */
|
||||
.list-group {
|
||||
.list-group-item {
|
||||
border-radius: 0;
|
||||
|
||||
&.panel-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
&:not(&.active) {
|
||||
background: $panel-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(&.panel-item) {
|
||||
&.current {
|
||||
border-left: 0.25rem solid $brand-primary;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.list-end {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: $text-light;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* TODO: Refactor this to use the new `HistoryScrollList` component
|
||||
*/
|
||||
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faListAlt } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faArrowDown, faColumns, faSignInAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -200,7 +204,7 @@ function setFilterValue(newFilter: string, newValue: string) {
|
||||
function openInMulti(history: HistorySummary) {
|
||||
router.push("/histories/view_multiple");
|
||||
historyStore.pinHistory(history.id);
|
||||
historyStore.loadHistoryById(history.id);
|
||||
historyStore.loadHistoryById(history.id); // TODO: remove
|
||||
modal.value?.hide();
|
||||
}
|
||||
|
||||
@@ -362,7 +366,7 @@ async function loadMore(noScroll = false) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import "theme/blue.scss";
|
||||
|
||||
.history-selector-modal .modal-open .modal {
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faCheckSquare, faClock, faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheckSquare, faClock, faTimes, faUndo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
import { HistoryFilters } from "@/components/History/HistoryFilters";
|
||||
import { Toast } from "@/composables/toast";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
import localize from "@/utils/localization";
|
||||
|
||||
import MultipleViewList from "./MultipleViewList.vue";
|
||||
import FilterMenu from "@/components/Common/FilterMenu.vue";
|
||||
import Heading from "@/components/Common/Heading.vue";
|
||||
import SelectorModal from "@/components/History/Modals/SelectorModal.vue";
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
|
||||
type PinnedHistory = { id: string };
|
||||
|
||||
const filter = ref("");
|
||||
const showAdvanced = ref(false);
|
||||
const showSelectModal = ref(false);
|
||||
const initialLoaded = ref(false);
|
||||
|
||||
library.add(faClock, faCheckSquare, faTimes);
|
||||
library.add(faCheckSquare, faClock, faTimes, faUndo);
|
||||
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
const { histories, currentHistory, historiesLoading } = storeToRefs(useHistoryStore());
|
||||
@@ -77,7 +81,6 @@ function addHistoriesToList(incomingHistories: PinnedHistory[]) {
|
||||
const historyExists = historyStore.pinnedHistories.some((h: PinnedHistory) => h.id === history.id);
|
||||
if (!historyExists) {
|
||||
historyStore.pinHistory(history.id);
|
||||
historyStore.loadHistoryById(history.id);
|
||||
}
|
||||
});
|
||||
// Unpin histories that are already pinned but not in the incoming list
|
||||
@@ -99,60 +102,57 @@ const showRecentTitle = computed(() => {
|
||||
function showRecent() {
|
||||
historyStore.pinnedHistories = [];
|
||||
Toast.info(
|
||||
"Showing the 4 most recently updated histories. Pin histories to this view by clicking on Select Histories."
|
||||
"Showing the 4 most recently updated histories. Pin histories to this view by clicking on Select Histories.",
|
||||
"History Multiview"
|
||||
);
|
||||
}
|
||||
|
||||
function updateFilter(newFilter: string) {
|
||||
filter.value = newFilter;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="currentUser">
|
||||
<div v-if="currentUser" class="d-flex flex-column">
|
||||
<div class="d-flex">
|
||||
<Heading h1 separator inline size="xl" class="flex-grow-1 mb-2">History Multiview</Heading>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<b-button-group v-b-tooltip.hover.noninteractive :title="showRecentTitle">
|
||||
<b-button
|
||||
size="sm"
|
||||
data-description="show recent histories"
|
||||
variant="outline-primary"
|
||||
:disabled="!hasPinnedHistories"
|
||||
@click="showRecent">
|
||||
<FontAwesomeIcon v-if="hasPinnedHistories" icon="undo" />
|
||||
<FontAwesomeIcon v-else icon="clock" />
|
||||
<span v-localize>Recent</span>
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
<b-button
|
||||
v-b-tooltip.hover.noninteractive
|
||||
:title="localize('Open modal to select/deselect histories')"
|
||||
size="sm"
|
||||
data-description="open select histories modal"
|
||||
variant="outline-primary"
|
||||
@click="showSelectModal = true">
|
||||
<FontAwesomeIcon icon="fa-check-square" />
|
||||
<span v-localize>Select</span>
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<b-alert v-if="!initialLoaded && historiesLoading" class="m-2" variant="info" show>
|
||||
<LoadingSpan message="Loading Histories" />
|
||||
</b-alert>
|
||||
<div v-else-if="histories.length" class="multi-history-panel d-flex flex-column h-100">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="w-100">
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
v-model="filter"
|
||||
size="sm"
|
||||
debounce="500"
|
||||
:class="filter && 'font-weight-bold'"
|
||||
:placeholder="localize('search datasets in selected histories')"
|
||||
data-description="filter text input"
|
||||
@keyup.esc="updateFilter('')" />
|
||||
<b-input-group-append>
|
||||
<b-button size="sm" data-description="show deleted filter toggle" @click="updateFilter('')">
|
||||
<FontAwesomeIcon icon="fa-times" fixed-width />
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</div>
|
||||
<b-button-group v-b-tooltip.hover.noninteractive :title="showRecentTitle">
|
||||
<b-button
|
||||
size="sm"
|
||||
data-description="show recent histories"
|
||||
class="text-nowrap ml-1"
|
||||
:disabled="!hasPinnedHistories"
|
||||
@click="showRecent">
|
||||
<FontAwesomeIcon icon="fa-clock" />
|
||||
<span v-localize>Show Recent</span>
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
<b-button
|
||||
size="sm"
|
||||
data-description="open select histories modal"
|
||||
class="text-nowrap ml-1"
|
||||
@click="showSelectModal = true">
|
||||
<FontAwesomeIcon icon="fa-check-square" />
|
||||
<span v-localize>Select Histories</span>
|
||||
</b-button>
|
||||
</div>
|
||||
<FilterMenu
|
||||
name="History Multiview"
|
||||
:placeholder="localize('Search datasets and collections in selected histories')"
|
||||
:filter-class="HistoryFilters"
|
||||
:filter-text.sync="filter"
|
||||
:loading="historiesLoading"
|
||||
:show-advanced.sync="showAdvanced" />
|
||||
<MultipleViewList
|
||||
v-show="!showAdvanced"
|
||||
:filter="filter"
|
||||
:current-history="currentHistory"
|
||||
:selected-histories="selectedHistories"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { BButton } from "bootstrap-vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
|
||||
@@ -21,6 +21,10 @@ const props = defineProps<Props>();
|
||||
const historyStore = useHistoryStore();
|
||||
const { currentHistoryId, pinnedHistories } = storeToRefs(historyStore);
|
||||
|
||||
onMounted(() => {
|
||||
historyStore.loadHistoryById(props.source.id);
|
||||
});
|
||||
|
||||
const selectedCollections = ref<any[]>([]);
|
||||
|
||||
const sameToCurrent = computed(() => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useAnimationFrameScroll } from "@/composables/sensors/animationFrameScr
|
||||
import { Toast } from "@/composables/toast";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import localize from "@/utils/localization";
|
||||
import { errorMessageAsString } from "@/utils/simple-error";
|
||||
|
||||
import HistoryDropZone from "../CurrentHistory/HistoryDropZone.vue";
|
||||
import MultipleViewItem from "./MultipleViewItem.vue";
|
||||
@@ -67,10 +68,9 @@ async function onDrop(evt: any) {
|
||||
} else {
|
||||
Toast.info(localize("Collection copied to new history"));
|
||||
}
|
||||
historyStore.loadHistoryById(currentHistoryId);
|
||||
})
|
||||
.catch((error) => {
|
||||
Toast.error(error);
|
||||
Toast.error(errorMessageAsString(error));
|
||||
});
|
||||
// pin the newly created history via the drop
|
||||
historyStore.pinHistory(currentHistoryId);
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faColumns, faUndo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router/composables";
|
||||
|
||||
import { HistoriesFilters } from "@/components/History/HistoriesFilters";
|
||||
import { Toast } from "@/composables/toast";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
import { localize } from "@/utils/localization";
|
||||
import { withPrefix } from "@/utils/redirect";
|
||||
import { errorMessageAsString } from "@/utils/simple-error";
|
||||
|
||||
import FilterMenu from "@/components/Common/FilterMenu.vue";
|
||||
import HistoryList from "@/components/History/HistoryScrollList.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// @ts-ignore bad library types
|
||||
library.add(faColumns, faUndo);
|
||||
|
||||
const filter = ref("");
|
||||
const showAdvanced = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const isAnonymous = computed(() => useUserStore().isAnonymous);
|
||||
const historyStore = useHistoryStore();
|
||||
const { histories, historiesLoading, currentHistoryId } = storeToRefs(historyStore);
|
||||
|
||||
const pinnedHistoryCount = computed(() => {
|
||||
return Object.keys(historyStore.pinnedHistories).length;
|
||||
});
|
||||
|
||||
const pinRecentTitle = computed(() => {
|
||||
if (pinnedHistoryCount.value > 0) {
|
||||
return localize("Reset selection to show 4 most recently updated histories instead");
|
||||
} else {
|
||||
return localize("Currently showing 4 most recently updated histories in Multiview");
|
||||
}
|
||||
});
|
||||
|
||||
const pinRecentText = computed(() => {
|
||||
if (pinnedHistoryCount.value > 1) {
|
||||
return localize(`${pinnedHistoryCount.value} histories pinned. Click here to reset`);
|
||||
} else if (pinnedHistoryCount.value == 1) {
|
||||
return localize("1 history pinned. Click here to reset");
|
||||
} else {
|
||||
return localize("Select histories to pin to Multiview");
|
||||
}
|
||||
});
|
||||
|
||||
async function createAndPin() {
|
||||
try {
|
||||
loading.value = true;
|
||||
await historyStore.createNewHistory();
|
||||
if (!currentHistoryId.value) {
|
||||
throw new Error("Error creating history");
|
||||
}
|
||||
historyStore.pinHistory(currentHistoryId.value);
|
||||
router.push("/histories/view_multiple");
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
Toast.error(errorMessageAsString(error), "Error creating and pinning history");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reset to _default_ state; showing 4 latest updated histories */
|
||||
function pinRecent() {
|
||||
historyStore.pinnedHistories = [];
|
||||
Toast.info(
|
||||
"Showing the 4 most recently updated histories in Multiview. Pin histories to History Multiview by selecting them in the panel.",
|
||||
"History Multiview"
|
||||
);
|
||||
}
|
||||
|
||||
function setFilter(newFilter: string, newValue: string) {
|
||||
filter.value = HistoriesFilters.setFilterValue(filter.value, newFilter, newValue);
|
||||
}
|
||||
|
||||
function userTitle(title: string) {
|
||||
if (isAnonymous.value == true) {
|
||||
return `Log in to ${title}`;
|
||||
} else {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="unified-panel" aria-labelledby="multiview-heading">
|
||||
<div unselectable="on">
|
||||
<div class="unified-panel-header-inner">
|
||||
<nav class="d-flex justify-content-between mx-3 my-2">
|
||||
<h2 v-localize class="m-1 h-sm">Select Histories</h2>
|
||||
<b-button-group>
|
||||
<b-button
|
||||
v-b-tooltip.bottom.hover
|
||||
data-description="create new history and pin"
|
||||
size="sm"
|
||||
variant="link"
|
||||
:title="userTitle('Create new history and pin to multiview')"
|
||||
:disabled="isAnonymous"
|
||||
@click="createAndPin">
|
||||
<Icon fixed-width icon="plus" />
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div class="unified-panel-controls">
|
||||
<FilterMenu
|
||||
class="mb-3"
|
||||
name="Histories"
|
||||
placeholder="search histories"
|
||||
:filter-class="HistoriesFilters"
|
||||
:filter-text.sync="filter"
|
||||
:loading="historiesLoading || loading"
|
||||
:show-advanced.sync="showAdvanced" />
|
||||
<section v-if="!showAdvanced">
|
||||
<b-button
|
||||
v-if="route.path !== '/histories/view_multiple'"
|
||||
v-b-tooltip.hover.noninteractive.bottom
|
||||
:aria-label="userTitle('Open History Multiview in center panel')"
|
||||
:title="userTitle('Open History Multiview in center panel')"
|
||||
class="w-100 mb-2"
|
||||
size="sm"
|
||||
:disabled="isAnonymous"
|
||||
@click="router.push('/histories/view_multiple')">
|
||||
<FontAwesomeIcon icon="columns" />
|
||||
<span v-localize>Open History Multiview</span>
|
||||
</b-button>
|
||||
<b-button-group
|
||||
v-else
|
||||
v-b-tooltip.hover.noninteractive.bottom
|
||||
class="w-100 mb-2"
|
||||
:aria-label="pinRecentTitle"
|
||||
:title="pinRecentTitle">
|
||||
<b-button size="sm" :disabled="!pinnedHistoryCount" @click="pinRecent">
|
||||
<span class="position-relative">
|
||||
<FontAwesomeIcon v-if="pinnedHistoryCount" icon="undo" class="mr-1" />
|
||||
<b>{{ pinRecentText }}</b>
|
||||
</span>
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</section>
|
||||
</div>
|
||||
<div v-if="isAnonymous">
|
||||
<b-badge class="alert-info w-100 mx-2">
|
||||
Please <a :href="withPrefix('/login')">log in or register</a> to create multiple histories.
|
||||
</b-badge>
|
||||
</div>
|
||||
<HistoryList
|
||||
v-show="!showAdvanced"
|
||||
multiple
|
||||
:filter="filter"
|
||||
:histories="histories"
|
||||
:loading.sync="loading"
|
||||
@setFilter="setFilter" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -280,6 +280,7 @@ export function getRouter(Galaxy) {
|
||||
path: "histories/view_multiple",
|
||||
component: HistoryMultipleView,
|
||||
props: true,
|
||||
redirect: redirectAnon(),
|
||||
},
|
||||
{
|
||||
path: "histories/list_published",
|
||||
|
||||
@@ -83,13 +83,13 @@ export const Activities = [
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
description: "Opens the History Multiview in the center panel.",
|
||||
description: "Displays the history selector panel and opens History Multiview in the center panel.",
|
||||
icon: "fa-columns",
|
||||
id: "multiview",
|
||||
mutable: false,
|
||||
optional: true,
|
||||
title: "History Multiview",
|
||||
tooltip: "Open History Multiview",
|
||||
tooltip: "Select histories to show in History Multiview",
|
||||
to: "/histories/view_multiple",
|
||||
visible: true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user