Raphaël Bosi
2025-01-23 14:44:21 +01:00
committed by GitHub
parent cc53cb3b7b
commit 337b6a86ab
21 changed files with 582 additions and 83 deletions

View File

@ -5,6 +5,7 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { IconCalendarEvent } from 'twenty-ui';
export const useOpenCalendarEventRightDrawer = () => {
const { openRightDrawer } = useRightDrawer();
@ -13,7 +14,10 @@ export const useOpenCalendarEventRightDrawer = () => {
const openCalendarEventRightDrawer = (calendarEventId: string) => {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
openRightDrawer(RightDrawerPages.ViewCalendarEvent);
openRightDrawer(RightDrawerPages.ViewCalendarEvent, {
title: 'Calendar Event',
Icon: IconCalendarEvent,
});
setViewableRecordId(calendarEventId);
};

View File

@ -2,6 +2,7 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { IconSparkles } from 'twenty-ui';
export const useOpenCopilotRightDrawer = () => {
const { openRightDrawer } = useRightDrawer();
@ -9,6 +10,9 @@ export const useOpenCopilotRightDrawer = () => {
return () => {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
openRightDrawer(RightDrawerPages.Copilot);
openRightDrawer(RightDrawerPages.Copilot, {
title: 'Copilot',
Icon: IconSparkles,
});
};
};

View File

@ -4,6 +4,7 @@ import { act } from 'react-dom/test-utils';
import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { IconMail } from 'twenty-ui';
const mockOpenRightDrawer = jest.fn();
const mockSetHotkeyScope = jest.fn();
@ -31,5 +32,9 @@ test('useOpenEmailThreadRightDrawer opens the email thread right drawer', () =>
);
expect(mockOpenRightDrawer).toHaveBeenCalledWith(
RightDrawerPages.ViewEmailThread,
{
title: 'Email Thread',
Icon: IconMail,
},
);
});

View File

@ -2,6 +2,7 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { IconMail } from 'twenty-ui';
export const useOpenEmailThreadRightDrawer = () => {
const { openRightDrawer } = useRightDrawer();
@ -9,6 +10,9 @@ export const useOpenEmailThreadRightDrawer = () => {
return () => {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
openRightDrawer(RightDrawerPages.ViewEmailThread);
openRightDrawer(RightDrawerPages.ViewEmailThread, {
title: 'Email Thread',
Icon: IconMail,
});
};
};

View File

@ -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>
);
};

View File

@ -1,30 +1,10 @@
import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip';
import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars';
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
import styled from '@emotion/styled';
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 = ({
objectMetadataItemId,
}: {
@ -43,21 +23,25 @@ export const CommandMenuContextRecordChip = ({
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 (
<StyledChip>
<StyledAvatarContainer>
{records.map((record) => (
<CommandMenuContextRecordChipAvatars
objectMetadataItem={objectMetadataItem}
key={record.id}
record={record}
/>
))}
</StyledAvatarContainer>
{totalCount === 1
? getObjectRecordIdentifier({ objectMetadataItem, record: records[0] })
.name
: `${totalCount} ${capitalize(objectMetadataItem.namePlural)}`}
</StyledChip>
<CommandMenuContextChip
text={text}
Icons={Avatars}
withIconBackground={true}
/>
);
};

View File

@ -3,22 +3,8 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
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 = ({
objectMetadataItem,
record,
@ -38,7 +24,7 @@ export const CommandMenuContextRecordChipAvatars = ({
const theme = useTheme();
return (
<StyledAvatarWrapper>
<>
{Icon ? (
<Icon color={IconColor} size={theme.icon.size.sm} />
) : (
@ -50,6 +36,6 @@ export const CommandMenuContextRecordChipAvatars = ({
size="sm"
/>
)}
</StyledAvatarWrapper>
</>
);
};

View File

@ -1,12 +1,15 @@
import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip';
import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip';
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil';
import { IconX, LightIconButton, isDefined, useIsMobile } from 'twenty-ui';
@ -82,6 +85,10 @@ export const CommandMenuTopBar = () => {
const commandMenuPage = useRecoilValue(commandMenuPageState);
const { title, Icon } = useRecoilValue(commandMenuPageInfoState);
const theme = useTheme();
return (
<StyledInputContainer>
<StyledContentContainer>
@ -90,6 +97,13 @@ export const CommandMenuTopBar = () => {
objectMetadataItemId={contextStoreCurrentObjectMetadataId}
/>
)}
{isDefined(Icon) && (
<CommandMenuContextChip
Icons={[<Icon size={theme.icon.size.sm} />]}
text={title}
/>
)}
{commandMenuPage === CommandMenuPages.Root && (
<StyledInput
autoFocus

View File

@ -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} />],
},
};

View File

@ -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>
);
},
],
};

View File

@ -9,6 +9,7 @@ import { isDefined } from '~/utils/isDefined';
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
@ -213,6 +214,10 @@ export const useCommandMenu = () => {
set(viewableRecordIdState, null);
set(commandMenuPageState, CommandMenuPages.Root);
set(commandMenuPageInfoState, {
title: undefined,
Icon: undefined,
});
set(isCommandMenuOpenedState, false);
resetSelectedItem();
goBackToPreviousHotkeyScope();
@ -278,6 +283,11 @@ export const useCommandMenu = () => {
}),
null,
);
set(commandMenuPageInfoState, {
title: undefined,
Icon: undefined,
});
};
}, []);

