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 047faeb17a..fc33654404 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 1ffaa18b24..6d8bc03565 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 0000000000..a4d12c023f --- /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 f53d4013e3..ad9a404337 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 0000000000..1d7dbc36eb --- /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 0000000000..c088d4bbbb --- /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 0000000000..4e4f450f52 --- /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 0000000000..75c86c5027 --- /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 0000000000..7ba7d7b2a6 --- /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 20881a6d9b..cb6c4d2c6e 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 0000000000..1f1fb11edb --- /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 0000000000..f47a2ebb4b --- /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 0000000000..e84de18067 --- /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 0000000000..d3301bc755 --- /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 d9ca5a3e4c..f32f03b009 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 a2a054d475..2345e9fbb1 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 0000000000..b956247091 --- /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 0000000000..091c174fd2 --- /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 0000000000..57a450ef1a --- /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 edf2da73ae..5b1c4f1603 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 2f5b1836c6..87752758f4 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 0af48112b6..2a1d619777 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(); };