fix(nextcloud-files): WR-07 Merkzettel angelegter Ordner gilt nur fuer eine Ablage
- der Merkzettel wird geleert, wenn neu abgelegt wird und nichts mehr laeuft, nach jedem Loeschen, Verschieben und Umbenennen in der Ansicht und wenn ein Hochladen mit pathConflict/notFound scheitert (dann legt "Erneut versuchen" den Ordner wieder an) - innerhalb einer Ablage wird jeder Ordner weiter nur einmal angelegt - Hook-Tests fuer alle drei Wege Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -475,6 +475,7 @@ export function FileBrowser({
|
||||
|
||||
const confirmDelete = async (list: NcEntry[]) => {
|
||||
const { failed, reason } = await runEach(list, (e) => deleteEntry(e.path));
|
||||
transfers.forgetFolders();
|
||||
setDialog(null);
|
||||
dispatch({ type: 'clear' });
|
||||
await load(pathRef.current, { quiet: true });
|
||||
@@ -486,6 +487,7 @@ export function FileBrowser({
|
||||
};
|
||||
|
||||
const confirmMove = async (list: NcEntry[], targetDir: string) => {
|
||||
transfers.forgetFolders();
|
||||
if (list.length === 1) {
|
||||
// Ein einzelner Eintrag: Fehler bleiben im Dialog sichtbar.
|
||||
await moveEntry(list[0].path, join(targetDir, list[0].name));
|
||||
@@ -515,6 +517,7 @@ export function FileBrowser({
|
||||
|
||||
const submitRename = async (entry: NcEntry, name: string) => {
|
||||
const to = join(parent(entry.path), name);
|
||||
transfers.forgetFolders();
|
||||
await moveEntry(entry.path, to);
|
||||
setDialog(null);
|
||||
focusAfterLoad.current = to;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { NextcloudFilesRequestError } from '@/lib/nextcloud-files-api';
|
||||
import { useTransfers } from './use-transfers';
|
||||
|
||||
const mockCreate = vi.fn();
|
||||
const mockUpload = vi.fn();
|
||||
|
||||
vi.mock('@/lib/nextcloud-files-api', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/nextcloud-files-api')>();
|
||||
return {
|
||||
...actual,
|
||||
createFolder: (...a: unknown[]) => mockCreate(...a),
|
||||
listFolder: vi.fn(),
|
||||
};
|
||||
});
|
||||
vi.mock('@/lib/nextcloud-files-upload', () => ({
|
||||
uploadFile: (...a: unknown[]) => mockUpload(...a),
|
||||
}));
|
||||
|
||||
const file = (name: string) => new File(['x'], name);
|
||||
|
||||
beforeEach(() => {
|
||||
mockCreate.mockReset().mockResolvedValue({ path: '/A' });
|
||||
mockUpload.mockReset().mockResolvedValue({ path: '/A/a.txt', size: 1 });
|
||||
});
|
||||
|
||||
describe('useTransfers — Ordner-Merkzettel (WR-07)', () => {
|
||||
it('eine neue Ablage nach dem Ende der vorigen legt den Ordner erneut an (er kann inzwischen geloescht sein)', async () => {
|
||||
const { result } = renderHook(() => useTransfers({ quotaAvailable: null }));
|
||||
act(() => result.current.enqueue([{ file: file('a.txt'), relativeDir: 'A' }], '/'));
|
||||
await waitFor(() => expect(result.current.transfers[0]?.status).toBe('done'));
|
||||
expect(mockCreate).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreate).toHaveBeenLastCalledWith('/A');
|
||||
|
||||
act(() => result.current.enqueue([{ file: file('b.txt'), relativeDir: 'A' }], '/'));
|
||||
await waitFor(() => expect(result.current.transfers[1]?.status).toBe('done'));
|
||||
expect(mockCreate).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('innerhalb einer Ablage wird jeder Ordner nur einmal angelegt', async () => {
|
||||
const { result } = renderHook(() => useTransfers({ quotaAvailable: null }));
|
||||
act(() =>
|
||||
result.current.enqueue(
|
||||
[
|
||||
{ file: file('a.txt'), relativeDir: 'A' },
|
||||
{ file: file('b.txt'), relativeDir: 'A' },
|
||||
{ file: file('c.txt'), relativeDir: 'A' },
|
||||
],
|
||||
'/',
|
||||
),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(result.current.transfers.every((t) => t.status === 'done')).toBe(true),
|
||||
);
|
||||
expect(mockCreate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('forgetFolders und ein Hochladen mit pathConflict leeren den Merkzettel', async () => {
|
||||
let release!: () => void;
|
||||
const hold = new Promise<void>((r) => {
|
||||
release = r;
|
||||
});
|
||||
mockUpload
|
||||
.mockImplementationOnce(async () => {
|
||||
await hold;
|
||||
return { path: '/A/a.txt', size: 1 };
|
||||
})
|
||||
.mockRejectedValueOnce(new NextcloudFilesRequestError(409, 'pathConflict', 'fehlt'))
|
||||
.mockResolvedValue({ path: '/A/c.txt', size: 1 });
|
||||
const { result } = renderHook(() => useTransfers({ quotaAvailable: null }));
|
||||
// Die erste Datei laeuft noch: die zweite Ablage nutzt den Merkzettel.
|
||||
act(() => result.current.enqueue([{ file: file('a.txt'), relativeDir: 'A' }], '/'));
|
||||
await waitFor(() => expect(mockUpload).toHaveBeenCalledTimes(1));
|
||||
act(() => result.current.enqueue([{ file: file('b.txt'), relativeDir: 'A' }], '/'));
|
||||
await waitFor(() => expect(result.current.transfers[1]?.status).toBe('error'));
|
||||
expect(mockCreate).toHaveBeenCalledTimes(1);
|
||||
// pathConflict: "Erneut versuchen" legt den Ordner wieder an.
|
||||
act(() => result.current.retry(result.current.transfers[1].id));
|
||||
await waitFor(() => expect(result.current.transfers[1]?.status).toBe('done'));
|
||||
expect(mockCreate).toHaveBeenCalledTimes(2);
|
||||
|
||||
act(() => result.current.forgetFolders());
|
||||
act(() => result.current.enqueue([{ file: file('c.txt'), relativeDir: 'A' }], '/'));
|
||||
await waitFor(() => expect(result.current.transfers[2]?.status).toBe('done'));
|
||||
expect(mockCreate).toHaveBeenCalledTimes(3);
|
||||
release();
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,12 @@ import { freeName, join } from './paths';
|
||||
* Dateien gleichzeitig, jede mit eigenem Abbruch. Ein vergebener Name endet nicht
|
||||
* als Fehler, sondern als Rueckfrage (Ersetzen, Beide behalten, Ueberspringen).
|
||||
* Was nicht in den freien Speicher passt, wird gar nicht erst gesendet.
|
||||
*
|
||||
* Angelegte Ordner werden nur fuer eine Ablage gemerkt (WR-07): Der Merkzettel wird
|
||||
* geleert, sobald nichts mehr laeuft und neu abgelegt wird, nach jedem Loeschen,
|
||||
* Verschieben oder Umbenennen in der Ansicht (`forgetFolders`) und wenn ein Hochladen
|
||||
* mit "Zielordner fehlt" scheitert — sonst liefe ein neu abgelegter, inzwischen
|
||||
* geloeschter Ordner gleichen Namens ohne MKCOL ins Leere.
|
||||
*/
|
||||
|
||||
export const MAX_PARALLEL = 2;
|
||||
@@ -174,6 +180,13 @@ export function useTransfers(options: UseTransfersOptions) {
|
||||
patch(id, { status: 'done', loaded: item.size });
|
||||
optsRef.current.onFolderChanged?.(item.targetDir);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof NextcloudFilesRequestError &&
|
||||
(err.code === 'pathConflict' || err.code === 'notFound')
|
||||
) {
|
||||
// Der Zielordner fehlt: beim naechsten Versuch die Ordner wieder anlegen.
|
||||
folders.current.clear();
|
||||
}
|
||||
if (controller.signal.aborted) {
|
||||
// Abgebrochen: die Zeile ist bereits entfernt.
|
||||
} else if (err instanceof NextcloudFilesRequestError && err.code === 'nameTaken') {
|
||||
@@ -217,6 +230,8 @@ export function useTransfers(options: UseTransfersOptions) {
|
||||
|
||||
const enqueue = useCallback(
|
||||
(files: readonly DroppedFile[], baseDir: string, emptyFolders: readonly string[] = []) => {
|
||||
// Neue Ablage, waehrend nichts laeuft: Ordner von frueher koennen inzwischen geloescht sein.
|
||||
if (!listRef.current.some(isActive)) folders.current.clear();
|
||||
for (const folder of emptyFolders) {
|
||||
void ensureFolders(baseDir, folder).catch(() => undefined);
|
||||
}
|
||||
@@ -254,6 +269,11 @@ export function useTransfers(options: UseTransfersOptions) {
|
||||
[ensureFolders],
|
||||
);
|
||||
|
||||
/** Nach Loeschen, Verschieben oder Umbenennen: gemerkte Ordner gelten nicht mehr. */
|
||||
const forgetFolders = useCallback(() => {
|
||||
folders.current.clear();
|
||||
}, []);
|
||||
|
||||
const cancel = useCallback((id: string) => {
|
||||
const it = internals.current.get(id);
|
||||
it?.controller?.abort();
|
||||
@@ -319,5 +339,5 @@ export function useTransfers(options: UseTransfersOptions) {
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { transfers, enqueue, cancel, retry, resolve, clearFinished };
|
||||
return { transfers, enqueue, cancel, retry, resolve, clearFinished, forgetFolders };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user