Rueckfrage vor loeschenden Aktionen: eigenes Blatt statt window.confirm
Gemeldet als "Verbindung trennen tut nichts". Kein Fehler der App: in eingebetteten Ansichten sind Dialoge abgeschaltet, window.confirm kehrt sofort mit false zurueck (gemessen: 1 ms), und abmelden() beginnt damit. Die Suche danach brachte den eigentlichen Befund: elf Stellen hingen an window.confirm - acht direkt, drei im Zeilenmenue (Wischgeste in Fahrten, Tankvorgaengen, Messwerten). Jede davon eine loeschende Aktion, jede in eingebetteten Ansichten still wirkungslos. Das Panel zeigt an denselben Stellen seit jeher sein eigenes Blatt. screens/bestaetigung.tsx: ein kleiner Speicher plus ein ActionSheet, das einmal in App.tsx haengt. Kein Hook je Aufrufstelle - der haette jede der elf Stellen gezwungen, das Blatt auch selbst zu rendern, elf Gelegenheiten es zu vergessen, ohne dass etwas auffaellt. Ohne gehaengtes Blatt (Testlauf, Fremdeinbettung) gilt weiter window.confirm. LOESCH_TEXT und LOESCH_HINWEIS wortgleich aus dem Panel, samt der Trennung in Frage, erklaerenden Satz und Knopfbeschriftung. 225 Tests (vier neue), live an der gemeldeten Stelle geprueft: das Blatt erscheint, Abbrechen schliesst es, die Sitzung bleibt. Bestaetigt wurde bewusst nicht - das haette den Token vom Geraet geloescht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,9 @@ und ohne Netz rendert; dazu der QR-Code aus einem gespeicherten Bild, ein stille
|
||||
Debug-Knopf und der HA-Zugang bei den Zugaengen; der Cache-Brecher der Bilder haengt
|
||||
jetzt am Foto statt am App-Start; dazu ein Audit mit zwei Befunden in der
|
||||
Wisch-Zeile und der Auswahlliste der App; der Leerzustand der Karte ist deckend,
|
||||
`2026.9.4.16`,
|
||||
Abschnitte CC bis CJ. Davor: Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt:
|
||||
dazu die Rueckfrage vor loeschenden Aktionen als eigenes Blatt statt als
|
||||
Browserdialog, `2026.9.4.17`,
|
||||
Abschnitte CC bis CK. Davor: Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt:
|
||||
das Fahrtfenster stand in Geraetezeit, der Verlauf war nach Ankunftszeit sortiert. Dazu die neun
|
||||
Paritaetsbefunde und die verunreinigte Batteriehistorie, `2026.9.4.1`, Abschnitt CB. Davor: Der Regler stellt den Schlaf-Timeout des Dongles, flespi lesend und
|
||||
schreibend, die eigene Warteschlange wieder entfallen, `2026.9.3.10`, Abschnitt BZ. Davor: Zugaenge: die Dienste-Token aus App und Panel bedienbar, der Wert kommt nie
|
||||
@@ -10312,3 +10313,53 @@ Verbesserung (eigene Gestaltung statt Systemdialog, funktioniert in jedem
|
||||
Kontext, Gleichlauf mit dem Panel), kostet aber acht Aufrufstellen den Umbau
|
||||
von synchron auf einen Zustand je Stelle - deshalb hier vermerkt statt
|
||||
nebenbei gemacht.
|
||||
|
||||
## CK. Die Rueckfrage ist ein eigenes Blatt, kein Browserdialog mehr (2026.9.4.17)
|
||||
|
||||
Ausloeser war ein Fehlerbericht, der keiner war: „Verbindung trennen tut
|
||||
nichts" (App unter `:5173`). Gemessen: `window.confirm` kehrt im
|
||||
Browser-Bereich sofort mit `false` zurueck (1 ms) - und `abmelden()` beginnt
|
||||
mit genau dieser Rueckfrage. Auf dem Telefon zeigt Capacitor dafuer einen
|
||||
Systemdialog, dort greift der Knopf.
|
||||
|
||||
Die Suche danach brachte den eigentlichen Befund: **elf** Stellen der App
|
||||
hingen an `window.confirm` - acht direkt, drei weitere im `Zeilenmenue`, das
|
||||
die Wischgeste in Fahrten, Tankvorgaengen und Messwerten traegt. Jede davon
|
||||
ist eine loeschende Aktion, und jede lief in eingebetteten Ansichten still ins
|
||||
Leere. Das Panel zeigt an denselben Stellen seit jeher sein eigenes Blatt.
|
||||
|
||||
### `screens/bestaetigung.tsx`
|
||||
|
||||
Ein winziger Speicher plus ein `ActionSheet`, das **einmal** in `App.tsx`
|
||||
haengt (und in der Musterseite). `bestaetigen()` bleibt fuer die Aufrufstellen
|
||||
fast unveraendert - nur mit `await` davor.
|
||||
|
||||
**Warum kein Hook je Aufrufstelle:** ein Hook haette jede der elf Stellen
|
||||
gezwungen, das Blatt auch selbst zu rendern - elf Gelegenheiten, es zu
|
||||
vergessen, ohne dass etwas auffaellt (der Aufruf wuerde einfach nie
|
||||
antworten). Dasselbe Muster wie `theme.ts`: Zustand ausserhalb des
|
||||
Komponentenbaums, gelesen ueber `useSyncExternalStore`.
|
||||
|
||||
**Der Rueckfall ist Absicht:** ist das Blatt nirgends gehaengt (Testlauf,
|
||||
Fremdeinbettung), gilt wieder `window.confirm`. Eine Rueckfrage, die niemand
|
||||
beantworten kann, waere schlechter als der Systemdialog.
|
||||
|
||||
**Wortgleich mit dem Panel:** `LOESCH_TEXT` und `LOESCH_HINWEIS` sind von dort
|
||||
uebernommen, samt der Trennung in Frage, erklaerenden Satz und
|
||||
Knopfbeschriftung - `window.confirm` kannte nur einen Text. „Neue Raeder
|
||||
anlegen?" ist die einzige Rueckfrage, die **nicht** rot ist: sie loescht
|
||||
nichts, sie archiviert.
|
||||
|
||||
### Verifiziert
|
||||
|
||||
`tsc --noEmit` sauber, **225 Tests** (vier neue in `bestaetigung.test.tsx`:
|
||||
Ja-Antwort, Abbrechen samt Schliessen, eigene Knopfbeschriftung ohne roten
|
||||
Knopf, und der Rueckfall auf den Systemdialog).
|
||||
|
||||
**Live in der App**, an der Stelle des Fehlerberichts: der Knopf zeigt jetzt
|
||||
das Blatt mit „Zugang von diesem Geraet entfernen?", dem erklaerenden Satz,
|
||||
rotem „Trennen" und abgesetztem „Abbrechen". **Bestaetigt wurde bewusst
|
||||
nicht** - das haette den eingefuegten Token des Eigentuemers geloescht;
|
||||
Abbrechen schliesst das Blatt, die Sitzung bleibt.
|
||||
|
||||
Kein `window.confirm` mehr im Quelltext ausser dem dokumentierten Rueckfall.
|
||||
|
||||
@@ -38,6 +38,7 @@ import { DatenAnbieter } from "../src/daten/DatenKontext"
|
||||
import { Shell } from "../src/Shell"
|
||||
import type { SeitenName } from "../src/navigation"
|
||||
import { TITEL } from "../src/navigation"
|
||||
import { Bestaetigungsblatt } from "../src/screens/bestaetigung"
|
||||
import { Einrichtung } from "../src/screens/Einrichtung"
|
||||
import { SeiteFuer } from "../src/screens/register"
|
||||
import { beispielApi } from "../src/tests/beispieldaten"
|
||||
@@ -52,6 +53,7 @@ function Muster() {
|
||||
if (gewaehlt === "einrichtung") return <Einrichtung fertig={() => {}} />
|
||||
return (
|
||||
<DatenAnbieter api={beispielApi()}>
|
||||
<Bestaetigungsblatt />
|
||||
<Shell route={route} geheZu={geheZu} tabBeschriftung>
|
||||
<SeiteFuer
|
||||
route={route}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { startklarMelden } from "./daten/ota"
|
||||
import { Shell } from "./Shell"
|
||||
import type { Route, SeitenName } from "./navigation"
|
||||
import { useTabBeschriftung, useTheme } from "./theme"
|
||||
import { Bestaetigungsblatt } from "./screens/bestaetigung"
|
||||
import { Einrichtung } from "./screens/Einrichtung"
|
||||
import { Hinweisleiste } from "./screens/Hinweisleiste"
|
||||
import { Versionshinweis } from "./screens/Versionshinweis"
|
||||
@@ -45,6 +46,10 @@ export function App() {
|
||||
|
||||
return (
|
||||
<div className="ads-root dm-wurzel" data-theme={theme}>
|
||||
{/* Einmal fuer die ganze App: die Rueckfrage vor loeschenden Aktionen
|
||||
(siehe screens/bestaetigung.tsx). Rendert nichts, solange nichts
|
||||
gefragt ist. */}
|
||||
<Bestaetigungsblatt />
|
||||
{start === "pruefen" && <div className="dm-laden">Einen Moment …</div>}
|
||||
{start === "einrichten" && <Einrichtung fertig={() => setzeStart("bereit")} />}
|
||||
{start === "bereit" && (
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useDaten } from "../daten/DatenKontext"
|
||||
import { datum, datumUndZeit, de } from "../format"
|
||||
import { Leerzustand } from "./bausteine"
|
||||
import { AGM_RUHE_MAX_V } from "./Batterie"
|
||||
import { LOESCH_HINWEIS } from "./bestaetigung"
|
||||
import { Zeilenmenue } from "./Zeilenmenue"
|
||||
|
||||
interface Tageswert {
|
||||
@@ -79,6 +80,7 @@ export function BatterieListe() {
|
||||
<Zeilenmenue
|
||||
key={t.datum}
|
||||
loeschFrage={`Messwert vom ${datum(t.datum)} löschen?`}
|
||||
loeschHinweis={LOESCH_HINWEIS.batt}
|
||||
beiLoeschen={() => void loeschen(t)}
|
||||
kinder={
|
||||
<div className="dm-listenzeile">
|
||||
|
||||
@@ -17,7 +17,8 @@ import { OtaFehler, buendelAnwenden, buendelPasst, otaMoeglich } from "../daten/
|
||||
import type { Einstellungen as EinstellungenWerte } from "../daten/profilAdapter"
|
||||
import { datum, datumZeit, de, isoTag, standAlter } from "../format"
|
||||
import { useTheme } from "../theme"
|
||||
import { BildMitMenue, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
||||
import { BildMitMenue, Wertzeile, Werteliste } from "./bausteine"
|
||||
import { bestaetigen } from "./bestaetigung"
|
||||
import { BILDPLAETZE, SEITE_WINTER, markenlogoUrl } from "./bilder"
|
||||
import { DatensatzPopup } from "./DatensatzPopup"
|
||||
import { HistorienImport } from "./HistorienImport"
|
||||
@@ -287,7 +288,13 @@ export function Einstellungen({
|
||||
}
|
||||
|
||||
const abmelden = async () => {
|
||||
if (!bestaetigen("Zugang von diesem Gerät entfernen? Die Daten auf dem Server bleiben.")) return
|
||||
if (
|
||||
!(await bestaetigen("Zugang von diesem Gerät entfernen?", {
|
||||
text: "Adresse und Token werden von diesem Gerät gelöscht. Die Daten auf dem Server bleiben unberührt.",
|
||||
knopf: "Trennen",
|
||||
}))
|
||||
)
|
||||
return
|
||||
await zugangVerwerfen()
|
||||
zugangVergessen()
|
||||
beiAbmeldung()
|
||||
@@ -929,9 +936,15 @@ export function Einstellungen({
|
||||
<ActionButton
|
||||
disabled={tokenLaeuft !== null}
|
||||
onClick={() => {
|
||||
if (bestaetigen("Token löschen?")) {
|
||||
void tokenSchreiben(z.dienst, "")
|
||||
}
|
||||
void (async () => {
|
||||
if (
|
||||
!(await bestaetigen("Token löschen?", {
|
||||
text: "Der hinterlegte Token wird entfernt. Bis ein neuer eingetragen ist, kann die Integration diesen Dienst nicht mehr erreichen.",
|
||||
}))
|
||||
)
|
||||
return
|
||||
await tokenSchreiben(z.dienst, "")
|
||||
})()
|
||||
}}
|
||||
>
|
||||
Löschen
|
||||
|
||||
@@ -17,7 +17,8 @@ import type { Fahrt } from "../api"
|
||||
import { useDaten } from "../daten/DatenKontext"
|
||||
import { datum, dauer, de, streckeTeile, uhrzeit } from "../format"
|
||||
import type { SeitenName } from "../navigation"
|
||||
import { Leerzustand, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
||||
import { Leerzustand, Wertzeile, Werteliste } from "./bausteine"
|
||||
import { LOESCH_HINWEIS, LOESCH_TEXT, bestaetigen } from "./bestaetigung"
|
||||
import { FahrtFelderFormular, fahrtFelderAusFahrt, fahrtFelderAuswerten } from "./FahrtFelder"
|
||||
import { Karte } from "./Karte"
|
||||
|
||||
@@ -83,8 +84,9 @@ export function FahrtDetail({ id, geheZu }: { id: string | undefined; geheZu: (n
|
||||
void api.fahrtAktualisieren(fahrt.trip_id, { art: neu }).then(() => jetztAktualisieren())
|
||||
}
|
||||
|
||||
const loeschen = () => {
|
||||
if (!fahrt || !bestaetigen("Fahrt löschen?")) return
|
||||
const loeschen = async () => {
|
||||
if (!fahrt) return
|
||||
if (!(await bestaetigen(LOESCH_TEXT.trip, { text: LOESCH_HINWEIS.trip }))) return
|
||||
void api.fahrtLoeschen(fahrt.trip_id)
|
||||
geheZu("trips")
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { SeitenName } from "../navigation"
|
||||
import { Blattzeile, Leerzustand } from "./bausteine"
|
||||
import { FahrtFelderFormular, LEERE_FAHRT_FELDER, fahrtFelderAuswerten } from "./FahrtFelder"
|
||||
import type { FahrtFelderEingabe } from "./FahrtFelder"
|
||||
import { LOESCH_HINWEIS } from "./bestaetigung"
|
||||
import { Zeilenmenue } from "./Zeilenmenue"
|
||||
|
||||
/** Vorlage: strecke() im Panel - "Start → Ziel", nur eine Seite, oder
|
||||
@@ -103,6 +104,7 @@ export function Fahrten({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
|
||||
<Zeilenmenue
|
||||
key={fahrt.trip_id}
|
||||
loeschFrage={`Fahrt vom ${datum(fahrt.ts_start)} löschen?`}
|
||||
loeschHinweis={LOESCH_HINWEIS.trip}
|
||||
beiLoeschen={() => void api.fahrtLoeschen(fahrt.trip_id)}
|
||||
kinder={
|
||||
<Blattzeile
|
||||
|
||||
@@ -16,7 +16,8 @@ import { useDaten } from "../daten/DatenKontext"
|
||||
import type { Reifensatz, ReifenArchivEintrag } from "../daten/profilAdapter"
|
||||
import { datum, de, deOderStrich } from "../format"
|
||||
import { SymbolEdit, SymbolPaket, SymbolZahnradVoll } from "../symbole"
|
||||
import { BildMitMenue, WischLoeschen, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
||||
import { BildMitMenue, WischLoeschen, Wertzeile, Werteliste } from "./bausteine"
|
||||
import { LOESCH_HINWEIS, LOESCH_TEXT, bestaetigen } from "./bestaetigung"
|
||||
import { Datumsfeld } from "./Datumsfeld"
|
||||
import { terminUebernehmen } from "./kalender"
|
||||
|
||||
@@ -94,7 +95,14 @@ export function Reifen() {
|
||||
}
|
||||
|
||||
const archivieren = async (satzSchluessel: SatzSchluessel) => {
|
||||
if (!bestaetigen("Neue Räder anlegen?")) return
|
||||
if (
|
||||
!(await bestaetigen("Neue Räder anlegen?", {
|
||||
text: "Der jetzige Satz wandert mit seiner Laufleistung ins Archiv, der laufende beginnt bei 0 km.",
|
||||
knopf: "Anlegen",
|
||||
destruktiv: false,
|
||||
}))
|
||||
)
|
||||
return
|
||||
setzeLaeuft(true)
|
||||
try {
|
||||
// Eigener Dienst wie reifen_wechseln: archiviert den bisherigen Stand
|
||||
@@ -369,7 +377,7 @@ function ArchivZeile({ eintrag }: { eintrag: ReifenArchivEintrag }) {
|
||||
}
|
||||
|
||||
const loeschen = async () => {
|
||||
if (!bestaetigen("Diesen archivierten Reifensatz löschen?")) return
|
||||
if (!(await bestaetigen(LOESCH_TEXT.rfarch, { text: LOESCH_HINWEIS.rfarch }))) return
|
||||
setzeLaeuft(true)
|
||||
try {
|
||||
await api.reifenArchivLoeschen(eintrag.id)
|
||||
|
||||
@@ -30,8 +30,8 @@ import {
|
||||
Blattzeile,
|
||||
Leerzustand,
|
||||
Zahnradknopf,
|
||||
bestaetigen,
|
||||
} from "./bausteine"
|
||||
import { LOESCH_HINWEIS, LOESCH_TEXT, bestaetigen } from "./bestaetigung"
|
||||
import { Datumsfeld } from "./Datumsfeld"
|
||||
import { terminUebernehmen } from "./kalender"
|
||||
import { vcardParsen } from "./vcard"
|
||||
@@ -487,7 +487,7 @@ export function Servicebuch({
|
||||
}
|
||||
|
||||
const loeschen = async () => {
|
||||
if (!bestaetigen("Diesen Wartungsplan-Eintrag löschen?")) return
|
||||
if (!(await bestaetigen(LOESCH_TEXT.sbuch, { text: LOESCH_HINWEIS.sbuch }))) return
|
||||
await schreiben(buchRoh.filter((_, i) => i !== index))
|
||||
geheZu("service")
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ import { useDaten } from "../daten/DatenKontext"
|
||||
import { kartendienstUrl, tankstelleZeilen } from "../daten/tankstelle"
|
||||
import { datum, de, deOderStrich, eur, uhrzeit } from "../format"
|
||||
import type { SeitenName } from "../navigation"
|
||||
import { KRAFTSTOFFSORTEN, Leerzustand, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
||||
import { KRAFTSTOFFSORTEN, Leerzustand, Wertzeile, Werteliste } from "./bausteine"
|
||||
import { LOESCH_HINWEIS, LOESCH_TEXT, bestaetigen } from "./bestaetigung"
|
||||
import { markenlogoUrl } from "./bilder"
|
||||
import { BelegPopup } from "./BelegPopup"
|
||||
import { TankstellenKarte } from "./TankstellenKarte"
|
||||
@@ -44,8 +45,9 @@ export function TankDetail({ id, geheZu }: { id: string | undefined; geheZu: (na
|
||||
)
|
||||
}
|
||||
|
||||
const loeschen = () => {
|
||||
if (!tank || !bestaetigen("Tankvorgang löschen?")) return
|
||||
const loeschen = async () => {
|
||||
if (!tank) return
|
||||
if (!(await bestaetigen(LOESCH_TEXT.fill, { text: LOESCH_HINWEIS.fill }))) return
|
||||
void api.tankvorgangLoeschen(tank.tank_id)
|
||||
geheZu("fuel")
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { Blattzeile, KRAFTSTOFFSORTEN, Leerzustand } from "./bausteine"
|
||||
import { BelegPopup } from "./BelegPopup"
|
||||
import { markenlogoUrl } from "./bilder"
|
||||
import { LOESCH_HINWEIS } from "./bestaetigung"
|
||||
import { Zeilenmenue } from "./Zeilenmenue"
|
||||
|
||||
export function Tanken({ geheZu }: { geheZu: (name: SeitenName, id?: string) => void }) {
|
||||
@@ -118,6 +119,7 @@ export function Tanken({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
|
||||
<Zeilenmenue
|
||||
key={tank.tank_id}
|
||||
loeschFrage={`Tankvorgang vom ${datum(tank.ts)} löschen?`}
|
||||
loeschHinweis={LOESCH_HINWEIS.fill}
|
||||
beiLoeschen={() => void api.tankvorgangLoeschen(tank.tank_id)}
|
||||
kinder={
|
||||
<Blattzeile
|
||||
|
||||
@@ -15,17 +15,28 @@ import type { ReactNode } from "react"
|
||||
|
||||
import { SwipeRow } from "@audi-dash/ui"
|
||||
|
||||
import { bestaetigen } from "./bestaetigung"
|
||||
|
||||
export function Zeilenmenue({
|
||||
beiLoeschen,
|
||||
loeschFrage,
|
||||
loeschHinweis,
|
||||
kinder,
|
||||
}: {
|
||||
beiLoeschen: () => void
|
||||
loeschFrage: string
|
||||
/** Der erklaerende Satz unter der Frage - wie LOESCH_HINWEIS im Panel. */
|
||||
loeschHinweis?: string
|
||||
kinder: ReactNode
|
||||
}) {
|
||||
// Eigenes Blatt statt window.confirm: in eingebetteten Ansichten sind
|
||||
// Dialoge abgeschaltet, dort lief das Loeschen still ins Leere. Siehe
|
||||
// bestaetigung.tsx.
|
||||
const loeschen = () => {
|
||||
if (window.confirm(loeschFrage)) beiLoeschen()
|
||||
void (async () => {
|
||||
if (await bestaetigen(loeschFrage, loeschHinweis ? { text: loeschHinweis } : {}))
|
||||
beiLoeschen()
|
||||
})()
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useDaten } from "../daten/DatenKontext"
|
||||
import { SymbolZahnradVoll } from "../symbole"
|
||||
import { Bild } from "./Bild"
|
||||
import { bildAlsBase64, bildVerkleinern, bildVersionErneuern } from "./bilder"
|
||||
import { LOESCH_HINWEIS, LOESCH_TEXT, bestaetigen } from "./bestaetigung"
|
||||
|
||||
/** Kraftstoffsorten zur Auswahl - dieselbe feste Liste wie `KRAFTSTOFFSORTEN`
|
||||
im Panel, geteilt zwischen Tanken.tsx (Neuanlegen) und TankDetail.tsx
|
||||
@@ -163,11 +164,13 @@ export function BildMitMenue({
|
||||
destructive: true,
|
||||
onClick: () => {
|
||||
setzeOffen(false)
|
||||
if (!bestaetigen("Foto löschen?")) return
|
||||
void api.bildLoeschen(datei).then(() => {
|
||||
void (async () => {
|
||||
if (!(await bestaetigen(LOESCH_TEXT.bild, { text: LOESCH_HINWEIS.bild })))
|
||||
return
|
||||
await api.bildLoeschen(datei)
|
||||
bildVersionErneuern()
|
||||
return jetztAktualisieren()
|
||||
})
|
||||
await jetztAktualisieren()
|
||||
})()
|
||||
},
|
||||
},
|
||||
]}
|
||||
@@ -291,10 +294,6 @@ export function Werteliste({ kinder }: { kinder: ReactNode }) {
|
||||
}
|
||||
|
||||
/** Bestätigung vor unwiderruflichen Aktionen. */
|
||||
export function bestaetigen(frage: string): boolean {
|
||||
return window.confirm(frage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kreis-Häkchen (ok) / Kreis-Kreuz (nicht ok) / Kreis-Fragezeichen (unbekannt)
|
||||
* statt eines einfarbigen Punkts, für die Sicherheit-Seiten (Fahrzeugstatus,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Die Rückfrage ist ab jetzt das Tor vor JEDER löschenden Aktion der App -
|
||||
* elf Aufrufstellen. Deshalb hier festgehalten, was sie können muss.
|
||||
*
|
||||
* Der Anlass: bis zum 04.09.2026 war es `window.confirm`, und das kehrt in
|
||||
* eingebetteten Ansichten sofort mit `false` zurück - jede Löschaktion lief
|
||||
* dort still ins Leere ("der Knopf tut nichts", gemeldet vom Eigentümer).
|
||||
*/
|
||||
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
|
||||
import { Bestaetigungsblatt, bestaetigen } from "./bestaetigung"
|
||||
|
||||
describe("Rückfrage", () => {
|
||||
it("antwortet mit true, wenn der bestätigende Knopf gedrückt wird", async () => {
|
||||
render(<Bestaetigungsblatt />)
|
||||
const antwort = bestaetigen("Fahrt löschen?", { text: "Das lässt sich nicht widerrufen." })
|
||||
await waitFor(() => expect(screen.getByText("Fahrt löschen?")).toBeTruthy())
|
||||
expect(screen.getByText("Das lässt sich nicht widerrufen.")).toBeTruthy()
|
||||
fireEvent.click(screen.getByText("Löschen"))
|
||||
expect(await antwort).toBe(true)
|
||||
})
|
||||
|
||||
it("antwortet mit false bei Abbrechen und schliesst das Blatt", async () => {
|
||||
render(<Bestaetigungsblatt />)
|
||||
const antwort = bestaetigen("Tankvorgang löschen?")
|
||||
await waitFor(() => expect(screen.getByText("Tankvorgang löschen?")).toBeTruthy())
|
||||
fireEvent.click(screen.getByText("Abbrechen"))
|
||||
expect(await antwort).toBe(false)
|
||||
await waitFor(() => expect(screen.queryByText("Tankvorgang löschen?")).toBeNull())
|
||||
})
|
||||
|
||||
it("nimmt die Knopfbeschriftung und laesst sich unauffaellig stellen", async () => {
|
||||
render(<Bestaetigungsblatt />)
|
||||
const antwort = bestaetigen("Neue Räder anlegen?", { knopf: "Anlegen", destruktiv: false })
|
||||
await waitFor(() => expect(screen.getByText("Anlegen")).toBeTruthy())
|
||||
expect(screen.getByText("Anlegen").className).not.toContain("loeschen")
|
||||
fireEvent.click(screen.getByText("Anlegen"))
|
||||
expect(await antwort).toBe(true)
|
||||
})
|
||||
|
||||
it("faellt ohne gehaengtes Blatt auf den Systemdialog zurueck", async () => {
|
||||
// Eine Rückfrage, die niemand beantworten kann, wäre schlechter als der
|
||||
// Systemdialog - deshalb dieser Rückfall (Testlauf, Fremdeinbettung).
|
||||
const stub = vi.spyOn(window, "confirm").mockReturnValue(true)
|
||||
expect(await bestaetigen("Foto löschen?", { text: "Weg ist weg." })).toBe(true)
|
||||
expect(stub).toHaveBeenCalledWith("Foto löschen?\n\nWeg ist weg.")
|
||||
stub.mockRestore()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Rückfrage vor einer löschenden Aktion — als eigenes Blatt, nicht als
|
||||
* Browserdialog.
|
||||
*
|
||||
* WARUM NICHT window.confirm
|
||||
* --------------------------
|
||||
* Bis zum 04.09.2026 war `bestaetigen()` genau das. Drei Gründe dagegen,
|
||||
* einer davon gemessen:
|
||||
*
|
||||
* 1. Es funktioniert nicht überall. In eingebetteten Ansichten sind Dialoge
|
||||
* abgeschaltet — `window.confirm` kehrt dort sofort mit `false` zurück
|
||||
* (im Browser-Bereich dieser Sitzung nachgemessen: 1 ms). Jede löschende
|
||||
* Aktion der App lief damit still ins Leere, sichtbar als „der Knopf tut
|
||||
* nichts" (gemeldet vom Eigentümer für „Verbindung trennen").
|
||||
* 2. Es sieht nicht aus wie die App, sondern wie das Betriebssystem — und
|
||||
* das Panel zeigt an derselben Stelle seit jeher sein eigenes Blatt
|
||||
* (`bestaetigen()` -> `.sheet` in audi-dashboard-app.js).
|
||||
* 3. Es kennt nur Titel und einen festen Knopf. Das Panel stellt Frage,
|
||||
* erklärenden Satz und Knopfbeschriftung getrennt — `LOESCH_TEXT` und
|
||||
* `LOESCH_HINWEIS` unten sind wortgleich von dort übernommen.
|
||||
*
|
||||
* WARUM EIN SPEICHER UND KEIN HOOK JE AUFRUFSTELLE
|
||||
* ------------------------------------------------
|
||||
* Die Rückfrage steht an acht Stellen in sechs Komponenten. Ein Hook hätte
|
||||
* jede davon zwingen, das Blatt auch selbst zu rendern — acht Stellen, an
|
||||
* denen man es vergessen kann, ohne dass etwas auffällt (der Aufruf würde
|
||||
* einfach nie antworten). Stattdessen hängt hier ein winziger Speicher, das
|
||||
* Blatt wird EINMAL in App.tsx gehängt, und `bestaetigen()` bleibt für die
|
||||
* Aufrufstellen fast unverändert — nur mit `await` davor.
|
||||
*
|
||||
* Dasselbe Muster wie `theme.ts`: Zustand außerhalb des Komponentenbaums,
|
||||
* gelesen über `useSyncExternalStore`.
|
||||
*/
|
||||
|
||||
import { useEffect, useSyncExternalStore } from "react"
|
||||
|
||||
import { ActionSheet } from "@audi-dash/ui"
|
||||
|
||||
/** Wortgleich mit LOESCH_TEXT im Panel. */
|
||||
export const LOESCH_TEXT = {
|
||||
trip: "Fahrt löschen?",
|
||||
fill: "Tankvorgang löschen?",
|
||||
sbuch: "Wartungsplan-Eintrag löschen?",
|
||||
bild: "Foto löschen?",
|
||||
batt: "Messwert löschen?",
|
||||
rfarch: "Archivierten Radsatz löschen?",
|
||||
} as const
|
||||
|
||||
/** Wortgleich mit LOESCH_HINWEIS im Panel. */
|
||||
export const LOESCH_HINWEIS = {
|
||||
trip: "Die Fahrt wird aus dem Fahrtenbuch entfernt. Das lässt sich nicht widerrufen.",
|
||||
fill: "Der Tankvorgang und der zugehörige Beleg werden entfernt. Das lässt sich nicht widerrufen.",
|
||||
sbuch: "Der Eintrag wird aus dem Wartungsplan entfernt. Das lässt sich nicht widerrufen.",
|
||||
bild: "Das Foto wird vom Server gelöscht. Der Platz bleibt bestehen und kann neu belegt werden.",
|
||||
batt: "Der Tageseintrag wird aus der Messwertliste entfernt. Das lässt sich nicht widerrufen.",
|
||||
rfarch:
|
||||
"Der archivierte Radsatz wird mit seiner Laufleistung entfernt. Das lässt sich nicht widerrufen.",
|
||||
} as const
|
||||
|
||||
export interface Rueckfrage {
|
||||
/** Die Frage selbst, fett über den Knöpfen. */
|
||||
titel: string
|
||||
/** Ein erklärender Satz darunter — was passiert, und ob es umkehrbar ist. */
|
||||
text?: string
|
||||
/** Beschriftung des bestätigenden Knopfes. */
|
||||
knopf?: string
|
||||
/** Rot, wenn etwas entfernt oder überschrieben wird. */
|
||||
destruktiv?: boolean
|
||||
}
|
||||
|
||||
/** Alles ausser der Frage selbst - die steht als erstes Argument. */
|
||||
export type Zusatz = Omit<Rueckfrage, "titel">
|
||||
|
||||
interface Offen extends Rueckfrage {
|
||||
loesen: (antwort: boolean) => void
|
||||
}
|
||||
|
||||
let offen: Offen | null = null
|
||||
let gehaengt = false
|
||||
const hoerer = new Set<() => void>()
|
||||
|
||||
function melden() {
|
||||
for (const h of hoerer) h()
|
||||
}
|
||||
|
||||
function abonnieren(h: () => void) {
|
||||
hoerer.add(h)
|
||||
return () => {
|
||||
hoerer.delete(h)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fragt nach und antwortet, sobald der Nutzer entschieden hat.
|
||||
*
|
||||
* Ist das Blatt nirgends gehängt (Testlauf, Fremdeinbettung), fällt es auf
|
||||
* `window.confirm` zurück — eine Rückfrage, die niemand beantworten kann,
|
||||
* wäre schlechter als der Systemdialog.
|
||||
*/
|
||||
export function bestaetigen(titel: string, angaben: Zusatz | string = {}): Promise<boolean> {
|
||||
const rest: Zusatz = typeof angaben === "string" ? { text: angaben } : angaben
|
||||
if (!gehaengt) {
|
||||
return Promise.resolve(
|
||||
typeof window === "undefined" ? false : window.confirm(rest.text ? `${titel}\n\n${rest.text}` : titel),
|
||||
)
|
||||
}
|
||||
// Eine zweite Frage, während die erste noch steht, gibt es in der
|
||||
// Oberfläche nicht - käme sie doch, gilt die ältere als abgelehnt, statt
|
||||
// ihr Versprechen offen zu lassen.
|
||||
offen?.loesen(false)
|
||||
return new Promise<boolean>((loesen) => {
|
||||
offen = { ...rest, titel, loesen }
|
||||
melden()
|
||||
})
|
||||
}
|
||||
|
||||
function lesen() {
|
||||
return offen
|
||||
}
|
||||
|
||||
/** Einmal in der App gehängt (App.tsx). Rendert nichts, solange nichts gefragt ist. */
|
||||
export function Bestaetigungsblatt() {
|
||||
const anfrage = useSyncExternalStore(abonnieren, lesen, () => null)
|
||||
// Erst im Effekt, nicht waehrend des Renderns: bis dahin gilt der
|
||||
// Rueckfall, und das ist genau richtig - vor dem Haengen kann das Blatt
|
||||
// niemanden fragen.
|
||||
useEffect(() => {
|
||||
gehaengt = true
|
||||
return () => {
|
||||
gehaengt = false
|
||||
}
|
||||
}, [])
|
||||
const antworten = (antwort: boolean) => {
|
||||
const a = offen
|
||||
offen = null
|
||||
melden()
|
||||
a?.loesen(antwort)
|
||||
}
|
||||
return (
|
||||
<ActionSheet
|
||||
open={anfrage !== null}
|
||||
onClose={() => antworten(false)}
|
||||
title={anfrage?.titel ?? ""}
|
||||
{...(anfrage?.text ? { description: anfrage.text } : {})}
|
||||
actions={
|
||||
anfrage
|
||||
? [
|
||||
{
|
||||
label: anfrage.knopf ?? "Löschen",
|
||||
destructive: anfrage.destruktiv ?? true,
|
||||
onClick: () => antworten(true),
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"version":"2026.9.4.16","sha256":"451fba321daec6b7281c7a8298670486c33cb2c09d2f2af5405d894792f4185f","bytes":321475,"gebaut":"2026-09-04T11:55:53Z"}
|
||||
{"version":"2026.9.4.17","sha256":"72442f3c75c5a9a467d552e55ce3892686d7bdecb7ff147920bf3bbcb78cb1e3","bytes":322203,"gebaut":"2026-09-04T12:06:58Z"}
|
||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.9.4.16",
|
||||
"version": "2026.9.4.17",
|
||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||
"codeowners": [
|
||||
|
||||
Reference in New Issue
Block a user