Favorite folders (#7998)
closes - #5755 --------- Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1,127 +1,197 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Avatar, isDefined } from 'twenty-ui';
|
||||
|
||||
import { FavoritesSkeletonLoader } from '@/favorites/components/FavoritesSkeletonLoader';
|
||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
||||
import { FavoriteFolderNavigationDrawerItemDropdown } from '@/favorites/components/FavoriteFolderNavigationDrawerItemDropdown';
|
||||
import { FavoriteIcon } from '@/favorites/components/FavoriteIcon';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useDeleteFavoriteFolder } from '@/favorites/hooks/useDeleteFavoriteFolder';
|
||||
import { useRenameFavoriteFolder } from '@/favorites/hooks/useRenameFavoriteFolder';
|
||||
import { useReorderFavorite } from '@/favorites/hooks/useReorderFavorite';
|
||||
import { activeFavoriteFolderIdState } from '@/favorites/states/activeFavoriteFolderIdState';
|
||||
import { isLocationMatchingFavorite } from '@/favorites/utils/isLocationMatchingFavorite';
|
||||
import { ProcessedFavorite } from '@/favorites/utils/sortFavorites';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
||||
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
|
||||
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { IconFolder, IconHeartOff, LightIconButton } from 'twenty-ui';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
||||
import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { useFavorites } from '../hooks/useFavorites';
|
||||
type CurrentWorkspaceMemberFavoritesProps = {
|
||||
folder: {
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
favorites: ProcessedFavorite[];
|
||||
};
|
||||
isGroup: boolean;
|
||||
};
|
||||
|
||||
const StyledContainer = styled(NavigationDrawerSection)`
|
||||
width: 100%;
|
||||
`;
|
||||
export const CurrentWorkspaceMemberFavorites = ({
|
||||
folder,
|
||||
isGroup,
|
||||
}: CurrentWorkspaceMemberFavoritesProps) => {
|
||||
const currentPath = useLocation().pathname;
|
||||
const currentViewPath = useLocation().pathname + useLocation().search;
|
||||
|
||||
const StyledAvatar = styled(Avatar)`
|
||||
:hover {
|
||||
cursor: grab;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledNavigationDrawerItem = styled(NavigationDrawerItem)`
|
||||
:active {
|
||||
cursor: grabbing;
|
||||
|
||||
.fav-avatar:hover {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CurrentWorkspaceMemberFavorites = () => {
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const { favorites, handleReorderFavorite } = useFavorites();
|
||||
const loading = useIsPrefetchLoading();
|
||||
const { toggleNavigationSection, isNavigationSectionOpenState } =
|
||||
useNavigationSection('Favorites');
|
||||
const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState);
|
||||
|
||||
if (loading && isDefined(currentWorkspaceMember)) {
|
||||
return <FavoritesSkeletonLoader />;
|
||||
}
|
||||
|
||||
const currentWorkspaceMemberFavorites = favorites.filter(
|
||||
(favorite) => favorite.workspaceMemberId === currentWorkspaceMember?.id,
|
||||
const [isFavoriteFolderRenaming, setIsFavoriteFolderRenaming] =
|
||||
useState(false);
|
||||
const [favoriteFolderName, setFavoriteFolderName] = useState(
|
||||
folder.folderName,
|
||||
);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [activeFavoriteFolderId, setActiveFavoriteFolderId] = useRecoilState(
|
||||
activeFavoriteFolderIdState,
|
||||
);
|
||||
const isOpen = activeFavoriteFolderId === folder.folderId;
|
||||
|
||||
if (
|
||||
!currentWorkspaceMemberFavorites ||
|
||||
currentWorkspaceMemberFavorites.length === 0
|
||||
)
|
||||
return <></>;
|
||||
const handleToggle = () => {
|
||||
setActiveFavoriteFolderId(isOpen ? null : folder.folderId);
|
||||
};
|
||||
|
||||
const isGroup = currentWorkspaceMemberFavorites.length > 1;
|
||||
const { renameFavoriteFolder } = useRenameFavoriteFolder();
|
||||
const { deleteFavoriteFolder } = useDeleteFavoriteFolder();
|
||||
const {
|
||||
closeDropdown: closeFavoriteFolderEditDropdown,
|
||||
isDropdownOpen: isFavoriteFolderEditDropdownOpen,
|
||||
} = useDropdown(`favorite-folder-edit-${folder.folderId}`);
|
||||
const selectedFavoriteIndex = folder.favorites.findIndex((favorite) =>
|
||||
isLocationMatchingFavorite(currentPath, currentViewPath, favorite),
|
||||
);
|
||||
const handleReorderFavorite = useReorderFavorite();
|
||||
|
||||
const draggableListContent = (
|
||||
<DraggableList
|
||||
onDragEnd={handleReorderFavorite}
|
||||
draggableItems={
|
||||
<>
|
||||
{currentWorkspaceMemberFavorites.map((favorite, index) => {
|
||||
const {
|
||||
id,
|
||||
labelIdentifier,
|
||||
avatarUrl,
|
||||
avatarType,
|
||||
link,
|
||||
recordId,
|
||||
} = favorite;
|
||||
const deleteFavorite = useDeleteFavorite();
|
||||
|
||||
return (
|
||||
<DraggableItem
|
||||
key={id}
|
||||
draggableId={id}
|
||||
index={index}
|
||||
itemComponent={
|
||||
<StyledNavigationDrawerItem
|
||||
key={id}
|
||||
label={labelIdentifier}
|
||||
Icon={() => (
|
||||
<StyledAvatar
|
||||
placeholderColorSeed={recordId}
|
||||
avatarUrl={avatarUrl}
|
||||
type={avatarType}
|
||||
placeholder={labelIdentifier}
|
||||
className="fav-avatar"
|
||||
/>
|
||||
)}
|
||||
to={link}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
}
|
||||
const favoriteFolderContentLength = folder.favorites.length;
|
||||
|
||||
const handleSubmitRename = async (value: string) => {
|
||||
if (value === '') return;
|
||||
await renameFavoriteFolder(folder.folderId, value);
|
||||
setIsFavoriteFolderRenaming(false);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleCancelRename = () => {
|
||||
setFavoriteFolderName(folder.folderName);
|
||||
setIsFavoriteFolderRenaming(false);
|
||||
};
|
||||
|
||||
const handleClickOutside = async (
|
||||
event: MouseEvent | TouchEvent,
|
||||
value: string,
|
||||
) => {
|
||||
if (!value) {
|
||||
setIsFavoriteFolderRenaming(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await renameFavoriteFolder(folder.folderId, value);
|
||||
setIsFavoriteFolderRenaming(false);
|
||||
};
|
||||
|
||||
const handleFavoriteFolderDelete = async () => {
|
||||
if (folder.favorites.length > 0) {
|
||||
setIsDeleteModalOpen(true);
|
||||
closeFavoriteFolderEditDropdown();
|
||||
} else {
|
||||
await deleteFavoriteFolder(folder.folderId);
|
||||
closeFavoriteFolderEditDropdown();
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
await deleteFavoriteFolder(folder.folderId);
|
||||
setIsDeleteModalOpen(false);
|
||||
};
|
||||
|
||||
const rightOptions = (
|
||||
<FavoriteFolderNavigationDrawerItemDropdown
|
||||
folderId={folder.folderId}
|
||||
onRename={() => setIsFavoriteFolderRenaming(true)}
|
||||
onDelete={handleFavoriteFolderDelete}
|
||||
closeDropdown={closeFavoriteFolderEditDropdown}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<NavigationDrawerSectionTitle
|
||||
label="Favorites"
|
||||
onClick={() => toggleNavigationSection()}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
<>
|
||||
<NavigationDrawerItemsCollapsableContainer
|
||||
key={folder.folderId}
|
||||
isGroup={isGroup}
|
||||
>
|
||||
{isFavoriteFolderRenaming ? (
|
||||
<NavigationDrawerInput
|
||||
Icon={IconFolder}
|
||||
value={favoriteFolderName}
|
||||
onChange={setFavoriteFolderName}
|
||||
onSubmit={handleSubmitRename}
|
||||
onCancel={handleCancelRename}
|
||||
onClickOutside={handleClickOutside}
|
||||
hotkeyScope="favorites-folder-input"
|
||||
/>
|
||||
) : (
|
||||
<NavigationDrawerItem
|
||||
key={folder.folderId}
|
||||
label={folder.folderName}
|
||||
Icon={IconFolder}
|
||||
onClick={handleToggle}
|
||||
rightOptions={rightOptions}
|
||||
className="navigation-drawer-item"
|
||||
active={isFavoriteFolderEditDropdownOpen}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isNavigationSectionOpen && (
|
||||
<ScrollWrapper contextProviderName="navigationDrawer">
|
||||
<NavigationDrawerItemsCollapsedContainer isGroup={isGroup}>
|
||||
{draggableListContent}
|
||||
</NavigationDrawerItemsCollapsedContainer>
|
||||
</ScrollWrapper>
|
||||
)}
|
||||
</StyledContainer>
|
||||
{isOpen && (
|
||||
<DraggableList
|
||||
onDragEnd={handleReorderFavorite}
|
||||
draggableItems={
|
||||
<>
|
||||
{folder.favorites.map((favorite, index) => (
|
||||
<DraggableItem
|
||||
key={favorite.id}
|
||||
draggableId={favorite.id}
|
||||
index={index}
|
||||
itemComponent={
|
||||
<NavigationDrawerSubItem
|
||||
key={favorite.id}
|
||||
label={favorite.labelIdentifier}
|
||||
Icon={() => <FavoriteIcon favorite={favorite} />}
|
||||
to={favorite.link}
|
||||
active={index === selectedFavoriteIndex}
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index,
|
||||
arrayLength: favoriteFolderContentLength,
|
||||
selectedIndex: selectedFavoriteIndex,
|
||||
})}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() => deleteFavorite(favorite.id)}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
isDraggable
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</NavigationDrawerItemsCollapsableContainer>
|
||||
|
||||
<ConfirmationModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
setIsOpen={setIsDeleteModalOpen}
|
||||
title={`Remove ${folder.favorites.length} ${folder.favorites.length > 1 ? 'favorites' : 'favorite'}?`}
|
||||
subtitle={`This action will delete this favorite folder ${folder.favorites.length > 1 ? `and all ${folder.favorites.length} favorites` : 'and the favorite'} inside. Do you want to continue?`}
|
||||
onConfirmClick={handleConfirmDelete}
|
||||
deleteButtonText="Delete Folder"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,137 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import {
|
||||
IconFolderPlus,
|
||||
IconHeartOff,
|
||||
isDefined,
|
||||
LightIconButton,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { FavoriteIcon } from '@/favorites/components/FavoriteIcon';
|
||||
import { FavoriteFolders } from '@/favorites/components/FavoritesFolders';
|
||||
import { FavoritesSkeletonLoader } from '@/favorites/components/FavoritesSkeletonLoader';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useReorderFavorite } from '@/favorites/hooks/useReorderFavorite';
|
||||
import { isFavoriteFolderCreatingState } from '@/favorites/states/isFavoriteFolderCreatingState';
|
||||
import { isLocationMatchingFavorite } from '@/favorites/utils/isLocationMatchingFavorite';
|
||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
||||
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
export const CurrentWorkspaceMemberFavoritesFolders = () => {
|
||||
const currentPath = useLocation().pathname;
|
||||
const currentViewPath = useLocation().pathname + useLocation().search;
|
||||
const theme = useTheme();
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
const favorites = useFavorites();
|
||||
const deleteFavorite = useDeleteFavorite();
|
||||
const handleReorderFavorite = useReorderFavorite();
|
||||
const [isFavoriteFolderCreating, setIsFavoriteFolderCreating] =
|
||||
useRecoilState(isFavoriteFolderCreatingState);
|
||||
|
||||
const isFavoriteFolderEnabled = useIsFeatureEnabled(
|
||||
'IS_FAVORITE_FOLDER_ENABLED',
|
||||
);
|
||||
const loading = useIsPrefetchLoading();
|
||||
|
||||
const { toggleNavigationSection, isNavigationSectionOpenState } =
|
||||
useNavigationSection('Favorites');
|
||||
const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState);
|
||||
|
||||
const toggleNewFolder = () => {
|
||||
setIsFavoriteFolderCreating((current) => !current);
|
||||
};
|
||||
|
||||
if (loading && isDefined(currentWorkspaceMember)) {
|
||||
return <FavoritesSkeletonLoader />;
|
||||
}
|
||||
|
||||
const currentWorkspaceMemberFavorites = favorites.filter(
|
||||
(favorite) => favorite.workspaceMemberId === currentWorkspaceMember?.id,
|
||||
);
|
||||
|
||||
const orphanFavorites = currentWorkspaceMemberFavorites.filter(
|
||||
(favorite) => !favorite.favoriteFolderId,
|
||||
);
|
||||
|
||||
if (
|
||||
(!currentWorkspaceMemberFavorites ||
|
||||
currentWorkspaceMemberFavorites.length === 0) &&
|
||||
!isFavoriteFolderCreating
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationDrawerSection>
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<NavigationDrawerSectionTitle
|
||||
label="Favorites"
|
||||
onClick={toggleNavigationSection}
|
||||
rightIcon={
|
||||
isFavoriteFolderEnabled ? (
|
||||
<IconFolderPlus size={theme.icon.size.sm} />
|
||||
) : undefined
|
||||
}
|
||||
onRightIconClick={
|
||||
isFavoriteFolderEnabled ? toggleNewFolder : undefined
|
||||
}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
|
||||
{isNavigationSectionOpen && (
|
||||
<>
|
||||
{isFavoriteFolderEnabled && (
|
||||
<FavoriteFolders
|
||||
isNavigationSectionOpen={isNavigationSectionOpen}
|
||||
/>
|
||||
)}
|
||||
|
||||
{orphanFavorites.length > 0 && (
|
||||
<DraggableList
|
||||
onDragEnd={handleReorderFavorite}
|
||||
draggableItems={orphanFavorites.map((favorite, index) => (
|
||||
<DraggableItem
|
||||
key={favorite.id}
|
||||
draggableId={favorite.id}
|
||||
index={index}
|
||||
itemComponent={
|
||||
<NavigationDrawerItem
|
||||
key={favorite.id}
|
||||
className="navigation-drawer-item"
|
||||
label={favorite.labelIdentifier}
|
||||
Icon={() => <FavoriteIcon favorite={favorite} />}
|
||||
active={isLocationMatchingFavorite(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
favorite,
|
||||
)}
|
||||
to={favorite.link}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() => deleteFavorite(favorite.id)}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
isDraggable={true}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</NavigationDrawerSection>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,65 @@
|
||||
import { FavoriteFolderHotkeyScope } from '@/favorites/constants/FavoriteFolderRightIconDropdownHotkeyScope';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import {
|
||||
IconDotsVertical,
|
||||
IconPencil,
|
||||
IconTrash,
|
||||
LightIconButton,
|
||||
MenuItem,
|
||||
} from 'twenty-ui';
|
||||
|
||||
type FavoriteFolderNavigationDrawerItemDropdownProps = {
|
||||
folderId: string;
|
||||
onRename: () => void;
|
||||
onDelete: () => void;
|
||||
closeDropdown: () => void;
|
||||
};
|
||||
|
||||
export const FavoriteFolderNavigationDrawerItemDropdown = ({
|
||||
folderId,
|
||||
onRename,
|
||||
onDelete,
|
||||
closeDropdown,
|
||||
}: FavoriteFolderNavigationDrawerItemDropdownProps) => {
|
||||
const handleRename = () => {
|
||||
onRename();
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
onDelete();
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={`favorite-folder-edit-${folderId}`}
|
||||
dropdownHotkeyScope={{
|
||||
scope: FavoriteFolderHotkeyScope.FavoriteFolderRightIconDropdown,
|
||||
}}
|
||||
data-select-disable
|
||||
clickableComponent={
|
||||
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
|
||||
}
|
||||
dropdownPlacement="right"
|
||||
dropdownOffset={{ y: -15 }}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
LeftIcon={IconPencil}
|
||||
onClick={handleRename}
|
||||
accent="default"
|
||||
text="Rename"
|
||||
/>
|
||||
<MenuItem
|
||||
LeftIcon={IconTrash}
|
||||
onClick={handleDelete}
|
||||
accent="danger"
|
||||
text="Delete"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,27 @@
|
||||
import { ProcessedFavorite } from '@/favorites/utils/sortFavorites';
|
||||
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { Avatar, useIcons } from 'twenty-ui';
|
||||
|
||||
export const FavoriteIcon = ({ favorite }: { favorite: ProcessedFavorite }) => {
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
const { Icon: StandardIcon, IconColor } = useGetStandardObjectIcon(
|
||||
favorite.objectNameSingular || '',
|
||||
);
|
||||
const IconToUse =
|
||||
StandardIcon || (favorite.Icon ? getIcon(favorite.Icon) : undefined);
|
||||
const iconColorToUse = StandardIcon ? IconColor : theme.font.color.secondary;
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
size="md"
|
||||
type={favorite.avatarType}
|
||||
Icon={IconToUse}
|
||||
iconColor={iconColorToUse}
|
||||
avatarUrl={favorite.avatarUrl}
|
||||
placeholder={favorite.labelIdentifier}
|
||||
placeholderColorSeed={favorite.recordId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,85 @@
|
||||
import { useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { IconFolder } from 'twenty-ui';
|
||||
|
||||
import { CurrentWorkspaceMemberFavorites } from '@/favorites/components/CurrentWorkspaceMemberFavorites';
|
||||
import { FavoriteFolderHotkeyScope } from '@/favorites/constants/FavoriteFolderRightIconDropdownHotkeyScope';
|
||||
import { useCreateFavoriteFolder } from '@/favorites/hooks/useCreateFavoriteFolder';
|
||||
import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder';
|
||||
import { isFavoriteFolderCreatingState } from '@/favorites/states/isFavoriteFolderCreatingState';
|
||||
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
|
||||
|
||||
type FavoriteFoldersProps = {
|
||||
isNavigationSectionOpen: boolean;
|
||||
};
|
||||
|
||||
export const FavoriteFolders = ({
|
||||
isNavigationSectionOpen,
|
||||
}: FavoriteFoldersProps) => {
|
||||
const [newFolderName, setNewFolderName] = useState('');
|
||||
|
||||
const favoritesByFolder = useFavoritesByFolder();
|
||||
const createFavoriteFolder = useCreateFavoriteFolder();
|
||||
|
||||
const [isFavoriteFolderCreating, setIsFavoriteFolderCreating] =
|
||||
useRecoilState(isFavoriteFolderCreatingState);
|
||||
|
||||
const handleFavoriteFolderNameChange = (value: string) => {
|
||||
setNewFolderName(value);
|
||||
};
|
||||
|
||||
const handleSubmitFavoriteFolderCreation = async (value: string) => {
|
||||
if (value === '') return;
|
||||
|
||||
setIsFavoriteFolderCreating(false);
|
||||
setNewFolderName('');
|
||||
await createFavoriteFolder(value);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleClickOutside = async (
|
||||
event: MouseEvent | TouchEvent,
|
||||
value: string,
|
||||
) => {
|
||||
if (!value) {
|
||||
setIsFavoriteFolderCreating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsFavoriteFolderCreating(false);
|
||||
setNewFolderName('');
|
||||
await createFavoriteFolder(value);
|
||||
};
|
||||
|
||||
const handleCancelFavoriteFolderCreation = () => {
|
||||
setNewFolderName('');
|
||||
setIsFavoriteFolderCreating(false);
|
||||
};
|
||||
|
||||
if (!isNavigationSectionOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFavoriteFolderCreating && (
|
||||
<NavigationDrawerInput
|
||||
Icon={IconFolder}
|
||||
value={newFolderName}
|
||||
onChange={handleFavoriteFolderNameChange}
|
||||
onSubmit={handleSubmitFavoriteFolderCreation}
|
||||
onCancel={handleCancelFavoriteFolderCreation}
|
||||
onClickOutside={handleClickOutside}
|
||||
hotkeyScope={FavoriteFolderHotkeyScope.FavoriteFolderNavigationInput}
|
||||
/>
|
||||
)}
|
||||
{favoritesByFolder.map((folder) => (
|
||||
<CurrentWorkspaceMemberFavorites
|
||||
key={folder.folderId}
|
||||
folder={folder}
|
||||
isGroup={favoritesByFolder.length > 1}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
export enum FavoriteFolderHotkeyScope {
|
||||
FavoriteFolderRightIconDropdown = 'favorite-folder-right-icon-dropdown',
|
||||
FavoriteFolderNavigationInput = 'favorite-folder-navigation-input',
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { MenuItemMultiSelect } from '@ui/navigation/menu-item/components/MenuItemMultiSelect';
|
||||
|
||||
const StyledNoGapMenuItem = styled(MenuItemMultiSelect)`
|
||||
& > div {
|
||||
gap: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FavoriteFolderMenuItemMultiSelect = StyledNoGapMenuItem;
|
||||
@ -0,0 +1,104 @@
|
||||
import { FavoriteFolderPickerFooter } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPickerFooter';
|
||||
import { FavoriteFolderPickerList } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPickerList';
|
||||
import { FavoriteFolderPickerSearchInput } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPickerSearchInput';
|
||||
import { useFavoriteFolderPicker } from '@/favorites/favorite-folder-picker/hooks/useFavoriteFolderPicker';
|
||||
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
||||
import { favoriteFolderSearchFilterComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFoldersSearchFilterComponentState';
|
||||
import { isFavoriteFolderCreatingState } from '@/favorites/states/isFavoriteFolderCreatingState';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
type FavoriteFolderPickerProps = {
|
||||
onSubmit?: () => void;
|
||||
record?: ObjectRecord;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
const NO_FOLDER_ID = 'no-folder';
|
||||
|
||||
export const FavoriteFolderPicker = ({
|
||||
onSubmit,
|
||||
record,
|
||||
objectNameSingular,
|
||||
}: FavoriteFolderPickerProps) => {
|
||||
const [isFavoriteFolderCreating, setIsFavoriteFolderCreating] =
|
||||
useRecoilState(isFavoriteFolderCreatingState);
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
FavoriteFolderPickerInstanceContext,
|
||||
);
|
||||
|
||||
const { getFoldersByIds, toggleFolderSelection } = useFavoriteFolderPicker({
|
||||
record,
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const [favoriteFoldersSearchFilter] = useRecoilComponentStateV2(
|
||||
favoriteFolderSearchFilterComponentState,
|
||||
);
|
||||
|
||||
const folders = getFoldersByIds();
|
||||
const filteredFolders = folders.filter((folder) =>
|
||||
folder.name
|
||||
.toLowerCase()
|
||||
.includes(favoriteFoldersSearchFilter.toLowerCase()),
|
||||
);
|
||||
|
||||
const showNoFolderOption =
|
||||
!favoriteFoldersSearchFilter ||
|
||||
'no folder'.includes(favoriteFoldersSearchFilter.toLowerCase());
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
if (isFavoriteFolderCreating) {
|
||||
setIsFavoriteFolderCreating(false);
|
||||
return;
|
||||
}
|
||||
onSubmit?.();
|
||||
},
|
||||
instanceId,
|
||||
[onSubmit, isFavoriteFolderCreating],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
() => {
|
||||
if (filteredFolders.length === 1 && !showNoFolderOption) {
|
||||
toggleFolderSelection(filteredFolders[0].id);
|
||||
onSubmit?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (showNoFolderOption && filteredFolders.length === 0) {
|
||||
toggleFolderSelection(NO_FOLDER_ID);
|
||||
onSubmit?.();
|
||||
return;
|
||||
}
|
||||
},
|
||||
instanceId,
|
||||
[filteredFolders, showNoFolderOption, toggleFolderSelection, onSubmit],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu data-select-disable>
|
||||
<FavoriteFolderPickerSearchInput />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<FavoriteFolderPickerList
|
||||
folders={folders}
|
||||
toggleFolderSelection={toggleFolderSelection}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
<FavoriteFolderPickerFooter />
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,82 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { favoriteFolderIdsPickerComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderIdPickerComponentState';
|
||||
import { favoriteFolderPickerCheckedComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerCheckedComponentState';
|
||||
import { favoriteFolderPickerComponentFamilyState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerComponentFamilyState';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { usePrefetchedFavoritesFoldersData } from '@/favorites/hooks/usePrefetchedFavoritesFoldersData';
|
||||
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
|
||||
type FavoriteFolderPickerEffectProps = {
|
||||
record?: ObjectRecord;
|
||||
};
|
||||
|
||||
export const FavoriteFolderPickerEffect = ({
|
||||
record,
|
||||
}: FavoriteFolderPickerEffectProps) => {
|
||||
const [favoriteFolderIdsPicker, setFavoriteFolderIdsPicker] =
|
||||
useRecoilComponentStateV2(favoriteFolderIdsPickerComponentState);
|
||||
|
||||
const favoriteFolderPickerFamilyState = useRecoilComponentCallbackStateV2(
|
||||
favoriteFolderPickerComponentFamilyState,
|
||||
);
|
||||
|
||||
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
|
||||
|
||||
const favorites = useFavorites();
|
||||
const setCheckedState = useSetRecoilComponentStateV2(
|
||||
favoriteFolderPickerCheckedComponentState,
|
||||
);
|
||||
|
||||
const updateFolders = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(folders: FavoriteFolder[]) => {
|
||||
folders.forEach((folder) => {
|
||||
const currentFolder = snapshot
|
||||
.getLoadable(favoriteFolderPickerFamilyState(folder.id))
|
||||
.getValue();
|
||||
|
||||
if (!isDeeplyEqual(folder, currentFolder)) {
|
||||
set(favoriteFolderPickerFamilyState(folder.id), folder);
|
||||
}
|
||||
});
|
||||
},
|
||||
[favoriteFolderPickerFamilyState],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(favoriteFolders)) {
|
||||
updateFolders(favoriteFolders);
|
||||
|
||||
const folderIds = favoriteFolders.map((folder) => folder.id);
|
||||
if (!isDeeplyEqual(folderIds, favoriteFolderIdsPicker)) {
|
||||
setFavoriteFolderIdsPicker(folderIds);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
favoriteFolders,
|
||||
favoriteFolderIdsPicker,
|
||||
setFavoriteFolderIdsPicker,
|
||||
updateFolders,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const targetId = record?.id;
|
||||
const checkedFolderIds = favorites
|
||||
.filter(
|
||||
(favorite) =>
|
||||
favorite.recordId === targetId && favorite.workspaceMemberId,
|
||||
)
|
||||
.map((favorite) => favorite.favoriteFolderId || 'no-folder');
|
||||
setCheckedState(checkedFolderIds);
|
||||
}, [favorites, setCheckedState, record?.id]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -0,0 +1,43 @@
|
||||
import { FAVORITE_FOLDER_PICKER_DROPDOWN_ID } from '@/favorites/favorite-folder-picker/constants/FavoriteFolderPickerDropdownId';
|
||||
import { isFavoriteFolderCreatingState } from '@/favorites/states/isFavoriteFolderCreatingState';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { IconPlus, MenuItem } from 'twenty-ui';
|
||||
|
||||
const StyledFooter = styled.div`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border-bottom-left-radius: ${({ theme }) => theme.border.radius.md};
|
||||
border-bottom-right-radius: ${({ theme }) => theme.border.radius.md};
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
`;
|
||||
|
||||
const StyledIconPlus = styled(IconPlus)`
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const FavoriteFolderPickerFooter = () => {
|
||||
const [, setIsFavoriteFolderCreating] = useRecoilState(
|
||||
isFavoriteFolderCreatingState,
|
||||
);
|
||||
const theme = useTheme();
|
||||
const { closeDropdown } = useDropdown(FAVORITE_FOLDER_PICKER_DROPDOWN_ID);
|
||||
|
||||
return (
|
||||
<StyledFooter>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
className="add-folder"
|
||||
onClick={() => {
|
||||
setIsFavoriteFolderCreating(true);
|
||||
closeDropdown();
|
||||
}}
|
||||
text="Add folder"
|
||||
LeftIcon={() => <StyledIconPlus size={theme.icon.size.md} />}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</StyledFooter>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,74 @@
|
||||
import { favoriteFolderPickerCheckedComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerCheckedComponentState';
|
||||
import { favoriteFolderSearchFilterComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFoldersSearchFilterComponentState';
|
||||
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { MenuItem } from 'twenty-ui';
|
||||
import { FavoriteFolderMenuItemMultiSelect } from './FavoriteFolderMenuItemMultiSelect';
|
||||
|
||||
const StyledItemsContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledDropdownMenuSeparator = styled(DropdownMenuSeparator)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
type FavoriteFolderPickerListProps = {
|
||||
folders: FavoriteFolder[];
|
||||
toggleFolderSelection: (folderId: string) => void;
|
||||
};
|
||||
|
||||
export const NO_FOLDER_ID = 'no-folder';
|
||||
|
||||
export const FavoriteFolderPickerList = ({
|
||||
folders,
|
||||
toggleFolderSelection,
|
||||
}: FavoriteFolderPickerListProps) => {
|
||||
const [favoriteFoldersSearchFilter] = useRecoilComponentStateV2(
|
||||
favoriteFolderSearchFilterComponentState,
|
||||
);
|
||||
const [favoriteFolderPickerChecked] = useRecoilComponentStateV2(
|
||||
favoriteFolderPickerCheckedComponentState,
|
||||
);
|
||||
|
||||
const filteredFolders = folders.filter((folder) =>
|
||||
folder.name
|
||||
.toLowerCase()
|
||||
.includes(favoriteFoldersSearchFilter.toLowerCase()),
|
||||
);
|
||||
|
||||
const showNoFolderOption =
|
||||
!favoriteFoldersSearchFilter ||
|
||||
'no folder'.includes(favoriteFoldersSearchFilter.toLowerCase());
|
||||
|
||||
return (
|
||||
<StyledItemsContainer>
|
||||
{showNoFolderOption && (
|
||||
<FavoriteFolderMenuItemMultiSelect
|
||||
key={`menu-${NO_FOLDER_ID}`}
|
||||
onSelectChange={() => toggleFolderSelection(NO_FOLDER_ID)}
|
||||
selected={favoriteFolderPickerChecked.includes(NO_FOLDER_ID)}
|
||||
text="No folder"
|
||||
className="no-folder-menu-item-multi-select"
|
||||
/>
|
||||
)}
|
||||
{showNoFolderOption && filteredFolders.length > 0 && (
|
||||
<StyledDropdownMenuSeparator />
|
||||
)}
|
||||
{filteredFolders.length > 0
|
||||
? filteredFolders.map((folder) => (
|
||||
<FavoriteFolderMenuItemMultiSelect
|
||||
key={`menu-${folder.id}`}
|
||||
onSelectChange={() => toggleFolderSelection(folder.id)}
|
||||
selected={favoriteFolderPickerChecked.includes(folder.id)}
|
||||
text={folder.name}
|
||||
className="folder-menu-item-multi-select"
|
||||
/>
|
||||
))
|
||||
: !showNoFolderOption && <MenuItem text="No folders found" />}
|
||||
</StyledItemsContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
import { favoriteFolderSearchFilterComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFoldersSearchFilterComponentState';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useCallback } from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
export const FavoriteFolderPickerSearchInput = () => {
|
||||
const [favoriteFoldersSearchFilter, setFavoriteFoldersSearchFilter] =
|
||||
useRecoilComponentStateV2(favoriteFolderSearchFilterComponentState);
|
||||
|
||||
const debouncedSetSearchFilter = useDebouncedCallback(
|
||||
setFavoriteFoldersSearchFilter,
|
||||
100,
|
||||
{ leading: true },
|
||||
);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
debouncedSetSearchFilter(event.currentTarget.value);
|
||||
},
|
||||
[debouncedSetSearchFilter],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuSearchInput
|
||||
value={favoriteFoldersSearchFilter}
|
||||
onChange={handleFilterChange}
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export const FAVORITE_FOLDER_PICKER_DROPDOWN_ID =
|
||||
'favorite-folder-picker-dropdown';
|
||||
@ -0,0 +1,129 @@
|
||||
import { favoriteFolderIdsPickerComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderIdPickerComponentState';
|
||||
import { favoriteFolderPickerCheckedComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerCheckedComponentState';
|
||||
import { favoriteFolderPickerComponentFamilyState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerComponentFamilyState';
|
||||
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
|
||||
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
type useFavoriteFolderPickerProps = {
|
||||
record?: ObjectRecord;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
type FolderOperations = {
|
||||
getFoldersByIds: () => FavoriteFolder[];
|
||||
toggleFolderSelection: (folderId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const useFavoriteFolderPicker = ({
|
||||
record,
|
||||
objectNameSingular,
|
||||
}: useFavoriteFolderPickerProps): FolderOperations => {
|
||||
const [favoriteFolderIdsPicker] = useRecoilComponentStateV2(
|
||||
favoriteFolderIdsPickerComponentState,
|
||||
);
|
||||
|
||||
const favoriteFoldersMultiSelectCheckedState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
favoriteFolderPickerCheckedComponentState,
|
||||
);
|
||||
|
||||
const favoriteFolderPickerFamilyState = useRecoilComponentCallbackStateV2(
|
||||
favoriteFolderPickerComponentFamilyState,
|
||||
);
|
||||
|
||||
const favorites = useFavorites();
|
||||
const createFavorite = useCreateFavorite();
|
||||
const deleteFavorite = useDeleteFavorite();
|
||||
|
||||
const getFoldersByIds = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(): FavoriteFolder[] => {
|
||||
return favoriteFolderIdsPicker
|
||||
.map((folderId) => {
|
||||
const folderValue = snapshot
|
||||
.getLoadable(favoriteFolderPickerFamilyState(folderId))
|
||||
.getValue();
|
||||
|
||||
return folderValue;
|
||||
})
|
||||
.filter((folder): folder is FavoriteFolder => isDefined(folder));
|
||||
},
|
||||
[favoriteFolderIdsPicker, favoriteFolderPickerFamilyState],
|
||||
);
|
||||
|
||||
const toggleFolderSelection = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (folderId: string) => {
|
||||
const targetId = record?.id;
|
||||
const targetObject = record;
|
||||
|
||||
if (!isDefined(targetObject) || !isDefined(targetId)) {
|
||||
throw new Error(
|
||||
`Cannot toggle folder selection: record ${
|
||||
!isDefined(targetObject) ? 'object' : 'id'
|
||||
} is not defined`,
|
||||
);
|
||||
}
|
||||
|
||||
const deleteFavoriteForRecord = async (isUnorganized: boolean) => {
|
||||
const favoriteToDelete = favorites.find(
|
||||
(favorite) =>
|
||||
favorite.recordId === targetId &&
|
||||
(isUnorganized
|
||||
? !favorite.favoriteFolderId
|
||||
: favorite.favoriteFolderId === folderId),
|
||||
);
|
||||
|
||||
if (!isDefined(favoriteToDelete)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteFavorite(favoriteToDelete.id);
|
||||
};
|
||||
|
||||
const checkedIds = snapshot
|
||||
.getLoadable(favoriteFoldersMultiSelectCheckedState)
|
||||
.getValue();
|
||||
|
||||
const isAlreadyChecked = checkedIds.includes(folderId);
|
||||
|
||||
if (isAlreadyChecked) {
|
||||
await deleteFavoriteForRecord(folderId === 'no-folder');
|
||||
|
||||
const newCheckedIds = checkedIds.filter((id) => id !== folderId);
|
||||
set(favoriteFoldersMultiSelectCheckedState, newCheckedIds);
|
||||
return;
|
||||
}
|
||||
|
||||
const folderIdToUse = folderId === 'no-folder' ? undefined : folderId;
|
||||
|
||||
if (isDefined(record)) {
|
||||
await createFavorite(record, objectNameSingular, folderIdToUse);
|
||||
}
|
||||
|
||||
const newCheckedIds = [...checkedIds, folderId];
|
||||
set(favoriteFoldersMultiSelectCheckedState, newCheckedIds);
|
||||
},
|
||||
[
|
||||
favoriteFoldersMultiSelectCheckedState,
|
||||
createFavorite,
|
||||
deleteFavorite,
|
||||
favorites,
|
||||
record,
|
||||
objectNameSingular,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
getFoldersByIds,
|
||||
toggleFolderSelection,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,20 @@
|
||||
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type FavoriteFolderPickerScopeProps = {
|
||||
children: ReactNode;
|
||||
favoriteFoldersScopeId: string;
|
||||
};
|
||||
|
||||
export const FavoriteFolderPickerScope = ({
|
||||
children,
|
||||
favoriteFoldersScopeId,
|
||||
}: FavoriteFolderPickerScopeProps) => {
|
||||
return (
|
||||
<FavoriteFolderPickerInstanceContext.Provider
|
||||
value={{ instanceId: favoriteFoldersScopeId }}
|
||||
>
|
||||
{children}
|
||||
</FavoriteFolderPickerInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const FavoriteFolderPickerInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
@ -0,0 +1,10 @@
|
||||
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const favoriteFolderIdsPickerComponentState = createComponentStateV2<
|
||||
string[]
|
||||
>({
|
||||
key: 'favoriteFolderIdsPickerComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: FavoriteFolderPickerInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const favoriteFolderLoadingComponentState =
|
||||
createComponentStateV2<boolean>({
|
||||
key: 'favoriteFoldersLoadingComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: FavoriteFolderPickerInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const favoriteFolderPickerCheckedComponentState = createComponentStateV2<
|
||||
string[]
|
||||
>({
|
||||
key: 'favoriteFolderPickerCheckedComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: FavoriteFolderPickerInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
||||
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
||||
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
|
||||
|
||||
export const favoriteFolderPickerComponentFamilyState =
|
||||
createComponentFamilyStateV2<FavoriteFolder | undefined, string>({
|
||||
key: 'favoriteFolderPickerComponentFamilyState',
|
||||
defaultValue: undefined,
|
||||
componentInstanceContext: FavoriteFolderPickerInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const favoriteFolderSearchFilterComponentState =
|
||||
createComponentStateV2<string>({
|
||||
key: 'favoriteFolderSearchFilterComponentState',
|
||||
defaultValue: '',
|
||||
componentInstanceContext: FavoriteFolderPickerInstanceContext,
|
||||
});
|
||||
@ -8,10 +8,12 @@ export const favoriteId = 'f088c8c9-05d2-4276-b065-b863cc7d0b33';
|
||||
const favoriteTargetObjectId = 'f2d8b9e9-7932-4065-bc09-baf12388b75d';
|
||||
export const favoriteTargetObjectRecord = {
|
||||
id: favoriteTargetObjectId,
|
||||
__typename: 'Person',
|
||||
};
|
||||
|
||||
export const initialFavorites = [
|
||||
{
|
||||
__typename: 'Favorite',
|
||||
id: '1',
|
||||
position: 0,
|
||||
key: mockId,
|
||||
@ -22,8 +24,11 @@ export const initialFavorites = [
|
||||
recordId: '1',
|
||||
person: { id: '1', name: 'John Doe' },
|
||||
company: { id: '2', name: 'ABC Corp' },
|
||||
workspaceMemberId: '1',
|
||||
favoriteFolderId: undefined,
|
||||
},
|
||||
{
|
||||
__typename: 'Favorite',
|
||||
id: '2',
|
||||
position: 1,
|
||||
key: mockId,
|
||||
@ -34,8 +39,12 @@ export const initialFavorites = [
|
||||
recordId: '1',
|
||||
person: { id: '3', name: 'Jane Doe' },
|
||||
company: { id: '4', name: 'Company Test' },
|
||||
workspaceMemberId: '1',
|
||||
favoriteFolderId: undefined,
|
||||
|
||||
},
|
||||
{
|
||||
__typename: 'Favorite',
|
||||
id: '3',
|
||||
position: 2,
|
||||
key: mockId,
|
||||
@ -44,27 +53,37 @@ export const initialFavorites = [
|
||||
avatarType: 'squared' as AvatarType,
|
||||
link: 'example.com',
|
||||
recordId: '1',
|
||||
workspaceMemberId: '1',
|
||||
favoriteFolderId: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
export const sortedFavorites = [
|
||||
{
|
||||
id: '1',
|
||||
recordId: '2',
|
||||
recordId: '1',
|
||||
position: 0,
|
||||
avatarType: 'squared',
|
||||
avatarUrl: undefined,
|
||||
labelIdentifier: 'ABC Corp',
|
||||
link: '/object/company/2',
|
||||
avatarType: 'rounded',
|
||||
avatarUrl: '',
|
||||
labelIdentifier: ' ',
|
||||
link: '/object/person/1',
|
||||
objectNameSingular: 'person',
|
||||
workspaceMemberId: '1',
|
||||
favoriteFolderId: undefined,
|
||||
__typename: 'Favorite',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
recordId: '4',
|
||||
recordId: '3',
|
||||
position: 1,
|
||||
avatarType: 'squared',
|
||||
avatarUrl: undefined,
|
||||
labelIdentifier: 'Company Test',
|
||||
link: '/object/company/4',
|
||||
avatarType: 'rounded',
|
||||
avatarUrl: '',
|
||||
labelIdentifier: ' ',
|
||||
link: '/object/person/3',
|
||||
objectNameSingular: 'person',
|
||||
workspaceMemberId: '1',
|
||||
favoriteFolderId: undefined,
|
||||
__typename: 'Favorite',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
@ -72,9 +91,12 @@ export const sortedFavorites = [
|
||||
key: '8f3b2121-f194-4ba4-9fbf-2d5a37126806',
|
||||
labelIdentifier: 'favoriteLabel',
|
||||
avatarUrl: 'example.com',
|
||||
avatarType: 'squared',
|
||||
link: 'example.com',
|
||||
recordId: '1',
|
||||
avatarType: 'squared',
|
||||
favoriteFolderId: undefined,
|
||||
workspaceMemberId: '1',
|
||||
__typename: 'Favorite',
|
||||
},
|
||||
];
|
||||
|
||||
@ -84,288 +106,301 @@ export const mocks = [
|
||||
query: gql`
|
||||
mutation CreateOneFavorite($input: FavoriteCreateInput!) {
|
||||
createFavorite(data: $input) {
|
||||
__typename
|
||||
company {
|
||||
__typename
|
||||
accountOwnerId
|
||||
address {
|
||||
addressStreet1
|
||||
addressStreet2
|
||||
addressCity
|
||||
addressState
|
||||
addressCountry
|
||||
addressPostcode
|
||||
addressLat
|
||||
addressLng
|
||||
}
|
||||
annualRecurringRevenue {
|
||||
amountMicros
|
||||
currencyCode
|
||||
}
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
__typename
|
||||
company {
|
||||
__typename
|
||||
accountOwnerId
|
||||
address {
|
||||
addressStreet1
|
||||
addressStreet2
|
||||
addressCity
|
||||
addressState
|
||||
addressCountry
|
||||
addressPostcode
|
||||
addressLat
|
||||
addressLng
|
||||
}
|
||||
annualRecurringRevenue {
|
||||
amountMicros
|
||||
currencyCode
|
||||
}
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
deletedAt
|
||||
domainName {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
employees
|
||||
id
|
||||
idealCustomerProfile
|
||||
introVideo {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
linkedinLink {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
name
|
||||
position
|
||||
tagline
|
||||
updatedAt
|
||||
visaSponsorship
|
||||
workPolicy
|
||||
xLink {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
}
|
||||
companyId
|
||||
createdAt
|
||||
deletedAt
|
||||
favoriteFolder {
|
||||
__typename
|
||||
createdAt
|
||||
deletedAt
|
||||
id
|
||||
name
|
||||
position
|
||||
updatedAt
|
||||
}
|
||||
favoriteFolderId
|
||||
id
|
||||
note {
|
||||
__typename
|
||||
body
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
deletedAt
|
||||
id
|
||||
position
|
||||
title
|
||||
updatedAt
|
||||
}
|
||||
noteId
|
||||
opportunity {
|
||||
__typename
|
||||
amount {
|
||||
amountMicros
|
||||
currencyCode
|
||||
}
|
||||
closeDate
|
||||
companyId
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
deletedAt
|
||||
id
|
||||
name
|
||||
pointOfContactId
|
||||
position
|
||||
stage
|
||||
updatedAt
|
||||
}
|
||||
opportunityId
|
||||
person {
|
||||
__typename
|
||||
avatarUrl
|
||||
city
|
||||
companyId
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
deletedAt
|
||||
emails {
|
||||
primaryEmail
|
||||
additionalEmails
|
||||
}
|
||||
id
|
||||
intro
|
||||
jobTitle
|
||||
linkedinLink {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
name {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
performanceRating
|
||||
phones {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
additionalPhones
|
||||
}
|
||||
position
|
||||
updatedAt
|
||||
whatsapp {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
additionalPhones
|
||||
}
|
||||
workPreference
|
||||
xLink {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
}
|
||||
personId
|
||||
position
|
||||
rocket {
|
||||
__typename
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
deletedAt
|
||||
id
|
||||
name
|
||||
position
|
||||
updatedAt
|
||||
}
|
||||
rocketId
|
||||
task {
|
||||
__typename
|
||||
assigneeId
|
||||
body
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
deletedAt
|
||||
dueAt
|
||||
id
|
||||
position
|
||||
status
|
||||
title
|
||||
updatedAt
|
||||
}
|
||||
taskId
|
||||
updatedAt
|
||||
view {
|
||||
__typename
|
||||
createdAt
|
||||
deletedAt
|
||||
icon
|
||||
id
|
||||
isCompact
|
||||
kanbanFieldMetadataId
|
||||
key
|
||||
name
|
||||
objectMetadataId
|
||||
position
|
||||
type
|
||||
updatedAt
|
||||
}
|
||||
viewId
|
||||
workflow {
|
||||
__typename
|
||||
createdAt
|
||||
deletedAt
|
||||
id
|
||||
lastPublishedVersionId
|
||||
name
|
||||
position
|
||||
statuses
|
||||
updatedAt
|
||||
}
|
||||
workflowId
|
||||
workflowRun {
|
||||
__typename
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
deletedAt
|
||||
endedAt
|
||||
id
|
||||
name
|
||||
output
|
||||
position
|
||||
startedAt
|
||||
status
|
||||
updatedAt
|
||||
workflowId
|
||||
workflowVersionId
|
||||
}
|
||||
workflowRunId
|
||||
workflowVersion {
|
||||
__typename
|
||||
createdAt
|
||||
deletedAt
|
||||
id
|
||||
name
|
||||
position
|
||||
status
|
||||
steps
|
||||
trigger
|
||||
updatedAt
|
||||
workflowId
|
||||
}
|
||||
workflowVersionId
|
||||
workspaceMember {
|
||||
__typename
|
||||
avatarUrl
|
||||
colorScheme
|
||||
createdAt
|
||||
dateFormat
|
||||
deletedAt
|
||||
id
|
||||
locale
|
||||
name {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
timeFormat
|
||||
timeZone
|
||||
updatedAt
|
||||
userEmail
|
||||
userId
|
||||
}
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
deletedAt
|
||||
domainName {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
employees
|
||||
id
|
||||
idealCustomerProfile
|
||||
introVideo {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
linkedinLink {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
name
|
||||
position
|
||||
tagline
|
||||
updatedAt
|
||||
visaSponsorship
|
||||
workPolicy
|
||||
xLink {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
}
|
||||
companyId
|
||||
createdAt
|
||||
deletedAt
|
||||
id
|
||||
note {
|
||||
__typename
|
||||
body
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
deletedAt
|
||||
id
|
||||
position
|
||||
title
|
||||
updatedAt
|
||||
}
|
||||
noteId
|
||||
opportunity {
|
||||
__typename
|
||||
amount {
|
||||
amountMicros
|
||||
currencyCode
|
||||
}
|
||||
closeDate
|
||||
companyId
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
deletedAt
|
||||
id
|
||||
name
|
||||
pointOfContactId
|
||||
position
|
||||
stage
|
||||
updatedAt
|
||||
}
|
||||
opportunityId
|
||||
person {
|
||||
__typename
|
||||
avatarUrl
|
||||
city
|
||||
companyId
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
deletedAt
|
||||
emails {
|
||||
primaryEmail
|
||||
additionalEmails
|
||||
}
|
||||
id
|
||||
intro
|
||||
jobTitle
|
||||
linkedinLink {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
name {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
performanceRating
|
||||
phones {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
additionalPhones
|
||||
}
|
||||
position
|
||||
updatedAt
|
||||
whatsapp {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
additionalPhones
|
||||
}
|
||||
workPreference
|
||||
xLink {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
}
|
||||
personId
|
||||
position
|
||||
rocket {
|
||||
__typename
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
deletedAt
|
||||
id
|
||||
name
|
||||
position
|
||||
updatedAt
|
||||
}
|
||||
rocketId
|
||||
task {
|
||||
__typename
|
||||
assigneeId
|
||||
body
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
deletedAt
|
||||
dueAt
|
||||
id
|
||||
position
|
||||
status
|
||||
title
|
||||
updatedAt
|
||||
}
|
||||
taskId
|
||||
updatedAt
|
||||
view {
|
||||
__typename
|
||||
createdAt
|
||||
deletedAt
|
||||
icon
|
||||
id
|
||||
isCompact
|
||||
kanbanFieldMetadataId
|
||||
key
|
||||
name
|
||||
objectMetadataId
|
||||
position
|
||||
type
|
||||
updatedAt
|
||||
}
|
||||
viewId
|
||||
workflow {
|
||||
__typename
|
||||
createdAt
|
||||
deletedAt
|
||||
id
|
||||
lastPublishedVersionId
|
||||
name
|
||||
position
|
||||
statuses
|
||||
updatedAt
|
||||
}
|
||||
workflowId
|
||||
workflowRun {
|
||||
__typename
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
deletedAt
|
||||
endedAt
|
||||
id
|
||||
name
|
||||
output
|
||||
position
|
||||
startedAt
|
||||
status
|
||||
updatedAt
|
||||
workflowId
|
||||
workflowVersionId
|
||||
}
|
||||
workflowRunId
|
||||
workflowVersion {
|
||||
__typename
|
||||
createdAt
|
||||
deletedAt
|
||||
id
|
||||
name
|
||||
position
|
||||
status
|
||||
steps
|
||||
trigger
|
||||
updatedAt
|
||||
workflowId
|
||||
}
|
||||
workflowVersionId
|
||||
workspaceMember {
|
||||
__typename
|
||||
avatarUrl
|
||||
colorScheme
|
||||
createdAt
|
||||
dateFormat
|
||||
deletedAt
|
||||
id
|
||||
locale
|
||||
name {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
timeFormat
|
||||
timeZone
|
||||
updatedAt
|
||||
userEmail
|
||||
userId
|
||||
}
|
||||
workspaceMemberId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
id: mockId,
|
||||
personId: favoriteTargetObjectId,
|
||||
position: 4,
|
||||
position: 3,
|
||||
workspaceMemberId: '1',
|
||||
favoriteFolderId: undefined,
|
||||
id: mockId,
|
||||
},
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
createFavorite: {
|
||||
__typename: 'Favorite',
|
||||
id: favoriteId,
|
||||
position: 1,
|
||||
},
|
||||
},
|
||||
})),
|
||||
@ -386,7 +421,9 @@ export const mocks = [
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
deleteFavorite: {
|
||||
__typename: 'Favorite',
|
||||
id: favoriteId,
|
||||
deletedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
})),
|
||||
@ -457,6 +494,16 @@ export const mocks = [
|
||||
companyId
|
||||
createdAt
|
||||
deletedAt
|
||||
favoriteFolder {
|
||||
__typename
|
||||
createdAt
|
||||
deletedAt
|
||||
id
|
||||
name
|
||||
position
|
||||
updatedAt
|
||||
}
|
||||
favoriteFolderId
|
||||
id
|
||||
note {
|
||||
__typename
|
||||
@ -678,7 +725,9 @@ export const mocks = [
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
updateFavorite: {
|
||||
__typename: 'Favorite',
|
||||
id: favoriteId,
|
||||
position: 2,
|
||||
},
|
||||
},
|
||||
})),
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import {
|
||||
favoriteTargetObjectRecord,
|
||||
initialFavorites,
|
||||
mockId,
|
||||
mockWorkspaceMember,
|
||||
mocks,
|
||||
} from '../__mocks__/useFavorites';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: () => mockId,
|
||||
}));
|
||||
|
||||
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
||||
useFindManyRecords: () => ({ records: initialFavorites }),
|
||||
}));
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: mocks,
|
||||
});
|
||||
|
||||
describe('useCreateFavorite', () => {
|
||||
it('should create favorite successfully', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
setCurrentWorkspaceMember(mockWorkspaceMember);
|
||||
|
||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
setMetadataItems(generatedMockObjectMetadataItems);
|
||||
|
||||
return useCreateFavorite();
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
result.current(favoriteTargetObjectRecord, CoreObjectNameSingular.Person);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[0].result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,47 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import {
|
||||
favoriteId,
|
||||
initialFavorites,
|
||||
mockWorkspaceMember,
|
||||
mocks,
|
||||
} from '../__mocks__/useFavorites';
|
||||
|
||||
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
||||
useFindManyRecords: () => ({ records: initialFavorites }),
|
||||
}));
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: mocks,
|
||||
});
|
||||
|
||||
describe('useDeleteFavorite', () => {
|
||||
it('should delete favorite successfully', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
setCurrentWorkspaceMember(mockWorkspaceMember);
|
||||
|
||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
setMetadataItems(generatedMockObjectMetadataItems);
|
||||
|
||||
return useDeleteFavorite();
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
result.current(favoriteId);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[1].result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,29 +1,18 @@
|
||||
import { DropResult, ResponderProvided } from '@hello-pangea/dnd';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
|
||||
import { act } from 'react';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import {
|
||||
favoriteId,
|
||||
favoriteTargetObjectRecord,
|
||||
initialFavorites,
|
||||
mockId,
|
||||
mocks,
|
||||
mockWorkspaceMember,
|
||||
sortedFavorites,
|
||||
} from '../__mocks__/useFavorites';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn(() => mockId),
|
||||
}));
|
||||
|
||||
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
||||
useFindManyRecords: () => ({ records: initialFavorites }),
|
||||
}));
|
||||
@ -33,7 +22,7 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
});
|
||||
|
||||
describe('useFavorites', () => {
|
||||
it('should fetch favorites successfully', async () => {
|
||||
it('should fetch and sort favorites successfully', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
@ -46,108 +35,9 @@ describe('useFavorites', () => {
|
||||
|
||||
return useFavorites();
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.favorites).toEqual(sortedFavorites);
|
||||
});
|
||||
|
||||
it('should createOneFavorite successfully', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
setCurrentWorkspaceMember(mockWorkspaceMember);
|
||||
|
||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
setMetadataItems(generatedMockObjectMetadataItems);
|
||||
|
||||
return useFavorites();
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
result.current.createFavorite(
|
||||
favoriteTargetObjectRecord,
|
||||
CoreObjectNameSingular.Person,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[0].result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should deleteOneRecord successfully', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
setCurrentWorkspaceMember(mockWorkspaceMember);
|
||||
|
||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
setMetadataItems(generatedMockObjectMetadataItems);
|
||||
|
||||
return useFavorites();
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
result.current.deleteFavorite(favoriteId);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[1].result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle reordering favorites successfully', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
setCurrentWorkspaceMember(mockWorkspaceMember);
|
||||
|
||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
setMetadataItems(generatedMockObjectMetadataItems);
|
||||
|
||||
return useFavorites();
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
const dragAndDropResult: DropResult = {
|
||||
source: { index: 0, droppableId: 'droppableId' },
|
||||
destination: { index: 2, droppableId: 'droppableId' },
|
||||
combine: null,
|
||||
mode: 'FLUID',
|
||||
draggableId: 'draggableId',
|
||||
type: 'type',
|
||||
reason: 'DROP',
|
||||
};
|
||||
|
||||
const responderProvided: ResponderProvided = {
|
||||
announce: () => {},
|
||||
};
|
||||
|
||||
result.current.handleReorderFavorite(
|
||||
dragAndDropResult,
|
||||
responderProvided,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[2].result).toHaveBeenCalled();
|
||||
});
|
||||
expect(result.current).toEqual(sortedFavorites);
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
import { DropResult, ResponderProvided } from '@hello-pangea/dnd';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useReorderFavorite } from '@/favorites/hooks/useReorderFavorite';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import {
|
||||
initialFavorites,
|
||||
mockWorkspaceMember,
|
||||
mocks,
|
||||
} from '../__mocks__/useFavorites';
|
||||
|
||||
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
||||
useFindManyRecords: () => ({ records: initialFavorites }),
|
||||
}));
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: mocks,
|
||||
});
|
||||
|
||||
describe('useReorderFavorite', () => {
|
||||
it('should handle reordering favorites successfully', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
setCurrentWorkspaceMember(mockWorkspaceMember);
|
||||
|
||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
setMetadataItems(generatedMockObjectMetadataItems);
|
||||
|
||||
return useReorderFavorite();
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
const dragAndDropResult: DropResult = {
|
||||
source: { index: 0, droppableId: 'droppableId' },
|
||||
destination: { index: 2, droppableId: 'droppableId' },
|
||||
combine: null,
|
||||
mode: 'FLUID',
|
||||
draggableId: '1',
|
||||
type: 'type',
|
||||
reason: 'DROP',
|
||||
};
|
||||
|
||||
const responderProvided: ResponderProvided = {
|
||||
announce: () => {},
|
||||
};
|
||||
|
||||
result.current(dragAndDropResult, responderProvided);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[2].result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,38 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
|
||||
export const useCreateFavorite = () => {
|
||||
const { favorites, currentWorkspaceMemberId } = usePrefetchedFavoritesData();
|
||||
const { createOneRecord: createOneFavorite } = useCreateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
|
||||
const createFavorite = (
|
||||
targetRecord: ObjectRecord,
|
||||
targetObjectNameSingular: string,
|
||||
favoriteFolderId?: string,
|
||||
) => {
|
||||
const relevantFavorites = favoriteFolderId
|
||||
? favorites.filter((fav) => fav.favoriteFolderId === favoriteFolderId)
|
||||
: favorites.filter(
|
||||
(fav) => !fav.favoriteFolderId && fav.workspaceMemberId,
|
||||
);
|
||||
|
||||
const maxPosition = Math.max(
|
||||
...relevantFavorites.map((fav) => fav.position),
|
||||
0,
|
||||
);
|
||||
|
||||
createOneFavorite({
|
||||
[targetObjectNameSingular]: targetRecord,
|
||||
[`${targetObjectNameSingular}Id`]: targetRecord.id,
|
||||
position: maxPosition + 1,
|
||||
workspaceMemberId: currentWorkspaceMemberId,
|
||||
favoriteFolderId,
|
||||
});
|
||||
};
|
||||
|
||||
return createFavorite;
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData';
|
||||
|
||||
export const useCreateFavoriteFolder = () => {
|
||||
const { createOneRecord: createFavoriteFolder } = useCreateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
|
||||
});
|
||||
|
||||
const { currentWorkspaceMemberId } = usePrefetchedFavoritesData();
|
||||
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
|
||||
|
||||
const createNewFavoriteFolder = async (name: string): Promise<void> => {
|
||||
if (!name || !currentWorkspaceMemberId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxPosition = Math.max(
|
||||
...favoriteFolders.map((folder) => folder.position),
|
||||
0,
|
||||
);
|
||||
|
||||
await createFavoriteFolder({
|
||||
workspaceMemberId: currentWorkspaceMemberId,
|
||||
name,
|
||||
position: maxPosition + 1,
|
||||
});
|
||||
};
|
||||
|
||||
return createNewFavoriteFolder;
|
||||
};
|
||||
@ -0,0 +1,14 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
|
||||
export const useDeleteFavorite = () => {
|
||||
const { deleteOneRecord } = useDeleteOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
|
||||
const deleteFavorite = (favoriteId: string) => {
|
||||
deleteOneRecord(favoriteId);
|
||||
};
|
||||
|
||||
return deleteFavorite;
|
||||
};
|
||||
@ -0,0 +1,26 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
|
||||
export const useDeleteFavoriteFolder = () => {
|
||||
const { deleteOneRecord } = useDeleteOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
|
||||
});
|
||||
const { upsertFavorites, favorites, workspaceFavorites } =
|
||||
usePrefetchedFavoritesData();
|
||||
|
||||
const deleteFavoriteFolder = async (folderId: string): Promise<void> => {
|
||||
await deleteOneRecord(folderId);
|
||||
|
||||
const updatedFavorites = [
|
||||
...favorites.filter((favorite) => favorite.favoriteFolderId !== folderId),
|
||||
...workspaceFavorites,
|
||||
];
|
||||
|
||||
upsertFavorites(updatedFavorites);
|
||||
};
|
||||
|
||||
return {
|
||||
deleteFavoriteFolder,
|
||||
};
|
||||
};
|
||||
@ -1,163 +1,56 @@
|
||||
import { OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { Favorite } from '@/favorites/types/Favorite';
|
||||
import { sortFavorites } from '@/favorites/utils/sortFavorites';
|
||||
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||
import { View } from '@/views/types/View';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
|
||||
export const useFavorites = () => {
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const { favorites } = usePrefetchedFavoritesData();
|
||||
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const { objectMetadataItem: favoriteObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
|
||||
const { deleteOneRecord } = useDeleteOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
|
||||
const { updateOneRecord: updateOneFavorite } = useUpdateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
|
||||
const { createOneRecord: createOneFavorite } = useCreateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
|
||||
const { records: favorites } = usePrefetchedData<Favorite>(
|
||||
PrefetchKey.AllFavorites,
|
||||
{
|
||||
workspaceMemberId: {
|
||||
eq: currentWorkspaceMember?.id ?? '',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { records: workspaceFavorites } = usePrefetchedData<Favorite>(
|
||||
PrefetchKey.AllFavorites,
|
||||
{
|
||||
workspaceMemberId: {
|
||||
eq: undefined,
|
||||
},
|
||||
},
|
||||
);
|
||||
const getObjectRecordIdentifierByNameSingular =
|
||||
useGetObjectRecordIdentifierByNameSingular();
|
||||
|
||||
const favoriteRelationFieldMetadataItems = useMemo(
|
||||
() =>
|
||||
favoriteObjectMetadataItem.fields.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||
fieldMetadataItem.name !== 'workspaceMember',
|
||||
fieldMetadataItem.name !== 'workspaceMember' &&
|
||||
fieldMetadataItem.name !== 'favoriteFolder',
|
||||
),
|
||||
[favoriteObjectMetadataItem.fields],
|
||||
);
|
||||
|
||||
const getObjectRecordIdentifierByNameSingular =
|
||||
useGetObjectRecordIdentifierByNameSingular();
|
||||
|
||||
const favoritesSorted = useMemo(() => {
|
||||
return sortFavorites(
|
||||
const sortedFavorites = useMemo(
|
||||
() =>
|
||||
sortFavorites(
|
||||
favorites,
|
||||
favoriteRelationFieldMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
true,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
),
|
||||
[
|
||||
favorites,
|
||||
favoriteRelationFieldMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
true,
|
||||
);
|
||||
}, [
|
||||
favoriteRelationFieldMetadataItems,
|
||||
favorites,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
]);
|
||||
views,
|
||||
objectMetadataItems,
|
||||
],
|
||||
);
|
||||
|
||||
const workspaceFavoritesSorted = useMemo(() => {
|
||||
return sortFavorites(
|
||||
workspaceFavorites.filter((favorite) => favorite.viewId),
|
||||
favoriteRelationFieldMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
false,
|
||||
);
|
||||
}, [
|
||||
favoriteRelationFieldMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
workspaceFavorites,
|
||||
]);
|
||||
|
||||
const createFavorite = (
|
||||
targetRecord: Record<string, any>,
|
||||
targetObjectNameSingular: string,
|
||||
) => {
|
||||
createOneFavorite({
|
||||
[targetObjectNameSingular]: targetRecord,
|
||||
[`${targetObjectNameSingular}Id`]: targetRecord.id,
|
||||
position: favorites.length + 1,
|
||||
workspaceMemberId: currentWorkspaceMember?.id,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteFavorite = (favoriteId: string) => {
|
||||
deleteOneRecord(favoriteId);
|
||||
};
|
||||
|
||||
const computeNewPosition = (destIndex: number, sourceIndex: number) => {
|
||||
const moveToFirstPosition = destIndex === 0;
|
||||
const moveToLastPosition = destIndex === favoritesSorted.length - 1;
|
||||
const moveAfterSource = destIndex > sourceIndex;
|
||||
|
||||
if (moveToFirstPosition) {
|
||||
return favoritesSorted[0].position / 2;
|
||||
} else if (moveToLastPosition) {
|
||||
return favoritesSorted[destIndex - 1].position + 1;
|
||||
} else if (moveAfterSource) {
|
||||
return (
|
||||
(favoritesSorted[destIndex + 1].position +
|
||||
favoritesSorted[destIndex].position) /
|
||||
2
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
favoritesSorted[destIndex].position -
|
||||
(favoritesSorted[destIndex].position -
|
||||
favoritesSorted[destIndex - 1].position) /
|
||||
2
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReorderFavorite: OnDragEndResponder = (result) => {
|
||||
if (!result.destination || !favoritesSorted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newPosition = computeNewPosition(
|
||||
result.destination.index,
|
||||
result.source.index,
|
||||
);
|
||||
|
||||
const updatedFavorite = favoritesSorted[result.source.index];
|
||||
|
||||
updateOneFavorite({
|
||||
idToUpdate: updatedFavorite.id,
|
||||
updateOneRecordInput: {
|
||||
position: newPosition,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
favorites: favoritesSorted,
|
||||
workspaceFavorites: workspaceFavoritesSorted,
|
||||
createFavorite,
|
||||
handleReorderFavorite,
|
||||
deleteFavorite,
|
||||
};
|
||||
return sortedFavorites;
|
||||
};
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import { sortFavorites } from '@/favorites/utils/sortFavorites';
|
||||
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||
import { View } from '@/views/types/View';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData';
|
||||
|
||||
export const useFavoritesByFolder = () => {
|
||||
const { favorites } = usePrefetchedFavoritesData();
|
||||
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
|
||||
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const getObjectRecordIdentifierByNameSingular =
|
||||
useGetObjectRecordIdentifierByNameSingular();
|
||||
|
||||
const { objectMetadataItem: favoriteObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
|
||||
const favoriteRelationFields = useMemo(
|
||||
() =>
|
||||
favoriteObjectMetadataItem.fields.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||
fieldMetadataItem.name !== 'workspaceMember' &&
|
||||
fieldMetadataItem.name !== 'favoriteFolder',
|
||||
),
|
||||
[favoriteObjectMetadataItem.fields],
|
||||
);
|
||||
|
||||
const favoritesByFolder = useMemo(() => {
|
||||
return favoriteFolders.map((folder) => ({
|
||||
folderId: folder.id,
|
||||
folderName: folder.name,
|
||||
favorites: sortFavorites(
|
||||
favorites.filter((favorite) => favorite.favoriteFolderId === folder.id),
|
||||
favoriteRelationFields,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
true,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
),
|
||||
}));
|
||||
}, [
|
||||
favoriteFolders,
|
||||
favorites,
|
||||
favoriteRelationFields,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
]);
|
||||
|
||||
return favoritesByFolder;
|
||||
};
|
||||
@ -0,0 +1,46 @@
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { Favorite } from '@/favorites/types/Favorite';
|
||||
import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery';
|
||||
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
type PrefetchedFavoritesData = {
|
||||
favorites: Favorite[];
|
||||
workspaceFavorites: Favorite[];
|
||||
upsertFavorites: (records: Favorite[]) => void;
|
||||
currentWorkspaceMemberId: string | undefined;
|
||||
};
|
||||
|
||||
export const usePrefetchedFavoritesData = (): PrefetchedFavoritesData => {
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
const currentWorkspaceMemberId = currentWorkspaceMember?.id;
|
||||
const { records: _favorites } = usePrefetchedData<Favorite>(
|
||||
PrefetchKey.AllFavorites,
|
||||
{
|
||||
workspaceMemberId: {
|
||||
eq: currentWorkspaceMemberId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const favorites = _favorites.filter(
|
||||
(favorite) => favorite.workspaceMemberId === currentWorkspaceMemberId,
|
||||
);
|
||||
|
||||
const workspaceFavorites = _favorites.filter(
|
||||
(favorite) => favorite.workspaceMemberId === null,
|
||||
);
|
||||
|
||||
const { upsertRecordsInCache: upsertFavorites } =
|
||||
usePrefetchRunQuery<Favorite>({
|
||||
prefetchKey: PrefetchKey.AllFavorites,
|
||||
});
|
||||
|
||||
return {
|
||||
favorites,
|
||||
workspaceFavorites,
|
||||
upsertFavorites,
|
||||
currentWorkspaceMemberId,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,38 @@
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
||||
import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery';
|
||||
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
type PrefetchedFavoritesFoldersData = {
|
||||
favoriteFolders: FavoriteFolder[];
|
||||
upsertFavoriteFolders: (records: FavoriteFolder[]) => void;
|
||||
currentWorkspaceMemberId: string | undefined;
|
||||
};
|
||||
|
||||
export const usePrefetchedFavoritesFoldersData =
|
||||
(): PrefetchedFavoritesFoldersData => {
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
const currentWorkspaceMemberId = currentWorkspaceMember?.id;
|
||||
|
||||
const { records: favoriteFolders } = usePrefetchedData<FavoriteFolder>(
|
||||
PrefetchKey.AllFavoritesFolders,
|
||||
{
|
||||
workspaceMemberId: {
|
||||
eq: currentWorkspaceMemberId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { upsertRecordsInCache: upsertFavoriteFolders } =
|
||||
usePrefetchRunQuery<FavoriteFolder>({
|
||||
prefetchKey: PrefetchKey.AllFavoritesFolders,
|
||||
});
|
||||
|
||||
return {
|
||||
favoriteFolders,
|
||||
upsertFavoriteFolders,
|
||||
currentWorkspaceMemberId,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
|
||||
export const useRenameFavoriteFolder = () => {
|
||||
const { updateOneRecord: updateFavoriteFolder } = useUpdateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
|
||||
});
|
||||
|
||||
const renameFavoriteFolder = async (
|
||||
folderId: string,
|
||||
newName: string,
|
||||
): Promise<void> => {
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateFavoriteFolder({
|
||||
idToUpdate: folderId,
|
||||
updateOneRecordInput: {
|
||||
name: newName,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
renameFavoriteFolder,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,41 @@
|
||||
import { useSortedFavorites } from '@/favorites/hooks/useSortedFavorites';
|
||||
import { calculateNewPosition } from '@/favorites/utils/calculateNewPosition';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
|
||||
export const useReorderFavorite = () => {
|
||||
const { favorites } = usePrefetchedFavoritesData();
|
||||
const { favoritesSorted } = useSortedFavorites();
|
||||
const { updateOneRecord: updateOneFavorite } = useUpdateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
|
||||
const reorderFavorite: OnDragEndResponder = (result) => {
|
||||
if (!result.destination) return;
|
||||
|
||||
const draggedFavoriteId = result.draggableId;
|
||||
const draggedFavorite = favorites.find((f) => f.id === draggedFavoriteId);
|
||||
|
||||
if (!draggedFavorite) return;
|
||||
|
||||
const inSameFolderFavorites = favoritesSorted.filter(
|
||||
(fav) => fav.favoriteFolderId === draggedFavorite.favoriteFolderId,
|
||||
);
|
||||
if (!inSameFolderFavorites.length) return;
|
||||
|
||||
const newPosition = calculateNewPosition({
|
||||
destinationIndex: result.destination.index,
|
||||
sourceIndex: result.source.index,
|
||||
items: inSameFolderFavorites,
|
||||
});
|
||||
|
||||
updateOneFavorite({
|
||||
idToUpdate: draggedFavoriteId,
|
||||
updateOneRecordInput: { position: newPosition },
|
||||
});
|
||||
};
|
||||
|
||||
return reorderFavorite;
|
||||
};
|
||||
@ -0,0 +1,75 @@
|
||||
import { sortFavorites } from '@/favorites/utils/sortFavorites';
|
||||
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||
import { View } from '@/views/types/View';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
|
||||
export const useSortedFavorites = () => {
|
||||
const { favorites, workspaceFavorites } = usePrefetchedFavoritesData();
|
||||
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const { objectMetadataItem: favoriteObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
|
||||
const getObjectRecordIdentifierByNameSingular =
|
||||
useGetObjectRecordIdentifierByNameSingular();
|
||||
|
||||
const favoriteRelationFieldMetadataItems = useMemo(
|
||||
() =>
|
||||
favoriteObjectMetadataItem.fields.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||
fieldMetadataItem.name !== 'workspaceMember' &&
|
||||
fieldMetadataItem.name !== 'favoriteFolder',
|
||||
),
|
||||
[favoriteObjectMetadataItem.fields],
|
||||
);
|
||||
|
||||
const favoritesSorted = useMemo(() => {
|
||||
return sortFavorites(
|
||||
favorites,
|
||||
favoriteRelationFieldMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
true,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
);
|
||||
}, [
|
||||
favoriteRelationFieldMetadataItems,
|
||||
favorites,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
]);
|
||||
|
||||
const workspaceFavoritesSorted = useMemo(() => {
|
||||
return sortFavorites(
|
||||
workspaceFavorites.filter((favorite) => favorite.viewId),
|
||||
favoriteRelationFieldMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
false,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
);
|
||||
}, [
|
||||
favoriteRelationFieldMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
workspaceFavorites,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
]);
|
||||
|
||||
return {
|
||||
favoritesSorted,
|
||||
workspaceFavoritesSorted,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
import { sortFavorites } from '@/favorites/utils/sortFavorites';
|
||||
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||
import { View } from '@/views/types/View';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
|
||||
export const useWorkspaceFavorites = () => {
|
||||
const { workspaceFavorites } = usePrefetchedFavoritesData();
|
||||
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const { objectMetadataItem: favoriteObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
const getObjectRecordIdentifierByNameSingular =
|
||||
useGetObjectRecordIdentifierByNameSingular();
|
||||
|
||||
const favoriteRelationFieldMetadataItems = useMemo(
|
||||
() =>
|
||||
favoriteObjectMetadataItem.fields.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||
fieldMetadataItem.name !== 'workspaceMember' &&
|
||||
fieldMetadataItem.name !== 'favoriteFolder',
|
||||
),
|
||||
[favoriteObjectMetadataItem.fields],
|
||||
);
|
||||
|
||||
const sortedWorkspaceFavorites = useMemo(
|
||||
() =>
|
||||
sortFavorites(
|
||||
workspaceFavorites.filter((favorite) => favorite.viewId),
|
||||
favoriteRelationFieldMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
false,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
),
|
||||
[
|
||||
workspaceFavorites,
|
||||
favoriteRelationFieldMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
],
|
||||
);
|
||||
|
||||
return sortedWorkspaceFavorites;
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const activeFavoriteFolderIdState = atom<string | null>({
|
||||
key: 'activeFavoriteFolderIdState',
|
||||
default: null,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isFavoriteFolderCreatingState = atom<boolean>({
|
||||
key: 'isFavoriteFolderCreatingState',
|
||||
default: false,
|
||||
});
|
||||
@ -10,5 +10,6 @@ export type Favorite = {
|
||||
link: string;
|
||||
recordId: string;
|
||||
workspaceMemberId: string;
|
||||
favoriteFolderId?: string;
|
||||
__typename: 'Favorite';
|
||||
};
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
export type FavoriteFolder = {
|
||||
id: string;
|
||||
name: string;
|
||||
position: number;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
deletedAt?: string | null;
|
||||
__typename: 'FavoriteFolder';
|
||||
};
|
||||
@ -0,0 +1,55 @@
|
||||
import { isLocationMatchingFavorite } from '../isLocationMatchingFavorite';
|
||||
|
||||
describe('isLocationMatchingFavorite', () => {
|
||||
it('should return true if favorite link matches current path', () => {
|
||||
const currentPath = '/app/objects/people';
|
||||
const currentViewPath = '/app/objects/people?view=123';
|
||||
const favorite = {
|
||||
objectNameSingular: 'object',
|
||||
link: '/app/objects/people',
|
||||
};
|
||||
|
||||
expect(
|
||||
isLocationMatchingFavorite(currentPath, currentViewPath, favorite),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if favorite link matches current view path', () => {
|
||||
const currentPath = '/app/object/company/12';
|
||||
const currentViewPath = '/app/object/company/12?view=123';
|
||||
const favorite = {
|
||||
objectNameSingular: 'company',
|
||||
link: '/app/object/company/12',
|
||||
};
|
||||
|
||||
expect(
|
||||
isLocationMatchingFavorite(currentPath, currentViewPath, favorite),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if favorite link does not match current path', () => {
|
||||
const currentPath = '/app/objects/people';
|
||||
const currentViewPath = '/app/objects/people?view=123';
|
||||
const favorite = {
|
||||
objectNameSingular: 'object',
|
||||
link: '/app/objects/company',
|
||||
};
|
||||
|
||||
expect(
|
||||
isLocationMatchingFavorite(currentPath, currentViewPath, favorite),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if favorite link does not match current view path', () => {
|
||||
const currentPath = '/app/objects/companies';
|
||||
const currentViewPath = '/app/objects/companies?view=123';
|
||||
const favorite = {
|
||||
objectNameSingular: 'view',
|
||||
link: '/app/objects/companies/view=246',
|
||||
};
|
||||
|
||||
expect(
|
||||
isLocationMatchingFavorite(currentPath, currentViewPath, favorite),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,33 @@
|
||||
type CalculateNewPositionParams = {
|
||||
destinationIndex: number;
|
||||
sourceIndex: number;
|
||||
items: Array<{ position: number }>;
|
||||
};
|
||||
|
||||
export const calculateNewPosition = ({
|
||||
destinationIndex,
|
||||
sourceIndex,
|
||||
items,
|
||||
}: CalculateNewPositionParams): number => {
|
||||
if (destinationIndex === 0) {
|
||||
return items[0].position / 2;
|
||||
}
|
||||
|
||||
if (destinationIndex === items.length - 1) {
|
||||
return items[destinationIndex - 1].position + 1;
|
||||
}
|
||||
|
||||
if (destinationIndex > sourceIndex) {
|
||||
return (
|
||||
(items[destinationIndex + 1].position +
|
||||
items[destinationIndex].position) /
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
items[destinationIndex].position -
|
||||
(items[destinationIndex].position - items[destinationIndex - 1].position) /
|
||||
2
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { View } from '@/views/types/View';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
type ReturnType = {
|
||||
labelPlural: string;
|
||||
view: View | null;
|
||||
};
|
||||
|
||||
export const getObjectMetadataLabelPluralFromViewId = (
|
||||
views: View[],
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
viewId: string,
|
||||
): ReturnType => {
|
||||
const view = views.find((view) => view.id === viewId);
|
||||
|
||||
if (!view) {
|
||||
return {
|
||||
labelPlural: '',
|
||||
view: null,
|
||||
};
|
||||
}
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) => objectMetadataItem.id === view.objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
`Object metadata item not found for id ${view.objectMetadataId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { labelPlural } = objectMetadataItem;
|
||||
|
||||
return {
|
||||
labelPlural,
|
||||
view,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,12 @@
|
||||
import { ProcessedFavorite } from '@/favorites/utils/sortFavorites';
|
||||
|
||||
// Todo: we could only path the fullPath here (which is currentViewPath) and then split it in the function
|
||||
export const isLocationMatchingFavorite = (
|
||||
currentPath: string,
|
||||
currentViewPath: string,
|
||||
favorite: Pick<ProcessedFavorite, 'objectNameSingular' | 'link'>,
|
||||
) => {
|
||||
return favorite.objectNameSingular === 'view'
|
||||
? favorite.link === currentViewPath
|
||||
: favorite.link === currentPath;
|
||||
};
|
||||
@ -1,19 +1,53 @@
|
||||
import { Favorite } from '@/favorites/types/Favorite';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
||||
import { View } from '@/views/types/View';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { getObjectMetadataLabelPluralFromViewId } from './getObjectMetadataLabelPluralFromViewId';
|
||||
|
||||
export type ProcessedFavorite = Favorite & {
|
||||
Icon?: string;
|
||||
objectNameSingular?: string;
|
||||
};
|
||||
|
||||
export const sortFavorites = (
|
||||
favorites: Favorite[],
|
||||
favoriteRelationFieldMetadataItems: FieldMetadataItem[],
|
||||
getObjectRecordIdentifierByNameSingular: (
|
||||
record: any,
|
||||
record: ObjectRecord,
|
||||
objectNameSingular: string,
|
||||
) => ObjectRecordIdentifier,
|
||||
hasLinkToShowPage: boolean,
|
||||
views: View[],
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
) => {
|
||||
return favorites
|
||||
.map((favorite) => {
|
||||
if (isDefined(favorite.viewId) && isDefined(favorite.workspaceMemberId)) {
|
||||
const { labelPlural, view } = getObjectMetadataLabelPluralFromViewId(
|
||||
views,
|
||||
objectMetadataItems,
|
||||
favorite.viewId,
|
||||
);
|
||||
|
||||
return {
|
||||
__typename: 'Favorite',
|
||||
id: favorite.id,
|
||||
recordId: view?.id,
|
||||
position: favorite.position,
|
||||
avatarType: 'icon',
|
||||
avatarUrl: '',
|
||||
labelIdentifier: view?.name,
|
||||
link: `/objects/${labelPlural.toLocaleLowerCase()}${favorite.viewId ? `?view=${favorite.viewId}` : ''}`,
|
||||
workspaceMemberId: favorite.workspaceMemberId,
|
||||
favoriteFolderId: favorite.favoriteFolderId,
|
||||
objectNameSingular: 'view',
|
||||
Icon: view?.icon,
|
||||
} as ProcessedFavorite;
|
||||
}
|
||||
|
||||
for (const relationField of favoriteRelationFieldMetadataItems) {
|
||||
if (isDefined(favorite[relationField.name])) {
|
||||
const relationObject = favorite[relationField.name];
|
||||
@ -29,6 +63,7 @@ export const sortFavorites = (
|
||||
);
|
||||
|
||||
return {
|
||||
__typename: 'Favorite',
|
||||
id: favorite.id,
|
||||
recordId: objectRecordIdentifier.id,
|
||||
position: favorite.position,
|
||||
@ -39,11 +74,14 @@ export const sortFavorites = (
|
||||
? objectRecordIdentifier.linkToShowPage
|
||||
: '',
|
||||
workspaceMemberId: favorite.workspaceMemberId,
|
||||
} as Favorite;
|
||||
favoriteFolderId: favorite.favoriteFolderId,
|
||||
objectNameSingular: relationObjectNameSingular,
|
||||
} as ProcessedFavorite;
|
||||
}
|
||||
}
|
||||
|
||||
return favorite;
|
||||
return {
|
||||
...favorite,
|
||||
} as ProcessedFavorite;
|
||||
})
|
||||
.sort((a, b) => a.position - b.position);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user