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:
+98
@@ -351,5 +351,103 @@ describe('WorkflowDatabaseEventTriggerListener', () => {
|
||||
{ retryLimit: 3 },
|
||||
);
|
||||
});
|
||||
|
||||
it('should trigger workflow for position-only updates when no fields are specified', async () => {
|
||||
const positionOnlyPayload: WorkspaceEventBatch<any> = {
|
||||
...mockPayload,
|
||||
events: [
|
||||
{
|
||||
...mockPayload.events[0],
|
||||
properties: {
|
||||
updatedFields: ['position'],
|
||||
before: { position: 1 },
|
||||
after: { position: 2 },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockRepository.find.mockResolvedValue([
|
||||
{
|
||||
...mockEventListeners[0],
|
||||
settings: {
|
||||
eventName: databaseEventName,
|
||||
fields: undefined,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await listener.handleObjectRecordUpdateEvent(positionOnlyPayload);
|
||||
|
||||
expect(messageQueueService.add).toHaveBeenCalledWith(
|
||||
WorkflowTriggerJob.name,
|
||||
{
|
||||
workspaceId,
|
||||
workflowId,
|
||||
payload: positionOnlyPayload.events[0],
|
||||
},
|
||||
{ retryLimit: 3 },
|
||||
);
|
||||
});
|
||||
|
||||
it('should trigger workflow when position changes alongside another field', async () => {
|
||||
const positionAndFieldPayload: WorkspaceEventBatch<any> = {
|
||||
...mockPayload,
|
||||
events: [
|
||||
{
|
||||
...mockPayload.events[0],
|
||||
properties: {
|
||||
updatedFields: ['field1', 'position'],
|
||||
before: { field1: 'old', position: 1 },
|
||||
after: { field1: 'new', position: 2 },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockRepository.find.mockResolvedValue([
|
||||
{
|
||||
...mockEventListeners[0],
|
||||
settings: {
|
||||
eventName: databaseEventName,
|
||||
fields: undefined,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await listener.handleObjectRecordUpdateEvent(positionAndFieldPayload);
|
||||
|
||||
expect(messageQueueService.add).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not trigger workflow for position-only updates when fields are specified', async () => {
|
||||
const positionOnlyPayload: WorkspaceEventBatch<any> = {
|
||||
...mockPayload,
|
||||
events: [
|
||||
{
|
||||
...mockPayload.events[0],
|
||||
properties: {
|
||||
updatedFields: ['position'],
|
||||
before: { position: 1 },
|
||||
after: { position: 2 },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockRepository.find.mockResolvedValue([
|
||||
{
|
||||
...mockEventListeners[0],
|
||||
settings: {
|
||||
eventName: databaseEventName,
|
||||
fields: ['field1'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await listener.handleObjectRecordUpdateEvent(positionOnlyPayload);
|
||||
|
||||
expect(messageQueueService.add).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+4
-6
@@ -394,26 +394,24 @@ export class WorkflowDatabaseEventTriggerListener {
|
||||
if (action === DatabaseEventAction.UPDATED) {
|
||||
const settings = eventListener.settings as UpdateEventTriggerSettings;
|
||||
const updateEventPayload = eventPayload as ObjectRecordUpdateEvent;
|
||||
const updatedFields = updateEventPayload?.properties?.updatedFields ?? [];
|
||||
|
||||
return (
|
||||
!settings.fields ||
|
||||
settings.fields.length === 0 ||
|
||||
settings.fields.some((field) =>
|
||||
updateEventPayload?.properties?.updatedFields?.includes(field),
|
||||
)
|
||||
settings.fields.some((field) => updatedFields.includes(field))
|
||||
);
|
||||
}
|
||||
|
||||
if (action === DatabaseEventAction.UPSERTED) {
|
||||
const settings = eventListener.settings as UpsertEventTriggerSettings;
|
||||
const upsertEventPayload = eventPayload as ObjectRecordUpsertEvent;
|
||||
const updatedFields = upsertEventPayload?.properties?.updatedFields ?? [];
|
||||
|
||||
return (
|
||||
!settings.fields ||
|
||||
settings.fields.length === 0 ||
|
||||
settings.fields.some((field) =>
|
||||
upsertEventPayload?.properties?.updatedFields?.includes(field),
|
||||
)
|
||||
settings.fields.some((field) => updatedFields.includes(field))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user