web/satellite/vuetify-poc: allow for deleting files and folders

This change allows files and folders to be deleted from within the
Vuetify project's file browser.

Resolves #6106

Change-Id: I0d7b0528b08333aeec29917c4ebef6ea966ac1fa
This commit is contained in:
Jeremy Wharton 2023-08-31 17:19:39 -05:00 committed by Storj Robot
parent 28ee6f024c
commit 088496efdf
8 changed files with 250 additions and 61 deletions

View File

@ -363,6 +363,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
Prefix: string;
}): BrowserObject => ({
Key: Prefix.slice(path.length, -1),
path: path,
LastModified: new Date(),
Size: 0,
type: 'folder',
@ -371,6 +372,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
const makeFileRelative = (file) => ({
...file,
Key: file.Key.slice(path.length),
path: path,
type: 'file',
});
@ -804,7 +806,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
function removeFileFromToBeDeleted(file): void {
state.filesToBeDeleted = state.filesToBeDeleted.filter(
singleFile => singleFile.Key !== file.Key,
singleFile => !(singleFile.Key === file.Key && singleFile.path === file.path),
);
}

View File

@ -70,7 +70,7 @@
<v-divider class="my-2" />
<v-list-item density="comfortable" link rounded="lg" base-color="error">
<v-list-item density="comfortable" link rounded="lg" base-color="error" @click="onDeleteClick">
<template #prepend>
<icon-trash bold />
</template>
@ -82,10 +82,24 @@
</v-menu>
</v-btn>
</div>
<v-overlay
v-model="isDeleting"
scrim="surface"
contained
persistent
no-click-animation
class="align-center justify-center browser-table__loader-overlay"
>
<div class="d-flex align-center">
<v-progress-circular size="23" width="2" color="error" indeterminate />
<p class="ml-3 text-subtitle-1 font-weight-medium text-error">Deleting...</p>
</div>
</v-overlay>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ref, computed } from 'vue';
import {
VMenu,
VList,
@ -97,11 +111,13 @@ import {
VIcon,
VBtn,
VTooltip,
VOverlay,
} from 'vuetify/components';
import { BrowserObject, useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
import { useNotify } from '@/utils/hooks';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import IconDownload from '@poc/components/icons/IconDownload.vue';
import IconShare from '@poc/components/icons/IconShare.vue';
@ -109,14 +125,27 @@ import IconPreview from '@poc/components/icons/IconPreview.vue';
import IconTrash from '@poc/components/icons/IconTrash.vue';
const obStore = useObjectBrowserStore();
const bucketsStore = useBucketsStore();
const notify = useNotify();
const props = defineProps<{
file: BrowserObject;
}>();
const emit = defineEmits<{
deleteFolderClick: [];
}>();
const isDownloading = ref<boolean>(false);
const filePath = computed<string>(() => bucketsStore.state.fileComponentPath);
const isDeleting = computed((): boolean => {
return obStore.state.filesToBeDeleted.some(
file => file.Key === props.file.Key && file.path === props.file.path,
);
});
async function onDownloadClick(): Promise<void> {
isDownloading.value = true;
await obStore.download(props.file).catch((err: Error) => {
@ -125,6 +154,20 @@ async function onDownloadClick(): Promise<void> {
});
isDownloading.value = false;
}
async function onDeleteClick(): Promise<void> {
if (props.file.type === 'folder') {
emit('deleteFolderClick');
return;
}
obStore.addFileToBeDeleted(props.file);
await obStore.deleteObject(filePath.value ? filePath.value + '/' : '', props.file).catch((err: Error) => {
err.message = `Error deleting ${props.file.type}. ${err.message}`;
notify.notifyError(err, AnalyticsErrorEventSource.FILE_BROWSER_ENTRY);
});
obStore.removeFileFromToBeDeleted(props.file);
}
</script>
<style scoped lang="scss">

View File

@ -35,39 +35,48 @@
@update:page="onPageChange"
@update:itemsPerPage="onLimitChange"
>
<template #item.name="{ item }: ItemSlotProps">
<v-btn
class="rounded-lg w-100 pl-1 pr-4 justify-start font-weight-bold"
variant="text"
height="40"
color="default"
block
@click="onFileClick(item.raw.browserObject)"
>
<img :src="item.raw.typeInfo.icon" :alt="item.raw.typeInfo.title + 'icon'" class="mr-3">
{{ item.raw.browserObject.Key }}
</v-btn>
</template>
<template #item="{ props: rowProps }">
<v-data-table-row class="pos-relative" v-bind="rowProps">
<template #item.name="{ item }: ItemSlotProps">
<v-btn
class="rounded-lg w-100 pl-1 pr-4 justify-start font-weight-bold"
variant="text"
height="40"
color="default"
block
@click="onFileClick(item.raw.browserObject)"
>
<img :src="item.raw.typeInfo.icon" :alt="item.raw.typeInfo.title + 'icon'" class="mr-3">
{{ item.raw.browserObject.Key }}
</v-btn>
</template>
<template #item.type="{ item }: ItemSlotProps">
{{ item.raw.typeInfo.title }}
</template>
<template #item.type="{ item }: ItemSlotProps">
{{ item.raw.typeInfo.title }}
</template>
<template #item.size="{ item }: ItemSlotProps">
{{ getFormattedSize(item.raw.browserObject) }}
</template>
<template #item.size="{ item }: ItemSlotProps">
{{ getFormattedSize(item.raw.browserObject) }}
</template>
<template #item.date="{ item }: ItemSlotProps">
{{ getFormattedDate(item.raw.browserObject) }}
</template>
<template #item.date="{ item }: ItemSlotProps">
{{ getFormattedDate(item.raw.browserObject) }}
</template>
<template #item.actions="{ item }: ItemSlotProps">
<browser-row-actions :file="item.raw.browserObject" />
<template #item.actions="{ item }: ItemSlotProps">
<browser-row-actions
:file="item.raw.browserObject"
@delete-folder-click="() => onDeleteFolderClick(item.raw.browserObject)"
/>
</template>
</v-data-table-row>
</template>
</v-data-table-server>
<file-preview-dialog v-model="previewDialog" />
</v-card>
<delete-folder-dialog v-if="folderToDelete" v-model="isDeleteFolderDialogShown" :folder="folderToDelete" />
</template>
<script setup lang="ts">
@ -78,7 +87,7 @@ import {
VTextField,
VBtn,
} from 'vuetify/components';
import { VDataTableServer } from 'vuetify/labs/components';
import { VDataTableServer, VDataTableRow } from 'vuetify/labs/components';
import {
BrowserObject,
@ -97,6 +106,7 @@ import { tableSizeOptions } from '@/types/common';
import BrowserRowActions from '@poc/components/BrowserRowActions.vue';
import FilePreviewDialog from '@poc/components/dialogs/FilePreviewDialog.vue';
import DeleteFolderDialog from '@poc/components/dialogs/DeleteFolderDialog.vue';
import folderIcon from '@poc/assets/icon-folder-tonal.svg';
import pdfIcon from '@poc/assets/icon-pdf-tonal.svg';
@ -154,6 +164,8 @@ const search = ref<string>('');
const selected = ref([]);
const previewDialog = ref<boolean>(false);
const options = ref<TableOptions>();
const folderToDelete = ref<BrowserObject>();
const isDeleteFolderDialogShown = ref<boolean>(false);
const sortBy = [{ key: 'name', order: 'asc' }];
const headers = [
@ -187,7 +199,7 @@ const fileInfo: BrowserObjectTypeInfo = { title: 'File', icon: fileIcon };
const bucketName = computed<string>(() => bucketsStore.state.fileComponentBucketName);
/**
* Returns the name of the current path within the selected bucket.
* Returns the current path within the selected bucket.
*/
const filePath = computed<string>(() => bucketsStore.state.fileComponentPath);
@ -385,6 +397,21 @@ async function fetchFiles(): Promise<void> {
isFetching.value = false;
}
/**
* Handles delete button click event for folder rows.
*/
function onDeleteFolderClick(folder: BrowserObject): void {
folderToDelete.value = folder;
isDeleteFolderDialogShown.value = true;
}
watch(filePath, fetchFiles, { immediate: true });
watch(() => props.forceEmpty, v => !v && fetchFiles());
</script>
<style scoped lang="scss">
.browser-table__loader-overlay :deep(.v-overlay__scrim) {
opacity: 1;
bottom: 0.8px;
}
</style>

View File

@ -10,7 +10,7 @@
:persistent="isCreating"
>
<v-card ref="innerContent" rounded="xlg">
<v-card-item class="pl-7 py-4 create-access-dialog__header">
<v-card-item class="pl-7 py-4 pos-relative">
<template #prepend>
<img class="d-block" :src="STEP_ICON_AND_TITLE[step].icon">
</template>
@ -582,20 +582,13 @@ watch(innerContent, async (comp: Component): Promise<void> => {
</script>
<style scoped lang="scss">
.create-access-dialog {
.create-access-dialog__window {
transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1);
&__header {
position: relative;
}
&__window {
transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1);
&--loading {
opacity: 0.3;
transition: opacity 0s;
pointer-events: none;
}
&--loading {
opacity: 0.3;
transition: opacity 0s;
pointer-events: none;
}
}
</style>

View File

@ -0,0 +1,123 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-dialog
v-model="model"
width="410px"
transition="fade-transition"
:persistent="isLoading"
>
<v-card rounded="xlg">
<v-card-item class="pl-7 py-4">
<template #prepend>
<v-sheet
class="bg-on-surface-variant d-flex justify-center align-center"
width="40"
height="40"
rounded="lg"
>
<icon-trash />
</v-sheet>
</template>
<v-card-title class="font-weight-bold">Delete Folder</v-card-title>
<template #append>
<v-btn
icon="$close"
variant="text"
size="small"
color="default"
:disabled="isLoading"
@click="model = false"
/>
</template>
</v-card-item>
<v-divider />
<div class="pa-7">
The following folder and all of its data will be deleted. This action cannot be undone.
<br><br>
<span class="font-weight-bold">{{ folder.Key }}</span>
</div>
<v-divider />
<v-card-actions class="pa-7">
<v-row>
<v-col>
<v-btn variant="outlined" color="default" block :disabled="isLoading" @click="model = false">
Cancel
</v-btn>
</v-col>
<v-col>
<v-btn color="error" variant="flat" block :loading="isLoading" @click="onDeleteClick">
Delete
</v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import {
VDialog,
VCard,
VCardItem,
VSheet,
VCardTitle,
VDivider,
VCardActions,
VRow,
VCol,
VBtn,
} from 'vuetify/components';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useLoading } from '@/composables/useLoading';
import { useNotify } from '@/utils/hooks';
import { BrowserObject, useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
import IconTrash from '@poc/components/icons/IconTrash.vue';
const props = defineProps<{
modelValue: boolean,
folder: BrowserObject,
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean],
}>();
const model = computed<boolean>({
get: () => props.modelValue,
set: value => emit('update:modelValue', value),
});
const obStore = useObjectBrowserStore();
const bucketsStore = useBucketsStore();
const { isLoading, withLoading } = useLoading();
const notify = useNotify();
const filePath = computed<string>(() => bucketsStore.state.fileComponentPath);
async function onDeleteClick(): Promise<void> {
await withLoading(async () => {
try {
await obStore.deleteFolder(props.folder, filePath.value ? filePath.value + '/' : '');
} catch (error) {
error.message = `Error deleting folder. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.FILE_BROWSER_ENTRY);
return;
}
notify.success('Folder deleted.');
model.value = false;
});
}
</script>

View File

@ -9,7 +9,7 @@
:persistent="isLoading"
>
<v-card ref="innerContent" rounded="xlg">
<v-card-item class="pl-7 py-4 share-dialog__header">
<v-card-item class="pl-7 py-4 pos-relative">
<template #prepend>
<v-sheet
class="bg-on-surface-variant d-flex justify-center align-center"
@ -194,24 +194,17 @@ watch(() => innerContent.value, async (comp: Component | null): Promise<void> =>
</script>
<style scoped lang="scss">
.share-dialog {
.share-dialog__content {
transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1);
&__header {
position: relative;
&--loading {
opacity: 0.3;
transition: opacity 0s;
pointer-events: none;
}
&__content {
transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1);
&--loading {
opacity: 0.3;
transition: opacity 0s;
pointer-events: none;
}
&__icon {
margin-right: 5.5px;
}
&__icon {
margin-right: 5.5px;
}
}
</style>

View File

@ -3,6 +3,6 @@
<template>
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.27197 13.699L9.24807 13.676L5.01126 9.43921C4.68952 9.11747 4.68952 8.59583 5.01126 8.27409C5.32516 7.9602 5.82932 7.95254 6.15249 8.25112L6.17638 8.27409L9.0343 11.1321V1.82387C9.0343 1.36886 9.40315 1 9.85816 1C10.3021 1 10.664 1.35108 10.6814 1.79073L10.682 1.82387V11.0772L13.4849 8.27409C13.7988 7.9602 14.3029 7.95254 14.6261 8.25112L14.65 8.27409C14.9639 8.58798 14.9716 9.09215 14.673 9.41532L14.65 9.43921L10.4132 13.676C10.0993 13.9899 9.59514 13.9976 9.27197 13.699ZM19 17.3777C19 17.8327 18.6311 18.2016 18.1761 18.2016H1.8486C1.39359 18.2016 1.02473 17.8327 1.02473 17.3777C1.02473 16.9227 1.39359 16.5538 1.8486 16.5538H18.1761C18.6311 16.5538 19 16.9227 19 17.3777ZM1.82387 18.139C1.36886 18.139 1 17.7702 1 17.3152V14.0197C1 13.5647 1.36886 13.1958 1.82387 13.1958C2.27888 13.1958 2.64773 13.5647 2.64773 14.0197V17.3152C2.64773 17.7702 2.27888 18.139 1.82387 18.139ZM18.1514 18.139C17.6964 18.139 17.3275 17.7702 17.3275 17.3152V14.0197C17.3275 13.5647 17.6964 13.1958 18.1514 13.1958C18.6064 13.1958 18.9753 13.5647 18.9753 14.0197V17.3152C18.9753 17.7702 18.6064 18.139 18.1514 18.139Z" fill="black" />
<path d="M9.27197 13.699L9.24807 13.676L5.01126 9.43921C4.68952 9.11747 4.68952 8.59583 5.01126 8.27409C5.32516 7.9602 5.82932 7.95254 6.15249 8.25112L6.17638 8.27409L9.0343 11.1321V1.82387C9.0343 1.36886 9.40315 1 9.85816 1C10.3021 1 10.664 1.35108 10.6814 1.79073L10.682 1.82387V11.0772L13.4849 8.27409C13.7988 7.9602 14.3029 7.95254 14.6261 8.25112L14.65 8.27409C14.9639 8.58798 14.9716 9.09215 14.673 9.41532L14.65 9.43921L10.4132 13.676C10.0993 13.9899 9.59514 13.9976 9.27197 13.699ZM19 17.3777C19 17.8327 18.6311 18.2016 18.1761 18.2016H1.8486C1.39359 18.2016 1.02473 17.8327 1.02473 17.3777C1.02473 16.9227 1.39359 16.5538 1.8486 16.5538H18.1761C18.6311 16.5538 19 16.9227 19 17.3777ZM1.82387 18.139C1.36886 18.139 1 17.7702 1 17.3152V14.0197C1 13.5647 1.36886 13.1958 1.82387 13.1958C2.27888 13.1958 2.64773 13.5647 2.64773 14.0197V17.3152C2.64773 17.7702 2.27888 18.139 1.82387 18.139ZM18.1514 18.139C17.6964 18.139 17.3275 17.7702 17.3275 17.3152V14.0197C17.3275 13.5647 17.6964 13.1958 18.1514 13.1958C18.6064 13.1958 18.9753 13.5647 18.9753 14.0197V17.3152C18.9753 17.7702 18.6064 18.139 18.1514 18.139Z" fill="currentColor" />
</svg>
</template>

View File

@ -166,10 +166,13 @@ html {
// Overlay behind modals/dialogs opacity and color
.v-overlay__scrim {
background: #000;
opacity: 0.75;
}
.v-theme--dark .v-overlay__scrim {
--v-theme-on-surface: 0,0,0;
}
// Align the checkboxes in the tables
.v-selection-control {
contain: inherit;
@ -315,3 +318,8 @@ table {
background-color: rgb(var(--v-theme-on-surface-variant)) !important;
color: rgb(var(--v-theme-surface-variant)) !important;
}
// Positions
.pos-relative {
position: relative !important;
}