feat(paket b): Umbenennen erreicht alle — PATCH /title und RENAMED im Feed (D85)

Backend: PATCH /documents/{id}/title mit expectedVersion (409 bei
veralteter Version, 400 bei leerem/zu langem Titel), neuer ChangeType
RENAMED (strukturell, immer Meilenstein), der Feed stellt den neuen Titel
im Klartext zu (ChangeEvent.title). Unter derselben Stripe-Sperre wie die
Inhalts-Patches; Owner-Vormerkung in der API-Beschreibung. Vier neue
Cucumber-Szenarien, zwei Unit-Tests.

Frontend: Der Zeilen-Stift eines Server-Dokuments benennt über den Server
um (optimistisch, 409-Retry, Rücknahme + Warnung bei Fehlschlag); fremde
Umbenennungen kommen als RENAMED über den Feed in Chip und Menü.
URL-Dokumente verlieren den Stift — ihr Name ist die URL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-27 08:13:25 +02:00
co-authored by Claude Fable 5
parent c438a88344
commit fbe9b1fd22
16 changed files with 380 additions and 12 deletions
+6 -2
View File
@@ -10,8 +10,12 @@ Kommandos in README.md hier. Live-Editing (D76,
`docs/live-editing-proposal.md`) ist in Arbeit: Schritte 15 der Reihenfolge
dort sind gebaut (Zeilen-Diff in `de.werkbaum.diff`, Historie in zwei Ebenen,
`PATCH /content` und der Änderungsfeed im `LiveEditingService`,
Master-Passwort für die Dokumentenliste); offen sind `PATCH /title` und der
Client im Frontend.
Master-Passwort für die Dokumentenliste, `PATCH /title` samt RENAMED-Ereignis
im Feed — D85), der Client im Frontend ist verdrahtet (D76-Nachtrag 7).
Verwaltungs-Aktionen (Umbenennen, künftig Löschen) perspektivisch an das
geplante Owner-Passwort binden (`#col.live.owner` im Plan) — Endpunkte so
schneiden, dass die Berechtigungsprüfung dazukommen kann, ohne die Signatur
zu brechen.
## Konventionen
- Kotlin, **Spring Boot 4**, Gradle (Kotlin DSL), JDK 21.
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
2026-08-27 08:10:36.668320+02:00 jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "DATABASECHANGELOGLOCK" not found; SQL statement:
SELECT COUNT(*) FROM PUBLIC.DATABASECHANGELOGLOCK [42102-240]
@@ -11,6 +11,7 @@ import de.werkbaum.generated.model.DocumentHistoryEntry as ApiHistoryEntry
import de.werkbaum.generated.model.DocumentUpdateRequest
import de.werkbaum.generated.model.RestoreRequest
import de.werkbaum.generated.model.ServiceInfo
import de.werkbaum.generated.model.TitlePatchRequest
import de.werkbaum.domain.ChangeAuthor
import de.werkbaum.domain.ChangeEvent
import de.werkbaum.domain.ChangeFeed
@@ -137,6 +138,18 @@ class DocumentsController(
.body(feed?.toApi())
}
override fun patchDocumentTitle(
documentId: UUID,
titlePatchRequest: TitlePatchRequest,
): ResponseEntity<ApiDocument> {
val renamed = liveEditing.renameDocument(
documentId,
titlePatchRequest.title,
titlePatchRequest.expectedVersion,
)
return ResponseEntity.ok(renamed.toApi())
}
override fun getDocumentHistory(documentId: UUID): ResponseEntity<List<ApiHistoryEntry>> =
ResponseEntity.ok(service.history(documentId).map { it.toApi() })
@@ -170,6 +183,7 @@ class DocumentsController(
changeType = ApiChangeEvent.ChangeType.valueOf(changeType.name),
clientId = author?.clientId,
displayName = author?.displayName,
title = title,
)
private fun DocumentHistoryEntry.toApi(): ApiHistoryEntry = ApiHistoryEntry(
@@ -2,11 +2,16 @@ package de.werkbaum.domain
import de.werkbaum.diff.LineOp
/** Was an einer Version geschehen ist und wer sie eingereicht hat. */
/**
* Was an einer Version geschehen ist und wer sie eingereicht hat.
* [title] nur bei [ChangeType.RENAMED]: der neue Titel im Klartext, damit der
* Client ihn ohne weiteren Abruf übernehmen kann (D76/D85).
*/
data class ChangeEvent(
val version: Long,
val changeType: ChangeType,
val author: ChangeAuthor? = null,
val title: String? = null,
)
/**
@@ -11,8 +11,11 @@ import java.util.UUID
* eines **lebenden** Dokuments auf eine alte Version; für den Client ein
* gewöhnlicher Inhaltswechsel. Beide trugen früher denselben Typ; ein Typ, der
* zwei Dinge bedeutet, ist die Unschärfe, aus der später Fehler werden (D76).
*
* [RENAMED] ändert nur den Titel — ein Metadatum, kein Zeileninhalt (D76/D85);
* der Feed stellt den neuen Titel im Klartext zu, der Inhalt bleibt.
*/
enum class ChangeType { CREATED, UPDATED, DELETED, RESTORED, ROLLED_BACK }
enum class ChangeType { CREATED, UPDATED, RENAMED, DELETED, RESTORED, ROLLED_BACK }
/**
* Ein Eintrag der Dokumenthistorie. Die Historie wird getrennt vom Dokument
@@ -76,6 +76,30 @@ class DocumentService(
return updated
}
/**
* Ändert nur den Titel (D76/D85): ein Metadatum mit eigener
* Versionsprüfung — weicht [expectedVersion] von der aktuellen Version
* ab, gibt es 409, und der Client setzt frisch auf. Die Umbenennung ist
* strukturell (Meilenstein) und erreicht alle über den Feed als
* [ChangeType.RENAMED] samt neuem Titel.
*/
fun rename(id: UUID, title: String, expectedVersion: Long): Document {
val existing = findById(id)
if (existing.version != expectedVersion) {
throw DocumentConflictException(
"Version $expectedVersion ist nicht mehr aktuell (jetzt: ${existing.version})"
)
}
val renamed = existing.copy(
title = title,
version = existing.version + 1,
updatedAt = OffsetDateTime.now(clock),
)
repository.save(renamed)
recordHistory(renamed, ChangeType.RENAMED)
return renamed
}
fun delete(id: UUID) {
val existing = findById(id)
repository.deleteById(id)
@@ -53,6 +53,21 @@ class LiveEditingService(
fun patchContent(documentId: UUID, patch: ContentPatch): ContentPatchOutcome =
lockFor(documentId).withLock { applyPatch(documentId, patch) }
/**
* Umbenennen unter derselben Sperre wie die Inhalts-Patches (D85): Auch
* der Titel bumpt die Version, und prüfen und schreiben gehören zusammen.
* Titel-Regeln liegen hier (400), die Versionsprüfung im [DocumentService].
*/
fun renameDocument(documentId: UUID, title: String, expectedVersion: Long) =
lockFor(documentId).withLock {
val bereinigt = title.trim()
if (bereinigt.isEmpty()) throw InvalidPatchException("Titel darf nicht leer sein")
if (bereinigt.length > 255) {
throw InvalidPatchException("Titel zu lang: ${bereinigt.length} (erlaubt: 255)")
}
documents.rename(documentId, bereinigt, expectedVersion)
}
private fun applyPatch(documentId: UUID, patch: ContentPatch): ContentPatchOutcome {
if (patch.ops.size > properties.maxOps) {
throw InvalidPatchException(
@@ -185,7 +200,10 @@ class LiveEditingService(
}
}
private fun DocumentHistoryEntry.toEvent() = ChangeEvent(version, changeType, author)
private fun DocumentHistoryEntry.toEvent() = ChangeEvent(
version, changeType, author,
title = title.takeIf { changeType == de.werkbaum.domain.ChangeType.RENAMED },
)
private fun lockFor(documentId: UUID): ReentrantLock =
stripes[Math.floorMod(documentId.hashCode(), stripes.size)]
+72 -2
View File
@@ -255,6 +255,56 @@ paths:
schema:
$ref: "#/components/schemas/ProblemDetail"
/documents/{documentId}/title:
parameters:
- name: documentId
in: path
required: true
schema:
type: string
format: uuid
patch:
tags: [Documents]
operationId: patchDocumentTitle
summary: Dokument umbenennen (Live-Editing)
description: >
Aendert nur den Titel - er ist ein Metadatum, kein Zeileninhalt, und
bekommt deshalb seinen eigenen Weg mit Versionspruefung (D76). Die
Umbenennung erzeugt eine neue Version vom Typ RENAMED, die der
Aenderungsfeed mit dem neuen Titel im Klartext zustellt - alle sehen
denselben Namen.
Verwaltungs-Aktion: Sie wird kuenftig an das geplante Owner-Passwort
gebunden (die Bindung kommt als Berechtigungspruefung dazu, die
Signatur bleibt).
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/TitlePatchRequest"
responses:
"200":
description: Umbenanntes Dokument
content:
application/json:
schema:
$ref: "#/components/schemas/Document"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"409":
description: >
Versionskonflikt - `expectedVersion` ist nicht mehr die aktuelle
Version. Der Client holt das Dokument frisch und versucht es mit
dessen Version erneut.
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetail"
/documents/{documentId}/changes:
parameters:
- name: documentId
@@ -427,11 +477,26 @@ components:
RESTORED heisst: ein geloeschtes Dokument ist wieder da.
ROLLED_BACK ist der Rueckfall eines lebenden Dokuments auf eine
aeltere Version.
enum: [CREATED, UPDATED, DELETED, RESTORED, ROLLED_BACK]
enum: [CREATED, UPDATED, RENAMED, DELETED, RESTORED, ROLLED_BACK]
timestamp:
type: string
format: date-time
TitlePatchRequest:
type: object
required: [title, expectedVersion]
properties:
title:
type: string
minLength: 1
maxLength: 255
expectedVersion:
type: integer
format: int64
description: >
Die Version, auf der die Umbenennung aufsetzt - weicht sie von der
aktuellen ab, antwortet der Server mit 409.
RestoreRequest:
type: object
properties:
@@ -546,7 +611,7 @@ components:
format: int64
changeType:
type: string
enum: [CREATED, UPDATED, DELETED, RESTORED, ROLLED_BACK]
enum: [CREATED, UPDATED, RENAMED, DELETED, RESTORED, ROLLED_BACK]
clientId:
type: string
description: Fehlt bei Aenderungen ohne Absender (etwa ueber PUT).
@@ -555,6 +620,11 @@ components:
description: >
Selbstgewaehlt und ohne Anmeldung eine Behauptung - in der
Oberflaeche nicht wie ein Nachweis darstellen.
title:
type: string
description: >
Nur bei RENAMED - der neue Titel im Klartext, damit der Client
ihn ohne weiteren Abruf uebernehmen kann (D76).
ChangeFeed:
type: object
@@ -205,6 +205,45 @@ class LiveEditingStepDefinitions {
feed.content shouldBe erwartet
}
@Wenn("das Dokument auf {string} umbenannt wird")
fun `das Dokument wird umbenannt`(titel: String) =
sendRename(titel, currentDocument().version)
@Wenn("das Dokument mit veralteter Version auf {string} umbenannt wird")
fun `veraltete Umbenennung`(titel: String) =
sendRename(titel, currentDocument().version - 1)
private fun sendRename(titel: String, version: Long) {
lastResponse = client.patch()
.uri("/api/v1/documents/$documentId/title")
.contentType(MediaType.APPLICATION_JSON)
.body("""{"title":${json(titel)},"expectedVersion":$version}""")
.exchange()
.returnResult(String::class.java)
}
@Dann("erhalte ich für das Umbenennen den Status {int}")
fun `status des Umbenennens`(erwartet: Int) {
status() shouldBe erwartet
}
@Und("das Dokument trägt den Titel {string}")
fun `dokument traegt den Titel`(titel: String) {
currentDocument().title shouldBe titel
}
@Und("das Dokument steht auf Version {long}")
fun `dokument steht auf Version`(version: Long) {
currentDocument().version shouldBe version
}
@Und("das RENAMED-Ereignis nennt den Titel {string}")
fun `renamed nennt den Titel`(titel: String) {
lastBody<ChangeFeed>().events
.first { it.changeType.value == "RENAMED" }
.title shouldBe titel
}
@Und("der Feed meldet das Ereignis {string}")
fun `der Feed meldet das Ereignis`(typ: String) {
lastBody<ChangeFeed>().events.map { it.changeType.value } shouldContain typ
@@ -112,6 +112,35 @@ class DocumentServiceTest {
entry.captured.version shouldBe 4
}
@Test
fun `rename inkrementiert Version und schreibt RENAMED-Meilenstein`() {
val doc = sampleDocument(version = 3)
every { repository.findById(doc.id) } returns doc
val saved = slot<Document>()
every { repository.save(capture(saved)) } answers { saved.captured }
val entry = captureAppended()
val result = service.rename(doc.id, title = "Team-Plan", expectedVersion = 3)
result.version shouldBe 4
result.title shouldBe "Team-Plan"
result.content shouldBe doc.content /* der Inhalt bleibt */
entry.captured.changeType shouldBe ChangeType.RENAMED
entry.captured.milestone shouldBe true /* strukturell, nie bloss Sync */
entry.captured.title shouldBe "Team-Plan"
}
@Test
fun `rename mit veralteter Version wirft Konflikt und schreibt nichts`() {
val doc = sampleDocument(version = 3)
every { repository.findById(doc.id) } returns doc
shouldThrow<DocumentConflictException> {
service.rename(doc.id, title = "Zu spaet", expectedVersion = 2)
}
verify(exactly = 0) { repository.save(any()) }
}
@Test
fun `delete entfernt Dokument und schreibt DELETED-Tombstone`() {
val doc = sampleDocument(version = 2)
@@ -0,0 +1,51 @@
# language: de
Funktionalität: Ein geteiltes Dokument umbenennen
Als Bearbeiter eines geteilten Plans
möchte ich den Titel für alle ändern,
damit jeder im Wähler denselben Namen sieht.
Grundlage: Der Titel ist ein Metadatum und bekommt seinen eigenen Weg mit
Versionsprüfung (D76/D85); der Änderungsfeed stellt die Umbenennung samt
neuem Titel zu.
Szenario: Umbenennen mit aktueller Version gilt und lässt den Inhalt stehen
Angenommen es existiert ein Dokument "Plan" mit den Zeilen:
"""
- [ ] Eins
"""
Wenn das Dokument auf "Team-Plan" umbenannt wird
Dann erhalte ich für das Umbenennen den Status 200
Und das Dokument trägt den Titel "Team-Plan"
Und das Dokument steht auf Version 2
Und das Dokument hat die Zeilen:
"""
- [ ] Eins
"""
Szenario: Der Feed stellt die Umbenennung samt neuem Titel zu
Angenommen es existiert ein Dokument "Plan" mit den Zeilen:
"""
- [ ] Eins
"""
Wenn das Dokument auf "Team-Plan" umbenannt wird
Und ich die Änderungen seit Version 1 abrufe
Dann der Feed meldet das Ereignis "RENAMED"
Und das RENAMED-Ereignis nennt den Titel "Team-Plan"
Szenario: Eine veraltete Version wird abgelehnt
Angenommen es existiert ein Dokument "Plan" mit den Zeilen:
"""
- [ ] Eins
"""
Wenn das Dokument mit veralteter Version auf "Zu spät" umbenannt wird
Dann erhalte ich für das Umbenennen den Status 409
Und das Dokument trägt den Titel "Plan"
Szenario: Ein leerer Titel wird abgelehnt
Angenommen es existiert ein Dokument "Plan" mit den Zeilen:
"""
- [ ] Eins
"""
Wenn das Dokument auf " " umbenannt wird
Dann erhalte ich für das Umbenennen den Status 400
Und das Dokument trägt den Titel "Plan"
+2
View File
@@ -19,6 +19,8 @@ reverse.
## 2026-08-27
- Renaming a shared document now renames it for everyone: the pencil patches the server title, and the change feed hands the new name to every open editor
- URL documents lost their pencil — their name is the URL, and a local name would not survive the next load
- If Werkbaum is open in a second browser tab, a warning says so — both tabs write the same document list, and the last one to save wins
- Reloading a URL document no longer asks — a page reload discards local changes silently anyway, so the question promised a protection that did not exist
- Deleting your last document now says that a fresh example will take its place
+41
View File
@@ -7290,3 +7290,44 @@ richtigen Texte, Abbruch lässt alles stehen; und die Tab-Warnung erschien
durch **echtes** Tippen in einem zweiten Tab (kein synthetisches Ereignis —
das storage-Ereignis kommt nur aus fremden Fenstern). 531 Tests, davon 2 neue
für `isDocKey`.
## D85 — Umbenennen erreicht alle: PATCH /title und das RENAMED-Ereignis
Paket B der Ungereimtheiten-Liste (D84). Der Befund: Umbenennen eines
geteilten Dokuments wirkte nur lokal und war flüchtig — der Server-Titel gilt
für alle (D76), und `adoptLive()` überschrieb den lokalen Namen beim nächsten
Laden. Dasselbe bei URL-Dokumenten, deren Name die URL ist (D23). Entschieden
(Multiple-Choice): der in D76 vorgesehene eigene Weg wird gebaut.
**Backend:** `PATCH /documents/{id}/title` mit `expectedVersion`
abweichende Version ⇒ 409, der Client setzt frisch auf; leerer oder zu langer
Titel ⇒ 400 (die Regeln liegen im `LiveEditingService`, unter derselben
Stripe-Sperre wie die Inhalts-Patches: auch der Titel bumpt die Version, und
prüfen und schreiben gehören zusammen). Die Umbenennung ist ein neuer
`ChangeType.RENAMED` — strukturell, also immer Meilenstein — und der
Änderungsfeed stellt den **neuen Titel im Klartext** zu (`ChangeEvent.title`,
nur bei RENAMED gefüllt): Kein Client braucht einen weiteren Abruf. In der
API-Beschreibung steht die Owner-Vormerkung: Die Aktion wird künftig an das
geplante Owner-Passwort gebunden (`#col.live.owner`), die Bindung kommt als
Berechtigungsprüfung dazu, die Signatur bleibt.
**Frontend:** Der Zeilen-Stift eines Server-Dokuments ruft `renameOnServer()`
optimistisch sofort anzeigen, dann GET (Version) + PATCH; bei 409 einmal mit
frischer Version erneut, bei endgültigem Scheitern kommt der alte Name zurück
und eine Warnung sagt warum. Die Version bumpt ohne Inhaltsänderung, die
Schattenkopie bleibt also gültig. Die übrigen Mitschreiber bekommen die
Umbenennung über `applyRenameEvents()` aus dem Feed — Chip und Menü folgen
ohne Neuladen. **URL-Dokumente verlieren den Stift** (wie Mitgelieferte,
D81-Nachtrag 3): Ihr Name ist die URL; ein lokaler Name würde beim nächsten
Laden überschrieben, und ein Server, den man umbenennen könnte, steht dort
nicht dahinter.
**Nachgemessen** Ende-zu-Ende gegen das lokal laufende Backend: Der Stift am
Server-Dokument benennt um (Server-Titel „Team-Plan X", Version 2, keine
Warnung); eine Umbenennung **von außen** (curl) erreicht den offenen Editor
über den Feed — Chip wechselt auf „Von aussen umbenannt", der Inhalt bleibt
unangetastet. Backend: vier neue Cucumber-Szenarien (gilt für alle und lässt
den Inhalt stehen; Feed samt Titel; veraltete Version ⇒ 409; leerer Titel ⇒
400) und zwei Unit-Tests (RENAMED-Meilenstein; Konflikt schreibt nichts),
BUILD SUCCESSFUL samt Coverage. Werkzeuggrenze wie in D76-Nachtrag 7: Der
Automations-Tab meldet sich dauerhaft als verborgen — der Feed lief nur mit
gestellter Sichtbarkeit.
+6
View File
@@ -154,6 +154,7 @@
- [^] #col.live.upload: Share: put a local plan on the server (S) %% a button in the editor title bar, see D81
- [?] #col.live.cursors: Cursors and who else is here (S)
- [ ] #col.live.owner: An owner password guards managing a shared plan (M) %% returned once on creation, admin can reclaim
- [x] #col.live.rename: Renaming reaches everyone (S) %% PATCH /title + RENAMED in the feed, see D85
- [-] #col.live.ws: Transport over a websocket (S) %% plain HTTP turned out to be enough
- [-] #col.live.own: A merge algorithm of our own (XL) %% solved problem, do not reinvent
- [?] #mmd: Mermaid plugin (XL)
@@ -918,6 +919,11 @@
another browser. Management actions bind to it; an admin with the master
password can still reclaim a document.
#col.live.rename
The title is server metadata, so renaming patches it with a version check
and the change feed hands the new title to every client in plain text. URL
documents lost their pencil instead - their name is the URL.
#col.live.cursors
Seeing where the others are. A by-product of the merge layer rather than a
feature in its own right.
+64 -5
View File
@@ -3714,7 +3714,12 @@ function docRowHtml(d){
`<span class="doccheck" aria-hidden="true">✓</span>` +
`<span class="docitem-name">${esc(d.name)}</span>${zusatz}</button>` +
`<span class="docacts">` +
(shippedStateOf(d.id) ? '' : iconBtn('rename', t('docRename'), IC_RENAME)) +
/* Umbenennen: bei Mitgelieferten nicht (der Name ist Auslieferungsstand,
D81-Nachtrag 3), bei URL-Dokumenten nicht (der Name IST die URL, D23
ein lokaler Name würde beim nächsten Laden überschrieben, D85). Bei
Server-Dokumenten wirkt es über PATCH /title für ALLE (D85). */
(['shipped', 'url'].includes(docKind(d.id, SHIPPED_IDS))
? '' : iconBtn('rename', t('docRename'), IC_RENAME)) +
/* Geteilte (Server- wie URL-Dokumente) werden VERLASSEN, nicht gelöscht
(D81-Nachtrag 5): Die Aktion tut lokal dasselbe, ist für den Benutzer
aber eine andere dem Dokument selbst geschieht nichts, man gibt nur
@@ -4213,9 +4218,9 @@ function finishNewDoc(){
}
function renameDoc(id){
if(!docs.some(x => x.id === id)) return;
/* Mitgelieferte nicht (D81-Nachtrag 3): Ihr Name ist Auslieferungsstand.
Die Prüfung liegt hier, nicht nur am ausgeblendeten Stift. */
if(shippedStateOf(id)) return;
/* Mitgelieferte nicht (D81-Nachtrag 3) und URL-Dokumente nicht (D85)
die Prüfung liegt hier, nicht nur am ausgeblendeten Stift. */
if(['shipped', 'url'].includes(docKind(id, SHIPPED_IDS))) return;
renamingId = id; /* Zeilen-Aktion (D81): jedes Dokument, nicht nur das aktive */
renameIsNew = false;
renderDocMenu();
@@ -4226,11 +4231,50 @@ function commitRename(){
const d = docs.find(x => x.id === renamingId);
const val = inp ? inp.value.trim() : '';
renamingId = null;
if(d && val){ d.name = val; persistDocs(); updateDocName(); }
if(d && val){
/* Server-Dokumente: Der Titel gehört dem Server alle sehen denselben
(D76). Der Weg dorthin ist PATCH /title (D85), nicht der lokale Name. */
if(String(d.id).startsWith('live:')) renameOnServer(d, val);
else { d.name = val; persistDocs(); updateDocName(); }
}
renderDocMenu();
finishNewDoc();
}
function cancelRename(){ renamingId = null; renderDocMenu(); finishNewDoc(); }
/* Umbenennen eines Server-Dokuments (D85): optimistisch sofort anzeigen;
scheitert der PATCH, kommt der alte Name zurück und die Warnung sagt
warum. Bei 409 jemand war zwischen Abruf und PATCH schneller einmal
mit frischer Version erneut. Die Version bumpt ohne Inhaltsänderung, die
Schattenkopie bleibt also gültig; die übrigen Mitschreiber bekommen die
Umbenennung als RENAMED-Ereignis über ihren Feed. */
async function renameOnServer(d, titel){
const url = String(d.id).slice(5);
const alt = d.name;
const zeige = () => { persistDocs(); updateDocName(); if(!docMenu.hidden) renderDocMenu(); };
d.name = titel; zeige();
const patch = async () => {
const doc = await fetchJson(url);
return fetchJson(url + '/title', {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({title: titel, expectedVersion: doc.version}),
});
};
try{
let neu;
try{ neu = await patch(); }
catch(err){ if(err && err.status === 409) neu = await patch(); else throw err; }
d.name = neu.title;
if(liveState && liveState.id === d.id) liveState.version = neu.version;
zeige();
}catch(err){
d.name = alt;
sourceWarning = {type: 'liveLoad', url: url + '/title',
error: (err && err.message) || String(err)};
zeige();
render();
}
}
/* Der gemeinsame Kern von Löschen und Verlassen: den Eintrag samt lokaler
Anhängsel entfernen. Die beiden Aktionen unterscheiden sich für den
Benutzer (Wort, Icon, Rückfrage D81-Nachtrag 5), lokal tun sie dasselbe. */
@@ -4878,6 +4922,7 @@ document.addEventListener('visibilitychange', () => {
function applyFeed(feed){
const what = live.feedAction(feed, liveState.version, liveState.busy);
if(what === 'skip') return;
applyRenameEvents(feed); /* Umbenennung durch andere (RENAMED, D85) */
if(what === 'replace'){
/* Volltext: die Basis ist verdichtet, ein Diff gibt es nicht mehr. */
liveState.version = feed.currentVersion;
@@ -4891,6 +4936,20 @@ function applyFeed(feed){
applyForeign(alt, feed.ops, liveState.shadow, liveState.version);
}
/* Hat jemand umbenannt, trägt der Feed den neuen Titel im Klartext (D85)
der letzte gewinnt. Der Name ist Index-Metadatum: persistieren ist ein
Flush-Ereignis, und Umbenennen ist selten. */
function applyRenameEvents(feed){
const ev = (feed.events || []).filter(e => e && e.changeType === 'RENAMED' && e.title).pop();
if(!ev) return;
const d = docs.find(x => x.id === liveState.id);
if(!d || d.name === ev.title) return;
d.name = ev.title;
persistDocs();
updateDocName();
if(!docMenu.hidden) renderDocMenu();
}
/* Fremde Operationen auf den **sichtbaren** Text anwenden.
Hat der Nutzer inzwischen selbst getippt, ist sein Text nicht mehr die alte
Schattenkopie: Dann wird die fremde Änderung auf seinen Stand angewendet und