mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-22 03:17:40 +03:00
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:
parent
5115022355
commit
0125d58ba8
@ -4,6 +4,7 @@ import { contextStoreFiltersComponentState } from '@/context-store/states/contex
|
|||||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||||
|
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
|
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
|
||||||
@ -36,7 +37,8 @@ export const DeleteRecordsActionEffect = ({
|
|||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { favorites, deleteFavorite } = useFavorites();
|
const favorites = useFavorites();
|
||||||
|
const deleteFavorite = useDeleteFavorite();
|
||||||
|
|
||||||
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
||||||
contextStoreNumberOfSelectedRecordsComponentState,
|
contextStoreNumberOfSelectedRecordsComponentState,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
|
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
|
||||||
|
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
@ -21,7 +23,11 @@ export const ManageFavoritesActionEffect = ({
|
|||||||
contextStoreTargetedRecordsRuleComponentState,
|
contextStoreTargetedRecordsRuleComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { favorites, createFavorite, deleteFavorite } = useFavorites();
|
const favorites = useFavorites();
|
||||||
|
|
||||||
|
const createFavorite = useCreateFavorite();
|
||||||
|
|
||||||
|
const deleteFavorite = useDeleteFavorite();
|
||||||
|
|
||||||
const selectedRecordId =
|
const selectedRecordId =
|
||||||
contextStoreTargetedRecordsRule.mode === 'selection'
|
contextStoreTargetedRecordsRule.mode === 'selection'
|
||||||
|
@ -11,16 +11,16 @@ import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBa
|
|||||||
|
|
||||||
export const RecordShowActionMenu = ({
|
export const RecordShowActionMenu = ({
|
||||||
isFavorite,
|
isFavorite,
|
||||||
handleFavoriteButtonClick,
|
|
||||||
record,
|
record,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
handleFavoriteButtonClick,
|
||||||
}: {
|
}: {
|
||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
handleFavoriteButtonClick: () => void;
|
|
||||||
record: ObjectRecord | undefined;
|
record: ObjectRecord | undefined;
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
|
handleFavoriteButtonClick: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
|
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
|
||||||
contextStoreCurrentObjectMetadataIdComponentState,
|
contextStoreCurrentObjectMetadataIdComponentState,
|
||||||
@ -40,10 +40,10 @@ export const RecordShowActionMenu = ({
|
|||||||
<RecordShowPageBaseHeader
|
<RecordShowPageBaseHeader
|
||||||
{...{
|
{...{
|
||||||
isFavorite,
|
isFavorite,
|
||||||
handleFavoriteButtonClick,
|
|
||||||
record,
|
record,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
handleFavoriteButtonClick,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ActionMenuConfirmationModals />
|
<ActionMenuConfirmationModals />
|
||||||
|
@ -84,6 +84,7 @@ const mocks: MockedResponse[] = [
|
|||||||
companyId
|
companyId
|
||||||
createdAt
|
createdAt
|
||||||
deletedAt
|
deletedAt
|
||||||
|
favoriteFolderId
|
||||||
id
|
id
|
||||||
noteId
|
noteId
|
||||||
opportunityId
|
opportunityId
|
||||||
|
@ -1,127 +1,197 @@
|
|||||||
import styled from '@emotion/styled';
|
import { FavoriteFolderNavigationDrawerItemDropdown } from '@/favorites/components/FavoriteFolderNavigationDrawerItemDropdown';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { FavoriteIcon } from '@/favorites/components/FavoriteIcon';
|
||||||
import { Avatar, isDefined } from 'twenty-ui';
|
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||||
|
import { useDeleteFavoriteFolder } from '@/favorites/hooks/useDeleteFavoriteFolder';
|
||||||
import { FavoritesSkeletonLoader } from '@/favorites/components/FavoritesSkeletonLoader';
|
import { useRenameFavoriteFolder } from '@/favorites/hooks/useRenameFavoriteFolder';
|
||||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
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 { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
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 { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||||
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||||
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
|
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';
|
type CurrentWorkspaceMemberFavoritesProps = {
|
||||||
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
folder: {
|
||||||
import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer';
|
folderId: string;
|
||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
folderName: string;
|
||||||
import { useFavorites } from '../hooks/useFavorites';
|
favorites: ProcessedFavorite[];
|
||||||
|
};
|
||||||
|
isGroup: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const StyledContainer = styled(NavigationDrawerSection)`
|
export const CurrentWorkspaceMemberFavorites = ({
|
||||||
width: 100%;
|
folder,
|
||||||
`;
|
isGroup,
|
||||||
|
}: CurrentWorkspaceMemberFavoritesProps) => {
|
||||||
|
const currentPath = useLocation().pathname;
|
||||||
|
const currentViewPath = useLocation().pathname + useLocation().search;
|
||||||
|
|
||||||
const StyledAvatar = styled(Avatar)`
|
const [isFavoriteFolderRenaming, setIsFavoriteFolderRenaming] =
|
||||||
:hover {
|
useState(false);
|
||||||
cursor: grab;
|
const [favoriteFolderName, setFavoriteFolderName] = useState(
|
||||||
}
|
folder.folderName,
|
||||||
`;
|
|
||||||
|
|
||||||
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 [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [activeFavoriteFolderId, setActiveFavoriteFolderId] = useRecoilState(
|
||||||
|
activeFavoriteFolderIdState,
|
||||||
|
);
|
||||||
|
const isOpen = activeFavoriteFolderId === folder.folderId;
|
||||||
|
|
||||||
if (
|
const handleToggle = () => {
|
||||||
!currentWorkspaceMemberFavorites ||
|
setActiveFavoriteFolderId(isOpen ? null : folder.folderId);
|
||||||
currentWorkspaceMemberFavorites.length === 0
|
};
|
||||||
)
|
|
||||||
return <></>;
|
|
||||||
|
|
||||||
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 = (
|
const deleteFavorite = useDeleteFavorite();
|
||||||
<DraggableList
|
|
||||||
onDragEnd={handleReorderFavorite}
|
|
||||||
draggableItems={
|
|
||||||
<>
|
|
||||||
{currentWorkspaceMemberFavorites.map((favorite, index) => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
labelIdentifier,
|
|
||||||
avatarUrl,
|
|
||||||
avatarType,
|
|
||||||
link,
|
|
||||||
recordId,
|
|
||||||
} = favorite;
|
|
||||||
|
|
||||||
return (
|
const favoriteFolderContentLength = folder.favorites.length;
|
||||||
<DraggableItem
|
|
||||||
key={id}
|
const handleSubmitRename = async (value: string) => {
|
||||||
draggableId={id}
|
if (value === '') return;
|
||||||
index={index}
|
await renameFavoriteFolder(folder.folderId, value);
|
||||||
itemComponent={
|
setIsFavoriteFolderRenaming(false);
|
||||||
<StyledNavigationDrawerItem
|
return true;
|
||||||
key={id}
|
};
|
||||||
label={labelIdentifier}
|
|
||||||
Icon={() => (
|
const handleCancelRename = () => {
|
||||||
<StyledAvatar
|
setFavoriteFolderName(folder.folderName);
|
||||||
placeholderColorSeed={recordId}
|
setIsFavoriteFolderRenaming(false);
|
||||||
avatarUrl={avatarUrl}
|
};
|
||||||
type={avatarType}
|
|
||||||
placeholder={labelIdentifier}
|
const handleClickOutside = async (
|
||||||
className="fav-avatar"
|
event: MouseEvent | TouchEvent,
|
||||||
/>
|
value: string,
|
||||||
)}
|
) => {
|
||||||
to={link}
|
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 (
|
return (
|
||||||
<StyledContainer>
|
<>
|
||||||
<NavigationDrawerAnimatedCollapseWrapper>
|
<NavigationDrawerItemsCollapsableContainer
|
||||||
<NavigationDrawerSectionTitle
|
key={folder.folderId}
|
||||||
label="Favorites"
|
isGroup={isGroup}
|
||||||
onClick={() => toggleNavigationSection()}
|
>
|
||||||
/>
|
{isFavoriteFolderRenaming ? (
|
||||||
</NavigationDrawerAnimatedCollapseWrapper>
|
<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 && (
|
{isOpen && (
|
||||||
<ScrollWrapper contextProviderName="navigationDrawer">
|
<DraggableList
|
||||||
<NavigationDrawerItemsCollapsedContainer isGroup={isGroup}>
|
onDragEnd={handleReorderFavorite}
|
||||||
{draggableListContent}
|
draggableItems={
|
||||||
</NavigationDrawerItemsCollapsedContainer>
|
<>
|
||||||
</ScrollWrapper>
|
{folder.favorites.map((favorite, index) => (
|
||||||
)}
|
<DraggableItem
|
||||||
</StyledContainer>
|
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';
|
const favoriteTargetObjectId = 'f2d8b9e9-7932-4065-bc09-baf12388b75d';
|
||||||
export const favoriteTargetObjectRecord = {
|
export const favoriteTargetObjectRecord = {
|
||||||
id: favoriteTargetObjectId,
|
id: favoriteTargetObjectId,
|
||||||
|
__typename: 'Person',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialFavorites = [
|
export const initialFavorites = [
|
||||||
{
|
{
|
||||||
|
__typename: 'Favorite',
|
||||||
id: '1',
|
id: '1',
|
||||||
position: 0,
|
position: 0,
|
||||||
key: mockId,
|
key: mockId,
|
||||||
@ -22,8 +24,11 @@ export const initialFavorites = [
|
|||||||
recordId: '1',
|
recordId: '1',
|
||||||
person: { id: '1', name: 'John Doe' },
|
person: { id: '1', name: 'John Doe' },
|
||||||
company: { id: '2', name: 'ABC Corp' },
|
company: { id: '2', name: 'ABC Corp' },
|
||||||
|
workspaceMemberId: '1',
|
||||||
|
favoriteFolderId: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
__typename: 'Favorite',
|
||||||
id: '2',
|
id: '2',
|
||||||
position: 1,
|
position: 1,
|
||||||
key: mockId,
|
key: mockId,
|
||||||
@ -34,8 +39,12 @@ export const initialFavorites = [
|
|||||||
recordId: '1',
|
recordId: '1',
|
||||||
person: { id: '3', name: 'Jane Doe' },
|
person: { id: '3', name: 'Jane Doe' },
|
||||||
company: { id: '4', name: 'Company Test' },
|
company: { id: '4', name: 'Company Test' },
|
||||||
|
workspaceMemberId: '1',
|
||||||
|
favoriteFolderId: undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
__typename: 'Favorite',
|
||||||
id: '3',
|
id: '3',
|
||||||
position: 2,
|
position: 2,
|
||||||
key: mockId,
|
key: mockId,
|
||||||
@ -44,27 +53,37 @@ export const initialFavorites = [
|
|||||||
avatarType: 'squared' as AvatarType,
|
avatarType: 'squared' as AvatarType,
|
||||||
link: 'example.com',
|
link: 'example.com',
|
||||||
recordId: '1',
|
recordId: '1',
|
||||||
|
workspaceMemberId: '1',
|
||||||
|
favoriteFolderId: undefined,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const sortedFavorites = [
|
export const sortedFavorites = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
recordId: '2',
|
recordId: '1',
|
||||||
position: 0,
|
position: 0,
|
||||||
avatarType: 'squared',
|
avatarType: 'rounded',
|
||||||
avatarUrl: undefined,
|
avatarUrl: '',
|
||||||
labelIdentifier: 'ABC Corp',
|
labelIdentifier: ' ',
|
||||||
link: '/object/company/2',
|
link: '/object/person/1',
|
||||||
|
objectNameSingular: 'person',
|
||||||
|
workspaceMemberId: '1',
|
||||||
|
favoriteFolderId: undefined,
|
||||||
|
__typename: 'Favorite',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
recordId: '4',
|
recordId: '3',
|
||||||
position: 1,
|
position: 1,
|
||||||
avatarType: 'squared',
|
avatarType: 'rounded',
|
||||||
avatarUrl: undefined,
|
avatarUrl: '',
|
||||||
labelIdentifier: 'Company Test',
|
labelIdentifier: ' ',
|
||||||
link: '/object/company/4',
|
link: '/object/person/3',
|
||||||
|
objectNameSingular: 'person',
|
||||||
|
workspaceMemberId: '1',
|
||||||
|
favoriteFolderId: undefined,
|
||||||
|
__typename: 'Favorite',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
@ -72,9 +91,12 @@ export const sortedFavorites = [
|
|||||||
key: '8f3b2121-f194-4ba4-9fbf-2d5a37126806',
|
key: '8f3b2121-f194-4ba4-9fbf-2d5a37126806',
|
||||||
labelIdentifier: 'favoriteLabel',
|
labelIdentifier: 'favoriteLabel',
|
||||||
avatarUrl: 'example.com',
|
avatarUrl: 'example.com',
|
||||||
avatarType: 'squared',
|
|
||||||
link: 'example.com',
|
link: 'example.com',
|
||||||
recordId: '1',
|
recordId: '1',
|
||||||
|
avatarType: 'squared',
|
||||||
|
favoriteFolderId: undefined,
|
||||||
|
workspaceMemberId: '1',
|
||||||
|
__typename: 'Favorite',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -84,288 +106,301 @@ export const mocks = [
|
|||||||
query: gql`
|
query: gql`
|
||||||
mutation CreateOneFavorite($input: FavoriteCreateInput!) {
|
mutation CreateOneFavorite($input: FavoriteCreateInput!) {
|
||||||
createFavorite(data: $input) {
|
createFavorite(data: $input) {
|
||||||
__typename
|
__typename
|
||||||
company {
|
company {
|
||||||
__typename
|
__typename
|
||||||
accountOwnerId
|
accountOwnerId
|
||||||
address {
|
address {
|
||||||
addressStreet1
|
addressStreet1
|
||||||
addressStreet2
|
addressStreet2
|
||||||
addressCity
|
addressCity
|
||||||
addressState
|
addressState
|
||||||
addressCountry
|
addressCountry
|
||||||
addressPostcode
|
addressPostcode
|
||||||
addressLat
|
addressLat
|
||||||
addressLng
|
addressLng
|
||||||
}
|
}
|
||||||
annualRecurringRevenue {
|
annualRecurringRevenue {
|
||||||
amountMicros
|
amountMicros
|
||||||
currencyCode
|
currencyCode
|
||||||
}
|
}
|
||||||
createdAt
|
createdAt
|
||||||
createdBy {
|
createdBy {
|
||||||
source
|
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
|
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: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
id: mockId,
|
|
||||||
personId: favoriteTargetObjectId,
|
personId: favoriteTargetObjectId,
|
||||||
position: 4,
|
position: 3,
|
||||||
workspaceMemberId: '1',
|
workspaceMemberId: '1',
|
||||||
|
favoriteFolderId: undefined,
|
||||||
|
id: mockId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
result: jest.fn(() => ({
|
result: jest.fn(() => ({
|
||||||
data: {
|
data: {
|
||||||
createFavorite: {
|
createFavorite: {
|
||||||
|
__typename: 'Favorite',
|
||||||
id: favoriteId,
|
id: favoriteId,
|
||||||
|
position: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
@ -386,7 +421,9 @@ export const mocks = [
|
|||||||
result: jest.fn(() => ({
|
result: jest.fn(() => ({
|
||||||
data: {
|
data: {
|
||||||
deleteFavorite: {
|
deleteFavorite: {
|
||||||
|
__typename: 'Favorite',
|
||||||
id: favoriteId,
|
id: favoriteId,
|
||||||
|
deletedAt: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
@ -457,6 +494,16 @@ export const mocks = [
|
|||||||
companyId
|
companyId
|
||||||
createdAt
|
createdAt
|
||||||
deletedAt
|
deletedAt
|
||||||
|
favoriteFolder {
|
||||||
|
__typename
|
||||||
|
createdAt
|
||||||
|
deletedAt
|
||||||
|
id
|
||||||
|
name
|
||||||
|
position
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
favoriteFolderId
|
||||||
id
|
id
|
||||||
note {
|
note {
|
||||||
__typename
|
__typename
|
||||||
@ -678,7 +725,9 @@ export const mocks = [
|
|||||||
result: jest.fn(() => ({
|
result: jest.fn(() => ({
|
||||||
data: {
|
data: {
|
||||||
updateFavorite: {
|
updateFavorite: {
|
||||||
|
__typename: 'Favorite',
|
||||||
id: favoriteId,
|
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 } from '@testing-library/react';
|
||||||
import { renderHook, waitFor } from '@testing-library/react';
|
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
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 { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||||
import {
|
import {
|
||||||
favoriteId,
|
|
||||||
favoriteTargetObjectRecord,
|
|
||||||
initialFavorites,
|
initialFavorites,
|
||||||
mockId,
|
|
||||||
mocks,
|
mocks,
|
||||||
mockWorkspaceMember,
|
mockWorkspaceMember,
|
||||||
sortedFavorites,
|
sortedFavorites,
|
||||||
} from '../__mocks__/useFavorites';
|
} from '../__mocks__/useFavorites';
|
||||||
|
|
||||||
jest.mock('uuid', () => ({
|
|
||||||
v4: jest.fn(() => mockId),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
||||||
useFindManyRecords: () => ({ records: initialFavorites }),
|
useFindManyRecords: () => ({ records: initialFavorites }),
|
||||||
}));
|
}));
|
||||||
@ -33,7 +22,7 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('useFavorites', () => {
|
describe('useFavorites', () => {
|
||||||
it('should fetch favorites successfully', async () => {
|
it('should fetch and sort favorites successfully', () => {
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => {
|
() => {
|
||||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||||
@ -46,108 +35,9 @@ describe('useFavorites', () => {
|
|||||||
|
|
||||||
return useFavorites();
|
return useFavorites();
|
||||||
},
|
},
|
||||||
{
|
{ wrapper: Wrapper },
|
||||||
wrapper: Wrapper,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.current.favorites).toEqual(sortedFavorites);
|
expect(result.current).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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 { sortFavorites } from '@/favorites/utils/sortFavorites';
|
||||||
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
|
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
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 { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
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 { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||||
|
|
||||||
export const useFavorites = () => {
|
export const useFavorites = () => {
|
||||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
const { favorites } = usePrefetchedFavoritesData();
|
||||||
|
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
|
||||||
|
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||||
const { objectMetadataItem: favoriteObjectMetadataItem } =
|
const { objectMetadataItem: favoriteObjectMetadataItem } =
|
||||||
useObjectMetadataItem({
|
useObjectMetadataItem({
|
||||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||||
});
|
});
|
||||||
|
const getObjectRecordIdentifierByNameSingular =
|
||||||
const { deleteOneRecord } = useDeleteOneRecord({
|
useGetObjectRecordIdentifierByNameSingular();
|
||||||
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 favoriteRelationFieldMetadataItems = useMemo(
|
const favoriteRelationFieldMetadataItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
favoriteObjectMetadataItem.fields.filter(
|
favoriteObjectMetadataItem.fields.filter(
|
||||||
(fieldMetadataItem) =>
|
(fieldMetadataItem) =>
|
||||||
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||||
fieldMetadataItem.name !== 'workspaceMember',
|
fieldMetadataItem.name !== 'workspaceMember' &&
|
||||||
|
fieldMetadataItem.name !== 'favoriteFolder',
|
||||||
),
|
),
|
||||||
[favoriteObjectMetadataItem.fields],
|
[favoriteObjectMetadataItem.fields],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getObjectRecordIdentifierByNameSingular =
|
const sortedFavorites = useMemo(
|
||||||
useGetObjectRecordIdentifierByNameSingular();
|
() =>
|
||||||
|
sortFavorites(
|
||||||
const favoritesSorted = useMemo(() => {
|
favorites,
|
||||||
return sortFavorites(
|
favoriteRelationFieldMetadataItems,
|
||||||
|
getObjectRecordIdentifierByNameSingular,
|
||||||
|
true,
|
||||||
|
views,
|
||||||
|
objectMetadataItems,
|
||||||
|
),
|
||||||
|
[
|
||||||
favorites,
|
favorites,
|
||||||
favoriteRelationFieldMetadataItems,
|
favoriteRelationFieldMetadataItems,
|
||||||
getObjectRecordIdentifierByNameSingular,
|
getObjectRecordIdentifierByNameSingular,
|
||||||
true,
|
views,
|
||||||
);
|
objectMetadataItems,
|
||||||
}, [
|
],
|
||||||
favoriteRelationFieldMetadataItems,
|
);
|
||||||
favorites,
|
|
||||||
getObjectRecordIdentifierByNameSingular,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const workspaceFavoritesSorted = useMemo(() => {
|
return sortedFavorites;
|
||||||
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,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
@ -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;
|
link: string;
|
||||||
recordId: string;
|
recordId: string;
|
||||||
workspaceMemberId: string;
|
workspaceMemberId: string;
|
||||||
|
favoriteFolderId?: string;
|
||||||
__typename: 'Favorite';
|
__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 { Favorite } from '@/favorites/types/Favorite';
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
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 { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
||||||
|
import { View } from '@/views/types/View';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
import { getObjectMetadataLabelPluralFromViewId } from './getObjectMetadataLabelPluralFromViewId';
|
||||||
|
|
||||||
|
export type ProcessedFavorite = Favorite & {
|
||||||
|
Icon?: string;
|
||||||
|
objectNameSingular?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const sortFavorites = (
|
export const sortFavorites = (
|
||||||
favorites: Favorite[],
|
favorites: Favorite[],
|
||||||
favoriteRelationFieldMetadataItems: FieldMetadataItem[],
|
favoriteRelationFieldMetadataItems: FieldMetadataItem[],
|
||||||
getObjectRecordIdentifierByNameSingular: (
|
getObjectRecordIdentifierByNameSingular: (
|
||||||
record: any,
|
record: ObjectRecord,
|
||||||
objectNameSingular: string,
|
objectNameSingular: string,
|
||||||
) => ObjectRecordIdentifier,
|
) => ObjectRecordIdentifier,
|
||||||
hasLinkToShowPage: boolean,
|
hasLinkToShowPage: boolean,
|
||||||
|
views: View[],
|
||||||
|
objectMetadataItems: ObjectMetadataItem[],
|
||||||
) => {
|
) => {
|
||||||
return favorites
|
return favorites
|
||||||
.map((favorite) => {
|
.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) {
|
for (const relationField of favoriteRelationFieldMetadataItems) {
|
||||||
if (isDefined(favorite[relationField.name])) {
|
if (isDefined(favorite[relationField.name])) {
|
||||||
const relationObject = favorite[relationField.name];
|
const relationObject = favorite[relationField.name];
|
||||||
@ -29,6 +63,7 @@ export const sortFavorites = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
__typename: 'Favorite',
|
||||||
id: favorite.id,
|
id: favorite.id,
|
||||||
recordId: objectRecordIdentifier.id,
|
recordId: objectRecordIdentifier.id,
|
||||||
position: favorite.position,
|
position: favorite.position,
|
||||||
@ -39,11 +74,14 @@ export const sortFavorites = (
|
|||||||
? objectRecordIdentifier.linkToShowPage
|
? objectRecordIdentifier.linkToShowPage
|
||||||
: '',
|
: '',
|
||||||
workspaceMemberId: favorite.workspaceMemberId,
|
workspaceMemberId: favorite.workspaceMemberId,
|
||||||
} as Favorite;
|
favoriteFolderId: favorite.favoriteFolderId,
|
||||||
|
objectNameSingular: relationObjectNameSingular,
|
||||||
|
} as ProcessedFavorite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
return favorite;
|
...favorite,
|
||||||
|
} as ProcessedFavorite;
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.position - b.position);
|
.sort((a, b) => a.position - b.position);
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import { useRecoilState, useSetRecoilState } from 'recoil';
|
|||||||
import { IconSearch, IconSettings } from 'twenty-ui';
|
import { IconSearch, IconSettings } from 'twenty-ui';
|
||||||
|
|
||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { CurrentWorkspaceMemberFavorites } from '@/favorites/components/CurrentWorkspaceMemberFavorites';
|
import { CurrentWorkspaceMemberFavoritesFolders } from '@/favorites/components/CurrentWorkspaceMemberFavoritesFolders';
|
||||||
import { WorkspaceFavorites } from '@/favorites/components/WorkspaceFavorites';
|
import { WorkspaceFavorites } from '@/favorites/components/WorkspaceFavorites';
|
||||||
import { NavigationDrawerOpenedSection } from '@/object-metadata/components/NavigationDrawerOpenedSection';
|
import { NavigationDrawerOpenedSection } from '@/object-metadata/components/NavigationDrawerOpenedSection';
|
||||||
import { NavigationDrawerSectionForObjectMetadataItemsWrapper } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper';
|
import { NavigationDrawerSectionForObjectMetadataItemsWrapper } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper';
|
||||||
@ -19,6 +19,10 @@ const StyledMainSection = styled(NavigationDrawerSection)`
|
|||||||
min-height: fit-content;
|
min-height: fit-content;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
`;
|
||||||
export const MainNavigationDrawerItems = () => {
|
export const MainNavigationDrawerItems = () => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const { toggleCommandMenu } = useCommandMenu();
|
const { toggleCommandMenu } = useCommandMenu();
|
||||||
@ -55,10 +59,15 @@ export const MainNavigationDrawerItems = () => {
|
|||||||
/>
|
/>
|
||||||
</StyledMainSection>
|
</StyledMainSection>
|
||||||
)}
|
)}
|
||||||
<NavigationDrawerOpenedSection />
|
<StyledContainer>
|
||||||
<CurrentWorkspaceMemberFavorites />
|
<NavigationDrawerOpenedSection />
|
||||||
<WorkspaceFavorites />
|
|
||||||
<NavigationDrawerSectionForObjectMetadataItemsWrapper isRemote={true} />
|
<CurrentWorkspaceMemberFavoritesFolders />
|
||||||
|
|
||||||
|
<WorkspaceFavorites />
|
||||||
|
|
||||||
|
<NavigationDrawerSectionForObjectMetadataItemsWrapper isRemote={true} />
|
||||||
|
</StyledContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
import { useWorkspaceFavorites } from '@/favorites/hooks/useWorkspaceFavorites';
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||||
@ -7,7 +7,7 @@ import { View } from '@/views/types/View';
|
|||||||
export const useFilteredObjectMetadataItemsForWorkspaceFavorites = () => {
|
export const useFilteredObjectMetadataItemsForWorkspaceFavorites = () => {
|
||||||
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
|
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
|
||||||
|
|
||||||
const { workspaceFavorites } = useFavorites();
|
const workspaceFavorites = useWorkspaceFavorites();
|
||||||
|
|
||||||
const workspaceFavoriteIds = new Set(
|
const workspaceFavoriteIds = new Set(
|
||||||
workspaceFavorites.map((favorite) => favorite.recordId),
|
workspaceFavorites.map((favorite) => favorite.recordId),
|
||||||
|
@ -3,9 +3,9 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|||||||
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||||
import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer';
|
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||||
import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState';
|
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||||
import { View } from '@/views/types/View';
|
import { View } from '@/views/types/View';
|
||||||
import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews';
|
import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@ -53,7 +53,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
|||||||
const subItemArrayLength = sortedObjectMetadataViews.length;
|
const subItemArrayLength = sortedObjectMetadataViews.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationDrawerItemsCollapsedContainer
|
<NavigationDrawerItemsCollapsableContainer
|
||||||
isGroup={shouldSubItemsBeDisplayed}
|
isGroup={shouldSubItemsBeDisplayed}
|
||||||
>
|
>
|
||||||
<NavigationDrawerItem
|
<NavigationDrawerItem
|
||||||
@ -69,7 +69,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
|||||||
label={view.name}
|
label={view.name}
|
||||||
to={`/objects/${objectMetadataItem.namePlural}?view=${view.id}`}
|
to={`/objects/${objectMetadataItem.namePlural}?view=${view.id}`}
|
||||||
active={viewId === view.id}
|
active={viewId === view.id}
|
||||||
subItemState={getNavigationSubItemState({
|
subItemState={getNavigationSubItemLeftAdornment({
|
||||||
index,
|
index,
|
||||||
arrayLength: subItemArrayLength,
|
arrayLength: subItemArrayLength,
|
||||||
selectedIndex: selectedSubItemIndex,
|
selectedIndex: selectedSubItemIndex,
|
||||||
@ -78,6 +78,6 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
|||||||
key={view.id}
|
key={view.id}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</NavigationDrawerItemsCollapsedContainer>
|
</NavigationDrawerItemsCollapsableContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,7 @@ export enum CoreObjectNameSingular {
|
|||||||
ConnectedAccount = 'connectedAccount',
|
ConnectedAccount = 'connectedAccount',
|
||||||
TimelineActivity = 'timelineActivity',
|
TimelineActivity = 'timelineActivity',
|
||||||
Favorite = 'favorite',
|
Favorite = 'favorite',
|
||||||
|
FavoriteFolder = 'favoriteFolder',
|
||||||
Message = 'message',
|
Message = 'message',
|
||||||
MessageChannel = 'messageChannel',
|
MessageChannel = 'messageChannel',
|
||||||
MessageParticipant = 'messageParticipant',
|
MessageParticipant = 'messageParticipant',
|
||||||
|
@ -163,6 +163,7 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
|
|||||||
companyId
|
companyId
|
||||||
createdAt
|
createdAt
|
||||||
deletedAt
|
deletedAt
|
||||||
|
favoriteFolderId
|
||||||
id
|
id
|
||||||
noteId
|
noteId
|
||||||
opportunityId
|
opportunityId
|
||||||
|
@ -102,6 +102,7 @@ const mocks: MockedResponse[] = [
|
|||||||
companyId
|
companyId
|
||||||
createdAt
|
createdAt
|
||||||
deletedAt
|
deletedAt
|
||||||
|
favoriteFolderId
|
||||||
id
|
id
|
||||||
noteId
|
noteId
|
||||||
opportunityId
|
opportunityId
|
||||||
|
@ -1,25 +1,50 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { FAVORITE_FOLDER_PICKER_DROPDOWN_ID } from '@/favorites/favorite-folder-picker/constants/FavoriteFolderPickerDropdownId';
|
||||||
import { useIcons } from 'twenty-ui';
|
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||||
|
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
|
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
|
||||||
import { RecordIndexPageKanbanAddButton } from '@/object-record/record-index/components/RecordIndexPageKanbanAddButton';
|
import { RecordIndexPageKanbanAddButton } from '@/object-record/record-index/components/RecordIndexPageKanbanAddButton';
|
||||||
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
||||||
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
|
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
|
||||||
|
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||||
|
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||||
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
|
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
|
||||||
|
import { PageFavoriteFoldersDropdown } from '@/ui/layout/page/components/PageFavoriteFolderDropdown';
|
||||||
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
|
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
|
||||||
import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect';
|
import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect';
|
||||||
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
|
||||||
|
import { View } from '@/views/types/View';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { useIcons } from 'twenty-ui';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
export const RecordIndexPageHeader = () => {
|
export const RecordIndexPageHeader = () => {
|
||||||
const { findObjectMetadataItemByNamePlural } =
|
const { findObjectMetadataItemByNamePlural } =
|
||||||
useFilteredObjectMetadataItems();
|
useFilteredObjectMetadataItems();
|
||||||
|
const isFavoriteFolderEnabled = useIsFeatureEnabled(
|
||||||
|
'IS_FAVORITE_FOLDER_ENABLED',
|
||||||
|
);
|
||||||
|
|
||||||
const { objectNamePlural, onCreateRecord } = useContext(
|
const { objectNamePlural, onCreateRecord, recordIndexId } = useContext(
|
||||||
RecordIndexRootPropsContext,
|
RecordIndexRootPropsContext,
|
||||||
);
|
);
|
||||||
|
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
|
||||||
|
const currentViewId = useRecoilComponentValueV2(
|
||||||
|
currentViewIdComponentState,
|
||||||
|
recordIndexId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const view = views.find((view) => view.id === currentViewId);
|
||||||
|
|
||||||
|
const favorites = useFavorites();
|
||||||
|
|
||||||
|
const isFavorite = favorites.some(
|
||||||
|
(favorite) =>
|
||||||
|
favorite.recordId === currentViewId && favorite.workspaceMemberId,
|
||||||
|
);
|
||||||
|
|
||||||
const objectMetadataItem =
|
const objectMetadataItem =
|
||||||
findObjectMetadataItemByNamePlural(objectNamePlural);
|
findObjectMetadataItemByNamePlural(objectNamePlural);
|
||||||
@ -47,6 +72,14 @@ export const RecordIndexPageHeader = () => {
|
|||||||
return (
|
return (
|
||||||
<PageHeader title={pageHeaderTitle} Icon={Icon}>
|
<PageHeader title={pageHeaderTitle} Icon={Icon}>
|
||||||
<PageHotkeysEffect onAddButtonClick={handleAddButtonClick} />
|
<PageHotkeysEffect onAddButtonClick={handleAddButtonClick} />
|
||||||
|
{isFavoriteFolderEnabled && (
|
||||||
|
<PageFavoriteFoldersDropdown
|
||||||
|
record={view}
|
||||||
|
dropdownId={FAVORITE_FOLDER_PICKER_DROPDOWN_ID}
|
||||||
|
objectNameSingular="view"
|
||||||
|
isFavorite={isFavorite}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{shouldDisplayAddButton &&
|
{shouldDisplayAddButton &&
|
||||||
(isTable ? (
|
(isTable ? (
|
||||||
<PageAddButton onClick={handleAddButtonClick} />
|
<PageAddButton onClick={handleAddButtonClick} />
|
||||||
|
@ -21,7 +21,6 @@ export const useHandleIndexIdentifierClick = ({
|
|||||||
recordId,
|
recordId,
|
||||||
currentViewId,
|
currentViewId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return showPageURL;
|
return showPageURL;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@ import { useParams } from 'react-router-dom';
|
|||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { useIcons } from 'twenty-ui';
|
import { useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
|
||||||
|
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||||
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
|
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
@ -34,7 +36,9 @@ export const useRecordShowPage = (
|
|||||||
const { objectMetadataItems } = useObjectMetadataItems();
|
const { objectMetadataItems } = useObjectMetadataItems();
|
||||||
const { labelIdentifierFieldMetadataItem } =
|
const { labelIdentifierFieldMetadataItem } =
|
||||||
useLabelIdentifierFieldMetadataItem({ objectNameSingular });
|
useLabelIdentifierFieldMetadataItem({ objectNameSingular });
|
||||||
const { favorites, createFavorite, deleteFavorite } = useFavorites();
|
const favorites = useFavorites();
|
||||||
|
const createFavorite = useCreateFavorite();
|
||||||
|
const deleteFavorite = useDeleteFavorite();
|
||||||
const setEntityFields = useSetRecoilState(
|
const setEntityFields = useSetRecoilState(
|
||||||
recordStoreFamilyState(objectRecordId),
|
recordStoreFamilyState(objectRecordId),
|
||||||
);
|
);
|
||||||
@ -98,8 +102,8 @@ export const useRecordShowPage = (
|
|||||||
pageTitle,
|
pageTitle,
|
||||||
pageName,
|
pageName,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
handleFavoriteButtonClick,
|
|
||||||
record,
|
record,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
|
handleFavoriteButtonClick,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -101,6 +101,7 @@ const companyMocks = [
|
|||||||
companyId
|
companyId
|
||||||
createdAt
|
createdAt
|
||||||
deletedAt
|
deletedAt
|
||||||
|
favoriteFolderId
|
||||||
id
|
id
|
||||||
noteId
|
noteId
|
||||||
opportunityId
|
opportunityId
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { PrefetchFavoriteFoldersRunQueriesEffect } from '@/prefetch/components/PrefetchFavortiteFoldersRunQueriesEffect';
|
||||||
import { PrefetchRunQueriesEffect } from '@/prefetch/components/PrefetchRunQueriesEffect';
|
import { PrefetchRunQueriesEffect } from '@/prefetch/components/PrefetchRunQueriesEffect';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
|
|
||||||
export const PrefetchDataProvider = ({ children }: React.PropsWithChildren) => {
|
export const PrefetchDataProvider = ({ children }: React.PropsWithChildren) => {
|
||||||
|
const isFavoriteFolderEnabled = useIsFeatureEnabled(
|
||||||
|
'IS_FAVORITE_FOLDER_ENABLED',
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PrefetchRunQueriesEffect />
|
<PrefetchRunQueriesEffect />
|
||||||
|
{isFavoriteFolderEnabled && <PrefetchFavoriteFoldersRunQueriesEffect />}
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
|
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
||||||
|
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||||
|
import { useCombinedFindManyRecords } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecords';
|
||||||
|
import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig';
|
||||||
|
import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery';
|
||||||
|
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
export const PrefetchFavoriteFoldersRunQueriesEffect = () => {
|
||||||
|
const currentUser = useRecoilValue(currentUserState);
|
||||||
|
|
||||||
|
const { upsertRecordsInCache: upsertFavoritesFoldersInCache } =
|
||||||
|
usePrefetchRunQuery<FavoriteFolder>({
|
||||||
|
prefetchKey: PrefetchKey.AllFavoritesFolders,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { objectMetadataItems } = useObjectMetadataItems();
|
||||||
|
|
||||||
|
// Only include favorite folders operation
|
||||||
|
const operationSignatures = Object.values(PREFETCH_CONFIG)
|
||||||
|
.filter(({ objectNameSingular }) => objectNameSingular === 'favoriteFolder')
|
||||||
|
.map(({ objectNameSingular, operationSignatureFactory }) => {
|
||||||
|
const objectMetadataItem = objectMetadataItems.find(
|
||||||
|
(item) => item.nameSingular === objectNameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
|
return operationSignatureFactory({ objectMetadataItem });
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = useCombinedFindManyRecords({
|
||||||
|
operationSignatures,
|
||||||
|
skip: !currentUser,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDefined(result.favoriteFolders)) {
|
||||||
|
upsertFavoritesFoldersInCache(result.favoriteFolders as FavoriteFolder[]);
|
||||||
|
}
|
||||||
|
}, [result, upsertFavoritesFoldersInCache]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
@ -9,10 +9,14 @@ import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig';
|
|||||||
import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery';
|
import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery';
|
||||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||||
import { View } from '@/views/types/View';
|
import { View } from '@/views/types/View';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const PrefetchRunQueriesEffect = () => {
|
export const PrefetchRunQueriesEffect = () => {
|
||||||
const currentUser = useRecoilValue(currentUserState);
|
const currentUser = useRecoilValue(currentUserState);
|
||||||
|
const isFavoriteFolderEnabled = useIsFeatureEnabled(
|
||||||
|
'IS_FAVORITE_FOLDER_ENABLED',
|
||||||
|
);
|
||||||
|
|
||||||
const { upsertRecordsInCache: upsertViewsInCache } =
|
const { upsertRecordsInCache: upsertViewsInCache } =
|
||||||
usePrefetchRunQuery<View>({
|
usePrefetchRunQuery<View>({
|
||||||
@ -26,15 +30,19 @@ export const PrefetchRunQueriesEffect = () => {
|
|||||||
|
|
||||||
const { objectMetadataItems } = useObjectMetadataItems();
|
const { objectMetadataItems } = useObjectMetadataItems();
|
||||||
|
|
||||||
const operationSignatures = Object.values(PREFETCH_CONFIG).map(
|
const operationSignatures = Object.values(PREFETCH_CONFIG)
|
||||||
({ objectNameSingular, operationSignatureFactory }) => {
|
.filter(
|
||||||
|
({ objectNameSingular }) =>
|
||||||
|
// Exclude favorite folders as they're handled separately
|
||||||
|
objectNameSingular !== 'favoriteFolder',
|
||||||
|
)
|
||||||
|
.map(({ objectNameSingular, operationSignatureFactory }) => {
|
||||||
const objectMetadataItem = objectMetadataItems.find(
|
const objectMetadataItem = objectMetadataItems.find(
|
||||||
(item) => item.nameSingular === objectNameSingular,
|
(item) => item.nameSingular === objectNameSingular,
|
||||||
);
|
);
|
||||||
|
|
||||||
return operationSignatureFactory({ objectMetadataItem });
|
return operationSignatureFactory({ objectMetadataItem });
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const { result } = useCombinedFindManyRecords({
|
const { result } = useCombinedFindManyRecords({
|
||||||
operationSignatures,
|
operationSignatures,
|
||||||
@ -49,7 +57,12 @@ export const PrefetchRunQueriesEffect = () => {
|
|||||||
if (isDefined(result.favorites)) {
|
if (isDefined(result.favorites)) {
|
||||||
upsertFavoritesInCache(result.favorites as Favorite[]);
|
upsertFavoritesInCache(result.favorites as Favorite[]);
|
||||||
}
|
}
|
||||||
}, [result, upsertViewsInCache, upsertFavoritesInCache]);
|
}, [
|
||||||
|
result,
|
||||||
|
upsertViewsInCache,
|
||||||
|
upsertFavoritesInCache,
|
||||||
|
isFavoriteFolderEnabled,
|
||||||
|
]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory';
|
import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory';
|
||||||
|
import { findAllFavoritesFolderOperationSignatureFactory } from '@/prefetch/graphql/operation-signatures/factories/findAllFavoritesFolderOperationSignatureFactory';
|
||||||
import { findAllFavoritesOperationSignatureFactory } from '@/prefetch/graphql/operation-signatures/factories/findAllFavoritesOperationSignatureFactory';
|
import { findAllFavoritesOperationSignatureFactory } from '@/prefetch/graphql/operation-signatures/factories/findAllFavoritesOperationSignatureFactory';
|
||||||
import { findAllViewsOperationSignatureFactory } from '@/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory';
|
import { findAllViewsOperationSignatureFactory } from '@/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory';
|
||||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||||
@ -19,4 +20,8 @@ export const PREFETCH_CONFIG: Record<
|
|||||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||||
operationSignatureFactory: findAllFavoritesOperationSignatureFactory,
|
operationSignatureFactory: findAllFavoritesOperationSignatureFactory,
|
||||||
},
|
},
|
||||||
|
ALL_FAVORITES_FOLDERS: {
|
||||||
|
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
|
||||||
|
operationSignatureFactory: findAllFavoritesFolderOperationSignatureFactory,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory';
|
||||||
|
|
||||||
|
export const findAllFavoritesFolderOperationSignatureFactory: RecordGqlOperationSignatureFactory =
|
||||||
|
() => ({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
|
||||||
|
variables: {},
|
||||||
|
fields: {
|
||||||
|
id: true,
|
||||||
|
position: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
deletedAt: true,
|
||||||
|
name: true,
|
||||||
|
icon: true,
|
||||||
|
},
|
||||||
|
});
|
@ -0,0 +1,11 @@
|
|||||||
|
import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState';
|
||||||
|
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
export const useIsFavoriteFoldersPrefetchLoading = () => {
|
||||||
|
const areFavoritesFolderPrefetched = useRecoilValue(
|
||||||
|
prefetchIsLoadedFamilyState(PrefetchKey.AllFavoritesFolders),
|
||||||
|
);
|
||||||
|
|
||||||
|
return !areFavoritesFolderPrefetched;
|
||||||
|
};
|
@ -1,14 +1,25 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
|
|
||||||
import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState';
|
import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState';
|
||||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { useIsFavoriteFoldersPrefetchLoading } from './useIsFavoriteFoldersPrefetchLoading';
|
||||||
|
|
||||||
export const useIsPrefetchLoading = () => {
|
export const useIsPrefetchLoading = () => {
|
||||||
|
const isFavoriteFolderEnabled = useIsFeatureEnabled(
|
||||||
|
'IS_FAVORITE_FOLDER_ENABLED',
|
||||||
|
);
|
||||||
|
const isFavoriteFoldersLoading = useIsFavoriteFoldersPrefetchLoading();
|
||||||
|
|
||||||
const areViewsPrefetched = useRecoilValue(
|
const areViewsPrefetched = useRecoilValue(
|
||||||
prefetchIsLoadedFamilyState(PrefetchKey.AllViews),
|
prefetchIsLoadedFamilyState(PrefetchKey.AllViews),
|
||||||
);
|
);
|
||||||
const areFavoritesPrefetched = useRecoilValue(
|
const areFavoritesPrefetched = useRecoilValue(
|
||||||
prefetchIsLoadedFamilyState(PrefetchKey.AllFavorites),
|
prefetchIsLoadedFamilyState(PrefetchKey.AllFavorites),
|
||||||
);
|
);
|
||||||
return !areViewsPrefetched || !areFavoritesPrefetched;
|
|
||||||
|
return (
|
||||||
|
!areViewsPrefetched ||
|
||||||
|
!areFavoritesPrefetched ||
|
||||||
|
(isFavoriteFolderEnabled && isFavoriteFoldersLoading)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export enum PrefetchKey {
|
export enum PrefetchKey {
|
||||||
AllViews = 'ALL_VIEWS',
|
AllViews = 'ALL_VIEWS',
|
||||||
AllFavorites = 'ALL_FAVORITES',
|
AllFavorites = 'ALL_FAVORITES',
|
||||||
|
AllFavoritesFolders = 'ALL_FAVORITES_FOLDERS',
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ import { NavigationDrawerItemGroup } from '@/ui/navigation/navigation-drawer/com
|
|||||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||||
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
||||||
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
||||||
import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState';
|
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
@ -143,7 +143,7 @@ export const SettingsNavigationDrawerItems = () => {
|
|||||||
path={navigationItem.path}
|
path={navigationItem.path}
|
||||||
Icon={navigationItem.Icon}
|
Icon={navigationItem.Icon}
|
||||||
indentationLevel={navigationItem.indentationLevel}
|
indentationLevel={navigationItem.indentationLevel}
|
||||||
subItemState={getNavigationSubItemState({
|
subItemState={getNavigationSubItemLeftAdornment({
|
||||||
arrayLength: accountSubSettings.length,
|
arrayLength: accountSubSettings.length,
|
||||||
index,
|
index,
|
||||||
selectedIndex,
|
selectedIndex,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import { Draggable } from '@hello-pangea/dnd';
|
import { Draggable } from '@hello-pangea/dnd';
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { IconButton, IconHeart } from 'twenty-ui';
|
|||||||
|
|
||||||
type PageFavoriteButtonProps = {
|
type PageFavoriteButtonProps = {
|
||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
onClick: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageFavoriteButton = ({
|
export const PageFavoriteButton = ({
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
import { FavoriteFolderPicker } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPicker';
|
||||||
|
import { FavoriteFolderPickerEffect } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPickerEffect';
|
||||||
|
import { FavoriteFolderPickerScope } from '@/favorites/favorite-folder-picker/scopes/FavoriteFolderPickerScope';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
|
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||||
|
import { PageFavoriteButton } from '@/ui/layout/page/components/PageFavoriteButton';
|
||||||
|
|
||||||
|
type PageFavoriteFoldersDropdownProps = {
|
||||||
|
dropdownId: string;
|
||||||
|
isFavorite: boolean;
|
||||||
|
record?: ObjectRecord;
|
||||||
|
objectNameSingular: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageFavoriteFoldersDropdown = ({
|
||||||
|
dropdownId,
|
||||||
|
isFavorite,
|
||||||
|
record,
|
||||||
|
objectNameSingular,
|
||||||
|
}: PageFavoriteFoldersDropdownProps) => {
|
||||||
|
const { closeDropdown } = useDropdown(dropdownId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FavoriteFolderPickerScope favoriteFoldersScopeId={dropdownId}>
|
||||||
|
<DropdownScope dropdownScopeId={dropdownId}>
|
||||||
|
<Dropdown
|
||||||
|
dropdownId={dropdownId}
|
||||||
|
dropdownPlacement="bottom-start"
|
||||||
|
clickableComponent={<PageFavoriteButton isFavorite={isFavorite} />}
|
||||||
|
dropdownComponents={
|
||||||
|
<>
|
||||||
|
<FavoriteFolderPickerEffect record={record} />
|
||||||
|
<FavoriteFolderPicker
|
||||||
|
onSubmit={closeDropdown}
|
||||||
|
record={record}
|
||||||
|
objectNameSingular={objectNameSingular}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
dropdownHotkeyScope={{
|
||||||
|
scope: dropdownId,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DropdownScope>
|
||||||
|
</FavoriteFolderPickerScope>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,136 @@
|
|||||||
|
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
||||||
|
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||||
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { ChangeEvent, FocusEvent, useRef } from 'react';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
import {
|
||||||
|
IconComponent,
|
||||||
|
isDefined,
|
||||||
|
TablerIconsProps,
|
||||||
|
TEXT_INPUT_STYLE,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
|
||||||
|
|
||||||
|
type NavigationDrawerInputProps = {
|
||||||
|
className?: string;
|
||||||
|
Icon: IconComponent | ((props: TablerIconsProps) => JSX.Element);
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onSubmit: (value: string) => void;
|
||||||
|
onCancel: (value: string) => void;
|
||||||
|
onClickOutside: (event: MouseEvent | TouchEvent, value: string) => void;
|
||||||
|
hotkeyScope: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledItem = styled.div<{ isNavigationDrawerExpanded: boolean }>`
|
||||||
|
align-items: center;
|
||||||
|
background-color: ${({ theme }) => theme.background.primary};
|
||||||
|
border: 1px solid ${({ theme }) => theme.color.blue};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
box-sizing: content-box;
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
display: flex;
|
||||||
|
font-family: ${({ theme }) => theme.font.family};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
height: calc(${({ theme }) => theme.spacing(5)} - 2px);
|
||||||
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
text-decoration: none;
|
||||||
|
user-select: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledItemElementsContainer = styled.span`
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTextInput = styled.input`
|
||||||
|
${TEXT_INPUT_STYLE}
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const NavigationDrawerInput = ({
|
||||||
|
className,
|
||||||
|
Icon,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
onClickOutside,
|
||||||
|
hotkeyScope,
|
||||||
|
}: NavigationDrawerInputProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [isNavigationDrawerExpanded] = useRecoilState(
|
||||||
|
isNavigationDrawerExpandedState,
|
||||||
|
);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useHotkeyScopeOnMount(hotkeyScope);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
[Key.Escape],
|
||||||
|
() => {
|
||||||
|
onCancel(value);
|
||||||
|
},
|
||||||
|
hotkeyScope,
|
||||||
|
);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
[Key.Enter],
|
||||||
|
() => {
|
||||||
|
onSubmit(value);
|
||||||
|
},
|
||||||
|
hotkeyScope,
|
||||||
|
);
|
||||||
|
|
||||||
|
useListenClickOutside({
|
||||||
|
refs: [inputRef],
|
||||||
|
callback: (event) => {
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
onClickOutside(event, value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = (event: FocusEvent<HTMLInputElement>) => {
|
||||||
|
if (isDefined(value)) {
|
||||||
|
event.target.select();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledItem
|
||||||
|
className={className}
|
||||||
|
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
|
||||||
|
>
|
||||||
|
<StyledItemElementsContainer>
|
||||||
|
{Icon && (
|
||||||
|
<Icon
|
||||||
|
style={{ minWidth: theme.icon.size.md }}
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
stroke={theme.icon.stroke.md}
|
||||||
|
color="currentColor"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<NavigationDrawerAnimatedCollapseWrapper>
|
||||||
|
<StyledTextInput
|
||||||
|
ref={inputRef}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</NavigationDrawerAnimatedCollapseWrapper>
|
||||||
|
</StyledItemElementsContainer>
|
||||||
|
</StyledItem>
|
||||||
|
);
|
||||||
|
};
|
@ -8,6 +8,7 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
|||||||
import isPropValid from '@emotion/is-prop-valid';
|
import isPropValid from '@emotion/is-prop-valid';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import {
|
import {
|
||||||
@ -35,16 +36,19 @@ export type NavigationDrawerItemProps = {
|
|||||||
soon?: boolean;
|
soon?: boolean;
|
||||||
count?: number;
|
count?: number;
|
||||||
keyboard?: string[];
|
keyboard?: string[];
|
||||||
|
rightOptions?: ReactNode;
|
||||||
|
isDraggable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StyledItemProps = Pick<
|
type StyledItemProps = Pick<
|
||||||
NavigationDrawerItemProps,
|
NavigationDrawerItemProps,
|
||||||
'active' | 'danger' | 'indentationLevel' | 'soon' | 'to'
|
'active' | 'danger' | 'indentationLevel' | 'soon' | 'to' | 'isDraggable'
|
||||||
> & { isNavigationDrawerExpanded: boolean };
|
> & { isNavigationDrawerExpanded: boolean };
|
||||||
|
|
||||||
const StyledItem = styled('button', {
|
const StyledItem = styled('button', {
|
||||||
shouldForwardProp: (prop) =>
|
shouldForwardProp: (prop) =>
|
||||||
!['active', 'danger', 'soon'].includes(prop) && isPropValid(prop),
|
!['active', 'danger', 'soon', 'isDraggable'].includes(prop) &&
|
||||||
|
isPropValid(prop),
|
||||||
})<StyledItemProps>`
|
})<StyledItemProps>`
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -85,6 +89,15 @@ const StyledItem = styled('button', {
|
|||||||
!props.isNavigationDrawerExpanded
|
!props.isNavigationDrawerExpanded
|
||||||
? `${NAV_DRAWER_WIDTHS.menu.desktop.collapsed - 24}px`
|
? `${NAV_DRAWER_WIDTHS.menu.desktop.collapsed - 24}px`
|
||||||
: '100%'};
|
: '100%'};
|
||||||
|
${({ isDraggable }) =>
|
||||||
|
isDraggable &&
|
||||||
|
`
|
||||||
|
cursor: grab;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
|
||||||
:hover {
|
:hover {
|
||||||
background: ${({ theme }) => theme.background.transparent.light};
|
background: ${({ theme }) => theme.background.transparent.light};
|
||||||
@ -150,6 +163,27 @@ const StyledSpacer = styled.span`
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledRightOptionsContainer = styled.div<{
|
||||||
|
isMobile: boolean;
|
||||||
|
active: boolean;
|
||||||
|
}>`
|
||||||
|
margin-left: auto;
|
||||||
|
visibility: ${({ isMobile, active }) =>
|
||||||
|
isMobile || active ? 'visible' : 'hidden'};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
:hover {
|
||||||
|
background: ${({ theme }) => theme.background.transparent.light};
|
||||||
|
}
|
||||||
|
width: ${({ theme }) => theme.spacing(6)};
|
||||||
|
height: ${({ theme }) => theme.spacing(6)};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
.navigation-drawer-item:hover & {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const NavigationDrawerItem = ({
|
export const NavigationDrawerItem = ({
|
||||||
className,
|
className,
|
||||||
label,
|
label,
|
||||||
@ -163,6 +197,8 @@ export const NavigationDrawerItem = ({
|
|||||||
count,
|
count,
|
||||||
keyboard,
|
keyboard,
|
||||||
subItemState,
|
subItemState,
|
||||||
|
rightOptions,
|
||||||
|
isDraggable,
|
||||||
}: NavigationDrawerItemProps) => {
|
}: NavigationDrawerItemProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
@ -185,7 +221,7 @@ export const NavigationDrawerItem = ({
|
|||||||
return (
|
return (
|
||||||
<StyledNavigationDrawerItemContainer>
|
<StyledNavigationDrawerItemContainer>
|
||||||
<StyledItem
|
<StyledItem
|
||||||
className={className}
|
className={`navigation-drawer-item ${className || ''}`}
|
||||||
onClick={handleItemClick}
|
onClick={handleItemClick}
|
||||||
active={active}
|
active={active}
|
||||||
aria-selected={active}
|
aria-selected={active}
|
||||||
@ -195,6 +231,7 @@ export const NavigationDrawerItem = ({
|
|||||||
to={to ? to : undefined}
|
to={to ? to : undefined}
|
||||||
indentationLevel={indentationLevel}
|
indentationLevel={indentationLevel}
|
||||||
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
|
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
|
||||||
|
isDraggable={isDraggable}
|
||||||
>
|
>
|
||||||
{showBreadcrumb && (
|
{showBreadcrumb && (
|
||||||
<NavigationDrawerAnimatedCollapseWrapper>
|
<NavigationDrawerAnimatedCollapseWrapper>
|
||||||
@ -240,6 +277,20 @@ export const NavigationDrawerItem = ({
|
|||||||
</StyledKeyBoardShortcut>
|
</StyledKeyBoardShortcut>
|
||||||
</NavigationDrawerAnimatedCollapseWrapper>
|
</NavigationDrawerAnimatedCollapseWrapper>
|
||||||
)}
|
)}
|
||||||
|
<NavigationDrawerAnimatedCollapseWrapper>
|
||||||
|
{rightOptions && (
|
||||||
|
<StyledRightOptionsContainer
|
||||||
|
isMobile={isMobile}
|
||||||
|
active={active || false}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rightOptions}
|
||||||
|
</StyledRightOptionsContainer>
|
||||||
|
)}
|
||||||
|
</NavigationDrawerAnimatedCollapseWrapper>
|
||||||
</StyledItemElementsContainer>
|
</StyledItemElementsContainer>
|
||||||
</StyledItem>
|
</StyledItem>
|
||||||
</StyledNavigationDrawerItemContainer>
|
</StyledNavigationDrawerItemContainer>
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
|
||||||
|
export const NavigationDrawerItemInput = () => {
|
||||||
|
return <TextInput />;
|
||||||
|
};
|
@ -1,22 +1,22 @@
|
|||||||
|
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
|
||||||
|
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { AnimationControls, motion, TargetAndTransition } from 'framer-motion';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
|
||||||
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
|
|
||||||
import { AnimationControls, motion, TargetAndTransition } from 'framer-motion';
|
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
|
|
||||||
const StyledAnimationGroupContainer = styled(motion.div)``;
|
const StyledAnimationGroupContainer = styled(motion.div)``;
|
||||||
|
|
||||||
type NavigationDrawerItemsCollapsedContainerProps = {
|
type NavigationDrawerItemsCollapsableContainerProps = {
|
||||||
isGroup?: boolean;
|
isGroup?: boolean;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NavigationDrawerItemsCollapsedContainer = ({
|
export const NavigationDrawerItemsCollapsableContainer = ({
|
||||||
isGroup = false,
|
isGroup = false,
|
||||||
children,
|
children,
|
||||||
}: NavigationDrawerItemsCollapsedContainerProps) => {
|
}: NavigationDrawerItemsCollapsableContainerProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isSettingsPage = useIsSettingsPage();
|
const isSettingsPage = useIsSettingsPage();
|
||||||
const isNavigationDrawerExpanded = useRecoilValue(
|
const isNavigationDrawerExpanded = useRecoilValue(
|
@ -7,7 +7,6 @@ const StyledSection = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: ${({ theme }) => theme.spacing(3)};
|
margin-bottom: ${({ theme }) => theme.spacing(3)};
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
overflow: hidden;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export { StyledSection as NavigationDrawerSection };
|
export { StyledSection as NavigationDrawerSection };
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
|
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
|
||||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
||||||
import { NavigationDrawerSectionTitleSkeletonLoader } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitleSkeletonLoader';
|
import { NavigationDrawerSectionTitleSkeletonLoader } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitleSkeletonLoader';
|
||||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||||
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import React from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
|
||||||
|
|
||||||
type NavigationDrawerSectionTitleProps = {
|
const StyledTitle = styled.div`
|
||||||
onClick?: () => void;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledTitle = styled.div<{ onClick?: () => void }>`
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
color: ${({ theme }) => theme.font.color.light};
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
@ -23,38 +18,92 @@ const StyledTitle = styled.div<{ onClick?: () => void }>`
|
|||||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
height: ${({ theme }) => theme.spacing(5)};
|
height: ${({ theme }) => theme.spacing(5)};
|
||||||
padding: ${({ theme }) => theme.spacing(1)};
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
${({ onClick, theme }) =>
|
&:hover {
|
||||||
!isUndefinedOrNull(onClick)
|
cursor: pointer;
|
||||||
? `&:hover {
|
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||||
cursor: pointer;
|
}
|
||||||
background-color:${theme.background.transparent.light};
|
|
||||||
}`
|
|
||||||
: ''}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledLabel = styled.div`
|
||||||
|
flex-grow: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type StyledRightIconProps = {
|
||||||
|
isMobile: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledRightIcon = styled.div<StyledRightIconProps>`
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||||
|
transition: opacity 150ms ease-in-out;
|
||||||
|
opacity: ${({ isMobile }) => (isMobile ? 1 : 0)};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
width: ${({ theme }) => theme.spacing(5)};
|
||||||
|
height: ${({ theme }) => theme.spacing(5)};
|
||||||
|
:hover {
|
||||||
|
background: ${({ theme }) => theme.background.transparent.light};
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title-container:hover & {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type NavigationDrawerSectionTitleProps = {
|
||||||
|
onClick?: () => void;
|
||||||
|
onRightIconClick?: () => void;
|
||||||
|
label: string;
|
||||||
|
rightIcon?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
export const NavigationDrawerSectionTitle = ({
|
export const NavigationDrawerSectionTitle = ({
|
||||||
onClick,
|
onClick,
|
||||||
|
onRightIconClick,
|
||||||
label,
|
label,
|
||||||
|
rightIcon,
|
||||||
}: NavigationDrawerSectionTitleProps) => {
|
}: NavigationDrawerSectionTitleProps) => {
|
||||||
const currentUser = useRecoilValue(currentUserState);
|
const isMobile = useIsMobile();
|
||||||
const loading = useIsPrefetchLoading();
|
|
||||||
const isNavigationDrawerExpanded = useRecoilValue(
|
const isNavigationDrawerExpanded = useRecoilValue(
|
||||||
isNavigationDrawerExpandedState,
|
isNavigationDrawerExpandedState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSettingsPage = useIsSettingsPage();
|
const isSettingsPage = useIsSettingsPage();
|
||||||
|
const currentUser = useRecoilValue(currentUserState);
|
||||||
|
const loading = useIsPrefetchLoading();
|
||||||
|
const handleTitleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isDefined(onClick) && (isNavigationDrawerExpanded || isSettingsPage)) {
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRightIconClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isDefined(onRightIconClick)) {
|
||||||
|
onRightIconClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading && isDefined(currentUser)) {
|
if (loading && isDefined(currentUser)) {
|
||||||
return <NavigationDrawerSectionTitleSkeletonLoader />;
|
return <NavigationDrawerSectionTitleSkeletonLoader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTitle
|
<StyledTitle className="section-title-container" onClick={handleTitleClick}>
|
||||||
onClick={
|
<StyledLabel>{label}</StyledLabel>
|
||||||
isNavigationDrawerExpanded || isSettingsPage ? onClick : undefined
|
{rightIcon && (
|
||||||
}
|
<StyledRightIcon isMobile={isMobile} onClick={handleRightIconClick}>
|
||||||
>
|
{rightIcon}
|
||||||
{label}
|
</StyledRightIcon>
|
||||||
|
)}
|
||||||
</StyledTitle>
|
</StyledTitle>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -17,6 +17,8 @@ export const NavigationDrawerSubItem = ({
|
|||||||
count,
|
count,
|
||||||
keyboard,
|
keyboard,
|
||||||
subItemState,
|
subItemState,
|
||||||
|
rightOptions,
|
||||||
|
isDraggable,
|
||||||
}: NavigationDrawerSubItemProps) => {
|
}: NavigationDrawerSubItemProps) => {
|
||||||
return (
|
return (
|
||||||
<NavigationDrawerItem
|
<NavigationDrawerItem
|
||||||
@ -32,6 +34,8 @@ export const NavigationDrawerSubItem = ({
|
|||||||
soon={soon}
|
soon={soon}
|
||||||
count={count}
|
count={count}
|
||||||
keyboard={keyboard}
|
keyboard={keyboard}
|
||||||
|
rightOptions={rightOptions}
|
||||||
|
isDraggable={isDraggable}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -22,7 +22,7 @@ import { SettingsPath } from '@/types/SettingsPath';
|
|||||||
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
|
||||||
import { CurrentWorkspaceMemberFavorites } from '@/favorites/components/CurrentWorkspaceMemberFavorites';
|
import { CurrentWorkspaceMemberFavoritesFolders } from '@/favorites/components/CurrentWorkspaceMemberFavoritesFolders';
|
||||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||||
import jsonPage from '../../../../../../../package.json';
|
import jsonPage from '../../../../../../../package.json';
|
||||||
import { NavigationDrawer } from '../NavigationDrawer';
|
import { NavigationDrawer } from '../NavigationDrawer';
|
||||||
@ -71,7 +71,7 @@ export const Default: Story = {
|
|||||||
/>
|
/>
|
||||||
</NavigationDrawerSection>
|
</NavigationDrawerSection>
|
||||||
|
|
||||||
<CurrentWorkspaceMemberFavorites />
|
<CurrentWorkspaceMemberFavoritesFolders />
|
||||||
|
|
||||||
<NavigationDrawerSection>
|
<NavigationDrawerSection>
|
||||||
<NavigationDrawerSectionTitle label="Workspace" />
|
<NavigationDrawerSectionTitle label="Workspace" />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { NavigationDrawerSubItemState } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerSubItemState';
|
import { NavigationDrawerSubItemState } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerSubItemState';
|
||||||
|
|
||||||
export const getNavigationSubItemState = ({
|
export const getNavigationSubItemLeftAdornment = ({
|
||||||
index,
|
index,
|
||||||
arrayLength,
|
arrayLength,
|
||||||
selectedIndex,
|
selectedIndex,
|
@ -16,4 +16,5 @@ export type FeatureFlagKey =
|
|||||||
| 'IS_JSON_FILTER_ENABLED'
|
| 'IS_JSON_FILTER_ENABLED'
|
||||||
| 'IS_MICROSOFT_SYNC_ENABLED'
|
| 'IS_MICROSOFT_SYNC_ENABLED'
|
||||||
| 'IS_ADVANCED_FILTERS_ENABLED'
|
| 'IS_ADVANCED_FILTERS_ENABLED'
|
||||||
| 'IS_AGGREGATE_QUERY_ENABLED';
|
| 'IS_AGGREGATE_QUERY_ENABLED'
|
||||||
|
| 'IS_FAVORITE_FOLDER_ENABLED';
|
||||||
|
@ -30,9 +30,9 @@ export const RecordShowPage = () => {
|
|||||||
pageTitle,
|
pageTitle,
|
||||||
pageName,
|
pageName,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
handleFavoriteButtonClick,
|
|
||||||
record,
|
record,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
|
handleFavoriteButtonClick,
|
||||||
} = useRecordShowPage(
|
} = useRecordShowPage(
|
||||||
parameters.objectNameSingular ?? '',
|
parameters.objectNameSingular ?? '',
|
||||||
parameters.objectRecordId ?? '',
|
parameters.objectRecordId ?? '',
|
||||||
@ -65,15 +65,17 @@ export const RecordShowPage = () => {
|
|||||||
workflowVersionId={objectRecordId}
|
workflowVersionId={objectRecordId}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RecordShowActionMenu
|
<>
|
||||||
{...{
|
<RecordShowActionMenu
|
||||||
isFavorite,
|
{...{
|
||||||
handleFavoriteButtonClick,
|
isFavorite,
|
||||||
record,
|
record,
|
||||||
objectMetadataItem,
|
handleFavoriteButtonClick,
|
||||||
objectNameSingular,
|
objectMetadataItem,
|
||||||
}}
|
objectNameSingular,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</RecordShowPageHeader>
|
</RecordShowPageHeader>
|
||||||
|
@ -1,28 +1,47 @@
|
|||||||
|
import { FAVORITE_FOLDER_PICKER_DROPDOWN_ID } from '@/favorites/favorite-folder-picker/constants/FavoriteFolderPickerDropdownId';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { PageFavoriteButton } from '@/ui/layout/page/components/PageFavoriteButton';
|
import { PageFavoriteButton } from '@/ui/layout/page/components/PageFavoriteButton';
|
||||||
|
import { PageFavoriteFoldersDropdown } from '@/ui/layout/page/components/PageFavoriteFolderDropdown';
|
||||||
import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton';
|
import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton';
|
||||||
import { ShowPageMoreButton } from '@/ui/layout/show-page/components/ShowPageMoreButton';
|
import { ShowPageMoreButton } from '@/ui/layout/show-page/components/ShowPageMoreButton';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
|
|
||||||
export const RecordShowPageBaseHeader = ({
|
type RecordShowPageBaseHeaderProps = {
|
||||||
isFavorite,
|
|
||||||
handleFavoriteButtonClick,
|
|
||||||
record,
|
|
||||||
objectMetadataItem,
|
|
||||||
objectNameSingular,
|
|
||||||
}: {
|
|
||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
handleFavoriteButtonClick: () => void;
|
|
||||||
record: ObjectRecord | undefined;
|
record: ObjectRecord | undefined;
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
}) => {
|
handleFavoriteButtonClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RecordShowPageBaseHeader = ({
|
||||||
|
isFavorite,
|
||||||
|
record,
|
||||||
|
objectMetadataItem,
|
||||||
|
objectNameSingular,
|
||||||
|
handleFavoriteButtonClick,
|
||||||
|
}: RecordShowPageBaseHeaderProps) => {
|
||||||
|
const isFavoriteFolderEnabled = useIsFeatureEnabled(
|
||||||
|
'IS_FAVORITE_FOLDER_ENABLED',
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageFavoriteButton
|
{isFavoriteFolderEnabled ? (
|
||||||
isFavorite={isFavorite}
|
<PageFavoriteFoldersDropdown
|
||||||
onClick={handleFavoriteButtonClick}
|
key={FAVORITE_FOLDER_PICKER_DROPDOWN_ID}
|
||||||
/>
|
dropdownId={FAVORITE_FOLDER_PICKER_DROPDOWN_ID}
|
||||||
|
isFavorite={isFavorite}
|
||||||
|
record={record}
|
||||||
|
objectNameSingular={objectNameSingular}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PageFavoriteButton
|
||||||
|
isFavorite={isFavorite}
|
||||||
|
onClick={handleFavoriteButtonClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ShowPageAddButton
|
<ShowPageAddButton
|
||||||
key="add"
|
key="add"
|
||||||
activityTargetObject={{
|
activityTargetObject={{
|
||||||
|
@ -11,9 +11,13 @@ export const PrefetchLoadedDecorator: Decorator = (Story) => {
|
|||||||
const setAreFavoritesPrefetched = useSetRecoilState(
|
const setAreFavoritesPrefetched = useSetRecoilState(
|
||||||
prefetchIsLoadedFamilyState(PrefetchKey.AllFavorites),
|
prefetchIsLoadedFamilyState(PrefetchKey.AllFavorites),
|
||||||
);
|
);
|
||||||
|
const setAreFavoritesFoldersPrefetched = useSetRecoilState(
|
||||||
|
prefetchIsLoadedFamilyState(PrefetchKey.AllFavoritesFolders),
|
||||||
|
);
|
||||||
|
|
||||||
setAreViewsPrefetched(true);
|
setAreViewsPrefetched(true);
|
||||||
setAreFavoritesPrefetched(true);
|
setAreFavoritesPrefetched(true);
|
||||||
|
setAreFavoritesFoldersPrefetched(true);
|
||||||
|
|
||||||
return <Story />;
|
return <Story />;
|
||||||
};
|
};
|
||||||
|
@ -225,6 +225,15 @@ export const graphqlMocks = {
|
|||||||
endCursor: null,
|
endCursor: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
favoriteFolders: {
|
||||||
|
edges: [],
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
startCursor: null,
|
||||||
|
endCursor: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
views: {
|
views: {
|
||||||
edges: mockedViewsData.map((view) => ({
|
edges: mockedViewsData.map((view) => ({
|
||||||
node: {
|
node: {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -85,6 +85,16 @@ export const seedFeatureFlags = async (
|
|||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
value: false,
|
value: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: FeatureFlagKey.IsFavoriteFolderEnabled,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: FeatureFlagKey.IsFavoriteFolderEntityEnabled,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
])
|
])
|
||||||
.execute();
|
.execute();
|
||||||
};
|
};
|
||||||
|
@ -15,4 +15,6 @@ export enum FeatureFlagKey {
|
|||||||
IsMicrosoftSyncEnabled = 'IS_MICROSOFT_SYNC_ENABLED',
|
IsMicrosoftSyncEnabled = 'IS_MICROSOFT_SYNC_ENABLED',
|
||||||
IsAdvancedFiltersEnabled = 'IS_ADVANCED_FILTERS_ENABLED',
|
IsAdvancedFiltersEnabled = 'IS_ADVANCED_FILTERS_ENABLED',
|
||||||
IsAggregateQueryEnabled = 'IS_AGGREGATE_QUERY_ENABLED',
|
IsAggregateQueryEnabled = 'IS_AGGREGATE_QUERY_ENABLED',
|
||||||
|
IsFavoriteFolderEnabled = 'IS_FAVORITE_FOLDER_ENABLED',
|
||||||
|
IsFavoriteFolderEntityEnabled = 'IS_FAVORITE_FOLDER_ENTITY_ENABLED',
|
||||||
}
|
}
|
||||||
|
@ -217,6 +217,13 @@ export const FAVORITE_STANDARD_FIELD_IDS = {
|
|||||||
note: '20202020-1f25-43fe-8b00-af212fdde824',
|
note: '20202020-1f25-43fe-8b00-af212fdde824',
|
||||||
view: '20202020-5a93-4fa9-acce-e73481a0bbdf',
|
view: '20202020-5a93-4fa9-acce-e73481a0bbdf',
|
||||||
custom: '20202020-855a-4bc8-9861-79deef37011f',
|
custom: '20202020-855a-4bc8-9861-79deef37011f',
|
||||||
|
favoriteFolder: '20202020-f658-4d12-8b4d-248356aa4bd9',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FAVORITE_FOLDER_STANDARD_FIELD_IDS = {
|
||||||
|
position: '20202020-5278-4bde-8909-2cec74d43744',
|
||||||
|
name: '20202020-82a3-4537-8ff0-dbce7eec35d6',
|
||||||
|
favorites: '20202020-b5e3-4b42-8af2-03cd4fd2e4d2',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS = {
|
export const MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_STANDARD_FIELD_IDS = {
|
||||||
|
@ -21,6 +21,7 @@ export const STANDARD_OBJECT_IDS = {
|
|||||||
company: '20202020-b374-4779-a561-80086cb2e17f',
|
company: '20202020-b374-4779-a561-80086cb2e17f',
|
||||||
connectedAccount: '20202020-977e-46b2-890b-c3002ddfd5c5',
|
connectedAccount: '20202020-977e-46b2-890b-c3002ddfd5c5',
|
||||||
favorite: '20202020-ab56-4e05-92a3-e2414a499860',
|
favorite: '20202020-ab56-4e05-92a3-e2414a499860',
|
||||||
|
favoriteFolder: '20202020-7cf8-401f-8211-a9587d27fd2d',
|
||||||
auditLog: '20202020-0566-476a-b4c4-a0f9781bd80a',
|
auditLog: '20202020-0566-476a-b4c4-a0f9781bd80a',
|
||||||
messageChannelMessageAssociation: '20202020-ad1e-4127-bccb-d83ae04d2ccb',
|
messageChannelMessageAssociation: '20202020-ad1e-4127-bccb-d83ae04d2ccb',
|
||||||
messageChannel: '20202020-fe8c-40bc-a681-b80b771449b7',
|
messageChannel: '20202020-fe8c-40bc-a681-b80b771449b7',
|
||||||
|
@ -7,6 +7,7 @@ import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/co
|
|||||||
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
|
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
|
||||||
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
||||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
|
import { FavoriteFolderWorkspaceEntity } from 'src/modules/favorite-folder/standard-objects/favorite-folder.workspace-entity';
|
||||||
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||||
@ -50,6 +51,7 @@ export const standardObjectMetadataDefinitions = [
|
|||||||
CompanyWorkspaceEntity,
|
CompanyWorkspaceEntity,
|
||||||
ConnectedAccountWorkspaceEntity,
|
ConnectedAccountWorkspaceEntity,
|
||||||
FavoriteWorkspaceEntity,
|
FavoriteWorkspaceEntity,
|
||||||
|
FavoriteFolderWorkspaceEntity,
|
||||||
TimelineActivityWorkspaceEntity,
|
TimelineActivityWorkspaceEntity,
|
||||||
ViewFieldWorkspaceEntity,
|
ViewFieldWorkspaceEntity,
|
||||||
ViewGroupWorkspaceEntity,
|
ViewGroupWorkspaceEntity,
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
|
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||||
|
import { FavoriteFolderDeletionListener } from 'src/modules/favorite-folder/listeners/favorite-folder.listener';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TwentyORMModule, FeatureFlagModule],
|
||||||
|
providers: [FavoriteFolderDeletionListener],
|
||||||
|
})
|
||||||
|
export class FavoriteFolderModule {}
|
@ -0,0 +1,48 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
|
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||||
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||||
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type';
|
||||||
|
import { FavoriteFolderWorkspaceEntity } from 'src/modules/favorite-folder/standard-objects/favorite-folder.workspace-entity';
|
||||||
|
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FavoriteFolderDeletionListener {
|
||||||
|
constructor(
|
||||||
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@OnEvent('favoriteFolder.deleted')
|
||||||
|
async handleDelete(
|
||||||
|
payload: WorkspaceEventBatch<
|
||||||
|
ObjectRecordDeleteEvent<FavoriteFolderWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
const isFavoriteFolderEntityEnabled =
|
||||||
|
await this.featureFlagService.isFeatureEnabled(
|
||||||
|
FeatureFlagKey.IsFavoriteFolderEntityEnabled,
|
||||||
|
payload.workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isFavoriteFolderEntityEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const eventPayload of payload.events) {
|
||||||
|
const favoriteRepository =
|
||||||
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace<FavoriteWorkspaceEntity>(
|
||||||
|
payload.workspaceId,
|
||||||
|
'favorite',
|
||||||
|
);
|
||||||
|
|
||||||
|
await favoriteRepository.update(
|
||||||
|
{ favoriteFolderId: eventPayload.recordId },
|
||||||
|
{ deletedAt: new Date().toISOString() },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||||
|
|
||||||
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
|
import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator';
|
||||||
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
|
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||||
|
import { FAVORITE_FOLDER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
|
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||||
|
|
||||||
|
@WorkspaceEntity({
|
||||||
|
standardId: STANDARD_OBJECT_IDS.favoriteFolder,
|
||||||
|
namePlural: 'favoriteFolders',
|
||||||
|
labelSingular: 'Favorite Folder',
|
||||||
|
labelPlural: 'Favorite Folders',
|
||||||
|
description: 'A Folder of favorites',
|
||||||
|
icon: 'IconFolder',
|
||||||
|
})
|
||||||
|
@WorkspaceIsSystem()
|
||||||
|
@WorkspaceGate({
|
||||||
|
featureFlag: FeatureFlagKey.IsFavoriteFolderEntityEnabled,
|
||||||
|
})
|
||||||
|
export class FavoriteFolderWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
|
@WorkspaceField({
|
||||||
|
standardId: FAVORITE_FOLDER_STANDARD_FIELD_IDS.position,
|
||||||
|
type: FieldMetadataType.NUMBER,
|
||||||
|
label: 'Position',
|
||||||
|
description: 'Favorite folder position',
|
||||||
|
icon: 'IconList',
|
||||||
|
defaultValue: 0,
|
||||||
|
})
|
||||||
|
position: number;
|
||||||
|
|
||||||
|
@WorkspaceField({
|
||||||
|
standardId: FAVORITE_FOLDER_STANDARD_FIELD_IDS.name,
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
label: 'Name',
|
||||||
|
description: 'Name of the favorite folder',
|
||||||
|
icon: 'IconText',
|
||||||
|
})
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@WorkspaceRelation({
|
||||||
|
standardId: FAVORITE_FOLDER_STANDARD_FIELD_IDS.favorites,
|
||||||
|
type: RelationMetadataType.ONE_TO_MANY,
|
||||||
|
label: 'Favorites',
|
||||||
|
description: 'Favorites in this folder',
|
||||||
|
icon: 'IconHeart',
|
||||||
|
inverseSideFieldKey: 'favoriteFolder',
|
||||||
|
inverseSideTarget: () => FavoriteWorkspaceEntity,
|
||||||
|
})
|
||||||
|
favorites: Relation<FavoriteWorkspaceEntity[]>;
|
||||||
|
}
|
@ -18,6 +18,7 @@ import { FAVORITE_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/worksp
|
|||||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
||||||
|
import { FavoriteFolderWorkspaceEntity } from 'src/modules/favorite-folder/standard-objects/favorite-folder.workspace-entity';
|
||||||
import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
|
import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
|
||||||
import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
|
import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
|
||||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||||
@ -95,6 +96,27 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
@WorkspaceJoinColumn('company')
|
@WorkspaceJoinColumn('company')
|
||||||
companyId: string;
|
companyId: string;
|
||||||
|
|
||||||
|
@WorkspaceRelation({
|
||||||
|
standardId: FAVORITE_STANDARD_FIELD_IDS.favoriteFolder,
|
||||||
|
type: RelationMetadataType.MANY_TO_ONE,
|
||||||
|
label: 'Favorite Folder',
|
||||||
|
description: 'The folder this favorite belongs to',
|
||||||
|
icon: 'IconFolder',
|
||||||
|
inverseSideTarget: () => FavoriteFolderWorkspaceEntity,
|
||||||
|
inverseSideFieldKey: 'favorites',
|
||||||
|
})
|
||||||
|
@WorkspaceGate({
|
||||||
|
featureFlag: FeatureFlagKey.IsFavoriteFolderEntityEnabled,
|
||||||
|
})
|
||||||
|
@WorkspaceIsNullable()
|
||||||
|
favoriteFolder: Relation<FavoriteFolderWorkspaceEntity> | null;
|
||||||
|
|
||||||
|
@WorkspaceJoinColumn('favoriteFolder')
|
||||||
|
@WorkspaceGate({
|
||||||
|
featureFlag: FeatureFlagKey.IsFavoriteFolderEntityEnabled,
|
||||||
|
})
|
||||||
|
favoriteFolderId: string;
|
||||||
|
|
||||||
@WorkspaceRelation({
|
@WorkspaceRelation({
|
||||||
standardId: FAVORITE_STANDARD_FIELD_IDS.opportunity,
|
standardId: FAVORITE_STANDARD_FIELD_IDS.opportunity,
|
||||||
type: RelationMetadataType.MANY_TO_ONE,
|
type: RelationMetadataType.MANY_TO_ONE,
|
||||||
|
@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
|
|
||||||
import { CalendarModule } from 'src/modules/calendar/calendar.module';
|
import { CalendarModule } from 'src/modules/calendar/calendar.module';
|
||||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||||
|
import { FavoriteFolderModule } from 'src/modules/favorite-folder/favorite-folder.module';
|
||||||
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
||||||
import { ViewModule } from 'src/modules/view/view.module';
|
import { ViewModule } from 'src/modules/view/view.module';
|
||||||
import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||||
@ -13,6 +14,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
|||||||
ConnectedAccountModule,
|
ConnectedAccountModule,
|
||||||
ViewModule,
|
ViewModule,
|
||||||
WorkflowModule,
|
WorkflowModule,
|
||||||
|
FavoriteFolderModule,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
exports: [],
|
exports: [],
|
||||||
|
@ -110,11 +110,7 @@ export const Avatar = ({
|
|||||||
<StyledAvatar
|
<StyledAvatar
|
||||||
size={size}
|
size={size}
|
||||||
backgroundColor={
|
backgroundColor={
|
||||||
Icon
|
Icon ? 'inherit' : showBackgroundColor ? fixedBackgroundColor : 'none'
|
||||||
? theme.background.tertiary
|
|
||||||
: showBackgroundColor
|
|
||||||
? fixedBackgroundColor
|
|
||||||
: 'none'
|
|
||||||
}
|
}
|
||||||
color={fixedColor}
|
color={fixedColor}
|
||||||
clickable={!isUndefined(onClick)}
|
clickable={!isUndefined(onClick)}
|
||||||
|
@ -131,6 +131,8 @@ export {
|
|||||||
IconFilterCog,
|
IconFilterCog,
|
||||||
IconFilterOff,
|
IconFilterOff,
|
||||||
IconFocusCentered,
|
IconFocusCentered,
|
||||||
|
IconFolder,
|
||||||
|
IconFolderPlus,
|
||||||
IconForbid,
|
IconForbid,
|
||||||
IconFunction,
|
IconFunction,
|
||||||
IconGauge,
|
IconGauge,
|
||||||
|
Loading…
Reference in New Issue
Block a user