From 2f9c16f8a7da0fddb24a166316d3cef5119bf8a4 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 18 Jul 2025 15:38:56 +0200 Subject: [PATCH] Add search any field front logic with its feature flag (#13278) This PR adds the frontend logic to handle the user input of a search any field value. It also adds the associated feature flag, that can be modified from the admin panel. This PR does not add the filtering part nor the saving on view logic, which will come in their separate PRs. https://github.com/user-attachments/assets/6a52c090-b957-46aa-bff7-a90b51109789 --- .../src/generated-metadata/graphql.ts | 1 + .../twenty-front/src/generated/graphql.ts | 1 + ...bjectFilterDropdownAnyFieldSearchInput.tsx | 27 +++++++++ .../ObjectFilterDropdownTextInput.tsx | 2 +- .../hooks/useResetFilterDropdown.ts | 9 +++ ...nAnyFieldSearchIsSelectedComponentState.ts | 9 +++ .../views/components/AnyFieldSearchChip.tsx | 29 ++++++++++ .../AnyFieldSearchDropdownButton.tsx | 28 +++++++++ .../AnyFieldSearchDropdownContent.tsx | 13 +++++ ...nyFieldSearchDropdownContentMenuHeader.tsx | 28 +++++++++ .../views/components/ViewBarDetails.tsx | 26 +++++++-- ...wBarFilterDropdownAnyFieldSearchButton.tsx | 17 ++++++ ...erDropdownAnyFieldSearchButtonMenuItem.tsx | 57 +++++++++++++++++++ ...ewBarFilterDropdownAnyFieldSearchInput.tsx | 13 +++++ ...pdownAnyFieldSearchInputDropdownHeader.tsx | 28 +++++++++ .../ViewBarFilterDropdownBottomMenu.tsx | 13 ++++- .../ViewBarFilterDropdownContent.tsx | 13 +++++ .../constants/AnyFieldSearchDropdownId.ts | 1 + .../useOpenAnyFieldSearchFilterFromViewBar.ts | 38 +++++++++++++ .../viewAnyFieldSearchValueComponentState.ts | 9 +++ .../enums/feature-flag-key.enum.ts | 1 + .../workspace-entity-manager.spec.ts | 1 + .../core/utils/seed-feature-flags.util.ts | 5 ++ 23 files changed, 363 insertions(+), 6 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownAnyFieldSearchInput.tsx create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownAnyFieldSearchIsSelectedComponentState.ts create mode 100644 packages/twenty-front/src/modules/views/components/AnyFieldSearchChip.tsx create mode 100644 packages/twenty-front/src/modules/views/components/AnyFieldSearchDropdownButton.tsx create mode 100644 packages/twenty-front/src/modules/views/components/AnyFieldSearchDropdownContent.tsx create mode 100644 packages/twenty-front/src/modules/views/components/AnyFieldSearchDropdownContentMenuHeader.tsx create mode 100644 packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchButton.tsx create mode 100644 packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchButtonMenuItem.tsx create mode 100644 packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchInput.tsx create mode 100644 packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchInputDropdownHeader.tsx create mode 100644 packages/twenty-front/src/modules/views/constants/AnyFieldSearchDropdownId.ts create mode 100644 packages/twenty-front/src/modules/views/hooks/useOpenAnyFieldSearchFilterFromViewBar.ts create mode 100644 packages/twenty-front/src/modules/views/states/viewAnyFieldSearchValueComponentState.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 047faeb17..fc3365440 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -732,6 +732,7 @@ export type FeatureFlagDto = { export enum FeatureFlagKey { IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED', IS_AI_ENABLED = 'IS_AI_ENABLED', + IS_ANY_FIELD_SEARCH_ENABLED = 'IS_ANY_FIELD_SEARCH_ENABLED', IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED', IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 1ffaa18b2..6d8bc0356 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -696,6 +696,7 @@ export type FeatureFlagDto = { export enum FeatureFlagKey { IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED', IS_AI_ENABLED = 'IS_AI_ENABLED', + IS_ANY_FIELD_SEARCH_ENABLED = 'IS_ANY_FIELD_SEARCH_ENABLED', IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED', IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownAnyFieldSearchInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownAnyFieldSearchInput.tsx new file mode 100644 index 000000000..a4d12c023 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownAnyFieldSearchInput.tsx @@ -0,0 +1,27 @@ +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { viewAnyFieldSearchValueComponentState } from '@/views/states/viewAnyFieldSearchValueComponentState'; +import { useLingui } from '@lingui/react/macro'; + +export const ObjectFilterDropdownAnyFieldSearchInput = () => { + const { t } = useLingui(); + + const [viewAnyFieldSearchValue, setViewAnyFieldSearchValue] = + useRecoilComponentStateV2(viewAnyFieldSearchValueComponentState); + + const handleSearchChange = (event: React.ChangeEvent) => { + const inputValue = event.target.value; + + setViewAnyFieldSearchValue(inputValue); + }; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput.tsx index f53d4013e..ad9a40433 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput.tsx @@ -42,7 +42,7 @@ export const ObjectFilterDropdownTextInput = () => { { componentInstanceId, ); + const objectFilterDropdownAnyFieldSearchIsSelectedCallbackState = + useRecoilComponentCallbackStateV2( + objectFilterDropdownAnyFieldSearchIsSelectedComponentState, + componentInstanceId, + ); + const objectFilterDropdownIsSelectingCompositeFieldCallbackState = useRecoilComponentCallbackStateV2( objectFilterDropdownIsSelectingCompositeFieldComponentState, @@ -53,6 +60,7 @@ export const useResetFilterDropdown = (componentInstanceId?: string) => { set(objectFilterDropdownIsSelectingCompositeFieldCallbackState, false); set(fieldMetadataItemIdUsedInDropdownCallbackState, null); set(objectFilterDropdownCurrentRecordFilterCallbackState, null); + set(objectFilterDropdownAnyFieldSearchIsSelectedCallbackState, false); }, [ objectFilterDropdownSearchInputCallbackState, @@ -61,6 +69,7 @@ export const useResetFilterDropdown = (componentInstanceId?: string) => { objectFilterDropdownIsSelectingCompositeFieldCallbackState, fieldMetadataItemIdUsedInDropdownCallbackState, objectFilterDropdownCurrentRecordFilterCallbackState, + objectFilterDropdownAnyFieldSearchIsSelectedCallbackState, ], ); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownAnyFieldSearchIsSelectedComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownAnyFieldSearchIsSelectedComponentState.ts new file mode 100644 index 000000000..1d7dbc36e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownAnyFieldSearchIsSelectedComponentState.ts @@ -0,0 +1,9 @@ +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const objectFilterDropdownAnyFieldSearchIsSelectedComponentState = + createComponentStateV2({ + key: 'objectFilterDropdownAnyFieldSearchIsSelectedComponentState', + defaultValue: false, + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/views/components/AnyFieldSearchChip.tsx b/packages/twenty-front/src/modules/views/components/AnyFieldSearchChip.tsx new file mode 100644 index 000000000..c088d4bbb --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/AnyFieldSearchChip.tsx @@ -0,0 +1,29 @@ +import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { SortOrFilterChip } from '@/views/components/SortOrFilterChip'; +import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId'; +import { viewAnyFieldSearchValueComponentState } from '@/views/states/viewAnyFieldSearchValueComponentState'; +import { IconFilter } from 'twenty-ui/display'; + +export const AnyFieldSearchChip = () => { + const { closeDropdown } = useCloseDropdown(); + + const [viewAnyFieldSearchValue, setViewAnyFieldSearchValue] = + useRecoilComponentStateV2(viewAnyFieldSearchValueComponentState); + + const handleRemoveClick = () => { + closeDropdown(); + setViewAnyFieldSearchValue(''); + }; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/AnyFieldSearchDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/AnyFieldSearchDropdownButton.tsx new file mode 100644 index 000000000..4e4f450f5 --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/AnyFieldSearchDropdownButton.tsx @@ -0,0 +1,28 @@ +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; + +import { DROPDOWN_OFFSET_Y } from '@/ui/layout/dropdown/constants/DropdownOffsetY'; +import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown'; +import { AnyFieldSearchChip } from '@/views/components/AnyFieldSearchChip'; +import { AnyFieldSearchDropdownContent } from '@/views/components/AnyFieldSearchDropdownContent'; +import { ANY_FIELD_SEARCH_DROPDOWN_ID } from '@/views/constants/AnyFieldSearchDropdownId'; + +export const AnyFieldSearchDropdownButton = () => { + const { openDropdown } = useOpenDropdown(); + + const handleOpenAnyFieldSearchDropdown = () => { + openDropdown({ + dropdownComponentInstanceIdFromProps: ANY_FIELD_SEARCH_DROPDOWN_ID, + }); + }; + + return ( + } + dropdownComponents={} + dropdownOffset={{ y: DROPDOWN_OFFSET_Y, x: 0 }} + dropdownPlacement="bottom-start" + onOpen={handleOpenAnyFieldSearchDropdown} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/AnyFieldSearchDropdownContent.tsx b/packages/twenty-front/src/modules/views/components/AnyFieldSearchDropdownContent.tsx new file mode 100644 index 000000000..75c86c502 --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/AnyFieldSearchDropdownContent.tsx @@ -0,0 +1,13 @@ +import { ObjectFilterDropdownAnyFieldSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownAnyFieldSearchInput'; +import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; +import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; +import { AnyFieldSearchDropdownContentMenuHeader } from '@/views/components/AnyFieldSearchDropdownContentMenuHeader'; + +export const AnyFieldSearchDropdownContent = () => { + return ( + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/AnyFieldSearchDropdownContentMenuHeader.tsx b/packages/twenty-front/src/modules/views/components/AnyFieldSearchDropdownContentMenuHeader.tsx new file mode 100644 index 000000000..7ba7d7b2a --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/AnyFieldSearchDropdownContentMenuHeader.tsx @@ -0,0 +1,28 @@ +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 { useLingui } from '@lingui/react/macro'; +import { IconX } from 'twenty-ui/display'; + +export const AnyFieldSearchDropdownContentMenuHeader = () => { + const { t } = useLingui(); + + const { closeDropdown } = useCloseDropdown(); + + const handleBackButtonClick = () => { + closeDropdown(); + }; + + return ( + + } + > + {t`Search any field`} + + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx index 20881a6d9..cb6c4d2c6 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx @@ -22,12 +22,16 @@ import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAr import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts'; import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; +import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; +import { AnyFieldSearchDropdownButton } from '@/views/components/AnyFieldSearchDropdownButton'; +import { ANY_FIELD_SEARCH_DROPDOWN_ID } from '@/views/constants/AnyFieldSearchDropdownId'; import { useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups } from '@/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups'; import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups'; import { isViewBarExpandedComponentState } from '@/views/states/isViewBarExpandedComponentState'; +import { viewAnyFieldSearchValueComponentState } from '@/views/states/viewAnyFieldSearchValueComponentState'; import { t } from '@lingui/core/macro'; -import { isNonEmptyArray } from '@sniptt/guards'; +import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; import { isDefined } from 'twenty-shared/utils'; import { LightButton } from 'twenty-ui/input'; @@ -119,6 +123,10 @@ export const ViewBarDetails = ({ currentRecordSortsComponentState, ); + const viewAnyFieldSearchValue = useRecoilComponentValueV2( + viewAnyFieldSearchValueComponentState, + ); + const { objectNameSingular } = useObjectNameSingularFromPlural({ objectNamePlural: objectNamePlural, }); @@ -172,7 +180,19 @@ export const ViewBarDetails = ({ toggleSoftDeleteFilterState(false); }; + const shouldShowAdvancedFilterDropdownButton = + currentRecordFilterGroups.length > 0; + + const isAnyFieldSearchDropdownOpen = useRecoilComponentValueV2( + isDropdownOpenComponentState, + ANY_FIELD_SEARCH_DROPDOWN_ID, + ); + + const shouldShowAnyFieldSearchChip = + isNonEmptyString(viewAnyFieldSearchValue) || isAnyFieldSearchDropdownOpen; + const shouldExpandViewBar = + shouldShowAnyFieldSearchChip || viewFiltersAreDifferentFromRecordFilters || viewSortsAreDifferentFromRecordSorts || viewFilterGroupsAreDifferentFromRecordFilterGroups || @@ -185,9 +205,6 @@ export const ViewBarDetails = ({ return null; } - const shouldShowAdvancedFilterDropdownButton = - currentRecordFilterGroups.length > 0; - return ( @@ -220,6 +237,7 @@ export const ViewBarDetails = ({ )} + {shouldShowAnyFieldSearchChip && } {shouldShowAdvancedFilterDropdownButton && ( )} diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchButton.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchButton.tsx new file mode 100644 index 000000000..1f1fb11ed --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchButton.tsx @@ -0,0 +1,17 @@ +import { ViewBarFilterDropdownAnyFieldSearchButtonMenuItem } from '@/views/components/ViewBarFilterDropdownAnyFieldSearchButtonMenuItem'; +import { useOpenAnyFieldSearchFilterFromViewBar } from '@/views/hooks/useOpenAnyFieldSearchFilterFromViewBar'; + +export const ViewBarFilterDropdownAnyFieldSearchButton = () => { + const { openAnyFieldSearchFilterFromViewBar } = + useOpenAnyFieldSearchFilterFromViewBar(); + + const handleSearchClick = () => { + openAnyFieldSearchFilterFromViewBar(); + }; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchButtonMenuItem.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchButtonMenuItem.tsx new file mode 100644 index 000000000..f47a2ebb4 --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchButtonMenuItem.tsx @@ -0,0 +1,57 @@ +import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; +import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; +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 { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; + +const StyledSearchText = styled.span` + color: ${({ theme }) => theme.font.color.light}; + margin-left: ${({ theme }) => theme.spacing(1)}; +`; + +type ViewBarFilterDropdownAnyFieldSearchButtonMenuItemProps = { + onClick?: () => void; +}; + +export const ViewBarFilterDropdownAnyFieldSearchButtonMenuItem = ({ + onClick, +}: ViewBarFilterDropdownAnyFieldSearchButtonMenuItemProps) => { + const { t } = useLingui(); + + const objectFilterDropdownSearchInput = useRecoilComponentValueV2( + objectFilterDropdownSearchInputComponentState, + ); + + const isSelected = useRecoilComponentFamilyValueV2( + isSelectedItemIdComponentFamilySelector, + VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS.SEARCH, + ); + + return ( + + + {t`Search any field`} + {objectFilterDropdownSearchInput && ( + {t`ยท ${objectFilterDropdownSearchInput}`} + )} + + } + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchInput.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchInput.tsx new file mode 100644 index 000000000..e84de1806 --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchInput.tsx @@ -0,0 +1,13 @@ +import { ObjectFilterDropdownAnyFieldSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownAnyFieldSearchInput'; +import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; +import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; +import { ViewBarFilterDropdownAnyFieldSearchInputDropdownHeader } from '@/views/components/ViewBarFilterDropdownAnyFieldSearchInputDropdownHeader'; + +export const ViewBarFilterDropdownAnyFieldSearchInput = () => { + return ( + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchInputDropdownHeader.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchInputDropdownHeader.tsx new file mode 100644 index 000000000..d3301bc75 --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAnyFieldSearchInputDropdownHeader.tsx @@ -0,0 +1,28 @@ +import { useResetFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useResetFilterDropdown'; +import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; +import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; +import { useLingui } from '@lingui/react/macro'; +import { IconChevronLeft } from 'twenty-ui/display'; + +export const ViewBarFilterDropdownAnyFieldSearchInputDropdownHeader = () => { + const { t } = useLingui(); + + const { resetFilterDropdown } = useResetFilterDropdown(); + + const handleBackButtonClick = () => { + resetFilterDropdown(); + }; + + return ( + + } + > + {t`Search any field`} + + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownBottomMenu.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownBottomMenu.tsx index d9ca5a3e4..f32f03b00 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownBottomMenu.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownBottomMenu.tsx @@ -1,6 +1,9 @@ import { ViewBarFilterDropdownAdvancedFilterButton } from '@/views/components/ViewBarFilterDropdownAdvancedFilterButton'; +import { ViewBarFilterDropdownAnyFieldSearchButton } from '@/views/components/ViewBarFilterDropdownAnyFieldSearchButton'; import { ViewBarFilterDropdownVectorSearchButton } from '@/views/components/ViewBarFilterDropdownVectorSearchButton'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import styled from '@emotion/styled'; +import { FeatureFlagKey } from '~/generated-metadata/graphql'; const StyledContainer = styled.div` display: flex; @@ -11,9 +14,17 @@ const StyledContainer = styled.div` `; export const ViewBarFilterDropdownBottomMenu = () => { + const isAnyFieldSearchEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_ANY_FIELD_SEARCH_ENABLED, + ); + return ( - + {isAnyFieldSearchEnabled ? ( + + ) : ( + + )} ); diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownContent.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownContent.tsx index a2a054d47..2345e9fbb 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownContent.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownContent.tsx @@ -1,7 +1,9 @@ +import { objectFilterDropdownAnyFieldSearchIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownAnyFieldSearchIsSelectedComponentState'; 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 { ViewBarFilterDropdownAnyFieldSearchInput } from '@/views/components/ViewBarFilterDropdownAnyFieldSearchInput'; import { ViewBarFilterDropdownFieldSelectMenu } from '@/views/components/ViewBarFilterDropdownFieldSelectMenu'; import { ViewBarFilterDropdownFilterInput } from '@/views/components/ViewBarFilterDropdownFilterInput'; import { ViewBarFilterDropdownVectorSearchInput } from '@/views/components/ViewBarFilterDropdownVectorSearchInput'; @@ -14,6 +16,11 @@ export const ViewBarFilterDropdownContent = () => { VIEW_BAR_FILTER_DROPDOWN_ID, ); + const objectFilterDropdownAnyFieldSearchIsSelected = + useRecoilComponentValueV2( + objectFilterDropdownAnyFieldSearchIsSelectedComponentState, + ); + const selectedOperandInDropdown = useRecoilComponentValueV2( selectedOperandInDropdownComponentState, ); @@ -21,6 +28,12 @@ export const ViewBarFilterDropdownContent = () => { const isVectorSearchFilter = selectedOperandInDropdown === ViewFilterOperand.VectorSearch; + const isAnyFieldSearchFilter = objectFilterDropdownAnyFieldSearchIsSelected; + + if (isAnyFieldSearchFilter) { + return ; + } + if (isVectorSearchFilter) { return ; } diff --git a/packages/twenty-front/src/modules/views/constants/AnyFieldSearchDropdownId.ts b/packages/twenty-front/src/modules/views/constants/AnyFieldSearchDropdownId.ts new file mode 100644 index 000000000..b95624709 --- /dev/null +++ b/packages/twenty-front/src/modules/views/constants/AnyFieldSearchDropdownId.ts @@ -0,0 +1 @@ +export const ANY_FIELD_SEARCH_DROPDOWN_ID = 'any-field-search-dropdown'; diff --git a/packages/twenty-front/src/modules/views/hooks/useOpenAnyFieldSearchFilterFromViewBar.ts b/packages/twenty-front/src/modules/views/hooks/useOpenAnyFieldSearchFilterFromViewBar.ts new file mode 100644 index 000000000..091c174fd --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useOpenAnyFieldSearchFilterFromViewBar.ts @@ -0,0 +1,38 @@ +import { objectFilterDropdownAnyFieldSearchIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownAnyFieldSearchIsSelectedComponentState'; +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { viewAnyFieldSearchValueComponentState } from '@/views/states/viewAnyFieldSearchValueComponentState'; +import { isNonEmptyString } from '@sniptt/guards'; + +export const useOpenAnyFieldSearchFilterFromViewBar = () => { + const setViewAnyFieldSearchValueComponentState = useSetRecoilComponentStateV2( + viewAnyFieldSearchValueComponentState, + ); + + const setObjectFilterDropdownAnyFieldSearchIsSelectedComponentState = + useSetRecoilComponentStateV2( + objectFilterDropdownAnyFieldSearchIsSelectedComponentState, + ); + + const objectFilterDropdownSearchInput = useRecoilComponentValueV2( + objectFilterDropdownSearchInputComponentState, + ); + + const openAnyFieldSearchFilterFromViewBar = () => { + const userHasAlreadyEnteredSearchInputForObjectDropdownSearch = + isNonEmptyString(objectFilterDropdownSearchInput); + + if (userHasAlreadyEnteredSearchInputForObjectDropdownSearch) { + const filterValue = objectFilterDropdownSearchInput; + + setViewAnyFieldSearchValueComponentState(filterValue); + } + + setObjectFilterDropdownAnyFieldSearchIsSelectedComponentState(true); + }; + + return { + openAnyFieldSearchFilterFromViewBar, + }; +}; diff --git a/packages/twenty-front/src/modules/views/states/viewAnyFieldSearchValueComponentState.ts b/packages/twenty-front/src/modules/views/states/viewAnyFieldSearchValueComponentState.ts new file mode 100644 index 000000000..57a450ef1 --- /dev/null +++ b/packages/twenty-front/src/modules/views/states/viewAnyFieldSearchValueComponentState.ts @@ -0,0 +1,9 @@ +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; + +export const viewAnyFieldSearchValueComponentState = + createComponentStateV2({ + key: 'viewAnyFieldSearchValueComponentState', + defaultValue: '', + componentInstanceContext: ViewComponentInstanceContext, + }); diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index edf2da73a..5b1c4f160 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -11,4 +11,5 @@ export enum FeatureFlagKey { IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED', IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED = 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED', IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED', + IS_ANY_FIELD_SEARCH_ENABLED = 'IS_ANY_FIELD_SEARCH_ENABLED', } diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 2f5b1836c..87752758f 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -107,6 +107,7 @@ describe('WorkspaceEntityManager', () => { IS_RELATION_CONNECT_ENABLED: false, IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED: false, IS_FIELDS_PERMISSIONS_ENABLED: false, + IS_ANY_FIELD_SEARCH_ENABLED: false, }, eventEmitterService: { emitMutationEvent: jest.fn(), diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts index 0af48112b..2a1d61977 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts @@ -55,6 +55,11 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, + { + key: FeatureFlagKey.IS_ANY_FIELD_SEARCH_ENABLED, + workspaceId: workspaceId, + value: true, + }, ]) .execute(); };