diff --git a/client/src/components/FilesDialog/FilesDialog.vue b/client/src/components/FilesDialog/FilesDialog.vue index 182553017c96..910461a4b3a7 100644 --- a/client/src/components/FilesDialog/FilesDialog.vue +++ b/client/src/components/FilesDialog/FilesDialog.vue @@ -11,14 +11,22 @@ import { } from "@/api/remoteFiles"; import { UrlTracker } from "@/components/DataDialog/utilities"; import { fileSourcePluginToItem, isSubPath } from "@/components/FilesDialog/utilities"; -import { SELECTION_STATES, type SelectionItem } from "@/components/SelectionDialog/selectionTypes"; +import { + ItemsProvider, + ItemsProviderContext, + SELECTION_STATES, + type SelectionItem, +} from "@/components/SelectionDialog/selectionTypes"; import { useConfig } from "@/composables/config"; +import { useFileSources } from "@/composables/fileSources"; import { errorMessageAsString } from "@/utils/simple-error"; import { Model } from "./model"; import SelectionDialog from "@/components/SelectionDialog/SelectionDialog.vue"; +const filesSources = useFileSources(); + interface FilesDialogProps { /** Callback function to be called passing the results when selection is complete */ callback?: (files: any) => void; @@ -55,6 +63,7 @@ const selectedDirectories = ref([]); const errorMessage = ref(); const filter = ref(); const items = ref([]); +const itemsProvider = ref(); const modalShow = ref(true); const optionsShow = ref(false); const undoShow = ref(false); @@ -232,6 +241,7 @@ function load(record?: SelectionItem) { optionsShow.value = false; undoShow.value = !urlTracker.value.atRoot(); if (urlTracker.value.atRoot() || errorMessage.value) { + itemsProvider.value = undefined; errorMessage.value = undefined; getFileSources(props.filterOptions) .then((results) => { @@ -260,6 +270,11 @@ function load(record?: SelectionItem) { showDetails.value = false; return; } + + if (shouldUseItemsProvider()) { + itemsProvider.value = provideItems; + } + browseRemoteFiles(currentDirectory.value?.url, false, props.requireWritable) .then((result) => { items.value = filterByMode(result.entries).map(entryToRecord); @@ -275,6 +290,47 @@ function load(record?: SelectionItem) { } } +/** + * Check if the current file source supports server-side pagination. + * If it does, we will use the items provider to fetch items. + */ +function shouldUseItemsProvider(): boolean { + const fileSource = filesSources.getFileSourceById(currentDirectory.value?.id!); + const supportsPagination = fileSource?.supports?.pagination; + return Boolean(supportsPagination); +} + +/** + * Fetches items from the server using server-side pagination and filtering. + **/ +async function provideItems(ctx: ItemsProviderContext): Promise { + isBusy.value = true; + try { + if (!currentDirectory.value) { + return []; + } + const limit = ctx.perPage; + const offset = (ctx.currentPage - 1) * ctx.perPage; + const query = ctx.filter; + const response = await browseRemoteFiles( + currentDirectory.value?.url, + false, + props.requireWritable, + limit, + offset, + query + ); + const result = response.entries.map(entryToRecord); + totalItems.value = response.totalMatches; + return result; + } catch (error) { + errorMessage.value = errorMessageAsString(error); + return []; + } finally { + isBusy.value = false; + } +} + function filterByMode(results: RemoteEntry[]): RemoteEntry[] { if (!fileMode.value) { // In directory mode, only show directories @@ -350,6 +406,7 @@ onMounted(() => { :fields="fields" :is-busy="isBusy" :items="items" + :items-provider="itemsProvider" :total-items="totalItems" :modal-show="modalShow" :modal-static="modalStatic" diff --git a/client/src/components/SelectionDialog/SelectionDialog.vue b/client/src/components/SelectionDialog/SelectionDialog.vue index 710ae48c42cb..c3958b198003 100644 --- a/client/src/components/SelectionDialog/SelectionDialog.vue +++ b/client/src/components/SelectionDialog/SelectionDialog.vue @@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { BAlert, BButton, BLink, BModal, BPagination, BSpinner, BTable } from "bootstrap-vue"; import { computed, ref, watch } from "vue"; -import { SELECTION_STATES } from "@/components/SelectionDialog/selectionTypes"; +import { ItemsProvider, SELECTION_STATES } from "@/components/SelectionDialog/selectionTypes"; import { type FieldEntry, type SelectionItem } from "./selectionTypes"; @@ -22,10 +22,11 @@ interface Props { disableOk?: boolean; errorMessage?: string; fileMode?: boolean; - fields?: Array; + fields?: FieldEntry[]; isBusy?: boolean; isEncoded?: boolean; - items?: Array; + items?: SelectionItem[]; + itemsProvider?: ItemsProvider; totalItems?: number; leafIcon?: string; modalShow?: boolean; @@ -46,6 +47,7 @@ const props = withDefaults(defineProps(), { isBusy: false, isEncoded: false, items: () => [], + itemsProvider: undefined, totalItems: 0, leafIcon: "fa fa-file-o", modalShow: true, @@ -69,7 +71,7 @@ const emit = defineEmits<{ const filter = ref(""); const currentPage = ref(1); -const perPage = ref(100); +const perPage = ref(25); const fieldDetails = computed(() => { const fields = props.fields.slice().map((x) => { @@ -98,7 +100,9 @@ function selectionIcon(variant: string) { /** Resets pagination when a filter/search word is entered **/ function filtered(items: Array) { - currentPage.value = 1; + if (props.itemsProvider === undefined) { + currentPage.value = 1; + } } /** Format time stamp */ @@ -148,7 +152,7 @@ watch( primary-key="id" :busy="isBusy" :current-page="currentPage" - :items="items" + :items="itemsProvider ?? items" :fields="fieldDetails" :filter="filter" :per-page="perPage" diff --git a/client/src/components/SelectionDialog/selectionTypes.ts b/client/src/components/SelectionDialog/selectionTypes.ts index 793fa7844d2d..894030920918 100644 --- a/client/src/components/SelectionDialog/selectionTypes.ts +++ b/client/src/components/SelectionDialog/selectionTypes.ts @@ -17,3 +17,14 @@ export interface SelectionItem { isLeaf: boolean; url: string; } + +export interface ItemsProviderContext { + apiUrl?: string; + currentPage: number; + perPage: number; + filter?: string; + sortBy?: string; + sortDesc?: boolean; +} + +export type ItemsProvider = (ctx: ItemsProviderContext) => Promise;