Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
'use client'
|
|
|
|
import { createContext, useContext, useRef, useState, type ReactNode } from 'react'
|
|
import { sectionText } from '@/lib/i18n/cases'
|
|
import type { SaveSectionResult, SaveSections } from './types'
|
|
|
|
/**
|
|
* Das Formular um die Abschnitte herum: **ein** Knopf für das ganze Blatt.
|
|
*
|
|
* Vorher trug jedes Feld seinen eigenen. Einen Arbeitsvertrag auszufüllen
|
|
* hiess damit zwölfmal speichern, und wer eines vergass, merkte es erst beim
|
|
* Nachlesen. Jetzt sammelt dieses Formular ein, was sich geändert hat, und
|
|
* schickt es in einem Zug.
|
|
*
|
|
* Gespeichert wird **je Feld** und nicht als Ganzes. Das ist kein Versehen:
|
|
* Abschnitte sind einzeln versioniert, und wenn eines zwischenzeitlich von
|
|
* jemand anderem geändert wurde, soll das die übrigen nicht aufhalten. Die
|
|
* Rückmeldung sagt dann genau, was durchging und was noch offen ist.
|
|
*
|
|
* Die Felder melden sich hier an, statt ihren Zustand nach oben zu geben:
|
|
* so löst ein Tastendruck in einem Feld kein Neuzeichnen des ganzen Blattes
|
|
* aus.
|
|
*/
|
|
export type FieldHandle = {
|
|
fieldKey: string
|
|
/** Der geänderte Wert, oder null wenn unverändert. */
|
|
collect: () => { fieldKey: string; value: unknown; baseVersion: number } | null
|
|
apply: (result: SaveSectionResult) => void
|
|
}
|
|
|
|
type Kontext = {
|
|
register: (handle: FieldHandle) => () => void
|
|
changed: (fieldKey: string, dirty: boolean) => void
|
|
pending: boolean
|
|
}
|
|
|
|
const SectionsContext = createContext<Kontext | null>(null)
|
|
|
|
export function useSectionsForm(): Kontext | null {
|
|
return useContext(SectionsContext)
|
|
}
|
|
|
|
export function SectionsForm({
|
|
save,
|
|
label,
|
|
children,
|
|
}: {
|
|
save: SaveSections
|
|
/** „Dokument speichern" oder „Abschnitte speichern" - je nachdem, was es ist. */
|
|
label: string
|
|
children: ReactNode
|
|
}) {
|
|
const felder = useRef(new Map<string, FieldHandle>())
|
|
const [geaendert, setGeaendert] = useState<Set<string>>(new Set())
|
|
const [pending, setPending] = useState(false)
|
|
const [meldung, setMeldung] = useState<string | null>(null)
|
|
|
|
const kontext: Kontext = {
|
|
register(handle) {
|
|
felder.current.set(handle.fieldKey, handle)
|
|
return () => {
|
|
felder.current.delete(handle.fieldKey)
|
|
}
|
|
},
|
|
changed(fieldKey, dirty) {
|
|
setGeaendert((bisher) => {
|
|
if (dirty === bisher.has(fieldKey)) return bisher
|
|
const naechste = new Set(bisher)
|
|
if (dirty) naechste.add(fieldKey)
|
|
else naechste.delete(fieldKey)
|
|
return naechste
|
|
})
|
|
},
|
|
pending,
|
|
}
|
|
|
|
async function speichern() {
|
|
const eintraege = [...felder.current.values()]
|
|
.map((handle) => handle.collect())
|
|
.filter((eintrag) => eintrag !== null)
|
|
|
|
if (eintraege.length === 0) {
|
|
setMeldung(sectionText.unchanged)
|
|
return
|
|
}
|
|
|
|
setPending(true)
|
|
setMeldung(null)
|
|
try {
|
|
const ergebnisse = await save(eintraege)
|
|
|
|
let gut = 0
|
|
let offen = 0
|
|
for (const [fieldKey, ergebnis] of Object.entries(ergebnisse)) {
|
|
felder.current.get(fieldKey)?.apply(ergebnis)
|
|
if (ergebnis.ok) gut++
|
|
else offen++
|
|
}
|
|
|
|
setMeldung(offen === 0 ? sectionText.savedCount(gut) : sectionText.partly(gut, offen))
|
|
} finally {
|
|
setPending(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<SectionsContext.Provider value={kontext}>
|
|
{children}
|
|
|
|
<div className="sticky bottom-0 mt-8 flex flex-wrap items-center gap-4 border-t border-line bg-surface py-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => void speichern()}
|
|
disabled={pending}
|
|
aria-busy={pending}
|
|
className="btn btn-primary"
|
|
>
|
|
{pending ? sectionText.saving : label}
|
|
</button>
|
|
<span className="font-condensed text-2xs text-text-dim">
|
|
{geaendert.size > 0 ? sectionText.changedCount(geaendert.size) : sectionText.unchanged}
|
|
</span>
|
|
{meldung ? <span className="font-condensed text-2xs text-accent">{meldung}</span> : null}
|
|
</div>
|
|
</SectionsContext.Provider>
|
|
)
|
|
}
|