diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useExitLayoutCustomizationMode.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useExitLayoutCustomizationMode.ts index 1b641e5c01dc9..60076e728c3e4 100644 --- a/packages/twenty-front/src/modules/layout-customization/hooks/useExitLayoutCustomizationMode.ts +++ b/packages/twenty-front/src/modules/layout-customization/hooks/useExitLayoutCustomizationMode.ts @@ -1,9 +1,7 @@ import { commandMenuItemsDraftState } from '@/command-menu-item/server-items/edit/states/commandMenuItemsDraftState'; -import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState'; import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState'; import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState'; -import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState'; import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { useStore } from 'jotai'; @@ -11,6 +9,7 @@ import { useCallback } from 'react'; export const useExitLayoutCustomizationMode = () => { const store = useStore(); + const { closeSidePanelMenu } = useSidePanelMenu(); const setNavigationMenuItemsDraft = useSetAtomState( @@ -27,9 +26,6 @@ export const useExitLayoutCustomizationMode = () => { setNavigationMenuItemsDraft(null); setSelectedNavigationMenuItemIdInEditMode(null); store.set(commandMenuItemsDraftState.atom, null); - - store.set(currentPageLayoutIdState.atom, null); - store.set(activeCustomizationPageLayoutIdsState.atom, []); setIsLayoutCustomizationModeEnabled(false); closeSidePanelMenu(); }, [ diff --git a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/computeDndReorderPosition.ts b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/computeDndReorderPosition.ts index 5c8b6e76f960a..d0b9672e3303a 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/common/utils/computeDndReorderPosition.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/common/utils/computeDndReorderPosition.ts @@ -18,11 +18,11 @@ export const computeDndReorderPosition = ({ const listWithoutDragged = sortedList.filter( (item) => item.id !== draggableId, ); - const adjustedIndex = - sourceIndexInList < destinationIndex && - destinationIndex <= listWithoutDragged.length - ? destinationIndex - 1 - : destinationIndex; + let adjustedIndex = destinationIndex; + if (sourceIndexInList < destinationIndex) { + adjustedIndex -= 1; + } + adjustedIndex = Math.min(adjustedIndex, listWithoutDragged.length); const prevItem = listWithoutDragged[adjustedIndex - 1]; const nextItem = listWithoutDragged[adjustedIndex]; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationItemDropTarget.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationItemDropTarget.tsx index 861774a8a0ae7..149fc937fcc96 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationItemDropTarget.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/dnd/components/NavigationItemDropTarget.tsx @@ -5,7 +5,10 @@ import { themeCssVariables } from 'twenty-ui/theme-constants'; import { type NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants'; import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext'; -const StyledDropTarget = styled.div<{ $compact?: boolean }>` +const StyledDropTarget = styled.div<{ + $compact?: boolean; + $highlightPosition?: 'top' | 'bottom'; +}>` min-height: ${({ $compact }) => $compact ? 0 : themeCssVariables.spacing[2]}; position: relative; @@ -17,13 +20,22 @@ const StyledDropTarget = styled.div<{ $compact?: boolean }>` &::before { content: ''; position: absolute; - bottom: 0; left: 0; width: 100%; height: 2px; background-color: ${themeCssVariables.color.blue}; + ${({ $highlightPosition }) => + $highlightPosition === 'top' + ? ` + top: 0; + border-radius: 0 0 ${themeCssVariables.border.radius.sm} + ${themeCssVariables.border.radius.sm}; + ` + : ` + bottom: 0; border-radius: ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0 0; + `} } } @@ -39,6 +51,7 @@ type NavigationItemDropTargetProps = { children?: ReactNode; compact?: boolean; dropTargetIdOverride?: string; + highlightPosition?: 'top' | 'bottom'; }; export const NavigationItemDropTarget = ({ @@ -48,6 +61,7 @@ export const NavigationItemDropTarget = ({ children, compact = false, dropTargetIdOverride, + highlightPosition = 'bottom', }: NavigationItemDropTargetProps) => { const { activeDropTargetId, forbiddenDropTargetId } = useContext( NavigationDropTargetContext, @@ -60,6 +74,7 @@ export const NavigationItemDropTarget = ({ return ( 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 fad97f320da6c..b99b75ae4fe3c 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,6 +1,7 @@ import { NavigationMenuItemType } from 'twenty-shared/types'; import { type NavigationMenuItem } from '~/generated-metadata/graphql'; +import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; import { getWorkspaceSidebarOrphanItemsInDisplayOrder } from '@/navigation-menu-item/display/utils/getWorkspaceSidebarOrphanItemsInDisplayOrder'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; @@ -22,6 +23,9 @@ export const useNavigationMenuItemSectionItems = (): NavigationMenuItem[] => { const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems(); const { workspaceNavigationMenuItemsByFolder } = useNavigationMenuItemsByFolder(); + const isLayoutCustomizationModeEnabled = useAtomStateValue( + isLayoutCustomizationModeEnabledState, + ); const views = useAtomStateValue(viewsSelector); const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); @@ -39,6 +43,7 @@ export const useNavigationMenuItemSectionItems = (): NavigationMenuItem[] => { objectMetadataItems, views, objectPermissionsByObjectMetadataId, + includeInaccessibleObjectBackedItems: isLayoutCustomizationModeEnabled, }); return flatItems.flatMap((item) => diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem.tsx index e1a3a58ee4a0c..31c053f4eaf5c 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem.tsx @@ -1,14 +1,18 @@ -import type { ReactNode } from 'react'; +import { t } from '@lingui/core/macro'; +import { isNonEmptyString } from '@sniptt/guards'; +import { Fragment, type ReactNode, useContext } from 'react'; import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState'; -import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/display/view/components/ObjectIconWithViewOverlay'; -import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback'; +import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/common/utils/recordIdentifierToObjectRecordIdentifier'; import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink'; import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel'; -import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/common/utils/recordIdentifierToObjectRecordIdentifier'; +import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/display/view/components/ObjectIconWithViewOverlay'; import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState'; import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; +import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback'; +import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { viewsSelector } from '@/views/states/selectors/viewsSelector'; @@ -19,7 +23,8 @@ import { NavigationMenuItemType, } from 'twenty-shared/types'; import { getAppPath, isDefined } from 'twenty-shared/utils'; -import { Avatar, useIcons } from 'twenty-ui/display'; +import { Avatar, IconLock, useIcons } from 'twenty-ui/display'; +import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; import { type NavigationMenuItem } from '~/generated-metadata/graphql'; export type NavigationDrawerItemForObjectMetadataItemProps = { @@ -44,12 +49,19 @@ export const NavigationDrawerItemForObjectMetadataItem = ({ const isLayoutCustomizationModeEnabled = useAtomStateValue( isLayoutCustomizationModeEnabledState, ); + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const { theme } = useContext(ThemeContext); const lastVisitedViewPerObjectMetadataItem = useAtomStateValue( lastVisitedViewPerObjectMetadataItemState, ); const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector); const views = useAtomStateValue(viewsSelector); + const canReadObjectRecords = getObjectPermissionsForObject( + objectPermissionsByObjectMetadataId, + objectMetadataItem.id, + ).canReadObjectRecords; + const lastVisitedViewId = lastVisitedViewPerObjectMetadataItem?.[objectMetadataItem.id]; @@ -107,12 +119,19 @@ export const NavigationDrawerItemForObjectMetadataItem = ({ ? getNavigationMenuItemLabel(navigationMenuItem, objectMetadataItems, views) : objectMetadataItem.labelPlural; - const label = isRecord - ? itemLabel - : isViewWithResolvedView + const primaryLabel = + isRecord || isViewWithResolvedView ? itemLabel : objectMetadataItem.labelPlural; + const needsInaccessibleRecordPlaceholder = + isLayoutCustomizationModeEnabled && + isRecord && + !canReadObjectRecords && + !isNonEmptyString(primaryLabel.trim()); + + const label = needsInaccessibleRecordPlaceholder ? t`Record` : primaryLabel; + const recordIdentifier = isRecord && isDefined(navigationMenuItem?.targetRecordIdentifier) ? recordIdentifierToObjectRecordIdentifier({ @@ -151,6 +170,9 @@ export const NavigationDrawerItemForObjectMetadataItem = ({ ? objectMetadataItem.labelSingular : undefined; + const showInaccessibleLock = + isLayoutCustomizationModeEnabled && !canReadObjectRecords; + return ( + + + ) : ( + rightOptions + ) + } /> ); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationMenuItemOrphanDropTarget.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationMenuItemOrphanDropTarget.tsx index 520f3d5986ee0..f3471c3c58c1e 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationMenuItemOrphanDropTarget.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/components/NavigationMenuItemOrphanDropTarget.tsx @@ -11,6 +11,7 @@ type NavigationMenuItemOrphanDropTargetProps = { children?: ReactNode; sectionId?: NavigationSections; droppableId?: string; + highlightPosition?: 'top' | 'bottom'; }; export const NavigationMenuItemOrphanDropTarget = ({ @@ -19,6 +20,7 @@ export const NavigationMenuItemOrphanDropTarget = ({ children, sectionId = NavigationSections.WORKSPACE, droppableId = NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS, + highlightPosition = 'bottom', }: NavigationMenuItemOrphanDropTargetProps) => ( {children} diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer.tsx index 9a68bbd994315..f723c913a0503 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer.tsx @@ -111,6 +111,10 @@ export const WorkspaceSectionContainer = ({ return false; }); + const workspaceOrphanItemsForSection = isLayoutCustomizationModeEnabled + ? flatItems + : filteredItems; + const getEditModeProps = (item: NavigationMenuItem): EditModeProps => { const itemId = item.id; return { @@ -159,14 +163,14 @@ export const WorkspaceSectionContainer = ({ } > item.type === NavigationMenuItemType.FOLDER, ).length; const isAddMenuItemButtonVisible = isLayoutCustomizationModeEnabled; + const orphanAppendDndIndex = filteredItems.length; return ( {filteredItems.map((item, index) => ( @@ -78,35 +86,41 @@ export const WorkspaceSectionListDndKit = ({ ))} - - + + {isAddMenuItemButtonVisible && } - - + + {addToNavigationFallbackDestination?.droppableId === NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS && - addToNavigationFallbackDestination.index > filteredItems.length && ( - - orphanAppendDndIndex && ( + + - + disabled={workspaceDropDisabled} + collisionPriority={FOLDER_HEADER_SLOT_COLLISION_PRIORITY} + > + + + )} ); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/utils/getWorkspaceSidebarOrphanItemsInDisplayOrder.ts b/packages/twenty-front/src/modules/navigation-menu-item/display/utils/getWorkspaceSidebarOrphanItemsInDisplayOrder.ts index 0fc44fd7edc82..d71379a525b19 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/utils/getWorkspaceSidebarOrphanItemsInDisplayOrder.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/utils/getWorkspaceSidebarOrphanItemsInDisplayOrder.ts @@ -17,6 +17,7 @@ type GetWorkspaceSidebarOrphanItemsInDisplayOrderArgs = { objectPermissionsByObjectMetadataId: Parameters< typeof getObjectPermissionsForObject >[0]; + includeInaccessibleObjectBackedItems?: boolean; }; export const getWorkspaceSidebarOrphanItemsInDisplayOrder = ({ @@ -25,6 +26,7 @@ export const getWorkspaceSidebarOrphanItemsInDisplayOrder = ({ objectMetadataItems, views, objectPermissionsByObjectMetadataId, + includeInaccessibleObjectBackedItems = false, }: GetWorkspaceSidebarOrphanItemsInDisplayOrderArgs): NavigationMenuItem[] => { const flatWorkspaceItems = workspaceNavigationMenuItems .filter((item) => !isDefined(item.folderId)) @@ -42,26 +44,34 @@ export const getWorkspaceSidebarOrphanItemsInDisplayOrder = ({ }); } else { const validItem = processedItemsById.get(item.id); - if (!isDefined(validItem)) { + if (!isDefined(validItem) && !includeInaccessibleObjectBackedItems) { return acc; } - if (validItem.type === NavigationMenuItemType.LINK) { - acc.push(validItem); - } else { - const objectMetadataItem = getObjectMetadataForNavigationMenuItem( - validItem, - objectMetadataItems, - views, - ); - if ( - isDefined(objectMetadataItem) && - getObjectPermissionsForObject( - objectPermissionsByObjectMetadataId, - objectMetadataItem.id, - ).canReadObjectRecords - ) { - acc.push(validItem); - } + const rowSource = isDefined(validItem) ? validItem : item; + + if (rowSource.type === NavigationMenuItemType.LINK) { + acc.push(rowSource); + return acc; + } + + if (includeInaccessibleObjectBackedItems) { + acc.push(rowSource); + return acc; + } + + const objectMetadataItem = getObjectMetadataForNavigationMenuItem( + rowSource, + objectMetadataItems, + views, + ); + if ( + isDefined(objectMetadataItem) && + getObjectPermissionsForObject( + objectPermissionsByObjectMetadataId, + objectMetadataItem.id, + ).canReadObjectRecords + ) { + acc.push(rowSource); } } return acc; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx index 9c6956ab67c4b..e2214d823a74c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx @@ -30,6 +30,7 @@ const StyledIndexContainer = styled.div` export const RecordIndexContainerGater = () => { const store = useStore(); + const { recordIndexId, objectMetadataItem } = useRecordIndexIdFromCurrentContextStore();