fix(tree): place remote moves by position; remove stale node on move-into-restricted
Release-cycle review found two move-path issues: - Remote moves were placed at index:0 (broadcastPageMoved hardcodes index:0), so every observer rendered the moved node at the TOP of its new siblings until refetch. Client moveTreeNode now places by fractional position (treeModel.placeByPosition, mirroring addTreeNode/insertByPosition) and applies the payload's pageData (title->name, icon, hasChildren) so receivers keep the node correct. - Moving a page under a restricted ancestor left a stale named node (title/ slugId/icon) in the trees of users who lost visibility. broadcastPageMoved now derives one FRESH hasRestrictedAncestor decision and drives both paths from it: when restricted, the move goes to authorized users only (emitToAuthorizedUsers, not the space-cache-gated emitTreeEvent) and a compensating deleteTreeNode goes to the unauthorized complement (same fresh getUserIdsWithPageAccess set) — disjoint, no stale-cache window. Non-restricted moves are unchanged (one moveTreeNode to the room). Follow-up (noted): invalidateSpaceRestrictionCache is still unwired at permission-mutation sites; the open-space fast path can lag up to the 30s TTL, but the move/delete consistency above no longer depends on it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -215,6 +215,30 @@ export const treeModel = {
|
||||
return treeModel.insert(removed, to.parentId, source, to.index);
|
||||
},
|
||||
|
||||
// Position-aware move for server-authoritative `moveTreeNode` broadcasts. Like
|
||||
// `place`, but instead of an absolute index (which the sender computed against
|
||||
// its own loaded set), it inserts the moved node among the destination's
|
||||
// already-loaded siblings ordered by the node's fractional `position`. This
|
||||
// keeps the visible order correct for every receiver — `place(..., index: 0)`
|
||||
// would wrongly drop the node at the TOP of its new sibling list.
|
||||
// Returns the same array reference (like `place`) when the source is missing
|
||||
// or the destination parent isn't loaded on this client, so callers can detect
|
||||
// that and fall back to removing the node.
|
||||
placeByPosition<T extends { position?: string }>(
|
||||
tree: TreeNode<T>[],
|
||||
sourceId: string,
|
||||
to: { parentId: string | null; position?: string },
|
||||
): TreeNode<T>[] {
|
||||
const source = treeModel.find(tree, sourceId);
|
||||
if (!source) return tree;
|
||||
if (to.parentId !== null && !treeModel.find(tree, to.parentId)) return tree;
|
||||
const removed = treeModel.remove(tree, sourceId);
|
||||
// Reuse the same position-ordered insertion as `insertByPosition` by
|
||||
// stamping the authoritative position onto the moved node first.
|
||||
const positioned = { ...source, position: to.position } as TreeNode<T>;
|
||||
return treeModel.insertByPosition(removed, to.parentId, positioned);
|
||||
},
|
||||
|
||||
move<T extends object>(
|
||||
tree: TreeNode<T>[],
|
||||
sourceId: string,
|
||||
|
||||
Reference in New Issue
Block a user