Improved dropdown menu headers for filter and sorts (#13177)

This PR improves dropdown menu headers for filter and sort dropdown in
view bar and editable filter chips.

It adds what's necessary to navigate back or close the dropdown, so that
we don't rely solely on click outside to exit the dropdown.

This PR also refactors the components so that we clearly identify the
two code paths that can use filter dropdowns : view bar and filter chip,
everything that can be DRY stays in the object-filter-dropdown module
but we try to have our wrapping components in each distinct module
instead of blending everything with ternaries inside
object-filter-dropdown module.

The vector search input value wasn't correctly handled across the
different dropdowns, due to a wrong component instance management, since
the dropdown menu header improvement put this into light, I also
refactored the state management of the vector search input.

@Bonapara please check the QA video and tell me if it's ok, I didn't add
dropdown menu header on the advanced filter field list dropdown because
it's a select more than a standalone dropdown, what do you think ?

QA : 


https://github.com/user-attachments/assets/17080f32-f302-436c-937b-3577715b7e84


QA Vector search fix : 



https://github.com/user-attachments/assets/6367bbf6-8a98-4b53-86cf-6ba92be130eb

Fixes https://github.com/twentyhq/core-team-issues/issues/640
Fixes https://github.com/twentyhq/core-team-issues/issues/1206
This commit is contained in:
Lucas Bordeau
2025-07-11 16:47:52 +02:00
committed by GitHub
parent 6285613a25
commit e53e09dfd3
21 changed files with 422 additions and 197 deletions

View File

@ -0,0 +1,38 @@
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
import { DATE_PICKER_DROPDOWN_CONTENT_WIDTH } from '@/object-record/object-filter-dropdown/constants/DatePickerDropdownContentWidth';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared/utils';
export const ObjectFilterDropdownContentWrapper = ({
children,
}: React.PropsWithChildren) => {
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
if (!isDefined(fieldMetadataItemUsedInDropdown)) {
return null;
}
const filterType = getFilterTypeFromFieldType(
fieldMetadataItemUsedInDropdown.type,
);
const isDateFilter = DATE_FILTER_TYPES.includes(filterType);
return (
<DropdownContent
widthInPixels={
isDateFilter
? DATE_PICKER_DROPDOWN_CONTENT_WIDTH
: GenericDropdownContentWidth.ExtraLarge
}
>
{children}
</DropdownContent>
);
};

View File

@ -5,22 +5,19 @@ import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-d
import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect';
import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { ViewBarFilterDropdownVectorSearchInput } from '@/views/components/ViewBarFilterDropdownVectorSearchInput';
import { ViewFilterOperand } from 'twenty-shared/src/types/ViewFilterOperand';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { ObjectFilterDropdownBooleanSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect';
import { ObjectFilterDropdownFilterInputHeader } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInputHeader';
import { ObjectFilterDropdownInnerSelectOperandDropdown } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownInnerSelectOperandDropdown';
import { ObjectFilterDropdownTextInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput';
import { ObjectFilterDropdownVectorSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownVectorSearchInput';
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
import { DATE_PICKER_DROPDOWN_CONTENT_WIDTH } from '@/object-record/object-filter-dropdown/constants/DatePickerDropdownContentWidth';
import { NUMBER_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/NumberFilterTypes';
import { TEXT_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/TextFilterTypes';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared/utils';
@ -62,11 +59,7 @@ export const ObjectFilterDropdownFilterInput = ({
selectedOperandInDropdown === ViewFilterOperand.VectorSearch;
if (isVectorSearchFilter && isDefined(filterDropdownId)) {
return (
<ViewBarFilterDropdownVectorSearchInput
filterDropdownId={filterDropdownId}
/>
);
return <ObjectFilterDropdownVectorSearchInput />;
}
if (!isDefined(fieldMetadataItemUsedInDropdown)) {
@ -82,24 +75,21 @@ export const ObjectFilterDropdownFilterInput = ({
if (isOnlyOperand) {
return (
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<ObjectFilterDropdownFilterInputHeader />
<>
<ObjectFilterDropdownInnerSelectOperandDropdown />
</DropdownContent>
</>
);
} else if (isDateFilter) {
return (
<DropdownContent widthInPixels={DATE_PICKER_DROPDOWN_CONTENT_WIDTH}>
<ObjectFilterDropdownFilterInputHeader />
<>
<ObjectFilterDropdownInnerSelectOperandDropdown />
<DropdownMenuSeparator />
<ObjectFilterDropdownDateInput />
</DropdownContent>
</>
);
} else {
return (
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<ObjectFilterDropdownFilterInputHeader />
<>
<ObjectFilterDropdownInnerSelectOperandDropdown />
<DropdownMenuSeparator />
{TEXT_FILTER_TYPES.includes(filterType) && (
@ -130,7 +120,7 @@ export const ObjectFilterDropdownFilterInput = ({
</>
)}
{filterType === 'BOOLEAN' && <ObjectFilterDropdownBooleanSelect />}
</DropdownContent>
</>
);
}
};

View File

@ -1,16 +1,56 @@
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewBarFilterDropdownFilterInputMenuHeader } from '@/views/components/ViewBarFilterDropdownFilterInputMenuHeader';
import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId';
import { useLingui } from '@lingui/react/macro';
import { useContext } from 'react';
import { ViewFilterOperand } from 'twenty-shared/types';
import { IconX } from 'twenty-ui/display';
export const ObjectFilterDropdownFilterInputHeader = () => {
const { t } = useLingui();
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
return (
<DropdownMenuHeader>
{fieldMetadataItemUsedInDropdown?.label}
</DropdownMenuHeader>
const selectedOperandInDropdown = useRecoilComponentValueV2(
selectedOperandInDropdownComponentState,
);
const { closeDropdown } = useCloseDropdown();
const dropdownInstanceId = useContext(
DropdownComponentInstanceContext,
)?.instanceId;
const isInViewBarFilterDropdown =
dropdownInstanceId === VIEW_BAR_FILTER_DROPDOWN_ID;
const isVectorSearchFilter =
selectedOperandInDropdown === ViewFilterOperand.VectorSearch;
if (isInViewBarFilterDropdown) {
return <ViewBarFilterDropdownFilterInputMenuHeader />;
} else {
return (
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={() => closeDropdown(dropdownInstanceId)}
Icon={IconX}
/>
}
>
{isVectorSearchFilter
? t`Search`
: fieldMetadataItemUsedInDropdown?.label}
</DropdownMenuHeader>
);
}
};

View File

@ -1,4 +1,6 @@
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
import { DATE_PICKER_DROPDOWN_CONTENT_WIDTH } from '@/object-record/object-filter-dropdown/constants/DatePickerDropdownContentWidth';
import { useApplyObjectFilterDropdownOperand } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownOperand';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
@ -7,6 +9,7 @@ import { getOperandLabel } from '@/object-record/object-filter-dropdown/utils/ge
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { DropdownMenuInnerSelect } from '@/ui/layout/dropdown/components/DropdownMenuInnerSelect';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared/utils';
import { SelectOption } from 'twenty-ui/input';
@ -54,16 +57,30 @@ export const ObjectFilterDropdownInnerSelectOperandDropdown = () => {
);
};
if (!isDefined(selectedOperandInDropdown)) {
if (
!isDefined(selectedOperandInDropdown) ||
!isDefined(fieldMetadataItemUsedInDropdown)
) {
return null;
}
const filterType = getFilterTypeFromFieldType(
fieldMetadataItemUsedInDropdown.type,
);
const isDateFilter = DATE_FILTER_TYPES.includes(filterType);
const widthInPixels = isDateFilter
? DATE_PICKER_DROPDOWN_CONTENT_WIDTH
: GenericDropdownContentWidth.ExtraLarge;
return (
<DropdownMenuInnerSelect
dropdownId={OBJECT_FILTER_DROPDOWN_INNER_SELECT_OPERAND_DROPDOWN_ID}
selectedOption={selectedOption}
onChange={handleOperandChange}
options={options}
widthInPixels={widthInPixels}
/>
);
};

View File

@ -0,0 +1,38 @@
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useVectorSearchFilterActions } from '@/views/hooks/useVectorSearchFilterActions';
import { vectorSearchInputComponentState } from '@/views/states/vectorSearchInputComponentState';
import { useLingui } from '@lingui/react/macro';
import { useDebouncedCallback } from 'use-debounce';
export const ObjectFilterDropdownVectorSearchInput = () => {
const { t } = useLingui();
const [vectorSearchInputValue, setVectorSearchInputValue] =
useRecoilComponentStateV2(vectorSearchInputComponentState);
const { applyVectorSearchFilter } = useVectorSearchFilterActions();
const debouncedApplyVectorSearchFilter = useDebouncedCallback(
(value: string) => {
applyVectorSearchFilter(value);
},
500,
);
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
setVectorSearchInputValue(inputValue);
debouncedApplyVectorSearchFilter(inputValue);
};
return (
<DropdownMenuSearchInput
autoFocus
type="text"
value={vectorSearchInputValue}
placeholder={t`Search`}
onChange={handleSearchChange}
/>
);
};

View File

@ -1,5 +1,3 @@
import styled from '@emotion/styled';
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { OBJECT_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ObjectSortDropdownId';
@ -20,7 +18,10 @@ import { visibleTableColumnsComponentSelector } from '@/object-record/record-tab
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuInnerSelect } from '@/ui/layout/dropdown/components/DropdownMenuInnerSelect';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
@ -32,54 +33,12 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useTheme } from '@emotion/react';
import { Trans, useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { IconChevronDown, useIcons } from 'twenty-ui/display';
import { IconX, useIcons } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { v4 } from 'uuid';
export const StyledInput = styled.input`
background: transparent;
border: none;
border-top: none;
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: 0;
color: ${({ theme }) => theme.font.color.primary};
margin: 0;
outline: none;
padding: ${({ theme }) => theme.spacing(2)};
height: 19px;
font-family: inherit;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: inherit;
max-width: 100%;
overflow: hidden;
text-decoration: none;
&::placeholder {
color: ${({ theme }) => theme.font.color.light};
}
`;
const StyledSelectedSortDirectionContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
box-shadow: ${({ theme }) => theme.boxShadow.light};
border-radius: ${({ theme }) => theme.border.radius.md};
position: absolute;
top: 32px;
width: 100%;
z-index: 1000;
`;
const StyledDropdownMenuHeaderEndComponent = styled.div`
padding: ${({ theme }) => theme.spacing(1)};
display: flex;
align-items: center;
`;
export const ObjectSortDropdownButton = () => {
const { resetRecordSortDropdownSearchInput } =
useResetRecordSortDropdownSearchInput();
@ -88,10 +47,6 @@ export const ObjectSortDropdownButton = () => {
objectSortDropdownSearchInputComponentState,
);
const isRecordSortDirectionMenuUnfolded = useRecoilComponentValueV2(
isRecordSortDirectionDropdownMenuUnfoldedComponentState,
);
const { resetSortDropdown } = useResetSortDropdown();
const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow();
@ -195,8 +150,6 @@ export const ObjectSortDropdownButton = () => {
const { t } = useLingui();
const theme = useTheme();
const selectableItemIdArray = [
...visibleFieldMetadataItems.map((item) => item.id),
...hiddenFieldMetadataItems.map((item) => item.id),
@ -227,51 +180,50 @@ export const ObjectSortDropdownButton = () => {
}
dropdownComponents={
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={() => closeSortDropdown()}
Icon={IconX}
/>
}
>
{t`Sort`}
</DropdownMenuHeader>
<DropdownMenuInnerSelect
dropdownId="record-sort-direction-dropdown"
options={RECORD_SORT_DIRECTIONS.map((sortDirection) => ({
value: sortDirection,
label: sortDirection === 'asc' ? t`Ascending` : t`Descending`,
}))}
selectedOption={{
value: selectedRecordSortDirection,
label:
selectedRecordSortDirection === 'asc'
? t`Ascending`
: t`Descending`,
}}
onChange={(sortDirection) =>
handleSortDirectionClick(
sortDirection.value as RecordSortDirection,
)
}
widthInPixels={GenericDropdownContentWidth.ExtraLarge}
/>
<DropdownMenuSeparator />
<DropdownMenuSearchInput
autoFocus
value={objectSortDropdownSearchInput}
placeholder={t`Search fields`}
onChange={(event) =>
setObjectSortDropdownSearchInput(event.target.value)
}
/>
<SelectableList
selectableListInstanceId={OBJECT_SORT_DROPDOWN_ID}
selectableItemIdArray={selectableItemIdArray}
focusId={OBJECT_SORT_DROPDOWN_ID}
>
{isRecordSortDirectionMenuUnfolded && (
<StyledSelectedSortDirectionContainer>
<DropdownMenuItemsContainer>
{RECORD_SORT_DIRECTIONS.map((sortDirection, index) => (
<MenuItem
key={index}
focused={selectedItemId === sortDirection}
onClick={() => handleSortDirectionClick(sortDirection)}
text={
sortDirection === 'asc' ? t`Ascending` : t`Descending`
}
/>
))}
</DropdownMenuItemsContainer>
</StyledSelectedSortDirectionContainer>
)}
<DropdownMenuHeader
onClick={() =>
setIsRecordSortDirectionMenuUnfolded(
!isRecordSortDirectionMenuUnfolded,
)
}
EndComponent={
<StyledDropdownMenuHeaderEndComponent>
<IconChevronDown size={theme.icon.size.md} />
</StyledDropdownMenuHeaderEndComponent>
}
>
{selectedRecordSortDirection === 'asc'
? t`Ascending`
: t`Descending`}
</DropdownMenuHeader>
<StyledInput
autoFocus
value={objectSortDropdownSearchInput}
placeholder={t`Search fields`}
onChange={(event) =>
setObjectSortDropdownSearchInput(event.target.value)
}
/>
{shouldShowVisibleFields && (
<>
<DropdownMenuSectionLabel label={t`Visible fields`} />

View File

@ -33,6 +33,7 @@ export type DropdownMenuInnerSelectProps = {
onChange: (value: SelectOption) => void;
options: SelectOption[];
dropdownId: string;
widthInPixels?: number;
};
export const DropdownMenuInnerSelect = ({
@ -40,6 +41,7 @@ export const DropdownMenuInnerSelect = ({
onChange,
options,
dropdownId,
widthInPixels,
}: DropdownMenuInnerSelectProps) => {
const theme = useTheme();
@ -54,7 +56,7 @@ export const DropdownMenuInnerSelect = ({
</StyledDropdownMenuInnerSelectDropdownButton>
}
dropdownComponents={
<DropdownContent>
<DropdownContent widthInPixels={widthInPixels}>
<DropdownMenuItemsContainer>
{options.map((selectOption) => (
<MenuItemSelect

View File

@ -0,0 +1,21 @@
import { ObjectFilterDropdownContentWrapper } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownContentWrapper';
import { ObjectFilterDropdownFilterInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput';
import { EditableFilterChipDropdownMenuHeader } from '@/views/components/EditableFilterChipDropdownMenuHeader';
type EditableFilterChipDropdownContentProps = {
recordFilterId: string;
};
export const EditableFilterChipDropdownContent = ({
recordFilterId,
}: EditableFilterChipDropdownContentProps) => {
return (
<ObjectFilterDropdownContentWrapper>
<EditableFilterChipDropdownMenuHeader />
<ObjectFilterDropdownFilterInput
filterDropdownId={recordFilterId}
recordFilterId={recordFilterId}
/>
</ObjectFilterDropdownContentWrapper>
);
};

View File

@ -0,0 +1,45 @@
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useLingui } from '@lingui/react/macro';
import { ViewFilterOperand } from 'twenty-shared/types';
import { IconX } from 'twenty-ui/display';
export const EditableFilterChipDropdownMenuHeader = () => {
const { t } = useLingui();
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const selectedOperandInDropdown = useRecoilComponentValueV2(
selectedOperandInDropdownComponentState,
);
const isVectorSearchFilter =
selectedOperandInDropdown === ViewFilterOperand.VectorSearch;
const { closeDropdown } = useCloseDropdown();
const handleBackButtonClick = () => {
closeDropdown();
};
return (
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={handleBackButtonClick}
Icon={IconX}
/>
}
>
{isVectorSearchFilter
? t`Search`
: fieldMetadataItemUsedInDropdown?.label}
</DropdownMenuHeader>
);
};

View File

@ -4,10 +4,11 @@ import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { EditableFilterChip } from '@/views/components/EditableFilterChip';
import { ObjectFilterDropdownFilterInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput';
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { EditableFilterChipDropdownContent } from '@/views/components/EditableFilterChipDropdownContent';
import { useClearVectorSearchInput } from '@/views/hooks/useClearVectorSearchInput';
import { useSetEditableFilterChipDropdownStates } from '@/views/hooks/useSetEditableFilterChipDropdownStates';
type EditableFilterDropdownButtonProps = {
@ -29,13 +30,17 @@ export const EditableFilterDropdownButton = ({
removeRecordFilter({ recordFilterId: recordFilter.id });
};
const { clearVectorSearchInput } = useClearVectorSearchInput();
const onFilterDropdownClose = useCallback(() => {
const recordFilterIsEmpty = isRecordFilterConsideredEmpty(recordFilter);
if (recordFilterIsEmpty) {
removeRecordFilter({ recordFilterId: recordFilter.id });
}
}, [recordFilter, removeRecordFilter]);
clearVectorSearchInput();
}, [recordFilter, removeRecordFilter, clearVectorSearchInput]);
const { setEditableFilterChipDropdownStates } =
useSetEditableFilterChipDropdownStates();
@ -56,7 +61,7 @@ export const EditableFilterDropdownButton = ({
/>
}
dropdownComponents={
<ObjectFilterDropdownFilterInput filterDropdownId={recordFilter.id} />
<EditableFilterChipDropdownContent recordFilterId={recordFilter.id} />
}
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"

View File

@ -8,6 +8,7 @@ import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRe
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewBarFilterDropdownContent } from '@/views/components/ViewBarFilterDropdownContent';
import { useClearVectorSearchInput } from '@/views/hooks/useClearVectorSearchInput';
import { isDefined } from 'twenty-shared/utils';
import { ViewBarFilterButton } from './ViewBarFilterButton';
@ -20,6 +21,8 @@ export const ViewBarFilterDropdown = () => {
objectFilterDropdownCurrentRecordFilterComponentState,
);
const { clearVectorSearchInput } = useClearVectorSearchInput();
const handleDropdownClickOutside = () => {
const recordFilterIsEmpty =
isDefined(objectFilterDropdownCurrentRecordFilter) &&
@ -37,6 +40,7 @@ export const ViewBarFilterDropdown = () => {
const handleDropdownClose = () => {
resetFilterDropdown();
removeEmptyVectorSearchFilter();
clearVectorSearchInput();
};
const handleDropdownOpen = () => {

View File

@ -1,8 +1,12 @@
import { ObjectFilterDropdownFilterInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput';
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewBarFilterDropdownFieldSelectMenu } from '@/views/components/ViewBarFilterDropdownFieldSelectMenu';
import { ViewBarFilterDropdownFilterInput } from '@/views/components/ViewBarFilterDropdownFilterInput';
import { ViewBarFilterDropdownVectorSearchInput } from '@/views/components/ViewBarFilterDropdownVectorSearchInput';
import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId';
import { ViewFilterOperand } from 'twenty-shared/types';
export const ViewBarFilterDropdownContent = () => {
const [objectFilterDropdownFilterIsSelected] = useRecoilComponentStateV2(
@ -10,14 +14,23 @@ export const ViewBarFilterDropdownContent = () => {
VIEW_BAR_FILTER_DROPDOWN_ID,
);
const selectedOperandInDropdown = useRecoilComponentValueV2(
selectedOperandInDropdownComponentState,
);
const isVectorSearchFilter =
selectedOperandInDropdown === ViewFilterOperand.VectorSearch;
if (isVectorSearchFilter) {
return <ViewBarFilterDropdownVectorSearchInput />;
}
const shouldShowFilterInput = objectFilterDropdownFilterIsSelected;
return (
<>
{shouldShowFilterInput ? (
<ObjectFilterDropdownFilterInput
filterDropdownId={VIEW_BAR_FILTER_DROPDOWN_ID}
/>
<ViewBarFilterDropdownFilterInput />
) : (
<ViewBarFilterDropdownFieldSelectMenu />
)}

View File

@ -16,10 +16,14 @@ import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/
import { ViewBarFilterDropdownBottomMenu } from '@/views/components/ViewBarFilterDropdownBottomMenu';
import { ViewBarFilterDropdownFieldSelectMenuItem } from '@/views/components/ViewBarFilterDropdownFieldSelectMenuItem';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS } from '@/views/constants/ViewBarFilterBottomMenuItemIds';
import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId';
import { useLingui } from '@lingui/react/macro';
import { IconX } from 'twenty-ui/display';
export const StyledInput = styled.input`
background: transparent;
@ -56,6 +60,8 @@ export const ViewBarFilterDropdownFieldSelectMenu = () => {
selectableVisibleFieldMetadataItems,
} = useFilterDropdownSelectableFieldMetadataItems();
const { closeDropdown } = useCloseDropdown();
const selectableFieldMetadataItemIds = [
...selectableVisibleFieldMetadataItems.map(
(fieldMetadataItem) => fieldMetadataItem.id,
@ -83,6 +89,16 @@ export const ViewBarFilterDropdownFieldSelectMenu = () => {
return (
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={() => closeDropdown()}
Icon={IconX}
/>
}
>
{t`Filter`}
</DropdownMenuHeader>
<StyledInput
value={objectFilterDropdownSearchInput}
autoFocus

View File

@ -0,0 +1,22 @@
import { ObjectFilterDropdownContentWrapper } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownContentWrapper';
import { ObjectFilterDropdownFilterInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput';
import { ViewBarFilterDropdownFilterInputMenuHeader } from '@/views/components/ViewBarFilterDropdownFilterInputMenuHeader';
import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId';
type ViewBarFilterDropdownFilterInputProps = {
recordFilterId?: string;
};
export const ViewBarFilterDropdownFilterInput = ({
recordFilterId,
}: ViewBarFilterDropdownFilterInputProps) => {
return (
<ObjectFilterDropdownContentWrapper>
<ViewBarFilterDropdownFilterInputMenuHeader />
<ObjectFilterDropdownFilterInput
filterDropdownId={VIEW_BAR_FILTER_DROPDOWN_ID}
recordFilterId={recordFilterId}
/>
</ObjectFilterDropdownContentWrapper>
);
};

View File

@ -0,0 +1,49 @@
import { useResetFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useResetFilterDropdown';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useClearVectorSearchInput } from '@/views/hooks/useClearVectorSearchInput';
import { useLingui } from '@lingui/react/macro';
import { ViewFilterOperand } from 'twenty-shared/types';
import { IconChevronLeft } from 'twenty-ui/display';
export const ViewBarFilterDropdownFilterInputMenuHeader = () => {
const { t } = useLingui();
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const selectedOperandInDropdown = useRecoilComponentValueV2(
selectedOperandInDropdownComponentState,
);
const isVectorSearchFilter =
selectedOperandInDropdown === ViewFilterOperand.VectorSearch;
const { clearVectorSearchInput } = useClearVectorSearchInput();
const { resetFilterDropdown } = useResetFilterDropdown();
const handleBackButtonClick = () => {
resetFilterDropdown();
clearVectorSearchInput();
};
return (
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={handleBackButtonClick}
Icon={IconChevronLeft}
/>
}
>
{isVectorSearchFilter
? t`Search`
: fieldMetadataItemUsedInDropdown?.label}
</DropdownMenuHeader>
);
};

View File

@ -9,7 +9,6 @@ import { IconSearch } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS } from '@/views/constants/ViewBarFilterBottomMenuItemIds';
import { VIEW_BAR_FILTER_DROPDOWN_ID } from '@/views/constants/ViewBarFilterDropdownId';
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { useOpenVectorSearchFilter } from '@/views/hooks/useOpenVectorSearchFilter';
@ -26,20 +25,16 @@ export const ViewBarFilterDropdownVectorSearchButton = () => {
const { t } = useLingui();
const [, setVectorSearchInputValue] = useRecoilComponentStateV2(
vectorSearchInputComponentState,
VIEW_BAR_FILTER_DROPDOWN_ID,
);
const { setVectorSearchInputValueFromExistingFilter } =
useSetVectorSearchInputValueFromExistingFilter(VIEW_BAR_FILTER_DROPDOWN_ID);
useSetVectorSearchInputValueFromExistingFilter();
const fieldSearchInputValue = useRecoilComponentValueV2(
const objectFilterDropdownSearchInput = useRecoilComponentValueV2(
objectFilterDropdownSearchInputComponentState,
VIEW_BAR_FILTER_DROPDOWN_ID,
);
const { applyVectorSearchFilter } = useVectorSearchFilterActions();
const { openVectorSearchFilter } = useOpenVectorSearchFilter(
VIEW_BAR_FILTER_DROPDOWN_ID,
);
const { openVectorSearchFilter } = useOpenVectorSearchFilter();
const isSelected = useRecoilComponentFamilyValueV2(
isSelectedItemIdComponentFamilySelector,
@ -49,9 +44,9 @@ export const ViewBarFilterDropdownVectorSearchButton = () => {
const handleSearchClick = () => {
openVectorSearchFilter();
if (fieldSearchInputValue.length > 0) {
setVectorSearchInputValue(fieldSearchInputValue);
applyVectorSearchFilter(fieldSearchInputValue);
if (objectFilterDropdownSearchInput.length > 0) {
setVectorSearchInputValue(objectFilterDropdownSearchInput);
applyVectorSearchFilter(objectFilterDropdownSearchInput);
} else {
setVectorSearchInputValueFromExistingFilter();
}
@ -69,8 +64,8 @@ export const ViewBarFilterDropdownVectorSearchButton = () => {
text={
<>
{t`Search`}
{fieldSearchInputValue && (
<StyledSearchText>{t`· ${fieldSearchInputValue}`}</StyledSearchText>
{objectFilterDropdownSearchInput && (
<StyledSearchText>{t`· ${objectFilterDropdownSearchInput}`}</StyledSearchText>
)}
</>
}

View File

@ -1,47 +1,13 @@
import { ObjectFilterDropdownVectorSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownVectorSearchInput';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useVectorSearchFilterActions } from '@/views/hooks/useVectorSearchFilterActions';
import { vectorSearchInputComponentState } from '@/views/states/vectorSearchInputComponentState';
import { useLingui } from '@lingui/react/macro';
import { useDebouncedCallback } from 'use-debounce';
export const ViewBarFilterDropdownVectorSearchInput = ({
filterDropdownId,
}: {
filterDropdownId: string;
}) => {
const { t } = useLingui();
const [vectorSearchInputValue, setVectorSearchInputValue] =
useRecoilComponentStateV2(
vectorSearchInputComponentState,
filterDropdownId,
);
const { applyVectorSearchFilter } = useVectorSearchFilterActions();
const debouncedApplyVectorSearchFilter = useDebouncedCallback(
(value: string) => {
applyVectorSearchFilter(value);
},
500,
);
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
setVectorSearchInputValue(inputValue);
debouncedApplyVectorSearchFilter(inputValue);
};
import { ViewBarFilterDropdownFilterInputMenuHeader } from '@/views/components/ViewBarFilterDropdownFilterInputMenuHeader';
export const ViewBarFilterDropdownVectorSearchInput = () => {
return (
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
<DropdownMenuSearchInput
autoFocus
type="text"
value={vectorSearchInputValue}
placeholder={t`Search`}
onChange={handleSearchChange}
/>
<ViewBarFilterDropdownFilterInputMenuHeader />
<ObjectFilterDropdownVectorSearchInput />
</DropdownContent>
);
};

View File

@ -0,0 +1,16 @@
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { vectorSearchInputComponentState } from '@/views/states/vectorSearchInputComponentState';
export const useClearVectorSearchInput = () => {
const setVectorSearchInputValue = useSetRecoilComponentStateV2(
vectorSearchInputComponentState,
);
const clearVectorSearchInput = () => {
setVectorSearchInputValue('');
};
return {
clearVectorSearchInput,
};
};

View File

@ -24,15 +24,6 @@ export const useSetEditableFilterChipDropdownStates = () => {
? filterableFieldMetadataItems.concat(vectorSearchField)
: filterableFieldMetadataItems;
const fieldMetadataItem = filterableFieldsWithVector.find(
(fieldMetadataItem) =>
fieldMetadataItem.id === recordFilter.fieldMetadataId,
);
if (!isDefined(fieldMetadataItem)) {
return;
}
if (isVectorSearchFilter(recordFilter)) {
set(
vectorSearchInputComponentState.atomFamily({
@ -42,13 +33,20 @@ export const useSetEditableFilterChipDropdownStates = () => {
);
}
set(
fieldMetadataItemIdUsedInDropdownComponentState.atomFamily({
instanceId: recordFilter.id,
}),
fieldMetadataItem.id,
const fieldMetadataItem = filterableFieldsWithVector.find(
(fieldMetadataItem) =>
fieldMetadataItem.id === recordFilter.fieldMetadataId,
);
if (isDefined(fieldMetadataItem)) {
set(
fieldMetadataItemIdUsedInDropdownComponentState.atomFamily({
instanceId: recordFilter.id,
}),
fieldMetadataItem.id,
);
}
set(
selectedOperandInDropdownComponentState.atomFamily({
instanceId: recordFilter.id,

View File

@ -3,17 +3,15 @@ import { vectorSearchInputComponentState } from '@/views/states/vectorSearchInpu
import { isDefined } from 'twenty-shared/utils';
import { useVectorSearchFilterState } from './useVectorSearchFilterState';
export const useSetVectorSearchInputValueFromExistingFilter = (
filterDropdownId: string,
) => {
export const useSetVectorSearchInputValueFromExistingFilter = () => {
const [, setVectorSearchInputValue] = useRecoilComponentStateV2(
vectorSearchInputComponentState,
filterDropdownId,
);
const { getExistingVectorSearchFilter } = useVectorSearchFilterState();
const setVectorSearchInputValueFromExistingFilter = () => {
const existingVectorSearchFilter = getExistingVectorSearchFilter();
if (isDefined(existingVectorSearchFilter)) {
setVectorSearchInputValue(existingVectorSearchFilter.value);
}

View File

@ -1,8 +1,8 @@
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
export const vectorSearchInputComponentState = createComponentStateV2<string>({
key: 'vectorSearchInputComponentState',
defaultValue: '',
componentInstanceContext: ViewComponentInstanceContext,
componentInstanceContext: ObjectFilterDropdownComponentInstanceContext,
});