fix(workflow): show account select in Send Email node when no account is connected (#23066)

# 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 `<Select>`; since #21075 it only
prepends it when `isNullable` is set, and the email account field
doesn't set it. With zero connected accounts, `<Select>` 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).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23066?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Thomas Trompette
2026-07-20 18:53:29 +02:00
committed by GitHub
parent 1be5a0e54a
commit 05132d262b
2 changed files with 33 additions and 3 deletions
@@ -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,
@@ -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',