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
This commit is contained in:
@ -732,6 +732,7 @@ export type FeatureFlagDto = {
|
|||||||
export enum FeatureFlagKey {
|
export enum FeatureFlagKey {
|
||||||
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
||||||
IS_AI_ENABLED = 'IS_AI_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_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||||
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||||
|
|||||||
@ -696,6 +696,7 @@ export type FeatureFlagDto = {
|
|||||||
export enum FeatureFlagKey {
|
export enum FeatureFlagKey {
|
||||||
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
||||||
IS_AI_ENABLED = 'IS_AI_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_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||||
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||||
|
|||||||
@ -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<HTMLInputElement>) => {
|
||||||
|
const inputValue = event.target.value;
|
||||||
|
|
||||||
|
setViewAnyFieldSearchValue(inputValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuSearchInput
|
||||||
|
autoFocus
|
||||||
|
type="text"
|
||||||
|
value={viewAnyFieldSearchValue}
|
||||||
|
placeholder={t`Search any field`}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -42,7 +42,7 @@ export const ObjectFilterDropdownTextInput = () => {
|
|||||||
<DropdownMenuInput
|
<DropdownMenuInput
|
||||||
instanceId="object-filter-dropdown-text-input"
|
instanceId="object-filter-dropdown-text-input"
|
||||||
ref={handleInputRef}
|
ref={handleInputRef}
|
||||||
value={objectFilterDropdownFilterValue}
|
value={objectFilterDropdownFilterValue ?? ''}
|
||||||
autoFocus
|
autoFocus
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={fieldMetadataItemUsedInDropdown?.label}
|
placeholder={fieldMetadataItemUsedInDropdown?.label}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
|
||||||
|
import { objectFilterDropdownAnyFieldSearchIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownAnyFieldSearchIsSelectedComponentState';
|
||||||
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
|
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
|
||||||
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
||||||
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
|
||||||
@ -32,6 +33,12 @@ export const useResetFilterDropdown = (componentInstanceId?: string) => {
|
|||||||
componentInstanceId,
|
componentInstanceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const objectFilterDropdownAnyFieldSearchIsSelectedCallbackState =
|
||||||
|
useRecoilComponentCallbackStateV2(
|
||||||
|
objectFilterDropdownAnyFieldSearchIsSelectedComponentState,
|
||||||
|
componentInstanceId,
|
||||||
|
);
|
||||||
|
|
||||||
const objectFilterDropdownIsSelectingCompositeFieldCallbackState =
|
const objectFilterDropdownIsSelectingCompositeFieldCallbackState =
|
||||||
useRecoilComponentCallbackStateV2(
|
useRecoilComponentCallbackStateV2(
|
||||||
objectFilterDropdownIsSelectingCompositeFieldComponentState,
|
objectFilterDropdownIsSelectingCompositeFieldComponentState,
|
||||||
@ -53,6 +60,7 @@ export const useResetFilterDropdown = (componentInstanceId?: string) => {
|
|||||||
set(objectFilterDropdownIsSelectingCompositeFieldCallbackState, false);
|
set(objectFilterDropdownIsSelectingCompositeFieldCallbackState, false);
|
||||||
set(fieldMetadataItemIdUsedInDropdownCallbackState, null);
|
set(fieldMetadataItemIdUsedInDropdownCallbackState, null);
|
||||||
set(objectFilterDropdownCurrentRecordFilterCallbackState, null);
|
set(objectFilterDropdownCurrentRecordFilterCallbackState, null);
|
||||||
|
set(objectFilterDropdownAnyFieldSearchIsSelectedCallbackState, false);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
objectFilterDropdownSearchInputCallbackState,
|
objectFilterDropdownSearchInputCallbackState,
|
||||||
@ -61,6 +69,7 @@ export const useResetFilterDropdown = (componentInstanceId?: string) => {
|
|||||||
objectFilterDropdownIsSelectingCompositeFieldCallbackState,
|
objectFilterDropdownIsSelectingCompositeFieldCallbackState,
|
||||||
fieldMetadataItemIdUsedInDropdownCallbackState,
|
fieldMetadataItemIdUsedInDropdownCallbackState,
|
||||||
objectFilterDropdownCurrentRecordFilterCallbackState,
|
objectFilterDropdownCurrentRecordFilterCallbackState,
|
||||||
|
objectFilterDropdownAnyFieldSearchIsSelectedCallbackState,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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<boolean>({
|
||||||
|
key: 'objectFilterDropdownAnyFieldSearchIsSelectedComponentState',
|
||||||
|
defaultValue: false,
|
||||||
|
componentInstanceContext: ObjectFilterDropdownComponentInstanceContext,
|
||||||
|
});
|
||||||
@ -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 (
|
||||||
|
<SortOrFilterChip
|
||||||
|
testId={ADVANCED_FILTER_DROPDOWN_ID}
|
||||||
|
labelKey={'Any field :'}
|
||||||
|
labelValue={viewAnyFieldSearchValue}
|
||||||
|
Icon={IconFilter}
|
||||||
|
onRemove={handleRemoveClick}
|
||||||
|
type="filter"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 (
|
||||||
|
<Dropdown
|
||||||
|
dropdownId={ANY_FIELD_SEARCH_DROPDOWN_ID}
|
||||||
|
clickableComponent={<AnyFieldSearchChip />}
|
||||||
|
dropdownComponents={<AnyFieldSearchDropdownContent />}
|
||||||
|
dropdownOffset={{ y: DROPDOWN_OFFSET_Y, x: 0 }}
|
||||||
|
dropdownPlacement="bottom-start"
|
||||||
|
onOpen={handleOpenAnyFieldSearchDropdown}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 (
|
||||||
|
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
|
||||||
|
<AnyFieldSearchDropdownContentMenuHeader />
|
||||||
|
<ObjectFilterDropdownAnyFieldSearchInput />
|
||||||
|
</DropdownContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 (
|
||||||
|
<DropdownMenuHeader
|
||||||
|
StartComponent={
|
||||||
|
<DropdownMenuHeaderLeftComponent
|
||||||
|
onClick={handleBackButtonClick}
|
||||||
|
Icon={IconX}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t`Search any field`}
|
||||||
|
</DropdownMenuHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -22,12 +22,16 @@ import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAr
|
|||||||
import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
|
import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
|
||||||
|
|
||||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
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 { 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 { useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups } from '@/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups';
|
||||||
import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups';
|
import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups';
|
||||||
import { isViewBarExpandedComponentState } from '@/views/states/isViewBarExpandedComponentState';
|
import { isViewBarExpandedComponentState } from '@/views/states/isViewBarExpandedComponentState';
|
||||||
|
import { viewAnyFieldSearchValueComponentState } from '@/views/states/viewAnyFieldSearchValueComponentState';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { isNonEmptyArray } from '@sniptt/guards';
|
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { LightButton } from 'twenty-ui/input';
|
import { LightButton } from 'twenty-ui/input';
|
||||||
|
|
||||||
@ -119,6 +123,10 @@ export const ViewBarDetails = ({
|
|||||||
currentRecordSortsComponentState,
|
currentRecordSortsComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const viewAnyFieldSearchValue = useRecoilComponentValueV2(
|
||||||
|
viewAnyFieldSearchValueComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||||
objectNamePlural: objectNamePlural,
|
objectNamePlural: objectNamePlural,
|
||||||
});
|
});
|
||||||
@ -172,7 +180,19 @@ export const ViewBarDetails = ({
|
|||||||
toggleSoftDeleteFilterState(false);
|
toggleSoftDeleteFilterState(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shouldShowAdvancedFilterDropdownButton =
|
||||||
|
currentRecordFilterGroups.length > 0;
|
||||||
|
|
||||||
|
const isAnyFieldSearchDropdownOpen = useRecoilComponentValueV2(
|
||||||
|
isDropdownOpenComponentState,
|
||||||
|
ANY_FIELD_SEARCH_DROPDOWN_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldShowAnyFieldSearchChip =
|
||||||
|
isNonEmptyString(viewAnyFieldSearchValue) || isAnyFieldSearchDropdownOpen;
|
||||||
|
|
||||||
const shouldExpandViewBar =
|
const shouldExpandViewBar =
|
||||||
|
shouldShowAnyFieldSearchChip ||
|
||||||
viewFiltersAreDifferentFromRecordFilters ||
|
viewFiltersAreDifferentFromRecordFilters ||
|
||||||
viewSortsAreDifferentFromRecordSorts ||
|
viewSortsAreDifferentFromRecordSorts ||
|
||||||
viewFilterGroupsAreDifferentFromRecordFilterGroups ||
|
viewFilterGroupsAreDifferentFromRecordFilterGroups ||
|
||||||
@ -185,9 +205,6 @@ export const ViewBarDetails = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldShowAdvancedFilterDropdownButton =
|
|
||||||
currentRecordFilterGroups.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBar>
|
<StyledBar>
|
||||||
<StyledFilterContainer>
|
<StyledFilterContainer>
|
||||||
@ -220,6 +237,7 @@ export const ViewBarDetails = ({
|
|||||||
<StyledSeperator />
|
<StyledSeperator />
|
||||||
</StyledSeperatorContainer>
|
</StyledSeperatorContainer>
|
||||||
)}
|
)}
|
||||||
|
{shouldShowAnyFieldSearchChip && <AnyFieldSearchDropdownButton />}
|
||||||
{shouldShowAdvancedFilterDropdownButton && (
|
{shouldShowAdvancedFilterDropdownButton && (
|
||||||
<AdvancedFilterDropdownButton />
|
<AdvancedFilterDropdownButton />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<ViewBarFilterDropdownAnyFieldSearchButtonMenuItem
|
||||||
|
onClick={handleSearchClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 (
|
||||||
|
<SelectableListItem
|
||||||
|
itemId={VIEW_BAR_FILTER_BOTTOM_MENU_ITEM_IDS.SEARCH}
|
||||||
|
onEnter={onClick}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
focused={isSelected}
|
||||||
|
onClick={onClick}
|
||||||
|
LeftIcon={IconSearch}
|
||||||
|
text={
|
||||||
|
<>
|
||||||
|
{t`Search any field`}
|
||||||
|
{objectFilterDropdownSearchInput && (
|
||||||
|
<StyledSearchText>{t`· ${objectFilterDropdownSearchInput}`}</StyledSearchText>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SelectableListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 (
|
||||||
|
<DropdownContent widthInPixels={GenericDropdownContentWidth.ExtraLarge}>
|
||||||
|
<ViewBarFilterDropdownAnyFieldSearchInputDropdownHeader />
|
||||||
|
<ObjectFilterDropdownAnyFieldSearchInput />
|
||||||
|
</DropdownContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 (
|
||||||
|
<DropdownMenuHeader
|
||||||
|
StartComponent={
|
||||||
|
<DropdownMenuHeaderLeftComponent
|
||||||
|
onClick={handleBackButtonClick}
|
||||||
|
Icon={IconChevronLeft}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t`Search any field`}
|
||||||
|
</DropdownMenuHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,6 +1,9 @@
|
|||||||
import { ViewBarFilterDropdownAdvancedFilterButton } from '@/views/components/ViewBarFilterDropdownAdvancedFilterButton';
|
import { ViewBarFilterDropdownAdvancedFilterButton } from '@/views/components/ViewBarFilterDropdownAdvancedFilterButton';
|
||||||
|
import { ViewBarFilterDropdownAnyFieldSearchButton } from '@/views/components/ViewBarFilterDropdownAnyFieldSearchButton';
|
||||||
import { ViewBarFilterDropdownVectorSearchButton } from '@/views/components/ViewBarFilterDropdownVectorSearchButton';
|
import { ViewBarFilterDropdownVectorSearchButton } from '@/views/components/ViewBarFilterDropdownVectorSearchButton';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -11,9 +14,17 @@ const StyledContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const ViewBarFilterDropdownBottomMenu = () => {
|
export const ViewBarFilterDropdownBottomMenu = () => {
|
||||||
|
const isAnyFieldSearchEnabled = useIsFeatureEnabled(
|
||||||
|
FeatureFlagKey.IS_ANY_FIELD_SEARCH_ENABLED,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<ViewBarFilterDropdownVectorSearchButton />
|
{isAnyFieldSearchEnabled ? (
|
||||||
|
<ViewBarFilterDropdownAnyFieldSearchButton />
|
||||||
|
) : (
|
||||||
|
<ViewBarFilterDropdownVectorSearchButton />
|
||||||
|
)}
|
||||||
<ViewBarFilterDropdownAdvancedFilterButton />
|
<ViewBarFilterDropdownAdvancedFilterButton />
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
|
||||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { ViewBarFilterDropdownAnyFieldSearchInput } from '@/views/components/ViewBarFilterDropdownAnyFieldSearchInput';
|
||||||
import { ViewBarFilterDropdownFieldSelectMenu } from '@/views/components/ViewBarFilterDropdownFieldSelectMenu';
|
import { ViewBarFilterDropdownFieldSelectMenu } from '@/views/components/ViewBarFilterDropdownFieldSelectMenu';
|
||||||
import { ViewBarFilterDropdownFilterInput } from '@/views/components/ViewBarFilterDropdownFilterInput';
|
import { ViewBarFilterDropdownFilterInput } from '@/views/components/ViewBarFilterDropdownFilterInput';
|
||||||
import { ViewBarFilterDropdownVectorSearchInput } from '@/views/components/ViewBarFilterDropdownVectorSearchInput';
|
import { ViewBarFilterDropdownVectorSearchInput } from '@/views/components/ViewBarFilterDropdownVectorSearchInput';
|
||||||
@ -14,6 +16,11 @@ export const ViewBarFilterDropdownContent = () => {
|
|||||||
VIEW_BAR_FILTER_DROPDOWN_ID,
|
VIEW_BAR_FILTER_DROPDOWN_ID,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const objectFilterDropdownAnyFieldSearchIsSelected =
|
||||||
|
useRecoilComponentValueV2(
|
||||||
|
objectFilterDropdownAnyFieldSearchIsSelectedComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const selectedOperandInDropdown = useRecoilComponentValueV2(
|
const selectedOperandInDropdown = useRecoilComponentValueV2(
|
||||||
selectedOperandInDropdownComponentState,
|
selectedOperandInDropdownComponentState,
|
||||||
);
|
);
|
||||||
@ -21,6 +28,12 @@ export const ViewBarFilterDropdownContent = () => {
|
|||||||
const isVectorSearchFilter =
|
const isVectorSearchFilter =
|
||||||
selectedOperandInDropdown === ViewFilterOperand.VectorSearch;
|
selectedOperandInDropdown === ViewFilterOperand.VectorSearch;
|
||||||
|
|
||||||
|
const isAnyFieldSearchFilter = objectFilterDropdownAnyFieldSearchIsSelected;
|
||||||
|
|
||||||
|
if (isAnyFieldSearchFilter) {
|
||||||
|
return <ViewBarFilterDropdownAnyFieldSearchInput />;
|
||||||
|
}
|
||||||
|
|
||||||
if (isVectorSearchFilter) {
|
if (isVectorSearchFilter) {
|
||||||
return <ViewBarFilterDropdownVectorSearchInput />;
|
return <ViewBarFilterDropdownVectorSearchInput />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const ANY_FIELD_SEARCH_DROPDOWN_ID = 'any-field-search-dropdown';
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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<string>({
|
||||||
|
key: 'viewAnyFieldSearchValueComponentState',
|
||||||
|
defaultValue: '',
|
||||||
|
componentInstanceContext: ViewComponentInstanceContext,
|
||||||
|
});
|
||||||
@ -11,4 +11,5 @@ export enum FeatureFlagKey {
|
|||||||
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
|
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
|
||||||
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED = 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED',
|
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED = 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED',
|
||||||
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||||
|
IS_ANY_FIELD_SEARCH_ENABLED = 'IS_ANY_FIELD_SEARCH_ENABLED',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,6 +107,7 @@ describe('WorkspaceEntityManager', () => {
|
|||||||
IS_RELATION_CONNECT_ENABLED: false,
|
IS_RELATION_CONNECT_ENABLED: false,
|
||||||
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED: false,
|
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED: false,
|
||||||
IS_FIELDS_PERMISSIONS_ENABLED: false,
|
IS_FIELDS_PERMISSIONS_ENABLED: false,
|
||||||
|
IS_ANY_FIELD_SEARCH_ENABLED: false,
|
||||||
},
|
},
|
||||||
eventEmitterService: {
|
eventEmitterService: {
|
||||||
emitMutationEvent: jest.fn(),
|
emitMutationEvent: jest.fn(),
|
||||||
|
|||||||
@ -55,6 +55,11 @@ export const seedFeatureFlags = async (
|
|||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: FeatureFlagKey.IS_ANY_FIELD_SEARCH_ENABLED,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
])
|
])
|
||||||
.execute();
|
.execute();
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user