diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts index fe71592a34..6b00e0c92e 100644 --- a/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts +++ b/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts @@ -9,6 +9,7 @@ import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/comm import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector'; import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState'; import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; +import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState'; import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector'; import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems'; @@ -60,6 +61,7 @@ export const useEnterLayoutCustomizationMode = () => { prefetchNavigationMenuItems, ); store.set(navigationMenuItemsDraftState.atom, workspaceNavigationMenuItems); + store.set(navigationMenuItemEditSectionState.atom, 'workspace'); const persistedCommandMenuItems = store.get(commandMenuItemsSelector.atom); store.set(commandMenuItemsDraftState.atom, persistedCommandMenuItems); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/states/isNavigationMenuItemFolderCreatingState.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/states/isNavigationMenuItemFolderCreatingState.ts deleted file mode 100644 index 5255d1fd75..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/states/isNavigationMenuItemFolderCreatingState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState'; - -export const isNavigationMenuItemFolderCreatingState = createAtomState( - { - key: 'isNavigationMenuItemFolderCreatingState', - defaultValue: false, - }, -); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/states/navigationMenuItemEditSectionState.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/states/navigationMenuItemEditSectionState.ts new file mode 100644 index 0000000000..f7609e3f27 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/states/navigationMenuItemEditSectionState.ts @@ -0,0 +1,9 @@ +import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection'; + +import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState'; + +export const navigationMenuItemEditSectionState = + createAtomState({ + key: 'navigationMenuItemEditSectionState', + defaultValue: 'workspace', + }); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/__tests__/computeInsertIndexAndPosition.test.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/__tests__/computeInsertIndexAndPosition.test.ts index 4a69cafc4b..5e13cf7629 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/__tests__/computeInsertIndexAndPosition.test.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/__tests__/computeInsertIndexAndPosition.test.ts @@ -18,7 +18,7 @@ describe('computeInsertIndexAndPosition', () => { expect(between.position).toBe(15); }); - it('should only consider items in target folder and exclude userWorkspaceId', () => { + it('should only consider items in the target folder', () => { const draft: NavigationMenuItem[] = [ { id: '1', folderId: 'folder-a', position: 10 } as NavigationMenuItem, { id: '2', folderId: 'folder-b', position: 20 } as NavigationMenuItem, @@ -27,9 +27,16 @@ describe('computeInsertIndexAndPosition', () => { const result = computeInsertIndexAndPosition(draft, 'folder-b', 1); expect(result.flatIndex).toBe(2); expect(result.position).toBe(25); + }); - const withWorkspace: NavigationMenuItem[] = [ - { id: '1', folderId: null, position: 10 } as NavigationMenuItem, + it('should consider personal items, as callers pass a section-scoped list', () => { + const personalItems: NavigationMenuItem[] = [ + { + id: '1', + folderId: null, + position: 10, + userWorkspaceId: 'ws-1', + } as NavigationMenuItem, { id: '2', folderId: null, @@ -37,7 +44,8 @@ describe('computeInsertIndexAndPosition', () => { userWorkspaceId: 'ws-1', } as NavigationMenuItem, ]; - const excluded = computeInsertIndexAndPosition(withWorkspace, null, 1); - expect(excluded.position).toBe(10.5); + const inserted = computeInsertIndexAndPosition(personalItems, null, 1); + expect(inserted.flatIndex).toBe(1); + expect(inserted.position).toBe(15); }); }); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/buildNavigationMenuLinkUpdate.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/buildNavigationMenuLinkUpdate.ts new file mode 100644 index 0000000000..295ede8f0e --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/buildNavigationMenuLinkUpdate.ts @@ -0,0 +1,20 @@ +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +// Normalizes a link rename/url edit: a blank name falls back to "Link", and a +// blank url is ignored so the existing link is kept. +export const buildNavigationMenuLinkUpdate = (updates: { + name?: string; + link?: string; +}): Partial => { + const normalized: Partial = {}; + + if (updates.name !== undefined) { + normalized.name = updates.name.trim() || 'Link'; + } + + if (updates.link !== undefined && updates.link.trim() !== '') { + normalized.link = updates.link.trim(); + } + + return normalized; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/computeInsertIndexAndPosition.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/computeInsertIndexAndPosition.ts index 87486681c0..1c4e284846 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/computeInsertIndexAndPosition.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/computeInsertIndexAndPosition.ts @@ -2,24 +2,20 @@ import { isDefined } from 'twenty-shared/utils'; import type { NavigationMenuItem } from '~/generated-metadata/graphql'; export const computeInsertIndexAndPosition = ( - currentDraft: NavigationMenuItem[], + items: NavigationMenuItem[], targetFolderId: string | null, targetIndex: number, ) => { - const itemsInFolder = currentDraft - .filter( - (item) => - (item.folderId ?? null) === targetFolderId && - !isDefined(item.userWorkspaceId), - ) + const itemsInFolder = items + .filter((item) => (item.folderId ?? null) === targetFolderId) .sort((a, b) => a.position - b.position); const insertRef = itemsInFolder[targetIndex]; const lastInFolder = itemsInFolder[itemsInFolder.length - 1]; const flatIndex = isDefined(insertRef) - ? currentDraft.indexOf(insertRef) + ? items.indexOf(insertRef) : isDefined(lastInFolder) - ? currentDraft.indexOf(lastInFolder) + 1 - : currentDraft.length; + ? items.indexOf(lastInFolder) + 1 + : items.length; const prevPosition = itemsInFolder[targetIndex - 1]?.position ?? 0; const nextPosition = itemsInFolder[targetIndex]?.position ?? prevPosition + 1; const position = (prevPosition + nextPosition) / 2; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/flattenNavigationMenuItemsWithFolderChildren.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/flattenNavigationMenuItemsWithFolderChildren.ts new file mode 100644 index 0000000000..b48bfd06ab --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/flattenNavigationMenuItemsWithFolderChildren.ts @@ -0,0 +1,28 @@ +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; + +type FolderWithChildren = { + id: string; + navigationMenuItems: NavigationMenuItem[]; +}; + +// Expand a display-ordered top-level list (orphans + folders) into a flat list +// where each folder is immediately followed by its children. +export const flattenNavigationMenuItemsWithFolderChildren = ( + topLevelItems: NavigationMenuItem[], + foldersWithChildren: FolderWithChildren[], +): NavigationMenuItem[] => { + const childrenByFolderId = new Map( + foldersWithChildren.map((folder) => [ + folder.id, + folder.navigationMenuItems, + ]), + ); + + return topLevelItems.flatMap((item) => + isNavigationMenuItemFolder(item) + ? [item, ...(childrenByFolderId.get(item.id) ?? [])] + : [item], + ); +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop.ts index 97f0b7dfa9..694f1136fd 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop.ts @@ -1,45 +1,37 @@ import { t } from '@lingui/core/macro'; +import { useStore } from 'jotai'; import { useCallback } from 'react'; -import { isDefined } from 'twenty-shared/utils'; +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { isDefined, normalizeUrl } from 'twenty-shared/utils'; import { IconFolder, IconLink, useIcons } from 'twenty-ui/display'; import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode'; +import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/common/constants/AddToNavSourceDroppableId'; +import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder'; +import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorLink'; import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState'; -import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; +import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState'; import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState'; import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn'; import { getObjectMetadataIdsInDraft } from '@/navigation-menu-item/common/utils/getObjectMetadataIdsInDraft'; import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/common/utils/validateAndExtractWorkspaceFolderId'; -import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/edit/folder/hooks/useAddFolderToNavigationMenuDraft'; -import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState'; +import { + type NewNavigationMenuItemInput, + useNavigationMenuItemEditController, +} from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; -import { useAddLinkToNavigationMenuDraft } from '@/navigation-menu-item/edit/link/hooks/useAddLinkToNavigationMenuDraft'; -import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/edit/object/hooks/useAddObjectToNavigationMenuDraft'; -import { useAddRecordToNavigationMenuDraft } from '@/navigation-menu-item/edit/record/hooks/useAddRecordToNavigationMenuDraft'; -import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/edit/view/hooks/useAddViewToNavigationMenuDraft'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { viewsSelector } from '@/views/states/selectors/viewsSelector'; -import { useStore } from 'jotai'; -import { NavigationMenuItemType } from 'twenty-shared/types'; - import type { NavigationMenuItemDropResult } from '@/navigation-menu-item/common/types/navigationMenuItemDropResult'; export const useHandleAddToNavigationDrop = () => { const store = useStore(); - const { addObjectToDraft } = useAddObjectToNavigationMenuDraft(); - const { addViewToDraft } = useAddViewToNavigationMenuDraft(); - const { addRecordToDraft } = useAddRecordToNavigationMenuDraft(); - const { addFolderToDraft } = useAddFolderToNavigationMenuDraft(); - const { addLinkToDraft } = useAddLinkToNavigationMenuDraft(); - const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState(); - const navigationMenuItemsDraft = useAtomStateValue( - navigationMenuItemsDraftState, - ); + const { currentItems, createItem } = useNavigationMenuItemEditController(); const { openNavigationMenuItemInSidePanel } = useOpenNavigationMenuItemInSidePanel(); const { objectMetadataItems } = useObjectMetadataItems(); @@ -52,6 +44,12 @@ export const useHandleAddToNavigationDrop = () => { const handleAddToNavigationDrop = useCallback( (result: NavigationMenuItemDropResult) => { + // Drag-to-add always targets the workspace sidebar; favorites are added + // by click only. + if (store.get(navigationMenuItemEditSectionState.atom) === 'favorite') { + return; + } + const { source, destination, draggableId } = result; if ( source.droppableId !== ADD_TO_NAV_SOURCE_DROPPABLE_ID || @@ -70,9 +68,6 @@ export const useHandleAddToNavigationDrop = () => { return; } - const currentDraft = isDefined(navigationMenuItemsDraft) - ? navigationMenuItemsDraft - : workspaceNavigationMenuItems; const folderId = validateAndExtractWorkspaceFolderId( destination.droppableId, ); @@ -88,74 +83,93 @@ export const useHandleAddToNavigationDrop = () => { ); } - const openEditForNewNavItem = ( - newItemId: string, - options: Omit< + // Enter customization mode first: it (re)seeds the workspace draft from + // the live items, so creating beforehand would have the new item + // overwritten. It also bails without enabling the mode when a dashboard + // is mid-edit, so skip the create unless the mode is actually on — + // otherwise the item lands in a draft that is neither shown nor saved. + const addToWorkspaceAndOpenEdit = ( + input: NewNavigationMenuItemInput, + position: { targetFolderId: string | null; targetIndex: number }, + openOptions: Omit< Parameters[0], 'itemId' >, ) => { enterLayoutCustomizationMode(); - openNavigationMenuItemInSidePanel({ ...options, itemId: newItemId }); + if (!store.get(isLayoutCustomizationModeEnabledState.atom)) { + return; + } + const newItemId = createItem(input, position); + openNavigationMenuItemInSidePanel({ + ...openOptions, + itemId: newItemId, + }); }; switch (payload.type) { case NavigationMenuItemType.FOLDER: { - const newFolderId = addFolderToDraft( - payload.name, - currentDraft, - null, - index, + addToWorkspaceAndOpenEdit( + { + type: NavigationMenuItemType.FOLDER, + name: payload.name, + color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER, + }, + { targetFolderId: null, targetIndex: index }, + { + pageTitle: t`Edit folder`, + pageIcon: IconFolder, + focusTitleInput: true, + }, ); - openEditForNewNavItem(newFolderId, { - pageTitle: t`Edit folder`, - pageIcon: IconFolder, - focusTitleInput: true, - }); return; } case NavigationMenuItemType.LINK: { - const newLinkId = addLinkToDraft( - payload.name || t`Link label`, - payload.link, - currentDraft, - folderId, - index, + addToWorkspaceAndOpenEdit( + { + type: NavigationMenuItemType.LINK, + name: payload.name || t`Link label`, + link: normalizeUrl(payload.link), + color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK, + }, + { targetFolderId: folderId, targetIndex: index }, + { + pageTitle: t`Edit link`, + pageIcon: IconLink, + focusTitleInput: true, + }, ); - openEditForNewNavItem(newLinkId, { - pageTitle: t`Edit link`, - pageIcon: IconLink, - focusTitleInput: true, - }); return; } case NavigationMenuItemType.OBJECT: { - const objectMetadataIdsInWorkspace = - getObjectMetadataIdsInDraft(currentDraft); - - if (objectMetadataIdsInWorkspace.has(payload.objectMetadataId)) { + if ( + getObjectMetadataIdsInDraft(currentItems).has( + payload.objectMetadataId, + ) + ) { return; } const objectMetadataItem = objectMetadataItems.find( (item) => item.id === payload.objectMetadataId, ); - const newItemId = addObjectToDraft({ - objectMetadataId: payload.objectMetadataId, - currentDraft, - targetFolderId: folderId, - targetIndex: index, - color: - payload.iconColor ?? - (objectMetadataItem - ? getObjectColorWithFallback(objectMetadataItem) - : undefined), - }); - openEditForNewNavItem(newItemId, { - pageTitle: objectMetadataItem?.labelPlural ?? payload.label, - pageIcon: objectMetadataItem - ? getIcon(objectMetadataItem.icon) - : IconFolder, - }); + addToWorkspaceAndOpenEdit( + { + type: NavigationMenuItemType.OBJECT, + targetObjectMetadataId: payload.objectMetadataId, + color: + payload.iconColor ?? + (objectMetadataItem + ? getObjectColorWithFallback(objectMetadataItem) + : undefined), + }, + { targetFolderId: folderId, targetIndex: index }, + { + pageTitle: objectMetadataItem?.labelPlural ?? payload.label, + pageIcon: objectMetadataItem + ? getIcon(objectMetadataItem.icon) + : IconFolder, + }, + ); return; } case NavigationMenuItemType.VIEW: { @@ -165,62 +179,61 @@ export const useHandleAddToNavigationDrop = () => { (item) => item.id === view.objectMetadataId, ) : undefined; - const newItemId = addViewToDraft( - payload.viewId, - currentDraft, - folderId, - index, - viewObjectMetadataItem - ? getObjectColorWithFallback(viewObjectMetadataItem) - : undefined, + addToWorkspaceAndOpenEdit( + { + type: NavigationMenuItemType.VIEW, + viewId: payload.viewId, + color: viewObjectMetadataItem + ? getObjectColorWithFallback(viewObjectMetadataItem) + : undefined, + }, + { targetFolderId: folderId, targetIndex: index }, + { + pageTitle: view?.name ?? payload.label, + pageIcon: view ? getIcon(view.icon) : IconFolder, + }, ); - openEditForNewNavItem(newItemId, { - pageTitle: view?.name ?? payload.label, - pageIcon: view ? getIcon(view.icon) : IconFolder, - }); return; } case NavigationMenuItemType.RECORD: { - const newItemId = addRecordToDraft( - { - recordId: payload.recordId, - objectMetadataId: payload.objectMetadataId, - objectNameSingular: payload.objectNameSingular, - label: payload.label, - imageUrl: payload.imageUrl, - }, - currentDraft, - folderId, - index, - ); - if (!isDefined(newItemId)) return; + if (!isDefined(payload.objectMetadataId)) { + return; + } const objectMetadataItem = objectMetadataItems.find( (item) => item.id === payload.objectMetadataId, ); - openEditForNewNavItem(newItemId, { - pageTitle: payload.label, - pageIcon: objectMetadataItem - ? getIcon(objectMetadataItem.icon) - : IconFolder, - }); + addToWorkspaceAndOpenEdit( + { + type: NavigationMenuItemType.RECORD, + targetObjectMetadataId: payload.objectMetadataId, + targetRecordId: payload.recordId, + targetRecordIdentifier: { + id: payload.recordId, + labelIdentifier: payload.label, + imageIdentifier: payload.imageUrl ?? null, + }, + }, + { targetFolderId: folderId, targetIndex: index }, + { + pageTitle: payload.label, + pageIcon: objectMetadataItem + ? getIcon(objectMetadataItem.icon) + : IconFolder, + }, + ); return; } } }, [ - addFolderToDraft, - addLinkToDraft, - addObjectToDraft, - addRecordToDraft, - addViewToDraft, + createItem, + currentItems, views, getIcon, - navigationMenuItemsDraft, objectMetadataItems, openNavigationMenuItemInSidePanel, setOpenNavigationMenuItemFolderIds, enterLayoutCustomizationMode, - workspaceNavigationMenuItems, store, ], ); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useCreateNavigationMenuItemFolder.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useCreateNavigationMenuItemFolder.ts deleted file mode 100644 index 1dc9024c91..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/folder/hooks/useCreateNavigationMenuItemFolder.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NavigationMenuItemType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { useCreateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useCreateManyNavigationMenuItems'; -import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData'; - -export const useCreateNavigationMenuItemFolder = () => { - const { navigationMenuItems, currentWorkspaceMemberId } = - useNavigationMenuItemsData(); - - const { createManyNavigationMenuItems } = useCreateManyNavigationMenuItems(); - - const createNewNavigationMenuItemFolder = async ( - name: string, - ): Promise => { - if (!name || !currentWorkspaceMemberId) { - return; - } - - const topLevelItems = navigationMenuItems.filter( - (item) => - !isDefined(item.folderId) && - item.userWorkspaceId === currentWorkspaceMemberId, - ); - - const minPosition = - topLevelItems.length > 0 - ? Math.min(...topLevelItems.map((item) => item.position)) - : 1; - - await createManyNavigationMenuItems([ - { - type: NavigationMenuItemType.FOLDER, - name, - targetRecordId: null, - targetObjectMetadataId: null, - userWorkspaceId: currentWorkspaceMemberId, - folderId: null, - position: minPosition - 1, - }, - ]); - }; - - return { createNewNavigationMenuItemFolder }; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems.ts index b99b75ae4f..67d8c6e22b 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems.ts @@ -1,7 +1,7 @@ -import { NavigationMenuItemType } from 'twenty-shared/types'; import { type NavigationMenuItem } from '~/generated-metadata/graphql'; import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; +import { flattenNavigationMenuItemsWithFolderChildren } from '@/navigation-menu-item/common/utils/flattenNavigationMenuItemsWithFolderChildren'; import { getWorkspaceSidebarOrphanItemsInDisplayOrder } from '@/navigation-menu-item/display/utils/getWorkspaceSidebarOrphanItemsInDisplayOrder'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; @@ -30,13 +30,6 @@ export const useNavigationMenuItemSectionItems = (): NavigationMenuItem[] => { const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); - const folderChildrenById = new Map( - workspaceNavigationMenuItemsByFolder.map((folder) => [ - folder.id, - folder.navigationMenuItems, - ]), - ); - const flatItems = getWorkspaceSidebarOrphanItemsInDisplayOrder({ workspaceNavigationMenuItems, workspaceNavigationMenuItemsSorted, @@ -46,9 +39,8 @@ export const useNavigationMenuItemSectionItems = (): NavigationMenuItem[] => { includeInaccessibleObjectBackedItems: isLayoutCustomizationModeEnabled, }); - return flatItems.flatMap((item) => - item.type === NavigationMenuItemType.FOLDER - ? [item, ...(folderChildrenById.get(item.id) ?? [])] - : [item], + return flattenNavigationMenuItemsWithFolderChildren( + flatItems, + workspaceNavigationMenuItemsByFolder, ); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx index a9c7115211..268a102bb0 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSection.tsx @@ -1,7 +1,8 @@ import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; -import { useCallback, useContext, useMemo, useState } from 'react'; -import { IconFolder, IconFolderPlus, IconHeartOff } from 'twenty-ui/display'; +import { useCallback, useContext, useMemo } from 'react'; +import { SidePanelPages } from 'twenty-shared/types'; +import { IconHeart, IconHeartOff, IconPlus } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; import { themeCssVariables } from 'twenty-ui/theme-constants'; import { type NavigationMenuItem } from '~/generated-metadata/graphql'; @@ -10,23 +11,23 @@ import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/co import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext'; import { useDeleteManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems'; -import { isNavigationMenuItemFolderCreatingState } from '@/navigation-menu-item/common/states/isNavigationMenuItemFolderCreatingState'; +import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState'; +import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState'; +import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState'; import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; import { NavigationMenuItemDisplay } from '@/navigation-menu-item/display/components/NavigationMenuItemDisplay'; import { NavigationMenuItemDroppableSlot } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemDroppableSlot'; import { NavigationMenuItemSortableItem } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem'; import { useIsDropDisabledForSection } from '@/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection'; -import { useCreateNavigationMenuItemFolder } from '@/navigation-menu-item/display/folder/hooks/useCreateNavigationMenuItemFolder'; import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemsByFolder'; import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems'; import { NavigationMenuItemOrphanDropTarget } from '@/navigation-menu-item/display/sections/components/NavigationMenuItemOrphanDropTarget'; import { NavigationMenuItemSection } from '@/navigation-menu-item/display/sections/components/NavigationMenuItemSection'; -import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; -import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput'; +import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel'; import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection'; import { isNavigationSectionOpenFamilyState } from '@/ui/navigation/navigation-drawer/states/isNavigationSectionOpenFamilyState'; import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue'; -import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; const StyledList = styled.div` display: flex; @@ -40,10 +41,6 @@ const StyledListItemRow = styled.div` gap: 0; `; -const StyledEmptyContainer = styled.div` - width: 100%; -`; - const ORPHAN_DROPPABLE_ID = NavigationMenuItemDroppableIds.FAVORITE_ORPHAN_NAVIGATION_MENU_ITEMS; @@ -54,14 +51,16 @@ export const FavoritesSection = () => { const { isDragging } = useContext(NavigationMenuItemDragContext); const favoritesDropDisabled = useIsDropDisabledForSection(false); - const [newFolderName, setNewFolderName] = useState(''); - const { createNewNavigationMenuItemFolder } = - useCreateNavigationMenuItemFolder(); - - const [ - isNavigationMenuItemFolderCreating, - setIsNavigationMenuItemFolderCreating, - ] = useAtomState(isNavigationMenuItemFolderCreatingState); + const { navigateSidePanel } = useNavigateSidePanel(); + const setNavigationMenuItemEditSection = useSetAtomState( + navigationMenuItemEditSectionState, + ); + const setPendingInsertionNavigationMenuItem = useSetAtomState( + pendingInsertionNavigationMenuItemState, + ); + const setSelectedNavigationMenuItemIdInEditMode = useSetAtomState( + selectedNavigationMenuItemIdInEditModeState, + ); const { t } = useLingui(); @@ -90,35 +89,20 @@ export const FavoritesSection = () => { [topLevelItems], ); - const toggleNewFolder = () => { + const handleAddFavorite = (event?: React.MouseEvent) => { + event?.stopPropagation(); + // Expansion is gated on items existing, so this stays collapsed if the user + // cancels and reveals the favorite as soon as it is added. openNavigationSection(); - setIsNavigationMenuItemFolderCreating((current) => !current); - }; - - const handleSubmitFolderCreation = async (value: string) => { - if (value === '') return; - setIsNavigationMenuItemFolderCreating(false); - setNewFolderName(''); - await createNewNavigationMenuItemFolder(value); - return true; - }; - - const handleClickOutside = async ( - _event: MouseEvent | TouchEvent, - value: string, - ) => { - if (!value) { - setIsNavigationMenuItemFolderCreating(false); - return; - } - setIsNavigationMenuItemFolderCreating(false); - setNewFolderName(''); - await createNewNavigationMenuItemFolder(value); - }; - - const handleCancelFolderCreation = () => { - setNewFolderName(''); - setIsNavigationMenuItemFolderCreating(false); + setNavigationMenuItemEditSection('favorite'); + setPendingInsertionNavigationMenuItem(null); + setSelectedNavigationMenuItemIdInEditMode(null); + navigateSidePanel({ + page: SidePanelPages.NavigationMenuAddItem, + pageTitle: t`New favorite`, + pageIcon: IconHeart, + resetNavigationStack: true, + }); }; const makeRightOptions = useCallback( @@ -135,40 +119,20 @@ export const FavoritesSection = () => { [deleteManyNavigationMenuItems], ); - if ( - topLevelItems.length === 0 && - !isNavigationMenuItemFolderCreating && - userNavigationMenuItemsByFolder.length === 0 - ) { - return null; - } - return ( 0 && isNavigationSectionOpen} onToggle={toggleNavigationSection} rightIcon={ } > - {isNavigationMenuItemFolderCreating && ( - - - - )} - {topLevelItems.length > 0 ? ( + {topLevelItems.length > 0 && ( {topLevelItems.map((item, index) => ( @@ -227,8 +191,6 @@ export const FavoritesSection = () => { /> - ) : ( - )} ); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSectionDispatcher.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSectionDispatcher.tsx index ba8e8d8a6c..f23857584d 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSectionDispatcher.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/favorites/components/FavoritesSectionDispatcher.tsx @@ -1,28 +1,14 @@ import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; -import { isNavigationMenuItemFolderCreatingState } from '@/navigation-menu-item/common/states/isNavigationMenuItemFolderCreatingState'; import { NavigationMenuItemDndKitProvider } from '@/navigation-menu-item/display/dnd/providers/NavigationMenuItemDndKitProvider'; -import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems'; import { FavoritesSection } from '@/navigation-menu-item/display/sections/favorites/components/FavoritesSection'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { AnimatedEaseInOut } from 'twenty-ui/utilities'; export const FavoritesSectionDispatcher = () => { - const { navigationMenuItemsSorted } = useSortedNavigationMenuItems(); const isLayoutCustomizationModeEnabled = useAtomStateValue( isLayoutCustomizationModeEnabledState, ); - const isNavigationMenuItemFolderCreating = useAtomStateValue( - isNavigationMenuItemFolderCreatingState, - ); - - const hasFavoritesSectionContent = - isNavigationMenuItemFolderCreating || - navigationMenuItemsSorted.some((item) => !item.folderId); - - if (!hasFavoritesSectionContent) { - return null; - } return ( diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useAddFolderToNavigationMenuDraft.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useAddFolderToNavigationMenuDraft.ts deleted file mode 100644 index 8ea389e9b6..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useAddFolderToNavigationMenuDraft.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { NavigationMenuItemType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { v4 } from 'uuid'; -import type { NavigationMenuItem } from '~/generated-metadata/graphql'; - -import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder'; -import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; -import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; - -export const useAddFolderToNavigationMenuDraft = () => { - const setNavigationMenuItemsDraft = useSetAtomState( - navigationMenuItemsDraftState, - ); - - const addFolderToDraft = ( - name: string, - currentDraft: NavigationMenuItem[], - targetFolderId?: string | null, - targetIndex?: number, - ): string => { - const folderId = targetFolderId ?? null; - - const itemsInFolder = currentDraft.filter( - (item) => - (item.folderId ?? null) === folderId && - !isDefined(item.userWorkspaceId), - ); - const index = targetIndex ?? itemsInFolder.length; - - const { flatIndex, position } = computeInsertIndexAndPosition( - currentDraft, - folderId, - index, - ); - - const newItemId = v4(); - const newItem: NavigationMenuItem = { - __typename: 'NavigationMenuItem', - id: newItemId, - type: NavigationMenuItemType.FOLDER, - viewId: undefined, - targetObjectMetadataId: undefined, - targetRecordId: undefined, - folderId: folderId ?? undefined, - position, - userWorkspaceId: undefined, - name: name.trim(), - color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER, - applicationId: undefined, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - - const newDraft = [ - ...currentDraft.slice(0, flatIndex), - newItem, - ...currentDraft.slice(flatIndex), - ]; - setNavigationMenuItemsDraft(newDraft); - return newItemId; - }; - - return { addFolderToDraft }; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useUpdateFolderInDraft.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useUpdateFolderInDraft.ts deleted file mode 100644 index 9c9695c9bd..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/folder/hooks/useUpdateFolderInDraft.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; -import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; - -export type UpdateFolderInDraftUpdates = { - name?: string; - icon?: string; -}; - -export const useUpdateFolderInDraft = () => { - const setNavigationMenuItemsDraft = useSetAtomState( - navigationMenuItemsDraftState, - ); - - const updateFolderInDraft = ( - folderId: string, - updates: UpdateFolderInDraftUpdates, - ) => { - setNavigationMenuItemsDraft((draft) => { - if (!draft) return draft; - - return draft.map((item) => { - if (!isNavigationMenuItemFolder(item) || item.id !== folderId) { - return item; - } - return { ...item, ...updates }; - }); - }); - }; - - return { updateFolderInDraft }; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems.ts deleted file mode 100644 index d26bac6f01..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState'; -import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; - -export const useDraftNavigationMenuItems = () => { - const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState(); - const navigationMenuItemsDraft = useAtomStateValue( - navigationMenuItemsDraftState, - ); - - const currentDraft = navigationMenuItemsDraft ?? workspaceNavigationMenuItems; - - return { currentDraft }; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useDraftNavigationMenuItemsAllFolders.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useDraftNavigationMenuItemsAllFolders.ts deleted file mode 100644 index 09d69d3144..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useDraftNavigationMenuItemsAllFolders.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems'; -import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; - -export const useDraftNavigationMenuItemsAllFolders = () => { - const { currentDraft } = useDraftNavigationMenuItems(); - - const allFolders = - currentDraft?.filter(isNavigationMenuItemFolder).map((item) => ({ - id: item.id, - name: item.name ?? 'Folder', - folderId: item.folderId, - icon: item.icon, - color: item.color, - })) ?? []; - - return { allFolders }; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useDraftNavigationMenuItemsWorkspaceFolders.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useDraftNavigationMenuItemsWorkspaceFolders.ts deleted file mode 100644 index 938cfe1128..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useDraftNavigationMenuItemsWorkspaceFolders.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemsByFolder'; - -export const useDraftNavigationMenuItemsWorkspaceFolders = () => { - const { workspaceNavigationMenuItemsByFolder } = - useNavigationMenuItemsByFolder(); - - const workspaceFolders = workspaceNavigationMenuItemsByFolder.map( - (folder) => ({ - id: folder.id, - name: folder.folderName, - icon: folder.icon, - color: folder.color, - }), - ); - - return { workspaceFolders }; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController.ts new file mode 100644 index 0000000000..483bbfa9b0 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController.ts @@ -0,0 +1,177 @@ +import { useLingui } from '@lingui/react/macro'; +import { type NavigationMenuItemType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +import { useCreateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useCreateManyNavigationMenuItems'; +import { useDeleteManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems'; +import { useUpdateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useUpdateManyNavigationMenuItems'; +import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState'; +import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; +import { buildCreateNavigationMenuItemInput } from '@/navigation-menu-item/common/utils/buildCreateNavigationMenuItemInput'; +import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition'; +import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; + +export type NewNavigationMenuItemInput = { + type: NavigationMenuItemType; + targetObjectMetadataId?: string | null; + viewId?: string | null; + targetRecordId?: string | null; + targetRecordIdentifier?: NavigationMenuItem['targetRecordIdentifier']; + name?: string | null; + link?: string | null; + color?: string | null; +}; + +type CreateItemOptions = { + targetFolderId?: string | null; + targetIndex?: number; +}; + +// The single create/update/delete API for the add/edit side panel. It forks on +// the active section (mirroring useHandleNavigationMenuItemDragAndDrop): the +// workspace section stages changes in the draft (saved on layout exit), while +// the favorite section persists personal items immediately and optimistically. +// On the immediate path the mutation hooks roll back on failure, so errors are +// surfaced here and the rejection is swallowed — callers fire-and-forget. +export const useNavigationMenuItemEditController = () => { + const { t } = useLingui(); + const { enqueueErrorSnackBar } = useSnackBar(); + const navigationMenuItemEditSection = useAtomStateValue( + navigationMenuItemEditSectionState, + ); + const { + navigationMenuItems, + workspaceNavigationMenuItems, + currentWorkspaceMemberId, + } = useNavigationMenuItemsData(); + const setNavigationMenuItemsDraft = useSetAtomState( + navigationMenuItemsDraftState, + ); + const { createManyNavigationMenuItems } = useCreateManyNavigationMenuItems(); + const { updateManyNavigationMenuItems } = useUpdateManyNavigationMenuItems(); + const { deleteManyNavigationMenuItems } = useDeleteManyNavigationMenuItems(); + + const isDraftMode = navigationMenuItemEditSection === 'workspace'; + const currentItems = isDraftMode + ? workspaceNavigationMenuItems + : navigationMenuItems; + const targetUserWorkspaceId = isDraftMode + ? undefined + : currentWorkspaceMemberId; + + const createItem = ( + input: NewNavigationMenuItemInput, + { targetFolderId, targetIndex }: CreateItemOptions = {}, + ): string => { + const folderId = targetFolderId ?? null; + const itemsInFolder = currentItems.filter( + (item) => (item.folderId ?? null) === folderId, + ); + const index = targetIndex ?? itemsInFolder.length; + const { flatIndex, position } = computeInsertIndexAndPosition( + currentItems, + folderId, + index, + ); + + const id = v4(); + const newItem: NavigationMenuItem = { + __typename: 'NavigationMenuItem', + id, + type: input.type, + position, + userWorkspaceId: targetUserWorkspaceId, + folderId: folderId ?? undefined, + targetObjectMetadataId: input.targetObjectMetadataId ?? undefined, + targetRecordId: input.targetRecordId ?? undefined, + targetRecordIdentifier: input.targetRecordIdentifier, + viewId: input.viewId ?? undefined, + name: input.name ?? undefined, + link: input.link ?? undefined, + color: input.color ?? undefined, + applicationId: undefined, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + if (isDraftMode) { + setNavigationMenuItemsDraft((draft) => { + const current = draft ?? currentItems; + return [ + ...current.slice(0, flatIndex), + newItem, + ...current.slice(flatIndex), + ]; + }); + return id; + } + + if (isDefined(targetUserWorkspaceId)) { + void createManyNavigationMenuItems([ + { + ...buildCreateNavigationMenuItemInput(newItem, (value) => value), + userWorkspaceId: targetUserWorkspaceId, + }, + ]).catch(() => + enqueueErrorSnackBar({ message: t`Couldn't add to favorites` }), + ); + } else { + enqueueErrorSnackBar({ message: t`Couldn't add to favorites` }); + } + + return id; + }; + + const updateItem = async ( + id: string, + update: Partial, + ): Promise => { + if (isDraftMode) { + setNavigationMenuItemsDraft((draft) => + isDefined(draft) + ? draft.map((item) => + item.id === id ? { ...item, ...update } : item, + ) + : draft, + ); + return; + } + + try { + await updateManyNavigationMenuItems([{ id, update }]); + } catch { + enqueueErrorSnackBar({ message: t`Couldn't update favorite` }); + } + }; + + const deleteItems = async (ids: string[]): Promise => { + if (isDraftMode) { + setNavigationMenuItemsDraft((draft) => + isDefined(draft) + ? draft.filter((item) => !ids.includes(item.id)) + : draft, + ); + return; + } + + try { + await deleteManyNavigationMenuItems(ids); + } catch { + enqueueErrorSnackBar({ message: t`Couldn't remove favorite` }); + } + }; + + return { + currentItems, + isDraftMode, + targetUserWorkspaceId, + createItem, + updateItem, + deleteItems, + }; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemEditSectionItems.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemEditSectionItems.ts new file mode 100644 index 0000000000..6e50fc6c3c --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemEditSectionItems.ts @@ -0,0 +1,36 @@ +import { type NavigationMenuItem } from '~/generated-metadata/graphql'; + +import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState'; +import { flattenNavigationMenuItemsWithFolderChildren } from '@/navigation-menu-item/common/utils/flattenNavigationMenuItemsWithFolderChildren'; +import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemsByFolder'; +import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; +import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; + +// Flat ordered item list (orphans + folders followed by their children) for the +// section the add/edit panel is operating on. The edit page resolves the +// selected item and computes move/organize bounds against this list, so it must +// return personal items in the favorite section and workspace items otherwise. +export const useNavigationMenuItemEditSectionItems = + (): NavigationMenuItem[] => { + const navigationMenuItemEditSection = useAtomStateValue( + navigationMenuItemEditSectionState, + ); + const workspaceSectionItems = useNavigationMenuItemSectionItems(); + const { navigationMenuItemsSorted } = useSortedNavigationMenuItems(); + const { userNavigationMenuItemsByFolder } = + useNavigationMenuItemsByFolder(); + + if (navigationMenuItemEditSection === 'workspace') { + return workspaceSectionItems; + } + + const topLevelItems = navigationMenuItemsSorted.filter( + (item) => !item.folderId, + ); + + return flattenNavigationMenuItemsWithFolderChildren( + topLevelItems, + userNavigationMenuItemsByFolder, + ); + }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove.ts index 66154b0da7..ee5908aa25 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove.ts @@ -1,184 +1,136 @@ import { isDefined } from 'twenty-shared/utils'; -import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; import { getPositionBetween } from '@/navigation-menu-item/common/utils/getPositionBetween'; import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; - -import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; +import { useNavigationMenuItemEditSectionItems } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditSectionItems'; export const useNavigationMenuItemMoveRemove = () => { - const setNavigationMenuItemsDraft = useSetAtomState( - navigationMenuItemsDraftState, - ); - const items = useNavigationMenuItemSectionItems(); + const { currentItems, updateItem, deleteItems } = + useNavigationMenuItemEditController(); + const items = useNavigationMenuItemEditSectionItems(); const visibleItemIds = new Set(items.map((item) => item.id)); - const moveUp = (navigationMenuItemId: string) => { - setNavigationMenuItemsDraft((draft) => { - if (!draft) { - return draft; - } + const getSortedSiblings = (navigationMenuItemId: string) => { + const currentItem = currentItems.find( + (item) => item.id === navigationMenuItemId, + ); + if (!isDefined(currentItem)) { + return null; + } + const folderId = currentItem.folderId ?? null; + return currentItems + .filter( + (item) => + (item.folderId ?? null) === folderId && visibleItemIds.has(item.id), + ) + .sort((a, b) => a.position - b.position); + }; - const currentItem = draft.find( - (item) => item.id === navigationMenuItemId, - ); - if (!currentItem) { - return draft; - } - - const folderId = currentItem.folderId ?? null; - const siblings = draft - .filter( - (item) => - (item.folderId ?? null) === folderId && visibleItemIds.has(item.id), - ) - .sort((a, b) => a.position - b.position); - - const currentIndex = siblings.findIndex( - (item) => item.id === navigationMenuItemId, - ); - if (currentIndex <= 0) { - return draft; - } - - const prev = siblings[currentIndex - 1]; - const prevPrev = siblings[currentIndex - 2]; - const newPosition = getPositionBetween(prevPrev?.position, prev.position); - - return draft.map((item) => - item.id === navigationMenuItemId - ? { ...item, position: newPosition } - : item, - ); + const moveUp = async (navigationMenuItemId: string) => { + const siblings = getSortedSiblings(navigationMenuItemId); + if (!isDefined(siblings)) { + return; + } + const currentIndex = siblings.findIndex( + (item) => item.id === navigationMenuItemId, + ); + if (currentIndex <= 0) { + return; + } + const prev = siblings[currentIndex - 1]; + const prevPrev = siblings[currentIndex - 2]; + await updateItem(navigationMenuItemId, { + position: getPositionBetween(prevPrev?.position, prev.position), }); }; - const moveDown = (navigationMenuItemId: string) => { - setNavigationMenuItemsDraft((draft) => { - if (!draft) { - return draft; - } - - const currentItem = draft.find( - (item) => item.id === navigationMenuItemId, - ); - if (!currentItem) { - return draft; - } - - const folderId = currentItem.folderId ?? null; - const siblings = draft - .filter( - (item) => - (item.folderId ?? null) === folderId && visibleItemIds.has(item.id), - ) - .sort((a, b) => a.position - b.position); - - const currentIndex = siblings.findIndex( - (item) => item.id === navigationMenuItemId, - ); - if (currentIndex < 0 || currentIndex >= siblings.length - 1) { - return draft; - } - - const next = siblings[currentIndex + 1]; - const nextNext = siblings[currentIndex + 2]; - const newPosition = getPositionBetween(next.position, nextNext?.position); - - return draft.map((item) => - item.id === navigationMenuItemId - ? { ...item, position: newPosition } - : item, - ); + const moveDown = async (navigationMenuItemId: string) => { + const siblings = getSortedSiblings(navigationMenuItemId); + if (!isDefined(siblings)) { + return; + } + const currentIndex = siblings.findIndex( + (item) => item.id === navigationMenuItemId, + ); + if (currentIndex < 0 || currentIndex >= siblings.length - 1) { + return; + } + const next = siblings[currentIndex + 1]; + const nextNext = siblings[currentIndex + 2]; + await updateItem(navigationMenuItemId, { + position: getPositionBetween(next.position, nextNext?.position), }); }; - const remove = (navigationMenuItemId: string) => { - setNavigationMenuItemsDraft((draft) => { - if (!draft) { - return draft; - } + const remove = async (navigationMenuItemId: string) => { + const itemToRemove = currentItems.find( + (item) => item.id === navigationMenuItemId, + ); + if (!isDefined(itemToRemove)) { + return; + } - const itemToRemove = draft.find( - (item) => item.id === navigationMenuItemId, - ); - if (!itemToRemove) { - return draft; - } + if (isNavigationMenuItemFolder(itemToRemove)) { + const childIds = currentItems + .filter((item) => item.folderId === navigationMenuItemId) + .map((item) => item.id); + await deleteItems([navigationMenuItemId, ...childIds]); + return; + } - const isFolder = isNavigationMenuItemFolder(itemToRemove); - - if (isFolder) { - return draft.filter( - (item) => - item.id !== navigationMenuItemId && - item.folderId !== navigationMenuItemId, - ); - } - - return draft.filter((item) => item.id !== navigationMenuItemId); - }); + await deleteItems([navigationMenuItemId]); }; - const moveToFolder = ( + const moveToFolder = async ( navigationMenuItemId: string, targetFolderId: string | null, ) => { - setNavigationMenuItemsDraft((draft) => { - if (!draft) { - return draft; + const itemToMove = currentItems.find( + (item) => item.id === navigationMenuItemId, + ); + if (!isDefined(itemToMove)) { + return; + } + + const isFolder = isNavigationMenuItemFolder(itemToMove); + if (isFolder && targetFolderId === navigationMenuItemId) { + return; + } + + // Block moving a folder into one of its own descendants (would orphan the subtree). + if (isFolder && isDefined(targetFolderId)) { + const descendantFolderIds = new Set(); + const collectDescendants = (folderId: string) => { + currentItems + .filter( + (item) => + isNavigationMenuItemFolder(item) && item.folderId === folderId, + ) + .forEach((item) => { + descendantFolderIds.add(item.id); + collectDescendants(item.id); + }); + }; + collectDescendants(navigationMenuItemId); + if (descendantFolderIds.has(targetFolderId)) { + return; } + } - const itemToMove = draft.find((item) => item.id === navigationMenuItemId); - if (!itemToMove) { - return draft; - } + const itemsInTargetFolder = currentItems.filter((item) => + targetFolderId === null + ? !isDefined(item.folderId) + : item.folderId === targetFolderId, + ); + const maxPositionInTarget = + itemsInTargetFolder.length > 0 + ? Math.max(...itemsInTargetFolder.map((item) => item.position)) + : -1; - const isFolder = isNavigationMenuItemFolder(itemToMove); - if (isFolder && targetFolderId === navigationMenuItemId) { - return draft; - } - - if (isFolder && isDefined(targetFolderId)) { - const descendantFolderIds = new Set(); - const collectDescendants = (folderId: string) => { - draft - ?.filter( - (item) => - isNavigationMenuItemFolder(item) && item.folderId === folderId, - ) - .forEach((item) => { - descendantFolderIds.add(item.id); - collectDescendants(item.id); - }); - }; - collectDescendants(navigationMenuItemId); - if (descendantFolderIds.has(targetFolderId)) { - return draft; - } - } - - const itemsInTargetFolder = draft.filter((item) => - targetFolderId === null - ? !isDefined(item.folderId) - : item.folderId === targetFolderId, - ); - const maxPositionInTarget = - itemsInTargetFolder.length > 0 - ? Math.max(...itemsInTargetFolder.map((item) => item.position)) - : -1; - const newPosition = maxPositionInTarget + 1; - - return draft.map((item) => - item.id === navigationMenuItemId - ? { - ...item, - folderId: targetFolderId ?? undefined, - position: newPosition, - } - : item, - ); + await updateItem(navigationMenuItemId, { + folderId: targetFolderId ?? null, + position: maxPositionInTarget + 1, }); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemSectionAllFolders.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemSectionAllFolders.ts new file mode 100644 index 0000000000..8c7ba586e2 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemSectionAllFolders.ts @@ -0,0 +1,18 @@ +import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; + +export const useNavigationMenuItemSectionAllFolders = () => { + const { currentItems } = useNavigationMenuItemEditController(); + + const allFolders = currentItems + .filter(isNavigationMenuItemFolder) + .map((item) => ({ + id: item.id, + name: item.name ?? 'Folder', + folderId: item.folderId, + icon: item.icon, + color: item.color, + })); + + return { allFolders }; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemSectionFolders.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemSectionFolders.ts new file mode 100644 index 0000000000..de1b2e1aee --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemSectionFolders.ts @@ -0,0 +1,27 @@ +import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState'; +import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemsByFolder'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; + +export const useNavigationMenuItemSectionFolders = () => { + const navigationMenuItemEditSection = useAtomStateValue( + navigationMenuItemEditSectionState, + ); + const { + workspaceNavigationMenuItemsByFolder, + userNavigationMenuItemsByFolder, + } = useNavigationMenuItemsByFolder(); + + const folders = + navigationMenuItemEditSection === 'workspace' + ? workspaceNavigationMenuItemsByFolder + : userNavigationMenuItemsByFolder; + + const sectionFolders = folders.map((folder) => ({ + id: folder.id, + name: folder.folderName, + icon: folder.icon, + color: folder.color, + })); + + return { sectionFolders }; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemTitleEdit.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemTitleEdit.ts new file mode 100644 index 0000000000..65c5765483 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuItemTitleEdit.ts @@ -0,0 +1,56 @@ +import { useState } from 'react'; + +import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; + +type UseNavigationMenuItemTitleEditParams = { + itemId: string | null; + itemName: string; + defaultLabel: string; + persistName: (name: string) => void; +}; + +// Title editing for a navigation menu item, section-aware: the workspace +// section writes the in-memory draft on every keystroke (live preview), while +// a personal favorite persists immediately, so keystrokes are buffered locally +// (keyed by item id, self-invalidating on item switch) and committed on save. +export const useNavigationMenuItemTitleEdit = ({ + itemId, + itemName, + defaultLabel, + persistName, +}: UseNavigationMenuItemTitleEditParams) => { + const navigationMenuItemEditSection = useAtomStateValue( + navigationMenuItemEditSectionState, + ); + const isDraftMode = navigationMenuItemEditSection === 'workspace'; + const [localNameEdit, setLocalNameEdit] = useState<{ + itemId: string; + text: string; + } | null>(null); + + const bufferedName = + localNameEdit?.itemId === itemId ? localNameEdit.text : null; + const value = isDraftMode ? itemName : (bufferedName ?? itemName); + + const handleChange = (text: string) => { + if (isDraftMode) { + persistName(text); + } else if (itemId !== null) { + setLocalNameEdit({ itemId, text }); + } + }; + + const handleSave = () => { + const trimmed = value.trim(); + const finalName = trimmed.length > 0 ? trimmed : defaultLabel; + if (finalName !== itemName) { + persistName(finalName); + } + if (!isDraftMode) { + setLocalNameEdit(null); + } + }; + + return { value, handleChange, handleSave }; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection.ts similarity index 66% rename from packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft.ts rename to packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection.ts index 8b81ce91eb..62a9489f9c 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection.ts @@ -6,18 +6,20 @@ import { viewsSelector } from '@/views/states/selectors/viewsSelector'; import { ViewKey } from '@/views/types/ViewKey'; import { type NavigationMenuItem } from '~/generated-metadata/graphql'; -type NavigationMenuItemDraft = Pick< +type NavigationMenuItemForDedup = Pick< NavigationMenuItem, 'id' | 'type' | 'viewId' | 'targetObjectMetadataId' >; -export const useNavigationMenuObjectMetadataFromDraft = ( - currentDraft: NavigationMenuItemDraft[], +// Derives which objects/views are already present in the given section's items, +// so the add pickers can hide or disable what's already there. +export const useNavigationMenuObjectMetadataForSection = ( + sectionItems: NavigationMenuItemForDedup[], ) => { const views = useAtomStateValue(viewsSelector); - const objectMetadataIdsInWorkspace = - getObjectMetadataIdsInDraft(currentDraft); + const objectMetadataIdsAlreadyAdded = + getObjectMetadataIdsInDraft(sectionItems); const objectMetadataIdsWithIndexView = new Set( views @@ -29,17 +31,17 @@ export const useNavigationMenuObjectMetadataFromDraft = ( views.map((view) => view.objectMetadataId), ); - const viewIdsInWorkspace = new Set( - currentDraft.flatMap((item) => + const viewIdsAlreadyAdded = new Set( + sectionItems.flatMap((item) => isDefined(item.viewId) ? [item.viewId] : [], ), ); return { views, - objectMetadataIdsInWorkspace, + objectMetadataIdsAlreadyAdded, objectMetadataIdsWithIndexView, objectMetadataIdsWithAnyView, - viewIdsInWorkspace, + viewIdsAlreadyAdded, }; }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem.ts index 5e22e1410f..c90d1c02aa 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem.ts @@ -1,12 +1,12 @@ -import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState'; +import { useNavigationMenuItemEditSectionItems } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditSectionItems'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; export const useSelectedNavigationMenuItemEditItem = () => { const selectedNavigationMenuItemIdInEditMode = useAtomStateValue( selectedNavigationMenuItemIdInEditModeState, ); - const items = useNavigationMenuItemSectionItems(); + const items = useNavigationMenuItemEditSectionItems(); const selectedItem = selectedNavigationMenuItemIdInEditMode ? items.find((item) => item.id === selectedNavigationMenuItemIdInEditMode) diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useUpdateNavigationMenuItemInDraft.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useUpdateNavigationMenuItemInDraft.ts deleted file mode 100644 index 4cd8f64ec0..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/hooks/useUpdateNavigationMenuItemInDraft.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; - -export type UpdateNavigationMenuItemInDraftUpdates = { - color?: string; - name?: string; - icon?: string; - link?: string; -}; - -export const useUpdateNavigationMenuItemInDraft = () => { - const setNavigationMenuItemsDraft = useSetAtomState( - navigationMenuItemsDraftState, - ); - - const updateNavigationMenuItemInDraft = ( - navigationMenuItemId: string, - updates: UpdateNavigationMenuItemInDraftUpdates, - ) => { - setNavigationMenuItemsDraft((draft) => { - if (!draft) return draft; - - return draft.map((item) => - item.id === navigationMenuItemId ? { ...item, ...updates } : item, - ); - }); - }; - - return { updateNavigationMenuItemInDraft }; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/link/hooks/useAddLinkToNavigationMenuDraft.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/link/hooks/useAddLinkToNavigationMenuDraft.ts deleted file mode 100644 index 82d2f43247..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/link/hooks/useAddLinkToNavigationMenuDraft.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { NavigationMenuItemType } from 'twenty-shared/types'; -import { isDefined, normalizeUrl } from 'twenty-shared/utils'; -import { v4 } from 'uuid'; -import type { NavigationMenuItem } from '~/generated-metadata/graphql'; - -import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorLink'; -import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; -import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; - -export const useAddLinkToNavigationMenuDraft = () => { - const setNavigationMenuItemsDraft = useSetAtomState( - navigationMenuItemsDraftState, - ); - - const addLinkToDraft = ( - label: string, - url: string, - currentDraft: NavigationMenuItem[], - targetFolderId?: string | null, - targetIndex?: number, - ): string => { - const normalizedUrl = normalizeUrl(url); - const folderId = targetFolderId ?? null; - - const itemsInFolder = currentDraft.filter( - (item) => - (item.folderId ?? null) === folderId && - !isDefined(item.userWorkspaceId), - ); - const index = targetIndex ?? itemsInFolder.length; - - const { flatIndex, position } = computeInsertIndexAndPosition( - currentDraft, - folderId, - index, - ); - - const newItemId = v4(); - const newItem: NavigationMenuItem = { - __typename: 'NavigationMenuItem', - id: newItemId, - type: NavigationMenuItemType.LINK, - viewId: undefined, - targetObjectMetadataId: undefined, - targetRecordId: undefined, - folderId: folderId ?? undefined, - position, - userWorkspaceId: undefined, - name: label.trim() || 'Link', - link: normalizedUrl, - color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK, - applicationId: undefined, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - - const newDraft = [ - ...currentDraft.slice(0, flatIndex), - newItem, - ...currentDraft.slice(flatIndex), - ]; - setNavigationMenuItemsDraft(newDraft); - return newItemId; - }; - - return { addLinkToDraft }; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/link/hooks/useUpdateLinkInDraft.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/link/hooks/useUpdateLinkInDraft.ts deleted file mode 100644 index ec74934c74..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/link/hooks/useUpdateLinkInDraft.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; -import { isNavigationMenuItemLink } from '@/navigation-menu-item/common/utils/isNavigationMenuItemLink'; - -export const useUpdateLinkInDraft = () => { - const setNavigationMenuItemsDraft = useSetAtomState( - navigationMenuItemsDraftState, - ); - - const updateLinkInDraft = ( - linkId: string, - updates: { name?: string; link?: string }, - ) => { - setNavigationMenuItemsDraft((draft) => { - if (!draft) return draft; - - return draft.map((item) => { - if (item.id !== linkId || !isNavigationMenuItemLink(item)) return item; - - const updated = { ...item }; - if (updates.name !== undefined) { - updated.name = updates.name.trim() || 'Link'; - } - if (updates.link !== undefined && updates.link.trim() !== '') { - updated.link = updates.link.trim(); - } - return updated; - }); - }); - }; - - return { updateLinkInDraft }; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/object/hooks/useAddObjectToNavigationMenuDraft.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/object/hooks/useAddObjectToNavigationMenuDraft.ts deleted file mode 100644 index ba36d9dfac..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/object/hooks/useAddObjectToNavigationMenuDraft.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { NavigationMenuItemType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { v4 } from 'uuid'; -import type { NavigationMenuItem } from '~/generated-metadata/graphql'; - -import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; -import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; - -export type AddObjectToNavigationMenuDraftParams = { - objectMetadataId: string; - currentDraft: NavigationMenuItem[]; - targetFolderId?: string | null; - targetIndex?: number; - color?: string | null; -}; - -export const useAddObjectToNavigationMenuDraft = () => { - const setNavigationMenuItemsDraft = useSetAtomState( - navigationMenuItemsDraftState, - ); - - const addObjectToDraft = ({ - objectMetadataId, - currentDraft, - targetFolderId, - targetIndex, - color, - }: AddObjectToNavigationMenuDraftParams): string => { - const folderId = targetFolderId ?? null; - - const itemsInFolder = currentDraft.filter( - (item) => - (item.folderId ?? null) === folderId && - !isDefined(item.userWorkspaceId), - ); - const index = targetIndex ?? itemsInFolder.length; - - const { flatIndex, position } = computeInsertIndexAndPosition( - currentDraft, - folderId, - index, - ); - - const newItemId = v4(); - const newItem: NavigationMenuItem = { - __typename: 'NavigationMenuItem', - id: newItemId, - type: NavigationMenuItemType.OBJECT, - viewId: undefined, - targetObjectMetadataId: objectMetadataId, - position, - userWorkspaceId: undefined, - targetRecordId: undefined, - folderId: folderId ?? undefined, - name: undefined, - applicationId: undefined, - color, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - - const newDraft = [ - ...currentDraft.slice(0, flatIndex), - newItem, - ...currentDraft.slice(flatIndex), - ]; - setNavigationMenuItemsDraft(newDraft); - return newItemId; - }; - - return { addObjectToDraft }; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/record/hooks/useAddRecordToNavigationMenuDraft.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/record/hooks/useAddRecordToNavigationMenuDraft.ts deleted file mode 100644 index b773acc435..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/record/hooks/useAddRecordToNavigationMenuDraft.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { v4 } from 'uuid'; -import { NavigationMenuItemType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import type { NavigationMenuItem } from '~/generated-metadata/graphql'; - -import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; -import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition'; - -type SearchRecord = { - recordId: string; - objectNameSingular: string; - label: string; - imageUrl?: string | null; -}; - -type SearchRecordWithOptionalMetadataId = SearchRecord & { - objectMetadataId?: string; -}; - -export const useAddRecordToNavigationMenuDraft = () => { - const setNavigationMenuItemsDraft = useSetAtomState( - navigationMenuItemsDraftState, - ); - const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); - - const addRecordToDraft = ( - searchRecord: SearchRecordWithOptionalMetadataId, - currentDraft: NavigationMenuItem[], - targetFolderId?: string | null, - targetIndex?: number, - ): string | undefined => { - const objectMetadataId = - searchRecord.objectMetadataId ?? - objectMetadataItems.find( - (item) => item.nameSingular === searchRecord.objectNameSingular, - )?.id; - - if (!isDefined(objectMetadataId)) { - return undefined; - } - - const folderId = targetFolderId ?? null; - - const itemsInFolder = currentDraft.filter( - (item) => - (item.folderId ?? null) === folderId && - !isDefined(item.userWorkspaceId), - ); - const index = targetIndex ?? itemsInFolder.length; - - const { flatIndex, position } = computeInsertIndexAndPosition( - currentDraft, - folderId, - index, - ); - - const newItemId = v4(); - const newItem: NavigationMenuItem = { - __typename: 'NavigationMenuItem', - id: newItemId, - type: NavigationMenuItemType.RECORD, - viewId: undefined, - targetObjectMetadataId: objectMetadataId, - targetRecordId: searchRecord.recordId, - targetRecordIdentifier: { - id: searchRecord.recordId, - labelIdentifier: searchRecord.label, - imageIdentifier: searchRecord.imageUrl ?? null, - }, - position, - userWorkspaceId: undefined, - folderId: folderId ?? undefined, - name: undefined, - applicationId: undefined, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - - const newDraft = [ - ...currentDraft.slice(0, flatIndex), - newItem, - ...currentDraft.slice(flatIndex), - ]; - setNavigationMenuItemsDraft(newDraft); - return newItemId; - }; - - return { addRecordToDraft }; -}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditColorOption.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditColorOption.tsx index b4f93ef891..e1265c5218 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditColorOption.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditColorOption.tsx @@ -2,8 +2,8 @@ import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuIt import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem'; import { isValidObjectNavigationMenuItem } from '@/navigation-menu-item/common/utils/isValidObjectNavigationMenuItem'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem'; -import { useUpdateNavigationMenuItemInDraft } from '@/navigation-menu-item/edit/hooks/useUpdateNavigationMenuItemInDraft'; import { ThemeColorPickerMenu } from '@/ui/input/components/ThemeColorPickerMenu'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; @@ -32,8 +32,7 @@ export const SidePanelEditColorOption = ({ color, }: SidePanelEditColorOptionProps) => { const { t } = useLingui(); - const { updateNavigationMenuItemInDraft } = - useUpdateNavigationMenuItemInDraft(); + const { updateItem, isDraftMode } = useNavigationMenuItemEditController(); const { closeDropdown } = useCloseDropdown(); const { updateInDraft, applyChanges } = useUpdateMetadataStoreDraft(); const { selectedItem } = useSelectedNavigationMenuItemEditItem(); @@ -42,11 +41,11 @@ export const SidePanelEditColorOption = ({ const colorLabel = DEFAULT_COLOR_LABELS[themeColor] ?? capitalize(themeColor); const handleSelectColor = (selectedColor: ThemeColor) => { - updateNavigationMenuItemInDraft(navigationMenuItemId, { - color: selectedColor, - }); + void updateItem(navigationMenuItemId, { color: selectedColor }); - if (isValidObjectNavigationMenuItem(selectedItem)) { + // A personal favorite's color is item-local; only workspace customization + // recolors the shared object metadata. + if (isDraftMode && isValidObjectNavigationMenuItem(selectedItem)) { updateInDraft('objectMetadataItems', [ { id: selectedItem.targetObjectMetadataId, diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditLinkItemView.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditLinkItemView.tsx index 10c54a4644..aa5ae72461 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditLinkItemView.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditLinkItemView.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { ensureAbsoluteUrl } from 'twenty-shared/utils'; import { type NavigationMenuItem } from '~/generated-metadata/graphql'; +import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState'; import { extractDomainFromUrl } from '@/navigation-menu-item/display/link/utils/extractDomainFromUrl'; import { type OrganizeActionsProps, @@ -14,6 +15,7 @@ import { getOrganizeActionsSelectableItemIds } from '@/navigation-menu-item/edit import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; import { SidePanelList } from '@/side-panel/components/SidePanelList'; import { TextInput } from '@/ui/input/components/TextInput'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; type SidePanelEditLinkItemViewProps = OrganizeActionsProps & { selectedItem: NavigationMenuItem; @@ -39,6 +41,10 @@ export const SidePanelEditLinkItemView = ({ showMoveToFolder = false, }: SidePanelEditLinkItemViewProps) => { const { t } = useLingui(); + const navigationMenuItemEditSection = useAtomStateValue( + navigationMenuItemEditSectionState, + ); + const isDraftMode = navigationMenuItemEditSection === 'workspace'; const [urlEditInput, setUrlEditInput] = useState(''); const [lastAutoSetName, setLastAutoSetName] = useState(null); @@ -57,7 +63,9 @@ export const SidePanelEditLinkItemView = ({ const handleUrlChange = (value: string) => { setUrlEditInput(value); - if (!canAutoUpdateName) return; + // A personal favorite persists immediately, so the auto-derived name is + // applied on blur (see handleUrlBlur) rather than on every keystroke. + if (!isDraftMode || !canAutoUpdateName) return; const trimmed = value.trim(); if (!isNonEmptyString(trimmed)) return; const domain = extractDomainFromUrl(ensureAbsoluteUrl(trimmed)); @@ -69,10 +77,21 @@ export const SidePanelEditLinkItemView = ({ const handleUrlBlur = (event: React.FocusEvent) => { const value = event.target.value.trim(); - if (isNonEmptyString(value)) { - onUpdateLink(selectedItem.id, { link: ensureAbsoluteUrl(value) }); - setUrlEditInput(''); + if (!isNonEmptyString(value)) { + return; } + const absoluteUrl = ensureAbsoluteUrl(value); + const updates: { link: string; name?: string } = { link: absoluteUrl }; + const autoName = + !isDraftMode && canAutoUpdateName + ? extractDomainFromUrl(absoluteUrl) + : undefined; + if (autoName !== undefined) { + setLastAutoSetName(autoName); + updates.name = autoName; + } + onUpdateLink(selectedItem.id, updates); + setUrlEditInput(''); }; return ( diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditOwnerSection.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditOwnerSection.tsx index cf13d9c124..f8554c5dae 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditOwnerSection.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditOwnerSection.tsx @@ -4,7 +4,7 @@ import { IconApps } from 'twenty-ui/display'; import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; -import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; import { useQuery } from '@apollo/client/react'; @@ -20,14 +20,13 @@ export const SidePanelEditOwnerSection = ({ const { t } = useLingui(); const { selectedItem } = useSelectedNavigationMenuItemEditItem(); - const { currentDraft } = useDraftNavigationMenuItems(); + const { currentItems } = useNavigationMenuItemEditController(); - const applicationIdFromDraft = - isDefined(selectedItem) && isDefined(currentDraft) - ? currentDraft.find((item) => item.id === selectedItem.id)?.applicationId - : undefined; + const applicationIdFromSection = isDefined(selectedItem) + ? currentItems.find((item) => item.id === selectedItem.id)?.applicationId + : undefined; - const applicationId = applicationIdProp ?? applicationIdFromDraft; + const applicationId = applicationIdProp ?? applicationIdFromSection; const { data } = useQuery(FindOneApplicationDocument, { variables: { id: applicationId ?? '' }, diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNavigationMenuItemEditPage.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNavigationMenuItemEditPage.tsx index 6c93cec625..ffafb6769f 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNavigationMenuItemEditPage.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNavigationMenuItemEditPage.tsx @@ -1,13 +1,13 @@ import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState'; -import { useDraftNavigationMenuItemsAllFolders } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItemsAllFolders'; -import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState'; +import { useNavigationMenuItemSectionAllFolders } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemSectionAllFolders'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem'; import { NavigationMenuItemType } from 'twenty-shared/types'; import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState'; import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItemLabel'; -import { useUpdateLinkInDraft } from '@/navigation-menu-item/edit/link/hooks/useUpdateLinkInDraft'; +import { buildNavigationMenuLinkUpdate } from '@/navigation-menu-item/common/utils/buildNavigationMenuLinkUpdate'; import { SidePanelEditColorOption } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditColorOption'; import { SidePanelEditLinkItemView } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditLinkItemView'; import { SidePanelEditObjectViewBase } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditObjectViewBase'; @@ -49,7 +49,7 @@ export const SidePanelNavigationMenuItemEditPage = () => { const { selectedItemLabel } = useSelectedNavigationMenuItemEditItemLabel(); const { selectedItem } = useSelectedNavigationMenuItemEditItem(); const selectedItemType = selectedItem?.type ?? null; - const { allFolders } = useDraftNavigationMenuItemsAllFolders(); + const { allFolders } = useNavigationMenuItemSectionAllFolders(); const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory(); const openFolderPicker = () => @@ -65,8 +65,7 @@ export const SidePanelNavigationMenuItemEditPage = () => { onAddAfter, } = useNavigationMenuItemEditOrganizeActions(); - const { updateLinkInDraft } = useUpdateLinkInDraft(); - const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState(); + const { currentItems, updateItem } = useNavigationMenuItemEditController(); const setPendingInsertionNavigationMenuItem = useSetAtomState( pendingInsertionNavigationMenuItemState, ); @@ -83,7 +82,7 @@ export const SidePanelNavigationMenuItemEditPage = () => { if (!selectedItem || selectedItem.type !== NavigationMenuItemType.FOLDER) { return; } - const folderItemCount = workspaceNavigationMenuItems.filter( + const folderItemCount = currentItems.filter( (item) => item.folderId === selectedItem.id, ).length; setPendingInsertionNavigationMenuItem({ @@ -143,9 +142,9 @@ export const SidePanelNavigationMenuItemEditPage = () => { - updateLinkInDraft(linkId, updates) - } + onUpdateLink={(linkId, updates) => { + void updateItem(linkId, buildNavigationMenuLinkUpdate(updates)); + }} onOpenFolderPicker={openFolderPicker} showMoveToFolder={canMoveToOtherFolder} canMoveUp={canMoveUp} diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemObjectSubPage.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemObjectSubPage.tsx index 328b53029b..44d7eb62d3 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemObjectSubPage.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemObjectSubPage.tsx @@ -1,16 +1,16 @@ import { useState } from 'react'; +import { NavigationMenuItemType } from 'twenty-shared/types'; -import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback'; -import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems'; -import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft'; -import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState'; -import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/edit/object/hooks/useAddObjectToNavigationMenuDraft'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; +import { useNavigationMenuObjectMetadataForSection } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection'; +import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; import { SidePanelObjectPickerSubView } from '@/navigation-menu-item/edit/side-panel/components/SidePanelObjectPickerSubView'; import { getAvailableObjectMetadataForNewSidebarItem } from '@/navigation-menu-item/edit/side-panel/utils/getAvailableObjectMetadataForNewSidebarItem'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; +import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback'; import { useSidePanelSubPageHistory } from '@/side-panel/hooks/useSidePanelSubPageHistory'; import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; @@ -22,9 +22,8 @@ export const SidePanelNewSidebarItemObjectSubPage = () => { const { getIcon } = useIcons(); const [objectSearchInput, setObjectSearchInput] = useState(''); - const { currentDraft } = useDraftNavigationMenuItems(); + const { currentItems, createItem } = useNavigationMenuItemEditController(); const { objectMetadataItems } = useObjectMetadataItems(); - const { addObjectToDraft } = useAddObjectToNavigationMenuDraft(); const { openNavigationMenuItemInSidePanel } = useOpenNavigationMenuItemInSidePanel(); const { activeNonSystemObjectMetadataItems } = @@ -36,8 +35,8 @@ export const SidePanelNewSidebarItemObjectSubPage = () => { const { views, objectMetadataIdsWithIndexView, - objectMetadataIdsInWorkspace, - } = useNavigationMenuObjectMetadataFromDraft(currentDraft); + objectMetadataIdsAlreadyAdded, + } = useNavigationMenuObjectMetadataForSection(currentItems); const objectMetadataIdsWithDisplayableViews = new Set( views @@ -56,16 +55,20 @@ export const SidePanelNewSidebarItemObjectSubPage = () => { const handleSelectObject = ( objectMetadataItem: EnrichedObjectMetadataItem, ) => { - if (objectMetadataIdsInWorkspace.has(objectMetadataItem.id)) { + if (objectMetadataIdsAlreadyAdded.has(objectMetadataItem.id)) { return; } - const itemId = addObjectToDraft({ - objectMetadataId: objectMetadataItem.id, - currentDraft, - targetFolderId: pendingInsertionNavigationMenuItem?.folderId, - targetIndex: pendingInsertionNavigationMenuItem?.position, - color: getObjectColorWithFallback(objectMetadataItem), - }); + const itemId = createItem( + { + type: NavigationMenuItemType.OBJECT, + targetObjectMetadataId: objectMetadataItem.id, + color: getObjectColorWithFallback(objectMetadataItem), + }, + { + targetFolderId: pendingInsertionNavigationMenuItem?.folderId, + targetIndex: pendingInsertionNavigationMenuItem?.position, + }, + ); setPendingInsertionNavigationMenuItem(null); openNavigationMenuItemInSidePanel({ itemId, diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemObjectSystemPickerSubPage.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemObjectSystemPickerSubPage.tsx index 7c8e3f8071..05a9934254 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemObjectSystemPickerSubPage.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemObjectSystemPickerSubPage.tsx @@ -1,19 +1,19 @@ import { useLingui } from '@lingui/react/macro'; +import { useState } from 'react'; +import { NavigationMenuItemType } from 'twenty-shared/types'; -import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback'; -import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems'; -import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft'; -import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState'; -import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/edit/object/hooks/useAddObjectToNavigationMenuDraft'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; +import { useNavigationMenuObjectMetadataForSection } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection'; +import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; import { SidePanelSystemObjectPickerSubView } from '@/navigation-menu-item/edit/side-panel/components/SidePanelSystemObjectPickerSubView'; import { getAvailableObjectMetadataForNewSidebarItem } from '@/navigation-menu-item/edit/side-panel/utils/getAvailableObjectMetadataForNewSidebarItem'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; +import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { ViewKey } from '@/views/types/ViewKey'; -import { useState } from 'react'; import { useIcons } from 'twenty-ui/display'; export const SidePanelNewSidebarItemObjectSystemPickerSubPage = () => { @@ -21,9 +21,8 @@ export const SidePanelNewSidebarItemObjectSystemPickerSubPage = () => { const { getIcon } = useIcons(); const [searchValue, setSearchValue] = useState(''); - const { currentDraft } = useDraftNavigationMenuItems(); + const { currentItems, createItem } = useNavigationMenuItemEditController(); const { objectMetadataItems } = useObjectMetadataItems(); - const { addObjectToDraft } = useAddObjectToNavigationMenuDraft(); const { openNavigationMenuItemInSidePanel } = useOpenNavigationMenuItemInSidePanel(); const { activeNonSystemObjectMetadataItems } = @@ -35,8 +34,8 @@ export const SidePanelNewSidebarItemObjectSystemPickerSubPage = () => { const { views, objectMetadataIdsWithIndexView, - objectMetadataIdsInWorkspace, - } = useNavigationMenuObjectMetadataFromDraft(currentDraft); + objectMetadataIdsAlreadyAdded, + } = useNavigationMenuObjectMetadataForSection(currentItems); const objectMetadataIdsWithDisplayableViews = new Set( views @@ -55,16 +54,20 @@ export const SidePanelNewSidebarItemObjectSystemPickerSubPage = () => { const handleSelectObject = ( objectMetadataItem: EnrichedObjectMetadataItem, ) => { - if (objectMetadataIdsInWorkspace.has(objectMetadataItem.id)) { + if (objectMetadataIdsAlreadyAdded.has(objectMetadataItem.id)) { return; } - const itemId = addObjectToDraft({ - objectMetadataId: objectMetadataItem.id, - currentDraft, - targetFolderId: pendingInsertionNavigationMenuItem?.folderId, - targetIndex: pendingInsertionNavigationMenuItem?.position, - color: getObjectColorWithFallback(objectMetadataItem), - }); + const itemId = createItem( + { + type: NavigationMenuItemType.OBJECT, + targetObjectMetadataId: objectMetadataItem.id, + color: getObjectColorWithFallback(objectMetadataItem), + }, + { + targetFolderId: pendingInsertionNavigationMenuItem?.folderId, + targetIndex: pendingInsertionNavigationMenuItem?.position, + }, + ); setPendingInsertionNavigationMenuItem(null); openNavigationMenuItemInSidePanel({ itemId, diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordItem.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordItem.tsx index e639852ca7..16508dafe9 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordItem.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordItem.tsx @@ -1,18 +1,18 @@ -import { Avatar, IconFolder, useIcons } from 'twenty-ui/display'; - -import { useAddRecordToNavigationMenuDraft } from '@/navigation-menu-item/edit/record/hooks/useAddRecordToNavigationMenuDraft'; -import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems'; -import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; -import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState'; -import type { AddToNavigationDragPayload } from '@/navigation-menu-item/common/types/add-to-navigation-drag-payload'; -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag'; -import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; -import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; +import { Avatar, useIcons } from 'twenty-ui/display'; import { CoreObjectNameSingular, NavigationMenuItemType, } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState'; +import type { AddToNavigationDragPayload } from '@/navigation-menu-item/common/types/add-to-navigation-drag-payload'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; +import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag'; +import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; +import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; type SearchRecord = { recordId: string; @@ -31,8 +31,7 @@ export const SidePanelNewSidebarItemRecordItem = ({ dragIndex, }: SidePanelNewSidebarItemRecordItemProps) => { const { getIcon } = useIcons(); - const { addRecordToDraft } = useAddRecordToNavigationMenuDraft(); - const { currentDraft } = useDraftNavigationMenuItems(); + const { createItem } = useNavigationMenuItemEditController(); const [ pendingInsertionNavigationMenuItem, setPendingInsertionNavigationMenuItem, @@ -53,24 +52,30 @@ export const SidePanelNewSidebarItemRecordItem = ({ }; const handleSelectRecord = () => { - const itemId = addRecordToDraft( + if (!isDefined(objectMetadataItem)) { + return; + } + const itemId = createItem( { - recordId: record.recordId, - objectNameSingular: record.objectNameSingular, - label: record.label, - imageUrl: record.imageUrl, + type: NavigationMenuItemType.RECORD, + targetObjectMetadataId: objectMetadataItem.id, + targetRecordId: record.recordId, + targetRecordIdentifier: { + id: record.recordId, + labelIdentifier: record.label, + imageIdentifier: record.imageUrl ?? null, + }, + }, + { + targetFolderId: pendingInsertionNavigationMenuItem?.folderId ?? null, + targetIndex: pendingInsertionNavigationMenuItem?.position, }, - currentDraft, - pendingInsertionNavigationMenuItem?.folderId ?? null, - pendingInsertionNavigationMenuItem?.position, ); setPendingInsertionNavigationMenuItem(null); openNavigationMenuItemInSidePanel({ itemId, pageTitle: record.label, - pageIcon: objectMetadataItem - ? getIcon(objectMetadataItem.icon) - : IconFolder, + pageIcon: getIcon(objectMetadataItem.icon), }); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordSubPage.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordSubPage.tsx index 67dec64893..512e0699f4 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordSubPage.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordSubPage.tsx @@ -4,7 +4,7 @@ import { isDefined } from 'twenty-shared/utils'; import { useDebounce } from 'use-debounce'; import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults'; -import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { useReadableObjectMetadataItems } from '@/object-metadata/hooks/useReadableObjectMetadataItems'; import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable'; @@ -27,7 +27,7 @@ type SearchRecordBase = { export const SidePanelNewSidebarItemRecordSubPage = () => { const { t } = useLingui(); - const { currentDraft } = useDraftNavigationMenuItems(); + const { currentItems } = useNavigationMenuItemEditController(); const [recordSearchInput, setRecordSearchInput] = useState(''); const [deferredRecordSearchInput] = useDebounce(recordSearchInput, 300); const coreClient = useApolloCoreClient(); @@ -65,8 +65,8 @@ export const SidePanelNewSidebarItemRecordSubPage = () => { }, ); - const workspaceRecordIds = new Set( - currentDraft.flatMap((item) => + const recordIdsAlreadyAdded = new Set( + currentItems.flatMap((item) => isDefined(item.targetRecordId) ? [item.targetRecordId] : [], ), ); @@ -74,7 +74,7 @@ export const SidePanelNewSidebarItemRecordSubPage = () => { const searchRecords = searchData?.search?.edges?.map((edge) => edge.node) ?? []; const availableSearchRecords = searchRecords.filter( - (record) => !workspaceRecordIds.has(record.recordId), + (record) => !recordIdsAlreadyAdded.has(record.recordId), ) as SearchRecordBase[]; const isEmpty = availableSearchRecords.length === 0 && !recordSearchLoading; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewObjectPickerSubPage.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewObjectPickerSubPage.tsx index 4c921fe7f8..19dba9cc83 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewObjectPickerSubPage.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewObjectPickerSubPage.tsx @@ -1,5 +1,5 @@ -import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems'; -import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; +import { useNavigationMenuObjectMetadataForSection } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection'; import { SidePanelNewSidebarItemViewObjectPickerSubView } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewObjectPickerSubView'; import { getAvailableObjectMetadataForNewSidebarItem } from '@/navigation-menu-item/edit/side-panel/utils/getAvailableObjectMetadataForNewSidebarItem'; import { isViewDisplayableInNavigationMenu } from '@/navigation-menu-item/edit/side-panel/utils/isViewDisplayableInNavigationMenu'; @@ -19,19 +19,19 @@ export const SidePanelNewSidebarItemViewObjectPickerSubPage = () => { selectedObjectMetadataIdForViewFlowState, ); - const { currentDraft } = useDraftNavigationMenuItems(); + const { currentItems } = useNavigationMenuItemEditController(); const { objectMetadataItems } = useObjectMetadataItems(); const { activeNonSystemObjectMetadataItems } = useFilteredObjectMetadataItems(); - const { views, objectMetadataIdsWithIndexView, viewIdsInWorkspace } = - useNavigationMenuObjectMetadataFromDraft(currentDraft); + const { views, objectMetadataIdsWithIndexView, viewIdsAlreadyAdded } = + useNavigationMenuObjectMetadataForSection(currentItems); const objectMetadataIdsWithDisplayableViews = new Set( views .filter( (view) => isViewDisplayableInNavigationMenu(view) && - !viewIdsInWorkspace.has(view.id), + !viewIdsAlreadyAdded.has(view.id), ) .map((view) => view.objectMetadataId), ); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewPickerSubView.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewPickerSubView.tsx index 379dcbe290..6e52cf434a 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewPickerSubView.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewPickerSubView.tsx @@ -1,8 +1,7 @@ import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/display/view/components/ObjectIconWithViewOverlay'; import { NavigationMenuItemType } from 'twenty-shared/types'; -import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/edit/view/hooks/useAddViewToNavigationMenuDraft'; -import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems'; -import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; +import { useNavigationMenuObjectMetadataForSection } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection'; import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; import { isViewDisplayableInNavigationMenu } from '@/navigation-menu-item/edit/side-panel/utils/isViewDisplayableInNavigationMenu'; import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState'; @@ -32,8 +31,7 @@ export const SidePanelNewSidebarItemViewPickerSubView = ({ const { t } = useLingui(); const { getIcon } = useIcons(); const [searchValue, setSearchValue] = useState(''); - const { addViewToDraft } = useAddViewToNavigationMenuDraft(); - const { currentDraft } = useDraftNavigationMenuItems(); + const { currentItems, createItem } = useNavigationMenuItemEditController(); const [ pendingInsertionNavigationMenuItem, setPendingInsertionNavigationMenuItem, @@ -41,7 +39,7 @@ export const SidePanelNewSidebarItemViewPickerSubView = ({ const { openNavigationMenuItemInSidePanel } = useOpenNavigationMenuItemInSidePanel(); const { objectMetadataItems } = useObjectMetadataItems(); - const { views } = useNavigationMenuObjectMetadataFromDraft(currentDraft); + const { views } = useNavigationMenuObjectMetadataForSection(currentItems); const viewsForSelectedObject = views .filter( @@ -74,12 +72,16 @@ export const SidePanelNewSidebarItemViewPickerSubView = ({ : undefined; const handleSelectView = (view: View) => { - const itemId = addViewToDraft( - view.id, - currentDraft, - pendingInsertionNavigationMenuItem?.folderId ?? null, - pendingInsertionNavigationMenuItem?.position, - selectedObjectIconColor, + const itemId = createItem( + { + type: NavigationMenuItemType.VIEW, + viewId: view.id, + color: selectedObjectIconColor, + }, + { + targetFolderId: pendingInsertionNavigationMenuItem?.folderId ?? null, + targetIndex: pendingInsertionNavigationMenuItem?.position, + }, ); setPendingInsertionNavigationMenuItem(null); openNavigationMenuItemInSidePanel({ diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewSystemPickerSubPage.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewSystemPickerSubPage.tsx index 1c3ec5e78d..fae0b18e79 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewSystemPickerSubPage.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewSystemPickerSubPage.tsx @@ -1,5 +1,5 @@ -import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems'; -import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; +import { useNavigationMenuObjectMetadataForSection } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection'; import { SidePanelNewSidebarItemViewSystemSubView } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewSystemSubView'; import { getAvailableObjectMetadataForNewSidebarItem } from '@/navigation-menu-item/edit/side-panel/utils/getAvailableObjectMetadataForNewSidebarItem'; import { isViewDisplayableInNavigationMenu } from '@/navigation-menu-item/edit/side-panel/utils/isViewDisplayableInNavigationMenu'; @@ -19,19 +19,19 @@ export const SidePanelNewSidebarItemViewSystemPickerSubPage = () => { selectedObjectMetadataIdForViewFlowState, ); - const { currentDraft } = useDraftNavigationMenuItems(); + const { currentItems } = useNavigationMenuItemEditController(); const { objectMetadataItems } = useObjectMetadataItems(); const { activeNonSystemObjectMetadataItems } = useFilteredObjectMetadataItems(); - const { views, objectMetadataIdsWithIndexView, viewIdsInWorkspace } = - useNavigationMenuObjectMetadataFromDraft(currentDraft); + const { views, objectMetadataIdsWithIndexView, viewIdsAlreadyAdded } = + useNavigationMenuObjectMetadataForSection(currentItems); const objectMetadataIdsWithDisplayableViews = new Set( views .filter( (view) => isViewDisplayableInNavigationMenu(view) && - !viewIdsInWorkspace.has(view.id), + !viewIdsAlreadyAdded.has(view.id), ) .map((view) => view.objectMetadataId), ); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelObjectMenuItem.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelObjectMenuItem.tsx index db71ddb503..781f56f95c 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelObjectMenuItem.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelObjectMenuItem.tsx @@ -3,8 +3,8 @@ import { isDefined } from 'twenty-shared/utils'; import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; import { ObjectMetadataIcon } from '@/object-metadata/components/ObjectMetadataIcon'; import { NavigationMenuItemType } from 'twenty-shared/types'; -import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems'; -import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; +import { useNavigationMenuObjectMetadataForSection } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection'; import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag'; @@ -28,10 +28,10 @@ export const SidePanelObjectMenuItem = ({ disableDrag = false, }: SidePanelObjectMenuItemProps) => { const { t } = useLingui(); - const { currentDraft } = useDraftNavigationMenuItems(); - const { objectMetadataIdsInWorkspace } = - useNavigationMenuObjectMetadataFromDraft(currentDraft); - const isAlreadyInNavbar = objectMetadataIdsInWorkspace.has( + const { currentItems } = useNavigationMenuItemEditController(); + const { objectMetadataIdsAlreadyAdded } = + useNavigationMenuObjectMetadataForSection(currentItems); + const isAlreadyInNavbar = objectMetadataIdsAlreadyAdded.has( objectMetadataItem.id, ); const defaultViewId = useAtomFamilySelectorValue( diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useAddFolderToNavigationMenu.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useAddFolderToNavigationMenu.ts index d75477a90d..ac316705b8 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useAddFolderToNavigationMenu.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useAddFolderToNavigationMenu.ts @@ -1,22 +1,16 @@ import { useLingui } from '@lingui/react/macro'; -import { isDefined } from 'twenty-shared/utils'; +import { NavigationMenuItemType } from 'twenty-shared/types'; import { IconFolder } from 'twenty-ui/display'; -import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/edit/folder/hooks/useAddFolderToNavigationMenuDraft'; -import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState'; -import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; +import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder'; import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState'; -import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; +import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; export const useAddFolderToNavigationMenu = () => { const { t } = useLingui(); - const { addFolderToDraft } = useAddFolderToNavigationMenuDraft(); - const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState(); - const navigationMenuItemsDraft = useAtomStateValue( - navigationMenuItemsDraftState, - ); + const { createItem } = useNavigationMenuItemEditController(); const { openNavigationMenuItemInSidePanel } = useOpenNavigationMenuItemInSidePanel(); const [ @@ -24,16 +18,17 @@ export const useAddFolderToNavigationMenu = () => { setPendingInsertionNavigationMenuItem, ] = useAtomState(pendingInsertionNavigationMenuItemState); - const currentDraft = isDefined(navigationMenuItemsDraft) - ? navigationMenuItemsDraft - : workspaceNavigationMenuItems; - const handleAddFolder = () => { - const itemId = addFolderToDraft( - t`New folder`, - currentDraft, - pendingInsertionNavigationMenuItem?.folderId ?? null, - pendingInsertionNavigationMenuItem?.position, + const itemId = createItem( + { + type: NavigationMenuItemType.FOLDER, + name: t`New folder`, + color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER, + }, + { + targetFolderId: pendingInsertionNavigationMenuItem?.folderId ?? null, + targetIndex: pendingInsertionNavigationMenuItem?.position, + }, ); setPendingInsertionNavigationMenuItem(null); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useAddLinkToNavigationMenu.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useAddLinkToNavigationMenu.ts index 9fafd697dc..4d921cd693 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useAddLinkToNavigationMenu.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useAddLinkToNavigationMenu.ts @@ -1,22 +1,17 @@ import { useLingui } from '@lingui/react/macro'; -import { isDefined } from 'twenty-shared/utils'; +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { normalizeUrl } from 'twenty-shared/utils'; import { IconLink } from 'twenty-ui/display'; -import { useAddLinkToNavigationMenuDraft } from '@/navigation-menu-item/edit/link/hooks/useAddLinkToNavigationMenuDraft'; -import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState'; -import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; +import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorLink'; import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState'; -import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; +import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; export const useAddLinkToNavigationMenu = () => { const { t } = useLingui(); - const { addLinkToDraft } = useAddLinkToNavigationMenuDraft(); - const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState(); - const navigationMenuItemsDraft = useAtomStateValue( - navigationMenuItemsDraftState, - ); + const { createItem } = useNavigationMenuItemEditController(); const { openNavigationMenuItemInSidePanel } = useOpenNavigationMenuItemInSidePanel(); const [ @@ -24,17 +19,18 @@ export const useAddLinkToNavigationMenu = () => { setPendingInsertionNavigationMenuItem, ] = useAtomState(pendingInsertionNavigationMenuItemState); - const currentDraft = isDefined(navigationMenuItemsDraft) - ? navigationMenuItemsDraft - : workspaceNavigationMenuItems; - const handleAddLink = () => { - const itemId = addLinkToDraft( - t`Link label`, - 'www.example.com', - currentDraft, - pendingInsertionNavigationMenuItem?.folderId ?? null, - pendingInsertionNavigationMenuItem?.position, + const itemId = createItem( + { + type: NavigationMenuItemType.LINK, + name: t`Link label`, + link: normalizeUrl('www.example.com'), + color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK, + }, + { + targetFolderId: pendingInsertionNavigationMenuItem?.folderId ?? null, + targetIndex: pendingInsertionNavigationMenuItem?.position, + }, ); setPendingInsertionNavigationMenuItem(null); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useFolderPickerSelectionData.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useFolderPickerSelectionData.ts index 200c2e6956..b036962717 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useFolderPickerSelectionData.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useFolderPickerSelectionData.ts @@ -1,8 +1,8 @@ import { isDefined } from 'twenty-shared/utils'; import { NavigationMenuItemType } from 'twenty-shared/types'; -import { useDraftNavigationMenuItemsAllFolders } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItemsAllFolders'; -import { useDraftNavigationMenuItemsWorkspaceFolders } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItemsWorkspaceFolders'; +import { useNavigationMenuItemSectionAllFolders } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemSectionAllFolders'; +import { useNavigationMenuItemSectionFolders } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemSectionFolders'; import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove'; import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem'; import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState'; @@ -46,8 +46,8 @@ export const useFolderPickerSelectionData = () => { ); const { selectedItem } = useSelectedNavigationMenuItemEditItem(); const selectedItemType = selectedItem?.type ?? null; - const { allFolders } = useDraftNavigationMenuItemsAllFolders(); - const { workspaceFolders } = useDraftNavigationMenuItemsWorkspaceFolders(); + const { allFolders } = useNavigationMenuItemSectionAllFolders(); + const { sectionFolders } = useNavigationMenuItemSectionFolders(); const selectedFolderId = selectedItemType === NavigationMenuItemType.FOLDER @@ -83,11 +83,11 @@ export const useFolderPickerSelectionData = () => { const foldersToShow = includeNoFolderOption ? folders - : excludeCurrentFolder(workspaceFolders, currentFolderId); + : excludeCurrentFolder(sectionFolders, currentFolderId); const handleSelectFolder = (folderId: string | null) => { if (isDefined(selectedNavigationMenuItemIdInEditMode)) { - moveToFolder(selectedNavigationMenuItemIdInEditMode, folderId); + void moveToFolder(selectedNavigationMenuItemIdInEditMode, folderId); closeSidePanelMenu(); } }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts index 452f126a24..06b1973b7d 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/hooks/useNavigationMenuItemEditOrganizeActions.ts @@ -8,10 +8,9 @@ import { IconDotsVertical } from 'twenty-ui/display'; import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState'; import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState'; import { type PendingInsertionNavigationMenuItem } from '@/navigation-menu-item/common/types/PendingInsertionNavigationMenuItem'; -import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; +import { useNavigationMenuItemEditSectionItems } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditSectionItems'; import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove'; -import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState'; import { type OrganizeActionsProps } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditOrganizeActions'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel'; @@ -25,16 +24,12 @@ import { type NavigationMenuItem } from '~/generated-metadata/graphql'; const computeInsertionPosition = ( selectedItem: { id: string; folderId?: string | null }, - workspaceNavigationMenuItems: NavigationMenuItem[], + sectionItems: NavigationMenuItem[], offset: 0 | 1, ): PendingInsertionNavigationMenuItem | null => { const folderId = selectedItem.folderId ?? null; - const itemsInFolderSorted = workspaceNavigationMenuItems - .filter( - (item) => - (item.folderId ?? null) === folderId && - !isDefined(item.userWorkspaceId), - ) + const itemsInFolderSorted = sectionItems + .filter((item) => (item.folderId ?? null) === folderId) .sort((a, b) => a.position - b.position); const selectedIndexSorted = itemsInFolderSorted.findIndex( (item) => item.id === selectedItem.id, @@ -66,8 +61,7 @@ export const useNavigationMenuItemEditOrganizeActions = const setPendingInsertionNavigationMenuItem = useSetAtomState( pendingInsertionNavigationMenuItemState, ); - const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState(); - const items = useNavigationMenuItemSectionItems(); + const items = useNavigationMenuItemEditSectionItems(); const { moveUp, moveDown, remove } = useNavigationMenuItemMoveRemove(); const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); const views = useAtomStateValue(viewsSelector); @@ -99,13 +93,13 @@ export const useNavigationMenuItemEditOrganizeActions = const handleMoveUp = () => { if (canMoveUp && isDefined(selectedNavigationMenuItemIdInEditMode)) { - moveUp(selectedNavigationMenuItemIdInEditMode); + void moveUp(selectedNavigationMenuItemIdInEditMode); } }; const handleMoveDown = () => { if (canMoveDown && isDefined(selectedNavigationMenuItemIdInEditMode)) { - moveDown(selectedNavigationMenuItemIdInEditMode); + void moveDown(selectedNavigationMenuItemIdInEditMode); } }; @@ -118,7 +112,7 @@ export const useNavigationMenuItemEditOrganizeActions = siblings[selectedIndexInSiblings + 1] ?? siblings[selectedIndexInSiblings - 1]; - remove(selectedNavigationMenuItemIdInEditMode); + void remove(selectedNavigationMenuItemIdInEditMode); if (isDefined(nextItem)) { setSelectedNavigationMenuItemIdInEditMode(nextItem.id); @@ -144,11 +138,7 @@ export const useNavigationMenuItemEditOrganizeActions = const handleAddAtOffset = (offset: 0 | 1) => { if (!isDefined(selectedItem)) return; - const insertion = computeInsertionPosition( - selectedItem, - workspaceNavigationMenuItems, - offset, - ); + const insertion = computeInsertionPosition(selectedItem, items, offset); if (!insertion) return; const title = diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/view/hooks/useAddViewToNavigationMenuDraft.ts b/packages/twenty-front/src/modules/navigation-menu-item/edit/view/hooks/useAddViewToNavigationMenuDraft.ts deleted file mode 100644 index bbd3fdfdcd..0000000000 --- a/packages/twenty-front/src/modules/navigation-menu-item/edit/view/hooks/useAddViewToNavigationMenuDraft.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { NavigationMenuItemType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { v4 } from 'uuid'; -import type { NavigationMenuItem } from '~/generated-metadata/graphql'; - -import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; -import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; - -export const useAddViewToNavigationMenuDraft = () => { - const setNavigationMenuItemsDraft = useSetAtomState( - navigationMenuItemsDraftState, - ); - - const addViewToDraft = ( - viewId: string, - currentDraft: NavigationMenuItem[], - targetFolderId?: string | null, - targetIndex?: number, - color?: string | null, - ): string => { - const folderId = targetFolderId ?? null; - - const itemsInFolder = currentDraft.filter( - (item) => - (item.folderId ?? null) === folderId && - !isDefined(item.userWorkspaceId), - ); - const index = targetIndex ?? itemsInFolder.length; - - const { flatIndex, position } = computeInsertIndexAndPosition( - currentDraft, - folderId, - index, - ); - - const newItemId = v4(); - const newItem: NavigationMenuItem = { - __typename: 'NavigationMenuItem', - id: newItemId, - type: NavigationMenuItemType.VIEW, - viewId, - targetObjectMetadataId: undefined, - position, - userWorkspaceId: undefined, - targetRecordId: undefined, - folderId: folderId ?? undefined, - name: undefined, - applicationId: undefined, - color, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - - const newDraft = [ - ...currentDraft.slice(0, flatIndex), - newItem, - ...currentDraft.slice(flatIndex), - ]; - setNavigationMenuItemsDraft(newDraft); - return newItemId; - }; - - return { addViewToDraft }; -}; diff --git a/packages/twenty-front/src/modules/side-panel/components/SidePanelFolderInfo.tsx b/packages/twenty-front/src/modules/side-panel/components/SidePanelFolderInfo.tsx index e4b9689f5f..8cf09e89ab 100644 --- a/packages/twenty-front/src/modules/side-panel/components/SidePanelFolderInfo.tsx +++ b/packages/twenty-front/src/modules/side-panel/components/SidePanelFolderInfo.tsx @@ -1,12 +1,14 @@ import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; import { NavigationMenuItemType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { TintedIconTile, useIcons } from 'twenty-ui/display'; import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault'; import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState'; -import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; -import { useUpdateFolderInDraft } from '@/navigation-menu-item/edit/folder/hooks/useUpdateFolderInDraft'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; +import { useNavigationMenuItemEditSectionItems } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditSectionItems'; +import { useNavigationMenuItemTitleEdit } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemTitleEdit'; import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout'; import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState'; import { sidePanelShouldFocusTitleInputComponentState } from '@/side-panel/states/sidePanelShouldFocusTitleInputComponentState'; @@ -31,8 +33,8 @@ export const SidePanelFolderInfo = () => { const selectedNavigationMenuItemIdInEditMode = useAtomStateValue( selectedNavigationMenuItemIdInEditModeState, ); - const items = useNavigationMenuItemSectionItems(); - const { updateFolderInDraft } = useUpdateFolderInDraft(); + const items = useNavigationMenuItemEditSectionItems(); + const { updateItem } = useNavigationMenuItemEditController(); const defaultLabel = t`New folder`; const placeholder = t`Folder name`; @@ -45,23 +47,18 @@ export const SidePanelFolderInfo = () => { ) : undefined; - if (!selectedItem) return null; + const { value, handleChange, handleSave } = useNavigationMenuItemTitleEdit({ + itemId: selectedItem?.id ?? null, + itemName: selectedItem?.name ?? defaultLabel, + defaultLabel, + persistName: (name) => { + if (isDefined(selectedItem)) { + void updateItem(selectedItem.id, { name }); + } + }, + }); - const itemId = selectedItem.id; - const itemName = selectedItem.name ?? defaultLabel; - - const handleChange = (text: string) => { - updateFolderInDraft(itemId, { name: text }); - }; - - const handleSave = () => { - const trimmed = itemName.trim(); - const finalName = trimmed.length > 0 ? trimmed : defaultLabel; - - if (finalName !== itemName) { - updateFolderInDraft(itemId, { name: finalName }); - } - }; + if (!isDefined(selectedItem)) return null; const selectedIconKey = selectedItem.icon ?? FOLDER_ICON_DEFAULT; const FolderIconComponent = getIcon(selectedIconKey); @@ -73,7 +70,7 @@ export const SidePanelFolderInfo = () => { dropdownId="side-panel-folder-icon-picker" selectedIconKey={selectedIconKey} onChange={({ iconKey }) => - updateFolderInDraft(itemId, { icon: iconKey }) + void updateItem(selectedItem.id, { icon: iconKey }) } clickableComponent={ @@ -87,9 +84,9 @@ export const SidePanelFolderInfo = () => { } title={ @@ -62,9 +64,16 @@ export const SidePanelItemWithAddToNavigationDrag = ({ const setAddToNavPayloadRegistry = useSetAtomState( addToNavPayloadRegistryState, ); + const navigationMenuItemEditSection = useAtomStateValue( + navigationMenuItemEditSectionState, + ); const [isHovered, setIsHovered] = useState(false); - const showDragAffordance = !disabled && !disableDrag && isHovered; + // Favorites are added by click only; drag-to-add targets the workspace + // sidebar and runs through layout-customization mode. + const effectiveDisableDrag = + disableDrag || navigationMenuItemEditSection === 'favorite'; + const showDragAffordance = !disabled && !effectiveDisableDrag && isHovered; const contextualDescription = showDragAffordance ? t`Drag to add to navbar` : description; @@ -76,12 +85,12 @@ export const SidePanelItemWithAddToNavigationDrag = ({ payload={payload} isHovered={showDragAffordance} disabled={disabled} - disableDrag={disableDrag} + disableDrag={effectiveDisableDrag} /> ); const registerPayload = () => { - if (!disabled && !disableDrag && isDefined(dragIndex)) { + if (!disabled && !effectiveDisableDrag && isDefined(dragIndex)) { setAddToNavPayloadRegistry((prev) => new Map(prev).set(id, payload)); } }; @@ -89,15 +98,15 @@ export const SidePanelItemWithAddToNavigationDrag = ({ const menuItemContent = ( { - if (!disabled && !disableDrag) { + if (!disabled && !effectiveDisableDrag) { setIsHovered(true); registerPayload(); } }} onMouseLeave={() => { - if (!disabled && !disableDrag) setIsHovered(false); + if (!disabled && !effectiveDisableDrag) setIsHovered(false); }} onMouseDown={registerPayload} > @@ -112,7 +121,7 @@ export const SidePanelItemWithAddToNavigationDrag = ({ ); - if (!isDefined(dragIndex) || disableDrag) { + if (!isDefined(dragIndex) || effectiveDisableDrag) { return menuItemContent; } diff --git a/packages/twenty-front/src/modules/side-panel/components/SidePanelLinkInfo.tsx b/packages/twenty-front/src/modules/side-panel/components/SidePanelLinkInfo.tsx index 5dea9813eb..8a03610147 100644 --- a/packages/twenty-front/src/modules/side-panel/components/SidePanelLinkInfo.tsx +++ b/packages/twenty-front/src/modules/side-panel/components/SidePanelLinkInfo.tsx @@ -1,11 +1,13 @@ import { useLingui } from '@lingui/react/macro'; +import { NavigationMenuItemType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { IconLink, IconWorld } from 'twenty-ui/display'; -import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/display/link/components/LinkIconWithLinkOverlay'; -import { NavigationMenuItemType } from 'twenty-shared/types'; -import { useUpdateLinkInDraft } from '@/navigation-menu-item/edit/link/hooks/useUpdateLinkInDraft'; -import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems'; import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState'; +import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/display/link/components/LinkIconWithLinkOverlay'; +import { useNavigationMenuItemEditSectionItems } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditSectionItems'; +import { useNavigationMenuItemTitleEdit } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemTitleEdit'; +import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController'; import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout'; import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState'; import { sidePanelShouldFocusTitleInputComponentState } from '@/side-panel/states/sidePanelShouldFocusTitleInputComponentState'; @@ -24,8 +26,8 @@ export const SidePanelLinkInfo = () => { const selectedNavigationMenuItemIdInEditMode = useAtomStateValue( selectedNavigationMenuItemIdInEditModeState, ); - const items = useNavigationMenuItemSectionItems(); - const { updateLinkInDraft } = useUpdateLinkInDraft(); + const items = useNavigationMenuItemEditSectionItems(); + const { updateItem } = useNavigationMenuItemEditController(); const defaultLabel = t`Link label`; const placeholder = t`Link label`; @@ -38,23 +40,18 @@ export const SidePanelLinkInfo = () => { ) : undefined; - if (!selectedItem) return null; + const { value, handleChange, handleSave } = useNavigationMenuItemTitleEdit({ + itemId: selectedItem?.id ?? null, + itemName: selectedItem?.name ?? defaultLabel, + defaultLabel, + persistName: (name) => { + if (isDefined(selectedItem)) { + void updateItem(selectedItem.id, { name }); + } + }, + }); - const itemId = selectedItem.id; - const itemName = selectedItem.name ?? defaultLabel; - - const handleChange = (text: string) => { - updateLinkInDraft(itemId, { name: text }); - }; - - const handleSave = () => { - const trimmed = itemName.trim(); - const finalName = trimmed.length > 0 ? trimmed : defaultLabel; - - if (finalName !== itemName) { - updateLinkInDraft(itemId, { name: finalName }); - } - }; + if (!isDefined(selectedItem)) return null; return ( { } title={ { const selectedNavigationMenuItemIdInEditMode = useAtomStateValue( selectedNavigationMenuItemIdInEditModeState, ); - const items = useNavigationMenuItemSectionItems(); + const items = useNavigationMenuItemEditSectionItems(); if (!isDefined(pageChip)) { return null; diff --git a/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelCloseAnimationCompleteCleanup.ts b/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelCloseAnimationCompleteCleanup.ts index 94ca051a34..38ab2e71cd 100644 --- a/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelCloseAnimationCompleteCleanup.ts +++ b/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelCloseAnimationCompleteCleanup.ts @@ -1,6 +1,7 @@ import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState'; +import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState'; import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState'; import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState'; import { viewableRecordIdState } from '@/object-record/record-side-panel/states/viewableRecordIdState'; @@ -110,6 +111,7 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => { store.set(sidePanelNavigationStackState.atom, []); store.set(selectedNavigationMenuItemIdInEditModeState.atom, null); store.set(pendingInsertionNavigationMenuItemState.atom, null); + store.set(navigationMenuItemEditSectionState.atom, 'workspace'); store.set(addToNavPayloadRegistryState.atom, new Map()); resetSelectedItem(); store.set(hasUserSelectedSidePanelListItemState.atom, false);