Skip to content

Commit

Permalink
Soft-delete/restore Feature
Browse files Browse the repository at this point in the history
delete/restore versions from File details page
soft-delete and restore files
Deleted files view for each folder
Uses new COMS copyVersion feature
  • Loading branch information
TimCsaky committed Dec 13, 2024
1 parent d5ba853 commit 89025c0
Show file tree
Hide file tree
Showing 19 changed files with 859 additions and 68 deletions.
7 changes: 7 additions & 0 deletions frontend/src/assets/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -231,6 +235,9 @@ div:focus-visible {
&.selected-row {
background: $bcbox-highlight-background !important;
}
&.deleted-row td:not(.action-buttons) {
opacity: 0.6 !important;
}
}
}

Expand Down
54 changes: 42 additions & 12 deletions frontend/src/components/object/DeleteObjectButton.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { Button, Dialog, useConfirm } from '@/lib/primevue';
Expand All @@ -14,10 +14,12 @@ type Props = {
ids: Array<string>;
mode: ButtonMode;
versionId?: string; // Only use this when deleting a single object
hard?: boolean
};
const props = withDefaults(defineProps<Props>(), {
versionId: undefined
versionId: undefined,
hard: false
});
// Emits
Expand All @@ -35,21 +37,42 @@ const confirm = useConfirm();
const confirmDelete = () => {
focusedElement.value = document.activeElement;
console.log('props.ids', props.ids);
if (props.ids.length) {
const item = props.versionId ? 'version' : 'object';
const item = props.versionId ? 'version' : 'file';
const msgContext = props.ids.length > 1 ? `the selected ${props.ids.length} ${item}s` : `this ${item}`;
const permText = props.hard || props.versionId ? 'permanently ' : '';
confirm.require({
message: `Please confirm that you want to delete ${msgContext}.`,
message: `Please confirm that you want to ${permText}delete ${msgContext}.`,
header: `Delete ${props.ids.length > 1 ? item + 's' : item}`,
acceptLabel: 'Confirm',
rejectLabel: 'Cancel',
accept: () => {
props.ids?.forEach((id: string) => {
// props.ids?.forEach((id: string) => {
// objectStore
// .deleteObject(id, props.versionId, props.hard)
// .then(() => emit('on-deleted-success',
// props.versionId, // version Id or undefined
// props.versionId || false, // true or false
// props.hard)) // if doing hard delete of object
// .catch(() => {});
// });
for (const id of props.ids) {
objectStore
.deleteObject(id, props.versionId)
.then(() => emit('on-deleted-success', props.versionId))
.deleteObject(id, props.versionId, props.hard)
.then(() => emit('on-deleted-success',
props.versionId, // version Id or undefined
props.versionId || false, // true or false
props.hard)) // if doing hard delete of object
.catch(() => {});
});
};
},
onHide: () => onDialogHide(),
reject: () => onDialogHide()
Expand All @@ -58,6 +81,13 @@ const confirmDelete = () => {
displayNoFileDialog.value = true;
}
};
const buttonLabel = computed(() => {
return props.hard ?
(props.versionId ?
'Permanently delete version' : (props.ids.length > 1 ?
'Permanently delete these files' : 'Permanently delete file')) :
(props.versionId ? 'Delete version' : 'Delete file' );
});
</script>

<template>
Expand All @@ -78,20 +108,20 @@ const confirmDelete = () => {

<Button
v-if="props.mode === ButtonMode.ICON"
v-tooltip.bottom="props.versionId ? 'Delete version' : 'Delete file'"
v-tooltip.bottom="buttonLabel"
class="p-button-lg p-button-text p-button-danger"
:disabled="props.disabled"
:aria-label="props.versionId ? 'Delete version' : 'Delete file'"
:aria-label="buttonLabel"
@click="confirmDelete()"
>
<font-awesome-icon icon="fa-solid fa-trash" />
</Button>
<Button
v-else
v-tooltip.bottom="props.versionId ? 'Delete version' : 'Delete file'"
v-tooltip.bottom="buttonLabel"
class="p-button-outlined p-button-danger"
:disabled="props.disabled"
:aria-label="props.versionId ? 'Delete version' : 'Delete file'"
:aria-label="buttonLabel"
@click="confirmDelete()"
>
<font-awesome-icon
Expand Down
56 changes: 35 additions & 21 deletions frontend/src/components/object/ObjectFileDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,29 +51,40 @@ const versionStore = useVersionStore();
const { getUserId } = storeToRefs(useAuthStore());
const { getObject } = storeToRefs(objectStore);
const { getVersionsByObjectId, getLatestVersionIdByObjectId } = storeToRefs(versionStore);
const { getIsDeleted, getLatestVersionIdByObjectId, getVersionsByObjectId } = storeToRefs(versionStore);
// State
const object: Ref<COMSObject | undefined> = ref(undefined);
const bucketId: Ref<string> = ref('');
const permissionsVisible: Ref<boolean> = ref(false);
// version stuff
const currentVersionId: Ref<string | undefined> = ref(props.versionId);
const latestVersionId = computed(() => getLatestVersionIdByObjectId.value(props.objectId));
const permissionsVisible: Ref<boolean> = ref(false);
const isDeleted: Ref<boolean> = computed(() => getIsDeleted.value(props.objectId));
async function onVersionsChanged() {
await Promise.all([
versionStore.fetchVersions({ objectId: props.objectId }),
metadataStore.fetchMetadata({ objectId: props.objectId }),
tagStore.fetchTagging({ objectId: props.objectId })
]).then(async () => {
currentVersionId.value = latestVersionId.value;
async function onVersionsChanged(changedVersionId, isVersion, hardDelete) {
// if doing hard delete or no versions left, redirect to object list
const otherVersions = getVersionsByObjectId.value(props.objectId)
.filter(v=>v.id !== changedVersionId);
if (hardDelete || (isVersion && otherVersions.length === 0)) {
router.push({ path: '/list/objects', query: { bucketId: bucketId.value }});
}
// else stay on page
else {
await Promise.all([
versionStore.fetchMetadata({ objectId: props.objectId }),
versionStore.fetchTagging({ objectId: props.objectId })
]);
});
versionStore.fetchVersions({ objectId: props.objectId }),
metadataStore.fetchMetadata({ objectId: props.objectId }),
tagStore.fetchTagging({ objectId: props.objectId })
]).then(async () => {
currentVersionId.value = latestVersionId.value;
await Promise.all([
versionStore.fetchMetadata({ objectId: props.objectId }),
versionStore.fetchTagging({ objectId: props.objectId })
]);
});
}
}
onMounted(async () => {
Expand All @@ -84,7 +95,7 @@ onMounted(async () => {
object.value = getObject.value(props.objectId);
bucketId.value = object.value ? object.value.bucketId : '';
if (
head?.status !== 204 &&
(head?.status !== 204 && !isDeleted.value) &&
(!object.value ||
!permissionStore.isObjectActionAllowed(object.value.id, getUserId.value, Permissions.READ, object.value.bucketId))
) {
Expand Down Expand Up @@ -121,19 +132,21 @@ onMounted(async () => {

<div class="action-buttons">
<ShareButton
v-if="!isDeleted"
:object-id="props.objectId"
label-text="File"
/>
<DownloadObjectButton
v-if="
object.public || permissionStore.isObjectActionAllowed(object.id, getUserId, Permissions.READ, bucketId)
"
v-if="(object.public ||
permissionStore.isObjectActionAllowed(object.id, getUserId, Permissions.READ, bucketId)) &&
!isDeleted"
:mode="ButtonMode.ICON"
:ids="[object.id]"
:version-id="currentVersionId"
/>
<Button
v-if="permissionStore.isObjectActionAllowed(object.id, getUserId, Permissions.MANAGE, bucketId)"
v-if="permissionStore.isObjectActionAllowed(object.id, getUserId, Permissions.MANAGE, bucketId) &&
!isDeleted"
v-tooltip.bottom="'File permissions'"
class="p-button-lg p-button-text"
aria-label="File permissions"
Expand All @@ -145,8 +158,7 @@ onMounted(async () => {
v-if="permissionStore.isObjectActionAllowed(object.id, getUserId, Permissions.DELETE, bucketId)"
:mode="ButtonMode.ICON"
:ids="[object.id]"
:version-id="currentVersionId"
:disabled="getVersionsByObjectId(object.id).length === 1"
:hard="isDeleted"
@on-deleted-success="onVersionsChanged"
/>
</div>
Expand All @@ -162,7 +174,7 @@ onMounted(async () => {
<ObjectAccess :object-id="object.id" />
<ObjectMetadata
v-model:version-id="currentVersionId"
:editable="currentVersionId === latestVersionId"
:editable="!isDeleted && (currentVersionId === latestVersionId)"
:object-id="object.id"
@on-metadata-success="onVersionsChanged"
/>
Expand All @@ -182,10 +194,12 @@ onMounted(async () => {
:bucket-id="bucketId"
:object-id="object.id"
@on-deleted-success="onVersionsChanged"
@on-restored-success="onVersionsChanged"
/>
<ObjectTag
v-model:version-id="currentVersionId"
:object-id="object.id"
:editable="!isDeleted"
/>
</div>
</div>
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/object/ObjectList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@/components/object';
import { Button } from '@/lib/primevue';
import { useAuthStore, useObjectStore, useNavStore, usePermissionStore } from '@/store';
import { Permissions } from '@/utils/constants';
import { Permissions, RouteNames } from '@/utils/constants';
import { ButtonMode } from '@/utils/enums';
import { onDialogHide } from '@/utils/utils';
Expand Down Expand Up @@ -94,14 +94,17 @@ const onDeletedSuccess = () => {
Upload
</Button>
<DownloadObjectButton
v-if="selectedObjectIds.length > 0"
:disabled="displayUpload"
:ids="selectedObjectIds"
:mode="ButtonMode.BUTTON"
/>
<DeleteObjectButton
v-if="selectedObjectIds.length > 0"
:disabled="displayUpload"
:ids="selectedObjectIds"
:mode="ButtonMode.BUTTON"
:hard="false"
@on-deleted-success="onDeletedSuccess"
/>
</div>
Expand All @@ -128,6 +131,9 @@ const onDeletedSuccess = () => {
/>
</div>
</div>
<router-link :to="{ name: RouteNames.LIST_OBJECTS_DELETED, query: { bucketId: props.bucketId } }">
Recycle Bin
</router-link>
</div>
</template>

Expand Down
99 changes: 99 additions & 0 deletions frontend/src/components/object/ObjectListDeleted.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { computed, ref } from 'vue';
import {
DeleteObjectButton,
ObjectSidebar,
ObjectTableDeleted,
} 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<Props>(), {
bucketId: undefined
});
//const navStore = useNavStore();
const objectStore = useObjectStore();
const { getSelectedObjects } = storeToRefs(objectStore);
// State
const objectInfoId: Ref<string | undefined> = ref(undefined);
const objectTableKey = ref(0);
const selectedObjectIds = computed(() => {
return getSelectedObjects.value.map((o) => o.id);
});
const showObjectInfo = async (objectId: string | undefined) => {
objectInfoId.value = objectId;
};
const closeObjectInfo = () => {
objectInfoId.value = undefined;
};
const onDeletedSuccess = () => {
objectTableKey.value += 1;
};
</script>

<template>
<div>
<div class="head-actions">
<DeleteObjectButton
v-if="selectedObjectIds.length > 0"
:ids="selectedObjectIds"
:mode="ButtonMode.BUTTON"
:hard="true"
@on-deleted-success="onDeletedSuccess"
/>
</div>

<div
class="flex mt-4"
:class="{ 'disable-overlay': false }"
>
<div class="flex-grow-1">
<ObjectTableDeleted
:key="objectTableKey"
:bucket-id="props.bucketId"
:object-info-id="objectInfoId"
@show-object-info="showObjectInfo"
/>
</div>
<div
v-if="objectInfoId"
class="flex-shrink-1 w-4"
>
<ObjectSidebar
:object-id="objectInfoId"
@close-object-info="closeObjectInfo"
/>
</div>
</div>
<router-link
class="deleted-files-link"
:to="{ name: RouteNames.LIST_OBJECTS, query: { bucketId: props.bucketId } }"
>
Back to folder
</router-link>
</div>
</template>

<style scoped>
.disable-overlay {
pointer-events: none;
opacity: 0.4;
}
</style>
2 changes: 1 addition & 1 deletion frontend/src/components/object/ObjectProperties.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const { getUser } = storeToRefs(userStore);
const object: Ref<COMSObject> = computed((): any => getObject.value(props.objectId));
const bucket: Ref<Bucket | undefined> = computed(() => getBucket.value(object.value?.bucketId as string));
const createdBy: Ref<string | undefined> = computed(() => getUser.value(object.value.createdBy)?.fullName);
const createdBy: Ref<string | undefined> = computed(() => getUser.value(object.value?.createdBy)?.fullName);
const updatedBy: Ref<string | undefined> = computed(() => getUser.value(object.value?.updatedBy)?.fullName);
onMounted(() => {
Expand Down
Loading

0 comments on commit 89025c0

Please sign in to comment.