From 4c904aa44c44075d26baa95c684481618d7ab272 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 28 Jul 2026 14:32:16 +0200 Subject: [PATCH] fix(workflow): keep Limit and Offset when changing the Search Records object (#23423) Fixes the second bug reported in #23387. ## Problem In the Search Records action, changing the Object silently reset `Limit` to `1`. A user who had set `Limit = 100` and then switched object (or switched away and back) ended up with a step that returns exactly one arbitrary record, with no indication beyond a small `1` in the side panel. Reproduced on `main` against a local instance, checking the persisted draft version: ```json { "limit": 1, "offset": 0, "objectName": "person" } ``` `handleOptionClick` rebuilt the entire form as `{ objectNameSingular, limit: 1, offset: 0 }`, discarding whatever the user had entered. `1` is the server-side default for a newly created `FIND_RECORDS` step, so this was effectively a revert-to-creation-default on every object change. ## Change Carry `limit` and `offset` over instead of hardcoding them. `filter` and `orderBy` are still dropped by omission, which is correct: they reference fields of the previous object. ## Test Added `KeepsLimitAndOffsetWhenObjectChanges` to the existing story file. It switches the object and asserts `onActionUpdate` receives `{ objectName: 'company', limit: 100, offset: 20 }`. Confirmed the test is not vacuous: reverting the fix makes it fail with exactly the reported symptom (`limit: 100 -> 1`, `offset: 20 -> 0`). ## Not addressed here The headline bug in #23387 (filters made only of value-less operators never persisting) does not reproduce on `main`. I ran the reporter's steps with `Is in past` OR `Is today (UTC)` and both rules plus the `OR` group were written to the draft version correctly. Persistence hangs off `useUpsertRecordFilter`, which fires the advanced-filter `onUpdate` on every upsert, so operand changes save just as value changes do. The reporter is on ~v2.18.x and did not re-test on a recent release. Review in cubic --- .../WorkflowEditActionFindRecords.stories.tsx | 54 ++++++++++++++++++- .../WorkflowEditActionFindRecords.tsx | 4 +- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFindRecords.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFindRecords.stories.tsx index bb0e9220a4..d334aec124 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFindRecords.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionFindRecords.stories.tsx @@ -1,7 +1,7 @@ import { type WorkflowFindRecordsAction } from '@/workflow/types/Workflow'; import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords'; import { type Meta, type StoryObj } from '@storybook/react-vite'; -import { expect, fn, userEvent, within } from 'storybook/test'; +import { expect, fn, screen, userEvent, waitFor, within } from 'storybook/test'; import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; @@ -65,6 +65,58 @@ export const Default: Story = { }, }; +const onActionUpdateMock = fn(); + +export const KeepsLimitAndOffsetWhenObjectChanges: Story = { + args: { + action: { + ...DEFAULT_ACTION, + settings: { + ...DEFAULT_ACTION.settings, + input: { + objectName: 'person', + limit: 100, + offset: 20, + }, + }, + }, + actionOptions: { + onActionUpdate: onActionUpdateMock, + }, + }, + play: async ({ canvasElement }) => { + onActionUpdateMock.mockClear(); + + const canvas = within(canvasElement); + + await userEvent.click(await canvas.findByText('People')); + + await userEvent.type( + await screen.findByPlaceholderText('Search'), + 'Companies', + ); + + await userEvent.click(await screen.findByText('Companies')); + + await waitFor( + () => { + expect(onActionUpdateMock).toHaveBeenCalledWith( + expect.objectContaining({ + settings: expect.objectContaining({ + input: expect.objectContaining({ + objectName: 'company', + limit: 100, + offset: 20, + }), + }), + }), + ); + }, + { timeout: 3000 }, + ); + }, +}; + export const DisabledWithEmptyValues: Story = { args: { actionOptions: { diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx index 1586bece65..c476e4d995 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx @@ -178,8 +178,8 @@ export const WorkflowEditActionFindRecords = ({ const newFormData: FindRecordsFormData = { objectNameSingular: value, - limit: 1, - offset: 0, + limit: formData.limit, + offset: formData.offset, }; setFormData(newFormData);