diff --git a/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.test.tsx b/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.test.tsx index 1e4ec17..70d13c5 100644 --- a/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.test.tsx +++ b/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.test.tsx @@ -1,6 +1,7 @@ import { act, cleanup, + createEvent, fireEvent, render as rtlRender, screen, @@ -549,6 +550,35 @@ describe('FileBrowser — Ziehen und Ablegen', () => { expect(screen.queryByTestId('nc-files-drop-overlay')).toBeNull(); }); + it('Ablegen bei offenem Dialog wird ignoriert, ohne dass der Browser die Datei oeffnet (IN-07)', async () => { + await mount(); + fireEvent.click(screen.getByRole('button', { name: 'Neuer Ordner' })); + const input = await screen.findByLabelText('Name des Ordners'); + const dt = files([new File(['x'], 'neu.txt')]); + fireEvent.dragEnter(input, { dataTransfer: dt }); + expect(screen.queryByTestId('nc-files-drop-overlay')).toBeNull(); + const over = createEvent.dragOver(input, { dataTransfer: dt }); + fireEvent(input, over); + expect(over.defaultPrevented).toBe(true); + const drop = createEvent.drop(input, { dataTransfer: dt }); + fireEvent(input, drop); + expect(drop.defaultPrevented).toBe(true); + await act(async () => {}); + expect(mockUpload).not.toHaveBeenCalled(); + expect(screen.queryByTestId('nc-files-drop-overlay')).toBeNull(); + }); + + it('ein schon behandeltes Ablegen (defaultPrevented) wird nicht hochgeladen', async () => { + await mount(); + const dt = files([new File(['x'], 'neu.txt')]); + const row = screen.getByRole('row', { name: 'Projekte' }); + const drop = createEvent.drop(row, { dataTransfer: dt }); + drop.preventDefault(); + fireEvent(row, drop); + await act(async () => {}); + expect(mockUpload).not.toHaveBeenCalled(); + }); + it('ein Ziehen ohne Dateien (markierter Text) wird ignoriert', async () => { await mount(); const dt = { types: ['text/plain'], files: [], items: [] }; diff --git a/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.tsx b/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.tsx index 960874a..4932460 100644 --- a/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.tsx +++ b/apps/web/src/app/(portal)/modules/nextcloud-files/components/FileBrowser.tsx @@ -417,9 +417,18 @@ export function FileBrowser({ return () => window.removeEventListener('beforeunload', warn); }, [anyActive]); - // Ziehen und Ablegen: am Fenster, mit Tiefenzaehler; nur Ziehen mit Dateien zaehlt. + // Ziehen und Ablegen: am Fenster, mit Tiefenzaehler; nur Ziehen mit Dateien zaehlt. Ist ein + // Dialog oder Menue offen (oder liegt das Ziel darin), wird nichts abgelegt (IN-07): der + // Browser darf die Datei dann aber auch nicht selbst oeffnen. + const dialogOpenRef = useRef(false); + dialogOpenRef.current = dialog !== null || menu !== null; useEffect(() => { let depth = 0; + const blocked = (e: DragEvent): boolean => { + if (dialogOpenRef.current) return true; + const el = e.target instanceof Element ? e.target : null; + return !!el?.closest('[role="dialog"], [role="alertdialog"], [role="menu"]'); + }; const targetOf = (e: DragEvent): string => { const el = e.target instanceof Element ? e.target.closest('[data-drop-folder]') : null; return el?.getAttribute('data-drop-folder') ?? pathRef.current; @@ -427,11 +436,16 @@ export function FileBrowser({ const onEnter = (e: DragEvent) => { if (!hasFiles(e.dataTransfer)) return; depth += 1; - setDropTarget(targetOf(e)); + setDropTarget(blocked(e) ? null : targetOf(e)); }; const onOver = (e: DragEvent) => { if (!hasFiles(e.dataTransfer)) return; e.preventDefault(); + if (blocked(e)) { + if (e.dataTransfer) e.dataTransfer.dropEffect = 'none'; + setDropTarget(null); + return; + } if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; const target = targetOf(e); setDropTarget((prev) => (prev === target ? prev : target)); @@ -443,10 +457,13 @@ export function FileBrowser({ }; const onDrop = (e: DragEvent) => { if (!hasFiles(e.dataTransfer) || !e.dataTransfer) return; + // Ein Bestandteil der Seite hat das Ablegen schon selbst behandelt. + const handled = e.defaultPrevented; e.preventDefault(); depth = 0; - const target = targetOf(e); setDropTarget(null); + if (handled || blocked(e)) return; + const target = targetOf(e); const snapshot = snapshotDrop(e.dataTransfer); void collectDrop(snapshot).then((result) => { if (result.problem) {