Files
twenty/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerOptionDropdown.tsx
T
Priyanshu Bartwal 4c966bfc32 [Twenty-front]: Bunch of View Picker Fixes and improvements. (#21290)
While working on #21208, I found a few related improvements and fixes
that were worth including in this PR.

1. Improved View Picker UX:
- Added optimistic updates when selecting a view from both the
drag-and-drop view picker
- Added optimistic updates when editing view. Before it used to close
the whole dropdown.
- Added highlighting for the currently selected view.
- Before:



https://github.com/user-attachments/assets/469fc60c-e65f-4452-a5a4-7df6188ab19d


- After:


https://github.com/user-attachments/assets/d3b151c1-0c10-45e7-a796-b5e6061c898d



2. Remove Favorites from the View Picker
- Added support for removing a favorite directly from the view picker
without needing to open additional menus.
- Before:


https://github.com/user-attachments/assets/70437fb9-d4c1-488b-aab9-0ea92d1bad99



- After:


https://github.com/user-attachments/assets/442546bd-24ae-43d5-abe1-268ef3ff6475

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-22 10:56:34 +02:00

168 lines
5.7 KiB
TypeScript

import { useCreateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useCreateManyNavigationMenuItems';
import { useDeleteManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems';
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { MenuItemWithOptionDropdown } from '@/ui/navigation/menu-item/components/MenuItemWithOptionDropdown';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { type View } from '@/views/types/View';
import { useDestroyViewFromCurrentState } from '@/views/view-picker/hooks/useDestroyViewFromCurrentState';
import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState';
import { useLingui } from '@lingui/react/macro';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
import {
IconHeart,
IconHeartOff,
IconLock,
IconPencil,
IconTrash,
useIcons,
} from 'twenty-ui/icon';
import { MenuItem } from 'twenty-ui/navigation';
import {
PermissionFlagType,
ViewVisibility,
} from '~/generated-metadata/graphql';
type ViewPickerOptionDropdownProps = {
isIndexView: boolean;
isLastView: boolean;
view: Pick<
View,
'id' | 'name' | 'icon' | 'visibility' | 'createdByUserWorkspaceId'
>;
onEdit: (event: React.MouseEvent<HTMLElement>, viewId: string) => void;
handleViewSelect: (viewId: string) => void;
isCurrentView: boolean;
};
export const ViewPickerOptionDropdown = ({
isIndexView,
isLastView,
onEdit,
view,
handleViewSelect,
isCurrentView,
}: ViewPickerOptionDropdownProps) => {
const dropdownId = `view-picker-options-${view.id}`;
const { t } = useLingui();
const { closeDropdown } = useCloseDropdown();
const { getIcon } = useIcons();
const { destroyViewFromCurrentState } = useDestroyViewFromCurrentState();
const setViewPickerReferenceViewId = useSetAtomComponentState(
viewPickerReferenceViewIdComponentState,
);
const hasViewsPermission = useHasPermissionFlag(PermissionFlagType.VIEWS);
const { createManyNavigationMenuItems } = useCreateManyNavigationMenuItems();
const { navigationMenuItems, currentUserWorkspaceId } =
useNavigationMenuItemsData();
const { deleteManyNavigationMenuItems } = useDeleteManyNavigationMenuItems();
// Users with VIEWS permission can edit all views
// Users without VIEWS permission can only edit unlisted views (which are always their own, filtered by backend)
const canEditView =
hasViewsPermission || view.visibility === ViewVisibility.UNLISTED;
const currentNavigationMenuItem = navigationMenuItems.find(
(item) =>
item.viewId === view.id &&
item.userWorkspaceId === currentUserWorkspaceId,
);
const isFavorite = isDefined(currentNavigationMenuItem);
const handleDelete = () => {
setViewPickerReferenceViewId(view.id);
destroyViewFromCurrentState();
closeDropdown(dropdownId);
};
const handleToggleFavorite = () => {
if (!isFavorite) {
const relevantItems = navigationMenuItems.filter(
(item) => !isDefined(item.folderId) && isDefined(item.userWorkspaceId),
);
const maxPosition = Math.max(
...relevantItems.map((item) => item.position),
0,
);
createManyNavigationMenuItems([
{
id: uuidv4(),
type: NavigationMenuItemType.VIEW,
viewId: view.id,
userWorkspaceId: currentUserWorkspaceId,
position: maxPosition + 1,
},
]);
} else {
deleteManyNavigationMenuItems([currentNavigationMenuItem.id]);
}
closeDropdown(dropdownId);
};
const getVisibilityIcon = () => {
if (isIndexView) {
return IconLock;
}
return null;
};
const shouldShowIconAlways = isIndexView;
return (
<>
<MenuItemWithOptionDropdown
text={view.name}
LeftIcon={getIcon(view.icon)}
onClick={() => handleViewSelect(view.id)}
isIconDisplayedOnHoverOnly={!shouldShowIconAlways}
RightIcon={getVisibilityIcon()}
dropdownPlacement="bottom-start"
dropdownId={`view-picker-options-${view.id}`}
selected={isCurrentView}
dropdownContent={
<DropdownContent>
<DropdownMenuItemsContainer>
<MenuItem
LeftIcon={isFavorite ? IconHeartOff : IconHeart}
text={isFavorite ? t`Remove Favorite` : t`Add to Favorite`}
onClick={handleToggleFavorite}
/>
{!isIndexView && canEditView && (
<>
<MenuItem
LeftIcon={IconPencil}
text={t`Edit`}
onClick={(event) => {
onEdit(event, view.id);
closeDropdown(dropdownId);
}}
/>
{!isLastView && (
<MenuItem
LeftIcon={IconTrash}
text={t`Delete`}
onClick={handleDelete}
accent="danger"
/>
)}
</>
)}
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
</>
);
};