add History Multiview activity panel, create HistoryScrollList

This commit is contained in:
Ahmed Awan
2024-02-20 12:13:19 -06:00
parent 1e9c433999
commit 331b948549
12 changed files with 685 additions and 63 deletions
@@ -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>
+1
View File
@@ -280,6 +280,7 @@ export function getRouter(Galaxy) {
path: "histories/view_multiple",
component: HistoryMultipleView,
props: true,
redirect: redirectAnon(),
},
{
path: "histories/list_published",
+2 -2
View File
@@ -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,
},