mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Navbar drag drop using dnd kit (#18288)
This commit is contained in:
parent
66d93c4d28
commit
5b28e59ca7
119 changed files with 3563 additions and 1297 deletions
|
|
@ -61,8 +61,8 @@ const jestConfig = {
|
|||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 49.5,
|
||||
lines: 48,
|
||||
statements: 49.3,
|
||||
lines: 47.9,
|
||||
functions: 39.5,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
"@calcom/embed-react": "^1.5.3",
|
||||
"@cyntler/react-doc-viewer": "^1.17.0",
|
||||
"@dagrejs/dagre": "^1.1.8",
|
||||
"@dnd-kit/react": "^0.3.2",
|
||||
"@floating-ui/react": "^0.24.3",
|
||||
"@graphiql/plugin-explorer": "^1.0.2",
|
||||
"@graphiql/react": "^0.23.0",
|
||||
|
|
|
|||
|
|
@ -29,19 +29,19 @@ import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/sin
|
|||
import { type ActionConfig } from '@/action-menu/actions/types/ActionConfig';
|
||||
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
|
||||
import { ActionType } from '@/action-menu/actions/types/ActionType';
|
||||
import {
|
||||
ActionViewType,
|
||||
CoreObjectNameSingular,
|
||||
AppPath,
|
||||
SettingsPath,
|
||||
} from 'twenty-shared/types';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import {
|
||||
BACKEND_BATCH_REQUEST_MAX_COUNT,
|
||||
MUTATION_MAX_MERGE_RECORDS,
|
||||
} from 'twenty-shared/constants';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import {
|
||||
ActionViewType,
|
||||
AppPath,
|
||||
CoreObjectNameSingular,
|
||||
SettingsPath,
|
||||
} from 'twenty-shared/types';
|
||||
import {
|
||||
IconArrowMerge,
|
||||
IconBuildingSkyscraper,
|
||||
|
|
@ -72,8 +72,8 @@ import {
|
|||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
PermissionFlagType,
|
||||
FeatureFlagKey,
|
||||
PermissionFlagType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import { Action } from '@/action-menu/actions/components/Action';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
export const EditNavigationSidebarNoSelectionRecordAction = () => {
|
||||
const setIsNavigationMenuInEditMode = useSetAtomState(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
|
||||
return (
|
||||
<Action
|
||||
onClick={() => setIsNavigationMenuInEditMode(true)}
|
||||
closeSidePanelOnCommandMenuListActionExecution
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,11 +1,18 @@
|
|||
import { ActionOpenSidePanelPage } from '@/action-menu/actions/components/ActionOpenSidePanelPage';
|
||||
import { EditNavigationSidebarNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/components/EditNavigationSidebarNoSelectionRecordAction';
|
||||
import { RecordAgnosticActionsKeys } from '@/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKeys';
|
||||
import { type ActionConfig } from '@/action-menu/actions/types/ActionConfig';
|
||||
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
|
||||
import { ActionType } from '@/action-menu/actions/types/ActionType';
|
||||
import { ActionViewType, SidePanelPages } from 'twenty-shared/types';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { IconHistory, IconSearch, IconSparkles } from 'twenty-ui/display';
|
||||
import { ActionViewType, SidePanelPages } from 'twenty-shared/types';
|
||||
import {
|
||||
IconHistory,
|
||||
IconLayout,
|
||||
IconSearch,
|
||||
IconSparkles,
|
||||
} from 'twenty-ui/display';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const RECORD_AGNOSTIC_ACTIONS_CONFIG: Record<string, ActionConfig> = {
|
||||
[RecordAgnosticActionsKeys.SEARCH_RECORDS]: {
|
||||
|
|
@ -88,4 +95,20 @@ export const RECORD_AGNOSTIC_ACTIONS_CONFIG: Record<string, ActionConfig> = {
|
|||
),
|
||||
shouldBeRegistered: () => true,
|
||||
},
|
||||
[RecordAgnosticActionsKeys.EDIT_NAVIGATION_SIDEBAR]: {
|
||||
type: ActionType.Navigation,
|
||||
scope: ActionScope.Global,
|
||||
key: RecordAgnosticActionsKeys.EDIT_NAVIGATION_SIDEBAR,
|
||||
label: msg`Edit navigation sidebar`,
|
||||
shortLabel: msg`Edit sidebar`,
|
||||
position: 4,
|
||||
Icon: IconLayout,
|
||||
isPinned: false,
|
||||
availableOn: [ActionViewType.GLOBAL],
|
||||
shouldBeRegistered: ({ isFeatureFlagEnabled }) =>
|
||||
isFeatureFlagEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
),
|
||||
component: <EditNavigationSidebarNoSelectionRecordAction />,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ export const useRecordAgnosticActions = () => {
|
|||
RECORD_AGNOSTIC_ACTIONS_CONFIG[
|
||||
RecordAgnosticActionsKeys.SEARCH_RECORDS_FALLBACK
|
||||
],
|
||||
[RecordAgnosticActionsKeys.EDIT_NAVIGATION_SIDEBAR]:
|
||||
RECORD_AGNOSTIC_ACTIONS_CONFIG[
|
||||
RecordAgnosticActionsKeys.EDIT_NAVIGATION_SIDEBAR
|
||||
],
|
||||
};
|
||||
|
||||
if (isAiEnabled) {
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ export enum RecordAgnosticActionsKeys {
|
|||
SEARCH_RECORDS_FALLBACK = 'search-records-fallback',
|
||||
ASK_AI = 'ask-ai',
|
||||
VIEW_PREVIOUS_AI_CHATS = 'view-previous-ai-chats',
|
||||
EDIT_NAVIGATION_SIDEBAR = 'edit-navigation-sidebar',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,19 +11,19 @@ import { useFavorites } from '@/favorites/hooks/useFavorites';
|
|||
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useShouldActionBeRegisteredParams = ({
|
||||
objectMetadataItem,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import { useDroppable } from '@dnd-kit/react';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
|
||||
import type { AddToNavDroppableProvided } from '@/command-menu/components/CommandMenuAddToNavDroppableTypes';
|
||||
|
||||
type CommandMenuAddToNavDroppableDndKitProps = {
|
||||
children: (provided: AddToNavDroppableProvided) => ReactNode;
|
||||
isDropDisabled: boolean;
|
||||
};
|
||||
|
||||
export const CommandMenuAddToNavDroppableDndKit = ({
|
||||
children,
|
||||
isDropDisabled,
|
||||
}: CommandMenuAddToNavDroppableDndKitProps) => {
|
||||
const { ref } = useDroppable({
|
||||
id: ADD_TO_NAV_SOURCE_DROPPABLE_ID,
|
||||
disabled: isDropDisabled,
|
||||
});
|
||||
const provided: AddToNavDroppableProvided = {
|
||||
innerRef: ref,
|
||||
droppableProps: { 'data-dnd-group': ADD_TO_NAV_SOURCE_DROPPABLE_ID },
|
||||
placeholder: null,
|
||||
};
|
||||
return <>{children(provided)}</>;
|
||||
};
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import type { ReactNode } from 'react';
|
||||
|
||||
export type AddToNavDroppableProvided = {
|
||||
innerRef: (element: HTMLElement | null) => void;
|
||||
droppableProps: object;
|
||||
placeholder: ReactNode;
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { useDraggable } from '@dnd-kit/react';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
|
||||
type CommandMenuItemWithAddToNavigationDragDndKitProps = {
|
||||
id: string;
|
||||
dragIndex: number;
|
||||
menuItemContent: ReactNode;
|
||||
};
|
||||
|
||||
export const CommandMenuItemWithAddToNavigationDragDndKit = ({
|
||||
id,
|
||||
dragIndex,
|
||||
menuItemContent,
|
||||
}: CommandMenuItemWithAddToNavigationDragDndKitProps) => {
|
||||
const { ref } = useDraggable({
|
||||
id,
|
||||
data: {
|
||||
sourceDroppableId: ADD_TO_NAV_SOURCE_DROPPABLE_ID,
|
||||
sourceIndex: dragIndex,
|
||||
},
|
||||
disabled: false,
|
||||
feedback: 'clone',
|
||||
});
|
||||
return <div ref={ref}>{menuItemContent}</div>;
|
||||
};
|
||||
|
|
@ -7,12 +7,10 @@ import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
|||
import { useDeleteFavoriteFolder } from '@/favorites/hooks/useDeleteFavoriteFolder';
|
||||
import { useRenameFavoriteFolder } from '@/favorites/hooks/useRenameFavoriteFolder';
|
||||
import { openFavoriteFolderIdsState } from '@/favorites/states/openFavoriteFolderIdsState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { getFavoriteSecondaryLabel } from '@/favorites/utils/getFavoriteSecondaryLabel';
|
||||
import { isLocationMatchingFavorite } from '@/favorites/utils/isLocationMatchingFavorite';
|
||||
import { type ProcessedFavorite } from '@/favorites/utils/sortFavorites';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
|
|
@ -24,9 +22,11 @@ import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/componen
|
|||
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { currentFavoriteFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentFavoriteFolderIdState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext, useState } from 'react';
|
||||
|
|
@ -239,7 +239,10 @@ export const CurrentWorkspaceMemberFavorites = ({
|
|||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() => deleteFavorite(favorite.id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteFavorite(favorite.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CurrentWorkspaceMemberOrphanFavorites } from '@/favorites/components/CurrentWorkspaceMemberOrphanFavorites';
|
||||
import { FavoritesDragProvider } from '@/favorites/components/FavoritesDragProvider';
|
||||
import { FavoriteFolders } from '@/favorites/components/FavoritesFolders';
|
||||
import { FavoritesSkeletonLoader } from '@/favorites/components/FavoritesSkeletonLoader';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder';
|
||||
import { isFavoriteFolderCreatingState } from '@/favorites/states/isFavoriteFolderCreatingState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
||||
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||
|
|
@ -13,11 +13,13 @@ import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/
|
|||
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 { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconFolderPlus } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
export const CurrentWorkspaceMemberFavoritesFolders = () => {
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
|
|
@ -69,14 +71,21 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => {
|
|||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
isOpen={isNavigationSectionOpen}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
{isNavigationSectionOpen && (
|
||||
<>
|
||||
<FavoriteFolders isNavigationSectionOpen={isNavigationSectionOpen} />
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isNavigationSectionOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={false}
|
||||
>
|
||||
<FavoritesDragProvider>
|
||||
<FavoriteFolders />
|
||||
<CurrentWorkspaceMemberOrphanFavorites />
|
||||
</>
|
||||
)}
|
||||
</FavoritesDragProvider>
|
||||
</AnimatedExpandableContainer>
|
||||
</NavigationDrawerSection>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -63,7 +63,10 @@ export const CurrentWorkspaceMemberOrphanFavorites = () => {
|
|||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() => deleteFavorite(favorite.id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteFavorite(favorite.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,10 @@ export const FavoritesFolderContent = ({
|
|||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() => deleteFavorite(favorite.id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteFavorite(favorite.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,13 @@ import { CurrentWorkspaceMemberFavorites } from '@/favorites/components/CurrentW
|
|||
import { useCreateFavoriteFolder } from '@/favorites/hooks/useCreateFavoriteFolder';
|
||||
import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder';
|
||||
import { isFavoriteFolderCreatingState } from '@/favorites/states/isFavoriteFolderCreatingState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
||||
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useState } from 'react';
|
||||
import { IconFolder } from 'twenty-ui/display';
|
||||
|
||||
type FavoriteFoldersProps = {
|
||||
isNavigationSectionOpen: boolean;
|
||||
};
|
||||
|
||||
export const FavoriteFolders = ({
|
||||
isNavigationSectionOpen,
|
||||
}: FavoriteFoldersProps) => {
|
||||
export const FavoriteFolders = () => {
|
||||
const [newFolderName, setNewFolderName] = useState('');
|
||||
|
||||
const { favoritesByFolder } = useFavoritesByFolder();
|
||||
|
|
@ -56,10 +50,6 @@ export const FavoriteFolders = ({
|
|||
setIsFavoriteFolderCreating(false);
|
||||
};
|
||||
|
||||
if (!isNavigationSectionOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFavoriteFolderCreating && (
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { FAVORITE_DROPPABLE_IDS } from '@/favorites/constants/FavoriteDroppableIds';
|
||||
import { useSortedFavorites } from '@/favorites/hooks/useSortedFavorites';
|
||||
import { openFavoriteFolderIdsState } from '@/favorites/states/openFavoriteFolderIdsState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { calculateNewPosition } from '@/ui/layout/draggable-list/utils/calculateNewPosition';
|
||||
import { validateAndExtractFolderId } from '@/ui/layout/draggable-list/utils/validateAndExtractFolderId';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { type OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
|
||||
export const useHandleFavoriteDragAndDrop = () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { styled } from '@linaria/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { type ReactNode, useContext } from 'react';
|
||||
import { useContext, type ReactNode } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconGripVertical, type IconComponent } from 'twenty-ui/display';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
|
@ -11,19 +11,26 @@ import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/
|
|||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
|
||||
|
||||
const StyledIconSlot = styled.div<{ $hasFixedSize: boolean }>`
|
||||
const StyledIconSlot = styled.div<{
|
||||
$hasFixedSize: boolean;
|
||||
$disabled?: boolean;
|
||||
$disableDrag?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
cursor: grab;
|
||||
cursor: ${({ $disabled, $disableDrag }) =>
|
||||
$disabled || $disableDrag ? 'default' : 'grab'};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: ${({ $hasFixedSize }) =>
|
||||
$hasFixedSize ? themeCssVariables.spacing[4] : 'auto'};
|
||||
justify-content: center;
|
||||
opacity: ${({ $disabled }) => ($disabled ? 0.5 : 1)};
|
||||
width: ${({ $hasFixedSize }) =>
|
||||
$hasFixedSize ? themeCssVariables.spacing[4] : 'auto'};
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
cursor: ${({ $disabled, $disableDrag }) =>
|
||||
$disabled || $disableDrag ? 'default' : 'grabbing'};
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -63,6 +70,8 @@ type AddToNavigationDragHandleProps = {
|
|||
customIconContent?: ReactNode;
|
||||
payload: AddToNavigationDragPayload;
|
||||
isHovered: boolean;
|
||||
disabled?: boolean;
|
||||
disableDrag?: boolean;
|
||||
};
|
||||
|
||||
export const AddToNavigationDragHandle = ({
|
||||
|
|
@ -70,6 +79,8 @@ export const AddToNavigationDragHandle = ({
|
|||
customIconContent,
|
||||
payload,
|
||||
isHovered,
|
||||
disabled = false,
|
||||
disableDrag = false,
|
||||
}: AddToNavigationDragHandleProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const effectiveColor =
|
||||
|
|
@ -89,6 +100,8 @@ export const AddToNavigationDragHandle = ({
|
|||
return (
|
||||
<StyledIconSlot
|
||||
$hasFixedSize={hasBackgroundColor || showCustomContentWithoutWrapper}
|
||||
$disabled={disabled}
|
||||
$disableDrag={disableDrag}
|
||||
>
|
||||
{isHovered ? (
|
||||
<IconGripVertical
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useLingui } from '@lingui/react/macro';
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconFolderPlus } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
import { CurrentWorkspaceMemberOrphanNavigationMenuItems } from '@/navigation-menu-item/components/CurrentWorkspaceMemberOrphanNavigationMenuItems';
|
||||
import { NavigationMenuItemFolders } from '@/navigation-menu-item/components/NavigationMenuItemFolders';
|
||||
import { NavigationMenuItemSkeletonLoader } from '@/navigation-menu-item/components/NavigationMenuItemSkeletonLoader';
|
||||
|
|
@ -70,16 +71,19 @@ export const CurrentWorkspaceMemberNavigationMenuItemFolders = () => {
|
|||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
isOpen={isNavigationSectionOpen}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
{isNavigationSectionOpen && (
|
||||
<>
|
||||
<NavigationMenuItemFolders
|
||||
isNavigationSectionOpen={isNavigationSectionOpen}
|
||||
/>
|
||||
<CurrentWorkspaceMemberOrphanNavigationMenuItems />
|
||||
</>
|
||||
)}
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isNavigationSectionOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={false}
|
||||
>
|
||||
<NavigationMenuItemFolders />
|
||||
<CurrentWorkspaceMemberOrphanNavigationMenuItems />
|
||||
</AnimatedExpandableContainer>
|
||||
</NavigationDrawerSection>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,16 +2,31 @@ import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
|||
|
||||
import { CurrentWorkspaceMemberFavoritesFolders } from '@/favorites/components/CurrentWorkspaceMemberFavoritesFolders';
|
||||
import { CurrentWorkspaceMemberNavigationMenuItemFolders } from '@/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItemFolders';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { FavoritesDragDropProviderContent } from '@/navigation/components/FavoritesDragDropProviderContent';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { AnimatedEaseInOut } from 'twenty-ui/utilities';
|
||||
|
||||
export const CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher = () => {
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
|
||||
if (isNavigationMenuItemEditingEnabled) {
|
||||
return <CurrentWorkspaceMemberNavigationMenuItemFolders />;
|
||||
}
|
||||
|
||||
return <CurrentWorkspaceMemberFavoritesFolders />;
|
||||
return (
|
||||
<AnimatedEaseInOut isOpen={!isNavigationMenuInEditMode} initial>
|
||||
{isNavigationMenuItemEditingEnabled ? (
|
||||
<FavoritesDragDropProviderContent>
|
||||
<CurrentWorkspaceMemberNavigationMenuItemFolders />
|
||||
</FavoritesDragDropProviderContent>
|
||||
) : (
|
||||
<FavoritesDragDropProviderContent>
|
||||
<CurrentWorkspaceMemberFavoritesFolders />
|
||||
</FavoritesDragDropProviderContent>
|
||||
)}
|
||||
</AnimatedEaseInOut>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -291,9 +291,10 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
|
|||
isWorkspaceFolder ? undefined : (
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() =>
|
||||
deleteNavigationMenuItem(navigationMenuItem.id)
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteNavigationMenuItem(navigationMenuItem.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -85,9 +85,10 @@ export const CurrentWorkspaceMemberOrphanNavigationMenuItems = () => {
|
|||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() =>
|
||||
deleteNavigationMenuItem(navigationMenuItem.id)
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteNavigationMenuItem(navigationMenuItem.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
import { styled } from '@linaria/react';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { IconComponent } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorLink';
|
||||
import { getLinkFaviconUrl } from '@/navigation-menu-item/utils/getLinkFaviconUrl';
|
||||
import { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/utils/getNavigationMenuItemIconStyleFromColor';
|
||||
|
||||
const failedFaviconUrls = new Set<string>();
|
||||
|
||||
const StyledCompositeContainer = styled.div`
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 16px;
|
||||
`;
|
||||
|
||||
const StyledMainIconWrapper = styled.div<{
|
||||
$backgroundColor: string;
|
||||
$borderColor?: string;
|
||||
$noBackgroundOrBorder?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
background-color: ${({ $backgroundColor, $noBackgroundOrBorder }) =>
|
||||
$noBackgroundOrBorder ? 'transparent' : $backgroundColor};
|
||||
border: ${({ $borderColor, $noBackgroundOrBorder }) =>
|
||||
$noBackgroundOrBorder || !$borderColor
|
||||
? 'none'
|
||||
: `1px solid ${$borderColor}`};
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
inset: 0;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
`;
|
||||
|
||||
const StyledFaviconImage = styled.img`
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledLinkOverlay = styled.div<{ $backgroundColor: string }>`
|
||||
align-items: center;
|
||||
background-color: ${({ $backgroundColor }) => $backgroundColor};
|
||||
border-radius: ${themeCssVariables.border.radius.xs};
|
||||
bottom: -5px;
|
||||
display: flex;
|
||||
height: 14px;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
width: 14px;
|
||||
`;
|
||||
|
||||
export type LinkIconWithLinkOverlayProps = {
|
||||
link: string | null | undefined;
|
||||
LinkIcon: IconComponent;
|
||||
DefaultIcon: IconComponent;
|
||||
color?: string | null;
|
||||
};
|
||||
|
||||
export const LinkIconWithLinkOverlay = ({
|
||||
link,
|
||||
LinkIcon,
|
||||
DefaultIcon,
|
||||
color: navItemColor,
|
||||
}: LinkIconWithLinkOverlayProps) => {
|
||||
const [localFailedLink, setLocalFailedLink] = useState<string | null>(null);
|
||||
const faviconUrl = getLinkFaviconUrl(link);
|
||||
const linkKey = link ?? '';
|
||||
const isKnownFailed = failedFaviconUrls.has(linkKey);
|
||||
const showFavicon =
|
||||
isDefined(faviconUrl) && !isKnownFailed && localFailedLink !== linkKey;
|
||||
|
||||
const linkStyle = getNavigationMenuItemIconStyleFromColor(
|
||||
navItemColor ?? DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledCompositeContainer>
|
||||
<StyledMainIconWrapper
|
||||
$backgroundColor={linkStyle.backgroundColor}
|
||||
$borderColor={linkStyle.borderColor}
|
||||
$noBackgroundOrBorder={showFavicon}
|
||||
>
|
||||
{showFavicon ? (
|
||||
<StyledFaviconImage
|
||||
src={faviconUrl}
|
||||
alt=""
|
||||
onError={() => {
|
||||
if (isDefined(link)) failedFaviconUrls.add(link);
|
||||
setLocalFailedLink(linkKey);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<DefaultIcon
|
||||
size="14px"
|
||||
stroke={themeCssVariables.icon.stroke.md}
|
||||
color={linkStyle.iconColor}
|
||||
/>
|
||||
)}
|
||||
</StyledMainIconWrapper>
|
||||
<StyledLinkOverlay $backgroundColor={themeCssVariables.grayScale.gray4}>
|
||||
<LinkIcon
|
||||
size="14px"
|
||||
stroke={themeCssVariables.icon.stroke.md}
|
||||
color={themeCssVariables.grayScale.gray10}
|
||||
/>
|
||||
</StyledLinkOverlay>
|
||||
</StyledCompositeContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -5,19 +5,13 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
|
|||
import { type NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
|
||||
|
||||
const StyledDropTarget = styled.div<{
|
||||
$isDragOver: boolean;
|
||||
$isDropForbidden: boolean;
|
||||
$compact?: boolean;
|
||||
}>`
|
||||
const StyledDropTarget = styled.div<{ $compact?: boolean }>`
|
||||
min-height: ${({ $compact }) =>
|
||||
$compact ? 0 : themeCssVariables.spacing[2]};
|
||||
position: relative;
|
||||
transition: all 150ms ease-in-out;
|
||||
|
||||
${({ $isDragOver }) =>
|
||||
$isDragOver
|
||||
? `
|
||||
&[data-drag-over='true'] {
|
||||
background-color: ${themeCssVariables.background.transparent.blue};
|
||||
|
||||
&::before {
|
||||
|
|
@ -28,29 +22,14 @@ const StyledDropTarget = styled.div<{
|
|||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: ${themeCssVariables.color.blue};
|
||||
border-radius: ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0 0;
|
||||
border-radius: ${themeCssVariables.border.radius.sm}
|
||||
${themeCssVariables.border.radius.sm} 0 0;
|
||||
}
|
||||
`
|
||||
: ''}
|
||||
}
|
||||
|
||||
${({ $isDropForbidden }) =>
|
||||
$isDropForbidden
|
||||
? `
|
||||
background-color: ${themeCssVariables.background.transparent.danger};
|
||||
&[data-drop-forbidden='true'] {
|
||||
cursor: not-allowed;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: ${themeCssVariables.color.red};
|
||||
border-radius: ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0 0;
|
||||
}
|
||||
`
|
||||
: ''}
|
||||
}
|
||||
`;
|
||||
|
||||
type NavigationItemDropTargetProps = {
|
||||
|
|
@ -59,6 +38,7 @@ type NavigationItemDropTargetProps = {
|
|||
sectionId: NavigationSections;
|
||||
children?: ReactNode;
|
||||
compact?: boolean;
|
||||
dropTargetIdOverride?: string;
|
||||
};
|
||||
|
||||
export const NavigationItemDropTarget = ({
|
||||
|
|
@ -67,19 +47,21 @@ export const NavigationItemDropTarget = ({
|
|||
sectionId,
|
||||
children,
|
||||
compact = false,
|
||||
dropTargetIdOverride,
|
||||
}: NavigationItemDropTargetProps) => {
|
||||
const { activeDropTargetId, forbiddenDropTargetId } = useContext(
|
||||
NavigationDropTargetContext,
|
||||
);
|
||||
const dropTargetId = `${sectionId}-${folderId ?? 'orphan'}-${index}`;
|
||||
const dropTargetId =
|
||||
dropTargetIdOverride ?? `${sectionId}-${folderId ?? 'orphan'}-${index}`;
|
||||
const isDragOver = activeDropTargetId === dropTargetId;
|
||||
const isDropForbidden = forbiddenDropTargetId === dropTargetId;
|
||||
|
||||
return (
|
||||
<StyledDropTarget
|
||||
$isDragOver={isDragOver}
|
||||
$isDropForbidden={isDropForbidden}
|
||||
$compact={compact}
|
||||
data-drag-over={isDragOver && !isDropForbidden ? 'true' : undefined}
|
||||
data-drop-forbidden={isDropForbidden ? 'true' : undefined}
|
||||
>
|
||||
{children}
|
||||
</StyledDropTarget>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
|
||||
import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/hooks/useSaveNavigationMenuItemsDraft';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
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';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useContext, useState } from 'react';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { IconCheck, useIcons } from 'twenty-ui/display';
|
||||
|
|
@ -96,24 +97,34 @@ export const NavigationMenuEditModeBar = () => {
|
|||
|
||||
const IconPaint = getIcon('IconPaint');
|
||||
|
||||
if (!showNavigationMenuEditModeBar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledTitle>
|
||||
<IconPaint size={theme.icon.size.md} />
|
||||
{t`Layout customization`}
|
||||
</StyledTitle>
|
||||
<SaveAndCancelButtons
|
||||
onSave={handleSave}
|
||||
onCancel={cancelEditMode}
|
||||
isSaveDisabled={!isDirty || isSaving}
|
||||
isLoading={isSaving}
|
||||
inverted
|
||||
saveIcon={IconCheck}
|
||||
/>
|
||||
</StyledContainer>
|
||||
<AnimatePresence>
|
||||
{showNavigationMenuEditModeBar && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
>
|
||||
<StyledContainer>
|
||||
<StyledTitle>
|
||||
<IconPaint size={theme.icon.size.md} />
|
||||
{t`Layout customization`}
|
||||
</StyledTitle>
|
||||
<SaveAndCancelButtons
|
||||
onSave={handleSave}
|
||||
onCancel={cancelEditMode}
|
||||
isSaveDisabled={!isDirty || isSaving}
|
||||
isLoading={isSaving}
|
||||
inverted
|
||||
saveIcon={IconCheck}
|
||||
/>
|
||||
</StyledContainer>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { styled } from '@linaria/react';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
|
||||
|
|
@ -12,38 +12,26 @@ type NavigationMenuItemDroppableProps = {
|
|||
isWorkspaceSection?: boolean;
|
||||
};
|
||||
|
||||
const StyledDroppableWrapper = styled.div<{
|
||||
isDraggingOver: boolean;
|
||||
isDragIndicatorVisible: boolean;
|
||||
showDropLine: boolean;
|
||||
}>`
|
||||
const StyledDroppableWrapper = styled.div`
|
||||
position: relative;
|
||||
transition: all 150ms ease-in-out;
|
||||
width: 100%;
|
||||
|
||||
${({ isDraggingOver, isDragIndicatorVisible, showDropLine }) =>
|
||||
isDraggingOver && isDragIndicatorVisible
|
||||
? `
|
||||
background-color: ${themeCssVariables.background.transparent.blue};
|
||||
&[data-dragging-over='true'] {
|
||||
background-color: ${themeCssVariables.background.transparent.blue};
|
||||
}
|
||||
|
||||
${
|
||||
showDropLine
|
||||
? `
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: ${themeCssVariables.color.blue};
|
||||
border-radius: ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0 0;
|
||||
}
|
||||
`
|
||||
: ''
|
||||
}
|
||||
`
|
||||
: ''}
|
||||
&[data-dragging-over='true'][data-show-drop-line='true']::before {
|
||||
background-color: ${themeCssVariables.color.blue};
|
||||
border-radius: ${themeCssVariables.border.radius.sm}
|
||||
${themeCssVariables.border.radius.sm} 0 0;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
height: 2px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const NavigationMenuItemDroppable = ({
|
||||
|
|
@ -59,9 +47,12 @@ export const NavigationMenuItemDroppable = ({
|
|||
<Droppable droppableId={droppableId} isDropDisabled={isDropDisabled}>
|
||||
{(provided, snapshot) => (
|
||||
<StyledDroppableWrapper
|
||||
isDraggingOver={snapshot.isDraggingOver}
|
||||
isDragIndicatorVisible={isDragIndicatorVisible}
|
||||
showDropLine={showDropLine}
|
||||
data-dragging-over={
|
||||
snapshot.isDraggingOver && isDragIndicatorVisible
|
||||
? 'true'
|
||||
: undefined
|
||||
}
|
||||
data-show-drop-line={showDropLine ? 'true' : undefined}
|
||||
>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
|
|
|
|||
|
|
@ -58,9 +58,10 @@ export const NavigationMenuItemFolderContent = ({
|
|||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() =>
|
||||
deleteNavigationMenuItem(navigationMenuItem.id)
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteNavigationMenuItem(navigationMenuItem.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,13 +9,7 @@ import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
|||
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
||||
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
|
||||
|
||||
type NavigationMenuItemFoldersProps = {
|
||||
isNavigationSectionOpen: boolean;
|
||||
};
|
||||
|
||||
export const NavigationMenuItemFolders = ({
|
||||
isNavigationSectionOpen,
|
||||
}: NavigationMenuItemFoldersProps) => {
|
||||
export const NavigationMenuItemFolders = () => {
|
||||
const [newFolderName, setNewFolderName] = useState('');
|
||||
|
||||
const { userNavigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
|
||||
|
|
@ -61,10 +55,6 @@ export const NavigationMenuItemFolders = ({
|
|||
setIsNavigationMenuItemFolderCreating(false);
|
||||
};
|
||||
|
||||
if (!isNavigationSectionOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isNavigationMenuItemFolderCreating && (
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Avatar, useIcons } from 'twenty-ui/display';
|
||||
import { Avatar, IconLink, IconWorld, useIcons } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/components/LinkIconWithLinkOverlay';
|
||||
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
|
||||
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
|
|
@ -63,6 +64,17 @@ export const NavigationMenuItemIcon = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (navigationMenuItem.itemType === NavigationMenuItemType.LINK) {
|
||||
return (
|
||||
<LinkIconWithLinkOverlay
|
||||
link={navigationMenuItem.link}
|
||||
LinkIcon={IconLink}
|
||||
DefaultIcon={IconWorld}
|
||||
color={getEffectiveNavigationMenuItemColor(navigationMenuItem)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const iconToUse =
|
||||
StandardIcon ??
|
||||
(navigationMenuItem.Icon ? getIcon(navigationMenuItem.Icon) : undefined);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { styled } from '@linaria/react';
|
||||
import type { IconComponent } from 'twenty-ui/display';
|
||||
import { useContext } from 'react';
|
||||
import type { IconComponent } from 'twenty-ui/display';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/utils/getNavigationMenuItemIconStyleFromColor';
|
||||
|
|
@ -37,13 +37,13 @@ const StyledViewOverlay = styled.div<{ $backgroundColor: string }>`
|
|||
align-items: center;
|
||||
background-color: ${({ $backgroundColor }) => $backgroundColor};
|
||||
border-radius: 4px;
|
||||
bottom: -7px;
|
||||
bottom: -5px;
|
||||
display: flex;
|
||||
height: 14px;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
right: -7px;
|
||||
width: 14px;
|
||||
right: -6px;
|
||||
width: 12px;
|
||||
`;
|
||||
|
||||
export type ObjectIconWithViewOverlayProps = {
|
||||
|
|
@ -74,8 +74,8 @@ export const ObjectIconWithViewOverlay = ({
|
|||
</StyledObjectIconWrapper>
|
||||
<StyledViewOverlay $backgroundColor={themeCssVariables.grayScale.gray4}>
|
||||
<ViewIcon
|
||||
size="10px"
|
||||
stroke={theme.icon.stroke.lg}
|
||||
size="12px"
|
||||
stroke={theme.icon.stroke.md}
|
||||
color={themeCssVariables.grayScale.gray10}
|
||||
/>
|
||||
</StyledViewOverlay>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import { useDroppable } from '@dnd-kit/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId';
|
||||
import type { DroppableData } from '@/navigation/types/workspaceDndKitDroppableData';
|
||||
|
||||
const StyledSlotWrapper = styled.div<{ $empty: boolean }>`
|
||||
min-height: 0;
|
||||
${({ $empty }) =>
|
||||
$empty ? `min-height: ${themeCssVariables.spacing[2]};` : ''}
|
||||
`;
|
||||
|
||||
const SLOT_COLLISION_PRIORITY = 1;
|
||||
|
||||
export const FOLDER_HEADER_SLOT_COLLISION_PRIORITY = 2;
|
||||
|
||||
type WorkspaceDndKitDroppableSlotProps = {
|
||||
droppableId: string;
|
||||
index: number;
|
||||
children?: ReactNode;
|
||||
disabled?: boolean;
|
||||
collisionPriority?: number;
|
||||
};
|
||||
|
||||
export const WorkspaceDndKitDroppableSlot = ({
|
||||
droppableId,
|
||||
index,
|
||||
children,
|
||||
disabled = false,
|
||||
collisionPriority = SLOT_COLLISION_PRIORITY,
|
||||
}: WorkspaceDndKitDroppableSlotProps) => {
|
||||
const id = getDndKitDropTargetId(droppableId, index);
|
||||
const data: DroppableData = { droppableId, index };
|
||||
const { ref } = useDroppable({
|
||||
id,
|
||||
disabled,
|
||||
collisionPriority,
|
||||
data,
|
||||
});
|
||||
|
||||
const isEmpty =
|
||||
children == null || (Array.isArray(children) && children.length === 0);
|
||||
|
||||
return (
|
||||
<StyledSlotWrapper ref={ref} $empty={isEmpty}>
|
||||
{children}
|
||||
</StyledSlotWrapper>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { SortableKeyboardPlugin } from '@dnd-kit/dom/sortable';
|
||||
import { useSortable } from '@dnd-kit/react/sortable';
|
||||
import { styled } from '@linaria/react';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { SortableDropTargetRefContext } from '@/navigation-menu-item/contexts/SortableDropTargetRefContext';
|
||||
|
||||
const SORTABLE_COLLISION_PRIORITY = 3;
|
||||
|
||||
const PLUGINS_WITHOUT_OPTIMISTIC = [SortableKeyboardPlugin];
|
||||
|
||||
const StyledSortableRoot = styled.div`
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
type WorkspaceDndKitSortableItemProps = {
|
||||
children: ReactNode;
|
||||
disabled?: boolean;
|
||||
group: string;
|
||||
id: string;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export const WorkspaceDndKitSortableItem = ({
|
||||
id,
|
||||
index,
|
||||
group,
|
||||
disabled = false,
|
||||
children,
|
||||
}: WorkspaceDndKitSortableItemProps) => {
|
||||
const { handleRef, ref, targetRef } = useSortable({
|
||||
id,
|
||||
index,
|
||||
group,
|
||||
collisionPriority: SORTABLE_COLLISION_PRIORITY,
|
||||
data: {
|
||||
sourceDroppableId: group,
|
||||
sourceIndex: index,
|
||||
},
|
||||
disabled,
|
||||
transition: null,
|
||||
plugins: PLUGINS_WITHOUT_OPTIMISTIC,
|
||||
feedback: 'clone',
|
||||
});
|
||||
|
||||
return (
|
||||
<SortableDropTargetRefContext.Provider value={targetRef}>
|
||||
<StyledSortableRoot
|
||||
ref={(el) => {
|
||||
ref(el);
|
||||
handleRef?.(el);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</StyledSortableRoot>
|
||||
</SortableDropTargetRefContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
|
||||
type WorkspaceNavigationMenuItemFolderSubItemProps = {
|
||||
navigationMenuItem: ProcessedNavigationMenuItem;
|
||||
index: number;
|
||||
arrayLength: number;
|
||||
selectedNavigationMenuItemIndex: number;
|
||||
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
|
||||
selectedNavigationMenuItemId: string | null;
|
||||
isContextDragging: boolean;
|
||||
};
|
||||
|
||||
export const WorkspaceNavigationMenuItemFolderSubItem = ({
|
||||
navigationMenuItem,
|
||||
index,
|
||||
arrayLength,
|
||||
selectedNavigationMenuItemIndex,
|
||||
onNavigationMenuItemClick,
|
||||
selectedNavigationMenuItemId,
|
||||
isContextDragging,
|
||||
}: WorkspaceNavigationMenuItemFolderSubItemProps) => {
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const coreViews = useAtomStateValue(coreViewsState);
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
const objectMetadataItem =
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.VIEW ||
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.RECORD
|
||||
? getObjectMetadataForNavigationMenuItem(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
)
|
||||
: null;
|
||||
const isEditableInEditMode =
|
||||
isNavigationMenuInEditMode &&
|
||||
isDefined(onNavigationMenuItemClick) &&
|
||||
(navigationMenuItem.itemType === NavigationMenuItemType.LINK ||
|
||||
isDefined(objectMetadataItem));
|
||||
|
||||
const handleEditModeClick =
|
||||
isEditableInEditMode && isDefined(onNavigationMenuItemClick)
|
||||
? () =>
|
||||
onNavigationMenuItemClick({
|
||||
item: navigationMenuItem,
|
||||
objectMetadataItem: objectMetadataItem ?? undefined,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<NavigationDrawerSubItem
|
||||
secondaryLabel={
|
||||
navigationMenuItem.viewKey === ViewKey.Index
|
||||
? undefined
|
||||
: getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
navigationMenuItem.objectNameSingular,
|
||||
})
|
||||
}
|
||||
label={navigationMenuItem.labelIdentifier}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon navigationMenuItem={navigationMenuItem} />
|
||||
)}
|
||||
to={
|
||||
isContextDragging || handleEditModeClick
|
||||
? undefined
|
||||
: navigationMenuItem.link
|
||||
}
|
||||
onClick={handleEditModeClick}
|
||||
active={index === selectedNavigationMenuItemIndex}
|
||||
isSelectedInEditMode={
|
||||
selectedNavigationMenuItemId === navigationMenuItem.id
|
||||
}
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index,
|
||||
arrayLength,
|
||||
selectedIndex: selectedNavigationMenuItemIndex,
|
||||
})}
|
||||
isDragging={isContextDragging}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -24,6 +24,7 @@ import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/nav
|
|||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { preloadWorkspaceDndKit } from '@/navigation/preloadWorkspaceDndKit';
|
||||
import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader';
|
||||
import { NavigationDrawerSectionForWorkspaceItems } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItems';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
|
@ -137,9 +138,6 @@ export const WorkspaceNavigationMenuItems = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const isEditMode =
|
||||
isNavigationMenuItemEditingEnabled && isNavigationMenuInEditMode;
|
||||
|
||||
if (loading) {
|
||||
return <NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader />;
|
||||
}
|
||||
|
|
@ -151,7 +149,7 @@ export const WorkspaceNavigationMenuItems = () => {
|
|||
rightIcon={
|
||||
isNavigationMenuItemEditingEnabled ? (
|
||||
<StyledRightIconsContainer>
|
||||
{isEditMode ? (
|
||||
{isNavigationMenuInEditMode ? (
|
||||
<LightIconButton
|
||||
Icon={IconPlus}
|
||||
accent="tertiary"
|
||||
|
|
@ -159,25 +157,21 @@ export const WorkspaceNavigationMenuItems = () => {
|
|||
onClick={handleAddMenuItem}
|
||||
/>
|
||||
) : (
|
||||
<LightIconButton
|
||||
Icon={IconTool}
|
||||
accent="tertiary"
|
||||
size="small"
|
||||
onClick={handleEditClick}
|
||||
/>
|
||||
<div onMouseEnter={preloadWorkspaceDndKit}>
|
||||
<LightIconButton
|
||||
Icon={IconTool}
|
||||
accent="tertiary"
|
||||
size="small"
|
||||
onClick={handleEditClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</StyledRightIconsContainer>
|
||||
) : undefined
|
||||
}
|
||||
onAddMenuItem={
|
||||
isNavigationMenuItemEditingEnabled && isEditMode
|
||||
? handleAddMenuItem
|
||||
: undefined
|
||||
}
|
||||
isEditMode={isEditMode}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemInEditMode}
|
||||
onNavigationMenuItemClick={
|
||||
isEditMode ? handleNavigationMenuItemClick : undefined
|
||||
isNavigationMenuInEditMode ? handleNavigationMenuItemClick : undefined
|
||||
}
|
||||
onActiveObjectMetadataItemClick={
|
||||
isNavigationMenuItemEditingEnabled
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { styled } from '@linaria/react';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconChevronDown,
|
||||
|
|
@ -15,36 +13,35 @@ import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
|||
|
||||
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
|
||||
import { useOpenAddItemToFolderPage } from '@/navigation-menu-item/hooks/useOpenAddItemToFolderPage';
|
||||
import { useWorkspaceFolderOpenState } from '@/navigation-menu-item/hooks/useWorkspaceFolderOpenState';
|
||||
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
|
||||
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
import { NavigationMenuItemDroppable } from '@/navigation-menu-item/components/NavigationMenuItemDroppable';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { WorkspaceNavigationMenuItemFolderDragClone } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderDragClone';
|
||||
import { NavigationItemDropTarget } from '@/navigation-menu-item/components/NavigationItemDropTarget';
|
||||
import {
|
||||
FOLDER_HEADER_SLOT_COLLISION_PRIORITY,
|
||||
WorkspaceDndKitDroppableSlot,
|
||||
} from '@/navigation-menu-item/components/WorkspaceDndKitDroppableSlot';
|
||||
import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/components/WorkspaceDndKitSortableItem';
|
||||
import { WorkspaceNavigationMenuItemFolderSubItem } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderSubItem';
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
|
||||
import { SortableDropTargetRefContext } from '@/navigation-menu-item/contexts/SortableDropTargetRefContext';
|
||||
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
|
||||
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { currentNavigationMenuItemFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdState';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
|
||||
const StyledFolderContainer = styled.div<{ $isSelectedInEditMode: boolean }>`
|
||||
border: ${({ $isSelectedInEditMode }) =>
|
||||
|
|
@ -52,15 +49,20 @@ const StyledFolderContainer = styled.div<{ $isSelectedInEditMode: boolean }>`
|
|||
? `1px solid ${themeCssVariables.color.blue}`
|
||||
: '1px solid transparent'};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
transition: background-color 150ms ease-in-out;
|
||||
|
||||
&[data-drag-over-header='true'] {
|
||||
background-color: ${themeCssVariables.background.transparent.blue};
|
||||
}
|
||||
|
||||
&[data-forbidden-drop-target='true'] {
|
||||
background-color: ${themeCssVariables.background.transparent.danger};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledFolderDroppableContent = styled.div<{
|
||||
$compact: boolean;
|
||||
}>`
|
||||
const StyledFolderDroppableContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: ${({ $compact }) =>
|
||||
$compact ? 0 : themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledFolderExpandableWrapper = styled.div`
|
||||
|
|
@ -76,7 +78,6 @@ type WorkspaceNavigationMenuItemsFolderProps = {
|
|||
folderColor?: string | null;
|
||||
navigationMenuItems: ProcessedNavigationMenuItem[];
|
||||
isGroup: boolean;
|
||||
isEditMode?: boolean;
|
||||
isSelectedInEditMode?: boolean;
|
||||
onEditModeClick?: () => void;
|
||||
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
|
||||
|
|
@ -91,40 +92,31 @@ export const WorkspaceNavigationMenuItemsFolder = ({
|
|||
folderColor,
|
||||
navigationMenuItems,
|
||||
isGroup,
|
||||
isEditMode = false,
|
||||
isSelectedInEditMode = false,
|
||||
onEditModeClick,
|
||||
onNavigationMenuItemClick,
|
||||
selectedNavigationMenuItemId = null,
|
||||
isDragging = false,
|
||||
}: WorkspaceNavigationMenuItemsFolderProps) => {
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { getIcon } = useIcons();
|
||||
const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const coreViews = useAtomStateValue(coreViewsState);
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const currentPath = location.pathname;
|
||||
const currentViewPath = location.pathname + location.search;
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { t } = useLingui();
|
||||
const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] =
|
||||
useAtomState(openNavigationMenuItemFolderIdsState);
|
||||
|
||||
const setCurrentNavigationMenuItemFolderId = useSetAtomState(
|
||||
currentNavigationMenuItemFolderIdState,
|
||||
);
|
||||
const { isOpen, handleToggle, selectedNavigationMenuItemIndex } =
|
||||
useWorkspaceFolderOpenState({ folderId, navigationMenuItems });
|
||||
const { openAddItemToFolderPage } = useOpenAddItemToFolderPage();
|
||||
const sidePanelPage = useAtomStateValue(sidePanelPageState);
|
||||
const addMenuItemInsertionContext = useAtomStateValue(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
|
||||
const isOpen = openNavigationMenuItemFolderIds.includes(folderId);
|
||||
|
||||
const folderContentLengthForTree =
|
||||
isEditMode && isSelectedInEditMode
|
||||
? navigationMenuItems.length + 1
|
||||
: navigationMenuItems.length;
|
||||
const folderContentLengthForTree = isNavigationMenuInEditMode
|
||||
? navigationMenuItems.length + 1
|
||||
: navigationMenuItems.length;
|
||||
|
||||
const handleAddMenuItemToFolder = () => {
|
||||
openAddItemToFolderPage({
|
||||
|
|
@ -134,208 +126,180 @@ export const WorkspaceNavigationMenuItemsFolder = ({
|
|||
});
|
||||
};
|
||||
|
||||
const handleToggle = () => {
|
||||
if (isMobile) {
|
||||
setCurrentNavigationMenuItemFolderId((prev) =>
|
||||
prev === folderId ? null : folderId,
|
||||
);
|
||||
} else {
|
||||
setOpenNavigationMenuItemFolderIds((current) =>
|
||||
isOpen
|
||||
? current.filter((id) => id !== folderId)
|
||||
: [...current, folderId],
|
||||
);
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
const firstNonLinkItem = navigationMenuItems.find(
|
||||
(item) =>
|
||||
item.itemType !== NavigationMenuItemType.LINK &&
|
||||
isNonEmptyString(item.link),
|
||||
);
|
||||
if (isDefined(firstNonLinkItem?.link)) {
|
||||
navigate(firstNonLinkItem.link);
|
||||
const shouldUseEditModeClick =
|
||||
isNavigationMenuInEditMode && isDefined(onEditModeClick);
|
||||
const handleClick = shouldUseEditModeClick
|
||||
? (e?: React.MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
if (isSelectedInEditMode) {
|
||||
handleToggle();
|
||||
} else {
|
||||
onEditModeClick?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
: handleToggle;
|
||||
|
||||
const shouldUseEditModeClick = isEditMode && isDefined(onEditModeClick);
|
||||
const handleClick = shouldUseEditModeClick ? onEditModeClick : handleToggle;
|
||||
const [skipInitialExpandAnimation] = useState(() => isOpen);
|
||||
|
||||
const selectedNavigationMenuItemIndex = navigationMenuItems.findIndex(
|
||||
(item) =>
|
||||
isLocationMatchingNavigationMenuItem(currentPath, currentViewPath, item),
|
||||
);
|
||||
|
||||
const navigationMenuItemFolderContentLength = navigationMenuItems.length;
|
||||
const { isDragging: isContextDragging } = useContext(
|
||||
NavigationMenuItemDragContext,
|
||||
);
|
||||
const setSortableDropTargetRef = useContext(SortableDropTargetRefContext);
|
||||
const folderContentDropDisabled = useIsDropDisabledForSection(true);
|
||||
|
||||
return (
|
||||
<StyledFolderContainer $isSelectedInEditMode={isSelectedInEditMode}>
|
||||
<NavigationDrawerItemsCollapsableContainer isGroup={isGroup}>
|
||||
<NavigationMenuItemDroppable
|
||||
droppableId={`${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${folderId}`}
|
||||
isWorkspaceSection={true}
|
||||
>
|
||||
<NavigationDrawerItem
|
||||
label={folderName}
|
||||
Icon={FolderIcon}
|
||||
iconColor={
|
||||
isDefined(folderColor)
|
||||
? folderColor
|
||||
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER
|
||||
}
|
||||
onClick={handleClick}
|
||||
className="navigation-drawer-item"
|
||||
triggerEvent="CLICK"
|
||||
preventCollapseOnMobile={isMobile}
|
||||
isDragging={isDragging}
|
||||
rightOptions={
|
||||
isOpen ? (
|
||||
<IconChevronDown
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
) : (
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</NavigationMenuItemDroppable>
|
||||
const { activeDropTargetId, forbiddenDropTargetId } = useContext(
|
||||
NavigationDropTargetContext,
|
||||
);
|
||||
const folderHeaderDroppableId = `${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${folderId}`;
|
||||
const folderContentDroppableId = `${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_PREFIX}${folderId}`;
|
||||
const folderHeaderSlotId = getDndKitDropTargetId(folderHeaderDroppableId, 0);
|
||||
const isForbiddenDropTarget =
|
||||
isDefined(forbiddenDropTargetId) &&
|
||||
(forbiddenDropTargetId.startsWith(`${folderContentDroppableId}::`) ||
|
||||
forbiddenDropTargetId.startsWith(`${folderHeaderDroppableId}::`));
|
||||
const isDragOverFolderHeader =
|
||||
!isForbiddenDropTarget && activeDropTargetId === folderHeaderSlotId;
|
||||
const isCompact =
|
||||
isNavigationMenuInEditMode || navigationMenuItems.length === 0;
|
||||
|
||||
const headerItem = (
|
||||
<NavigationDrawerItem
|
||||
label={folderName}
|
||||
Icon={FolderIcon}
|
||||
iconColor={
|
||||
isDefined(folderColor)
|
||||
? folderColor
|
||||
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER
|
||||
}
|
||||
active={!isOpen && selectedNavigationMenuItemIndex >= 0}
|
||||
onClick={handleClick}
|
||||
className="navigation-drawer-item"
|
||||
triggerEvent="CLICK"
|
||||
preventCollapseOnMobile={isMobile}
|
||||
isDragging={isDragging}
|
||||
alwaysShowRightOptions
|
||||
rightOptions={
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
>
|
||||
{isOpen ? (
|
||||
<IconChevronDown
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
) : (
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledFolderContainer
|
||||
$isSelectedInEditMode={isSelectedInEditMode}
|
||||
data-drag-over-header={isDragOverFolderHeader ? 'true' : undefined}
|
||||
data-forbidden-drop-target={isForbiddenDropTarget ? 'true' : undefined}
|
||||
>
|
||||
<NavigationDrawerItemsCollapsableContainer isGroup={isGroup}>
|
||||
<div ref={setSortableDropTargetRef ?? undefined}>
|
||||
<WorkspaceDndKitDroppableSlot
|
||||
droppableId={folderHeaderDroppableId}
|
||||
index={0}
|
||||
disabled={folderContentDropDisabled}
|
||||
collisionPriority={FOLDER_HEADER_SLOT_COLLISION_PRIORITY}
|
||||
>
|
||||
{headerItem}
|
||||
</WorkspaceDndKitDroppableSlot>
|
||||
</div>
|
||||
<StyledFolderExpandableWrapper>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={!skipInitialExpandAnimation}
|
||||
>
|
||||
<Droppable
|
||||
droppableId={`${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_PREFIX}${folderId}`}
|
||||
isDropDisabled={folderContentDropDisabled}
|
||||
ignoreContainerClipping
|
||||
renderClone={(provided, snapshot, rubric) => (
|
||||
<WorkspaceNavigationMenuItemFolderDragClone
|
||||
draggableProvided={provided}
|
||||
draggableSnapshot={snapshot}
|
||||
rubric={rubric}
|
||||
navigationMenuItems={navigationMenuItems}
|
||||
navigationMenuItemFolderContentLength={
|
||||
navigationMenuItemFolderContentLength
|
||||
}
|
||||
selectedNavigationMenuItemIndex={
|
||||
selectedNavigationMenuItemIndex
|
||||
}
|
||||
<StyledFolderDroppableContent>
|
||||
{navigationMenuItems.map((navigationMenuItem, index) => (
|
||||
<React.Fragment key={navigationMenuItem.id}>
|
||||
<NavigationItemDropTarget
|
||||
folderId={folderId}
|
||||
index={index}
|
||||
sectionId={NavigationSections.WORKSPACE}
|
||||
compact={isCompact}
|
||||
dropTargetIdOverride={getDndKitDropTargetId(
|
||||
folderContentDroppableId,
|
||||
index,
|
||||
)}
|
||||
/>
|
||||
<WorkspaceDndKitSortableItem
|
||||
id={navigationMenuItem.id}
|
||||
index={index}
|
||||
group={folderContentDroppableId}
|
||||
disabled={
|
||||
!isNavigationMenuInEditMode || folderContentDropDisabled
|
||||
}
|
||||
>
|
||||
<WorkspaceNavigationMenuItemFolderSubItem
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
index={index}
|
||||
arrayLength={folderContentLengthForTree}
|
||||
selectedNavigationMenuItemIndex={
|
||||
selectedNavigationMenuItemIndex
|
||||
}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
selectedNavigationMenuItemId={
|
||||
selectedNavigationMenuItemId ?? null
|
||||
}
|
||||
isContextDragging={isContextDragging}
|
||||
/>
|
||||
</WorkspaceDndKitSortableItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<WorkspaceDndKitDroppableSlot
|
||||
droppableId={folderContentDroppableId}
|
||||
index={navigationMenuItems.length}
|
||||
disabled={folderContentDropDisabled}
|
||||
>
|
||||
<NavigationItemDropTarget
|
||||
folderId={folderId}
|
||||
index={navigationMenuItems.length}
|
||||
sectionId={NavigationSections.WORKSPACE}
|
||||
compact
|
||||
dropTargetIdOverride={getDndKitDropTargetId(
|
||||
folderContentDroppableId,
|
||||
navigationMenuItems.length,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
getContainerForClone={() => document.body}
|
||||
>
|
||||
{(provided) => (
|
||||
<StyledFolderDroppableContent
|
||||
ref={provided.innerRef}
|
||||
$compact={isEditMode || navigationMenuItems.length === 0}
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{navigationMenuItems.map((navigationMenuItem, index) => {
|
||||
const objectMetadataItem =
|
||||
navigationMenuItem.itemType ===
|
||||
NavigationMenuItemType.VIEW ||
|
||||
navigationMenuItem.itemType ===
|
||||
NavigationMenuItemType.RECORD
|
||||
? getObjectMetadataForNavigationMenuItem(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
)
|
||||
: null;
|
||||
const handleEditModeClick =
|
||||
isEditMode &&
|
||||
isDefined(onNavigationMenuItemClick) &&
|
||||
(navigationMenuItem.itemType ===
|
||||
NavigationMenuItemType.LINK ||
|
||||
isDefined(objectMetadataItem))
|
||||
? () =>
|
||||
onNavigationMenuItemClick({
|
||||
item: navigationMenuItem,
|
||||
objectMetadataItem:
|
||||
objectMetadataItem ?? undefined,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<DraggableItem
|
||||
key={navigationMenuItem.id}
|
||||
draggableId={navigationMenuItem.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer
|
||||
isDragDisabled={!isEditMode}
|
||||
disableInteractiveElementBlocking={isEditMode}
|
||||
itemComponent={
|
||||
<NavigationDrawerSubItem
|
||||
secondaryLabel={
|
||||
navigationMenuItem.viewKey === ViewKey.Index
|
||||
? undefined
|
||||
: getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
navigationMenuItem.objectNameSingular,
|
||||
})
|
||||
}
|
||||
label={navigationMenuItem.labelIdentifier}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
/>
|
||||
)}
|
||||
to={
|
||||
isContextDragging || handleEditModeClick
|
||||
? undefined
|
||||
: navigationMenuItem.link
|
||||
}
|
||||
onClick={handleEditModeClick}
|
||||
active={index === selectedNavigationMenuItemIndex}
|
||||
isSelectedInEditMode={
|
||||
selectedNavigationMenuItemId ===
|
||||
navigationMenuItem.id
|
||||
}
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index,
|
||||
arrayLength: folderContentLengthForTree,
|
||||
selectedIndex: selectedNavigationMenuItemIndex,
|
||||
})}
|
||||
isDragging={isContextDragging}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</StyledFolderDroppableContent>
|
||||
)}
|
||||
</Droppable>
|
||||
{isEditMode && isSelectedInEditMode && (
|
||||
<NavigationDrawerSubItem
|
||||
label={t`Add menu item`}
|
||||
Icon={IconPlus}
|
||||
onClick={handleAddMenuItemToFolder}
|
||||
triggerEvent="CLICK"
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index: navigationMenuItems.length,
|
||||
arrayLength: folderContentLengthForTree,
|
||||
selectedIndex: selectedNavigationMenuItemIndex,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{isNavigationMenuInEditMode && (
|
||||
<NavigationDrawerSubItem
|
||||
label={t`Add menu item`}
|
||||
Icon={IconPlus}
|
||||
onClick={handleAddMenuItemToFolder}
|
||||
triggerEvent="CLICK"
|
||||
variant="tertiary"
|
||||
isSelectedInEditMode={
|
||||
sidePanelPage === SidePanelPages.NavigationMenuAddItem &&
|
||||
addMenuItemInsertionContext?.targetFolderId === folderId
|
||||
}
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index: navigationMenuItems.length,
|
||||
arrayLength: folderContentLengthForTree,
|
||||
selectedIndex: selectedNavigationMenuItemIndex,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</WorkspaceDndKitDroppableSlot>
|
||||
</StyledFolderDroppableContent>
|
||||
</AnimatedExpandableContainer>
|
||||
</StyledFolderExpandableWrapper>
|
||||
</NavigationDrawerItemsCollapsableContainer>
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export const DND_KIT_DROP_TARGET_ID_SEPARATOR = '::';
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { createContext } from 'react';
|
||||
|
||||
export const SortableDropTargetRefContext = createContext<
|
||||
((element: Element | null) => void) | null
|
||||
>(null);
|
||||
|
|
@ -17,7 +17,6 @@ import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addT
|
|||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { getObjectMetadataIdsInDraft } from '@/navigation-menu-item/utils/getObjectMetadataIdsInDraft';
|
||||
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
|
||||
|
|
@ -45,9 +44,6 @@ export const useHandleAddToNavigationDrop = () => {
|
|||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const coreViews = useAtomStateValue(coreViewsState);
|
||||
const { getIcon } = useIcons();
|
||||
const setSelectedNavigationMenuItemInEditMode = useSetAtomState(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const setIsNavigationMenuInEditMode = useSetAtomState(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
|
|
@ -92,11 +88,13 @@ export const useHandleAddToNavigationDrop = () => {
|
|||
|
||||
const openEditForNewNavItem = (
|
||||
newItemId: string,
|
||||
options: Parameters<typeof openNavigationMenuItemInSidePanel>[0],
|
||||
options: Omit<
|
||||
Parameters<typeof openNavigationMenuItemInSidePanel>[0],
|
||||
'itemId'
|
||||
>,
|
||||
) => {
|
||||
setIsNavigationMenuInEditMode(true);
|
||||
setSelectedNavigationMenuItemInEditMode(newItemId);
|
||||
openNavigationMenuItemInSidePanel(options);
|
||||
openNavigationMenuItemInSidePanel({ ...options, itemId: newItemId });
|
||||
};
|
||||
|
||||
switch (payload.type) {
|
||||
|
|
@ -223,7 +221,6 @@ export const useHandleAddToNavigationDrop = () => {
|
|||
openNavigationMenuItemInSidePanel,
|
||||
setOpenNavigationMenuItemFolderIds,
|
||||
setIsNavigationMenuInEditMode,
|
||||
setSelectedNavigationMenuItemInEditMode,
|
||||
workspaceNavigationMenuItems,
|
||||
store,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants
|
|||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { getPositionBetween } from '@/navigation-menu-item/utils/getPositionBetween';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
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';
|
||||
|
|
@ -83,6 +85,13 @@ export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => {
|
|||
const destinationFolderId = validateAndExtractWorkspaceFolderId(
|
||||
destination.droppableId,
|
||||
);
|
||||
|
||||
if (
|
||||
isNavigationMenuItemFolder(draggedItem) &&
|
||||
isDefined(destinationFolderId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const sourceFolderId = validateAndExtractWorkspaceFolderId(
|
||||
source.droppableId,
|
||||
);
|
||||
|
|
@ -108,69 +117,41 @@ export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => {
|
|||
}
|
||||
|
||||
const isSameList = sourceFolderId === destinationFolderId;
|
||||
let reorderedDestinationList: NavigationMenuItem[];
|
||||
|
||||
if (isSameList) {
|
||||
const listWithoutDragged = sourceList.filter(
|
||||
(item) => item.id !== draggableId,
|
||||
);
|
||||
reorderedDestinationList = [
|
||||
...listWithoutDragged.slice(0, destination.index),
|
||||
draggedItem,
|
||||
...listWithoutDragged.slice(destination.index),
|
||||
];
|
||||
} else {
|
||||
const destinationListWithInsertedItem = [
|
||||
...destinationList.slice(0, destination.index),
|
||||
{ ...draggedItem, folderId: destinationFolderId },
|
||||
...destinationList.slice(destination.index),
|
||||
];
|
||||
reorderedDestinationList = destinationListWithInsertedItem;
|
||||
}
|
||||
|
||||
const destinationWithNormalizedPositions = reorderedDestinationList.map(
|
||||
(item, index) => ({
|
||||
...item,
|
||||
position: index,
|
||||
folderId: isSameList ? item.folderId : destinationFolderId,
|
||||
}),
|
||||
);
|
||||
|
||||
const positionUpdates = new Map<
|
||||
string,
|
||||
{ position: number; folderId: string | null }
|
||||
>();
|
||||
destinationWithNormalizedPositions.forEach((item) => {
|
||||
positionUpdates.set(item.id, {
|
||||
position: item.position,
|
||||
folderId: item.folderId ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
if (!isSameList) {
|
||||
const sourceListWithoutDragged = sourceList.filter(
|
||||
(item) => item.id !== draggableId,
|
||||
const prevItem = listWithoutDragged[destination.index - 1];
|
||||
const nextItem = listWithoutDragged[destination.index];
|
||||
const newPosition = getPositionBetween(
|
||||
prevItem?.position,
|
||||
nextItem?.position,
|
||||
);
|
||||
sourceListWithoutDragged.forEach((item, index) => {
|
||||
positionUpdates.set(item.id, {
|
||||
position: index,
|
||||
folderId: sourceFolderId,
|
||||
});
|
||||
});
|
||||
const updatedDraft = navigationMenuItemsDraft.map(
|
||||
(item): NavigationMenuItem =>
|
||||
item.id === draggableId ? { ...item, position: newPosition } : item,
|
||||
);
|
||||
setNavigationMenuItemsDraft(updatedDraft);
|
||||
return;
|
||||
}
|
||||
|
||||
const prevItem = destinationList[destination.index - 1];
|
||||
const nextItem = destinationList[destination.index];
|
||||
const newPosition = getPositionBetween(
|
||||
prevItem?.position,
|
||||
nextItem?.position,
|
||||
);
|
||||
const updatedDraft = navigationMenuItemsDraft.map(
|
||||
(item): NavigationMenuItem => {
|
||||
const update = positionUpdates.get(item.id);
|
||||
if (!update) return item;
|
||||
if (item.id !== draggableId) return item;
|
||||
return {
|
||||
...item,
|
||||
position: update.position,
|
||||
folderId: update.folderId,
|
||||
position: newPosition,
|
||||
folderId: destinationFolderId,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
setNavigationMenuItemsDraft(updatedDraft);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,66 +1,78 @@
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { getPositionBetween } from '@/navigation-menu-item/utils/getPositionBetween';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
const swapPositionsInDraft = (
|
||||
draft: NavigationMenuItem[],
|
||||
itemA: NavigationMenuItem,
|
||||
itemB: NavigationMenuItem,
|
||||
): NavigationMenuItem[] =>
|
||||
draft.map((item) => {
|
||||
if (item.id === itemA.id) {
|
||||
return { ...item, position: itemB.position };
|
||||
}
|
||||
if (item.id === itemB.id) {
|
||||
return { ...item, position: itemA.position };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
import { useWorkspaceSectionItems } from './useWorkspaceSectionItems';
|
||||
|
||||
export const useNavigationMenuItemMoveRemove = () => {
|
||||
const setNavigationMenuItemsDraft = useSetAtomState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const items = useWorkspaceSectionItems();
|
||||
const visibleItemIds = new Set(items.map((item) => item.id));
|
||||
|
||||
const moveUp = (navigationMenuItemId: string) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) return draft;
|
||||
if (!draft) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const currentItem = draft.find(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (!currentItem) return draft;
|
||||
if (!currentItem) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const folderId = currentItem.folderId ?? null;
|
||||
const siblings = draft
|
||||
.filter((item) => (item.folderId ?? null) === folderId)
|
||||
.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;
|
||||
if (currentIndex <= 0) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const itemAbove = siblings[currentIndex - 1];
|
||||
return swapPositionsInDraft(draft, currentItem, itemAbove);
|
||||
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 moveDown = (navigationMenuItemId: string) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) return draft;
|
||||
if (!draft) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const currentItem = draft.find(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (!currentItem) return draft;
|
||||
if (!currentItem) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const folderId = currentItem.folderId ?? null;
|
||||
const siblings = draft
|
||||
.filter((item) => (item.folderId ?? null) === folderId)
|
||||
.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId && visibleItemIds.has(item.id),
|
||||
)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
const currentIndex = siblings.findIndex(
|
||||
|
|
@ -70,19 +82,30 @@ export const useNavigationMenuItemMoveRemove = () => {
|
|||
return draft;
|
||||
}
|
||||
|
||||
const itemBelow = siblings[currentIndex + 1];
|
||||
return swapPositionsInDraft(draft, currentItem, itemBelow);
|
||||
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 remove = (navigationMenuItemId: string) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) return draft;
|
||||
if (!draft) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const itemToRemove = draft.find(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (!itemToRemove) return draft;
|
||||
if (!itemToRemove) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const isFolder = isNavigationMenuItemFolder(itemToRemove);
|
||||
|
||||
|
|
@ -103,10 +126,14 @@ export const useNavigationMenuItemMoveRemove = () => {
|
|||
targetFolderId: string | null,
|
||||
) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) return draft;
|
||||
if (!draft) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const itemToMove = draft.find((item) => item.id === navigationMenuItemId);
|
||||
if (!itemToMove) return draft;
|
||||
if (!itemToMove) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const isFolder = isNavigationMenuItemFolder(itemToMove);
|
||||
if (isFolder && targetFolderId === navigationMenuItemId) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
|
|
@ -17,12 +18,16 @@ export const useOpenAddItemToFolderPage = () => {
|
|||
const setAddMenuItemInsertionContext = useSetAtomState(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
const setSelectedNavigationMenuItemInEditMode = useSetAtomState(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
|
||||
const openAddItemToFolderPage = ({
|
||||
targetFolderId,
|
||||
targetIndex,
|
||||
resetNavigationStack = true,
|
||||
}: OpenAddItemToFolderPageParams) => {
|
||||
setSelectedNavigationMenuItemInEditMode(null);
|
||||
setAddMenuItemInsertionContext({
|
||||
targetFolderId,
|
||||
targetIndex,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,30 @@
|
|||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { IconComponent } from 'twenty-ui/display';
|
||||
|
||||
export const useOpenNavigationMenuItemInSidePanel = () => {
|
||||
const { navigateSidePanel } = useNavigateSidePanel();
|
||||
const setSelectedNavigationMenuItemInEditMode = useSetAtomState(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
|
||||
const openNavigationMenuItemInSidePanel = ({
|
||||
itemId,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
focusTitleInput = false,
|
||||
}: {
|
||||
itemId?: string;
|
||||
pageTitle: string;
|
||||
pageIcon: IconComponent;
|
||||
focusTitleInput?: boolean;
|
||||
}) => {
|
||||
if (isDefined(itemId)) {
|
||||
setSelectedNavigationMenuItemInEditMode(itemId);
|
||||
}
|
||||
navigateSidePanel({
|
||||
page: SidePanelPages.NavigationMenuItemEdit,
|
||||
pageTitle,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type CreateNavigationMenuItemInput,
|
||||
useCreateNavigationMenuItemMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useCreateNavigationMenuItemMutation } from '~/generated-metadata/graphql';
|
||||
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
|
||||
import { useUpdateNavigationMenuItem } from '@/navigation-menu-item/hooks/useUpdateNavigationMenuItem';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { buildCreateNavigationMenuItemInput } from '@/navigation-menu-item/utils/buildCreateNavigationMenuItemInput';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink';
|
||||
import { orderFoldersForCreation } from '@/navigation-menu-item/utils/orderFoldersForCreation';
|
||||
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
|
|
@ -76,37 +75,39 @@ export const useSaveNavigationMenuItemsDraft = () => {
|
|||
await deleteNavigationMenuItem(draftItem.id);
|
||||
}
|
||||
|
||||
const idsToCreateIncludingRecreated = [...idsToCreate, ...idsToRecreate];
|
||||
const itemsToCreate = [...idsToCreate, ...idsToRecreate];
|
||||
const foldersToCreate = itemsToCreate.filter(isNavigationMenuItemFolder);
|
||||
const nonFoldersToCreate = itemsToCreate.filter(
|
||||
(item) => !isNavigationMenuItemFolder(item),
|
||||
);
|
||||
|
||||
for (const draftItem of idsToCreateIncludingRecreated) {
|
||||
const input: CreateNavigationMenuItemInput = {
|
||||
position: Math.max(0, Math.round(draftItem.position)),
|
||||
};
|
||||
const createdFolderIdByDraftId = new Map<string, string>();
|
||||
const resolveFolderId = (draftFolderId: string): string =>
|
||||
createdFolderIdByDraftId.get(draftFolderId) ?? draftFolderId;
|
||||
|
||||
if (isNavigationMenuItemFolder(draftItem)) {
|
||||
input.name = draftItem.name ?? undefined;
|
||||
input.icon = draftItem.icon ?? null;
|
||||
} else if (isNavigationMenuItemLink(draftItem)) {
|
||||
input.name = draftItem.name ?? 'Link';
|
||||
const linkUrl = (draftItem.link ?? '').trim();
|
||||
input.link =
|
||||
linkUrl.startsWith('http://') || linkUrl.startsWith('https://')
|
||||
? linkUrl
|
||||
: linkUrl
|
||||
? `https://${linkUrl}`
|
||||
: undefined;
|
||||
} else if (isDefined(draftItem.viewId)) {
|
||||
input.viewId = draftItem.viewId;
|
||||
} else if (isDefined(draftItem.targetRecordId)) {
|
||||
input.targetRecordId = draftItem.targetRecordId;
|
||||
input.targetObjectMetadataId =
|
||||
draftItem.targetObjectMetadataId ?? undefined;
|
||||
}
|
||||
|
||||
if (isDefined(draftItem.folderId)) {
|
||||
input.folderId = draftItem.folderId;
|
||||
const orderedFolders = orderFoldersForCreation(
|
||||
foldersToCreate,
|
||||
prefetchIds,
|
||||
);
|
||||
for (const draftItem of orderedFolders) {
|
||||
const input = buildCreateNavigationMenuItemInput(
|
||||
draftItem,
|
||||
resolveFolderId,
|
||||
);
|
||||
const result = await createNavigationMenuItemMutation({
|
||||
variables: { input },
|
||||
});
|
||||
const created = result.data?.createNavigationMenuItem;
|
||||
if (isDefined(created?.id)) {
|
||||
createdFolderIdByDraftId.set(draftItem.id, created.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const draftItem of nonFoldersToCreate) {
|
||||
const input = buildCreateNavigationMenuItemInput(
|
||||
draftItem,
|
||||
resolveFolderId,
|
||||
);
|
||||
await createNavigationMenuItemMutation({
|
||||
variables: { input },
|
||||
});
|
||||
|
|
@ -150,10 +151,13 @@ export const useSaveNavigationMenuItemsDraft = () => {
|
|||
} = { id: draftItem.id };
|
||||
|
||||
if (positionChanged) {
|
||||
updateInput.position = Math.max(0, Math.round(draftItem.position));
|
||||
updateInput.position = draftItem.position;
|
||||
}
|
||||
if (folderIdChanged) {
|
||||
updateInput.folderId = draftItem.folderId ?? null;
|
||||
updateInput.folderId =
|
||||
draftItem.folderId != null
|
||||
? resolveFolderId(draftItem.folderId)
|
||||
: null;
|
||||
}
|
||||
if (nameChanged && isNavigationMenuItemFolder(draftItem)) {
|
||||
updateInput.name = draftItem.name ?? undefined;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
|
||||
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
|
||||
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { currentNavigationMenuItemFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
type UseWorkspaceFolderOpenStateParams = {
|
||||
folderId: string;
|
||||
navigationMenuItems: ProcessedNavigationMenuItem[];
|
||||
};
|
||||
|
||||
export const useWorkspaceFolderOpenState = ({
|
||||
folderId,
|
||||
navigationMenuItems,
|
||||
}: UseWorkspaceFolderOpenStateParams) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const currentPath = location.pathname;
|
||||
const currentViewPath = location.pathname + location.search;
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] =
|
||||
useAtomState(openNavigationMenuItemFolderIdsState);
|
||||
const setCurrentNavigationMenuItemFolderId = useSetAtomState(
|
||||
currentNavigationMenuItemFolderIdState,
|
||||
);
|
||||
|
||||
const isOpen = openNavigationMenuItemFolderIds.includes(folderId);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (isMobile) {
|
||||
setCurrentNavigationMenuItemFolderId((prev) =>
|
||||
prev === folderId ? null : folderId,
|
||||
);
|
||||
} else {
|
||||
setOpenNavigationMenuItemFolderIds((current) =>
|
||||
isOpen
|
||||
? current.filter((id) => id !== folderId)
|
||||
: [...current, folderId],
|
||||
);
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
const firstNonLinkItem = navigationMenuItems.find(
|
||||
(item) =>
|
||||
item.itemType !== NavigationMenuItemType.LINK &&
|
||||
isNonEmptyString(item.link),
|
||||
);
|
||||
if (isDefined(firstNonLinkItem?.link)) {
|
||||
navigate(firstNonLinkItem.link);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const selectedNavigationMenuItemIndex = navigationMenuItems.findIndex(
|
||||
(item) =>
|
||||
isLocationMatchingNavigationMenuItem(currentPath, currentViewPath, item),
|
||||
);
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
handleToggle,
|
||||
selectedNavigationMenuItemIndex,
|
||||
};
|
||||
};
|
||||
|
|
@ -5,8 +5,10 @@ import { NavigationMenuItemType } from '@/navigation-menu-item/constants/Navigat
|
|||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
|
|
@ -36,6 +38,7 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
|
|||
useNavigationMenuItemsByFolder();
|
||||
const coreViews = useAtomStateValue(coreViewsState);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
|
||||
|
|
@ -76,7 +79,13 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
|
|||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
if (isDefined(objectMetadataItem)) {
|
||||
if (
|
||||
isDefined(objectMetadataItem) &&
|
||||
getObjectPermissionsForObject(
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataItem.id,
|
||||
).canReadObjectRecords
|
||||
) {
|
||||
acc.push(processedItem);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export type AddMenuItemInsertionContext = {
|
||||
targetFolderId: string | null;
|
||||
targetIndex: number;
|
||||
disableDrag?: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import { extractDomainFromUrl } from '@/navigation-menu-item/utils/extractDomainFromUrl';
|
||||
|
||||
describe('extractDomainFromUrl', () => {
|
||||
it('returns hostname and strips www prefix', () => {
|
||||
expect(extractDomainFromUrl('https://www.example.com/path')).toBe(
|
||||
'example.com',
|
||||
);
|
||||
expect(extractDomainFromUrl('https://example.com')).toBe('example.com');
|
||||
});
|
||||
|
||||
it('returns undefined for invalid URLs', () => {
|
||||
expect(extractDomainFromUrl('not-a-url')).toBeUndefined();
|
||||
expect(extractDomainFromUrl('')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { getPositionBetween } from '@/navigation-menu-item/utils/getPositionBetween';
|
||||
|
||||
describe('getPositionBetween', () => {
|
||||
it('returns next - 1 when only next is defined (insert before)', () => {
|
||||
expect(getPositionBetween(null, 10)).toBe(9);
|
||||
expect(getPositionBetween(undefined, 1)).toBe(0);
|
||||
});
|
||||
|
||||
it('returns prev + 1 when only prev is defined (insert after)', () => {
|
||||
expect(getPositionBetween(5, null)).toBe(6);
|
||||
expect(getPositionBetween(0, undefined)).toBe(1);
|
||||
});
|
||||
|
||||
it('returns midpoint when both are defined and different', () => {
|
||||
expect(getPositionBetween(0, 10)).toBe(5);
|
||||
expect(getPositionBetween(1, 3)).toBe(2);
|
||||
});
|
||||
|
||||
it('returns prev - 1 when both defined and equal', () => {
|
||||
expect(getPositionBetween(5, 5)).toBe(4);
|
||||
});
|
||||
|
||||
it('returns 0 when both are null or undefined', () => {
|
||||
expect(getPositionBetween(null, null)).toBe(0);
|
||||
expect(getPositionBetween(undefined, undefined)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { orderFoldersForCreation } from '@/navigation-menu-item/utils/orderFoldersForCreation';
|
||||
|
||||
describe('orderFoldersForCreation', () => {
|
||||
it('returns empty array for empty input', () => {
|
||||
expect(orderFoldersForCreation([], new Set())).toEqual([]);
|
||||
});
|
||||
|
||||
it('orders so parent folders come before their children', () => {
|
||||
const parent = { id: 'parent', folderId: null };
|
||||
const child = { id: 'child', folderId: 'parent' };
|
||||
expect(orderFoldersForCreation([child, parent], new Set())).toEqual([
|
||||
parent,
|
||||
child,
|
||||
]);
|
||||
});
|
||||
|
||||
it('treats folderId in existingIds as already created so child can be placed', () => {
|
||||
const child = { id: 'child', folderId: 'existing-parent' };
|
||||
expect(
|
||||
orderFoldersForCreation([child], new Set(['existing-parent'])),
|
||||
).toEqual([child]);
|
||||
});
|
||||
|
||||
it('orders multiple levels: root first, then children in dependency order', () => {
|
||||
const a = { id: 'a', folderId: undefined };
|
||||
const b = { id: 'b', folderId: 'a' };
|
||||
const c = { id: 'c', folderId: 'b' };
|
||||
expect(orderFoldersForCreation([c, a, b], new Set())).toEqual([a, b, c]);
|
||||
});
|
||||
|
||||
it('leaves out folders whose parent is missing and not in existingIds', () => {
|
||||
const child = { id: 'child', folderId: 'missing-parent' };
|
||||
expect(orderFoldersForCreation([child], new Set())).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import type {
|
||||
CreateNavigationMenuItemInput,
|
||||
NavigationMenuItem,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink';
|
||||
|
||||
export const buildCreateNavigationMenuItemInput = (
|
||||
draftItem: NavigationMenuItem,
|
||||
resolveFolderId: (draftFolderId: string) => string,
|
||||
): CreateNavigationMenuItemInput => {
|
||||
const input: CreateNavigationMenuItemInput = {
|
||||
position: draftItem.position,
|
||||
};
|
||||
|
||||
if (isNavigationMenuItemFolder(draftItem)) {
|
||||
input.name = draftItem.name ?? undefined;
|
||||
input.icon = draftItem.icon ?? null;
|
||||
} else if (isNavigationMenuItemLink(draftItem)) {
|
||||
input.name = draftItem.name ?? 'Link';
|
||||
const linkUrl = (draftItem.link ?? '').trim();
|
||||
input.link =
|
||||
linkUrl.startsWith('http://') || linkUrl.startsWith('https://')
|
||||
? linkUrl
|
||||
: linkUrl
|
||||
? `https://${linkUrl}`
|
||||
: undefined;
|
||||
} else if (isDefined(draftItem.viewId)) {
|
||||
input.viewId = draftItem.viewId;
|
||||
} else if (isDefined(draftItem.targetRecordId)) {
|
||||
input.targetRecordId = draftItem.targetRecordId;
|
||||
input.targetObjectMetadataId =
|
||||
draftItem.targetObjectMetadataId ?? undefined;
|
||||
}
|
||||
|
||||
if (isDefined(draftItem.folderId)) {
|
||||
input.folderId = resolveFolderId(draftItem.folderId);
|
||||
}
|
||||
|
||||
if (isDefined(draftItem.color)) {
|
||||
input.color = draftItem.color;
|
||||
}
|
||||
|
||||
return input;
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export const extractDomainFromUrl = (url: string): string | undefined => {
|
||||
try {
|
||||
const hostname = new URL(url).hostname;
|
||||
return hostname.replace(/^www\./, '') || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { DND_KIT_DROP_TARGET_ID_SEPARATOR } from '@/navigation-menu-item/constants/DndKitDropTargetIdSeparator';
|
||||
|
||||
export const getDndKitDropTargetId = (
|
||||
droppableId: string,
|
||||
index: number,
|
||||
): string => `${droppableId}${DND_KIT_DROP_TARGET_ID_SEPARATOR}${index}`;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
|
||||
import type { DropResult } from '@hello-pangea/dnd';
|
||||
|
||||
const FOLDER_PREFIX = 'folder-';
|
||||
|
||||
export const getFavoritesDropTargetIdFromDestination = (
|
||||
destination: DropResult['destination'],
|
||||
): string | null => {
|
||||
if (!destination) return null;
|
||||
const { droppableId, index } = destination;
|
||||
if (
|
||||
droppableId === NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS
|
||||
) {
|
||||
return `${NavigationSections.FAVORITES}-orphan-${index}`;
|
||||
}
|
||||
if (droppableId.startsWith(FOLDER_PREFIX)) {
|
||||
const folderId = droppableId.slice(FOLDER_PREFIX.length);
|
||||
return `${NavigationSections.FAVORITES}-${folderId}-${index}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { getLogoUrlFromDomainName } from 'twenty-shared/utils';
|
||||
|
||||
export const getLinkFaviconUrl = (
|
||||
link: string | null | undefined,
|
||||
): string | undefined => {
|
||||
const trimmed = (link ?? '').trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized =
|
||||
trimmed.startsWith('http://') || trimmed.startsWith('https://')
|
||||
? trimmed
|
||||
: `https://${trimmed}`;
|
||||
try {
|
||||
const hostname = new URL(normalized).hostname;
|
||||
return getLogoUrlFromDomainName(hostname);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const getPositionBetween = (
|
||||
prevPosition: number | null | undefined,
|
||||
nextPosition: number | null | undefined,
|
||||
): number => {
|
||||
if (!isDefined(prevPosition) && isDefined(nextPosition))
|
||||
return nextPosition - 1;
|
||||
if (isDefined(prevPosition) && !isDefined(nextPosition))
|
||||
return prevPosition + 1;
|
||||
if (isDefined(prevPosition) && isDefined(nextPosition)) {
|
||||
if (prevPosition === nextPosition) {
|
||||
return prevPosition - 1;
|
||||
}
|
||||
return (prevPosition + nextPosition) / 2;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
export const orderFoldersForCreation = <
|
||||
T extends { id: string; folderId?: string | null },
|
||||
>(
|
||||
folders: T[],
|
||||
existingIds: Set<string>,
|
||||
): T[] => {
|
||||
const result: T[] = [];
|
||||
let remaining = [...folders];
|
||||
|
||||
while (remaining.length > 0) {
|
||||
const readyIndex = remaining.findIndex(
|
||||
(folder) =>
|
||||
!folder.folderId ||
|
||||
existingIds.has(folder.folderId) ||
|
||||
result.some((r) => r.id === folder.folderId),
|
||||
);
|
||||
if (readyIndex === -1) break;
|
||||
|
||||
const [ready] = remaining.splice(readyIndex, 1);
|
||||
result.push(ready);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
import {
|
||||
DragDropContext,
|
||||
type DragStart,
|
||||
type DropResult,
|
||||
type OnDragUpdateResponder,
|
||||
type ResponderProvided,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
|
||||
import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext';
|
||||
import { useHandleFavoriteDragAndDrop } from '@/favorites/hooks/useHandleFavoriteDragAndDrop';
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
|
||||
import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/hooks/useHandleAddToNavigationDrop';
|
||||
import { useHandleNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/hooks/useHandleNavigationMenuItemDragAndDrop';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
|
||||
import { getDropTargetIdFromDestination } from '@/navigation-menu-item/utils/getDropTargetIdFromDestination';
|
||||
import { getFavoritesDropTargetIdFromDestination } from '@/navigation-menu-item/utils/getFavoritesDropTargetIdFromDestination';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
|
||||
import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type FavoritesDragDropProviderContentProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const FavoritesDragDropProviderContent = ({
|
||||
children,
|
||||
}: FavoritesDragDropProviderContentProps) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [sourceDroppableId, setSourceDroppableId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [activeDropTargetId, setActiveDropTargetId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [
|
||||
addToNavigationFallbackDestination,
|
||||
setAddToNavigationFallbackDestination,
|
||||
] = useState<{ droppableId: string; index: number } | null>(null);
|
||||
|
||||
const store = useStore();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop();
|
||||
const { handleFavoriteDragAndDrop } = useHandleFavoriteDragAndDrop();
|
||||
const { handleNavigationMenuItemDragAndDrop } =
|
||||
useHandleNavigationMenuItemDragAndDrop();
|
||||
|
||||
const isFavoritesDroppableId = (droppableId: string) =>
|
||||
droppableId ===
|
||||
NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS ||
|
||||
droppableId.startsWith('folder-');
|
||||
|
||||
const orphanItemCount = workspaceNavigationMenuItems.filter(
|
||||
(item: { folderId?: string | null }) => !isDefined(item.folderId),
|
||||
).length;
|
||||
|
||||
const handleDragStart = (dragStart: DragStart) => {
|
||||
setIsDragging(true);
|
||||
setSourceDroppableId(dragStart.source.droppableId);
|
||||
if (dragStart.source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
const defaultDestination = {
|
||||
droppableId:
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
index: orphanItemCount,
|
||||
};
|
||||
setAddToNavigationFallbackDestination(defaultDestination);
|
||||
setActiveDropTargetId(getDropTargetIdFromDestination(defaultDestination));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragUpdate = (update: Parameters<OnDragUpdateResponder>[0]) => {
|
||||
const { source, destination } = update;
|
||||
|
||||
if (source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
if (
|
||||
destination !== null &&
|
||||
isWorkspaceDroppableId(destination.droppableId)
|
||||
) {
|
||||
setAddToNavigationFallbackDestination(destination);
|
||||
const dropTargetId = getDropTargetIdFromDestination(destination);
|
||||
setActiveDropTargetId(dropTargetId);
|
||||
|
||||
const payload =
|
||||
store
|
||||
.get(addToNavPayloadRegistryState.atom)
|
||||
.get(update.draggableId) ?? null;
|
||||
const folderId = validateAndExtractWorkspaceFolderId(
|
||||
destination.droppableId,
|
||||
);
|
||||
const isFolderOverFolder =
|
||||
payload?.type === 'folder' && folderId !== null;
|
||||
setForbiddenDropTargetId(isFolderOverFolder ? dropTargetId : null);
|
||||
} else {
|
||||
setForbiddenDropTargetId(null);
|
||||
const fallback = addToNavigationFallbackDestination;
|
||||
setActiveDropTargetId(
|
||||
fallback ? getDropTargetIdFromDestination(fallback) : null,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFavoritesDroppableId(source.droppableId)) {
|
||||
if (isDefined(destination)) {
|
||||
const dropTargetId =
|
||||
getFavoritesDropTargetIdFromDestination(destination);
|
||||
setActiveDropTargetId(dropTargetId);
|
||||
} else {
|
||||
setActiveDropTargetId(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (result: DropResult, provided: ResponderProvided) => {
|
||||
const isAddToNavigationSource =
|
||||
result.source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID;
|
||||
const effectiveResult: DropResult =
|
||||
isAddToNavigationSource &&
|
||||
!result.destination &&
|
||||
addToNavigationFallbackDestination
|
||||
? { ...result, destination: addToNavigationFallbackDestination }
|
||||
: result;
|
||||
|
||||
setIsDragging(false);
|
||||
setSourceDroppableId(null);
|
||||
setActiveDropTargetId(null);
|
||||
setForbiddenDropTargetId(null);
|
||||
setAddToNavigationFallbackDestination(null);
|
||||
|
||||
if (isAddToNavigationSource) {
|
||||
handleAddToNavigationDrop(effectiveResult, provided);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFavoritesDroppableId(result.source.droppableId)) {
|
||||
handleNavigationMenuItemDragAndDrop(result, provided);
|
||||
return;
|
||||
}
|
||||
|
||||
handleFavoriteDragAndDrop(result, provided);
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigationDragSourceContext.Provider value={{ sourceDroppableId }}>
|
||||
<NavigationMenuItemDragContext.Provider value={{ isDragging }}>
|
||||
<FavoritesDragContext.Provider value={{ isDragging }}>
|
||||
<NavigationDropTargetContext.Provider
|
||||
value={{
|
||||
activeDropTargetId,
|
||||
setActiveDropTargetId,
|
||||
forbiddenDropTargetId,
|
||||
setForbiddenDropTargetId,
|
||||
addToNavigationFallbackDestination,
|
||||
}}
|
||||
>
|
||||
<DragDropContext
|
||||
onDragStart={handleDragStart}
|
||||
onDragUpdate={handleDragUpdate}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{children}
|
||||
</DragDropContext>
|
||||
</NavigationDropTargetContext.Provider>
|
||||
</FavoritesDragContext.Provider>
|
||||
</NavigationMenuItemDragContext.Provider>
|
||||
</NavigationDragSourceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
import { NavigationDrawerOpenedSection } from '@/object-metadata/components/NavigationDrawerOpenedSection';
|
||||
import { NavigationDrawerWorkspaceSectionSkeletonLoader } from '@/object-metadata/components/NavigationDrawerWorkspaceSectionSkeletonLoader';
|
||||
import { RemoteNavigationDrawerSection } from '@/object-metadata/components/RemoteNavigationDrawerSection';
|
||||
|
||||
import { NavigationDrawerOtherSection } from '@/navigation/components/NavigationDrawerOtherSection';
|
||||
import { styled } from '@linaria/react';
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
import { NavigationDrawerOtherSection } from '@/navigation/components/NavigationDrawerOtherSection';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher = lazy(() =>
|
||||
|
|
@ -32,10 +34,8 @@ export const MainNavigationDrawerScrollableItems = () => {
|
|||
return (
|
||||
<StyledScrollableItemsContainer>
|
||||
<NavigationDrawerOpenedSection />
|
||||
<Suspense fallback={null}>
|
||||
<Suspense fallback={<NavigationDrawerWorkspaceSectionSkeletonLoader />}>
|
||||
<CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher />
|
||||
</Suspense>
|
||||
<Suspense fallback={null}>
|
||||
<WorkspaceNavigationMenuItemsDispatcher />
|
||||
</Suspense>
|
||||
<RemoteNavigationDrawerSection />
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { IconHelpCircle, IconSettings } from 'twenty-ui/display';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl';
|
||||
|
|
@ -54,24 +55,29 @@ export const NavigationDrawerOtherSection = () => {
|
|||
<NavigationDrawerSectionTitle
|
||||
label={t`Other`}
|
||||
onClick={toggleNavigationSection}
|
||||
isOpen={isNavigationSectionOpen}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
{isNavigationSectionOpen && (
|
||||
<>
|
||||
<NavigationDrawerItem
|
||||
label={t`Settings`}
|
||||
Icon={IconSettings}
|
||||
onClick={handleSettingsClick}
|
||||
/>
|
||||
<NavigationDrawerItem
|
||||
label={t`Documentation`}
|
||||
to={getDocumentationUrl({
|
||||
locale: currentWorkspaceMember?.locale,
|
||||
})}
|
||||
Icon={IconHelpCircle}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isNavigationSectionOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={false}
|
||||
>
|
||||
<NavigationDrawerItem
|
||||
label={t`Settings`}
|
||||
Icon={IconSettings}
|
||||
onClick={handleSettingsClick}
|
||||
/>
|
||||
<NavigationDrawerItem
|
||||
label={t`Documentation`}
|
||||
to={getDocumentationUrl({
|
||||
locale: currentWorkspaceMember?.locale,
|
||||
})}
|
||||
Icon={IconHelpCircle}
|
||||
/>
|
||||
</AnimatedExpandableContainer>
|
||||
</NavigationDrawerSection>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,31 +1,12 @@
|
|||
import {
|
||||
DragDropContext,
|
||||
type DragStart,
|
||||
type DropResult,
|
||||
type OnDragUpdateResponder,
|
||||
type ResponderProvided,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { type ReactNode, useCallback, useState } from 'react';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
import { lazy, Suspense, useState, type ReactNode } from 'react';
|
||||
|
||||
import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext';
|
||||
import { useHandleFavoriteDragAndDrop } from '@/favorites/hooks/useHandleFavoriteDragAndDrop';
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
|
||||
import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/hooks/useHandleAddToNavigationDrop';
|
||||
import { useHandleNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/hooks/useHandleNavigationMenuItemDragAndDrop';
|
||||
import { useHandleWorkspaceNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
|
||||
import { getDropTargetIdFromDestination } from '@/navigation-menu-item/utils/getDropTargetIdFromDestination';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
|
||||
import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId';
|
||||
import { useStore } from 'jotai';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageDragDropProviderMountEffect } from '@/navigation/components/PageDragDropProviderMountEffect';
|
||||
|
||||
const LazyWorkspaceDndKitProvider = lazy(() =>
|
||||
import('@/navigation/components/WorkspaceDndKitProvider').then((m) => ({
|
||||
default: m.WorkspaceDndKitProvider,
|
||||
})),
|
||||
);
|
||||
|
||||
type PageDragDropProviderProps = {
|
||||
children: ReactNode;
|
||||
|
|
@ -34,144 +15,22 @@ type PageDragDropProviderProps = {
|
|||
export const PageDragDropProvider = ({
|
||||
children,
|
||||
}: PageDragDropProviderProps) => {
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [sourceDroppableId, setSourceDroppableId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [activeDropTargetId, setActiveDropTargetId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [
|
||||
addToNavigationFallbackDestination,
|
||||
setAddToNavigationFallbackDestination,
|
||||
] = useState<{ droppableId: string; index: number } | null>(null);
|
||||
const [hasProviderMounted, setHasProviderMounted] = useState(false);
|
||||
|
||||
const store = useStore();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop();
|
||||
const { handleNavigationMenuItemDragAndDrop } =
|
||||
useHandleNavigationMenuItemDragAndDrop();
|
||||
const { handleWorkspaceNavigationMenuItemDragAndDrop } =
|
||||
useHandleWorkspaceNavigationMenuItemDragAndDrop();
|
||||
const { handleFavoriteDragAndDrop } = useHandleFavoriteDragAndDrop();
|
||||
|
||||
const orphanItemCount = workspaceNavigationMenuItems.filter(
|
||||
(item) => !isDefined(item.folderId),
|
||||
).length;
|
||||
|
||||
const handleDragStart = (dragStart: DragStart) => {
|
||||
setIsDragging(true);
|
||||
setSourceDroppableId(dragStart.source.droppableId);
|
||||
if (dragStart.source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
const defaultDestination = {
|
||||
droppableId:
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
index: orphanItemCount,
|
||||
};
|
||||
setAddToNavigationFallbackDestination(defaultDestination);
|
||||
setActiveDropTargetId(getDropTargetIdFromDestination(defaultDestination));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragUpdate = useCallback(
|
||||
((update: Parameters<OnDragUpdateResponder>[0]) => {
|
||||
const { source, destination } = update;
|
||||
if (source.droppableId !== ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
destination !== null &&
|
||||
isWorkspaceDroppableId(destination.droppableId)
|
||||
) {
|
||||
setAddToNavigationFallbackDestination(destination);
|
||||
const dropTargetId = getDropTargetIdFromDestination(destination);
|
||||
setActiveDropTargetId(dropTargetId);
|
||||
|
||||
const payload =
|
||||
store
|
||||
.get(addToNavPayloadRegistryState.atom)
|
||||
.get(update.draggableId) ?? null;
|
||||
const folderId = validateAndExtractWorkspaceFolderId(
|
||||
destination.droppableId,
|
||||
);
|
||||
const isFolderOverFolder =
|
||||
payload?.type === 'folder' && folderId !== null;
|
||||
setForbiddenDropTargetId(isFolderOverFolder ? dropTargetId : null);
|
||||
} else {
|
||||
setForbiddenDropTargetId(null);
|
||||
const fallback = addToNavigationFallbackDestination;
|
||||
setActiveDropTargetId(
|
||||
fallback ? getDropTargetIdFromDestination(fallback) : null,
|
||||
);
|
||||
}
|
||||
}) as OnDragUpdateResponder,
|
||||
[addToNavigationFallbackDestination, store],
|
||||
);
|
||||
|
||||
const handleDragEnd = (result: DropResult, provided: ResponderProvided) => {
|
||||
const isAddToNavigationSource =
|
||||
result.source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID;
|
||||
const effectiveResult: DropResult =
|
||||
isAddToNavigationSource &&
|
||||
!result.destination &&
|
||||
addToNavigationFallbackDestination
|
||||
? { ...result, destination: addToNavigationFallbackDestination }
|
||||
: result;
|
||||
|
||||
setIsDragging(false);
|
||||
setSourceDroppableId(null);
|
||||
setActiveDropTargetId(null);
|
||||
setForbiddenDropTargetId(null);
|
||||
setAddToNavigationFallbackDestination(null);
|
||||
|
||||
if (isAddToNavigationSource) {
|
||||
handleAddToNavigationDrop(effectiveResult, provided);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNavigationMenuItemEditingEnabled) {
|
||||
const isWorkspaceDrop =
|
||||
isWorkspaceDroppableId(result.source?.droppableId) &&
|
||||
isWorkspaceDroppableId(result.destination?.droppableId);
|
||||
if (isWorkspaceDrop) {
|
||||
handleWorkspaceNavigationMenuItemDragAndDrop(result, provided);
|
||||
} else {
|
||||
handleNavigationMenuItemDragAndDrop(result, provided);
|
||||
}
|
||||
} else {
|
||||
handleFavoriteDragAndDrop(result, provided);
|
||||
}
|
||||
};
|
||||
if (!hasProviderMounted) {
|
||||
return (
|
||||
<>
|
||||
<PageDragDropProviderMountEffect
|
||||
onEnterEditMode={() => setHasProviderMounted(true)}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationDragSourceContext.Provider value={{ sourceDroppableId }}>
|
||||
<NavigationMenuItemDragContext.Provider value={{ isDragging }}>
|
||||
<FavoritesDragContext.Provider value={{ isDragging }}>
|
||||
<NavigationDropTargetContext.Provider
|
||||
value={{
|
||||
activeDropTargetId,
|
||||
setActiveDropTargetId,
|
||||
forbiddenDropTargetId,
|
||||
setForbiddenDropTargetId,
|
||||
addToNavigationFallbackDestination,
|
||||
}}
|
||||
>
|
||||
<DragDropContext
|
||||
onDragStart={handleDragStart}
|
||||
onDragUpdate={handleDragUpdate}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{children}
|
||||
</DragDropContext>
|
||||
</NavigationDropTargetContext.Provider>
|
||||
</FavoritesDragContext.Provider>
|
||||
</NavigationMenuItemDragContext.Provider>
|
||||
</NavigationDragSourceContext.Provider>
|
||||
<Suspense fallback={<>{children}</>}>
|
||||
<LazyWorkspaceDndKitProvider>{children}</LazyWorkspaceDndKitProvider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
type PageDragDropProviderMountEffectProps = {
|
||||
onEnterEditMode: () => void;
|
||||
};
|
||||
|
||||
export const PageDragDropProviderMountEffect = ({
|
||||
onEnterEditMode,
|
||||
}: PageDragDropProviderMountEffectProps) => {
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isNavigationMenuInEditMode) {
|
||||
onEnterEditMode();
|
||||
}
|
||||
}, [isNavigationMenuInEditMode, onEnterEditMode]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { PointerActivationConstraints } from '@dnd-kit/dom';
|
||||
import {
|
||||
DragDropProvider,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
} from '@dnd-kit/react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
|
||||
import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData';
|
||||
|
||||
import { useWorkspaceDndKit } from '@/navigation/hooks/useWorkspaceDndKit';
|
||||
|
||||
const WORKSPACE_DND_SENSORS = [
|
||||
PointerSensor.configure({
|
||||
activationConstraints: [
|
||||
new PointerActivationConstraints.Distance({ value: 8 }),
|
||||
],
|
||||
}),
|
||||
KeyboardSensor,
|
||||
];
|
||||
|
||||
type WorkspaceDndKitProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const WorkspaceDndKitProvider = ({
|
||||
children,
|
||||
}: WorkspaceDndKitProviderProps) => {
|
||||
const { contextValues, handlers } = useWorkspaceDndKit();
|
||||
|
||||
return (
|
||||
<NavigationDragSourceContext.Provider value={contextValues.dragSource}>
|
||||
<NavigationMenuItemDragContext.Provider value={contextValues.drag}>
|
||||
<NavigationDropTargetContext.Provider value={contextValues.dropTarget}>
|
||||
<DragDropProvider<DraggableData>
|
||||
sensors={WORKSPACE_DND_SENSORS}
|
||||
onDragStart={handlers.onDragStart}
|
||||
onDragOver={handlers.onDragOver}
|
||||
onDragEnd={handlers.onDragEnd}
|
||||
>
|
||||
{children}
|
||||
</DragDropProvider>
|
||||
</NavigationDropTargetContext.Provider>
|
||||
</NavigationMenuItemDragContext.Provider>
|
||||
</NavigationDragSourceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const DROP_RESULT_OPTIONS = {
|
||||
reason: 'DROP' as const,
|
||||
combine: null,
|
||||
mode: 'FLUID' as const,
|
||||
type: 'DEFAULT' as const,
|
||||
};
|
||||
|
|
@ -0,0 +1,351 @@
|
|||
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 { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/hooks/useHandleAddToNavigationDrop';
|
||||
import { useHandleWorkspaceNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
|
||||
import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId';
|
||||
|
||||
import { DROP_RESULT_OPTIONS } from '@/navigation/constants/workspaceDndKitDropResultOptions';
|
||||
import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData';
|
||||
import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination';
|
||||
import { isFolderDrag } from '@/navigation/utils/workspaceDndKitIsFolderDrag';
|
||||
import { resolveDropTarget } from '@/navigation/utils/workspaceDndKitResolveDropTarget';
|
||||
import { toDropResult } from '@/navigation/utils/workspaceDndKitToDropResult';
|
||||
|
||||
type DragStartPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragStart']
|
||||
>
|
||||
>[0];
|
||||
type DragOverPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragOver']
|
||||
>
|
||||
>[0];
|
||||
type DragEndPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragEnd']
|
||||
>
|
||||
>[0];
|
||||
|
||||
export type WorkspaceDndKitContextValues = {
|
||||
dragSource: { sourceDroppableId: string | null };
|
||||
drag: { isDragging: boolean };
|
||||
dropTarget: {
|
||||
activeDropTargetId: string | null;
|
||||
setActiveDropTargetId: (id: string | null) => void;
|
||||
forbiddenDropTargetId: string | null;
|
||||
setForbiddenDropTargetId: (id: string | null) => void;
|
||||
addToNavigationFallbackDestination: DropDestination | null;
|
||||
};
|
||||
};
|
||||
|
||||
export const useWorkspaceDndKit = (): {
|
||||
contextValues: WorkspaceDndKitContextValues;
|
||||
handlers: {
|
||||
onDragStart: (event: DragStartPayload) => void;
|
||||
onDragOver: (event: DragOverPayload) => void;
|
||||
onDragEnd: (event: DragEndPayload) => void;
|
||||
};
|
||||
} => {
|
||||
const store = useStore();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [sourceDroppableId, setSourceDroppableId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [activeDropTargetId, setActiveDropTargetId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [
|
||||
addToNavigationFallbackDestination,
|
||||
setAddToNavigationFallbackDestination,
|
||||
] = useState<DropDestination | null>(null);
|
||||
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop();
|
||||
const { handleWorkspaceNavigationMenuItemDragAndDrop } =
|
||||
useHandleWorkspaceNavigationMenuItemDragAndDrop();
|
||||
|
||||
const orphanItemCount = workspaceNavigationMenuItems.filter(
|
||||
(item: { folderId?: string | null }) => !isDefined(item.folderId),
|
||||
).length;
|
||||
|
||||
const getNavItemById = useCallback(
|
||||
(id: string | undefined) =>
|
||||
id
|
||||
? workspaceNavigationMenuItems.find((item) => item.id === id)
|
||||
: undefined,
|
||||
[workspaceNavigationMenuItems],
|
||||
);
|
||||
|
||||
const applyWorkspaceReorderIfAllowed = (
|
||||
id: string,
|
||||
source: DropDestination,
|
||||
destination: DropDestination,
|
||||
) => {
|
||||
const draggedItem = getNavItemById(id);
|
||||
const destFolderId = validateAndExtractWorkspaceFolderId(
|
||||
destination.droppableId,
|
||||
);
|
||||
if (
|
||||
isDefined(destFolderId) &&
|
||||
isDefined(draggedItem) &&
|
||||
isNavigationMenuItemFolder(draggedItem)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const result = toDropResult(
|
||||
id,
|
||||
{
|
||||
sourceDroppableId: source.droppableId,
|
||||
sourceIndex: source.index,
|
||||
},
|
||||
destination,
|
||||
);
|
||||
const provided: ResponderProvided = { announce: () => {} };
|
||||
handleWorkspaceNavigationMenuItemDragAndDrop(
|
||||
{ ...result, ...DROP_RESULT_OPTIONS },
|
||||
provided,
|
||||
);
|
||||
};
|
||||
|
||||
const handleDragStart = (event: DragStartPayload) => {
|
||||
const { operation } = event;
|
||||
setIsDragging(true);
|
||||
const source = operation.source;
|
||||
const sourceId = source?.data?.sourceDroppableId ?? null;
|
||||
setSourceDroppableId(sourceId);
|
||||
|
||||
if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
const defaultDestination: DropDestination = {
|
||||
droppableId:
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
index: orphanItemCount,
|
||||
};
|
||||
setAddToNavigationFallbackDestination(defaultDestination);
|
||||
setActiveDropTargetId(
|
||||
getDndKitDropTargetId(
|
||||
defaultDestination.droppableId,
|
||||
defaultDestination.index,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = useCallback(
|
||||
(event: DragOverPayload) => {
|
||||
const { operation } = event;
|
||||
const source = operation.source;
|
||||
const target = operation.target;
|
||||
const isAddToNavDrag =
|
||||
sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID;
|
||||
const sourceIsSortable = source !== null && isSortable(source);
|
||||
const resolved = resolveDropTarget(target, getNavItemById);
|
||||
|
||||
const getPayload = () =>
|
||||
store.get(addToNavPayloadRegistryState.atom).get(String(source?.id)) ??
|
||||
null;
|
||||
const getSourceItem = () =>
|
||||
getNavItemById(source?.id != null ? String(source.id) : undefined);
|
||||
|
||||
if (
|
||||
resolved !== null &&
|
||||
source !== null &&
|
||||
target !== null &&
|
||||
isSortable(source) &&
|
||||
isSortable(target)
|
||||
) {
|
||||
setActiveDropTargetId(resolved.effectiveDropTargetId);
|
||||
if (isAddToNavDrag) {
|
||||
setForbiddenDropTargetId(null);
|
||||
} else {
|
||||
const destFolderId =
|
||||
'group' in target
|
||||
? validateAndExtractWorkspaceFolderId(String(target.group))
|
||||
: validateAndExtractWorkspaceFolderId(
|
||||
resolved.destination.droppableId,
|
||||
);
|
||||
const folderDrag = isFolderDrag(getPayload(), getSourceItem());
|
||||
const isFolderOverFolder = resolved.isTargetFolder && folderDrag;
|
||||
const isFolderOverFolderInList =
|
||||
!resolved.isTargetFolder && isDefined(destFolderId) && folderDrag;
|
||||
setForbiddenDropTargetId(
|
||||
isFolderOverFolder
|
||||
? resolved.effectiveDropTargetId
|
||||
: isFolderOverFolderInList
|
||||
? resolved.dropTargetId
|
||||
: null,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolved !== null && sourceIsSortable) {
|
||||
setActiveDropTargetId(resolved.effectiveDropTargetId);
|
||||
setAddToNavigationFallbackDestination(resolved.destination);
|
||||
if (!isAddToNavDrag) {
|
||||
const destFolderId = validateAndExtractWorkspaceFolderId(
|
||||
resolved.destination.droppableId,
|
||||
);
|
||||
const folderDrag = isFolderDrag(null, getSourceItem());
|
||||
setForbiddenDropTargetId(
|
||||
isDefined(destFolderId) && folderDrag
|
||||
? resolved.effectiveDropTargetId
|
||||
: null,
|
||||
);
|
||||
} else {
|
||||
setForbiddenDropTargetId(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAddToNavDrag) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolved !== null) {
|
||||
setAddToNavigationFallbackDestination(resolved.destination);
|
||||
setActiveDropTargetId(resolved.effectiveDropTargetId);
|
||||
const folderId = validateAndExtractWorkspaceFolderId(
|
||||
resolved.destination.droppableId,
|
||||
);
|
||||
const folderDrag =
|
||||
getPayload()?.type === 'folder' && isDefined(folderId);
|
||||
setForbiddenDropTargetId(
|
||||
folderDrag ? resolved.effectiveDropTargetId : null,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const fallback = addToNavigationFallbackDestination;
|
||||
setActiveDropTargetId(
|
||||
fallback
|
||||
? getDndKitDropTargetId(fallback.droppableId, fallback.index)
|
||||
: null,
|
||||
);
|
||||
setForbiddenDropTargetId(null);
|
||||
},
|
||||
[
|
||||
sourceDroppableId,
|
||||
addToNavigationFallbackDestination,
|
||||
getNavItemById,
|
||||
setActiveDropTargetId,
|
||||
setForbiddenDropTargetId,
|
||||
setAddToNavigationFallbackDestination,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDragEnd = (event: DragEndPayload) => {
|
||||
const { operation } = event;
|
||||
const source = operation.source;
|
||||
const target = operation.target;
|
||||
const draggableId = String(source?.id);
|
||||
const data = source?.data;
|
||||
const sourceId = data?.sourceDroppableId ?? null;
|
||||
const fallback = addToNavigationFallbackDestination;
|
||||
|
||||
setIsDragging(false);
|
||||
setSourceDroppableId(null);
|
||||
setActiveDropTargetId(null);
|
||||
setForbiddenDropTargetId(null);
|
||||
setAddToNavigationFallbackDestination(null);
|
||||
|
||||
const sourceIsSortable = source !== null && isSortable(source);
|
||||
const targetIsSortable = target !== null && isSortable(target);
|
||||
const sortableToSortable =
|
||||
sourceIsSortable &&
|
||||
targetIsSortable &&
|
||||
isDefined(source) &&
|
||||
isDefined(target);
|
||||
const resolved = resolveDropTarget(target, getNavItemById);
|
||||
|
||||
if (sortableToSortable && resolved !== null) {
|
||||
const sourceDraggable = 'initialGroup' in source ? source : null;
|
||||
const initialGroup = sourceDraggable?.initialGroup ?? '';
|
||||
const initialIndex = sourceDraggable?.initialIndex ?? 0;
|
||||
const initialGroupStr = String(initialGroup);
|
||||
const destGroup = String(target.group ?? '');
|
||||
const bothWorkspace =
|
||||
isWorkspaceDroppableId(initialGroupStr) &&
|
||||
isWorkspaceDroppableId(destGroup);
|
||||
if (bothWorkspace) {
|
||||
applyWorkspaceReorderIfAllowed(
|
||||
draggableId,
|
||||
{ droppableId: initialGroupStr, index: initialIndex },
|
||||
resolved.destination,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let destination: DropDestination | null = resolved?.destination ?? null;
|
||||
if (
|
||||
destination == null &&
|
||||
isDefined(fallback) &&
|
||||
isWorkspaceDroppableId(fallback.droppableId)
|
||||
) {
|
||||
destination = fallback;
|
||||
}
|
||||
|
||||
const result = toDropResult(draggableId, data, destination);
|
||||
const provided: ResponderProvided = { announce: () => {} };
|
||||
const dropResult = { ...result, ...DROP_RESULT_OPTIONS };
|
||||
|
||||
if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
handleAddToNavigationDrop(dropResult, provided);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(sourceId) &&
|
||||
isWorkspaceDroppableId(sourceId) &&
|
||||
isDefined(destination) &&
|
||||
isWorkspaceDroppableId(destination.droppableId)
|
||||
) {
|
||||
applyWorkspaceReorderIfAllowed(
|
||||
draggableId,
|
||||
{
|
||||
droppableId: data?.sourceDroppableId ?? '',
|
||||
index: data?.sourceIndex ?? 0,
|
||||
},
|
||||
destination,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const contextValues: WorkspaceDndKitContextValues = {
|
||||
dragSource: { sourceDroppableId },
|
||||
drag: { isDragging },
|
||||
dropTarget: {
|
||||
activeDropTargetId,
|
||||
setActiveDropTargetId,
|
||||
forbiddenDropTargetId,
|
||||
setForbiddenDropTargetId,
|
||||
addToNavigationFallbackDestination,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
contextValues,
|
||||
handlers: {
|
||||
onDragStart: handleDragStart,
|
||||
onDragOver: handleDragOver,
|
||||
onDragEnd: handleDragEnd,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
let preloadScheduled = false;
|
||||
|
||||
const preload = () => {
|
||||
void import('@/navigation/components/WorkspaceDndKitProvider');
|
||||
void import(
|
||||
'@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListDndKit'
|
||||
);
|
||||
};
|
||||
|
||||
export const preloadWorkspaceDndKit = (): void => {
|
||||
if (preloadScheduled) {
|
||||
return;
|
||||
}
|
||||
preloadScheduled = true;
|
||||
preload();
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export type DraggableData = {
|
||||
sourceDroppableId?: string;
|
||||
sourceIndex?: number;
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export type DropDestination = { droppableId: string; index: number };
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export type DroppableData = {
|
||||
droppableId: string;
|
||||
index: number;
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination';
|
||||
|
||||
export type SortableTargetDestination = {
|
||||
destination: DropDestination;
|
||||
effectiveDropTargetId: string;
|
||||
isTargetFolder: boolean;
|
||||
dropTargetId: string;
|
||||
};
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
|
||||
import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination';
|
||||
import type { SortableTargetDestination } from '@/navigation/types/workspaceDndKitSortableTargetDestination';
|
||||
|
||||
type GetNavItemById = (
|
||||
id: string | undefined,
|
||||
) => NavigationMenuItem | undefined;
|
||||
|
||||
export const getDestinationFromSortableTarget = (
|
||||
target: { id: unknown; group?: unknown; index?: unknown },
|
||||
getNavItemById: GetNavItemById,
|
||||
): SortableTargetDestination | null => {
|
||||
const group = target.group;
|
||||
const rawIndex = target.index;
|
||||
if (!isDefined(group) || !isDefined(rawIndex)) {
|
||||
return null;
|
||||
}
|
||||
const index = Number(rawIndex);
|
||||
if (!Number.isInteger(index) || index < 0) {
|
||||
return null;
|
||||
}
|
||||
const destDroppableId = String(group);
|
||||
const targetItem = getNavItemById(
|
||||
target.id != null ? String(target.id) : undefined,
|
||||
);
|
||||
const isTargetFolder =
|
||||
isDefined(targetItem) && isNavigationMenuItemFolder(targetItem);
|
||||
const dropTargetId = getDndKitDropTargetId(destDroppableId, index);
|
||||
const effectiveDropTargetId = isTargetFolder
|
||||
? getDndKitDropTargetId(
|
||||
`${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${target.id}`,
|
||||
0,
|
||||
)
|
||||
: dropTargetId;
|
||||
const destination: DropDestination = {
|
||||
droppableId: isTargetFolder
|
||||
? `${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${target.id}`
|
||||
: destDroppableId,
|
||||
index: isTargetFolder ? 0 : index,
|
||||
};
|
||||
return {
|
||||
destination,
|
||||
effectiveDropTargetId,
|
||||
isTargetFolder,
|
||||
dropTargetId,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
|
||||
type AddToNavPayload = { type?: string } | null;
|
||||
|
||||
export const isFolderDrag = (
|
||||
payload: AddToNavPayload,
|
||||
sourceItem: NavigationMenuItem | undefined,
|
||||
): boolean =>
|
||||
payload?.type === 'folder' ||
|
||||
(isDefined(sourceItem) && isNavigationMenuItemFolder(sourceItem));
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
|
||||
|
||||
import type { DroppableData } from '@/navigation/types/workspaceDndKitDroppableData';
|
||||
import type { SortableTargetDestination } from '@/navigation/types/workspaceDndKitSortableTargetDestination';
|
||||
import { getDestinationFromSortableTarget } from '@/navigation/utils/workspaceDndKitGetDestinationFromSortableTarget';
|
||||
|
||||
type GetNavItemById = (
|
||||
id: string | undefined,
|
||||
) => NavigationMenuItem | undefined;
|
||||
|
||||
const isDroppableData = (data: unknown): data is DroppableData =>
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
typeof (data as DroppableData).droppableId === 'string' &&
|
||||
typeof (data as DroppableData).index === 'number';
|
||||
|
||||
export const resolveDropTarget = (
|
||||
target: {
|
||||
id?: unknown;
|
||||
group?: unknown;
|
||||
index?: unknown;
|
||||
data?: unknown;
|
||||
} | null,
|
||||
getNavItemById: GetNavItemById,
|
||||
): SortableTargetDestination | null => {
|
||||
if (target === null || target === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (isDefined(target.group) && isDefined(target.index)) {
|
||||
return getDestinationFromSortableTarget(
|
||||
{ id: target.id, group: target.group, index: target.index },
|
||||
getNavItemById,
|
||||
);
|
||||
}
|
||||
if (isDroppableData(target.data)) {
|
||||
const { droppableId, index } = target.data;
|
||||
if (isWorkspaceDroppableId(droppableId)) {
|
||||
return {
|
||||
destination: { droppableId, index },
|
||||
effectiveDropTargetId: String(target.id),
|
||||
isTargetFolder: false,
|
||||
dropTargetId: String(target.id),
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination';
|
||||
import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData';
|
||||
|
||||
export const toDropResult = (
|
||||
draggableId: string,
|
||||
data: DraggableData | undefined,
|
||||
destination: DropDestination | null,
|
||||
): {
|
||||
source: DropDestination;
|
||||
destination: DropDestination | null;
|
||||
draggableId: string;
|
||||
} => {
|
||||
const sourceDroppableId = data?.sourceDroppableId ?? '';
|
||||
const sourceIndex = data?.sourceIndex ?? 0;
|
||||
return {
|
||||
source: { droppableId: sourceDroppableId, index: sourceIndex },
|
||||
destination,
|
||||
draggableId,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export type EditModeProps = {
|
||||
isSelectedInEditMode: boolean;
|
||||
onEditModeClick?: () => void;
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@ import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/
|
|||
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useObjectNavItemColor } from '@/navigation-menu-item/hooks/useObjectNavItemColor';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
|
||||
|
|
@ -28,7 +29,6 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
|||
export type NavigationDrawerItemForObjectMetadataItemProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
navigationMenuItem?: ProcessedNavigationMenuItem;
|
||||
isEditMode?: boolean;
|
||||
isSelectedInEditMode?: boolean;
|
||||
onEditModeClick?: () => void;
|
||||
onActiveItemClickWhenNotInEditMode?: () => void;
|
||||
|
|
@ -38,12 +38,14 @@ export type NavigationDrawerItemForObjectMetadataItemProps = {
|
|||
export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
objectMetadataItem,
|
||||
navigationMenuItem,
|
||||
isEditMode = false,
|
||||
isSelectedInEditMode = false,
|
||||
onEditModeClick,
|
||||
onActiveItemClickWhenNotInEditMode,
|
||||
onActiveItemClickWhenNotInEditMode: _onActiveItemClickWhenNotInEditMode,
|
||||
isDragging = false,
|
||||
}: NavigationDrawerItemForObjectMetadataItemProps) => {
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
|
@ -99,18 +101,9 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
|||
}) + '/',
|
||||
);
|
||||
|
||||
const shouldUseClickHandler = isEditMode
|
||||
? Boolean(onEditModeClick)
|
||||
: isActive && Boolean(onActiveItemClickWhenNotInEditMode);
|
||||
const handleClick = isNavigationMenuInEditMode ? onEditModeClick : undefined;
|
||||
|
||||
const handleClick = shouldUseClickHandler
|
||||
? isEditMode
|
||||
? onEditModeClick
|
||||
: onActiveItemClickWhenNotInEditMode
|
||||
: undefined;
|
||||
|
||||
const shouldNavigate =
|
||||
!isEditMode && !(isActive && onActiveItemClickWhenNotInEditMode);
|
||||
const shouldNavigate = !isNavigationMenuInEditMode;
|
||||
|
||||
const isViewWithCustomName =
|
||||
isView &&
|
||||
|
|
@ -218,7 +211,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
|||
label={label}
|
||||
secondaryLabel={secondaryLabel}
|
||||
to={
|
||||
isEditMode || isDragging
|
||||
isNavigationMenuInEditMode || isDragging
|
||||
? undefined
|
||||
: shouldNavigate
|
||||
? navigationPath
|
||||
|
|
@ -230,7 +223,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
|||
active={isActive}
|
||||
isSelectedInEditMode={isSelectedInEditMode}
|
||||
isDragging={isDragging}
|
||||
triggerEvent={isEditMode ? 'CLICK' : undefined}
|
||||
triggerEvent={isNavigationMenuInEditMode ? 'CLICK' : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import { useParams } from 'react-router-dom';
|
|||
import { useWorkspaceFavorites } from '@/favorites/hooks/useWorkspaceFavorites';
|
||||
import { useWorkspaceNavigationMenuItems } from '@/navigation-menu-item/hooks/useWorkspaceNavigationMenuItems';
|
||||
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
|
||||
import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
||||
import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState';
|
||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
|
@ -24,14 +26,22 @@ export const NavigationDrawerOpenedSection = () => {
|
|||
const filteredActiveNonSystemObjectMetadataItems =
|
||||
activeObjectMetadataItems.filter((item) => !item.isRemote);
|
||||
|
||||
const loading = useIsPrefetchLoading();
|
||||
const isPrefetchLoading = useIsPrefetchLoading();
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
const prefetchIsLoaded = useAtomFamilyStateValue(
|
||||
prefetchIsLoadedFamilyState,
|
||||
PrefetchKey.AllNavigationMenuItems,
|
||||
);
|
||||
|
||||
const loading =
|
||||
isPrefetchLoading ||
|
||||
(isNavigationMenuItemEditingEnabled && !prefetchIsLoaded);
|
||||
|
||||
const { workspaceFavoritesObjectMetadataItems } = useWorkspaceFavorites();
|
||||
const { workspaceNavigationMenuItemsObjectMetadataItems } =
|
||||
useWorkspaceNavigationMenuItems();
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
const {
|
||||
objectNamePlural: currentObjectNamePlural,
|
||||
|
|
@ -67,7 +77,7 @@ export const NavigationDrawerOpenedSection = () => {
|
|||
.includes(objectMetadataItem.id);
|
||||
|
||||
if (loading) {
|
||||
return <NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader />;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ type NavigationDrawerSectionForObjectMetadataItemsProps = {
|
|||
isRemote: boolean;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
rightIcon?: React.ReactNode;
|
||||
isEditMode?: boolean;
|
||||
selectedObjectMetadataItemId?: string | null;
|
||||
onObjectMetadataItemClick?: (objectMetadataItem: ObjectMetadataItem) => void;
|
||||
onActiveObjectMetadataItemClick?: (
|
||||
|
|
@ -41,7 +40,6 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
|
|||
isRemote,
|
||||
objectMetadataItems,
|
||||
rightIcon,
|
||||
isEditMode = false,
|
||||
selectedObjectMetadataItemId = null,
|
||||
onObjectMetadataItemClick,
|
||||
onActiveObjectMetadataItemClick,
|
||||
|
|
@ -119,6 +117,7 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
|
|||
label={sectionTitle}
|
||||
onClick={() => toggleNavigationSection()}
|
||||
rightIcon={rightIcon}
|
||||
isOpen={isNavigationSectionOpen}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
{isNavigationSectionOpen &&
|
||||
|
|
@ -127,7 +126,6 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
|
|||
<NavigationDrawerItemForObjectMetadataItem
|
||||
key={`navigation-drawer-item-${objectMetadataItem.id}`}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
isEditMode={isEditMode}
|
||||
isSelectedInEditMode={
|
||||
selectedObjectMetadataItemId === objectMetadataItem.id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationDrawerSectionForWorkspaceItemFolderContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemFolderContent';
|
||||
import { NavigationDrawerSectionForWorkspaceItemLinkContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemLinkContent';
|
||||
import { NavigationDrawerSectionForWorkspaceItemObjectContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemObjectContent';
|
||||
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
|
||||
|
||||
type NavigationDrawerSectionForWorkspaceItemContentProps =
|
||||
WorkspaceSectionItemContentProps;
|
||||
|
||||
export const NavigationDrawerSectionForWorkspaceItemContent = ({
|
||||
item,
|
||||
editModeProps,
|
||||
isDragging,
|
||||
folderChildrenById,
|
||||
folderCount,
|
||||
selectedNavigationMenuItemId,
|
||||
onNavigationMenuItemClick,
|
||||
onActiveObjectMetadataItemClick,
|
||||
readOnly,
|
||||
}: NavigationDrawerSectionForWorkspaceItemContentProps) => {
|
||||
switch (item.itemType) {
|
||||
case NavigationMenuItemType.FOLDER:
|
||||
return (
|
||||
<NavigationDrawerSectionForWorkspaceItemFolderContent
|
||||
item={item}
|
||||
editModeProps={editModeProps}
|
||||
isDragging={isDragging}
|
||||
folderChildrenById={folderChildrenById}
|
||||
folderCount={folderCount}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
case NavigationMenuItemType.LINK:
|
||||
return (
|
||||
<NavigationDrawerSectionForWorkspaceItemLinkContent
|
||||
item={item}
|
||||
editModeProps={editModeProps}
|
||||
isDragging={isDragging}
|
||||
folderChildrenById={folderChildrenById}
|
||||
folderCount={folderCount}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<NavigationDrawerSectionForWorkspaceItemObjectContent
|
||||
item={item}
|
||||
editModeProps={editModeProps}
|
||||
isDragging={isDragging}
|
||||
folderChildrenById={folderChildrenById}
|
||||
folderCount={folderCount}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { lazy, Suspense } from 'react';
|
||||
|
||||
import { WorkspaceFolderReadOnly } from '@/object-metadata/components/WorkspaceFolderReadOnly';
|
||||
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
|
||||
|
||||
const LazyWorkspaceNavigationMenuItemsFolder = lazy(() =>
|
||||
import(
|
||||
'@/navigation-menu-item/components/WorkspaceNavigationMenuItemsFolder'
|
||||
).then((m) => ({ default: m.WorkspaceNavigationMenuItemsFolder })),
|
||||
);
|
||||
|
||||
type NavigationDrawerSectionForWorkspaceItemFolderContentProps =
|
||||
WorkspaceSectionItemContentProps;
|
||||
|
||||
export const NavigationDrawerSectionForWorkspaceItemFolderContent = ({
|
||||
item,
|
||||
editModeProps,
|
||||
isDragging,
|
||||
folderChildrenById,
|
||||
folderCount,
|
||||
selectedNavigationMenuItemId,
|
||||
onNavigationMenuItemClick,
|
||||
readOnly = false,
|
||||
}: NavigationDrawerSectionForWorkspaceItemFolderContentProps) => {
|
||||
const folderId = item.id;
|
||||
const folderName = item.name ?? 'Folder';
|
||||
const folderIconKey = item.Icon;
|
||||
const folderColor = 'color' in item ? item.color : undefined;
|
||||
const navigationMenuItems = folderChildrenById.get(item.id) ?? [];
|
||||
const isGroup = folderCount > 1;
|
||||
|
||||
if (readOnly) {
|
||||
return (
|
||||
<WorkspaceFolderReadOnly
|
||||
folderId={folderId}
|
||||
folderName={folderName}
|
||||
folderIconKey={folderIconKey}
|
||||
folderColor={folderColor}
|
||||
navigationMenuItems={navigationMenuItems}
|
||||
isGroup={isGroup}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<WorkspaceFolderReadOnly
|
||||
folderId={folderId}
|
||||
folderName={folderName}
|
||||
folderIconKey={folderIconKey}
|
||||
folderColor={folderColor}
|
||||
navigationMenuItems={navigationMenuItems}
|
||||
isGroup={isGroup}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<LazyWorkspaceNavigationMenuItemsFolder
|
||||
folderId={folderId}
|
||||
folderName={folderName}
|
||||
folderIconKey={folderIconKey}
|
||||
folderColor={folderColor}
|
||||
navigationMenuItems={navigationMenuItems}
|
||||
isGroup={isGroup}
|
||||
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
|
||||
onEditModeClick={editModeProps.onEditModeClick}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
|
||||
isDragging={isDragging}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { IconArrowUpRight } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type NavigationDrawerSectionForWorkspaceItemLinkContentProps =
|
||||
WorkspaceSectionItemContentProps;
|
||||
|
||||
export const NavigationDrawerSectionForWorkspaceItemLinkContent = ({
|
||||
item,
|
||||
editModeProps,
|
||||
isDragging,
|
||||
}: NavigationDrawerSectionForWorkspaceItemLinkContentProps) => {
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const linkItem = item as ProcessedNavigationMenuItem;
|
||||
return (
|
||||
<NavigationDrawerItem
|
||||
label={linkItem.labelIdentifier}
|
||||
to={isNavigationMenuInEditMode || isDragging ? undefined : linkItem.link}
|
||||
onClick={
|
||||
isNavigationMenuInEditMode ? editModeProps.onEditModeClick : undefined
|
||||
}
|
||||
Icon={() => <NavigationMenuItemIcon navigationMenuItem={linkItem} />}
|
||||
active={false}
|
||||
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
rightOptions={
|
||||
!isNavigationMenuInEditMode && (
|
||||
<IconArrowUpRight
|
||||
size={themeCssVariables.icon.size.sm}
|
||||
stroke={themeCssVariables.icon.stroke.md}
|
||||
color={themeCssVariables.font.color.light}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem';
|
||||
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
|
||||
type NavigationDrawerSectionForWorkspaceItemObjectContentProps =
|
||||
WorkspaceSectionItemContentProps;
|
||||
|
||||
export const NavigationDrawerSectionForWorkspaceItemObjectContent = ({
|
||||
item,
|
||||
editModeProps,
|
||||
isDragging,
|
||||
onActiveObjectMetadataItemClick,
|
||||
}: NavigationDrawerSectionForWorkspaceItemObjectContentProps) => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const coreViews = useAtomStateValue(coreViewsState);
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
|
||||
item as ProcessedNavigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
if (!objectMetadataItem) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<NavigationDrawerItemForObjectMetadataItem
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
navigationMenuItem={item as ProcessedNavigationMenuItem}
|
||||
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
|
||||
onEditModeClick={editModeProps.onEditModeClick}
|
||||
isDragging={isDragging}
|
||||
onActiveItemClickWhenNotInEditMode={
|
||||
onActiveObjectMetadataItemClick
|
||||
? () => onActiveObjectMetadataItemClick(objectMetadataItem, item.id)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,33 +1,25 @@
|
|||
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
|
||||
import { styled } from '@linaria/react';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext } from 'react';
|
||||
import React, { lazy, Suspense, useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconLink, IconPlus } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
import { NavigationItemDropTarget } from '@/navigation-menu-item/components/NavigationItemDropTarget';
|
||||
import { WorkspaceNavigationMenuItemsFolder } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemsFolder';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
|
||||
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
|
||||
import {
|
||||
type FlatWorkspaceItem,
|
||||
type NavigationMenuItemClickParams,
|
||||
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem';
|
||||
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
|
||||
import { NavigationDrawerSectionForWorkspaceItemsListReadOnly } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly';
|
||||
import { WorkspaceSectionListEditModeFallback } from '@/object-metadata/components/WorkspaceSectionListEditModeFallback';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
||||
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
|
||||
|
|
@ -35,18 +27,16 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
|
|||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
|
||||
const StyledWorkspaceDroppableList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.betweenSiblingsGap};
|
||||
`;
|
||||
const LazyWorkspaceSectionListDndKit = lazy(() =>
|
||||
import(
|
||||
'@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListDndKit'
|
||||
).then((m) => ({ default: m.WorkspaceSectionListDndKit })),
|
||||
);
|
||||
|
||||
type NavigationDrawerSectionForWorkspaceItemsProps = {
|
||||
sectionTitle: string;
|
||||
items: FlatWorkspaceItem[];
|
||||
rightIcon?: React.ReactNode;
|
||||
onAddMenuItem?: () => void;
|
||||
isEditMode?: boolean;
|
||||
selectedNavigationMenuItemId?: string | null;
|
||||
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
|
||||
onActiveObjectMetadataItemClick?: (
|
||||
|
|
@ -59,14 +49,13 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
|
|||
sectionTitle,
|
||||
items,
|
||||
rightIcon,
|
||||
onAddMenuItem,
|
||||
isEditMode = false,
|
||||
selectedNavigationMenuItemId = null,
|
||||
onNavigationMenuItemClick,
|
||||
onActiveObjectMetadataItemClick,
|
||||
}: NavigationDrawerSectionForWorkspaceItemsProps) => {
|
||||
const { t } = useLingui();
|
||||
const workspaceDropDisabled = useIsDropDisabledForSection(true);
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const { toggleNavigationSection, isNavigationSectionOpen } =
|
||||
useNavigationSection('Workspace');
|
||||
const coreViews = useAtomStateValue(coreViewsState);
|
||||
|
|
@ -74,7 +63,6 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
|
|||
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const { isDragging } = useContext(NavigationMenuItemDragContext);
|
||||
const { addToNavigationFallbackDestination } = useContext(
|
||||
NavigationDropTargetContext,
|
||||
);
|
||||
|
|
@ -95,10 +83,6 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
|
|||
return acc;
|
||||
}, new Map());
|
||||
|
||||
const folderCount = flatItems.filter(
|
||||
(item) => item.itemType === NavigationMenuItemType.FOLDER,
|
||||
).length;
|
||||
|
||||
const filteredItems = flatItems.filter((item) => {
|
||||
const type = item.itemType;
|
||||
if (
|
||||
|
|
@ -127,7 +111,7 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
|
|||
return false;
|
||||
});
|
||||
|
||||
const getEditModeProps = (item: FlatWorkspaceItem) => {
|
||||
const getEditModeProps = (item: FlatWorkspaceItem): EditModeProps => {
|
||||
const itemId = item.id;
|
||||
return {
|
||||
isSelectedInEditMode: selectedNavigationMenuItemId === itemId,
|
||||
|
|
@ -151,9 +135,6 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
|
|||
};
|
||||
};
|
||||
|
||||
const isAddMenuItemButtonVisible =
|
||||
isEditMode && isDefined(onAddMenuItem) && !isDragging;
|
||||
|
||||
if (flatItems.length === 0 && !isAddToNavigationDropTargetVisible) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -165,193 +146,48 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
|
|||
label={sectionTitle}
|
||||
onClick={() => toggleNavigationSection()}
|
||||
rightIcon={rightIcon}
|
||||
alwaysShowRightIcon={isEditMode}
|
||||
alwaysShowRightIcon={isNavigationMenuInEditMode}
|
||||
isOpen={isNavigationSectionOpen}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
{(isNavigationSectionOpen || isAddToNavigationDropTargetVisible) && (
|
||||
<Droppable
|
||||
droppableId={
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
|
||||
}
|
||||
isDropDisabled={workspaceDropDisabled}
|
||||
>
|
||||
{(provided) => (
|
||||
<StyledWorkspaceDroppableList
|
||||
ref={provided.innerRef}
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{filteredItems.map((item, index) => {
|
||||
const type = item.itemType;
|
||||
const editModeProps = getEditModeProps(item);
|
||||
|
||||
if (type === 'folder') {
|
||||
return (
|
||||
<NavigationItemDropTarget
|
||||
key={item.id}
|
||||
folderId={null}
|
||||
index={index}
|
||||
sectionId={NavigationSections.WORKSPACE}
|
||||
>
|
||||
<DraggableItem
|
||||
draggableId={item.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer
|
||||
isDragDisabled={!isEditMode}
|
||||
disableInteractiveElementBlocking={isEditMode}
|
||||
itemComponent={
|
||||
<WorkspaceNavigationMenuItemsFolder
|
||||
folderId={item.id}
|
||||
folderName={item.name ?? 'Folder'}
|
||||
folderIconKey={item.Icon}
|
||||
folderColor={
|
||||
'color' in item ? item.color : undefined
|
||||
}
|
||||
navigationMenuItems={
|
||||
folderChildrenById.get(item.id) ?? []
|
||||
}
|
||||
isGroup={folderCount > 1}
|
||||
isEditMode={isEditMode}
|
||||
isSelectedInEditMode={
|
||||
editModeProps.isSelectedInEditMode
|
||||
}
|
||||
onEditModeClick={editModeProps.onEditModeClick}
|
||||
onNavigationMenuItemClick={
|
||||
onNavigationMenuItemClick
|
||||
}
|
||||
selectedNavigationMenuItemId={
|
||||
selectedNavigationMenuItemId
|
||||
}
|
||||
isDragging={isDragging}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</NavigationItemDropTarget>
|
||||
);
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={
|
||||
isNavigationSectionOpen || isAddToNavigationDropTargetVisible
|
||||
}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={false}
|
||||
>
|
||||
{isNavigationMenuInEditMode ? (
|
||||
<Suspense
|
||||
fallback={
|
||||
<WorkspaceSectionListEditModeFallback
|
||||
filteredItems={filteredItems}
|
||||
folderChildrenById={folderChildrenById}
|
||||
onActiveObjectMetadataItemClick={
|
||||
onActiveObjectMetadataItemClick
|
||||
}
|
||||
|
||||
if (type === 'link') {
|
||||
const linkItem = item as ProcessedNavigationMenuItem;
|
||||
return (
|
||||
<NavigationItemDropTarget
|
||||
key={item.id}
|
||||
folderId={null}
|
||||
index={index}
|
||||
sectionId={NavigationSections.WORKSPACE}
|
||||
>
|
||||
<DraggableItem
|
||||
draggableId={item.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer
|
||||
isDragDisabled={!isEditMode}
|
||||
disableInteractiveElementBlocking={isEditMode}
|
||||
itemComponent={
|
||||
<NavigationDrawerItem
|
||||
label={linkItem.labelIdentifier}
|
||||
to={
|
||||
isEditMode || isDragging
|
||||
? undefined
|
||||
: linkItem.link
|
||||
}
|
||||
onClick={
|
||||
isEditMode
|
||||
? editModeProps.onEditModeClick
|
||||
: undefined
|
||||
}
|
||||
Icon={IconLink}
|
||||
iconColor={linkItem.color}
|
||||
active={false}
|
||||
isSelectedInEditMode={
|
||||
editModeProps.isSelectedInEditMode
|
||||
}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</NavigationItemDropTarget>
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadataItem =
|
||||
getObjectMetadataForNavigationMenuItem(
|
||||
item as ProcessedNavigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
if (!objectMetadataItem) return null;
|
||||
|
||||
return (
|
||||
<NavigationItemDropTarget
|
||||
key={item.id}
|
||||
folderId={null}
|
||||
index={index}
|
||||
sectionId={NavigationSections.WORKSPACE}
|
||||
>
|
||||
<DraggableItem
|
||||
draggableId={item.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer
|
||||
isDragDisabled={!isEditMode}
|
||||
disableInteractiveElementBlocking={isEditMode}
|
||||
itemComponent={
|
||||
<NavigationDrawerItemForObjectMetadataItem
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
navigationMenuItem={
|
||||
item as ProcessedNavigationMenuItem
|
||||
}
|
||||
isEditMode={isEditMode}
|
||||
isSelectedInEditMode={
|
||||
editModeProps.isSelectedInEditMode
|
||||
}
|
||||
onEditModeClick={editModeProps.onEditModeClick}
|
||||
isDragging={isDragging}
|
||||
onActiveItemClickWhenNotInEditMode={
|
||||
onActiveObjectMetadataItemClick
|
||||
? () =>
|
||||
onActiveObjectMetadataItemClick(
|
||||
objectMetadataItem,
|
||||
item.id,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</NavigationItemDropTarget>
|
||||
);
|
||||
})}
|
||||
<NavigationItemDropTarget
|
||||
folderId={null}
|
||||
index={filteredItems.length}
|
||||
sectionId={NavigationSections.WORKSPACE}
|
||||
compact={!isAddMenuItemButtonVisible}
|
||||
>
|
||||
{isAddMenuItemButtonVisible && (
|
||||
<NavigationDrawerItem
|
||||
Icon={IconPlus}
|
||||
label={t`Add menu item`}
|
||||
onClick={onAddMenuItem}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
)}
|
||||
</NavigationItemDropTarget>
|
||||
{addToNavigationFallbackDestination?.droppableId ===
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS &&
|
||||
addToNavigationFallbackDestination.index >
|
||||
filteredItems.length && (
|
||||
<NavigationItemDropTarget
|
||||
folderId={null}
|
||||
index={addToNavigationFallbackDestination.index}
|
||||
sectionId={NavigationSections.WORKSPACE}
|
||||
compact
|
||||
/>
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</StyledWorkspaceDroppableList>
|
||||
)}
|
||||
</Droppable>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<LazyWorkspaceSectionListDndKit
|
||||
filteredItems={filteredItems}
|
||||
getEditModeProps={getEditModeProps}
|
||||
folderChildrenById={folderChildrenById}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
/>
|
||||
</Suspense>
|
||||
) : (
|
||||
<NavigationDrawerSectionForWorkspaceItemsListReadOnly
|
||||
filteredItems={filteredItems}
|
||||
folderChildrenById={folderChildrenById}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
/>
|
||||
)}
|
||||
</AnimatedExpandableContainer>
|
||||
</NavigationDrawerSection>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
import { WorkspaceDndKitDroppableSlot } from '@/navigation-menu-item/components/WorkspaceDndKitDroppableSlot';
|
||||
import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/components/WorkspaceDndKitSortableItem';
|
||||
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';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { NavigationDrawerSectionForWorkspaceItemContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent';
|
||||
import { WorkspaceOrphanDropTarget } from '@/object-metadata/components/WorkspaceOrphanDropTarget';
|
||||
import { WorkspaceSectionAddMenuItemButton } from '@/object-metadata/components/WorkspaceSectionAddMenuItemButton';
|
||||
import type { WorkspaceSectionListDndKitProps } from '@/object-metadata/components/WorkspaceSectionListDndKitProps';
|
||||
|
||||
const StyledList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.betweenSiblingsGap};
|
||||
`;
|
||||
|
||||
const StyledListItemRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
`;
|
||||
|
||||
export const WorkspaceSectionListDndKit = ({
|
||||
filteredItems,
|
||||
getEditModeProps,
|
||||
folderChildrenById,
|
||||
selectedNavigationMenuItemId,
|
||||
onNavigationMenuItemClick,
|
||||
onActiveObjectMetadataItemClick,
|
||||
}: WorkspaceSectionListDndKitProps) => {
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const workspaceDropDisabled = useIsDropDisabledForSection(true);
|
||||
const { isDragging } = useContext(NavigationMenuItemDragContext);
|
||||
const { addToNavigationFallbackDestination } = useContext(
|
||||
NavigationDropTargetContext,
|
||||
);
|
||||
const folderCount = filteredItems.filter(
|
||||
(item) => item.itemType === NavigationMenuItemType.FOLDER,
|
||||
).length;
|
||||
const isAddMenuItemButtonVisible = isNavigationMenuInEditMode;
|
||||
return (
|
||||
<StyledList>
|
||||
{filteredItems.map((item, index) => (
|
||||
<StyledListItemRow key={item.id}>
|
||||
<WorkspaceOrphanDropTarget index={index} compact />
|
||||
<WorkspaceDndKitSortableItem
|
||||
id={item.id}
|
||||
index={index}
|
||||
group={
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
|
||||
}
|
||||
disabled={!isNavigationMenuInEditMode || workspaceDropDisabled}
|
||||
>
|
||||
<NavigationDrawerSectionForWorkspaceItemContent
|
||||
item={item}
|
||||
editModeProps={getEditModeProps(item)}
|
||||
isDragging={isDragging}
|
||||
folderChildrenById={folderChildrenById}
|
||||
folderCount={folderCount}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
/>
|
||||
</WorkspaceDndKitSortableItem>
|
||||
</StyledListItemRow>
|
||||
))}
|
||||
<WorkspaceDndKitDroppableSlot
|
||||
droppableId={
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
|
||||
}
|
||||
index={filteredItems.length}
|
||||
disabled={workspaceDropDisabled}
|
||||
>
|
||||
<WorkspaceOrphanDropTarget
|
||||
index={filteredItems.length}
|
||||
compact={!isAddMenuItemButtonVisible}
|
||||
>
|
||||
{isAddMenuItemButtonVisible && <WorkspaceSectionAddMenuItemButton />}
|
||||
</WorkspaceOrphanDropTarget>
|
||||
</WorkspaceDndKitDroppableSlot>
|
||||
{addToNavigationFallbackDestination?.droppableId ===
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS &&
|
||||
addToNavigationFallbackDestination.index > filteredItems.length && (
|
||||
<WorkspaceDndKitDroppableSlot
|
||||
droppableId={
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
|
||||
}
|
||||
index={addToNavigationFallbackDestination.index}
|
||||
disabled={workspaceDropDisabled}
|
||||
>
|
||||
<WorkspaceOrphanDropTarget
|
||||
index={addToNavigationFallbackDestination.index}
|
||||
compact
|
||||
/>
|
||||
</WorkspaceDndKitDroppableSlot>
|
||||
)}
|
||||
</StyledList>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import type { FlatWorkspaceItem } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import type { WorkspaceSectionListDndKitProps } from '@/object-metadata/components/WorkspaceSectionListDndKitProps';
|
||||
import { NavigationDrawerSectionForWorkspaceItemContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent';
|
||||
|
||||
const StyledList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.betweenSiblingsGap};
|
||||
`;
|
||||
|
||||
type NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps = Pick<
|
||||
WorkspaceSectionListDndKitProps,
|
||||
'filteredItems' | 'folderChildrenById' | 'onActiveObjectMetadataItemClick'
|
||||
>;
|
||||
|
||||
const READ_ONLY_EDIT_MODE_PROPS = {
|
||||
isSelectedInEditMode: false,
|
||||
onEditModeClick: undefined,
|
||||
} as const;
|
||||
|
||||
export const NavigationDrawerSectionForWorkspaceItemsListReadOnly = ({
|
||||
filteredItems,
|
||||
folderChildrenById,
|
||||
onActiveObjectMetadataItemClick,
|
||||
}: NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps) => {
|
||||
const folderCount = filteredItems.filter(
|
||||
(item) => item.itemType === NavigationMenuItemType.FOLDER,
|
||||
).length;
|
||||
|
||||
return (
|
||||
<StyledList>
|
||||
{filteredItems.map((item: FlatWorkspaceItem) => (
|
||||
<NavigationDrawerSectionForWorkspaceItemContent
|
||||
key={item.id}
|
||||
item={item}
|
||||
editModeProps={READ_ONLY_EDIT_MODE_PROPS}
|
||||
isDragging={false}
|
||||
folderChildrenById={folderChildrenById}
|
||||
folderCount={folderCount}
|
||||
selectedNavigationMenuItemId={null}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
readOnly
|
||||
/>
|
||||
))}
|
||||
</StyledList>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||
import { styled } from '@linaria/react';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledTitleSkeleton = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: ${themeCssVariables.spacing[5]};
|
||||
padding-left: ${themeCssVariables.spacing[1]};
|
||||
padding-right: ${themeCssVariables.spacing['0.5']};
|
||||
`;
|
||||
|
||||
const StyledRowsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
padding-left: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
export const NavigationDrawerWorkspaceSectionSkeletonLoader = () => {
|
||||
return (
|
||||
<NavigationDrawerSection>
|
||||
<SkeletonTheme
|
||||
baseColor={themeCssVariables.background.tertiary}
|
||||
highlightColor={themeCssVariables.background.transparent.light}
|
||||
borderRadius={4}
|
||||
>
|
||||
<StyledTitleSkeleton>
|
||||
<Skeleton
|
||||
width={72}
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.xs}
|
||||
/>
|
||||
</StyledTitleSkeleton>
|
||||
<StyledRowsContainer>
|
||||
<Skeleton
|
||||
width={196}
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
||||
/>
|
||||
<Skeleton
|
||||
width={196}
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
||||
/>
|
||||
<Skeleton
|
||||
width={196}
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
||||
/>
|
||||
</StyledRowsContainer>
|
||||
</SkeletonTheme>
|
||||
</NavigationDrawerSection>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import { styled } from '@linaria/react';
|
||||
import { IconChevronDown, IconChevronRight, useIcons } from 'twenty-ui/display';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { WorkspaceNavigationMenuItemFolderSubItem } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderSubItem';
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
|
||||
import { useWorkspaceFolderOpenState } from '@/navigation-menu-item/hooks/useWorkspaceFolderOpenState';
|
||||
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||
|
||||
const StyledFolderContainer = styled.div`
|
||||
border: 1px solid transparent;
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
`;
|
||||
|
||||
const StyledFolderContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledFolderExpandableWrapper = styled.div`
|
||||
& > div {
|
||||
overflow: visible !important;
|
||||
}
|
||||
`;
|
||||
|
||||
type WorkspaceFolderReadOnlyProps = {
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderIconKey?: string | null;
|
||||
folderColor?: string | null;
|
||||
navigationMenuItems: ProcessedNavigationMenuItem[];
|
||||
isGroup: boolean;
|
||||
};
|
||||
|
||||
export const WorkspaceFolderReadOnly = ({
|
||||
folderId,
|
||||
folderName,
|
||||
folderIconKey,
|
||||
folderColor,
|
||||
navigationMenuItems,
|
||||
isGroup,
|
||||
}: WorkspaceFolderReadOnlyProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT);
|
||||
const isMobile = useIsMobile();
|
||||
const { isOpen, handleToggle, selectedNavigationMenuItemIndex } =
|
||||
useWorkspaceFolderOpenState({ folderId, navigationMenuItems });
|
||||
|
||||
const [skipInitialExpandAnimation] = useState(() => isOpen);
|
||||
|
||||
return (
|
||||
<StyledFolderContainer>
|
||||
<NavigationDrawerItemsCollapsableContainer isGroup={isGroup}>
|
||||
<NavigationDrawerItem
|
||||
label={folderName}
|
||||
Icon={FolderIcon}
|
||||
iconColor={
|
||||
isDefined(folderColor)
|
||||
? folderColor
|
||||
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER
|
||||
}
|
||||
active={!isOpen && selectedNavigationMenuItemIndex >= 0}
|
||||
onClick={handleToggle}
|
||||
className="navigation-drawer-item"
|
||||
triggerEvent="CLICK"
|
||||
preventCollapseOnMobile={isMobile}
|
||||
alwaysShowRightOptions
|
||||
rightOptions={
|
||||
isOpen ? (
|
||||
<IconChevronDown
|
||||
size={themeCssVariables.icon.size.sm}
|
||||
stroke={themeCssVariables.icon.stroke.sm}
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
/>
|
||||
) : (
|
||||
<IconChevronRight
|
||||
size={themeCssVariables.icon.size.sm}
|
||||
stroke={themeCssVariables.icon.stroke.sm}
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<StyledFolderExpandableWrapper>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={!skipInitialExpandAnimation}
|
||||
>
|
||||
<StyledFolderContent>
|
||||
{navigationMenuItems.map((navigationMenuItem, index) => (
|
||||
<WorkspaceNavigationMenuItemFolderSubItem
|
||||
key={navigationMenuItem.id}
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
index={index}
|
||||
arrayLength={navigationMenuItems.length}
|
||||
selectedNavigationMenuItemIndex={
|
||||
selectedNavigationMenuItemIndex
|
||||
}
|
||||
onNavigationMenuItemClick={undefined}
|
||||
selectedNavigationMenuItemId={null}
|
||||
isContextDragging={false}
|
||||
/>
|
||||
))}
|
||||
</StyledFolderContent>
|
||||
</AnimatedExpandableContainer>
|
||||
</StyledFolderExpandableWrapper>
|
||||
</NavigationDrawerItemsCollapsableContainer>
|
||||
</StyledFolderContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import type { ReactNode } from 'react';
|
||||
|
||||
import { NavigationItemDropTarget } from '@/navigation-menu-item/components/NavigationItemDropTarget';
|
||||
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId';
|
||||
|
||||
type WorkspaceOrphanDropTargetProps = {
|
||||
index: number;
|
||||
compact?: boolean;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const WorkspaceOrphanDropTarget = ({
|
||||
index,
|
||||
compact = false,
|
||||
children,
|
||||
}: WorkspaceOrphanDropTargetProps) => (
|
||||
<NavigationItemDropTarget
|
||||
folderId={null}
|
||||
index={index}
|
||||
sectionId={NavigationSections.WORKSPACE}
|
||||
compact={compact}
|
||||
dropTargetIdOverride={getDndKitDropTargetId(
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
index,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</NavigationItemDropTarget>
|
||||
);
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { useLingui } from '@lingui/react/macro';
|
||||
import React from 'react';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { IconColumnInsertRight, IconPlus } from 'twenty-ui/display';
|
||||
|
||||
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
export const WorkspaceSectionAddMenuItemButton = () => {
|
||||
const { t } = useLingui();
|
||||
const { navigateSidePanel } = useNavigateSidePanel();
|
||||
const sidePanelPage = useAtomStateValue(sidePanelPageState);
|
||||
const addMenuItemInsertionContext = useAtomStateValue(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
const setAddMenuItemInsertionContext = useSetAtomState(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
|
||||
const setSelectedNavigationMenuItemInEditMode = useSetAtomState(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
|
||||
const handleClick = (event?: React.MouseEvent) => {
|
||||
event?.stopPropagation();
|
||||
setAddMenuItemInsertionContext(null);
|
||||
setSelectedNavigationMenuItemInEditMode(null);
|
||||
navigateSidePanel({
|
||||
page: SidePanelPages.NavigationMenuAddItem,
|
||||
pageTitle: t`New sidebar item`,
|
||||
pageIcon: IconColumnInsertRight,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
};
|
||||
|
||||
const isSelected =
|
||||
sidePanelPage === SidePanelPages.NavigationMenuAddItem &&
|
||||
addMenuItemInsertionContext === null;
|
||||
|
||||
return (
|
||||
<NavigationDrawerItem
|
||||
Icon={IconPlus}
|
||||
label={t`Add menu item`}
|
||||
onClick={handleClick}
|
||||
triggerEvent="CLICK"
|
||||
variant="tertiary"
|
||||
isSelectedInEditMode={isSelected}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import type {
|
||||
FlatWorkspaceItem,
|
||||
NavigationMenuItemClickParams,
|
||||
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
|
||||
|
||||
export type WorkspaceSectionItemContentProps = {
|
||||
item: FlatWorkspaceItem;
|
||||
editModeProps: EditModeProps;
|
||||
isDragging: boolean;
|
||||
folderChildrenById: Map<string, ProcessedNavigationMenuItem[]>;
|
||||
folderCount: number;
|
||||
selectedNavigationMenuItemId: string | null;
|
||||
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
|
||||
onActiveObjectMetadataItemClick?: (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
navigationMenuItemId: string,
|
||||
) => void;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import type {
|
||||
FlatWorkspaceItem,
|
||||
NavigationMenuItemClickParams,
|
||||
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
|
||||
|
||||
export type WorkspaceSectionListDndKitProps = {
|
||||
filteredItems: FlatWorkspaceItem[];
|
||||
getEditModeProps: (item: FlatWorkspaceItem) => EditModeProps;
|
||||
folderChildrenById: Map<string, ProcessedNavigationMenuItem[]>;
|
||||
selectedNavigationMenuItemId: string | null;
|
||||
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
|
||||
onActiveObjectMetadataItemClick?: (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
navigationMenuItemId: string,
|
||||
) => void;
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { styled } from '@linaria/react';
|
||||
|
||||
import { NavigationDrawerSectionForWorkspaceItemsListReadOnly } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly';
|
||||
import { WorkspaceSectionAddMenuItemButton } from '@/object-metadata/components/WorkspaceSectionAddMenuItemButton';
|
||||
import type { WorkspaceSectionListDndKitProps } from '@/object-metadata/components/WorkspaceSectionListDndKitProps';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledFallback = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
type WorkspaceSectionListEditModeFallbackProps = Pick<
|
||||
WorkspaceSectionListDndKitProps,
|
||||
'filteredItems' | 'folderChildrenById' | 'onActiveObjectMetadataItemClick'
|
||||
>;
|
||||
|
||||
export const WorkspaceSectionListEditModeFallback = ({
|
||||
filteredItems,
|
||||
folderChildrenById,
|
||||
onActiveObjectMetadataItemClick,
|
||||
}: WorkspaceSectionListEditModeFallbackProps) => (
|
||||
<StyledFallback>
|
||||
<NavigationDrawerSectionForWorkspaceItemsListReadOnly
|
||||
filteredItems={filteredItems}
|
||||
folderChildrenById={folderChildrenById}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
/>
|
||||
<WorkspaceSectionAddMenuItemButton />
|
||||
</StyledFallback>
|
||||
);
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { isRecordReadOnly } from '@/object-record/read-only/utils/isRecordReadOnly';
|
||||
import { useIsRecordDeleted } from '@/object-record/record-field/ui/hooks/useIsRecordDeleted';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
type UseIsRecordReadOnlyParams = {
|
||||
recordId: string;
|
||||
|
|
@ -13,6 +15,10 @@ export const useIsRecordReadOnly = ({
|
|||
recordId,
|
||||
objectMetadataId,
|
||||
}: UseIsRecordReadOnlyParams) => {
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId,
|
||||
});
|
||||
|
|
@ -26,9 +32,12 @@ export const useIsRecordReadOnly = ({
|
|||
|
||||
const isRecordDeleted = useIsRecordDeleted({ recordId });
|
||||
|
||||
return isRecordReadOnly({
|
||||
objectPermissions,
|
||||
isRecordDeleted,
|
||||
objectMetadataItem,
|
||||
});
|
||||
return (
|
||||
isNavigationMenuInEditMode ||
|
||||
isRecordReadOnly({
|
||||
objectPermissions,
|
||||
isRecordDeleted,
|
||||
objectMetadataItem,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
|
||||
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
|
||||
|
|
@ -5,6 +6,7 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
|
|||
import { isRecordTableCreateDisabled } from '@/object-record/record-table/utils/isRecordTableCreateDisabled';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
|
|
@ -53,10 +55,15 @@ export const RecordTableEmptyStateDisplay = (
|
|||
const objectPermissions = useObjectPermissionsForObject(
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
const isReadOnly = isObjectMetadataReadOnly({
|
||||
objectPermissions,
|
||||
objectMetadataItem,
|
||||
});
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const isReadOnly =
|
||||
isNavigationMenuInEditMode ||
|
||||
isObjectMetadataReadOnly({
|
||||
objectPermissions,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const hasAnySoftDeleteFilterOnView = useAtomComponentSelectorValue(
|
||||
hasAnySoftDeleteFilterOnViewComponentSelector,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
|
||||
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
|
||||
import { isRecordTableCreateDisabled } from '@/object-record/record-table/utils/isRecordTableCreateDisabled';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
|
@ -20,6 +22,9 @@ export const RecordTableHeaderLabelIdentifierCellPlusButton = () => {
|
|||
useRecordTableContextOrThrow();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
|
||||
const { createNewIndexRecord } = useCreateNewIndexRecord({
|
||||
objectMetadataItem,
|
||||
|
|
@ -31,10 +36,12 @@ export const RecordTableHeaderLabelIdentifierCellPlusButton = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const isReadOnly = isObjectMetadataReadOnly({
|
||||
objectPermissions,
|
||||
objectMetadataItem,
|
||||
});
|
||||
const isReadOnly =
|
||||
isNavigationMenuInEditMode ||
|
||||
isObjectMetadataReadOnly({
|
||||
objectPermissions,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const hasAnySoftDeleteFilterOnView = useAtomComponentSelectorValue(
|
||||
hasAnySoftDeleteFilterOnViewComponentSelector,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,24 @@
|
|||
import { Droppable, type DroppableProvided } from '@hello-pangea/dnd';
|
||||
import { type ReactNode, useContext } from 'react';
|
||||
import { lazy, Suspense, useContext, type ReactNode } from 'react';
|
||||
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext';
|
||||
|
||||
import type { AddToNavDroppableProvided } from '@/command-menu/components/CommandMenuAddToNavDroppableTypes';
|
||||
|
||||
const FALLBACK_PROVIDED: AddToNavDroppableProvided = {
|
||||
innerRef: () => {},
|
||||
droppableProps: {},
|
||||
placeholder: null,
|
||||
};
|
||||
|
||||
const CommandMenuAddToNavDroppableDndKit = lazy(() =>
|
||||
import('@/command-menu/components/CommandMenuAddToNavDroppableDndKit').then(
|
||||
(m) => ({ default: m.CommandMenuAddToNavDroppableDndKit }),
|
||||
),
|
||||
);
|
||||
|
||||
type SidePanelAddToNavigationDroppableProps = {
|
||||
children: (provided: DroppableProvided) => ReactNode;
|
||||
children: (provided: AddToNavDroppableProvided) => ReactNode;
|
||||
};
|
||||
|
||||
export const SidePanelAddToNavigationDroppable = ({
|
||||
|
|
@ -15,11 +28,11 @@ export const SidePanelAddToNavigationDroppable = ({
|
|||
const isDropDisabled = sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID;
|
||||
|
||||
return (
|
||||
<Droppable
|
||||
droppableId={ADD_TO_NAV_SOURCE_DROPPABLE_ID}
|
||||
isDropDisabled={isDropDisabled}
|
||||
>
|
||||
{(provided) => children(provided)}
|
||||
</Droppable>
|
||||
<Suspense fallback={children(FALLBACK_PROVIDED)}>
|
||||
<CommandMenuAddToNavDroppableDndKit
|
||||
children={children}
|
||||
isDropDisabled={isDropDisabled}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
import { styled } from '@linaria/react';
|
||||
import { Draggable } from '@hello-pangea/dnd';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import { type ReactNode, lazy, Suspense, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { AddToNavigationDragHandle } from '@/navigation-menu-item/components/AddToNavigationDragHandle';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
const StyledDraggableMenuItem = styled.div`
|
||||
cursor: grab;
|
||||
width: 100%;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`;
|
||||
const CommandMenuItemWithAddToNavigationDragDndKit = lazy(() =>
|
||||
import(
|
||||
'@/command-menu/components/CommandMenuItemWithAddToNavigationDragDndKit'
|
||||
).then((m) => ({
|
||||
default: m.CommandMenuItemWithAddToNavigationDragDndKit,
|
||||
})),
|
||||
);
|
||||
|
||||
type SidePanelItemWithAddToNavigationDragProps = {
|
||||
icon?: IconComponent;
|
||||
|
|
@ -28,8 +27,25 @@ type SidePanelItemWithAddToNavigationDragProps = {
|
|||
onClick: () => void;
|
||||
payload: AddToNavigationDragPayload;
|
||||
dragIndex?: number;
|
||||
disabled?: boolean;
|
||||
disableDrag?: boolean;
|
||||
};
|
||||
|
||||
const StyledDraggableMenuItem = styled.div<{
|
||||
$disabled?: boolean;
|
||||
$disableDrag?: boolean;
|
||||
}>`
|
||||
cursor: ${({ $disabled, $disableDrag }) =>
|
||||
$disabled || $disableDrag ? 'default' : 'grab'};
|
||||
pointer-events: ${({ $disabled }) => ($disabled ? 'none' : 'auto')};
|
||||
width: 100%;
|
||||
|
||||
&:active {
|
||||
cursor: ${({ $disabled, $disableDrag }) =>
|
||||
$disabled || $disableDrag ? 'default' : 'grabbing'};
|
||||
}
|
||||
`;
|
||||
|
||||
export const SidePanelItemWithAddToNavigationDrag = ({
|
||||
icon,
|
||||
customIconContent,
|
||||
|
|
@ -39,6 +55,8 @@ export const SidePanelItemWithAddToNavigationDrag = ({
|
|||
onClick,
|
||||
payload,
|
||||
dragIndex,
|
||||
disabled = false,
|
||||
disableDrag = false,
|
||||
}: SidePanelItemWithAddToNavigationDragProps) => {
|
||||
const { t } = useLingui();
|
||||
const setAddToNavPayloadRegistry = useSetAtomState(
|
||||
|
|
@ -46,7 +64,8 @@ export const SidePanelItemWithAddToNavigationDrag = ({
|
|||
);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const contextualDescription = isHovered
|
||||
const showDragAffordance = !disabled && !disableDrag && isHovered;
|
||||
const contextualDescription = showDragAffordance
|
||||
? t`Drag to add to navbar`
|
||||
: description;
|
||||
|
||||
|
|
@ -55,23 +74,31 @@ export const SidePanelItemWithAddToNavigationDrag = ({
|
|||
icon={icon}
|
||||
customIconContent={customIconContent}
|
||||
payload={payload}
|
||||
isHovered={isHovered}
|
||||
isHovered={showDragAffordance}
|
||||
disabled={disabled}
|
||||
disableDrag={disableDrag}
|
||||
/>
|
||||
);
|
||||
|
||||
const registerPayload = () => {
|
||||
if (dragIndex !== undefined) {
|
||||
if (!disabled && !disableDrag && isDefined(dragIndex)) {
|
||||
setAddToNavPayloadRegistry((prev) => new Map(prev).set(id, payload));
|
||||
}
|
||||
};
|
||||
|
||||
const menuItemContent = (
|
||||
<StyledDraggableMenuItem
|
||||
$disabled={disabled}
|
||||
$disableDrag={disableDrag}
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true);
|
||||
registerPayload();
|
||||
if (!disabled && !disableDrag) {
|
||||
setIsHovered(true);
|
||||
registerPayload();
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (!disabled && !disableDrag) setIsHovered(false);
|
||||
}}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onMouseDown={registerPayload}
|
||||
>
|
||||
<CommandMenuItem
|
||||
|
|
@ -80,27 +107,22 @@ export const SidePanelItemWithAddToNavigationDrag = ({
|
|||
description={contextualDescription}
|
||||
id={id}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</StyledDraggableMenuItem>
|
||||
);
|
||||
|
||||
if (dragIndex !== undefined) {
|
||||
return (
|
||||
<Draggable draggableId={id} index={dragIndex} isDragDisabled={false}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.draggableProps}
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
{menuItemContent}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
if (!isDefined(dragIndex) || disableDrag) {
|
||||
return menuItemContent;
|
||||
}
|
||||
|
||||
return menuItemContent;
|
||||
return (
|
||||
<Suspense fallback={menuItemContent}>
|
||||
<CommandMenuItemWithAddToNavigationDragDndKit
|
||||
id={id}
|
||||
dragIndex={dragIndex}
|
||||
menuItemContent={menuItemContent}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconLink } from 'twenty-ui/display';
|
||||
import { IconLink, IconWorld } from 'twenty-ui/display';
|
||||
|
||||
import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout';
|
||||
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
|
||||
import { sidePanelShouldFocusTitleInputComponentState } from '@/side-panel/states/sidePanelShouldFocusTitleInputComponentState';
|
||||
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
|
||||
import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/components/LinkIconWithLinkOverlay';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout';
|
||||
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
|
||||
import { sidePanelShouldFocusTitleInputComponentState } from '@/side-panel/states/sidePanelShouldFocusTitleInputComponentState';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
|
@ -59,8 +59,10 @@ export const SidePanelLinkInfo = () => {
|
|||
return (
|
||||
<SidePanelPageInfoLayout
|
||||
icon={
|
||||
<NavigationMenuItemStyleIcon
|
||||
Icon={IconLink}
|
||||
<LinkIconWithLinkOverlay
|
||||
link={selectedItem.link}
|
||||
LinkIcon={IconLink}
|
||||
DefaultIcon={IconWorld}
|
||||
color={selectedItem.color}
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
|||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useContext } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
|
||||
|
|
@ -21,11 +21,16 @@ export const SidePanelRouter = () => {
|
|||
const sidePanelPage = useAtomStateValue(sidePanelPageState);
|
||||
const sidePanelPageInfo = useAtomStateValue(sidePanelPageInfoState);
|
||||
|
||||
const sidePanelPageComponent = isDefined(sidePanelPage) ? (
|
||||
SIDE_PANEL_PAGES_CONFIG.get(sidePanelPage)
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
const rawPageComponent = isDefined(sidePanelPage)
|
||||
? SIDE_PANEL_PAGES_CONFIG.get(sidePanelPage)
|
||||
: null;
|
||||
|
||||
const sidePanelPageComponent =
|
||||
isDefined(rawPageComponent) && React.isValidElement(rawPageComponent)
|
||||
? React.cloneElement(rawPageComponent, {
|
||||
key: sidePanelPageInfo.instanceId,
|
||||
})
|
||||
: rawPageComponent;
|
||||
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,22 +3,24 @@ import { isNonEmptyString } from '@sniptt/guards';
|
|||
import { useState } from 'react';
|
||||
import { getAbsoluteUrl } from 'twenty-shared/utils';
|
||||
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
import { extractDomainFromUrl } from '@/navigation-menu-item/utils/extractDomainFromUrl';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { SidePanelEditColorOption } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditColorOption';
|
||||
import {
|
||||
type OrganizeActionsProps,
|
||||
SidePanelEditOrganizeActions,
|
||||
} from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOrganizeActions';
|
||||
import { SidePanelEditOwnerSection } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOwnerSection';
|
||||
import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
type SidePanelEditLinkItemViewProps = OrganizeActionsProps & {
|
||||
selectedItem: ProcessedNavigationMenuItem;
|
||||
onUpdateLink: (linkId: string, link: string) => void;
|
||||
onUpdateLink: (
|
||||
linkId: string,
|
||||
updates: { link?: string; name?: string },
|
||||
) => void;
|
||||
onOpenFolderPicker: () => void;
|
||||
};
|
||||
|
||||
|
|
@ -36,28 +38,49 @@ export const SidePanelEditLinkItemView = ({
|
|||
}: SidePanelEditLinkItemViewProps) => {
|
||||
const { t } = useLingui();
|
||||
const [urlEditInput, setUrlEditInput] = useState('');
|
||||
const [lastAutoSetName, setLastAutoSetName] = useState<string | null>(null);
|
||||
|
||||
const defaultLabel = t`Link label`;
|
||||
const selectableItemIds = getOrganizeActionsSelectableItemIds(true);
|
||||
|
||||
const currentName = selectedItem.name ?? defaultLabel;
|
||||
const currentDomain = selectedItem.link
|
||||
? extractDomainFromUrl(getAbsoluteUrl(selectedItem.link))
|
||||
: undefined;
|
||||
const canAutoUpdateName =
|
||||
currentName === defaultLabel ||
|
||||
currentName === currentDomain ||
|
||||
currentName === lastAutoSetName;
|
||||
|
||||
const handleUrlChange = (value: string) => {
|
||||
setUrlEditInput(value);
|
||||
if (!canAutoUpdateName) return;
|
||||
const trimmed = value.trim();
|
||||
if (!isNonEmptyString(trimmed)) return;
|
||||
const domain = extractDomainFromUrl(getAbsoluteUrl(trimmed));
|
||||
if (domain !== undefined) {
|
||||
setLastAutoSetName(domain);
|
||||
onUpdateLink(selectedItem.id, { name: domain });
|
||||
}
|
||||
};
|
||||
|
||||
const handleUrlBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value.trim();
|
||||
if (isNonEmptyString(value)) {
|
||||
onUpdateLink(selectedItem.id, { link: getAbsoluteUrl(value) });
|
||||
setUrlEditInput('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SidePanelList commandGroups={[]} selectableItemIds={selectableItemIds}>
|
||||
<SidePanelGroup heading={t`Customize`}>
|
||||
<SidePanelEditColorOption
|
||||
navigationMenuItemId={selectedItem.id}
|
||||
color={parseThemeColor(selectedItem.color)}
|
||||
/>
|
||||
<TextInput
|
||||
fullWidth
|
||||
placeholder="www.google.com"
|
||||
value={urlEditInput || selectedItem.link}
|
||||
onChange={(value) => setUrlEditInput(value)}
|
||||
onBlur={(event) => {
|
||||
const value = event.target.value.trim();
|
||||
if (isNonEmptyString(value)) {
|
||||
onUpdateLink(selectedItem.id, getAbsoluteUrl(value));
|
||||
setUrlEditInput('');
|
||||
}
|
||||
}}
|
||||
onChange={handleUrlChange}
|
||||
onBlur={handleUrlBlur}
|
||||
/>
|
||||
</SidePanelGroup>
|
||||
<SidePanelEditOrganizeActions
|
||||
|
|
|
|||
|
|
@ -1,14 +1,4 @@
|
|||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { SidePanelEditColorOption } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditColorOption';
|
||||
import { SidePanelEditFolderPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditFolderPickerSubView';
|
||||
import { SidePanelEditLinkItemView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditLinkItemView';
|
||||
import { SidePanelEditObjectViewBase } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditObjectViewBase';
|
||||
import { SidePanelEditOrganizeActions } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOrganizeActions';
|
||||
import { SidePanelEditOwnerSection } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOwnerSection';
|
||||
import { useNavigationMenuItemEditOrganizeActions } from '@/side-panel/pages/navigation-menu-item/hooks/useNavigationMenuItemEditOrganizeActions';
|
||||
import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
|
||||
import { useOpenAddItemToFolderPage } from '@/navigation-menu-item/hooks/useOpenAddItemToFolderPage';
|
||||
|
|
@ -18,6 +8,16 @@ import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigatio
|
|||
import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { SidePanelEditColorOption } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditColorOption';
|
||||
import { SidePanelEditFolderPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditFolderPickerSubView';
|
||||
import { SidePanelEditLinkItemView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditLinkItemView';
|
||||
import { SidePanelEditObjectViewBase } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditObjectViewBase';
|
||||
import { SidePanelEditOrganizeActions } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOrganizeActions';
|
||||
import { SidePanelEditOwnerSection } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOwnerSection';
|
||||
import { useNavigationMenuItemEditOrganizeActions } from '@/side-panel/pages/navigation-menu-item/hooks/useNavigationMenuItemEditOrganizeActions';
|
||||
import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
|
|
@ -132,7 +132,9 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
|||
<SidePanelEditLinkItemView
|
||||
key={selectedItem.id}
|
||||
selectedItem={selectedItem}
|
||||
onUpdateLink={(linkId, link) => updateLinkInDraft(linkId, { link })}
|
||||
onUpdateLink={(linkId, updates) =>
|
||||
updateLinkInDraft(linkId, updates)
|
||||
}
|
||||
onOpenFolderPicker={openFolderPicker}
|
||||
canMoveUp={canMoveUp}
|
||||
canMoveDown={canMoveDown}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue