TWNTY-6808 - Ability to Filter by Creation Source (#7078)
### Description - Ability to Filter by Creation Source ### Demo LOOM: <https://www.loom.com/share/dba9c3d37a4242fe90f977b1babffbde?sid=59b07c51-d245-43cc-bb38-7d898ef72878> ### Refs #6808 Fixes #6808 --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
committed by
GitHub
parent
2cd3219636
commit
35788af351
@ -92,6 +92,7 @@ export type LinksFilter = {
|
|||||||
|
|
||||||
export type ActorFilter = {
|
export type ActorFilter = {
|
||||||
name?: StringFilter;
|
name?: StringFilter;
|
||||||
|
source?: IsFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EmailsFilter = {
|
export type EmailsFilter = {
|
||||||
|
|||||||
@ -2,6 +2,11 @@ import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-d
|
|||||||
import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput';
|
import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput';
|
||||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
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 styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
|
import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
|
||||||
@ -11,8 +16,6 @@ import { ObjectFilterDropdownNumberInput } from './ObjectFilterDropdownNumberInp
|
|||||||
import { ObjectFilterDropdownOperandButton } from './ObjectFilterDropdownOperandButton';
|
import { ObjectFilterDropdownOperandButton } from './ObjectFilterDropdownOperandButton';
|
||||||
import { ObjectFilterDropdownOperandSelect } from './ObjectFilterDropdownOperandSelect';
|
import { ObjectFilterDropdownOperandSelect } from './ObjectFilterDropdownOperandSelect';
|
||||||
import { ObjectFilterDropdownOptionSelect } from './ObjectFilterDropdownOptionSelect';
|
import { ObjectFilterDropdownOptionSelect } from './ObjectFilterDropdownOptionSelect';
|
||||||
import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect';
|
|
||||||
import { ObjectFilterDropdownTextSearchInput } from './ObjectFilterDropdownTextSearchInput';
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -113,6 +116,12 @@ export const MultipleFiltersDropdownContent = ({
|
|||||||
<ObjectFilterDropdownRecordSelect />
|
<ObjectFilterDropdownRecordSelect />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{filterDefinitionUsedInDropdown.type === 'SOURCE' && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<ObjectFilterDropdownSourceSelect />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{filterDefinitionUsedInDropdown.type === 'SELECT' && (
|
{filterDefinitionUsedInDropdown.type === 'SELECT' && (
|
||||||
<>
|
<>
|
||||||
<ObjectFilterDropdownSearchInput />
|
<ObjectFilterDropdownSearchInput />
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { ObjectFilterSelectMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterSelectMenu';
|
||||||
|
import { ObjectFilterSelectSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterSelectSubMenu';
|
||||||
import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
|
|
||||||
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
||||||
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
|
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
|
||||||
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
|
import { currentSubMenuState } from '@/object-record/object-filter-dropdown/states/subMenuStates';
|
||||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
|
||||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
|
||||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
export const StyledInput = styled.input`
|
export const StyledInput = styled.input`
|
||||||
@ -47,6 +45,9 @@ export const ObjectFilterDropdownFilterSelect = () => {
|
|||||||
availableFilterDefinitionsComponentState,
|
availableFilterDefinitionsComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [currentSubMenu, setCurrentSubMenu] =
|
||||||
|
useRecoilState(currentSubMenuState);
|
||||||
|
|
||||||
const sortedAvailableFilterDefinitions = [...availableFilterDefinitions]
|
const sortedAvailableFilterDefinitions = [...availableFilterDefinitions]
|
||||||
.sort((a, b) => a.label.localeCompare(b.label))
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
.filter((item) =>
|
.filter((item) =>
|
||||||
@ -75,37 +76,21 @@ export const ObjectFilterDropdownFilterSelect = () => {
|
|||||||
selectFilter({ filterDefinition: selectedFilterDefinition });
|
selectFilter({ filterDefinition: selectedFilterDefinition });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<>
|
return () => {
|
||||||
<StyledInput
|
setCurrentSubMenu(null);
|
||||||
value={searchText}
|
};
|
||||||
autoFocus
|
}, [setCurrentSubMenu]);
|
||||||
placeholder="Search fields"
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
return !currentSubMenu ? (
|
||||||
setSearchText(event.target.value)
|
<ObjectFilterSelectMenu
|
||||||
}
|
searchText={searchText}
|
||||||
/>
|
setSearchText={setSearchText}
|
||||||
<SelectableList
|
sortedAvailableFilterDefinitions={sortedAvailableFilterDefinitions}
|
||||||
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
|
selectableListItemIds={selectableListItemIds}
|
||||||
selectableItemIdArray={selectableListItemIds}
|
handleEnter={handleEnter}
|
||||||
selectableListId={OBJECT_FILTER_DROPDOWN_ID}
|
/>
|
||||||
onEnter={handleEnter}
|
) : (
|
||||||
>
|
<ObjectFilterSelectSubMenu />
|
||||||
<DropdownMenuItemsContainer>
|
|
||||||
{sortedAvailableFilterDefinitions.map(
|
|
||||||
(availableFilterDefinition, index) => (
|
|
||||||
<SelectableItem
|
|
||||||
itemId={availableFilterDefinition.fieldMetadataId}
|
|
||||||
key={`select-filter-${index}`}
|
|
||||||
>
|
|
||||||
<ObjectFilterDropdownFilterSelectMenuItem
|
|
||||||
filterDefinition={availableFilterDefinition}
|
|
||||||
/>
|
|
||||||
</SelectableItem>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</SelectableList>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
||||||
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
|
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 { 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 { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||||
import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect';
|
import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { useIcons } from 'twenty-ui';
|
import { useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
export type ObjectFilterDropdownFilterSelectMenuItemProps = {
|
export type ObjectFilterDropdownFilterSelectMenuItemProps = {
|
||||||
@ -23,12 +28,24 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
|
|||||||
isSelectedItemIdSelector(filterDefinition.fieldMetadataId),
|
isSelectedItemIdSelector(filterDefinition.fieldMetadataId),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasSubMenu = hasSubMenuFilter(filterDefinition.type);
|
||||||
|
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const setCurrentSubMenu = useSetRecoilState(currentSubMenuState);
|
||||||
|
const setCurrentParentFilterDefinition = useSetRecoilState(
|
||||||
|
currentParentFilterDefinitionState,
|
||||||
|
);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
resetSelectedItem();
|
resetSelectedItem();
|
||||||
|
|
||||||
selectFilter({ filterDefinition });
|
if (hasSubMenu) {
|
||||||
|
setCurrentSubMenu(filterDefinition.type);
|
||||||
|
setCurrentParentFilterDefinition(filterDefinition);
|
||||||
|
} else {
|
||||||
|
selectFilter({ filterDefinition });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -38,6 +55,7 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
LeftIcon={getIcon(filterDefinition.iconName)}
|
LeftIcon={getIcon(filterDefinition.iconName)}
|
||||||
text={filterDefinition.label}
|
text={filterDefinition.label}
|
||||||
|
hasSubMenu={hasSubMenu}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import { v4 } from 'uuid';
|
|||||||
|
|
||||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||||
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
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 { 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 { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
|
||||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -66,7 +66,7 @@ export const ObjectFilterDropdownRecordSelect = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleMultipleRecordSelectChange = (
|
const handleMultipleRecordSelectChange = (
|
||||||
recordToSelect: SelectableRecord,
|
recordToSelect: SelectableItem,
|
||||||
newSelectedValue: boolean,
|
newSelectedValue: boolean,
|
||||||
) => {
|
) => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -134,15 +134,15 @@ export const ObjectFilterDropdownRecordSelect = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultipleRecordSelectDropdown
|
<MultipleSelectDropdown
|
||||||
selectableListId="object-filter-record-select-id"
|
selectableListId="object-filter-record-select-id"
|
||||||
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
||||||
recordsToSelect={recordsToSelect}
|
itemsToSelect={recordsToSelect}
|
||||||
filteredSelectedRecords={filteredSelectedRecords}
|
filteredSelectedItems={filteredSelectedRecords}
|
||||||
selectedRecords={selectedRecords}
|
selectedItems={selectedRecords}
|
||||||
onChange={handleMultipleRecordSelectChange}
|
onChange={handleMultipleRecordSelectChange}
|
||||||
searchFilter={objectFilterDropdownSearchInput}
|
searchFilter={objectFilterDropdownSearchInput}
|
||||||
loadingRecords={loading}
|
loadingItems={loading}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
|
||||||
|
import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
|
||||||
|
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
|
||||||
|
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||||
|
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';
|
||||||
|
|
||||||
|
export const StyledInput = styled.input`
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
border-radius: 0;
|
||||||
|
border-top-left-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
|
border-top-right-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
margin: 0;
|
||||||
|
outline: none;
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
|
height: 19px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
|
|
||||||
|
font-weight: inherit;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ObjectFilterSelectMenuProps = {
|
||||||
|
searchText: string;
|
||||||
|
setSearchText: (searchText: string) => void;
|
||||||
|
sortedAvailableFilterDefinitions: FilterDefinition[];
|
||||||
|
selectableListItemIds: string[];
|
||||||
|
handleEnter: (itemId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ObjectFilterSelectMenu = ({
|
||||||
|
searchText,
|
||||||
|
setSearchText,
|
||||||
|
sortedAvailableFilterDefinitions,
|
||||||
|
selectableListItemIds,
|
||||||
|
handleEnter,
|
||||||
|
}: ObjectFilterSelectMenuProps) => {
|
||||||
|
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: FilterDefinition, index: number) => (
|
||||||
|
<SelectableItem
|
||||||
|
key={`selectable-item-${availableFilterDefinition.fieldMetadataId}`}
|
||||||
|
itemId={availableFilterDefinition.fieldMetadataId}
|
||||||
|
>
|
||||||
|
<ObjectFilterDropdownFilterSelectMenuItem
|
||||||
|
key={`select-filter-${index}`}
|
||||||
|
filterDefinition={availableFilterDefinition}
|
||||||
|
/>
|
||||||
|
</SelectableItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</SelectableList>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,102 @@
|
|||||||
|
import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect';
|
||||||
|
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||||
|
import {
|
||||||
|
currentParentFilterDefinitionState,
|
||||||
|
currentSubMenuState,
|
||||||
|
} from '@/object-record/object-filter-dropdown/states/subMenuStates';
|
||||||
|
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||||
|
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
|
||||||
|
import { getHeaderTitle } from '@/object-record/object-filter-dropdown/utils/getHeaderTitle';
|
||||||
|
import { getOperandsForFilterType } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
|
||||||
|
import { getSubMenuOptions } from '@/object-record/object-filter-dropdown/utils/getSubMenuOptions';
|
||||||
|
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
||||||
|
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
import { IconChevronLeft, useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const ObjectFilterSelectSubMenu = () => {
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const [currentSubMenu, setCurrentSubMenu] =
|
||||||
|
useRecoilState(currentSubMenuState);
|
||||||
|
|
||||||
|
const currentParentFilterDefinition = useRecoilValue(
|
||||||
|
currentParentFilterDefinitionState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
setFilterDefinitionUsedInDropdown,
|
||||||
|
setSelectedOperandInDropdown,
|
||||||
|
setObjectFilterDropdownSearchInput,
|
||||||
|
} = useFilterDropdown();
|
||||||
|
|
||||||
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
|
const handleSelectFilter = (definition: FilterDefinition | null) => {
|
||||||
|
if (definition !== null) {
|
||||||
|
setFilterDefinitionUsedInDropdown(definition);
|
||||||
|
if (definition.type === 'SOURCE') {
|
||||||
|
setHotkeyScope(RelationPickerHotkeyScope.RelationPicker);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedOperandInDropdown(
|
||||||
|
getOperandsForFilterType(definition.type)?.[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
setObjectFilterDropdownSearchInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenuHeader
|
||||||
|
StartIcon={IconChevronLeft}
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentSubMenu(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getHeaderTitle(currentSubMenu)}
|
||||||
|
</DropdownMenuHeader>
|
||||||
|
<StyledInput
|
||||||
|
value={searchText}
|
||||||
|
autoFocus
|
||||||
|
placeholder="Search fields"
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setSearchText(event.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
{getSubMenuOptions(currentSubMenu)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.filter((item) =>
|
||||||
|
item.name
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.includes(searchText.toLocaleLowerCase()),
|
||||||
|
)
|
||||||
|
.map((menuOption, index) => (
|
||||||
|
<MenuItem
|
||||||
|
key={`select-filter-${index}`}
|
||||||
|
testId={`select-filter-${index}`}
|
||||||
|
onClick={() => {
|
||||||
|
currentParentFilterDefinition &&
|
||||||
|
handleSelectFilter({
|
||||||
|
...currentParentFilterDefinition,
|
||||||
|
label: menuOption.name,
|
||||||
|
type: menuOption.type as FilterType,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
text={menuOption.name}
|
||||||
|
LeftIcon={getIcon(
|
||||||
|
menuOption.icon || currentParentFilterDefinition?.iconName,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||||
|
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
|
||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const currentSubMenuState = atom<FilterType | null>({
|
||||||
|
key: 'currentSubMenuState',
|
||||||
|
default: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const currentParentFilterDefinitionState = atom<FilterDefinition | null>(
|
||||||
|
{
|
||||||
|
key: 'currentParentFilterDefinitionState',
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
);
|
||||||
@ -11,4 +11,5 @@ export type FilterDefinition = {
|
|||||||
relationObjectMetadataNameSingular?: string;
|
relationObjectMetadataNameSingular?: string;
|
||||||
selectAllLabel?: string;
|
selectAllLabel?: string;
|
||||||
SelectAllIcon?: IconComponent;
|
SelectAllIcon?: IconComponent;
|
||||||
|
subFieldType?: FilterType;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,4 +17,5 @@ export type FilterType =
|
|||||||
| 'RATING'
|
| 'RATING'
|
||||||
| 'MULTI_SELECT'
|
| 'MULTI_SELECT'
|
||||||
| 'ACTOR'
|
| 'ACTOR'
|
||||||
| 'ARRAY';
|
| 'ARRAY'
|
||||||
|
| 'SOURCE';
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
|
||||||
|
|
||||||
|
export const getHeaderTitle = (
|
||||||
|
subMenu: FilterType | null,
|
||||||
|
): string | undefined => {
|
||||||
|
switch (subMenu) {
|
||||||
|
case 'ACTOR':
|
||||||
|
return 'Actor';
|
||||||
|
case 'SOURCE':
|
||||||
|
return 'Creation Source';
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -57,6 +57,8 @@ export const getOperandsForFilterType = (
|
|||||||
];
|
];
|
||||||
case 'RELATION':
|
case 'RELATION':
|
||||||
return [...relationOperands, ...emptyOperands];
|
return [...relationOperands, ...emptyOperands];
|
||||||
|
case 'SOURCE':
|
||||||
|
return [...relationOperands];
|
||||||
case 'SELECT':
|
case 'SELECT':
|
||||||
return [...relationOperands];
|
return [...relationOperands];
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -0,0 +1,56 @@
|
|||||||
|
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
|
||||||
|
import {
|
||||||
|
IconApi,
|
||||||
|
IconCsv,
|
||||||
|
IconGmail,
|
||||||
|
IconGoogleCalendar,
|
||||||
|
IconSettingsAutomation,
|
||||||
|
IconUserCircle,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
|
||||||
|
export const getSourceEnumOptions = (
|
||||||
|
selectedItemIds: string[],
|
||||||
|
): SelectableItem[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'MANUAL',
|
||||||
|
name: 'User',
|
||||||
|
isSelected: selectedItemIds.includes('MANUAL'),
|
||||||
|
AvatarIcon: IconUserCircle,
|
||||||
|
isIconInverted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'IMPORT',
|
||||||
|
name: 'Import',
|
||||||
|
isSelected: selectedItemIds.includes('IMPORT'),
|
||||||
|
AvatarIcon: IconCsv,
|
||||||
|
isIconInverted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'API',
|
||||||
|
name: 'Api',
|
||||||
|
isSelected: selectedItemIds.includes('API'),
|
||||||
|
AvatarIcon: IconApi,
|
||||||
|
isIconInverted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'EMAIL',
|
||||||
|
name: 'Email',
|
||||||
|
isSelected: selectedItemIds.includes('EMAIL'),
|
||||||
|
AvatarIcon: IconGmail,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'CALENDAR',
|
||||||
|
name: 'Calendar',
|
||||||
|
isSelected: selectedItemIds.includes('CALENDAR'),
|
||||||
|
AvatarIcon: IconGoogleCalendar,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'WORKFLOW',
|
||||||
|
name: 'Workflow',
|
||||||
|
isSelected: selectedItemIds.includes('WORKFLOW'),
|
||||||
|
AvatarIcon: IconSettingsAutomation,
|
||||||
|
isIconInverted: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
|
||||||
|
|
||||||
|
export const getSubMenuOptions = (subMenu: FilterType | null) => {
|
||||||
|
switch (subMenu) {
|
||||||
|
case 'ACTOR':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Creation Source',
|
||||||
|
icon: 'IconPlug',
|
||||||
|
type: 'SOURCE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Creator Name',
|
||||||
|
icon: 'IconId',
|
||||||
|
type: 'ACTOR',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
|
||||||
|
|
||||||
|
export const hasSubMenuFilter = (type: FilterType) => ['ACTOR'].includes(type);
|
||||||
@ -30,12 +30,6 @@ import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Filter } from '../../object-filter-dropdown/types/Filter';
|
import { Filter } from '../../object-filter-dropdown/types/Filter';
|
||||||
|
|
||||||
export type ObjectDropdownFilter = Omit<Filter, 'definition'> & {
|
|
||||||
definition: {
|
|
||||||
type: Filter['definition']['type'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyEmptyFilters = (
|
const applyEmptyFilters = (
|
||||||
operand: ViewFilterOperand,
|
operand: ViewFilterOperand,
|
||||||
correspondingField: Pick<Field, 'id' | 'name'>,
|
correspondingField: Pick<Field, 'id' | 'name'>,
|
||||||
@ -282,7 +276,7 @@ const applyEmptyFilters = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const turnObjectDropdownFilterIntoQueryFilter = (
|
export const turnObjectDropdownFilterIntoQueryFilter = (
|
||||||
rawUIFilters: ObjectDropdownFilter[],
|
rawUIFilters: Filter[],
|
||||||
fields: Pick<Field, 'id' | 'name'>[],
|
fields: Pick<Field, 'id' | 'name'>[],
|
||||||
): RecordGqlOperationFilter | undefined => {
|
): RecordGqlOperationFilter | undefined => {
|
||||||
const objectRecordFilters: RecordGqlOperationFilter[] = [];
|
const objectRecordFilters: RecordGqlOperationFilter[] = [];
|
||||||
@ -894,48 +888,87 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ACTOR':
|
case 'ACTOR':
|
||||||
switch (rawUIFilter.operand) {
|
if (rawUIFilter.definition.subFieldType !== undefined) {
|
||||||
case ViewFilterOperand.Contains:
|
const parsedRecordIds = JSON.parse(rawUIFilter.value) as string[];
|
||||||
objectRecordFilters.push({
|
switch (rawUIFilter.definition.subFieldType) {
|
||||||
or: [
|
case 'SOURCE':
|
||||||
{
|
switch (rawUIFilter.operand) {
|
||||||
[correspondingField.name]: {
|
case ViewFilterOperand.Is:
|
||||||
name: {
|
objectRecordFilters.push({
|
||||||
ilike: `%${rawUIFilter.value}%`,
|
[correspondingField.name]: {
|
||||||
|
source: {
|
||||||
|
in: parsedRecordIds,
|
||||||
|
} as RelationFilter,
|
||||||
},
|
},
|
||||||
} as ActorFilter,
|
});
|
||||||
},
|
|
||||||
],
|
break;
|
||||||
});
|
case ViewFilterOperand.IsNot:
|
||||||
break;
|
if (parsedRecordIds.length > 0) {
|
||||||
case ViewFilterOperand.DoesNotContain:
|
objectRecordFilters.push({
|
||||||
objectRecordFilters.push({
|
not: {
|
||||||
and: [
|
[correspondingField.name]: {
|
||||||
{
|
[rawUIFilter.definition.subFieldType.toLowerCase()]: {
|
||||||
not: {
|
in: parsedRecordIds,
|
||||||
|
} as RelationFilter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.subFieldType} filter`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (rawUIFilter.operand) {
|
||||||
|
case ViewFilterOperand.Contains:
|
||||||
|
objectRecordFilters.push({
|
||||||
|
or: [
|
||||||
|
{
|
||||||
[correspondingField.name]: {
|
[correspondingField.name]: {
|
||||||
name: {
|
name: {
|
||||||
ilike: `%${rawUIFilter.value}%`,
|
ilike: `%${rawUIFilter.value}%`,
|
||||||
},
|
},
|
||||||
} as ActorFilter,
|
} as ActorFilter,
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case ViewFilterOperand.DoesNotContain:
|
||||||
case ViewFilterOperand.IsEmpty:
|
objectRecordFilters.push({
|
||||||
case ViewFilterOperand.IsNotEmpty:
|
and: [
|
||||||
applyEmptyFilters(
|
{
|
||||||
rawUIFilter.operand,
|
not: {
|
||||||
correspondingField,
|
[correspondingField.name]: {
|
||||||
objectRecordFilters,
|
name: {
|
||||||
rawUIFilter.definition.type,
|
ilike: `%${rawUIFilter.value}%`,
|
||||||
);
|
},
|
||||||
break;
|
} as ActorFilter,
|
||||||
default:
|
},
|
||||||
throw new Error(
|
},
|
||||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
],
|
||||||
);
|
});
|
||||||
|
break;
|
||||||
|
case ViewFilterOperand.IsEmpty:
|
||||||
|
case ViewFilterOperand.IsNotEmpty:
|
||||||
|
applyEmptyFilters(
|
||||||
|
rawUIFilter.operand,
|
||||||
|
correspondingField,
|
||||||
|
objectRecordFilters,
|
||||||
|
rawUIFilter.definition.type,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'EMAILS':
|
case 'EMAILS':
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { Avatar } from 'twenty-ui';
|
import { AvatarChip } from 'twenty-ui';
|
||||||
|
|
||||||
import { SelectableRecord } from '@/object-record/select/types/SelectableRecord';
|
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
|
||||||
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
|
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
@ -14,26 +15,36 @@ import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
|||||||
import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
|
import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
|
||||||
export const MultipleRecordSelectDropdown = ({
|
const StyledAvatarChip = styled(AvatarChip)`
|
||||||
|
&.avatar-icon-container {
|
||||||
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MultipleSelectDropdown = ({
|
||||||
selectableListId,
|
selectableListId,
|
||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
recordsToSelect,
|
itemsToSelect,
|
||||||
loadingRecords,
|
loadingItems,
|
||||||
filteredSelectedRecords,
|
filteredSelectedItems,
|
||||||
onChange,
|
onChange,
|
||||||
searchFilter,
|
searchFilter,
|
||||||
}: {
|
}: {
|
||||||
selectableListId: string;
|
selectableListId: string;
|
||||||
hotkeyScope: string;
|
hotkeyScope: string;
|
||||||
recordsToSelect: SelectableRecord[];
|
itemsToSelect: SelectableItem[];
|
||||||
filteredSelectedRecords: SelectableRecord[];
|
filteredSelectedItems: SelectableItem[];
|
||||||
selectedRecords: SelectableRecord[];
|
selectedItems: SelectableItem[];
|
||||||
searchFilter: string;
|
searchFilter: string;
|
||||||
onChange: (
|
onChange: (
|
||||||
changedRecordToSelect: SelectableRecord,
|
changedItemToSelect: SelectableItem,
|
||||||
newSelectedValue: boolean,
|
newSelectedValue: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
loadingRecords: boolean;
|
loadingItems: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { closeDropdown } = useDropdown();
|
const { closeDropdown } = useDropdown();
|
||||||
const { selectedItemIdState } = useSelectableListStates({
|
const { selectedItemIdState } = useSelectableListStates({
|
||||||
@ -44,32 +55,32 @@ export const MultipleRecordSelectDropdown = ({
|
|||||||
|
|
||||||
const selectedItemId = useRecoilValue(selectedItemIdState);
|
const selectedItemId = useRecoilValue(selectedItemIdState);
|
||||||
|
|
||||||
const handleRecordSelectChange = (
|
const handleItemSelectChange = (
|
||||||
recordToSelect: SelectableRecord,
|
itemToSelect: SelectableItem,
|
||||||
newSelectedValue: boolean,
|
newSelectedValue: boolean,
|
||||||
) => {
|
) => {
|
||||||
onChange(
|
onChange(
|
||||||
{
|
{
|
||||||
...recordToSelect,
|
...itemToSelect,
|
||||||
isSelected: newSelectedValue,
|
isSelected: newSelectedValue,
|
||||||
},
|
},
|
||||||
newSelectedValue,
|
newSelectedValue,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [recordsInDropdown, setRecordInDropdown] = useState([
|
const [itemsInDropdown, setItemInDropdown] = useState([
|
||||||
...(filteredSelectedRecords ?? []),
|
...(filteredSelectedItems ?? []),
|
||||||
...(recordsToSelect ?? []),
|
...(itemsToSelect ?? []),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadingRecords) {
|
if (!loadingItems) {
|
||||||
setRecordInDropdown([
|
setItemInDropdown([
|
||||||
...(filteredSelectedRecords ?? []),
|
...(filteredSelectedItems ?? []),
|
||||||
...(recordsToSelect ?? []),
|
...(itemsToSelect ?? []),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}, [recordsToSelect, filteredSelectedRecords, loadingRecords]);
|
}, [itemsToSelect, filteredSelectedItems, loadingItems]);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
[Key.Escape],
|
[Key.Escape],
|
||||||
@ -82,12 +93,12 @@ export const MultipleRecordSelectDropdown = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const showNoResult =
|
const showNoResult =
|
||||||
recordsToSelect?.length === 0 &&
|
itemsToSelect?.length === 0 &&
|
||||||
searchFilter !== '' &&
|
searchFilter !== '' &&
|
||||||
filteredSelectedRecords?.length === 0 &&
|
filteredSelectedItems?.length === 0 &&
|
||||||
!loadingRecords;
|
!loadingItems;
|
||||||
|
|
||||||
const selectableItemIds = recordsInDropdown.map((record) => record.id);
|
const selectableItemIds = itemsInDropdown.map((item) => item.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectableList
|
<SelectableList
|
||||||
@ -95,45 +106,46 @@ export const MultipleRecordSelectDropdown = ({
|
|||||||
selectableItemIdArray={selectableItemIds}
|
selectableItemIdArray={selectableItemIds}
|
||||||
hotkeyScope={hotkeyScope}
|
hotkeyScope={hotkeyScope}
|
||||||
onEnter={(itemId) => {
|
onEnter={(itemId) => {
|
||||||
const record = recordsInDropdown.findIndex(
|
const item = itemsInDropdown.findIndex(
|
||||||
(entity) => entity.id === itemId,
|
(entity) => entity.id === itemId,
|
||||||
);
|
);
|
||||||
const recordIsSelectedInDropwdown = filteredSelectedRecords.find(
|
const itemIsSelectedInDropwdown = filteredSelectedItems.find(
|
||||||
(entity) => entity.id === itemId,
|
(entity) => entity.id === itemId,
|
||||||
);
|
);
|
||||||
handleRecordSelectChange(
|
handleItemSelectChange(
|
||||||
recordsInDropdown[record],
|
itemsInDropdown[item],
|
||||||
!recordIsSelectedInDropwdown,
|
!itemIsSelectedInDropwdown,
|
||||||
);
|
);
|
||||||
resetSelectedItem();
|
resetSelectedItem();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
{recordsInDropdown?.map((record) => {
|
{itemsInDropdown?.map((item) => {
|
||||||
return (
|
return (
|
||||||
<MenuItemMultiSelectAvatar
|
<MenuItemMultiSelectAvatar
|
||||||
key={record.id}
|
key={item.id}
|
||||||
selected={record.isSelected}
|
selected={item.isSelected}
|
||||||
isKeySelected={record.id === selectedItemId}
|
isKeySelected={item.id === selectedItemId}
|
||||||
onSelectChange={(newCheckedValue) => {
|
onSelectChange={(newCheckedValue) => {
|
||||||
resetSelectedItem();
|
resetSelectedItem();
|
||||||
handleRecordSelectChange(record, newCheckedValue);
|
handleItemSelectChange(item, newCheckedValue);
|
||||||
}}
|
}}
|
||||||
avatar={
|
avatar={
|
||||||
<Avatar
|
<StyledAvatarChip
|
||||||
avatarUrl={record.avatarUrl}
|
className="avatar-icon-container"
|
||||||
placeholderColorSeed={record.id}
|
name={item.name}
|
||||||
placeholder={record.name}
|
avatarUrl={item.avatarUrl}
|
||||||
size="md"
|
LeftIcon={item.AvatarIcon}
|
||||||
type={record.avatarType ?? 'rounded'}
|
avatarType={item.avatarType}
|
||||||
|
isIconInverted={item.isIconInverted}
|
||||||
|
placeholderColorSeed={item.id}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
text={record.name}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{showNoResult && <MenuItem text="No result" />}
|
{showNoResult && <MenuItem text="No result" />}
|
||||||
{loadingRecords && <DropdownMenuSkeletonItem />}
|
{loadingItems && <DropdownMenuSkeletonItem />}
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
</SelectableList>
|
</SelectableList>
|
||||||
);
|
);
|
||||||
@ -5,7 +5,7 @@ import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapTo
|
|||||||
|
|
||||||
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
import { SelectableRecord } from '@/object-record/select/types/SelectableRecord';
|
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
|
||||||
import { getObjectFilterFields } from '@/object-record/select/utils/getObjectFilterFields';
|
import { getObjectFilterFields } from '@/object-record/select/utils/getObjectFilterFields';
|
||||||
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
|
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
|
||||||
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
||||||
@ -109,19 +109,19 @@ export const useRecordsForSelect = ({
|
|||||||
.map((record) => ({
|
.map((record) => ({
|
||||||
...record,
|
...record,
|
||||||
isSelected: true,
|
isSelected: true,
|
||||||
})) as SelectableRecord[],
|
})) as SelectableItem[],
|
||||||
filteredSelectedRecords: filteredSelectedRecordsData
|
filteredSelectedRecords: filteredSelectedRecordsData
|
||||||
.map(mapToObjectRecordIdentifier)
|
.map(mapToObjectRecordIdentifier)
|
||||||
.map((record) => ({
|
.map((record) => ({
|
||||||
...record,
|
...record,
|
||||||
isSelected: true,
|
isSelected: true,
|
||||||
})) as SelectableRecord[],
|
})) as SelectableItem[],
|
||||||
recordsToSelect: recordsToSelectData
|
recordsToSelect: recordsToSelectData
|
||||||
.map(mapToObjectRecordIdentifier)
|
.map(mapToObjectRecordIdentifier)
|
||||||
.map((record) => ({
|
.map((record) => ({
|
||||||
...record,
|
...record,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
})) as SelectableRecord[],
|
})) as SelectableItem[],
|
||||||
loading:
|
loading:
|
||||||
recordsToSelectLoading ||
|
recordsToSelectLoading ||
|
||||||
filteredSelectedRecordsLoading ||
|
filteredSelectedRecordsLoading ||
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { AvatarType, IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
|
export type SelectableItem<T = object> = T & {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
avatarType?: AvatarType;
|
||||||
|
AvatarIcon?: IconComponent;
|
||||||
|
isSelected: boolean;
|
||||||
|
isIconInverted?: boolean;
|
||||||
|
};
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { AvatarType } from 'twenty-ui';
|
|
||||||
|
|
||||||
export type SelectableRecord = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
avatarUrl?: string;
|
|
||||||
avatarType?: AvatarType;
|
|
||||||
record: any;
|
|
||||||
isSelected: boolean;
|
|
||||||
};
|
|
||||||
@ -22,7 +22,7 @@ type MenuItemMultiSelectAvatarProps = {
|
|||||||
avatar?: ReactNode;
|
avatar?: ReactNode;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
isKeySelected?: boolean;
|
isKeySelected?: boolean;
|
||||||
text: string;
|
text?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onSelectChange?: (selected: boolean) => void;
|
onSelectChange?: (selected: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { css, useTheme } from '@emotion/react';
|
import { css, useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { IconCheck, IconComponent } from 'twenty-ui';
|
import { IconCheck, IconChevronRight, IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
|
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
|
||||||
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
|
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
|
||||||
@ -45,6 +45,7 @@ type MenuItemSelectProps = {
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hovered?: boolean;
|
hovered?: boolean;
|
||||||
|
hasSubMenu?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MenuItemSelect = ({
|
export const MenuItemSelect = ({
|
||||||
@ -55,6 +56,7 @@ export const MenuItemSelect = ({
|
|||||||
onClick,
|
onClick,
|
||||||
disabled,
|
disabled,
|
||||||
hovered,
|
hovered,
|
||||||
|
hasSubMenu = false,
|
||||||
}: MenuItemSelectProps) => {
|
}: MenuItemSelectProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@ -68,6 +70,12 @@ export const MenuItemSelect = ({
|
|||||||
>
|
>
|
||||||
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
|
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
|
||||||
{selected && <IconCheck size={theme.icon.size.md} />}
|
{selected && <IconCheck size={theme.icon.size.md} />}
|
||||||
|
{hasSubMenu && (
|
||||||
|
<IconChevronRight
|
||||||
|
size={theme.icon.size.sm}
|
||||||
|
color={theme.font.color.tertiary}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</StyledMenuItemSelect>
|
</StyledMenuItemSelect>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||||
import { ViewFilterOperand } from './ViewFilterOperand';
|
import { ViewFilterOperand } from './ViewFilterOperand';
|
||||||
|
|
||||||
export type ViewFilter = {
|
export type ViewFilter = {
|
||||||
@ -11,4 +12,5 @@ export type ViewFilter = {
|
|||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
viewId?: string;
|
viewId?: string;
|
||||||
|
definition?: FilterDefinition;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||||
|
import { hasSubMenuFilter } from '@/object-record/object-filter-dropdown/utils/hasSubMenuFilter';
|
||||||
|
import { ViewFilter } from '../types/ViewFilter';
|
||||||
|
|
||||||
|
export const getFilterDefinitionForViewFilter = (
|
||||||
|
viewFilter: ViewFilter,
|
||||||
|
availableFilterDefinition: FilterDefinition,
|
||||||
|
): FilterDefinition => {
|
||||||
|
return {
|
||||||
|
...availableFilterDefinition,
|
||||||
|
subFieldType:
|
||||||
|
hasSubMenuFilter(availableFilterDefinition.type) &&
|
||||||
|
viewFilter.definition?.type !== availableFilterDefinition.type
|
||||||
|
? viewFilter.definition?.type
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -2,6 +2,7 @@ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
|||||||
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
import { getFilterDefinitionForViewFilter } from '@/views/utils/getFilterDefinitionForViewFilter';
|
||||||
import { ViewFilter } from '../types/ViewFilter';
|
import { ViewFilter } from '../types/ViewFilter';
|
||||||
|
|
||||||
export const mapViewFiltersToFilters = (
|
export const mapViewFiltersToFilters = (
|
||||||
@ -23,7 +24,10 @@ export const mapViewFiltersToFilters = (
|
|||||||
value: viewFilter.value,
|
value: viewFilter.value,
|
||||||
displayValue: viewFilter.displayValue,
|
displayValue: viewFilter.displayValue,
|
||||||
operand: viewFilter.operand,
|
operand: viewFilter.operand,
|
||||||
definition: availableFilterDefinition,
|
definition: getFilterDefinitionForViewFilter(
|
||||||
|
viewFilter,
|
||||||
|
availableFilterDefinition,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(isDefined);
|
.filter(isDefined);
|
||||||
|
|||||||
@ -127,6 +127,7 @@ export const Chip = ({
|
|||||||
rightComponent,
|
rightComponent,
|
||||||
accent = ChipAccent.TextPrimary,
|
accent = ChipAccent.TextPrimary,
|
||||||
onClick,
|
onClick,
|
||||||
|
className,
|
||||||
}: ChipProps) => {
|
}: ChipProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledContainer
|
<StyledContainer
|
||||||
@ -137,6 +138,7 @@ export const Chip = ({
|
|||||||
size={size}
|
size={size}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
className={className}
|
||||||
>
|
>
|
||||||
{leftComponent}
|
{leftComponent}
|
||||||
<OverflowingTextWithTooltip
|
<OverflowingTextWithTooltip
|
||||||
|
|||||||
Reference in New Issue
Block a user