"Datensicherung pruefen" steht in vier der sieben mitgelieferten Vorlagen, wortgleich. Wer den Ablauf aendert, aendert ihn viermal -- oder dreimal und vergisst den vierten. Entschieden wie im Issue vorgeschlagen: ein Entwurf **verweist** auf eine veroeffentlichte Baustein-Fassung, und beim Veroeffentlichen der Vorlage setzt der Dienst deren Schritte als tiefe Kopie ein. Danach sieht ein Lauf weiterhin konkrete Schritte, und Schrittergebnis, Druckansicht und Archiv muessen nichts von Bausteinen wissen. Die Alternative -- Laeufe verweisen zur Laufzeit -- haette die Unveraenderlichkeit an einer zweiten Stelle durchsetzen muessen. Eine neue Fassung aendert damit keine veroeffentlichte Vorlagenversion und keinen laufenden Lauf. Sie wirkt in der naechsten veroeffentlichten Version; die Oberflaeche sagt das an zwei Stellen, weil man sonst Durchschlag erwartet. Vier Garantien liegen in der Datenbank, nicht nur im Dienst: hoechstens ein Entwurf je Baustein, eine veroeffentlichte Fassung ist unveraenderlich, der Weg zurueck zum Entwurf ist versperrt, und ein Abschnitt kann nur auf eine veroeffentlichte Fassung verweisen. Der Titel kommt aus dem Baustein, nicht aus dem Abschnitt der Vorlage: sonst muesste er in jeder Vorlage einzeln gepflegt werden, und zwei Vorlagen zeigten denselben Baustein unter verschiedenen Namen. Ein Export traegt die aufgeloesten Schritte, keinen Verweis -- eine Kennung aus dieser Installation bedeutet in einer anderen nichts. Das Durchklicken des gebauten Stands fand einen Fehler, den die Suite nicht gesehen haette: die Platzhalter-Schritte eines Baustein-Abschnitts haben keinen Titel, wurden aber mitgeschickt, und SchrittSchema wies den ganzen Entwurf ab. Behoben, mit Test und Gegenprobe. Der ganze Weg danach am gebauten Stand nachgemessen: Baustein anlegen, veroeffentlichen, einsetzen, Vorlage veroeffentlichen -- die Schritte stehen drin, mit Herkunft und Protokolleintrag. Closes #50
558 lines
23 KiB
TypeScript
558 lines
23 KiB
TypeScript
"use client";
|
|
|
|
import { useActionState, useEffect, useMemo, useState } from "react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { ArrowLeft, Lock, Save, Send, TriangleAlert } from "lucide-react";
|
|
import { AbschnittKarte } from "@/components/vorlagen/editor-abschnitt";
|
|
import { Kennzeichen, TextKnopf } from "@/components/vorlagen/editor-felder";
|
|
import { SortierBereich, SortierZeile } from "@/components/vorlagen/editor-sortieren";
|
|
import { VariablenBereich } from "@/components/vorlagen/editor-variablen";
|
|
import { PlatzhalterHilfe } from "@/components/vorlagen/platzhalter-hilfe";
|
|
import { Vorschau } from "@/components/vorlagen/vorschau";
|
|
import {
|
|
alsEditorAufbau,
|
|
alsStruktur,
|
|
bausteineAnreichern,
|
|
neuerAbschnitt,
|
|
neuerBausteinAbschnitt,
|
|
neuerSchritt,
|
|
neueVariable,
|
|
umsortieren,
|
|
verwendetePlatzhalter,
|
|
type EditorAbschnitt,
|
|
type EditorAufbau,
|
|
type EditorSchritt,
|
|
type EditorVariable,
|
|
} from "@/lib/editor-aufbau";
|
|
import { unbekanntePlatzhalter } from "@/lib/platzhalter";
|
|
import { GRENZEN, type Struktur } from "@/lib/vorlagen-format";
|
|
import { entwurfSpeichernAktion, veroeffentlichenAktion } from "@/server/actions/vorlagen";
|
|
|
|
/**
|
|
* Der Vorlagen-Editor (#010).
|
|
*
|
|
* Haelt den ganzen Aufbau im Zustand und schickt ihn beim Speichern als ein
|
|
* Stueck JSON an `entwurfSpeichernAktion`. Kein Autosave: eine halb umgebaute
|
|
* Vorlage soll nicht nebenbei zur Wahrheit werden, und ein Speichern, das
|
|
* niemand ausgeloest hat, ueberschreibt im Zweifel die Arbeit eines anderen.
|
|
*
|
|
* Eine veroeffentlichte Version wird mit denselben Bauteilen gezeigt, nur mit
|
|
* `lesend`. Zwei Codepfade waeren zwei Gelegenheiten, dass die Ansicht etwas
|
|
* anderes zeigt als der Editor.
|
|
*/
|
|
|
|
export type EditorVersion = {
|
|
readonly id: string;
|
|
readonly vorlageId: string;
|
|
readonly vorlageName: string;
|
|
readonly version: number;
|
|
readonly veroeffentlicht: boolean;
|
|
readonly struktur: Struktur;
|
|
/** Stand beim Laden als ISO-Zeichenkette; geht beim Speichern zurueck. */
|
|
readonly stand: string;
|
|
readonly aenderungsnotiz: string | null;
|
|
};
|
|
|
|
/**
|
|
* Was der Editor ueber das Speichern wissen muss.
|
|
*
|
|
* Steckt in einem Zustand, weil beides ueber mehrere Versuche hinweg gelten
|
|
* muss: der `stand` des letzten erfolgreichen Speicherns und der Aufbau, der
|
|
* dabei geschrieben wurde. Nur so kann ein fehlgeschlagener zweiter Versuch
|
|
* nicht den Stand des ersten wieder vergessen.
|
|
*/
|
|
type SpeicherZustand = {
|
|
readonly fehler?: string;
|
|
readonly erfolg?: string;
|
|
readonly stand: string;
|
|
readonly gespeichertesJson: string;
|
|
};
|
|
|
|
export type EinsetzbarerBaustein = {
|
|
readonly bausteinVersionId: string;
|
|
readonly name: string;
|
|
readonly titel: string;
|
|
readonly version: number;
|
|
readonly anzahlSchritte: number;
|
|
};
|
|
|
|
export function Editor({
|
|
version,
|
|
lesend,
|
|
darfBearbeiten,
|
|
beispielDatum,
|
|
bausteine = [],
|
|
}: {
|
|
version: EditorVersion;
|
|
lesend: boolean;
|
|
darfBearbeiten: boolean;
|
|
/** Heutiges Datum, auf dem Server formatiert — siehe `beispielwerte`. */
|
|
beispielDatum: string;
|
|
/** Veröffentlichte Bausteine, die sich einsetzen lassen (#030). */
|
|
bausteine?: readonly EinsetzbarerBaustein[];
|
|
}) {
|
|
const router = useRouter();
|
|
const [aufbau, setzeAufbau] = useState<EditorAufbau>(() =>
|
|
bausteineAnreichern(alsEditorAufbau(version.struktur), bausteine),
|
|
);
|
|
const aktuellesJson = useMemo(() => JSON.stringify(alsStruktur(aufbau)), [aufbau]);
|
|
|
|
/**
|
|
* Der geladene Aufbau, einmal durch den Editor gedreht.
|
|
*
|
|
* Der Vergleichspunkt fuer „nicht gespeichert“ muss dieselbe Reise gemacht
|
|
* haben wie der Zustand: ein Titel, der seine mitgespeicherte Nummer verliert,
|
|
* ist keine Aenderung des Benutzers. Die Angleichung wandert mit dem naechsten
|
|
* Speichern in die Datenbank.
|
|
*/
|
|
const anfangsJson = useMemo(
|
|
() => JSON.stringify(alsStruktur(bausteineAnreichern(alsEditorAufbau(version.struktur), bausteine))),
|
|
[version.struktur, bausteine],
|
|
);
|
|
|
|
/**
|
|
* Das Speichern haengt sich um die Server Action und merkt sich zwei Dinge,
|
|
* die sie nicht zurueckgibt: den letzten guten `stand` und den Aufbau, der
|
|
* damit geschrieben wurde. Der Aufbau kommt aus dem abgeschickten Formular —
|
|
* damit ist es genau der, den der Server gesehen hat, und nicht der, an dem
|
|
* jemand waehrend der Anfrage weitergeschrieben hat.
|
|
*/
|
|
async function speichernMitStand(vorher: SpeicherZustand, formular: FormData): Promise<SpeicherZustand> {
|
|
const ergebnis = await entwurfSpeichernAktion({}, formular);
|
|
const gesendet = formular.get("struktur");
|
|
return {
|
|
...ergebnis,
|
|
stand: ergebnis.stand ?? vorher.stand,
|
|
gespeichertesJson: ergebnis.stand && typeof gesendet === "string" ? gesendet : vorher.gespeichertesJson,
|
|
};
|
|
}
|
|
|
|
const [speicherErgebnis, speichern, speichertGerade] = useActionState(speichernMitStand, {
|
|
stand: version.stand,
|
|
gespeichertesJson: anfangsJson,
|
|
});
|
|
const [freigabeErgebnis, veroeffentlichen, veroeffentlichtGerade] = useActionState(
|
|
veroeffentlichenAktion,
|
|
{},
|
|
);
|
|
|
|
const nichtGespeichert = aktuellesJson !== speicherErgebnis.gespeichertesJson;
|
|
|
|
/**
|
|
* Die zuletzt angelegte Zeile wird aufgeklappt gezeigt — alles andere bleibt
|
|
* zu. Nur die neueste: aeltere Karten haengen laengst an ihrem eigenen
|
|
* Zustand, den sie beim Anlegen bekommen haben, und behalten ihn.
|
|
*/
|
|
const [zuletztAngelegt, setzeZuletztAngelegt] = useState<string | null>(null);
|
|
|
|
// Nach dem Veroeffentlichen ist diese Version unveraenderlich. Die Seite muss
|
|
// neu geladen werden, damit der Editor in den Lesemodus geht; ein
|
|
// revalidatePath auf /vorlagen erreicht diese Adresse nicht.
|
|
useEffect(() => {
|
|
if (freigabeErgebnis.erfolg) router.refresh();
|
|
}, [freigabeErgebnis, router]);
|
|
|
|
/**
|
|
* Der Browser fragt vor dem Verlassen nach, solange etwas nicht gespeichert
|
|
* ist. Greift nur bei echtem Verlassen der Seite, nicht bei einem Klick in der
|
|
* Navigation — deshalb steht das Kennzeichen zusaetzlich sichtbar oben.
|
|
*/
|
|
useEffect(() => {
|
|
if (lesend || !nichtGespeichert) return undefined;
|
|
const warnen = (ereignis: BeforeUnloadEvent) => ereignis.preventDefault();
|
|
window.addEventListener("beforeunload", warnen);
|
|
return () => window.removeEventListener("beforeunload", warnen);
|
|
}, [lesend, nichtGespeichert]);
|
|
|
|
const verwendet = verwendetePlatzhalter(aufbau);
|
|
const unbekannt = unbekanntePlatzhalter(
|
|
verwendet,
|
|
aufbau.variablen.map((variable) => variable.schluessel.trim()),
|
|
);
|
|
const konflikt = speicherErgebnis.fehler?.includes("zwischenzeitlich") === true;
|
|
|
|
// Abschnitte
|
|
function abschnitteSetzen(neue: readonly EditorAbschnitt[]) {
|
|
setzeAufbau((alt) => ({ ...alt, abschnitte: neue }));
|
|
}
|
|
function abschnittAendern(kennung: string, teil: Partial<EditorAbschnitt>) {
|
|
setzeAufbau((alt) => ({
|
|
...alt,
|
|
abschnitte: alt.abschnitte.map((abschnitt) =>
|
|
abschnitt.kennung === kennung ? { ...abschnitt, ...teil } : abschnitt,
|
|
),
|
|
}));
|
|
}
|
|
function schritteSetzen(
|
|
abschnittKennung: string,
|
|
aendere: (schritte: readonly EditorSchritt[]) => readonly EditorSchritt[],
|
|
) {
|
|
setzeAufbau((alt) => ({
|
|
...alt,
|
|
abschnitte: alt.abschnitte.map((abschnitt) =>
|
|
abschnitt.kennung === abschnittKennung
|
|
? { ...abschnitt, schritte: aendere(abschnitt.schritte) }
|
|
: abschnitt,
|
|
),
|
|
}));
|
|
}
|
|
|
|
// Variablen
|
|
function variablenSetzen(neue: readonly EditorVariable[]) {
|
|
setzeAufbau((alt) => ({ ...alt, variablen: neue }));
|
|
}
|
|
|
|
const abschnitteVoll = aufbau.abschnitte.length >= GRENZEN.abschnitte;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6">
|
|
<header className="flex flex-col gap-3">
|
|
<Link
|
|
href={`/vorlagen/${version.vorlageId}`}
|
|
className="flex min-h-11 items-center gap-2 self-start text-sm text-nebentext hover:text-text md:min-h-10"
|
|
>
|
|
<ArrowLeft className="size-[18px]" strokeWidth={1.75} aria-hidden="true" />
|
|
Zur Vorlage
|
|
</Link>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<h1 className="text-2xl font-bold">{version.vorlageName}</h1>
|
|
<Kennzeichen>Version {version.version}</Kennzeichen>
|
|
<Kennzeichen ton={version.veroeffentlicht ? "akzent" : "still"}>
|
|
{version.veroeffentlicht ? "veröffentlicht" : "Entwurf"}
|
|
</Kennzeichen>
|
|
{lesend ? null : nichtGespeichert ? (
|
|
<Kennzeichen ton="faellig">Nicht gespeichert</Kennzeichen>
|
|
) : (
|
|
<Kennzeichen>gespeichert</Kennzeichen>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
{lesend ? (
|
|
<div className="flex items-start gap-3 rounded-karte border border-linie bg-flaeche p-4">
|
|
<Lock
|
|
className="mt-0.5 size-[18px] shrink-0 text-nebentext"
|
|
strokeWidth={1.75}
|
|
aria-hidden="true"
|
|
/>
|
|
<p className="max-w-prose text-sm text-nebentext">
|
|
{version.veroeffentlicht ? (
|
|
<>
|
|
Diese Version ist veröffentlicht und wird nicht mehr verändert — Läufe hängen daran.{" "}
|
|
{darfBearbeiten ? (
|
|
<>
|
|
„Bearbeiten“ auf der{" "}
|
|
<Link
|
|
href={`/vorlagen/${version.vorlageId}`}
|
|
className="text-akzent underline underline-offset-2"
|
|
>
|
|
Vorlagenseite
|
|
</Link>{" "}
|
|
erzeugt daraus einen neuen Entwurf; je Vorlage gibt es höchstens einen.
|
|
</>
|
|
) : (
|
|
"Ein neuer Entwurf entsteht über „Bearbeiten“ auf der Vorlagenseite; dafür braucht es die Rolle Redakteur oder Administrator."
|
|
)}
|
|
</>
|
|
) : (
|
|
"Dieser Entwurf lässt sich ansehen, aber nicht ändern — dafür braucht es die Rolle Redakteur oder Administrator."
|
|
)}
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
|
|
{unbekannt.length > 0 ? (
|
|
<div className="flex items-start gap-3 rounded-karte border border-linie bg-flaeche p-4">
|
|
<TriangleAlert
|
|
className="mt-0.5 size-[18px] shrink-0 text-faellig"
|
|
strokeWidth={1.75}
|
|
aria-hidden="true"
|
|
/>
|
|
<div className="min-w-0">
|
|
<p className="text-sm text-faellig">
|
|
{unbekannt.length === 1
|
|
? "Ein Platzhalter gehört zu keiner Variablen und zu keinem eingebauten Wert:"
|
|
: `${unbekannt.length} Platzhalter gehören zu keiner Variablen und zu keinem eingebauten Wert:`}
|
|
</p>
|
|
<ul className="mt-2 flex flex-wrap gap-2">
|
|
{unbekannt.map((schluessel) => (
|
|
<li key={schluessel}>
|
|
<code className="mono rounded-pille bg-flaeche-hoch px-2.5 py-1 text-xs text-faellig">
|
|
{`{{${schluessel}}}`}
|
|
</code>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<p className="mt-2 max-w-prose text-sm text-nebentext">
|
|
Im Lauf bleibt ein unbekannter Platzhalter als Text stehen. Speichern und Veröffentlichen sind
|
|
trotzdem möglich — vielleicht kommt die Variable ja noch.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<VariablenBereich
|
|
variablen={aufbau.variablen}
|
|
verwendet={verwendet}
|
|
lesend={lesend}
|
|
zuletztAngelegt={zuletztAngelegt}
|
|
aendern={(kennung, teil) =>
|
|
variablenSetzen(
|
|
aufbau.variablen.map((variable) =>
|
|
variable.kennung === kennung ? { ...variable, ...teil } : variable,
|
|
),
|
|
)
|
|
}
|
|
hinzufuegen={() => {
|
|
const variable = neueVariable();
|
|
setzeZuletztAngelegt(variable.kennung);
|
|
variablenSetzen([...aufbau.variablen, variable]);
|
|
}}
|
|
loeschen={(kennung) =>
|
|
variablenSetzen(aufbau.variablen.filter((variable) => variable.kennung !== kennung))
|
|
}
|
|
umsortieren={(von, nach) => variablenSetzen(umsortieren(aufbau.variablen, von, nach))}
|
|
/>
|
|
|
|
<section className="flex flex-col gap-3">
|
|
<h2 className="text-lg font-semibold">Abschnitte</h2>
|
|
{aufbau.abschnitte.length === 0 ? (
|
|
<p className="text-sm text-nebentext">
|
|
Noch kein Abschnitt. Ein Abschnitt fasst die Schritte zusammen, die zusammengehören.
|
|
</p>
|
|
) : (
|
|
<SortierBereich
|
|
kennungen={aufbau.abschnitte.map((abschnitt) => abschnitt.kennung)}
|
|
art="Abschnitt"
|
|
lesend={lesend}
|
|
aufSortiert={(von, nach) => abschnitteSetzen(umsortieren(aufbau.abschnitte, von, nach))}
|
|
>
|
|
<ul className="flex flex-col gap-4">
|
|
{aufbau.abschnitte.map((abschnitt, index) => (
|
|
<SortierZeile key={abschnitt.kennung} kennung={abschnitt.kennung} lesend={lesend}>
|
|
{(anfasser) => (
|
|
<AbschnittKarte
|
|
abschnitt={abschnitt}
|
|
index={index}
|
|
anzahl={aufbau.abschnitte.length}
|
|
lesend={lesend}
|
|
zuletztAngelegt={zuletztAngelegt}
|
|
anfasser={anfasser}
|
|
aendern={(teil) => abschnittAendern(abschnitt.kennung, teil)}
|
|
loeschen={() =>
|
|
abschnitteSetzen(
|
|
aufbau.abschnitte.filter((eintrag) => eintrag.kennung !== abschnitt.kennung),
|
|
)
|
|
}
|
|
aufSortiert={(von, nach) => abschnitteSetzen(umsortieren(aufbau.abschnitte, von, nach))}
|
|
schrittAendern={(kennung, teil) =>
|
|
schritteSetzen(abschnitt.kennung, (schritte) =>
|
|
schritte.map((schritt) =>
|
|
schritt.kennung === kennung ? { ...schritt, ...teil } : schritt,
|
|
),
|
|
)
|
|
}
|
|
schrittHinzufuegen={() => {
|
|
const schritt = neuerSchritt();
|
|
setzeZuletztAngelegt(schritt.kennung);
|
|
schritteSetzen(abschnitt.kennung, (schritte) => [...schritte, schritt]);
|
|
}}
|
|
schrittLoeschen={(kennung) =>
|
|
schritteSetzen(abschnitt.kennung, (schritte) =>
|
|
schritte.filter((schritt) => schritt.kennung !== kennung),
|
|
)
|
|
}
|
|
schritteUmsortieren={(von, nach) =>
|
|
schritteSetzen(abschnitt.kennung, (schritte) => umsortieren(schritte, von, nach))
|
|
}
|
|
/>
|
|
)}
|
|
</SortierZeile>
|
|
))}
|
|
</ul>
|
|
</SortierBereich>
|
|
)}
|
|
{lesend ? null : (
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<TextKnopf
|
|
beschriftung={
|
|
abschnitteVoll ? `Höchstens ${GRENZEN.abschnitte} Abschnitte` : "Abschnitt hinzufügen"
|
|
}
|
|
aus={abschnitteVoll}
|
|
aufKlick={() => {
|
|
const abschnitt = neuerAbschnitt();
|
|
setzeZuletztAngelegt(abschnitt.kennung);
|
|
abschnitteSetzen([...aufbau.abschnitte, abschnitt]);
|
|
}}
|
|
/>
|
|
{/*
|
|
Bausteine nur anbieten, wenn es welche gibt. Ein leeres
|
|
Auswahlfeld mit „keine Bausteine“ wäre eine Frage ohne Antwort
|
|
(#030).
|
|
*/}
|
|
{bausteine.length > 0 ? (
|
|
<label className="flex min-w-0 items-center gap-2 text-sm">
|
|
<span className="shrink-0 text-nebentext">Baustein einsetzen</span>
|
|
<select
|
|
aria-label="Baustein einsetzen"
|
|
value=""
|
|
disabled={abschnitteVoll}
|
|
onChange={(ereignis) => {
|
|
const gewaehlt = bausteine.find(
|
|
(eintrag) => eintrag.bausteinVersionId === ereignis.target.value,
|
|
);
|
|
if (!gewaehlt) return;
|
|
const abschnitt = neuerBausteinAbschnitt(gewaehlt);
|
|
setzeZuletztAngelegt(abschnitt.kennung);
|
|
abschnitteSetzen([...aufbau.abschnitte, abschnitt]);
|
|
}}
|
|
className="min-h-11 min-w-0 rounded-pille border border-linie bg-flaeche-hoch px-4 text-sm md:min-h-10"
|
|
>
|
|
<option value="">auswählen …</option>
|
|
{bausteine.map((eintrag) => (
|
|
<option key={eintrag.bausteinVersionId} value={eintrag.bausteinVersionId}>
|
|
{eintrag.name} (Fassung {eintrag.version}, {eintrag.anzahlSchritte}{" "}
|
|
{eintrag.anzahlSchritte === 1 ? "Schritt" : "Schritte"})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
{/* Ohne Set stuende ein doppelt vergebener Schluessel zweimal in der Hilfe. */}
|
|
<PlatzhalterHilfe
|
|
variablen={[
|
|
...new Set(
|
|
aufbau.variablen
|
|
.map((variable) => variable.schluessel.trim())
|
|
.filter((schluessel) => schluessel !== ""),
|
|
),
|
|
]}
|
|
/>
|
|
|
|
<Vorschau aufbau={aufbau} beispielDatum={beispielDatum} />
|
|
|
|
{lesend ? null : (
|
|
<>
|
|
<section className="flex flex-col gap-3 rounded-karte border border-linie bg-flaeche p-4">
|
|
<h2 className="text-lg font-semibold">Veröffentlichen</h2>
|
|
<p className="max-w-prose text-sm text-nebentext">
|
|
Aus dem Entwurf wird Version {version.version}. Sie ist danach unveränderlich, und Läufe können
|
|
daran starten. Gespeichert wird veröffentlicht — nicht, was hier gerade auf dem Bildschirm
|
|
steht.
|
|
</p>
|
|
<form action={veroeffentlichen} className="flex flex-col gap-3">
|
|
<input type="hidden" name="versionId" value={version.id} />
|
|
<label className="flex flex-col gap-1.5 text-sm">
|
|
<span className="text-nebentext">Änderungsnotiz (optional)</span>
|
|
<textarea
|
|
name="aenderungsnotiz"
|
|
rows={2}
|
|
defaultValue={version.aenderungsnotiz ?? ""}
|
|
maxLength={GRENZEN.text}
|
|
className="min-h-11 w-full rounded-karte border border-linie bg-grund px-3 py-2 text-sm text-text outline-none focus-visible:border-akzent"
|
|
/>
|
|
</label>
|
|
{nichtGespeichert ? (
|
|
<p className="flex items-start gap-2 text-sm text-faellig">
|
|
<TriangleAlert className="mt-0.5 size-4 shrink-0" strokeWidth={1.75} aria-hidden="true" />
|
|
Es gibt nicht gespeicherte Änderungen. Sie wären in der veröffentlichten Version nicht
|
|
enthalten — erst speichern.
|
|
</p>
|
|
) : null}
|
|
{freigabeErgebnis.fehler ? (
|
|
<p role="alert" className="text-sm text-fehler">
|
|
{freigabeErgebnis.fehler}
|
|
</p>
|
|
) : null}
|
|
{freigabeErgebnis.erfolg ? (
|
|
<p role="status" className="text-sm text-erledigt">
|
|
{freigabeErgebnis.erfolg}
|
|
</p>
|
|
) : null}
|
|
<button
|
|
type="submit"
|
|
disabled={veroeffentlichtGerade}
|
|
className="flex min-h-11 items-center gap-2 self-start rounded-pille border border-linie px-5 text-sm font-semibold text-akzent hover:bg-akzent-flaeche disabled:pointer-events-none disabled:opacity-40 md:min-h-10"
|
|
>
|
|
<Send className="size-[18px]" strokeWidth={1.75} aria-hidden="true" />
|
|
{veroeffentlichtGerade ? "Wird veröffentlicht …" : "Veröffentlichen"}
|
|
</button>
|
|
</form>
|
|
</section>
|
|
|
|
<form
|
|
action={speichern}
|
|
className="sticky bottom-0 z-10 -mx-4 flex flex-col gap-2 border-t border-linie bg-flaeche px-4 py-3 md:-mx-8 md:px-8"
|
|
>
|
|
<input type="hidden" name="versionId" value={version.id} />
|
|
<input type="hidden" name="stand" value={speicherErgebnis.stand} />
|
|
<input type="hidden" name="struktur" value={aktuellesJson} />
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<button
|
|
type="submit"
|
|
disabled={speichertGerade}
|
|
className="flex min-h-11 items-center gap-2 rounded-pille bg-akzent px-5 text-sm font-semibold text-auf-akzent hover:bg-akzent/80 disabled:pointer-events-none disabled:opacity-40 md:min-h-10"
|
|
>
|
|
<Save className="size-[18px]" strokeWidth={2} aria-hidden="true" />
|
|
{speichertGerade ? "Wird gespeichert …" : "Speichern"}
|
|
</button>
|
|
<span className={`text-sm ${nichtGespeichert ? "text-faellig" : "text-nebentext"}`}>
|
|
{nichtGespeichert ? "Nicht gespeicherte Änderungen" : "Alles gespeichert"}
|
|
</span>
|
|
</div>
|
|
{speicherErgebnis.fehler ? (
|
|
<p role="alert" className="text-sm text-fehler">
|
|
{speicherErgebnis.fehler}
|
|
</p>
|
|
) : null}
|
|
{speicherErgebnis.erfolg && !nichtGespeichert ? (
|
|
<p role="status" className="text-sm text-erledigt">
|
|
{speicherErgebnis.erfolg}
|
|
</p>
|
|
) : null}
|
|
{/*
|
|
* Der Rettungsanker haengt an **jedem** fehlgeschlagenen Speichern,
|
|
* nicht nur am Wettlauf zweier Redakteure. Wer eine Stunde an einer
|
|
* Vorlage gesessen hat, soll seinen Aufbau herauskopieren koennen,
|
|
* egal woran es lag — an einer abgelaufenen Anmeldung, an einem
|
|
* leeren Pflichtfeld oder an der Datenbank.
|
|
*/}
|
|
{speicherErgebnis.fehler ? (
|
|
<div className="flex flex-col gap-2">
|
|
{konflikt ? (
|
|
<p className="max-w-prose text-sm text-nebentext">
|
|
Die Eingaben stehen noch hier und sind nicht verloren — ein Neuladen dieser Seite würde
|
|
sie allerdings verwerfen. Am besten den Entwurf in einem zweiten Tab öffnen, die fremde
|
|
Fassung ansehen und die eigenen Änderungen dort nachtragen.
|
|
</p>
|
|
) : (
|
|
<p className="max-w-prose text-sm text-nebentext">
|
|
Die Eingaben stehen noch hier und sind nicht verloren — ein Neuladen dieser Seite würde
|
|
sie allerdings verwerfen.
|
|
</p>
|
|
)}
|
|
<details>
|
|
<summary className="flex min-h-11 cursor-pointer list-none items-center text-sm text-nebentext hover:text-text md:min-h-10">
|
|
Aufbau als JSON herauskopieren
|
|
</summary>
|
|
<textarea
|
|
readOnly
|
|
value={aktuellesJson}
|
|
rows={6}
|
|
className="mono mt-1 w-full rounded-karte border border-linie bg-grund p-3 text-xs text-nebentext"
|
|
/>
|
|
</details>
|
|
</div>
|
|
) : null}
|
|
</form>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|