Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Soft-delete/restore Feature #226

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion 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 All @@ -134,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 {
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
11 changes: 11 additions & 0 deletions frontend/src/components/layout/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ const { getIsAuthenticated } = storeToRefs(useAuthStore());
My Files
</router-link>
</li>
<li
v-if="getIsAuthenticated"
class="mr-2"
>
<router-link
:to="{ name: RouteNames.LIST_OBJECTS_DELETED }"
aria-label="Recycle Bin"
>
Recycle Bin
</router-link>
</li>
<li class="mr-2">
<a
target="_blank"
Expand Down
51 changes: 39 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 @@ -36,20 +38,38 @@ const confirm = useConfirm();
const confirmDelete = () => {
focusedElement.value = document.activeElement;
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 +78,13 @@ const confirmDelete = () => {
displayNoFileDialog.value = true;
}
};
const buttonLabel = computed(() => {
return props.hard ?
(props.versionId ?
'Permanently delete version' : (props.ids.length > 1 ?
'Permanently delete selected files' : 'Permanently delete file')) :
(props.versionId ? 'Delete version' : 'Delete file' );
});
</script>

<template>
Expand All @@ -78,20 +105,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
62 changes: 41 additions & 21 deletions frontend/src/components/object/ObjectFileDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,29 +51,46 @@ const versionStore = useVersionStore();

const { getUserId } = storeToRefs(useAuthStore());
const { getObject } = storeToRefs(objectStore);
const { getVersionsByObjectId, getLatestVersionIdByObjectId } = storeToRefs(versionStore);
const {
getIsDeleted,
getLatestVersionIdByObjectId,
getLatestNonDmVersionIdByObjectId,
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 allVersions = computed(() => getVersionsByObjectId.value(props.objectId));
const latestNonDmVersionId = computed(() => getLatestNonDmVersionIdByObjectId.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;
const isDeleted: Ref<boolean> = 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 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 }});
}
// 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 = latestNonDmVersionId.value;
await Promise.all([
versionStore.fetchMetadata({ objectId: props.objectId }),
versionStore.fetchTagging({ objectId: props.objectId })
]);
});
}
}

onMounted(async () => {
Expand All @@ -84,7 +101,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 +138,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 +164,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 +180,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 +200,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
3 changes: 3 additions & 0 deletions frontend/src/components/object/ObjectList.vue
Original file line number Diff line number Diff line change
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 Down
94 changes: 94 additions & 0 deletions frontend/src/components/object/ObjectListDeleted.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { computed, ref } from 'vue';

import {
DeleteObjectButton,
ObjectSidebar,
ObjectTableDeleted,
RestoreObjectButton
} from '@/components/object';
import { useObjectStore } from '@/store';
import { ButtonMode } from '@/utils/enums';

import type { Ref } from 'vue';

//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;
};

const onRestoredSuccess = () => {
objectTableKey.value += 1;
};
</script>

<template>
<div>
<div class="flex align-items-center justify-content-start">
<RestoreObjectButton
v-if="selectedObjectIds.length > 0"
:ids="selectedObjectIds"
:mode="ButtonMode.BUTTON"
:hard="true"
@on-restored-success="onRestoredSuccess"
/>
<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"
: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>
</div>
</template>

<style scoped>
.disable-overlay {
pointer-events: none;
opacity: 0.4;
}
</style>
Loading
Loading