View File

@ -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 },
});

View File

@ -5,9 +5,11 @@ import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/righ
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
import { mapRightDrawerPageToCommandMenuPage } from '@/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { IconComponent } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
import { rightDrawerPageState } from '../states/rightDrawerPageState';
@ -27,12 +29,22 @@ export const useRightDrawer = () => {
const openRightDrawer = useRecoilCallback(
({ set }) =>
(rightDrawerPage: RightDrawerPages) => {
(
rightDrawerPage: RightDrawerPages,
commandMenuPageInfo?: {
title?: string;
Icon?: IconComponent;
},
) => {
if (isCommandMenuV2Enabled) {
const commandMenuPage =
mapRightDrawerPageToCommandMenuPage(rightDrawerPage);
set(commandMenuPageState, commandMenuPage);
set(commandMenuPageInfoState, {
title: commandMenuPageInfo?.title,
Icon: commandMenuPageInfo?.Icon,
});
openCommandMenu();
return;
}

View File

@ -8,11 +8,15 @@ import { EMPTY_TRIGGER_STEP_ID } from '@/workflow/workflow-diagram/constants/Emp
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { useTriggerNodeSelection } from '@/workflow/workflow-diagram/hooks/useTriggerNodeSelection';
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 { useCallback } from 'react';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-ui';
import { IconBolt, isDefined } from 'twenty-ui';
export const WorkflowDiagramCanvasEditableEffect = () => {
const { startNodeCreation } = useStartNodeCreation();
@ -37,7 +41,10 @@ export const WorkflowDiagramCanvasEditableEffect = () => {
const isEmptyTriggerNode = selectedNode.type === EMPTY_TRIGGER_STEP_ID;
if (isEmptyTriggerNode) {
openRightDrawer(RightDrawerPages.WorkflowStepSelectTriggerType);
openRightDrawer(RightDrawerPages.WorkflowStepSelectTriggerType, {
title: 'Trigger Type',
Icon: IconBolt,
});
return;
}
@ -53,9 +60,14 @@ export const WorkflowDiagramCanvasEditableEffect = () => {
return;
}
const selectedNodeData = selectedNode.data as WorkflowDiagramStepNodeData;
setWorkflowSelectedNode(selectedNode.id);
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
openRightDrawer(RightDrawerPages.WorkflowStepEdit, {
title: selectedNodeData.name,
Icon: getWorkflowNodeIcon(selectedNodeData),
});
},
[
setWorkflowSelectedNode,

View File

@ -5,7 +5,11 @@ import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPage
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useTriggerNodeSelection } from '@/workflow/workflow-diagram/hooks/useTriggerNodeSelection';
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 { useCallback } from 'react';
import { useSetRecoilState } from 'recoil';
@ -30,7 +34,12 @@ export const WorkflowDiagramCanvasReadonlyEffect = () => {
setWorkflowSelectedNode(selectedNode.id);
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
openRightDrawer(RightDrawerPages.WorkflowStepView);
const selectedNodeData = selectedNode.data as WorkflowDiagramStepNodeData;
openRightDrawer(RightDrawerPages.WorkflowStepView, {
title: selectedNodeData.name,
Icon: getWorkflowNodeIcon(selectedNodeData),
});
},
[
setWorkflowSelectedNode,

View File

@ -1,15 +1,9 @@
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { WorkflowDiagramBaseStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseStepNode';
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowNodeIcon } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIcon';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
IconAddressBook,
IconCode,
IconHandMove,
IconMail,
IconPlaylistAdd,
} from 'twenty-ui';
const StyledStepNodeLabelIconContainer = styled.div`
align-items: center;
@ -29,6 +23,8 @@ export const WorkflowDiagramStepNodeBase = ({
}) => {
const theme = useTheme();
const Icon = getWorkflowNodeIcon(data);
const renderStepIcon = () => {
switch (data.nodeType) {
case 'trigger': {
@ -36,7 +32,7 @@ export const WorkflowDiagramStepNodeBase = ({
case 'DATABASE_EVENT': {
return (
<StyledStepNodeLabelIconContainer>
<IconPlaylistAdd
<Icon
size={theme.icon.size.lg}
color={theme.font.color.tertiary}
/>
@ -46,7 +42,7 @@ export const WorkflowDiagramStepNodeBase = ({
case 'MANUAL': {
return (
<StyledStepNodeLabelIconContainer>
<IconHandMove
<Icon
size={theme.icon.size.lg}
color={theme.font.color.tertiary}
/>
@ -62,17 +58,14 @@ export const WorkflowDiagramStepNodeBase = ({
case 'CODE': {
return (
<StyledStepNodeLabelIconContainer>
<IconCode
size={theme.icon.size.lg}
color={theme.color.orange}
/>
<Icon size={theme.icon.size.lg} color={theme.color.orange} />
</StyledStepNodeLabelIconContainer>
);
}
case 'SEND_EMAIL': {
return (
<StyledStepNodeLabelIconContainer>
<IconMail size={theme.icon.size.lg} color={theme.color.blue} />
<Icon size={theme.icon.size.lg} color={theme.color.blue} />
</StyledStepNodeLabelIconContainer>
);
}
@ -81,7 +74,7 @@ export const WorkflowDiagramStepNodeBase = ({
case 'DELETE_RECORD': {
return (
<StyledStepNodeLabelIconContainer>
<IconAddressBook
<Icon
size={theme.icon.size.lg}
color={theme.font.color.tertiary}
stroke={theme.icon.stroke.sm}

View File

@ -5,6 +5,7 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope
import { workflowCreateStepFromParentStepIdState } from '@/workflow/workflow-steps/states/workflowCreateStepFromParentStepIdState';
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil';
import { IconSettingsAutomation } from 'twenty-ui';
export const useStartNodeCreation = () => {
const { openRightDrawer } = useRightDrawer();
@ -22,7 +23,10 @@ export const useStartNodeCreation = () => {
setWorkflowCreateStepFromParentStepId(parentNodeId);
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
openRightDrawer(RightDrawerPages.WorkflowStepSelectAction);
openRightDrawer(RightDrawerPages.WorkflowStepSelectAction, {
title: 'Select Action',
Icon: IconSettingsAutomation,
});
},
[openRightDrawer, setWorkflowCreateStepFromParentStepId, setHotkeyScope],
);

View File

@ -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);
}
}
};

View File

@ -5,7 +5,7 @@ import { useCreateStep } from '../useCreateStep';
const mockOpenRightDrawer = jest.fn();
const mockCreateDraftFromWorkflowVersion = jest.fn().mockResolvedValue('457');
const mockCreateWorkflowVersionStep = jest.fn().mockResolvedValue({
data: { createWorkflowVersionStep: { id: '1' } },
data: { createWorkflowVersionStep: { id: '1', type: 'CODE' } },
});
jest.mock('recoil', () => ({

View File

@ -7,6 +7,7 @@ import {
WorkflowWithCurrentVersion,
} from '@/workflow/types/Workflow';
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 { workflowCreateStepFromParentStepIdState } from '@/workflow/workflow-steps/states/workflowCreateStepFromParentStepIdState';
import { useRecoilValue, useSetRecoilState } from 'recoil';
@ -17,7 +18,6 @@ export const useCreateStep = ({
}: {
workflow: WorkflowWithCurrentVersion;
}) => {
const { openRightDrawer } = useRightDrawer();
const { createWorkflowVersionStep } = useCreateWorkflowVersionStep();
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
const setWorkflowLastCreatedStepId = useSetRecoilState(
@ -30,6 +30,8 @@ export const useCreateStep = ({
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
const { openRightDrawer } = useRightDrawer();
const createStep = async (newStepType: WorkflowStepType) => {
if (!isDefined(workflowCreateStepFromParentStepId)) {
throw new Error('Select a step to create a new step from first.');
@ -50,7 +52,14 @@ export const useCreateStep = ({
setWorkflowSelectedNode(createdStep.id);
setWorkflowLastCreatedStepId(createdStep.id);
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
openRightDrawer(RightDrawerPages.WorkflowStepEdit, {
title: createdStep.name,
Icon: getWorkflowNodeIcon({
nodeType: 'action',
actionType: createdStep.type as WorkflowStepType,
}),
});
};
return {

View File

@ -63,7 +63,10 @@ export const RightDrawerWorkflowSelectTriggerTypeContent = ({
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);
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
openRightDrawer(RightDrawerPages.WorkflowStepEdit, {
title: action.name,
Icon: action.icon,
});
}}
/>
))}