mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
[WIP] Paginate History Store
Trying an approach with v-infinite-scroll in selector modal
This commit is contained in:
@@ -114,11 +114,10 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadHistories();
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useHistoryStore, ["loadHistories", "applyFilters"]),
|
||||
...mapActions(useHistoryStore, ["applyFilters"]),
|
||||
load(concat = false) {
|
||||
this.loading = true;
|
||||
getDatasets({
|
||||
|
||||
@@ -22,8 +22,13 @@ import { useRouter } from "vue-router/composables";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faColumns, faSignInAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faColumns, faSignInAlt, faCaretRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faListAlt } from "@fortawesome/free-regular-svg-icons";
|
||||
import Vue from "vue";
|
||||
//@ts-ignore missing typedefs
|
||||
import infiniteScroll from "vue-infinite-scroll";
|
||||
|
||||
Vue.use(infiniteScroll);
|
||||
|
||||
type AdditionalOptions = "set-current" | "multi" | "center";
|
||||
|
||||
@@ -40,10 +45,13 @@ const emit = defineEmits<{
|
||||
(e: "selectHistories", histories: HistorySummary[]): void;
|
||||
}>();
|
||||
|
||||
library.add(faColumns, faSignInAlt, faListAlt);
|
||||
// @ts-ignore bad library types
|
||||
library.add(faColumns, faSignInAlt, faListAlt, faCaretRight);
|
||||
|
||||
const filter = ref("");
|
||||
const currentPage = ref(1);
|
||||
const busy = ref(false);
|
||||
const allLoaded = ref(false);
|
||||
const modal: Ref<BModal | null> = ref(null);
|
||||
|
||||
// reactive proxy for props.histories, as the prop is not
|
||||
@@ -75,8 +83,8 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const paginated = computed(() =>
|
||||
filtered.value.slice((currentPage.value - 1) * props.perPage, currentPage.value * props.perPage)
|
||||
const paginated = computed(
|
||||
() => filtered.value //.slice((currentPage.value - 1) * props.perPage, currentPage.value * props.perPage)
|
||||
);
|
||||
|
||||
const totalRows = computed(() => filtered.value.length);
|
||||
@@ -121,11 +129,24 @@ function openInMulti(history: HistorySummary) {
|
||||
historyStore.pinHistory(history.id);
|
||||
modal.value?.hide();
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (!allLoaded.value) {
|
||||
busy.value = true;
|
||||
await historyStore.loadHistories();
|
||||
busy.value = false;
|
||||
await historyStore.getTotalHistoryCount.then((count) => {
|
||||
if (count <= paginated.value.length) {
|
||||
allLoaded.value = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<b-modal ref="modal" v-bind="$attrs" content-class="history-selector-modal" v-on="$listeners">
|
||||
<b-modal ref="modal" v-bind="$attrs" scrollable content-class="history-selector-modal" v-on="$listeners">
|
||||
<template v-slot:modal-title>
|
||||
<Heading h2 inline size="sm">{{ localize(title) }}</Heading>
|
||||
</template>
|
||||
@@ -134,86 +155,96 @@ function openInMulti(history: HistorySummary) {
|
||||
<b-form-input v-model="filter" type="search" debounce="400" :placeholder="localize('Search Filter')" />
|
||||
</b-form-group>
|
||||
|
||||
<b-list-group>
|
||||
<b-list-group-item
|
||||
v-for="history in paginated"
|
||||
:key="history.id"
|
||||
:data-pk="history.id"
|
||||
button
|
||||
:class="{ current: history.id === currentHistoryId }"
|
||||
:active="selectedHistories.includes(history)"
|
||||
@click="() => historyClicked(history)">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<Heading h3 inline bold size="text">
|
||||
{{ history.name }}
|
||||
<i v-if="history.id === currentHistoryId">(Current)</i>
|
||||
</Heading>
|
||||
<div v-infinite-scroll="loadMore">
|
||||
<b-list-group>
|
||||
<b-list-group-item
|
||||
v-for="history in paginated"
|
||||
:key="history.id"
|
||||
:data-pk="history.id"
|
||||
button
|
||||
:class="{ current: history.id === currentHistoryId }"
|
||||
:active="selectedHistories.includes(history)"
|
||||
@click="() => historyClicked(history)">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<Heading h3 inline bold size="text">
|
||||
{{ history.name }}
|
||||
<i v-if="history.id === currentHistoryId">(Current)</i>
|
||||
</Heading>
|
||||
|
||||
<div class="d-flex align-items-center flex-gapx-1">
|
||||
<b-badge v-b-tooltip pill :title="localize('Amount of items in history')">
|
||||
{{ history.count }} {{ localize("items") }}
|
||||
</b-badge>
|
||||
<b-badge v-b-tooltip pill :title="localize('Last edited')">
|
||||
<UtcDate :date="history.update_time" mode="elapsed" />
|
||||
</b-badge>
|
||||
<div class="d-flex align-items-center flex-gapx-1">
|
||||
<b-badge v-b-tooltip pill :title="localize('Amount of items in history')">
|
||||
{{ history.count }} {{ localize("items") }}
|
||||
</b-badge>
|
||||
<b-badge v-b-tooltip pill :title="localize('Last edited')">
|
||||
<UtcDate :date="history.update_time" mode="elapsed" />
|
||||
</b-badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="history.annotation" class="my-1">{{ history.annotation }}</p>
|
||||
<p v-if="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="10" />
|
||||
<StatelessTags
|
||||
v-if="history.tags.length > 0"
|
||||
class="my-1"
|
||||
:value="history.tags"
|
||||
:disabled="true"
|
||||
:max-visible-tags="10" />
|
||||
|
||||
<div
|
||||
v-if="props.additionalOptions.length > 0"
|
||||
class="d-flex justify-content-end align-items-center mt-1">
|
||||
<b-button-group>
|
||||
<b-button
|
||||
v-if="props.additionalOptions.includes('set-current')"
|
||||
v-b-tooltip
|
||||
:title="localize('Set as current history')"
|
||||
variant="link"
|
||||
class="p-0 px-1"
|
||||
@click.stop="() => setCurrentHistory(history)">
|
||||
<FontAwesomeIcon icon="fa-sign-in-alt" />
|
||||
</b-button>
|
||||
<div
|
||||
v-if="props.additionalOptions.length > 0"
|
||||
class="d-flex justify-content-end align-items-center mt-1">
|
||||
<b-button-group>
|
||||
<b-button
|
||||
v-if="props.additionalOptions.includes('set-current')"
|
||||
v-b-tooltip
|
||||
:title="localize('Set as current history')"
|
||||
variant="link"
|
||||
class="p-0 px-1"
|
||||
@click.stop="() => setCurrentHistory(history)">
|
||||
<FontAwesomeIcon icon="fa-sign-in-alt" />
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
v-if="props.additionalOptions.includes('multi')"
|
||||
v-b-tooltip
|
||||
:title="localize('Open in multi-view')"
|
||||
variant="link"
|
||||
class="p-0 px-1"
|
||||
@click.stop="() => openInMulti(history)">
|
||||
<FontAwesomeIcon icon="fa-columns" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="props.additionalOptions.includes('multi')"
|
||||
v-b-tooltip
|
||||
:title="localize('Open in multi-view')"
|
||||
variant="link"
|
||||
class="p-0 px-1"
|
||||
@click.stop="() => openInMulti(history)">
|
||||
<FontAwesomeIcon icon="fa-columns" />
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
v-if="props.additionalOptions.includes('center')"
|
||||
v-b-tooltip
|
||||
:title="localize('Open in center panel')"
|
||||
variant="link"
|
||||
class="p-0 px-1"
|
||||
@click.stop="() => setCenterPanelHistory(history)">
|
||||
<FontAwesomeIcon icon="far fa-list-alt" />
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</div>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
<b-button
|
||||
v-if="props.additionalOptions.includes('center')"
|
||||
v-b-tooltip
|
||||
:title="localize('Open in center panel')"
|
||||
variant="link"
|
||||
class="p-0 px-1"
|
||||
@click.stop="() => setCenterPanelHistory(history)">
|
||||
<FontAwesomeIcon icon="far fa-list-alt" />
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</div>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</div>
|
||||
|
||||
<b-pagination
|
||||
<!-- <b-pagination
|
||||
v-if="totalRows > props.perPage"
|
||||
v-model="currentPage"
|
||||
class="mt-4"
|
||||
:total-rows="totalRows"
|
||||
:per-page="props.perPage"></b-pagination>
|
||||
:per-page="props.perPage"></b-pagination> -->
|
||||
|
||||
<template v-slot:modal-footer>
|
||||
<b-button
|
||||
v-b-tooltip
|
||||
:title="localize('Load More Histories')"
|
||||
:disabled="busy || allLoaded"
|
||||
variant="primary"
|
||||
@click="loadMore">
|
||||
<FontAwesomeIcon icon="fa-solid fa-caret-right" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="multiple"
|
||||
v-localize
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
deleteHistoryById,
|
||||
getCurrentHistoryFromServer,
|
||||
getHistoryByIdFromServer,
|
||||
getHistoryCount,
|
||||
getHistoryList,
|
||||
secureHistoryOnServer,
|
||||
setCurrentHistoryOnServer,
|
||||
@@ -18,6 +19,7 @@ import { useUserLocalStorage } from "@/composables/userLocalStorage";
|
||||
|
||||
export type HistorySummary = components["schemas"]["HistorySummary"];
|
||||
|
||||
const PAGINATION_LENGTH = 7;
|
||||
const isLoadingHistory = new Set();
|
||||
|
||||
export const useHistoryStore = defineStore("historyStore", () => {
|
||||
@@ -31,6 +33,15 @@ export const useHistoryStore = defineStore("historyStore", () => {
|
||||
return Object.values(storedHistories.value).sort(sortByObjectProp("name"));
|
||||
});
|
||||
|
||||
const historiesOffset = computed(() => {
|
||||
return histories.value.length;
|
||||
});
|
||||
|
||||
const getTotalHistoryCount = computed(async () => {
|
||||
const count = await getHistoryCount();
|
||||
return count;
|
||||
});
|
||||
|
||||
const getFirstHistoryId = computed(() => {
|
||||
return histories.value[0]?.id ?? null;
|
||||
});
|
||||
@@ -39,7 +50,6 @@ export const useHistoryStore = defineStore("historyStore", () => {
|
||||
if (storedCurrentHistoryId.value !== null) {
|
||||
return storedHistories.value[storedCurrentHistoryId.value];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const currentHistoryId = computed(() => {
|
||||
@@ -58,15 +68,19 @@ export const useHistoryStore = defineStore("historyStore", () => {
|
||||
}
|
||||
});
|
||||
|
||||
/** Returns history from storedHistories, will load history if not in store */
|
||||
const getHistoryById = computed(() => {
|
||||
return (historyId: string) => {
|
||||
if (!storedHistories.value[historyId]) {
|
||||
loadHistoryById(historyId);
|
||||
}
|
||||
return storedHistories.value[historyId] ?? null;
|
||||
};
|
||||
});
|
||||
|
||||
const getHistoryNameById = computed(() => {
|
||||
return (historyId: string) => {
|
||||
const history = storedHistories.value[historyId];
|
||||
const history = getHistoryById.value(historyId);
|
||||
if (history) {
|
||||
return history.name;
|
||||
} else {
|
||||
@@ -173,10 +187,15 @@ export const useHistoryStore = defineStore("historyStore", () => {
|
||||
selectHistory(history as HistorySummary);
|
||||
}
|
||||
|
||||
async function loadHistories() {
|
||||
async function loadHistories(paginate: boolean = true) {
|
||||
if (!historiesLoading.value) {
|
||||
if (paginate && historiesOffset.value >= (await getTotalHistoryCount.value)) {
|
||||
return;
|
||||
}
|
||||
setHistoriesLoading(true);
|
||||
await getHistoryList()
|
||||
const limit = paginate ? PAGINATION_LENGTH : null;
|
||||
const offset = paginate ? historiesOffset.value : 0;
|
||||
await getHistoryList(offset, limit)
|
||||
.then((histories) => setHistories(histories))
|
||||
.catch((error) => console.warn(error))
|
||||
.finally(() => {
|
||||
@@ -215,6 +234,7 @@ export const useHistoryStore = defineStore("historyStore", () => {
|
||||
pinnedHistories,
|
||||
getHistoryById,
|
||||
getHistoryNameById,
|
||||
getTotalHistoryCount,
|
||||
setCurrentHistory,
|
||||
setCurrentHistoryId,
|
||||
setFilterText,
|
||||
@@ -233,5 +253,6 @@ export const useHistoryStore = defineStore("historyStore", () => {
|
||||
secureHistory,
|
||||
updateHistory,
|
||||
historiesLoading,
|
||||
historiesOffset,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -108,11 +108,29 @@ export async function setCurrentHistoryOnServer(historyId) {
|
||||
|
||||
/**
|
||||
* Get list of histories from server and return them.
|
||||
* @param {Number} offset
|
||||
* @param {Number | null} limit
|
||||
* @return {Promise.<Array>} list of histories
|
||||
*/
|
||||
export async function getHistoryList() {
|
||||
export async function getHistoryList(offset = 0, limit = null) {
|
||||
const url = "api/histories";
|
||||
const response = await axios.get(prependPath(url), { params: stdHistoryParams });
|
||||
const paginatedParams = {
|
||||
view: "summary",
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
order: "update_time",
|
||||
};
|
||||
const response = await axios.get(prependPath(url), { params: paginatedParams });
|
||||
return doResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of histories from server and return them.
|
||||
* @return {Promise.<Number>} number of histories
|
||||
*/
|
||||
export async function getHistoryCount() {
|
||||
const url = "api/histories/count";
|
||||
const response = await axios.get(prependPath(url));
|
||||
return doResponse(response);
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,17 @@ class FastAPIHistories:
|
||||
trans, serialization_params, filter_query_params, deleted_only=deleted, all_histories=all
|
||||
)
|
||||
|
||||
# THIS IS JUST A DUMMY ROUTE FOR WIP testing
|
||||
@router.get(
|
||||
"/api/histories/count",
|
||||
summary="Returns number of histories for the current user.",
|
||||
)
|
||||
def count(
|
||||
self,
|
||||
trans: ProvidesHistoryContext = DependsOnTrans,
|
||||
) -> int:
|
||||
return self.service.count(trans)
|
||||
|
||||
@router.get(
|
||||
"/api/histories/deleted",
|
||||
summary="Returns deleted histories for the current user.",
|
||||
|
||||
@@ -462,6 +462,23 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores):
|
||||
]
|
||||
return rval
|
||||
|
||||
# THIS IS JUST A DUMMY FN FOR WIP testing
|
||||
def count(
|
||||
self,
|
||||
trans: ProvidesHistoryContext,
|
||||
):
|
||||
"""
|
||||
Returns number of histories for the current user.
|
||||
"""
|
||||
# bail early with current history if user is anonymous
|
||||
current_user = self.user_manager.current_user(trans)
|
||||
if self.user_manager.is_anonymous(current_user):
|
||||
current_history = self.manager.get_current(trans)
|
||||
if not current_history:
|
||||
return 0
|
||||
return 1
|
||||
return len(current_user.active_histories)
|
||||
|
||||
def published(
|
||||
self,
|
||||
trans: ProvidesHistoryContext,
|
||||
|
||||
Reference in New Issue
Block a user