feat(ui): Rückmeldung zu Ende verdrahtet – und zwei Lügen entfernt
Die Meldungen sprachen bisher nur im Dateibrowser. Papierkorb, Freigabe-Links,
„Für mich freigegeben", Profil, Theming, Audit und die Nutzerliste schwiegen
weiter – und das war schlechter als vorher: Wer gelernt hat, dass Aether
Bescheid gibt, hält Schweigen für Erfolg.
Nicht blind verdrahtet. Sechs Stellen bleiben absichtlich still, samt Grund in
TESTING.md: die Suche (bricht bei jedem Anschlag die vorige ab), das Kopieren
der Prüfsumme (ohne HTTPS gibt es keine Zwischenablage, der Wert steht ja da),
die Theming-Vorgaben, die Absender-Vorschläge, die OCR-Abfrage im Hintergrund
und useAuth. Wo ein Dialog seinen Fehler bei sich anzeigt, bleibt es dabei –
dort gehört er hin, nicht an den Bildschirmrand.
Dabei zwei Fälle gefunden, die schlimmer sind als Schweigen:
**ProfilePage meldete „gespeichert", wenn das Speichern fehlgeschlagen war.**
`updateProfile(...).catch(() => {})`, danach ungeprüft das Häkchen.
**TagEditDialog riet die Ursache:** Jeder Fehler meldete „That name is already
taken" – auch ein Netzausfall, auch ein 500er. Jetzt steht dort der Satz des
Servers. Im TagDialog war genau das schon einmal aufgefallen und behoben
worden; hier stand es noch.
Dazu zwei Aufrufe ganz ohne Absicherung: das Wiederherstellen aus dem
Papierkorb und das Löschen eines Freigabe-Links. Und das Kopieren eines
Links scheiterte ohne HTTPS stumm – der Knopf tat schlicht nichts.
Eigener Fehler nebenbei behoben: Ich hatte deutsche Anführungszeichen in
englische Meldungen gesetzt.
Geprüft: tsc sauber, 41 Vitest-Tests, alle 234 Prüfungen der sieben
Selbsttest-Suiten grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
16258640ad
commit
8586664fae
10 files changed
+133
-35
No files matched your search
+37
@@ -1487,6 +1487,43 @@ klappte – nichts.
|
|||||||
- [ ] Fünfte Meldung verdrängt die erste (höchstens vier stehen)
|
- [ ] Fünfte Meldung verdrängt die erste (höchstens vier stehen)
|
||||||
- [ ] Betriebssystem auf „Bewegung reduzieren" → keine Einblendung
|
- [ ] Betriebssystem auf „Bewegung reduzieren" → keine Einblendung
|
||||||
|
|
||||||
|
### Wo es weiterhin still bleibt – und warum
|
||||||
|
|
||||||
|
Nicht jeder verschluckte Fehler ist einer. Diese bleiben absichtlich stumm,
|
||||||
|
damit niemand sie später "nachrüstet":
|
||||||
|
|
||||||
|
| Stelle | Grund |
|
||||||
|
|---|---|
|
||||||
|
| `SearchBox` | Bricht bei jedem Tastendruck die vorige Suche ab. Eine Meldung je Anschlag wäre nur im Weg. |
|
||||||
|
| `FileInfoDialog` (Prüfsumme kopieren) | Ohne HTTPS gibt es keine Zwischenablage – der Wert steht vollständig da und lässt sich markieren. |
|
||||||
|
| `useAppSettings` | Erreichbarkeit des Servers ist nicht Sache des Themings; die Vorgaben bleiben stehen. |
|
||||||
|
| `TagDialog` (Absender-Vorschläge) | Ohne Vorschläge kann man trotzdem tippen. |
|
||||||
|
| `ProfilePage` (OCR-Stand) | Hintergrundabfrage im Sekundentakt. |
|
||||||
|
| `useAuth` | Kein Konto = abgemeldet, das ist die Antwort und kein Fehler. |
|
||||||
|
|
||||||
|
Wo ein Dialog seinen Fehler **bei sich** anzeigt (ShareWithDialog, ThemingTab
|
||||||
|
beim Hochladen, TagDialog, CalendarPage, OfficeEditor, FilePreview), bleibt es
|
||||||
|
dabei: Dort gehört die Meldung hin, nicht an den unteren Bildschirmrand.
|
||||||
|
|
||||||
|
### Zwei Fehler, die schlimmer waren als Schweigen
|
||||||
|
|
||||||
|
**ProfilePage meldete "gespeichert", wenn das Speichern fehlgeschlagen war.**
|
||||||
|
`updateProfile(...).catch(() => {})`, danach ungeprüft das Häkchen. Das ist die
|
||||||
|
übelste Sorte: Sie behauptet das Gegenteil dessen, was geschehen ist.
|
||||||
|
|
||||||
|
**TagEditDialog riet die Ursache.** Jeder Fehler meldete "That name is already
|
||||||
|
taken" – auch ein Netzausfall, auch ein 500er. Jetzt steht dort der Satz des
|
||||||
|
Servers. Im TagDialog war genau das schon einmal aufgefallen und behoben
|
||||||
|
worden; hier stand es noch.
|
||||||
|
|
||||||
|
- [ ] Netz trennen, Profilnamen speichern -> rote Meldung, **kein** Häkchen
|
||||||
|
- [ ] Netz trennen, Etikett umbenennen -> "No connection to the server.",
|
||||||
|
nicht "That name is already taken"
|
||||||
|
- [ ] Papierkorb: Wiederherstellen und Leeren melden sich
|
||||||
|
- [ ] Freigabe-Link kopieren ohne HTTPS -> Hinweis auf HTTPS statt gar nichts
|
||||||
|
- [ ] Freigabe-Link löschen -> Meldung, **kein** Rückgängig (ein neuer Link
|
||||||
|
bekäme ein neues Token; der alte bliebe tot)
|
||||||
|
|
||||||
## Der Zurück-Fehler und was der Test **nicht** kann (16.–18.09.2026)
|
## Der Zurück-Fehler und was der Test **nicht** kann (16.–18.09.2026)
|
||||||
|
|
||||||
Auf „Edit" in der PDF-Ansicht schloss sich die Datei, statt den Editor zu
|
Auf „Edit" in der PDF-Ansicht schloss sich die Datei, statt den Editor zu
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { tagsApi, correspondentsApi, notifyTagsChanged, type Correspondent, type Tag } from '../api/client'
|
import { tagsApi, correspondentsApi, notifyTagsChanged, type Correspondent, type Tag } from '../api/client'
|
||||||
import { tagPalette, tagColor } from './TagChip'
|
import { tagPalette, tagColor } from './TagChip'
|
||||||
|
import { fehlertext } from './Toasts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ein Etikett umbenennen, einfärben oder löschen.
|
* Ein Etikett umbenennen, einfärben oder löschen.
|
||||||
@@ -28,8 +29,12 @@ export default function TagEditDialog({ tag, onClose, onChanged }: {
|
|||||||
notifyTagsChanged()
|
notifyTagsChanged()
|
||||||
onChanged(updated)
|
onChanged(updated)
|
||||||
onClose()
|
onClose()
|
||||||
} catch {
|
} catch (err) {
|
||||||
setError('That name is already taken')
|
// **Nicht mehr raten.** Die feste Meldung „That name is already taken"
|
||||||
|
// behauptete eine Ursache und lag bei jedem anderen Fehler daneben –
|
||||||
|
// Netz weg, Server kaputt, Recht fehlt. Der Server schreibt seine
|
||||||
|
// Meldungen selbst; dieselbe Entscheidung wie im TagDialog.
|
||||||
|
setError(fehlertext(err, 'Could not save the change'))
|
||||||
setBusy(false)
|
setBusy(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,8 +247,12 @@ export function CorrespondentRenameDialog({ correspondent, onClose, onChanged }:
|
|||||||
notifyTagsChanged()
|
notifyTagsChanged()
|
||||||
onChanged(updated)
|
onChanged(updated)
|
||||||
onClose()
|
onClose()
|
||||||
} catch {
|
} catch (err) {
|
||||||
setError('That name is already taken')
|
// **Nicht mehr raten.** Die feste Meldung „That name is already taken"
|
||||||
|
// behauptete eine Ursache und lag bei jedem anderen Fehler daneben –
|
||||||
|
// Netz weg, Server kaputt, Recht fehlt. Der Server schreibt seine
|
||||||
|
// Meldungen selbst; dieselbe Entscheidung wie im TagDialog.
|
||||||
|
setError(fehlertext(err, 'Could not save the change'))
|
||||||
setBusy(false)
|
setBusy(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
|
|||||||
import { adminApi, type AdminUser } from '../api/client'
|
import { adminApi, type AdminUser } from '../api/client'
|
||||||
import PageHeader from '../components/PageHeader'
|
import PageHeader from '../components/PageHeader'
|
||||||
import Tabs from '../components/Tabs'
|
import Tabs from '../components/Tabs'
|
||||||
|
import { toast, fehlertext } from '../components/Toasts'
|
||||||
type Tab = 'users' | 'groups' | 'theming' | 'audit'
|
type Tab = 'users' | 'groups' | 'theming' | 'audit'
|
||||||
|
|
||||||
import UsersTab from './admin/UsersTab'
|
import UsersTab from './admin/UsersTab'
|
||||||
@@ -14,7 +15,8 @@ export default function AdminPage() {
|
|||||||
const [allUsers, setAllUsers] = useState<AdminUser[]>([])
|
const [allUsers, setAllUsers] = useState<AdminUser[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
adminApi.listUsers().then(data => setAllUsers(data ?? [])).catch(() => {})
|
adminApi.listUsers().then(data => setAllUsers(data ?? []))
|
||||||
|
.catch(err => toast.error(fehlertext(err, 'Could not load the user list')))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const tabs: { id: Tab; label: string }[] = [
|
const tabs: { id: Tab; label: string }[] = [
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ export default function FileBrowser({
|
|||||||
// Löschen ist bei uns der Papierkorb, Zurückholen ein einziger Aufruf.
|
// Löschen ist bei uns der Papierkorb, Zurückholen ein einziger Aufruf.
|
||||||
// Deshalb kostet das Rückgängig hier fast nichts – und nimmt dem
|
// Deshalb kostet das Rückgängig hier fast nichts – und nimmt dem
|
||||||
// Löschen den Schrecken.
|
// Löschen den Schrecken.
|
||||||
toast.undo(`Deleted „${file.name}"`, async () => {
|
toast.undo(`Deleted “${file.name}”`, async () => {
|
||||||
try {
|
try {
|
||||||
await trashApi.restore(file.id)
|
await trashApi.restore(file.id)
|
||||||
await load()
|
await load()
|
||||||
@@ -410,7 +410,7 @@ export default function FileBrowser({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(fehlertext(err, `Could not delete „${file.name}"`))
|
toast.error(fehlertext(err, `Could not delete “${file.name}”`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,7 +455,7 @@ export default function FileBrowser({
|
|||||||
const updated = await filesApi.rename(file.id, name)
|
const updated = await filesApi.rename(file.id, name)
|
||||||
setFiles(f => f.map(x => x.id === file.id ? updated : x))
|
setFiles(f => f.map(x => x.id === file.id ? updated : x))
|
||||||
const alterName = file.name
|
const alterName = file.name
|
||||||
toast.undo(`Renamed to „${name}"`, async () => {
|
toast.undo(`Renamed to “${name}”`, async () => {
|
||||||
try {
|
try {
|
||||||
const zurueck = await filesApi.rename(file.id, alterName)
|
const zurueck = await filesApi.rename(file.id, alterName)
|
||||||
setFiles(f => f.map(x => x.id === file.id ? zurueck : x))
|
setFiles(f => f.map(x => x.id === file.id ? zurueck : x))
|
||||||
@@ -482,7 +482,7 @@ export default function FileBrowser({
|
|||||||
const updated = await foldersApi.rename(folder.id, name)
|
const updated = await foldersApi.rename(folder.id, name)
|
||||||
setFolders(f => f.map(x => x.id === folder.id ? updated : x))
|
setFolders(f => f.map(x => x.id === folder.id ? updated : x))
|
||||||
const alterName = folder.name
|
const alterName = folder.name
|
||||||
toast.undo(`Renamed to „${name}"`, async () => {
|
toast.undo(`Renamed to “${name}”`, async () => {
|
||||||
try {
|
try {
|
||||||
const zurueck = await foldersApi.rename(folder.id, alterName)
|
const zurueck = await foldersApi.rename(folder.id, alterName)
|
||||||
setFolders(f => f.map(x => x.id === folder.id ? zurueck : x))
|
setFolders(f => f.map(x => x.id === folder.id ? zurueck : x))
|
||||||
@@ -506,9 +506,9 @@ export default function FileBrowser({
|
|||||||
// anlegen und alles zurücksortieren. Ein Knopf, der etwas anderes tut
|
// anlegen und alles zurücksortieren. Ein Knopf, der etwas anderes tut
|
||||||
// als er verspricht, wäre schlimmer als keiner; deshalb sagt die
|
// als er verspricht, wäre schlimmer als keiner; deshalb sagt die
|
||||||
// Meldung stattdessen, wo die Dateien geblieben sind.
|
// Meldung stattdessen, wo die Dateien geblieben sind.
|
||||||
toast.info(`Deleted „${folder.name}" – its files are in the trash`)
|
toast.info(`Deleted “${folder.name}” – its files are in the trash`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(fehlertext(err, `Could not delete „${folder.name}"`))
|
toast.error(fehlertext(err, `Could not delete “${folder.name}”`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,7 +518,7 @@ export default function FileBrowser({
|
|||||||
const alterOrdner = file.folder_id ?? null
|
const alterOrdner = file.folder_id ?? null
|
||||||
await filesApi.move(file.id, targetFolderId)
|
await filesApi.move(file.id, targetFolderId)
|
||||||
await reload()
|
await reload()
|
||||||
toast.undo(`Moved „${file.name}"`, async () => {
|
toast.undo(`Moved “${file.name}”`, async () => {
|
||||||
try {
|
try {
|
||||||
await filesApi.move(file.id, alterOrdner)
|
await filesApi.move(file.id, alterOrdner)
|
||||||
await reload()
|
await reload()
|
||||||
@@ -527,7 +527,7 @@ export default function FileBrowser({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(fehlertext(err, `Could not move „${file.name}"`))
|
toast.error(fehlertext(err, `Could not move “${file.name}”`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useAppSettings } from '../hooks/useAppSettings'
|
|||||||
import AppPasswords from '../components/AppPasswords'
|
import AppPasswords from '../components/AppPasswords'
|
||||||
import PageHeader from '../components/PageHeader'
|
import PageHeader from '../components/PageHeader'
|
||||||
import Tabs from '../components/Tabs'
|
import Tabs from '../components/Tabs'
|
||||||
|
import { toast, fehlertext } from '../components/Toasts'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User
|
user: User
|
||||||
@@ -201,7 +202,11 @@ export default function ProfilePage({ user, onUserChanged }: Props) {
|
|||||||
enabled ? primary : globalColors.primary,
|
enabled ? primary : globalColors.primary,
|
||||||
enabled ? accent : globalColors.accent,
|
enabled ? accent : globalColors.accent,
|
||||||
)
|
)
|
||||||
await authApi.setUseCustomTheme(enabled).catch(() => {})
|
try {
|
||||||
|
await authApi.setUseCustomTheme(enabled)
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(fehlertext(err, 'Could not change the theme setting'))
|
||||||
|
}
|
||||||
await onUserChanged?.()
|
await onUserChanged?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,11 +222,18 @@ export default function ProfilePage({ user, onUserChanged }: Props) {
|
|||||||
|
|
||||||
async function saveProfile() {
|
async function saveProfile() {
|
||||||
setProfileLoading(true)
|
setProfileLoading(true)
|
||||||
await authApi.updateProfile(displayName).catch(() => {})
|
// Vorher wurde der Fehler verschluckt und danach **trotzdem**
|
||||||
|
// „gespeichert" gemeldet. Das ist die schlimmste Sorte stiller Fehler:
|
||||||
|
// Sie behauptet das Gegenteil dessen, was geschehen ist.
|
||||||
|
try {
|
||||||
|
await authApi.updateProfile(displayName)
|
||||||
|
setProfileSaved(true)
|
||||||
|
setTimeout(() => setProfileSaved(false), 2000)
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(fehlertext(err, 'Could not save your profile'))
|
||||||
|
}
|
||||||
await onUserChanged?.()
|
await onUserChanged?.()
|
||||||
setProfileLoading(false)
|
setProfileLoading(false)
|
||||||
setProfileSaved(true)
|
|
||||||
setTimeout(() => setProfileSaved(false), 2000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changePassword() {
|
async function changePassword() {
|
||||||
@@ -243,11 +255,15 @@ export default function ProfilePage({ user, onUserChanged }: Props) {
|
|||||||
|
|
||||||
async function saveTheme() {
|
async function saveTheme() {
|
||||||
setThemeLoading(true)
|
setThemeLoading(true)
|
||||||
const result = await authApi.updateColors(primary, accent).catch(() => null)
|
const result = await authApi.updateColors(primary, accent).catch(err => {
|
||||||
|
toast.error(fehlertext(err, 'Could not save your colors'))
|
||||||
|
return null
|
||||||
|
})
|
||||||
// Wer Farben speichert, will sie auch sehen.
|
// Wer Farben speichert, will sie auch sehen.
|
||||||
if (result && !useCustom) {
|
if (result && !useCustom) {
|
||||||
setUseCustom(true)
|
setUseCustom(true)
|
||||||
await authApi.setUseCustomTheme(true).catch(() => {})
|
await authApi.setUseCustomTheme(true).catch(err =>
|
||||||
|
toast.error(fehlertext(err, 'Could not switch to your own colors')))
|
||||||
applyTheme(primary, accent)
|
applyTheme(primary, accent)
|
||||||
}
|
}
|
||||||
setThemeLoading(false)
|
setThemeLoading(false)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import ConfirmDialog from '../components/ConfirmDialog'
|
|||||||
import PageHeader from '../components/PageHeader'
|
import PageHeader from '../components/PageHeader'
|
||||||
import { SkeletonRows, useSkeleton } from '../components/Skeleton'
|
import { SkeletonRows, useSkeleton } from '../components/Skeleton'
|
||||||
import EmptyState from '../components/EmptyState'
|
import EmptyState from '../components/EmptyState'
|
||||||
|
import { toast, fehlertext } from '../components/Toasts'
|
||||||
|
|
||||||
function shareURL(token: string): string {
|
function shareURL(token: string): string {
|
||||||
return `${window.location.origin}/s/${token}`
|
return `${window.location.origin}/s/${token}`
|
||||||
@@ -27,20 +28,35 @@ export default function SharedPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sharesApi.list()
|
sharesApi.list()
|
||||||
.then(data => setLinks(data ?? []))
|
.then(data => setLinks(data ?? []))
|
||||||
.catch(() => {})
|
.catch(err => toast.error(fehlertext(err, 'Could not load the share links')))
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
async function copy(token: string) {
|
async function copy(token: string) {
|
||||||
await navigator.clipboard.writeText(shareURL(token))
|
// Ohne sicheren Kontext (http:// im LAN) gibt es keine Zwischenablage.
|
||||||
setCopied(token)
|
// Vorher blieb der Aufruf unbehandelt liegen: Der Knopf tat schlicht
|
||||||
setTimeout(() => setCopied(null), 2000)
|
// nichts, und der Grund stand allenfalls in der Konsole.
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(shareURL(token))
|
||||||
|
setCopied(token)
|
||||||
|
setTimeout(() => setCopied(null), 2000)
|
||||||
|
} catch {
|
||||||
|
toast.error('Could not copy – the clipboard needs HTTPS.')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteLink(link: ShareLink) {
|
async function deleteLink(link: ShareLink) {
|
||||||
await sharesApi.delete(link.id)
|
|
||||||
setLinks(l => l.filter(x => x.id !== link.id))
|
|
||||||
setConfirmDelete(null)
|
setConfirmDelete(null)
|
||||||
|
// **Kein Rückgängig.** Ein neuer Link bekäme ein neues Token; der alte
|
||||||
|
// bliebe tot. Ein Knopf, der etwas anderes wiederherstellt als das, was
|
||||||
|
// weg ist, wäre eine Lüge.
|
||||||
|
try {
|
||||||
|
await sharesApi.delete(link.id)
|
||||||
|
setLinks(l => l.filter(x => x.id !== link.id))
|
||||||
|
toast.info('Share link deleted')
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(fehlertext(err, 'Could not delete the share link'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import PageHeader from '../components/PageHeader'
|
|||||||
import { FolderGlyph } from '../components/icons'
|
import { FolderGlyph } from '../components/icons'
|
||||||
import { SkeletonRows, useSkeleton } from '../components/Skeleton'
|
import { SkeletonRows, useSkeleton } from '../components/Skeleton'
|
||||||
import EmptyState from '../components/EmptyState'
|
import EmptyState from '../components/EmptyState'
|
||||||
|
import { toast, fehlertext } from '../components/Toasts'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** Öffnet einen freigegebenen Ordner in der Dateiansicht. */
|
/** Öffnet einen freigegebenen Ordner in der Dateiansicht. */
|
||||||
@@ -22,7 +23,7 @@ export default function SharedWithMePage({ onOpenFolder }: Props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
aclApi.sharedWithMe()
|
aclApi.sharedWithMe()
|
||||||
.then(d => setItems(d ?? []))
|
.then(d => setItems(d ?? []))
|
||||||
.catch(() => {})
|
.catch(err => toast.error(fehlertext(err, 'Could not load what was shared with you')))
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import PageHeader from '../components/PageHeader'
|
|||||||
import ConfirmDialog from '../components/ConfirmDialog'
|
import ConfirmDialog from '../components/ConfirmDialog'
|
||||||
import { SkeletonRows, useSkeleton } from '../components/Skeleton'
|
import { SkeletonRows, useSkeleton } from '../components/Skeleton'
|
||||||
import EmptyState from '../components/EmptyState'
|
import EmptyState from '../components/EmptyState'
|
||||||
|
import { toast, fehlertext } from '../components/Toasts'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** Wiederherstellen verändert den belegten Speicher – Balken nachziehen. */
|
/** Wiederherstellen verändert den belegten Speicher – Balken nachziehen. */
|
||||||
@@ -22,14 +23,21 @@ export default function TrashPage({ onUserChanged }: Props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
trashApi.list()
|
trashApi.list()
|
||||||
.then(data => setItems(data ?? []))
|
.then(data => setItems(data ?? []))
|
||||||
.catch(() => {})
|
.catch(err => toast.error(fehlertext(err, 'Could not load the trash')))
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
async function restore(file: TrashItem) {
|
async function restore(file: TrashItem) {
|
||||||
await trashApi.restore(file.id)
|
// Vorher ohne jede Absicherung: Schlug das Wiederherstellen fehl, blieb
|
||||||
setItems(i => i.filter(x => x.id !== file.id))
|
// die Zeile stehen und nichts sagte, warum.
|
||||||
await onUserChanged?.()
|
try {
|
||||||
|
await trashApi.restore(file.id)
|
||||||
|
setItems(i => i.filter(x => x.id !== file.id))
|
||||||
|
await onUserChanged?.()
|
||||||
|
toast.info(`Restored “${file.name}”`)
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(fehlertext(err, `Could not restore “${file.name}”`))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function emptyAll() {
|
async function emptyAll() {
|
||||||
@@ -38,7 +46,9 @@ export default function TrashPage({ onUserChanged }: Props) {
|
|||||||
await trashApi.emptyAll()
|
await trashApi.emptyAll()
|
||||||
setItems([])
|
setItems([])
|
||||||
await onUserChanged?.()
|
await onUserChanged?.()
|
||||||
} catch { /* Liste bleibt stehen, damit der Fehlschlag sichtbar ist */ }
|
} catch (err) {
|
||||||
|
toast.error(fehlertext(err, 'Could not empty the trash'))
|
||||||
|
}
|
||||||
setEmptying(false)
|
setEmptying(false)
|
||||||
setConfirmEmpty(false)
|
setConfirmEmpty(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import RescanCard from './RescanCard'
|
import RescanCard from './RescanCard'
|
||||||
import { adminApi, type AuditEntry, type OcrStatus } from '../../api/client'
|
import { adminApi, type AuditEntry, type OcrStatus } from '../../api/client'
|
||||||
|
import { toast, fehlertext } from '../../components/Toasts'
|
||||||
import { formatBytes } from './ui'
|
import { formatBytes } from './ui'
|
||||||
|
|
||||||
// Audit-Log, Speicher-Bereinigung und der Stand der Texterkennung –
|
// Audit-Log, Speicher-Bereinigung und der Stand der Texterkennung –
|
||||||
@@ -154,7 +155,8 @@ function StorageCleanup() {
|
|||||||
const [derived, setDerived] = useState<{ prefix: string; files: number; bytes: number }[]>([])
|
const [derived, setDerived] = useState<{ prefix: string; files: number; bytes: number }[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
adminApi.derivedStorage().then(setDerived).catch(() => {})
|
adminApi.derivedStorage().then(setDerived)
|
||||||
|
.catch(err => toast.error(fehlertext(err, 'Could not load the derived-storage figures')))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
@@ -254,7 +256,8 @@ export default function AuditTab() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
adminApi.listAuditActions().then(a => setActions(a ?? [])).catch(() => {})
|
adminApi.listAuditActions().then(a => setActions(a ?? []))
|
||||||
|
.catch(err => toast.error(fehlertext(err, 'Could not load the action list')))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { adminApi, type AppSettings } from '../../api/client'
|
|||||||
import { applyTheme } from '../../theme'
|
import { applyTheme } from '../../theme'
|
||||||
import { useAppSettings } from '../../hooks/useAppSettings'
|
import { useAppSettings } from '../../hooks/useAppSettings'
|
||||||
import ConfirmDialog from '../../components/ConfirmDialog'
|
import ConfirmDialog from '../../components/ConfirmDialog'
|
||||||
|
import { toast, fehlertext } from '../../components/Toasts'
|
||||||
import { Card, Btn, Input } from './ui'
|
import { Card, Btn, Input } from './ui'
|
||||||
|
|
||||||
// Erscheinungsbild: Name, Farben, Logo und App-Symbol.
|
// Erscheinungsbild: Name, Farben, Logo und App-Symbol.
|
||||||
@@ -178,7 +179,7 @@ export default function ThemingTab() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
adminApi.getSettings()
|
adminApi.getSettings()
|
||||||
.then(data => setSettings(data ?? {}))
|
.then(data => setSettings(data ?? {}))
|
||||||
.catch(() => {})
|
.catch(err => toast.error(fehlertext(err, 'Could not load the settings')))
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -199,7 +200,10 @@ export default function ThemingTab() {
|
|||||||
cloud_name: settings.cloud_name,
|
cloud_name: settings.cloud_name,
|
||||||
primary_color: settings.primary_color,
|
primary_color: settings.primary_color,
|
||||||
accent_color: settings.accent_color,
|
accent_color: settings.accent_color,
|
||||||
}).catch(() => null)
|
}).catch(err => {
|
||||||
|
toast.error(fehlertext(err, 'Could not save the settings'))
|
||||||
|
return null
|
||||||
|
})
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
if (result) {
|
if (result) {
|
||||||
setSettings(result)
|
setSettings(result)
|
||||||
|
|||||||
Reference in new issue
Block a user