diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx index 2ecf676cf6..f61efba877 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx @@ -285,6 +285,7 @@ export const ObjectOptionsDropdownCustomView = ({ LeftIcon={IconTrash} text={t`Delete view`} disabled={isDefaultView || isLastView} + accent="danger" /> diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemWithOptionDropdown.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemWithOptionDropdown.tsx index 7a3d40cb2b..4830f12307 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemWithOptionDropdown.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemWithOptionDropdown.tsx @@ -44,6 +44,7 @@ export type MenuItemWithOptionDropdownProps = { text: ReactNode; hasSubMenu?: boolean; dropdownPlacement?: Placement; + selected?: boolean; }; // TODO: refactor this @@ -62,6 +63,7 @@ export const MenuItemWithOptionDropdown = ({ text, hasSubMenu = false, dropdownPlacement = 'bottom-end', + selected = false, }: MenuItemWithOptionDropdownProps) => { const { theme } = useContext(ThemeContext); const handleMenuItemClick = (event: MouseEvent) => { @@ -81,6 +83,7 @@ export const MenuItemWithOptionDropdown = ({ isIconDisplayedOnHoverOnly={isIconDisplayedOnHoverOnly} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} + focused={selected} > diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePerformViewAPIUpdate.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePerformViewAPIUpdate.ts index 99edd3454a..6e82e78fad 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePerformViewAPIUpdate.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePerformViewAPIUpdate.ts @@ -1,10 +1,14 @@ import { useCallback } from 'react'; import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; +import { metadataStoreState } from '@/metadata-store/states/metadataStoreState'; +import { type FlatView } from '@/metadata-store/types/FlatView'; import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { CombinedGraphQLErrors } from '@apollo/client/errors'; import { t } from '@lingui/core/macro'; +import { useStore } from 'jotai'; import { CrudOperationType } from 'twenty-shared/types'; import { useMutation } from '@apollo/client/react'; import { @@ -15,15 +19,27 @@ import { export const usePerformViewAPIUpdate = () => { const [updateViewMutation] = useMutation(UpdateViewDocument); + const { updateInDraft, applyChanges } = useUpdateMetadataStoreDraft(); + const { handleMetadataError } = useMetadataErrorHandler(); const { enqueueErrorSnackBar } = useSnackBar(); + const store = useStore(); + const performViewAPIUpdate = useCallback( async ( variables: UpdateViewMutationVariables, ): Promise< MetadataRequestResult>> > => { + const viewsStoreAtom = metadataStoreState.atomFamily('views'); + const previousViewsEntry = store.get(viewsStoreAtom); + + updateInDraft('views', [ + { id: variables.id, ...variables.input } as FlatView, + ]); + applyChanges(); + try { const result = await updateViewMutation({ variables, @@ -34,6 +50,8 @@ export const usePerformViewAPIUpdate = () => { response: result, }; } catch (error) { + store.set(viewsStoreAtom, previousViewsEntry); + if (CombinedGraphQLErrors.is(error)) { handleMetadataError(error, { primaryMetadataName: 'view', @@ -49,7 +67,14 @@ export const usePerformViewAPIUpdate = () => { }; } }, - [updateViewMutation, handleMetadataError, enqueueErrorSnackBar], + [ + updateViewMutation, + handleMetadataError, + enqueueErrorSnackBar, + updateInDraft, + applyChanges, + store, + ], ); return { performViewAPIUpdate }; diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEditMode.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEditMode.tsx index d807796a4b..66401992f1 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEditMode.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEditMode.tsx @@ -60,7 +60,7 @@ export const ViewPickerContentEditMode = () => { setViewPickerSelectedIcon(iconKey); }; - const handleClose = async () => { + const handleGoBack = async () => { await updateViewFromCurrentState(); setViewPickerMode('list'); @@ -71,7 +71,7 @@ export const ViewPickerContentEditMode = () => { } diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx index efd3df25a3..cd95d9025f 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx @@ -146,6 +146,7 @@ export const ViewPickerListContent = () => { onDragEnd={handleWorkspaceDragEnd} draggableItems={workspaceViews.map((view, index) => { const isIndexView = view.key === 'INDEX'; + const isCurrentView = currentView?.id === view.id; return ( { isIndexView={isIndexView} isLastView={isLastView} onEdit={handleEditViewButtonClick} + isCurrentView={isCurrentView} /> } /> @@ -179,6 +181,7 @@ export const ViewPickerListContent = () => { onDragEnd={handleUnlistedDragEnd} draggableItems={unlistedViews.map((view, index) => { const isIndexView = view.key === 'INDEX'; + const isCurrentView = currentView?.id === view.id; return ( { isIndexView={isIndexView} isLastView={isLastView} onEdit={handleEditViewButtonClick} + isCurrentView={isCurrentView} /> } /> diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerOptionDropdown.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerOptionDropdown.tsx index a1392967e7..50f1ca2d74 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerOptionDropdown.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerOptionDropdown.tsx @@ -1,4 +1,5 @@ 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'; @@ -15,6 +16,7 @@ import { isDefined } from 'twenty-shared/utils'; import { v4 as uuidv4 } from 'uuid'; import { IconHeart, + IconHeartOff, IconLock, IconPencil, IconTrash, @@ -35,6 +37,7 @@ type ViewPickerOptionDropdownProps = { >; onEdit: (event: React.MouseEvent, viewId: string) => void; handleViewSelect: (viewId: string) => void; + isCurrentView: boolean; }; export const ViewPickerOptionDropdown = ({ @@ -43,6 +46,7 @@ export const ViewPickerOptionDropdown = ({ onEdit, view, handleViewSelect, + isCurrentView, }: ViewPickerOptionDropdownProps) => { const dropdownId = `view-picker-options-${view.id}`; @@ -59,16 +63,19 @@ export const ViewPickerOptionDropdown = ({ 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 isFavorite = navigationMenuItems.some( + const currentNavigationMenuItem = navigationMenuItems.find( (item) => item.viewId === view.id && item.userWorkspaceId === currentUserWorkspaceId, ); + const isFavorite = isDefined(currentNavigationMenuItem); const handleDelete = () => { setViewPickerReferenceViewId(view.id); @@ -76,7 +83,7 @@ export const ViewPickerOptionDropdown = ({ closeDropdown(dropdownId); }; - const handleAddToFavorites = () => { + const handleToggleFavorite = () => { if (!isFavorite) { const relevantItems = navigationMenuItems.filter( (item) => !isDefined(item.folderId) && isDefined(item.userWorkspaceId), @@ -96,6 +103,8 @@ export const ViewPickerOptionDropdown = ({ position: maxPosition + 1, }, ]); + } else { + deleteManyNavigationMenuItems([currentNavigationMenuItem.id]); } closeDropdown(dropdownId); }; @@ -120,42 +129,32 @@ export const ViewPickerOptionDropdown = ({ RightIcon={getVisibilityIcon()} dropdownPlacement="bottom-start" dropdownId={`view-picker-options-${view.id}`} + selected={isCurrentView} dropdownContent={ - {isIndexView ? ( - - ) : ( + + {!isIndexView && canEditView && ( <> { + onEdit(event, view.id); + closeDropdown(dropdownId); + }} /> - - {canEditView && ( - <> - { - onEdit(event, view.id); - closeDropdown(dropdownId); - }} - /> - {!isLastView && ( - - )} - + {!isLastView && ( + )} )} diff --git a/packages/twenty-front/src/modules/views/view-picker/hooks/useUpdateViewFromCurrentState.ts b/packages/twenty-front/src/modules/views/view-picker/hooks/useUpdateViewFromCurrentState.ts index 43ca24a3e6..fb54c8140b 100644 --- a/packages/twenty-front/src/modules/views/view-picker/hooks/useUpdateViewFromCurrentState.ts +++ b/packages/twenty-front/src/modules/views/view-picker/hooks/useUpdateViewFromCurrentState.ts @@ -8,6 +8,7 @@ import { useCloseAndResetViewPicker } from '@/views/view-picker/hooks/useCloseAn import { viewPickerInputNameComponentState } from '@/views/view-picker/states/viewPickerInputNameComponentState'; import { viewPickerIsDirtyComponentState } from '@/views/view-picker/states/viewPickerIsDirtyComponentState'; import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states/viewPickerIsPersistingComponentState'; +import { viewPickerModeComponentState } from '@/views/view-picker/states/viewPickerModeComponentState'; import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState'; import { viewPickerSelectedIconComponentState } from '@/views/view-picker/states/viewPickerSelectedIconComponentState'; import { viewPickerVisibilityComponentState } from '@/views/view-picker/states/viewPickerVisibilityComponentState'; @@ -30,6 +31,10 @@ export const useUpdateViewFromCurrentState = () => { viewPickerIsDirtyComponentState, ); + const viewPickerModeCallbackState = useAtomComponentStateCallbackState( + viewPickerModeComponentState, + ); + const viewPickerReferenceViewIdCallbackState = useAtomComponentStateCallbackState(viewPickerReferenceViewIdComponentState); @@ -49,7 +54,7 @@ export const useUpdateViewFromCurrentState = () => { store.set(viewPickerIsPersistingCallbackState, true); store.set(viewPickerIsDirtyCallbackState, false); - closeAndResetViewPicker(); + store.set(viewPickerModeCallbackState, 'list'); const viewPickerReferenceViewId = store.get( viewPickerReferenceViewIdCallbackState, @@ -60,18 +65,23 @@ export const useUpdateViewFromCurrentState = () => { ); const visibility = store.get(viewPickerVisibilityCallbackState); - await performViewAPIUpdate({ - id: viewPickerReferenceViewId, - input: { - name: viewPickerInputName, - icon: viewPickerSelectedIcon, - visibility: visibility, - }, - }); + try { + await performViewAPIUpdate({ + id: viewPickerReferenceViewId, + input: { + name: viewPickerInputName, + icon: viewPickerSelectedIcon, + visibility: visibility, + }, + }); + } finally { + store.set(viewPickerIsPersistingCallbackState, false); + } }, [ canPersistChanges, viewPickerIsPersistingCallbackState, viewPickerIsDirtyCallbackState, + viewPickerModeCallbackState, closeAndResetViewPicker, viewPickerReferenceViewIdCallbackState, viewPickerInputNameCallbackState,