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:
P Harshith Rao
2025-08-20 15:01:03 +05:30
committed by GitHub
parent ae160e8b15
commit 521ef5ea4c
20 changed files with 279 additions and 239 deletions
@@ -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,
@@ -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',
},
];
@@ -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',
},
];
@@ -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',
},
];
@@ -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');
});
});
@@ -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;
});
};
@@ -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');
});
});
@@ -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,
@@ -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';
}
};
@@ -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: