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:
parent
28ee6f024c
commit
088496efdf
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user