Files
wekan/models/lib/cardMoveModifier.js
T
Lauri Ojansivu 2e7c4ede2a - **[Reduce card flicker on drag by only writing changed fields on a card move](https://github.com/wekan/wekan/issues/6430)** (#6430):
`Card.move()` wrote `boardId`/`swimlaneId`/`listId` into the update unconditionally, even for a
  same-board drag to another list (the reported case). Keeping `boardId` in the `$set` on every drag
  re-ran the `boardId`-gated `Cards.after.update` hook that re-syncs the card's checklists and
  checklist items via `multi` updates, ran the cross-board consistency guard and the
  `denyCrossBoardMove` deny-rule DB lookup, and invalidated more reactive dependents than
  necessary — server work and reactivity churn that contributed to a ~1s card flicker on large
  boards. `Card.move()` now writes **only the fields that actually change** (via the pure
  `computeCardMoveModifier` helper) and skips the write entirely when a card is dropped back in the
  same place, so a same-board move no longer touches boardId or the cross-board hooks. The
  `moveCard`/`moveCardBoard` activity generators already re-check `doc.X !== oldX`, so trimming the
  modifier does not drop any activity. Note: this reduces the drag-time work behind the flicker; the
  residual reactive re-render cost on very large boards is a separate performance topic.

Thanks to mimZD and xet7 !

Fixes #6430
2026-07-05 23:12:16 +03:00

35 lines
1.6 KiB
JavaScript

'use strict';
// Pure helper that computes the $set modifier for Card.move(): only the fields
// that ACTUALLY change relative to the card's current position. Extracted from
// models/cards.js so it is unit-testable without Meteor.
//
// #6430 (card flicker ~1s on drag on large boards): Card.move() used to write
// boardId/swimlaneId/listId unconditionally, even when the card stayed on the
// same board/swimlane (the common "drag to another list" case). That meant every
// drag:
// - re-ran the Cards.after.update hook that boardId-re-syncs the card's
// checklists and checklistItems via multi updates (server load + reactive
// re-publish of those collections),
// - ran the cross-board consistency before-hook and the denyCrossBoardMove
// deny-rule DB lookup, and
// - invalidated more reactive dependents than necessary.
// Writing only the changed fields skips all of that for same-board moves, and an
// empty result means a true no-op (card dropped back in place) that the caller
// can skip entirely.
function computeCardMoveModifier(card, target = {}) {
const { boardId, swimlaneId, listId, sort } = target;
const fields = {};
if (boardId !== undefined && boardId !== card.boardId) fields.boardId = boardId;
if (swimlaneId !== undefined && swimlaneId !== card.swimlaneId) {
fields.swimlaneId = swimlaneId;
}
if (listId !== undefined && listId !== card.listId) fields.listId = listId;
// sort === null means "keep current" (Card.move's default), so it is never a change.
if (sort !== undefined && sort !== null && sort !== card.sort) fields.sort = sort;
return fields;
}
module.exports = { computeCardMoveModifier };