Fix navigation drag drop indicator position (#18515)

Closes [#2295](https://github.com/twentyhq/core-team-issues/issues/2295)

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Abdul Rahman
2026-03-11 18:05:09 +05:30
committed by GitHub
parent ab5fb1f658
commit 413d1124bb
6 changed files with 81 additions and 125 deletions
@@ -6257,14 +6257,6 @@ export type GetLogicFunctionSourceCodeQuery = { __typename?: 'Query', getLogicFu
export type MarketplaceAppFieldsFragment = { __typename?: 'MarketplaceApp', id: string, name: string, description: string, icon: string, version: string, author: string, category: string, logo?: string | null, screenshots: Array<string>, aboutDescription: string, providers: Array<string>, websiteUrl?: string | null, termsUrl?: string | null, sourcePackage?: string | null, isFeatured: boolean, objects: Array<{ __typename?: 'MarketplaceAppObject', universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, fields: Array<{ __typename?: 'MarketplaceAppField', universalIdentifier?: string | null, name: string, type: string, label: string, description?: string | null, icon?: string | null }> }>, fields: Array<{ __typename?: 'MarketplaceAppField', name: string, type: string, label: string, description?: string | null, icon?: string | null, objectUniversalIdentifier?: string | null }>, logicFunctions: Array<{ __typename?: 'MarketplaceAppLogicFunction', name: string, description?: string | null, timeoutSeconds?: number | null }>, frontComponents: Array<{ __typename?: 'MarketplaceAppFrontComponent', name: string, description?: string | null }>, defaultRole?: { __typename?: 'MarketplaceAppDefaultRole', id: string, label: string, description?: string | null, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canUpdateAllSettings: boolean, canAccessAllTools: boolean, permissionFlags: Array<string>, objectPermissions: Array<{ __typename?: 'MarketplaceAppRoleObjectPermission', objectUniversalIdentifier: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }>, fieldPermissions: Array<{ __typename?: 'MarketplaceAppRoleFieldPermission', objectUniversalIdentifier: string, fieldUniversalIdentifier: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null }> } | null };
export type InstallApplicationMutationVariables = Exact<{
appRegistrationId: Scalars['String'];
version?: InputMaybe<Scalars['String']>;
}>;
export type InstallApplicationMutation = { __typename?: 'Mutation', installApplication: boolean };
export type InstallMarketplaceAppMutationVariables = Exact<{
universalIdentifier: Scalars['String'];
version?: InputMaybe<Scalars['String']>;
@@ -6281,14 +6273,6 @@ export type UpgradeApplicationMutationVariables = Exact<{
export type UpgradeApplicationMutation = { __typename?: 'Mutation', upgradeApplication: boolean };
export type UploadAppTarballMutationVariables = Exact<{
file: Scalars['Upload'];
universalIdentifier?: InputMaybe<Scalars['String']>;
}>;
export type UploadAppTarballMutation = { __typename?: 'Mutation', uploadAppTarball: { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string } };
export type FindManyMarketplaceAppsQueryVariables = Exact<{ [key: string]: never; }>;
@@ -11538,38 +11522,6 @@ export function useGetLogicFunctionSourceCodeLazyQuery(baseOptions?: Apollo.Lazy
export type GetLogicFunctionSourceCodeQueryHookResult = ReturnType<typeof useGetLogicFunctionSourceCodeQuery>;
export type GetLogicFunctionSourceCodeLazyQueryHookResult = ReturnType<typeof useGetLogicFunctionSourceCodeLazyQuery>;
export type GetLogicFunctionSourceCodeQueryResult = Apollo.QueryResult<GetLogicFunctionSourceCodeQuery, GetLogicFunctionSourceCodeQueryVariables>;
export const InstallApplicationDocument = gql`
mutation InstallApplication($appRegistrationId: String!, $version: String) {
installApplication(appRegistrationId: $appRegistrationId, version: $version)
}
`;
export type InstallApplicationMutationFn = Apollo.MutationFunction<InstallApplicationMutation, InstallApplicationMutationVariables>;
/**
* __useInstallApplicationMutation__
*
* To run a mutation, you first call `useInstallApplicationMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useInstallApplicationMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [installApplicationMutation, { data, loading, error }] = useInstallApplicationMutation({
* variables: {
* appRegistrationId: // value for 'appRegistrationId'
* version: // value for 'version'
* },
* });
*/
export function useInstallApplicationMutation(baseOptions?: Apollo.MutationHookOptions<InstallApplicationMutation, InstallApplicationMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<InstallApplicationMutation, InstallApplicationMutationVariables>(InstallApplicationDocument, options);
}
export type InstallApplicationMutationHookResult = ReturnType<typeof useInstallApplicationMutation>;
export type InstallApplicationMutationResult = Apollo.MutationResult<InstallApplicationMutation>;
export type InstallApplicationMutationOptions = Apollo.BaseMutationOptions<InstallApplicationMutation, InstallApplicationMutationVariables>;
export const InstallMarketplaceAppDocument = gql`
mutation InstallMarketplaceApp($universalIdentifier: String!, $version: String) {
installMarketplaceApp(
@@ -11640,42 +11592,6 @@ export function useUpgradeApplicationMutation(baseOptions?: Apollo.MutationHookO
export type UpgradeApplicationMutationHookResult = ReturnType<typeof useUpgradeApplicationMutation>;
export type UpgradeApplicationMutationResult = Apollo.MutationResult<UpgradeApplicationMutation>;
export type UpgradeApplicationMutationOptions = Apollo.BaseMutationOptions<UpgradeApplicationMutation, UpgradeApplicationMutationVariables>;
export const UploadAppTarballDocument = gql`
mutation UploadAppTarball($file: Upload!, $universalIdentifier: String) {
uploadAppTarball(file: $file, universalIdentifier: $universalIdentifier) {
id
universalIdentifier
name
}
}
`;
export type UploadAppTarballMutationFn = Apollo.MutationFunction<UploadAppTarballMutation, UploadAppTarballMutationVariables>;
/**
* __useUploadAppTarballMutation__
*
* To run a mutation, you first call `useUploadAppTarballMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUploadAppTarballMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [uploadAppTarballMutation, { data, loading, error }] = useUploadAppTarballMutation({
* variables: {
* file: // value for 'file'
* universalIdentifier: // value for 'universalIdentifier'
* },
* });
*/
export function useUploadAppTarballMutation(baseOptions?: Apollo.MutationHookOptions<UploadAppTarballMutation, UploadAppTarballMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UploadAppTarballMutation, UploadAppTarballMutationVariables>(UploadAppTarballDocument, options);
}
export type UploadAppTarballMutationHookResult = ReturnType<typeof useUploadAppTarballMutation>;
export type UploadAppTarballMutationResult = Apollo.MutationResult<UploadAppTarballMutation>;
export type UploadAppTarballMutationOptions = Apollo.BaseMutationOptions<UploadAppTarballMutation, UploadAppTarballMutationVariables>;
export const FindManyMarketplaceAppsDocument = gql`
query FindManyMarketplaceApps {
findManyMarketplaceApps {
@@ -1,4 +1,5 @@
import { type OnDragEndResponder } from '@hello-pangea/dnd';
import { useStore } from 'jotai';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
@@ -11,21 +12,15 @@ import {
matchesWorkspaceFolderId,
validateAndExtractWorkspaceFolderId,
} from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { isDefined } from 'twenty-shared/utils';
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => {
const store = useStore();
const { workspaceNavigationMenuItems } =
usePrefetchedNavigationMenuItemsData();
const isNavigationMenuInEditMode = useAtomStateValue(
isNavigationMenuInEditModeState,
);
const navigationMenuItemsDraft = useAtomStateValue(
navigationMenuItemsDraftState,
);
const setNavigationMenuItemsDraft = useSetAtomState(
navigationMenuItemsDraftState,
);
@@ -47,7 +42,9 @@ export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => {
};
const handleWorkspaceNavigationMenuItemDragAndDrop: OnDragEndResponder = (
result,
result: Parameters<OnDragEndResponder>[0] & {
insertBeforeItemId?: string | null;
},
) => {
const { destination, source, draggableId } = result;
@@ -70,6 +67,12 @@ export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => {
return;
}
const navigationMenuItemsDraft = store.get(
navigationMenuItemsDraftState.atom,
);
const isNavigationMenuInEditMode = store.get(
isNavigationMenuInEditModeState.atom,
);
if (!isNavigationMenuInEditMode || !navigationMenuItemsDraft) {
return;
}
@@ -122,8 +125,24 @@ export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => {
const listWithoutDragged = sourceList.filter(
(item) => item.id !== draggableId,
);
const prevItem = listWithoutDragged[destination.index - 1];
const nextItem = listWithoutDragged[destination.index];
const sourceIndexInList = sourceList.findIndex(
(item) => item.id === draggableId,
);
const insertBeforeIndex =
result.insertBeforeItemId != null
? sourceList.findIndex(
(item) => item.id === result.insertBeforeItemId,
)
: -1;
const destinationIndexInFullList =
insertBeforeIndex >= 0 ? insertBeforeIndex : destination.index;
const destIndexInListWithoutDragged =
sourceIndexInList < destinationIndexInFullList &&
destinationIndexInFullList <= listWithoutDragged.length
? destinationIndexInFullList - 1
: destinationIndexInFullList;
const prevItem = listWithoutDragged[destIndexInListWithoutDragged - 1];
const nextItem = listWithoutDragged[destIndexInListWithoutDragged];
const newPosition = getPositionBetween(
prevItem?.position,
nextItem?.position,
@@ -1,8 +1,8 @@
import { type DragDropProvider } from '@dnd-kit/react';
import { isSortable } from '@dnd-kit/react/sortable';
import type { ResponderProvided } from '@hello-pangea/dnd';
import { type ComponentProps, useCallback, useState } from 'react';
import { useStore } from 'jotai';
import { type ComponentProps, useCallback, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
@@ -96,6 +96,7 @@ export const useWorkspaceDndKit = (): {
id: string,
source: DropDestination,
destination: DropDestination,
insertBeforeItemId?: string | null,
) => {
const draggedItem = getNavItemById(id);
const destFolderId = validateAndExtractWorkspaceFolderId(
@@ -118,7 +119,11 @@ export const useWorkspaceDndKit = (): {
);
const provided: ResponderProvided = { announce: () => {} };
handleWorkspaceNavigationMenuItemDragAndDrop(
{ ...result, ...DROP_RESULT_OPTIONS },
{
...result,
...DROP_RESULT_OPTIONS,
...(insertBeforeItemId != null && { insertBeforeItemId }),
},
provided,
);
};
@@ -284,10 +289,14 @@ export const useWorkspaceDndKit = (): {
isWorkspaceDroppableId(initialGroupStr) &&
isWorkspaceDroppableId(destGroup);
if (bothWorkspace) {
const insertBeforeItemId = resolved.isTargetFolder
? null
: String(target?.id ?? '');
applyWorkspaceReorderIfAllowed(
draggableId,
{ droppableId: initialGroupStr, index: initialIndex },
resolved.destination,
insertBeforeItemId || undefined,
);
return;
}
@@ -26,6 +26,8 @@ import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/us
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const LazyWorkspaceSectionListDndKit = lazy(() =>
import(
@@ -33,6 +35,10 @@ const LazyWorkspaceSectionListDndKit = lazy(() =>
).then((m) => ({ default: m.WorkspaceSectionListDndKit })),
);
const StyledWorkspaceSectionContentGapOffset = styled.div`
margin-top: calc(-1 * ${themeCssVariables.betweenSiblingsGap});
`;
type NavigationDrawerSectionForWorkspaceItemsProps = {
sectionTitle: string;
items: FlatWorkspaceItem[];
@@ -150,44 +156,48 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
isOpen={isNavigationSectionOpen}
/>
</NavigationDrawerAnimatedCollapseWrapper>
<AnimatedExpandableContainer
isExpanded={
isNavigationSectionOpen || isAddToNavigationDropTargetVisible
}
dimension="height"
mode="fit-content"
containAnimation
initial={false}
>
{isNavigationMenuInEditMode ? (
<Suspense
fallback={
<WorkspaceSectionListEditModeFallback
<StyledWorkspaceSectionContentGapOffset>
<AnimatedExpandableContainer
isExpanded={
isNavigationSectionOpen || isAddToNavigationDropTargetVisible
}
dimension="height"
mode="fit-content"
containAnimation
initial={false}
>
{isNavigationMenuInEditMode ? (
<Suspense
fallback={
<WorkspaceSectionListEditModeFallback
filteredItems={filteredItems}
folderChildrenById={folderChildrenById}
onActiveObjectMetadataItemClick={
onActiveObjectMetadataItemClick
}
/>
}
>
<LazyWorkspaceSectionListDndKit
filteredItems={filteredItems}
getEditModeProps={getEditModeProps}
folderChildrenById={folderChildrenById}
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
onNavigationMenuItemClick={onNavigationMenuItemClick}
onActiveObjectMetadataItemClick={
onActiveObjectMetadataItemClick
}
/>
}
>
<LazyWorkspaceSectionListDndKit
</Suspense>
) : (
<NavigationDrawerSectionForWorkspaceItemsListReadOnly
filteredItems={filteredItems}
getEditModeProps={getEditModeProps}
folderChildrenById={folderChildrenById}
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
onNavigationMenuItemClick={onNavigationMenuItemClick}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
/>
</Suspense>
) : (
<NavigationDrawerSectionForWorkspaceItemsListReadOnly
filteredItems={filteredItems}
folderChildrenById={folderChildrenById}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
/>
)}
</AnimatedExpandableContainer>
)}
</AnimatedExpandableContainer>
</StyledWorkspaceSectionContentGapOffset>
</NavigationDrawerSection>
);
};
@@ -3,7 +3,6 @@ import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/components/W
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -11,6 +10,7 @@ import { styled } from '@linaria/react';
import { useContext } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
import { NavigationDrawerSectionForWorkspaceItemContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent';
import { WorkspaceOrphanDropTarget } from '@/object-metadata/components/WorkspaceOrphanDropTarget';
import { WorkspaceSectionAddMenuItemButton } from '@/object-metadata/components/WorkspaceSectionAddMenuItemButton';
@@ -20,6 +20,7 @@ const StyledList = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.betweenSiblingsGap};
padding-top: ${themeCssVariables.betweenSiblingsGap};
`;
const StyledListItemRow = styled.div`
@@ -10,6 +10,7 @@ const StyledList = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.betweenSiblingsGap};
padding-top: ${themeCssVariables.betweenSiblingsGap};
`;
type NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps = Pick<