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 = {
|
||||
name?: StringFilter;
|
||||
source?: IsFilter;
|
||||
};
|
||||
|
||||
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 { 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';
|
||||
@ -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;
|
||||
@ -113,6 +116,12 @@ export const MultipleFiltersDropdownContent = ({
|
||||
<ObjectFilterDropdownRecordSelect />
|
||||
</>
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'SOURCE' && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<ObjectFilterDropdownSourceSelect />
|
||||
</>
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'SELECT' && (
|
||||
<>
|
||||
<ObjectFilterDropdownSearchInput />
|
||||
|
||||
@ -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`
|
||||
@ -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) =>
|
||||
@ -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 />
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 = {
|
||||
@ -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 (
|
||||
@ -38,6 +55,7 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
|
||||
onClick={handleClick}
|
||||
LeftIcon={getIcon(filterDefinition.iconName)}
|
||||
text={filterDefinition.label}
|
||||
hasSubMenu={hasSubMenu}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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';
|
||||
@ -66,7 +66,7 @@ export const ObjectFilterDropdownRecordSelect = ({
|
||||
});
|
||||
|
||||
const handleMultipleRecordSelectChange = (
|
||||
recordToSelect: SelectableRecord,
|
||||
recordToSelect: SelectableItem,
|
||||
newSelectedValue: boolean,
|
||||
) => {
|
||||
if (loading) {
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
selectAllLabel?: string;
|
||||
SelectAllIcon?: IconComponent;
|
||||
subFieldType?: FilterType;
|
||||
};
|
||||
|
||||
@ -17,4 +17,5 @@ export type FilterType =
|
||||
| 'RATING'
|
||||
| 'MULTI_SELECT'
|
||||
| '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':
|
||||
return [...relationOperands, ...emptyOperands];
|
||||
case 'SOURCE':
|
||||
return [...relationOperands];
|
||||
case 'SELECT':
|
||||
return [...relationOperands];
|
||||
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 { Filter } from '../../object-filter-dropdown/types/Filter';
|
||||
|
||||
export type ObjectDropdownFilter = Omit<Filter, 'definition'> & {
|
||||
definition: {
|
||||
type: Filter['definition']['type'];
|
||||
};
|
||||
};
|
||||
|
||||
const applyEmptyFilters = (
|
||||
operand: ViewFilterOperand,
|
||||
correspondingField: Pick<Field, 'id' | 'name'>,
|
||||
@ -282,7 +276,7 @@ const applyEmptyFilters = (
|
||||
};
|
||||
|
||||
export const turnObjectDropdownFilterIntoQueryFilter = (
|
||||
rawUIFilters: ObjectDropdownFilter[],
|
||||
rawUIFilters: Filter[],
|
||||
fields: Pick<Field, 'id' | 'name'>[],
|
||||
): RecordGqlOperationFilter | undefined => {
|
||||
const objectRecordFilters: RecordGqlOperationFilter[] = [];
|
||||
@ -894,48 +888,87 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
||||
break;
|
||||
}
|
||||
case 'ACTOR':
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
objectRecordFilters.push({
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
name: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
if (rawUIFilter.definition.subFieldType !== undefined) {
|
||||
const parsedRecordIds = JSON.parse(rawUIFilter.value) as string[];
|
||||
switch (rawUIFilter.definition.subFieldType) {
|
||||
case 'SOURCE':
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
source: {
|
||||
in: parsedRecordIds,
|
||||
} as RelationFilter,
|
||||
},
|
||||
} as ActorFilter,
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
objectRecordFilters.push({
|
||||
and: [
|
||||
{
|
||||
not: {
|
||||
});
|
||||
|
||||
break;
|
||||
case ViewFilterOperand.IsNot:
|
||||
if (parsedRecordIds.length > 0) {
|
||||
objectRecordFilters.push({
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
[rawUIFilter.definition.subFieldType.toLowerCase()]: {
|
||||
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]: {
|
||||
name: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as ActorFilter,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
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;
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
objectRecordFilters.push({
|
||||
and: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
name: {
|
||||
ilike: `%${rawUIFilter.value}%`,
|
||||
},
|
||||
} as ActorFilter,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
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;
|
||||
case 'EMAILS':
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
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 { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
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 { 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,
|
||||
hotkeyScope,
|
||||
recordsToSelect,
|
||||
loadingRecords,
|
||||
filteredSelectedRecords,
|
||||
itemsToSelect,
|
||||
loadingItems,
|
||||
filteredSelectedItems,
|
||||
onChange,
|
||||
searchFilter,
|
||||
}: {
|
||||
selectableListId: string;
|
||||
hotkeyScope: string;
|
||||
recordsToSelect: SelectableRecord[];
|
||||
filteredSelectedRecords: SelectableRecord[];
|
||||
selectedRecords: SelectableRecord[];
|
||||
itemsToSelect: SelectableItem[];
|
||||
filteredSelectedItems: SelectableItem[];
|
||||
selectedItems: SelectableItem[];
|
||||
searchFilter: string;
|
||||
onChange: (
|
||||
changedRecordToSelect: SelectableRecord,
|
||||
changedItemToSelect: SelectableItem,
|
||||
newSelectedValue: boolean,
|
||||
) => void;
|
||||
loadingRecords: boolean;
|
||||
loadingItems: boolean;
|
||||
}) => {
|
||||
const { closeDropdown } = useDropdown();
|
||||
const { selectedItemIdState } = useSelectableListStates({
|
||||
@ -44,32 +55,32 @@ export const MultipleRecordSelectDropdown = ({
|
||||
|
||||
const selectedItemId = useRecoilValue(selectedItemIdState);
|
||||
|
||||
const handleRecordSelectChange = (
|
||||
recordToSelect: SelectableRecord,
|
||||
const handleItemSelectChange = (
|
||||
itemToSelect: SelectableItem,
|
||||
newSelectedValue: boolean,
|
||||
) => {
|
||||
onChange(
|
||||
{
|
||||
...recordToSelect,
|
||||
...itemToSelect,
|
||||
isSelected: newSelectedValue,
|
||||
},
|
||||
newSelectedValue,
|
||||
);
|
||||
};
|
||||
|
||||
const [recordsInDropdown, setRecordInDropdown] = useState([
|
||||
...(filteredSelectedRecords ?? []),
|
||||
...(recordsToSelect ?? []),
|
||||
const [itemsInDropdown, setItemInDropdown] = useState([
|
||||
...(filteredSelectedItems ?? []),
|
||||
...(itemsToSelect ?? []),
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loadingRecords) {
|
||||
setRecordInDropdown([
|
||||
...(filteredSelectedRecords ?? []),
|
||||
...(recordsToSelect ?? []),
|
||||
if (!loadingItems) {
|
||||
setItemInDropdown([
|
||||
...(filteredSelectedItems ?? []),
|
||||
...(itemsToSelect ?? []),
|
||||
]);
|
||||
}
|
||||
}, [recordsToSelect, filteredSelectedRecords, loadingRecords]);
|
||||
}, [itemsToSelect, filteredSelectedItems, loadingItems]);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
@ -82,12 +93,12 @@ export const MultipleRecordSelectDropdown = ({
|
||||
);
|
||||
|
||||
const showNoResult =
|
||||
recordsToSelect?.length === 0 &&
|
||||
itemsToSelect?.length === 0 &&
|
||||
searchFilter !== '' &&
|
||||
filteredSelectedRecords?.length === 0 &&
|
||||
!loadingRecords;
|
||||
filteredSelectedItems?.length === 0 &&
|
||||
!loadingItems;
|
||||
|
||||
const selectableItemIds = recordsInDropdown.map((record) => record.id);
|
||||
const selectableItemIds = itemsInDropdown.map((item) => item.id);
|
||||
|
||||
return (
|
||||
<SelectableList
|
||||
@ -95,45 +106,46 @@ export const MultipleRecordSelectDropdown = ({
|
||||
selectableItemIdArray={selectableItemIds}
|
||||
hotkeyScope={hotkeyScope}
|
||||
onEnter={(itemId) => {
|
||||
const record = recordsInDropdown.findIndex(
|
||||
const item = itemsInDropdown.findIndex(
|
||||
(entity) => entity.id === itemId,
|
||||
);
|
||||
const recordIsSelectedInDropwdown = filteredSelectedRecords.find(
|
||||
const itemIsSelectedInDropwdown = filteredSelectedItems.find(
|
||||
(entity) => entity.id === itemId,
|
||||
);
|
||||
handleRecordSelectChange(
|
||||
recordsInDropdown[record],
|
||||
!recordIsSelectedInDropwdown,
|
||||
handleItemSelectChange(
|
||||
itemsInDropdown[item],
|
||||
!itemIsSelectedInDropwdown,
|
||||
);
|
||||
resetSelectedItem();
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{recordsInDropdown?.map((record) => {
|
||||
{itemsInDropdown?.map((item) => {
|
||||
return (
|
||||
<MenuItemMultiSelectAvatar
|
||||
key={record.id}
|
||||
selected={record.isSelected}
|
||||
isKeySelected={record.id === selectedItemId}
|
||||
key={item.id}
|
||||
selected={item.isSelected}
|
||||
isKeySelected={item.id === selectedItemId}
|
||||
onSelectChange={(newCheckedValue) => {
|
||||
resetSelectedItem();
|
||||
handleRecordSelectChange(record, newCheckedValue);
|
||||
handleItemSelectChange(item, newCheckedValue);
|
||||
}}
|
||||
avatar={
|
||||
<Avatar
|
||||
avatarUrl={record.avatarUrl}
|
||||
placeholderColorSeed={record.id}
|
||||
placeholder={record.name}
|
||||
size="md"
|
||||
type={record.avatarType ?? 'rounded'}
|
||||
<StyledAvatarChip
|
||||
className="avatar-icon-container"
|
||||
name={item.name}
|
||||
avatarUrl={item.avatarUrl}
|
||||
LeftIcon={item.AvatarIcon}
|
||||
avatarType={item.avatarType}
|
||||
isIconInverted={item.isIconInverted}
|
||||
placeholderColorSeed={item.id}
|
||||
/>
|
||||
}
|
||||
text={record.name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{showNoResult && <MenuItem text="No result" />}
|
||||
{loadingRecords && <DropdownMenuSkeletonItem />}
|
||||
{loadingItems && <DropdownMenuSkeletonItem />}
|
||||
</DropdownMenuItemsContainer>
|
||||
</SelectableList>
|
||||
);
|
||||
@ -5,7 +5,7 @@ import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapTo
|
||||
|
||||
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
||||
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 { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
|
||||
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
||||
@ -109,19 +109,19 @@ export const useRecordsForSelect = ({
|
||||
.map((record) => ({
|
||||
...record,
|
||||
isSelected: true,
|
||||
})) as SelectableRecord[],
|
||||
})) as SelectableItem[],
|
||||
filteredSelectedRecords: filteredSelectedRecordsData
|
||||
.map(mapToObjectRecordIdentifier)
|
||||
.map((record) => ({
|
||||
...record,
|
||||
isSelected: true,
|
||||
})) as SelectableRecord[],
|
||||
})) as SelectableItem[],
|
||||
recordsToSelect: recordsToSelectData
|
||||
.map(mapToObjectRecordIdentifier)
|
||||
.map((record) => ({
|
||||
...record,
|
||||
isSelected: false,
|
||||
})) as SelectableRecord[],
|
||||
})) as SelectableItem[],
|
||||
loading:
|
||||
recordsToSelectLoading ||
|
||||
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;
|
||||
selected: boolean;
|
||||
isKeySelected?: boolean;
|
||||
text: string;
|
||||
text?: string;
|
||||
className?: string;
|
||||
onSelectChange?: (selected: boolean) => void;
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
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 { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
|
||||
@ -45,6 +45,7 @@ type MenuItemSelectProps = {
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
hovered?: boolean;
|
||||
hasSubMenu?: boolean;
|
||||
};
|
||||
|
||||
export const MenuItemSelect = ({
|
||||
@ -55,6 +56,7 @@ export const MenuItemSelect = ({
|
||||
onClick,
|
||||
disabled,
|
||||
hovered,
|
||||
hasSubMenu = false,
|
||||
}: MenuItemSelectProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@ -68,6 +70,12 @@ export const MenuItemSelect = ({
|
||||
>
|
||||
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
|
||||
{selected && <IconCheck size={theme.icon.size.md} />}
|
||||
{hasSubMenu && (
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
)}
|
||||
</StyledMenuItemSelect>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||
import { ViewFilterOperand } from './ViewFilterOperand';
|
||||
|
||||
export type ViewFilter = {
|
||||
@ -11,4 +12,5 @@ export type ViewFilter = {
|
||||
createdAt?: string;
|
||||
updatedAt?: 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 { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { getFilterDefinitionForViewFilter } from '@/views/utils/getFilterDefinitionForViewFilter';
|
||||
import { ViewFilter } from '../types/ViewFilter';
|
||||
|
||||
export const mapViewFiltersToFilters = (
|
||||
@ -23,7 +24,10 @@ export const mapViewFiltersToFilters = (
|
||||
value: viewFilter.value,
|
||||
displayValue: viewFilter.displayValue,
|
||||
operand: viewFilter.operand,
|
||||
definition: availableFilterDefinition,
|
||||
definition: getFilterDefinitionForViewFilter(
|
||||
viewFilter,
|
||||
availableFilterDefinition,
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
@ -127,6 +127,7 @@ export const Chip = ({
|
||||
rightComponent,
|
||||
accent = ChipAccent.TextPrimary,
|
||||
onClick,
|
||||
className,
|
||||
}: ChipProps) => {
|
||||
return (
|
||||
<StyledContainer
|
||||
@ -137,6 +138,7 @@ export const Chip = ({
|
||||
size={size}
|
||||
variant={variant}
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
>
|
||||
{leftComponent}
|
||||
<OverflowingTextWithTooltip
|
||||
|
||||
Reference in New Issue
Block a user