fix: workflow node classification (#13945)
closes #13391 As the solution said to classify workflow nodes based on colors I have updated them with the following colors and new categories to better classify and identify different node types as told: - Trigger Colors: Data (palette blue), Other (palette purple) - Action Colors: Data (text/tertiary), AI (palette pink), Flow (tag green), Human Input (palette orange) <img width="451" height="690" alt="image" src="https://github.com/user-attachments/assets/8c000f22-8f25-4b1d-ac6f-f2639b1eff94" /> <img width="451" height="690" alt="image" src="https://github.com/user-attachments/assets/15a11a98-3fec-4374-a5f7-d7caf2ae5334" /> <img width="480" height="690" alt="image" src="https://github.com/user-attachments/assets/6382f538-be6d-4595-b967-e7911f7ad61f" /> <img width="899" height="761" alt="image" src="https://github.com/user-attachments/assets/98c97cd4-54cd-4bee-91b5-dd7cd3b03a5e" /> --------- Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
This commit is contained in:
co-authored by
Thomas Trompette
parent
ae160e8b15
commit
521ef5ea4c
+30
-21
@@ -1,4 +1,5 @@
|
||||
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
||||
import { WorkflowActionMenuItems } from '@/command-menu/pages/workflow/action/components/WorkflowActionMenuItems';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
@@ -12,13 +13,18 @@ import { RightDrawerStepListContainer } from '@/workflow/workflow-steps/componen
|
||||
import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle';
|
||||
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
|
||||
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
||||
import { AI_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/AiActions';
|
||||
import { CORE_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/CoreActions';
|
||||
import { HUMAN_INPUT_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/HumanInputActions';
|
||||
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
|
||||
import { useFilteredOtherActions } from '@/workflow/workflow-steps/workflow-actions/hooks/useFilteredOtherActions';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemCommand } from 'twenty-ui/navigation';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
export const CommandMenuWorkflowSelectActionContent = ({
|
||||
workflow,
|
||||
@@ -30,7 +36,6 @@ export const CommandMenuWorkflowSelectActionContent = ({
|
||||
const { createStep } = useCreateStep({
|
||||
workflow,
|
||||
});
|
||||
const filteredOtherActions = useFilteredOtherActions();
|
||||
|
||||
const { closeRightClickMenu } = useCloseRightClickMenu();
|
||||
|
||||
@@ -83,30 +88,34 @@ export const CommandMenuWorkflowSelectActionContent = ({
|
||||
);
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<RightDrawerStepListContainer>
|
||||
<RightDrawerWorkflowSelectStepTitle>
|
||||
Records
|
||||
{t`Data`}
|
||||
</RightDrawerWorkflowSelectStepTitle>
|
||||
{RECORD_ACTIONS.map((action) => (
|
||||
<MenuItemCommand
|
||||
key={action.type}
|
||||
LeftIcon={getIcon(action.icon)}
|
||||
text={action.label}
|
||||
onClick={() => handleCreateStep(action.type)}
|
||||
/>
|
||||
))}
|
||||
{WorkflowActionMenuItems(RECORD_ACTIONS, theme, handleCreateStep)}
|
||||
{isAiEnabled && (
|
||||
<>
|
||||
<RightDrawerWorkflowSelectStepTitle>
|
||||
{t`AI`}
|
||||
</RightDrawerWorkflowSelectStepTitle>
|
||||
{WorkflowActionMenuItems(AI_ACTIONS, theme, handleCreateStep)}
|
||||
</>
|
||||
)}
|
||||
<RightDrawerWorkflowSelectStepTitle>
|
||||
Other
|
||||
{t`Core`}
|
||||
</RightDrawerWorkflowSelectStepTitle>
|
||||
{filteredOtherActions.map((action) => (
|
||||
<MenuItemCommand
|
||||
key={action.type}
|
||||
LeftIcon={getIcon(action.icon)}
|
||||
text={action.label}
|
||||
onClick={() => handleCreateStep(action.type)}
|
||||
/>
|
||||
))}
|
||||
{WorkflowActionMenuItems(CORE_ACTIONS, theme, handleCreateStep)}
|
||||
<RightDrawerWorkflowSelectStepTitle>
|
||||
{t`Human Input`}
|
||||
</RightDrawerWorkflowSelectStepTitle>
|
||||
{WorkflowActionMenuItems(HUMAN_INPUT_ACTIONS, theme, handleCreateStep)}
|
||||
</RightDrawerStepListContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemCommand } from 'twenty-ui/navigation';
|
||||
|
||||
type Action = { type: WorkflowActionType; label: string; icon: string };
|
||||
|
||||
export const WorkflowActionMenuItems = (
|
||||
actions: Action[],
|
||||
theme: any,
|
||||
onClick: (actionType: WorkflowActionType) => void,
|
||||
) => {
|
||||
const { getIcon } = useIcons();
|
||||
return actions.map((action) => {
|
||||
const Icon = getIcon(action.icon);
|
||||
return (
|
||||
<MenuItemCommand
|
||||
key={action.type}
|
||||
LeftIcon={() => (
|
||||
<Icon
|
||||
color={getActionIconColorOrThrow({
|
||||
theme,
|
||||
actionType: action.type,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
text={action.label}
|
||||
onClick={() => onClick(action.type)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
+26
-16
@@ -13,6 +13,7 @@ import { OTHER_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/Other
|
||||
import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger';
|
||||
import { getTriggerDefaultDefinition } from '@/workflow/workflow-trigger/utils/getTriggerDefaultDefinition';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemCommand } from 'twenty-ui/navigation';
|
||||
@@ -67,30 +68,39 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = ({
|
||||
};
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<RightDrawerStepListContainer>
|
||||
<RightDrawerWorkflowSelectStepTitle>
|
||||
Data
|
||||
</RightDrawerWorkflowSelectStepTitle>
|
||||
{DATABASE_TRIGGER_TYPES.map((action) => (
|
||||
<MenuItemCommand
|
||||
key={action.defaultLabel}
|
||||
LeftIcon={getIcon(action.icon)}
|
||||
text={action.defaultLabel}
|
||||
onClick={handleTriggerTypeClick(action)}
|
||||
/>
|
||||
))}
|
||||
{DATABASE_TRIGGER_TYPES.map((action) => {
|
||||
const Icon = getIcon(action.icon);
|
||||
return (
|
||||
<MenuItemCommand
|
||||
key={action.defaultLabel}
|
||||
LeftIcon={() => <Icon color={theme.color.blue} />}
|
||||
text={action.defaultLabel}
|
||||
onClick={handleTriggerTypeClick(action)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<RightDrawerWorkflowSelectStepTitle>
|
||||
Others
|
||||
</RightDrawerWorkflowSelectStepTitle>
|
||||
{OTHER_TRIGGER_TYPES.map((action) => (
|
||||
<MenuItemCommand
|
||||
key={action.defaultLabel}
|
||||
LeftIcon={getIcon(action.icon)}
|
||||
text={action.defaultLabel}
|
||||
onClick={handleTriggerTypeClick(action)}
|
||||
/>
|
||||
))}
|
||||
{OTHER_TRIGGER_TYPES.map((action) => {
|
||||
const Icon = getIcon(action.icon);
|
||||
return (
|
||||
<MenuItemCommand
|
||||
key={action.defaultLabel}
|
||||
LeftIcon={() => <Icon color={theme.color.purple} />}
|
||||
text={action.defaultLabel}
|
||||
onClick={handleTriggerTypeClick(action)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</RightDrawerStepListContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+12
-11
@@ -16,13 +16,13 @@ export const WorkflowDiagramStepNodeIcon = ({
|
||||
switch (data.nodeType) {
|
||||
case 'trigger': {
|
||||
switch (data.triggerType) {
|
||||
case 'DATABASE_EVENT':
|
||||
case 'DATABASE_EVENT': {
|
||||
return <Icon size={theme.icon.size.md} color={theme.color.blue} />;
|
||||
}
|
||||
case 'MANUAL':
|
||||
case 'CRON':
|
||||
case 'WEBHOOK': {
|
||||
return (
|
||||
<Icon size={theme.icon.size.lg} color={theme.font.color.tertiary} />
|
||||
);
|
||||
return <Icon size={theme.icon.size.md} color={theme.color.purple} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,25 +31,26 @@ export const WorkflowDiagramStepNodeIcon = ({
|
||||
case 'action': {
|
||||
switch (data.actionType) {
|
||||
case 'CODE':
|
||||
case 'HTTP_REQUEST': {
|
||||
case 'HTTP_REQUEST':
|
||||
case 'SEND_EMAIL': {
|
||||
return (
|
||||
<Icon
|
||||
size={theme.icon.size.lg}
|
||||
color={theme.color.orange}
|
||||
size={theme.icon.size.md}
|
||||
color={theme.color.red}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'SEND_EMAIL': {
|
||||
return <Icon size={theme.icon.size.lg} color={theme.color.blue} />;
|
||||
case 'FORM': {
|
||||
return <Icon size={theme.icon.size.md} color={theme.color.orange} />;
|
||||
}
|
||||
case 'AI_AGENT': {
|
||||
return <Icon size={theme.icon.size.lg} color={theme.color.pink} />;
|
||||
return <Icon size={theme.icon.size.md} color={theme.color.pink} />;
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<Icon
|
||||
size={theme.icon.size.lg}
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
|
||||
+4
-1
@@ -56,7 +56,10 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
: getActionIcon(stepDefinition.definition.type);
|
||||
const headerIconColor =
|
||||
stepDefinition.type === 'trigger'
|
||||
? getTriggerIconColor({ theme })
|
||||
? getTriggerIconColor({
|
||||
theme,
|
||||
triggerType: stepDefinition.definition.type,
|
||||
})
|
||||
: getActionIconColorOrThrow({
|
||||
theme,
|
||||
actionType: stepDefinition.definition.type,
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
|
||||
export const AI_ACTIONS: Array<{
|
||||
label: string;
|
||||
type: Extract<WorkflowActionType, 'AI_AGENT'>;
|
||||
icon: string;
|
||||
}> = [
|
||||
{
|
||||
label: 'AI Agent',
|
||||
type: 'AI_AGENT',
|
||||
icon: 'IconBrain',
|
||||
},
|
||||
];
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
|
||||
export const CORE_ACTIONS: Array<{
|
||||
label: string;
|
||||
type: Extract<WorkflowActionType, 'CODE' | 'SEND_EMAIL' | 'HTTP_REQUEST'>;
|
||||
icon: string;
|
||||
}> = [
|
||||
{
|
||||
label: 'Send Email',
|
||||
type: 'SEND_EMAIL',
|
||||
icon: 'IconSend',
|
||||
},
|
||||
{
|
||||
label: 'Code',
|
||||
type: 'CODE',
|
||||
icon: 'IconCode',
|
||||
},
|
||||
{
|
||||
label: 'HTTP Request',
|
||||
type: 'HTTP_REQUEST',
|
||||
icon: 'IconWorld',
|
||||
},
|
||||
];
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
|
||||
export const HUMAN_INPUT_ACTIONS: Array<{
|
||||
label: string;
|
||||
type: Extract<WorkflowActionType, 'FORM'>;
|
||||
icon: string;
|
||||
}> = [
|
||||
{
|
||||
label: 'Form',
|
||||
type: 'FORM',
|
||||
icon: 'IconForms',
|
||||
},
|
||||
];
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useFilteredOtherActions } from '../useFilteredOtherActions';
|
||||
|
||||
jest.mock('@/workspace/hooks/useIsFeatureEnabled', () => ({
|
||||
useIsFeatureEnabled: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../constants/OtherActions', () => ({
|
||||
OTHER_ACTIONS: [
|
||||
{ type: 'CODE', icon: 'IconCode', label: 'Code' },
|
||||
{ type: 'HTTP_REQUEST', icon: 'IconHttp', label: 'HTTP Request' },
|
||||
{ type: 'SEND_EMAIL', icon: 'IconMail', label: 'Send Email' },
|
||||
{ type: 'AI_AGENT', icon: 'IconBrain', label: 'AI Agent' },
|
||||
{ type: 'FORM', icon: 'IconForm', label: 'Form' },
|
||||
],
|
||||
}));
|
||||
|
||||
describe('useFilteredOtherActions', () => {
|
||||
const mockUseIsFeatureEnabled = jest.mocked(
|
||||
jest.requireMock('@/workspace/hooks/useIsFeatureEnabled')
|
||||
.useIsFeatureEnabled,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return all actions when AI is enabled', () => {
|
||||
mockUseIsFeatureEnabled.mockReturnValue(true);
|
||||
|
||||
const { result } = renderHook(() => useFilteredOtherActions());
|
||||
|
||||
expect(result.current).toHaveLength(5);
|
||||
expect(result.current).toEqual([
|
||||
{ type: 'CODE', icon: 'IconCode', label: 'Code' },
|
||||
{ type: 'HTTP_REQUEST', icon: 'IconHttp', label: 'HTTP Request' },
|
||||
{ type: 'SEND_EMAIL', icon: 'IconMail', label: 'Send Email' },
|
||||
{ type: 'AI_AGENT', icon: 'IconBrain', label: 'AI Agent' },
|
||||
{ type: 'FORM', icon: 'IconForm', label: 'Form' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter out AI_AGENT when AI is disabled', () => {
|
||||
mockUseIsFeatureEnabled.mockReturnValue(false);
|
||||
|
||||
const { result } = renderHook(() => useFilteredOtherActions());
|
||||
|
||||
expect(result.current).toHaveLength(4);
|
||||
expect(result.current).toEqual([
|
||||
{ type: 'CODE', icon: 'IconCode', label: 'Code' },
|
||||
{ type: 'HTTP_REQUEST', icon: 'IconHttp', label: 'HTTP Request' },
|
||||
{ type: 'SEND_EMAIL', icon: 'IconMail', label: 'Send Email' },
|
||||
{ type: 'FORM', icon: 'IconForm', label: 'Form' },
|
||||
]);
|
||||
expect(
|
||||
result.current.find((action) => action.type === 'AI_AGENT'),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should call useIsFeatureEnabled with correct feature flag', () => {
|
||||
mockUseIsFeatureEnabled.mockReturnValue(true);
|
||||
|
||||
renderHook(() => useFilteredOtherActions());
|
||||
|
||||
expect(mockUseIsFeatureEnabled).toHaveBeenCalledWith('IS_AI_ENABLED');
|
||||
expect(mockUseIsFeatureEnabled).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle feature flag hook returning undefined', () => {
|
||||
mockUseIsFeatureEnabled.mockReturnValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useFilteredOtherActions());
|
||||
|
||||
expect(result.current).toHaveLength(4);
|
||||
expect(
|
||||
result.current.find((action) => action.type === 'AI_AGENT'),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle feature flag hook returning null', () => {
|
||||
mockUseIsFeatureEnabled.mockReturnValue(null);
|
||||
|
||||
const { result } = renderHook(() => useFilteredOtherActions());
|
||||
|
||||
expect(result.current).toHaveLength(4);
|
||||
expect(
|
||||
result.current.find((action) => action.type === 'AI_AGENT'),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle feature flag hook returning false string', () => {
|
||||
mockUseIsFeatureEnabled.mockReturnValue('false');
|
||||
|
||||
const { result } = renderHook(() => useFilteredOtherActions());
|
||||
|
||||
expect(result.current).toHaveLength(5);
|
||||
expect(
|
||||
result.current.find((action) => action.type === 'AI_AGENT'),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle feature flag hook throwing error', () => {
|
||||
mockUseIsFeatureEnabled.mockImplementation(() => {
|
||||
throw new Error('Feature flag error');
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => useFilteredOtherActions());
|
||||
}).toThrow('Feature flag error');
|
||||
});
|
||||
});
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { OTHER_ACTIONS } from '../constants/OtherActions';
|
||||
|
||||
export const useFilteredOtherActions = () => {
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
|
||||
return OTHER_ACTIONS.filter((action) => {
|
||||
return action.type !== 'AI_AGENT' || isAiEnabled;
|
||||
});
|
||||
};
|
||||
+14
-4
@@ -1,4 +1,6 @@
|
||||
import { OTHER_ACTIONS } from '../../constants/OtherActions';
|
||||
import { AI_ACTIONS } from '../../constants/AiActions';
|
||||
import { CORE_ACTIONS } from '../../constants/CoreActions';
|
||||
import { HUMAN_INPUT_ACTIONS } from '../../constants/HumanInputActions';
|
||||
import { RECORD_ACTIONS } from '../../constants/RecordActions';
|
||||
import { getActionIcon } from '../getActionIcon';
|
||||
|
||||
@@ -8,13 +10,21 @@ describe('getActionIcon', () => {
|
||||
expect(getActionIcon(action.type)).toBe(action.icon);
|
||||
});
|
||||
|
||||
OTHER_ACTIONS.forEach((action) => {
|
||||
AI_ACTIONS.forEach((action) => {
|
||||
expect(getActionIcon(action.type)).toBe(action.icon);
|
||||
});
|
||||
|
||||
CORE_ACTIONS.forEach((action) => {
|
||||
expect(getActionIcon(action.type)).toBe(action.icon);
|
||||
});
|
||||
|
||||
HUMAN_INPUT_ACTIONS.forEach((action) => {
|
||||
expect(getActionIcon(action.type)).toBe(action.icon);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for unknown action type', () => {
|
||||
it('should return IconDefault for unknown action type', () => {
|
||||
// @ts-expect-error Testing invalid action type
|
||||
expect(getActionIcon('UNKNOWN_ACTION')).toBeUndefined();
|
||||
expect(getActionIcon('UNKNOWN_ACTION')).toBe('IconDefault');
|
||||
});
|
||||
});
|
||||
|
||||
+34
-23
@@ -6,8 +6,8 @@ import { getActionIconColorOrThrow } from '../getActionIconColorOrThrow';
|
||||
const mockTheme: Theme = {
|
||||
color: {
|
||||
orange: COLOR.orange,
|
||||
blue: COLOR.blue,
|
||||
pink: COLOR.pink,
|
||||
red: COLOR.red,
|
||||
},
|
||||
font: {
|
||||
color: {
|
||||
@@ -17,10 +17,23 @@ const mockTheme: Theme = {
|
||||
} as Theme;
|
||||
|
||||
describe('getActionIconColorOrThrow', () => {
|
||||
it('should return orange color for CODE action type', () => {
|
||||
expect(
|
||||
getActionIconColorOrThrow({ theme: mockTheme, actionType: 'CODE' }),
|
||||
).toBe(mockTheme.color.orange);
|
||||
describe('action types that return red color', () => {
|
||||
const coreActionTypes: WorkflowActionType[] = [
|
||||
'CODE',
|
||||
'HTTP_REQUEST',
|
||||
'SEND_EMAIL',
|
||||
];
|
||||
|
||||
coreActionTypes.forEach((actionType) => {
|
||||
it(`should return red color for ${actionType} action type`, () => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
|
||||
expect(result).toBe(mockTheme.color.red);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('action types that return tertiary font color', () => {
|
||||
@@ -29,7 +42,6 @@ describe('getActionIconColorOrThrow', () => {
|
||||
'UPDATE_RECORD',
|
||||
'DELETE_RECORD',
|
||||
'FIND_RECORDS',
|
||||
'FORM',
|
||||
];
|
||||
|
||||
recordActionTypes.forEach((actionType) => {
|
||||
@@ -44,14 +56,14 @@ describe('getActionIconColorOrThrow', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('action types that return blue color', () => {
|
||||
it('should return blue color for SEND_EMAIL action type', () => {
|
||||
describe('action types that return orange color', () => {
|
||||
it('should return orange color for FORM action type', () => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'SEND_EMAIL',
|
||||
actionType: 'FORM',
|
||||
});
|
||||
|
||||
expect(result).toBe(mockTheme.color.blue);
|
||||
expect(result).toBe(mockTheme.color.orange);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,8 +93,8 @@ describe('getActionIconColorOrThrow', () => {
|
||||
it('should use the provided theme colors correctly', () => {
|
||||
const customTheme: Theme = {
|
||||
color: {
|
||||
orange: COLOR.red,
|
||||
blue: COLOR.purple,
|
||||
red: COLOR.red,
|
||||
orange: COLOR.orange,
|
||||
pink: COLOR.turquoise,
|
||||
},
|
||||
font: {
|
||||
@@ -104,7 +116,7 @@ describe('getActionIconColorOrThrow', () => {
|
||||
theme: customTheme,
|
||||
actionType: 'SEND_EMAIL',
|
||||
}),
|
||||
).toBe(COLOR.purple);
|
||||
).toBe(COLOR.red);
|
||||
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
@@ -158,12 +170,12 @@ describe('getActionIconColorOrThrow', () => {
|
||||
});
|
||||
|
||||
expect(result1).toBe(result2);
|
||||
expect(result1).toBe(mockTheme.color.orange);
|
||||
expect(result1).toBe(mockTheme.color.red);
|
||||
});
|
||||
});
|
||||
|
||||
describe('color grouping logic', () => {
|
||||
it('should group CODE and HTTP_REQUEST actions with orange color', () => {
|
||||
it('should group CODE and HTTP_REQUEST actions with red color', () => {
|
||||
const orangeActions: WorkflowActionType[] = ['CODE', 'HTTP_REQUEST'];
|
||||
|
||||
orangeActions.forEach((actionType) => {
|
||||
@@ -171,7 +183,7 @@ describe('getActionIconColorOrThrow', () => {
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
expect(result).toBe(mockTheme.color.orange);
|
||||
expect(result).toBe(mockTheme.color.red);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -181,7 +193,6 @@ describe('getActionIconColorOrThrow', () => {
|
||||
'UPDATE_RECORD',
|
||||
'DELETE_RECORD',
|
||||
'FIND_RECORDS',
|
||||
'FORM',
|
||||
];
|
||||
|
||||
recordActions.forEach((actionType) => {
|
||||
@@ -202,13 +213,13 @@ describe('getActionIconColorOrThrow', () => {
|
||||
expect(tertiaryResult).toBe(mockTheme.font.color.tertiary);
|
||||
});
|
||||
|
||||
it('should return blue color for SEND_EMAIL action type', () => {
|
||||
it('should return red color for SEND_EMAIL action type', () => {
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'SEND_EMAIL',
|
||||
}),
|
||||
).toBe(mockTheme.color.blue);
|
||||
).toBe(mockTheme.color.red);
|
||||
});
|
||||
|
||||
it('should return pink color for AI_AGENT action type', () => {
|
||||
@@ -220,8 +231,8 @@ describe('getActionIconColorOrThrow', () => {
|
||||
it('should use the provided theme colors correctly', () => {
|
||||
const customTheme: Theme = {
|
||||
color: {
|
||||
orange: COLOR.red,
|
||||
blue: COLOR.purple,
|
||||
red: COLOR.red,
|
||||
orange: COLOR.orange,
|
||||
pink: COLOR.turquoise,
|
||||
},
|
||||
font: {
|
||||
@@ -239,7 +250,7 @@ describe('getActionIconColorOrThrow', () => {
|
||||
theme: customTheme,
|
||||
actionType: 'SEND_EMAIL',
|
||||
}),
|
||||
).toBe(COLOR.purple);
|
||||
).toBe(COLOR.red);
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
@@ -254,7 +265,7 @@ describe('getActionIconColorOrThrow', () => {
|
||||
).toBe(GRAY_SCALE.gray50);
|
||||
});
|
||||
|
||||
it('should return undefined when blue color is missing for SEND_EMAIL action', () => {
|
||||
it('should return undefined when red color is missing for SEND_EMAIL action', () => {
|
||||
const themeWithoutBlue: Theme = {
|
||||
color: {
|
||||
orange: COLOR.orange,
|
||||
|
||||
+12
-2
@@ -1,5 +1,7 @@
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
import { OTHER_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/OtherActions';
|
||||
import { AI_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/AiActions';
|
||||
import { CORE_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/CoreActions';
|
||||
import { HUMAN_INPUT_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/HumanInputActions';
|
||||
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
|
||||
|
||||
export const getActionIcon = (actionType: WorkflowActionType) => {
|
||||
@@ -11,7 +13,15 @@ export const getActionIcon = (actionType: WorkflowActionType) => {
|
||||
return RECORD_ACTIONS.find((item) => item.type === actionType)?.icon;
|
||||
case 'FILTER':
|
||||
return 'IconFilter';
|
||||
case 'AI_AGENT':
|
||||
return AI_ACTIONS.find((item) => item.type === actionType)?.icon;
|
||||
case 'CODE':
|
||||
case 'HTTP_REQUEST':
|
||||
case 'SEND_EMAIL':
|
||||
return CORE_ACTIONS.find((item) => item.type === actionType)?.icon;
|
||||
case 'FORM':
|
||||
return HUMAN_INPUT_ACTIONS.find((item) => item.type === actionType)?.icon;
|
||||
default:
|
||||
return OTHER_ACTIONS.find((item) => item.type === actionType)?.icon;
|
||||
return 'IconDefault';
|
||||
}
|
||||
};
|
||||
|
||||
+4
-3
@@ -12,16 +12,17 @@ export const getActionIconColorOrThrow = ({
|
||||
switch (actionType) {
|
||||
case 'CODE':
|
||||
case 'HTTP_REQUEST':
|
||||
return theme.color.orange;
|
||||
case 'SEND_EMAIL':
|
||||
return theme.color.red;
|
||||
case 'CREATE_RECORD':
|
||||
case 'UPDATE_RECORD':
|
||||
case 'DELETE_RECORD':
|
||||
case 'FIND_RECORDS':
|
||||
return theme.font.color.tertiary;
|
||||
case 'FORM':
|
||||
return theme.color.orange;
|
||||
case 'FILTER':
|
||||
return theme.font.color.tertiary;
|
||||
case 'SEND_EMAIL':
|
||||
return theme.color.blue;
|
||||
case 'AI_AGENT':
|
||||
return theme.color.pink;
|
||||
default:
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@ import { getCronTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNumber } from '@sniptt/guards';
|
||||
@@ -75,7 +76,7 @@ export const WorkflowEditTriggerCronForm = ({
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
iconColor={getTriggerIconColor({ theme, triggerType: trigger.type })}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={triggerOptions.readonly}
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@ import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/Workflo
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
@@ -181,7 +182,7 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
iconColor={getTriggerIconColor({ theme, triggerType: trigger.type })}
|
||||
initialTitle={defaultLabel}
|
||||
headerType={headerType}
|
||||
disabled={triggerOptions.readonly}
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/uti
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
@@ -103,7 +104,7 @@ export const WorkflowEditTriggerManualForm = ({
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
iconColor={getTriggerIconColor({ theme, triggerType: trigger.type })}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={triggerOptions.readonly}
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ import { WEBHOOK_TRIGGER_HTTP_METHOD_OPTIONS } from '@/workflow/workflow-trigger
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
|
||||
import { getWebhookTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getWebhookTriggerDefaultSettings';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
@@ -95,7 +96,7 @@ export const WorkflowEditTriggerWebhookForm = ({
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
iconColor={getTriggerIconColor({ theme, triggerType: trigger.type })}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={triggerOptions.readonly}
|
||||
|
||||
+36
-30
@@ -1,56 +1,62 @@
|
||||
/* eslint-disable @nx/workspace-no-hardcoded-colors */
|
||||
import { type Theme } from '@emotion/react';
|
||||
import { COLOR } from 'twenty-ui/theme';
|
||||
import { getTriggerIconColor } from '../getTriggerIconColor';
|
||||
|
||||
describe('getTriggerIconColor', () => {
|
||||
const mockTheme: Theme = {
|
||||
font: {
|
||||
color: {
|
||||
primary: '#2c2c2c',
|
||||
secondary: '#666666',
|
||||
tertiary: '#999999',
|
||||
light: '#cccccc',
|
||||
},
|
||||
color: {
|
||||
blue: COLOR.blue,
|
||||
purple: COLOR.purple,
|
||||
},
|
||||
} as unknown as Theme;
|
||||
|
||||
it('returns the tertiary font color from theme', () => {
|
||||
const result = getTriggerIconColor({ theme: mockTheme });
|
||||
it('returns the blue color for database event from theme', () => {
|
||||
const result = getTriggerIconColor({
|
||||
theme: mockTheme,
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
});
|
||||
|
||||
expect(result).toBe('#999999');
|
||||
expect(result).toBe(COLOR.blue);
|
||||
});
|
||||
|
||||
it('returns the purple color for cron from theme', () => {
|
||||
const result = getTriggerIconColor({
|
||||
theme: mockTheme,
|
||||
triggerType: 'CRON',
|
||||
});
|
||||
|
||||
expect(result).toBe(COLOR.purple);
|
||||
});
|
||||
|
||||
it('works with different theme configurations', () => {
|
||||
const differentTheme: Theme = {
|
||||
font: {
|
||||
color: {
|
||||
primary: '#000000',
|
||||
secondary: '#444444',
|
||||
tertiary: '#888888',
|
||||
light: '#ffffff',
|
||||
},
|
||||
color: {
|
||||
blue: COLOR.blue,
|
||||
purple: COLOR.purple,
|
||||
},
|
||||
} as unknown as Theme;
|
||||
|
||||
const result = getTriggerIconColor({ theme: differentTheme });
|
||||
const result = getTriggerIconColor({
|
||||
theme: differentTheme,
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
});
|
||||
|
||||
expect(result).toBe('#888888');
|
||||
expect(result).toBe(COLOR.blue);
|
||||
});
|
||||
|
||||
it('maintains reference to theme.font.color.tertiary', () => {
|
||||
it('maintains reference to theme.color.blue', () => {
|
||||
const customTheme: Theme = {
|
||||
font: {
|
||||
color: {
|
||||
primary: '#111111',
|
||||
secondary: '#333333',
|
||||
tertiary: '#custom-tertiary-color',
|
||||
light: '#eeeeee',
|
||||
},
|
||||
color: {
|
||||
blue: COLOR.blue,
|
||||
purple: COLOR.purple,
|
||||
},
|
||||
} as unknown as Theme;
|
||||
|
||||
const result = getTriggerIconColor({ theme: customTheme });
|
||||
const result = getTriggerIconColor({
|
||||
theme: customTheme,
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
});
|
||||
|
||||
expect(result).toBe('#custom-tertiary-color');
|
||||
expect(result).toBe(COLOR.blue);
|
||||
});
|
||||
});
|
||||
|
||||
+18
-2
@@ -1,5 +1,21 @@
|
||||
import { type Theme } from '@emotion/react';
|
||||
import { type WorkflowTriggerType } from '@/workflow/types/Workflow';
|
||||
|
||||
export const getTriggerIconColor = ({ theme }: { theme: Theme }) => {
|
||||
return theme.font.color.tertiary;
|
||||
export const getTriggerIconColor = ({
|
||||
theme,
|
||||
triggerType,
|
||||
}: {
|
||||
theme: Theme;
|
||||
triggerType: WorkflowTriggerType;
|
||||
}) => {
|
||||
switch (triggerType) {
|
||||
case 'DATABASE_EVENT':
|
||||
return theme.color.blue;
|
||||
case 'CRON':
|
||||
case 'MANUAL':
|
||||
case 'WEBHOOK':
|
||||
return theme.color.purple;
|
||||
default:
|
||||
return theme.color.purple;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user