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
@@ -19,8 +19,12 @@ const StyledFilterRow = styled.div<{ isMobile: boolean }>`
gap: ${themeCssVariables.spacing[2]};
grid-template-columns: ${({ isMobile }) =>
isMobile
? `${OBJECT_MOBILE_WIDTH}px ${ACTION_MOBILE_WIDTH}px auto`
: `${OBJECT_DROPDOWN_WIDTH}px ${ACTION_DROPDOWN_WIDTH}px auto`};
? `minmax(${OBJECT_MOBILE_WIDTH}px, 1fr) ${ACTION_MOBILE_WIDTH}px ${
themeCssVariables.spacing[8]
}`
: `minmax(${OBJECT_DROPDOWN_WIDTH}px, 1fr) ${ACTION_DROPDOWN_WIDTH}px ${
themeCssVariables.spacing[8]
}`};
margin-bottom: ${themeCssVariables.spacing[2]};
`;
@@ -91,6 +95,7 @@ export const SettingsDatabaseEventsForm = ({
Icon={IconTrash}
variant="tertiary"
size="medium"
ariaLabel={t`Remove filter`}
onClick={() => removeOperation?.(index)}
/>
) : (
@@ -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}
@@ -5,7 +5,8 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledExternalContainer = styled.div<{
maxHeight?: number;
}>`
--padding: ${themeCssVariables.spacing[1]};
--dropdown-menu-items-padding: ${themeCssVariables.spacing[1]};
--dropdown-menu-items-row-gap: 2px;
align-items: flex-start;
box-sizing: border-box;
@@ -17,7 +18,7 @@ const StyledExternalContainer = styled.div<{
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : 'none')};
padding: var(--padding);
padding: var(--dropdown-menu-items-padding);
width: 100%;
`;
@@ -41,10 +42,34 @@ const StyledInternalContainer = styled.div`
display: flex;
flex-direction: column;
gap: 2px;
gap: var(--dropdown-menu-items-row-gap);
height: 100%;
width: 100%;
> [data-dropdown-menu-section-label],
> [data-dropdown-menu-separator] {
margin-left: calc(0px - var(--dropdown-menu-items-padding));
margin-right: calc(0px - var(--dropdown-menu-items-padding));
width: calc(
100% + var(--dropdown-menu-items-padding) +
var(--dropdown-menu-items-padding)
);
}
> [data-dropdown-menu-section-label]:first-child {
margin-top: calc(0px - var(--dropdown-menu-items-padding));
}
> [data-dropdown-menu-section-label] + * {
margin-top: calc(
var(--dropdown-menu-items-padding) - var(--dropdown-menu-items-row-gap)
);
}
> [data-dropdown-menu-separator] + [data-dropdown-menu-section-label] {
margin-top: calc(0px - var(--dropdown-menu-items-row-gap));
}
`;
export const DropdownMenuItemsContainer = ({
@@ -4,14 +4,16 @@ import { styled } from '@linaria/react';
const StyledDropdownMenuSectionLabel = styled.div`
align-items: center;
background-color: ${themeCssVariables.background.transparent.lighter};
box-sizing: border-box;
color: ${themeCssVariables.font.color.tertiary};
display: flex;
font-size: ${themeCssVariables.font.size.xxs};
justify-content: flex-start;
min-height: 20px;
padding-left: ${themeCssVariables.spacing[1]};
padding-left: ${themeCssVariables.spacing[2]};
padding-right: ${themeCssVariables.spacing[2]};
user-select: none;
width: auto;
width: 100%;
`;
export type DropdownMenuSectionLabelProps = {
@@ -22,6 +24,8 @@ export const DropdownMenuSectionLabel = ({
label,
}: DropdownMenuSectionLabelProps) => {
return (
<StyledDropdownMenuSectionLabel>{label}</StyledDropdownMenuSectionLabel>
<StyledDropdownMenuSectionLabel data-dropdown-menu-section-label>
{label}
</StyledDropdownMenuSectionLabel>
);
};
@@ -1,4 +1,5 @@
import { styled } from '@linaria/react';
import { forwardRef } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledDropdownMenuSeparator = styled.div`
@@ -7,4 +8,8 @@ const StyledDropdownMenuSeparator = styled.div`
width: 100%;
`;
export const DropdownMenuSeparator = StyledDropdownMenuSeparator;
export const DropdownMenuSeparator = forwardRef<HTMLDivElement>((_, ref) => {
return <StyledDropdownMenuSeparator ref={ref} data-dropdown-menu-separator />;
});
DropdownMenuSeparator.displayName = 'DropdownMenuSeparator';