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