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:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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={() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user