refactor(twenty-front): reorganize NavigationMenuItem module into common/display/edit subfolders (#18691)

## Summary

- Reorganizes the `navigation-menu-item` frontend module from a flat
structure into `common/`, `display/`, and `edit/` subfolders with
type-specific subdirectories (`link/`, `folder/`, `object/`, `view/`,
`record/`)
- Every file is now in a leaf folder describing its type: `components/`,
`hooks/`, `utils/`, `types/`, or `constants/`
- Moves 14 NavigationMenuItem-related components out of
`object-metadata/` and `side-panel/pages/` into the
`navigation-menu-item` module where they belong
- Creates type-specific display utility functions (e.g.,
`getLinkNavigationMenuItemLabel`,
`getObjectNavigationMenuItemComputedLink`) to replace generic
switch-based functions
- Unifies the Favorites section drag-and-drop from `@hello-pangea/dnd`
to `@dnd-kit/react`, matching the Workspace section's DnD library
- Renames Favorites section components from `CurrentWorkspaceMember*` to
`Favorites*` for clarity
- Deletes unused `FavoritesDragDropProviderContent` and
`NavbarDragProvider`

## Test plan

- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx lint:diff-with-main twenty-front` passes (0 warnings, 0
errors)
- [x] `npx nx test twenty-front` passes (763 suites, 4467 tests)
- [ ] Verify favorites drag-and-drop still works in the UI (reorder
items, move between folders)
- [ ] Verify workspace edit mode drag-and-drop still works
- [ ] Verify "add to navigation" drag from command menu/side panel still
works
This commit is contained in:
Charles Bochet
2026-03-17 06:46:50 +01:00
committed by GitHub
parent 1be87eb97b
commit f1dfcfd163
251 changed files with 1361 additions and 1194 deletions
@@ -1,7 +1,7 @@
import { lazy, Suspense, useContext, type ReactNode } from 'react';
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext';
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/common/constants/AddToNavSourceDroppableId';
import { NavigationDragSourceContext } from '@/navigation-menu-item/common/contexts/NavigationDragSourceContext';
import type { AddToNavDroppableProvided } from '@/command-menu/components/CommandMenuAddToNavDroppableTypes';
@@ -5,12 +5,12 @@ import { useIcons } from 'twenty-ui/display';
import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout';
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
import { sidePanelShouldFocusTitleInputComponentState } from '@/side-panel/states/sidePanelShouldFocusTitleInputComponentState';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemStyleIcon';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault';
import { NavigationMenuItemType } from 'twenty-shared/types';
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 { useUpdateFolderInDraft } from '@/navigation-menu-item/edit/folder/hooks/useUpdateFolderInDraft';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { TitleInput } from '@/ui/input/components/TitleInput';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
@@ -5,9 +5,9 @@ import { isDefined } from 'twenty-shared/utils';
import { type IconComponent } from 'twenty-ui/display';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { AddToNavigationDragHandle } from '@/navigation-menu-item/components/AddToNavigationDragHandle';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
import { AddToNavigationDragHandle } from '@/navigation-menu-item/display/dnd/components/AddToNavigationDragHandle';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/common/types/add-to-navigation-drag-payload';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
const CommandMenuItemWithAddToNavigationDragDndKit = lazy(() =>
@@ -1,11 +1,11 @@
import { useLingui } from '@lingui/react/macro';
import { IconLink, IconWorld } from 'twenty-ui/display';
import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/components/LinkIconWithLinkOverlay';
import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/display/link/components/LinkIconWithLinkOverlay';
import { NavigationMenuItemType } from 'twenty-shared/types';
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 { useUpdateLinkInDraft } from '@/navigation-menu-item/edit/link/hooks/useUpdateLinkInDraft';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout';
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
import { sidePanelShouldFocusTitleInputComponentState } from '@/side-panel/states/sidePanelShouldFocusTitleInputComponentState';
@@ -2,11 +2,11 @@ import { useLingui } from '@lingui/react/macro';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon';
import { NavigationMenuItemType } from 'twenty-shared/types';
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 { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem';
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItemLabel';
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
export const SidePanelObjectViewRecordInfo = () => {
const { t } = useLingui();
@@ -6,8 +6,8 @@ import {
} from 'twenty-ui/display';
import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
import { SidePanelAskAIInfo } from '@/side-panel/components/SidePanelAskAIInfo';
import { SidePanelFolderInfo } from '@/side-panel/components/SidePanelFolderInfo';
import { SidePanelLinkInfo } from '@/side-panel/components/SidePanelLinkInfo';
@@ -3,8 +3,8 @@ import { SidePanelAskAIPage } from '@/side-panel/pages/ask-ai/components/SidePan
import { SidePanelCalendarEventPage } from '@/side-panel/pages/calendar-event/components/SidePanelCalendarEventPage';
import { SidePanelFrontComponentPage } from '@/side-panel/pages/front-component/components/SidePanelFrontComponentPage';
import { SidePanelMessageThreadPage } from '@/side-panel/pages/message-thread/components/SidePanelMessageThreadPage';
import { SidePanelNavigationMenuItemEditPage } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNavigationMenuItemEditPage';
import { SidePanelNewSidebarItemPage } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemPage';
import { SidePanelNavigationMenuItemEditPage } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNavigationMenuItemEditPage';
import { SidePanelNewSidebarItemPage } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemPage';
import { SidePanelPageLayoutChartSettings } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutChartSettings';
import { SidePanelPageLayoutFieldsSettings } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutFieldsSettings';
import { SidePanelPageLayoutIframeSettings } from '@/side-panel/pages/page-layout/components/SidePanelPageLayoutIframeSettings';
@@ -1,10 +1,10 @@
import { SidePanelEditFolderPickerSubPage } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditFolderPickerSubPage';
import { SidePanelNewSidebarItemObjectSubPage } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemObjectSubPage';
import { SidePanelNewSidebarItemObjectSystemPickerSubPage } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemObjectSystemPickerSubPage';
import { SidePanelNewSidebarItemRecordSubPage } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemRecordSubPage';
import { SidePanelNewSidebarItemViewObjectPickerSubPage } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewObjectPickerSubPage';
import { SidePanelNewSidebarItemViewPickerSubPage } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewPickerSubPage';
import { SidePanelNewSidebarItemViewSystemPickerSubPage } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewSystemPickerSubPage';
import { SidePanelEditFolderPickerSubPage } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditFolderPickerSubPage';
import { SidePanelNewSidebarItemObjectSubPage } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemObjectSubPage';
import { SidePanelNewSidebarItemObjectSystemPickerSubPage } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemObjectSystemPickerSubPage';
import { SidePanelNewSidebarItemRecordSubPage } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemRecordSubPage';
import { SidePanelNewSidebarItemViewObjectPickerSubPage } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewObjectPickerSubPage';
import { SidePanelNewSidebarItemViewPickerSubPage } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewPickerSubPage';
import { SidePanelNewSidebarItemViewSystemPickerSubPage } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewSystemPickerSubPage';
import { SidePanelChartFilterSubPage } from '@/side-panel/pages/page-layout/components/SidePanelChartFilterSubPage';
import { SidePanelFieldsLayoutSubPage } from '@/side-panel/pages/page-layout/components/SidePanelFieldsLayoutSubPage';
import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages';
@@ -3,9 +3,9 @@ import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
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 { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/common/states/isNavigationMenuInEditModeState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
import { emitSidePanelOpenEvent } from '@/ui/layout/side-panel/utils/emitSidePanelOpenEvent';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
@@ -1,93 +0,0 @@
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 '@linaria/react';
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 { themeCssVariables } from 'twenty-ui/theme-constants';
import { type ThemeColor, MAIN_COLOR_NAMES } from 'twenty-ui/theme';
const NAVIGATION_MENU_ITEM_COLOR_DROPDOWN_ID = 'navigation-menu-item-color';
const StyledMenuStyleText = styled.span`
color: ${themeCssVariables.font.color.light};
font-size: 13px;
`;
type SidePanelEditColorOptionProps = {
navigationMenuItemId: string;
color: ThemeColor;
};
export const SidePanelEditColorOption = ({
navigationMenuItemId,
color,
}: SidePanelEditColorOptionProps) => {
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,91 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useIcons } from 'twenty-ui/display';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
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 { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch';
import { useFolderPickerSelectionData } from '@/side-panel/pages/navigation-menu-item/hooks/useFolderPickerSelectionData';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
export const SidePanelEditFolderPickerSubPage = () => {
const { t } = useLingui();
const { getIcon } = useIcons();
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 (
<SidePanelSubViewWithSearch
searchPlaceholder={t`Search a folder...`}
searchValue={searchValue}
onSearchChange={setSearchValue}
>
<SidePanelList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
<SidePanelGroup 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) => {
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>
);
})}
</SidePanelGroup>
</SidePanelList>
</SidePanelSubViewWithSearch>
);
};
@@ -1,101 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { getAbsoluteUrl } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { extractDomainFromUrl } from '@/navigation-menu-item/utils/extractDomainFromUrl';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import {
type OrganizeActionsProps,
SidePanelEditOrganizeActions,
} from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOrganizeActions';
import { SidePanelEditOwnerSection } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOwnerSection';
import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
import { TextInput } from '@/ui/input/components/TextInput';
type SidePanelEditLinkItemViewProps = OrganizeActionsProps & {
selectedItem: NavigationMenuItem;
onUpdateLink: (
linkId: string,
updates: { link?: string; name?: string },
) => void;
onOpenFolderPicker: () => void;
};
export const SidePanelEditLinkItemView = ({
selectedItem,
onUpdateLink,
canMoveUp,
canMoveDown,
onOpenFolderPicker,
onMoveUp,
onMoveDown,
onRemove,
onAddBefore,
onAddAfter,
}: SidePanelEditLinkItemViewProps) => {
const { t } = useLingui();
const [urlEditInput, setUrlEditInput] = useState('');
const [lastAutoSetName, setLastAutoSetName] = useState<string | null>(null);
const defaultLabel = t`Link label`;
const selectableItemIds = getOrganizeActionsSelectableItemIds(true);
const currentName = selectedItem.name ?? defaultLabel;
const currentDomain = selectedItem.link
? extractDomainFromUrl(getAbsoluteUrl(selectedItem.link))
: undefined;
const canAutoUpdateName =
currentName === defaultLabel ||
currentName === currentDomain ||
currentName === lastAutoSetName;
const handleUrlChange = (value: string) => {
setUrlEditInput(value);
if (!canAutoUpdateName) return;
const trimmed = value.trim();
if (!isNonEmptyString(trimmed)) return;
const domain = extractDomainFromUrl(getAbsoluteUrl(trimmed));
if (domain !== undefined) {
setLastAutoSetName(domain);
onUpdateLink(selectedItem.id, { name: domain });
}
};
const handleUrlBlur = (event: React.FocusEvent<HTMLInputElement>) => {
const value = event.target.value.trim();
if (isNonEmptyString(value)) {
onUpdateLink(selectedItem.id, { link: getAbsoluteUrl(value) });
setUrlEditInput('');
}
};
return (
<SidePanelList commandGroups={[]} selectableItemIds={selectableItemIds}>
<SidePanelGroup heading={t`Customize`}>
<TextInput
fullWidth
placeholder="www.google.com"
value={urlEditInput || selectedItem.link || ''}
onChange={handleUrlChange}
onBlur={handleUrlBlur}
/>
</SidePanelGroup>
<SidePanelEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
showMoveToFolder
onMoveToFolder={onOpenFolderPicker}
moveToFolderHasSubMenu
/>
<SidePanelEditOwnerSection applicationId={selectedItem.applicationId} />
</SidePanelList>
);
};
@@ -1,93 +0,0 @@
import { useObjectNavItemColor } from '@/navigation-menu-item/hooks/useObjectNavItemColor';
import { navigationMenuItemsSelector } from '@/navigation-menu-item/states/navigationMenuItemsSelector';
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { SidePanelEditColorOption } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditColorOption';
import {
type OrganizeActionsProps,
SidePanelEditOrganizeActions,
} from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOrganizeActions';
import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
type SidePanelEditObjectViewBaseProps = OrganizeActionsProps & {
onOpenFolderPicker: () => void;
showColorOption?: boolean;
selectedItem?: NavigationMenuItem | null;
};
export const SidePanelEditObjectViewBase = ({
onOpenFolderPicker,
canMoveUp,
canMoveDown,
onMoveUp,
onMoveDown,
onRemove,
onAddBefore,
onAddAfter,
showColorOption = false,
selectedItem,
}: SidePanelEditObjectViewBaseProps) => {
const { t } = useLingui();
const selectableItemIds = getOrganizeActionsSelectableItemIds(true);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const objectNameSingular = isDefined(selectedItem)
? (getNavigationMenuItemObjectNameSingular(
selectedItem,
objectMetadataItems,
views,
) ?? '')
: '';
const objectColor = useObjectNavItemColor(objectNameSingular);
const navigationMenuItems = useAtomStateValue(navigationMenuItemsSelector);
const persistedNavItem = navigationMenuItems.find(
(item) => item.id === selectedItem?.id,
);
const hasUserChangedColor =
isNonEmptyString(selectedItem?.color) &&
selectedItem.color !== (persistedNavItem?.color ?? undefined);
const effectiveColor = isDefined(selectedItem)
? getEffectiveNavigationMenuItemColor(selectedItem, objectColor)
: undefined;
const displayColor = hasUserChangedColor
? selectedItem.color
: effectiveColor;
return (
<SidePanelList commandGroups={[]} selectableItemIds={selectableItemIds}>
{showColorOption && isDefined(selectedItem) && (
<SidePanelGroup heading={t`Customize`}>
<SidePanelEditColorOption
navigationMenuItemId={selectedItem.id}
color={parseThemeColor(displayColor)}
/>
</SidePanelGroup>
)}
<SidePanelEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
showMoveToFolder
onMoveToFolder={onOpenFolderPicker}
/>
</SidePanelList>
);
};
@@ -1,125 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import {
IconChevronDown,
IconChevronUp,
IconFolderSymlink,
IconRowInsertBottom,
IconRowInsertTop,
IconTrash,
} from 'twenty-ui/display';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { SidePanelNavigationItemActions } from '@/side-panel/pages/navigation-menu-item/side-panel-navigation-item-actions';
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 SidePanelEditOrganizeActionsProps = OrganizeActionsProps & {
showMoveToFolder?: boolean;
onMoveToFolder?: () => void;
moveToFolderHasSubMenu?: boolean;
};
export const SidePanelEditOrganizeActions = ({
canMoveUp,
canMoveDown,
onMoveUp,
onMoveDown,
onRemove,
onAddBefore,
onAddAfter,
showMoveToFolder = false,
onMoveToFolder,
moveToFolderHasSubMenu = false,
}: SidePanelEditOrganizeActionsProps) => {
const { t } = useLingui();
return (
<SidePanelGroup heading={t`Organize`}>
<SelectableListItem
itemId={SidePanelNavigationItemActions.MOVE_UP}
onEnter={canMoveUp ? onMoveUp : undefined}
>
<CommandMenuItem
Icon={IconChevronUp}
label={t`Move up`}
id={SidePanelNavigationItemActions.MOVE_UP}
onClick={onMoveUp}
disabled={!canMoveUp}
/>
</SelectableListItem>
<SelectableListItem
itemId={SidePanelNavigationItemActions.MOVE_DOWN}
onEnter={canMoveDown ? onMoveDown : undefined}
>
<CommandMenuItem
Icon={IconChevronDown}
label={t`Move down`}
id={SidePanelNavigationItemActions.MOVE_DOWN}
onClick={onMoveDown}
disabled={!canMoveDown}
/>
</SelectableListItem>
{showMoveToFolder && onMoveToFolder && (
<SelectableListItem
itemId={SidePanelNavigationItemActions.MOVE_TO_FOLDER}
onEnter={onMoveToFolder}
>
<CommandMenuItem
Icon={IconFolderSymlink}
label={t`Move to folder`}
id={SidePanelNavigationItemActions.MOVE_TO_FOLDER}
hasSubMenu={moveToFolderHasSubMenu}
onClick={onMoveToFolder}
/>
</SelectableListItem>
)}
{onAddBefore && (
<SelectableListItem
itemId={SidePanelNavigationItemActions.ADD_BEFORE}
onEnter={onAddBefore}
>
<CommandMenuItem
Icon={IconRowInsertTop}
label={t`Add menu item before`}
id={SidePanelNavigationItemActions.ADD_BEFORE}
onClick={onAddBefore}
/>
</SelectableListItem>
)}
{onAddAfter && (
<SelectableListItem
itemId={SidePanelNavigationItemActions.ADD_AFTER}
onEnter={onAddAfter}
>
<CommandMenuItem
Icon={IconRowInsertBottom}
label={t`Add menu item after`}
id={SidePanelNavigationItemActions.ADD_AFTER}
onClick={onAddAfter}
/>
</SelectableListItem>
)}
<SelectableListItem
itemId={SidePanelNavigationItemActions.REMOVE}
onEnter={onRemove}
>
<CommandMenuItem
Icon={IconTrash}
label={t`Remove from sidebar`}
id={SidePanelNavigationItemActions.REMOVE}
onClick={onRemove}
/>
</SelectableListItem>
</SidePanelGroup>
);
};
@@ -1,55 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconApps } from 'twenty-ui/display';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useQuery } from '@apollo/client/react';
import { FindOneApplicationDocument } from '~/generated-metadata/graphql';
type SidePanelEditOwnerSectionProps = {
applicationId?: string | null;
};
export const SidePanelEditOwnerSection = ({
applicationId: applicationIdProp,
}: SidePanelEditOwnerSectionProps) => {
const { t } = useLingui();
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const { currentDraft } = useDraftNavigationMenuItems();
const applicationIdFromDraft =
isDefined(selectedItem) && isDefined(currentDraft)
? currentDraft.find((item) => item.id === selectedItem.id)?.applicationId
: undefined;
const applicationId = applicationIdProp ?? applicationIdFromDraft;
const { data } = useQuery(FindOneApplicationDocument, {
variables: { id: applicationId ?? '' },
skip: !isDefined(applicationId),
});
const applicationName = data?.findOneApplication?.name;
if (!isDefined(applicationName)) {
return null;
}
return (
<SidePanelGroup heading={t`Owner`}>
<SelectableListItem itemId="owner-app" onEnter={() => {}}>
<CommandMenuItem
Icon={IconApps}
label={applicationName}
id="owner-app"
disabled
/>
</SelectableListItem>
</SidePanelGroup>
);
};
@@ -1,206 +0,0 @@
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { NavigationMenuItemType } from 'twenty-shared/types';
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 { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { useSidePanelSubPageHistory } from '@/side-panel/hooks/useSidePanelSubPageHistory';
import { SidePanelEditColorOption } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditColorOption';
import { SidePanelEditLinkItemView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditLinkItemView';
import { SidePanelEditObjectViewBase } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditObjectViewBase';
import { SidePanelEditOrganizeActions } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOrganizeActions';
import { SidePanelEditOwnerSection } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOwnerSection';
import { useNavigationMenuItemEditOrganizeActions } from '@/side-panel/pages/navigation-menu-item/hooks/useNavigationMenuItemEditOrganizeActions';
import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const ADD_ITEM_TO_FOLDER_ACTION_ID = 'add-item-to-folder';
const StyledSidePanelPlaceholder = styled.p`
color: ${themeCssVariables.font.color.tertiary};
font-size: ${themeCssVariables.font.size.sm};
`;
const StyledSidePanelPageContainer = styled.div`
padding: ${themeCssVariables.spacing[3]};
`;
export const SidePanelNavigationMenuItemEditPage = () => {
const { t } = useLingui();
const selectedNavigationMenuItemInEditMode = useAtomStateValue(
selectedNavigationMenuItemInEditModeState,
);
const { selectedItemLabel } = useSelectedNavigationMenuItemEditItemLabel();
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const selectedItemType = selectedItem?.type ?? null;
const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory();
const openFolderPicker = () =>
navigateToSidePanelSubPage(SidePanelSubPages.EditFolderPicker);
const {
canMoveUp,
canMoveDown,
onMoveUp,
onMoveDown,
onRemove,
onAddBefore,
onAddAfter,
} = useNavigationMenuItemEditOrganizeActions();
const { updateLinkInDraft } = useUpdateLinkInDraft();
const { openAddItemToFolderPage } = useOpenAddItemToFolderPage();
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
const handleAddItemToFolder = () => {
if (!selectedItem || selectedItem.type !== 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 (
<StyledSidePanelPageContainer>
<StyledSidePanelPlaceholder>
{t`Select a navigation item to edit`}
</StyledSidePanelPlaceholder>
</StyledSidePanelPageContainer>
);
}
switch (selectedItemType) {
case NavigationMenuItemType.OBJECT:
return (
<SidePanelEditObjectViewBase
onOpenFolderPicker={openFolderPicker}
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
showColorOption={isDefined(selectedItem)}
selectedItem={selectedItem}
/>
);
case NavigationMenuItemType.VIEW:
return (
<SidePanelEditObjectViewBase
onOpenFolderPicker={openFolderPicker}
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
/>
);
case NavigationMenuItemType.LINK:
if (
isDefined(selectedItem) &&
selectedItem.type === NavigationMenuItemType.LINK
) {
return (
<SidePanelEditLinkItemView
key={selectedItem.id}
selectedItem={selectedItem}
onUpdateLink={(linkId, updates) =>
updateLinkInDraft(linkId, updates)
}
onOpenFolderPicker={openFolderPicker}
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
/>
);
}
return null;
case NavigationMenuItemType.FOLDER:
return (
<SidePanelList
commandGroups={[]}
selectableItemIds={[
ADD_ITEM_TO_FOLDER_ACTION_ID,
...getOrganizeActionsSelectableItemIds(false),
]}
>
{selectedItem && (
<SidePanelGroup 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>
<SidePanelEditColorOption
navigationMenuItemId={selectedItem.id}
color={parseThemeColor(selectedItem.color)}
/>
</SidePanelGroup>
)}
<SidePanelEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
/>
<SidePanelEditOwnerSection />
</SidePanelList>
);
default:
return (
<SidePanelList
commandGroups={[]}
selectableItemIds={getOrganizeActionsSelectableItemIds(true)}
>
<SidePanelEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
showMoveToFolder
onMoveToFolder={openFolderPicker}
/>
</SidePanelList>
);
}
};
@@ -1,167 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import {
Avatar,
IconBuildingSkyscraper,
IconFolder,
IconLink,
IconTable,
} from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { useAddFolderToNavigationMenu } from '@/side-panel/pages/navigation-menu-item/hooks/useAddFolderToNavigationMenu';
import { useAddLinkToNavigationMenu } from '@/side-panel/pages/navigation-menu-item/hooks/useAddLinkToNavigationMenu';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
type SidePanelNewSidebarItemMainMenuProps = {
onSelectObject: () => void;
onSelectView: () => void;
onSelectRecord: () => void;
};
const MAIN_MENU_ITEM_TYPES = [
NavigationMenuItemType.OBJECT,
NavigationMenuItemType.VIEW,
NavigationMenuItemType.RECORD,
NavigationMenuItemType.FOLDER,
NavigationMenuItemType.LINK,
] as const;
export const SidePanelNewSidebarItemMainMenu = ({
onSelectObject,
onSelectView,
onSelectRecord,
}: SidePanelNewSidebarItemMainMenuProps) => {
const { t } = useLingui();
const addMenuItemInsertionContext = useAtomStateValue(
addMenuItemInsertionContextState,
);
const { handleAddFolder } = useAddFolderToNavigationMenu();
const { handleAddLink } = useAddLinkToNavigationMenu();
const isAddingToFolder = isDefined(
addMenuItemInsertionContext?.targetFolderId,
);
const isDragDisabled = addMenuItemInsertionContext?.disableDrag === true;
const selectableItemIds = isAddingToFolder
? MAIN_MENU_ITEM_TYPES.filter(
(type) => type !== NavigationMenuItemType.FOLDER,
)
: [...MAIN_MENU_ITEM_TYPES];
return (
<SidePanelAddToNavigationDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<SidePanelList commandGroups={[]} selectableItemIds={selectableItemIds}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
<SidePanelGroup heading={t`Data`}>
<SelectableListItem
itemId={NavigationMenuItemType.OBJECT}
onEnter={onSelectObject}
>
<CommandMenuItem
Icon={() => (
<NavigationMenuItemStyleIcon
Icon={IconBuildingSkyscraper}
color="blue"
/>
)}
label={t`Object`}
id={NavigationMenuItemType.OBJECT}
hasSubMenu={true}
onClick={onSelectObject}
/>
</SelectableListItem>
<SelectableListItem
itemId={NavigationMenuItemType.VIEW}
onEnter={onSelectView}
>
<CommandMenuItem
Icon={() => (
<NavigationMenuItemStyleIcon
Icon={IconTable}
color="gray"
/>
)}
label={t`View`}
id={NavigationMenuItemType.VIEW}
hasSubMenu={true}
onClick={onSelectView}
/>
</SelectableListItem>
<SelectableListItem
itemId={NavigationMenuItemType.RECORD}
onEnter={onSelectRecord}
>
<CommandMenuItem
Icon={() => (
<Avatar
placeholder="L"
type="rounded"
backgroundColor={themeCssVariables.color.green4}
/>
)}
label={t`Record`}
id={NavigationMenuItemType.RECORD}
hasSubMenu={true}
onClick={onSelectRecord}
/>
</SelectableListItem>
</SidePanelGroup>
<SidePanelGroup heading={t`Other`}>
<SelectableListItem
itemId={NavigationMenuItemType.FOLDER}
onEnter={isAddingToFolder ? undefined : handleAddFolder}
>
<SidePanelItemWithAddToNavigationDrag
icon={IconFolder}
label={t`Folder`}
id={NavigationMenuItemType.FOLDER}
onClick={handleAddFolder}
dragIndex={isDragDisabled ? undefined : 3}
payload={{
type: NavigationMenuItemType.FOLDER,
folderId: 'new',
name: t`New folder`,
}}
disabled={isAddingToFolder}
disableDrag={isDragDisabled}
/>
</SelectableListItem>
<SelectableListItem
itemId={NavigationMenuItemType.LINK}
onEnter={handleAddLink}
>
<SidePanelItemWithAddToNavigationDrag
icon={IconLink}
label={t`Link`}
id={NavigationMenuItemType.LINK}
onClick={handleAddLink}
dragIndex={isDragDisabled ? undefined : 4}
payload={{
type: NavigationMenuItemType.LINK,
linkId: 'new',
name: t`Link label`,
link: 'https://www.example.com',
}}
disableDrag={isDragDisabled}
/>
</SelectableListItem>
</SidePanelGroup>
{placeholder}
</div>
</SidePanelList>
)}
</SidePanelAddToNavigationDroppable>
);
};
@@ -1,96 +0,0 @@
import { useState } from 'react';
import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddObjectToNavigationMenuDraft';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useSidePanelSubPageHistory } from '@/side-panel/hooks/useSidePanelSubPageHistory';
import { SidePanelObjectPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerSubView';
import { getAvailableObjectMetadataForNewSidebarItem } from '@/side-panel/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem';
import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { ViewKey } from '@/views/types/ViewKey';
import { useIcons } from 'twenty-ui/display';
export const SidePanelNewSidebarItemObjectSubPage = () => {
const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory();
const { getIcon } = useIcons();
const [objectSearchInput, setObjectSearchInput] = useState('');
const { currentDraft } = useDraftNavigationMenuItems();
const { objectMetadataItems } = useObjectMetadataItems();
const { addObjectToDraft } = useAddObjectToNavigationMenuDraft();
const { openNavigationMenuItemInSidePanel } =
useOpenNavigationMenuItemInSidePanel();
const { activeNonSystemObjectMetadataItems } =
useFilteredObjectMetadataItems();
const addMenuItemInsertionContext = useAtomStateValue(
addMenuItemInsertionContextState,
);
const setAddMenuItemInsertionContext = useSetAtomState(
addMenuItemInsertionContextState,
);
const {
views,
objectMetadataIdsWithIndexView,
objectMetadataIdsInWorkspace,
} = useNavigationMenuObjectMetadataFromDraft(currentDraft);
const objectMetadataIdsWithDisplayableViews = new Set(
views
.filter((view) => view.key !== ViewKey.INDEX)
.map((view) => view.objectMetadataId),
);
const { availableObjectMetadataItems } =
getAvailableObjectMetadataForNewSidebarItem({
objectMetadataItems,
activeNonSystemObjectMetadataItems,
objectMetadataIdsWithIndexView,
objectMetadataIdsWithDisplayableViews,
});
const handleSelectObject = (objectMetadataItem: ObjectMetadataItem) => {
if (objectMetadataIdsInWorkspace.has(objectMetadataItem.id)) {
return;
}
const itemId = addObjectToDraft(
objectMetadataItem.id,
currentDraft,
addMenuItemInsertionContext?.targetFolderId,
addMenuItemInsertionContext?.targetIndex,
getStandardObjectIconColor(objectMetadataItem.nameSingular),
);
setAddMenuItemInsertionContext(null);
openNavigationMenuItemInSidePanel({
itemId,
pageTitle: objectMetadataItem.labelSingular,
pageIcon: getIcon(objectMetadataItem.icon),
});
};
const disableDrag = addMenuItemInsertionContext?.disableDrag === true;
return (
<SidePanelObjectPickerSubView
objects={availableObjectMetadataItems}
searchValue={objectSearchInput}
onSearchChange={setObjectSearchInput}
onOpenSystemPicker={() =>
navigateToSidePanelSubPage(
SidePanelSubPages.NewSidebarItemObjectSystemPicker,
)
}
isViewItem={false}
onChangeObject={handleSelectObject}
objectMenuItemVariant="add"
disableDrag={disableDrag}
/>
);
};
@@ -1,91 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddObjectToNavigationMenuDraft';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SidePanelSystemObjectPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelSystemObjectPickerSubView';
import { getAvailableObjectMetadataForNewSidebarItem } from '@/side-panel/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { ViewKey } from '@/views/types/ViewKey';
import { useState } from 'react';
import { useIcons } from 'twenty-ui/display';
export const SidePanelNewSidebarItemObjectSystemPickerSubPage = () => {
const { t } = useLingui();
const { getIcon } = useIcons();
const [searchValue, setSearchValue] = useState('');
const { currentDraft } = useDraftNavigationMenuItems();
const { objectMetadataItems } = useObjectMetadataItems();
const { addObjectToDraft } = useAddObjectToNavigationMenuDraft();
const { openNavigationMenuItemInSidePanel } =
useOpenNavigationMenuItemInSidePanel();
const { activeNonSystemObjectMetadataItems } =
useFilteredObjectMetadataItems();
const addMenuItemInsertionContext = useAtomStateValue(
addMenuItemInsertionContextState,
);
const setAddMenuItemInsertionContext = useSetAtomState(
addMenuItemInsertionContextState,
);
const {
views,
objectMetadataIdsWithIndexView,
objectMetadataIdsInWorkspace,
} = useNavigationMenuObjectMetadataFromDraft(currentDraft);
const objectMetadataIdsWithDisplayableViews = new Set(
views
.filter((view) => view.key !== ViewKey.INDEX)
.map((view) => view.objectMetadataId),
);
const { availableSystemObjectMetadataItems } =
getAvailableObjectMetadataForNewSidebarItem({
objectMetadataItems,
activeNonSystemObjectMetadataItems,
objectMetadataIdsWithIndexView,
objectMetadataIdsWithDisplayableViews,
});
const handleSelectObject = (objectMetadataItem: ObjectMetadataItem) => {
if (objectMetadataIdsInWorkspace.has(objectMetadataItem.id)) {
return;
}
const itemId = addObjectToDraft(
objectMetadataItem.id,
currentDraft,
addMenuItemInsertionContext?.targetFolderId,
addMenuItemInsertionContext?.targetIndex,
getStandardObjectIconColor(objectMetadataItem.nameSingular),
);
setAddMenuItemInsertionContext(null);
openNavigationMenuItemInSidePanel({
itemId,
pageTitle: objectMetadataItem.labelSingular,
pageIcon: getIcon(objectMetadataItem.icon),
});
};
const disableDrag = addMenuItemInsertionContext?.disableDrag === true;
return (
<SidePanelSystemObjectPickerSubView
systemObjects={availableSystemObjectMetadataItems}
searchValue={searchValue}
onSearchChange={setSearchValue}
isViewItem={false}
onChangeObject={handleSelectObject}
objectMenuItemVariant="add"
emptyNoResultsText={t`All system objects are already in the sidebar`}
disableDrag={disableDrag}
/>
);
};
@@ -1,23 +0,0 @@
import { useSidePanelSubPageHistory } from '@/side-panel/hooks/useSidePanelSubPageHistory';
import { SidePanelNewSidebarItemMainMenu } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemMainMenu';
import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages';
export const SidePanelNewSidebarItemPage = () => {
const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory();
return (
<SidePanelNewSidebarItemMainMenu
onSelectRecord={() =>
navigateToSidePanelSubPage(SidePanelSubPages.NewSidebarItemRecord)
}
onSelectObject={() =>
navigateToSidePanelSubPage(SidePanelSubPages.NewSidebarItemObjectPicker)
}
onSelectView={() =>
navigateToSidePanelSubPage(
SidePanelSubPages.NewSidebarItemViewObjectPicker,
)
}
/>
);
};
@@ -1,109 +0,0 @@
import { Avatar, IconFolder, useIcons } from 'twenty-ui/display';
import { useAddRecordToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddRecordToNavigationMenuDraft';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel';
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 { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag';
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 {
CoreObjectNameSingular,
NavigationMenuItemType,
} from 'twenty-shared/types';
type SearchRecord = {
recordId: string;
objectNameSingular: string;
label: string;
imageUrl?: string | null;
};
type SidePanelNewSidebarItemRecordItemProps = {
record: SearchRecord;
dragIndex?: number;
disableDrag?: boolean;
};
export const SidePanelNewSidebarItemRecordItem = ({
record,
dragIndex,
disableDrag = false,
}: SidePanelNewSidebarItemRecordItemProps) => {
const { getIcon } = useIcons();
const { addRecordToDraft } = useAddRecordToNavigationMenuDraft();
const { currentDraft } = useDraftNavigationMenuItems();
const addMenuItemInsertionContext = useAtomStateValue(
addMenuItemInsertionContextState,
);
const setAddMenuItemInsertionContext = useSetAtomState(
addMenuItemInsertionContextState,
);
const { openNavigationMenuItemInSidePanel } =
useOpenNavigationMenuItemInSidePanel();
const { objectMetadataItems } = useObjectMetadataItems();
const objectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === record.objectNameSingular,
);
const recordPayload: AddToNavigationDragPayload = {
type: NavigationMenuItemType.RECORD,
recordId: record.recordId,
objectMetadataId: objectMetadataItem?.id ?? '',
objectNameSingular: record.objectNameSingular,
label: record.label,
imageUrl: record.imageUrl,
};
const handleSelectRecord = () => {
const itemId = addRecordToDraft(
{
recordId: record.recordId,
objectNameSingular: record.objectNameSingular,
label: record.label,
imageUrl: record.imageUrl,
},
currentDraft,
addMenuItemInsertionContext?.targetFolderId ?? null,
addMenuItemInsertionContext?.targetIndex,
);
setAddMenuItemInsertionContext(null);
openNavigationMenuItemInSidePanel({
itemId,
pageTitle: record.label,
pageIcon: objectMetadataItem
? getIcon(objectMetadataItem.icon)
: IconFolder,
});
};
return (
<SelectableListItem itemId={record.recordId} onEnter={handleSelectRecord}>
<SidePanelItemWithAddToNavigationDrag
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}
disableDrag={disableDrag}
payload={recordPayload}
/>
</SelectableListItem>
);
};
@@ -1,122 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useDebounce } from 'use-debounce';
import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
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 { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch';
import { SidePanelNewSidebarItemRecordItem } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemRecordItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useQuery } from '@apollo/client/react';
import { SearchDocument } from '~/generated/graphql';
type SearchRecordBase = {
recordId: string;
objectNameSingular: string;
label: string;
imageUrl?: string | null;
};
export const SidePanelNewSidebarItemRecordSubPage = () => {
const { t } = useLingui();
const addMenuItemInsertionContext = useAtomStateValue(
addMenuItemInsertionContextState,
);
const disableDrag = addMenuItemInsertionContext?.disableDrag === true;
const { currentDraft } = useDraftNavigationMenuItems();
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 } = useQuery(
SearchDocument,
{
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 (
<SidePanelSubViewWithSearch
searchPlaceholder={t`Search records...`}
searchValue={recordSearchInput}
onSearchChange={setRecordSearchInput}
>
<SidePanelAddToNavigationDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<SidePanelList
commandGroups={[]}
selectableItemIds={selectableItemIds}
loading={recordSearchLoading}
noResults={isEmpty}
noResultsText={noResultsText}
>
{/* oxlint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
<SidePanelGroup heading={t`Results`}>
{availableSearchRecords.map((record, index) => (
<SidePanelNewSidebarItemRecordItem
key={record.recordId}
record={record}
dragIndex={disableDrag ? undefined : index}
disableDrag={disableDrag}
/>
))}
</SidePanelGroup>
{placeholder}
</div>
</SidePanelList>
)}
</SidePanelAddToNavigationDroppable>
</SidePanelSubViewWithSearch>
);
};
@@ -1,73 +0,0 @@
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useSidePanelSubPageHistory } from '@/side-panel/hooks/useSidePanelSubPageHistory';
import { SidePanelNewSidebarItemViewObjectPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewObjectPickerSubView';
import { getAvailableObjectMetadataForNewSidebarItem } from '@/side-panel/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem';
import { selectedObjectMetadataIdForViewFlowState } from '@/side-panel/states/selectedObjectMetadataIdForViewFlowState';
import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { ViewKey } from '@/views/types/ViewKey';
import { ViewType } from '@/views/types/ViewType';
import { useState } from 'react';
export const SidePanelNewSidebarItemViewObjectPickerSubPage = () => {
const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory();
const [searchValue, setSearchValue] = useState('');
const setSelectedObjectMetadataIdForViewFlow = useSetAtomComponentState(
selectedObjectMetadataIdForViewFlowState,
);
const { currentDraft } = useDraftNavigationMenuItems();
const { objectMetadataItems } = useObjectMetadataItems();
const { activeNonSystemObjectMetadataItems } =
useFilteredObjectMetadataItems();
const { views, objectMetadataIdsWithIndexView } =
useNavigationMenuObjectMetadataFromDraft(currentDraft);
const objectMetadataIdsWithDisplayableViews = new Set(
views
.filter(
(view) =>
view.key !== ViewKey.INDEX && view.type !== ViewType.FIELDS_WIDGET,
)
.map((view) => view.objectMetadataId),
);
const {
objectMetadataItemsWithViews,
availableSystemObjectMetadataItemsForView,
} = getAvailableObjectMetadataForNewSidebarItem({
objectMetadataItems,
activeNonSystemObjectMetadataItems,
objectMetadataIdsWithIndexView,
objectMetadataIdsWithDisplayableViews,
});
const handleSelectObject = (objectMetadataItem: ObjectMetadataItem) => {
setSelectedObjectMetadataIdForViewFlow(objectMetadataItem.id);
navigateToSidePanelSubPage(
SidePanelSubPages.NewSidebarItemViewPicker,
objectMetadataItem.labelPlural,
);
};
return (
<SidePanelNewSidebarItemViewObjectPickerSubView
objects={objectMetadataItemsWithViews}
searchValue={searchValue}
onSearchChange={setSearchValue}
onOpenSystemPicker={() =>
navigateToSidePanelSubPage(
SidePanelSubPages.NewSidebarItemViewSystemPicker,
)
}
onSelectObject={handleSelectObject}
showSystemObjectsOption={
availableSystemObjectMetadataItemsForView.length > 0
}
/>
);
};
@@ -1,94 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import { IconSettings, useIcons } from 'twenty-ui/display';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
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 { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch';
import { useSidePanelFilteredPickerItems } from '@/side-panel/hooks/useSidePanelFilteredPickerItems';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type SidePanelNewSidebarItemViewObjectPickerSubViewProps = {
objects: ObjectMetadataItem[];
searchValue: string;
onSearchChange: (value: string) => void;
onOpenSystemPicker: () => void;
onSelectObject: (objectMetadataItem: ObjectMetadataItem) => void;
showSystemObjectsOption?: boolean;
};
export const SidePanelNewSidebarItemViewObjectPickerSubView = ({
objects,
searchValue,
onSearchChange,
onOpenSystemPicker,
onSelectObject,
showSystemObjectsOption = true,
}: SidePanelNewSidebarItemViewObjectPickerSubViewProps) => {
const { t } = useLingui();
const { getIcon } = useIcons();
const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } =
useSidePanelFilteredPickerItems({
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 (
<SidePanelSubViewWithSearch
searchPlaceholder={t`Search an object...`}
searchValue={searchValue}
onSearchChange={onSearchChange}
>
<SidePanelList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
<SidePanelGroup heading={t`Objects`}>
{filteredItems.map((objectMetadataItem) => (
<SelectableListItem
key={objectMetadataItem.id}
itemId={objectMetadataItem.id}
onEnter={() => onSelectObject(objectMetadataItem)}
>
<CommandMenuItem
Icon={() => (
<NavigationMenuItemStyleIcon
Icon={getIcon(objectMetadataItem.icon)}
color={getStandardObjectIconColor(
objectMetadataItem.nameSingular,
)}
/>
)}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
hasSubMenu
onClick={() => onSelectObject(objectMetadataItem)}
/>
</SelectableListItem>
))}
{showSystemObjectsOption && (
<SelectableListItem itemId="system" onEnter={onOpenSystemPicker}>
<CommandMenuItem
Icon={() => <NavigationMenuItemStyleIcon Icon={IconSettings} />}
label={t`System objects`}
id="system"
hasSubMenu={true}
onClick={onOpenSystemPicker}
/>
</SelectableListItem>
)}
</SidePanelGroup>
</SidePanelList>
</SidePanelSubViewWithSearch>
);
};
@@ -1,20 +0,0 @@
import { SidePanelNewSidebarItemViewPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewPickerSubView';
import { selectedObjectMetadataIdForViewFlowState } from '@/side-panel/states/selectedObjectMetadataIdForViewFlowState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { isDefined } from 'twenty-shared/utils';
export const SidePanelNewSidebarItemViewPickerSubPage = () => {
const selectedObjectMetadataIdForViewFlow = useAtomComponentStateValue(
selectedObjectMetadataIdForViewFlowState,
);
if (!isDefined(selectedObjectMetadataIdForViewFlow)) {
return null;
}
return (
<SidePanelNewSidebarItemViewPickerSubView
selectedObjectMetadataIdForView={selectedObjectMetadataIdForViewFlow}
/>
);
};
@@ -1,162 +0,0 @@
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
import { NavigationMenuItemType } from 'twenty-shared/types';
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 { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch';
import { useSidePanelFilteredPickerItems } from '@/side-panel/hooks/useSidePanelFilteredPickerItems';
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 { ViewType } from '@/views/types/ViewType';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { ViewVisibility } from '~/generated-metadata/graphql';
type SidePanelNewSidebarItemViewPickerSubViewProps = {
selectedObjectMetadataIdForView: string;
};
export const SidePanelNewSidebarItemViewPickerSubView = ({
selectedObjectMetadataIdForView,
}: SidePanelNewSidebarItemViewPickerSubViewProps) => {
const { t } = useLingui();
const { getIcon } = useIcons();
const [searchValue, setSearchValue] = useState('');
const { addViewToDraft } = useAddViewToNavigationMenuDraft();
const { currentDraft } = useDraftNavigationMenuItems();
const addMenuItemInsertionContext = useAtomStateValue(
addMenuItemInsertionContextState,
);
const setAddMenuItemInsertionContext = useSetAtomState(
addMenuItemInsertionContextState,
);
const { openNavigationMenuItemInSidePanel } =
useOpenNavigationMenuItemInSidePanel();
const { objectMetadataItems } = useObjectMetadataItems();
const { views } = useNavigationMenuObjectMetadataFromDraft(currentDraft);
const viewsForSelectedObject = views
.filter(
(view) =>
view.objectMetadataId === selectedObjectMetadataIdForView &&
view.key !== ViewKey.INDEX &&
view.type !== ViewType.FIELDS_WIDGET &&
view.visibility === ViewVisibility.WORKSPACE,
)
.sort((a, b) => a.position - b.position);
const selectedObjectMetadataItem = objectMetadataItems.find(
(item) => item.id === selectedObjectMetadataIdForView,
);
const {
filteredItems: filteredViews,
selectableItemIds,
isEmpty,
hasSearchQuery,
} = useSidePanelFilteredPickerItems({
items: viewsForSelectedObject,
searchQuery: searchValue,
getSearchableValues: (view) => [view.name],
});
const noResultsText = hasSearchQuery
? t`No results found`
: t`No custom views available`;
const isDragDisabled = addMenuItemInsertionContext?.disableDrag === true;
const handleSelectView = (view: View) => {
const itemId = addViewToDraft(
view.id,
currentDraft,
addMenuItemInsertionContext?.targetFolderId ?? null,
addMenuItemInsertionContext?.targetIndex,
isDefined(selectedObjectMetadataItem)
? getStandardObjectIconColor(selectedObjectMetadataItem.nameSingular)
: undefined,
);
setAddMenuItemInsertionContext(null);
openNavigationMenuItemInSidePanel({
itemId,
pageTitle: view.name,
pageIcon: getIcon(view.icon),
});
};
return (
<SidePanelSubViewWithSearch
searchPlaceholder={t`Search a view...`}
searchValue={searchValue}
onSearchChange={setSearchValue}
>
<SidePanelAddToNavigationDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<SidePanelList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
{/* oxlint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
<SidePanelGroup heading={t`Views`}>
{filteredViews.map((view, index) => (
<SelectableListItem
key={view.id}
itemId={view.id}
onEnter={() => handleSelectView(view)}
>
<SidePanelItemWithAddToNavigationDrag
customIconContent={
selectedObjectMetadataItem ? (
<ObjectIconWithViewOverlay
ObjectIcon={getIcon(
selectedObjectMetadataItem.icon,
)}
ViewIcon={getIcon(view.icon)}
objectColor={getStandardObjectIconColor(
selectedObjectMetadataItem.nameSingular,
)}
/>
) : undefined
}
icon={
selectedObjectMetadataItem
? undefined
: getIcon(view.icon)
}
label={view.name}
id={view.id}
onClick={() => handleSelectView(view)}
dragIndex={isDragDisabled ? undefined : index}
disableDrag={isDragDisabled}
payload={{
type: NavigationMenuItemType.VIEW,
viewId: view.id,
label: view.name,
}}
/>
</SelectableListItem>
))}
</SidePanelGroup>
{placeholder}
</div>
</SidePanelList>
)}
</SidePanelAddToNavigationDroppable>
</SidePanelSubViewWithSearch>
);
};
@@ -1,63 +0,0 @@
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useSidePanelSubPageHistory } from '@/side-panel/hooks/useSidePanelSubPageHistory';
import { SidePanelNewSidebarItemViewSystemSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewSystemSubView';
import { getAvailableObjectMetadataForNewSidebarItem } from '@/side-panel/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem';
import { selectedObjectMetadataIdForViewFlowState } from '@/side-panel/states/selectedObjectMetadataIdForViewFlowState';
import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { ViewKey } from '@/views/types/ViewKey';
import { ViewType } from '@/views/types/ViewType';
import { useState } from 'react';
export const SidePanelNewSidebarItemViewSystemPickerSubPage = () => {
const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory();
const [searchValue, setSearchValue] = useState('');
const setSelectedObjectMetadataIdForViewFlow = useSetAtomComponentState(
selectedObjectMetadataIdForViewFlowState,
);
const { currentDraft } = useDraftNavigationMenuItems();
const { objectMetadataItems } = useObjectMetadataItems();
const { activeNonSystemObjectMetadataItems } =
useFilteredObjectMetadataItems();
const { views, objectMetadataIdsWithIndexView } =
useNavigationMenuObjectMetadataFromDraft(currentDraft);
const objectMetadataIdsWithDisplayableViews = new Set(
views
.filter(
(view) =>
view.key !== ViewKey.INDEX && view.type !== ViewType.FIELDS_WIDGET,
)
.map((view) => view.objectMetadataId),
);
const { availableSystemObjectMetadataItemsForView } =
getAvailableObjectMetadataForNewSidebarItem({
objectMetadataItems,
activeNonSystemObjectMetadataItems,
objectMetadataIdsWithIndexView,
objectMetadataIdsWithDisplayableViews,
});
const handleSelectObject = (objectMetadataItem: ObjectMetadataItem) => {
setSelectedObjectMetadataIdForViewFlow(objectMetadataItem.id);
navigateToSidePanelSubPage(
SidePanelSubPages.NewSidebarItemViewPicker,
objectMetadataItem.labelPlural,
);
};
return (
<SidePanelNewSidebarItemViewSystemSubView
systemObjects={availableSystemObjectMetadataItemsForView}
searchValue={searchValue}
onSearchChange={setSearchValue}
onSelectObject={handleSelectObject}
/>
);
};
@@ -1,81 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import { useIcons } from 'twenty-ui/display';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
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 { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch';
import { useSidePanelFilteredPickerItems } from '@/side-panel/hooks/useSidePanelFilteredPickerItems';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type SidePanelNewSidebarItemViewSystemSubViewProps = {
systemObjects: ObjectMetadataItem[];
searchValue: string;
onSearchChange: (value: string) => void;
onSelectObject: (objectMetadataItem: ObjectMetadataItem) => void;
};
export const SidePanelNewSidebarItemViewSystemSubView = ({
systemObjects,
searchValue,
onSearchChange,
onSelectObject,
}: SidePanelNewSidebarItemViewSystemSubViewProps) => {
const { t } = useLingui();
const { getIcon } = useIcons();
const {
filteredItems: filteredSystemObjectMetadataItems,
selectableItemIds,
isEmpty,
hasSearchQuery,
} = useSidePanelFilteredPickerItems({
items: systemObjects,
searchQuery: searchValue,
getSearchableValues: (item) => [item.labelPlural],
});
const noResultsText = hasSearchQuery
? t`No results found`
: t`No system objects with views found`;
return (
<SidePanelSubViewWithSearch
searchPlaceholder={t`Search a system object...`}
searchValue={searchValue}
onSearchChange={onSearchChange}
>
<SidePanelList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
<SidePanelGroup heading={t`System objects`}>
{filteredSystemObjectMetadataItems.map((objectMetadataItem) => (
<SelectableListItem
key={objectMetadataItem.id}
itemId={objectMetadataItem.id}
onEnter={() => onSelectObject(objectMetadataItem)}
>
<CommandMenuItem
Icon={() => (
<NavigationMenuItemStyleIcon
Icon={getIcon(objectMetadataItem.icon)}
color={getStandardObjectIconColor(
objectMetadataItem.nameSingular,
)}
/>
)}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
onClick={() => onSelectObject(objectMetadataItem)}
/>
</SelectableListItem>
))}
</SidePanelGroup>
</SidePanelList>
</SidePanelSubViewWithSearch>
);
};
@@ -1,88 +0,0 @@
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 { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { NavigationMenuItemType } from 'twenty-shared/types';
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 { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { indexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/indexViewIdFromObjectMetadataItemFamilySelector';
type SidePanelObjectMenuItemProps = {
objectMetadataItem: ObjectMetadataItem;
onSelect: (objectMetadataItem: ObjectMetadataItem) => void;
variant: 'add' | 'edit';
dragIndex?: number;
disableDrag?: boolean;
};
export const SidePanelObjectMenuItem = ({
objectMetadataItem,
onSelect,
variant,
dragIndex,
disableDrag = false,
}: SidePanelObjectMenuItemProps) => {
const { t } = useLingui();
const { getIcon } = useIcons();
const { currentDraft } = useDraftNavigationMenuItems();
const { objectMetadataIdsInWorkspace } =
useNavigationMenuObjectMetadataFromDraft(currentDraft);
const isAlreadyInNavbar = objectMetadataIdsInWorkspace.has(
objectMetadataItem.id,
);
const defaultViewId = useAtomFamilySelectorValue(
indexViewIdFromObjectMetadataItemFamilySelector,
{ objectMetadataItemId: objectMetadataItem.id },
);
const Icon = getIcon(objectMetadataItem.icon);
const iconColor = getStandardObjectIconColor(objectMetadataItem.nameSingular);
const isDisabled = isAlreadyInNavbar || !isDefined(defaultViewId);
const handleClick = () => {
if (isDisabled || !defaultViewId) {
return;
}
onSelect(objectMetadataItem);
};
const styledIcon = () => (
<NavigationMenuItemStyleIcon Icon={Icon} color={iconColor} />
);
return (
<SelectableListItem itemId={objectMetadataItem.id} onEnter={handleClick}>
{variant === 'add' && !isDisabled ? (
<SidePanelItemWithAddToNavigationDrag
icon={styledIcon}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
onClick={handleClick}
dragIndex={dragIndex}
disableDrag={disableDrag}
payload={{
type: NavigationMenuItemType.OBJECT,
objectMetadataId: objectMetadataItem.id,
label: objectMetadataItem.labelPlural,
iconColor,
}}
/>
) : (
<CommandMenuItem
Icon={styledIcon}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
onClick={handleClick}
disabled={isDisabled}
description={isAlreadyInNavbar ? t`Already in navbar` : undefined}
/>
)}
</SelectableListItem>
);
};
@@ -1,64 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
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 { SidePanelObjectMenuItem } from '@/side-panel/pages/navigation-menu-item/components/SidePanelObjectMenuItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type SidePanelObjectPickerItemProps = {
objectMetadataItem: ObjectMetadataItem;
isViewItem: boolean;
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
onChangeObject: (objectMetadataItem: ObjectMetadataItem) => void;
objectMenuItemVariant?: 'add' | 'edit';
dragIndex?: number;
disableDrag?: boolean;
};
export const SidePanelObjectPickerItem = ({
objectMetadataItem,
isViewItem,
onSelectObjectForViewEdit,
onChangeObject,
objectMenuItemVariant = 'edit',
dragIndex,
disableDrag = false,
}: SidePanelObjectPickerItemProps) => {
const { getIcon } = useIcons();
if (isViewItem && isDefined(onSelectObjectForViewEdit)) {
return (
<SelectableListItem
itemId={objectMetadataItem.id}
onEnter={() => onSelectObjectForViewEdit(objectMetadataItem)}
>
<CommandMenuItem
Icon={() => (
<NavigationMenuItemStyleIcon
Icon={getIcon(objectMetadataItem.icon)}
color={getStandardObjectIconColor(
objectMetadataItem.nameSingular,
)}
/>
)}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
onClick={() => onSelectObjectForViewEdit(objectMetadataItem)}
/>
</SelectableListItem>
);
}
return (
<SidePanelObjectMenuItem
objectMetadataItem={objectMetadataItem}
onSelect={onChangeObject}
variant={objectMenuItemVariant}
dragIndex={dragIndex}
disableDrag={disableDrag}
/>
);
};
@@ -1,116 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import { IconSettings } from 'twenty-ui/display';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch';
import { useSidePanelFilteredPickerItems } from '@/side-panel/hooks/useSidePanelFilteredPickerItems';
import { SidePanelObjectPickerItem } from '@/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type SidePanelObjectPickerSubViewProps = {
objects: ObjectMetadataItem[];
searchValue: string;
onSearchChange: (value: string) => void;
onOpenSystemPicker: () => void;
isViewItem: boolean;
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
onChangeObject: (objectMetadataItem: ObjectMetadataItem) => void;
objectMenuItemVariant?: 'add' | 'edit';
emptyNoResultsText?: string;
disableDrag?: boolean;
};
export const SidePanelObjectPickerSubView = ({
objects,
searchValue,
onSearchChange,
onOpenSystemPicker,
isViewItem,
onSelectObjectForViewEdit,
onChangeObject,
objectMenuItemVariant = 'edit',
emptyNoResultsText,
disableDrag = false,
}: SidePanelObjectPickerSubViewProps) => {
const { t } = useLingui();
const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } =
useSidePanelFilteredPickerItems({
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 = (
<SidePanelGroup heading={t`Objects`}>
{filteredItems.map((objectMetadataItem, index) => (
<SidePanelObjectPickerItem
key={objectMetadataItem.id}
objectMetadataItem={objectMetadataItem}
isViewItem={isViewItem}
onSelectObjectForViewEdit={onSelectObjectForViewEdit}
onChangeObject={onChangeObject}
objectMenuItemVariant={objectMenuItemVariant}
dragIndex={isAddVariant && !disableDrag ? index : undefined}
disableDrag={disableDrag}
/>
))}
<SelectableListItem itemId="system" onEnter={onOpenSystemPicker}>
<CommandMenuItem
Icon={() => <NavigationMenuItemStyleIcon Icon={IconSettings} />}
label={t`System objects`}
id="system"
hasSubMenu
onClick={onOpenSystemPicker}
/>
</SelectableListItem>
</SidePanelGroup>
);
return (
<SidePanelSubViewWithSearch
searchPlaceholder={t`Search an object...`}
searchValue={searchValue}
onSearchChange={onSearchChange}
>
{isAddVariant ? (
<SidePanelAddToNavigationDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<SidePanelList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
{/* oxlint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
{listContent}
{placeholder}
</div>
</SidePanelList>
)}
</SidePanelAddToNavigationDroppable>
) : (
<SidePanelList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
{listContent}
</SidePanelList>
)}
</SidePanelSubViewWithSearch>
);
};
@@ -1,100 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch';
import { useSidePanelFilteredPickerItems } from '@/side-panel/hooks/useSidePanelFilteredPickerItems';
import { SidePanelObjectPickerItem } from '@/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerItem';
type SidePanelSystemObjectPickerSubViewProps = {
systemObjects: ObjectMetadataItem[];
searchValue: string;
onSearchChange: (value: string) => void;
isViewItem: boolean;
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
onChangeObject: (objectMetadataItem: ObjectMetadataItem) => void;
objectMenuItemVariant?: 'add' | 'edit';
emptyNoResultsText?: string;
disableDrag?: boolean;
};
export const SidePanelSystemObjectPickerSubView = ({
systemObjects,
searchValue,
onSearchChange,
isViewItem,
onSelectObjectForViewEdit,
onChangeObject,
objectMenuItemVariant = 'edit',
emptyNoResultsText,
disableDrag = false,
}: SidePanelSystemObjectPickerSubViewProps) => {
const { t } = useLingui();
const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } =
useSidePanelFilteredPickerItems({
items: systemObjects,
searchQuery: searchValue,
getSearchableValues: (item) => [item.labelPlural],
});
const noResultsText = hasSearchQuery
? t`No results found`
: (emptyNoResultsText ?? t`No system objects available`);
const isAddVariant = objectMenuItemVariant === 'add';
const listContent = (
<SidePanelGroup heading={t`System objects`}>
{filteredItems.map((objectMetadataItem, index) => (
<SidePanelObjectPickerItem
key={objectMetadataItem.id}
objectMetadataItem={objectMetadataItem}
isViewItem={isViewItem}
onSelectObjectForViewEdit={onSelectObjectForViewEdit}
onChangeObject={onChangeObject}
objectMenuItemVariant={objectMenuItemVariant}
dragIndex={isAddVariant && !disableDrag ? index : undefined}
disableDrag={disableDrag}
/>
))}
</SidePanelGroup>
);
return (
<SidePanelSubViewWithSearch
searchPlaceholder={t`Search a system object...`}
searchValue={searchValue}
onSearchChange={onSearchChange}
>
{isAddVariant ? (
<SidePanelAddToNavigationDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<SidePanelList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
{listContent}
{placeholder}
</div>
</SidePanelList>
)}
</SidePanelAddToNavigationDroppable>
) : (
<SidePanelList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
{listContent}
</SidePanelList>
)}
</SidePanelSubViewWithSearch>
);
};
@@ -1,54 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconFolder } from 'twenty-ui/display';
import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddFolderToNavigationMenuDraft';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
export const useAddFolderToNavigationMenu = () => {
const { t } = useLingui();
const { addFolderToDraft } = useAddFolderToNavigationMenuDraft();
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
const navigationMenuItemsDraft = useAtomStateValue(
navigationMenuItemsDraftState,
);
const { openNavigationMenuItemInSidePanel } =
useOpenNavigationMenuItemInSidePanel();
const addMenuItemInsertionContext = useAtomStateValue(
addMenuItemInsertionContextState,
);
const setAddMenuItemInsertionContext = useSetAtomState(
addMenuItemInsertionContextState,
);
const currentDraft = isDefined(navigationMenuItemsDraft)
? navigationMenuItemsDraft
: workspaceNavigationMenuItems;
const handleAddFolder = () => {
const targetFolderId = addMenuItemInsertionContext?.targetFolderId ?? null;
const targetIndex = addMenuItemInsertionContext?.targetIndex;
const itemId = addFolderToDraft(
t`New folder`,
currentDraft,
targetFolderId,
targetIndex,
);
setAddMenuItemInsertionContext(null);
openNavigationMenuItemInSidePanel({
itemId,
pageTitle: t`Edit folder`,
pageIcon: IconFolder,
focusTitleInput: true,
});
};
return { handleAddFolder };
};
@@ -1,55 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconLink } from 'twenty-ui/display';
import { useAddLinkToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddLinkToNavigationMenuDraft';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
export const useAddLinkToNavigationMenu = () => {
const { t } = useLingui();
const { addLinkToDraft } = useAddLinkToNavigationMenuDraft();
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
const navigationMenuItemsDraft = useAtomStateValue(
navigationMenuItemsDraftState,
);
const { openNavigationMenuItemInSidePanel } =
useOpenNavigationMenuItemInSidePanel();
const addMenuItemInsertionContext = useAtomStateValue(
addMenuItemInsertionContextState,
);
const setAddMenuItemInsertionContext = useSetAtomState(
addMenuItemInsertionContextState,
);
const currentDraft = isDefined(navigationMenuItemsDraft)
? navigationMenuItemsDraft
: workspaceNavigationMenuItems;
const handleAddLink = () => {
const targetFolderId = addMenuItemInsertionContext?.targetFolderId ?? null;
const targetIndex = addMenuItemInsertionContext?.targetIndex;
const itemId = addLinkToDraft(
t`Link label`,
'www.example.com',
currentDraft,
targetFolderId,
targetIndex,
);
setAddMenuItemInsertionContext(null);
openNavigationMenuItemInSidePanel({
itemId,
pageTitle: t`Edit link`,
pageIcon: IconLink,
focusTitleInput: true,
});
};
return { handleAddLink };
};
@@ -1,100 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { useDraftNavigationMenuItemsAllFolders } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItemsAllFolders';
import { useDraftNavigationMenuItemsWorkspaceFolders } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItemsWorkspaceFolders';
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/hooks/useNavigationMenuItemMoveRemove';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
type FolderOption = {
id: string;
name: string;
folderId?: string | null;
};
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 { closeSidePanelMenu } = useSidePanelMenu();
const { moveToFolder } = useNavigationMenuItemMoveRemove();
const selectedNavigationMenuItemInEditMode = useAtomStateValue(
selectedNavigationMenuItemInEditModeState,
);
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const selectedItemType = selectedItem?.type ?? null;
const { allFolders } = useDraftNavigationMenuItemsAllFolders();
const { workspaceFolders } = useDraftNavigationMenuItemsWorkspaceFolders();
const selectedFolderId =
selectedItemType === NavigationMenuItemType.FOLDER
? selectedNavigationMenuItemInEditMode
: null;
const currentFolderId =
selectedItemType === NavigationMenuItemType.FOLDER
? (selectedItem?.id ?? null)
: (selectedItem?.folderId ?? null);
const descendantFolderIds =
selectedItemType === NavigationMenuItemType.FOLDER &&
isDefined(selectedFolderId)
? getDescendantFolderIds(selectedFolderId, allFolders)
: new Set<string>();
const includeNoFolderOption =
(selectedItemType === NavigationMenuItemType.FOLDER &&
isDefined(selectedFolderId)) ||
(selectedItemType === NavigationMenuItemType.LINK &&
isDefined(currentFolderId));
const folders =
includeNoFolderOption &&
selectedItemType === NavigationMenuItemType.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);
closeSidePanelMenu();
}
};
return {
foldersToShow,
includeNoFolderOption,
handleSelectFolder,
};
};
@@ -1,140 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconColumnInsertRight } from 'twenty-ui/display';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { type OrganizeActionsProps } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOrganizeActions';
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';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { SidePanelPages } from 'twenty-shared/types';
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,
disableDrag: true,
};
};
export const useNavigationMenuItemEditOrganizeActions =
(): OrganizeActionsProps => {
const { t } = useLingui();
const { closeSidePanelMenu } = useSidePanelMenu();
const { navigateSidePanel } = useNavigateSidePanel();
const selectedNavigationMenuItemInEditMode = useAtomStateValue(
selectedNavigationMenuItemInEditModeState,
);
const setSelectedNavigationMenuItemInEditMode = useSetAtomState(
selectedNavigationMenuItemInEditModeState,
);
const setAddMenuItemInsertionContext = useSetAtomState(
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);
closeSidePanelMenu();
}
};
const handleAddAtOffset = (offset: 0 | 1) => {
if (!isDefined(selectedItem)) return;
const context = getAddMenuItemInsertionContext(
selectedItem,
workspaceNavigationMenuItems,
offset,
);
if (!context) return;
setAddMenuItemInsertionContext(context);
navigateSidePanel({
page: SidePanelPages.NavigationMenuAddItem,
pageTitle: t`New sidebar item`,
pageIcon: IconColumnInsertRight,
resetNavigationStack: true,
});
};
const handleAddBefore = () => handleAddAtOffset(0);
const handleAddAfter = () => handleAddAtOffset(1);
return {
canMoveUp,
canMoveDown,
onMoveUp: handleMoveUp,
onMoveDown: handleMoveDown,
onRemove: handleRemove,
onAddBefore: handleAddBefore,
onAddAfter: handleAddAfter,
};
};
@@ -1,8 +0,0 @@
export enum SidePanelNavigationItemActions {
MOVE_UP = 'move-up',
MOVE_DOWN = 'move-down',
MOVE_TO_FOLDER = 'move-to-folder',
ADD_BEFORE = 'add-before',
ADD_AFTER = 'add-after',
REMOVE = 'remove',
}
@@ -1,55 +0,0 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
type GetAvailableObjectMetadataForNewSidebarItemProps = {
objectMetadataItems: ObjectMetadataItem[];
activeNonSystemObjectMetadataItems: ObjectMetadataItem[];
objectMetadataIdsWithIndexView: Set<string>;
objectMetadataIdsWithDisplayableViews: Set<string>;
};
type GetAvailableObjectMetadataForNewSidebarItemResult = {
availableObjectMetadataItems: ObjectMetadataItem[];
availableSystemObjectMetadataItems: ObjectMetadataItem[];
objectMetadataItemsWithViews: ObjectMetadataItem[];
availableSystemObjectMetadataItemsForView: ObjectMetadataItem[];
};
export const getAvailableObjectMetadataForNewSidebarItem = ({
objectMetadataItems,
activeNonSystemObjectMetadataItems,
objectMetadataIdsWithIndexView,
objectMetadataIdsWithDisplayableViews,
}: GetAvailableObjectMetadataForNewSidebarItemProps): GetAvailableObjectMetadataForNewSidebarItemResult => {
const availableObjectMetadataItems = activeNonSystemObjectMetadataItems
.filter((item) => objectMetadataIdsWithIndexView.has(item.id))
.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),
);
return {
availableObjectMetadataItems,
availableSystemObjectMetadataItems,
objectMetadataItemsWithViews,
availableSystemObjectMetadataItemsForView,
};
};
@@ -1,26 +0,0 @@
import { SidePanelNavigationItemActions } from '@/side-panel/pages/navigation-menu-item/side-panel-navigation-item-actions';
const ORGANIZE_ACTIONS_WITHOUT_MOVE_TO_FOLDER: SidePanelNavigationItemActions[] =
[
SidePanelNavigationItemActions.MOVE_UP,
SidePanelNavigationItemActions.MOVE_DOWN,
SidePanelNavigationItemActions.ADD_BEFORE,
SidePanelNavigationItemActions.ADD_AFTER,
SidePanelNavigationItemActions.REMOVE,
];
const ORGANIZE_ACTIONS_WITH_MOVE_TO_FOLDER: SidePanelNavigationItemActions[] = [
SidePanelNavigationItemActions.MOVE_UP,
SidePanelNavigationItemActions.MOVE_DOWN,
SidePanelNavigationItemActions.MOVE_TO_FOLDER,
SidePanelNavigationItemActions.ADD_BEFORE,
SidePanelNavigationItemActions.ADD_AFTER,
SidePanelNavigationItemActions.REMOVE,
];
export const getOrganizeActionsSelectableItemIds = (
includeMoveToFolder: boolean,
): SidePanelNavigationItemActions[] =>
includeMoveToFolder
? ORGANIZE_ACTIONS_WITH_MOVE_TO_FOLDER
: ORGANIZE_ACTIONS_WITHOUT_MOVE_TO_FOLDER;