delete multiple files with selection

This commit is contained in:
2025-09-04 16:26:23 +02:00
parent 49b4162448
commit 8b2ed0e700
17 changed files with 250 additions and 137 deletions

View File

@@ -6,7 +6,6 @@ import {
ContextMenuItem,
ContextMenuSeparator,
} from '@/components/ui/context-menu';
import { deleteWarrenDirectory, deleteWarrenFile } from '~/lib/api/warrens';
import type { DirectoryEntry } from '#shared/types';
const warrenStore = useWarrenStore();
@@ -26,9 +25,9 @@ const {
const emit = defineEmits<{
'entry-click': [entry: DirectoryEntry, event: MouseEvent];
'entry-download': [entry: DirectoryEntry];
'entry-delete': [entry: DirectoryEntry, force: boolean];
}>();
const deleting = ref(false);
const isCopied = computed(
() =>
warrenStore.current != null &&
@@ -39,32 +38,11 @@ const isCopied = computed(
);
const isSelected = computed(() => warrenStore.isSelected(entry));
async function submitDelete(force: boolean = false) {
if (warrenStore.current == null) {
return;
}
deleting.value = true;
if (entry.fileType === 'directory') {
await deleteWarrenDirectory(
warrenStore.current.warrenId,
warrenStore.current.path,
entry.name,
force
);
} else {
await deleteWarrenFile(
warrenStore.current.warrenId,
warrenStore.current.path,
entry.name
);
}
deleting.value = false;
function onDelete(force: boolean = false) {
emit('entry-delete', entry, force);
}
async function openRenameDialog() {
function openRenameDialog() {
renameDialog.openDialog(entry);
}
@@ -183,15 +161,14 @@ function onClearCopy() {
<ContextMenuItem
:class="[warrenStore.current == null && 'hidden']"
@select="() => submitDelete(false)"
@select="() => onDelete(false)"
>
<Icon name="lucide:trash-2" />
Delete
</ContextMenuItem>
<ContextMenuItem
v-if="entry.fileType === 'directory'"
:class="[warrenStore.current == null && 'hidden']"
@select="() => submitDelete(true)"
@select="() => onDelete(true)"
>
<Icon
class="text-destructive-foreground"

View File

@@ -19,6 +19,7 @@ const {
const emit = defineEmits<{
'entry-click': [entry: DirectoryEntry, event: MouseEvent];
'entry-download': [entry: DirectoryEntry];
'entry-delete': [entry: DirectoryEntry, force: boolean];
back: [];
}>();
@@ -35,6 +36,10 @@ function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) {
function onEntryDownload(entry: DirectoryEntry) {
emit('entry-download', entry);
}
function onEntryDelete(entry: DirectoryEntry, force: boolean) {
emit('entry-delete', entry, force);
}
</script>
<template>
@@ -61,6 +66,7 @@ function onEntryDownload(entry: DirectoryEntry) {
:draggable="entriesDraggable"
@entry-click="onEntryClicked"
@entry-download="onEntryDownload"
@entry-delete="onEntryDelete"
/>
</div>
</ScrollArea>

View File

@@ -87,6 +87,40 @@ export async function createDirectory(
return { success: true };
}
export async function warrenRm(
warrenId: string,
paths: string[],
force: boolean
): Promise<{ success: boolean }> {
const { status } = await useFetch(getApiUrl(`warrens/files/rm`), {
method: 'POST',
headers: getApiHeaders(),
body: JSON.stringify({
warrenId,
paths,
force,
}),
});
const TOAST_TITLE = 'Delete';
if (status.value !== 'success') {
toast.error(TOAST_TITLE, {
id: 'WARREN_RM_TOAST',
description: `Failed to delete directory`,
});
return { success: false };
}
await refreshNuxtData('current-directory');
toast.success(TOAST_TITLE, {
id: 'WARREN_RM_TOAST',
description: `Successfully deleted files`,
});
return { success: true };
}
export async function deleteWarrenDirectory(
warrenId: string,
path: string,
@@ -104,7 +138,7 @@ export async function deleteWarrenDirectory(
headers: getApiHeaders(),
body: JSON.stringify({
warrenId,
path,
paths: [path],
force,
}),
});
@@ -113,7 +147,7 @@ export async function deleteWarrenDirectory(
if (status.value !== 'success') {
toast.error(TOAST_TITLE, {
id: 'DELETE_DIRECTORY_TOAST',
id: 'WARREN_RM_TOAST',
description: `Failed to delete directory`,
});
return { success: false };
@@ -122,7 +156,7 @@ export async function deleteWarrenDirectory(
await refreshNuxtData('current-directory');
toast.success(TOAST_TITLE, {
id: 'DELETE_DIRECTORY_TOAST',
id: 'WARREN_RM_TOAST',
description: `Successfully deleted ${directoryName}`,
});
return { success: true };
@@ -144,7 +178,7 @@ export async function deleteWarrenFile(
headers: getApiHeaders(),
body: JSON.stringify({
warrenId,
path,
paths: [path],
force: false,
}),
});
@@ -153,7 +187,7 @@ export async function deleteWarrenFile(
if (status.value !== 'success') {
toast.error(TOAST_TITLE, {
id: 'DELETE_FILE_TOAST',
id: 'WARREN_RM_TOAST',
description: `Failed to delete file`,
});
return { success: false };
@@ -162,7 +196,7 @@ export async function deleteWarrenFile(
await refreshNuxtData('current-directory');
toast.success(TOAST_TITLE, {
id: 'DELETE_FILE_TOAST',
id: 'WARREN_RM_TOAST',
description: `Successfully deleted ${fileName}`,
});
return { success: true };

View File

@@ -3,7 +3,13 @@ import { useDropZone } from '@vueuse/core';
import { toast } from 'vue-sonner';
import DirectoryListContextMenu from '~/components/DirectoryListContextMenu.vue';
import RenameEntryDialog from '~/components/actions/RenameEntryDialog.vue';
import { fetchFile, getWarrenDirectory } from '~/lib/api/warrens';
import {
deleteWarrenDirectory,
deleteWarrenFile,
fetchFile,
getWarrenDirectory,
warrenRm,
} from '~/lib/api/warrens';
import type { DirectoryEntry } from '~/shared/types';
definePageMeta({
@@ -119,27 +125,39 @@ function onEntryDownload(entry: DirectoryEntry) {
let downloadName: string;
let downloadApiUrl: string;
const selectionSize = warrenStore.selection.size;
if (selectionSize === 0 || !warrenStore.isSelected(entry)) {
const targets = getTargetsFromSelection(entry, warrenStore.selection);
if (targets.length === 1) {
downloadName =
entry.fileType === 'directory' ? `${entry.name}.zip` : entry.name;
entry.fileType === 'directory'
? `${targets[0].name}.zip`
: targets[0].name;
downloadApiUrl = getApiUrl(
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&paths=${joinPaths(warrenStore.current.path, entry.name)}`
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&paths=${joinPaths(warrenStore.current.path, targets[0].name)}`
);
} else {
downloadName = 'download.zip';
const paths = Array.from(warrenStore.selection).map((entry) =>
joinPaths(warrenStore.current!.path, entry.name)
);
const paths = targets
.map((entry) => joinPaths(warrenStore.current!.path, entry.name))
.join(':');
downloadApiUrl = getApiUrl(
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&paths=${paths.join(':')}`
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&paths=${paths}`
);
}
downloadFile(downloadName, downloadApiUrl);
}
async function onEntryDelete(entry: DirectoryEntry, force: boolean) {
if (warrenStore.current == null) {
return;
}
const targets = getTargetsFromSelection(entry, warrenStore.selection).map(
(entry) => joinPaths(warrenStore.current!.path, entry.name)
);
await warrenRm(warrenStore.current.warrenId, targets, force);
}
function onBack() {
warrenStore.backCurrentPath();
@@ -162,6 +180,7 @@ function onBack() {
:parent="warrenStore.current.dir.parent"
@entry-click="onEntryClicked"
@entry-download="onEntryDownload"
@entry-delete="onEntryDelete"
@back="onBack"
/>
</DirectoryListContextMenu>

View File

@@ -0,0 +1,17 @@
import type { DirectoryEntry } from '~/shared/types';
/** Converts a selection and the entry that triggered an action into the target entries
* @param targetEntry - The entry that triggered an action
* @param selection - The selected entries
* @returns If there are no selected elements or the target was not included only the target is returned. Otherwise the selection is returned
*/
export function getTargetsFromSelection(
targetEntry: DirectoryEntry,
selection: Set<DirectoryEntry>
): DirectoryEntry[] {
if (selection.size === 0 || !selection.has(targetEntry)) {
return [targetEntry];
}
return Array.from(selection);
}