web/satellite/vuetify-poc: add pagination to browser card view
This change adds pagination, sort and search to the object browser card view. Issue: #6427 Change-Id: I4cc42f3062a7cbd06d9f253e4864478f0b430fc8
This commit is contained in:
parent
83ea3772e2
commit
76594466ef
@ -200,7 +200,7 @@ function sizeChanged(size: number) {
|
||||
}
|
||||
// if the new size is large enough to cause the page index to be out of range
|
||||
// we calculate an appropriate new page index.
|
||||
const maxPage = Math.ceil(Math.ceil(props.totalItemsCount / size));
|
||||
const maxPage = Math.ceil(props.totalItemsCount / size);
|
||||
const page = currentPageNumber.value > maxPage ? maxPage : currentPageNumber.value;
|
||||
if (!props.onPageChange) {
|
||||
return;
|
||||
|
@ -2,25 +2,139 @@
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<v-card v-if="!allFiles.length" title="No objects uploaded" cols="12" sm="6" md="4" lg="3" variant="flat" :border="true" rounded="xlg">
|
||||
<v-card-text>
|
||||
<v-divider class="mt-1 mb-4" />
|
||||
<v-btn color="primary" size="small" class="mr-2" @click="emit('uploadClick')">Upload</v-btn>
|
||||
<v-btn size="small" class="mr-2" @click="emit('newFolderClick')">New Folder</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-row v-else>
|
||||
<v-col v-for="item in allFiles" :key="item.browserObject.Key" cols="12" sm="6" md="4" lg="3">
|
||||
<file-card
|
||||
:item="item"
|
||||
class="h-100"
|
||||
@preview-click="onFileClick(item.browserObject)"
|
||||
@delete-file-click="onDeleteFileClick(item.browserObject)"
|
||||
@share-click="onShareClick(item.browserObject)"
|
||||
<v-row align="center" class="mb-3">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
label="Search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
single-line
|
||||
variant="solo-filled"
|
||||
flat
|
||||
hide-details
|
||||
clearable
|
||||
density="comfortable"
|
||||
rounded="lg"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-menu>
|
||||
<template #activator="{ props: sortProps }">
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
append-icon="mdi-chevron-down"
|
||||
v-bind="sortProps"
|
||||
>
|
||||
Sort by {{ sortKey }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(key, index) in sortKeys"
|
||||
:key="index"
|
||||
:title="key"
|
||||
@click="() => sortKey = key.toLowerCase()"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn-toggle
|
||||
v-model="sortOrder"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
rounded="lg"
|
||||
mandatory
|
||||
>
|
||||
<v-btn value="asc">
|
||||
<v-icon>mdi-arrow-up</v-icon>
|
||||
</v-btn>
|
||||
<v-btn value="desc">
|
||||
<v-icon>mdi-arrow-down</v-icon>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-data-iterator
|
||||
:page="cursor.page"
|
||||
:items-per-page="cursor.limit"
|
||||
:items="browserFiles"
|
||||
:search="search"
|
||||
:sort-by="sortBy"
|
||||
:loading="isFetching"
|
||||
>
|
||||
<template #no-data>
|
||||
<div class="d-flex justify-center">No results found</div>
|
||||
</template>
|
||||
|
||||
<template #default="fileProps">
|
||||
<v-row>
|
||||
<v-col v-for="item in fileProps.items" :key="item.raw.browserObject.Key" cols="12" sm="6" md="4" lg="3" xl="2">
|
||||
<file-card
|
||||
:item="item.raw"
|
||||
class="h-100"
|
||||
@preview-click="onFileClick(item.raw.browserObject)"
|
||||
@delete-file-click="onDeleteFileClick(item.raw.browserObject)"
|
||||
@share-click="onShareClick(item.raw.browserObject)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="d-flex align-center py-5">
|
||||
<v-menu>
|
||||
<template #activator="{ props: limitProps }">
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
append-icon="mdi-chevron-down"
|
||||
v-bind="limitProps"
|
||||
>
|
||||
{{ cursor.limit }} files per page
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(number, index) in tableSizeOptions(totalObjectCount, true)"
|
||||
:key="index"
|
||||
:title="number.title"
|
||||
@click="() => onLimitChange(number.value)"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<span class="mr-4">
|
||||
Page {{ cursor.page }} of {{ lastPage }}
|
||||
</span>
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
variant="tonal"
|
||||
:disabled="cursor.page === 1"
|
||||
@click="() => onPageChange(cursor.page - 1)"
|
||||
>
|
||||
<v-icon>mdi-chevron-left</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="ml-2"
|
||||
:disabled="cursor.page === lastPage"
|
||||
@click="() => onPageChange(cursor.page + 1)"
|
||||
>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-iterator>
|
||||
<file-preview-dialog v-model="previewDialog" />
|
||||
|
||||
<delete-file-dialog
|
||||
@ -41,9 +155,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { VBtn, VCard, VCardText, VCol, VDivider, VRow } from 'vuetify/components';
|
||||
import { VBtn, VBtnToggle, VCol, VIcon, VList, VListItem, VMenu, VRow, VSpacer, VTextField } from 'vuetify/components';
|
||||
import { VDataIterator } from 'vuetify/labs/components';
|
||||
|
||||
import { BrowserObject, PreviewCache, useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
|
||||
import {
|
||||
BrowserObject,
|
||||
MAX_KEY_COUNT,
|
||||
ObjectBrowserCursor,
|
||||
PreviewCache,
|
||||
useObjectBrowserStore,
|
||||
} from '@/store/modules/objectBrowserStore';
|
||||
import { useProjectsStore } from '@/store/modules/projectsStore';
|
||||
import { useNotify } from '@/utils/hooks';
|
||||
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
|
||||
@ -53,6 +174,7 @@ import { LocalData } from '@/utils/localData';
|
||||
import { useAppStore } from '@/store/modules/appStore';
|
||||
import { BrowserObjectTypeInfo, BrowserObjectWrapper, EXTENSION_INFOS, FILE_INFO, FOLDER_INFO } from '@/types/browser';
|
||||
import { useLinksharing } from '@/composables/useLinksharing';
|
||||
import { tableSizeOptions } from '@/types/common';
|
||||
|
||||
import FilePreviewDialog from '@poc/components/dialogs/FilePreviewDialog.vue';
|
||||
import DeleteFileDialog from '@poc/components/dialogs/DeleteFileDialog.vue';
|
||||
@ -60,6 +182,8 @@ import ShareDialog from '@poc/components/dialogs/ShareDialog.vue';
|
||||
import BrowserSnackbarComponent from '@poc/components/BrowserSnackbarComponent.vue';
|
||||
import FileCard from '@poc/components/FileCard.vue';
|
||||
|
||||
type SortKey = 'name' | 'type' | 'size' | 'date';
|
||||
|
||||
const props = defineProps<{
|
||||
forceEmpty?: boolean;
|
||||
}>();
|
||||
@ -88,7 +212,12 @@ const fileToDelete = ref<BrowserObject | null>(null);
|
||||
const isDeleteFileDialogShown = ref<boolean>(false);
|
||||
const fileToShare = ref<BrowserObject | null>(null);
|
||||
const isShareDialogShown = ref<boolean>(false);
|
||||
const isFileGuideShown = ref<boolean>(false);
|
||||
const routePageCache = new Map<string, number>();
|
||||
|
||||
const sortKey = ref<string>('name');
|
||||
const sortOrder = ref<string>('asc');
|
||||
const sortKeys = ['Name', 'Type', 'Size', 'Date'];
|
||||
const collator = new Intl.Collator('en', { sensitivity: 'case' });
|
||||
|
||||
/**
|
||||
* Returns object preview URLs cache from store.
|
||||
@ -112,6 +241,29 @@ const filePath = computed<string>(() => bucketsStore.state.fileComponentPath);
|
||||
*/
|
||||
const isObjectsUploadModal = computed<boolean>(() => appStore.state.isUploadingModal);
|
||||
|
||||
/**
|
||||
* Returns total object count from store.
|
||||
*/
|
||||
const isPaginationEnabled = computed<boolean>(() => config.state.config.objectBrowserPaginationEnabled);
|
||||
|
||||
/**
|
||||
* Returns total object count from store.
|
||||
*/
|
||||
const totalObjectCount = computed<number>(() => isPaginationEnabled.value ? obStore.state.totalObjectCount : allFiles.value.length);
|
||||
|
||||
/**
|
||||
* Returns browser cursor from store.
|
||||
*/
|
||||
const cursor = computed<ObjectBrowserCursor>(() => obStore.state.cursor);
|
||||
|
||||
/**
|
||||
* Returns the last page of the file list.
|
||||
*/
|
||||
const lastPage = computed<number>(() => {
|
||||
const page = Math.ceil(totalObjectCount.value / cursor.value.limit);
|
||||
return page === 0 ? page + 1 : page;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns every file under the current path.
|
||||
*/
|
||||
@ -132,6 +284,98 @@ const allFiles = computed<BrowserObjectWrapper[]>(() => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns every file under the current path that matchs the search query.
|
||||
*/
|
||||
const filteredFiles = computed<BrowserObjectWrapper[]>(() => {
|
||||
if (!search.value) return allFiles.value;
|
||||
const searchLower = search.value.toLowerCase();
|
||||
return allFiles.value.filter(file => file.lowerName.includes(searchLower));
|
||||
});
|
||||
|
||||
/**
|
||||
* The sorting criteria to be used for the file list.
|
||||
*/
|
||||
const sortBy = computed(() => [{ key: sortKey.value, order: sortOrder.value }]);
|
||||
|
||||
/**
|
||||
* Returns the files to be displayed in the browser.
|
||||
*/
|
||||
const browserFiles = computed<BrowserObjectWrapper[]>(() => {
|
||||
const files = [...filteredFiles.value];
|
||||
|
||||
if (sortBy.value.length) {
|
||||
const sort = sortBy.value[0];
|
||||
|
||||
type CompareFunc = (a: BrowserObjectWrapper, b: BrowserObjectWrapper) => number;
|
||||
const compareFuncs: Record<SortKey, CompareFunc> = {
|
||||
name: (a, b) => collator.compare(a.browserObject.Key, b.browserObject.Key),
|
||||
type: (a, b) => collator.compare(a.typeInfo.title, b.typeInfo.title) || collator.compare(a.ext, b.ext),
|
||||
size: (a, b) => a.browserObject.Size - b.browserObject.Size,
|
||||
date: (a, b) => a.browserObject.LastModified.getTime() - b.browserObject.LastModified.getTime(),
|
||||
};
|
||||
|
||||
files.sort((a, b) => {
|
||||
const objA = a.browserObject, objB = b.browserObject;
|
||||
if (sort.key !== 'type') {
|
||||
if (objA.type === 'folder') {
|
||||
if (objB.type !== 'folder') return -1;
|
||||
if (sort.key === 'size' || sort.key === 'date') return 0;
|
||||
} else if (objB.type === 'folder') {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const cmp = compareFuncs[sort.key](a, b);
|
||||
return sort.order === 'asc' ? cmp : -cmp;
|
||||
});
|
||||
}
|
||||
|
||||
if (cursor.value.limit === -1 || isPaginationEnabled.value) return files;
|
||||
|
||||
return files.slice((cursor.value.page - 1) * cursor.value.limit, cursor.value.page * cursor.value.limit);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles page change event.
|
||||
*/
|
||||
function onPageChange(page: number): void {
|
||||
if (page < 1) return;
|
||||
if (page > lastPage.value) return;
|
||||
const path = filePath.value ? filePath.value + '/' : '';
|
||||
routePageCache.set(path, page);
|
||||
obStore.setCursor({ page, limit: cursor.value?.limit ?? 10 });
|
||||
|
||||
const lastObjectOnPage = page * cursor.value.limit;
|
||||
const activeRange = obStore.state.activeObjectsRange;
|
||||
|
||||
if (lastObjectOnPage > activeRange.start && lastObjectOnPage <= activeRange.end) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenKey = Math.ceil(lastObjectOnPage / MAX_KEY_COUNT) * MAX_KEY_COUNT;
|
||||
|
||||
const tokenToBeFetched = obStore.state.continuationTokens.get(tokenKey);
|
||||
if (!tokenToBeFetched) {
|
||||
obStore.initList(path);
|
||||
return;
|
||||
}
|
||||
|
||||
obStore.listByToken(path, tokenKey, tokenToBeFetched);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles items per page change event.
|
||||
*/
|
||||
function onLimitChange(newLimit: number): void {
|
||||
// if the new limit is large enough to cause the page index to be out of range
|
||||
// we calculate an appropriate new page index.
|
||||
const oldPage = cursor.value.page ?? 1;
|
||||
const maxPage = Math.ceil(totalObjectCount.value / newLimit);
|
||||
const page = oldPage > maxPage ? maxPage : oldPage;
|
||||
obStore.setCursor({ page, limit: newLimit });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title and icon representing a file's type.
|
||||
*/
|
||||
@ -164,7 +408,6 @@ function onFileClick(file: BrowserObject): void {
|
||||
|
||||
obStore.setObjectPathForModal(file.path + file.Key);
|
||||
previewDialog.value = true;
|
||||
isFileGuideShown.value = false;
|
||||
LocalData.setFileGuideHidden();
|
||||
}
|
||||
|
||||
@ -178,9 +421,22 @@ async function fetchFiles(): Promise<void> {
|
||||
try {
|
||||
const path = filePath.value ? filePath.value + '/' : '';
|
||||
|
||||
if (isPaginationEnabled.value) {
|
||||
await obStore.initList(path);
|
||||
} else {
|
||||
await obStore.list(path);
|
||||
}
|
||||
|
||||
selected.value = [];
|
||||
|
||||
if (isPaginationEnabled.value) {
|
||||
const cachedPage = routePageCache.get(path);
|
||||
if (cachedPage !== undefined) {
|
||||
obStore.setCursor({ limit: cursor.value.limit, page: cachedPage });
|
||||
} else {
|
||||
obStore.setCursor({ limit: cursor.value.limit, page: 1 });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
err.message = `Error fetching files. ${err.message}`;
|
||||
notify.notifyError(err, AnalyticsErrorEventSource.FILE_BROWSER_LIST_CALL);
|
||||
@ -242,7 +498,8 @@ function findCachedURL(file: BrowserObject): string | undefined {
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads object URL from cache or generates new URL.
|
||||
* Loads object URL from cache or generates new URL for previewing
|
||||
* images on card items.
|
||||
*/
|
||||
async function processFilePath(file: BrowserObjectWrapper) {
|
||||
if (file.browserObject.type === 'folder') return;
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<div class="text-no-wrap text-right">
|
||||
<div class="text-no-wrap" :class="alignClass">
|
||||
<v-btn
|
||||
v-if="file.type !== 'folder'"
|
||||
variant="outlined"
|
||||
@ -85,7 +85,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, h } from 'vue';
|
||||
import { ref, h, computed } from 'vue';
|
||||
import {
|
||||
VMenu,
|
||||
VList,
|
||||
@ -97,7 +97,6 @@ import {
|
||||
VIcon,
|
||||
VBtn,
|
||||
VTooltip,
|
||||
VOverlay,
|
||||
} from 'vuetify/components';
|
||||
|
||||
import { BrowserObject, useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
|
||||
@ -116,6 +115,7 @@ const notify = useNotify();
|
||||
|
||||
const props = defineProps<{
|
||||
file: BrowserObject;
|
||||
align: 'left' | 'right';
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -126,6 +126,10 @@ const emit = defineEmits<{
|
||||
|
||||
const isDownloading = ref<boolean>(false);
|
||||
|
||||
const alignClass = computed<string>(() => {
|
||||
return 'text-' + props.align;
|
||||
});
|
||||
|
||||
async function onDownloadClick(): Promise<void> {
|
||||
if (isDownloading.value) {
|
||||
return;
|
||||
|
@ -81,6 +81,7 @@
|
||||
<template #item.actions="{ item }: ItemSlotProps">
|
||||
<browser-row-actions
|
||||
:file="item.raw.browserObject"
|
||||
align="right"
|
||||
@preview-click="onFileClick(item.raw.browserObject)"
|
||||
@delete-file-click="onDeleteFileClick(item.raw.browserObject)"
|
||||
@share-click="onShareClick(item.raw.browserObject)"
|
||||
|
@ -42,6 +42,7 @@
|
||||
<v-divider class="mt-1 mb-4" />
|
||||
<browser-row-actions
|
||||
:file="item.browserObject"
|
||||
align="left"
|
||||
@preview-click="emit('previewClick', item.browserObject)"
|
||||
@delete-file-click="emit('deleteFileClick', item.browserObject)"
|
||||
@share-click="emit('shareClick', item.browserObject)"
|
||||
|
Loading…
Reference in New Issue
Block a user