Navbar customization improvements (#17863)

- Add folder icon editing support
- And other navbar customization fixes/improvements.

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Abdul Rahman
2026-02-18 01:23:52 +05:30
committed by GitHub
parent 3d362e6e01
commit 015ccbf0a7
44 changed files with 601 additions and 141 deletions
@@ -1042,6 +1042,7 @@ export type CreateLogicFunctionFromSourceInput = {
export type CreateNavigationMenuItemInput = {
folderId?: InputMaybe<Scalars['UUID']>;
icon?: InputMaybe<Scalars['String']>;
link?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;
position?: InputMaybe<Scalars['Int']>;
@@ -3229,6 +3230,7 @@ export type NavigationMenuItem = {
applicationId?: Maybe<Scalars['UUID']>;
createdAt: Scalars['DateTime'];
folderId?: Maybe<Scalars['UUID']>;
icon?: Maybe<Scalars['String']>;
id: Scalars['UUID'];
link?: Maybe<Scalars['String']>;
name?: Maybe<Scalars['String']>;
@@ -4548,6 +4550,7 @@ export type UpdateLogicFunctionFromSourceInputUpdates = {
export type UpdateNavigationMenuItemInput = {
folderId?: InputMaybe<Scalars['UUID']>;
icon?: InputMaybe<Scalars['String']>;
link?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;
position?: InputMaybe<Scalars['Int']>;
@@ -5836,42 +5839,42 @@ export type FindManyMarketplaceAppsQueryVariables = Exact<{ [key: string]: never
export type FindManyMarketplaceAppsQuery = { __typename?: 'Query', findManyMarketplaceApps: Array<{ __typename?: 'MarketplaceApp', id: string, name: string, description: string, icon: string, version: string, author: string, category: string, logo?: string | null, screenshots: Array<string>, aboutDescription: string, providers: Array<string>, websiteUrl?: string | null, termsUrl?: string | null, objects: Array<{ __typename?: 'MarketplaceAppObject', universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, fields: Array<{ __typename?: 'MarketplaceAppField', universalIdentifier?: string | null, name: string, type: string, label: string, description?: string | null, icon?: string | null }> }>, fields: Array<{ __typename?: 'MarketplaceAppField', name: string, type: string, label: string, description?: string | null, icon?: string | null, objectUniversalIdentifier?: string | null }>, logicFunctions: Array<{ __typename?: 'MarketplaceAppLogicFunction', name: string, description?: string | null, timeoutSeconds?: number | null }>, frontComponents: Array<{ __typename?: 'MarketplaceAppFrontComponent', name: string, description?: string | null }>, defaultRole?: { __typename?: 'MarketplaceAppDefaultRole', id: string, label: string, description?: string | null, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canUpdateAllSettings: boolean, canAccessAllTools: boolean, permissionFlags: Array<string>, objectPermissions: Array<{ __typename?: 'MarketplaceAppRoleObjectPermission', objectUniversalIdentifier: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }>, fieldPermissions: Array<{ __typename?: 'MarketplaceAppRoleFieldPermission', objectUniversalIdentifier: string, fieldUniversalIdentifier: string, canReadFieldValue?: boolean | null, canUpdateFieldValue?: boolean | null }> } | null }> };
export type NavigationMenuItemFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string };
export type NavigationMenuItemFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string };
export type NavigationMenuItemQueryFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null };
export type NavigationMenuItemQueryFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null };
export type CreateNavigationMenuItemMutationVariables = Exact<{
input: CreateNavigationMenuItemInput;
}>;
export type CreateNavigationMenuItemMutation = { __typename?: 'Mutation', createNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type CreateNavigationMenuItemMutation = { __typename?: 'Mutation', createNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type DeleteNavigationMenuItemMutationVariables = Exact<{
id: Scalars['UUID'];
}>;
export type DeleteNavigationMenuItemMutation = { __typename?: 'Mutation', deleteNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type DeleteNavigationMenuItemMutation = { __typename?: 'Mutation', deleteNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type UpdateNavigationMenuItemMutationVariables = Exact<{
input: UpdateOneNavigationMenuItemInput;
}>;
export type UpdateNavigationMenuItemMutation = { __typename?: 'Mutation', updateNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type UpdateNavigationMenuItemMutation = { __typename?: 'Mutation', updateNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type FindManyNavigationMenuItemsQueryVariables = Exact<{ [key: string]: never; }>;
export type FindManyNavigationMenuItemsQuery = { __typename?: 'Query', navigationMenuItems: Array<{ __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null }> };
export type FindManyNavigationMenuItemsQuery = { __typename?: 'Query', navigationMenuItems: Array<{ __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null }> };
export type FindOneNavigationMenuItemQueryVariables = Exact<{
id: Scalars['UUID'];
}>;
export type FindOneNavigationMenuItemQuery = { __typename?: 'Query', navigationMenuItem?: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null } | null };
export type FindOneNavigationMenuItemQuery = { __typename?: 'Query', navigationMenuItem?: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null } | null };
export type ObjectMetadataFieldsFragment = { __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> };
@@ -7361,6 +7364,7 @@ export const NavigationMenuItemFieldsFragmentDoc = gql`
folderId
name
link
icon
position
applicationId
createdAt
@@ -5,7 +5,7 @@ import {
useNavigate,
useParams,
} from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import {
setSessionId,
@@ -17,6 +17,8 @@ import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCapt
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
@@ -97,6 +99,20 @@ export const PageChangeEffect = () => {
const { closeCommandMenu } = useCommandMenu();
const closeCommandMenuUnlessOnEditPage = useRecoilCallback(
({ snapshot }) =>
() => {
const currentPage = snapshot
.getLoadable(commandMenuPageState)
.getValue();
if (currentPage === CommandMenuPages.NavigationMenuItemEdit) {
return;
}
closeCommandMenu();
},
[closeCommandMenu],
);
const { resetFocusStackToFocusItem } = useResetFocusStackToFocusItem();
const { resetFocusStackToRecordIndex } = useResetFocusStackToRecordIndex();
@@ -104,8 +120,8 @@ export const PageChangeEffect = () => {
const { openNewRecordTitleCell } = useOpenNewRecordTitleCell();
useEffect(() => {
closeCommandMenu();
}, [location.pathname, closeCommandMenu]);
closeCommandMenuUnlessOnEditPage();
}, [location.pathname, closeCommandMenuUnlessOnEditPage]);
useEffect(() => {
if (!previousLocation || previousLocation !== location.pathname) {
@@ -1,24 +1,32 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { IconFolder } from 'twenty-ui/display';
import { useIcons } from 'twenty-ui/display';
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
import { useUpdateFolderNameInDraft } from '@/navigation-menu-item/hooks/useUpdateFolderNameInDraft';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useUpdateFolderInDraft } from '@/navigation-menu-item/hooks/useUpdateFolderInDraft';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeStateV2 } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { TitleInput } from '@/ui/input/components/TitleInput';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
const StyledClickableIconWrapper = styled.div`
cursor: pointer;
`;
export const CommandMenuFolderInfo = () => {
const theme = useTheme();
const { t } = useLingui();
const { getIcon } = useIcons();
const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState);
const [shouldFocusTitleInput, setShouldFocusTitleInput] =
useRecoilComponentState(
@@ -29,7 +37,7 @@ export const CommandMenuFolderInfo = () => {
selectedNavigationMenuItemInEditModeStateV2,
);
const items = useWorkspaceSectionItems();
const { updateFolderNameInDraft } = useUpdateFolderNameInDraft();
const { updateFolderInDraft } = useUpdateFolderInDraft();
const defaultLabel = t`New folder`;
const placeholder = t`Folder name`;
@@ -48,7 +56,7 @@ export const CommandMenuFolderInfo = () => {
const itemName = selectedItem.name ?? defaultLabel;
const handleChange = (text: string) => {
updateFolderNameInDraft(itemId, text);
updateFolderInDraft(itemId, { name: text });
};
const handleSave = () => {
@@ -56,22 +64,36 @@ export const CommandMenuFolderInfo = () => {
const finalName = trimmed.length > 0 ? trimmed : defaultLabel;
if (finalName !== itemName) {
updateFolderNameInDraft(itemId, finalName);
updateFolderInDraft(itemId, { name: finalName });
}
};
const selectedIconKey = selectedItem.icon ?? FOLDER_ICON_DEFAULT;
const FolderIconComponent = getIcon(selectedIconKey);
return (
<CommandMenuPageInfoLayout
icon={
<StyledNavigationMenuItemIconContainer
$backgroundColor={getNavigationMenuItemIconColors(theme).folder}
>
<IconFolder
size={theme.spacing(3.5)}
color={theme.grayScale.gray1}
stroke={theme.icon.stroke.md}
/>
</StyledNavigationMenuItemIconContainer>
<IconPicker
dropdownId="command-menu-folder-icon-picker"
selectedIconKey={selectedIconKey}
onChange={({ iconKey }) =>
updateFolderInDraft(itemId, { icon: iconKey })
}
clickableComponent={
<StyledClickableIconWrapper>
<StyledNavigationMenuItemIconContainer
$backgroundColor={getNavigationMenuItemIconColors(theme).folder}
>
<FolderIconComponent
size={theme.spacing(3.5)}
color={theme.grayScale.gray1}
stroke={theme.icon.stroke.md}
/>
</StyledNavigationMenuItemIconContainer>
</StyledClickableIconWrapper>
}
/>
}
title={
<TitleInput
@@ -3,10 +3,9 @@ import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemLabel';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { ViewKey } from '@/views/types/ViewKey';
export const CommandMenuObjectViewRecordInfo = () => {
const { t } = useLingui();
@@ -22,19 +21,20 @@ export const CommandMenuObjectViewRecordInfo = () => {
return null;
}
const isViewOrRecord =
processedItem.itemType === NavigationMenuItemType.VIEW ||
processedItem.itemType === NavigationMenuItemType.RECORD;
const isViewOrRecord = [
NavigationMenuItemType.VIEW,
NavigationMenuItemType.RECORD,
].includes(processedItem.itemType);
if (!isViewOrRecord) {
return null;
}
const label =
processedItem.itemType === NavigationMenuItemType.RECORD
const label = isViewOrRecord
? processedItem.itemType === NavigationMenuItemType.RECORD
? t`record`
: processedItem.viewKey === ViewKey.Index
? t`object`
: t`view`;
: t`view`
: t`object`;
return (
<CommandMenuPageInfoLayout
@@ -10,6 +10,7 @@ import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandM
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddViewToNavigationMenuDraft';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
@@ -110,7 +111,21 @@ export const CommandMenuNewSidebarItemViewPickerSubView = ({
onEnter={() => handleSelectView(view)}
>
<CommandMenuItemWithAddToNavigationDrag
icon={getIcon(view.icon)}
customIconContent={
selectedObjectMetadataItem ? (
<ObjectIconWithViewOverlay
ObjectIcon={getIcon(
selectedObjectMetadataItem.icon,
)}
ViewIcon={getIcon(view.icon)}
/>
) : undefined
}
icon={
selectedObjectMetadataItem
? undefined
: getIcon(view.icon)
}
label={view.name}
id={view.id}
onClick={() => handleSelectView(view)}
@@ -72,14 +72,22 @@ export const AddToNavigationDragHandle = ({
const iconSize = theme.icon.size.md;
const iconStroke = theme.icon.stroke.sm;
const showCustomContentWithoutWrapper = isDefined(customIconContent);
return (
<StyledIconSlot $hasFixedSize={payloadHasBackgroundColor}>
<StyledIconSlot
$hasFixedSize={
payloadHasBackgroundColor || showCustomContentWithoutWrapper
}
>
{isHovered ? (
<IconGripVertical
size={iconSize}
stroke={iconStroke}
color={theme.font.color.tertiary}
/>
) : showCustomContentWithoutWrapper ? (
customIconContent
) : hasBackgroundColor ? (
<StyledNavigationMenuItemIconContainer
$backgroundColor={iconBackgroundColor}
@@ -1,20 +1,22 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { IconCheck, useIcons } from 'twenty-ui/display';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/hooks/useSaveNavigationMenuItemsDraft';
import { isNavigationMenuInEditModeStateV2 } from '@/navigation-menu-item/states/isNavigationMenuInEditModeStateV2';
import { navigationMenuItemsDraftStateV2 } from '@/navigation-menu-item/states/navigationMenuItemsDraftStateV2';
import { selectedNavigationMenuItemInEditModeStateV2 } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { IconCheck, useIcons } from 'twenty-ui/display';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledContainer = styled.div`
@@ -40,6 +42,7 @@ export const NavigationMenuEditModeBar = () => {
const { getIcon } = useIcons();
const [isSaving, setIsSaving] = useState(false);
const { closeCommandMenu } = useCommandMenu();
const commandMenuPage = useRecoilValue(commandMenuPageState);
const { enqueueErrorSnackBar } = useSnackBar();
const setNavigationMenuItemsDraft = useSetRecoilStateV2(
navigationMenuItemsDraftStateV2,
@@ -57,6 +60,12 @@ export const NavigationMenuEditModeBar = () => {
setNavigationMenuItemsDraft(null);
setSelectedNavigationMenuItemInEditMode(null);
setIsNavigationMenuInEditMode(false);
const isNavItemPageOpen =
commandMenuPage === CommandMenuPages.NavigationMenuAddItem ||
commandMenuPage === CommandMenuPages.NavigationMenuItemEdit;
if (isNavItemPageOpen) {
closeCommandMenu();
}
};
const isNavigationMenuInEditMode = useRecoilValueV2(
@@ -1,12 +1,18 @@
import { useTheme } from '@emotion/react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { Avatar, useIcons } from 'twenty-ui/display';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ViewKey } from '@/views/types/ViewKey';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
export const NavigationMenuItemIcon = ({
navigationMenuItem,
@@ -15,30 +21,61 @@ export const NavigationMenuItemIcon = ({
}) => {
const theme = useTheme();
const { getIcon } = useIcons();
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { Icon: StandardIcon, IconColor } = useGetStandardObjectIcon(
navigationMenuItem.objectNameSingular || '',
);
const isRecord =
navigationMenuItem.itemType === NavigationMenuItemType.RECORD;
const isLink = navigationMenuItem.itemType === NavigationMenuItemType.LINK;
const isObjectIndexView =
navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
navigationMenuItem.viewKey === ViewKey.Index;
const isViewWithOverlay =
navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
!isObjectIndexView;
const objectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === navigationMenuItem.objectNameSingular,
);
const ObjectIconForView =
objectMetadataItem?.icon != null
? getIcon(objectMetadataItem.icon)
: StandardIcon;
if (
isViewWithOverlay &&
isDefined(ObjectIconForView) &&
isDefined(navigationMenuItem.Icon)
) {
const ViewIcon = getIcon(navigationMenuItem.Icon);
return (
<ObjectIconWithViewOverlay
ObjectIcon={ObjectIconForView}
ViewIcon={ViewIcon}
/>
);
}
const IconToUse =
StandardIcon ||
(navigationMenuItem.Icon ? getIcon(navigationMenuItem.Icon) : undefined);
const placeholderColorSeed = navigationMenuItem.targetRecordId ?? undefined;
const isRecord =
navigationMenuItem.itemType === NavigationMenuItemType.RECORD;
const isLink = navigationMenuItem.itemType === NavigationMenuItemType.LINK;
const iconColors = getNavigationMenuItemIconColors(theme);
const isObjectIndexView =
navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
navigationMenuItem.viewKey === ViewKey.Index;
const iconBackgroundColor = isRecord
? undefined
: isLink
? iconColors.link
: navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
!isObjectIndexView
? iconColors.view
: iconColors.object;
const iconBackgroundColor = isNavigationMenuItemEditingEnabled
? isRecord
? undefined
: isLink
? iconColors.link
: isViewWithOverlay
? iconColors.view
: iconColors.object
: undefined;
const iconColorToUse = iconBackgroundColor
? theme.grayScale.gray1
@@ -0,0 +1,72 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import type { IconComponent } from 'twenty-ui/display';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
const StyledCompositeContainer = styled.div`
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.xs};
display: flex;
flex-shrink: 0;
height: ${({ theme }) => theme.spacing(4.5)};
justify-content: center;
position: relative;
width: ${({ theme }) => theme.spacing(4.5)};
`;
const StyledObjectIconWrapper = styled.div<{ $backgroundColor: string }>`
position: absolute;
inset: 0;
border-radius: ${({ theme }) => theme.border.radius.xs};
background-color: ${({ $backgroundColor }) => $backgroundColor};
display: flex;
align-items: center;
justify-content: center;
`;
const StyledViewOverlay = styled.div<{ $backgroundColor: string }>`
align-items: center;
background-color: ${({ $backgroundColor }) => $backgroundColor};
border: 1px solid ${({ theme }) => theme.background.primary};
border-radius: ${({ theme }) => theme.border.radius.xs};
bottom: -7px;
display: flex;
height: ${({ theme }) => theme.spacing(3.5)};
justify-content: center;
position: absolute;
right: -7px;
width: ${({ theme }) => theme.spacing(3.5)};
`;
export type ObjectIconWithViewOverlayProps = {
ObjectIcon: IconComponent;
ViewIcon: IconComponent;
};
export const ObjectIconWithViewOverlay = ({
ObjectIcon,
ViewIcon,
}: ObjectIconWithViewOverlayProps) => {
const theme = useTheme();
const iconColors = getNavigationMenuItemIconColors(theme);
return (
<StyledCompositeContainer>
<StyledObjectIconWrapper $backgroundColor={iconColors.object}>
<ObjectIcon
size={theme.spacing(3.5)}
stroke={theme.icon.stroke.md}
color={theme.grayScale.gray1}
/>
</StyledObjectIconWrapper>
<StyledViewOverlay $backgroundColor={iconColors.view}>
<ViewIcon
size={theme.spacing(2)}
stroke={theme.icon.stroke.sm}
color={theme.grayScale.gray1}
/>
</StyledViewOverlay>
</StyledCompositeContainer>
);
};
@@ -1,39 +1,36 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import {
IconFolder,
IconLink,
IconPlus,
IconTool,
useIcons,
} from 'twenty-ui/display';
import { useNavigate } from 'react-router-dom';
import { useRecoilCallback } from 'recoil';
import { IconLink, IconPlus, IconTool, useIcons } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useOpenNavigationMenuItemInCommandMenu } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInCommandMenu';
import {
type NavigationMenuItemClickParams,
useWorkspaceSectionItems,
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { isNavigationMenuInEditModeStateV2 } from '@/navigation-menu-item/states/isNavigationMenuInEditModeStateV2';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { navigationMenuItemsDraftStateV2 } from '@/navigation-menu-item/states/navigationMenuItemsDraftStateV2';
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
import { openNavigationMenuItemFolderIdsStateV2 } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsStateV2';
import { selectedNavigationMenuItemInEditModeStateV2 } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader';
import { NavigationDrawerSectionForWorkspaceItems } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilCallback } from 'recoil';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
const StyledRightIconsContainer = styled.div`
@@ -74,6 +71,7 @@ export const WorkspaceNavigationMenuItems = () => {
const setOpenNavigationMenuItemFolderIds = useSetRecoilStateV2(
openNavigationMenuItemFolderIdsStateV2,
);
const navigate = useNavigate();
const { navigateCommandMenu } = useNavigateCommandMenu();
const { openNavigationMenuItemInCommandMenu } =
useOpenNavigationMenuItemInCommandMenu();
@@ -101,7 +99,7 @@ export const WorkspaceNavigationMenuItems = () => {
);
openNavigationMenuItemInCommandMenu({
pageTitle: t`Edit folder`,
pageIcon: IconFolder,
pageIcon: getIcon(item.icon ?? item.Icon ?? FOLDER_ICON_DEFAULT),
});
} else if (item.itemType === NavigationMenuItemType.LINK) {
openNavigationMenuItemInCommandMenu({
@@ -113,6 +111,10 @@ export const WorkspaceNavigationMenuItems = () => {
pageTitle: objectMetadataItem.labelPlural,
pageIcon: getIcon(objectMetadataItem.icon),
});
if (isNonEmptyString(item.link)) {
navigate(item.link);
}
}
};
@@ -8,19 +8,19 @@ import { isDefined } from 'twenty-shared/utils';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { IconFolder, IconFolderOpen } from 'twenty-ui/display';
import { IconChevronDown, IconChevronRight, useIcons } from 'twenty-ui/display';
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 { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { openNavigationMenuItemFolderIdsStateV2 } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsStateV2';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
@@ -32,11 +32,14 @@ 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 { currentNavigationMenuItemFolderIdStateV2 } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdStateV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { coreViewsState } from '@/views/states/coreViewState';
import { ViewKey } from '@/views/types/ViewKey';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledFolderContainer = styled.div<{ $isSelectedInEditMode: boolean }>`
border: ${({ theme, $isSelectedInEditMode }) =>
@@ -49,6 +52,9 @@ const StyledFolderContainer = styled.div<{ $isSelectedInEditMode: boolean }>`
const StyledFolderDroppableContent = styled.div<{
$compact: boolean;
}>`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.betweenSiblingsGap};
padding-bottom: ${({ theme, $compact }) => ($compact ? 0 : theme.spacing(2))};
`;
@@ -61,6 +67,7 @@ const StyledFolderExpandableWrapper = styled.div`
type WorkspaceNavigationMenuItemsFolderProps = {
folderId: string;
folderName: string;
folderIconKey?: string | null;
navigationMenuItems: ProcessedNavigationMenuItem[];
isGroup: boolean;
isEditMode?: boolean;
@@ -74,6 +81,7 @@ type WorkspaceNavigationMenuItemsFolderProps = {
export const WorkspaceNavigationMenuItemsFolder = ({
folderId,
folderName,
folderIconKey,
navigationMenuItems,
isGroup,
isEditMode = false,
@@ -84,7 +92,12 @@ export const WorkspaceNavigationMenuItemsFolder = ({
isDragging = false,
}: WorkspaceNavigationMenuItemsFolderProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const iconColors = getNavigationMenuItemIconColors(theme);
const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const coreViews = useRecoilValue(coreViewsState);
const views = coreViews.map(convertCoreViewToView);
@@ -149,13 +162,30 @@ export const WorkspaceNavigationMenuItemsFolder = ({
>
<NavigationDrawerItem
label={folderName}
Icon={isOpen ? IconFolderOpen : IconFolder}
iconBackgroundColor={iconColors.folder}
Icon={FolderIcon}
iconBackgroundColor={
isNavigationMenuItemEditingEnabled ? iconColors.folder : undefined
}
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>
@@ -0,0 +1 @@
export const FOLDER_ICON_DEFAULT = 'IconFolder';
@@ -1,10 +1,16 @@
import { createContext } from 'react';
export type AddToNavigationFallbackDestination = {
droppableId: string;
index: number;
} | null;
type NavigationDropTargetContextType = {
activeDropTargetId: string | null;
setActiveDropTargetId: (id: string | null) => void;
forbiddenDropTargetId: string | null;
setForbiddenDropTargetId: (id: string | null) => void;
addToNavigationFallbackDestination: AddToNavigationFallbackDestination;
};
export const NavigationDropTargetContext =
@@ -13,4 +19,5 @@ export const NavigationDropTargetContext =
setActiveDropTargetId: () => {},
forbiddenDropTargetId: null,
setForbiddenDropTargetId: () => {},
addToNavigationFallbackDestination: null,
});
@@ -10,6 +10,7 @@ export const NAVIGATION_MENU_ITEM_FRAGMENT = gql`
folderId
name
link
icon
position
applicationId
createdAt
@@ -1,7 +1,10 @@
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useCreateNavigationMenuItemMutation } from '~/generated-metadata/graphql';
import {
type CreateNavigationMenuItemInput,
useCreateNavigationMenuItemMutation,
} from '~/generated-metadata/graphql';
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
import { useUpdateNavigationMenuItem } from '@/navigation-menu-item/hooks/useUpdateNavigationMenuItem';
@@ -81,20 +84,13 @@ export const useSaveNavigationMenuItemsDraft = () => {
];
for (const draftItem of idsToCreateIncludingRecreated) {
const input: {
position: number;
folderId?: string | null;
name?: string;
link?: string;
viewId?: string;
targetObjectMetadataId?: string;
targetRecordId?: string;
} = {
const input: CreateNavigationMenuItemInput = {
position: Math.max(0, Math.round(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();
@@ -138,12 +134,16 @@ export const useSaveNavigationMenuItemsDraft = () => {
const linkChanged =
isNavigationMenuItemLink(draftItem) &&
(original.link ?? null) !== (draftItem.link ?? null);
const iconChanged =
isNavigationMenuItemFolder(draftItem) &&
(original.icon ?? null) !== (draftItem.icon ?? null);
if (
positionChanged ||
folderIdChanged ||
nameChanged ||
linkChanged
linkChanged ||
iconChanged
) {
const updateInput: {
id: string;
@@ -151,6 +151,7 @@ export const useSaveNavigationMenuItemsDraft = () => {
folderId?: string | null;
name?: string;
link?: string | null;
icon?: string | null;
} = { id: draftItem.id };
if (positionChanged) {
@@ -177,6 +178,9 @@ export const useSaveNavigationMenuItemsDraft = () => {
: `https://${linkUrl}`
: null;
}
if (iconChanged && isNavigationMenuItemFolder(draftItem)) {
updateInput.icon = draftItem.icon ?? null;
}
await updateNavigationMenuItem(updateInput);
}
@@ -0,0 +1,32 @@
import { navigationMenuItemsDraftStateV2 } from '@/navigation-menu-item/states/navigationMenuItemsDraftStateV2';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
export type UpdateFolderInDraftUpdates = {
name?: string;
icon?: string;
};
export const useUpdateFolderInDraft = () => {
const setNavigationMenuItemsDraft = useSetRecoilStateV2(
navigationMenuItemsDraftStateV2,
);
const updateFolderInDraft = (
folderId: string,
updates: UpdateFolderInDraftUpdates,
) => {
setNavigationMenuItemsDraft((draft) => {
if (!draft) return draft;
return draft.map((item) => {
if (!isNavigationMenuItemFolder(item) || item.id !== folderId) {
return item;
}
return { ...item, ...updates };
});
});
};
return { updateFolderInDraft };
};
@@ -0,0 +1,31 @@
import { navigationMenuItemsDraftStateV2 } from '@/navigation-menu-item/states/navigationMenuItemsDraftStateV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
export type UpdateNavigationMenuItemInDraftUpdates = {
color?: string;
name?: string;
icon?: string;
link?: string;
};
export const useUpdateNavigationMenuItemInDraft = () => {
const setNavigationMenuItemsDraft = useSetRecoilStateV2(
navigationMenuItemsDraftStateV2,
);
const updateNavigationMenuItemInDraft = (
navigationMenuItemId: string,
updates: UpdateNavigationMenuItemInDraftUpdates,
) => {
setNavigationMenuItemsDraft((draft) => {
if (!draft) return draft;
return draft.map((item) => {
if (item.id !== navigationMenuItemId) return item;
return { ...item, ...updates };
});
});
};
return { updateNavigationMenuItemInDraft };
};
@@ -1,12 +1,13 @@
import { useRecoilValue } from 'recoil';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { coreViewsState } from '@/views/states/coreViewState';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { isDefined } from 'twenty-shared/utils';
@@ -19,6 +20,7 @@ export type FlatWorkspaceItem =
| ProcessedNavigationMenuItem
| (NavigationMenuItem & {
itemType: NavigationMenuItemType.FOLDER;
Icon: string;
});
export type NavigationMenuItemClickParams = {
@@ -56,7 +58,11 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
FlatWorkspaceItem[]
>((acc, item) => {
if (isNavigationMenuItemFolder(item)) {
acc.push({ ...item, itemType: NavigationMenuItemType.FOLDER });
acc.push({
...item,
itemType: NavigationMenuItemType.FOLDER,
Icon: item.icon ?? FOLDER_ICON_DEFAULT,
});
} else {
const processedItem = processedObjectViewsById.get(item.id);
if (!isDefined(processedItem)) {
@@ -12,18 +12,21 @@ import { FeatureFlagKey } from '~/generated-metadata/graphql';
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 { addToNavPayloadRegistryStateV2 } from '@/navigation-menu-item/states/addToNavPayloadRegistryStateV2';
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 { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared/utils';
type PageDragDropProviderProps = {
children: ReactNode;
@@ -45,7 +48,12 @@ export const PageDragDropProvider = ({
const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState<
string | null
>(null);
const [
addToNavigationFallbackDestination,
setAddToNavigationFallbackDestination,
] = useState<{ droppableId: string; index: number } | null>(null);
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop();
const { handleNavigationMenuItemDragAndDrop } =
useHandleNavigationMenuItemDragAndDrop();
@@ -53,9 +61,22 @@ export const PageDragDropProvider = ({
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 = useRecoilCallback(
@@ -65,36 +86,53 @@ export const PageDragDropProvider = ({
if (source.droppableId !== ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
return;
}
if (!destination || !isWorkspaceDroppableId(destination.droppableId)) {
setActiveDropTargetId(null);
setForbiddenDropTargetId(null);
return;
}
const dropTargetId = getDropTargetIdFromDestination(destination);
setActiveDropTargetId(dropTargetId);
if (
destination !== null &&
isWorkspaceDroppableId(destination.droppableId)
) {
setAddToNavigationFallbackDestination(destination);
const dropTargetId = getDropTargetIdFromDestination(destination);
setActiveDropTargetId(dropTargetId);
const payload =
jotaiStore
.get(addToNavPayloadRegistryStateV2.atom)
.get(update.draggableId) ?? null;
const folderId = validateAndExtractWorkspaceFolderId(
destination.droppableId,
);
const isFolderOverFolder =
payload?.type === 'folder' && folderId !== null;
setForbiddenDropTargetId(isFolderOverFolder ? dropTargetId : null);
const payload =
jotaiStore
.get(addToNavPayloadRegistryStateV2.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],
);
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 (result.source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
handleAddToNavigationDrop(result, provided);
if (isAddToNavigationSource) {
handleAddToNavigationDrop(effectiveResult, provided);
return;
}
@@ -122,6 +160,7 @@ export const PageDragDropProvider = ({
setActiveDropTargetId,
forbiddenDropTargetId,
setForbiddenDropTargetId,
addToNavigationFallbackDestination,
}}
>
<DragDropContext
@@ -1,3 +1,4 @@
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
@@ -12,6 +13,9 @@ import { useRecoilValue } from 'recoil';
import { AppPath } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { Avatar, useIcons } from 'twenty-ui/display';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
export type NavigationDrawerItemForObjectMetadataItemProps = {
objectMetadataItem: ObjectMetadataItem;
@@ -33,6 +37,9 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
isDragging = false,
}: NavigationDrawerItemForObjectMetadataItemProps) => {
const theme = useTheme();
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const iconColors = getNavigationMenuItemIconColors(theme);
const lastVisitedViewPerObjectMetadataItem = useRecoilValue(
lastVisitedViewPerObjectMetadataItemState,
@@ -111,14 +118,18 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
/>
)
: isViewWithCustomName && isDefined(navigationMenuItem?.Icon)
? getIcon(navigationMenuItem.Icon)
? () => (
<ObjectIconWithViewOverlay
ObjectIcon={getIcon(objectMetadataItem.icon)}
ViewIcon={getIcon(navigationMenuItem!.Icon!)}
/>
)
: getIcon(objectMetadataItem.icon);
const iconBackgroundColor = isRecord
? undefined
: isViewWithCustomName
? iconColors.view
: iconColors.object;
const iconBackgroundColor =
isNavigationMenuItemEditingEnabled && !isRecord && !isViewWithCustomName
? iconColors.object
: undefined;
const secondaryLabel =
isRecord || isViewWithCustomName
@@ -1,4 +1,6 @@
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Droppable } from '@hello-pangea/dnd';
import { useLingui } from '@lingui/react/macro';
import { useContext } from 'react';
@@ -7,12 +9,12 @@ import { isDefined } from 'twenty-shared/utils';
import { IconLink, IconPlus } from 'twenty-ui/display';
import { NavigationItemDropTarget } from '@/navigation-menu-item/components/NavigationItemDropTarget';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
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,
@@ -25,15 +27,21 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
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 { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
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';
import { coreViewsState } from '@/views/states/coreViewState';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
const StyledWorkspaceDroppableList = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.betweenSiblingsGap};
`;
type NavigationDrawerSectionForWorkspaceItemsProps = {
sectionTitle: string;
items: FlatWorkspaceItem[];
@@ -70,8 +78,14 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { isDragging } = useContext(NavigationMenuItemDragContext);
const { addToNavigationFallbackDestination } = useContext(
NavigationDropTargetContext,
);
const flatItems = items.filter((item) => !isDefined(item.folderId));
const isAddToNavigationDropTargetVisible =
addToNavigationFallbackDestination?.droppableId ===
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS;
const folderChildrenById = items.reduce<
Map<string, ProcessedNavigationMenuItem[]>
>((acc, item) => {
@@ -140,7 +154,10 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
};
};
if (flatItems.length === 0) {
const isAddMenuItemButtonVisible =
isEditMode && isDefined(onAddMenuItem) && !isDragging;
if (flatItems.length === 0 && !isAddToNavigationDropTargetVisible) {
return null;
}
@@ -154,7 +171,7 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
alwaysShowRightIcon={isEditMode}
/>
</NavigationDrawerAnimatedCollapseWrapper>
{isNavigationSectionOpen && (
{(isNavigationSectionOpen || isAddToNavigationDropTargetVisible) && (
<Droppable
droppableId={
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
@@ -162,7 +179,7 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
isDropDisabled={workspaceDropDisabled}
>
{(provided) => (
<div
<StyledWorkspaceDroppableList
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
@@ -189,6 +206,7 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
<WorkspaceNavigationMenuItemsFolder
folderId={item.id}
folderName={item.name ?? 'Folder'}
folderIconKey={item.Icon}
navigationMenuItems={
folderChildrenById.get(item.id) ?? []
}
@@ -308,9 +326,9 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
folderId={null}
index={filteredItems.length}
sectionId={NavigationSections.WORKSPACE}
compact={!!(isEditMode && onAddMenuItem)}
compact={!isAddMenuItemButtonVisible}
>
{isEditMode && onAddMenuItem && (
{isAddMenuItemButtonVisible && (
<NavigationDrawerItem
Icon={IconPlus}
label={t`Add menu item`}
@@ -319,8 +337,19 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
/>
)}
</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}
</div>
</StyledWorkspaceDroppableList>
)}
</Droppable>
)}
@@ -1,9 +1,11 @@
import { COMMAND_MENU_SEARCH_BAR_HEIGHT_MOBILE } from '@/command-menu/constants/CommandMenuSearchBarHeightMobile';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { isCommandMenuOpenedStateV2 } from '@/command-menu/states/isCommandMenuOpenedStateV2';
import { isNavigationMenuInEditModeStateV2 } from '@/navigation-menu-item/states/isNavigationMenuInEditModeStateV2';
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderCommandMenuButtonClickOutsideId';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useTheme } from '@emotion/react';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { i18n } from '@lingui/core';
import { t } from '@lingui/core/macro';
@@ -12,8 +14,20 @@ import { AppTooltip, TooltipDelay, TooltipPosition } from 'twenty-ui/display';
import { AnimatedButton } from 'twenty-ui/input';
import { getOsControlSymbol, useIsMobile } from 'twenty-ui/utilities';
const StyledButtonWrapper = styled.div`
const StyledButtonWrapper = styled.div<{
alignWithCommandMenuTopBar: boolean;
}>`
z-index: ${RootStackingContextZIndices.CommandMenuButton};
${({ alignWithCommandMenuTopBar, theme }) =>
alignWithCommandMenuTopBar &&
css`
align-items: center;
display: flex;
height: ${COMMAND_MENU_SEARCH_BAR_HEIGHT_MOBILE}px;
position: fixed;
right: ${theme.spacing(3)};
top: 0;
`}
`;
const StyledTooltipWrapper = styled.div`
@@ -109,9 +123,15 @@ const AnimatedIcon = ({
export const PageHeaderToggleCommandMenuButton = () => {
const { toggleCommandMenu } = useCommandMenu();
const isCommandMenuOpened = useRecoilValueV2(isCommandMenuOpenedStateV2);
const isNavigationMenuInEditMode = useRecoilValueV2(
isNavigationMenuInEditModeStateV2,
);
const isMobile = useIsMobile();
const alignWithCommandMenuTopBar =
isMobile && isNavigationMenuInEditMode && isCommandMenuOpened;
const ariaLabel = isCommandMenuOpened
? t`Close command menu`
: t`Open command menu`;
@@ -119,7 +139,9 @@ export const PageHeaderToggleCommandMenuButton = () => {
const theme = useTheme();
return (
<StyledButtonWrapper>
<StyledButtonWrapper
alignWithCommandMenuTopBar={alignWithCommandMenuTopBar}
>
<div id="toggle-command-menu-button">
<AnimatedButton
animatedSvg={
@@ -356,6 +356,7 @@ export const NavigationDrawerItem = ({
as={
to ? (isExternalLink ? 'a' : Link) : rightOptions ? 'div' : undefined
}
role={to ? undefined : rightOptions ? 'button' : undefined}
to={isExternalLink ? undefined : to}
href={isExternalLink ? to : undefined}
target={isExternalLink ? '_blank' : undefined}
@@ -284,8 +284,9 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
folderId: null,
folderUniversalIdentifier: null,
name: favoriteFolder.name,
position: favoriteFolder.position,
link: null,
icon: null,
position: favoriteFolder.position,
workspaceId,
applicationId: workspaceCustomApplicationId,
applicationUniversalIdentifier:
@@ -317,8 +318,9 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
folderId: null,
folderUniversalIdentifier: null,
name: favoriteFolder.name,
position: favoriteFolder.position,
link: null,
icon: null,
position: favoriteFolder.position,
workspaceId,
applicationId: workspaceCustomApplicationId,
applicationUniversalIdentifier:
@@ -523,8 +525,9 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
folderId,
folderUniversalIdentifier: folderId,
name: null,
position: favorite.position,
link: null,
icon: null,
position: favorite.position,
workspaceId,
applicationId,
applicationUniversalIdentifier,
@@ -569,8 +572,9 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
folderId,
folderUniversalIdentifier: folderId,
name: null,
position: favorite.position,
link: null,
icon: null,
position: favorite.position,
workspaceId,
applicationId: workspaceCustomApplicationId,
applicationUniversalIdentifier:
@@ -0,0 +1,19 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddIconToNavigationMenuItem1771247783542
implements MigrationInterface
{
name = 'AddIconToNavigationMenuItem1771247783542';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."navigationMenuItem" ADD "icon" text`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."navigationMenuItem" DROP COLUMN "icon"`,
);
}
}
@@ -16,6 +16,7 @@ export const fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem =
universalIdentifier: navigationMenuItemManifest.universalIdentifier,
applicationUniversalIdentifier,
name: navigationMenuItemManifest.name ?? null,
icon: navigationMenuItemManifest.icon ?? null,
position: navigationMenuItemManifest.position,
viewUniversalIdentifier:
navigationMenuItemManifest.viewUniversalIdentifier ?? null,
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should match snapshot 1`] = `
{
@@ -103,6 +103,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"folderUniversalIdentifier",
"name",
"link",
"icon",
],
"propertiesToStringify": [],
},
@@ -1019,6 +1019,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
},
name: { toCompare: true, toStringify: false, universalProperty: undefined },
link: { toCompare: true, toStringify: false, universalProperty: undefined },
icon: { toCompare: true, toStringify: false, universalProperty: undefined },
createdAt: {
toCompare: false,
toStringify: false,
@@ -5,4 +5,5 @@ export const FLAT_NAVIGATION_MENU_ITEM_EDITABLE_PROPERTIES = [
'folderId',
'name',
'link',
'icon',
] as const satisfies MetadataEntityPropertyName<'navigationMenuItem'>[];
@@ -81,6 +81,7 @@ export const fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate =
folderUniversalIdentifier,
name: createNavigationMenuItemInput.name ?? null,
link: createNavigationMenuItemInput.link ?? null,
icon: createNavigationMenuItemInput.icon ?? null,
position,
workspaceId,
applicationId: flatApplication.id,
@@ -13,6 +13,7 @@ export const fromFlatNavigationMenuItemToNavigationMenuItemDto = (
folderId: flatNavigationMenuItem.folderId ?? undefined,
name: flatNavigationMenuItem.name ?? undefined,
link: flatNavigationMenuItem.link ?? undefined,
icon: flatNavigationMenuItem.icon ?? undefined,
position: flatNavigationMenuItem.position,
workspaceId: flatNavigationMenuItem.workspaceId,
applicationId: flatNavigationMenuItem.applicationId ?? undefined,
@@ -82,6 +82,7 @@ export const fromNavigationMenuItemEntityToFlatNavigationMenuItem = ({
folderId: navigationMenuItemEntity.folderId,
name: navigationMenuItemEntity.name,
link: navigationMenuItemEntity.link,
icon: navigationMenuItemEntity.icon,
position: navigationMenuItemEntity.position,
workspaceId: navigationMenuItemEntity.workspaceId,
universalIdentifier: navigationMenuItemEntity.universalIdentifier,
@@ -36,6 +36,11 @@ export class CreateNavigationMenuItemInput {
@Field(() => String, { nullable: true })
link?: string | null;
@IsOptional()
@IsString()
@Field(() => String, { nullable: true })
icon?: string | null;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
@@ -47,6 +47,10 @@ export class NavigationMenuItemDTO {
@Field(() => String, { nullable: true })
link?: string | null;
@IsOptional()
@Field(() => String, { nullable: true })
icon?: string | null;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
@@ -35,6 +35,11 @@ export class UpdateNavigationMenuItemInput {
@IsString()
@Field(() => String, { nullable: true })
link?: string | null;
@IsOptional()
@IsString()
@Field(() => String, { nullable: true })
icon?: string | null;
}
@InputType()
@@ -84,6 +84,9 @@ export class NavigationMenuItemEntity
@Column({ nullable: true, type: 'text' })
link: string | null;
@Column({ nullable: true, type: 'text' })
icon: string | null;
@ManyToOne(() => NavigationMenuItemEntity, {
onDelete: 'CASCADE',
nullable: true,
@@ -898,8 +898,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
folderId: null,
folderUniversalIdentifier: null,
name: null,
position: nextPosition,
link: null,
icon: null,
position: nextPosition,
workspaceId,
applicationId: workspaceCustomApplicationId,
applicationUniversalIdentifier:
@@ -86,6 +86,11 @@ export const seedFeatureFlags = async ({
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_MARKETPLACE_ENABLED,
workspaceId: workspaceId,
@@ -40,6 +40,7 @@ export const STANDARD_NAVIGATION_MENU_ITEMS = {
workflowsFolder: {
universalIdentifier: '20202020-b007-4b07-8b07-c0aba11c0007',
name: 'Workflows',
icon: 'IconSettingsAutomation',
position: 6,
},
workflowsFolderAllWorkflows: {
@@ -77,6 +77,7 @@ export const buildStandardFlatNavigationMenuItemMaps = ({
const workflowsFolder = createStandardNavigationMenuItemFolderFlatMetadata({
universalIdentifier: workflowsFolderDefinition.universalIdentifier,
name: workflowsFolderDefinition.name,
icon: workflowsFolderDefinition.icon,
position: workflowsFolderDefinition.position,
navigationMenuItemId: workflowsFolderId,
workspaceId,
@@ -64,8 +64,9 @@ export const createStandardNavigationMenuItemFlatMetadata = ({
folderId: null,
folderUniversalIdentifier: null,
name: null,
position,
link: null,
icon: null,
position,
createdAt: now,
updatedAt: now,
};
@@ -7,6 +7,7 @@ import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty
export const createStandardNavigationMenuItemFolderFlatMetadata = ({
universalIdentifier,
name,
icon,
position,
navigationMenuItemId,
workspaceId,
@@ -15,6 +16,7 @@ export const createStandardNavigationMenuItemFolderFlatMetadata = ({
}: {
universalIdentifier: string;
name: string;
icon?: string | null;
position: number;
navigationMenuItemId: string;
workspaceId: string;
@@ -36,8 +38,9 @@ export const createStandardNavigationMenuItemFolderFlatMetadata = ({
folderId: null,
folderUniversalIdentifier: null,
name,
position,
link: null,
icon: icon ?? null,
position,
createdAt: now,
updatedAt: now,
});
@@ -95,6 +98,7 @@ export const createStandardNavigationMenuItemFolderItemFlatMetadata = ({
folderUniversalIdentifier,
name: null,
link: null,
icon: null,
position,
createdAt: now,
updatedAt: now,
@@ -2,6 +2,7 @@ import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsT
export type NavigationMenuItemManifest = SyncableEntityOptions & {
name?: string;
icon?: string;
position: number;
viewUniversalIdentifier?: string;
link?: string;