Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TWNTY-6808 - Ability to Filter by Creation Source #7078

Merged
merged 10 commits into from
Oct 2, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export type LinksFilter = {

export type ActorFilter = {
name?: StringFilter;
source?: IsFilter;
};

export type EmailsFilter = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-d
import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';

import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect';
import { ObjectFilterDropdownSourceSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect';
import { ObjectFilterDropdownTextSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
Expand All @@ -11,8 +16,6 @@ import { ObjectFilterDropdownNumberInput } from './ObjectFilterDropdownNumberInp
import { ObjectFilterDropdownOperandButton } from './ObjectFilterDropdownOperandButton';
import { ObjectFilterDropdownOperandSelect } from './ObjectFilterDropdownOperandSelect';
import { ObjectFilterDropdownOptionSelect } from './ObjectFilterDropdownOptionSelect';
import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect';
import { ObjectFilterDropdownTextSearchInput } from './ObjectFilterDropdownTextSearchInput';

const StyledContainer = styled.div`
position: relative;
Expand Down Expand Up @@ -113,6 +116,12 @@ export const MultipleFiltersDropdownContent = ({
<ObjectFilterDropdownRecordSelect />
</>
)}
{filterDefinitionUsedInDropdown.type === 'SOURCE' && (
<>
<DropdownMenuSeparator />
<ObjectFilterDropdownSourceSelect />
</>
)}
{filterDefinitionUsedInDropdown.type === 'SELECT' && (
<>
<ObjectFilterDropdownSearchInput />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import styled from '@emotion/styled';
import { useState } from 'react';
import { useEffect, useState } from 'react';

import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';

import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
import { ObjectFilterSelectMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterSelectMenu';
import { ObjectFilterSelectSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterSelectSubMenu';
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { currentSubMenuState } from '@/object-record/object-filter-dropdown/states/subMenuStates';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-ui';

export const StyledInput = styled.input`
Expand Down Expand Up @@ -47,6 +45,9 @@ export const ObjectFilterDropdownFilterSelect = () => {
availableFilterDefinitionsComponentState,
);

const [currentSubMenu, setCurrentSubMenu] =
useRecoilState(currentSubMenuState);

const sortedAvailableFilterDefinitions = [...availableFilterDefinitions]
.sort((a, b) => a.label.localeCompare(b.label))
.filter((item) =>
Expand Down Expand Up @@ -75,37 +76,21 @@ export const ObjectFilterDropdownFilterSelect = () => {
selectFilter({ filterDefinition: selectedFilterDefinition });
};

return (
<>
<StyledInput
value={searchText}
autoFocus
placeholder="Search fields"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setSearchText(event.target.value)
}
/>
<SelectableList
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={selectableListItemIds}
selectableListId={OBJECT_FILTER_DROPDOWN_ID}
onEnter={handleEnter}
>
<DropdownMenuItemsContainer>
{sortedAvailableFilterDefinitions.map(
(availableFilterDefinition, index) => (
<SelectableItem
itemId={availableFilterDefinition.fieldMetadataId}
key={`select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
filterDefinition={availableFilterDefinition}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
</SelectableList>
</>
useEffect(() => {
return () => {
setCurrentSubMenu(null);
};
}, [setCurrentSubMenu]);

return !currentSubMenu ? (
<ObjectFilterSelectMenu
searchText={searchText}
setSearchText={setSearchText}
sortedAvailableFilterDefinitions={sortedAvailableFilterDefinitions}
selectableListItemIds={selectableListItemIds}
handleEnter={handleEnter}
/>
) : (
<ObjectFilterSelectSubMenu />
);
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
import {
currentParentFilterDefinitionState,
currentSubMenuState,
} from '@/object-record/object-filter-dropdown/states/subMenuStates';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { hasSubMenuFilter } from '@/object-record/object-filter-dropdown/utils/hasSubMenuFilter';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect';
import { useRecoilValue } from 'recoil';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useIcons } from 'twenty-ui';

export type ObjectFilterDropdownFilterSelectMenuItemProps = {
Expand All @@ -23,12 +28,24 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
isSelectedItemIdSelector(filterDefinition.fieldMetadataId),
);

const hasSubMenu = hasSubMenuFilter(filterDefinition.type);

const { getIcon } = useIcons();

const setCurrentSubMenu = useSetRecoilState(currentSubMenuState);
const setCurrentParentFilterDefinition = useSetRecoilState(
currentParentFilterDefinitionState,
);

const handleClick = () => {
resetSelectedItem();

selectFilter({ filterDefinition });
if (hasSubMenu) {
setCurrentSubMenu(filterDefinition.type);
setCurrentParentFilterDefinition(filterDefinition);
} else {
selectFilter({ filterDefinition });
}
};

return (
Expand All @@ -38,6 +55,7 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
onClick={handleClick}
LeftIcon={getIcon(filterDefinition.iconName)}
text={filterDefinition.label}
hasSubMenu={hasSubMenu}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { v4 } from 'uuid';

import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { MultipleRecordSelectDropdown } from '@/object-record/select/components/MultipleRecordSelectDropdown';
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect';
import { SelectableRecord } from '@/object-record/select/types/SelectableRecord';
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { isDefined } from '~/utils/isDefined';
Expand Down Expand Up @@ -66,7 +66,7 @@ export const ObjectFilterDropdownRecordSelect = ({
});

const handleMultipleRecordSelectChange = (
recordToSelect: SelectableRecord,
recordToSelect: SelectableItem,
newSelectedValue: boolean,
) => {
if (loading) {
Expand Down Expand Up @@ -134,15 +134,15 @@ export const ObjectFilterDropdownRecordSelect = ({
};

return (
<MultipleRecordSelectDropdown
<MultipleSelectDropdown
selectableListId="object-filter-record-select-id"
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
recordsToSelect={recordsToSelect}
filteredSelectedRecords={filteredSelectedRecords}
selectedRecords={selectedRecords}
itemsToSelect={recordsToSelect}
filteredSelectedItems={filteredSelectedRecords}
selectedItems={selectedRecords}
onChange={handleMultipleRecordSelectChange}
searchFilter={objectFilterDropdownSearchInput}
loadingRecords={loading}
loadingItems={loading}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { v4 } from 'uuid';

import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { getSourceEnumOptions } from '@/object-record/object-filter-dropdown/utils/getSourceEnumOptions';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { isDefined } from '~/utils/isDefined';

export const EMPTY_FILTER_VALUE = '[]';
export const MAX_ITEMS_TO_DISPLAY = 3;

type ObjectFilterDropdownSourceSelectProps = {
viewComponentId?: string;
};

export const ObjectFilterDropdownSourceSelect = ({
viewComponentId,
}: ObjectFilterDropdownSourceSelectProps) => {
const {
filterDefinitionUsedInDropdownState,
objectFilterDropdownSearchInputState,
selectedOperandInDropdownState,
selectedFilterState,
setObjectFilterDropdownSelectedRecordIds,
objectFilterDropdownSelectedRecordIdsState,
selectFilter,
emptyFilterButKeepDefinition,
} = useFilterDropdown();

const { deleteCombinedViewFilter } =
useDeleteCombinedViewFilters(viewComponentId);

const { currentViewWithCombinedFiltersAndSorts } =
useGetCurrentView(viewComponentId);

const filterDefinitionUsedInDropdown = useRecoilValue(
filterDefinitionUsedInDropdownState,
);
const objectFilterDropdownSearchInput = useRecoilValue(
objectFilterDropdownSearchInputState,
);
const selectedOperandInDropdown = useRecoilValue(
selectedOperandInDropdownState,
);
const objectFilterDropdownSelectedRecordIds = useRecoilValue(
objectFilterDropdownSelectedRecordIdsState,
);
const [fieldId] = useState(v4());

const selectedFilter = useRecoilValue(selectedFilterState);

const sourceTypes = getSourceEnumOptions(
objectFilterDropdownSelectedRecordIds,
);

const filteredSelectedItems = sourceTypes.filter((option) =>
objectFilterDropdownSelectedRecordIds.includes(option.id),
);

const handleMultipleItemSelectChange = (
itemToSelect: SelectableItem,
newSelectedValue: boolean,
) => {
const newSelectedItemIds = newSelectedValue
? [...objectFilterDropdownSelectedRecordIds, itemToSelect.id]
: objectFilterDropdownSelectedRecordIds.filter(
(id) => id !== itemToSelect.id,
);

if (newSelectedItemIds.length === 0) {
emptyFilterButKeepDefinition();
deleteCombinedViewFilter(fieldId);
return;
}

setObjectFilterDropdownSelectedRecordIds(newSelectedItemIds);

const selectedItemNames = sourceTypes
.filter((option) => newSelectedItemIds.includes(option.id))
.map((option) => option.name);

const filterDisplayValue =
selectedItemNames.length > MAX_ITEMS_TO_DISPLAY
? `${selectedItemNames.length} source types`
: selectedItemNames.join(', ');

if (
isDefined(filterDefinitionUsedInDropdown) &&
isDefined(selectedOperandInDropdown)
) {
const newFilterValue =
newSelectedItemIds.length > 0
? JSON.stringify(newSelectedItemIds)
: EMPTY_FILTER_VALUE;

const viewFilter =
currentViewWithCombinedFiltersAndSorts?.viewFilters.find(
(viewFilter) =>
viewFilter.fieldMetadataId ===
filterDefinitionUsedInDropdown.fieldMetadataId,
);

const filterId = viewFilter?.id ?? fieldId;

selectFilter({
id: selectedFilter?.id ? selectedFilter.id : filterId,
definition: filterDefinitionUsedInDropdown,
operand: selectedOperandInDropdown || ViewFilterOperand.Is,
displayValue: filterDisplayValue,
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
value: newFilterValue,
});
}
};

return (
<MultipleSelectDropdown
selectableListId="object-filter-source-select-id"
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
itemsToSelect={sourceTypes.filter(
(item) =>
!filteredSelectedItems.some((selected) => selected.id === item.id),
)}
filteredSelectedItems={filteredSelectedItems}
selectedItems={filteredSelectedItems}
onChange={handleMultipleItemSelectChange}
searchFilter={objectFilterDropdownSearchInput}
loadingItems={false}
/>
);
};
Loading
Loading