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:
+91
@@ -333,6 +333,97 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('position-only updates', () => {
|
||||
it('should include position-only events when the trigger has no updatedFields filter', () => {
|
||||
const workspaceEventBatch = createMockWorkspaceEventBatch({
|
||||
name: 'company.updated',
|
||||
events: [
|
||||
createMockEvent({
|
||||
recordId: 'record-1',
|
||||
properties: { after: {}, updatedFields: ['position'] },
|
||||
}),
|
||||
createMockEvent({
|
||||
recordId: 'record-2',
|
||||
properties: { after: {}, updatedFields: ['name'] },
|
||||
}),
|
||||
],
|
||||
});
|
||||
const logicFunctions = [
|
||||
createMockLogicFunction({
|
||||
databaseEventTriggerSettings: { eventName: 'company.updated' },
|
||||
}),
|
||||
];
|
||||
|
||||
const result = transformEventBatchToEventPayloads({
|
||||
workspaceEventBatch,
|
||||
logicFunctions,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(
|
||||
result.map((r) => (r.payload as ObjectRecordEvent).recordId),
|
||||
).toEqual(['record-1', 'record-2']);
|
||||
});
|
||||
|
||||
it('should include events that change other fields alongside position', () => {
|
||||
const workspaceEventBatch = createMockWorkspaceEventBatch({
|
||||
name: 'company.updated',
|
||||
events: [
|
||||
createMockEvent({
|
||||
recordId: 'record-1',
|
||||
properties: { after: {}, updatedFields: ['position', 'name'] },
|
||||
}),
|
||||
],
|
||||
});
|
||||
const logicFunctions = [
|
||||
createMockLogicFunction({
|
||||
databaseEventTriggerSettings: { eventName: 'company.updated' },
|
||||
}),
|
||||
];
|
||||
|
||||
const result = transformEventBatchToEventPayloads({
|
||||
workspaceEventBatch,
|
||||
logicFunctions,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should exclude position-only events when the trigger filters on another field', () => {
|
||||
const workspaceEventBatch = createMockWorkspaceEventBatch({
|
||||
name: 'company.updated',
|
||||
events: [
|
||||
createMockEvent({
|
||||
recordId: 'record-1',
|
||||
properties: { after: {}, updatedFields: ['position'] },
|
||||
}),
|
||||
createMockEvent({
|
||||
recordId: 'record-2',
|
||||
properties: { after: {}, updatedFields: ['name'] },
|
||||
}),
|
||||
],
|
||||
});
|
||||
const logicFunctions = [
|
||||
createMockLogicFunction({
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'company.updated',
|
||||
updatedFields: ['name'],
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const result = transformEventBatchToEventPayloads({
|
||||
workspaceEventBatch,
|
||||
logicFunctions,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(
|
||||
result.map((r) => (r.payload as ObjectRecordEvent).recordId),
|
||||
).toEqual(['record-2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should return empty array when no logic functions provided', () => {
|
||||
const workspaceEventBatch = createMockWorkspaceEventBatch();
|
||||
|
||||
+5
-5
@@ -53,11 +53,11 @@ const filterEventsByUpdatedFields = ({
|
||||
operation: string;
|
||||
triggerUpdatedFields?: string[];
|
||||
}): ObjectRecordEvent[] => {
|
||||
if (
|
||||
operation !== 'updated' ||
|
||||
!isDefined(triggerUpdatedFields) ||
|
||||
triggerUpdatedFields.length === 0
|
||||
) {
|
||||
if (operation !== 'updated') {
|
||||
return events;
|
||||
}
|
||||
|
||||
if (!isDefined(triggerUpdatedFields) || triggerUpdatedFields.length === 0) {
|
||||
return events;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user