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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user