fix(nextcloud-files): WR-03 hoechstens vier gleichzeitige Nextcloud-Aufrufe je Zugang
- die Aufrufsperre vergibt je Zugangsschluessel vier Plaetze bis zur Antwort (Kopfzeilen); weitere Aufrufe warten der Reihe nach und pruefen danach, ob der Zugang inzwischen tot oder der Ursprung gesperrt ist; ein widerrufener Zugang erzeugt so hoechstens vier 401 - Hochladen und Zusammenbau (Antwort erst nach dem ganzen Koerper) belegen keinen Platz - Weboberflaeche: Vorschaubilder laden weiter lazy; schlaegt eines fehl, prueft die Ansicht hoechstens alle 10 s die Verbindung und kehrt bei connectionExpired zur Anmeldung zurueck Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -520,3 +520,27 @@ describe('FileBrowser — Ziehen und Ablegen', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('FileBrowser — Vorschaubilder (WR-03)', () => {
|
||||
it('ein fehlgeschlagenes Vorschaubild prueft die Verbindung; ist sie abgelaufen, geht es zum Anmeldebildschirm', async () => {
|
||||
const photos = [
|
||||
folder('/Projekte'),
|
||||
entry('/Foto.jpg', { mime: 'image/jpeg', hasPreview: true, fileId: '501' }),
|
||||
entry('/Bild.png', { mime: 'image/png', hasPreview: true, fileId: '502' }),
|
||||
];
|
||||
mockList.mockImplementation(async (p: string) => listing(p, p === '/' ? photos : []));
|
||||
const { container } = await mount();
|
||||
const imgs = container.querySelectorAll('img');
|
||||
expect(imgs.length).toBe(2);
|
||||
expect(Array.from(imgs).every((i) => i.getAttribute('loading') === 'lazy')).toBe(true);
|
||||
const callsBefore = mockList.mock.calls.length;
|
||||
mockList.mockRejectedValue(
|
||||
new NextcloudFilesRequestError(409, 'connectionExpired', 'abgelaufen'),
|
||||
);
|
||||
fireEvent.error(imgs[0]);
|
||||
fireEvent.error(imgs[1]);
|
||||
await waitFor(() => expect(onExpired).toHaveBeenCalledTimes(1));
|
||||
// Zwei Fehler kurz hintereinander loesen nur EINE Pruefung aus.
|
||||
expect(mockList.mock.calls.length - callsBefore).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -736,6 +736,16 @@ export function FileBrowser({
|
||||
}
|
||||
};
|
||||
|
||||
// Ein Vorschaubild schlaegt fehl: hoechstens alle 10 s den Ordner still neu laden. Ist die
|
||||
// Verbindung abgelaufen, fuehrt das zum Anmeldebildschirm, und keine weiteren Bilder laden (WR-03).
|
||||
const previewCheckAt = useRef(0);
|
||||
const onPreviewError = useCallback(() => {
|
||||
const at = Date.now();
|
||||
if (at - previewCheckAt.current < 10_000) return;
|
||||
previewCheckAt.current = at;
|
||||
void load(pathRef.current, { quiet: true });
|
||||
}, [load]);
|
||||
|
||||
// --- Darstellung ------------------------------------------------------------------------------
|
||||
|
||||
const viewProps = {
|
||||
@@ -750,6 +760,7 @@ export function FileBrowser({
|
||||
onToggle,
|
||||
onMenu: openMenu,
|
||||
onFocusItem: setFocusIndex,
|
||||
onPreviewError,
|
||||
};
|
||||
|
||||
const targetName =
|
||||
|
||||
@@ -10,7 +10,7 @@ import { CheckIcon, MoreIcon } from './icons';
|
||||
import { TypeTile } from './TypeTile';
|
||||
|
||||
/** Vorschauflaeche: echtes Bild, wenn die Nextcloud eines liefert, sonst die grosse Typkachel. */
|
||||
function PreviewSurface({ entry }: { entry: NcEntry }) {
|
||||
function PreviewSurface({ entry, onError }: { entry: NcEntry; onError?: () => void }) {
|
||||
const [failed, setFailed] = useState(false);
|
||||
// Echte Vorschauen fuer Bilder und Videos; Textvorschauen waeren nur ein fast leeres Blatt.
|
||||
const family = fileTypeOf(entry).family;
|
||||
@@ -27,7 +27,10 @@ function PreviewSurface({ entry }: { entry: NcEntry }) {
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-full w-full object-cover"
|
||||
onError={() => setFailed(true)}
|
||||
onError={() => {
|
||||
setFailed(true);
|
||||
onError?.();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TypeTile entry={entry} size="lg" />
|
||||
@@ -51,6 +54,7 @@ export function FileGrid({
|
||||
onToggle,
|
||||
onMenu,
|
||||
onFocusItem,
|
||||
onPreviewError,
|
||||
}: EntryViewProps) {
|
||||
const t = useTranslations('nextcloudFiles.browser');
|
||||
const locale = useLocale();
|
||||
@@ -101,7 +105,7 @@ export function FileGrid({
|
||||
: 'hover:bg-accent/80'
|
||||
}`}
|
||||
>
|
||||
<PreviewSurface entry={entry} />
|
||||
<PreviewSurface entry={entry} onError={onPreviewError} />
|
||||
<input
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
|
||||
@@ -29,6 +29,11 @@ export interface EntryViewProps {
|
||||
onToggle: (entry: NcEntry, index: number) => void;
|
||||
onMenu: (entry: NcEntry, x: number, y: number, trigger: HTMLElement | null) => void;
|
||||
onFocusItem: (index: number) => void;
|
||||
/**
|
||||
* Ein Vorschaubild liess sich nicht laden (WR-03). Die Ansicht prueft dann einmal, ob die
|
||||
* Verbindung abgelaufen ist, und kehrt gegebenenfalls zum Anmeldebildschirm zurueck.
|
||||
*/
|
||||
onPreviewError?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,6 +55,7 @@ export function FileList({
|
||||
onToggle,
|
||||
onMenu,
|
||||
onFocusItem,
|
||||
onPreviewError,
|
||||
sort,
|
||||
onSortChange,
|
||||
onSelectAll,
|
||||
@@ -184,7 +190,7 @@ export function FileList({
|
||||
</td>
|
||||
<td className="py-0 pl-1 pr-2">
|
||||
<span className="flex min-w-0 items-center gap-3">
|
||||
<TypeTile entry={entry} preview />
|
||||
<TypeTile entry={entry} preview onPreviewError={onPreviewError} />
|
||||
{isFolder ? (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -40,12 +40,15 @@ export function TypeTile({
|
||||
entry,
|
||||
size = 'sm',
|
||||
preview = false,
|
||||
onPreviewError,
|
||||
}: {
|
||||
entry: Pick<NcEntry, 'name' | 'type' | 'mime'> &
|
||||
Partial<Pick<NcEntry, 'hasPreview' | 'fileId' | 'etag'>>;
|
||||
size?: 'sm' | 'lg';
|
||||
/** Vorschaubild statt Kurzzeichen, wenn die Nextcloud eines liefert. */
|
||||
preview?: boolean;
|
||||
/** Das Vorschaubild liess sich nicht laden. */
|
||||
onPreviewError?: () => void;
|
||||
}) {
|
||||
const [failed, setFailed] = useState(false);
|
||||
const { family, code } = fileTypeOf(entry);
|
||||
@@ -69,7 +72,10 @@ export function TypeTile({
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-full w-full object-cover"
|
||||
onError={() => setFailed(true)}
|
||||
onError={() => {
|
||||
setFailed(true);
|
||||
onPreviewError?.();
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user