Verschieben hatte denselben Fehler -- und Ordner entfernen gab es nur im Dienst

VERSCHIEBEN: derselbe Griff wie beim Loeschen, dieselbe Wirkung.

    onClick={() => setVerschieben(items.filter((i) => gewaehlt.has(i.id)))}

`items` ist die offene Seite; von 3565 gewaehlten Bildern wanderten 60. Ich
hatte gestern nur das Loeschen repariert und diese Stelle nicht gesucht --
zwei Knoepfe mit demselben Muster, und ich habe einen davon uebersehen.

Jetzt haelt der Auftrag auch hier `ids` (alle) und `namen` (die benennbaren)
getrennt, und verschoben wird in Haeppchen zu 200 mit Fortschritt im Knopf.
Was nicht ging, wird benannt statt verschwiegen.

ORDNER ENTFERNEN: die Route gab es (`DELETE /folders/:pfad`), die
Schnittstelle auch (`ordnerEntfernen` in api.ts) -- nur wurde sie NIRGENDS
aufgerufen. Eine Funktion ohne Knopf ist fuer den, der sie sucht, nicht
vorhanden; dieselbe Lehre steht schon ueber "Neuer Ordner" in derselben Datei.

Der Knopf sitzt jetzt neben dem Umbenennen, erst beim Darueberfahren sichtbar.
Die REGEL bleibt beim Dienst: er lehnt einen Ordner mit Inhalt ab und sagt,
wieviele Dateien darin liegen. Sie in der Oberflaeche zu wiederholen hiesse,
sie zweimal zu pflegen -- und die zweite Fassung ist irgendwann die falsche.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 13:44:38 +02:00
co-authored by Claude Opus 5
parent 8a17b8b974
commit c9e5bb3c31
3 changed files with 116 additions and 25 deletions
+42 -1
View File
@@ -1,5 +1,5 @@
import { useState } from 'react' import { useState } from 'react'
import { Check, Folder, FolderOpen, FolderPlus, Layers, PenLine, X } from 'lucide-react' import { Check, Folder, FolderOpen, FolderPlus, Layers, PenLine, Trash2, X } from 'lucide-react'
import { api, type Folders } from '../api' import { api, type Folders } from '../api'
import { bytes } from '../format' import { bytes } from '../format'
import { run } from '../store' import { run } from '../store'
@@ -22,6 +22,7 @@ export function Ordner({
onWahl, onWahl,
onAngelegt, onAngelegt,
onUmbenennen, onUmbenennen,
onEntfernt,
}: { }: {
daten: Folders | null daten: Folders | null
gewaehlt: string gewaehlt: string
@@ -29,12 +30,28 @@ export function Ordner({
/** Damit die Liste den neuen Ordner auch zeigt. */ /** Damit die Liste den neuen Ordner auch zeigt. */
onAngelegt: () => void onAngelegt: () => void
onUmbenennen: (pfad: string) => void onUmbenennen: (pfad: string) => void
/** Nach dem Entfernen muss die Liste neu geladen werden -- sonst steht
* dort ein Ordner, den es nicht mehr gibt. */
onEntfernt: () => void
}) { }) {
const [neu, setNeu] = useState<string | null>(null) const [neu, setNeu] = useState<string | null>(null)
const [legtAn, setLegtAn] = useState(false) const [legtAn, setLegtAn] = useState(false)
if (!daten) return null if (!daten) return null
async function entfernen(pfad: string) {
// Nachfragen, und dabei sagen, was NICHT passiert: der Ordner geht,
// die Dateien darin gehen nicht -- der Dienst laesst ihn ohnehin nur
// leer entfernen.
if (!confirm(`Ordner "${pfad}" entfernen?
Er muss leer sein; Dateien darin werden nicht angetastet.`)) {
return
}
const r = await run(() => api.ordnerEntfernen(pfad), `Ordner ${pfad} entfernt.`)
if (r) onEntfernt()
}
async function anlegen() { async function anlegen() {
const name = (neu ?? '').trim() const name = (neu ?? '').trim()
if (!name) return if (!name) return
@@ -96,6 +113,30 @@ export function Ordner({
</button> </button>
)} )}
{/* ORDNER ENTFERNEN. Den Weg gab es im Dienst und in `api.ts`
laengst -- es fehlte allein der Knopf, und damit war die
Funktion fuer den, der sie sucht, nicht vorhanden. Dieselbe
Lehre wie beim "Neuer Ordner" ein paar Zeilen weiter oben.
DIE PRUEFUNG BLEIBT BEIM DIENST: er lehnt einen Ordner mit
Inhalt ab und sagt, wieviele Dateien darin liegen. Diese Regel
hier zu wiederholen hiesse, sie zweimal zu pflegen -- und die
zweite Fassung ist irgendwann die falsche. */}
{pfad !== undefined && (
<button
onClick={(e) => {
e.stopPropagation()
void entfernen(pfad)
}}
className="shrink-0 text-schrift-leise opacity-0 transition-opacity
group-hover:opacity-100 hover:text-schlecht focus-visible:opacity-100"
aria-label={`${pfad} entfernen`}
title="Ordner entfernen (muss leer sein)"
>
<Trash2 size={12} />
</button>
)}
<span className="shrink-0 text-[11px] tabular-nums text-schrift-leise"> <span className="shrink-0 text-[11px] tabular-nums text-schrift-leise">
{anzahl} {anzahl}
</span> </span>
+60 -20
View File
@@ -1,7 +1,7 @@
import { useState } from 'react' import { useState } from 'react'
import { AlertTriangle, FolderInput, Plus } from 'lucide-react' import { AlertTriangle, FolderInput, Plus } from 'lucide-react'
import { api, type Folders, type MediaItem } from '../api' import { api, type Folders, type MediaItem } from '../api'
import { run } from '../store' import { run, useStore } from '../store'
import { Feld, Knopf } from './Bausteine' import { Feld, Knopf } from './Bausteine'
/** Dateien in einen anderen Ordner legen. /** Dateien in einen anderen Ordner legen.
@@ -12,12 +12,17 @@ import { Feld, Knopf } from './Bausteine'
* Zwischenspeicher. Das ist kein Fehler, sondern der Preis vorhersagbarer * Zwischenspeicher. Das ist kein Fehler, sondern der Preis vorhersagbarer
* Adressen; nur muss man es vorher wissen und nicht hinterher suchen. */ * Adressen; nur muss man es vorher wissen und nicht hinterher suchen. */
export function Verschieben({ export function Verschieben({
dateien, ids,
namen,
ordner, ordner,
onFertig, onFertig,
onAbbruch, onAbbruch,
}: { }: {
dateien: MediaItem[] /** ALLE Gewaehlten. */
ids: number[]
/** Nur die, deren Pfad bekannt ist -- die der offenen Seite. Fuer die
* Anzeige, nicht fuer die Handlung. */
namen: MediaItem[]
ordner: Folders | null ordner: Folders | null
onFertig: () => void onFertig: () => void
onAbbruch: () => void onAbbruch: () => void
@@ -25,27 +30,56 @@ export function Verschieben({
const [ziel, setZiel] = useState('') const [ziel, setZiel] = useState('')
const [neuerOrdner, setNeuerOrdner] = useState('') const [neuerOrdner, setNeuerOrdner] = useState('')
const [laeuft, setLaeuft] = useState(false) const [laeuft, setLaeuft] = useState(false)
const [fortschritt, setFortschritt] = useState(0)
const beispiel = dateien[0] const beispiel = namen[0]
const name = beispiel?.path.split('/').pop() ?? '' const name = beispiel?.path.split('/').pop() ?? ''
const neuerPfad = ziel ? `${ziel}/${name}` : name const neuerPfad = ziel ? `${ziel}/${name}` : name
async function verschieben() { async function verschieben() {
setLaeuft(true) setLaeuft(true)
const r = await run( const { note } = useStore.getState()
() => api.verschieben(dateien.map((d) => d.id), ziel),
(x) => // IN HAEPPCHEN, wie beim Loeschen: der Dienst verschiebt jede Datei
x.fehlgeschlagen === 0 // einzeln, und tausende in einer Anfrage laufen in die Zeitgrenze des
? `${x.verschoben} verschoben.` // Proxys. 200 je Runde, und der Knopf sagt, wie weit es ist.
: `${x.verschoben} verschoben, ${x.fehlgeschlagen} nicht: ` + const PORTION = 200
x.ergebnisse let bewegt = 0
.filter((e) => !e.ok) let daneben: string[] = []
.slice(0, 3) let abbruch: string | null = null
.map((e) => `${e.path} (${e.error})`)
.join('; '), for (let i = 0; i < ids.length; i += PORTION) {
try {
const x = await api.verschieben(ids.slice(i, i + PORTION), ziel)
bewegt += x.verschoben
daneben.push(
...x.ergebnisse.filter((e) => !e.ok).map((e) => `${e.path} (${e.error})`),
) )
} catch (e) {
abbruch = e instanceof Error ? e.message : 'Unbekannt'
break
}
setFortschritt(bewegt)
}
setLaeuft(false) setLaeuft(false)
if (r) onFertig() if (abbruch) {
note('fehler', `${bewegt} von ${ids.length} verschoben, dann brach es ab: ${abbruch}`)
return
}
if (daneben.length === 0) {
note('ok', `${bewegt} verschoben.`)
} else {
// Was NICHT ging, wird benannt -- ein "verschoben" ueber einer
// halben Bewegung waere die Sorte Rueckmeldung, die hier nicht
// vorkommt.
note(
'hinweis',
`${bewegt} verschoben, ${daneben.length} nicht: ${daneben.slice(0, 3).join('; ')}` +
(daneben.length > 3 ? ` … und ${daneben.length - 3} weitere` : ''),
)
}
onFertig()
} }
async function ordnerAnlegen() { async function ordnerAnlegen() {
@@ -69,7 +103,7 @@ export function Verschieben({
> >
<h3 className="flex items-center gap-2 border-b border-rand px-4 py-3 text-sm font-semibold"> <h3 className="flex items-center gap-2 border-b border-rand px-4 py-3 text-sm font-semibold">
<FolderInput size={16} className="text-akzent" /> <FolderInput size={16} className="text-akzent" />
{dateien.length === 1 ? 'Datei verschieben' : `${dateien.length} Dateien verschieben`} {ids.length === 1 ? 'Datei verschieben' : `${ids.length} Dateien verschieben`}
</h3> </h3>
<div className="flex flex-col gap-3 px-4 py-4"> <div className="flex flex-col gap-3 px-4 py-4">
@@ -126,9 +160,9 @@ export function Verschieben({
<div className="rounded border border-rand bg-grund p-2 font-mono text-xs"> <div className="rounded border border-rand bg-grund p-2 font-mono text-xs">
<div className="text-schrift-leise line-through">{beispiel.path}</div> <div className="text-schrift-leise line-through">{beispiel.path}</div>
<div className="text-gut">{neuerPfad}</div> <div className="text-gut">{neuerPfad}</div>
{dateien.length > 1 && ( {ids.length > 1 && (
<div className="mt-1 text-schrift-leise"> <div className="mt-1 text-schrift-leise">
und {dateien.length - 1} weitere und {ids.length - 1} weitere
</div> </div>
)} )}
</div> </div>
@@ -141,7 +175,13 @@ export function Verschieben({
art="haupt" art="haupt"
wartet={laeuft} wartet={laeuft}
onClick={() => void verschieben()} onClick={() => void verschieben()}
kind="Verschieben" kind={
// Bei tausenden Dateien dauert es; Stillstand ist
// sonst von Haengen nicht zu unterscheiden.
laeuft && fortschritt > 0
? `${fortschritt} von ${ids.length}`
: 'Verschieben'
}
/> />
</div> </div>
</div> </div>
+13 -3
View File
@@ -74,7 +74,10 @@ export function Galerie() {
// Welche Datei gerade gross zu sehen ist -- als ID und nicht als Objekt: // Welche Datei gerade gross zu sehen ist -- als ID und nicht als Objekt:
// nach einem Neuladen der Liste zeigt sie sonst auf einen alten Datensatz. // nach einem Neuladen der Liste zeigt sie sonst auf einen alten Datensatz.
const [ansehen, setAnsehen] = useState<number | null>(null) const [ansehen, setAnsehen] = useState<number | null>(null)
const [verschieben, setVerschieben] = useState<MediaItem[] | null>(null) /** Dieselbe Trennung wie beim Loeschen -- und aus demselben Grund: hier
* stand `items.filter(...)`, also die offene Seite, und von 3565
* gewaehlten Bildern wanderten 60. */
const [verschieben, setVerschieben] = useState<{ ids: number[]; namen: MediaItem[] } | null>(null)
const [umbenennen, setUmbenennen] = useState<MediaItem | null>(null) const [umbenennen, setUmbenennen] = useState<MediaItem | null>(null)
const [ordnerUmbenennen, setOrdnerUmbenennen] = useState<string | null>(null) const [ordnerUmbenennen, setOrdnerUmbenennen] = useState<string | null>(null)
const { kopiert, kopieren } = useKopieren() const { kopiert, kopieren } = useKopieren()
@@ -282,6 +285,7 @@ export function Galerie() {
daten={ordnerDaten} daten={ordnerDaten}
gewaehlt={ordner} gewaehlt={ordner}
onAngelegt={() => void ordnerLaden()} onAngelegt={() => void ordnerLaden()}
onEntfernt={() => void ordnerLaden()}
onUmbenennen={setOrdnerUmbenennen} onUmbenennen={setOrdnerUmbenennen}
onWahl={(o) => { onWahl={(o) => {
setOrdner(o) setOrdner(o)
@@ -467,7 +471,12 @@ export function Galerie() {
{gewaehlt.size > 0 && ( {gewaehlt.size > 0 && (
<Knopf <Knopf
onClick={() => setVerschieben(items.filter((i) => gewaehlt.has(i.id)))} onClick={() =>
setVerschieben({
ids: [...gewaehlt],
namen: items.filter((i) => gewaehlt.has(i.id)),
})
}
kind={ kind={
<> <>
<FolderInput size={15} /> <FolderInput size={15} />
@@ -760,7 +769,8 @@ export function Galerie() {
{verschieben && ( {verschieben && (
<Verschieben <Verschieben
dateien={verschieben} ids={verschieben.ids}
namen={verschieben.namen}
ordner={ordnerDaten} ordner={ordnerDaten}
onAbbruch={() => setVerschieben(null)} onAbbruch={() => setVerschieben(null)}
onFertig={() => { onFertig={() => {