fix(kanban): preserve scroll on board re-init + propagate same-column reorders via SSE (#20637)

closes
https://discord.com/channels/1130383047699738754/1504130730840821860


https://github.com/user-attachments/assets/d5833031-01c6-4e46-b699-c29c42435a53





## Summary

Fixes two related issues with the kanban (board view) collaboration
experience:

1. **Scroll-to-top on every data change** —
`triggerRecordBoardInitialQuery` always scrolled the board to the top,
even when re-initializing for a single-record data change (SSE echo of
your own mutation, a collaborator's update). Scroll reset only makes
sense when the dataset itself changes (filter / sort / group).
2. **Same-column reorders by other users did not propagate** — the
server's diff function stripped `FieldMetadataType.POSITION`, so
position-only updates produced empty `updatedFields` and short-circuited
event emission entirely. SSE clients never received them.

## What's in here

- **Frontend** — `useTriggerRecordBoardInitialQuery` now exposes a
`triggerRecordBoardInitialQueryWithoutScrollReset` variant; data-driven
re-inits in `RecordBoardDataChangedEffect` use it, while genuine filter
/ sort / group changes keep the scroll-resetting
`triggerRecordBoardInitialQuery`. `getRecordBoardEffectsForUpdateInputs`
classifies each update as `trigger-initial-query` / `reposition-records`
/ `none`. For position- or group-only changes we skip the re-query and
reposition records in place in the store
(`useRepositionRecordsOnBoard`), which avoids the flicker and preserves
scroll.
- **Server** — removes `POSITION` from `objectRecordChangedValues`'
strip list, so position-only updates emit a non-empty diff and flow
through SSE. Position is now treated as a field like any other across
all event consumers (SSE, webhooks, workflows, logic functions); a
trigger with an explicit field filter still excludes it.
This commit is contained in:
nitin
2026-06-11 12:56:51 +05:30
committed by GitHub
parent 9100fb1e9f
commit 20c83e1f86
14 changed files with 670 additions and 137 deletions
@@ -245,4 +245,53 @@ describe('transformEventBatchToWebhookEvents', () => {
expect(resultWithoutEventDate).toEqual(expectedResultWithoutEventDate);
});
it('should include position-only update events', () => {
const workspaceEventBatch: WorkspaceEventBatch<ObjectRecordEvent> = {
workspaceId: 'workspaceId',
objectMetadata: mockObjectMetadata,
name: 'objectNameSingular.updated',
events: [
{
recordId: 'recordId-1',
properties: {
after: { id: 'id-1', nameSingular: 'nameSingular-1' },
updatedFields: ['position'],
},
},
{
recordId: 'recordId-2',
properties: {
after: { id: 'id-2', nameSingular: 'nameSingular-2' },
updatedFields: ['nameSingular', 'position'],
},
},
],
};
const webhooks = [
{
id: 'webhook-id',
targetUrl: 'targetUrl',
secret: 'secret',
},
] as WebhookEntity[];
const result = transformEventBatchToWebhookEvents({
workspaceEventBatch,
webhooks,
});
expect(result).toHaveLength(2);
expect(result[0].record).toEqual({
id: 'id-1',
nameSingular: 'nameSingular-1',
});
expect(result[0].updatedFields).toEqual(['position']);
expect(result[1].record).toEqual({
id: 'id-2',
nameSingular: 'nameSingular-2',
});
expect(result[1].updatedFields).toEqual(['nameSingular', 'position']);
});
});