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 14:47:52 +00:00
committed by GitHub
parent 6285613a25
commit e53e09dfd3
21 changed files with 422 additions and 197 deletions
@@ -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>
);
};
@@ -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>
</>
);
}
};
@@ -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>
);
}
};
@@ -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}
/>
);
};
@@ -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}
/>
);
};
@@ -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`} />
@@ -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
@@ -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>
);
};
@@ -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>
);
};
@@ -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"
@@ -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 = () => {
@@ -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 />
)}
@@ -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
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
)}
</>
}
@@ -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>
);
};
@@ -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,
};
};
@@ -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,
@@ -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);
}
@@ -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,
});