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 @@ -6,15 +6,16 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';

import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
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 { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
import { ObjectFilterDropdownDateInput } from './ObjectFilterDropdownDateInput';
import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect';
import { ObjectFilterDropdownNumberInput } from './ObjectFilterDropdownNumberInput';
import { ObjectFilterDropdownOperandButton } from './ObjectFilterDropdownOperandButton';
import { ObjectFilterDropdownOperandSelect } from './ObjectFilterDropdownOperandSelect';
import { ObjectFilterDropdownOptionSelect } from './ObjectFilterDropdownOptionSelect';
import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect';
import { ObjectFilterDropdownTextSearchInput } from './ObjectFilterDropdownTextSearchInput';

type MultipleFiltersDropdownContentProps = {
filterDropdownId?: string;
Expand Down Expand Up @@ -88,6 +89,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
Expand Up @@ -6,8 +6,11 @@ import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';

import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
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 { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
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';
Expand Down Expand Up @@ -42,6 +45,9 @@ export const StyledInput = styled.input`

export const ObjectFilterDropdownFilterSelect = () => {
const [searchText, setSearchText] = useState('');
const [currentSubMenu, setCurrentSubMenu] = useState<FilterType | null>(null);
const [currentParentFilterDefinition, setCurrentParentFilterDefinition] =
useState<FilterDefinition | null>(null);

const { availableFilterDefinitionsState } = useFilterDropdown();

Expand Down Expand Up @@ -79,35 +85,50 @@ export const ObjectFilterDropdownFilterSelect = () => {

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}
>
<ObjectFilterDropdownFilterSelectMenuItem
key={`select-filter-${index}`}
filterDefinition={availableFilterDefinition}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
</SelectableList>
{!currentSubMenu ? (
<>
<StyledInput
gitstart-twenty marked this conversation as resolved.
Show resolved Hide resolved
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
key={`selectable-item-${availableFilterDefinition.fieldMetadataId}`}
itemId={availableFilterDefinition.fieldMetadataId}
>
<ObjectFilterDropdownFilterSelectMenuItem
key={`select-filter-${index}`}
filterDefinition={availableFilterDefinition}
setCurrentSubMenu={setCurrentSubMenu}
setCurrentParentFilterDefinition={
setCurrentParentFilterDefinition
}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
</SelectableList>
</>
) : (
<ObjectFilterSelectSubMenu
currentSubMenu={currentSubMenu}
parent={currentParentFilterDefinition}
setCurrentSubMenu={setCurrentSubMenu}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
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 { useIcons } from 'twenty-ui';

export type ObjectFilterDropdownFilterSelectMenuItemProps = {
filterDefinition: FilterDefinition;
setCurrentSubMenu: (subMenu: FilterType) => void;
gitstart-twenty marked this conversation as resolved.
Show resolved Hide resolved
setCurrentParentFilterDefinition: (
filterDefinition: FilterDefinition,
) => void;
};

export const ObjectFilterDropdownFilterSelectMenuItem = ({
filterDefinition,
setCurrentSubMenu,
setCurrentParentFilterDefinition,
}: ObjectFilterDropdownFilterSelectMenuItemProps) => {
const { selectFilter } = useSelectFilter();

Expand All @@ -23,12 +31,19 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
isSelectedItemIdSelector(filterDefinition.fieldMetadataId),
);

const hasSubMenu = hasSubMenuFilter(filterDefinition.type);

const { getIcon } = useIcons();

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

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

return (
Expand All @@ -38,6 +53,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
@@ -0,0 +1,166 @@
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
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 { SelectableRecord } from '@/object-record/select/types/SelectableRecord';
import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters';
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_RECORDS_TO_DISPLAY = 3;

type ObjectFilterDropdownSourceSelectProps = {
viewComponentId?: string;
};

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

const { removeCombinedViewFilter } = useCombinedViewFilters(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);

// Dynamically update sourceEnumOptions based on selectedRecordIds
const sourceEnumOptions: SelectableRecord[] = [
gitstart-twenty marked this conversation as resolved.
Show resolved Hide resolved
{
id: 'MANUAL',
name: 'MANUAL',
record: null,
isSelected: objectFilterDropdownSelectedRecordIds.includes('MANUAL'),
},
{
id: 'IMPORT',
name: 'IMPORT',
record: null,
isSelected: objectFilterDropdownSelectedRecordIds.includes('IMPORT'),
},
{
id: 'API',
name: 'API',
record: null,
isSelected: objectFilterDropdownSelectedRecordIds.includes('API'),
},
{
id: 'EMAIL',
name: 'EMAIL',
record: null,
isSelected: objectFilterDropdownSelectedRecordIds.includes('EMAIL'),
},
{
id: 'CALENDAR',
name: 'CALENDAR',
record: null,
isSelected: objectFilterDropdownSelectedRecordIds.includes('CALENDAR'),
},
];

const filteredSelectedRecords = sourceEnumOptions.filter((option) =>
objectFilterDropdownSelectedRecordIds.includes(option.id),
);

const handleMultipleRecordSelectChange = (
recordToSelect: SelectableRecord,
newSelectedValue: boolean,
) => {
const newSelectedRecordIds = newSelectedValue
? [...objectFilterDropdownSelectedRecordIds, recordToSelect.id]
: objectFilterDropdownSelectedRecordIds.filter(
(id) => id !== recordToSelect.id,
);

if (newSelectedRecordIds.length === 0) {
emptyFilterButKeepDefinition();
removeCombinedViewFilter(fieldId);
return;
}

setObjectFilterDropdownSelectedRecordIds(newSelectedRecordIds);

const selectedRecordNames = sourceEnumOptions
.filter((option) => newSelectedRecordIds.includes(option.id))
.map((option) => option.name);

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

if (
isDefined(filterDefinitionUsedInDropdown) &&
isDefined(selectedOperandInDropdown)
) {
const newFilterValue =
newSelectedRecordIds.length > 0
? JSON.stringify(newSelectedRecordIds)
: 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 (
<MultipleRecordSelectDropdown
selectableListId="object-filter-source-select-id"
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
recordsToSelect={sourceEnumOptions.filter(
(record) =>
!filteredSelectedRecords.some(
(selected) => selected.id === record.id,
),
)}
filteredSelectedRecords={filteredSelectedRecords}
selectedRecords={filteredSelectedRecords}
onChange={handleMultipleRecordSelectChange}
searchFilter={objectFilterDropdownSearchInput}
loadingRecords={false}
/>
);
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
};
Loading
Loading