mirror of
https://github.com/yjs/yjs.git
synced 2026-07-09 20:08:51 +02:00
fix undoing setAttribute combined with delete corrupts remote state - closes #757
When an attribute change and a delete on the same XmlText are bundled in a single undo stack item, the redone attribute item kept an `origin` pointing at an item under the now-deleted original parent. The encoder then omitted parent info (origin was non-null), and the remote derived parent from origin -- attaching the attribute to the wrong, deleted type. The restored attribute went missing on sync. In redoItem, after determining `left` for parentSub items, drop it if it refers to a different parent than the redo target. This forces the encoder to write parent info directly, so the remote attaches the attribute to the correct (redone) parent.
This commit is contained in:
@@ -223,6 +223,10 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
|
||||
} else {
|
||||
left = parentType._map.get(item.parentSub) || null
|
||||
}
|
||||
// drop cross-parent left so origin doesn't mislead the remote (#757)
|
||||
if (left !== null && left.parent !== parentType) {
|
||||
left = null
|
||||
}
|
||||
}
|
||||
const nextClock = getState(store, ownClientID)
|
||||
const nextId = createID(ownClientID, nextClock)
|
||||
|
||||
@@ -811,3 +811,35 @@ export const testUndoDoingStackItem = async (_tc) => {
|
||||
t.compare(metaRedo, '42', 'currStackItem is accessible while redoing')
|
||||
t.compare(undoManager.currStackItem, null, 'currStackItem is null after observe/transaction')
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://github.com/yjs/yjs/issues/757
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testUndoSetAttributeAndDeleteSyncsAttributes = _tc => {
|
||||
const doc = new Y.Doc()
|
||||
const root = /** @type {Y.XmlText} */ (doc.get('sharedRoot', Y.XmlText))
|
||||
const button = new Y.XmlText()
|
||||
button.setAttribute('type', 'button')
|
||||
button.setAttribute('test', true)
|
||||
button.insert(0, 'Click me')
|
||||
root.insertEmbed(0, button)
|
||||
|
||||
const undoManager = new Y.UndoManager(root)
|
||||
undoManager.stopCapturing()
|
||||
|
||||
button.setAttribute('type', 'paragraph')
|
||||
root.delete(0, 1)
|
||||
undoManager.undo()
|
||||
|
||||
const expectedAttrs = { type: 'button', test: true }
|
||||
const expectedText = 'Click me'
|
||||
t.compare(root.toDelta()[0].insert.getAttributes(), expectedAttrs)
|
||||
t.compare(root.toDelta()[0].insert.toString(), expectedText)
|
||||
|
||||
const remoteDoc = new Y.Doc()
|
||||
Y.applyUpdateV2(remoteDoc, Y.encodeStateAsUpdateV2(doc))
|
||||
const remoteRoot = /** @type {Y.XmlText} */ (remoteDoc.get('sharedRoot', Y.XmlText))
|
||||
t.compare(remoteRoot.toDelta()[0].insert.getAttributes(), expectedAttrs)
|
||||
t.compare(remoteRoot.toDelta()[0].insert.toString(), expectedText)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user