Navbar customization v2 (#18026)

Adds color support for navigation menu items.

---------

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: Charles Bochet <charles@twenty.com>
This commit is contained in:
Abdul Rahman
2026-02-28 16:18:28 +05:30
committed by GitHub
parent 63f17eec2c
commit 9fe2a07c55
94 changed files with 1204 additions and 474 deletions
@@ -1063,6 +1063,7 @@ export type CreateLogicFunctionFromSourceInput = {
};
export type CreateNavigationMenuItemInput = {
color?: InputMaybe<Scalars['String']>;
folderId?: InputMaybe<Scalars['UUID']>;
icon?: InputMaybe<Scalars['String']>;
link?: InputMaybe<Scalars['String']>;
@@ -3301,6 +3302,7 @@ export type NativeModelCapabilities = {
export type NavigationMenuItem = {
__typename?: 'NavigationMenuItem';
applicationId?: Maybe<Scalars['UUID']>;
color?: Maybe<Scalars['String']>;
createdAt: Scalars['DateTime'];
folderId?: Maybe<Scalars['UUID']>;
icon?: Maybe<Scalars['String']>;
@@ -4644,6 +4646,7 @@ export type UpdateLogicFunctionFromSourceInputUpdates = {
};
export type UpdateNavigationMenuItemInput = {
color?: InputMaybe<Scalars['String']>;
folderId?: InputMaybe<Scalars['UUID']>;
icon?: InputMaybe<Scalars['String']>;
link?: InputMaybe<Scalars['String']>;
@@ -5972,42 +5975,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, icon?: 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, color?: 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, 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 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, color?: 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, icon?: 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, color?: 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, icon?: 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, color?: 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, icon?: 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, color?: 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, 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 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, color?: 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, 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 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, color?: 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, universalIdentifier: 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, universalIdentifier: 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 }> };
@@ -7568,6 +7571,7 @@ export const NavigationMenuItemFieldsFragmentDoc = gql`
name
link
icon
color
position
applicationId
createdAt
@@ -1,4 +1,3 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useIcons } from 'twenty-ui/display';
@@ -6,24 +5,22 @@ 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 { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
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 { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { IconPicker } from '@/ui/input/components/IconPicker';
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';
const StyledClickableIconWrapper = styled.div`
cursor: pointer;
`;
export const CommandMenuFolderInfo = () => {
const theme = useTheme();
const { t } = useLingui();
const { getIcon } = useIcons();
const commandMenuPageInfo = useAtomStateValue(commandMenuPageInfoState);
@@ -83,15 +80,10 @@ export const CommandMenuFolderInfo = () => {
}
clickableComponent={
<StyledClickableIconWrapper>
<StyledNavigationMenuItemIconContainer
$backgroundColor={getNavigationMenuItemIconColors(theme).folder}
>
<FolderIconComponent
size={theme.spacing(3.5)}
color={theme.grayScale.gray1}
stroke={theme.icon.stroke.md}
/>
</StyledNavigationMenuItemIconContainer>
<NavigationMenuItemStyleIcon
Icon={FolderIconComponent}
color={selectedItem.color}
/>
</StyledClickableIconWrapper>
}
/>
@@ -112,6 +104,7 @@ export const CommandMenuFolderInfo = () => {
onFocus={() => setCommandMenuShouldFocusTitleInput(false)}
/>
}
label={t`Folder`}
/>
);
};
@@ -67,7 +67,7 @@ export const CommandMenuItem = ({
})
: undefined
}
focused={isSelectedItemId}
focused={!disabled && isSelectedItemId}
RightComponent={RightComponent}
hasSubMenu={hasSubMenu}
isSubMenuOpened={isSubMenuOpened}
@@ -1,22 +1,19 @@
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { IconLink } 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 { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
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 { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
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';
export const CommandMenuLinkInfo = () => {
const theme = useTheme();
const { t } = useLingui();
const commandMenuPageInfo = useAtomStateValue(commandMenuPageInfoState);
const [
@@ -64,15 +61,10 @@ export const CommandMenuLinkInfo = () => {
return (
<CommandMenuPageInfoLayout
icon={
<StyledNavigationMenuItemIconContainer
$backgroundColor={getNavigationMenuItemIconColors(theme).link}
>
<IconLink
size={theme.spacing(3.5)}
color={theme.grayScale.gray1}
stroke={theme.icon.stroke.md}
/>
</StyledNavigationMenuItemIconContainer>
<NavigationMenuItemStyleIcon
Icon={IconLink}
color={selectedItem.color}
/>
}
title={
<TitleInput
@@ -90,7 +82,7 @@ export const CommandMenuLinkInfo = () => {
onFocus={() => setCommandMenuShouldFocusTitleInput(false)}
/>
}
label={t`link`}
label={t`Link`}
/>
);
};
@@ -6,11 +6,15 @@ import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/Naviga
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 { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
import { ViewKey } from '@/views/types/ViewKey';
export const CommandMenuObjectViewRecordInfo = () => {
const { t } = useLingui();
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const { selectedItemLabel } = useSelectedNavigationMenuItemEditItemLabel();
const { selectedItemObjectMetadata } =
useSelectedNavigationMenuItemEditItemObjectMetadata();
const processedItem =
selectedItem && selectedItem.itemType !== NavigationMenuItemType.FOLDER
@@ -30,11 +34,12 @@ export const CommandMenuObjectViewRecordInfo = () => {
return null;
}
const label = isViewOrRecord
? processedItem.itemType === NavigationMenuItemType.RECORD
? t`record`
: t`view`
: t`object`;
const label =
processedItem.itemType === NavigationMenuItemType.RECORD
? selectedItemObjectMetadata?.labelSingular
: processedItem.viewKey === ViewKey.Index
? t`Object`
: t`View`;
return (
<CommandMenuPageInfoLayout
@@ -1,12 +1,16 @@
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import {
IconColumnInsertRight,
OverflowingTextWithTooltip,
} from 'twenty-ui/display';
import { CommandMenuAskAIInfo } from '@/command-menu/components/CommandMenuAskAIInfo';
import { CommandMenuFolderInfo } from '@/command-menu/components/CommandMenuFolderInfo';
import { CommandMenuLinkInfo } from '@/command-menu/components/CommandMenuLinkInfo';
import { CommandMenuMultipleRecordsInfo } from '@/command-menu/components/CommandMenuMultipleRecordsInfo';
import { CommandMenuObjectViewRecordInfo } from '@/command-menu/components/CommandMenuObjectViewRecordInfo';
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
import { CommandMenuPageLayoutInfo } from '@/command-menu/components/CommandMenuPageLayoutInfo';
import { CommandMenuRecordInfo } from '@/command-menu/components/CommandMenuRecordInfo';
import { CommandMenuWorkflowStepInfo } from '@/command-menu/components/CommandMenuWorkflowStepInfo';
@@ -16,6 +20,7 @@ import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-ite
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { CommandMenuPages } from 'twenty-shared/types';
import { useTheme } from '@emotion/react';
import { type CommandMenuContextChipProps } from './CommandMenuContextChip';
const StyledPageTitle = styled.div`
@@ -29,6 +34,7 @@ type CommandMenuPageInfoProps = {
};
export const CommandMenuPageInfo = ({ pageChip }: CommandMenuPageInfoProps) => {
const theme = useTheme();
const selectedNavigationMenuItemInEditMode = useAtomStateValue(
selectedNavigationMenuItemInEditModeState,
);
@@ -121,6 +127,20 @@ export const CommandMenuPageInfo = ({ pageChip }: CommandMenuPageInfoProps) => {
return <CommandMenuAskAIInfo />;
}
if (pageChip.page?.page === CommandMenuPages.NavigationMenuAddItem) {
return (
<CommandMenuPageInfoLayout
icon={
<IconColumnInsertRight
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
}
title={<OverflowingTextWithTooltip text={pageChip.text ?? ''} />}
/>
);
}
return (
<StyledPageTitle>
<OverflowingTextWithTooltip text={pageChip.text ?? ''} />
@@ -12,9 +12,12 @@ const StyledSubViewContainer = styled.div`
`;
const StyledSearchContainer = styled.div`
align-items: center;
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
display: flex;
height: ${({ theme }) => theme.spacing(10)};
min-width: 0;
padding: ${({ theme }) => theme.spacing(2, 3)};
padding-inline: ${({ theme }) => theme.spacing(3)};
`;
const StyledSearchInput = styled.input`
@@ -22,8 +25,7 @@ const StyledSearchInput = styled.input`
border: none;
border-radius: ${({ theme }) => theme.border.radius.sm};
box-sizing: border-box;
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
padding: 0;
width: 100%;
outline: none;
@@ -4,14 +4,17 @@ import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchS
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
import { emitSidePanelOpenEvent } from '@/ui/layout/right-drawer/utils/emitSidePanelOpenEvent';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { t } from '@lingui/core/macro';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconDotsVertical } from 'twenty-ui/display';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import { IconColumnInsertRight, IconDotsVertical } from 'twenty-ui/display';
export const useCommandMenu = () => {
const store = useStore();
@@ -38,13 +41,35 @@ export const useCommandMenu = () => {
const openCommandMenu = useCallback(() => {
emitSidePanelOpenEvent();
closeAnyOpenDropdown();
navigateCommandMenu({
page: CommandMenuPages.Root,
pageTitle: t`Command Menu`,
pageIcon: IconDotsVertical,
resetNavigationStack: true,
});
}, [closeAnyOpenDropdown, navigateCommandMenu]);
const isNavigationMenuInEditMode = store.get(
isNavigationMenuInEditModeState.atom,
);
const selectedNavigationItemId = store.get(
selectedNavigationMenuItemInEditModeState.atom,
);
if (isNavigationMenuInEditMode && isDefined(selectedNavigationItemId)) {
navigateCommandMenu({
page: CommandMenuPages.NavigationMenuItemEdit,
pageTitle: t`Edit`,
pageIcon: IconDotsVertical,
resetNavigationStack: true,
});
} else if (isNavigationMenuInEditMode) {
navigateCommandMenu({
page: CommandMenuPages.NavigationMenuAddItem,
pageTitle: t`New sidebar item`,
pageIcon: IconColumnInsertRight,
resetNavigationStack: true,
});
} else {
navigateCommandMenu({
page: CommandMenuPages.Root,
pageTitle: t`Command Menu`,
pageIcon: IconDotsVertical,
resetNavigationStack: true,
});
}
}, [closeAnyOpenDropdown, navigateCommandMenu, store]);
const toggleCommandMenu = useCallback(() => {
const isCommandMenuOpened = store.get(isCommandMenuOpenedState.atom);
@@ -0,0 +1,93 @@
import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuItemDropdown';
import { useUpdateNavigationMenuItemInDraft } from '@/navigation-menu-item/hooks/useUpdateNavigationMenuItemInDraft';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { capitalize } from 'twenty-shared/utils';
import { IconColorSwatch } from 'twenty-ui/display';
import {
DEFAULT_COLOR_LABELS,
MenuItemSelectColor,
} from 'twenty-ui/navigation';
import { MAIN_COLOR_NAMES, type ThemeColor } from 'twenty-ui/theme';
const NAVIGATION_MENU_ITEM_COLOR_DROPDOWN_ID = 'navigation-menu-item-color';
const StyledMenuStyleText = styled.span`
color: ${({ theme }) => theme.font.color.light};
font-size: 13px;
`;
type CommandMenuEditColorOptionProps = {
navigationMenuItemId: string;
color: ThemeColor;
};
export const CommandMenuEditColorOption = ({
navigationMenuItemId,
color,
}: CommandMenuEditColorOptionProps) => {
const { t } = useLingui();
const { updateNavigationMenuItemInDraft } =
useUpdateNavigationMenuItemInDraft();
const { closeDropdown } = useCloseDropdown();
const [searchValue, setSearchValue] = useState('');
const themeColor = color ?? 'gray';
const colorLabel = DEFAULT_COLOR_LABELS[themeColor] ?? capitalize(themeColor);
const query = searchValue.trim().toLowerCase();
const filteredColorNames = isNonEmptyString(query)
? MAIN_COLOR_NAMES.filter(
(colorName) =>
colorName.toLowerCase().includes(query) ||
(DEFAULT_COLOR_LABELS[colorName] ?? '').toLowerCase().includes(query),
)
: MAIN_COLOR_NAMES;
const handleSelectColor = (selectedColor: ThemeColor) => {
updateNavigationMenuItemInDraft(navigationMenuItemId, {
color: selectedColor,
});
closeDropdown(NAVIGATION_MENU_ITEM_COLOR_DROPDOWN_ID);
};
return (
<CommandMenuItemDropdown
id="edit-navigation-menu-item-color"
label={t`Color`}
Icon={IconColorSwatch}
dropdownId={NAVIGATION_MENU_ITEM_COLOR_DROPDOWN_ID}
dropdownPlacement="bottom-start"
RightComponent={<StyledMenuStyleText>{colorLabel}</StyledMenuStyleText>}
dropdownComponents={
<DropdownContent>
<DropdownMenuSearchInput
placeholder={t`Search colors...`}
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{filteredColorNames.map((colorName) => (
<MenuItemSelectColor
key={colorName}
onClick={() => handleSelectColor(colorName)}
color={colorName}
selected={colorName === themeColor}
colorLabels={DEFAULT_COLOR_LABELS}
/>
))}
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
);
};
@@ -1,12 +1,15 @@
import { useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { IconFolderPlus } from 'twenty-ui/display';
import { useState } from 'react';
import { useIcons } from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
import { useFolderPickerSelectionData } from '@/command-menu/pages/navigation-menu-item/hooks/useFolderPickerSelectionData';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
@@ -18,6 +21,7 @@ export const CommandMenuEditFolderPickerSubView = ({
onBack,
}: CommandMenuEditFolderPickerSubViewProps) => {
const { t } = useLingui();
const { getIcon } = useIcons();
const [searchValue, setSearchValue] = useState('');
const { foldersToShow, includeNoFolderOption, handleSelectFolder } =
useFolderPickerSelectionData({ onCloseSubView: onBack });
@@ -64,20 +68,30 @@ export const CommandMenuEditFolderPickerSubView = ({
/>
</SelectableListItem>
)}
{filteredFolders.map((folder) => (
<SelectableListItem
key={folder.id}
itemId={folder.id}
onEnter={() => handleSelectFolder(folder.id)}
>
<CommandMenuItem
Icon={IconFolderPlus}
label={folder.name}
id={folder.id}
onClick={() => handleSelectFolder(folder.id)}
/>
</SelectableListItem>
))}
{filteredFolders.map((folder) => {
const FolderIcon = getIcon(folder.icon ?? FOLDER_ICON_DEFAULT);
const folderColor =
folder.color ?? DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER;
return (
<SelectableListItem
key={folder.id}
itemId={folder.id}
onEnter={() => handleSelectFolder(folder.id)}
>
<CommandMenuItem
LeftComponent={
<NavigationMenuItemStyleIcon
Icon={FolderIcon}
color={folderColor}
/>
}
label={folder.name}
id={folder.id}
onClick={() => handleSelectFolder(folder.id)}
/>
</SelectableListItem>
);
})}
</CommandGroup>
</CommandMenuList>
</CommandMenuSubViewWithSearch>
@@ -5,13 +5,15 @@ import { getAbsoluteUrl } from 'twenty-shared/utils';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuEditColorOption } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditColorOption';
import {
type OrganizeActionsProps,
CommandMenuEditOrganizeActions,
} from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOrganizeActions';
import { getOrganizeActionsSelectableItemIds } from '@/command-menu/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
import { CommandMenuEditOwnerSection } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOwnerSection';
import { getOrganizeActionsSelectableItemIds } from '@/command-menu/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 CommandMenuEditLinkItemViewProps = OrganizeActionsProps & {
@@ -40,6 +42,10 @@ export const CommandMenuEditLinkItemView = ({
return (
<CommandMenuList commandGroups={[]} selectableItemIds={selectableItemIds}>
<CommandGroup heading={t`Customize`}>
<CommandMenuEditColorOption
navigationMenuItemId={selectedItem.id}
color={parseThemeColor(selectedItem.color)}
/>
<TextInput
fullWidth
placeholder="www.google.com"
@@ -1,12 +1,18 @@
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuEditColorOption } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditColorOption';
import {
type OrganizeActionsProps,
CommandMenuEditOrganizeActions,
} from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOrganizeActions';
import { getOrganizeActionsSelectableItemIds } from '@/command-menu/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor';
import { useLingui } from '@lingui/react/macro';
type CommandMenuEditObjectViewBaseProps = OrganizeActionsProps & {
onOpenFolderPicker: () => void;
showColorOption?: boolean;
};
export const CommandMenuEditObjectViewBase = ({
@@ -18,11 +24,22 @@ export const CommandMenuEditObjectViewBase = ({
onRemove,
onAddBefore,
onAddAfter,
showColorOption = false,
}: CommandMenuEditObjectViewBaseProps) => {
const { t } = useLingui();
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const selectableItemIds = getOrganizeActionsSelectableItemIds(true);
return (
<CommandMenuList commandGroups={[]} selectableItemIds={selectableItemIds}>
{showColorOption && selectedItem && (
<CommandGroup heading={t`Customize`}>
<CommandMenuEditColorOption
navigationMenuItemId={selectedItem.id}
color={parseThemeColor(selectedItem.color)}
/>
</CommandGroup>
)}
<CommandMenuEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
@@ -1,4 +1,7 @@
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuEditColorOption } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditColorOption';
import { CommandMenuEditFolderPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditFolderPickerSubView';
import { CommandMenuEditLinkItemView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditLinkItemView';
import { CommandMenuEditObjectViewBase } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditObjectViewBase';
@@ -7,16 +10,24 @@ import { CommandMenuEditOwnerSection } from '@/command-menu/pages/navigation-men
import { useNavigationMenuItemEditOrganizeActions } from '@/command-menu/pages/navigation-menu-item/hooks/useNavigationMenuItemEditOrganizeActions';
import { getOrganizeActionsSelectableItemIds } from '@/command-menu/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';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemLabel';
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
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 { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { ViewKey } from '@/views/types/ViewKey';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { IconPlus } from 'twenty-ui/display';
const ADD_ITEM_TO_FOLDER_ACTION_ID = 'add-item-to-folder';
const StyledCommandMenuPlaceholder = styled.p`
color: ${({ theme }) => theme.font.color.tertiary};
@@ -40,7 +51,7 @@ export const CommandMenuNavigationMenuItemEditPage = () => {
const selectedItemType = selectedItem?.itemType ?? null;
const [isFolderPickerOpen, setIsFolderPickerOpen] = useState(false);
const setFolderPicker = () => setIsFolderPickerOpen(true);
const openFolderPicker = () => setIsFolderPickerOpen(true);
const {
canMoveUp,
@@ -53,6 +64,25 @@ export const CommandMenuNavigationMenuItemEditPage = () => {
} = useNavigationMenuItemEditOrganizeActions();
const { updateLinkInDraft } = useUpdateLinkInDraft();
const { openAddItemToFolderPage } = useOpenAddItemToFolderPage();
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
const handleAddItemToFolder = () => {
if (
!selectedItem ||
selectedItem.itemType !== NavigationMenuItemType.FOLDER
) {
return;
}
const folderItemCount = workspaceNavigationMenuItems.filter(
(item) => (item.folderId ?? null) === selectedItem.id,
).length;
openAddItemToFolderPage({
targetFolderId: selectedItem.id,
targetIndex: folderItemCount,
resetNavigationStack: false,
});
};
if (!selectedNavigationMenuItemInEditMode || !selectedItemLabel) {
return (
@@ -77,7 +107,7 @@ export const CommandMenuNavigationMenuItemEditPage = () => {
if (!selectedItemObjectMetadata) return null;
return (
<CommandMenuEditObjectViewBase
onOpenFolderPicker={setFolderPicker}
onOpenFolderPicker={openFolderPicker}
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
@@ -85,6 +115,11 @@ export const CommandMenuNavigationMenuItemEditPage = () => {
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
showColorOption={
selectedItem &&
'viewKey' in selectedItem &&
selectedItem.viewKey === ViewKey.Index
}
/>
);
case NavigationMenuItemType.LINK:
@@ -97,7 +132,7 @@ export const CommandMenuNavigationMenuItemEditPage = () => {
key={selectedItem.id}
selectedItem={selectedItem}
onUpdateLink={(linkId, link) => updateLinkInDraft(linkId, { link })}
onOpenFolderPicker={setFolderPicker}
onOpenFolderPicker={openFolderPicker}
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
@@ -113,8 +148,31 @@ export const CommandMenuNavigationMenuItemEditPage = () => {
return (
<CommandMenuList
commandGroups={[]}
selectableItemIds={getOrganizeActionsSelectableItemIds(false)}
selectableItemIds={[
ADD_ITEM_TO_FOLDER_ACTION_ID,
...getOrganizeActionsSelectableItemIds(false),
]}
>
{selectedItem && (
<CommandGroup heading={t`Customize`}>
<SelectableListItem
itemId={ADD_ITEM_TO_FOLDER_ACTION_ID}
onEnter={handleAddItemToFolder}
>
<CommandMenuItem
Icon={IconPlus}
label={t`Add item to folder`}
id={ADD_ITEM_TO_FOLDER_ACTION_ID}
onClick={handleAddItemToFolder}
hasSubMenu
/>
</SelectableListItem>
<CommandMenuEditColorOption
navigationMenuItemId={selectedItem.id}
color={parseThemeColor(selectedItem.color)}
/>
</CommandGroup>
)}
<CommandMenuEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
@@ -133,6 +191,14 @@ export const CommandMenuNavigationMenuItemEditPage = () => {
commandGroups={[]}
selectableItemIds={getOrganizeActionsSelectableItemIds(true)}
>
{selectedItem && (
<CommandGroup heading={t`Customize`}>
<CommandMenuEditColorOption
navigationMenuItemId={selectedItem.id}
color={parseThemeColor(selectedItem.color)}
/>
</CommandGroup>
)}
<CommandMenuEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
@@ -142,7 +208,7 @@ export const CommandMenuNavigationMenuItemEditPage = () => {
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
showMoveToFolder
onMoveToFolder={setFolderPicker}
onMoveToFolder={openFolderPicker}
/>
</CommandMenuList>
);
@@ -1,10 +1,11 @@
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import {
IconAddressBook,
IconCube,
Avatar,
IconBuildingSkyscraper,
IconFolder,
IconLink,
IconList,
IconTable,
} from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
@@ -15,8 +16,9 @@ import { CommandMenuItemWithAddToNavigationDrag } from '@/command-menu/component
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { useAddFolderToNavigationMenu } from '@/command-menu/pages/navigation-menu-item/hooks/useAddFolderToNavigationMenu';
import { useAddLinkToNavigationMenu } from '@/command-menu/pages/navigation-menu-item/hooks/useAddLinkToNavigationMenu';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type CommandMenuNewSidebarItemMainMenuProps = {
onSelectObject: () => void;
@@ -30,6 +32,7 @@ export const CommandMenuNewSidebarItemMainMenu = ({
onSelectRecord,
}: CommandMenuNewSidebarItemMainMenuProps) => {
const { t } = useLingui();
const theme = useTheme();
const { handleAddFolder } = useAddFolderToNavigationMenu();
const { handleAddLink } = useAddLinkToNavigationMenu();
@@ -46,7 +49,12 @@ export const CommandMenuNewSidebarItemMainMenu = ({
<CommandMenuAddToNavDraggablePlaceholder index={0}>
<SelectableListItem itemId="object" onEnter={onSelectObject}>
<CommandMenuItem
Icon={IconCube}
Icon={() => (
<NavigationMenuItemStyleIcon
Icon={IconBuildingSkyscraper}
color="blue"
/>
)}
label={t`Object`}
id="object"
hasSubMenu={true}
@@ -57,7 +65,12 @@ export const CommandMenuNewSidebarItemMainMenu = ({
<CommandMenuAddToNavDraggablePlaceholder index={1}>
<SelectableListItem itemId="view" onEnter={onSelectView}>
<CommandMenuItem
Icon={IconList}
Icon={() => (
<NavigationMenuItemStyleIcon
Icon={IconTable}
color="gray"
/>
)}
label={t`View`}
id="view"
hasSubMenu={true}
@@ -68,7 +81,13 @@ export const CommandMenuNewSidebarItemMainMenu = ({
<CommandMenuAddToNavDraggablePlaceholder index={2}>
<SelectableListItem itemId="record" onEnter={onSelectRecord}>
<CommandMenuItem
Icon={IconAddressBook}
Icon={() => (
<Avatar
placeholder="L"
type="rounded"
backgroundColor={theme.color.green4}
/>
)}
label={t`Record`}
id="record"
hasSubMenu={true}
@@ -6,6 +6,7 @@ import { CommandMenuObjectPickerSubView } from '@/command-menu/pages/navigation-
import { CommandMenuSystemObjectPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuSystemObjectPickerSubView';
import { getAvailableObjectMetadataForNewSidebarItem } from '@/command-menu/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem';
import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddObjectToNavigationMenuDraft';
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
@@ -40,8 +41,11 @@ export const CommandMenuNewSidebarItemObjectFlow = ({
const setAddMenuItemInsertionContext = useSetAtomState(
addMenuItemInsertionContextState,
);
const { views, objectMetadataIdsWithIndexView } =
useNavigationMenuObjectMetadataFromDraft(currentDraft);
const {
views,
objectMetadataIdsWithIndexView,
objectMetadataIdsInWorkspace,
} = useNavigationMenuObjectMetadataFromDraft(currentDraft);
const objectMetadataIdsWithDisplayableViews = new Set(
views
@@ -61,12 +65,16 @@ export const CommandMenuNewSidebarItemObjectFlow = ({
objectMetadataItem: ObjectMetadataItem,
defaultViewId: string,
) => {
if (objectMetadataIdsInWorkspace.has(objectMetadataItem.id)) {
return;
}
addObjectToDraft(
objectMetadataItem.id,
defaultViewId,
currentDraft,
addMenuItemInsertionContext?.targetFolderId,
addMenuItemInsertionContext?.targetIndex,
getStandardObjectIconColor(objectMetadataItem.nameSingular),
);
setAddMenuItemInsertionContext(null);
closeCommandMenu();
@@ -1,4 +1,3 @@
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { IconSettings, useIcons } from 'twenty-ui/display';
@@ -7,8 +6,8 @@ import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
import { IconWithBackground } from '@/navigation-menu-item/components/IconWithBackground';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
@@ -32,9 +31,7 @@ export const CommandMenuNewSidebarItemViewObjectPickerSubView = ({
showSystemObjectsOption = true,
}: CommandMenuNewSidebarItemViewObjectPickerSubViewProps) => {
const { t } = useLingui();
const theme = useTheme();
const { getIcon } = useIcons();
const iconColors = getNavigationMenuItemIconColors(theme);
const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } =
useFilteredPickerItems({
items: objects,
@@ -68,12 +65,12 @@ export const CommandMenuNewSidebarItemViewObjectPickerSubView = ({
onEnter={() => onSelectObject(objectMetadataItem)}
>
<CommandMenuItem
Icon={({ size, stroke }) => (
<IconWithBackground
Icon={() => (
<NavigationMenuItemStyleIcon
Icon={getIcon(objectMetadataItem.icon)}
backgroundColor={iconColors.object}
size={size}
stroke={stroke}
color={getStandardObjectIconColor(
objectMetadataItem.nameSingular,
)}
/>
)}
label={objectMetadataItem.labelPlural}
@@ -86,7 +83,7 @@ export const CommandMenuNewSidebarItemViewObjectPickerSubView = ({
{showSystemObjectsOption && (
<SelectableListItem itemId="system" onEnter={onOpenSystemPicker}>
<CommandMenuItem
Icon={IconSettings}
Icon={() => <NavigationMenuItemStyleIcon Icon={IconSettings} />}
label={t`System objects`}
id="system"
hasSubMenu={true}
@@ -1,7 +1,3 @@
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useIcons } from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuAddToNavDroppable } from '@/command-menu/components/CommandMenuAddToNavDroppable';
import { CommandMenuItemWithAddToNavigationDrag } from '@/command-menu/components/CommandMenuItemWithAddToNavigationDrag';
@@ -9,18 +5,23 @@ import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
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 { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { type View } from '@/views/types/View';
import { ViewKey } from '@/views/types/ViewKey';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
type CommandMenuNewSidebarItemViewPickerSubViewProps = {
selectedObjectMetadataIdForView: string;
@@ -80,6 +81,9 @@ export const CommandMenuNewSidebarItemViewPickerSubView = ({
currentDraft,
addMenuItemInsertionContext?.targetFolderId ?? null,
addMenuItemInsertionContext?.targetIndex,
isDefined(selectedObjectMetadataItem)
? getStandardObjectIconColor(selectedObjectMetadataItem.nameSingular)
: undefined,
);
setAddMenuItemInsertionContext(null);
closeCommandMenu();
@@ -118,6 +122,9 @@ export const CommandMenuNewSidebarItemViewPickerSubView = ({
selectedObjectMetadataItem.icon,
)}
ViewIcon={getIcon(view.icon)}
objectColor={getStandardObjectIconColor(
selectedObjectMetadataItem.nameSingular,
)}
/>
) : undefined
}
@@ -1,4 +1,3 @@
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { useIcons } from 'twenty-ui/display';
@@ -6,9 +5,9 @@ import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
import { IconWithBackground } from '@/navigation-menu-item/components/IconWithBackground';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
@@ -28,9 +27,7 @@ export const CommandMenuNewSidebarItemViewSystemSubView = ({
onSelectObject,
}: CommandMenuNewSidebarItemViewSystemSubViewProps) => {
const { t } = useLingui();
const theme = useTheme();
const { getIcon } = useIcons();
const iconColors = getNavigationMenuItemIconColors(theme);
const {
filteredItems: filteredSystemObjectMetadataItems,
selectableItemIds,
@@ -67,12 +64,12 @@ export const CommandMenuNewSidebarItemViewSystemSubView = ({
onEnter={() => onSelectObject(objectMetadataItem)}
>
<CommandMenuItem
Icon={({ size, stroke }) => (
<IconWithBackground
Icon={() => (
<NavigationMenuItemStyleIcon
Icon={getIcon(objectMetadataItem.icon)}
backgroundColor={iconColors.object}
size={size}
stroke={stroke}
color={getStandardObjectIconColor(
objectMetadataItem.nameSingular,
)}
/>
)}
label={objectMetadataItem.labelPlural}
@@ -1,12 +1,14 @@
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuItemWithAddToNavigationDrag } from '@/command-menu/components/CommandMenuItemWithAddToNavigationDrag';
import { IconWithBackground } from '@/navigation-menu-item/components/IconWithBackground';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
@@ -28,28 +30,38 @@ export const CommandMenuObjectMenuItem = ({
variant,
dragIndex,
}: CommandMenuObjectMenuItemProps) => {
const theme = useTheme();
const { t } = useLingui();
const { getIcon } = useIcons();
const iconColors = getNavigationMenuItemIconColors(theme);
const { currentDraft } = useDraftNavigationMenuItems();
const { objectMetadataIdsInWorkspace } =
useNavigationMenuObjectMetadataFromDraft(currentDraft);
const isAlreadyInNavbar = objectMetadataIdsInWorkspace.has(
objectMetadataItem.id,
);
const defaultViewId = useAtomFamilySelectorValue(
coreIndexViewIdFromObjectMetadataItemFamilySelector,
{ objectMetadataItemId: objectMetadataItem.id },
);
const Icon = getIcon(objectMetadataItem.icon);
const isDisabled = !isDefined(defaultViewId);
const iconColor = getStandardObjectIconColor(objectMetadataItem.nameSingular);
const isDisabled = isAlreadyInNavbar || !isDefined(defaultViewId);
const handleClick = () => {
if (!defaultViewId) {
if (isDisabled || !defaultViewId) {
return;
}
onSelect(objectMetadataItem, defaultViewId);
};
const styledIcon = () => (
<NavigationMenuItemStyleIcon Icon={Icon} color={iconColor} />
);
return (
<SelectableListItem itemId={objectMetadataItem.id} onEnter={handleClick}>
{variant === 'add' && !isDisabled ? (
<CommandMenuItemWithAddToNavigationDrag
icon={Icon}
icon={styledIcon}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
onClick={handleClick}
@@ -63,18 +75,12 @@ export const CommandMenuObjectMenuItem = ({
/>
) : (
<CommandMenuItem
Icon={({ size, stroke }) => (
<IconWithBackground
Icon={Icon}
backgroundColor={iconColors.object}
size={size}
stroke={stroke}
/>
)}
Icon={styledIcon}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
onClick={handleClick}
disabled={isDisabled}
description={isAlreadyInNavbar ? t`Already in navbar` : undefined}
/>
)}
</SelectableListItem>
@@ -1,11 +1,10 @@
import { useTheme } from '@emotion/react';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuObjectMenuItem } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuObjectMenuItem';
import { IconWithBackground } from '@/navigation-menu-item/components/IconWithBackground';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
@@ -29,9 +28,7 @@ export const CommandMenuObjectPickerItem = ({
objectMenuItemVariant = 'edit',
dragIndex,
}: CommandMenuObjectPickerItemProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const iconColors = getNavigationMenuItemIconColors(theme);
if (isViewItem && isDefined(onSelectObjectForViewEdit)) {
return (
@@ -40,12 +37,12 @@ export const CommandMenuObjectPickerItem = ({
onEnter={() => onSelectObjectForViewEdit(objectMetadataItem)}
>
<CommandMenuItem
Icon={({ size, stroke }) => (
<IconWithBackground
Icon={() => (
<NavigationMenuItemStyleIcon
Icon={getIcon(objectMetadataItem.icon)}
backgroundColor={iconColors.object}
size={size}
stroke={stroke}
color={getStandardObjectIconColor(
objectMetadataItem.nameSingular,
)}
/>
)}
label={objectMetadataItem.labelPlural}
@@ -9,6 +9,7 @@ import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
import { CommandMenuObjectPickerItem } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuObjectPickerItem';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
@@ -72,10 +73,10 @@ export const CommandMenuObjectPickerSubView = ({
<CommandMenuAddToNavDraggablePlaceholder index={filteredItems.length}>
<SelectableListItem itemId="system" onEnter={onOpenSystemPicker}>
<CommandMenuItem
Icon={IconSettings}
Icon={() => <NavigationMenuItemStyleIcon Icon={IconSettings} />}
label={t`System objects`}
id="system"
hasSubMenu={true}
hasSubMenu
onClick={onOpenSystemPicker}
/>
</SelectableListItem>
@@ -83,10 +84,10 @@ export const CommandMenuObjectPickerSubView = ({
) : (
<SelectableListItem itemId="system" onEnter={onOpenSystemPicker}>
<CommandMenuItem
Icon={IconSettings}
Icon={() => <NavigationMenuItemStyleIcon Icon={IconSettings} />}
label={t`System objects`}
id="system"
hasSubMenu={true}
hasSubMenu
onClick={onOpenSystemPicker}
/>
</SelectableListItem>
@@ -12,7 +12,7 @@ import { NavigationMenuItemType } from '@/navigation-menu-item/constants/Navigat
type FolderOption = {
id: string;
name: string;
folderId?: string;
folderId?: string | null;
};
const getDescendantFolderIds = (
@@ -1,6 +1,6 @@
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
import { IconColumnInsertRight } from 'twenty-ui/display';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
@@ -119,7 +119,7 @@ export const useNavigationMenuItemEditOrganizeActions =
navigateCommandMenu({
page: CommandMenuPages.NavigationMenuAddItem,
pageTitle: t`New sidebar item`,
pageIcon: IconPlus,
pageIcon: IconColumnInsertRight,
resetNavigationStack: true,
});
};
@@ -1,12 +1,15 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import type { ReactNode } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconGripVertical, type IconComponent } from 'twenty-ui/display';
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorLink';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
import { getIconBackgroundColorForPayload } from '@/navigation-menu-item/utils/getIconBackgroundColorForPayload';
const StyledIconSlot = styled.div<{ $hasFixedSize: boolean }>`
align-items: center;
@@ -18,8 +21,8 @@ const StyledIconSlot = styled.div<{ $hasFixedSize: boolean }>`
${({ theme, $hasFixedSize }) =>
$hasFixedSize &&
css`
height: ${theme.spacing(4.5)};
width: ${theme.spacing(4.5)};
height: ${theme.spacing(4)};
width: ${theme.spacing(4)};
`}
&:active {
@@ -30,11 +33,13 @@ const StyledIconSlot = styled.div<{ $hasFixedSize: boolean }>`
type AddToNavigationDragHandleIconProps = {
icon?: IconComponent;
customIconContent?: ReactNode;
iconColor?: string;
};
const AddToNavigationDragHandleIcon = ({
icon,
customIconContent,
iconColor,
}: AddToNavigationDragHandleIconProps) => {
const theme = useTheme();
const iconSize = theme.icon.size.md;
@@ -47,7 +52,11 @@ const AddToNavigationDragHandleIcon = ({
if (isDefined(icon)) {
const Icon = icon;
return (
<Icon size={iconSize} stroke={iconStroke} color={theme.grayScale.gray1} />
<Icon
size={iconSize}
stroke={iconStroke}
color={iconColor ?? theme.grayScale.gray1}
/>
);
}
};
@@ -66,37 +75,34 @@ export const AddToNavigationDragHandle = ({
isHovered,
}: AddToNavigationDragHandleProps) => {
const theme = useTheme();
const iconBackgroundColor = getIconBackgroundColorForPayload(payload, theme);
const hasBackgroundColor = !!iconBackgroundColor && !isHovered;
const payloadHasBackgroundColor = !!iconBackgroundColor;
const iconSize = theme.icon.size.md;
const iconStroke = theme.icon.stroke.sm;
const effectiveColor =
payload.type === 'object' && isNonEmptyString(payload.iconColor)
? payload.iconColor
: payload.type === 'folder'
? DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER
: payload.type === 'link'
? DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK
: undefined;
const hasBackgroundColor =
payload.type !== NavigationMenuItemType.RECORD &&
isDefined(effectiveColor) &&
!isHovered;
const showCustomContentWithoutWrapper = isDefined(customIconContent);
return (
<StyledIconSlot
$hasFixedSize={
payloadHasBackgroundColor || showCustomContentWithoutWrapper
}
$hasFixedSize={hasBackgroundColor || showCustomContentWithoutWrapper}
>
{isHovered ? (
<IconGripVertical
size={iconSize}
stroke={iconStroke}
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
) : showCustomContentWithoutWrapper ? (
customIconContent
) : hasBackgroundColor ? (
<StyledNavigationMenuItemIconContainer
$backgroundColor={iconBackgroundColor}
>
<AddToNavigationDragHandleIcon
icon={icon}
customIconContent={customIconContent}
/>
</StyledNavigationMenuItemIconContainer>
) : hasBackgroundColor && icon ? (
<NavigationMenuItemStyleIcon Icon={icon} color={effectiveColor} />
) : (
<AddToNavigationDragHandleIcon
icon={icon}
@@ -1,4 +1,3 @@
import { useTheme } from '@emotion/react';
import { Droppable } from '@hello-pangea/dnd';
import { useLingui } from '@lingui/react/macro';
import { useContext, useState } from 'react';
@@ -9,7 +8,6 @@ import { createPortal } from 'react-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { IconFolder, IconFolderOpen, IconHeartOff } from 'twenty-ui/display';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { LightIconButton } from 'twenty-ui/input';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { useIsMobile } from 'twenty-ui/utilities';
@@ -18,6 +16,7 @@ import { NavigationItemDropTarget } from '@/navigation-menu-item/components/Navi
import { NavigationMenuItemDroppable } from '@/navigation-menu-item/components/NavigationMenuItemDroppable';
import { NavigationMenuItemFolderNavigationDrawerItemDropdown } from '@/navigation-menu-item/components/NavigationMenuItemFolderNavigationDrawerItemDropdown';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
import { NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID } from '@/navigation-menu-item/constants/NavigationMenuItemFolderDeleteModalId';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
@@ -25,12 +24,11 @@ import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDel
import { useDeleteNavigationMenuItemFolder } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItemFolder';
import { useRenameNavigationMenuItemFolder } from '@/navigation-menu-item/hooks/useRenameNavigationMenuItemFolder';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
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';
@@ -42,9 +40,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 { currentNavigationMenuItemFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdState';
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 { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
@@ -64,8 +64,6 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
isWorkspaceFolder = false,
}: CurrentWorkspaceMemberNavigationMenuItemsProps) => {
const { t } = useLingui();
const theme = useTheme();
const iconColors = getNavigationMenuItemIconColors(theme);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const location = useLocation();
const navigate = useNavigate();
@@ -235,7 +233,7 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
<NavigationDrawerItem
label={folder.folderName}
Icon={isOpen ? IconFolderOpen : IconFolder}
iconBackgroundColor={iconColors.folder}
iconColor={DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER}
onClick={handleToggle}
rightOptions={rightOptions}
className="navigation-drawer-item"
@@ -302,6 +300,9 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
}
isDragging={isDragging}
triggerEvent="CLICK"
iconColor={getEffectiveNavigationMenuItemColor(
navigationMenuItem,
)}
/>
}
/>
@@ -5,13 +5,14 @@ import { IconHeartOff } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { NavigationItemDropTarget } from '@/navigation-menu-item/components/NavigationItemDropTarget';
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
import { NavigationMenuItemDroppable } from '@/navigation-menu-item/components/NavigationMenuItemDroppable';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/hooks/useSortedNavigationMenuItems';
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
@@ -71,6 +72,9 @@ export const CurrentWorkspaceMemberOrphanNavigationMenuItems = () => {
navigationMenuItem={navigationMenuItem}
/>
)}
iconColor={getEffectiveNavigationMenuItemColor(
navigationMenuItem,
)}
active={isLocationMatchingNavigationMenuItem(
currentPath,
currentViewPath,
@@ -1,29 +0,0 @@
import { useTheme } from '@emotion/react';
import type { IconComponent } from 'twenty-ui/display';
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
type IconWithBackgroundProps = {
Icon: IconComponent;
backgroundColor: string;
size?: number | string;
stroke?: number | string;
};
export const IconWithBackground = ({
Icon,
backgroundColor,
size,
stroke,
}: IconWithBackgroundProps) => {
const theme = useTheme();
return (
<StyledNavigationMenuItemIconContainer $backgroundColor={backgroundColor}>
<Icon
size={size ?? theme.icon.size.md}
stroke={stroke ?? theme.icon.stroke.md}
color={theme.grayScale.gray1}
/>
</StyledNavigationMenuItemIconContainer>
);
};
@@ -1,4 +1,5 @@
import { useTheme } from '@emotion/react';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { Avatar, useIcons } from 'twenty-ui/display';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
@@ -6,13 +7,15 @@ 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 { useObjectNavItemColor } from '@/navigation-menu-item/hooks/useObjectNavItemColor';
import { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/utils/get-navigation-menu-item-icon-style-from-color';
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { ViewKey } from '@/views/types/ViewKey';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const NavigationMenuItemIcon = ({
navigationMenuItem,
@@ -26,82 +29,80 @@ export const NavigationMenuItemIcon = ({
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const { Icon: StandardIcon, IconColor } = useGetStandardObjectIcon(
navigationMenuItem.objectNameSingular || '',
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;
navigationMenuItem.viewKey !== ViewKey.Index;
const objectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === navigationMenuItem.objectNameSingular,
);
const ObjectIconForView =
const objectNavItemColor = useObjectNavItemColor(
navigationMenuItem.objectNameSingular ?? '',
);
const objectIconForView =
objectMetadataItem?.icon != null
? getIcon(objectMetadataItem.icon)
: StandardIcon;
if (
const canShowViewOverlay =
isViewWithOverlay &&
isDefined(ObjectIconForView) &&
isDefined(navigationMenuItem.Icon)
) {
const ViewIcon = getIcon(navigationMenuItem.Icon);
isDefined(objectIconForView) &&
isDefined(navigationMenuItem.Icon);
if (canShowViewOverlay) {
return (
<ObjectIconWithViewOverlay
ObjectIcon={ObjectIconForView}
ViewIcon={ViewIcon}
ObjectIcon={objectIconForView}
ViewIcon={getIcon(navigationMenuItem.Icon!)}
objectColor={objectNavItemColor}
/>
);
}
const IconToUse =
StandardIcon ||
const iconToUse =
StandardIcon ??
(navigationMenuItem.Icon ? getIcon(navigationMenuItem.Icon) : undefined);
const effectiveColor =
getEffectiveNavigationMenuItemColor(navigationMenuItem);
const useStyledIcon =
isNavigationMenuItemEditingEnabled &&
!isRecord &&
isNonEmptyString(effectiveColor);
const iconStyle = useStyledIcon
? getNavigationMenuItemIconStyleFromColor(theme, effectiveColor)
: null;
const placeholderColorSeed = navigationMenuItem.targetRecordId ?? undefined;
const iconColors = getNavigationMenuItemIconColors(theme);
const iconBackgroundColor = isNavigationMenuItemEditingEnabled
? isRecord
? undefined
: isLink
? iconColors.link
: isViewWithOverlay
? iconColors.view
: iconColors.object
: undefined;
const iconColorToUse = iconBackgroundColor
? theme.grayScale.gray1
const iconColorToUse = iconStyle
? iconStyle.iconColor
: StandardIcon
? IconColor
: theme.font.color.secondary;
const avatar = (
<Avatar
size={iconBackgroundColor ? 'sm' : 'md'}
size={iconStyle ? 'sm' : 'md'}
type={navigationMenuItem.avatarType}
Icon={IconToUse}
Icon={iconToUse}
iconColor={iconColorToUse}
avatarUrl={navigationMenuItem.avatarUrl}
placeholder={navigationMenuItem.labelIdentifier}
placeholderColorSeed={placeholderColorSeed}
placeholderColorSeed={navigationMenuItem.targetRecordId ?? undefined}
/>
);
if (!iconBackgroundColor) {
if (!iconStyle) {
return avatar;
}
return (
<StyledNavigationMenuItemIconContainer
$backgroundColor={iconBackgroundColor}
$backgroundColor={iconStyle.backgroundColor}
$borderColor={iconStyle.borderColor}
>
{avatar}
</StyledNavigationMenuItemIconContainer>
@@ -2,15 +2,19 @@ import styled from '@emotion/styled';
export const StyledNavigationMenuItemIconContainer = styled.div<{
$backgroundColor?: string;
$borderColor?: string;
}>`
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.xs};
border-radius: 4px;
box-sizing: border-box;
display: flex;
flex-shrink: 0;
height: ${({ theme }) => theme.spacing(4.5)};
height: ${({ theme }) => theme.spacing(4)};
justify-content: center;
width: ${({ theme }) => theme.spacing(4.5)};
width: ${({ theme }) => theme.spacing(4)};
${({ $backgroundColor }) =>
$backgroundColor ? `background-color: ${$backgroundColor};` : ''}
${({ $borderColor }) =>
$borderColor ? `border: 1px solid ${$borderColor};` : ''}
`;
@@ -0,0 +1,30 @@
import { useTheme } from '@emotion/react';
import type { IconComponent } from 'twenty-ui/display';
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
import { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/utils/get-navigation-menu-item-icon-style-from-color';
export type NavigationMenuItemStyleIconProps = {
Icon: IconComponent;
color?: string | null;
};
export const NavigationMenuItemStyleIcon = ({
Icon,
color,
}: NavigationMenuItemStyleIconProps) => {
const theme = useTheme();
const style = getNavigationMenuItemIconStyleFromColor(theme, color);
return (
<StyledNavigationMenuItemIconContainer
$backgroundColor={style.backgroundColor}
$borderColor={style.borderColor}
>
<Icon
size={theme.icon.size.md}
stroke={theme.icon.stroke.md}
color={style.iconColor}
/>
</StyledNavigationMenuItemIconContainer>
);
};
@@ -2,34 +2,40 @@ 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';
import { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/utils/get-navigation-menu-item-icon-style-from-color';
const StyledCompositeContainer = styled.div`
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.xs};
border-radius: 4px;
box-sizing: border-box;
display: flex;
flex-shrink: 0;
height: ${({ theme }) => theme.spacing(4.5)};
height: 16px;
justify-content: center;
position: relative;
width: ${({ theme }) => theme.spacing(4.5)};
width: 16px;
`;
const StyledObjectIconWrapper = styled.div<{ $backgroundColor: string }>`
const StyledObjectIconWrapper = styled.div<{
$backgroundColor: string;
$borderColor?: string;
}>`
position: absolute;
inset: 0;
border-radius: ${({ theme }) => theme.border.radius.xs};
border-radius: 4px;
box-sizing: border-box;
background-color: ${({ $backgroundColor }) => $backgroundColor};
display: flex;
align-items: center;
justify-content: center;
${({ $borderColor }) =>
$borderColor ? `border: 1px solid ${$borderColor};` : ''}
`;
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};
border-radius: 4px;
bottom: -7px;
display: flex;
height: ${({ theme }) => theme.spacing(3.5)};
@@ -42,29 +48,37 @@ const StyledViewOverlay = styled.div<{ $backgroundColor: string }>`
export type ObjectIconWithViewOverlayProps = {
ObjectIcon: IconComponent;
ViewIcon: IconComponent;
objectColor?: string | null;
};
export const ObjectIconWithViewOverlay = ({
ObjectIcon,
ViewIcon,
objectColor,
}: ObjectIconWithViewOverlayProps) => {
const theme = useTheme();
const iconColors = getNavigationMenuItemIconColors(theme);
const objectStyle = getNavigationMenuItemIconStyleFromColor(
theme,
objectColor,
);
return (
<StyledCompositeContainer>
<StyledObjectIconWrapper $backgroundColor={iconColors.object}>
<StyledObjectIconWrapper
$backgroundColor={objectStyle.backgroundColor}
$borderColor={objectStyle.borderColor}
>
<ObjectIcon
size={theme.spacing(3.5)}
stroke={theme.icon.stroke.md}
color={theme.grayScale.gray1}
color={objectStyle.iconColor}
/>
</StyledObjectIconWrapper>
<StyledViewOverlay $backgroundColor={iconColors.view}>
<StyledViewOverlay $backgroundColor={theme.grayScale.gray4}>
<ViewIcon
size={theme.spacing(2)}
stroke={theme.icon.stroke.sm}
color={theme.grayScale.gray1}
size={theme.spacing(2.5)}
stroke={theme.icon.stroke.lg}
color={theme.grayScale.gray10}
/>
</StyledViewOverlay>
</StyledCompositeContainer>
@@ -1,7 +1,12 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useNavigate } from 'react-router-dom';
import { IconLink, IconPlus, IconTool, useIcons } from 'twenty-ui/display';
import {
IconColumnInsertRight,
IconLink,
IconPlus,
IconTool,
useIcons,
} from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
@@ -26,9 +31,8 @@ import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavi
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 { useStore } from 'jotai';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isNonEmptyString } from '@sniptt/guards';
import { useStore } from 'jotai';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -64,7 +68,6 @@ export const WorkspaceNavigationMenuItems = () => {
const setOpenNavigationMenuItemFolderIds = useSetAtomState(
openNavigationMenuItemFolderIdsState,
);
const navigate = useNavigate();
const { navigateCommandMenu } = useNavigateCommandMenu();
const { openNavigationMenuItemInCommandMenu } =
useOpenNavigationMenuItemInCommandMenu();
@@ -100,14 +103,14 @@ export const WorkspaceNavigationMenuItems = () => {
pageIcon: IconLink,
});
} else if (isDefined(objectMetadataItem)) {
const pageTitle =
item.itemType === NavigationMenuItemType.VIEW
? item.labelIdentifier
: objectMetadataItem.labelSingular;
openNavigationMenuItemInCommandMenu({
pageTitle: objectMetadataItem.labelPlural,
pageTitle,
pageIcon: getIcon(objectMetadataItem.icon),
});
if (isNonEmptyString(item.link)) {
navigate(item.link);
}
}
};
@@ -118,7 +121,7 @@ export const WorkspaceNavigationMenuItems = () => {
enterEditMode();
setSelectedNavigationMenuItemInEditMode(navigationMenuItemId);
openNavigationMenuItemInCommandMenu({
pageTitle: objectMetadataItem.labelPlural,
pageTitle: objectMetadataItem.labelSingular,
pageIcon: getIcon(objectMetadataItem.icon),
});
};
@@ -128,7 +131,7 @@ export const WorkspaceNavigationMenuItems = () => {
navigateCommandMenu({
page: CommandMenuPages.NavigationMenuAddItem,
pageTitle: t`New sidebar item`,
pageIcon: IconPlus,
pageIcon: IconColumnInsertRight,
resetNavigationStack: true,
});
};
@@ -1,13 +1,20 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
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 { isDefined } from 'twenty-shared/utils';
import {
IconChevronDown,
IconChevronRight,
IconPlus,
useIcons,
} from 'twenty-ui/display';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
import { useLocation, useNavigate } from 'react-router-dom';
import { IconChevronDown, IconChevronRight, useIcons } from 'twenty-ui/display';
import { useOpenAddItemToFolderPage } from '@/navigation-menu-item/hooks/useOpenAddItemToFolderPage';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { useIsMobile } from 'twenty-ui/utilities';
@@ -15,18 +22,17 @@ import { NavigationMenuItemDroppable } from '@/navigation-menu-item/components/N
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 { 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 { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
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 { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
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 { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
@@ -34,12 +40,11 @@ import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/compo
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';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledFolderContainer = styled.div<{ $isSelectedInEditMode: boolean }>`
border: ${({ theme, $isSelectedInEditMode }) =>
@@ -54,7 +59,6 @@ const StyledFolderDroppableContent = styled.div<{
}>`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.betweenSiblingsGap};
padding-bottom: ${({ theme, $compact }) => ($compact ? 0 : theme.spacing(2))};
`;
@@ -68,6 +72,7 @@ type WorkspaceNavigationMenuItemsFolderProps = {
folderId: string;
folderName: string;
folderIconKey?: string | null;
folderColor?: string | null;
navigationMenuItems: ProcessedNavigationMenuItem[];
isGroup: boolean;
isEditMode?: boolean;
@@ -82,6 +87,7 @@ export const WorkspaceNavigationMenuItemsFolder = ({
folderId,
folderName,
folderIconKey,
folderColor,
navigationMenuItems,
isGroup,
isEditMode = false,
@@ -93,10 +99,6 @@ export const WorkspaceNavigationMenuItemsFolder = ({
}: 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 = useAtomStateValue(objectMetadataItemsState);
const coreViews = useAtomStateValue(coreViewsState);
@@ -107,15 +109,30 @@ export const WorkspaceNavigationMenuItemsFolder = ({
const currentViewPath = location.pathname + location.search;
const isMobile = useIsMobile();
const { t } = useLingui();
const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] =
useAtomState(openNavigationMenuItemFolderIdsState);
const setCurrentNavigationMenuItemFolderId = useSetAtomState(
currentNavigationMenuItemFolderIdState,
);
const { openAddItemToFolderPage } = useOpenAddItemToFolderPage();
const isOpen = openNavigationMenuItemFolderIds.includes(folderId);
const folderContentLengthForTree =
isEditMode && isSelectedInEditMode
? navigationMenuItems.length + 1
: navigationMenuItems.length;
const handleAddMenuItemToFolder = () => {
openAddItemToFolderPage({
targetFolderId: folderId,
targetIndex: navigationMenuItems.length,
resetNavigationStack: true,
});
};
const handleToggle = () => {
if (isMobile) {
setCurrentNavigationMenuItemFolderId((prev) =>
@@ -165,8 +182,10 @@ export const WorkspaceNavigationMenuItemsFolder = ({
<NavigationDrawerItem
label={folderName}
Icon={FolderIcon}
iconBackgroundColor={
isNavigationMenuItemEditingEnabled ? iconColors.folder : undefined
iconColor={
isDefined(folderColor)
? folderColor
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER
}
onClick={handleClick}
className="navigation-drawer-item"
@@ -289,8 +308,7 @@ export const WorkspaceNavigationMenuItemsFolder = ({
}
subItemState={getNavigationSubItemLeftAdornment({
index,
arrayLength:
navigationMenuItemFolderContentLength,
arrayLength: folderContentLengthForTree,
selectedIndex: selectedNavigationMenuItemIndex,
})}
isDragging={isContextDragging}
@@ -304,6 +322,19 @@ export const WorkspaceNavigationMenuItemsFolder = ({
</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,
})}
/>
)}
</AnimatedExpandableContainer>
</StyledFolderExpandableWrapper>
</NavigationDrawerItemsCollapsableContainer>
@@ -0,0 +1 @@
export const DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER = 'orange';
@@ -0,0 +1 @@
export const DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK = 'red';
@@ -0,0 +1,3 @@
import type { ThemeColor } from 'twenty-ui/theme';
export const DEFAULT_NAV_ITEM_ICON_COLOR: ThemeColor = 'gray';
@@ -11,6 +11,7 @@ export const NAVIGATION_MENU_ITEM_FRAGMENT = gql`
name
link
icon
color
position
applicationId
createdAt
@@ -1,10 +1,11 @@
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/utils/computeInsertIndexAndPosition';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
export const useAddFolderToNavigationMenuDraft = () => {
const setNavigationMenuItemsDraft = useSetAtomState(
@@ -43,6 +44,7 @@ export const useAddFolderToNavigationMenuDraft = () => {
position,
userWorkspaceId: undefined,
name: name.trim(),
color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER,
applicationId: undefined,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
@@ -2,10 +2,11 @@ import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorLink';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/utils/computeInsertIndexAndPosition';
import { normalizeUrl } from '@/navigation-menu-item/utils/normalizeUrl';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
export const useAddLinkToNavigationMenuDraft = () => {
const setNavigationMenuItemsDraft = useSetAtomState(
@@ -47,6 +48,7 @@ export const useAddLinkToNavigationMenuDraft = () => {
userWorkspaceId: undefined,
name: label.trim() || 'Link',
link: normalizedUrl,
color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK,
applicationId: undefined,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
@@ -1,10 +1,10 @@
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
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 { computeInsertIndexAndPosition } from '@/navigation-menu-item/utils/computeInsertIndexAndPosition';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
export const useAddObjectToNavigationMenuDraft = () => {
const setNavigationMenuItemsDraft = useSetAtomState(
@@ -17,6 +17,7 @@ export const useAddObjectToNavigationMenuDraft = () => {
currentDraft: NavigationMenuItem[],
targetFolderId?: string | null,
targetIndex?: number,
color?: string | null,
): string => {
const folderId = targetFolderId ?? null;
@@ -45,6 +46,7 @@ export const useAddObjectToNavigationMenuDraft = () => {
folderId: folderId ?? undefined,
name: undefined,
applicationId: undefined,
color,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
@@ -1,10 +1,10 @@
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
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 { computeInsertIndexAndPosition } from '@/navigation-menu-item/utils/computeInsertIndexAndPosition';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
export const useAddViewToNavigationMenuDraft = () => {
const setNavigationMenuItemsDraft = useSetAtomState(
@@ -16,6 +16,7 @@ export const useAddViewToNavigationMenuDraft = () => {
currentDraft: NavigationMenuItem[],
targetFolderId?: string | null,
targetIndex?: number,
color?: string | null,
): string => {
const folderId = targetFolderId ?? null;
@@ -44,6 +45,7 @@ export const useAddViewToNavigationMenuDraft = () => {
folderId: folderId ?? undefined,
name: undefined,
applicationId: undefined,
color,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
@@ -8,7 +8,9 @@ export const useDraftNavigationMenuItemsAllFolders = () => {
currentDraft?.filter(isNavigationMenuItemFolder).map((item) => ({
id: item.id,
name: item.name ?? 'Folder',
folderId: item.folderId ?? undefined,
folderId: item.folderId,
icon: item.icon,
color: item.color,
})) ?? [];
return { allFolders };
@@ -8,6 +8,8 @@ export const useDraftNavigationMenuItemsWorkspaceFolders = () => {
(folder) => ({
id: folder.id,
name: folder.folderName,
icon: folder.icon,
color: folder.color,
}),
);
@@ -18,11 +18,13 @@ import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/i
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 { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { getObjectMetadataIdsInDraft } from '@/navigation-menu-item/utils/getObjectMetadataIdsInDraft';
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
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 { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { useStore } from 'jotai';
@@ -128,15 +130,27 @@ export const useHandleAddToNavigationDrop = () => {
return;
}
case NavigationMenuItemType.OBJECT: {
const views = coreViews.map(convertCoreViewToView);
const objectMetadataIdsInWorkspace = getObjectMetadataIdsInDraft(
currentDraft,
views,
);
if (objectMetadataIdsInWorkspace.has(payload.objectMetadataId)) {
return;
}
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === payload.objectMetadataId,
);
const newItemId = addObjectToDraft(
payload.objectMetadataId,
payload.defaultViewId,
currentDraft,
folderId,
index,
);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === payload.objectMetadataId,
payload.iconColor ??
(objectMetadataItem
? getStandardObjectIconColor(objectMetadataItem.nameSingular)
: undefined),
);
openEditForNewNavItem(newItemId, {
pageTitle: objectMetadataItem?.labelPlural ?? payload.label,
@@ -147,14 +161,22 @@ export const useHandleAddToNavigationDrop = () => {
return;
}
case NavigationMenuItemType.VIEW: {
const views = coreViews.map(convertCoreViewToView);
const view = views.find((v) => v.id === payload.viewId);
const viewObjectMetadataItem = view
? objectMetadataItems.find(
(item) => item.id === view.objectMetadataId,
)
: undefined;
const newItemId = addViewToDraft(
payload.viewId,
currentDraft,
folderId,
index,
viewObjectMetadataItem
? getStandardObjectIconColor(viewObjectMetadataItem.nameSingular)
: undefined,
);
const views = coreViews.map(convertCoreViewToView);
const view = views.find((v) => v.id === payload.viewId);
openEditForNewNavItem(newItemId, {
pageTitle: view?.name ?? payload.label,
pageIcon: view ? getIcon(view.icon) : IconFolder,
@@ -15,9 +15,16 @@ import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationM
type NavigationMenuItemFolder = {
id: string;
folderName: string;
icon?: string | null;
color?: string | null;
navigationMenuItems: ReturnType<typeof sortNavigationMenuItems>[number][];
};
type NavigationMenuItemFolderEntry = Pick<
NavigationMenuItemFolder,
'id' | 'folderName' | 'icon' | 'color'
>;
export const useNavigationMenuItemsByFolder = () => {
const coreViews = useAtomStateValue(coreViewsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
@@ -34,13 +41,18 @@ export const useNavigationMenuItemsByFolder = () => {
const { workspaceFolders, userFolders, itemsByFolderId } =
allNavigationMenuItems.reduce<{
workspaceFolders: Array<{ id: string; name: string }>;
userFolders: Array<{ id: string; name: string }>;
workspaceFolders: NavigationMenuItemFolderEntry[];
userFolders: NavigationMenuItemFolderEntry[];
itemsByFolderId: Map<string, NavigationMenuItem[]>;
}>(
(acc, item) => {
if (isNavigationMenuItemFolder(item)) {
const folderEntry = { id: item.id, name: item.name || 'Folder' };
const folderEntry: NavigationMenuItemFolderEntry = {
id: item.id,
folderName: item.name || 'Folder',
icon: item.icon ?? undefined,
color: item.color ?? undefined,
};
if (isDefined(item.userWorkspaceId)) {
acc.userFolders.push(folderEntry);
} else {
@@ -64,7 +76,7 @@ export const useNavigationMenuItemsByFolder = () => {
},
);
const buildFoldersList = (folders: Array<{ id: string; name: string }>) => {
const buildFoldersList = (folders: NavigationMenuItemFolderEntry[]) => {
const sortedFolders = [...folders].sort((a, b) => {
const folderA = allNavigationMenuItems.find((item) => item.id === a.id);
const folderB = allNavigationMenuItems.find((item) => item.id === b.id);
@@ -117,7 +129,9 @@ export const useNavigationMenuItemsByFolder = () => {
acc.push({
id: folder.id,
folderName: folder.name,
folderName: folder.folderName,
icon: folder.icon,
color: folder.color,
navigationMenuItems: sortedItems,
});
@@ -1,5 +1,6 @@
import { isDefined } from 'twenty-shared/utils';
import { getObjectMetadataIdsInDraft } from '@/navigation-menu-item/utils/getObjectMetadataIdsInDraft';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { ViewKey } from '@/views/types/ViewKey';
@@ -17,20 +18,9 @@ export const useNavigationMenuObjectMetadataFromDraft = (
const coreViews = useAtomStateValue(coreViewsState);
const views = coreViews.map(convertCoreViewToView);
const objectMetadataIdsInWorkspace = currentDraft.reduce<Set<string>>(
(ids, item) => {
const view = isDefined(item.viewId)
? views.find((view) => view.id === item.viewId)
: undefined;
if (isDefined(view)) {
ids.add(view.objectMetadataId);
}
if (isDefined(item.targetObjectMetadataId)) {
ids.add(item.targetObjectMetadataId);
}
return ids;
},
new Set<string>(),
const objectMetadataIdsInWorkspace = getObjectMetadataIdsInDraft(
currentDraft,
views,
);
const objectMetadataIdsWithIndexView = new Set(
@@ -0,0 +1,17 @@
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
import { ViewKey } from '@/views/types/ViewKey';
export const useObjectNavItemColor = (objectNameSingular: string): string => {
const items = useWorkspaceSectionItems();
const objectNavItem = items.find(
(item) =>
'viewKey' in item &&
item.viewKey === ViewKey.Index &&
item.objectNameSingular === objectNameSingular,
);
return (
(objectNavItem && 'color' in objectNavItem ? objectNavItem.color : null) ??
getStandardObjectIconColor(objectNameSingular)
);
};
@@ -0,0 +1,39 @@
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { useLingui } from '@lingui/react/macro';
import { CommandMenuPages } from 'twenty-shared/types';
import { IconColumnInsertRight } from 'twenty-ui/display';
type OpenAddItemToFolderPageParams = {
targetFolderId: string;
targetIndex: number;
resetNavigationStack?: boolean;
};
export const useOpenAddItemToFolderPage = () => {
const { t } = useLingui();
const { navigateCommandMenu } = useNavigateCommandMenu();
const setAddMenuItemInsertionContext = useSetAtomState(
addMenuItemInsertionContextState,
);
const openAddItemToFolderPage = ({
targetFolderId,
targetIndex,
resetNavigationStack = true,
}: OpenAddItemToFolderPageParams) => {
setAddMenuItemInsertionContext({
targetFolderId,
targetIndex,
});
navigateCommandMenu({
page: CommandMenuPages.NavigationMenuAddItem,
pageTitle: t`New sidebar item`,
pageIcon: IconColumnInsertRight,
resetNavigationStack,
});
};
return { openAddItemToFolderPage };
};
@@ -12,8 +12,8 @@ import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/nav
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 { useStore } from 'jotai';
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
import { useStore } from 'jotai';
export const useSaveNavigationMenuItemsDraft = () => {
const { updateNavigationMenuItem } = useUpdateNavigationMenuItem();
@@ -1,6 +1,23 @@
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
const getLabelForItemType = (
itemType: NavigationMenuItemType,
item: { name?: string | null; labelIdentifier?: string | null },
objectLabelSingular?: string | null,
): string => {
switch (itemType) {
case NavigationMenuItemType.FOLDER:
return item.name ?? 'Folder';
case NavigationMenuItemType.LINK:
return item.name ?? 'Link';
case NavigationMenuItemType.VIEW:
return item.labelIdentifier ?? objectLabelSingular ?? '';
default:
return objectLabelSingular ?? '';
}
};
export const useSelectedNavigationMenuItemEditItemLabel = () => {
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
@@ -8,11 +25,11 @@ export const useSelectedNavigationMenuItemEditItemLabel = () => {
useSelectedNavigationMenuItemEditItemObjectMetadata();
const selectedItemLabel = selectedItem
? selectedItem.itemType === NavigationMenuItemType.FOLDER
? (selectedItem.name ?? 'Folder')
: selectedItem.itemType === NavigationMenuItemType.LINK
? (selectedItem.name ?? 'Link')
: (selectedItemObjectMetadata?.labelPlural ?? '')
? getLabelForItemType(
selectedItem.itemType,
selectedItem,
selectedItemObjectMetadata?.labelSingular,
)
: null;
return { selectedItemLabel };
@@ -20,10 +20,9 @@ export const useUpdateNavigationMenuItemInDraft = () => {
setNavigationMenuItemsDraft((draft) => {
if (!draft) return draft;
return draft.map((item) => {
if (item.id !== navigationMenuItemId) return item;
return { ...item, ...updates };
});
return draft.map((item) =>
item.id === navigationMenuItemId ? { ...item, ...updates } : item,
);
});
};
@@ -3,6 +3,7 @@ export type AddToNavigationDragPayloadObject = {
objectMetadataId: string;
defaultViewId: string;
label: string;
iconColor?: string;
};
export type AddToNavigationDragPayloadView = {
@@ -0,0 +1 @@
export const COLOR_SHADE_BACKGROUND = 5;
@@ -0,0 +1 @@
export const COLOR_SHADE_BORDER = 6;
@@ -0,0 +1 @@
export const COLOR_SHADE_ICON = 11;
@@ -0,0 +1,12 @@
import type { Theme } from '@emotion/react';
import type { ThemeColor } from 'twenty-ui/theme';
export const getColorFromTheme = (
theme: Theme,
themeColor: ThemeColor,
shade: number,
): string => {
const colorMap = theme.color as unknown as Record<string, string>;
const key = `${themeColor}${shade}`;
return colorMap[key] ?? theme.tag.text[themeColor];
};
@@ -0,0 +1,10 @@
import type { Theme } from '@emotion/react';
import type { ThemeColor } from 'twenty-ui/theme';
import { getColorFromTheme } from '@/navigation-menu-item/utils/get-color-from-theme.util';
import { COLOR_SHADE_BORDER } from '@/navigation-menu-item/utils/NavigationMenuItemIconColorShadeBorder.constant';
export const getNavigationMenuItemIconBorderColor = (
theme: Theme,
themeColor: ThemeColor,
): string => getColorFromTheme(theme, themeColor, COLOR_SHADE_BORDER);
@@ -0,0 +1,25 @@
import type { Theme } from '@emotion/react';
import { DEFAULT_NAV_ITEM_ICON_COLOR } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultIconColor.constant';
import { getColorFromTheme } from '@/navigation-menu-item/utils/get-color-from-theme.util';
import { COLOR_SHADE_BACKGROUND } from '@/navigation-menu-item/utils/NavigationMenuItemIconColorShadeBackground.constant';
import { COLOR_SHADE_BORDER } from '@/navigation-menu-item/utils/NavigationMenuItemIconColorShadeBorder.constant';
import { COLOR_SHADE_ICON } from '@/navigation-menu-item/utils/NavigationMenuItemIconColorShadeIcon.constant';
import { type NavigationMenuItemIconStyle } from '@/navigation-menu-item/utils/navigation-menu-item-icon-style.type';
import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor';
export const getNavigationMenuItemIconStyleFromColor = (
theme: Theme,
color: string | null | undefined,
): NavigationMenuItemIconStyle => {
const themeColor = parseThemeColor(color ?? DEFAULT_NAV_ITEM_ICON_COLOR);
return {
backgroundColor: getColorFromTheme(
theme,
themeColor,
COLOR_SHADE_BACKGROUND,
),
iconColor: getColorFromTheme(theme, themeColor, COLOR_SHADE_ICON),
borderColor: getColorFromTheme(theme, themeColor, COLOR_SHADE_BORDER),
};
};
@@ -0,0 +1,21 @@
import { isNonEmptyString } from '@sniptt/guards';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorLink';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
export const getEffectiveNavigationMenuItemColor = (
navigationMenuItem: ProcessedNavigationMenuItem,
): string | undefined => {
if (isNonEmptyString(navigationMenuItem.color)) {
return navigationMenuItem.color;
}
if (navigationMenuItem.itemType === NavigationMenuItemType.FOLDER) {
return DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER;
}
if (navigationMenuItem.itemType === NavigationMenuItemType.LINK) {
return DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK;
}
return undefined;
};
@@ -1,26 +0,0 @@
import type { Theme } from '@emotion/react';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
export const getIconBackgroundColorForPayload = (
payload: AddToNavigationDragPayload,
theme: Theme,
): string | undefined => {
const colors = getNavigationMenuItemIconColors(theme);
switch (payload.type) {
case NavigationMenuItemType.OBJECT:
return colors.object;
case NavigationMenuItemType.VIEW:
return colors.view;
case NavigationMenuItemType.FOLDER:
return colors.folder;
case NavigationMenuItemType.LINK:
return colors.link;
case NavigationMenuItemType.RECORD:
return undefined;
default:
return undefined;
}
};
@@ -1,8 +0,0 @@
import type { Theme } from '@emotion/react';
export const getNavigationMenuItemIconColors = (theme: Theme) => ({
folder: theme.color.orange,
link: theme.color.red,
view: theme.grayScale.gray8,
object: theme.color.blue,
});
@@ -0,0 +1,28 @@
import { isDefined } from 'twenty-shared/utils';
type NavigationMenuItemDraftForObjectIds = {
viewId?: string | null;
targetObjectMetadataId?: string | null;
};
type ViewForObjectIds = {
id: string;
objectMetadataId: string;
};
export const getObjectMetadataIdsInDraft = (
draft: NavigationMenuItemDraftForObjectIds[],
views: ViewForObjectIds[],
): Set<string> =>
draft.reduce<Set<string>>((ids, item) => {
const view = isDefined(item.viewId)
? views.find((view) => view.id === item.viewId)
: undefined;
if (isDefined(view)) {
ids.add(view.objectMetadataId);
}
if (isDefined(item.targetObjectMetadataId)) {
ids.add(item.targetObjectMetadataId);
}
return ids;
}, new Set<string>());
@@ -0,0 +1,38 @@
import { type ThemeColor, MAIN_COLOR_NAMES } from 'twenty-ui/theme';
import { DEFAULT_NAV_ITEM_ICON_COLOR } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultIconColor.constant';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
const STANDARD_OBJECT_ICON_COLOR: Partial<
Record<CoreObjectNameSingular, ThemeColor>
> = {
[CoreObjectNameSingular.Company]: 'blue',
[CoreObjectNameSingular.Person]: 'blue',
[CoreObjectNameSingular.Task]: 'turquoise',
[CoreObjectNameSingular.TaskTarget]: 'turquoise',
[CoreObjectNameSingular.Note]: 'turquoise',
[CoreObjectNameSingular.NoteTarget]: 'turquoise',
[CoreObjectNameSingular.Opportunity]: 'red',
[CoreObjectNameSingular.Dashboard]: DEFAULT_NAV_ITEM_ICON_COLOR,
[CoreObjectNameSingular.Workflow]: DEFAULT_NAV_ITEM_ICON_COLOR,
[CoreObjectNameSingular.WorkflowRun]: DEFAULT_NAV_ITEM_ICON_COLOR,
[CoreObjectNameSingular.WorkflowVersion]: DEFAULT_NAV_ITEM_ICON_COLOR,
};
const CUSTOM_OBJECT_ICON_COLORS: ThemeColor[] = MAIN_COLOR_NAMES.filter(
(color) => color !== 'gray',
);
const getColorForCustomObject = (seed: string): ThemeColor => {
let hash = 0;
for (let index = 0; index < seed.length; index += 1) {
hash = (hash << 5) - hash + seed.charCodeAt(index);
hash |= 0;
}
const index = Math.abs(hash) % CUSTOM_OBJECT_ICON_COLORS.length;
return CUSTOM_OBJECT_ICON_COLORS[index];
};
export const getStandardObjectIconColor = (nameSingular: string): ThemeColor =>
STANDARD_OBJECT_ICON_COLOR[nameSingular as CoreObjectNameSingular] ??
getColorForCustomObject(nameSingular);
@@ -0,0 +1,5 @@
export type NavigationMenuItemIconStyle = {
backgroundColor: string;
iconColor: string;
borderColor: string;
};
@@ -0,0 +1,10 @@
import { themeColorSchema, type ThemeColor } from 'twenty-ui/theme';
import { DEFAULT_NAV_ITEM_ICON_COLOR } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultIconColor.constant';
export const parseThemeColor = (
color: string | null | undefined,
): ThemeColor => {
const result = themeColorSchema.safeParse(color ?? '');
return result.success ? result.data : DEFAULT_NAV_ITEM_ICON_COLOR;
};
@@ -2,7 +2,8 @@ import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainCo
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
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 { useObjectNavItemColor } from '@/navigation-menu-item/hooks/useObjectNavItemColor';
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@@ -13,11 +14,10 @@ import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/compo
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { coreViewsFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreViewsFromObjectMetadataItemFamilySelector';
import { ViewKey } from '@/views/types/ViewKey';
import { useTheme } from '@emotion/react';
import { useLocation } from 'react-router-dom';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { AppPath } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { Avatar, useIcons } from 'twenty-ui/display';
@@ -45,11 +45,9 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
onActiveItemClickWhenNotInEditMode,
isDragging = false,
}: NavigationDrawerItemForObjectMetadataItemProps) => {
const theme = useTheme();
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const iconColors = getNavigationMenuItemIconColors(theme);
const lastVisitedViewPerObjectMetadataItem = useAtomStateValue(
lastVisitedViewPerObjectMetadataItemState,
);
@@ -68,6 +66,9 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
lastVisitedViewPerObjectMetadataItem?.[objectMetadataItem.id];
const { getIcon } = useIcons();
const objectNavItemColor = useObjectNavItemColor(
objectMetadataItem.nameSingular,
);
const location = useLocation();
const currentPath = location.pathname;
const currentPathWithSearch = `${location.pathname}${location.search}`;
@@ -141,13 +142,17 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
<ObjectIconWithViewOverlay
ObjectIcon={getIcon(objectMetadataItem.icon)}
ViewIcon={getIcon(navigationMenuItem!.Icon!)}
objectColor={objectNavItemColor}
/>
)
: getIcon(objectMetadataItem.icon);
const iconBackgroundColor =
isNavigationMenuItemEditingEnabled && !isRecord && !isViewWithCustomName
? iconColors.object
const iconThemeColor =
isNavigationMenuItemEditingEnabled && !isRecord
? isDefined(navigationMenuItem?.color)
? navigationMenuItem.color
: (getStandardObjectIconColor(objectMetadataItem.nameSingular) ??
'gray')
: undefined;
const secondaryLabel =
@@ -222,7 +227,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
}
onClick={handleClick}
Icon={Icon}
iconBackgroundColor={iconBackgroundColor}
iconColor={iconThemeColor}
active={isActive}
isSelectedInEditMode={isSelectedInEditMode}
isDragging={isDragging}
@@ -1,5 +1,4 @@
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';
@@ -18,7 +17,6 @@ import {
type FlatWorkspaceItem,
type NavigationMenuItemClickParams,
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem';
@@ -67,7 +65,6 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
onActiveObjectMetadataItemClick,
}: NavigationDrawerSectionForWorkspaceItemsProps) => {
const { t } = useLingui();
const theme = useTheme();
const workspaceDropDisabled = useIsDropDisabledForSection(true);
const { toggleNavigationSection, isNavigationSectionOpen } =
useNavigationSection('Workspace');
@@ -206,6 +203,9 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
folderId={item.id}
folderName={item.name ?? 'Folder'}
folderIconKey={item.Icon}
folderColor={
'color' in item ? item.color : undefined
}
navigationMenuItems={
folderChildrenById.get(item.id) ?? []
}
@@ -231,7 +231,6 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
if (type === 'link') {
const linkItem = item as ProcessedNavigationMenuItem;
const iconColors = getNavigationMenuItemIconColors(theme);
return (
<NavigationItemDropTarget
key={item.id}
@@ -259,7 +258,7 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
: undefined
}
Icon={IconLink}
iconBackgroundColor={iconColors.link}
iconColor={linkItem.color}
active={false}
isSelectedInEditMode={
editModeProps.isSelectedInEditMode
@@ -3,6 +3,7 @@ import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainCo
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { RecordIndexPageHeaderIcon } from '@/object-record/record-index/components/RecordIndexPageHeaderIcon';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { PageHeaderToggleCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderToggleCommandMenuButton';
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
@@ -10,7 +11,6 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
const StyledTitleWithSelectedRecords = styled.div`
display: flex;
@@ -41,9 +41,6 @@ export const RecordIndexPageHeader = () => {
const objectMetadataItem =
findObjectMetadataItemByNamePlural(objectNamePlural);
const { getIcon } = useIcons();
const Icon = getIcon(objectMetadataItem?.icon);
const label = objectMetadataItem?.labelPlural ?? objectNamePlural;
const pageHeaderTitle =
@@ -65,7 +62,12 @@ export const RecordIndexPageHeader = () => {
);
return (
<PageHeader title={pageHeaderTitle} Icon={Icon}>
<PageHeader
title={pageHeaderTitle}
Icon={() => (
<RecordIndexPageHeaderIcon objectMetadataItem={objectMetadataItem} />
)}
>
{isDefined(contextStoreCurrentViewId) && (
<>
<RecordIndexActionMenu />
@@ -0,0 +1,59 @@
import { isNonEmptyString } from '@sniptt/guards';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const RecordIndexPageHeaderIcon = ({
objectMetadataItem,
}: {
objectMetadataItem?: ObjectMetadataItem;
}) => {
const theme = useTheme();
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const { workspaceNavigationMenuItems } =
usePrefetchedNavigationMenuItemsData();
const coreIndexViewId = useAtomFamilySelectorValue(
coreIndexViewIdFromObjectMetadataItemFamilySelector,
{ objectMetadataItemId: objectMetadataItem?.id ?? '' },
);
const { getIcon } = useIcons();
const ObjectIcon = getIcon(objectMetadataItem?.icon);
if (!ObjectIcon) {
return null;
}
if (!isNavigationMenuItemEditingEnabled) {
return <ObjectIcon size={theme.icon.size.md} />;
}
const navItem = isDefined(coreIndexViewId)
? workspaceNavigationMenuItems.find(
(item) => item.viewId === coreIndexViewId,
)
: undefined;
const navigationMenuItemColor = isNonEmptyString(navItem?.color)
? navItem.color
: undefined;
const iconColor =
navigationMenuItemColor ??
getStandardObjectIconColor(objectMetadataItem?.nameSingular ?? '');
return (
<NavigationMenuItemStyleIcon
Icon={ObjectIcon}
color={iconColor ?? undefined}
/>
);
};
@@ -1,3 +1,4 @@
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { NAVIGATION_DRAWER_COLLAPSED_WIDTH } from '@/ui/layout/resizable-panel/constants/NavigationDrawerCollapsedWidth';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
@@ -7,10 +8,12 @@ import { type NavigationDrawerSubItemState } from '@/ui/navigation/navigation-dr
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import isPropValid from '@emotion/is-prop-valid';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { type ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
@@ -29,6 +32,7 @@ import {
type TriggerEventType,
useMouseDownNavigation,
} from 'twenty-ui/utilities';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const DEFAULT_INDENTATION_LEVEL = 1;
@@ -43,7 +47,7 @@ export type NavigationDrawerItemProps = {
to?: string;
onClick?: () => void;
Icon?: IconComponent | ((props: TablerIconsProps) => JSX.Element);
iconBackgroundColor?: string;
iconColor?: string | null;
active?: boolean;
danger?: boolean;
soon?: boolean;
@@ -217,7 +221,10 @@ const StyledSpacer = styled.span`
flex-grow: 1;
`;
const StyledIcon = styled.div<{ $backgroundColor?: string }>`
const StyledIcon = styled.div<{
$backgroundColor?: string;
$borderColor?: string;
}>`
align-items: center;
display: flex;
flex-grow: 0;
@@ -225,13 +232,15 @@ const StyledIcon = styled.div<{ $backgroundColor?: string }>`
justify-content: center;
margin-right: ${({ theme }) => theme.spacing(2)};
${({ theme, $backgroundColor }) =>
${({ theme, $backgroundColor, $borderColor }) =>
$backgroundColor &&
css`
background-color: ${$backgroundColor};
border-radius: ${theme.border.radius.xs};
height: ${theme.spacing(4.5)};
width: ${theme.spacing(4.5)};
border-radius: 4px;
box-sizing: border-box;
height: ${theme.spacing(4)};
width: ${theme.spacing(4)};
${$borderColor ? `border: 1px solid ${$borderColor};` : ''}
`}
`;
@@ -286,7 +295,7 @@ export const NavigationDrawerItem = ({
secondaryLabel,
indentationLevel = DEFAULT_INDENTATION_LEVEL,
Icon,
iconBackgroundColor,
iconColor,
to,
onClick,
active,
@@ -308,6 +317,9 @@ export const NavigationDrawerItem = ({
const theme = useTheme();
const isMobile = useIsMobile();
const isSettingsPage = useIsSettingsPage();
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
useAtomState(isNavigationDrawerExpandedState);
@@ -379,30 +391,30 @@ export const NavigationDrawerItem = ({
</NavigationDrawerAnimatedCollapseWrapper>
)}
{Icon && (
<StyledIcon $backgroundColor={iconBackgroundColor}>
<Icon
style={{
minWidth: iconBackgroundColor
? theme.spacing(3.5)
: theme.icon.size.md,
}}
size={
iconBackgroundColor ? theme.spacing(3.5) : theme.icon.size.md
}
stroke={theme.icon.stroke.md}
color={
iconBackgroundColor
? theme.grayScale.gray1
: showBreadcrumb &&
!isSettingsPage &&
!isNavigationDrawerExpanded
{Icon &&
(isNavigationMenuItemEditingEnabled &&
isNonEmptyString(iconColor) ? (
<StyledIcon>
<NavigationMenuItemStyleIcon Icon={Icon} color={iconColor} />
</StyledIcon>
) : (
<StyledIcon>
<Icon
style={{
minWidth: theme.icon.size.md,
}}
size={theme.icon.size.md}
stroke={theme.icon.stroke.md}
color={
showBreadcrumb &&
!isSettingsPage &&
!isNavigationDrawerExpanded
? theme.font.color.light
: 'currentColor'
}
/>
</StyledIcon>
)}
}
/>
</StyledIcon>
))}
<StyledLabelParent>
<OverflowingTextWithTooltip
@@ -10,7 +10,7 @@ export const NavigationDrawerSubItem = ({
label,
secondaryLabel,
Icon,
iconBackgroundColor,
iconColor,
to,
onClick,
active,
@@ -32,7 +32,7 @@ export const NavigationDrawerSubItem = ({
indentationLevel={2}
subItemState={subItemState}
Icon={Icon}
iconBackgroundColor={iconBackgroundColor}
iconColor={iconColor}
to={to}
onClick={onClick}
active={active}
@@ -286,6 +286,7 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
name: favoriteFolder.name,
link: null,
icon: null,
color: null,
position: favoriteFolder.position,
workspaceId,
applicationId: workspaceCustomApplicationId,
@@ -320,6 +321,7 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
name: favoriteFolder.name,
link: null,
icon: null,
color: null,
position: favoriteFolder.position,
workspaceId,
applicationId: workspaceCustomApplicationId,
@@ -528,6 +530,7 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
name: null,
link: null,
icon: null,
color: null,
position: favorite.position,
workspaceId,
applicationId,
@@ -582,6 +585,7 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
name: null,
link: null,
icon: null,
color: null,
position: favorite.position,
workspaceId,
applicationId: workspaceCustomApplicationId,
@@ -0,0 +1,19 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddColorToNavigationMenuItem1771146443209
implements MigrationInterface
{
name = 'AddColorToNavigationMenuItem1771146443209';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."navigationMenuItem" ADD "color" text`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."navigationMenuItem" DROP COLUMN "color"`,
);
}
}
@@ -17,6 +17,7 @@ export const fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem =
applicationUniversalIdentifier,
name: navigationMenuItemManifest.name ?? null,
icon: navigationMenuItemManifest.icon ?? null,
color: navigationMenuItemManifest.color ?? null,
position: navigationMenuItemManifest.position,
viewUniversalIdentifier:
navigationMenuItemManifest.viewUniversalIdentifier ?? null,
@@ -106,6 +106,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"name",
"link",
"icon",
"color",
],
"propertiesToStringify": [],
},
@@ -1020,6 +1020,11 @@ 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 },
color: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
createdAt: {
toCompare: false,
toStringify: false,
@@ -6,4 +6,5 @@ export const FLAT_NAVIGATION_MENU_ITEM_EDITABLE_PROPERTIES = [
'name',
'link',
'icon',
'color',
] as const satisfies MetadataEntityPropertyName<'navigationMenuItem'>[];
@@ -82,6 +82,7 @@ export const fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate =
name: createNavigationMenuItemInput.name ?? null,
link: createNavigationMenuItemInput.link ?? null,
icon: createNavigationMenuItemInput.icon ?? null,
color: createNavigationMenuItemInput.color ?? null,
position,
workspaceId,
applicationId: flatApplication.id,
@@ -14,6 +14,7 @@ export const fromFlatNavigationMenuItemToNavigationMenuItemDto = (
name: flatNavigationMenuItem.name ?? undefined,
link: flatNavigationMenuItem.link ?? undefined,
icon: flatNavigationMenuItem.icon ?? undefined,
color: flatNavigationMenuItem.color ?? undefined,
position: flatNavigationMenuItem.position,
workspaceId: flatNavigationMenuItem.workspaceId,
applicationId: flatNavigationMenuItem.applicationId ?? undefined,
@@ -83,6 +83,7 @@ export const fromNavigationMenuItemEntityToFlatNavigationMenuItem = ({
name: navigationMenuItemEntity.name,
link: navigationMenuItemEntity.link,
icon: navigationMenuItemEntity.icon,
color: navigationMenuItemEntity.color,
position: navigationMenuItemEntity.position,
workspaceId: navigationMenuItemEntity.workspaceId,
universalIdentifier: navigationMenuItemEntity.universalIdentifier,
@@ -41,6 +41,11 @@ export class CreateNavigationMenuItemInput {
@Field(() => String, { nullable: true })
icon?: string | null;
@IsOptional()
@IsString()
@Field(() => String, { nullable: true })
color?: string | null;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
@@ -51,6 +51,10 @@ export class NavigationMenuItemDTO {
@Field(() => String, { nullable: true })
icon?: string | null;
@IsOptional()
@Field(() => String, { nullable: true })
color?: string | null;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
@@ -38,6 +38,11 @@ export class UpdateNavigationMenuItemInput {
@IsString()
@Field(() => String, { nullable: true })
icon?: string | null;
@IsOptional()
@IsString()
@Field(() => String, { nullable: true })
color?: string | null;
}
@InputType()
@@ -87,6 +87,9 @@ export class NavigationMenuItemEntity
@Column({ nullable: true, type: 'text' })
icon: string | null;
@Column({ nullable: true, type: 'text' })
color: string | null;
@ManyToOne(() => NavigationMenuItemEntity, {
onDelete: 'CASCADE',
nullable: true,
@@ -896,6 +896,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
name: null,
link: null,
icon: null,
color: null,
position: nextPosition,
workspaceId,
applicationId: workspaceCustomApplicationId,
@@ -7,34 +7,34 @@ export const STANDARD_NAVIGATION_MENU_ITEMS = {
STANDARD_OBJECTS.company.views.allCompanies.universalIdentifier,
position: 0,
},
allDashboards: {
universalIdentifier: '20202020-b002-4b02-8b02-c0aba11c0002',
allPeople: {
universalIdentifier: '20202020-b005-4b05-8b05-c0aba11c0005',
viewUniversalIdentifier:
STANDARD_OBJECTS.dashboard.views.allDashboards.universalIdentifier,
STANDARD_OBJECTS.person.views.allPeople.universalIdentifier,
position: 1,
},
allNotes: {
universalIdentifier: '20202020-b003-4b03-8b03-c0aba11c0003',
viewUniversalIdentifier:
STANDARD_OBJECTS.note.views.allNotes.universalIdentifier,
position: 2,
},
allOpportunities: {
universalIdentifier: '20202020-b004-4b04-8b04-c0aba11c0004',
viewUniversalIdentifier:
STANDARD_OBJECTS.opportunity.views.allOpportunities.universalIdentifier,
position: 3,
},
allPeople: {
universalIdentifier: '20202020-b005-4b05-8b05-c0aba11c0005',
viewUniversalIdentifier:
STANDARD_OBJECTS.person.views.allPeople.universalIdentifier,
position: 4,
position: 2,
},
allTasks: {
universalIdentifier: '20202020-b006-4b06-8b06-c0aba11c0006',
viewUniversalIdentifier:
STANDARD_OBJECTS.task.views.allTasks.universalIdentifier,
position: 3,
},
allNotes: {
universalIdentifier: '20202020-b003-4b03-8b03-c0aba11c0003',
viewUniversalIdentifier:
STANDARD_OBJECTS.note.views.allNotes.universalIdentifier,
position: 4,
},
allDashboards: {
universalIdentifier: '20202020-b002-4b02-8b02-c0aba11c0002',
viewUniversalIdentifier:
STANDARD_OBJECTS.dashboard.views.allDashboards.universalIdentifier,
position: 5,
},
workflowsFolder: {
@@ -66,3 +66,18 @@ export const STANDARD_NAVIGATION_MENU_ITEMS = {
position: 2,
},
} as const;
export const STANDARD_NAVIGATION_MENU_ITEM_DEFAULT_COLORS: Partial<
Record<keyof typeof STANDARD_NAVIGATION_MENU_ITEMS, string>
> = {
allCompanies: 'blue',
allPeople: 'blue',
allTasks: 'turquoise',
allNotes: 'turquoise',
allOpportunities: 'red',
workflowsFolder: 'orange',
allDashboards: 'gray',
workflowsFolderAllWorkflows: 'gray',
workflowsFolderAllWorkflowRuns: 'gray',
workflowsFolderAllWorkflowVersions: 'gray',
};
@@ -1,11 +1,14 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { STANDARD_NAVIGATION_MENU_ITEMS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-navigation-menu-item.constant';
import {
STANDARD_NAVIGATION_MENU_ITEM_DEFAULT_COLORS,
STANDARD_NAVIGATION_MENU_ITEMS,
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-navigation-menu-item.constant';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
export const createStandardNavigationMenuItemFlatMetadata = ({
workspaceId,
@@ -66,6 +69,9 @@ export const createStandardNavigationMenuItemFlatMetadata = ({
name: null,
link: null,
icon: null,
color:
STANDARD_NAVIGATION_MENU_ITEM_DEFAULT_COLORS[navigationMenuItemName] ??
null,
position,
createdAt: now,
updatedAt: now,
@@ -40,6 +40,7 @@ export const createStandardNavigationMenuItemFolderFlatMetadata = ({
name,
link: null,
icon: icon ?? null,
color: 'orange',
position,
createdAt: now,
updatedAt: now,
@@ -99,6 +100,7 @@ export const createStandardNavigationMenuItemFolderItemFlatMetadata = ({
name: null,
link: null,
icon: null,
color: 'gray',
position,
createdAt: now,
updatedAt: now,
@@ -3,6 +3,7 @@ import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsT
export type NavigationMenuItemManifest = SyncableEntityOptions & {
name?: string;
icon?: string;
color?: string;
position: number;
viewUniversalIdentifier?: string;
link?: string;
@@ -91,6 +91,7 @@ export {
IconCodeCircle,
IconCoins,
IconColorSwatch,
IconColumnInsertRight,
IconColumns,
IconCommand,
IconMessageCircle as IconComment,
@@ -268,8 +269,8 @@ export {
IconMoodSmile,
IconMoon,
IconMouse2,
IconNorthStar,
IconNewSection,
IconNorthStar,
IconNoteOff,
IconNotes,
IconNumber,
+2 -1
View File
@@ -166,6 +166,7 @@ export {
IconCodeCircle,
IconCoins,
IconColorSwatch,
IconColumnInsertRight,
IconColumns,
IconCommand,
IconComment,
@@ -343,8 +344,8 @@ export {
IconMoodSmile,
IconMoon,
IconMouse2,
IconNorthStar,
IconNewSection,
IconNorthStar,
IconNoteOff,
IconNotes,
IconNumber,
+2 -1
View File
@@ -41,13 +41,14 @@ export {
MenuItemSelect,
} from './menu/menu-item/components/MenuItemSelect';
export { MenuItemSelectAvatar } from './menu/menu-item/components/MenuItemSelectAvatar';
export type { ColorLabels } from './menu/menu-item/components/MenuItemSelectColor';
export { MenuItemSelectColor } from './menu/menu-item/components/MenuItemSelectColor';
export { MenuItemSelectTag } from './menu/menu-item/components/MenuItemSelectTag';
export type { MenuItemSuggestionProps } from './menu/menu-item/components/MenuItemSuggestion';
export { MenuItemSuggestion } from './menu/menu-item/components/MenuItemSuggestion';
export type { MenuItemToggleProps } from './menu/menu-item/components/MenuItemToggle';
export { MenuItemToggle } from './menu/menu-item/components/MenuItemToggle';
export type { ColorLabels } from './menu/menu-item/constants/DefaultColorLabels';
export { DEFAULT_COLOR_LABELS } from './menu/menu-item/constants/DefaultColorLabels';
export type { MenuItemIconProps } from './menu/menu-item/internals/components/MenuItemIcon';
export { MenuItemIcon } from './menu/menu-item/internals/components/MenuItemIcon';
export {
@@ -8,38 +8,12 @@ import {
import { ColorSample, type ColorSampleVariant } from '@ui/display';
import { type ThemeColor } from '@ui/theme';
import {
DEFAULT_COLOR_LABELS,
type ColorLabels,
} from '../constants/DefaultColorLabels';
import { StyledMenuItemSelect } from './MenuItemSelect';
export type ColorLabels = Record<ThemeColor, string>;
const DEFAULT_COLOR_LABELS: ColorLabels = {
gray: 'Gray',
tomato: 'Tomato',
red: 'Red',
ruby: 'Ruby',
crimson: 'Crimson',
pink: 'Pink',
plum: 'Plum',
purple: 'Purple',
violet: 'Violet',
iris: 'Iris',
cyan: 'Cyan',
turquoise: 'Turquoise',
sky: 'Sky',
blue: 'Blue',
jade: 'Jade',
green: 'Green',
grass: 'Grass',
mint: 'Mint',
lime: 'Lime',
bronze: 'Bronze',
gold: 'Gold',
brown: 'Brown',
orange: 'Orange',
amber: 'Amber',
yellow: 'Yellow',
};
type MenuItemSelectColorProps = {
selected: boolean;
className?: string;
@@ -0,0 +1,31 @@
import { type ThemeColor } from '@ui/theme';
export type ColorLabels = Record<ThemeColor, string>;
export const DEFAULT_COLOR_LABELS: ColorLabels = {
gray: 'Gray',
tomato: 'Tomato',
red: 'Red',
ruby: 'Ruby',
crimson: 'Crimson',
pink: 'Pink',
plum: 'Plum',
purple: 'Purple',
violet: 'Violet',
iris: 'Iris',
cyan: 'Cyan',
turquoise: 'Turquoise',
sky: 'Sky',
blue: 'Blue',
jade: 'Jade',
green: 'Green',
grass: 'Grass',
mint: 'Mint',
lime: 'Lime',
bronze: 'Bronze',
gold: 'Gold',
brown: 'Brown',
orange: 'Orange',
amber: 'Amber',
yellow: 'Yellow',
};