251 create top bar chips inside the command menu (#9809)
Closes #https://github.com/twentyhq/core-team-issues/issues/251 https://github.com/user-attachments/assets/065c97fe-1daf-4b48-9d57-6bbb96d24ede
This commit is contained in:
@ -5,6 +5,7 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
|||||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
import { IconCalendarEvent } from 'twenty-ui';
|
||||||
|
|
||||||
export const useOpenCalendarEventRightDrawer = () => {
|
export const useOpenCalendarEventRightDrawer = () => {
|
||||||
const { openRightDrawer } = useRightDrawer();
|
const { openRightDrawer } = useRightDrawer();
|
||||||
@ -13,7 +14,10 @@ export const useOpenCalendarEventRightDrawer = () => {
|
|||||||
|
|
||||||
const openCalendarEventRightDrawer = (calendarEventId: string) => {
|
const openCalendarEventRightDrawer = (calendarEventId: string) => {
|
||||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||||
openRightDrawer(RightDrawerPages.ViewCalendarEvent);
|
openRightDrawer(RightDrawerPages.ViewCalendarEvent, {
|
||||||
|
title: 'Calendar Event',
|
||||||
|
Icon: IconCalendarEvent,
|
||||||
|
});
|
||||||
setViewableRecordId(calendarEventId);
|
setViewableRecordId(calendarEventId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
|||||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
import { IconSparkles } from 'twenty-ui';
|
||||||
|
|
||||||
export const useOpenCopilotRightDrawer = () => {
|
export const useOpenCopilotRightDrawer = () => {
|
||||||
const { openRightDrawer } = useRightDrawer();
|
const { openRightDrawer } = useRightDrawer();
|
||||||
@ -9,6 +10,9 @@ export const useOpenCopilotRightDrawer = () => {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||||
openRightDrawer(RightDrawerPages.Copilot);
|
openRightDrawer(RightDrawerPages.Copilot, {
|
||||||
|
title: 'Copilot',
|
||||||
|
Icon: IconSparkles,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { act } from 'react-dom/test-utils';
|
|||||||
import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer';
|
import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer';
|
||||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||||
|
import { IconMail } from 'twenty-ui';
|
||||||
|
|
||||||
const mockOpenRightDrawer = jest.fn();
|
const mockOpenRightDrawer = jest.fn();
|
||||||
const mockSetHotkeyScope = jest.fn();
|
const mockSetHotkeyScope = jest.fn();
|
||||||
@ -31,5 +32,9 @@ test('useOpenEmailThreadRightDrawer opens the email thread right drawer', () =>
|
|||||||
);
|
);
|
||||||
expect(mockOpenRightDrawer).toHaveBeenCalledWith(
|
expect(mockOpenRightDrawer).toHaveBeenCalledWith(
|
||||||
RightDrawerPages.ViewEmailThread,
|
RightDrawerPages.ViewEmailThread,
|
||||||
|
{
|
||||||
|
title: 'Email Thread',
|
||||||
|
Icon: IconMail,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
|||||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
import { IconMail } from 'twenty-ui';
|
||||||
|
|
||||||
export const useOpenEmailThreadRightDrawer = () => {
|
export const useOpenEmailThreadRightDrawer = () => {
|
||||||
const { openRightDrawer } = useRightDrawer();
|
const { openRightDrawer } = useRightDrawer();
|
||||||
@ -9,6 +10,9 @@ export const useOpenEmailThreadRightDrawer = () => {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||||
openRightDrawer(RightDrawerPages.ViewEmailThread);
|
openRightDrawer(RightDrawerPages.ViewEmailThread, {
|
||||||
|
title: 'Email Thread',
|
||||||
|
Icon: IconMail,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,63 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const StyledChip = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
background: ${({ theme }) => theme.background.transparent.light};
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
height: ${({ theme }) => theme.spacing(8)};
|
||||||
|
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
line-height: ${({ theme }) => theme.text.lineHeight.lg};
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIconsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIconWrapper = styled.div<{ withIconBackground?: boolean }>`
|
||||||
|
background: ${({ theme, withIconBackground }) =>
|
||||||
|
withIconBackground ? theme.background.primary : 'unset'};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
padding: ${({ theme }) => theme.spacing(0.5)};
|
||||||
|
border: 1px solid
|
||||||
|
${({ theme, withIconBackground }) =>
|
||||||
|
withIconBackground ? theme.border.color.medium : 'transparent'};
|
||||||
|
&:not(:first-of-type) {
|
||||||
|
margin-left: -${({ theme }) => theme.spacing(1)};
|
||||||
|
}
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CommandMenuContextChip = ({
|
||||||
|
Icons,
|
||||||
|
text,
|
||||||
|
withIconBackground,
|
||||||
|
}: {
|
||||||
|
Icons: React.ReactNode[];
|
||||||
|
text?: string;
|
||||||
|
withIconBackground?: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StyledChip>
|
||||||
|
<StyledIconsContainer>
|
||||||
|
{Icons.map((Icon, index) => (
|
||||||
|
<StyledIconWrapper
|
||||||
|
key={index}
|
||||||
|
withIconBackground={withIconBackground}
|
||||||
|
>
|
||||||
|
{Icon}
|
||||||
|
</StyledIconWrapper>
|
||||||
|
))}
|
||||||
|
</StyledIconsContainer>
|
||||||
|
<span>{text}</span>
|
||||||
|
</StyledChip>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,30 +1,10 @@
|
|||||||
|
import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip';
|
||||||
import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars';
|
import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars';
|
||||||
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
|
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
|
||||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||||
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
|
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { capitalize } from 'twenty-shared';
|
import { capitalize } from 'twenty-shared';
|
||||||
|
|
||||||
const StyledChip = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
background: ${({ theme }) => theme.background.transparent.light};
|
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
|
||||||
height: ${({ theme }) => theme.spacing(8)};
|
|
||||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
|
||||||
font-size: ${({ theme }) => theme.font.size.sm};
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
|
||||||
line-height: ${({ theme }) => theme.text.lineHeight.lg};
|
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledAvatarContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const CommandMenuContextRecordChip = ({
|
export const CommandMenuContextRecordChip = ({
|
||||||
objectMetadataItemId,
|
objectMetadataItemId,
|
||||||
}: {
|
}: {
|
||||||
@ -43,21 +23,25 @@ export const CommandMenuContextRecordChip = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Avatars = records.map((record) => (
|
||||||
|
<CommandMenuContextRecordChipAvatars
|
||||||
|
objectMetadataItem={objectMetadataItem}
|
||||||
|
key={record.id}
|
||||||
|
record={record}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
const text =
|
||||||
|
totalCount === 1
|
||||||
|
? getObjectRecordIdentifier({ objectMetadataItem, record: records[0] })
|
||||||
|
.name
|
||||||
|
: `${totalCount} ${capitalize(objectMetadataItem.namePlural)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledChip>
|
<CommandMenuContextChip
|
||||||
<StyledAvatarContainer>
|
text={text}
|
||||||
{records.map((record) => (
|
Icons={Avatars}
|
||||||
<CommandMenuContextRecordChipAvatars
|
withIconBackground={true}
|
||||||
objectMetadataItem={objectMetadataItem}
|
/>
|
||||||
key={record.id}
|
|
||||||
record={record}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</StyledAvatarContainer>
|
|
||||||
{totalCount === 1
|
|
||||||
? getObjectRecordIdentifier({ objectMetadataItem, record: records[0] })
|
|
||||||
.name
|
|
||||||
: `${totalCount} ${capitalize(objectMetadataItem.namePlural)}`}
|
|
||||||
</StyledChip>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,22 +3,8 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|||||||
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
|
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { Avatar } from 'twenty-ui';
|
import { Avatar } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledAvatarWrapper = styled.div`
|
|
||||||
background-color: ${({ theme }) => theme.background.primary};
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
padding: ${({ theme }) => theme.spacing(0.5)};
|
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
|
||||||
&:not(:first-of-type) {
|
|
||||||
margin-left: -${({ theme }) => theme.spacing(1)};
|
|
||||||
}
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const CommandMenuContextRecordChipAvatars = ({
|
export const CommandMenuContextRecordChipAvatars = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
record,
|
record,
|
||||||
@ -38,7 +24,7 @@ export const CommandMenuContextRecordChipAvatars = ({
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledAvatarWrapper>
|
<>
|
||||||
{Icon ? (
|
{Icon ? (
|
||||||
<Icon color={IconColor} size={theme.icon.size.sm} />
|
<Icon color={IconColor} size={theme.icon.size.sm} />
|
||||||
) : (
|
) : (
|
||||||
@ -50,6 +36,6 @@ export const CommandMenuContextRecordChipAvatars = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledAvatarWrapper>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
|
import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip';
|
||||||
import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip';
|
import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip';
|
||||||
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
|
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
|
||||||
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
|
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
|
||||||
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
|
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
|
||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||||
|
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { IconX, LightIconButton, isDefined, useIsMobile } from 'twenty-ui';
|
import { IconX, LightIconButton, isDefined, useIsMobile } from 'twenty-ui';
|
||||||
@ -82,6 +85,10 @@ export const CommandMenuTopBar = () => {
|
|||||||
|
|
||||||
const commandMenuPage = useRecoilValue(commandMenuPageState);
|
const commandMenuPage = useRecoilValue(commandMenuPageState);
|
||||||
|
|
||||||
|
const { title, Icon } = useRecoilValue(commandMenuPageInfoState);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledInputContainer>
|
<StyledInputContainer>
|
||||||
<StyledContentContainer>
|
<StyledContentContainer>
|
||||||
@ -90,6 +97,13 @@ export const CommandMenuTopBar = () => {
|
|||||||
objectMetadataItemId={contextStoreCurrentObjectMetadataId}
|
objectMetadataItemId={contextStoreCurrentObjectMetadataId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isDefined(Icon) && (
|
||||||
|
<CommandMenuContextChip
|
||||||
|
Icons={[<Icon size={theme.icon.size.sm} />]}
|
||||||
|
text={title}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{commandMenuPage === CommandMenuPages.Root && (
|
{commandMenuPage === CommandMenuPages.Root && (
|
||||||
<StyledInput
|
<StyledInput
|
||||||
autoFocus
|
autoFocus
|
||||||
|
|||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ComponentDecorator,
|
||||||
|
IconBuildingSkyscraper,
|
||||||
|
IconUser,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
import { CommandMenuContextChip } from '../CommandMenuContextChip';
|
||||||
|
|
||||||
|
const meta: Meta<typeof CommandMenuContextChip> = {
|
||||||
|
title: 'Modules/CommandMenu/CommandMenuContextChip',
|
||||||
|
component: CommandMenuContextChip,
|
||||||
|
decorators: [ComponentDecorator],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof CommandMenuContextChip>;
|
||||||
|
|
||||||
|
export const SingleIcon: Story = {
|
||||||
|
args: {
|
||||||
|
Icons: [<IconUser size={16} />],
|
||||||
|
text: 'Person',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MultipleIcons: Story = {
|
||||||
|
args: {
|
||||||
|
Icons: [<IconUser size={16} />, <IconBuildingSkyscraper size={16} />],
|
||||||
|
text: 'Person & Company',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithIconBackground: Story = {
|
||||||
|
args: {
|
||||||
|
Icons: [<IconUser size={16} />],
|
||||||
|
text: 'Person',
|
||||||
|
withIconBackground: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MultipleIconsWithIconBackground: Story = {
|
||||||
|
args: {
|
||||||
|
Icons: [<IconUser size={16} />, <IconBuildingSkyscraper size={16} />],
|
||||||
|
text: 'Person & Company',
|
||||||
|
withIconBackground: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IconsOnly: Story = {
|
||||||
|
args: {
|
||||||
|
Icons: [<IconUser size={16} />, <IconBuildingSkyscraper size={16} />],
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,261 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip';
|
||||||
|
import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext';
|
||||||
|
import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
|
||||||
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
|
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
|
||||||
|
import { getCompaniesMock } from '~/testing/mock-data/companies';
|
||||||
|
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/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
|
||||||
|
}
|
||||||
|
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 companiesMock = getCompaniesMock();
|
||||||
|
|
||||||
|
const companyMock = companiesMock[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 companiesMock;
|
||||||
|
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,
|
||||||
|
onInitializeRecoilSnapshot: (snapshot) => {
|
||||||
|
for (const company of companies) {
|
||||||
|
snapshot.set(recordStoreFamilyState(company.id), company);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContextStoreDecorator: Decorator = (Story) => {
|
||||||
|
const ContextStoreWrapper = createContextStoreWrapper({
|
||||||
|
companies: [companyMock],
|
||||||
|
componentInstanceId: '1',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextStoreWrapper>
|
||||||
|
<Story />
|
||||||
|
</ContextStoreWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const meta: Meta<typeof CommandMenuContextRecordChip> = {
|
||||||
|
title: 'Modules/CommandMenu/CommandMenuContextRecordChip',
|
||||||
|
component: CommandMenuContextRecordChip,
|
||||||
|
decorators: [
|
||||||
|
ContextStoreDecorator,
|
||||||
|
ChipGeneratorsDecorator,
|
||||||
|
ComponentDecorator,
|
||||||
|
],
|
||||||
|
args: {
|
||||||
|
objectMetadataItemId: companyMockObjectMetadataItem?.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof CommandMenuContextRecordChip>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const WithTwoCompanies: Story = {
|
||||||
|
decorators: [
|
||||||
|
(Story) => {
|
||||||
|
const twoCompaniesMock = companiesMock.slice(0, 2);
|
||||||
|
const TwoCompaniesWrapper = createContextStoreWrapper({
|
||||||
|
companies: twoCompaniesMock,
|
||||||
|
componentInstanceId: '2',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TwoCompaniesWrapper>
|
||||||
|
<Story />
|
||||||
|
</TwoCompaniesWrapper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithTenCompanies: Story = {
|
||||||
|
decorators: [
|
||||||
|
(Story) => {
|
||||||
|
const tenCompaniesMock = companiesMock.slice(0, 10);
|
||||||
|
const TenCompaniesWrapper = createContextStoreWrapper({
|
||||||
|
companies: tenCompaniesMock,
|
||||||
|
componentInstanceId: '3',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TenCompaniesWrapper>
|
||||||
|
<Story />
|
||||||
|
</TenCompaniesWrapper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -9,6 +9,7 @@ import { isDefined } from '~/utils/isDefined';
|
|||||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||||
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
|
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
|
||||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||||
|
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||||
@ -213,6 +214,10 @@ export const useCommandMenu = () => {
|
|||||||
|
|
||||||
set(viewableRecordIdState, null);
|
set(viewableRecordIdState, null);
|
||||||
set(commandMenuPageState, CommandMenuPages.Root);
|
set(commandMenuPageState, CommandMenuPages.Root);
|
||||||
|
set(commandMenuPageInfoState, {
|
||||||
|
title: undefined,
|
||||||
|
Icon: undefined,
|
||||||
|
});
|
||||||
set(isCommandMenuOpenedState, false);
|
set(isCommandMenuOpenedState, false);
|
||||||
resetSelectedItem();
|
resetSelectedItem();
|
||||||
goBackToPreviousHotkeyScope();
|
goBackToPreviousHotkeyScope();
|
||||||
@ -278,6 +283,11 @@ export const useCommandMenu = () => {
|
|||||||
}),
|
}),
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
set(commandMenuPageInfoState, {
|
||||||
|
title: undefined,
|
||||||
|
Icon: undefined,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { createState } from '@ui/utilities/state/utils/createState';
|
||||||
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const commandMenuPageInfoState = createState<{
|
||||||
|
title: string | undefined;
|
||||||
|
Icon: IconComponent | undefined;
|
||||||
|
}>({
|
||||||
|
key: 'command-menu/commandMenuPageInfoState',
|
||||||
|
defaultValue: { title: undefined, Icon: undefined },
|
||||||
|
});
|
||||||
@ -5,9 +5,11 @@ import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/righ
|
|||||||
|
|
||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||||
|
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||||
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
|
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
|
||||||
import { mapRightDrawerPageToCommandMenuPage } from '@/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage';
|
import { mapRightDrawerPageToCommandMenuPage } from '@/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage';
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
|
import { IconComponent } from 'twenty-ui';
|
||||||
import { FeatureFlagKey } from '~/generated/graphql';
|
import { FeatureFlagKey } from '~/generated/graphql';
|
||||||
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
|
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
|
||||||
import { rightDrawerPageState } from '../states/rightDrawerPageState';
|
import { rightDrawerPageState } from '../states/rightDrawerPageState';
|
||||||
@ -27,12 +29,22 @@ export const useRightDrawer = () => {
|
|||||||
|
|
||||||
const openRightDrawer = useRecoilCallback(
|
const openRightDrawer = useRecoilCallback(
|
||||||
({ set }) =>
|
({ set }) =>
|
||||||
(rightDrawerPage: RightDrawerPages) => {
|
(
|
||||||
|
rightDrawerPage: RightDrawerPages,
|
||||||
|
commandMenuPageInfo?: {
|
||||||
|
title?: string;
|
||||||
|
Icon?: IconComponent;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
if (isCommandMenuV2Enabled) {
|
if (isCommandMenuV2Enabled) {
|
||||||
const commandMenuPage =
|
const commandMenuPage =
|
||||||
mapRightDrawerPageToCommandMenuPage(rightDrawerPage);
|
mapRightDrawerPageToCommandMenuPage(rightDrawerPage);
|
||||||
|
|
||||||
set(commandMenuPageState, commandMenuPage);
|
set(commandMenuPageState, commandMenuPage);
|
||||||
|
set(commandMenuPageInfoState, {
|
||||||
|
title: commandMenuPageInfo?.title,
|
||||||
|
Icon: commandMenuPageInfo?.Icon,
|
||||||
|
});
|
||||||
openCommandMenu();
|
openCommandMenu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,11 +8,15 @@ import { EMPTY_TRIGGER_STEP_ID } from '@/workflow/workflow-diagram/constants/Emp
|
|||||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||||
import { useTriggerNodeSelection } from '@/workflow/workflow-diagram/hooks/useTriggerNodeSelection';
|
import { useTriggerNodeSelection } from '@/workflow/workflow-diagram/hooks/useTriggerNodeSelection';
|
||||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||||
import { WorkflowDiagramNode } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
import {
|
||||||
|
WorkflowDiagramNode,
|
||||||
|
WorkflowDiagramStepNodeData,
|
||||||
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { getWorkflowNodeIcon } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIcon';
|
||||||
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { IconBolt, isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
export const WorkflowDiagramCanvasEditableEffect = () => {
|
export const WorkflowDiagramCanvasEditableEffect = () => {
|
||||||
const { startNodeCreation } = useStartNodeCreation();
|
const { startNodeCreation } = useStartNodeCreation();
|
||||||
@ -37,7 +41,10 @@ export const WorkflowDiagramCanvasEditableEffect = () => {
|
|||||||
|
|
||||||
const isEmptyTriggerNode = selectedNode.type === EMPTY_TRIGGER_STEP_ID;
|
const isEmptyTriggerNode = selectedNode.type === EMPTY_TRIGGER_STEP_ID;
|
||||||
if (isEmptyTriggerNode) {
|
if (isEmptyTriggerNode) {
|
||||||
openRightDrawer(RightDrawerPages.WorkflowStepSelectTriggerType);
|
openRightDrawer(RightDrawerPages.WorkflowStepSelectTriggerType, {
|
||||||
|
title: 'Trigger Type',
|
||||||
|
Icon: IconBolt,
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -53,9 +60,14 @@ export const WorkflowDiagramCanvasEditableEffect = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedNodeData = selectedNode.data as WorkflowDiagramStepNodeData;
|
||||||
|
|
||||||
setWorkflowSelectedNode(selectedNode.id);
|
setWorkflowSelectedNode(selectedNode.id);
|
||||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||||
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
|
openRightDrawer(RightDrawerPages.WorkflowStepEdit, {
|
||||||
|
title: selectedNodeData.name,
|
||||||
|
Icon: getWorkflowNodeIcon(selectedNodeData),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
setWorkflowSelectedNode,
|
setWorkflowSelectedNode,
|
||||||
|
|||||||
@ -5,7 +5,11 @@ import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPage
|
|||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { useTriggerNodeSelection } from '@/workflow/workflow-diagram/hooks/useTriggerNodeSelection';
|
import { useTriggerNodeSelection } from '@/workflow/workflow-diagram/hooks/useTriggerNodeSelection';
|
||||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||||
import { WorkflowDiagramNode } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
import {
|
||||||
|
WorkflowDiagramNode,
|
||||||
|
WorkflowDiagramStepNodeData,
|
||||||
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { getWorkflowNodeIcon } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIcon';
|
||||||
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
@ -30,7 +34,12 @@ export const WorkflowDiagramCanvasReadonlyEffect = () => {
|
|||||||
|
|
||||||
setWorkflowSelectedNode(selectedNode.id);
|
setWorkflowSelectedNode(selectedNode.id);
|
||||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||||
openRightDrawer(RightDrawerPages.WorkflowStepView);
|
|
||||||
|
const selectedNodeData = selectedNode.data as WorkflowDiagramStepNodeData;
|
||||||
|
openRightDrawer(RightDrawerPages.WorkflowStepView, {
|
||||||
|
title: selectedNodeData.name,
|
||||||
|
Icon: getWorkflowNodeIcon(selectedNodeData),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
setWorkflowSelectedNode,
|
setWorkflowSelectedNode,
|
||||||
|
|||||||
@ -1,15 +1,9 @@
|
|||||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||||
import { WorkflowDiagramBaseStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseStepNode';
|
import { WorkflowDiagramBaseStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseStepNode';
|
||||||
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { getWorkflowNodeIcon } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIcon';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {
|
|
||||||
IconAddressBook,
|
|
||||||
IconCode,
|
|
||||||
IconHandMove,
|
|
||||||
IconMail,
|
|
||||||
IconPlaylistAdd,
|
|
||||||
} from 'twenty-ui';
|
|
||||||
|
|
||||||
const StyledStepNodeLabelIconContainer = styled.div`
|
const StyledStepNodeLabelIconContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -29,6 +23,8 @@ export const WorkflowDiagramStepNodeBase = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const Icon = getWorkflowNodeIcon(data);
|
||||||
|
|
||||||
const renderStepIcon = () => {
|
const renderStepIcon = () => {
|
||||||
switch (data.nodeType) {
|
switch (data.nodeType) {
|
||||||
case 'trigger': {
|
case 'trigger': {
|
||||||
@ -36,7 +32,7 @@ export const WorkflowDiagramStepNodeBase = ({
|
|||||||
case 'DATABASE_EVENT': {
|
case 'DATABASE_EVENT': {
|
||||||
return (
|
return (
|
||||||
<StyledStepNodeLabelIconContainer>
|
<StyledStepNodeLabelIconContainer>
|
||||||
<IconPlaylistAdd
|
<Icon
|
||||||
size={theme.icon.size.lg}
|
size={theme.icon.size.lg}
|
||||||
color={theme.font.color.tertiary}
|
color={theme.font.color.tertiary}
|
||||||
/>
|
/>
|
||||||
@ -46,7 +42,7 @@ export const WorkflowDiagramStepNodeBase = ({
|
|||||||
case 'MANUAL': {
|
case 'MANUAL': {
|
||||||
return (
|
return (
|
||||||
<StyledStepNodeLabelIconContainer>
|
<StyledStepNodeLabelIconContainer>
|
||||||
<IconHandMove
|
<Icon
|
||||||
size={theme.icon.size.lg}
|
size={theme.icon.size.lg}
|
||||||
color={theme.font.color.tertiary}
|
color={theme.font.color.tertiary}
|
||||||
/>
|
/>
|
||||||
@ -62,17 +58,14 @@ export const WorkflowDiagramStepNodeBase = ({
|
|||||||
case 'CODE': {
|
case 'CODE': {
|
||||||
return (
|
return (
|
||||||
<StyledStepNodeLabelIconContainer>
|
<StyledStepNodeLabelIconContainer>
|
||||||
<IconCode
|
<Icon size={theme.icon.size.lg} color={theme.color.orange} />
|
||||||
size={theme.icon.size.lg}
|
|
||||||
color={theme.color.orange}
|
|
||||||
/>
|
|
||||||
</StyledStepNodeLabelIconContainer>
|
</StyledStepNodeLabelIconContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'SEND_EMAIL': {
|
case 'SEND_EMAIL': {
|
||||||
return (
|
return (
|
||||||
<StyledStepNodeLabelIconContainer>
|
<StyledStepNodeLabelIconContainer>
|
||||||
<IconMail size={theme.icon.size.lg} color={theme.color.blue} />
|
<Icon size={theme.icon.size.lg} color={theme.color.blue} />
|
||||||
</StyledStepNodeLabelIconContainer>
|
</StyledStepNodeLabelIconContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -81,7 +74,7 @@ export const WorkflowDiagramStepNodeBase = ({
|
|||||||
case 'DELETE_RECORD': {
|
case 'DELETE_RECORD': {
|
||||||
return (
|
return (
|
||||||
<StyledStepNodeLabelIconContainer>
|
<StyledStepNodeLabelIconContainer>
|
||||||
<IconAddressBook
|
<Icon
|
||||||
size={theme.icon.size.lg}
|
size={theme.icon.size.lg}
|
||||||
color={theme.font.color.tertiary}
|
color={theme.font.color.tertiary}
|
||||||
stroke={theme.icon.stroke.sm}
|
stroke={theme.icon.stroke.sm}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope
|
|||||||
import { workflowCreateStepFromParentStepIdState } from '@/workflow/workflow-steps/states/workflowCreateStepFromParentStepIdState';
|
import { workflowCreateStepFromParentStepIdState } from '@/workflow/workflow-steps/states/workflowCreateStepFromParentStepIdState';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import { IconSettingsAutomation } from 'twenty-ui';
|
||||||
|
|
||||||
export const useStartNodeCreation = () => {
|
export const useStartNodeCreation = () => {
|
||||||
const { openRightDrawer } = useRightDrawer();
|
const { openRightDrawer } = useRightDrawer();
|
||||||
@ -22,7 +23,10 @@ export const useStartNodeCreation = () => {
|
|||||||
setWorkflowCreateStepFromParentStepId(parentNodeId);
|
setWorkflowCreateStepFromParentStepId(parentNodeId);
|
||||||
|
|
||||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||||
openRightDrawer(RightDrawerPages.WorkflowStepSelectAction);
|
openRightDrawer(RightDrawerPages.WorkflowStepSelectAction, {
|
||||||
|
title: 'Select Action',
|
||||||
|
Icon: IconSettingsAutomation,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[openRightDrawer, setWorkflowCreateStepFromParentStepId, setHotkeyScope],
|
[openRightDrawer, setWorkflowCreateStepFromParentStepId, setHotkeyScope],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
WorkflowActionType,
|
||||||
|
WorkflowTriggerType,
|
||||||
|
} from '@/workflow/types/Workflow';
|
||||||
|
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||||
|
import {
|
||||||
|
IconAddressBook,
|
||||||
|
IconCode,
|
||||||
|
IconHandMove,
|
||||||
|
IconMail,
|
||||||
|
IconPlaylistAdd,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
|
||||||
|
export const getWorkflowNodeIcon = (
|
||||||
|
data:
|
||||||
|
| {
|
||||||
|
nodeType: 'trigger';
|
||||||
|
triggerType: WorkflowTriggerType;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
nodeType: 'action';
|
||||||
|
actionType: WorkflowActionType;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
switch (data.nodeType) {
|
||||||
|
case 'trigger': {
|
||||||
|
switch (data.triggerType) {
|
||||||
|
case 'DATABASE_EVENT': {
|
||||||
|
return IconPlaylistAdd;
|
||||||
|
}
|
||||||
|
case 'MANUAL': {
|
||||||
|
return IconHandMove;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return assertUnreachable(data.triggerType);
|
||||||
|
}
|
||||||
|
case 'action': {
|
||||||
|
switch (data.actionType) {
|
||||||
|
case 'CODE': {
|
||||||
|
return IconCode;
|
||||||
|
}
|
||||||
|
case 'SEND_EMAIL': {
|
||||||
|
return IconMail;
|
||||||
|
}
|
||||||
|
case 'CREATE_RECORD':
|
||||||
|
case 'UPDATE_RECORD':
|
||||||
|
case 'DELETE_RECORD': {
|
||||||
|
return IconAddressBook;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return assertUnreachable(data.actionType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -5,7 +5,7 @@ import { useCreateStep } from '../useCreateStep';
|
|||||||
const mockOpenRightDrawer = jest.fn();
|
const mockOpenRightDrawer = jest.fn();
|
||||||
const mockCreateDraftFromWorkflowVersion = jest.fn().mockResolvedValue('457');
|
const mockCreateDraftFromWorkflowVersion = jest.fn().mockResolvedValue('457');
|
||||||
const mockCreateWorkflowVersionStep = jest.fn().mockResolvedValue({
|
const mockCreateWorkflowVersionStep = jest.fn().mockResolvedValue({
|
||||||
data: { createWorkflowVersionStep: { id: '1' } },
|
data: { createWorkflowVersionStep: { id: '1', type: 'CODE' } },
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('recoil', () => ({
|
jest.mock('recoil', () => ({
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
WorkflowWithCurrentVersion,
|
WorkflowWithCurrentVersion,
|
||||||
} from '@/workflow/types/Workflow';
|
} from '@/workflow/types/Workflow';
|
||||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||||
|
import { getWorkflowNodeIcon } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIcon';
|
||||||
import { useCreateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep';
|
import { useCreateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep';
|
||||||
import { workflowCreateStepFromParentStepIdState } from '@/workflow/workflow-steps/states/workflowCreateStepFromParentStepIdState';
|
import { workflowCreateStepFromParentStepIdState } from '@/workflow/workflow-steps/states/workflowCreateStepFromParentStepIdState';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
@ -17,7 +18,6 @@ export const useCreateStep = ({
|
|||||||
}: {
|
}: {
|
||||||
workflow: WorkflowWithCurrentVersion;
|
workflow: WorkflowWithCurrentVersion;
|
||||||
}) => {
|
}) => {
|
||||||
const { openRightDrawer } = useRightDrawer();
|
|
||||||
const { createWorkflowVersionStep } = useCreateWorkflowVersionStep();
|
const { createWorkflowVersionStep } = useCreateWorkflowVersionStep();
|
||||||
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
||||||
const setWorkflowLastCreatedStepId = useSetRecoilState(
|
const setWorkflowLastCreatedStepId = useSetRecoilState(
|
||||||
@ -30,6 +30,8 @@ export const useCreateStep = ({
|
|||||||
|
|
||||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||||
|
|
||||||
|
const { openRightDrawer } = useRightDrawer();
|
||||||
|
|
||||||
const createStep = async (newStepType: WorkflowStepType) => {
|
const createStep = async (newStepType: WorkflowStepType) => {
|
||||||
if (!isDefined(workflowCreateStepFromParentStepId)) {
|
if (!isDefined(workflowCreateStepFromParentStepId)) {
|
||||||
throw new Error('Select a step to create a new step from first.');
|
throw new Error('Select a step to create a new step from first.');
|
||||||
@ -50,7 +52,14 @@ export const useCreateStep = ({
|
|||||||
|
|
||||||
setWorkflowSelectedNode(createdStep.id);
|
setWorkflowSelectedNode(createdStep.id);
|
||||||
setWorkflowLastCreatedStepId(createdStep.id);
|
setWorkflowLastCreatedStepId(createdStep.id);
|
||||||
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
|
|
||||||
|
openRightDrawer(RightDrawerPages.WorkflowStepEdit, {
|
||||||
|
title: createdStep.name,
|
||||||
|
Icon: getWorkflowNodeIcon({
|
||||||
|
nodeType: 'action',
|
||||||
|
actionType: createdStep.type as WorkflowStepType,
|
||||||
|
}),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -63,7 +63,10 @@ export const RightDrawerWorkflowSelectTriggerTypeContent = ({
|
|||||||
|
|
||||||
setWorkflowSelectedNode(TRIGGER_STEP_ID);
|
setWorkflowSelectedNode(TRIGGER_STEP_ID);
|
||||||
|
|
||||||
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
|
openRightDrawer(RightDrawerPages.WorkflowStepEdit, {
|
||||||
|
title: action.name,
|
||||||
|
Icon: action.icon,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -84,7 +87,10 @@ export const RightDrawerWorkflowSelectTriggerTypeContent = ({
|
|||||||
|
|
||||||
setWorkflowSelectedNode(TRIGGER_STEP_ID);
|
setWorkflowSelectedNode(TRIGGER_STEP_ID);
|
||||||
|
|
||||||
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
|
openRightDrawer(RightDrawerPages.WorkflowStepEdit, {
|
||||||
|
title: action.name,
|
||||||
|
Icon: action.icon,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user