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:
gitstart-app[bot]
2024-10-02 17:56:09 +02:00
committed by GitHub
parent 2cd3219636
commit 35788af351
27 changed files with 686 additions and 155 deletions

View File

@ -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>
);

View File

@ -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 ||

View File

@ -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;
};

View File

@ -1,10 +0,0 @@
import { AvatarType } from 'twenty-ui';
export type SelectableRecord = {
id: string;
name: string;
avatarUrl?: string;
avatarType?: AvatarType;
record: any;
isSelected: boolean;
};