Feat: Navbar customization (#17728)

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
This commit is contained in:
Abdul Rahman
2026-02-11 21:03:51 +05:30
committed by GitHub
parent 52e57e70fd
commit 0902579fbe
166 changed files with 7716 additions and 667 deletions
@@ -0,0 +1,28 @@
import { Draggable } from '@hello-pangea/dnd';
import { type ReactNode } from 'react';
type CommandMenuAddToNavDraggablePlaceholderProps = {
index: number;
children: ReactNode;
};
export const CommandMenuAddToNavDraggablePlaceholder = ({
index,
children,
}: CommandMenuAddToNavDraggablePlaceholderProps) => (
<Draggable
draggableId={`add-to-nav-placeholder-${index}`}
index={index}
isDragDisabled={true}
>
{(provided) => (
<div
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.draggableProps}
>
{children}
</div>
)}
</Draggable>
);
@@ -0,0 +1,25 @@
import { Droppable, type DroppableProvided } from '@hello-pangea/dnd';
import { type ReactNode, useContext } from 'react';
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext';
type CommandMenuAddToNavDroppableProps = {
children: (provided: DroppableProvided) => ReactNode;
};
export const CommandMenuAddToNavDroppable = ({
children,
}: CommandMenuAddToNavDroppableProps) => {
const { sourceDroppableId } = useContext(NavigationDragSourceContext);
const isDropDisabled = sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID;
return (
<Droppable
droppableId={ADD_TO_NAV_SOURCE_DROPPABLE_ID}
isDropDisabled={isDropDisabled}
>
{(provided) => children(provided)}
</Droppable>
);
};
@@ -0,0 +1,116 @@
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { IconFolder, 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 { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
import { useUpdateFolderNameInDraft } from '@/navigation-menu-item/hooks/useUpdateFolderNameInDraft';
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 { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { TitleInput } from '@/ui/input/components/TitleInput';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
const ICON_CONFIG = {
folder: { Icon: IconFolder, colorKey: 'folder' },
link: { Icon: IconLink, colorKey: 'link' },
} as const;
export const CommandMenuFolderLinkInfo = ({
type,
}: {
type: 'folder' | 'link';
}) => {
const theme = useTheme();
const { t } = useLingui();
const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState);
const [shouldFocusTitleInput, setShouldFocusTitleInput] =
useRecoilComponentState(
commandMenuShouldFocusTitleInputComponentState,
commandMenuPageInfo.instanceId,
);
const selectedNavigationMenuItemInEditMode = useRecoilValue(
selectedNavigationMenuItemInEditModeState,
);
const items = useWorkspaceSectionItems();
const { updateFolderNameInDraft } = useUpdateFolderNameInDraft();
const { updateLinkInDraft } = useUpdateLinkInDraft();
const defaultLabel = type === 'folder' ? t`New folder` : t`Link label`;
const placeholder = type === 'folder' ? t`Folder name` : t`Link label`;
const selectedItem = selectedNavigationMenuItemInEditMode
? items.find(
(item) =>
item.itemType === type &&
item.id === selectedNavigationMenuItemInEditMode,
)
: undefined;
if (!selectedItem) return null;
const itemId = selectedItem.id;
const itemName = selectedItem.name ?? defaultLabel;
const handleChange = (text: string) => {
if (type === 'folder') {
updateFolderNameInDraft(itemId, text);
} else {
updateLinkInDraft(itemId, { name: text });
}
};
const handleSave = () => {
const trimmed = itemName.trim();
const finalName = trimmed.length > 0 ? trimmed : defaultLabel;
if (finalName !== itemName) {
if (type === 'folder') {
updateFolderNameInDraft(itemId, finalName);
} else {
updateLinkInDraft(itemId, { name: finalName });
}
}
};
const { Icon, colorKey } = ICON_CONFIG[type];
return (
<CommandMenuPageInfoLayout
icon={
<StyledNavigationMenuItemIconContainer
$backgroundColor={getNavigationMenuItemIconColors(theme)[colorKey]}
>
<Icon
size={theme.spacing(3.5)}
color={theme.grayScale.gray1}
stroke={theme.icon.stroke.md}
/>
</StyledNavigationMenuItemIconContainer>
}
title={
<TitleInput
instanceId={
type === 'folder' ? `folder-name-${itemId}` : `link-label-${itemId}`
}
sizeVariant="sm"
value={itemName}
onChange={handleChange}
placeholder={placeholder}
onEnter={handleSave}
onEscape={handleSave}
onClickOutside={handleSave}
onTab={handleSave}
onShiftTab={handleSave}
shouldFocus={shouldFocusTitleInput}
onFocus={() => setShouldFocusTitleInput(false)}
/>
}
label={type === 'link' ? t`link` : undefined}
/>
);
};
@@ -1,11 +1,11 @@
import { isNonEmptyString } from '@sniptt/guards';
import { type ReactNode } from 'react';
import { IconArrowUpRight, type IconComponent } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { type ReactNode } from 'react';
import { IconArrowUpRight, type IconComponent } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
export type CommandMenuItemProps = {
label: string;
@@ -15,6 +15,7 @@ export type CommandMenuItemProps = {
onClick?: () => void;
Icon?: IconComponent;
hotKeys?: string[];
LeftComponent?: ReactNode;
RightComponent?: ReactNode;
contextualTextPosition?: 'left' | 'right';
hasSubMenu?: boolean;
@@ -31,6 +32,7 @@ export const CommandMenuItem = ({
onClick,
Icon,
hotKeys,
LeftComponent,
RightComponent,
hasSubMenu = false,
isSubMenuOpened = false,
@@ -49,8 +51,9 @@ export const CommandMenuItem = ({
return (
<MenuItem
withIconContainer={true}
LeftIcon={Icon}
withIconContainer={!LeftComponent}
LeftIcon={LeftComponent ? undefined : Icon}
LeftComponent={LeftComponent}
text={label}
contextualText={description}
contextualTextPosition={contextualTextPosition}
@@ -0,0 +1,104 @@
import styled from '@emotion/styled';
import { Draggable } from '@hello-pangea/dnd';
import { useLingui } from '@lingui/react/macro';
import { type ReactNode, useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { type IconComponent } from 'twenty-ui/display';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { AddToNavigationDragHandle } from '@/navigation-menu-item/components/AddToNavigationDragHandle';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
type CommandMenuItemWithAddToNavigationDragProps = {
icon?: IconComponent;
customIconContent?: ReactNode;
label: string;
description?: string;
id: string;
onClick: () => void;
payload: AddToNavigationDragPayload;
dragIndex?: number;
};
const StyledDraggableMenuItem = styled.div`
cursor: grab;
width: 100%;
&:active {
cursor: grabbing;
}
`;
export const CommandMenuItemWithAddToNavigationDrag = ({
icon,
customIconContent,
label,
description,
id,
onClick,
payload,
dragIndex,
}: CommandMenuItemWithAddToNavigationDragProps) => {
const { t } = useLingui();
const setRegistry = useSetRecoilState(addToNavPayloadRegistryState);
const [isHovered, setIsHovered] = useState(false);
const contextualDescription = isHovered
? t`Drag to add to navbar`
: description;
const DragHandleIcon = () => (
<AddToNavigationDragHandle
icon={icon}
customIconContent={customIconContent}
payload={payload}
isHovered={isHovered}
/>
);
const registerPayload = () => {
if (dragIndex !== undefined) {
setRegistry((prev) => new Map(prev).set(id, payload));
}
};
const menuItemContent = (
<StyledDraggableMenuItem
onMouseEnter={() => {
setIsHovered(true);
registerPayload();
}}
onMouseLeave={() => setIsHovered(false)}
onMouseDown={registerPayload}
>
<CommandMenuItem
Icon={DragHandleIcon}
label={label}
description={contextualDescription}
id={id}
onClick={onClick}
/>
</StyledDraggableMenuItem>
);
if (dragIndex !== undefined) {
return (
<Draggable draggableId={id} index={dragIndex} isDragDisabled={false}>
{(provided) => (
<div
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.draggableProps}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.dragHandleProps}
>
{menuItemContent}
</div>
)}
</Draggable>
);
}
return menuItemContent;
};
@@ -20,6 +20,7 @@ export type CommandMenuListProps = {
children?: React.ReactNode;
loading?: boolean;
noResults?: boolean;
noResultsText?: string;
};
const StyledInnerList = styled.div`
@@ -60,6 +61,7 @@ export const CommandMenuList = ({
children,
loading = false,
noResults = false,
noResultsText,
}: CommandMenuListProps) => {
const setHasUserSelectedCommand = useSetRecoilState(
hasUserSelectedCommandState,
@@ -91,7 +93,7 @@ export const CommandMenuList = ({
) : null,
)}
{noResults && !loading && (
<StyledEmpty>{t`No results found`}</StyledEmpty>
<StyledEmpty>{noResultsText ?? t`No results found`}</StyledEmpty>
)}
</SelectableList>
</StyledInnerList>
@@ -0,0 +1,38 @@
import { useLingui } from '@lingui/react/macro';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
import { useSelectedNavigationMenuItemEditData } from '@/command-menu/pages/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditData';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { ViewKey } from '@/views/types/ViewKey';
export const CommandMenuObjectViewRecordInfo = () => {
const { t } = useLingui();
const { processedItem, selectedItemLabel } =
useSelectedNavigationMenuItemEditData();
if (!processedItem || !selectedItemLabel) {
return null;
}
const isViewOrRecord =
processedItem.itemType === 'view' || processedItem.itemType === 'record';
if (!isViewOrRecord) {
return null;
}
const label =
processedItem.itemType === 'record'
? t`record`
: processedItem.viewKey === ViewKey.Index
? t`object`
: t`view`;
return (
<CommandMenuPageInfoLayout
icon={<NavigationMenuItemIcon navigationMenuItem={processedItem} />}
title={<OverflowingTextWithTooltip text={selectedItemLabel} />}
label={label}
/>
);
};
@@ -1,11 +1,18 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { CommandMenuFolderLinkInfo } from '@/command-menu/components/CommandMenuFolderLinkInfo';
import { CommandMenuMultipleRecordsInfo } from '@/command-menu/components/CommandMenuMultipleRecordsInfo';
import { CommandMenuObjectViewRecordInfo } from '@/command-menu/components/CommandMenuObjectViewRecordInfo';
import { CommandMenuPageLayoutInfo } from '@/command-menu/components/CommandMenuPageLayoutInfo';
import { CommandMenuRecordInfo } from '@/command-menu/components/CommandMenuRecordInfo';
import { CommandMenuWorkflowStepInfo } from '@/command-menu/components/CommandMenuWorkflowStepInfo';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { type CommandMenuContextChipProps } from './CommandMenuContextChip';
const StyledPageTitle = styled.div`
@@ -19,10 +26,33 @@ type CommandMenuPageInfoProps = {
};
export const CommandMenuPageInfo = ({ pageChip }: CommandMenuPageInfoProps) => {
const selectedNavigationMenuItemInEditMode = useRecoilValue(
selectedNavigationMenuItemInEditModeState,
);
const items = useWorkspaceSectionItems();
if (!isDefined(pageChip)) {
return null;
}
const isNavigationMenuItemEditPage =
pageChip.page?.page === CommandMenuPages.NavigationMenuItemEdit;
const selectedNavItem = isNavigationMenuItemEditPage
? items.find((item) => item.id === selectedNavigationMenuItemInEditMode)
: undefined;
if (isNavigationMenuItemEditPage && isDefined(selectedNavItem)) {
const itemType = selectedNavItem.itemType;
if (itemType === 'folder' || itemType === 'link') {
return <CommandMenuFolderLinkInfo type={itemType} />;
}
if (itemType === 'view' || itemType === 'record') {
return <CommandMenuObjectViewRecordInfo />;
}
}
const isRecordPage = pageChip.page?.page === CommandMenuPages.ViewRecord;
if (isRecordPage && isDefined(pageChip.page?.pageId)) {
@@ -134,6 +134,7 @@ export const CommandMenuSidePanelForDesktop = () => {
isOpen={isCommandMenuOpened}
isResizing={isResizing}
onTransitionEnd={handleTransitionEnd}
data-command-menu-panel=""
>
<StyledSidePanel>
<StyledModalContainer ref={handleModalContainerRef} />
@@ -0,0 +1,87 @@
import styled from '@emotion/styled';
import { type ReactNode } from 'react';
import { SidePanelSubPageNavigationHeader } from '@/command-menu/pages/common/components/SidePanelSubPageNavigationHeader';
const StyledSubViewContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
overflow: hidden;
`;
const StyledSearchContainer = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
min-width: 0;
padding: ${({ theme }) => theme.spacing(2, 3)};
`;
const StyledSearchInput = styled.input`
background: transparent;
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;
&::placeholder {
color: ${({ theme }) => theme.font.color.tertiary};
}
`;
const StyledScrollableListWrapper = styled.div`
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
& > * {
flex: 1;
min-height: 0;
}
`;
type CommandMenuSubViewWithSearchProps = {
backBarTitle: string;
onBack: () => void;
searchPlaceholder: string;
searchValue: string;
onSearchChange: (value: string) => void;
searchInputProps?: React.InputHTMLAttributes<HTMLInputElement>;
children?: ReactNode;
};
export const CommandMenuSubViewWithSearch = ({
backBarTitle,
onBack,
searchPlaceholder,
searchValue,
onSearchChange,
searchInputProps,
children,
}: CommandMenuSubViewWithSearchProps) => (
<StyledSubViewContainer>
<SidePanelSubPageNavigationHeader
title={backBarTitle}
onBackClick={onBack}
/>
<StyledSearchContainer>
<StyledSearchInput
placeholder={searchPlaceholder}
value={searchValue}
onChange={(event) => onSearchChange(event.target.value)}
autoFocus
// eslint-disable-next-line react/jsx-props-no-spreading
{...searchInputProps}
/>
</StyledSearchContainer>
{children != null && (
<StyledScrollableListWrapper>{children}</StyledScrollableListWrapper>
)}
</StyledSubViewContainer>
);
@@ -4,6 +4,8 @@ import { CommandMenuAskAIPage } from '@/command-menu/pages/ask-ai/components/Com
import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage';
import { CommandMenuFrontComponentPage } from '@/command-menu/pages/front-component/components/CommandMenuFrontComponentPage';
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
import { CommandMenuNavigationMenuItemEditPage } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNavigationMenuItemEditPage';
import { CommandMenuNewSidebarItemPage } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemPage';
import { CommandMenuPageLayoutChartSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings';
import { CommandMenuPageLayoutFieldsLayout } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout';
import { CommandMenuPageLayoutFieldsSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings';
@@ -76,4 +78,9 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
<CommandMenuPageLayoutFieldsLayout />,
],
[CommandMenuPages.ViewFrontComponent, <CommandMenuFrontComponentPage />],
[
CommandMenuPages.NavigationMenuItemEdit,
<CommandMenuNavigationMenuItemEditPage />,
],
[CommandMenuPages.NavigationMenuAddItem, <CommandMenuNewSidebarItemPage />],
]);
@@ -1,12 +1,12 @@
import { useRecoilCallback } from 'recoil';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
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';
@@ -29,6 +29,7 @@ export const useCommandMenu = () => {
.getValue();
if (isCommandMenuOpened) {
set(addToNavPayloadRegistryState, new Map());
set(isCommandMenuOpenedState, false);
set(isCommandMenuClosingState, true);
closeAnyOpenDropdown();
@@ -86,6 +86,9 @@ export const useCommandMenuHotKeys = () => {
},
focusId: SIDE_PANEL_FOCUS_ID,
dependencies: [goBackFromCommandMenu],
options: {
enableOnFormTags: false,
},
});
useHotkeysOnFocusedElement({
@@ -119,6 +122,7 @@ export const useCommandMenuHotKeys = () => {
],
options: {
preventDefault: false,
enableOnFormTags: false,
},
});
};
@@ -0,0 +1,43 @@
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
type UseFilteredPickerItemsParams<T> = {
items: T[];
searchQuery: string;
getSearchableValues: (item: T) => string[];
appendSelectableIds?: string[];
};
type UseFilteredPickerItemsResult<T> = {
filteredItems: T[];
selectableItemIds: string[];
isEmpty: boolean;
hasSearchQuery: boolean;
};
export const useFilteredPickerItems = <T extends { id: string }>({
items,
searchQuery,
getSearchableValues,
appendSelectableIds = [],
}: UseFilteredPickerItemsParams<T>): UseFilteredPickerItemsResult<T> => {
const filteredItems = filterBySearchQuery({
items,
searchQuery,
getSearchableValues,
});
const selectableItemIds =
filteredItems.length > 0
? [...filteredItems.map((item) => item.id), ...appendSelectableIds]
: appendSelectableIds;
const isEmpty = filteredItems.length === 0;
const hasSearchQuery = searchQuery.trim().length > 0;
return {
filteredItems,
selectableItemIds,
isEmpty,
hasSearchQuery,
};
};
@@ -0,0 +1,85 @@
import { useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { IconFolderPlus } 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 { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
type CommandMenuEditFolderPickerSubViewProps = {
onBack: () => void;
};
export const CommandMenuEditFolderPickerSubView = ({
onBack,
}: CommandMenuEditFolderPickerSubViewProps) => {
const { t } = useLingui();
const [searchValue, setSearchValue] = useState('');
const { foldersToShow, includeNoFolderOption, handleSelectFolder } =
useFolderPickerSelectionData();
const filteredFolders = filterBySearchQuery({
items: foldersToShow,
searchQuery: searchValue,
getSearchableValues: (folder) => [folder.name],
});
const isEmpty = filteredFolders.length === 0 && !includeNoFolderOption;
const selectableItemIds = [
...(includeNoFolderOption ? ['no-folder'] : []),
...(filteredFolders.length > 0 ? filteredFolders.map((f) => f.id) : []),
];
const noResultsText =
searchValue.trim().length > 0
? t`No results found`
: t`No folders available`;
return (
<CommandMenuSubViewWithSearch
backBarTitle={t`Move to a folder`}
onBack={onBack}
searchPlaceholder={t`Search a folder...`}
searchValue={searchValue}
onSearchChange={setSearchValue}
>
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
<CommandGroup heading={t`Folders`}>
{includeNoFolderOption && (
<SelectableListItem
itemId="no-folder"
onEnter={() => handleSelectFolder(null)}
>
<CommandMenuItem
label={t`No folder`}
id="no-folder"
onClick={() => handleSelectFolder(null)}
/>
</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>
))}
</CommandGroup>
</CommandMenuList>
</CommandMenuSubViewWithSearch>
);
};
@@ -0,0 +1,72 @@
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { getAbsoluteUrl } from 'twenty-shared/utils';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
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 { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
import { TextInput } from '@/ui/input/components/TextInput';
type CommandMenuEditLinkItemViewProps = OrganizeActionsProps & {
selectedItem: ProcessedNavigationMenuItem;
onUpdateLink: (linkId: string, link: string) => void;
onOpenFolderPicker: () => void;
};
export const CommandMenuEditLinkItemView = ({
selectedItem,
onUpdateLink,
canMoveUp,
canMoveDown,
onOpenFolderPicker,
onMoveUp,
onMoveDown,
onRemove,
onAddBefore,
onAddAfter,
}: CommandMenuEditLinkItemViewProps) => {
const { t } = useLingui();
const [urlEditInput, setUrlEditInput] = useState('');
const selectableItemIds = getOrganizeActionsSelectableItemIds(true);
return (
<CommandMenuList commandGroups={[]} selectableItemIds={selectableItemIds}>
<CommandGroup heading={t`Customize`}>
<TextInput
fullWidth
placeholder="www.google.com"
value={urlEditInput || selectedItem.link}
onChange={(value) => setUrlEditInput(value)}
onBlur={(event) => {
const value = event.target.value.trim();
if (isNonEmptyString(value)) {
onUpdateLink(selectedItem.id, getAbsoluteUrl(value));
setUrlEditInput('');
}
}}
/>
</CommandGroup>
<CommandMenuEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
showMoveToFolder
onMoveToFolder={onOpenFolderPicker}
moveToFolderHasSubMenu
/>
<CommandMenuEditOwnerSection applicationId={selectedItem.applicationId} />
</CommandMenuList>
);
};
@@ -0,0 +1,39 @@
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';
type CommandMenuEditObjectViewBaseProps = OrganizeActionsProps & {
onOpenFolderPicker: () => void;
};
export const CommandMenuEditObjectViewBase = ({
onOpenFolderPicker,
canMoveUp,
canMoveDown,
onMoveUp,
onMoveDown,
onRemove,
onAddBefore,
onAddAfter,
}: CommandMenuEditObjectViewBaseProps) => {
const selectableItemIds = getOrganizeActionsSelectableItemIds(true);
return (
<CommandMenuList commandGroups={[]} selectableItemIds={selectableItemIds}>
<CommandMenuEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
showMoveToFolder
onMoveToFolder={onOpenFolderPicker}
/>
</CommandMenuList>
);
};
@@ -0,0 +1,112 @@
import { useLingui } from '@lingui/react/macro';
import {
IconChevronDown,
IconChevronUp,
IconFolderSymlink,
IconRowInsertBottom,
IconRowInsertTop,
IconTrash,
} from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
export type OrganizeActionsProps = {
canMoveUp: boolean;
canMoveDown: boolean;
onMoveUp: () => void;
onMoveDown: () => void;
onRemove: () => void;
onAddBefore?: () => void;
onAddAfter?: () => void;
};
type CommandMenuEditOrganizeActionsProps = OrganizeActionsProps & {
showMoveToFolder?: boolean;
onMoveToFolder?: () => void;
moveToFolderHasSubMenu?: boolean;
};
export const CommandMenuEditOrganizeActions = ({
canMoveUp,
canMoveDown,
onMoveUp,
onMoveDown,
onRemove,
onAddBefore,
onAddAfter,
showMoveToFolder = false,
onMoveToFolder,
moveToFolderHasSubMenu = false,
}: CommandMenuEditOrganizeActionsProps) => {
const { t } = useLingui();
return (
<CommandGroup heading={t`Organize`}>
<SelectableListItem
itemId="move-up"
onEnter={canMoveUp ? onMoveUp : undefined}
>
<CommandMenuItem
Icon={IconChevronUp}
label={t`Move up`}
id="move-up"
onClick={onMoveUp}
disabled={!canMoveUp}
/>
</SelectableListItem>
<SelectableListItem
itemId="move-down"
onEnter={canMoveDown ? onMoveDown : undefined}
>
<CommandMenuItem
Icon={IconChevronDown}
label={t`Move down`}
id="move-down"
onClick={onMoveDown}
disabled={!canMoveDown}
/>
</SelectableListItem>
{showMoveToFolder && onMoveToFolder && (
<SelectableListItem itemId="move-to-folder" onEnter={onMoveToFolder}>
<CommandMenuItem
Icon={IconFolderSymlink}
label={t`Move to folder`}
id="move-to-folder"
hasSubMenu={moveToFolderHasSubMenu}
onClick={onMoveToFolder}
/>
</SelectableListItem>
)}
{onAddBefore && (
<SelectableListItem itemId="add-before" onEnter={onAddBefore}>
<CommandMenuItem
Icon={IconRowInsertTop}
label={t`Add menu item before`}
id="add-before"
onClick={onAddBefore}
/>
</SelectableListItem>
)}
{onAddAfter && (
<SelectableListItem itemId="add-after" onEnter={onAddAfter}>
<CommandMenuItem
Icon={IconRowInsertBottom}
label={t`Add menu item after`}
id="add-after"
onClick={onAddAfter}
/>
</SelectableListItem>
)}
<SelectableListItem itemId="remove" onEnter={onRemove}>
<CommandMenuItem
Icon={IconTrash}
label={t`Remove from sidebar`}
id="remove"
onClick={onRemove}
/>
</SelectableListItem>
</CommandGroup>
);
};
@@ -0,0 +1,54 @@
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconApps } from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { useNavigationMenuItemEditFolderData } from '@/command-menu/pages/navigation-menu-item/hooks/useNavigationMenuItemEditFolderData';
import { useSelectedNavigationMenuItemEditData } from '@/command-menu/pages/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditData';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useFindOneApplicationQuery } from '~/generated-metadata/graphql';
type CommandMenuEditOwnerSectionProps = {
applicationId?: string | null;
};
export const CommandMenuEditOwnerSection = ({
applicationId: applicationIdProp,
}: CommandMenuEditOwnerSectionProps) => {
const { t } = useLingui();
const { selectedItem } = useSelectedNavigationMenuItemEditData();
const { currentDraft } = useNavigationMenuItemEditFolderData();
const applicationIdFromDraft =
selectedItem && currentDraft
? currentDraft.find((item) => item.id === selectedItem.id)?.applicationId
: undefined;
const applicationId = applicationIdProp ?? applicationIdFromDraft;
const { data } = useFindOneApplicationQuery({
variables: { id: applicationId ?? '' },
skip: !isDefined(applicationId),
});
const applicationName = data?.findOneApplication?.name;
if (!isDefined(applicationName)) {
return null;
}
return (
<CommandGroup heading={t`Owner`}>
<SelectableListItem itemId="owner-app" onEnter={() => {}}>
<CommandMenuItem
Icon={IconApps}
label={applicationName}
id="owner-app"
disabled
/>
</SelectableListItem>
</CommandGroup>
);
};
@@ -0,0 +1,155 @@
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
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';
import { CommandMenuEditOrganizeActions } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOrganizeActions';
import { CommandMenuEditOwnerSection } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOwnerSection';
import { useNavigationMenuItemEditOrganizeActions } from '@/command-menu/pages/navigation-menu-item/hooks/useNavigationMenuItemEditOrganizeActions';
import { useNavigationMenuItemEditSubView } from '@/command-menu/pages/navigation-menu-item/hooks/useNavigationMenuItemEditSubView';
import { useSelectedNavigationMenuItemEditData } from '@/command-menu/pages/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditData';
import { getOrganizeActionsSelectableItemIds } from '@/command-menu/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { NAVIGATION_MENU_ITEM_TYPE } from '@/navigation-menu-item/types/navigation-menu-item-type';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
const StyledCommandMenuPlaceholder = styled.p`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.sm};
`;
const StyledCommandMenuPageContainer = styled.div`
padding: ${({ theme }) => theme.spacing(3)};
`;
export const CommandMenuNavigationMenuItemEditPage = () => {
const { t } = useLingui();
const selectedNavigationMenuItemInEditMode = useRecoilValue(
selectedNavigationMenuItemInEditModeState,
);
const {
selectedItemLabel,
selectedItem,
selectedItemObjectMetadata,
selectedItemType,
} = useSelectedNavigationMenuItemEditData();
const { editSubView, setFolderPicker, clearSubView } =
useNavigationMenuItemEditSubView();
const {
canMoveUp,
canMoveDown,
onMoveUp,
onMoveDown,
onRemove,
onAddBefore,
onAddAfter,
} = useNavigationMenuItemEditOrganizeActions();
const { updateLinkInDraft } = useUpdateLinkInDraft();
if (!selectedNavigationMenuItemInEditMode || !selectedItemLabel) {
return (
<StyledCommandMenuPageContainer>
<StyledCommandMenuPlaceholder>
{t`Select a navigation item to edit`}
</StyledCommandMenuPlaceholder>
</StyledCommandMenuPageContainer>
);
}
if (editSubView === 'folder-picker') {
return <CommandMenuEditFolderPickerSubView onBack={clearSubView} />;
}
if (
selectedItemType === NAVIGATION_MENU_ITEM_TYPE.VIEW &&
!selectedItemObjectMetadata
) {
return null;
}
if (selectedItemType === NAVIGATION_MENU_ITEM_TYPE.VIEW) {
return (
<CommandMenuEditObjectViewBase
onOpenFolderPicker={setFolderPicker}
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
/>
);
}
if (
isDefined(selectedItem) &&
selectedItem.itemType === NAVIGATION_MENU_ITEM_TYPE.LINK
) {
return (
<CommandMenuEditLinkItemView
key={selectedItem.id}
selectedItem={selectedItem}
onUpdateLink={(linkId, link) => updateLinkInDraft(linkId, { link })}
onOpenFolderPicker={setFolderPicker}
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
/>
);
}
if (selectedItemType === NAVIGATION_MENU_ITEM_TYPE.LINK) {
return null;
}
if (selectedItemType === NAVIGATION_MENU_ITEM_TYPE.FOLDER) {
return (
<CommandMenuList
commandGroups={[]}
selectableItemIds={getOrganizeActionsSelectableItemIds(false)}
>
<CommandMenuEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
/>
<CommandMenuEditOwnerSection />
</CommandMenuList>
);
}
return (
<CommandMenuList
commandGroups={[]}
selectableItemIds={getOrganizeActionsSelectableItemIds(true)}
>
<CommandMenuEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
showMoveToFolder
onMoveToFolder={setFolderPicker}
/>
</CommandMenuList>
);
};
@@ -0,0 +1,116 @@
import { useLingui } from '@lingui/react/macro';
import {
IconAddressBook,
IconCube,
IconFolder,
IconLink,
IconList,
} from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuAddToNavDraggablePlaceholder } from '@/command-menu/components/CommandMenuAddToNavDraggablePlaceholder';
import { CommandMenuAddToNavDroppable } from '@/command-menu/components/CommandMenuAddToNavDroppable';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuItemWithAddToNavigationDrag } from '@/command-menu/components/CommandMenuItemWithAddToNavigationDrag';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type CommandMenuNewSidebarItemMainMenuProps = {
onSelectObject: () => void;
onSelectView: () => void;
onSelectRecord: () => void;
onAddFolder: () => void;
onAddLink: () => void;
};
export const CommandMenuNewSidebarItemMainMenu = ({
onSelectObject,
onSelectView,
onSelectRecord,
onAddFolder,
onAddLink,
}: CommandMenuNewSidebarItemMainMenuProps) => {
const { t } = useLingui();
return (
<CommandMenuAddToNavDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<CommandMenuList
commandGroups={[]}
selectableItemIds={['object', 'view', 'record', 'folder', 'link']}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
<CommandGroup heading={t`Data`}>
<CommandMenuAddToNavDraggablePlaceholder index={0}>
<SelectableListItem itemId="object" onEnter={onSelectObject}>
<CommandMenuItem
Icon={IconCube}
label={t`Object`}
id="object"
hasSubMenu={true}
onClick={onSelectObject}
/>
</SelectableListItem>
</CommandMenuAddToNavDraggablePlaceholder>
<CommandMenuAddToNavDraggablePlaceholder index={1}>
<SelectableListItem itemId="view" onEnter={onSelectView}>
<CommandMenuItem
Icon={IconList}
label={t`View`}
id="view"
hasSubMenu={true}
onClick={onSelectView}
/>
</SelectableListItem>
</CommandMenuAddToNavDraggablePlaceholder>
<CommandMenuAddToNavDraggablePlaceholder index={2}>
<SelectableListItem itemId="record" onEnter={onSelectRecord}>
<CommandMenuItem
Icon={IconAddressBook}
label={t`Record`}
id="record"
hasSubMenu={true}
onClick={onSelectRecord}
/>
</SelectableListItem>
</CommandMenuAddToNavDraggablePlaceholder>
</CommandGroup>
<CommandGroup heading={t`Other`}>
<SelectableListItem itemId="folder" onEnter={onAddFolder}>
<CommandMenuItemWithAddToNavigationDrag
icon={IconFolder}
label={t`Folder`}
id="folder"
onClick={onAddFolder}
dragIndex={3}
payload={{
type: 'folder',
folderId: 'new',
name: t`New folder`,
}}
/>
</SelectableListItem>
<SelectableListItem itemId="link" onEnter={onAddLink}>
<CommandMenuItemWithAddToNavigationDrag
icon={IconLink}
label={t`Link`}
id="link"
onClick={onAddLink}
dragIndex={4}
payload={{
type: 'link',
linkId: 'new',
name: t`Link label`,
link: 'https://www.example.com',
}}
/>
</SelectableListItem>
</CommandGroup>
{placeholder}
</div>
</CommandMenuList>
)}
</CommandMenuAddToNavDroppable>
);
};
@@ -0,0 +1,265 @@
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconFolder, IconLink } from 'twenty-ui/display';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandMenuNewSidebarItemMainMenu } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemMainMenu';
import { CommandMenuNewSidebarItemRecordSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemRecordSubView';
import { CommandMenuNewSidebarItemViewObjectPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemViewObjectPickerSubView';
import { CommandMenuNewSidebarItemViewPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemViewPickerSubView';
import { CommandMenuNewSidebarItemViewSystemSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemViewSystemSubView';
import { CommandMenuObjectPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuObjectPickerSubView';
import { CommandMenuSystemObjectPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuSystemObjectPickerSubView';
import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddFolderToNavigationMenuDraft';
import { useAddLinkToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddLinkToNavigationMenuDraft';
import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddObjectToNavigationMenuDraft';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
import { useOpenNavigationMenuItemInCommandMenu } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInCommandMenu';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ViewKey } from '@/views/types/ViewKey';
type SelectedOption =
| 'object'
| 'record'
| 'system'
| 'view'
| 'view-system'
| null;
export const CommandMenuNewSidebarItemPage = () => {
const { t } = useLingui();
const { closeCommandMenu } = useCommandMenu();
const [selectedOption, setSelectedOption] = useState<SelectedOption>(null);
const [selectedObjectMetadataIdForView, setSelectedObjectMetadataIdForView] =
useState<string | null>(null);
const [objectSearchInput, setObjectSearchInput] = useState('');
const [systemObjectSearchInput, setSystemObjectSearchInput] = useState('');
const { objectMetadataItems } = useObjectMetadataItems();
const { addObjectToDraft } = useAddObjectToNavigationMenuDraft();
const { addFolderToDraft } = useAddFolderToNavigationMenuDraft();
const { addLinkToDraft } = useAddLinkToNavigationMenuDraft();
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
const navigationMenuItemsDraft = useRecoilValue(
navigationMenuItemsDraftState,
);
const setSelectedNavigationMenuItemInEditMode = useSetRecoilState(
selectedNavigationMenuItemInEditModeState,
);
const { openNavigationMenuItemInCommandMenu } =
useOpenNavigationMenuItemInCommandMenu();
const { activeNonSystemObjectMetadataItems } =
useFilteredObjectMetadataItems();
const currentDraft = isDefined(navigationMenuItemsDraft)
? navigationMenuItemsDraft
: workspaceNavigationMenuItems;
const addMenuItemInsertionContext = useRecoilValue(
addMenuItemInsertionContextState,
);
const setAddMenuItemInsertionContext = useSetRecoilState(
addMenuItemInsertionContextState,
);
const { views, objectMetadataIdsWithIndexView } =
useNavigationMenuObjectMetadataFromDraft(currentDraft);
const objectMetadataIdsWithDisplayableViews = new Set(
views
.filter((view) => view.key !== ViewKey.Index)
.map((view) => view.objectMetadataId),
);
const availableObjectMetadataItems = [
...activeNonSystemObjectMetadataItems,
].sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
const activeSystemObjectMetadataItems = objectMetadataItems
.filter((item) => item.isActive && item.isSystem)
.sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
const availableSystemObjectMetadataItems =
activeSystemObjectMetadataItems.filter((item) =>
objectMetadataIdsWithIndexView.has(item.id),
);
const objectMetadataItemsWithViews = objectMetadataItems
.filter(
(item) =>
item.isActive && objectMetadataIdsWithDisplayableViews.has(item.id),
)
.filter((item) => !item.isSystem)
.sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
const availableSystemObjectMetadataItemsForView =
activeSystemObjectMetadataItems.filter((item) =>
objectMetadataIdsWithDisplayableViews.has(item.id),
);
const handleSelectObject = (
objectMetadataItem: ObjectMetadataItem,
defaultViewId: string,
) => {
addObjectToDraft(
objectMetadataItem.id,
defaultViewId,
currentDraft,
addMenuItemInsertionContext?.targetFolderId,
addMenuItemInsertionContext?.targetIndex,
);
setAddMenuItemInsertionContext(null);
closeCommandMenu();
};
const handleBackToMain = () => {
setSelectedOption(null);
setSelectedObjectMetadataIdForView(null);
setObjectSearchInput('');
setSystemObjectSearchInput('');
};
const handleBackToObjectList = () => {
setSelectedOption('object');
setSystemObjectSearchInput('');
};
const handleBackToViewObjectList = () => {
const selectedObjectMetadataItem = isDefined(
selectedObjectMetadataIdForView,
)
? objectMetadataItems.find(
(item) => item.id === selectedObjectMetadataIdForView,
)
: undefined;
const cameFromSystemObjects = selectedObjectMetadataItem?.isSystem ?? false;
setSelectedObjectMetadataIdForView(null);
if (cameFromSystemObjects) {
setSelectedOption('view-system');
}
};
const handleBackToViewObjectListFromSystem = () => {
setSelectedOption('view');
setSystemObjectSearchInput('');
};
const handleAddFolderAndOpenEdit = () => {
const newFolderId = addFolderToDraft(
t`New folder`,
currentDraft,
addMenuItemInsertionContext?.targetFolderId ?? null,
addMenuItemInsertionContext?.targetIndex,
);
setAddMenuItemInsertionContext(null);
setSelectedNavigationMenuItemInEditMode(newFolderId);
openNavigationMenuItemInCommandMenu({
pageTitle: t`Edit folder`,
pageIcon: IconFolder,
focusTitleInput: true,
});
};
const handleAddLinkAndOpenEdit = () => {
const newLinkId = addLinkToDraft(
t`Link label`,
'www.example.com',
currentDraft,
addMenuItemInsertionContext?.targetFolderId ?? null,
addMenuItemInsertionContext?.targetIndex,
);
setAddMenuItemInsertionContext(null);
setSelectedNavigationMenuItemInEditMode(newLinkId);
openNavigationMenuItemInCommandMenu({
pageTitle: t`Edit link`,
pageIcon: IconLink,
focusTitleInput: true,
});
};
switch (selectedOption) {
case 'view':
if (isDefined(selectedObjectMetadataIdForView)) {
return (
<CommandMenuNewSidebarItemViewPickerSubView
selectedObjectMetadataIdForView={selectedObjectMetadataIdForView}
onBack={handleBackToViewObjectList}
/>
);
}
return (
<CommandMenuNewSidebarItemViewObjectPickerSubView
objects={objectMetadataItemsWithViews}
searchValue={objectSearchInput}
onSearchChange={setObjectSearchInput}
onBack={handleBackToMain}
onOpenSystemPicker={() => setSelectedOption('view-system')}
onSelectObject={(item) => setSelectedObjectMetadataIdForView(item.id)}
showSystemObjectsOption={
availableSystemObjectMetadataItemsForView.length > 0
}
/>
);
case 'view-system':
return (
<CommandMenuNewSidebarItemViewSystemSubView
systemObjects={availableSystemObjectMetadataItemsForView}
searchValue={systemObjectSearchInput}
onSearchChange={setSystemObjectSearchInput}
onBack={handleBackToViewObjectListFromSystem}
onSelectObject={(item) => {
setSelectedObjectMetadataIdForView(item.id);
setSelectedOption('view');
}}
/>
);
case 'object':
return (
<CommandMenuObjectPickerSubView
objects={availableObjectMetadataItems}
searchValue={objectSearchInput}
onSearchChange={setObjectSearchInput}
onBack={handleBackToMain}
onOpenSystemPicker={() => setSelectedOption('system')}
isViewItem={false}
onChangeObject={handleSelectObject}
objectMenuItemVariant="add"
/>
);
case 'system':
return (
<CommandMenuSystemObjectPickerSubView
systemObjects={availableSystemObjectMetadataItems}
searchValue={systemObjectSearchInput}
onSearchChange={setSystemObjectSearchInput}
onBack={handleBackToObjectList}
isViewItem={false}
onChangeObject={handleSelectObject}
objectMenuItemVariant="add"
emptyNoResultsText={t`All system objects are already in the sidebar`}
/>
);
case 'record':
return (
<CommandMenuNewSidebarItemRecordSubView onBack={handleBackToMain} />
);
default:
return (
<CommandMenuNewSidebarItemMainMenu
onSelectObject={() => setSelectedOption('object')}
onSelectView={() => setSelectedOption('view')}
onSelectRecord={() => setSelectedOption('record')}
onAddFolder={handleAddFolderAndOpenEdit}
onAddLink={handleAddLinkAndOpenEdit}
/>
);
}
};
@@ -0,0 +1,94 @@
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Avatar } from 'twenty-ui/display';
import { CommandMenuItemWithAddToNavigationDrag } from '@/command-menu/components/CommandMenuItemWithAddToNavigationDrag';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useNavigationMenuItemEditFolderData } from '@/command-menu/pages/navigation-menu-item/hooks/useNavigationMenuItemEditFolderData';
import { useAddRecordToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddRecordToNavigationMenuDraft';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type SearchRecord = {
recordId: string;
objectNameSingular: string;
label: string;
imageUrl?: string | null;
};
type CommandMenuNewSidebarItemRecordItemProps = {
record: SearchRecord;
dragIndex?: number;
};
export const CommandMenuNewSidebarItemRecordItem = ({
record,
dragIndex,
}: CommandMenuNewSidebarItemRecordItemProps) => {
const { closeCommandMenu } = useCommandMenu();
const { addRecordToDraft } = useAddRecordToNavigationMenuDraft();
const { currentDraft } = useNavigationMenuItemEditFolderData();
const addMenuItemInsertionContext = useRecoilValue(
addMenuItemInsertionContextState,
);
const setAddMenuItemInsertionContext = useSetRecoilState(
addMenuItemInsertionContextState,
);
const { objectMetadataItems } = useObjectMetadataItems();
const objectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === record.objectNameSingular,
);
const recordPayload: AddToNavigationDragPayload = {
type: 'record',
recordId: record.recordId,
objectMetadataId: objectMetadataItem?.id ?? '',
objectNameSingular: record.objectNameSingular,
label: record.label,
imageUrl: record.imageUrl,
};
const handleSelectRecord = () => {
addRecordToDraft(
{
recordId: record.recordId,
objectNameSingular: record.objectNameSingular,
label: record.label,
imageUrl: record.imageUrl,
},
currentDraft,
addMenuItemInsertionContext?.targetFolderId ?? null,
addMenuItemInsertionContext?.targetIndex,
);
setAddMenuItemInsertionContext(null);
closeCommandMenu();
};
return (
<SelectableListItem itemId={record.recordId} onEnter={handleSelectRecord}>
<CommandMenuItemWithAddToNavigationDrag
customIconContent={
<Avatar
type={
record.objectNameSingular === CoreObjectNameSingular.Company
? 'squared'
: 'rounded'
}
avatarUrl={record.imageUrl}
placeholderColorSeed={record.recordId}
placeholder={record.label}
/>
}
label={record.label}
description={
objectMetadataItem?.labelSingular ?? record.objectNameSingular
}
id={record.recordId}
onClick={handleSelectRecord}
dragIndex={dragIndex}
payload={recordPayload}
/>
</SelectableListItem>
);
};
@@ -0,0 +1,119 @@
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useDebounce } from 'use-debounce';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuAddToNavDroppable } from '@/command-menu/components/CommandMenuAddToNavDroppable';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults';
import { CommandMenuNewSidebarItemRecordItem } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemRecordItem';
import { useNavigationMenuItemEditFolderData } from '@/command-menu/pages/navigation-menu-item/hooks/useNavigationMenuItemEditFolderData';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { useSearchQuery } from '~/generated/graphql';
type SearchRecordBase = {
recordId: string;
objectNameSingular: string;
label: string;
imageUrl?: string | null;
};
type CommandMenuNewSidebarItemRecordSubViewProps = {
onBack: () => void;
};
export const CommandMenuNewSidebarItemRecordSubView = ({
onBack,
}: CommandMenuNewSidebarItemRecordSubViewProps) => {
const { t } = useLingui();
const { currentDraft } = useNavigationMenuItemEditFolderData();
const { objectMetadataItems } = useObjectMetadataItems();
const [recordSearchInput, setRecordSearchInput] = useState('');
const [deferredRecordSearchInput] = useDebounce(recordSearchInput, 300);
const coreClient = useApolloCoreClient();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const nonReadableObjectMetadataItemsNameSingular = objectMetadataItems
.filter(
(objectMetadataItem) =>
!getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
})?.canReadObjectRecords,
)
.map((objectMetadataItem) => objectMetadataItem.nameSingular);
const { data: searchData, loading: recordSearchLoading } = useSearchQuery({
client: coreClient,
variables: {
searchInput: deferredRecordSearchInput ?? '',
limit: MAX_SEARCH_RESULTS,
excludedObjectNameSingulars: [
'workspaceMember',
...nonReadableObjectMetadataItemsNameSingular,
],
},
});
const workspaceRecordIds = new Set(
currentDraft.flatMap((item) =>
isDefined(item.targetRecordId) ? [item.targetRecordId] : [],
),
);
const searchRecords =
searchData?.search?.edges?.map((edge) => edge.node) ?? [];
const availableSearchRecords = searchRecords.filter(
(record) => !workspaceRecordIds.has(record.recordId),
) as SearchRecordBase[];
const isEmpty = availableSearchRecords.length === 0 && !recordSearchLoading;
const selectableItemIds = isEmpty
? []
: availableSearchRecords.map((record) => record.recordId);
const noResultsText =
deferredRecordSearchInput.length > 0
? t`No results found`
: t`Type to search records`;
return (
<CommandMenuSubViewWithSearch
backBarTitle={t`Add a record`}
onBack={onBack}
searchPlaceholder={t`Search records...`}
searchValue={recordSearchInput}
onSearchChange={setRecordSearchInput}
>
<CommandMenuAddToNavDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
loading={recordSearchLoading}
noResults={isEmpty}
noResultsText={noResultsText}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
<CommandGroup heading={t`Results`}>
{availableSearchRecords.map((record, index) => (
<CommandMenuNewSidebarItemRecordItem
key={record.recordId}
record={record}
dragIndex={index}
/>
))}
</CommandGroup>
{placeholder}
</div>
</CommandMenuList>
)}
</CommandMenuAddToNavDroppable>
</CommandMenuSubViewWithSearch>
);
};
@@ -0,0 +1,101 @@
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { IconSettings, 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 { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
import { IconWithBackground } from '@/navigation-menu-item/components/IconWithBackground';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type CommandMenuNewSidebarItemViewObjectPickerSubViewProps = {
objects: ObjectMetadataItem[];
searchValue: string;
onSearchChange: (value: string) => void;
onBack: () => void;
onOpenSystemPicker: () => void;
onSelectObject: (objectMetadataItem: ObjectMetadataItem) => void;
showSystemObjectsOption?: boolean;
};
export const CommandMenuNewSidebarItemViewObjectPickerSubView = ({
objects,
searchValue,
onSearchChange,
onBack,
onOpenSystemPicker,
onSelectObject,
showSystemObjectsOption = true,
}: CommandMenuNewSidebarItemViewObjectPickerSubViewProps) => {
const { t } = useLingui();
const theme = useTheme();
const { getIcon } = useIcons();
const iconColors = getNavigationMenuItemIconColors(theme);
const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } =
useFilteredPickerItems({
items: objects,
searchQuery: searchValue,
getSearchableValues: (item) => [item.labelPlural],
appendSelectableIds: showSystemObjectsOption ? ['system'] : [],
});
const noResultsText = hasSearchQuery
? t`No results found`
: t`No objects with views found`;
return (
<CommandMenuSubViewWithSearch
backBarTitle={t`Pick an object`}
onBack={onBack}
searchPlaceholder={t`Search an object...`}
searchValue={searchValue}
onSearchChange={onSearchChange}
>
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
<CommandGroup heading={t`Objects`}>
{filteredItems.map((objectMetadataItem) => (
<SelectableListItem
key={objectMetadataItem.id}
itemId={objectMetadataItem.id}
onEnter={() => onSelectObject(objectMetadataItem)}
>
<CommandMenuItem
Icon={({ size, stroke }) => (
<IconWithBackground
Icon={getIcon(objectMetadataItem.icon)}
backgroundColor={iconColors.object}
size={size}
stroke={stroke}
/>
)}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
hasSubMenu
onClick={() => onSelectObject(objectMetadataItem)}
/>
</SelectableListItem>
))}
{showSystemObjectsOption && (
<SelectableListItem itemId="system" onEnter={onOpenSystemPicker}>
<CommandMenuItem
Icon={IconSettings}
label={t`System objects`}
id="system"
hasSubMenu={true}
onClick={onOpenSystemPicker}
/>
</SelectableListItem>
)}
</CommandGroup>
</CommandMenuList>
</CommandMenuSubViewWithSearch>
);
};
@@ -0,0 +1,132 @@
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
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';
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 { useNavigationMenuItemEditFolderData } from '@/command-menu/pages/navigation-menu-item/hooks/useNavigationMenuItemEditFolderData';
import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddViewToNavigationMenuDraft';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { type View } from '@/views/types/View';
import { ViewKey } from '@/views/types/ViewKey';
type CommandMenuNewSidebarItemViewPickerSubViewProps = {
selectedObjectMetadataIdForView: string;
onBack: () => void;
};
export const CommandMenuNewSidebarItemViewPickerSubView = ({
selectedObjectMetadataIdForView,
onBack,
}: CommandMenuNewSidebarItemViewPickerSubViewProps) => {
const { t } = useLingui();
const { getIcon } = useIcons();
const [searchValue, setSearchValue] = useState('');
const { closeCommandMenu } = useCommandMenu();
const { addViewToDraft } = useAddViewToNavigationMenuDraft();
const { currentDraft } = useNavigationMenuItemEditFolderData();
const addMenuItemInsertionContext = useRecoilValue(
addMenuItemInsertionContextState,
);
const setAddMenuItemInsertionContext = useSetRecoilState(
addMenuItemInsertionContextState,
);
const { objectMetadataItems } = useObjectMetadataItems();
const { views } = useNavigationMenuObjectMetadataFromDraft(currentDraft);
const viewsForSelectedObject = views
.filter(
(view) =>
view.objectMetadataId === selectedObjectMetadataIdForView &&
view.key !== ViewKey.Index,
)
.sort((a, b) => a.position - b.position);
const selectedObjectMetadataItem = objectMetadataItems.find(
(item) => item.id === selectedObjectMetadataIdForView,
);
const backBarTitle =
selectedObjectMetadataItem?.labelPlural ?? t`Pick a view`;
const {
filteredItems: filteredViews,
selectableItemIds,
isEmpty,
hasSearchQuery,
} = useFilteredPickerItems({
items: viewsForSelectedObject,
searchQuery: searchValue,
getSearchableValues: (view) => [view.name],
});
const noResultsText = hasSearchQuery
? t`No results found`
: t`No custom views available`;
const handleSelectView = (view: View) => {
addViewToDraft(
view.id,
currentDraft,
addMenuItemInsertionContext?.targetFolderId ?? null,
addMenuItemInsertionContext?.targetIndex,
);
setAddMenuItemInsertionContext(null);
closeCommandMenu();
};
return (
<CommandMenuSubViewWithSearch
backBarTitle={backBarTitle}
onBack={onBack}
searchPlaceholder={t`Search a view...`}
searchValue={searchValue}
onSearchChange={setSearchValue}
>
<CommandMenuAddToNavDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
<CommandGroup heading={t`Views`}>
{filteredViews.map((view, index) => (
<SelectableListItem
key={view.id}
itemId={view.id}
onEnter={() => handleSelectView(view)}
>
<CommandMenuItemWithAddToNavigationDrag
icon={getIcon(view.icon)}
label={view.name}
id={view.id}
onClick={() => handleSelectView(view)}
dragIndex={index}
payload={{
type: 'view',
viewId: view.id,
label: view.name,
}}
/>
</SelectableListItem>
))}
</CommandGroup>
{placeholder}
</div>
</CommandMenuList>
)}
</CommandMenuAddToNavDroppable>
</CommandMenuSubViewWithSearch>
);
};
@@ -0,0 +1,88 @@
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
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 { IconWithBackground } from '@/navigation-menu-item/components/IconWithBackground';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type CommandMenuNewSidebarItemViewSystemSubViewProps = {
systemObjects: ObjectMetadataItem[];
searchValue: string;
onSearchChange: (value: string) => void;
onBack: () => void;
onSelectObject: (objectMetadataItem: ObjectMetadataItem) => void;
};
export const CommandMenuNewSidebarItemViewSystemSubView = ({
systemObjects,
searchValue,
onSearchChange,
onBack,
onSelectObject,
}: CommandMenuNewSidebarItemViewSystemSubViewProps) => {
const { t } = useLingui();
const theme = useTheme();
const { getIcon } = useIcons();
const iconColors = getNavigationMenuItemIconColors(theme);
const {
filteredItems: filteredSystemObjectMetadataItems,
selectableItemIds,
isEmpty,
hasSearchQuery,
} = useFilteredPickerItems({
items: systemObjects,
searchQuery: searchValue,
getSearchableValues: (item) => [item.labelPlural],
});
const noResultsText = hasSearchQuery
? t`No results found`
: t`No system objects with views found`;
return (
<CommandMenuSubViewWithSearch
backBarTitle={t`System objects`}
onBack={onBack}
searchPlaceholder={t`Search a system object...`}
searchValue={searchValue}
onSearchChange={onSearchChange}
>
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
<CommandGroup heading={t`System objects`}>
{filteredSystemObjectMetadataItems.map((objectMetadataItem) => (
<SelectableListItem
key={objectMetadataItem.id}
itemId={objectMetadataItem.id}
onEnter={() => onSelectObject(objectMetadataItem)}
>
<CommandMenuItem
Icon={({ size, stroke }) => (
<IconWithBackground
Icon={getIcon(objectMetadataItem.icon)}
backgroundColor={iconColors.object}
size={size}
stroke={stroke}
/>
)}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
onClick={() => onSelectObject(objectMetadataItem)}
/>
</SelectableListItem>
))}
</CommandGroup>
</CommandMenuList>
</CommandMenuSubViewWithSearch>
);
};
@@ -0,0 +1,82 @@
import { useTheme } from '@emotion/react';
import { useRecoilValue } from 'recoil';
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 { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
type CommandMenuObjectMenuItemProps = {
objectMetadataItem: ObjectMetadataItem;
onSelect: (
objectMetadataItem: ObjectMetadataItem,
defaultViewId: string,
) => void;
variant: 'add' | 'edit';
dragIndex?: number;
};
export const CommandMenuObjectMenuItem = ({
objectMetadataItem,
onSelect,
variant,
dragIndex,
}: CommandMenuObjectMenuItemProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const iconColors = getNavigationMenuItemIconColors(theme);
const defaultViewId = useRecoilValue(
coreIndexViewIdFromObjectMetadataItemFamilySelector({
objectMetadataItemId: objectMetadataItem.id,
}),
);
const Icon = getIcon(objectMetadataItem.icon);
const isDisabled = !isDefined(defaultViewId);
const handleClick = () => {
if (!defaultViewId) {
return;
}
onSelect(objectMetadataItem, defaultViewId);
};
return (
<SelectableListItem itemId={objectMetadataItem.id} onEnter={handleClick}>
{variant === 'add' && !isDisabled ? (
<CommandMenuItemWithAddToNavigationDrag
icon={Icon}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
onClick={handleClick}
dragIndex={dragIndex}
payload={{
type: 'object' as const,
objectMetadataId: objectMetadataItem.id,
defaultViewId: defaultViewId ?? '',
label: objectMetadataItem.labelPlural,
}}
/>
) : (
<CommandMenuItem
Icon={({ size, stroke }) => (
<IconWithBackground
Icon={Icon}
backgroundColor={iconColors.object}
size={size}
stroke={stroke}
/>
)}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
onClick={handleClick}
disabled={isDisabled}
/>
)}
</SelectableListItem>
);
};
@@ -0,0 +1,67 @@
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 { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type CommandMenuObjectPickerItemProps = {
objectMetadataItem: ObjectMetadataItem;
isViewItem: boolean;
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
onChangeObject: (
objectMetadataItem: ObjectMetadataItem,
defaultViewId: string,
) => void;
objectMenuItemVariant?: 'add' | 'edit';
dragIndex?: number;
};
export const CommandMenuObjectPickerItem = ({
objectMetadataItem,
isViewItem,
onSelectObjectForViewEdit,
onChangeObject,
objectMenuItemVariant = 'edit',
dragIndex,
}: CommandMenuObjectPickerItemProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const iconColors = getNavigationMenuItemIconColors(theme);
if (isViewItem && isDefined(onSelectObjectForViewEdit)) {
return (
<SelectableListItem
itemId={objectMetadataItem.id}
onEnter={() => onSelectObjectForViewEdit(objectMetadataItem)}
>
<CommandMenuItem
Icon={({ size, stroke }) => (
<IconWithBackground
Icon={getIcon(objectMetadataItem.icon)}
backgroundColor={iconColors.object}
size={size}
stroke={stroke}
/>
)}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
onClick={() => onSelectObjectForViewEdit(objectMetadataItem)}
/>
</SelectableListItem>
);
}
return (
<CommandMenuObjectMenuItem
objectMetadataItem={objectMetadataItem}
onSelect={onChangeObject}
variant={objectMenuItemVariant}
dragIndex={dragIndex}
/>
);
};
@@ -0,0 +1,134 @@
import { useLingui } from '@lingui/react/macro';
import { IconSettings } from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuAddToNavDraggablePlaceholder } from '@/command-menu/components/CommandMenuAddToNavDraggablePlaceholder';
import { CommandMenuAddToNavDroppable } from '@/command-menu/components/CommandMenuAddToNavDroppable';
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 { CommandMenuObjectPickerItem } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuObjectPickerItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type CommandMenuObjectPickerSubViewProps = {
objects: ObjectMetadataItem[];
searchValue: string;
onSearchChange: (value: string) => void;
onBack: () => void;
onOpenSystemPicker: () => void;
isViewItem: boolean;
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
onChangeObject: (
objectMetadataItem: ObjectMetadataItem,
defaultViewId: string,
) => void;
objectMenuItemVariant?: 'add' | 'edit';
emptyNoResultsText?: string;
};
export const CommandMenuObjectPickerSubView = ({
objects,
searchValue,
onSearchChange,
onBack,
onOpenSystemPicker,
isViewItem,
onSelectObjectForViewEdit,
onChangeObject,
objectMenuItemVariant = 'edit',
emptyNoResultsText,
}: CommandMenuObjectPickerSubViewProps) => {
const { t } = useLingui();
const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } =
useFilteredPickerItems({
items: objects,
searchQuery: searchValue,
getSearchableValues: (item) => [item.labelPlural],
appendSelectableIds: ['system'],
});
const noResultsText = hasSearchQuery
? t`No results found`
: (emptyNoResultsText ?? t`All objects are already in the sidebar`);
const isAddVariant = objectMenuItemVariant === 'add';
const listContent = (
<CommandGroup heading={t`Objects`}>
{filteredItems.map((objectMetadataItem, index) => (
<CommandMenuObjectPickerItem
key={objectMetadataItem.id}
objectMetadataItem={objectMetadataItem}
isViewItem={isViewItem}
onSelectObjectForViewEdit={onSelectObjectForViewEdit}
onChangeObject={onChangeObject}
objectMenuItemVariant={objectMenuItemVariant}
dragIndex={isAddVariant ? index : undefined}
/>
))}
{isAddVariant ? (
<CommandMenuAddToNavDraggablePlaceholder index={filteredItems.length}>
<SelectableListItem itemId="system" onEnter={onOpenSystemPicker}>
<CommandMenuItem
Icon={IconSettings}
label={t`System objects`}
id="system"
hasSubMenu={true}
onClick={onOpenSystemPicker}
/>
</SelectableListItem>
</CommandMenuAddToNavDraggablePlaceholder>
) : (
<SelectableListItem itemId="system" onEnter={onOpenSystemPicker}>
<CommandMenuItem
Icon={IconSettings}
label={t`System objects`}
id="system"
hasSubMenu={true}
onClick={onOpenSystemPicker}
/>
</SelectableListItem>
)}
</CommandGroup>
);
return (
<CommandMenuSubViewWithSearch
backBarTitle={t`Pick an object`}
onBack={onBack}
searchPlaceholder={t`Search an object...`}
searchValue={searchValue}
onSearchChange={onSearchChange}
>
{isAddVariant ? (
<CommandMenuAddToNavDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
{listContent}
{placeholder}
</div>
</CommandMenuList>
)}
</CommandMenuAddToNavDroppable>
) : (
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
{listContent}
</CommandMenuList>
)}
</CommandMenuSubViewWithSearch>
);
};
@@ -0,0 +1,77 @@
import { useLingui } from '@lingui/react/macro';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuObjectPickerItem } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuObjectPickerItem';
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
type CommandMenuSystemObjectPickerSubViewProps = {
systemObjects: ObjectMetadataItem[];
searchValue: string;
onSearchChange: (value: string) => void;
onBack: () => void;
isViewItem: boolean;
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
onChangeObject: (
objectMetadataItem: ObjectMetadataItem,
defaultViewId: string,
) => void;
objectMenuItemVariant?: 'add' | 'edit';
emptyNoResultsText?: string;
};
export const CommandMenuSystemObjectPickerSubView = ({
systemObjects,
searchValue,
onSearchChange,
onBack,
isViewItem,
onSelectObjectForViewEdit,
onChangeObject,
objectMenuItemVariant = 'edit',
emptyNoResultsText,
}: CommandMenuSystemObjectPickerSubViewProps) => {
const { t } = useLingui();
const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } =
useFilteredPickerItems({
items: systemObjects,
searchQuery: searchValue,
getSearchableValues: (item) => [item.labelPlural],
});
const noResultsText = hasSearchQuery
? t`No results found`
: (emptyNoResultsText ?? t`No system objects available`);
return (
<CommandMenuSubViewWithSearch
backBarTitle={t`System objects`}
onBack={onBack}
searchPlaceholder={t`Search a system object...`}
searchValue={searchValue}
onSearchChange={onSearchChange}
>
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
<CommandGroup heading={t`System objects`}>
{filteredItems.map((objectMetadataItem) => (
<CommandMenuObjectPickerItem
key={objectMetadataItem.id}
objectMetadataItem={objectMetadataItem}
isViewItem={isViewItem}
onSelectObjectForViewEdit={onSelectObjectForViewEdit}
onChangeObject={onChangeObject}
objectMenuItemVariant={objectMenuItemVariant}
/>
))}
</CommandGroup>
</CommandMenuList>
</CommandMenuSubViewWithSearch>
);
};
@@ -0,0 +1,102 @@
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useNavigationMenuItemEditFolderData } from '@/command-menu/pages/navigation-menu-item/hooks/useNavigationMenuItemEditFolderData';
import { useNavigationMenuItemEditSubView } from '@/command-menu/pages/navigation-menu-item/hooks/useNavigationMenuItemEditSubView';
import { useSelectedNavigationMenuItemEditData } from '@/command-menu/pages/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditData';
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/hooks/useNavigationMenuItemMoveRemove';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { NAVIGATION_MENU_ITEM_TYPE } from '@/navigation-menu-item/types/navigation-menu-item-type';
type FolderOption = {
id: string;
name: string;
folderId?: string;
};
const getDescendantFolderIds = (
folderId: string,
allFolders: FolderOption[],
): Set<string> => {
const result = new Set<string>();
for (const folder of allFolders) {
if (folder.folderId !== folderId) continue;
result.add(folder.id);
getDescendantFolderIds(folder.id, allFolders).forEach((id) =>
result.add(id),
);
}
return result;
};
const excludeCurrentFolder = <T extends { id: string }>(
folders: T[],
currentFolderId: string | null,
): T[] =>
!isDefined(currentFolderId)
? folders
: folders.filter((folder) => folder.id !== currentFolderId);
export const useFolderPickerSelectionData = () => {
const { closeCommandMenu } = useCommandMenu();
const { clearSubView } = useNavigationMenuItemEditSubView();
const { moveToFolder } = useNavigationMenuItemMoveRemove();
const selectedNavigationMenuItemInEditMode = useRecoilValue(
selectedNavigationMenuItemInEditModeState,
);
const { selectedItem, selectedItemType } =
useSelectedNavigationMenuItemEditData();
const { allFolders, workspaceFolders } =
useNavigationMenuItemEditFolderData();
const selectedFolderId =
selectedItemType === NAVIGATION_MENU_ITEM_TYPE.FOLDER
? selectedNavigationMenuItemInEditMode
: null;
const currentFolderId =
selectedItemType === NAVIGATION_MENU_ITEM_TYPE.FOLDER
? (selectedItem?.id ?? null)
: (selectedItem?.folderId ?? null);
const descendantFolderIds =
selectedItemType === NAVIGATION_MENU_ITEM_TYPE.FOLDER &&
isDefined(selectedFolderId)
? getDescendantFolderIds(selectedFolderId, allFolders)
: new Set<string>();
const includeNoFolderOption =
(selectedItemType === NAVIGATION_MENU_ITEM_TYPE.FOLDER &&
isDefined(selectedFolderId)) ||
(selectedItemType === NAVIGATION_MENU_ITEM_TYPE.LINK &&
isDefined(currentFolderId));
const folders =
includeNoFolderOption &&
selectedItemType === NAVIGATION_MENU_ITEM_TYPE.FOLDER &&
isDefined(selectedFolderId)
? allFolders.filter(
(folder) =>
folder.id !== selectedFolderId &&
!descendantFolderIds.has(folder.id),
)
: excludeCurrentFolder(allFolders, currentFolderId);
const foldersToShow = includeNoFolderOption
? folders
: excludeCurrentFolder(workspaceFolders, currentFolderId);
const handleSelectFolder = (folderId: string | null) => {
if (isDefined(selectedNavigationMenuItemInEditMode)) {
moveToFolder(selectedNavigationMenuItemInEditMode, folderId);
clearSubView();
closeCommandMenu();
}
};
return {
foldersToShow,
includeNoFolderOption,
handleSelectFolder,
};
};
@@ -0,0 +1,37 @@
import { useRecoilValue } from 'recoil';
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/hooks/useNavigationMenuItemsByFolder';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
export const useNavigationMenuItemEditFolderData = () => {
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
const navigationMenuItemsDraft = useRecoilValue(
navigationMenuItemsDraftState,
);
const { workspaceNavigationMenuItemsByFolder } =
useNavigationMenuItemsByFolder();
const currentDraft = navigationMenuItemsDraft ?? workspaceNavigationMenuItems;
const workspaceFolders = workspaceNavigationMenuItemsByFolder.map(
(folder) => ({
id: folder.id,
name: folder.folderName,
}),
);
const allFolders =
currentDraft?.filter(isNavigationMenuItemFolder).map((item) => ({
id: item.id,
name: item.name ?? 'Folder',
folderId: item.folderId ?? undefined,
})) ?? [];
return {
allFolders,
workspaceFolders,
currentDraft,
};
};
@@ -0,0 +1,138 @@
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { type OrganizeActionsProps } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOrganizeActions';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/hooks/useNavigationMenuItemMoveRemove';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { type AddMenuItemInsertionContext } from '@/navigation-menu-item/types/AddMenuItemInsertionContext';
const getAddMenuItemInsertionContext = (
selectedItem: { id: string; folderId?: string | null },
workspaceNavigationMenuItems: Array<{
id: string;
folderId?: string | null;
userWorkspaceId?: string | null;
}>,
offset: 0 | 1,
): AddMenuItemInsertionContext | null => {
const targetFolderId = selectedItem.folderId ?? null;
const itemsInFolder = workspaceNavigationMenuItems.filter(
(item) =>
(item.folderId ?? null) === targetFolderId &&
!isDefined(item.userWorkspaceId),
);
const selectedIndexInFolder = itemsInFolder.findIndex(
(item) => item.id === selectedItem.id,
);
if (selectedIndexInFolder < 0) {
return null;
}
return {
targetFolderId,
targetIndex: selectedIndexInFolder + offset,
};
};
export const useNavigationMenuItemEditOrganizeActions =
(): OrganizeActionsProps => {
const { t } = useLingui();
const { closeCommandMenu } = useCommandMenu();
const { navigateCommandMenu } = useNavigateCommandMenu();
const selectedNavigationMenuItemInEditMode = useRecoilValue(
selectedNavigationMenuItemInEditModeState,
);
const setSelectedNavigationMenuItemInEditMode = useSetRecoilState(
selectedNavigationMenuItemInEditModeState,
);
const setAddMenuItemInsertionContext = useSetRecoilState(
addMenuItemInsertionContextState,
);
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
const items = useWorkspaceSectionItems();
const { moveUp, moveDown, remove } = useNavigationMenuItemMoveRemove();
const selectedItem = selectedNavigationMenuItemInEditMode
? items.find((item) => item.id === selectedNavigationMenuItemInEditMode)
: undefined;
const folderId = selectedItem?.folderId ?? null;
const siblings = items
.filter(
(item) =>
('folderId' in item ? (item.folderId ?? null) : null) === folderId,
)
.sort((a, b) => a.position - b.position);
const selectedIndexInSiblings = selectedItem
? siblings.findIndex((item) => item.id === selectedItem.id)
: -1;
const canMoveUp =
selectedIndexInSiblings > 0 &&
selectedItem != null &&
isDefined(selectedNavigationMenuItemInEditMode);
const canMoveDown =
selectedIndexInSiblings >= 0 &&
selectedIndexInSiblings < siblings.length - 1 &&
selectedItem != null &&
isDefined(selectedNavigationMenuItemInEditMode);
const handleMoveUp = () => {
if (canMoveUp && isDefined(selectedNavigationMenuItemInEditMode)) {
moveUp(selectedNavigationMenuItemInEditMode);
}
};
const handleMoveDown = () => {
if (canMoveDown && isDefined(selectedNavigationMenuItemInEditMode)) {
moveDown(selectedNavigationMenuItemInEditMode);
}
};
const handleRemove = () => {
if (isDefined(selectedNavigationMenuItemInEditMode)) {
remove(selectedNavigationMenuItemInEditMode);
setSelectedNavigationMenuItemInEditMode(null);
closeCommandMenu();
}
};
const handleAddAtOffset = (offset: 0 | 1) => {
if (!isDefined(selectedItem)) return;
const context = getAddMenuItemInsertionContext(
selectedItem,
workspaceNavigationMenuItems,
offset,
);
if (!context) return;
setAddMenuItemInsertionContext(context);
navigateCommandMenu({
page: CommandMenuPages.NavigationMenuAddItem,
pageTitle: t`New sidebar item`,
pageIcon: IconPlus,
resetNavigationStack: true,
});
};
const handleAddBefore = () => handleAddAtOffset(0);
const handleAddAfter = () => handleAddAtOffset(1);
return {
canMoveUp,
canMoveDown,
onMoveUp: handleMoveUp,
onMoveDown: handleMoveDown,
onRemove: handleRemove,
onAddBefore: handleAddBefore,
onAddAfter: handleAddAfter,
};
};
@@ -0,0 +1,16 @@
import { useState } from 'react';
export type EditSubView = 'folder-picker' | null;
export const useNavigationMenuItemEditSubView = () => {
const [editSubView, setEditSubView] = useState<EditSubView>(null);
const setFolderPicker = () => setEditSubView('folder-picker');
const clearSubView = () => setEditSubView(null);
return {
editSubView,
setFolderPicker,
clearSubView,
};
};
@@ -0,0 +1,52 @@
import { useRecoilValue } from 'recoil';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { NAVIGATION_MENU_ITEM_TYPE } from '@/navigation-menu-item/types/navigation-menu-item-type';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { coreViewsState } from '@/views/states/coreViewState';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
export const useSelectedNavigationMenuItemEditData = () => {
const selectedNavigationMenuItemInEditMode = useRecoilValue(
selectedNavigationMenuItemInEditModeState,
);
const items = useWorkspaceSectionItems();
const coreViews = useRecoilValue(coreViewsState);
const views = coreViews.map(convertCoreViewToView);
const { objectMetadataItems } = useObjectMetadataItems();
const selectedItem = selectedNavigationMenuItemInEditMode
? items.find((item) => item.id === selectedNavigationMenuItemInEditMode)
: undefined;
const selectedItemType = selectedItem?.itemType ?? null;
const selectedItemObjectMetadata = selectedItem
? getObjectMetadataForNavigationMenuItem(
selectedItem,
objectMetadataItems,
views,
)
: null;
const selectedItemLabel = selectedItem
? selectedItemType === NAVIGATION_MENU_ITEM_TYPE.FOLDER
? (selectedItem.name ?? 'Folder')
: selectedItemType === NAVIGATION_MENU_ITEM_TYPE.LINK
? (selectedItem.name ?? 'Link')
: (selectedItemObjectMetadata?.labelPlural ?? '')
: null;
const processedItem =
selectedItem && selectedItem.itemType !== NAVIGATION_MENU_ITEM_TYPE.FOLDER
? selectedItem
: undefined;
return {
selectedItem,
selectedItemType,
selectedItemObjectMetadata,
selectedItemLabel,
processedItem,
};
};
@@ -0,0 +1,10 @@
export const getOrganizeActionsSelectableItemIds = (
includeMoveToFolder: boolean,
) => [
'move-up',
'move-down',
...(includeMoveToFolder ? ['move-to-folder'] : []),
'add-before',
'add-after',
'remove',
];
@@ -24,4 +24,6 @@ export enum CommandMenuPages {
PageLayoutFieldsSettings = 'page-layout-fields-settings',
PageLayoutFieldsLayout = 'page-layout-fields-layout',
ViewFrontComponent = 'view-front-component',
NavigationMenuItemEdit = 'navigation-menu-item-edit',
NavigationMenuAddItem = 'navigation-menu-add-item',
}