From ae8b1209a4671049fa25486d8a84f516a2b4bccf Mon Sep 17 00:00:00 2001 From: Csaky Date: Fri, 13 Dec 2024 15:32:43 -0800 Subject: [PATCH 1/2] Soft-delete/restore Feature delete/restore versions from File details page soft-delete and restore files Deleted files view for each folder Uses new COMS copyVersion feature --- frontend/src/assets/main.scss | 7 + .../components/object/DeleteObjectButton.vue | 51 ++- .../components/object/ObjectFileDetails.vue | 56 ++- frontend/src/components/object/ObjectList.vue | 8 +- .../components/object/ObjectListDeleted.vue | 99 +++++ .../components/object/ObjectProperties.vue | 2 +- .../src/components/object/ObjectTable.vue | 3 +- .../components/object/ObjectTableDeleted.vue | 367 ++++++++++++++++++ .../src/components/object/ObjectVersion.vue | 54 +-- .../components/object/RestoreObjectButton.vue | 106 +++++ frontend/src/components/object/index.ts | 3 + frontend/src/router/index.ts | 7 + frontend/src/services/objectService.ts | 16 + frontend/src/store/objectStore.ts | 56 ++- frontend/src/store/versionStore.ts | 6 +- frontend/src/utils/constants.ts | 1 + .../src/views/detail/DetailObjectsView.vue | 2 +- .../src/views/list/ListObjectsDeletedView.vue | 74 ++++ frontend/tests/unit/store/objectStore.spec.ts | 6 +- .../tests/unit/store/versionStore.spec.ts | 3 +- 20 files changed, 853 insertions(+), 74 deletions(-) create mode 100644 frontend/src/components/object/ObjectListDeleted.vue create mode 100644 frontend/src/components/object/ObjectTableDeleted.vue create mode 100644 frontend/src/components/object/RestoreObjectButton.vue create mode 100644 frontend/src/views/list/ListObjectsDeletedView.vue diff --git a/frontend/src/assets/main.scss b/frontend/src/assets/main.scss index f41b9182..797ef5bb 100644 --- a/frontend/src/assets/main.scss +++ b/frontend/src/assets/main.scss @@ -121,6 +121,10 @@ div:focus-visible { box-shadow: 0 6px 6px -1px rgb(145, 145, 145); } +.p-tooltip{ + max-width: 400px !important; +} + /* layout */ .layout-main { margin: 1rem; @@ -231,6 +235,9 @@ div:focus-visible { &.selected-row { background: $bcbox-highlight-background !important; } + &.deleted-row td:not(.action-buttons) { + opacity: 0.6 !important; + } } } diff --git a/frontend/src/components/object/DeleteObjectButton.vue b/frontend/src/components/object/DeleteObjectButton.vue index 2c8d9b5d..110086fe 100644 --- a/frontend/src/components/object/DeleteObjectButton.vue +++ b/frontend/src/components/object/DeleteObjectButton.vue @@ -1,6 +1,6 @@ diff --git a/frontend/src/components/object/ObjectListDeleted.vue b/frontend/src/components/object/ObjectListDeleted.vue new file mode 100644 index 00000000..08d99dd8 --- /dev/null +++ b/frontend/src/components/object/ObjectListDeleted.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/frontend/src/components/object/ObjectProperties.vue b/frontend/src/components/object/ObjectProperties.vue index fb995b3e..f369a9e4 100644 --- a/frontend/src/components/object/ObjectProperties.vue +++ b/frontend/src/components/object/ObjectProperties.vue @@ -28,7 +28,7 @@ const { getUser } = storeToRefs(userStore); const object: Ref = computed((): any => getObject.value(props.objectId)); const bucket: Ref = computed(() => getBucket.value(object.value?.bucketId as string)); -const createdBy: Ref = computed(() => getUser.value(object.value.createdBy)?.fullName); +const createdBy: Ref = computed(() => getUser.value(object.value?.createdBy)?.fullName); const updatedBy: Ref = computed(() => getUser.value(object.value?.updatedBy)?.fullName); onMounted(() => { diff --git a/frontend/src/components/object/ObjectTable.vue b/frontend/src/components/object/ObjectTable.vue index 39a42b39..f789586b 100644 --- a/frontend/src/components/object/ObjectTable.vue +++ b/frontend/src/components/object/ObjectTable.vue @@ -254,7 +254,7 @@ const selectedFilters = (payload: any) => { v-if="!loading" class="flex justify-content-center" > -

There are no objects associated with your account in this bucket.

+

There are no files associated with your account in this folder.

diff --git a/frontend/src/components/object/RestoreObjectButton.vue b/frontend/src/components/object/RestoreObjectButton.vue new file mode 100644 index 00000000..2124ac1d --- /dev/null +++ b/frontend/src/components/object/RestoreObjectButton.vue @@ -0,0 +1,106 @@ + + + diff --git a/frontend/src/components/object/index.ts b/frontend/src/components/object/index.ts index f0781066..b65f7c00 100644 --- a/frontend/src/components/object/index.ts +++ b/frontend/src/components/object/index.ts @@ -1,9 +1,11 @@ export { default as DeleteObjectButton } from './DeleteObjectButton.vue'; export { default as DownloadObjectButton } from './DownloadObjectButton.vue'; +export { default as RestoreObjectButton } from './RestoreObjectButton.vue'; export { default as ObjectAccess } from './ObjectAccess.vue'; export { default as ObjectFileDetails } from './ObjectFileDetails.vue'; export { default as ObjectFilters } from './ObjectFilters.vue'; export { default as ObjectList } from './ObjectList.vue'; +export { default as ObjectListDeleted } from './ObjectListDeleted.vue'; export { default as ObjectMetadata } from './ObjectMetadata.vue'; export { default as ObjectMetadataTagForm } from './ObjectMetadataTagForm.vue'; export { default as ObjectPermission } from './ObjectPermission.vue'; @@ -12,6 +14,7 @@ export { default as ObjectProperties } from './ObjectProperties.vue'; export { default as ObjectPublicToggle } from './ObjectPublicToggle.vue'; export { default as ObjectSidebar } from './ObjectSidebar.vue'; export { default as ObjectTable } from './ObjectTable.vue'; +export { default as ObjectTableDeleted } from './ObjectTableDeleted.vue'; export { default as ObjectTag } from './ObjectTag.vue'; export { default as ObjectUpload } from './ObjectUpload.vue'; export { default as ObjectUploadBasic } from './ObjectUploadBasic.vue'; diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index b25368c2..0dfc215a 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -58,6 +58,13 @@ const routes: Array = [ name: RouteNames.LIST_OBJECTS, component: () => import('@/views/list/ListObjectsView.vue'), meta: { requiresAuth: true, breadcrumb: '__listObjectsDynamic', title: 'My Objects' }, + props: createProps, + }, + { + path: 'deleted', + name: RouteNames.LIST_OBJECTS_DELETED, + component: () => import('@/views/list/ListObjectsDeletedView.vue'), + meta: { requiresAuth: true, breadcrumb: '__listObjectsDeletedDynamic', title: 'My Deleted Objects' }, props: createProps } ] diff --git a/frontend/src/services/objectService.ts b/frontend/src/services/objectService.ts index bd831c00..31f3e14b 100644 --- a/frontend/src/services/objectService.ts +++ b/frontend/src/services/objectService.ts @@ -148,6 +148,22 @@ export default { return comsAxios().head(`${PATH}/${objectId}`); }, + /** + * @function copyObjectVersion + * Copies a previous version of an object and places on top of the version 'stack'. + * If no version is provided to copy, the latest existing version will be copied. + * @param {string} objectId The id for the object to get + * @param {string} versionId An optional versionId + * @returns {Promise} An axios response + */ + copyObjectVersion(objectId: string, versionId: string) { + return comsAxios().put(`${PATH}/${objectId}/version`, undefined, { + params: { + versionId: versionId + } + }); + }, + /** * @function listObjectVersion * Returns the object version history diff --git a/frontend/src/store/objectStore.ts b/frontend/src/store/objectStore.ts index 6c40322a..b059fa7b 100644 --- a/frontend/src/store/objectStore.ts +++ b/frontend/src/store/objectStore.ts @@ -3,7 +3,7 @@ import { computed, ref } from 'vue'; import { useToast } from '@/lib/primevue'; import { objectService } from '@/services'; -import { useAppStore, useAuthStore, usePermissionStore } from '@/store'; +import { useAppStore, useAuthStore, usePermissionStore, useVersionStore } from '@/store'; import { partition } from '@/utils/utils'; import type { AxiosRequestConfig } from 'axios'; @@ -22,6 +22,7 @@ export const useObjectStore = defineStore('object', () => { // Store const appStore = useAppStore(); const permissionStore = usePermissionStore(); + const versionStore = useVersionStore(); const { getUserId } = storeToRefs(useAuthStore()); // State @@ -87,14 +88,30 @@ export const useObjectStore = defineStore('object', () => { } } - async function deleteObject(objectId: string, versionId?: string) { + async function deleteObject(objectId: string, versionId?: string, hard = false) { const bucketId = getters.getObject.value(objectId)?.bucketId; - try { appStore.beginIndeterminateLoading(); - await objectService.deleteObject(objectId, versionId); + + + + if(!versionId) { + if(hard) { + // await versionStore.fetchVersions({ objectId: objectId }); + const versions = await versionStore.getVersionsByObjectId(objectId); + for (const v of versions) { + await objectService.deleteObject(objectId, v.id); + } + } else{ + await objectService.deleteObject(objectId, versionId); + } + toast.success('File deleted'); + } + else{ + await objectService.deleteObject(objectId, versionId); + toast.success('Version deleted'); + } removeSelectedObject(objectId); - toast.success('Object deleted'); } catch (error: any) { toast.error('deleting object.'); throw error; @@ -104,6 +121,28 @@ export const useObjectStore = defineStore('object', () => { } } + async function restoreObject(objectId: string, versionId?: string) { + const bucketId = getters.getObject.value(objectId)?.bucketId; + try { + appStore.beginIndeterminateLoading(); + // if restoring a specific version.. copy and make latest + if(versionId) { + await objectService.copyObjectVersion(objectId, versionId); + } + const versions = await versionStore.getVersionsByObjectId(objectId); + versions + .filter(v => v.deleteMarker) + .forEach(v => objectService.deleteObject(objectId, v.id)); + toast.success('File restored'); + } catch (error: any) { + toast.error('restoring file'); + throw error; + } finally { + fetchObjects({ bucketId: bucketId, userId: getUserId.value, bucketPerms: true }); + appStore.endIndeterminateLoading(); + } + } + async function getObjectUrl(objectId: string, versionId?: string) { try { appStore.beginIndeterminateLoading(); @@ -155,8 +194,8 @@ export const useObjectStore = defineStore('object', () => { // Added to allow deletion of objects before versioning implementation // TODO: Verify if needed after versioning implemented - deleteMarker: false, - latest: true + // deleteMarker: false, + // latest: true }, headers ) @@ -197,7 +236,7 @@ export const useObjectStore = defineStore('object', () => { // Return full response as data will always be No Content return await objectService.headObject(objectId); } catch (error: any) { - toast.error('Fetching head', error); + // toast.error('Fetching head', error); } finally { appStore.endIndeterminateLoading(); } @@ -284,6 +323,7 @@ export const useObjectStore = defineStore('object', () => { // Actions createObject, deleteObject, + restoreObject, getObjectUrl, fetchObjects, headObject, diff --git a/frontend/src/store/versionStore.ts b/frontend/src/store/versionStore.ts index 0876ed1d..8ce2f0b6 100644 --- a/frontend/src/store/versionStore.ts +++ b/frontend/src/store/versionStore.ts @@ -39,6 +39,9 @@ export const useVersionStore = defineStore('version', () => { getVersionsByObjectId: computed( () => (objectId: string) => state.versions.value.filter((x: Version) => x.objectId === objectId) ), + getIsDeleted: computed(() => (objectId: string) => state.versions.value.find((x: Version) => + ((x.objectId === objectId) && (x.isLatest) && (x.deleteMarker))) ? true : false + ), getLatestVersionIdByObjectId: computed( () => (objectId: string) => state.versions.value.find((x: Version) => x.objectId === objectId && x.isLatest)?.id ), @@ -94,7 +97,8 @@ export const useVersionStore = defineStore('version', () => { appStore.beginIndeterminateLoading(); state.versions.value = (await objectService.listObjectVersion(params.objectId)).data; } catch (error: any) { - toast.error('Fetching versions', error); + // toast.error('Fetching versions', error); + state.versions.value = []; } finally { appStore.endIndeterminateLoading(); } diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 8d1ab0ad..b2668021 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -46,6 +46,7 @@ export const RouteNames = Object.freeze({ INVITE: 'invite', LIST_BUCKETS: 'listBuckets', LIST_OBJECTS: 'listObjects', + LIST_OBJECTS_DELETED: 'listObjectsDeleted', LOGIN: 'login', LOGOUT: 'logout' }); diff --git a/frontend/src/views/detail/DetailObjectsView.vue b/frontend/src/views/detail/DetailObjectsView.vue index 9ef1560d..4db40ed6 100644 --- a/frontend/src/views/detail/DetailObjectsView.vue +++ b/frontend/src/views/detail/DetailObjectsView.vue @@ -49,7 +49,7 @@ onBeforeMount(async () => { :version-id="versionId" />
-

No object or version provided

+

File not found

diff --git a/frontend/src/views/list/ListObjectsDeletedView.vue b/frontend/src/views/list/ListObjectsDeletedView.vue new file mode 100644 index 00000000..4c32eed4 --- /dev/null +++ b/frontend/src/views/list/ListObjectsDeletedView.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/frontend/tests/unit/store/objectStore.spec.ts b/frontend/tests/unit/store/objectStore.spec.ts index 25351022..5109d69a 100644 --- a/frontend/tests/unit/store/objectStore.spec.ts +++ b/frontend/tests/unit/store/objectStore.spec.ts @@ -189,8 +189,7 @@ describe('Object Store', () => { { bucketId: ['000'], objectId: ['000'], - deleteMarker: false, - latest: true + tagset: undefined }, {} ); @@ -216,8 +215,7 @@ describe('Object Store', () => { { bucketId: ['000'], objectId: ['000'], - deleteMarker: false, - latest: true + tagset: undefined }, {} ); diff --git a/frontend/tests/unit/store/versionStore.spec.ts b/frontend/tests/unit/store/versionStore.spec.ts index 7c7668fc..1004b8b2 100644 --- a/frontend/tests/unit/store/versionStore.spec.ts +++ b/frontend/tests/unit/store/versionStore.spec.ts @@ -163,8 +163,7 @@ describe('Version Store', () => { expect(beginIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); expect(getVersionsSpy).toHaveBeenCalledTimes(1); expect(getVersionsSpy).toHaveBeenCalledWith('000'); - expect(mockToast).toHaveBeenCalledTimes(1); - expect(mockToast).toHaveBeenCalledWith('Fetching versions', new Error()); + expect(mockToast).toHaveBeenCalledTimes(0); expect(endIndeterminateLoadingSpy).toHaveBeenCalledTimes(1); expect(versionStore.getTagging).toStrictEqual([]); }); From 2f391537958d1cc16ad0064eb17b4fb61361e3e1 Mon Sep 17 00:00:00 2001 From: Csaky Date: Thu, 16 Jan 2025 17:25:35 -0800 Subject: [PATCH 2/2] global recycle bin --- frontend/src/assets/main.scss | 2 +- frontend/src/components/layout/Navbar.vue | 11 +++ .../components/object/DeleteObjectButton.vue | 2 +- .../components/object/ObjectFileDetails.vue | 18 +++-- frontend/src/components/object/ObjectList.vue | 5 +- .../components/object/ObjectListDeleted.vue | 31 ++++---- .../components/object/ObjectTableDeleted.vue | 76 ++++++++++--------- .../src/components/object/ObjectVersion.vue | 5 +- .../components/object/RestoreObjectButton.vue | 6 +- frontend/src/store/objectStore.ts | 5 -- frontend/src/store/versionStore.ts | 8 ++ frontend/src/types/Version.ts | 1 + .../src/views/detail/DetailObjectsView.vue | 4 +- .../views/list/ListObjectsDeletedView-old.vue | 74 ++++++++++++++++++ .../src/views/list/ListObjectsDeletedView.vue | 56 +------------- 15 files changed, 175 insertions(+), 129 deletions(-) create mode 100644 frontend/src/views/list/ListObjectsDeletedView-old.vue diff --git a/frontend/src/assets/main.scss b/frontend/src/assets/main.scss index 797ef5bb..235fa359 100644 --- a/frontend/src/assets/main.scss +++ b/frontend/src/assets/main.scss @@ -138,7 +138,7 @@ div:focus-visible { /* footer */ .gov-footer { - background-color: #003366 !important; + background-color: #003366; border-top: 2px solid #fcba19; padding-bottom: 3px; a { diff --git a/frontend/src/components/layout/Navbar.vue b/frontend/src/components/layout/Navbar.vue index 4b410d53..3582dcff 100644 --- a/frontend/src/components/layout/Navbar.vue +++ b/frontend/src/components/layout/Navbar.vue @@ -28,6 +28,17 @@ const { getIsAuthenticated } = storeToRefs(useAuthStore()); My Files +
  • + + Recycle Bin + +
  • { return props.hard ? (props.versionId ? 'Permanently delete version' : (props.ids.length > 1 ? - 'Permanently delete these files' : 'Permanently delete file')) : + 'Permanently delete selected files' : 'Permanently delete file')) : (props.versionId ? 'Delete version' : 'Delete file' ); }); diff --git a/frontend/src/components/object/ObjectFileDetails.vue b/frontend/src/components/object/ObjectFileDetails.vue index 498c2db4..0816553e 100644 --- a/frontend/src/components/object/ObjectFileDetails.vue +++ b/frontend/src/components/object/ObjectFileDetails.vue @@ -51,7 +51,12 @@ const versionStore = useVersionStore(); const { getUserId } = storeToRefs(useAuthStore()); const { getObject } = storeToRefs(objectStore); -const { getIsDeleted, getLatestVersionIdByObjectId, getVersionsByObjectId } = storeToRefs(versionStore); +const { + getIsDeleted, + getLatestVersionIdByObjectId, + getLatestNonDmVersionIdByObjectId, + getVersionsByObjectId +} = storeToRefs(versionStore); // State const object: Ref = ref(undefined); @@ -61,13 +66,14 @@ const permissionsVisible: Ref = ref(false); // version stuff const currentVersionId: Ref = ref(props.versionId); const latestVersionId = computed(() => getLatestVersionIdByObjectId.value(props.objectId)); +const allVersions = computed(() => getVersionsByObjectId.value(props.objectId)); +const latestNonDmVersionId = computed(() => getLatestNonDmVersionIdByObjectId.value(props.objectId)); + const isDeleted: Ref = computed(() => getIsDeleted.value(props.objectId)); async function onVersionsChanged(changedVersionId: string | undefined, isVersion: boolean, hardDelete: boolean) { - // if doing hard delete or no versions left, redirect to object list - const otherVersions = getVersionsByObjectId.value(props.objectId) - .filter(v=>v.id !== changedVersionId); - + // if doing hard delete or no versions left, redirect to parent folder + const otherVersions = allVersions.value.filter(v=>v.id !== changedVersionId); if (hardDelete || (isVersion && otherVersions.length === 0)) { router.push({ path: '/list/objects', query: { bucketId: bucketId.value }}); } @@ -78,7 +84,7 @@ async function onVersionsChanged(changedVersionId: string | undefined, isVersion metadataStore.fetchMetadata({ objectId: props.objectId }), tagStore.fetchTagging({ objectId: props.objectId }) ]).then(async () => { - currentVersionId.value = latestVersionId.value; + currentVersionId.value = latestNonDmVersionId.value; await Promise.all([ versionStore.fetchMetadata({ objectId: props.objectId }), versionStore.fetchTagging({ objectId: props.objectId }) diff --git a/frontend/src/components/object/ObjectList.vue b/frontend/src/components/object/ObjectList.vue index 6a5b3a5c..a79ab9af 100644 --- a/frontend/src/components/object/ObjectList.vue +++ b/frontend/src/components/object/ObjectList.vue @@ -11,7 +11,7 @@ import { } from '@/components/object'; import { Button } from '@/lib/primevue'; import { useAuthStore, useObjectStore, useNavStore, usePermissionStore } from '@/store'; -import { Permissions, RouteNames } from '@/utils/constants'; +import { Permissions } from '@/utils/constants'; import { ButtonMode } from '@/utils/enums'; import { onDialogHide } from '@/utils/utils'; @@ -131,9 +131,6 @@ const onDeletedSuccess = () => { />
  • - - Recycle Bin - diff --git a/frontend/src/components/object/ObjectListDeleted.vue b/frontend/src/components/object/ObjectListDeleted.vue index 08d99dd8..ad601163 100644 --- a/frontend/src/components/object/ObjectListDeleted.vue +++ b/frontend/src/components/object/ObjectListDeleted.vue @@ -6,22 +6,13 @@ import { DeleteObjectButton, ObjectSidebar, ObjectTableDeleted, + RestoreObjectButton } from '@/components/object'; import { useObjectStore } from '@/store'; -import { RouteNames } from '@/utils/constants'; import { ButtonMode } from '@/utils/enums'; import type { Ref } from 'vue'; -// Props -type Props = { - bucketId?: string; -}; - -const props = withDefaults(defineProps(), { - bucketId: undefined -}); - //const navStore = useNavStore(); const objectStore = useObjectStore(); @@ -46,11 +37,22 @@ const closeObjectInfo = () => { const onDeletedSuccess = () => { objectTableKey.value += 1; }; + +const onRestoredSuccess = () => { + objectTableKey.value += 1; +}; diff --git a/frontend/src/components/object/ObjectTableDeleted.vue b/frontend/src/components/object/ObjectTableDeleted.vue index 0def8fd0..1a81249d 100644 --- a/frontend/src/components/object/ObjectTableDeleted.vue +++ b/frontend/src/components/object/ObjectTableDeleted.vue @@ -6,10 +6,11 @@ import { Spinner } from '@/components/layout'; import { DeleteObjectButton, ObjectPermission, + RestoreObjectButton } from '@/components/object'; import { SyncButton } from '@/components/common'; import { Button, Column, DataTable, Dialog, InputText, useToast } from '@/lib/primevue'; -import { useAuthStore, useObjectStore, useNavStore, usePermissionStore } from '@/store'; +import { useAuthStore, useBucketStore, useObjectStore, useNavStore, usePermissionStore } from '@/store'; import { Permissions, RouteNames } from '@/utils/constants'; import { onDialogHide } from '@/utils/utils'; import { ButtonMode } from '@/utils/enums'; @@ -30,23 +31,15 @@ type DataTableObjectSource = { type DataTableFilter = { [key: string]: { value: any; matchMode: string }; }; -// Props -type Props = { - bucketId?: string; - objectInfoId?: string; -}; - -const props = withDefaults(defineProps(), { - bucketId: undefined, - objectInfoId: undefined -}); // Emits const emit = defineEmits(['show-object-info']); // Store +const bucketStore = useBucketStore(); const objectStore = useObjectStore(); const permissionStore = usePermissionStore(); +const { getBuckets } = storeToRefs(useBucketStore()); const { getUserId } = storeToRefs(useAuthStore()); const { focusedElement } = storeToRefs(useNavStore()); @@ -67,7 +60,11 @@ const filters: Ref = ref({ // Actions const toast = useToast(); const onDeletedSuccess = () => { - toast.success('File deleted'); + toast.success('File permanently deleted'); + loadLazyData(); +}; +const onRestoredSuccess = () => { + toast.success('File restored'); loadLazyData(); }; @@ -85,8 +82,10 @@ async function showPermissions(objectId: string) { focusedElement.value = document.activeElement; } -onMounted(() => { +onMounted( async () => { loading.value = true; + await bucketStore.fetchBuckets({ userId: getUserId.value, objectPerms: true }); + lazyParams.value = { first: 0, rows: lazyDataTable.value.rows, @@ -103,7 +102,6 @@ const loadLazyData = (event?: any) => { objectService .searchObjects( { - bucketId: props.bucketId ? [props.bucketId] : undefined, deleteMarker: true, latest: true, page: lazyParams.value?.page ? ++lazyParams.value.page : 1, @@ -114,10 +112,16 @@ const loadLazyData = (event?: any) => { } ) .then((r: any) => { - tableData.value = r.data.map((item: any) => ({ - ...item, - updatedAt: item.updatedAt === null ? item.createdAt : item.updatedAt - })); + // add full object url to table data + tableData.value = r.data.map((o: COMSObject) => { + const bucket = getBuckets.value.find((b) => b.bucketId === o.bucketId); + return { + ...o, + location: `${bucket?.endpoint}/${bucket?.bucket}/${o.path}`, + updatedAt: o.updatedAt === null ? o.createdAt : o.updatedAt + }; + }); + totalRecords.value = +r?.headers['x-total-rows']; // add objects to store objectStore.setObjects(r.data); @@ -221,7 +225,7 @@ onUnmounted(() => { v-if="!loading" class="flex justify-content-center" > -

    There are no deleted files in this folder

    +

    Nothing to see here

    - +