Skip to content

Commit

Permalink
Favorite folders (#7998)
Browse files Browse the repository at this point in the history
closes - #5755

---------

Co-authored-by: martmull <[email protected]>
Co-authored-by: Lucas Bordeau <[email protected]>
Co-authored-by: Charles Bochet <[email protected]>
  • Loading branch information
4 people authored Nov 18, 2024
1 parent 5115022 commit 0125d58
Show file tree
Hide file tree
Showing 100 changed files with 23,995 additions and 21,450 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { contextStoreFiltersComponentState } from '@/context-store/states/contex
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
Expand Down Expand Up @@ -36,7 +37,8 @@ export const DeleteRecordsActionEffect = ({
objectNameSingular: objectMetadataItem.nameSingular,
});

const { favorites, deleteFavorite } = useFavorites();
const favorites = useFavorites();
const deleteFavorite = useDeleteFavorite();

const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
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 { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
Expand All @@ -21,7 +23,11 @@ export const ManageFavoritesActionEffect = ({
contextStoreTargetedRecordsRuleComponentState,
);

const { favorites, createFavorite, deleteFavorite } = useFavorites();
const favorites = useFavorites();

const createFavorite = useCreateFavorite();

const deleteFavorite = useDeleteFavorite();

const selectedRecordId =
contextStoreTargetedRecordsRule.mode === 'selection'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBa

export const RecordShowActionMenu = ({
isFavorite,
handleFavoriteButtonClick,
record,
objectMetadataItem,
objectNameSingular,
handleFavoriteButtonClick,
}: {
isFavorite: boolean;
handleFavoriteButtonClick: () => void;
record: ObjectRecord | undefined;
objectMetadataItem: ObjectMetadataItem;
objectNameSingular: string;
handleFavoriteButtonClick: () => void;
}) => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
Expand All @@ -40,10 +40,10 @@ export const RecordShowActionMenu = ({
<RecordShowPageBaseHeader
{...{
isFavorite,
handleFavoriteButtonClick,
record,
objectMetadataItem,
objectNameSingular,
handleFavoriteButtonClick,
}}
/>
<ActionMenuConfirmationModals />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const mocks: MockedResponse[] = [
companyId
createdAt
deletedAt
favoriteFolderId
id
noteId
opportunityId
Expand Down
Original file line number Diff line number Diff line change
@@ -1,127 +1,197 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { Avatar, isDefined } from 'twenty-ui';

import { FavoritesSkeletonLoader } from '@/favorites/components/FavoritesSkeletonLoader';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { FavoriteFolderNavigationDrawerItemDropdown } from '@/favorites/components/FavoriteFolderNavigationDrawerItemDropdown';
import { FavoriteIcon } from '@/favorites/components/FavoriteIcon';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useDeleteFavoriteFolder } from '@/favorites/hooks/useDeleteFavoriteFolder';
import { useRenameFavoriteFolder } from '@/favorites/hooks/useRenameFavoriteFolder';
import { useReorderFavorite } from '@/favorites/hooks/useReorderFavorite';
import { activeFavoriteFolderIdState } from '@/favorites/states/activeFavoriteFolderIdState';
import { isLocationMatchingFavorite } from '@/favorites/utils/isLocationMatchingFavorite';
import { ProcessedFavorite } from '@/favorites/utils/sortFavorites';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';

import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useFavorites } from '../hooks/useFavorites';

const StyledContainer = styled(NavigationDrawerSection)`
width: 100%;
`;

const StyledAvatar = styled(Avatar)`
:hover {
cursor: grab;
}
`;

const StyledNavigationDrawerItem = styled(NavigationDrawerItem)`
:active {
cursor: grabbing;
.fav-avatar:hover {
cursor: grabbing;
}
}
`;

export const CurrentWorkspaceMemberFavorites = () => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);

const { favorites, handleReorderFavorite } = useFavorites();
const loading = useIsPrefetchLoading();
const { toggleNavigationSection, isNavigationSectionOpenState } =
useNavigationSection('Favorites');
const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState);

if (loading && isDefined(currentWorkspaceMember)) {
return <FavoritesSkeletonLoader />;
}
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useRecoilState } from 'recoil';
import { IconFolder, IconHeartOff, LightIconButton } from 'twenty-ui';

type CurrentWorkspaceMemberFavoritesProps = {
folder: {
folderId: string;
folderName: string;
favorites: ProcessedFavorite[];
};
isGroup: boolean;
};

const currentWorkspaceMemberFavorites = favorites.filter(
(favorite) => favorite.workspaceMemberId === currentWorkspaceMember?.id,
export const CurrentWorkspaceMemberFavorites = ({
folder,
isGroup,
}: CurrentWorkspaceMemberFavoritesProps) => {
const currentPath = useLocation().pathname;
const currentViewPath = useLocation().pathname + useLocation().search;

const [isFavoriteFolderRenaming, setIsFavoriteFolderRenaming] =
useState(false);
const [favoriteFolderName, setFavoriteFolderName] = useState(
folder.folderName,
);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [activeFavoriteFolderId, setActiveFavoriteFolderId] = useRecoilState(
activeFavoriteFolderIdState,
);
const isOpen = activeFavoriteFolderId === folder.folderId;

const handleToggle = () => {
setActiveFavoriteFolderId(isOpen ? null : folder.folderId);
};

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 deleteFavorite = useDeleteFavorite();

const favoriteFolderContentLength = folder.favorites.length;

const handleSubmitRename = async (value: string) => {
if (value === '') return;
await renameFavoriteFolder(folder.folderId, value);
setIsFavoriteFolderRenaming(false);
return true;
};

const handleCancelRename = () => {
setFavoriteFolderName(folder.folderName);
setIsFavoriteFolderRenaming(false);
};

const handleClickOutside = async (
event: MouseEvent | TouchEvent,
value: string,
) => {
if (!value) {
setIsFavoriteFolderRenaming(false);
return;
}

if (
!currentWorkspaceMemberFavorites ||
currentWorkspaceMemberFavorites.length === 0
)
return <></>;

const isGroup = currentWorkspaceMemberFavorites.length > 1;

const draggableListContent = (
<DraggableList
onDragEnd={handleReorderFavorite}
draggableItems={
<>
{currentWorkspaceMemberFavorites.map((favorite, index) => {
const {
id,
labelIdentifier,
avatarUrl,
avatarType,
link,
recordId,
} = favorite;

return (
<DraggableItem
key={id}
draggableId={id}
index={index}
itemComponent={
<StyledNavigationDrawerItem
key={id}
label={labelIdentifier}
Icon={() => (
<StyledAvatar
placeholderColorSeed={recordId}
avatarUrl={avatarUrl}
type={avatarType}
placeholder={labelIdentifier}
className="fav-avatar"
/>
)}
to={link}
/>
}
/>
);
})}
</>
}
await renameFavoriteFolder(folder.folderId, value);
setIsFavoriteFolderRenaming(false);
};

const handleFavoriteFolderDelete = async () => {
if (folder.favorites.length > 0) {
setIsDeleteModalOpen(true);
closeFavoriteFolderEditDropdown();
} else {
await deleteFavoriteFolder(folder.folderId);
closeFavoriteFolderEditDropdown();
}
};

const handleConfirmDelete = async () => {
await deleteFavoriteFolder(folder.folderId);
setIsDeleteModalOpen(false);
};

const rightOptions = (
<FavoriteFolderNavigationDrawerItemDropdown
folderId={folder.folderId}
onRename={() => setIsFavoriteFolderRenaming(true)}
onDelete={handleFavoriteFolderDelete}
closeDropdown={closeFavoriteFolderEditDropdown}
/>
);

return (
<StyledContainer>
<NavigationDrawerAnimatedCollapseWrapper>
<NavigationDrawerSectionTitle
label="Favorites"
onClick={() => toggleNavigationSection()}
/>
</NavigationDrawerAnimatedCollapseWrapper>

{isNavigationSectionOpen && (
<ScrollWrapper contextProviderName="navigationDrawer">
<NavigationDrawerItemsCollapsedContainer isGroup={isGroup}>
{draggableListContent}
</NavigationDrawerItemsCollapsedContainer>
</ScrollWrapper>
)}
</StyledContainer>
<>
<NavigationDrawerItemsCollapsableContainer
key={folder.folderId}
isGroup={isGroup}
>
{isFavoriteFolderRenaming ? (
<NavigationDrawerInput
Icon={IconFolder}
value={favoriteFolderName}
onChange={setFavoriteFolderName}
onSubmit={handleSubmitRename}
onCancel={handleCancelRename}
onClickOutside={handleClickOutside}
hotkeyScope="favorites-folder-input"
/>
) : (
<NavigationDrawerItem
key={folder.folderId}
label={folder.folderName}
Icon={IconFolder}
onClick={handleToggle}
rightOptions={rightOptions}
className="navigation-drawer-item"
active={isFavoriteFolderEditDropdownOpen}
/>
)}

{isOpen && (
<DraggableList
onDragEnd={handleReorderFavorite}
draggableItems={
<>
{folder.favorites.map((favorite, index) => (
<DraggableItem
key={favorite.id}
draggableId={favorite.id}
index={index}
itemComponent={
<NavigationDrawerSubItem
key={favorite.id}
label={favorite.labelIdentifier}
Icon={() => <FavoriteIcon favorite={favorite} />}
to={favorite.link}
active={index === selectedFavoriteIndex}
subItemState={getNavigationSubItemLeftAdornment({
index,
arrayLength: favoriteFolderContentLength,
selectedIndex: selectedFavoriteIndex,
})}
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={() => deleteFavorite(favorite.id)}
accent="tertiary"
/>
}
isDraggable
/>
}
/>
))}
</>
}
/>
)}
</NavigationDrawerItemsCollapsableContainer>

<ConfirmationModal
isOpen={isDeleteModalOpen}
setIsOpen={setIsDeleteModalOpen}
title={`Remove ${folder.favorites.length} ${folder.favorites.length > 1 ? 'favorites' : 'favorite'}?`}
subtitle={`This action will delete this favorite folder ${folder.favorites.length > 1 ? `and all ${folder.favorites.length} favorites` : 'and the favorite'} inside. Do you want to continue?`}
onConfirmClick={handleConfirmDelete}
deleteButtonText="Delete Folder"
/>
</>
);
};
Loading

0 comments on commit 0125d58

Please sign in to comment.