From 05132d262b04f09cdf48dc07783c0b2e559a9aba Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Mon, 20 Jul 2026 18:53:29 +0200 Subject: [PATCH] fix(workflow): show account select in Send Email node when no account is connected (#23066) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Why In the workflow **Send Email** (and Draft Email) node, when the workspace has no eligible connected account, the **Account** field disappears entirely — only the variable picker icon remains. The "Add account" call-to-action is unreachable, so there is no way to connect an account from the node. ## Root cause Regression from #21075. `FormSelectFieldInput` used to always pass a default empty option ("No Account") to `` then has no option to resolve a selected option from and bails out rendering an empty fragment — even though a `callToActionButton` is configured: ```tsx // Select.tsx if (!isDefined(controlSelectedOption)) { return <>; } ``` # What changed `FormSelectFieldInput` now prepends the empty option whenever the field is nullable **or there are no options at all**. A populated non-nullable select still offers no clearing choice (the #21075 behavior is preserved); an empty one renders its "No X" state so the control — and its call-to-action — stay visible and clickable. # Test plan - New story `FormSelectFieldInput > NoOptionsWithCallToAction`: zero options + CTA renders the "No Work Policy" control, the dropdown opens, and the CTA is clickable. - New story `WorkflowEditActionEmailBase > NoConnectedAccounts`: with `MyConnectedAccounts` mocked to `[]`, the Account field renders "No Account" instead of vanishing. The meta's msw handlers move to the keyed-object form so the story can override a single query (story-level handler arrays get concatenated after the meta's, and msw's first match wins), and the story evicts the module-singleton Apollo client's cached accounts so it actually hits the empty mock. - `npx nx typecheck twenty-front` and `npx nx lint:diff-with-main twenty-front` pass; both story files pass under the storybook vitest project (13 stories total). Review in cubic --- .../components/FormSelectFieldInput.tsx | 5 ++- .../FormSelectFieldInput.stories.tsx | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSelectFieldInput.tsx index e8149052b1..06477fce6d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSelectFieldInput.tsx @@ -102,9 +102,8 @@ export const FormSelectFieldInput = ({ Icon: IconCircleOff, }; - const optionsWithEmptyOption = isNullable - ? [emptyOption, ...options] - : options; + const optionsWithEmptyOption = + isNullable || options.length === 0 ? [emptyOption, ...options] : options; const selectedOption = optionsWithEmptyOption.find( (option) => option.value === draftValue.value, diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormSelectFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormSelectFieldInput.stories.tsx index b10ea34429..5608716e38 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormSelectFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormSelectFieldInput.stories.tsx @@ -126,6 +126,37 @@ export const NonNullable: Story = { }, }; +export const NoOptionsWithCallToAction: Story = { + args: { + label: 'Work Policy', + defaultValue: undefined, + options: [], + onChange: fn(), + callToActionButton: { + text: 'Add work policy', + onClick: fn(), + }, + }, + play: async ({ args, canvasElement }) => { + const canvas = within(canvasElement); + + const selectControl = await canvas.findByText('No Work Policy'); + expect(selectControl).toBeVisible(); + + await userEvent.click(selectControl); + + const dropdown = within(canvasElement.ownerDocument.body); + + await waitFor(() => { + expect(dropdown.getByText('Add work policy')).toBeVisible(); + }); + + await userEvent.click(dropdown.getByText('Add work policy')); + + expect(args.callToActionButton?.onClick).toHaveBeenCalled(); + }, +}; + export const WithVariablePicker: Story = { args: { label: 'Work Policy',