internal/folderapi: neue Backend-Voraussetzung - GET /folders/{id}/
children (lazy Baum-Ladung), .../breadcrumb (ueber DOC-09s Materialized
Path), .../documents (Dokumentenliste, whitelisted Sortierspalten).
folder.go ergaenzt ListChildren/Breadcrumb (additiv, keine bestehende
Funktion geaendert).
web/dms-browser: Next.js/React/TypeScript. Navigationsbaum laedt Kinder
lazy je Ebene, Dokumentenliste ist virtualisiert (lib/virtualization.ts,
abhaengigkeitsfrei - nur sichtbarer Ausschnitt + Overscan wird gerendert),
Mehrfachauswahl (lib/selection.ts: Klick/Strg/Umschalt), Tastatur-
navigation im Baum (lib/treeKeyboard.ts). Aktueller Ordner steckt in der
URL (Catch-all-Route /browse/[[...path]]) statt nur im React-State - der
Browser verwaltet Vor-/Zurueck-Navigation selbst.
Echter Bug beim Schreiben der Virtualisierungs-Tests gefunden und
behoben: startIndex war bei scrollTop jenseits des tatsaechlichen Inhalts
nicht auf totalRows geklemmt (haette zu ungueltigem slice() gefuehrt).
Pruefungen:
1. Liste mit 10.000 Zeilen scrollt ruckelfrei - Virtualisierung rendert
nachweislich nur ~20 statt 10.000 Zeilen
2. Navigation vollstaendig per Tastatur - 5 Vitest-Tests fuer Pfeiltasten/
Enter im Baum
3. Zustand bleibt beim Zurueck-Navigieren erhalten - Ordner-ID in der URL,
Browser-History uebernimmt die Wiederherstellung
Regressionsfrei: alle bestehenden Go-Pakete PASS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
56 lines
1.7 KiB
TypeScript
56 lines
1.7 KiB
TypeScript
// Reine Mehrfachauswahl-Logik (Akzeptanzkriterium 3: Umschalt- und
|
|
// Strg-Taste) — unabhängig von React/DOM testbar.
|
|
|
|
export interface SelectionState {
|
|
selected: Set<string>;
|
|
anchorIndex: number | null; // letzter "einfacher" Klick, Basis fuer Umschalt-Bereiche
|
|
}
|
|
|
|
export function initialSelection(): SelectionState {
|
|
return { selected: new Set(), anchorIndex: null };
|
|
}
|
|
|
|
export interface ClickModifiers {
|
|
shiftKey: boolean;
|
|
ctrlOrMetaKey: boolean;
|
|
}
|
|
|
|
// applyClick bildet das aus Dateimanagern bekannte Verhalten nach:
|
|
// - einfacher Klick: genau dieses Element auswaehlen, neuer Anker
|
|
// - Strg/Cmd+Klick: dieses Element zur Auswahl hinzufuegen/entfernen (toggle)
|
|
// - Umschalt+Klick: zusammenhaengenden Bereich vom Anker bis hierher waehlen
|
|
export function applyClick(
|
|
state: SelectionState,
|
|
ids: string[],
|
|
clickedIndex: number,
|
|
modifiers: ClickModifiers
|
|
): SelectionState {
|
|
const clickedID = ids[clickedIndex];
|
|
|
|
if (modifiers.shiftKey && state.anchorIndex !== null) {
|
|
const [from, to] = [state.anchorIndex, clickedIndex].sort((a, b) => a - b);
|
|
const range = ids.slice(from, to + 1);
|
|
return { selected: new Set(range), anchorIndex: state.anchorIndex };
|
|
}
|
|
|
|
if (modifiers.ctrlOrMetaKey) {
|
|
const next = new Set(state.selected);
|
|
if (next.has(clickedID)) {
|
|
next.delete(clickedID);
|
|
} else {
|
|
next.add(clickedID);
|
|
}
|
|
return { selected: next, anchorIndex: clickedIndex };
|
|
}
|
|
|
|
return { selected: new Set([clickedID]), anchorIndex: clickedIndex };
|
|
}
|
|
|
|
export function isSelected(state: SelectionState, id: string): boolean {
|
|
return state.selected.has(id);
|
|
}
|
|
|
|
export function clearSelection(state: SelectionState): SelectionState {
|
|
return { selected: new Set(), anchorIndex: null };
|
|
}
|