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" />  ## 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:
committed by
GitHub
parent
25fe66565c
commit
1db9b0c657
+7
-2
@@ -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)}
|
||||
/>
|
||||
) : (
|
||||
|
||||
+37
-91
@@ -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}
|
||||
|
||||
+28
-3
@@ -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 = ({
|
||||
|
||||
+7
-3
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+6
-1
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user