Fix webhook entity dropdown layout (#22497)

## Summary
- Reuse the shared `SelectControl` for the webhook entity selector.
- Update webhook entity menu icons and object ordering.
- Fix dropdown section labels and separators so headers span full width
while menu items keep the expected inset and 4px header spacing.
- Keep the webhook filter row responsive and the remove-filter button at
icon-button width.

## Before/After
<img width="3454" height="2000" alt="image"
src="https://github.com/user-attachments/assets/bfe0cc39-ad66-4cae-98be-1eddc66f12f3"
/>

![Webhook entity
dropdown](https://gist.githubusercontent.com/Bonapara/42ae4270ead4791e53636b728fb4a284/raw/d3b89c7d19cb7dd10d237a3279e93a98169177c6/webhook-entity-dropdown.svg)

## Tests
- `git diff --check`
- `npx nx typecheck twenty-front`


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22497?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 des Francs
2026-07-03 11:03:41 +02:00
committed by GitHub
parent 25fe66565c
commit 1db9b0c657
5 changed files with 85 additions and 100 deletions
@@ -1,5 +1,6 @@
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useObjectMetadataSelectHelpers } from '@/object-metadata/hooks/useObjectMetadataSelectHelpers';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@@ -12,66 +13,22 @@ import { SelectableList } from '@/ui/layout/selectable-list/components/Selectabl
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { useContext, useState } from 'react';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
IconBox,
IconChevronDown,
IconCode,
IconEye,
IconNorthStar,
IconSettings,
IconDatabase,
IconFileInfo,
IconListDetails,
IconTable,
IconWebhook,
} from 'twenty-ui/icon';
import { type SelectOption } from 'twenty-ui/input';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
const WEBHOOK_ENTITY_DROPDOWN_ID = 'webhook-entity-select';
const StyledControlContainer = styled.div<{ disabled?: boolean }>`
align-items: center;
background-color: ${themeCssVariables.background.transparent.lighter};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.sm};
box-sizing: border-box;
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
gap: ${themeCssVariables.spacing[1]};
height: ${themeCssVariables.spacing[8]};
justify-content: space-between;
padding: 0 ${themeCssVariables.spacing[2]};
width: 100%;
&:hover {
background-color: ${({ disabled }) =>
disabled
? themeCssVariables.background.transparent.lighter
: themeCssVariables.background.transparent.light};
}
`;
const StyledControlLabel = styled.span`
color: ${themeCssVariables.font.color.primary};
font-size: ${themeCssVariables.font.size.md};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const StyledControlIconChevronDownContainer = styled.span<{
disabled?: boolean;
}>`
align-items: center;
color: ${({ disabled }) =>
disabled
? themeCssVariables.font.color.extraLight
: themeCssVariables.font.color.tertiary};
display: flex;
`;
type WebhookEntitySelectProps = {
value: string | null;
onChange: (value: string | null) => void;
@@ -85,7 +42,6 @@ export const WebhookEntitySelect = ({
disabled = false,
dropdownId = WEBHOOK_ENTITY_DROPDOWN_ID,
}: WebhookEntitySelectProps) => {
const { theme } = useContext(ThemeContext);
const { getSelectIconPropsFromObjectMetadataItem } =
useObjectMetadataSelectHelpers();
const [searchInput, setSearchInput] = useState('');
@@ -97,23 +53,29 @@ export const WebhookEntitySelect = ({
dropdownId,
);
const metadataOptions = [
{ label: t`All Metadata`, value: 'metadata.*', icon: IconNorthStar },
{ label: t`Object`, value: 'metadata.objectMetadata', icon: IconTable },
{ label: t`Field`, value: 'metadata.fieldMetadata', icon: IconBox },
{ label: t`View`, value: 'metadata.view', icon: IconEye },
{ label: t`View Field`, value: 'metadata.viewField', icon: IconEye },
{ label: t`Index`, value: 'metadata.index', icon: IconSettings },
{ label: t`Webhook`, value: 'metadata.webhook', icon: IconCode },
const metadataOptions: SelectOption<string>[] = [
{ label: t`All Metadata`, value: 'metadata.*', Icon: IconFileInfo },
{ label: t`Object`, value: 'metadata.objectMetadata', Icon: IconBox },
{ label: t`Field`, value: 'metadata.fieldMetadata', Icon: IconListDetails },
{ label: t`View`, value: 'metadata.view', Icon: IconTable },
{
label: t`View Field`,
value: 'metadata.viewField',
Icon: IconListDetails,
},
{ label: t`Index`, value: 'metadata.index', Icon: IconDatabase },
{ label: t`Webhook`, value: 'metadata.webhook', Icon: IconWebhook },
];
const objectOptions: SelectOption<string>[] = [
{ label: t`All Objects`, value: '*', Icon: IconNorthStar },
...objectMetadataItems.map((item) => ({
label: item.labelPlural,
value: item.nameSingular,
...getSelectIconPropsFromObjectMetadataItem(item),
})),
{ label: t`All Objects`, value: '*', Icon: IconBox },
...[...objectMetadataItems]
.sort((a, b) => a.labelPlural.localeCompare(b.labelPlural))
.map((item) => ({
label: item.labelPlural,
value: item.nameSingular,
...getSelectIconPropsFromObjectMetadataItem(item),
})),
];
const filteredObjectOptions = objectOptions.filter((option) =>
@@ -124,26 +86,11 @@ export const WebhookEntitySelect = ({
option.label.toLowerCase().includes(searchInput.toLowerCase()),
);
const getSelectedLabel = () => {
if (!isDefined(value)) {
return t`Select entity`;
}
if (value === '*') {
return t`All Objects`;
}
const metadataOption = metadataOptions.find((opt) => opt.value === value);
if (isDefined(metadataOption)) {
return metadataOption.label;
}
const objectOption = objectOptions.find((opt) => opt.value === value);
if (isDefined(objectOption)) {
return objectOption.label;
}
return value;
};
const selectedOption = !isDefined(value)
? { label: t`Select entity`, value: '' }
: ([...objectOptions, ...metadataOptions].find(
(option) => option.value === value,
) ?? { label: value, value, Icon: IconBox });
const handleSelect = (selectedValue: string) => {
if (disabled) return;
@@ -168,12 +115,11 @@ export const WebhookEntitySelect = ({
disableClickForClickableComponent={disabled}
onClose={() => setSearchInput('')}
clickableComponent={
<StyledControlContainer disabled={disabled}>
<StyledControlLabel>{getSelectedLabel()}</StyledControlLabel>
<StyledControlIconChevronDownContainer disabled={disabled}>
<IconChevronDown size={theme.icon.size.md} />
</StyledControlIconChevronDownContainer>
</StyledControlContainer>
<SelectControl
selectedOption={selectedOption}
isDisabled={disabled}
textAccent={!isDefined(value) ? 'placeholder' : 'default'}
/>
}
dropdownComponents={
<DropdownContent widthInPixels={GenericDropdownContentWidth.Medium}>
@@ -222,7 +168,7 @@ export const WebhookEntitySelect = ({
onEnter={() => handleSelect(option.value)}
>
<MenuItemSelect
LeftIcon={option.icon}
LeftIcon={option.Icon}
text={option.label}
selected={value === option.value}
focused={selectedItemId === option.value}