refactor!: rename Command Menu page/navigation layer to Side Panel (#18393)
This commit is contained in:
@@ -1,36 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import React from 'react';
|
||||
import { Label } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledGroupHeading = styled(Label)`
|
||||
align-items: center;
|
||||
padding-bottom: ${themeCssVariables.spacing[1]};
|
||||
padding-left: ${themeCssVariables.spacing[1]};
|
||||
padding-right: ${themeCssVariables.spacing[1]};
|
||||
padding-top: ${themeCssVariables.spacing[2]};
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
const StyledGroup = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[0.5]};
|
||||
`;
|
||||
|
||||
type CommandGroupProps = {
|
||||
heading: string;
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
};
|
||||
|
||||
export const CommandGroup = ({ heading, children }: CommandGroupProps) => {
|
||||
if (!children || !React.Children.count(children)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<StyledGroupHeading>{heading}</StyledGroupHeading>
|
||||
<StyledGroup>{children}</StyledGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,105 +0,0 @@
|
||||
import { type ActionConfig } from '@/action-menu/actions/types/ActionConfig';
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { ResetContextToSelectionCommandButton } from '@/command-menu/components/ResetContextToSelectionCommandButton';
|
||||
import { RESET_CONTEXT_TO_SELECTION } from '@/command-menu/constants/ResetContextToSelection';
|
||||
import { useMatchingCommandMenuActions } from '@/command-menu/hooks/useMatchingCommandMenuActions';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type ActionGroupConfig = {
|
||||
heading: string;
|
||||
items?: ActionConfig[];
|
||||
};
|
||||
|
||||
export const CommandMenu = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const commandMenuSearch = useAtomStateValue(commandMenuSearchState);
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const {
|
||||
noResults,
|
||||
matchingStandardActionRecordSelectionActions,
|
||||
matchingStandardActionObjectActions,
|
||||
matchingWorkflowRunRecordSelectionActions,
|
||||
matchingFrontComponentRecordSelectionActions,
|
||||
matchingStandardActionGlobalActions,
|
||||
matchingWorkflowRunGlobalActions,
|
||||
matchingFrontComponentGlobalActions,
|
||||
matchingNavigateActions,
|
||||
fallbackActions,
|
||||
matchingCreateRelatedRecordActions,
|
||||
} = useMatchingCommandMenuActions({
|
||||
commandMenuSearch,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line twenty/matching-state-variable
|
||||
const previousContextStoreCurrentObjectMetadataItemId =
|
||||
useAtomComponentStateValue(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
'command-menu-previous',
|
||||
);
|
||||
|
||||
const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
);
|
||||
const currentObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === contextStoreCurrentObjectMetadataItemId,
|
||||
);
|
||||
|
||||
const commandGroups: ActionGroupConfig[] = [
|
||||
{
|
||||
heading: t`Record Selection`,
|
||||
items: matchingStandardActionRecordSelectionActions
|
||||
.concat(matchingWorkflowRunRecordSelectionActions)
|
||||
.concat(matchingFrontComponentRecordSelectionActions),
|
||||
},
|
||||
{
|
||||
heading: t`Create Related Record`,
|
||||
items: matchingCreateRelatedRecordActions,
|
||||
},
|
||||
{
|
||||
heading: currentObjectMetadataItem?.labelPlural ?? t`Object`,
|
||||
items: matchingStandardActionObjectActions,
|
||||
},
|
||||
{
|
||||
heading: t`Global`,
|
||||
items: matchingStandardActionGlobalActions
|
||||
.concat(matchingWorkflowRunGlobalActions)
|
||||
.concat(matchingFrontComponentGlobalActions)
|
||||
.concat(matchingNavigateActions),
|
||||
},
|
||||
{
|
||||
heading: t`Search ''${commandMenuSearch}'' with...`,
|
||||
items: fallbackActions,
|
||||
},
|
||||
];
|
||||
|
||||
const selectableItems = commandGroups.flatMap((group) => group.items ?? []);
|
||||
|
||||
const selectableItemIds = selectableItems.map((item) => item.key);
|
||||
|
||||
if (isDefined(previousContextStoreCurrentObjectMetadataItemId)) {
|
||||
selectableItemIds.unshift(RESET_CONTEXT_TO_SELECTION);
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandMenuList
|
||||
commandGroups={commandGroups}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={noResults}
|
||||
>
|
||||
{isDefined(previousContextStoreCurrentObjectMetadataItemId) && (
|
||||
<CommandGroup heading={t`Context`}>
|
||||
<ResetContextToSelectionCommandButton />
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandMenuList>
|
||||
);
|
||||
};
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
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
@@ -1,25 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTitleState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledPageTitle = styled.div`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
export const CommandMenuAskAIInfo = () => {
|
||||
const currentAIChatThreadTitle = useAtomStateValue(
|
||||
currentAIChatThreadTitleState,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledPageTitle>
|
||||
<OverflowingTextWithTooltip
|
||||
text={currentAIChatThreadTitle ?? t`Ask AI`}
|
||||
/>
|
||||
</StyledPageTitle>
|
||||
);
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
import { COMMAND_MENU_NAVIGATION_HISTORY_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuNavigationHistoryDropdownId';
|
||||
import { useCommandMenuContextChips } from '@/command-menu/hooks/useCommandMenuContextChips';
|
||||
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
||||
import { styled } from '@linaria/react';
|
||||
import { IconChevronLeft } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledNavigationIcon = styled.div`
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledIconChevronLeft = styled(IconChevronLeft)`
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
`;
|
||||
|
||||
export const CommandMenuBackButton = () => {
|
||||
const { goBackFromCommandMenu } = useCommandMenuHistory();
|
||||
|
||||
const { contextChips } = useCommandMenuContextChips();
|
||||
|
||||
const { openDropdown } = useOpenDropdown();
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const handleBackButtonContextMenu = (
|
||||
event: React.MouseEvent<HTMLDivElement>,
|
||||
) => {
|
||||
if (contextChips.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
openDropdown({
|
||||
dropdownComponentInstanceIdFromProps:
|
||||
COMMAND_MENU_NAVIGATION_HISTORY_DROPDOWN_ID,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
clickableComponent={
|
||||
<StyledNavigationIcon onContextMenu={handleBackButtonContextMenu}>
|
||||
<IconButton
|
||||
Icon={StyledIconChevronLeft}
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onClick={goBackFromCommandMenu}
|
||||
/>
|
||||
</StyledNavigationIcon>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
{contextChips.slice(0, -1).map((chip, index) => (
|
||||
<MenuItem
|
||||
key={index}
|
||||
LeftComponent={chip.Icons}
|
||||
onClick={() => {
|
||||
closeDropdown(COMMAND_MENU_NAVIGATION_HISTORY_DROPDOWN_ID);
|
||||
chip.onClick?.();
|
||||
}}
|
||||
text={chip.text}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
dropdownId={COMMAND_MENU_NAVIGATION_HISTORY_DROPDOWN_ID}
|
||||
dropdownPlacement="bottom-start"
|
||||
disableClickForClickableComponent={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledCommandMenuContainer = styled.div<{ isMobile: boolean }>`
|
||||
max-height: ${({ isMobile }) => {
|
||||
const mobileOffset = isMobile ? themeCssVariables.spacing[16] : '0px';
|
||||
|
||||
return `calc(100% - ${mobileOffset})`;
|
||||
}};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
type CommandMenuContainerProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CommandMenuContainer = ({
|
||||
children,
|
||||
}: CommandMenuContainerProps) => {
|
||||
const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.id === contextStoreCurrentObjectMetadataItemId,
|
||||
);
|
||||
|
||||
const contextStoreCurrentViewId = useAtomComponentStateValue(
|
||||
contextStoreCurrentViewIdComponentState,
|
||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem?.namePlural ?? '',
|
||||
contextStoreCurrentViewId ?? '',
|
||||
);
|
||||
|
||||
return (
|
||||
<RecordComponentInstanceContextsWrapper componentInstanceId={recordIndexId}>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
|
||||
>
|
||||
<StyledCommandMenuContainer isMobile={isMobile}>
|
||||
{children}
|
||||
</StyledCommandMenuContainer>
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</RecordComponentInstanceContextsWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,96 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Fragment } from 'react/jsx-runtime';
|
||||
import { type CommandMenuPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledChip = styled.button<{
|
||||
withText: boolean;
|
||||
maxWidth?: string;
|
||||
onClick?: () => void;
|
||||
}>`
|
||||
all: unset;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: ${themeCssVariables.background.transparent.light};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
height: ${themeCssVariables.spacing[6]};
|
||||
/* If the chip has text, we add extra padding to have a more balanced design */
|
||||
padding: 0
|
||||
${({ withText }) =>
|
||||
withText ? themeCssVariables.spacing[2] : themeCssVariables.spacing[1]};
|
||||
font-family: inherit;
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
line-height: ${themeCssVariables.text.lineHeight.lg};
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
cursor: ${({ onClick }) => (isDefined(onClick) ? 'pointer' : 'default')};
|
||||
|
||||
&:hover {
|
||||
background: ${({ onClick }) =>
|
||||
isDefined(onClick)
|
||||
? themeCssVariables.background.transparent.medium
|
||||
: themeCssVariables.background.transparent.light};
|
||||
}
|
||||
max-width: ${({ maxWidth }) => maxWidth ?? ''};
|
||||
`;
|
||||
|
||||
const StyledIconsContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledEmptyText = styled.div`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
`;
|
||||
|
||||
export type CommandMenuContextChipProps = {
|
||||
Icons: React.ReactNode[];
|
||||
text?: string;
|
||||
onClick?: () => void;
|
||||
testId?: string;
|
||||
maxWidth?: string;
|
||||
forceEmptyText?: boolean;
|
||||
page?: {
|
||||
page: CommandMenuPages;
|
||||
pageId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const CommandMenuContextChip = ({
|
||||
Icons,
|
||||
text,
|
||||
onClick,
|
||||
testId,
|
||||
maxWidth,
|
||||
forceEmptyText = false,
|
||||
}: CommandMenuContextChipProps) => {
|
||||
return (
|
||||
<StyledChip
|
||||
withText={isNonEmptyString(text)}
|
||||
onClick={onClick}
|
||||
data-testid={testId}
|
||||
maxWidth={maxWidth}
|
||||
>
|
||||
<StyledIconsContainer>
|
||||
{Icons.map((Icon, index) => (
|
||||
<Fragment key={index}>{Icon}</Fragment>
|
||||
))}
|
||||
</StyledIconsContainer>
|
||||
{text?.trim?.() ? (
|
||||
<OverflowingTextWithTooltip text={text} />
|
||||
) : !forceEmptyText ? (
|
||||
<StyledEmptyText>{t`Untitled`}</StyledEmptyText>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</StyledChip>
|
||||
);
|
||||
};
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledCommandMenuContextChipIconWrapper = styled.div`
|
||||
align-items: center;
|
||||
background: ${themeCssVariables.background.primary};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const CommandMenuContextChipIconWrapper =
|
||||
StyledCommandMenuContextChipIconWrapper;
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
const StyledIconWrapper = styled.div<{ withIconBackground?: boolean }>`
|
||||
background: ${({ withIconBackground }) =>
|
||||
withIconBackground ? themeCssVariables.background.primary : 'unset'};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
border: 1px solid
|
||||
${({ withIconBackground }) =>
|
||||
withIconBackground
|
||||
? themeCssVariables.border.color.medium
|
||||
: 'transparent'};
|
||||
&:not(:first-of-type) {
|
||||
margin-left: -${themeCssVariables.spacing[1]};
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const CommandMenuContextRecordChipAvatars = ({
|
||||
objectMetadataItem,
|
||||
record,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
record: ObjectRecord;
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { recordChipData } = useRecordChipData({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
record,
|
||||
});
|
||||
const { Icon, IconColor } = useGetStandardObjectIcon(
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
return (
|
||||
<StyledIconWrapper
|
||||
withIconBackground={recordChipData.avatarType !== 'rounded'}
|
||||
>
|
||||
{Icon ? (
|
||||
<Icon color={IconColor} size={theme.icon.size.sm} />
|
||||
) : (
|
||||
<Avatar
|
||||
avatarUrl={recordChipData.avatarUrl}
|
||||
placeholderColorSeed={recordChipData.recordId}
|
||||
placeholder={recordChipData.name}
|
||||
type={recordChipData.avatarType}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
</StyledIconWrapper>
|
||||
);
|
||||
};
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip';
|
||||
import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars';
|
||||
import { getSelectedRecordsContextText } from '@/command-menu/utils/getRecordContextText';
|
||||
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const CommandMenuContextRecordsChip = ({
|
||||
objectMetadataItemId,
|
||||
instanceId,
|
||||
}: {
|
||||
objectMetadataItemId: string;
|
||||
instanceId?: string;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataItemId,
|
||||
});
|
||||
const allowRequestsToTwentyIcons = useAtomStateValue(
|
||||
allowRequestsToTwentyIconsState,
|
||||
);
|
||||
|
||||
const { records, loading, totalCount } =
|
||||
useFindManyRecordsSelectedInContextStore({
|
||||
limit: 3,
|
||||
instanceId,
|
||||
});
|
||||
|
||||
if (loading || !totalCount || records.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Avatars = records.map((record) => (
|
||||
<CommandMenuContextRecordChipAvatars
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
key={record.id}
|
||||
record={record}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<CommandMenuContextChip
|
||||
text={getSelectedRecordsContextText(
|
||||
objectMetadataItem,
|
||||
records,
|
||||
totalCount,
|
||||
allowRequestsToTwentyIcons,
|
||||
)}
|
||||
Icons={Avatars}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import { CommandMenuPageLayoutInfoContent } from '@/command-menu/components/CommandMenuPageLayoutInfoContent';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const CommandMenuDashboardPageLayoutInfo = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
|
||||
if (!isDefined(pageLayoutId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <CommandMenuPageLayoutInfoContent pageLayoutId={pageLayoutId} />;
|
||||
};
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { COMMAND_MENU_LIST_SELECTABLE_LIST_ID } from '@/command-menu/constants/CommandMenuListSelectableListId';
|
||||
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const CommandMenuDefaultSelectionEffect = ({
|
||||
selectableItemIds,
|
||||
}: {
|
||||
selectableItemIds: string[];
|
||||
}) => {
|
||||
const { setSelectedItemId } = useSelectableList(
|
||||
COMMAND_MENU_LIST_SELECTABLE_LIST_ID,
|
||||
);
|
||||
|
||||
const selectedItemId = useAtomComponentStateValue(
|
||||
selectedItemIdComponentState,
|
||||
COMMAND_MENU_LIST_SELECTABLE_LIST_ID,
|
||||
);
|
||||
|
||||
const hasUserSelectedCommand = useAtomStateValue(hasUserSelectedCommandState);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isDefined(selectedItemId) &&
|
||||
selectableItemIds.includes(selectedItemId) &&
|
||||
hasUserSelectedCommand
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectableItemIds.length > 0) {
|
||||
setSelectedItemId(selectableItemIds[0]);
|
||||
}
|
||||
}, [
|
||||
hasUserSelectedCommand,
|
||||
selectableItemIds,
|
||||
selectedItemId,
|
||||
setSelectedItemId,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,110 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState';
|
||||
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useUpdateFolderInDraft } from '@/navigation-menu-item/hooks/useUpdateFolderInDraft';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
const StyledClickableIconWrapper = styled.div`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const CommandMenuFolderInfo = () => {
|
||||
const { t } = useLingui();
|
||||
const { getIcon } = useIcons();
|
||||
const commandMenuPageInfo = useAtomStateValue(commandMenuPageInfoState);
|
||||
const [
|
||||
commandMenuShouldFocusTitleInput,
|
||||
setCommandMenuShouldFocusTitleInput,
|
||||
] = useAtomComponentState(
|
||||
commandMenuShouldFocusTitleInputComponentState,
|
||||
commandMenuPageInfo.instanceId,
|
||||
);
|
||||
const selectedNavigationMenuItemInEditMode = useAtomStateValue(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const items = useWorkspaceSectionItems();
|
||||
const { updateFolderInDraft } = useUpdateFolderInDraft();
|
||||
|
||||
const defaultLabel = t`New folder`;
|
||||
const placeholder = t`Folder name`;
|
||||
|
||||
const selectedItem = selectedNavigationMenuItemInEditMode
|
||||
? items.find(
|
||||
(item) =>
|
||||
item.itemType === NavigationMenuItemType.FOLDER &&
|
||||
item.id === selectedNavigationMenuItemInEditMode,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (!selectedItem) return null;
|
||||
|
||||
const itemId = selectedItem.id;
|
||||
const itemName = selectedItem.name ?? defaultLabel;
|
||||
|
||||
const handleChange = (text: string) => {
|
||||
updateFolderInDraft(itemId, { name: text });
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const trimmed = itemName.trim();
|
||||
const finalName = trimmed.length > 0 ? trimmed : defaultLabel;
|
||||
|
||||
if (finalName !== itemName) {
|
||||
updateFolderInDraft(itemId, { name: finalName });
|
||||
}
|
||||
};
|
||||
|
||||
const selectedIconKey = selectedItem.icon ?? FOLDER_ICON_DEFAULT;
|
||||
const FolderIconComponent = getIcon(selectedIconKey);
|
||||
|
||||
return (
|
||||
<CommandMenuPageInfoLayout
|
||||
icon={
|
||||
<IconPicker
|
||||
dropdownId="command-menu-folder-icon-picker"
|
||||
selectedIconKey={selectedIconKey}
|
||||
onChange={({ iconKey }) =>
|
||||
updateFolderInDraft(itemId, { icon: iconKey })
|
||||
}
|
||||
clickableComponent={
|
||||
<StyledClickableIconWrapper>
|
||||
<NavigationMenuItemStyleIcon
|
||||
Icon={FolderIconComponent}
|
||||
color={selectedItem.color}
|
||||
/>
|
||||
</StyledClickableIconWrapper>
|
||||
}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<TitleInput
|
||||
instanceId={`folder-name-${itemId}`}
|
||||
sizeVariant="sm"
|
||||
value={itemName}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
onEnter={handleSave}
|
||||
onEscape={handleSave}
|
||||
onClickOutside={handleSave}
|
||||
onTab={handleSave}
|
||||
onShiftTab={handleSave}
|
||||
shouldFocus={commandMenuShouldFocusTitleInput}
|
||||
onFocus={() => setCommandMenuShouldFocusTitleInput(false)}
|
||||
/>
|
||||
}
|
||||
label={t`Folder`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CommandMenuOpenContainer } from '@/command-menu/components/CommandMenuOpenContainer';
|
||||
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { SidePanelRouter } from '@/side-panel/components/SidePanelRouter';
|
||||
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
@@ -13,16 +13,16 @@ const StyledCommandMenuMobileFullScreenContainer = styled.div`
|
||||
`;
|
||||
|
||||
export const CommandMenuForMobile = () => {
|
||||
const isCommandMenuOpened = useAtomStateValue(isCommandMenuOpenedState);
|
||||
const isSidePanelOpened = useAtomStateValue(isSidePanelOpenedState);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isCommandMenuOpened && (
|
||||
{isSidePanelOpened && (
|
||||
<>
|
||||
{createPortal(
|
||||
<StyledCommandMenuMobileFullScreenContainer>
|
||||
<CommandMenuOpenContainer>
|
||||
<CommandMenuRouter />
|
||||
<SidePanelRouter />
|
||||
</CommandMenuOpenContainer>
|
||||
</StyledCommandMenuMobileFullScreenContainer>,
|
||||
document.body,
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import {
|
||||
CommandMenuItem,
|
||||
type CommandMenuItemProps,
|
||||
} from '@/command-menu/components/CommandMenuItem';
|
||||
import { COMMAND_MENU_LIST_SELECTABLE_LIST_ID } from '@/command-menu/constants/CommandMenuListSelectableListId';
|
||||
import { SIDE_PANEL_SELECTABLE_LIST_ID } from '@/side-panel/constants/SidePanelSelectableListId';
|
||||
import {
|
||||
Dropdown,
|
||||
type DropdownProps,
|
||||
@@ -37,7 +37,7 @@ export const CommandMenuItemDropdown = ({
|
||||
);
|
||||
|
||||
const { setSelectedItemId } = useSelectableList(
|
||||
COMMAND_MENU_LIST_SELECTABLE_LIST_ID,
|
||||
SIDE_PANEL_SELECTABLE_LIST_ID,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { Draggable } from '@hello-pangea/dnd';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
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 { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
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 setAddToNavPayloadRegistry = useSetAtomState(
|
||||
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) {
|
||||
setAddToNavPayloadRegistry((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;
|
||||
};
|
||||
@@ -1,88 +0,0 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconLink } from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState';
|
||||
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const CommandMenuLinkInfo = () => {
|
||||
const { t } = useLingui();
|
||||
const commandMenuPageInfo = useAtomStateValue(commandMenuPageInfoState);
|
||||
const [
|
||||
commandMenuShouldFocusTitleInput,
|
||||
setCommandMenuShouldFocusTitleInput,
|
||||
] = useAtomComponentState(
|
||||
commandMenuShouldFocusTitleInputComponentState,
|
||||
commandMenuPageInfo.instanceId,
|
||||
);
|
||||
const selectedNavigationMenuItemInEditMode = useAtomStateValue(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const items = useWorkspaceSectionItems();
|
||||
const { updateLinkInDraft } = useUpdateLinkInDraft();
|
||||
|
||||
const defaultLabel = t`Link label`;
|
||||
const placeholder = t`Link label`;
|
||||
|
||||
const selectedItem = selectedNavigationMenuItemInEditMode
|
||||
? items.find(
|
||||
(item) =>
|
||||
item.itemType === NavigationMenuItemType.LINK &&
|
||||
item.id === selectedNavigationMenuItemInEditMode,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (!selectedItem) return null;
|
||||
|
||||
const itemId = selectedItem.id;
|
||||
const itemName = selectedItem.name ?? defaultLabel;
|
||||
|
||||
const handleChange = (text: string) => {
|
||||
updateLinkInDraft(itemId, { name: text });
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const trimmed = itemName.trim();
|
||||
const finalName = trimmed.length > 0 ? trimmed : defaultLabel;
|
||||
|
||||
if (finalName !== itemName) {
|
||||
updateLinkInDraft(itemId, { name: finalName });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandMenuPageInfoLayout
|
||||
icon={
|
||||
<NavigationMenuItemStyleIcon
|
||||
Icon={IconLink}
|
||||
color={selectedItem.color}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<TitleInput
|
||||
instanceId={`link-label-${itemId}`}
|
||||
sizeVariant="sm"
|
||||
value={itemName}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
onEnter={handleSave}
|
||||
onEscape={handleSave}
|
||||
onClickOutside={handleSave}
|
||||
onTab={handleSave}
|
||||
onShiftTab={handleSave}
|
||||
shouldFocus={commandMenuShouldFocusTitleInput}
|
||||
onFocus={() => setCommandMenuShouldFocusTitleInput(false)}
|
||||
/>
|
||||
}
|
||||
label={t`Link`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,103 +0,0 @@
|
||||
import { ActionComponent } from '@/action-menu/actions/display/components/ActionComponent';
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { type ActionGroupConfig } from '@/command-menu/components/CommandMenu';
|
||||
import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect';
|
||||
import { COMMAND_MENU_LIST_SELECTABLE_LIST_ID } from '@/command-menu/constants/CommandMenuListSelectableListId';
|
||||
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
|
||||
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
|
||||
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
export type CommandMenuListProps = {
|
||||
commandGroups: ActionGroupConfig[];
|
||||
selectableItemIds: string[];
|
||||
children?: React.ReactNode;
|
||||
loading?: boolean;
|
||||
noResults?: boolean;
|
||||
noResultsText?: string;
|
||||
};
|
||||
|
||||
const StyledInnerList = styled.div`
|
||||
max-height: calc(
|
||||
100dvh - ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px -
|
||||
${COMMAND_MENU_SEARCH_BAR_PADDING * 2}px
|
||||
);
|
||||
padding-left: ${themeCssVariables.spacing[2]};
|
||||
padding-right: ${themeCssVariables.spacing[2]};
|
||||
padding-top: ${themeCssVariables.spacing[2]};
|
||||
width: calc(100% - ${themeCssVariables.spacing[4]});
|
||||
|
||||
@media (min-width: ${MOBILE_VIEWPORT}px) {
|
||||
max-height: calc(
|
||||
100dvh - ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px -
|
||||
${COMMAND_MENU_SEARCH_BAR_PADDING * 2}px
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledCommandMenuList = styled.div`
|
||||
overflow-y: hidden;
|
||||
`;
|
||||
|
||||
const StyledEmpty = styled.div`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
height: 64px;
|
||||
justify-content: center;
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
export const CommandMenuList = ({
|
||||
commandGroups,
|
||||
selectableItemIds,
|
||||
children,
|
||||
loading = false,
|
||||
noResults = false,
|
||||
noResultsText,
|
||||
}: CommandMenuListProps) => {
|
||||
const setHasUserSelectedCommand = useSetAtomState(
|
||||
hasUserSelectedCommandState,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledCommandMenuList>
|
||||
<CommandMenuDefaultSelectionEffect
|
||||
selectableItemIds={selectableItemIds}
|
||||
/>
|
||||
<ScrollWrapper componentInstanceId={`scroll-wrapper-command-menu`}>
|
||||
<StyledInnerList>
|
||||
<SelectableList
|
||||
selectableListInstanceId={COMMAND_MENU_LIST_SELECTABLE_LIST_ID}
|
||||
focusId={SIDE_PANEL_FOCUS_ID}
|
||||
selectableItemIdArray={selectableItemIds}
|
||||
onSelect={() => {
|
||||
setHasUserSelectedCommand(true);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{commandGroups.map(({ heading, items }) =>
|
||||
items?.length ? (
|
||||
<CommandGroup heading={heading} key={heading}>
|
||||
{items.map((item) => (
|
||||
<ActionComponent action={item} key={item.key} />
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : null,
|
||||
)}
|
||||
{noResults && !loading && (
|
||||
<StyledEmpty>{noResultsText ?? t`No results found`}</StyledEmpty>
|
||||
)}
|
||||
</SelectableList>
|
||||
</StyledInnerList>
|
||||
</ScrollWrapper>
|
||||
</StyledCommandMenuList>
|
||||
);
|
||||
};
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import { DEFAULT_RECORD_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig';
|
||||
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
|
||||
import { getActionLabel } from '@/action-menu/utils/getActionLabel';
|
||||
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
|
||||
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useContext } from 'react';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
|
||||
type CommandMenuMultipleRecordsInfoProps = {
|
||||
commandMenuPageInstanceId: string;
|
||||
};
|
||||
|
||||
export const CommandMenuMultipleRecordsInfo = ({
|
||||
commandMenuPageInstanceId,
|
||||
}: CommandMenuMultipleRecordsInfoProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { totalCount } = useFindManyRecordsSelectedInContextStore({
|
||||
instanceId: commandMenuPageInstanceId,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const { Icon, label } =
|
||||
DEFAULT_RECORD_ACTIONS_CONFIG[MultipleRecordsActionKeys.UPDATE];
|
||||
|
||||
return (
|
||||
<CommandMenuPageInfoLayout
|
||||
icon={<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
title={getActionLabel(label)}
|
||||
label={t`${totalCount} selected`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemLabel';
|
||||
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
|
||||
export const CommandMenuObjectViewRecordInfo = () => {
|
||||
const { t } = useLingui();
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const { selectedItemLabel } = useSelectedNavigationMenuItemEditItemLabel();
|
||||
const { selectedItemObjectMetadata } =
|
||||
useSelectedNavigationMenuItemEditItemObjectMetadata();
|
||||
|
||||
const processedItem =
|
||||
selectedItem && selectedItem.itemType !== NavigationMenuItemType.FOLDER
|
||||
? selectedItem
|
||||
: undefined;
|
||||
|
||||
if (!processedItem || !selectedItemLabel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isViewOrRecord = [
|
||||
NavigationMenuItemType.VIEW,
|
||||
NavigationMenuItemType.RECORD,
|
||||
].includes(processedItem.itemType);
|
||||
|
||||
if (!isViewOrRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label =
|
||||
processedItem.itemType === NavigationMenuItemType.RECORD
|
||||
? selectedItemObjectMetadata?.labelSingular
|
||||
: processedItem.viewKey === ViewKey.Index
|
||||
? t`Object`
|
||||
: t`View`;
|
||||
|
||||
return (
|
||||
<CommandMenuPageInfoLayout
|
||||
icon={<NavigationMenuItemIcon navigationMenuItem={processedItem} />}
|
||||
title={<OverflowingTextWithTooltip text={selectedItemLabel} />}
|
||||
label={label}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+15
-15
@@ -1,29 +1,29 @@
|
||||
import { COMMAND_MENU_ANIMATION_VARIANTS } from '@/command-menu/constants/CommandMenuAnimationVariants';
|
||||
import { COMMAND_MENU_CLICK_OUTSIDE_ID } from '@/command-menu/constants/CommandMenuClickOutsideId';
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { isSidePanelAnimatingState } from '@/command-menu/states/isSidePanelAnimatingState';
|
||||
import { type CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
|
||||
import { RECORD_CHIP_CLICK_OUTSIDE_ID } from '@/object-record/record-table/constants/RecordChipClickOutsideId';
|
||||
import { SIDE_PANEL_ANIMATION_VARIANTS } from '@/side-panel/constants/SidePanelAnimationVariants';
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { isSidePanelAnimatingState } from '@/side-panel/states/isSidePanelAnimatingState';
|
||||
import { type SidePanelAnimationVariant } from '@/side-panel/types/SidePanelAnimationVariant';
|
||||
import { MENTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/ui/input/constants/MentionMenuDropdownClickOutsideId';
|
||||
import { SLASH_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/ui/input/constants/SlashMenuDropdownClickOutsideId';
|
||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||
import { PAGE_HEADER_SIDE_PANEL_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderSidePanelButtonClickOutsideId';
|
||||
import { NAVIGATION_DRAWER_CLICK_OUTSIDE_ID } from '@/ui/navigation/navigation-drawer/constants/NavigationDrawerClickOutsideId';
|
||||
import { PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderCommandMenuButtonClickOutsideId';
|
||||
import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useStore } from 'jotai';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { WORKFLOW_DIAGRAM_CREATE_STEP_NODE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramCreateStepNodeClickOutsideId';
|
||||
import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId';
|
||||
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
import { styled } from '@linaria/react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useCallback, useContext, useRef } from 'react';
|
||||
import { LINK_CHIP_CLICK_OUTSIDE_ID } from 'twenty-ui/components';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
const StyledCommandMenuBase = styled.div`
|
||||
background: ${themeCssVariables.background.primary};
|
||||
border-left: 1px solid ${themeCssVariables.border.color.medium};
|
||||
@@ -35,7 +35,7 @@ const StyledCommandMenuBase = styled.div`
|
||||
position: fixed;
|
||||
right: 0%;
|
||||
top: 0%;
|
||||
z-index: ${RootStackingContextZIndices.CommandMenu};
|
||||
z-index: ${RootStackingContextZIndices.SidePanel};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
@@ -47,10 +47,10 @@ export const CommandMenuOpenContainer = ({
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const targetVariantForAnimation: CommandMenuAnimationVariant = isMobile
|
||||
const targetVariantForAnimation: SidePanelAnimationVariant = isMobile
|
||||
? 'fullScreen'
|
||||
: 'normal';
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const commandMenuRef = useRef<HTMLDivElement>(null);
|
||||
const setIsSidePanelAnimating = useSetAtomState(isSidePanelAnimatingState);
|
||||
@@ -64,10 +64,10 @@ export const CommandMenuOpenContainer = ({
|
||||
if (currentFocusId === SIDE_PANEL_FOCUS_ID) {
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
closeCommandMenu();
|
||||
closeSidePanelMenu();
|
||||
}
|
||||
},
|
||||
[closeCommandMenu, store],
|
||||
[closeSidePanelMenu, store],
|
||||
);
|
||||
|
||||
useListenClickOutside({
|
||||
@@ -76,7 +76,7 @@ export const CommandMenuOpenContainer = ({
|
||||
listenerId: 'COMMAND_MENU_LISTENER_ID',
|
||||
excludedClickOutsideIds: [
|
||||
NAVIGATION_DRAWER_CLICK_OUTSIDE_ID,
|
||||
PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID,
|
||||
PAGE_HEADER_SIDE_PANEL_BUTTON_CLICK_OUTSIDE_ID,
|
||||
LINK_CHIP_CLICK_OUTSIDE_ID,
|
||||
RECORD_CHIP_CLICK_OUTSIDE_ID,
|
||||
SLASH_MENU_DROPDOWN_CLICK_OUTSIDE_ID,
|
||||
@@ -95,7 +95,7 @@ export const CommandMenuOpenContainer = ({
|
||||
animate={targetVariantForAnimation}
|
||||
initial="closed"
|
||||
exit="closed"
|
||||
variants={COMMAND_MENU_ANIMATION_VARIANTS}
|
||||
variants={SIDE_PANEL_ANIMATION_VARIANTS}
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconColumnInsertRight,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuAskAIInfo } from '@/command-menu/components/CommandMenuAskAIInfo';
|
||||
import { CommandMenuFolderInfo } from '@/command-menu/components/CommandMenuFolderInfo';
|
||||
import { CommandMenuLinkInfo } from '@/command-menu/components/CommandMenuLinkInfo';
|
||||
import { CommandMenuMultipleRecordsInfo } from '@/command-menu/components/CommandMenuMultipleRecordsInfo';
|
||||
import { CommandMenuObjectViewRecordInfo } from '@/command-menu/components/CommandMenuObjectViewRecordInfo';
|
||||
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
|
||||
import { CommandMenuPageLayoutInfo } from '@/command-menu/components/CommandMenuPageLayoutInfo';
|
||||
import { CommandMenuRecordInfo } from '@/command-menu/components/CommandMenuRecordInfo';
|
||||
import { CommandMenuWorkflowStepInfo } from '@/command-menu/components/CommandMenuWorkflowStepInfo';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
|
||||
import { useContext } from 'react';
|
||||
import { type CommandMenuContextChipProps } from './CommandMenuContextChip';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
const StyledPageTitle = styled.div`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
type CommandMenuPageInfoProps = {
|
||||
pageChip: CommandMenuContextChipProps | undefined;
|
||||
};
|
||||
|
||||
export const CommandMenuPageInfo = ({ pageChip }: CommandMenuPageInfoProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const selectedNavigationMenuItemInEditMode = useAtomStateValue(
|
||||
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 === NavigationMenuItemType.FOLDER) {
|
||||
return <CommandMenuFolderInfo />;
|
||||
}
|
||||
|
||||
if (itemType === NavigationMenuItemType.LINK) {
|
||||
return <CommandMenuLinkInfo />;
|
||||
}
|
||||
|
||||
if (
|
||||
itemType === NavigationMenuItemType.VIEW ||
|
||||
itemType === NavigationMenuItemType.RECORD
|
||||
) {
|
||||
return <CommandMenuObjectViewRecordInfo />;
|
||||
}
|
||||
}
|
||||
|
||||
const isRecordPage = pageChip.page?.page === CommandMenuPages.ViewRecord;
|
||||
|
||||
if (isRecordPage && isDefined(pageChip.page?.pageId)) {
|
||||
return (
|
||||
<CommandMenuRecordInfo commandMenuPageInstanceId={pageChip.page.pageId} />
|
||||
);
|
||||
}
|
||||
|
||||
const isWorkflowStepPage = pageChip.page?.page
|
||||
? [
|
||||
CommandMenuPages.WorkflowStepEdit,
|
||||
CommandMenuPages.WorkflowStepView,
|
||||
CommandMenuPages.WorkflowRunStepView,
|
||||
].includes(pageChip.page?.page)
|
||||
: false;
|
||||
|
||||
if (isWorkflowStepPage && isDefined(pageChip.page?.pageId)) {
|
||||
return (
|
||||
<CommandMenuWorkflowStepInfo
|
||||
key={pageChip.page.pageId}
|
||||
commandMenuPageInstanceId={pageChip.page.pageId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const isPageLayoutPage = pageChip.page?.page
|
||||
? [
|
||||
CommandMenuPages.PageLayoutWidgetTypeSelect,
|
||||
CommandMenuPages.PageLayoutGraphTypeSelect,
|
||||
CommandMenuPages.PageLayoutGraphFilter,
|
||||
CommandMenuPages.PageLayoutIframeSettings,
|
||||
CommandMenuPages.PageLayoutTabSettings,
|
||||
CommandMenuPages.PageLayoutFieldsSettings,
|
||||
CommandMenuPages.PageLayoutFieldsLayout,
|
||||
].includes(pageChip.page?.page)
|
||||
: false;
|
||||
|
||||
if (isPageLayoutPage) {
|
||||
return <CommandMenuPageLayoutInfo />;
|
||||
}
|
||||
|
||||
const isMultipleRecordsPage =
|
||||
pageChip.page?.page === CommandMenuPages.UpdateRecords;
|
||||
|
||||
if (isMultipleRecordsPage && isDefined(pageChip.page?.pageId)) {
|
||||
return (
|
||||
<CommandMenuMultipleRecordsInfo
|
||||
commandMenuPageInstanceId={pageChip.page.pageId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const isAskAIPage = pageChip.page?.page === CommandMenuPages.AskAI;
|
||||
|
||||
if (isAskAIPage) {
|
||||
return <CommandMenuAskAIInfo />;
|
||||
}
|
||||
|
||||
if (pageChip.page?.page === CommandMenuPages.NavigationMenuAddItem) {
|
||||
return (
|
||||
<CommandMenuPageInfoLayout
|
||||
icon={
|
||||
<IconColumnInsertRight
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
}
|
||||
title={<OverflowingTextWithTooltip text={pageChip.text ?? ''} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledPageTitle>
|
||||
<OverflowingTextWithTooltip text={pageChip.text ?? ''} />
|
||||
</StyledPageTitle>
|
||||
);
|
||||
};
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
export const StyledPageInfoContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[0.5]};
|
||||
`;
|
||||
|
||||
export const StyledPageInfoIcon = styled.div<{ iconColor?: string }>`
|
||||
align-items: center;
|
||||
background: ${themeCssVariables.background.transparent.light};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
color: ${({ iconColor }) => iconColor ?? ''};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
padding: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
export const StyledPageInfoTextContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: ${themeCssVariables.spacing[0.5]};
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
export const StyledPageInfoTitleContainer = styled.div`
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
padding-inline: ${themeCssVariables.spacing[1]};
|
||||
min-width: 0;
|
||||
max-width: 150px;
|
||||
`;
|
||||
|
||||
export const StyledPageInfoLabel = styled.div`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
type CommandMenuPageInfoLayoutProps = {
|
||||
icon?: ReactNode;
|
||||
iconColor?: string;
|
||||
title: ReactNode;
|
||||
label?: ReactNode;
|
||||
};
|
||||
|
||||
export const CommandMenuPageInfoLayout = ({
|
||||
icon,
|
||||
iconColor,
|
||||
title,
|
||||
label,
|
||||
}: CommandMenuPageInfoLayoutProps) => {
|
||||
return (
|
||||
<StyledPageInfoContainer>
|
||||
{icon && (
|
||||
<StyledPageInfoIcon iconColor={iconColor}>{icon}</StyledPageInfoIcon>
|
||||
)}
|
||||
<StyledPageInfoTextContainer>
|
||||
<StyledPageInfoTitleContainer>{title}</StyledPageInfoTitleContainer>
|
||||
{label && <StyledPageInfoLabel>{label}</StyledPageInfoLabel>}
|
||||
</StyledPageInfoTextContainer>
|
||||
</StyledPageInfoContainer>
|
||||
);
|
||||
};
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import { CommandMenuDashboardPageLayoutInfo } from '@/command-menu/components/CommandMenuDashboardPageLayoutInfo';
|
||||
import { CommandMenuRecordPageLayoutInfo } from '@/command-menu/components/CommandMenuRecordPageLayoutInfo';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const CommandMenuPageLayoutInfo = () => {
|
||||
const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
);
|
||||
|
||||
if (!isDefined(contextStoreCurrentObjectMetadataItemId)) {
|
||||
throw new Error('Object metadata ID is not defined');
|
||||
}
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: contextStoreCurrentObjectMetadataItemId,
|
||||
});
|
||||
|
||||
const isDashboardContext =
|
||||
objectMetadataItem.nameSingular === CoreObjectNameSingular.Dashboard;
|
||||
|
||||
if (isDashboardContext) {
|
||||
return <CommandMenuDashboardPageLayoutInfo />;
|
||||
}
|
||||
|
||||
return <CommandMenuRecordPageLayoutInfo />;
|
||||
};
|
||||
-147
@@ -1,147 +0,0 @@
|
||||
import { usePageLayoutHeaderInfo } from '@/command-menu/components/hooks/usePageLayoutHeaderInfo';
|
||||
import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState';
|
||||
import { useUpdatePageLayoutTab } from '@/page-layout/hooks/useUpdatePageLayoutTab';
|
||||
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useContext, useState } from 'react';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { CommandMenuPageInfoLayout } from './CommandMenuPageInfoLayout';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
export const CommandMenuPageLayoutInfoContent = ({
|
||||
pageLayoutId,
|
||||
}: {
|
||||
pageLayoutId: string;
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { getIcon } = useIcons();
|
||||
const commandMenuPage = useAtomStateValue(commandMenuPageState);
|
||||
const commandMenuPageInfo = useAtomStateValue(commandMenuPageInfoState);
|
||||
|
||||
const [
|
||||
commandMenuShouldFocusTitleInput,
|
||||
setCommandMenuShouldFocusTitleInput,
|
||||
] = useAtomComponentState(
|
||||
commandMenuShouldFocusTitleInputComponentState,
|
||||
commandMenuPageInfo.instanceId,
|
||||
);
|
||||
|
||||
const handleTitleInputOpen = () => {
|
||||
setCommandMenuShouldFocusTitleInput(false);
|
||||
};
|
||||
|
||||
const pageLayoutDraft = useAtomComponentStateValue(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutEditingWidgetId = useAtomComponentStateValue(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const [pageLayoutTabSettingsOpenTabId] = useAtomComponentState(
|
||||
pageLayoutTabSettingsOpenTabIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo();
|
||||
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
|
||||
const { updatePageLayoutTab } = useUpdatePageLayoutTab(pageLayoutId);
|
||||
|
||||
const [editedTitle, setEditedTitle] = useState<string | null>(null);
|
||||
|
||||
const headerInfo = usePageLayoutHeaderInfo({
|
||||
commandMenuPage,
|
||||
draftPageLayout: pageLayoutDraft,
|
||||
pageLayoutEditingWidgetId,
|
||||
openTabId: pageLayoutTabSettingsOpenTabId,
|
||||
editedTitle,
|
||||
});
|
||||
|
||||
if (!headerInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
headerIcon,
|
||||
headerIconColor,
|
||||
headerType,
|
||||
title,
|
||||
isReadonly,
|
||||
tab,
|
||||
widgetInEditMode,
|
||||
} = headerInfo;
|
||||
|
||||
const Icon = headerIcon ?? getIcon('IconDefault');
|
||||
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
setEditedTitle(newTitle);
|
||||
};
|
||||
|
||||
const saveTitle = async () => {
|
||||
const finalTitle = editedTitle ?? title;
|
||||
|
||||
if (!isNonEmptyString(finalTitle)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateCommandMenuPageInfo({
|
||||
pageTitle: finalTitle,
|
||||
pageIcon: Icon,
|
||||
});
|
||||
|
||||
if (
|
||||
commandMenuPage === CommandMenuPages.PageLayoutTabSettings &&
|
||||
isDefined(tab)
|
||||
) {
|
||||
updatePageLayoutTab(tab.id, { title: finalTitle });
|
||||
} else if (isDefined(widgetInEditMode)) {
|
||||
updatePageLayoutWidget(widgetInEditMode.id, {
|
||||
title: finalTitle,
|
||||
});
|
||||
}
|
||||
|
||||
setEditedTitle(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandMenuPageInfoLayout
|
||||
icon={
|
||||
isDefined(headerIcon) ? (
|
||||
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||
) : undefined
|
||||
}
|
||||
iconColor={headerIconColor}
|
||||
title={
|
||||
<TitleInput
|
||||
instanceId={`page-layout-title-${commandMenuPage}-${pageLayoutId}`}
|
||||
disabled={isReadonly}
|
||||
sizeVariant="sm"
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
placeholder={headerType}
|
||||
onEnter={saveTitle}
|
||||
onEscape={() => setEditedTitle(null)}
|
||||
onClickOutside={saveTitle}
|
||||
onTab={saveTitle}
|
||||
onShiftTab={saveTitle}
|
||||
shouldFocus={commandMenuShouldFocusTitleInput}
|
||||
onFocus={handleTitleInputOpen}
|
||||
/>
|
||||
}
|
||||
label={headerType}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,138 +0,0 @@
|
||||
import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons';
|
||||
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
|
||||
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
|
||||
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
|
||||
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierFamilySelector';
|
||||
import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell';
|
||||
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||
import { CommandMenuPageInfoLayout } from './CommandMenuPageInfoLayout';
|
||||
|
||||
export const CommandMenuRecordInfo = ({
|
||||
commandMenuPageInstanceId,
|
||||
}: {
|
||||
commandMenuPageInstanceId: string;
|
||||
}) => {
|
||||
const viewableRecordNameSingular = useAtomComponentStateValue(
|
||||
viewableRecordNameSingularComponentState,
|
||||
commandMenuPageInstanceId,
|
||||
);
|
||||
const allowRequestsToTwentyIcons = useAtomStateValue(
|
||||
allowRequestsToTwentyIconsState,
|
||||
);
|
||||
|
||||
const viewableRecordId = useAtomComponentStateValue(
|
||||
viewableRecordIdComponentState,
|
||||
commandMenuPageInstanceId,
|
||||
);
|
||||
|
||||
const { objectNameSingular, objectRecordId } = useRecordShowPage(
|
||||
viewableRecordNameSingular!,
|
||||
viewableRecordId!,
|
||||
);
|
||||
|
||||
const recordCreatedAt = useAtomFamilySelectorValue(
|
||||
recordStoreFamilySelector,
|
||||
{
|
||||
recordId: objectRecordId,
|
||||
fieldName: 'createdAt',
|
||||
},
|
||||
) as string | null;
|
||||
|
||||
const recordIdentifier = useAtomFamilySelectorValue(
|
||||
recordStoreIdentifierFamilySelector,
|
||||
{
|
||||
recordId: objectRecordId,
|
||||
allowRequestsToTwentyIcons,
|
||||
},
|
||||
);
|
||||
|
||||
const { localeCatalog } = useAtomStateValue(dateLocaleState);
|
||||
const beautifiedCreatedAt = isNonEmptyString(recordCreatedAt)
|
||||
? beautifyPastDateRelativeToNow(recordCreatedAt, localeCatalog)
|
||||
: '';
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { labelIdentifierFieldMetadataItem } =
|
||||
useLabelIdentifierFieldMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const isTitleReadOnly = useIsRecordFieldReadOnly({
|
||||
recordId: objectRecordId,
|
||||
fieldMetadataId: labelIdentifierFieldMetadataItem?.id ?? '',
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
});
|
||||
|
||||
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const fieldDefinition = {
|
||||
type: labelIdentifierFieldMetadataItem?.type ?? FieldMetadataType.TEXT,
|
||||
iconName: '',
|
||||
fieldMetadataId: labelIdentifierFieldMetadataItem?.id ?? '',
|
||||
label: labelIdentifierFieldMetadataItem?.label ?? '',
|
||||
metadata: {
|
||||
fieldName: labelIdentifierFieldMetadataItem?.name ?? '',
|
||||
objectMetadataNameSingular: objectNameSingular,
|
||||
},
|
||||
defaultValue: labelIdentifierFieldMetadataItem?.defaultValue,
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandMenuPageInfoLayout
|
||||
icon={
|
||||
recordIdentifier ? (
|
||||
<Avatar
|
||||
avatarUrl={recordIdentifier.avatarUrl}
|
||||
placeholder={recordIdentifier.name}
|
||||
placeholderColorSeed={objectRecordId}
|
||||
size="md"
|
||||
type={recordIdentifier.avatarType}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
title={
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
recordId: objectRecordId,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition,
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
isCentered: false,
|
||||
isDisplayModeFixHeight: true,
|
||||
isRecordFieldReadOnly: isTitleReadOnly,
|
||||
}}
|
||||
>
|
||||
<RecordTitleCell
|
||||
sizeVariant="sm"
|
||||
containerType={RecordTitleCellContainerType.PageHeader}
|
||||
/>
|
||||
</FieldContext.Provider>
|
||||
}
|
||||
label={
|
||||
beautifiedCreatedAt ? (
|
||||
<Trans>Created {beautifiedCreatedAt}</Trans>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { CommandMenuPageLayoutInfoContent } from '@/command-menu/components/CommandMenuPageLayoutInfoContent';
|
||||
import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
|
||||
|
||||
export const CommandMenuRecordPageLayoutInfo = () => {
|
||||
const { pageLayoutId } =
|
||||
usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord();
|
||||
|
||||
return <CommandMenuPageLayoutInfoContent pageLayoutId={pageLayoutId} />;
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import { ActionMenuContextProvider } from '@/action-menu/contexts/ActionMenuContextProvider';
|
||||
import { CommandMenuContainer } from '@/command-menu/components/CommandMenuContainer';
|
||||
import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar';
|
||||
import { COMMAND_MENU_PAGES_CONFIG } from '@/command-menu/constants/CommandMenuPagesConfig';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledCommandMenuContent = styled.div`
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
export const CommandMenuRouter = () => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const commandMenuPage = useAtomStateValue(commandMenuPageState);
|
||||
const commandMenuPageInfo = useAtomStateValue(commandMenuPageInfoState);
|
||||
|
||||
const commandMenuPageComponent = isDefined(commandMenuPage) ? (
|
||||
COMMAND_MENU_PAGES_CONFIG.get(commandMenuPage)
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
return (
|
||||
<CommandMenuContainer>
|
||||
<CommandMenuPageComponentInstanceContext.Provider
|
||||
value={{ instanceId: commandMenuPageInfo.instanceId }}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
duration: theme.animation.duration.instant,
|
||||
delay: 0.1,
|
||||
}}
|
||||
>
|
||||
<CommandMenuTopBar />
|
||||
</motion.div>
|
||||
<StyledCommandMenuContent>
|
||||
<ActionMenuContextProvider
|
||||
isInRightDrawer={true}
|
||||
displayType="listItem"
|
||||
actionMenuType="command-menu"
|
||||
>
|
||||
{commandMenuPageComponent}
|
||||
</ActionMenuContextProvider>
|
||||
</StyledCommandMenuContent>
|
||||
</CommandMenuPageComponentInstanceContext.Provider>
|
||||
</CommandMenuContainer>
|
||||
);
|
||||
};
|
||||
-153
@@ -1,153 +0,0 @@
|
||||
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
|
||||
import { CommandMenuWidthEffect } from '@/command-menu/components/CommandMenuWidthEffect';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup';
|
||||
import {
|
||||
COMMAND_MENU_WIDTH_VAR,
|
||||
commandMenuWidthState,
|
||||
} from '@/command-menu/states/commandMenuWidthState';
|
||||
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { tableWidthResizeIsActiveState } from '@/object-record/record-table/states/tableWidthResizeIsActivedState';
|
||||
import { ModalContainerContext } from '@/ui/layout/modal/contexts/ModalContainerContext';
|
||||
import { ResizablePanelGap } from '@/ui/layout/resizable-panel/components/ResizablePanelGap';
|
||||
import { COMMAND_MENU_CONSTRAINTS } from '@/ui/layout/resizable-panel/constants/CommandMenuConstraints';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledSidePanelWrapper = styled.div<{
|
||||
isOpen: boolean;
|
||||
isResizing: boolean;
|
||||
}>`
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
width: ${({ isOpen }) => (isOpen ? `var(${COMMAND_MENU_WIDTH_VAR})` : '0px')};
|
||||
transition: ${({ isResizing }) =>
|
||||
isResizing
|
||||
? 'none'
|
||||
: `width ${themeCssVariables.animation.duration.normal}s`};
|
||||
`;
|
||||
|
||||
const StyledSidePanel = styled.aside`
|
||||
background: ${themeCssVariables.background.primary};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
const StyledModalContainer = styled.div`
|
||||
height: 100%;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const GAP_WIDTH = 8;
|
||||
|
||||
export const CommandMenuSidePanelForDesktop = () => {
|
||||
const isCommandMenuOpened = useAtomStateValue(isCommandMenuOpenedState);
|
||||
const isCommandMenuClosing = useAtomStateValue(isCommandMenuClosingState);
|
||||
const [commandMenuWidth, setCommandMenuWidth] = useAtomState(
|
||||
commandMenuWidthState,
|
||||
);
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
const { commandMenuCloseAnimationCompleteCleanup } =
|
||||
useCommandMenuCloseAnimationCompleteCleanup();
|
||||
|
||||
const [modalContainer, setModalContainer] = useState<HTMLDivElement | null>(
|
||||
null,
|
||||
);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [shouldRenderContent, setShouldRenderContent] =
|
||||
useState(isCommandMenuOpened);
|
||||
|
||||
const setTableWidthResizeIsActive = useSetAtomState(
|
||||
tableWidthResizeIsActiveState,
|
||||
);
|
||||
|
||||
const shouldShowContent = isCommandMenuOpened || shouldRenderContent;
|
||||
|
||||
const handleTransitionEnd = () => {
|
||||
if (isCommandMenuOpened) {
|
||||
// Open animation completed - ensure content persists for close animation
|
||||
setShouldRenderContent(true);
|
||||
} else {
|
||||
// Close animation completed
|
||||
setShouldRenderContent(false);
|
||||
if (isCommandMenuClosing) {
|
||||
commandMenuCloseAnimationCompleteCleanup();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalContainerRef = useCallback(
|
||||
(element: HTMLDivElement | null) => {
|
||||
setModalContainer(element);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleWidthChange = useCallback(
|
||||
(width: number) => {
|
||||
setCommandMenuWidth(width);
|
||||
setIsResizing(false);
|
||||
setTableWidthResizeIsActive(true);
|
||||
},
|
||||
[setCommandMenuWidth, setTableWidthResizeIsActive],
|
||||
);
|
||||
|
||||
const handleResizeStart = useCallback(() => {
|
||||
setIsResizing(true);
|
||||
setTableWidthResizeIsActive(false);
|
||||
}, [setTableWidthResizeIsActive]);
|
||||
|
||||
const handleCollapse = useCallback(() => {
|
||||
closeCommandMenu();
|
||||
setIsResizing(false);
|
||||
setTableWidthResizeIsActive(true);
|
||||
}, [closeCommandMenu, setTableWidthResizeIsActive]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommandMenuWidthEffect />
|
||||
<ResizablePanelGap
|
||||
side="left"
|
||||
constraints={COMMAND_MENU_CONSTRAINTS}
|
||||
currentWidth={commandMenuWidth}
|
||||
onWidthChange={handleWidthChange}
|
||||
onCollapse={handleCollapse}
|
||||
gapWidth={isCommandMenuOpened ? GAP_WIDTH : 0}
|
||||
cssVariableName={COMMAND_MENU_WIDTH_VAR}
|
||||
onResizeStart={handleResizeStart}
|
||||
/>
|
||||
|
||||
<StyledSidePanelWrapper
|
||||
isOpen={isCommandMenuOpened}
|
||||
isResizing={isResizing}
|
||||
onTransitionEnd={handleTransitionEnd}
|
||||
data-command-menu-panel=""
|
||||
>
|
||||
<StyledSidePanel>
|
||||
<StyledModalContainer ref={handleModalContainerRef} />
|
||||
<ModalContainerContext.Provider value={{ container: modalContainer }}>
|
||||
{shouldShowContent && <CommandMenuRouter />}
|
||||
</ModalContainerContext.Provider>
|
||||
</StyledSidePanel>
|
||||
</StyledSidePanelWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { SidePanelSubPageNavigationHeader } from '@/command-menu/pages/common/components/SidePanelSubPageNavigationHeader';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledSubViewContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledSearchContainer = styled.div`
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
||||
display: flex;
|
||||
height: ${themeCssVariables.spacing[10]};
|
||||
min-width: 0;
|
||||
padding-inline: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
const StyledSearchInput = styled.input`
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
box-sizing: border-box;
|
||||
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: ${themeCssVariables.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>
|
||||
);
|
||||
@@ -1,192 +0,0 @@
|
||||
import { CommandMenuBackButton } from '@/command-menu/components/CommandMenuBackButton';
|
||||
import { CommandMenuPageInfo } from '@/command-menu/components/CommandMenuPageInfo';
|
||||
import { CommandMenuTopBarInputFocusEffect } from '@/command-menu/components/CommandMenuTopBarInputFocusEffect';
|
||||
import { CommandMenuTopBarRightCornerIcon } from '@/command-menu/components/CommandMenuTopBarRightCornerIcon';
|
||||
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
|
||||
import { COMMAND_MENU_SEARCH_BAR_HEIGHT_MOBILE } from '@/command-menu/constants/CommandMenuSearchBarHeightMobile';
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useCommandMenuContextChips } from '@/command-menu/hooks/useCommandMenuContextChips';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useContext, useRef } from 'react';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { IconX } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledInputContainer = styled.div<{ isMobile: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
border: none;
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: ${themeCssVariables.font.size.lg};
|
||||
height: ${({ isMobile }) =>
|
||||
isMobile
|
||||
? COMMAND_MENU_SEARCH_BAR_HEIGHT_MOBILE
|
||||
: COMMAND_MENU_SEARCH_BAR_HEIGHT}px;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
padding: 0 ${themeCssVariables.spacing[2]};
|
||||
gap: ${themeCssVariables.spacing[4]};
|
||||
flex-shrink: 0;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
margin: 0;
|
||||
outline: none;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
|
||||
&::placeholder {
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const CommandMenuTopBar = () => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const [commandMenuSearch, setCommandMenuSearch] = useAtomState(
|
||||
commandMenuSearchState,
|
||||
);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCommandMenuSearch(event.target.value);
|
||||
};
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
|
||||
const commandMenuPage = useAtomStateValue(commandMenuPageState);
|
||||
|
||||
const commandMenuNavigationStack = useAtomStateValue(
|
||||
commandMenuNavigationStackState,
|
||||
);
|
||||
|
||||
const { contextChips } = useCommandMenuContextChips();
|
||||
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { removeFocusItemFromFocusStackById } =
|
||||
useRemoveFocusItemFromFocusStackById();
|
||||
|
||||
const handleInputFocus = () => {
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: SIDE_PANEL_FOCUS_ID,
|
||||
component: {
|
||||
type: FocusComponentType.TEXT_INPUT,
|
||||
instanceId: SIDE_PANEL_FOCUS_ID,
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysConflictingWithKeyboard: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleInputBlur = () => {
|
||||
removeFocusItemFromFocusStackById({
|
||||
focusId: SIDE_PANEL_FOCUS_ID,
|
||||
});
|
||||
};
|
||||
|
||||
const canGoBack = commandMenuNavigationStack.length > 1;
|
||||
|
||||
const shouldShowCloseButton =
|
||||
!isMobile && commandMenuNavigationStack.length === 1;
|
||||
|
||||
const shouldShowBackButton = canGoBack;
|
||||
|
||||
const lastChip = contextChips.at(-1);
|
||||
|
||||
return (
|
||||
<StyledInputContainer isMobile={isMobile}>
|
||||
<StyledContentContainer>
|
||||
<AnimatePresence>
|
||||
{shouldShowBackButton && (
|
||||
<motion.div
|
||||
exit={{ opacity: 0, width: 0 }}
|
||||
transition={{
|
||||
duration: theme.animation.duration.instant,
|
||||
}}
|
||||
>
|
||||
<CommandMenuBackButton />
|
||||
</motion.div>
|
||||
)}
|
||||
{shouldShowCloseButton && (
|
||||
<motion.div
|
||||
exit={{ opacity: 0, width: 0 }}
|
||||
transition={{
|
||||
duration: theme.animation.duration.instant,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
Icon={IconX}
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onClick={closeCommandMenu}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{lastChip &&
|
||||
commandMenuPage !== CommandMenuPages.Root &&
|
||||
commandMenuPage !== CommandMenuPages.SearchRecords && (
|
||||
<CommandMenuPageInfo pageChip={lastChip} />
|
||||
)}
|
||||
{(commandMenuPage === CommandMenuPages.Root ||
|
||||
commandMenuPage === CommandMenuPages.SearchRecords) && (
|
||||
<>
|
||||
<StyledInput
|
||||
data-testid={SIDE_PANEL_FOCUS_ID}
|
||||
ref={inputRef}
|
||||
value={commandMenuSearch}
|
||||
placeholder={t`Type anything...`}
|
||||
onChange={handleSearchChange}
|
||||
onFocus={handleInputFocus}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
<CommandMenuTopBarInputFocusEffect inputRef={inputRef} />
|
||||
</>
|
||||
)}
|
||||
</StyledContentContainer>
|
||||
<CommandMenuTopBarRightCornerIcon />
|
||||
</StyledInputContainer>
|
||||
);
|
||||
};
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useEffect } from 'react';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
|
||||
type CommandMenuTopBarInputFocusEffectProps = {
|
||||
inputRef: React.RefObject<HTMLInputElement>;
|
||||
};
|
||||
|
||||
export const CommandMenuTopBarInputFocusEffect = ({
|
||||
inputRef,
|
||||
}: CommandMenuTopBarInputFocusEffectProps) => {
|
||||
const commandMenuPage = useAtomStateValue(commandMenuPageState);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
commandMenuPage === CommandMenuPages.Root ||
|
||||
commandMenuPage === CommandMenuPages.SearchRecords
|
||||
) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [commandMenuPage, inputRef]);
|
||||
|
||||
return null;
|
||||
};
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { IconEdit, IconSparkles } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
import { useCreateNewAIChatThread } from '@/ai/hooks/useCreateNewAIChatThread';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledIconButton = styled(IconButton)`
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
`;
|
||||
|
||||
export const CommandMenuTopBarRightCornerIcon = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
const commandMenuPage = useAtomStateValue(commandMenuPageState);
|
||||
const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
|
||||
const { createChatThread } = useCreateNewAIChatThread();
|
||||
|
||||
if (isMobile || !isAiEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isOnAskAIPage = [
|
||||
CommandMenuPages.AskAI,
|
||||
CommandMenuPages.ViewPreviousAIChats,
|
||||
].includes(commandMenuPage);
|
||||
|
||||
if (!isOnAskAIPage) {
|
||||
return (
|
||||
<StyledIconButton
|
||||
onClick={() => openAskAIPage({ resetNavigationStack: false })}
|
||||
Icon={IconSparkles}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledIconButton
|
||||
Icon={IconEdit}
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onClick={() => createChatThread()}
|
||||
ariaLabel={t`New conversation`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
COMMAND_MENU_WIDTH_VAR,
|
||||
commandMenuWidthState,
|
||||
} from '@/command-menu/states/commandMenuWidthState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const CommandMenuWidthEffect = () => {
|
||||
const commandMenuWidth = useAtomStateValue(commandMenuWidthState);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty(
|
||||
COMMAND_MENU_WIDTH_VAR,
|
||||
`${commandMenuWidth}px`,
|
||||
);
|
||||
}, [commandMenuWidth]);
|
||||
|
||||
return null;
|
||||
};
|
||||
-195
@@ -1,195 +0,0 @@
|
||||
import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo';
|
||||
import { useCommandMenuWorkflowIdOrThrow } from '@/command-menu/pages/workflow/hooks/useCommandMenuWorkflowIdOrThrow';
|
||||
import { commandMenuWorkflowStepIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowStepIdComponentState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { getAgentIdFromStep } from '@/workflow/utils/getAgentIdFromStep';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
|
||||
import { useUpdateAgentLabel } from '@/workflow/workflow-steps/hooks/useUpdateAgentLabel';
|
||||
import { useUpdateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useContext, useState } from 'react';
|
||||
import { CommandMenuPages, CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { CommandMenuPageInfoLayout } from './CommandMenuPageInfoLayout';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
|
||||
export const CommandMenuWorkflowStepInfo = ({
|
||||
commandMenuPageInstanceId,
|
||||
}: {
|
||||
commandMenuPageInstanceId: string;
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const commandMenuPage = useAtomStateValue(commandMenuPageState);
|
||||
|
||||
const workflowId = useCommandMenuWorkflowIdOrThrow();
|
||||
|
||||
const commandMenuWorkflowStepId = useAtomComponentStateValue(
|
||||
commandMenuWorkflowStepIdComponentState,
|
||||
commandMenuPageInstanceId,
|
||||
);
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
const isReadonly =
|
||||
commandMenuPage === CommandMenuPages.WorkflowStepView ||
|
||||
commandMenuPage === CommandMenuPages.WorkflowRunStepView;
|
||||
|
||||
const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo();
|
||||
|
||||
const instanceId = getWorkflowVisualizerComponentInstanceId({
|
||||
recordId: workflowId,
|
||||
});
|
||||
const { getUpdatableWorkflowVersion } =
|
||||
useGetUpdatableWorkflowVersionOrThrow(instanceId);
|
||||
|
||||
const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep();
|
||||
const { updateOneRecord: updateOneWorkflowVersion } = useUpdateOneRecord();
|
||||
|
||||
const {
|
||||
trigger,
|
||||
steps,
|
||||
id: workflowVersionId,
|
||||
} = workflowWithCurrentVersion?.currentVersion ?? {
|
||||
trigger: null,
|
||||
steps: null,
|
||||
id: undefined,
|
||||
};
|
||||
|
||||
const isTriggerStep = commandMenuWorkflowStepId === TRIGGER_STEP_ID;
|
||||
|
||||
const stepDefinition =
|
||||
isDefined(commandMenuWorkflowStepId) && isDefined(trigger)
|
||||
? isTriggerStep || isDefined(steps)
|
||||
? getStepDefinitionOrThrow({
|
||||
stepId: commandMenuWorkflowStepId,
|
||||
trigger,
|
||||
steps,
|
||||
})
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
const isTrigger = stepDefinition?.type === 'trigger';
|
||||
|
||||
const agentId = getAgentIdFromStep(stepDefinition);
|
||||
const { updateAgentLabel } = useUpdateAgentLabel(agentId);
|
||||
const stepName =
|
||||
isDefined(stepDefinition) && isDefined(stepDefinition.definition)
|
||||
? isTrigger
|
||||
? (stepDefinition.definition.name ??
|
||||
(stepDefinition.definition.type === 'MANUAL'
|
||||
? t`Launch manually`
|
||||
: t`Trigger`))
|
||||
: (stepDefinition.definition.name ?? t`Action`)
|
||||
: '';
|
||||
|
||||
const [editedTitle, setEditedTitle] = useState<string | null>(null);
|
||||
|
||||
const title =
|
||||
editedTitle ?? (isDefined(stepName) && stepName !== '' ? stepName : '');
|
||||
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
setEditedTitle(newTitle);
|
||||
};
|
||||
|
||||
if (
|
||||
!isDefined(workflowId) ||
|
||||
!isDefined(commandMenuWorkflowStepId) ||
|
||||
!isDefined(workflowWithCurrentVersion?.currentVersion) ||
|
||||
!isDefined(stepDefinition) ||
|
||||
!isDefined(stepDefinition.definition)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const headerIcon = isTrigger
|
||||
? getTriggerIcon(stepDefinition.definition)
|
||||
: getActionIcon(stepDefinition.definition.type);
|
||||
|
||||
const headerIconColor = isTrigger
|
||||
? getTriggerIconColor(stepDefinition.definition.type)
|
||||
: getActionIconColorOrThrow(stepDefinition.definition.type);
|
||||
|
||||
const headerType = isTrigger ? t`Trigger` : t`Action`;
|
||||
|
||||
const Icon = getIcon(headerIcon ?? 'IconDefault');
|
||||
|
||||
const saveTitle = async () => {
|
||||
if (!isDefined(workflowVersionId) || !isDefined(workflowId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateCommandMenuPageInfo({
|
||||
pageTitle: title,
|
||||
pageIcon: Icon,
|
||||
});
|
||||
|
||||
const targetWorkflowVersionId = await getUpdatableWorkflowVersion();
|
||||
|
||||
if (isTrigger) {
|
||||
await updateOneWorkflowVersion({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
idToUpdate: targetWorkflowVersionId,
|
||||
updateOneRecordInput: {
|
||||
trigger: {
|
||||
...stepDefinition.definition,
|
||||
name: title,
|
||||
} as typeof stepDefinition.definition,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await updateWorkflowVersionStep({
|
||||
workflowVersionId: targetWorkflowVersionId,
|
||||
step: {
|
||||
...stepDefinition.definition,
|
||||
name: title,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(agentId)) {
|
||||
await updateAgentLabel(title);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandMenuPageInfoLayout
|
||||
icon={
|
||||
headerIcon ? (
|
||||
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||
) : undefined
|
||||
}
|
||||
iconColor={headerIconColor}
|
||||
title={
|
||||
<TitleInput
|
||||
instanceId={`workflow-step-title-${commandMenuPageInstanceId}`}
|
||||
disabled={isReadonly}
|
||||
sizeVariant="sm"
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
placeholder={headerType}
|
||||
onEnter={saveTitle}
|
||||
onEscape={() => setEditedTitle(null)}
|
||||
onClickOutside={saveTitle}
|
||||
onTab={saveTitle}
|
||||
onShiftTab={saveTitle}
|
||||
/>
|
||||
}
|
||||
label={isTrigger ? t`Trigger` : t`Action`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
import { CommandMenuContextRecordsChip } from '@/command-menu/components/CommandMenuContextRecordsChip';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId';
|
||||
import { RESET_CONTEXT_TO_SELECTION } from '@/command-menu/constants/ResetContextToSelection';
|
||||
import { useResetPreviousCommandMenuContext } from '@/command-menu/hooks/useResetPreviousCommandMenuContext';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconArrowBackUp } from 'twenty-ui/display';
|
||||
|
||||
export const ResetContextToSelectionCommandButton = () => {
|
||||
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
'command-menu-previous',
|
||||
);
|
||||
|
||||
const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
'command-menu-previous',
|
||||
);
|
||||
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.id === contextStoreCurrentObjectMetadataItemId,
|
||||
);
|
||||
|
||||
const { resetPreviousCommandMenuContext } =
|
||||
useResetPreviousCommandMenuContext();
|
||||
|
||||
if (
|
||||
!isDefined(objectMetadataItem) ||
|
||||
(contextStoreTargetedRecordsRule.mode === 'selection' &&
|
||||
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectableListItem
|
||||
itemId={RESET_CONTEXT_TO_SELECTION}
|
||||
onEnter={resetPreviousCommandMenuContext}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id={RESET_CONTEXT_TO_SELECTION}
|
||||
Icon={IconArrowBackUp}
|
||||
label={t`Reset to`}
|
||||
RightComponent={
|
||||
<CommandMenuContextRecordsChip
|
||||
objectMetadataItemId={objectMetadataItem.id}
|
||||
instanceId={COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID}
|
||||
/>
|
||||
}
|
||||
onClick={resetPreviousCommandMenuContext}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
};
|
||||
+17
-17
@@ -22,12 +22,12 @@ import { sleep } from '~/utils/sleep';
|
||||
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { type CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { SidePanelRouter } from '@/side-panel/components/SidePanelRouter';
|
||||
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
|
||||
import { type SidePanelRootPage } from '@/side-panel/pages/root/components/SidePanelRootPage';
|
||||
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
|
||||
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
@@ -37,7 +37,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
|
||||
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
import { HttpResponse, graphql } from 'msw';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconDotsVertical } from 'twenty-ui/display';
|
||||
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
|
||||
@@ -47,16 +47,16 @@ const openTimeout = 50;
|
||||
const ContextStoreDecorator: Decorator = (Story) => {
|
||||
return (
|
||||
<RecordComponentInstanceContextsWrapper
|
||||
componentInstanceId={COMMAND_MENU_COMPONENT_INSTANCE_ID}
|
||||
componentInstanceId={SIDE_PANEL_COMPONENT_INSTANCE_ID}
|
||||
>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
|
||||
value={{ instanceId: SIDE_PANEL_COMPONENT_INSTANCE_ID }}
|
||||
>
|
||||
<ViewComponentInstanceContext.Provider
|
||||
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
|
||||
value={{ instanceId: SIDE_PANEL_COMPONENT_INSTANCE_ID }}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
|
||||
value={{ instanceId: SIDE_PANEL_COMPONENT_INSTANCE_ID }}
|
||||
>
|
||||
<JestContextStoreSetter
|
||||
contextStoreCurrentObjectMetadataNameSingular="company"
|
||||
@@ -72,9 +72,9 @@ const ContextStoreDecorator: Decorator = (Story) => {
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<typeof CommandMenu> = {
|
||||
const meta: Meta<typeof SidePanelRootPage> = {
|
||||
title: 'Modules/CommandMenu/CommandMenu',
|
||||
component: CommandMenuRouter,
|
||||
component: SidePanelRouter,
|
||||
decorators: [
|
||||
(Story) => {
|
||||
jotaiStore.set(currentWorkspaceState.atom, mockCurrentWorkspace);
|
||||
@@ -86,10 +86,10 @@ const meta: Meta<typeof CommandMenu> = {
|
||||
currentUserWorkspaceState.atom,
|
||||
mockedUserData.currentUserWorkspace,
|
||||
);
|
||||
jotaiStore.set(isCommandMenuOpenedState.atom, true);
|
||||
jotaiStore.set(commandMenuNavigationStackState.atom, [
|
||||
jotaiStore.set(isSidePanelOpenedState.atom, true);
|
||||
jotaiStore.set(sidePanelNavigationStackState.atom, [
|
||||
{
|
||||
page: CommandMenuPages.Root,
|
||||
page: SidePanelPages.Root,
|
||||
pageTitle: 'Command Menu',
|
||||
pageIcon: IconDotsVertical,
|
||||
pageId: '1',
|
||||
@@ -128,7 +128,7 @@ const meta: Meta<typeof CommandMenu> = {
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CommandMenu>;
|
||||
type Story = StoryObj<typeof SidePanelRootPage>;
|
||||
|
||||
export const DefaultWithoutSearch: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
|
||||
-268
@@ -1,268 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import {
|
||||
type Decorator,
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
} from '@storybook/react-vite';
|
||||
|
||||
import { CommandMenuContextRecordsChip } from '@/command-menu/components/CommandMenuContextRecordsChip';
|
||||
import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext';
|
||||
import { type RecordChipData } from '@/object-record/record-field/ui/types/RecordChipData';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
|
||||
import { mockedCompanyRecords } from '~/testing/mock-data/generated/data/companies/mock-companies-data';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
|
||||
const FIND_MANY_COMPANIES = gql`
|
||||
query FindManyCompanies(
|
||||
$filter: CompanyFilterInput
|
||||
$orderBy: [CompanyOrderByInput]
|
||||
$lastCursor: String
|
||||
$limit: Int
|
||||
) {
|
||||
companies(
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
first: $limit
|
||||
after: $lastCursor
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
accountOwnerId
|
||||
address {
|
||||
addressStreet1
|
||||
addressStreet2
|
||||
addressCity
|
||||
addressState
|
||||
addressCountry
|
||||
addressPostcode
|
||||
addressLat
|
||||
addressLng
|
||||
}
|
||||
annualRecurringRevenue {
|
||||
amountMicros
|
||||
currencyCode
|
||||
}
|
||||
createdAt
|
||||
createdBy {
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
context
|
||||
}
|
||||
deletedAt
|
||||
domainName {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
employees
|
||||
id
|
||||
idealCustomerProfile
|
||||
introVideo {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
linkedinLink {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
name
|
||||
position
|
||||
tagline
|
||||
updatedAt
|
||||
visaSponsorship
|
||||
workPolicy
|
||||
xLink {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
}
|
||||
cursor
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'company',
|
||||
);
|
||||
|
||||
const companyMock = mockedCompanyRecords[0];
|
||||
|
||||
const chipGeneratorPerObjectPerField: Record<
|
||||
string,
|
||||
Record<string, (record: ObjectRecord) => RecordChipData>
|
||||
> = {
|
||||
company: {
|
||||
name: (record: ObjectRecord): RecordChipData => ({
|
||||
recordId: record.id,
|
||||
name: record.name as string,
|
||||
avatarUrl: '',
|
||||
avatarType: 'rounded',
|
||||
isLabelIdentifier: true,
|
||||
objectNameSingular: 'company',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const identifierChipGeneratorPerObject: Record<
|
||||
string,
|
||||
(record: ObjectRecord) => RecordChipData
|
||||
> = {
|
||||
company: chipGeneratorPerObjectPerField.company.name,
|
||||
};
|
||||
|
||||
const ChipGeneratorsDecorator: Decorator = (Story) => (
|
||||
<PreComputedChipGeneratorsContext.Provider
|
||||
value={{
|
||||
chipGeneratorPerObjectPerField,
|
||||
identifierChipGeneratorPerObject,
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</PreComputedChipGeneratorsContext.Provider>
|
||||
);
|
||||
|
||||
const createContextStoreWrapper = ({
|
||||
companies,
|
||||
componentInstanceId,
|
||||
}: {
|
||||
companies: typeof mockedCompanyRecords;
|
||||
componentInstanceId: string;
|
||||
}) => {
|
||||
return getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||
apolloMocks: [
|
||||
{
|
||||
request: {
|
||||
query: FIND_MANY_COMPANIES,
|
||||
variables: {
|
||||
filter: {
|
||||
id: { in: companies.map((company) => company.id) },
|
||||
deletedAt: { is: 'NOT_NULL' },
|
||||
},
|
||||
orderBy: [{ position: 'AscNullsFirst' }],
|
||||
limit: 3,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
companies: {
|
||||
edges: companies.slice(0, 3).map((company, index) => ({
|
||||
node: company,
|
||||
cursor: `cursor-${index + 1}`,
|
||||
})),
|
||||
pageInfo: {
|
||||
hasNextPage: companies.length > 3,
|
||||
hasPreviousPage: false,
|
||||
startCursor: 'cursor-1',
|
||||
endCursor:
|
||||
companies.length > 0
|
||||
? `cursor-${Math.min(companies.length, 3)}`
|
||||
: null,
|
||||
},
|
||||
totalCount: companies.length,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
componentInstanceId,
|
||||
contextStoreCurrentObjectMetadataNameSingular:
|
||||
companyMockObjectMetadataItem?.nameSingular,
|
||||
contextStoreTargetedRecordsRule: {
|
||||
mode: 'selection',
|
||||
selectedRecordIds: companies.map((company) => company.id),
|
||||
},
|
||||
contextStoreNumberOfSelectedRecords: companies.length,
|
||||
onInitializeJotaiStore: () => {
|
||||
for (const company of companies) {
|
||||
jotaiStore.set(
|
||||
recordStoreFamilyState.atomFamily(company.id),
|
||||
company as ObjectRecord,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const ContextStoreDecorator: Decorator = (Story) => {
|
||||
const ContextStoreWrapper = createContextStoreWrapper({
|
||||
companies: [companyMock],
|
||||
componentInstanceId: '1',
|
||||
});
|
||||
|
||||
return (
|
||||
<ContextStoreWrapper>
|
||||
<Story />
|
||||
</ContextStoreWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<typeof CommandMenuContextRecordsChip> = {
|
||||
title: 'Modules/CommandMenu/CommandMenuContextRecordChip',
|
||||
component: CommandMenuContextRecordsChip,
|
||||
decorators: [
|
||||
ContextStoreDecorator,
|
||||
ChipGeneratorsDecorator,
|
||||
ComponentDecorator,
|
||||
],
|
||||
args: {
|
||||
objectMetadataItemId: companyMockObjectMetadataItem?.id,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CommandMenuContextRecordsChip>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithTwoCompanies: Story = {
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const twoCompaniesMock = mockedCompanyRecords.slice(0, 2);
|
||||
const TwoCompaniesWrapper = createContextStoreWrapper({
|
||||
companies: twoCompaniesMock,
|
||||
componentInstanceId: '2',
|
||||
});
|
||||
|
||||
return (
|
||||
<TwoCompaniesWrapper>
|
||||
<Story />
|
||||
</TwoCompaniesWrapper>
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const WithTenCompanies: Story = {
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const tenCompaniesMock = mockedCompanyRecords.slice(0, 10);
|
||||
const TenCompaniesWrapper = createContextStoreWrapper({
|
||||
companies: tenCompaniesMock,
|
||||
componentInstanceId: '3',
|
||||
});
|
||||
|
||||
return (
|
||||
<TenCompaniesWrapper>
|
||||
<Story />
|
||||
</TenCompaniesWrapper>
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
-199
@@ -1,199 +0,0 @@
|
||||
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
|
||||
import { getCurrentGraphTypeFromConfig } from '@/command-menu/pages/page-layout/utils/getCurrentGraphTypeFromConfig';
|
||||
import { isWidgetConfigurationOfTypeGraph } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfTypeGraph';
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconAppWindow,
|
||||
IconFrame,
|
||||
IconList,
|
||||
IconPlus,
|
||||
type IconComponent,
|
||||
} from 'twenty-ui/display';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
import { useContext } from 'react';
|
||||
|
||||
type PageLayoutHeaderInfo = {
|
||||
headerIcon: IconComponent | undefined;
|
||||
headerIconColor: string;
|
||||
headerType: string;
|
||||
title: string;
|
||||
isReadonly: boolean;
|
||||
tab: PageLayoutTab | undefined;
|
||||
widgetInEditMode: PageLayoutWidget | undefined;
|
||||
};
|
||||
|
||||
type UsePageLayoutHeaderInfoParams = {
|
||||
commandMenuPage: CommandMenuPages;
|
||||
draftPageLayout: {
|
||||
tabs: PageLayoutTab[];
|
||||
};
|
||||
pageLayoutEditingWidgetId: string | null | undefined;
|
||||
openTabId: string | null | undefined;
|
||||
editedTitle: string | null | undefined;
|
||||
};
|
||||
|
||||
export const usePageLayoutHeaderInfo = ({
|
||||
commandMenuPage,
|
||||
draftPageLayout,
|
||||
pageLayoutEditingWidgetId,
|
||||
openTabId,
|
||||
editedTitle,
|
||||
}: UsePageLayoutHeaderInfoParams): PageLayoutHeaderInfo | null => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const iconColor = theme.font.color.tertiary;
|
||||
|
||||
switch (commandMenuPage) {
|
||||
case CommandMenuPages.PageLayoutTabSettings: {
|
||||
if (!isDefined(openTabId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tab = draftPageLayout.tabs.find((t) => t.id === openTabId);
|
||||
|
||||
if (!isDefined(tab)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = isDefined(editedTitle)
|
||||
? editedTitle
|
||||
: isDefined(tab.title) && tab.title !== ''
|
||||
? tab.title
|
||||
: '';
|
||||
|
||||
return {
|
||||
headerIcon: IconAppWindow,
|
||||
headerIconColor: iconColor,
|
||||
headerType: t`Tab`,
|
||||
title,
|
||||
isReadonly: false,
|
||||
tab,
|
||||
widgetInEditMode: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
case CommandMenuPages.PageLayoutIframeSettings: {
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const widgetInEditMode = draftPageLayout.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((widget) => widget.id === pageLayoutEditingWidgetId);
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = isDefined(editedTitle)
|
||||
? editedTitle
|
||||
: isDefined(widgetInEditMode.title) && widgetInEditMode.title !== ''
|
||||
? widgetInEditMode.title
|
||||
: '';
|
||||
|
||||
return {
|
||||
headerIcon: IconFrame,
|
||||
headerIconColor: iconColor,
|
||||
headerType: t`iFrame Widget`,
|
||||
title,
|
||||
isReadonly: false,
|
||||
tab: undefined,
|
||||
widgetInEditMode,
|
||||
};
|
||||
}
|
||||
|
||||
case CommandMenuPages.PageLayoutGraphTypeSelect:
|
||||
case CommandMenuPages.PageLayoutGraphFilter: {
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const widgetInEditMode = draftPageLayout.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((widget) => widget.id === pageLayoutEditingWidgetId);
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isWidgetConfigurationOfTypeGraph(widgetInEditMode.configuration)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentGraphType = getCurrentGraphTypeFromConfig(
|
||||
widgetInEditMode.configuration,
|
||||
);
|
||||
const graphTypeInfo = GRAPH_TYPE_INFORMATION[currentGraphType];
|
||||
const graphTypeLabel = t(graphTypeInfo.label);
|
||||
|
||||
const headerType =
|
||||
commandMenuPage === CommandMenuPages.PageLayoutGraphFilter
|
||||
? graphTypeLabel
|
||||
: t`Chart`;
|
||||
|
||||
const title = isDefined(editedTitle)
|
||||
? editedTitle
|
||||
: isDefined(widgetInEditMode.title) && widgetInEditMode.title !== ''
|
||||
? widgetInEditMode.title
|
||||
: '';
|
||||
|
||||
return {
|
||||
headerIcon: graphTypeInfo.icon,
|
||||
headerIconColor: iconColor,
|
||||
headerType,
|
||||
title,
|
||||
isReadonly: commandMenuPage === CommandMenuPages.PageLayoutGraphFilter,
|
||||
tab: undefined,
|
||||
widgetInEditMode,
|
||||
};
|
||||
}
|
||||
|
||||
case CommandMenuPages.PageLayoutFieldsSettings:
|
||||
case CommandMenuPages.PageLayoutFieldsLayout: {
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const widgetInEditMode = draftPageLayout.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((widget) => widget.id === pageLayoutEditingWidgetId);
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = isDefined(editedTitle)
|
||||
? editedTitle
|
||||
: isDefined(widgetInEditMode.title) && widgetInEditMode.title !== ''
|
||||
? widgetInEditMode.title
|
||||
: '';
|
||||
|
||||
return {
|
||||
headerIcon: IconList,
|
||||
headerIconColor: iconColor,
|
||||
headerType: t`Fields Widget`,
|
||||
title,
|
||||
isReadonly: false,
|
||||
tab: undefined,
|
||||
widgetInEditMode,
|
||||
};
|
||||
}
|
||||
|
||||
case CommandMenuPages.PageLayoutWidgetTypeSelect: {
|
||||
return {
|
||||
headerIcon: IconPlus,
|
||||
headerIconColor: iconColor,
|
||||
headerType: '',
|
||||
title: t`New widget`,
|
||||
isReadonly: true,
|
||||
tab: undefined,
|
||||
widgetInEditMode: undefined,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
export const COMMAND_MENU_ANIMATION_VARIANTS = {
|
||||
fullScreen: {
|
||||
x: '0%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
},
|
||||
normal: {
|
||||
x: '0%',
|
||||
width: themeCssVariables.rightDrawerWidth,
|
||||
height: '100%',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
},
|
||||
closed: {
|
||||
x: '100%',
|
||||
width: themeCssVariables.rightDrawerWidth,
|
||||
height: '100%',
|
||||
bottom: '0',
|
||||
top: 'auto',
|
||||
},
|
||||
};
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const COMMAND_MENU_COMPONENT_INSTANCE_ID = 'command-menu';
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
export const COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID =
|
||||
'command-menu-context-chip-groups-dropdown';
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const COMMAND_MENU_LIST_SELECTABLE_LIST_ID = 'command-menu-list';
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
export const COMMAND_MENU_NAVIGATION_HISTORY_DROPDOWN_ID =
|
||||
'command-menu-navigation-history-dropdown';
|
||||
@@ -1,86 +0,0 @@
|
||||
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||
import { CommandMenuAIChatThreadsPage } from '@/command-menu/pages/AIChatThreads/components/CommandMenuAIChatThreadsPage';
|
||||
import { CommandMenuAskAIPage } from '@/command-menu/pages/ask-ai/components/CommandMenuAskAIPage';
|
||||
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';
|
||||
import { CommandMenuPageLayoutGraphFilter } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphFilter';
|
||||
import { CommandMenuPageLayoutIframeSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings';
|
||||
import { CommandMenuPageLayoutTabSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutTabSettings';
|
||||
import { CommandMenuPageLayoutWidgetTypeSelect } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutWidgetTypeSelect';
|
||||
import { CommandMenuMergeRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuMergeRecordPage';
|
||||
import { CommandMenuRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuRecordPage';
|
||||
import { CommandMenuUpdateMultipleRecords } from '@/command-menu/pages/record-page/components/CommandMenuUpdateMultipleRecords';
|
||||
import { CommandMenuEditRichTextPage } from '@/command-menu/pages/rich-text-page/components/CommandMenuEditRichTextPage';
|
||||
import { CommandMenuSearchRecordsPage } from '@/command-menu/pages/search/components/CommandMenuSearchRecordsPage';
|
||||
import { CommandMenuWorkflowCreateStep } from '@/command-menu/pages/workflow/step/create/components/CommandMenuWorkflowCreateStep';
|
||||
import { CommandMenuWorkflowEditStep } from '@/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStep';
|
||||
import { CommandMenuWorkflowEditStepType } from '@/command-menu/pages/workflow/step/edit/components/CommandMenuWorkflowEditStepType';
|
||||
import { CommandMenuWorkflowRunViewStep } from '@/command-menu/pages/workflow/step/view-run/components/CommandMenuWorkflowRunViewStep';
|
||||
import { CommandMenuWorkflowViewStep } from '@/command-menu/pages/workflow/step/view/components/CommandMenuWorkflowViewStep';
|
||||
import { CommandMenuWorkflowSelectTriggerType } from '@/command-menu/pages/workflow/trigger-type/components/CommandMenuWorkflowSelectTriggerType';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
|
||||
export const COMMAND_MENU_PAGES_CONFIG = new Map<
|
||||
CommandMenuPages,
|
||||
React.ReactNode
|
||||
>([
|
||||
[CommandMenuPages.Root, <CommandMenu />],
|
||||
[CommandMenuPages.ViewRecord, <CommandMenuRecordPage />],
|
||||
[CommandMenuPages.MergeRecords, <CommandMenuMergeRecordPage />],
|
||||
[CommandMenuPages.UpdateRecords, <CommandMenuUpdateMultipleRecords />],
|
||||
[CommandMenuPages.ViewEmailThread, <CommandMenuMessageThreadPage />],
|
||||
[CommandMenuPages.ViewCalendarEvent, <CommandMenuCalendarEventPage />],
|
||||
[CommandMenuPages.EditRichText, <CommandMenuEditRichTextPage />],
|
||||
[
|
||||
CommandMenuPages.WorkflowTriggerSelectType,
|
||||
<CommandMenuWorkflowSelectTriggerType />,
|
||||
],
|
||||
[CommandMenuPages.WorkflowStepCreate, <CommandMenuWorkflowCreateStep />],
|
||||
[CommandMenuPages.WorkflowStepEditType, <CommandMenuWorkflowEditStepType />],
|
||||
[CommandMenuPages.WorkflowStepEdit, <CommandMenuWorkflowEditStep />],
|
||||
[CommandMenuPages.WorkflowStepView, <CommandMenuWorkflowViewStep />],
|
||||
[CommandMenuPages.WorkflowRunStepView, <CommandMenuWorkflowRunViewStep />],
|
||||
[CommandMenuPages.SearchRecords, <CommandMenuSearchRecordsPage />],
|
||||
[CommandMenuPages.AskAI, <CommandMenuAskAIPage />],
|
||||
[CommandMenuPages.ViewPreviousAIChats, <CommandMenuAIChatThreadsPage />],
|
||||
[
|
||||
CommandMenuPages.PageLayoutWidgetTypeSelect,
|
||||
<CommandMenuPageLayoutWidgetTypeSelect />,
|
||||
],
|
||||
[
|
||||
CommandMenuPages.PageLayoutGraphTypeSelect,
|
||||
<CommandMenuPageLayoutChartSettings />,
|
||||
],
|
||||
[
|
||||
CommandMenuPages.PageLayoutGraphFilter,
|
||||
<CommandMenuPageLayoutGraphFilter />,
|
||||
],
|
||||
[
|
||||
CommandMenuPages.PageLayoutIframeSettings,
|
||||
<CommandMenuPageLayoutIframeSettings />,
|
||||
],
|
||||
[
|
||||
CommandMenuPages.PageLayoutTabSettings,
|
||||
<CommandMenuPageLayoutTabSettings />,
|
||||
],
|
||||
[
|
||||
CommandMenuPages.PageLayoutFieldsSettings,
|
||||
<CommandMenuPageLayoutFieldsSettings />,
|
||||
],
|
||||
[
|
||||
CommandMenuPages.PageLayoutFieldsLayout,
|
||||
<CommandMenuPageLayoutFieldsLayout />,
|
||||
],
|
||||
[CommandMenuPages.ViewFrontComponent, <CommandMenuFrontComponentPage />],
|
||||
[
|
||||
CommandMenuPages.NavigationMenuItemEdit,
|
||||
<CommandMenuNavigationMenuItemEditPage />,
|
||||
],
|
||||
[CommandMenuPages.NavigationMenuAddItem, <CommandMenuNewSidebarItemPage />],
|
||||
]);
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
export const COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID =
|
||||
'command-menu-previous';
|
||||
@@ -1 +0,0 @@
|
||||
export const COMMAND_MENU_SEARCH_BAR_HEIGHT = 40;
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const COMMAND_MENU_SEARCH_BAR_HEIGHT_MOBILE = 52;
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const COMMAND_MENU_SEARCH_BAR_PADDING = 2;
|
||||
@@ -1 +0,0 @@
|
||||
export const RESET_CONTEXT_TO_SELECTION = 'reset-context-to-selection';
|
||||
@@ -1 +0,0 @@
|
||||
export const SIDE_PANEL_FOCUS_ID = 'command-menu';
|
||||
+30
-40
@@ -3,13 +3,13 @@ import { Provider as JotaiProvider } from 'jotai';
|
||||
import { act } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
|
||||
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
|
||||
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { IconDotsVertical } from 'twenty-ui/display';
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
@@ -26,7 +26,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const commandMenu = useCommandMenu();
|
||||
const commandMenu = useSidePanelMenu();
|
||||
|
||||
return {
|
||||
commandMenu,
|
||||
@@ -39,7 +39,7 @@ const renderHooks = () => {
|
||||
return { result };
|
||||
};
|
||||
|
||||
describe('useCommandMenu', () => {
|
||||
describe('useSidePanelMenu', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -48,88 +48,78 @@ describe('useCommandMenu', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.openCommandMenu();
|
||||
result.current.commandMenu.openSidePanelMenu();
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(isCommandMenuOpenedState.atom)).toBe(true);
|
||||
expect(jotaiStore.get(isSidePanelOpenedState.atom)).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.closeCommandMenu();
|
||||
result.current.commandMenu.closeSidePanelMenu();
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(isCommandMenuOpenedState.atom)).toBe(false);
|
||||
expect(jotaiStore.get(isSidePanelOpenedState.atom)).toBe(false);
|
||||
});
|
||||
|
||||
it('should toggle the command menu', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
expect(jotaiStore.get(isCommandMenuOpenedState.atom)).toBe(false);
|
||||
expect(jotaiStore.get(isSidePanelOpenedState.atom)).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.toggleCommandMenu();
|
||||
result.current.commandMenu.toggleSidePanelMenu();
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(isCommandMenuOpenedState.atom)).toBe(true);
|
||||
expect(jotaiStore.get(isSidePanelOpenedState.atom)).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.toggleCommandMenu();
|
||||
result.current.commandMenu.toggleSidePanelMenu();
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(isCommandMenuOpenedState.atom)).toBe(false);
|
||||
expect(jotaiStore.get(isSidePanelOpenedState.atom)).toBe(false);
|
||||
});
|
||||
|
||||
it('should navigate command menu and reset navigation stack when resetNavigationStack is true', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.Root,
|
||||
result.current.commandMenu.navigateSidePanelMenu({
|
||||
page: SidePanelPages.Root,
|
||||
pageTitle: 'First Page',
|
||||
pageIcon: IconDotsVertical,
|
||||
resetNavigationStack: false,
|
||||
});
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(commandMenuPageState.atom)).toBe(
|
||||
CommandMenuPages.Root,
|
||||
);
|
||||
expect(jotaiStore.get(commandMenuPageInfoState.atom).title).toBe(
|
||||
expect(jotaiStore.get(sidePanelPageState.atom)).toBe(SidePanelPages.Root);
|
||||
expect(jotaiStore.get(sidePanelPageInfoState.atom).title).toBe(
|
||||
'First Page',
|
||||
);
|
||||
expect(jotaiStore.get(commandMenuNavigationStackState.atom)).toHaveLength(
|
||||
1,
|
||||
);
|
||||
expect(jotaiStore.get(sidePanelNavigationStackState.atom)).toHaveLength(1);
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
result.current.commandMenu.navigateSidePanelMenu({
|
||||
page: SidePanelPages.SearchRecords,
|
||||
pageTitle: 'Second Page',
|
||||
pageIcon: IconDotsVertical,
|
||||
resetNavigationStack: false,
|
||||
});
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(commandMenuNavigationStackState.atom)).toHaveLength(
|
||||
2,
|
||||
);
|
||||
expect(jotaiStore.get(sidePanelNavigationStackState.atom)).toHaveLength(2);
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.Root,
|
||||
result.current.commandMenu.navigateSidePanelMenu({
|
||||
page: SidePanelPages.Root,
|
||||
pageTitle: 'Reset Page',
|
||||
pageIcon: IconDotsVertical,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(commandMenuPageState.atom)).toBe(
|
||||
CommandMenuPages.Root,
|
||||
);
|
||||
expect(jotaiStore.get(commandMenuPageInfoState.atom).title).toBe(
|
||||
expect(jotaiStore.get(sidePanelPageState.atom)).toBe(SidePanelPages.Root);
|
||||
expect(jotaiStore.get(sidePanelPageInfoState.atom).title).toBe(
|
||||
'Reset Page',
|
||||
);
|
||||
expect(jotaiStore.get(commandMenuNavigationStackState.atom)).toHaveLength(
|
||||
1,
|
||||
);
|
||||
expect(jotaiStore.get(sidePanelNavigationStackState.atom)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
import { act } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuContextChipGroupsDropdownId';
|
||||
import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId';
|
||||
import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
|
||||
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { IconList } from 'twenty-ui/display';
|
||||
|
||||
const mockCloseDropdown = jest.fn();
|
||||
const mockResetContextStoreStates = jest.fn();
|
||||
const mockResetSelectedItem = jest.fn();
|
||||
const mockEmitSidePanelCloseEvent = jest.fn();
|
||||
|
||||
jest.mock('@/ui/layout/dropdown/hooks/useCloseDropdown', () => ({
|
||||
useCloseDropdown: () => ({
|
||||
closeDropdown: mockCloseDropdown,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@/command-menu/hooks/useResetContextStoreStates', () => ({
|
||||
useResetContextStoreStates: () => ({
|
||||
resetContextStoreStates: mockResetContextStoreStates,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@/ui/layout/selectable-list/hooks/useSelectableList', () => ({
|
||||
useSelectableList: () => ({
|
||||
resetSelectedItem: mockResetSelectedItem,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@/ui/layout/right-drawer/utils/emitSidePanelCloseEvent', () => ({
|
||||
emitSidePanelCloseEvent: () => {
|
||||
mockEmitSidePanelCloseEvent();
|
||||
},
|
||||
}));
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JotaiProvider store={jotaiStore}>
|
||||
<MemoryRouter>{children}</MemoryRouter>
|
||||
</JotaiProvider>
|
||||
);
|
||||
|
||||
describe('useCommandMenuCloseAnimationCompleteCleanup', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const { commandMenuCloseAnimationCompleteCleanup } =
|
||||
useCommandMenuCloseAnimationCompleteCleanup();
|
||||
|
||||
const viewableRecordId = useAtomStateValue(viewableRecordIdState);
|
||||
|
||||
const setViewableRecordId = useSetAtomState(viewableRecordIdState);
|
||||
|
||||
return {
|
||||
commandMenuCloseAnimationCompleteCleanup,
|
||||
viewableRecordId,
|
||||
setViewableRecordId,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
return { result };
|
||||
};
|
||||
|
||||
it('should reset modified states back to default values', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
jotaiStore.set(commandMenuPageState.atom, CommandMenuPages.ViewRecord);
|
||||
jotaiStore.set(commandMenuPageInfoState.atom, {
|
||||
title: 'Test Record',
|
||||
Icon: IconList,
|
||||
instanceId: 'test-id',
|
||||
});
|
||||
jotaiStore.set(isCommandMenuOpenedState.atom, true);
|
||||
jotaiStore.set(commandMenuSearchState.atom, 'test search');
|
||||
jotaiStore.set(commandMenuNavigationStackState.atom, [
|
||||
{
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconList,
|
||||
pageId: '1',
|
||||
},
|
||||
]);
|
||||
jotaiStore.set(hasUserSelectedCommandState.atom, true);
|
||||
jotaiStore.set(isCommandMenuClosingState.atom, true);
|
||||
result.current.setViewableRecordId('record-123');
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(commandMenuPageState.atom)).toBe(
|
||||
CommandMenuPages.ViewRecord,
|
||||
);
|
||||
expect(jotaiStore.get(commandMenuPageInfoState.atom)).toEqual({
|
||||
title: 'Test Record',
|
||||
Icon: IconList,
|
||||
instanceId: 'test-id',
|
||||
});
|
||||
expect(jotaiStore.get(isCommandMenuOpenedState.atom)).toBe(true);
|
||||
expect(jotaiStore.get(commandMenuSearchState.atom)).toBe('test search');
|
||||
expect(jotaiStore.get(commandMenuNavigationStackState.atom)).toEqual([
|
||||
{
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconList,
|
||||
pageId: '1',
|
||||
},
|
||||
]);
|
||||
expect(jotaiStore.get(hasUserSelectedCommandState.atom)).toBe(true);
|
||||
expect(jotaiStore.get(isCommandMenuClosingState.atom)).toBe(true);
|
||||
expect(result.current.viewableRecordId).toBe('record-123');
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenuCloseAnimationCompleteCleanup();
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(commandMenuPageState.atom)).toBe(
|
||||
CommandMenuPages.Root,
|
||||
);
|
||||
expect(jotaiStore.get(commandMenuPageInfoState.atom)).toEqual({
|
||||
title: undefined,
|
||||
Icon: undefined,
|
||||
instanceId: '',
|
||||
});
|
||||
expect(jotaiStore.get(isCommandMenuOpenedState.atom)).toBe(false);
|
||||
expect(jotaiStore.get(commandMenuSearchState.atom)).toBe('');
|
||||
expect(jotaiStore.get(commandMenuNavigationStackState.atom)).toEqual([]);
|
||||
expect(jotaiStore.get(hasUserSelectedCommandState.atom)).toBe(false);
|
||||
expect(jotaiStore.get(isCommandMenuClosingState.atom)).toBe(false);
|
||||
expect(result.current.viewableRecordId).toBe(null);
|
||||
});
|
||||
|
||||
it('should call all dependent functions correctly', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenuCloseAnimationCompleteCleanup();
|
||||
});
|
||||
|
||||
expect(mockCloseDropdown).toHaveBeenCalledTimes(1);
|
||||
expect(mockResetContextStoreStates).toHaveBeenCalledTimes(2);
|
||||
expect(mockResetSelectedItem).toHaveBeenCalledTimes(1);
|
||||
expect(mockEmitSidePanelCloseEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockCloseDropdown).toHaveBeenCalledWith(
|
||||
COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID,
|
||||
);
|
||||
expect(mockResetContextStoreStates).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
expect(mockResetContextStoreStates).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
});
|
||||
});
|
||||
-143
@@ -1,143 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
import { act } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup';
|
||||
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { IconList, IconSearch } from 'twenty-ui/display';
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JotaiProvider store={jotaiStore}>
|
||||
<MemoryRouter>{children}</MemoryRouter>
|
||||
</JotaiProvider>
|
||||
);
|
||||
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const commandMenu = useCommandMenu();
|
||||
const commandMenuHistory = useCommandMenuHistory();
|
||||
const commandMenuCloseAnimationCompleteCleanup =
|
||||
useCommandMenuCloseAnimationCompleteCleanup();
|
||||
|
||||
return {
|
||||
commandMenu,
|
||||
commandMenuHistory,
|
||||
commandMenuCloseAnimationCompleteCleanup,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
return { result };
|
||||
};
|
||||
|
||||
describe('useCommandMenuHistory', () => {
|
||||
it('should go back from a page', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
pageId: '1',
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'Company',
|
||||
pageIcon: IconList,
|
||||
pageId: '2',
|
||||
});
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(commandMenuNavigationStackState.atom)).toEqual([
|
||||
{
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
pageId: '1',
|
||||
},
|
||||
{
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'Company',
|
||||
pageIcon: IconList,
|
||||
pageId: '2',
|
||||
},
|
||||
]);
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenuHistory.goBackFromCommandMenu();
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(commandMenuNavigationStackState.atom)).toEqual([
|
||||
{
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
pageId: '1',
|
||||
},
|
||||
]);
|
||||
expect(jotaiStore.get(commandMenuPageState.atom)).toBe(
|
||||
CommandMenuPages.SearchRecords,
|
||||
);
|
||||
expect(jotaiStore.get(commandMenuPageInfoState.atom)).toEqual({
|
||||
title: 'Search',
|
||||
Icon: IconSearch,
|
||||
instanceId: '1',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenuHistory.goBackFromCommandMenu();
|
||||
result.current.commandMenuCloseAnimationCompleteCleanup.commandMenuCloseAnimationCompleteCleanup();
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(commandMenuNavigationStackState.atom)).toEqual([]);
|
||||
expect(jotaiStore.get(commandMenuPageState.atom)).toBe(
|
||||
CommandMenuPages.Root,
|
||||
);
|
||||
expect(jotaiStore.get(commandMenuPageInfoState.atom)).toEqual({
|
||||
title: undefined,
|
||||
instanceId: '',
|
||||
Icon: undefined,
|
||||
});
|
||||
expect(jotaiStore.get(isCommandMenuOpenedState.atom)).toBe(false);
|
||||
});
|
||||
|
||||
it('should navigate to a page in history', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
pageId: '1',
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenuHistory.navigateCommandMenuHistory(0);
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(commandMenuPageState.atom)).toBe(
|
||||
CommandMenuPages.SearchRecords,
|
||||
);
|
||||
expect(jotaiStore.get(commandMenuPageInfoState.atom)).toEqual({
|
||||
title: 'Search',
|
||||
Icon: IconSearch,
|
||||
instanceId: '1',
|
||||
});
|
||||
});
|
||||
});
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
import { useCommandMenuUpdateNavigationMorphItemsByPage } from '@/command-menu/hooks/useCommandMenuUpdateNavigationMorphItemsByPage';
|
||||
import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
import { act } from 'react';
|
||||
const pageId = 'merge-page-id';
|
||||
const objectMetadataId = 'company-metadata-id';
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JotaiProvider store={jotaiStore}>{children}</JotaiProvider>
|
||||
);
|
||||
|
||||
const renderHooks = (initialRecordIds: string[]) => {
|
||||
jotaiStore.set(
|
||||
commandMenuNavigationMorphItemsByPageState.atom,
|
||||
new Map([
|
||||
[
|
||||
pageId,
|
||||
initialRecordIds.map((recordId) => ({
|
||||
objectMetadataId,
|
||||
recordId,
|
||||
})),
|
||||
],
|
||||
]),
|
||||
);
|
||||
|
||||
return renderHook(
|
||||
() => {
|
||||
const { updateCommandMenuNavigationMorphItemsByPage } =
|
||||
useCommandMenuUpdateNavigationMorphItemsByPage();
|
||||
|
||||
return {
|
||||
updateCommandMenuNavigationMorphItemsByPage,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
describe('useCommandMenuUpdateNavigationMorphItemsByPage', () => {
|
||||
it('should replace existing items for a page instead of appending', async () => {
|
||||
const { result } = renderHooks(['record-1', 'record-2']);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.updateCommandMenuNavigationMorphItemsByPage({
|
||||
pageId,
|
||||
objectMetadataId,
|
||||
objectRecordIds: ['record-2', 'record-1'],
|
||||
});
|
||||
});
|
||||
|
||||
expect(
|
||||
jotaiStore
|
||||
.get(commandMenuNavigationMorphItemsByPageState.atom)
|
||||
.get(pageId),
|
||||
).toEqual([
|
||||
{
|
||||
objectMetadataId,
|
||||
recordId: 'record-2',
|
||||
},
|
||||
{
|
||||
objectMetadataId,
|
||||
recordId: 'record-1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should keep only the latest payload when called twice for the same page', async () => {
|
||||
const { result } = renderHooks([]);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.updateCommandMenuNavigationMorphItemsByPage({
|
||||
pageId,
|
||||
objectMetadataId,
|
||||
objectRecordIds: ['record-1', 'record-2'],
|
||||
});
|
||||
await result.current.updateCommandMenuNavigationMorphItemsByPage({
|
||||
pageId,
|
||||
objectMetadataId,
|
||||
objectRecordIds: ['record-2', 'record-1'],
|
||||
});
|
||||
});
|
||||
|
||||
expect(
|
||||
jotaiStore
|
||||
.get(commandMenuNavigationMorphItemsByPageState.atom)
|
||||
.get(pageId),
|
||||
).toEqual([
|
||||
{
|
||||
objectMetadataId,
|
||||
recordId: 'record-2',
|
||||
},
|
||||
{
|
||||
objectMetadataId,
|
||||
recordId: 'record-1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
import { type ActionConfig } from '@/action-menu/actions/types/ActionConfig';
|
||||
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
|
||||
import { ActionType } from '@/action-menu/actions/types/ActionType';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { useFilterActionsWithCommandMenuSearch } from '@/command-menu/hooks/useFilterActionsWithCommandMenuSearch';
|
||||
|
||||
const MockComponent = <div>Mock Component</div>;
|
||||
|
||||
describe('useFilterActionsWithCommandMenuSearch', () => {
|
||||
const mockActions: ActionConfig[] = [
|
||||
{
|
||||
key: 'action-1',
|
||||
label: 'Create Record',
|
||||
type: ActionType.Standard,
|
||||
scope: ActionScope.Global,
|
||||
position: 1,
|
||||
Icon: IconPlus,
|
||||
shouldBeRegistered: () => true,
|
||||
component: MockComponent,
|
||||
hotKeys: ['c', 'r'],
|
||||
},
|
||||
{
|
||||
key: 'action-2',
|
||||
label: 'Delete Record',
|
||||
type: ActionType.Standard,
|
||||
scope: ActionScope.RecordSelection,
|
||||
position: 2,
|
||||
Icon: IconPlus,
|
||||
shouldBeRegistered: () => true,
|
||||
component: MockComponent,
|
||||
hotKeys: ['d', 'e', 'l'],
|
||||
},
|
||||
{
|
||||
key: 'action-3',
|
||||
label: 'Update Record',
|
||||
type: ActionType.Standard,
|
||||
scope: ActionScope.Object,
|
||||
position: 3,
|
||||
Icon: IconPlus,
|
||||
shouldBeRegistered: () => true,
|
||||
component: MockComponent,
|
||||
},
|
||||
];
|
||||
|
||||
it('should return all actions when search is empty', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFilterActionsWithCommandMenuSearch({ commandMenuSearch: '' }),
|
||||
);
|
||||
|
||||
const filtered =
|
||||
result.current.filterActionsWithCommandMenuSearch(mockActions);
|
||||
|
||||
expect(filtered).toEqual(mockActions);
|
||||
});
|
||||
|
||||
it('should filter actions by label', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFilterActionsWithCommandMenuSearch({ commandMenuSearch: 'Create' }),
|
||||
);
|
||||
|
||||
const filtered =
|
||||
result.current.filterActionsWithCommandMenuSearch(mockActions);
|
||||
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].key).toBe('action-1');
|
||||
});
|
||||
|
||||
it('should filter actions by hotkeys', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFilterActionsWithCommandMenuSearch({ commandMenuSearch: 'del' }),
|
||||
);
|
||||
|
||||
const filtered =
|
||||
result.current.filterActionsWithCommandMenuSearch(mockActions);
|
||||
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].key).toBe('action-2');
|
||||
});
|
||||
|
||||
it('should return empty array when no actions match', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFilterActionsWithCommandMenuSearch({ commandMenuSearch: 'xyz' }),
|
||||
);
|
||||
|
||||
const filtered =
|
||||
result.current.filterActionsWithCommandMenuSearch(mockActions);
|
||||
|
||||
expect(filtered).toEqual([]);
|
||||
});
|
||||
|
||||
it('should match actions by either label or hotkeys', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFilterActionsWithCommandMenuSearch({ commandMenuSearch: 'Record' }),
|
||||
);
|
||||
|
||||
const filtered =
|
||||
result.current.filterActionsWithCommandMenuSearch(mockActions);
|
||||
|
||||
expect(filtered).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { Icon123, useIcons } from 'twenty-ui/display';
|
||||
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('mocked-uuid'),
|
||||
}));
|
||||
|
||||
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'person',
|
||||
)!;
|
||||
|
||||
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||
apolloMocks: [],
|
||||
componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
contextStoreCurrentObjectMetadataNameSingular:
|
||||
personMockObjectMetadataItem.nameSingular,
|
||||
contextStoreCurrentViewId: 'my-view-id',
|
||||
contextStoreTargetedRecordsRule: {
|
||||
mode: 'selection',
|
||||
selectedRecordIds: [],
|
||||
},
|
||||
contextStoreNumberOfSelectedRecords: 0,
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
});
|
||||
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
return {
|
||||
navigateCommandMenu,
|
||||
getIcon,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
return { result };
|
||||
};
|
||||
|
||||
describe('useNavigateCommandMenu', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should navigate to the correct page', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.navigateCommandMenu({
|
||||
page: CommandMenuPages.Root,
|
||||
pageTitle: 'Root',
|
||||
pageIcon: Icon123,
|
||||
pageIconColor: 'red',
|
||||
pageId: 'mocked-uuid',
|
||||
resetNavigationStack: false,
|
||||
});
|
||||
});
|
||||
|
||||
expect(jotaiStore.get(commandMenuPageState.atom)).toBe(
|
||||
CommandMenuPages.Root,
|
||||
);
|
||||
expect(jotaiStore.get(commandMenuNavigationStackState.atom)).toEqual([
|
||||
{
|
||||
page: CommandMenuPages.Root,
|
||||
pageTitle: 'Root',
|
||||
pageIcon: Icon123,
|
||||
pageIconColor: 'red',
|
||||
pageId: 'mocked-uuid',
|
||||
},
|
||||
]);
|
||||
expect(jotaiStore.get(commandMenuPageInfoState.atom)).toEqual({
|
||||
title: 'Root',
|
||||
Icon: Icon123,
|
||||
instanceId: 'mocked-uuid',
|
||||
});
|
||||
});
|
||||
});
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { IconSparkles } from 'twenty-ui/display';
|
||||
|
||||
const navigateCommandMenuMock = jest.fn();
|
||||
|
||||
jest.mock('@/command-menu/hooks/useCommandMenu', () => ({
|
||||
useCommandMenu: () => ({
|
||||
navigateCommandMenu: navigateCommandMenuMock,
|
||||
openCommandMenu: jest.fn(),
|
||||
closeCommandMenu: jest.fn(),
|
||||
toggleCommandMenu: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<JotaiProvider store={jotaiStore}>{children}</JotaiProvider>
|
||||
);
|
||||
|
||||
describe('useOpenAskAIPageInCommandMenu', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jotaiStore.set(isCommandMenuOpenedState.atom, false);
|
||||
});
|
||||
|
||||
it('should navigate to AskAI page with correct defaults', () => {
|
||||
const { result } = renderHook(() => useOpenAskAIPageInCommandMenu(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.openAskAIPage();
|
||||
});
|
||||
|
||||
expect(navigateCommandMenuMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
page: CommandMenuPages.AskAI,
|
||||
pageTitle: 'Ask AI',
|
||||
pageIcon: IconSparkles,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use resetNavigationStack from argument when provided', () => {
|
||||
jotaiStore.set(isCommandMenuOpenedState.atom, true);
|
||||
|
||||
const { result } = renderHook(() => useOpenAskAIPageInCommandMenu(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.openAskAIPage({ resetNavigationStack: false });
|
||||
});
|
||||
|
||||
expect(navigateCommandMenuMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
resetNavigationStack: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should default resetNavigationStack to isCommandMenuOpened', () => {
|
||||
jotaiStore.set(isCommandMenuOpenedState.atom, true);
|
||||
|
||||
const { result } = renderHook(() => useOpenAskAIPageInCommandMenu(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.openAskAIPage();
|
||||
});
|
||||
|
||||
expect(navigateCommandMenuMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
resetNavigationStack: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { useOpenCalendarEventInCommandMenu } from '@/command-menu/hooks/useOpenCalendarEventInCommandMenu';
|
||||
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { IconCalendarEvent } from 'twenty-ui/display';
|
||||
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('mocked-uuid'),
|
||||
}));
|
||||
|
||||
const mockNavigateCommandMenu = jest.fn();
|
||||
jest.mock('@/command-menu/hooks/useNavigateCommandMenu', () => ({
|
||||
useNavigateCommandMenu: () => ({
|
||||
navigateCommandMenu: mockNavigateCommandMenu,
|
||||
}),
|
||||
}));
|
||||
|
||||
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'person',
|
||||
)!;
|
||||
|
||||
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||
apolloMocks: [],
|
||||
componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
contextStoreCurrentObjectMetadataNameSingular:
|
||||
personMockObjectMetadataItem.nameSingular,
|
||||
contextStoreCurrentViewId: 'my-view-id',
|
||||
contextStoreTargetedRecordsRule: {
|
||||
mode: 'selection',
|
||||
selectedRecordIds: [],
|
||||
},
|
||||
contextStoreNumberOfSelectedRecords: 0,
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
});
|
||||
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const { openCalendarEventInCommandMenu } =
|
||||
useOpenCalendarEventInCommandMenu();
|
||||
|
||||
const viewableRecordId = useAtomComponentStateValue(
|
||||
viewableRecordIdComponentState,
|
||||
'mocked-uuid',
|
||||
);
|
||||
|
||||
return {
|
||||
openCalendarEventInCommandMenu,
|
||||
viewableRecordId,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
return { result };
|
||||
};
|
||||
|
||||
describe('useOpenCalendarEventInCommandMenu', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should set the correct states and navigate to the calendar event page', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
const calendarEventId = 'calendar-event-123';
|
||||
|
||||
act(() => {
|
||||
result.current.openCalendarEventInCommandMenu(calendarEventId);
|
||||
});
|
||||
|
||||
expect(result.current.viewableRecordId).toBe(calendarEventId);
|
||||
|
||||
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
|
||||
page: CommandMenuPages.ViewCalendarEvent,
|
||||
pageTitle: 'Calendar Event',
|
||||
pageIcon: IconCalendarEvent,
|
||||
pageId: 'mocked-uuid',
|
||||
});
|
||||
});
|
||||
});
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { useOpenEmailThreadInCommandMenu } from '@/command-menu/hooks/useOpenEmailThreadInCommandMenu';
|
||||
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { IconMail } from 'twenty-ui/display';
|
||||
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('mocked-uuid'),
|
||||
}));
|
||||
|
||||
const mockNavigateCommandMenu = jest.fn();
|
||||
jest.mock('@/command-menu/hooks/useNavigateCommandMenu', () => ({
|
||||
useNavigateCommandMenu: () => ({
|
||||
navigateCommandMenu: mockNavigateCommandMenu,
|
||||
}),
|
||||
}));
|
||||
|
||||
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'person',
|
||||
)!;
|
||||
|
||||
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||
apolloMocks: [],
|
||||
componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
contextStoreCurrentObjectMetadataNameSingular:
|
||||
personMockObjectMetadataItem.nameSingular,
|
||||
contextStoreCurrentViewId: 'my-view-id',
|
||||
contextStoreTargetedRecordsRule: {
|
||||
mode: 'selection',
|
||||
selectedRecordIds: [],
|
||||
},
|
||||
contextStoreNumberOfSelectedRecords: 0,
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
});
|
||||
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const { openEmailThreadInCommandMenu } =
|
||||
useOpenEmailThreadInCommandMenu();
|
||||
|
||||
const viewableRecordId = useAtomComponentStateValue(
|
||||
viewableRecordIdComponentState,
|
||||
'mocked-uuid',
|
||||
);
|
||||
|
||||
return {
|
||||
openEmailThreadInCommandMenu,
|
||||
viewableRecordId,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
return { result };
|
||||
};
|
||||
|
||||
describe('useOpenEmailThreadInCommandMenu', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should set the correct states and navigate to the email thread page', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
const emailThreadId = 'email-thread-123';
|
||||
|
||||
act(() => {
|
||||
result.current.openEmailThreadInCommandMenu(emailThreadId);
|
||||
});
|
||||
|
||||
expect(result.current.viewableRecordId).toBe(emailThreadId);
|
||||
|
||||
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
|
||||
page: CommandMenuPages.ViewEmailThread,
|
||||
pageTitle: 'Email Thread',
|
||||
pageIcon: IconMail,
|
||||
pageId: 'mocked-uuid',
|
||||
});
|
||||
});
|
||||
});
|
||||
-219
@@ -1,219 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
|
||||
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
|
||||
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
|
||||
import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('mocked-uuid'),
|
||||
}));
|
||||
|
||||
const mockNavigateCommandMenu = jest.fn();
|
||||
jest.mock('@/command-menu/hooks/useNavigateCommandMenu', () => ({
|
||||
useNavigateCommandMenu: () => ({
|
||||
navigateCommandMenu: mockNavigateCommandMenu,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockOpenNewRecordTitleCell = jest.fn();
|
||||
jest.mock(
|
||||
'@/object-record/record-title-cell/hooks/useOpenNewRecordTitleCell',
|
||||
() => ({
|
||||
useOpenNewRecordTitleCell: () => ({
|
||||
openNewRecordTitleCell: mockOpenNewRecordTitleCell,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'person',
|
||||
)!;
|
||||
|
||||
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||
apolloMocks: [],
|
||||
componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
contextStoreCurrentObjectMetadataNameSingular:
|
||||
personMockObjectMetadataItem.nameSingular,
|
||||
contextStoreCurrentViewId: 'my-view-id',
|
||||
contextStoreTargetedRecordsRule: {
|
||||
mode: 'selection',
|
||||
selectedRecordIds: [],
|
||||
},
|
||||
contextStoreNumberOfSelectedRecords: 0,
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
});
|
||||
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
|
||||
|
||||
const viewableRecordId = useAtomComponentStateValue(
|
||||
viewableRecordIdComponentState,
|
||||
'mocked-uuid',
|
||||
);
|
||||
const viewableRecordNameSingular = useAtomComponentStateValue(
|
||||
viewableRecordNameSingularComponentState,
|
||||
'mocked-uuid',
|
||||
);
|
||||
const contextStoreCurrentObjectMetadataItemId =
|
||||
useAtomComponentStateValue(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
'mocked-uuid',
|
||||
);
|
||||
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
'mocked-uuid',
|
||||
);
|
||||
const contextStoreNumberOfSelectedRecords = useAtomComponentStateValue(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
'mocked-uuid',
|
||||
);
|
||||
const contextStoreCurrentViewType = useAtomComponentStateValue(
|
||||
contextStoreCurrentViewTypeComponentState,
|
||||
'mocked-uuid',
|
||||
);
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
return {
|
||||
openRecordInCommandMenu,
|
||||
viewableRecordId,
|
||||
viewableRecordNameSingular,
|
||||
contextStoreCurrentObjectMetadataItemId,
|
||||
contextStoreTargetedRecordsRule,
|
||||
contextStoreNumberOfSelectedRecords,
|
||||
contextStoreCurrentViewType,
|
||||
getIcon,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
return { result };
|
||||
};
|
||||
|
||||
describe('useOpenRecordInCommandMenu', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should set the correct states and navigate to the record page', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
const recordId = 'record-123';
|
||||
const objectNameSingular = 'person';
|
||||
|
||||
act(() => {
|
||||
result.current.openRecordInCommandMenu({
|
||||
recordId,
|
||||
objectNameSingular,
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.viewableRecordId).toBe(recordId);
|
||||
expect(result.current.viewableRecordNameSingular).toBe(objectNameSingular);
|
||||
expect(result.current.contextStoreCurrentObjectMetadataItemId).toBe(
|
||||
personMockObjectMetadataItem.id,
|
||||
);
|
||||
expect(result.current.contextStoreTargetedRecordsRule).toEqual({
|
||||
mode: 'selection',
|
||||
selectedRecordIds: [recordId],
|
||||
});
|
||||
expect(result.current.contextStoreNumberOfSelectedRecords).toBe(1);
|
||||
expect(result.current.contextStoreCurrentViewType).toBe(
|
||||
ContextStoreViewType.ShowPage,
|
||||
);
|
||||
|
||||
const commandMenuNavigationMorphItemsByPage = jotaiStore.get(
|
||||
commandMenuNavigationMorphItemsByPageState.atom,
|
||||
);
|
||||
expect(commandMenuNavigationMorphItemsByPage.size).toBe(1);
|
||||
expect(commandMenuNavigationMorphItemsByPage.get('mocked-uuid')).toEqual([
|
||||
{
|
||||
objectMetadataId: personMockObjectMetadataItem.id,
|
||||
recordId,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'Person',
|
||||
pageIcon: result.current.getIcon(personMockObjectMetadataItem.icon),
|
||||
pageIconColor: 'currentColor',
|
||||
pageId: 'mocked-uuid',
|
||||
resetNavigationStack: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the correct page title for a new record', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
const recordId = 'new-record-123';
|
||||
const objectNameSingular = 'person';
|
||||
|
||||
act(() => {
|
||||
result.current.openRecordInCommandMenu({
|
||||
recordId,
|
||||
objectNameSingular,
|
||||
isNewRecord: true,
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: 'New Person',
|
||||
pageIcon: result.current.getIcon(personMockObjectMetadataItem.icon),
|
||||
pageIconColor: 'currentColor',
|
||||
pageId: 'mocked-uuid',
|
||||
resetNavigationStack: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should open title cell in edit mode when isNewRecord is true', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.openRecordInCommandMenu({
|
||||
recordId: 'new-record-123',
|
||||
objectNameSingular: 'person',
|
||||
isNewRecord: true,
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockOpenNewRecordTitleCell).toHaveBeenCalledWith({
|
||||
recordId: 'new-record-123',
|
||||
fieldName: getLabelIdentifierFieldMetadataItem(
|
||||
personMockObjectMetadataItem,
|
||||
)?.name,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not open title cell when isNewRecord is false', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.openRecordInCommandMenu({
|
||||
recordId: 'record-123',
|
||||
objectNameSingular: 'person',
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockOpenNewRecordTitleCell).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
describe('useOpenUpdateMultipleRecordsPageInCommandMenu', () => {
|
||||
it('should work', () => {
|
||||
// const { result } = renderHook(() => useOpenUpdateMultipleRecordsPageInCommandMenu({ contextStoreInstanceId: 'test' }));
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
+23
-25
@@ -1,11 +1,11 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId';
|
||||
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
|
||||
import { SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelPreviousComponentInstanceId';
|
||||
import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
|
||||
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
|
||||
import { hasUserSelectedSidePanelListItemState } from '@/side-panel/states/hasUserSelectedSidePanelListItemState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
@@ -37,7 +37,7 @@ jotaiStore.set(
|
||||
|
||||
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||
apolloMocks: [],
|
||||
componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
componentInstanceId: SIDE_PANEL_COMPONENT_INSTANCE_ID,
|
||||
contextStoreCurrentObjectMetadataNameSingular:
|
||||
personMockObjectMetadataItem.nameSingular,
|
||||
contextStoreCurrentViewId: 'my-view-id',
|
||||
@@ -62,32 +62,32 @@ describe('useSetGlobalCommandMenuContext', () => {
|
||||
|
||||
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
SIDE_PANEL_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const contextStoreNumberOfSelectedRecords = useAtomComponentStateValue(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
SIDE_PANEL_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const contextStoreFilters = useAtomComponentStateValue(
|
||||
contextStoreFiltersComponentState,
|
||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
SIDE_PANEL_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const contextStoreFilterGroups = useAtomComponentStateValue(
|
||||
contextStoreFilterGroupsComponentState,
|
||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
SIDE_PANEL_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const contextStoreAnyFieldFilterValue = useAtomComponentStateValue(
|
||||
contextStoreAnyFieldFilterValueComponentState,
|
||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
SIDE_PANEL_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const contextStoreCurrentViewType = useAtomComponentStateValue(
|
||||
contextStoreCurrentViewTypeComponentState,
|
||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
SIDE_PANEL_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -115,16 +115,16 @@ describe('useSetGlobalCommandMenuContext', () => {
|
||||
expect(result.current.contextStoreCurrentViewType).toBe(
|
||||
ContextStoreViewType.Table,
|
||||
);
|
||||
const commandMenuPageInfo = jotaiStore.get(commandMenuPageInfoState.atom);
|
||||
expect(commandMenuPageInfo).toEqual({
|
||||
const sidePanelPageInfo = jotaiStore.get(sidePanelPageInfoState.atom);
|
||||
expect(sidePanelPageInfo).toEqual({
|
||||
title: undefined,
|
||||
Icon: undefined,
|
||||
instanceId: '',
|
||||
});
|
||||
const hasUserSelectedCommand = jotaiStore.get(
|
||||
hasUserSelectedCommandState.atom,
|
||||
const hasUserSelectedSidePanelListItem = jotaiStore.get(
|
||||
hasUserSelectedSidePanelListItemState.atom,
|
||||
);
|
||||
expect(hasUserSelectedCommand).toBe(false);
|
||||
expect(hasUserSelectedSidePanelListItem).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.setGlobalCommandMenuContext();
|
||||
@@ -140,18 +140,16 @@ describe('useSetGlobalCommandMenuContext', () => {
|
||||
expect(result.current.contextStoreCurrentViewType).toBe(
|
||||
ContextStoreViewType.Table,
|
||||
);
|
||||
const commandMenuPageInfoAfter = jotaiStore.get(
|
||||
commandMenuPageInfoState.atom,
|
||||
);
|
||||
expect(commandMenuPageInfoAfter).toEqual({
|
||||
const sidePanelPageInfoAfter = jotaiStore.get(sidePanelPageInfoState.atom);
|
||||
expect(sidePanelPageInfoAfter).toEqual({
|
||||
title: undefined,
|
||||
Icon: undefined,
|
||||
instanceId: '',
|
||||
});
|
||||
const hasUserSelectedCommandAfter = jotaiStore.get(
|
||||
hasUserSelectedCommandState.atom,
|
||||
const hasUserSelectedSidePanelListItemAfter = jotaiStore.get(
|
||||
hasUserSelectedSidePanelListItemState.atom,
|
||||
);
|
||||
expect(hasUserSelectedCommandAfter).toBe(false);
|
||||
expect(hasUserSelectedSidePanelListItemAfter).toBe(false);
|
||||
});
|
||||
|
||||
it('should copy context store states to previous instance before resetting', () => {
|
||||
@@ -163,13 +161,13 @@ describe('useSetGlobalCommandMenuContext', () => {
|
||||
// eslint-disable-next-line twenty/matching-state-variable
|
||||
const previousTargetedRecordsRule = useAtomComponentStateValue(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID,
|
||||
SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line twenty/matching-state-variable
|
||||
const previousNumberOfSelectedRecords = useAtomComponentStateValue(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID,
|
||||
SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
import { act } from 'react';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { IconArrowDown, IconDotsVertical } from 'twenty-ui/display';
|
||||
|
||||
const mockedPageInfo = {
|
||||
title: 'Initial Title',
|
||||
Icon: IconDotsVertical,
|
||||
instanceId: 'test-instance',
|
||||
};
|
||||
|
||||
const mockedNavigationStack = [
|
||||
{
|
||||
page: CommandMenuPages.Root,
|
||||
pageTitle: 'Initial Title',
|
||||
pageIcon: IconDotsVertical,
|
||||
pageId: 'test-page-id',
|
||||
},
|
||||
];
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JotaiProvider store={jotaiStore}>{children}</JotaiProvider>
|
||||
);
|
||||
|
||||
describe('useUpdateCommandMenuPageInfo', () => {
|
||||
beforeEach(() => {
|
||||
jotaiStore.set(commandMenuNavigationStackState.atom, mockedNavigationStack);
|
||||
jotaiStore.set(commandMenuPageInfoState.atom, mockedPageInfo);
|
||||
});
|
||||
|
||||
const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo();
|
||||
|
||||
return {
|
||||
updateCommandMenuPageInfo,
|
||||
};
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
return {
|
||||
result,
|
||||
};
|
||||
};
|
||||
|
||||
it('should update command menu page info with new title and icon', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.updateCommandMenuPageInfo({
|
||||
pageTitle: 'New Title',
|
||||
pageIcon: IconArrowDown,
|
||||
});
|
||||
});
|
||||
|
||||
const commandMenuNavigationStack = jotaiStore.get(
|
||||
commandMenuNavigationStackState.atom,
|
||||
);
|
||||
expect(commandMenuNavigationStack).toEqual([
|
||||
{
|
||||
page: CommandMenuPages.Root,
|
||||
pageTitle: 'New Title',
|
||||
pageIcon: IconArrowDown,
|
||||
pageId: 'test-page-id',
|
||||
},
|
||||
]);
|
||||
|
||||
const commandMenuPageInfo = jotaiStore.get(commandMenuPageInfoState.atom);
|
||||
expect(commandMenuPageInfo).toEqual({
|
||||
title: 'New Title',
|
||||
Icon: IconArrowDown,
|
||||
instanceId: 'test-instance',
|
||||
});
|
||||
});
|
||||
|
||||
it('should update command menu page info with new title', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.updateCommandMenuPageInfo({
|
||||
pageTitle: 'New Title',
|
||||
});
|
||||
});
|
||||
|
||||
const commandMenuNavigationStack = jotaiStore.get(
|
||||
commandMenuNavigationStackState.atom,
|
||||
);
|
||||
expect(commandMenuNavigationStack).toEqual([
|
||||
{
|
||||
page: CommandMenuPages.Root,
|
||||
pageTitle: 'New Title',
|
||||
pageIcon: IconDotsVertical,
|
||||
pageId: 'test-page-id',
|
||||
},
|
||||
]);
|
||||
|
||||
const commandMenuPageInfo = jotaiStore.get(commandMenuPageInfoState.atom);
|
||||
expect(commandMenuPageInfo).toEqual({
|
||||
title: 'New Title',
|
||||
Icon: IconDotsVertical,
|
||||
instanceId: 'test-instance',
|
||||
});
|
||||
});
|
||||
|
||||
it('should update command menu page info with new icon', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.updateCommandMenuPageInfo({
|
||||
pageIcon: IconArrowDown,
|
||||
});
|
||||
});
|
||||
|
||||
const commandMenuNavigationStack = jotaiStore.get(
|
||||
commandMenuNavigationStackState.atom,
|
||||
);
|
||||
expect(commandMenuNavigationStack).toEqual([
|
||||
{
|
||||
page: CommandMenuPages.Root,
|
||||
pageTitle: 'Initial Title',
|
||||
pageIcon: IconArrowDown,
|
||||
pageId: 'test-page-id',
|
||||
},
|
||||
]);
|
||||
|
||||
const commandMenuPageInfo = jotaiStore.get(commandMenuPageInfoState.atom);
|
||||
expect(commandMenuPageInfo).toEqual({
|
||||
title: 'Initial Title',
|
||||
Icon: IconArrowDown,
|
||||
instanceId: 'test-instance',
|
||||
});
|
||||
});
|
||||
});
|
||||
+44
-44
@@ -1,10 +1,10 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
||||
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
|
||||
import { commandMenuWorkflowIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowIdComponentState';
|
||||
import { commandMenuWorkflowVersionIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowVersionIdComponentState';
|
||||
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
|
||||
import { useSidePanelWorkflowNavigation } from '@/side-panel/pages/workflow/hooks/useSidePanelWorkflowNavigation';
|
||||
import { viewableRecordNameSingularComponentState } from '@/side-panel/pages/record-page/states/viewableRecordNameSingularComponentState';
|
||||
import { sidePanelWorkflowIdComponentState } from '@/side-panel/pages/workflow/states/sidePanelWorkflowIdComponentState';
|
||||
import { sidePanelWorkflowVersionIdComponentState } from '@/side-panel/pages/workflow/states/sidePanelWorkflowVersionIdComponentState';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
@@ -13,7 +13,7 @@ import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { act } from 'react';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { IconBolt, IconSettingsAutomation, useIcons } from 'twenty-ui/display';
|
||||
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
@@ -23,9 +23,9 @@ jest.mock('uuid', () => ({
|
||||
}));
|
||||
|
||||
const mockNavigateCommandMenu = jest.fn();
|
||||
jest.mock('@/command-menu/hooks/useNavigateCommandMenu', () => ({
|
||||
useNavigateCommandMenu: () => ({
|
||||
navigateCommandMenu: mockNavigateCommandMenu,
|
||||
jest.mock('@/side-panel/hooks/useNavigateSidePanel', () => ({
|
||||
useNavigateSidePanel: () => ({
|
||||
navigateSidePanel: mockNavigateCommandMenu,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -41,7 +41,7 @@ jest.mock('@/object-metadata/hooks/useObjectMetadataItem', () => ({
|
||||
|
||||
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||
apolloMocks: [],
|
||||
componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
componentInstanceId: SIDE_PANEL_COMPONENT_INSTANCE_ID,
|
||||
contextStoreCurrentObjectMetadataNameSingular:
|
||||
workflowMockObjectMetadataItem.nameSingular,
|
||||
contextStoreCurrentViewId: 'my-view-id',
|
||||
@@ -57,12 +57,12 @@ const renderHooks = () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const {
|
||||
openWorkflowTriggerTypeInCommandMenu,
|
||||
openWorkflowCreateStepInCommandMenu,
|
||||
openWorkflowEditStepInCommandMenu,
|
||||
openWorkflowEditStepTypeInCommandMenu,
|
||||
openWorkflowViewStepInCommandMenu,
|
||||
} = useWorkflowCommandMenu();
|
||||
openWorkflowTriggerTypeInSidePanel,
|
||||
openWorkflowCreateStepInSidePanel,
|
||||
openWorkflowEditStepInSidePanel,
|
||||
openWorkflowEditStepTypeInSidePanel,
|
||||
openWorkflowViewStepInSidePanel,
|
||||
} = useSidePanelWorkflowNavigation();
|
||||
|
||||
const viewableRecordNameSingular = useAtomComponentStateValue(
|
||||
viewableRecordNameSingularComponentState,
|
||||
@@ -85,24 +85,24 @@ const renderHooks = () => {
|
||||
contextStoreCurrentViewTypeComponentState,
|
||||
'mocked-uuid',
|
||||
);
|
||||
const commandMenuWorkflowId = useAtomComponentStateValue(
|
||||
commandMenuWorkflowIdComponentState,
|
||||
const sidePanelWorkflowId = useAtomComponentStateValue(
|
||||
sidePanelWorkflowIdComponentState,
|
||||
'mocked-uuid',
|
||||
);
|
||||
const commandMenuWorkflowVersionId = useAtomComponentStateValue(
|
||||
commandMenuWorkflowVersionIdComponentState,
|
||||
const sidePanelWorkflowVersionId = useAtomComponentStateValue(
|
||||
sidePanelWorkflowVersionIdComponentState,
|
||||
'mocked-uuid',
|
||||
);
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
return {
|
||||
openWorkflowTriggerTypeInCommandMenu,
|
||||
openWorkflowCreateStepInCommandMenu,
|
||||
openWorkflowEditStepInCommandMenu,
|
||||
openWorkflowEditStepTypeInCommandMenu,
|
||||
openWorkflowViewStepInCommandMenu,
|
||||
commandMenuWorkflowId,
|
||||
commandMenuWorkflowVersionId,
|
||||
openWorkflowTriggerTypeInSidePanel,
|
||||
openWorkflowCreateStepInSidePanel,
|
||||
openWorkflowEditStepInSidePanel,
|
||||
openWorkflowEditStepTypeInSidePanel,
|
||||
openWorkflowViewStepInSidePanel,
|
||||
sidePanelWorkflowId,
|
||||
sidePanelWorkflowVersionId,
|
||||
viewableRecordNameSingular,
|
||||
contextStoreCurrentObjectMetadataItemId,
|
||||
contextStoreTargetedRecordsRule,
|
||||
@@ -118,7 +118,7 @@ const renderHooks = () => {
|
||||
return { result };
|
||||
};
|
||||
|
||||
describe('useWorkflowCommandMenu', () => {
|
||||
describe('useSidePanelWorkflowNavigation', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -127,13 +127,13 @@ describe('useWorkflowCommandMenu', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.openWorkflowTriggerTypeInCommandMenu('test-workflow-id');
|
||||
result.current.openWorkflowTriggerTypeInSidePanel('test-workflow-id');
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuWorkflowId).toBe('test-workflow-id');
|
||||
expect(result.current.sidePanelWorkflowId).toBe('test-workflow-id');
|
||||
|
||||
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
|
||||
page: CommandMenuPages.WorkflowTriggerSelectType,
|
||||
page: SidePanelPages.WorkflowTriggerSelectType,
|
||||
pageTitle: t`Trigger Type`,
|
||||
pageIcon: IconBolt,
|
||||
pageId: 'mocked-uuid',
|
||||
@@ -144,13 +144,13 @@ describe('useWorkflowCommandMenu', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.openWorkflowCreateStepInCommandMenu('test-workflow-id');
|
||||
result.current.openWorkflowCreateStepInSidePanel('test-workflow-id');
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuWorkflowId).toBe('test-workflow-id');
|
||||
expect(result.current.sidePanelWorkflowId).toBe('test-workflow-id');
|
||||
|
||||
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
|
||||
page: CommandMenuPages.WorkflowStepCreate,
|
||||
page: SidePanelPages.WorkflowStepCreate,
|
||||
pageTitle: t`Select Action`,
|
||||
pageIcon: IconSettingsAutomation,
|
||||
pageId: 'mocked-uuid',
|
||||
@@ -161,13 +161,13 @@ describe('useWorkflowCommandMenu', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.openWorkflowEditStepTypeInCommandMenu('test-workflow-id');
|
||||
result.current.openWorkflowEditStepTypeInSidePanel('test-workflow-id');
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuWorkflowId).toBe('test-workflow-id');
|
||||
expect(result.current.sidePanelWorkflowId).toBe('test-workflow-id');
|
||||
|
||||
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
|
||||
page: CommandMenuPages.WorkflowStepEditType,
|
||||
page: SidePanelPages.WorkflowStepEditType,
|
||||
pageTitle: t`Select action`,
|
||||
pageIcon: IconSettingsAutomation,
|
||||
pageId: 'mocked-uuid',
|
||||
@@ -178,17 +178,17 @@ describe('useWorkflowCommandMenu', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.openWorkflowEditStepInCommandMenu(
|
||||
result.current.openWorkflowEditStepInSidePanel(
|
||||
'test-workflow-id',
|
||||
'Edit Step',
|
||||
IconSettingsAutomation,
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuWorkflowId).toBe('test-workflow-id');
|
||||
expect(result.current.sidePanelWorkflowId).toBe('test-workflow-id');
|
||||
|
||||
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
|
||||
page: CommandMenuPages.WorkflowStepEdit,
|
||||
page: SidePanelPages.WorkflowStepEdit,
|
||||
pageTitle: 'Edit Step',
|
||||
pageIcon: IconSettingsAutomation,
|
||||
pageId: 'mocked-uuid',
|
||||
@@ -199,7 +199,7 @@ describe('useWorkflowCommandMenu', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.openWorkflowViewStepInCommandMenu({
|
||||
result.current.openWorkflowViewStepInSidePanel({
|
||||
workflowId: 'test-workflow-id',
|
||||
workflowVersionId: 'test-workflow-version-id',
|
||||
icon: IconSettingsAutomation,
|
||||
@@ -207,13 +207,13 @@ describe('useWorkflowCommandMenu', () => {
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuWorkflowId).toBe('test-workflow-id');
|
||||
expect(result.current.commandMenuWorkflowVersionId).toBe(
|
||||
expect(result.current.sidePanelWorkflowId).toBe('test-workflow-id');
|
||||
expect(result.current.sidePanelWorkflowVersionId).toBe(
|
||||
'test-workflow-version-id',
|
||||
);
|
||||
|
||||
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
|
||||
page: CommandMenuPages.WorkflowStepView,
|
||||
page: SidePanelPages.WorkflowStepView,
|
||||
pageTitle: 'View Step',
|
||||
pageIcon: IconSettingsAutomation,
|
||||
pageId: 'mocked-uuid',
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
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 { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
|
||||
import { emitSidePanelOpenEvent } from '@/ui/layout/right-drawer/utils/emitSidePanelOpenEvent';
|
||||
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconColumnInsertRight, IconDotsVertical } from 'twenty-ui/display';
|
||||
|
||||
export const useCommandMenu = () => {
|
||||
const store = useStore();
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
const { closeAnyOpenDropdown } = useCloseAnyOpenDropdown();
|
||||
|
||||
const { removeFocusItemFromFocusStackById } =
|
||||
useRemoveFocusItemFromFocusStackById();
|
||||
|
||||
const closeCommandMenu = useCallback(() => {
|
||||
const isCommandMenuOpened = store.get(isCommandMenuOpenedState.atom);
|
||||
|
||||
if (isCommandMenuOpened) {
|
||||
store.set(addToNavPayloadRegistryState.atom, new Map());
|
||||
store.set(isCommandMenuOpenedState.atom, false);
|
||||
store.set(isCommandMenuClosingState.atom, true);
|
||||
closeAnyOpenDropdown();
|
||||
removeFocusItemFromFocusStackById({
|
||||
focusId: SIDE_PANEL_FOCUS_ID,
|
||||
});
|
||||
}
|
||||
}, [closeAnyOpenDropdown, removeFocusItemFromFocusStackById, store]);
|
||||
|
||||
const openCommandMenu = useCallback(() => {
|
||||
emitSidePanelOpenEvent();
|
||||
closeAnyOpenDropdown();
|
||||
const isNavigationMenuInEditMode = store.get(
|
||||
isNavigationMenuInEditModeState.atom,
|
||||
);
|
||||
const selectedNavigationItemId = store.get(
|
||||
selectedNavigationMenuItemInEditModeState.atom,
|
||||
);
|
||||
if (isNavigationMenuInEditMode && isDefined(selectedNavigationItemId)) {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.NavigationMenuItemEdit,
|
||||
pageTitle: t`Edit`,
|
||||
pageIcon: IconDotsVertical,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
} else if (isNavigationMenuInEditMode) {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.NavigationMenuAddItem,
|
||||
pageTitle: t`New sidebar item`,
|
||||
pageIcon: IconColumnInsertRight,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
} else {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.Root,
|
||||
pageTitle: t`Command Menu`,
|
||||
pageIcon: IconDotsVertical,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
}
|
||||
}, [closeAnyOpenDropdown, navigateCommandMenu, store]);
|
||||
|
||||
const toggleCommandMenu = useCallback(() => {
|
||||
const isCommandMenuOpened = store.get(isCommandMenuOpenedState.atom);
|
||||
|
||||
store.set(commandMenuSearchState.atom, '');
|
||||
|
||||
if (isCommandMenuOpened) {
|
||||
closeCommandMenu();
|
||||
} else {
|
||||
openCommandMenu();
|
||||
}
|
||||
}, [closeCommandMenu, openCommandMenu, store]);
|
||||
|
||||
return {
|
||||
openCommandMenu,
|
||||
closeCommandMenu,
|
||||
navigateCommandMenu,
|
||||
toggleCommandMenu,
|
||||
};
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
import { type ActionConfig } from '@/action-menu/actions/types/ActionConfig';
|
||||
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
|
||||
import { ActionType } from '@/action-menu/actions/types/ActionType';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const useCommandMenuActions = () => {
|
||||
const { actions } = useContext(ActionMenuContext);
|
||||
|
||||
const navigateActions = actions?.filter(
|
||||
(action) => action.type === ActionType.Navigation,
|
||||
);
|
||||
|
||||
const actionRecordSelectionActions: ActionConfig[] = actions?.filter(
|
||||
(action) =>
|
||||
action.type === ActionType.Standard &&
|
||||
action.scope === ActionScope.RecordSelection,
|
||||
);
|
||||
|
||||
const actionObjectActions: ActionConfig[] = actions?.filter(
|
||||
(action) =>
|
||||
action.type === ActionType.Standard &&
|
||||
action.scope === ActionScope.Object,
|
||||
);
|
||||
|
||||
const actionGlobalActions: ActionConfig[] = actions?.filter(
|
||||
(action) =>
|
||||
action.type === ActionType.Standard &&
|
||||
action.scope === ActionScope.Global,
|
||||
);
|
||||
|
||||
const workflowRunRecordSelectionActions: ActionConfig[] = actions?.filter(
|
||||
(action) =>
|
||||
action.type === ActionType.WorkflowRun &&
|
||||
action.scope === ActionScope.RecordSelection,
|
||||
);
|
||||
|
||||
const workflowRunGlobalActions: ActionConfig[] = actions?.filter(
|
||||
(action) =>
|
||||
action.type === ActionType.WorkflowRun &&
|
||||
action.scope === ActionScope.Global,
|
||||
);
|
||||
|
||||
const frontComponentGlobalActions: ActionConfig[] = actions?.filter(
|
||||
(action) =>
|
||||
action.type === ActionType.FrontComponent &&
|
||||
action.scope === ActionScope.Global,
|
||||
);
|
||||
|
||||
const frontComponentRecordSelectionActions: ActionConfig[] = actions?.filter(
|
||||
(action) =>
|
||||
action.type === ActionType.FrontComponent &&
|
||||
action.scope === ActionScope.RecordSelection,
|
||||
);
|
||||
|
||||
const fallbackActions: ActionConfig[] = actions?.filter(
|
||||
(action) => action.type === ActionType.Fallback,
|
||||
);
|
||||
|
||||
const createRelatedRecordActions: ActionConfig[] = actions?.filter(
|
||||
(action) =>
|
||||
action.type === ActionType.Standard &&
|
||||
action.scope === ActionScope.CreateRelatedRecord,
|
||||
);
|
||||
|
||||
return {
|
||||
navigateActions,
|
||||
actionRecordSelectionActions,
|
||||
actionGlobalActions,
|
||||
actionObjectActions,
|
||||
workflowRunRecordSelectionActions,
|
||||
workflowRunGlobalActions,
|
||||
frontComponentGlobalActions,
|
||||
frontComponentRecordSelectionActions,
|
||||
fallbackActions,
|
||||
createRelatedRecordActions,
|
||||
};
|
||||
};
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuContextChipGroupsDropdownId';
|
||||
import { COMMAND_MENU_LIST_SELECTABLE_LIST_ID } from '@/command-menu/constants/CommandMenuListSelectableListId';
|
||||
import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId';
|
||||
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
|
||||
import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
|
||||
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { emitSidePanelCloseEvent } from '@/ui/layout/right-drawer/utils/emitSidePanelCloseEvent';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { WORKFLOW_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowLogicFunctionTabListComponentId';
|
||||
import { WorkflowLogicFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowLogicFunctionTabId';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useCommandMenuCloseAnimationCompleteCleanup = () => {
|
||||
const store = useStore();
|
||||
const { resetSelectedItem } = useSelectableList(
|
||||
COMMAND_MENU_LIST_SELECTABLE_LIST_ID,
|
||||
);
|
||||
|
||||
const { resetContextStoreStates } = useResetContextStoreStates();
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const commandMenuCloseAnimationCompleteCleanup = useCallback(
|
||||
(options?: { emitSidePanelCloseEvent?: boolean }) => {
|
||||
closeDropdown(COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID);
|
||||
|
||||
// Snapshot values before any mutations (Jotai store.get is live and
|
||||
// reflects the latest state, so we capture before mutating).
|
||||
const currentPage = store.get(commandMenuPageState.atom);
|
||||
const targetedRecordsRule = store.get(
|
||||
contextStoreTargetedRecordsRuleComponentState.atomFamily({
|
||||
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
}),
|
||||
);
|
||||
const morphItemsByPage = store.get(
|
||||
commandMenuNavigationMorphItemsByPageState.atom,
|
||||
);
|
||||
|
||||
resetContextStoreStates(COMMAND_MENU_COMPONENT_INSTANCE_ID);
|
||||
resetContextStoreStates(COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID);
|
||||
|
||||
const isPageLayoutEditingPage =
|
||||
currentPage === CommandMenuPages.PageLayoutWidgetTypeSelect ||
|
||||
currentPage === CommandMenuPages.PageLayoutGraphTypeSelect ||
|
||||
currentPage === CommandMenuPages.PageLayoutIframeSettings ||
|
||||
currentPage === CommandMenuPages.PageLayoutTabSettings;
|
||||
|
||||
if (isPageLayoutEditingPage) {
|
||||
if (
|
||||
targetedRecordsRule.mode === 'selection' &&
|
||||
targetedRecordsRule.selectedRecordIds.length === 1
|
||||
) {
|
||||
const recordId = targetedRecordsRule.selectedRecordIds[0];
|
||||
const record = store.get(recordStoreFamilyState.atomFamily(recordId));
|
||||
|
||||
if (isDefined(record) && isDefined(record.pageLayoutId)) {
|
||||
store.set(
|
||||
pageLayoutEditingWidgetIdComponentState.atomFamily({
|
||||
instanceId: record.pageLayoutId,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
store.set(
|
||||
pageLayoutTabSettingsOpenTabIdComponentState.atomFamily({
|
||||
instanceId: record.pageLayoutId,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
store.set(
|
||||
pageLayoutDraggedAreaComponentState.atomFamily({
|
||||
instanceId: record.pageLayoutId,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
store.set(viewableRecordIdState.atom, null);
|
||||
store.set(commandMenuPageState.atom, CommandMenuPages.Root);
|
||||
store.set(commandMenuPageInfoState.atom, {
|
||||
title: undefined,
|
||||
Icon: undefined,
|
||||
instanceId: '',
|
||||
});
|
||||
store.set(isCommandMenuOpenedState.atom, false);
|
||||
store.set(commandMenuSearchState.atom, '');
|
||||
store.set(commandMenuNavigationMorphItemsByPageState.atom, new Map());
|
||||
store.set(commandMenuNavigationStackState.atom, []);
|
||||
resetSelectedItem();
|
||||
store.set(hasUserSelectedCommandState.atom, false);
|
||||
|
||||
if (options?.emitSidePanelCloseEvent !== false) {
|
||||
emitSidePanelCloseEvent();
|
||||
}
|
||||
store.set(isCommandMenuClosingState.atom, false);
|
||||
store.set(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: WORKFLOW_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID,
|
||||
}),
|
||||
WorkflowLogicFunctionTabId.CODE,
|
||||
);
|
||||
|
||||
for (const [pageId, morphItems] of morphItemsByPage) {
|
||||
store.set(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: getShowPageTabListComponentId({
|
||||
pageId,
|
||||
targetObjectId: morphItems[0].recordId,
|
||||
}),
|
||||
}),
|
||||
null,
|
||||
);
|
||||
}
|
||||
},
|
||||
[closeDropdown, resetContextStoreStates, resetSelectedItem, store],
|
||||
);
|
||||
|
||||
return {
|
||||
commandMenuCloseAnimationCompleteCleanup,
|
||||
};
|
||||
};
|
||||
@@ -1,150 +0,0 @@
|
||||
import { CommandMenuContextChipIconWrapper } from '@/command-menu/components/CommandMenuContextChipIconWrapper';
|
||||
import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars';
|
||||
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
|
||||
import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { recordStoreIdentifiersFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifiersSelector';
|
||||
import { recordStoreRecordsSelector } from '@/object-record/record-store/states/selectors/recordStoreRecordsSelector';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
export const useCommandMenuContextChips = () => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const iconSizeSm = theme.icon.size.sm;
|
||||
const commandMenuNavigationStack = useAtomStateValue(
|
||||
commandMenuNavigationStackState,
|
||||
);
|
||||
|
||||
const allowRequestsToTwentyIcons = useAtomStateValue(
|
||||
allowRequestsToTwentyIconsState,
|
||||
);
|
||||
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
|
||||
const { navigateCommandMenuHistory } = useCommandMenuHistory();
|
||||
|
||||
const commandMenuNavigationMorphItemsByPage = useAtomStateValue(
|
||||
commandMenuNavigationMorphItemsByPageState,
|
||||
);
|
||||
|
||||
const allRecordIds = Array.from(
|
||||
commandMenuNavigationMorphItemsByPage.entries(),
|
||||
).flatMap(([, morphItems]) =>
|
||||
morphItems.map((morphItem) => morphItem.recordId),
|
||||
);
|
||||
|
||||
const recordIdentifiers = useAtomFamilySelectorValue(
|
||||
recordStoreIdentifiersFamilySelector,
|
||||
{
|
||||
recordIds: allRecordIds,
|
||||
allowRequestsToTwentyIcons,
|
||||
},
|
||||
);
|
||||
const records = useAtomFamilySelectorValue(recordStoreRecordsSelector, {
|
||||
recordIds: allRecordIds,
|
||||
});
|
||||
|
||||
const contextChips = useMemo(() => {
|
||||
const filteredCommandMenuNavigationStack =
|
||||
commandMenuNavigationStack.filter(
|
||||
(page) => page.page !== CommandMenuPages.Root,
|
||||
);
|
||||
|
||||
return filteredCommandMenuNavigationStack
|
||||
.map((page, index) => {
|
||||
const isLastChip =
|
||||
index === filteredCommandMenuNavigationStack.length - 1;
|
||||
|
||||
const isRecordPage = page.page === CommandMenuPages.ViewRecord;
|
||||
|
||||
if (isRecordPage && !isLastChip) {
|
||||
const commandMenuNavigationMorphItem =
|
||||
commandMenuNavigationMorphItemsByPage.get(page.pageId)?.[0];
|
||||
|
||||
if (!isDefined(commandMenuNavigationMorphItem?.recordId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) =>
|
||||
item.id === commandMenuNavigationMorphItem.objectMetadataId,
|
||||
);
|
||||
|
||||
const recordIdentifier = recordIdentifiers.find(
|
||||
(recordIdentifier) =>
|
||||
recordIdentifier.id === commandMenuNavigationMorphItem.recordId,
|
||||
);
|
||||
|
||||
const record = records.find(
|
||||
(record) => record.id === commandMenuNavigationMorphItem.recordId,
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(objectMetadataItem) ||
|
||||
!isDefined(recordIdentifier) ||
|
||||
!isDefined(record)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
Icons: [
|
||||
<CommandMenuContextRecordChipAvatars
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
record={record}
|
||||
/>,
|
||||
],
|
||||
text: recordIdentifier.name,
|
||||
onClick: () => {
|
||||
navigateCommandMenuHistory(index);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
Icons: isLastChip
|
||||
? [<page.pageIcon size={iconSizeSm} />]
|
||||
: [
|
||||
<CommandMenuContextChipIconWrapper>
|
||||
<page.pageIcon
|
||||
size={iconSizeSm}
|
||||
color={
|
||||
isDefined(page.pageIconColor) &&
|
||||
page.pageIconColor !== 'currentColor'
|
||||
? page.pageIconColor
|
||||
: themeCssVariables.font.color.tertiary
|
||||
}
|
||||
/>
|
||||
</CommandMenuContextChipIconWrapper>,
|
||||
],
|
||||
text: page.pageTitle,
|
||||
onClick: isLastChip
|
||||
? undefined
|
||||
: () => {
|
||||
navigateCommandMenuHistory(index);
|
||||
},
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
}, [
|
||||
commandMenuNavigationMorphItemsByPage,
|
||||
commandMenuNavigationStack,
|
||||
iconSizeSm,
|
||||
navigateCommandMenuHistory,
|
||||
objectMetadataItems,
|
||||
recordIdentifiers,
|
||||
records,
|
||||
]);
|
||||
|
||||
return {
|
||||
contextChips,
|
||||
};
|
||||
};
|
||||
@@ -1,134 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
|
||||
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useCommandMenuHistory = () => {
|
||||
const store = useStore();
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
|
||||
const goBackFromCommandMenu = useCallback(() => {
|
||||
const currentNavigationStack = store.get(
|
||||
commandMenuNavigationStackState.atom,
|
||||
);
|
||||
|
||||
const newNavigationStack = currentNavigationStack.slice(0, -1);
|
||||
const lastNavigationStackItem = newNavigationStack.at(-1);
|
||||
|
||||
if (!isDefined(lastNavigationStackItem)) {
|
||||
closeCommandMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
store.set(commandMenuPageState.atom, lastNavigationStackItem.page);
|
||||
|
||||
store.set(commandMenuPageInfoState.atom, {
|
||||
title: lastNavigationStackItem.pageTitle,
|
||||
Icon: lastNavigationStackItem.pageIcon,
|
||||
instanceId: lastNavigationStackItem.pageId,
|
||||
});
|
||||
|
||||
store.set(commandMenuNavigationStackState.atom, newNavigationStack);
|
||||
|
||||
const currentMorphItems = store.get(
|
||||
commandMenuNavigationMorphItemsByPageState.atom,
|
||||
);
|
||||
|
||||
if (currentNavigationStack.length > 0) {
|
||||
const removedItem = currentNavigationStack.at(-1);
|
||||
|
||||
if (isDefined(removedItem)) {
|
||||
const newMorphItems = new Map(currentMorphItems);
|
||||
newMorphItems.delete(removedItem.pageId);
|
||||
store.set(
|
||||
commandMenuNavigationMorphItemsByPageState.atom,
|
||||
newMorphItems,
|
||||
);
|
||||
|
||||
const morphItems = currentMorphItems.get(removedItem.pageId);
|
||||
if (isNonEmptyArray(morphItems)) {
|
||||
store.set(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: getShowPageTabListComponentId({
|
||||
pageId: removedItem.pageId,
|
||||
targetObjectId: morphItems[0].recordId,
|
||||
}),
|
||||
}),
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
store.set(hasUserSelectedCommandState.atom, false);
|
||||
}, [closeCommandMenu, store]);
|
||||
|
||||
const navigateCommandMenuHistory = useCallback(
|
||||
(pageIndex: number) => {
|
||||
const currentNavigationStack = store.get(
|
||||
commandMenuNavigationStackState.atom,
|
||||
);
|
||||
|
||||
const newNavigationStack = currentNavigationStack.slice(0, pageIndex + 1);
|
||||
|
||||
store.set(commandMenuNavigationStackState.atom, newNavigationStack);
|
||||
|
||||
const newNavigationStackItem = newNavigationStack.at(-1);
|
||||
|
||||
if (!isDefined(newNavigationStackItem)) {
|
||||
throw new Error(
|
||||
`No command menu navigation stack item found for index ${pageIndex}`,
|
||||
);
|
||||
}
|
||||
|
||||
store.set(commandMenuPageState.atom, newNavigationStackItem.page);
|
||||
store.set(commandMenuPageInfoState.atom, {
|
||||
title: newNavigationStackItem.pageTitle,
|
||||
Icon: newNavigationStackItem.pageIcon,
|
||||
instanceId: newNavigationStackItem.pageId,
|
||||
});
|
||||
const currentMorphItems = store.get(
|
||||
commandMenuNavigationMorphItemsByPageState.atom,
|
||||
);
|
||||
|
||||
for (const [pageId, morphItems] of currentMorphItems.entries()) {
|
||||
if (!newNavigationStack.some((item) => item.pageId === pageId)) {
|
||||
store.set(
|
||||
activeTabIdComponentState.atomFamily({
|
||||
instanceId: getShowPageTabListComponentId({
|
||||
pageId,
|
||||
targetObjectId: morphItems[0].recordId,
|
||||
}),
|
||||
}),
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const newMorphItems = new Map(
|
||||
Array.from(currentMorphItems.entries()).filter(([pageId]) =>
|
||||
newNavigationStack.some((item) => item.pageId === pageId),
|
||||
),
|
||||
);
|
||||
|
||||
store.set(commandMenuNavigationMorphItemsByPageState.atom, newMorphItems);
|
||||
|
||||
store.set(hasUserSelectedCommandState.atom, false);
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return {
|
||||
goBackFromCommandMenu,
|
||||
navigateCommandMenuHistory,
|
||||
};
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
|
||||
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
|
||||
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
|
||||
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { useSidePanelHistory } from '@/side-panel/hooks/useSidePanelHistory';
|
||||
import { useOpenAskAIPageInSidePanel } from '@/side-panel/hooks/useOpenAskAIPageInSidePanel';
|
||||
import { useOpenRecordsSearchPageInSidePanel } from '@/side-panel/hooks/useOpenRecordsSearchPageInSidePanel';
|
||||
import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
|
||||
import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys';
|
||||
@@ -16,41 +16,41 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useCommandMenuHotKeys = () => {
|
||||
const { toggleCommandMenu } = useCommandMenu();
|
||||
const { toggleSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu();
|
||||
const { openRecordsSearchPage } = useOpenRecordsSearchPageInSidePanel();
|
||||
|
||||
const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
|
||||
const { openAskAIPage } = useOpenAskAIPageInSidePanel();
|
||||
|
||||
const { goBackFromCommandMenu } = useCommandMenuHistory();
|
||||
const { goBackFromSidePanel } = useSidePanelHistory();
|
||||
|
||||
const { setGlobalCommandMenuContext } = useSetGlobalCommandMenuContext();
|
||||
|
||||
const commandMenuSearch = useAtomStateValue(commandMenuSearchState);
|
||||
const sidePanelSearch = useAtomStateValue(sidePanelSearchState);
|
||||
|
||||
const { closeKeyboardShortcutMenu } = useKeyboardShortcutMenu();
|
||||
|
||||
const commandMenuPage = useAtomStateValue(commandMenuPageState);
|
||||
const sidePanelPage = useAtomStateValue(sidePanelPageState);
|
||||
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
|
||||
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
SIDE_PANEL_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
useGlobalHotkeys({
|
||||
keys: ['ctrl+k', 'meta+k'],
|
||||
callback: () => {
|
||||
closeKeyboardShortcutMenu();
|
||||
toggleCommandMenu();
|
||||
toggleSidePanelMenu();
|
||||
},
|
||||
containsModifier: true,
|
||||
dependencies: [closeKeyboardShortcutMenu, toggleCommandMenu],
|
||||
dependencies: [closeKeyboardShortcutMenu, toggleSidePanelMenu],
|
||||
});
|
||||
|
||||
useGlobalHotkeys({
|
||||
@@ -82,10 +82,10 @@ export const useCommandMenuHotKeys = () => {
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: [Key.Escape],
|
||||
callback: () => {
|
||||
goBackFromCommandMenu();
|
||||
goBackFromSidePanel();
|
||||
},
|
||||
focusId: SIDE_PANEL_FOCUS_ID,
|
||||
dependencies: [goBackFromCommandMenu],
|
||||
dependencies: [goBackFromSidePanel],
|
||||
options: {
|
||||
enableOnFormTags: false,
|
||||
},
|
||||
@@ -94,12 +94,12 @@ export const useCommandMenuHotKeys = () => {
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: [Key.Backspace, Key.Delete],
|
||||
callback: () => {
|
||||
if (isNonEmptyString(commandMenuSearch)) {
|
||||
if (isNonEmptyString(sidePanelSearch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
commandMenuPage === CommandMenuPages.Root &&
|
||||
sidePanelPage === SidePanelPages.Root &&
|
||||
!(
|
||||
contextStoreTargetedRecordsRule.mode === 'selection' &&
|
||||
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0
|
||||
@@ -107,16 +107,16 @@ export const useCommandMenuHotKeys = () => {
|
||||
) {
|
||||
setGlobalCommandMenuContext();
|
||||
}
|
||||
if (commandMenuPage !== CommandMenuPages.Root) {
|
||||
goBackFromCommandMenu();
|
||||
if (sidePanelPage !== SidePanelPages.Root) {
|
||||
goBackFromSidePanel();
|
||||
}
|
||||
},
|
||||
focusId: SIDE_PANEL_FOCUS_ID,
|
||||
dependencies: [
|
||||
commandMenuPage,
|
||||
commandMenuSearch,
|
||||
sidePanelPage,
|
||||
sidePanelSearch,
|
||||
contextStoreTargetedRecordsRule,
|
||||
goBackFromCommandMenu,
|
||||
goBackFromSidePanel,
|
||||
setGlobalCommandMenuContext,
|
||||
],
|
||||
options: {
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import { Action } from '@/action-menu/actions/components/Action';
|
||||
import { ActionLink } from '@/action-menu/actions/components/ActionLink';
|
||||
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
|
||||
import { ActionType } from '@/action-menu/actions/types/ActionType';
|
||||
import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults';
|
||||
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { CoreObjectNameSingular, AppPath } from 'twenty-shared/types';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useMemo } from 'react';
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { useSearchQuery } from '~/generated/graphql';
|
||||
|
||||
export const useCommandMenuSearchRecords = () => {
|
||||
const commandMenuSearch = useAtomStateValue(commandMenuSearchState);
|
||||
const coreClient = useApolloCoreClient();
|
||||
|
||||
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300);
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const nonReadableObjectMetadataItemsNameSingular = useMemo(() => {
|
||||
return Object.values(objectMetadataItems)
|
||||
.filter((objectMetadataItem) => {
|
||||
const objectPermission = getObjectPermissionsFromMapByObjectMetadataId({
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
});
|
||||
|
||||
return !objectPermission?.canReadObjectRecords;
|
||||
})
|
||||
.map((objectMetadataItem) => objectMetadataItem.nameSingular);
|
||||
}, [objectMetadataItems, objectPermissionsByObjectMetadataId]);
|
||||
|
||||
const { data: searchData, loading } = useSearchQuery({
|
||||
client: coreClient,
|
||||
variables: {
|
||||
searchInput: deferredCommandMenuSearch ?? '',
|
||||
limit: MAX_SEARCH_RESULTS,
|
||||
excludedObjectNameSingulars: [
|
||||
'workspaceMember',
|
||||
...nonReadableObjectMetadataItemsNameSingular,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
|
||||
|
||||
const actionItems = useMemo(() => {
|
||||
return (searchData?.search.edges.map((edge) => edge.node) ?? []).map(
|
||||
(searchRecord, index) => {
|
||||
const baseAction = {
|
||||
type: ActionType.Navigation,
|
||||
scope: ActionScope.Global,
|
||||
key: searchRecord.recordId,
|
||||
label: searchRecord.label,
|
||||
position: index,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type={
|
||||
searchRecord.objectNameSingular ===
|
||||
CoreObjectNameSingular.Company
|
||||
? 'squared'
|
||||
: 'rounded'
|
||||
}
|
||||
avatarUrl={searchRecord.imageUrl}
|
||||
placeholderColorSeed={searchRecord.recordId}
|
||||
placeholder={searchRecord.label}
|
||||
/>
|
||||
),
|
||||
shouldBeRegistered: () => true,
|
||||
description:
|
||||
objectMetadataItems.find(
|
||||
(item) => item.nameSingular === searchRecord.objectNameSingular,
|
||||
)?.labelSingular ?? searchRecord.objectNameSingular,
|
||||
};
|
||||
|
||||
if (
|
||||
[CoreObjectNameSingular.Task, CoreObjectNameSingular.Note].includes(
|
||||
searchRecord.objectNameSingular as CoreObjectNameSingular,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
...baseAction,
|
||||
component: (
|
||||
<Action
|
||||
onClick={() => {
|
||||
searchRecord.objectNameSingular === 'task'
|
||||
? openRecordInCommandMenu({
|
||||
recordId: searchRecord.recordId,
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
})
|
||||
: openRecordInCommandMenu({
|
||||
recordId: searchRecord.recordId,
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
});
|
||||
}}
|
||||
closeSidePanelOnCommandMenuListActionExecution={false}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...baseAction,
|
||||
component: (
|
||||
<ActionLink
|
||||
to={AppPath.RecordShowPage}
|
||||
params={{
|
||||
objectNameSingular: searchRecord.objectNameSingular,
|
||||
objectRecordId: searchRecord.recordId,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
}, [searchData, openRecordInCommandMenu, objectMetadataItems]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
noResults: !actionItems?.length,
|
||||
commandGroups: [
|
||||
{
|
||||
heading: t`Results`,
|
||||
items: actionItems,
|
||||
},
|
||||
],
|
||||
hasMore: false,
|
||||
pageSize: 0,
|
||||
onLoadMore: () => {},
|
||||
};
|
||||
};
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState';
|
||||
import { useCallback } from 'react';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
type UpdateNavigationMorphItemsByPageParams = {
|
||||
pageId: string;
|
||||
objectMetadataId: string;
|
||||
objectRecordIds: string[];
|
||||
};
|
||||
|
||||
export const useCommandMenuUpdateNavigationMorphItemsByPage = () => {
|
||||
const store = useStore();
|
||||
const updateCommandMenuNavigationMorphItemsByPage = useCallback(
|
||||
async ({
|
||||
pageId,
|
||||
objectMetadataId,
|
||||
objectRecordIds,
|
||||
}: UpdateNavigationMorphItemsByPageParams) => {
|
||||
const currentMorphItems = store.get(
|
||||
commandMenuNavigationMorphItemsByPageState.atom,
|
||||
);
|
||||
|
||||
const newMorphItems = objectRecordIds.map((recordId) => ({
|
||||
objectMetadataId,
|
||||
recordId,
|
||||
}));
|
||||
|
||||
const newMorphItemsMap = new Map(currentMorphItems);
|
||||
newMorphItemsMap.set(pageId, newMorphItems);
|
||||
store.set(
|
||||
commandMenuNavigationMorphItemsByPageState.atom,
|
||||
newMorphItemsMap,
|
||||
);
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return {
|
||||
updateCommandMenuNavigationMorphItemsByPage,
|
||||
};
|
||||
};
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import { type ActionConfig } from '@/action-menu/actions/types/ActionConfig';
|
||||
import { getActionLabel } from '@/action-menu/utils/getActionLabel';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useCallback } from 'react';
|
||||
import { normalizeSearchText } from '~/utils/normalizeSearchText';
|
||||
|
||||
const checkInShortcuts = (action: ActionConfig, search: string) => {
|
||||
const concatenatedString = action.hotKeys?.join('') ?? '';
|
||||
const searchNormalized = normalizeSearchText(search.trim());
|
||||
return normalizeSearchText(concatenatedString).includes(searchNormalized);
|
||||
};
|
||||
|
||||
const checkInLabels = (action: ActionConfig, search: string) => {
|
||||
const actionLabel = getActionLabel(action.label);
|
||||
if (isNonEmptyString(actionLabel)) {
|
||||
const searchNormalized = normalizeSearchText(search);
|
||||
return normalizeSearchText(actionLabel).includes(searchNormalized);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
type UseFilterActionsWithCommandMenuSearchProps = {
|
||||
commandMenuSearch: string;
|
||||
};
|
||||
|
||||
export const useFilterActionsWithCommandMenuSearch = ({
|
||||
commandMenuSearch,
|
||||
}: UseFilterActionsWithCommandMenuSearchProps) => {
|
||||
const filterActionsWithCommandMenuSearch = useCallback(
|
||||
(actions: ActionConfig[]) => {
|
||||
return actions.filter((action) =>
|
||||
commandMenuSearch.length > 0
|
||||
? checkInShortcuts(action, commandMenuSearch) ||
|
||||
checkInLabels(action, commandMenuSearch)
|
||||
: true,
|
||||
);
|
||||
},
|
||||
[commandMenuSearch],
|
||||
);
|
||||
|
||||
return {
|
||||
filterActionsWithCommandMenuSearch,
|
||||
};
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
import { useCommandMenuActions } from '@/command-menu/hooks/useCommandMenuActions';
|
||||
import { useFilterActionsWithCommandMenuSearch } from '@/command-menu/hooks/useFilterActionsWithCommandMenuSearch';
|
||||
|
||||
export const useMatchingCommandMenuActions = ({
|
||||
commandMenuSearch,
|
||||
}: {
|
||||
commandMenuSearch: string;
|
||||
}) => {
|
||||
const { filterActionsWithCommandMenuSearch } =
|
||||
useFilterActionsWithCommandMenuSearch({
|
||||
commandMenuSearch,
|
||||
});
|
||||
|
||||
const {
|
||||
navigateActions,
|
||||
actionRecordSelectionActions,
|
||||
actionObjectActions,
|
||||
actionGlobalActions,
|
||||
workflowRunRecordSelectionActions,
|
||||
workflowRunGlobalActions,
|
||||
frontComponentGlobalActions,
|
||||
frontComponentRecordSelectionActions,
|
||||
fallbackActions,
|
||||
createRelatedRecordActions,
|
||||
} = useCommandMenuActions();
|
||||
|
||||
const matchingNavigateActions =
|
||||
filterActionsWithCommandMenuSearch(navigateActions);
|
||||
|
||||
const matchingStandardActionRecordSelectionActions =
|
||||
filterActionsWithCommandMenuSearch(actionRecordSelectionActions);
|
||||
|
||||
const matchingStandardActionObjectActions =
|
||||
filterActionsWithCommandMenuSearch(actionObjectActions);
|
||||
|
||||
const matchingStandardActionGlobalActions =
|
||||
filterActionsWithCommandMenuSearch(actionGlobalActions);
|
||||
|
||||
const matchingWorkflowRunRecordSelectionActions =
|
||||
filterActionsWithCommandMenuSearch(workflowRunRecordSelectionActions);
|
||||
|
||||
const matchingWorkflowRunGlobalActions = filterActionsWithCommandMenuSearch(
|
||||
workflowRunGlobalActions,
|
||||
);
|
||||
|
||||
const matchingFrontComponentGlobalActions =
|
||||
filterActionsWithCommandMenuSearch(frontComponentGlobalActions);
|
||||
|
||||
const matchingFrontComponentRecordSelectionActions =
|
||||
filterActionsWithCommandMenuSearch(frontComponentRecordSelectionActions);
|
||||
|
||||
const matchingCreateRelatedRecordActions = filterActionsWithCommandMenuSearch(
|
||||
createRelatedRecordActions,
|
||||
);
|
||||
|
||||
const noResults =
|
||||
!matchingStandardActionRecordSelectionActions.length &&
|
||||
!matchingWorkflowRunRecordSelectionActions.length &&
|
||||
!matchingFrontComponentRecordSelectionActions.length &&
|
||||
!matchingStandardActionGlobalActions.length &&
|
||||
!matchingWorkflowRunGlobalActions.length &&
|
||||
!matchingFrontComponentGlobalActions.length &&
|
||||
!matchingStandardActionObjectActions.length &&
|
||||
!matchingNavigateActions.length &&
|
||||
!matchingCreateRelatedRecordActions.length;
|
||||
|
||||
return {
|
||||
noResults,
|
||||
matchingStandardActionRecordSelectionActions,
|
||||
matchingStandardActionObjectActions,
|
||||
matchingWorkflowRunRecordSelectionActions,
|
||||
matchingFrontComponentRecordSelectionActions,
|
||||
matchingStandardActionGlobalActions,
|
||||
matchingWorkflowRunGlobalActions,
|
||||
matchingFrontComponentGlobalActions,
|
||||
matchingNavigateActions,
|
||||
matchingCreateRelatedRecordActions,
|
||||
fallbackActions: noResults ? fallbackActions : [],
|
||||
};
|
||||
};
|
||||
@@ -1,148 +0,0 @@
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
|
||||
import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup';
|
||||
import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates';
|
||||
import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState';
|
||||
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
|
||||
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { type CommandMenuPages } from 'twenty-shared/types';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export type CommandMenuNavigationStackItem = {
|
||||
page: CommandMenuPages;
|
||||
pageTitle: string;
|
||||
pageIcon: IconComponent;
|
||||
pageIconColor?: string;
|
||||
pageId?: string;
|
||||
};
|
||||
|
||||
export const useNavigateCommandMenu = () => {
|
||||
const store = useStore();
|
||||
const { copyContextStoreStates } = useCopyContextStoreStates();
|
||||
|
||||
const { commandMenuCloseAnimationCompleteCleanup } =
|
||||
useCommandMenuCloseAnimationCompleteCleanup();
|
||||
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
const openCommandMenu = useCallback(() => {
|
||||
const isCommandMenuOpened = store.get(isCommandMenuOpenedState.atom);
|
||||
|
||||
const isCommandMenuClosing = store.get(isCommandMenuClosingState.atom);
|
||||
|
||||
if (isCommandMenuClosing) {
|
||||
commandMenuCloseAnimationCompleteCleanup({
|
||||
emitSidePanelCloseEvent: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (isCommandMenuOpened) {
|
||||
return;
|
||||
}
|
||||
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: SIDE_PANEL_FOCUS_ID,
|
||||
component: {
|
||||
type: FocusComponentType.SIDE_PANEL,
|
||||
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysConflictingWithKeyboard: false,
|
||||
},
|
||||
});
|
||||
|
||||
copyContextStoreStates({
|
||||
instanceIdToCopyFrom: MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||
instanceIdToCopyTo: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
});
|
||||
|
||||
store.set(isCommandMenuOpenedState.atom, true);
|
||||
store.set(hasUserSelectedCommandState.atom, false);
|
||||
}, [
|
||||
copyContextStoreStates,
|
||||
commandMenuCloseAnimationCompleteCleanup,
|
||||
pushFocusItemToFocusStack,
|
||||
store,
|
||||
]);
|
||||
|
||||
const navigateCommandMenu = useCallback(
|
||||
({
|
||||
page,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
pageIconColor,
|
||||
pageId,
|
||||
focusTitleInput = false,
|
||||
resetNavigationStack = false,
|
||||
}: CommandMenuNavigationStackItem & {
|
||||
resetNavigationStack?: boolean;
|
||||
focusTitleInput?: boolean;
|
||||
}) => {
|
||||
const computedPageId = pageId || v4();
|
||||
|
||||
openCommandMenu();
|
||||
store.set(commandMenuPageState.atom, page);
|
||||
store.set(commandMenuPageInfoState.atom, {
|
||||
title: pageTitle,
|
||||
Icon: pageIcon,
|
||||
instanceId: computedPageId,
|
||||
});
|
||||
|
||||
if (focusTitleInput) {
|
||||
store.set(
|
||||
commandMenuShouldFocusTitleInputComponentState.atomFamily({
|
||||
instanceId: computedPageId,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
const isCommandMenuClosing = store.get(isCommandMenuClosingState.atom);
|
||||
|
||||
const currentNavigationStack = isCommandMenuClosing
|
||||
? []
|
||||
: store.get(commandMenuNavigationStackState.atom);
|
||||
|
||||
if (resetNavigationStack) {
|
||||
store.set(commandMenuNavigationStackState.atom, [
|
||||
{
|
||||
page,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
pageIconColor,
|
||||
pageId: computedPageId,
|
||||
},
|
||||
]);
|
||||
|
||||
store.set(commandMenuNavigationMorphItemsByPageState.atom, new Map());
|
||||
} else {
|
||||
store.set(commandMenuNavigationStackState.atom, [
|
||||
...currentNavigationStack,
|
||||
{
|
||||
page,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
pageIconColor,
|
||||
pageId: computedPageId,
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
[openCommandMenu, store],
|
||||
);
|
||||
|
||||
return {
|
||||
navigateCommandMenu,
|
||||
};
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback } from 'react';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { IconSparkles } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const useOpenAskAIPageInCommandMenu = () => {
|
||||
const { navigateCommandMenu } = useCommandMenu();
|
||||
const isCommandMenuOpened = useAtomStateValue(isCommandMenuOpenedState);
|
||||
|
||||
const openAskAIPage = useCallback(
|
||||
({
|
||||
resetNavigationStack,
|
||||
}: {
|
||||
resetNavigationStack?: boolean;
|
||||
} = {}) => {
|
||||
const shouldReset =
|
||||
resetNavigationStack !== undefined
|
||||
? resetNavigationStack
|
||||
: isCommandMenuOpened;
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.AskAI,
|
||||
pageTitle: t`Ask AI`,
|
||||
pageIcon: IconSparkles,
|
||||
pageId: v4(),
|
||||
resetNavigationStack: shouldReset,
|
||||
});
|
||||
},
|
||||
[navigateCommandMenu, isCommandMenuOpened],
|
||||
);
|
||||
|
||||
return {
|
||||
openAskAIPage,
|
||||
};
|
||||
};
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback } from 'react';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { IconCalendarEvent } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useOpenCalendarEventInCommandMenu = () => {
|
||||
const store = useStore();
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
|
||||
const openCalendarEventInCommandMenu = useCallback(
|
||||
(calendarEventId: string) => {
|
||||
const pageComponentInstanceId = v4();
|
||||
|
||||
store.set(
|
||||
viewableRecordIdComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
calendarEventId,
|
||||
);
|
||||
|
||||
// TODO: Uncomment this once we need to calendar event title in the navigation
|
||||
// const objectMetadataItem = snapshot
|
||||
// .getLoadable(objectMetadataItemsState)
|
||||
// .getValue()
|
||||
// .find(
|
||||
// ({ nameSingular }) =>
|
||||
// nameSingular === CoreObjectNameSingular.CalendarEvent,
|
||||
// );
|
||||
|
||||
// set(
|
||||
// commandMenuNavigationMorphItemsState,
|
||||
// new Map([
|
||||
// ...snapshot
|
||||
// .getLoadable(commandMenuNavigationMorphItemsState)
|
||||
// .getValue(),
|
||||
// [
|
||||
// pageComponentInstanceId,
|
||||
// {
|
||||
// objectMetadataId: objectMetadataItem?.id,
|
||||
// recordId: calendarEventId,
|
||||
// },
|
||||
// ],
|
||||
// ]),
|
||||
// );
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewCalendarEvent,
|
||||
pageTitle: t`Calendar Event`,
|
||||
pageIcon: IconCalendarEvent,
|
||||
pageId: pageComponentInstanceId,
|
||||
});
|
||||
},
|
||||
[navigateCommandMenu, store],
|
||||
);
|
||||
|
||||
return {
|
||||
openCalendarEventInCommandMenu,
|
||||
};
|
||||
};
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback } from 'react';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { IconMail } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useOpenEmailThreadInCommandMenu = () => {
|
||||
const store = useStore();
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
|
||||
const openEmailThreadInCommandMenu = useCallback(
|
||||
(emailThreadId: string) => {
|
||||
const pageComponentInstanceId = v4();
|
||||
|
||||
store.set(
|
||||
viewableRecordIdComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
emailThreadId,
|
||||
);
|
||||
|
||||
// TODO: Uncomment this once we need to show the thread title in the navigation
|
||||
// const objectMetadataItem = snapshot
|
||||
// .getLoadable(objectMetadataItemsState)
|
||||
// .getValue()
|
||||
// .find(
|
||||
// ({ nameSingular }) =>
|
||||
// nameSingular === CoreObjectNameSingular.MessageThread,
|
||||
// );
|
||||
|
||||
// set(
|
||||
// commandMenuNavigationMorphItemsState,
|
||||
// new Map([
|
||||
// ...snapshot
|
||||
// .getLoadable(commandMenuNavigationMorphItemsState)
|
||||
// .getValue(),
|
||||
// [
|
||||
// pageComponentInstanceId,
|
||||
// {
|
||||
// objectMetadataId: objectMetadataItem?.id,
|
||||
// recordId: emailThreadId,
|
||||
// },
|
||||
// ],
|
||||
// ]),
|
||||
// );
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewEmailThread,
|
||||
pageTitle: t`Email Thread`,
|
||||
pageIcon: IconMail,
|
||||
pageId: pageComponentInstanceId,
|
||||
});
|
||||
},
|
||||
[navigateCommandMenu, store],
|
||||
);
|
||||
|
||||
return {
|
||||
openEmailThreadInCommandMenu,
|
||||
};
|
||||
};
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { viewableFrontComponentIdComponentState } from '@/command-menu/pages/front-component/states/viewableFrontComponentIdComponentState';
|
||||
import { viewableFrontComponentRecordContextComponentState } from '@/command-menu/pages/front-component/states/viewableFrontComponentRecordContextComponentState';
|
||||
import { useStore } from 'jotai';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const useOpenFrontComponentInCommandMenu = () => {
|
||||
const store = useStore();
|
||||
const { navigateCommandMenu } = useCommandMenu();
|
||||
|
||||
const openFrontComponentInCommandMenu = ({
|
||||
frontComponentId,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
resetNavigationStack = false,
|
||||
recordContext,
|
||||
}: {
|
||||
frontComponentId: string;
|
||||
pageTitle: string;
|
||||
pageIcon: IconComponent;
|
||||
resetNavigationStack?: boolean;
|
||||
recordContext?: {
|
||||
recordId: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
}) => {
|
||||
const pageComponentInstanceId = v4();
|
||||
|
||||
store.set(
|
||||
viewableFrontComponentIdComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
frontComponentId,
|
||||
);
|
||||
|
||||
store.set(
|
||||
viewableFrontComponentRecordContextComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
recordContext ?? null,
|
||||
);
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewFrontComponent,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
pageId: pageComponentInstanceId,
|
||||
resetNavigationStack,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
openFrontComponentInCommandMenu,
|
||||
};
|
||||
};
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
import { useCommandMenuUpdateNavigationMorphItemsByPage } from '@/command-menu/hooks/useCommandMenuUpdateNavigationMorphItemsByPage';
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { useCallback } from 'react';
|
||||
import { IconArrowMerge } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
type UseOpenMergeRecordsPageInCommandMenuProps = {
|
||||
objectNameSingular: string;
|
||||
objectRecordIds: string[];
|
||||
};
|
||||
|
||||
export const useOpenMergeRecordsPageInCommandMenu = ({
|
||||
objectNameSingular,
|
||||
objectRecordIds,
|
||||
}: UseOpenMergeRecordsPageInCommandMenuProps) => {
|
||||
const store = useStore();
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
const { updateCommandMenuNavigationMorphItemsByPage } =
|
||||
useCommandMenuUpdateNavigationMorphItemsByPage();
|
||||
|
||||
const { upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||
|
||||
const { findManyRecordsLazy } = useLazyFindManyRecords({
|
||||
objectNameSingular,
|
||||
filter: {
|
||||
id: {
|
||||
in: objectRecordIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const openMergeRecordsPageInCommandMenu = useCallback(async () => {
|
||||
const pageId = v4();
|
||||
|
||||
store.set(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
|
||||
await updateCommandMenuNavigationMorphItemsByPage({
|
||||
pageId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
objectRecordIds,
|
||||
});
|
||||
const { records } = await findManyRecordsLazy();
|
||||
upsertRecordsInStore({ partialRecords: records ?? [] });
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.MergeRecords,
|
||||
pageTitle: t(msg`Merge records`),
|
||||
pageIcon: IconArrowMerge,
|
||||
pageId,
|
||||
});
|
||||
}, [
|
||||
objectMetadataItem.id,
|
||||
objectRecordIds,
|
||||
findManyRecordsLazy,
|
||||
upsertRecordsInStore,
|
||||
navigateCommandMenu,
|
||||
updateCommandMenuNavigationMorphItemsByPage,
|
||||
store,
|
||||
]);
|
||||
|
||||
return {
|
||||
openMergeRecordsPageInCommandMenu,
|
||||
};
|
||||
};
|
||||
@@ -1,217 +0,0 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
|
||||
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
|
||||
import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||
import { getIconColorForObjectType } from '@/object-metadata/utils/getIconColorForObjectType';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { useOpenNewRecordTitleCell } from '@/object-record/record-title-cell/hooks/useOpenNewRecordTitleCell';
|
||||
import { CommandMenuPages, CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
|
||||
import { useRunWorkflowRunOpeningInCommandMenuSideEffects } from '@/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const useOpenRecordInCommandMenu = () => {
|
||||
const store = useStore();
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { navigateCommandMenu } = useCommandMenu();
|
||||
const { runWorkflowRunOpeningInCommandMenuSideEffects } =
|
||||
useRunWorkflowRunOpeningInCommandMenuSideEffects();
|
||||
const { openNewRecordTitleCell } = useOpenNewRecordTitleCell();
|
||||
|
||||
const openRecordInCommandMenu = useCallback(
|
||||
({
|
||||
recordId,
|
||||
objectNameSingular,
|
||||
isNewRecord = false,
|
||||
resetNavigationStack = false,
|
||||
}: {
|
||||
recordId: string;
|
||||
objectNameSingular: string;
|
||||
isNewRecord?: boolean;
|
||||
resetNavigationStack?: boolean;
|
||||
}) => {
|
||||
const navigationStack = store.get(commandMenuNavigationStackState.atom);
|
||||
|
||||
const currentNavigationStackItem = navigationStack.at(-1);
|
||||
|
||||
if (isDefined(currentNavigationStackItem)) {
|
||||
const currentRecordId = store.get(
|
||||
viewableRecordIdComponentState.atomFamily({
|
||||
instanceId: currentNavigationStackItem.pageId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (currentRecordId === recordId) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const pageComponentInstanceId = v4();
|
||||
|
||||
store.set(
|
||||
viewableRecordNameSingularComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
objectNameSingular,
|
||||
);
|
||||
store.set(
|
||||
viewableRecordIdComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
recordId,
|
||||
);
|
||||
store.set(viewableRecordIdState.atom, recordId);
|
||||
|
||||
const objectMetadataItem = store.get(
|
||||
objectMetadataItemFamilySelector.selectorFamily({
|
||||
objectName: objectNameSingular,
|
||||
objectNameType: 'singular',
|
||||
}),
|
||||
);
|
||||
|
||||
if (!objectMetadataItem) {
|
||||
throw new Error(
|
||||
`No object metadata item found for object name ${objectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
store.set(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
|
||||
store.set(
|
||||
contextStoreTargetedRecordsRuleComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
{
|
||||
mode: 'selection',
|
||||
selectedRecordIds: [recordId],
|
||||
},
|
||||
);
|
||||
|
||||
store.set(
|
||||
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
1,
|
||||
);
|
||||
|
||||
store.set(
|
||||
contextStoreCurrentViewTypeComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
ContextStoreViewType.ShowPage,
|
||||
);
|
||||
|
||||
store.set(
|
||||
contextStoreCurrentViewIdComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
store.get(
|
||||
contextStoreCurrentViewIdComponentState.atomFamily({
|
||||
instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
store.set(
|
||||
contextStoreIsPageInEditModeComponentState.atomFamily({
|
||||
instanceId: pageComponentInstanceId,
|
||||
}),
|
||||
store.get(
|
||||
contextStoreIsPageInEditModeComponentState.atomFamily({
|
||||
instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const currentMorphItems = store.get(
|
||||
commandMenuNavigationMorphItemsByPageState.atom,
|
||||
);
|
||||
|
||||
const morphItemToAdd = {
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
recordId,
|
||||
};
|
||||
|
||||
const newMorphItemsMap = new Map(currentMorphItems);
|
||||
newMorphItemsMap.set(pageComponentInstanceId, [morphItemToAdd]);
|
||||
|
||||
store.set(
|
||||
commandMenuNavigationMorphItemsByPageState.atom,
|
||||
newMorphItemsMap,
|
||||
);
|
||||
|
||||
const Icon = objectMetadataItem?.icon
|
||||
? getIcon(objectMetadataItem.icon)
|
||||
: getIcon('IconList');
|
||||
|
||||
const IconColor = getIconColorForObjectType(
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
const objectLabelSingular = objectMetadataItem.labelSingular;
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: isNewRecord
|
||||
? t`New ${objectLabelSingular}`
|
||||
: objectLabelSingular,
|
||||
pageIcon: Icon,
|
||||
pageIconColor: IconColor,
|
||||
pageId: pageComponentInstanceId,
|
||||
resetNavigationStack,
|
||||
});
|
||||
|
||||
if (objectNameSingular === CoreObjectNameSingular.WorkflowRun) {
|
||||
runWorkflowRunOpeningInCommandMenuSideEffects({
|
||||
objectMetadataItem,
|
||||
recordId,
|
||||
});
|
||||
}
|
||||
|
||||
if (isNewRecord) {
|
||||
const labelIdentifierField =
|
||||
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
|
||||
|
||||
if (isDefined(labelIdentifierField)) {
|
||||
openNewRecordTitleCell({
|
||||
recordId,
|
||||
fieldName: labelIdentifierField.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
getIcon,
|
||||
navigateCommandMenu,
|
||||
openNewRecordTitleCell,
|
||||
runWorkflowRunOpeningInCommandMenuSideEffects,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
openRecordInCommandMenu,
|
||||
};
|
||||
};
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { IconSearch } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const useOpenRecordsSearchPageInCommandMenu = () => {
|
||||
const { navigateCommandMenu } = useCommandMenu();
|
||||
const isCommandMenuOpened = useAtomStateValue(isCommandMenuOpenedState);
|
||||
|
||||
const openRecordsSearchPage = () => {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: t`Search`,
|
||||
pageIcon: IconSearch,
|
||||
pageId: v4(),
|
||||
resetNavigationStack: isCommandMenuOpened,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
openRecordsSearchPage,
|
||||
};
|
||||
};
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { useCallback } from 'react';
|
||||
import { IconBoxMultiple } from 'twenty-ui/display';
|
||||
|
||||
type UseOpenUpdateMultipleRecordsPageInCommandMenuProps = {
|
||||
contextStoreInstanceId: string;
|
||||
};
|
||||
|
||||
export const useOpenUpdateMultipleRecordsPageInCommandMenu = ({
|
||||
contextStoreInstanceId,
|
||||
}: UseOpenUpdateMultipleRecordsPageInCommandMenuProps) => {
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
|
||||
const openUpdateMultipleRecordsPageInCommandMenu = useCallback(async () => {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.UpdateRecords,
|
||||
pageTitle: t(msg`Update records`),
|
||||
pageIcon: IconBoxMultiple,
|
||||
pageId: contextStoreInstanceId,
|
||||
});
|
||||
}, [navigateCommandMenu, contextStoreInstanceId]);
|
||||
|
||||
return {
|
||||
openUpdateMultipleRecordsPageInCommandMenu,
|
||||
};
|
||||
};
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId';
|
||||
import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates';
|
||||
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
|
||||
|
||||
export const useResetPreviousCommandMenuContext = () => {
|
||||
const { copyContextStoreStates } = useCopyContextStoreStates();
|
||||
const { resetContextStoreStates } = useResetContextStoreStates();
|
||||
|
||||
const resetPreviousCommandMenuContext = () => {
|
||||
copyContextStoreStates({
|
||||
instanceIdToCopyFrom: COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID,
|
||||
instanceIdToCopyTo: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
});
|
||||
resetContextStoreStates(COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID);
|
||||
};
|
||||
|
||||
return {
|
||||
resetPreviousCommandMenuContext,
|
||||
};
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { viewableRichTextComponentState } from '@/command-menu/pages/rich-text-page/states/viewableRichTextComponentState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { IconPencil } from 'twenty-ui/display';
|
||||
|
||||
export const useRichTextCommandMenu = () => {
|
||||
const { navigateCommandMenu, openCommandMenu } = useCommandMenu();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const openRichTextInCommandMenu = useCallback(
|
||||
({
|
||||
activityId,
|
||||
activityObjectNameSingular,
|
||||
}: {
|
||||
activityId: string;
|
||||
activityObjectNameSingular: string;
|
||||
}) => {
|
||||
store.set(viewableRichTextComponentState.atom, {
|
||||
activityId,
|
||||
activityObjectNameSingular,
|
||||
});
|
||||
|
||||
openCommandMenu();
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.EditRichText,
|
||||
pageTitle: t`Rich Text`,
|
||||
pageIcon: IconPencil,
|
||||
});
|
||||
},
|
||||
[navigateCommandMenu, openCommandMenu, store],
|
||||
);
|
||||
|
||||
const editRichText = useCallback(
|
||||
(activityId: string, activityObjectNameSingular: string) => {
|
||||
openRichTextInCommandMenu({ activityId, activityObjectNameSingular });
|
||||
},
|
||||
[openRichTextInCommandMenu],
|
||||
);
|
||||
|
||||
return {
|
||||
editRichText,
|
||||
};
|
||||
};
|
||||
+8
-8
@@ -1,7 +1,7 @@
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
|
||||
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
|
||||
import { SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelPreviousComponentInstanceId';
|
||||
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
|
||||
import { hasUserSelectedSidePanelListItemState } from '@/side-panel/states/hasUserSelectedSidePanelListItemState';
|
||||
import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
@@ -21,8 +21,8 @@ export const useSetGlobalCommandMenuContext = () => {
|
||||
const setGlobalCommandMenuContext = useCallback(() => {
|
||||
store.set(
|
||||
atom(null, (get, batchSet) => {
|
||||
const fromId = COMMAND_MENU_COMPONENT_INSTANCE_ID;
|
||||
const toId = COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID;
|
||||
const fromId = SIDE_PANEL_COMPONENT_INSTANCE_ID;
|
||||
const toId = SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID;
|
||||
|
||||
batchSet(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
|
||||
@@ -165,13 +165,13 @@ export const useSetGlobalCommandMenuContext = () => {
|
||||
ContextStoreViewType.Table,
|
||||
);
|
||||
|
||||
batchSet(commandMenuPageInfoState.atom, {
|
||||
batchSet(sidePanelPageInfoState.atom, {
|
||||
title: undefined,
|
||||
Icon: undefined,
|
||||
instanceId: '',
|
||||
});
|
||||
|
||||
batchSet(hasUserSelectedCommandState.atom, false);
|
||||
batchSet(hasUserSelectedSidePanelListItemState.atom, false);
|
||||
}),
|
||||
);
|
||||
}, [store]);
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { useCallback } from 'react';
|
||||
import { type IconComponent, IconDotsVertical } from 'twenty-ui/display';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useUpdateCommandMenuPageInfo = () => {
|
||||
const store = useStore();
|
||||
const updateCommandMenuPageInfo = useCallback(
|
||||
({
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
}: {
|
||||
pageTitle?: string;
|
||||
pageIcon?: IconComponent;
|
||||
}) => {
|
||||
const commandMenuPageInfo = store.get(commandMenuPageInfoState.atom);
|
||||
|
||||
const newCommandMenuPageInfo = {
|
||||
...commandMenuPageInfo,
|
||||
title: pageTitle ?? commandMenuPageInfo.title ?? '',
|
||||
Icon: pageIcon ?? commandMenuPageInfo.Icon ?? IconDotsVertical,
|
||||
};
|
||||
|
||||
store.set(commandMenuPageInfoState.atom, newCommandMenuPageInfo);
|
||||
|
||||
const commandMenuNavigationStack = store.get(
|
||||
commandMenuNavigationStackState.atom,
|
||||
);
|
||||
|
||||
const lastCommandMenuNavigationStackItem =
|
||||
commandMenuNavigationStack.at(-1);
|
||||
|
||||
if (!lastCommandMenuNavigationStackItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newCommandMenuNavigationStack = [
|
||||
...commandMenuNavigationStack.slice(0, -1),
|
||||
{
|
||||
page: lastCommandMenuNavigationStackItem.page,
|
||||
pageTitle: newCommandMenuPageInfo.title,
|
||||
pageIcon: newCommandMenuPageInfo.Icon,
|
||||
pageId: lastCommandMenuNavigationStackItem.pageId,
|
||||
},
|
||||
];
|
||||
|
||||
store.set(
|
||||
commandMenuNavigationStackState.atom,
|
||||
newCommandMenuNavigationStack,
|
||||
);
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return {
|
||||
updateCommandMenuPageInfo,
|
||||
};
|
||||
};
|
||||
@@ -1,254 +0,0 @@
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { commandMenuWorkflowIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowIdComponentState';
|
||||
import { commandMenuWorkflowRunIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowRunIdComponentState';
|
||||
import { commandMenuWorkflowStepIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowStepIdComponentState';
|
||||
import { commandMenuWorkflowVersionIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowVersionIdComponentState';
|
||||
import { type WorkflowRunStepStatus } from '@/workflow/types/Workflow';
|
||||
import { useSetInitialWorkflowRunRightDrawerTab } from '@/workflow/workflow-diagram/hooks/useSetInitialWorkflowRunRightDrawerTab';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback } from 'react';
|
||||
import { CommandMenuPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconBolt,
|
||||
type IconComponent,
|
||||
IconSettingsAutomation,
|
||||
} from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useWorkflowCommandMenu = () => {
|
||||
const store = useStore();
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
const { setInitialWorkflowRunRightDrawerTab } =
|
||||
useSetInitialWorkflowRunRightDrawerTab();
|
||||
|
||||
const openWorkflowTriggerTypeInCommandMenu = useCallback(
|
||||
(workflowId: string) => {
|
||||
const pageId = v4();
|
||||
|
||||
store.set(
|
||||
commandMenuWorkflowIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
workflowId,
|
||||
);
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.WorkflowTriggerSelectType,
|
||||
pageTitle: t`Trigger Type`,
|
||||
pageIcon: IconBolt,
|
||||
pageId,
|
||||
});
|
||||
},
|
||||
[navigateCommandMenu, store],
|
||||
);
|
||||
|
||||
const openWorkflowCreateStepInCommandMenu = useCallback(
|
||||
(workflowId: string) => {
|
||||
const pageId = v4();
|
||||
|
||||
store.set(
|
||||
commandMenuWorkflowIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
workflowId,
|
||||
);
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.WorkflowStepCreate,
|
||||
pageTitle: t`Select Action`,
|
||||
pageIcon: IconSettingsAutomation,
|
||||
pageId,
|
||||
});
|
||||
},
|
||||
[navigateCommandMenu, store],
|
||||
);
|
||||
|
||||
const openWorkflowEditStepInCommandMenu = useCallback(
|
||||
(
|
||||
workflowId: string,
|
||||
title: string,
|
||||
icon: IconComponent,
|
||||
stepId?: string,
|
||||
) => {
|
||||
const pageId = v4();
|
||||
|
||||
store.set(
|
||||
commandMenuWorkflowIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
workflowId,
|
||||
);
|
||||
|
||||
if (isDefined(stepId)) {
|
||||
store.set(
|
||||
commandMenuWorkflowStepIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
stepId,
|
||||
);
|
||||
|
||||
store.set(
|
||||
workflowSelectedNodeComponentState.atomFamily({
|
||||
instanceId: workflowId,
|
||||
}),
|
||||
stepId,
|
||||
);
|
||||
}
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.WorkflowStepEdit,
|
||||
pageTitle: title,
|
||||
pageIcon: icon,
|
||||
pageId,
|
||||
});
|
||||
},
|
||||
[navigateCommandMenu, store],
|
||||
);
|
||||
|
||||
const openWorkflowEditStepTypeInCommandMenu = useCallback(
|
||||
(workflowId: string) => {
|
||||
const pageId = v4();
|
||||
|
||||
store.set(
|
||||
commandMenuWorkflowIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
workflowId,
|
||||
);
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.WorkflowStepEditType,
|
||||
pageTitle: t`Select action`,
|
||||
pageIcon: IconSettingsAutomation,
|
||||
pageId,
|
||||
});
|
||||
},
|
||||
[navigateCommandMenu, store],
|
||||
);
|
||||
|
||||
const openWorkflowViewStepInCommandMenu = useCallback(
|
||||
({
|
||||
workflowId,
|
||||
workflowVersionId,
|
||||
title,
|
||||
icon,
|
||||
stepId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
workflowVersionId: string;
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
stepId?: string;
|
||||
}) => {
|
||||
const pageId = v4();
|
||||
|
||||
store.set(
|
||||
commandMenuWorkflowIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
workflowId,
|
||||
);
|
||||
store.set(
|
||||
commandMenuWorkflowVersionIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
workflowVersionId,
|
||||
);
|
||||
|
||||
if (isDefined(stepId)) {
|
||||
store.set(
|
||||
commandMenuWorkflowStepIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
stepId,
|
||||
);
|
||||
|
||||
store.set(
|
||||
workflowSelectedNodeComponentState.atomFamily({
|
||||
instanceId: workflowVersionId,
|
||||
}),
|
||||
stepId,
|
||||
);
|
||||
}
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.WorkflowStepView,
|
||||
pageTitle: title,
|
||||
pageIcon: icon,
|
||||
pageId,
|
||||
});
|
||||
},
|
||||
[navigateCommandMenu, store],
|
||||
);
|
||||
|
||||
const openWorkflowRunViewStepInCommandMenu = useCallback(
|
||||
({
|
||||
workflowId,
|
||||
workflowRunId,
|
||||
title,
|
||||
icon,
|
||||
workflowSelectedNode,
|
||||
stepExecutionStatus,
|
||||
}: {
|
||||
workflowId: string;
|
||||
workflowRunId: string;
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
workflowSelectedNode: string;
|
||||
stepExecutionStatus: WorkflowRunStepStatus;
|
||||
}) => {
|
||||
const pageId = v4();
|
||||
|
||||
store.set(
|
||||
commandMenuWorkflowIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
workflowId,
|
||||
);
|
||||
store.set(
|
||||
commandMenuWorkflowRunIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
workflowRunId,
|
||||
);
|
||||
store.set(
|
||||
commandMenuWorkflowStepIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
workflowSelectedNode,
|
||||
);
|
||||
|
||||
store.set(
|
||||
workflowSelectedNodeComponentState.atomFamily({
|
||||
instanceId: workflowRunId,
|
||||
}),
|
||||
workflowSelectedNode,
|
||||
);
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.WorkflowRunStepView,
|
||||
pageTitle: title,
|
||||
pageIcon: icon,
|
||||
pageId,
|
||||
});
|
||||
|
||||
setInitialWorkflowRunRightDrawerTab({
|
||||
workflowSelectedNode,
|
||||
stepExecutionStatus,
|
||||
});
|
||||
},
|
||||
[navigateCommandMenu, setInitialWorkflowRunRightDrawerTab, store],
|
||||
);
|
||||
|
||||
return {
|
||||
openWorkflowTriggerTypeInCommandMenu,
|
||||
openWorkflowCreateStepInCommandMenu,
|
||||
openWorkflowEditStepInCommandMenu,
|
||||
openWorkflowEditStepTypeInCommandMenu,
|
||||
openWorkflowViewStepInCommandMenu,
|
||||
openWorkflowRunViewStepInCommandMenu,
|
||||
};
|
||||
};
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { AIChatThreadsList } from '@/ai/components/AIChatThreadsList';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const CommandMenuAIChatThreadsPage = () => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<AIChatThreadsList />
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { AIChatTab } from '@/ai/components/AIChatTab';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const CommandMenuAskAIPage = () => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<AIChatTab />
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
import { CalendarEventDetails } from '@/activities/calendar/components/CalendarEventDetails';
|
||||
import { CalendarEventDetailsEffect } from '@/activities/calendar/components/CalendarEventDetailsEffect';
|
||||
import { type CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const CommandMenuCalendarEventPage = () => {
|
||||
const { upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||
const viewableRecordId = useAtomComponentStateValue(
|
||||
viewableRecordIdComponentState,
|
||||
);
|
||||
|
||||
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
|
||||
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
const calendarEventRecordGqlFields = {
|
||||
...recordGqlFields,
|
||||
calendarEventParticipants: {
|
||||
id: true,
|
||||
person: true,
|
||||
workspaceMember: true,
|
||||
isOrganizer: true,
|
||||
responseStatus: true,
|
||||
handle: true,
|
||||
createdAt: true,
|
||||
calendarEventId: true,
|
||||
updatedAt: true,
|
||||
displayName: true,
|
||||
},
|
||||
};
|
||||
|
||||
const { record: calendarEvent } = useFindOneRecord<CalendarEvent>({
|
||||
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
|
||||
objectRecordId: viewableRecordId ?? '',
|
||||
recordGqlFields: calendarEventRecordGqlFields,
|
||||
// TODO: this is not executed on sub-sequent runs, make sure that it is intended
|
||||
onCompleted: (record) => {
|
||||
upsertRecordsInStore({ partialRecords: [record] });
|
||||
},
|
||||
});
|
||||
|
||||
if (!calendarEvent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecordIdentifier: {
|
||||
id: calendarEvent.id,
|
||||
targetObjectNameSingular: CoreObjectNameSingular.CalendarEvent,
|
||||
},
|
||||
layoutType: PageLayoutType.RECORD_PAGE,
|
||||
isInRightDrawer: true,
|
||||
}}
|
||||
>
|
||||
<CalendarEventDetailsEffect record={calendarEvent} />
|
||||
<CalendarEventDetails calendarEvent={calendarEvent} />
|
||||
</LayoutRenderingProvider>
|
||||
);
|
||||
};
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { IconChevronLeft } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
padding: 0 ${themeCssVariables.spacing[2]};
|
||||
height: 40px;
|
||||
`;
|
||||
|
||||
const StyledText = styled.span`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
`;
|
||||
|
||||
type SidePanelSubPageNavigationHeaderProps = {
|
||||
title: string;
|
||||
onBackClick: () => void;
|
||||
};
|
||||
|
||||
export const SidePanelSubPageNavigationHeader = ({
|
||||
onBackClick,
|
||||
title,
|
||||
}: SidePanelSubPageNavigationHeaderProps) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<IconButton
|
||||
onClick={onBackClick}
|
||||
Icon={IconChevronLeft}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
/>
|
||||
<StyledText>{title}</StyledText>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
import { Suspense, lazy } from 'react';
|
||||
|
||||
import { viewableFrontComponentIdComponentState } from '@/command-menu/pages/front-component/states/viewableFrontComponentIdComponentState';
|
||||
import { viewableFrontComponentRecordContextComponentState } from '@/command-menu/pages/front-component/states/viewableFrontComponentRecordContextComponentState';
|
||||
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
const FrontComponentRenderer = lazy(() =>
|
||||
import('@/front-components/components/FrontComponentRenderer').then(
|
||||
(module) => ({ default: module.FrontComponentRenderer }),
|
||||
),
|
||||
);
|
||||
|
||||
export const CommandMenuFrontComponentPage = () => {
|
||||
const viewableFrontComponentId = useAtomComponentStateValue(
|
||||
viewableFrontComponentIdComponentState,
|
||||
);
|
||||
|
||||
const viewableFrontComponentRecordContext = useAtomComponentStateValue(
|
||||
viewableFrontComponentRecordContextComponentState,
|
||||
);
|
||||
|
||||
if (!isDefined(viewableFrontComponentId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecordIdentifier: isDefined(viewableFrontComponentRecordContext)
|
||||
? {
|
||||
id: viewableFrontComponentRecordContext.recordId,
|
||||
targetObjectNameSingular:
|
||||
viewableFrontComponentRecordContext.objectNameSingular,
|
||||
}
|
||||
: undefined,
|
||||
layoutType: PageLayoutType.DASHBOARD,
|
||||
isInRightDrawer: true,
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<FrontComponentRenderer frontComponentId={viewableFrontComponentId} />
|
||||
</Suspense>
|
||||
</LayoutRenderingProvider>
|
||||
);
|
||||
};
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
|
||||
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
|
||||
|
||||
export const viewableFrontComponentIdComponentState = createAtomComponentState<
|
||||
string | null
|
||||
>({
|
||||
key: 'command-menu/viewable-front-component-id',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: CommandMenuPageComponentInstanceContext,
|
||||
});
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
|
||||
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
|
||||
|
||||
type FrontComponentRecordContext = {
|
||||
recordId: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const viewableFrontComponentRecordContextComponentState =
|
||||
createAtomComponentState<FrontComponentRecordContext | null>({
|
||||
key: 'command-menu/viewable-front-component-record-context',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: CommandMenuPageComponentInstanceContext,
|
||||
});
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
|
||||
import { type EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { IconArrowsVertical } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
||||
padding: 16px 24px;
|
||||
`;
|
||||
|
||||
export const CommandMenuMessageThreadIntermediaryMessages = ({
|
||||
messages,
|
||||
}: {
|
||||
messages: EmailThreadMessageWithSender[];
|
||||
}) => {
|
||||
const [areMessagesOpen, setAreMessagesOpen] = useState(false);
|
||||
|
||||
if (messages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return areMessagesOpen ? (
|
||||
messages.map((message) => (
|
||||
<EmailThreadMessage
|
||||
key={message.id}
|
||||
sender={message.sender}
|
||||
participants={message.messageParticipants}
|
||||
body={message.text}
|
||||
sentAt={message.receivedAt}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
Icon={IconArrowsVertical}
|
||||
title={`${messages.length} email${messages.length > 1 ? 's' : ''}`}
|
||||
size="small"
|
||||
onClick={() => setAreMessagesOpen(true)}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
);
|
||||
};
|
||||
-182
@@ -1,182 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
||||
import { EmailLoader } from '@/activities/emails/components/EmailLoader';
|
||||
import { EmailThreadHeader } from '@/activities/emails/components/EmailThreadHeader';
|
||||
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
|
||||
import { CommandMenuMessageThreadIntermediaryMessages } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadIntermediaryMessages';
|
||||
import { useEmailThreadInCommandMenu } from '@/command-menu/pages/message-thread/hooks/useEmailThreadInCommandMenu';
|
||||
import { messageThreadComponentState } from '@/command-menu/pages/message-thread/states/messageThreadComponentState';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { IconArrowBackUp } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 85%;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
background: ${themeCssVariables.background.secondary};
|
||||
border-top: 1px solid ${themeCssVariables.border.color.light};
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
const ALLOWED_REPLY_PROVIDERS = [
|
||||
ConnectedAccountProvider.GOOGLE,
|
||||
ConnectedAccountProvider.MICROSOFT,
|
||||
ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||
];
|
||||
|
||||
export const CommandMenuMessageThreadPage = () => {
|
||||
const setMessageThread = useSetAtomComponentState(
|
||||
messageThreadComponentState,
|
||||
);
|
||||
|
||||
const {
|
||||
thread,
|
||||
messages,
|
||||
fetchMoreMessages,
|
||||
threadLoading,
|
||||
messageThreadExternalId,
|
||||
connectedAccountHandle,
|
||||
messageChannelLoading,
|
||||
connectedAccountProvider,
|
||||
lastMessageExternalId,
|
||||
connectedAccountConnectionParameters,
|
||||
} = useEmailThreadInCommandMenu();
|
||||
|
||||
useEffect(() => {
|
||||
if (!messages[0]?.messageThread) {
|
||||
return;
|
||||
}
|
||||
setMessageThread(messages[0]?.messageThread);
|
||||
}, [messages, setMessageThread]);
|
||||
|
||||
const messagesCount = messages.length;
|
||||
const is5OrMoreMessages = messagesCount >= 5;
|
||||
const firstMessages = messages.slice(
|
||||
0,
|
||||
is5OrMoreMessages ? 2 : messagesCount - 1,
|
||||
);
|
||||
const intermediaryMessages = is5OrMoreMessages
|
||||
? messages.slice(2, messagesCount - 1)
|
||||
: [];
|
||||
const lastMessage = messages[messagesCount - 1];
|
||||
const subject = messages[0]?.subject;
|
||||
|
||||
const canReply = useMemo(() => {
|
||||
return (
|
||||
connectedAccountHandle &&
|
||||
connectedAccountProvider &&
|
||||
ALLOWED_REPLY_PROVIDERS.includes(connectedAccountProvider) &&
|
||||
(connectedAccountProvider !== ConnectedAccountProvider.IMAP_SMTP_CALDAV ||
|
||||
isDefined(connectedAccountConnectionParameters?.SMTP)) &&
|
||||
lastMessage &&
|
||||
messageThreadExternalId != null
|
||||
);
|
||||
}, [
|
||||
connectedAccountConnectionParameters,
|
||||
connectedAccountHandle,
|
||||
connectedAccountProvider,
|
||||
lastMessage,
|
||||
messageThreadExternalId,
|
||||
]);
|
||||
|
||||
const handleReplyClick = () => {
|
||||
if (!canReply) {
|
||||
return;
|
||||
}
|
||||
|
||||
let url: string;
|
||||
switch (connectedAccountProvider) {
|
||||
case ConnectedAccountProvider.MICROSOFT:
|
||||
url = `https://outlook.office.com/mail/deeplink?ItemID=${lastMessageExternalId}`;
|
||||
window.open(url, '_blank');
|
||||
break;
|
||||
case ConnectedAccountProvider.GOOGLE:
|
||||
url = `https://mail.google.com/mail/?authuser=${connectedAccountHandle}#all/${messageThreadExternalId}`;
|
||||
window.open(url, '_blank');
|
||||
break;
|
||||
case ConnectedAccountProvider.IMAP_SMTP_CALDAV:
|
||||
throw new Error('Account provider not supported');
|
||||
case null:
|
||||
throw new Error('Account provider not provided');
|
||||
default:
|
||||
assertUnreachable(connectedAccountProvider);
|
||||
}
|
||||
};
|
||||
if (!thread || !messages.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<StyledContainer>
|
||||
{threadLoading ? (
|
||||
<EmailLoader loadingText={t`Loading thread`} />
|
||||
) : (
|
||||
<>
|
||||
<EmailThreadHeader
|
||||
subject={subject}
|
||||
lastMessageSentAt={lastMessage.receivedAt}
|
||||
/>
|
||||
{firstMessages.map((message) => (
|
||||
<EmailThreadMessage
|
||||
key={message.id}
|
||||
sender={message.sender}
|
||||
participants={message.messageParticipants}
|
||||
body={message.text}
|
||||
sentAt={message.receivedAt}
|
||||
/>
|
||||
))}
|
||||
<CommandMenuMessageThreadIntermediaryMessages
|
||||
messages={intermediaryMessages}
|
||||
/>
|
||||
<EmailThreadMessage
|
||||
key={lastMessage.id}
|
||||
sender={lastMessage.sender}
|
||||
participants={lastMessage.messageParticipants}
|
||||
body={lastMessage.text}
|
||||
sentAt={lastMessage.receivedAt}
|
||||
isExpanded
|
||||
/>
|
||||
<CustomResolverFetchMoreLoader
|
||||
loading={threadLoading}
|
||||
onLastRowVisible={fetchMoreMessages}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</StyledContainer>
|
||||
{canReply && !messageChannelLoading && (
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleReplyClick}
|
||||
title={t`Reply`}
|
||||
Icon={IconArrowBackUp}
|
||||
disabled={!canReply}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user