Add first filter step version (#13093)

I rebuilt the advanced filters used in views and workflow search for a
specific filter step.

Components structure remains the same, using `stepFilterGroups` and
`stepFilters`. But those filters are directly sent to backend.

Also re-using the same kind of states we use for advanced filters to
share the current filters used. And a context to share what's coming
from workflow props (function to update step settings and readonly)

⚠️ this PR only focusses on the content of the step. There is still a
lot to do on the filter icon behavior in the workflow



https://github.com/user-attachments/assets/8a6a76f0-11fa-444a-82b9-71fc96b18af4
This commit is contained in:
Thomas Trompette
2025-07-09 13:52:07 +02:00
committed by GitHub
parent 0316c857d8
commit 6e79339e64
67 changed files with 3851 additions and 396 deletions
@@ -0,0 +1,152 @@
import { WorkflowStep } from '@/workflow/types/Workflow';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { findStepPosition } from '../findStepPosition';
describe('findStepPosition', () => {
const mockSteps: WorkflowStep[] = [
{
id: 'step-1',
name: 'First Step',
type: 'CREATE_RECORD',
valid: true,
settings: {
errorHandlingOptions: {
continueOnFailure: { value: false },
retryOnFailure: { value: false },
},
input: {
objectName: 'Company',
objectRecord: {},
},
outputSchema: {},
},
} as WorkflowStep,
{
id: 'step-2',
name: 'Second Step',
type: 'UPDATE_RECORD',
valid: true,
settings: {
errorHandlingOptions: {
continueOnFailure: { value: false },
retryOnFailure: { value: false },
},
input: {
objectName: 'Company',
objectRecord: {},
objectRecordId: 'test-id',
fieldsToUpdate: ['name'],
},
outputSchema: {},
},
} as WorkflowStep,
{
id: 'step-3',
name: 'Third Step',
type: 'DELETE_RECORD',
valid: true,
settings: {
errorHandlingOptions: {
continueOnFailure: { value: false },
retryOnFailure: { value: false },
},
input: {
objectName: 'Company',
objectRecordId: 'test-id',
},
outputSchema: {},
},
} as WorkflowStep,
];
it('should return index 0 when stepId is undefined', () => {
const result = findStepPosition({
steps: mockSteps,
stepId: undefined,
});
expect(result).toEqual({
steps: mockSteps,
index: 0,
});
});
it('should return index 0 when stepId is TRIGGER_STEP_ID', () => {
const result = findStepPosition({
steps: mockSteps,
stepId: TRIGGER_STEP_ID,
});
expect(result).toEqual({
steps: mockSteps,
index: 0,
});
});
it('should find the correct position for an existing step', () => {
const result = findStepPosition({
steps: mockSteps,
stepId: 'step-2',
});
expect(result).toEqual({
steps: mockSteps,
index: 1,
});
});
it('should find the first step position', () => {
const result = findStepPosition({
steps: mockSteps,
stepId: 'step-1',
});
expect(result).toEqual({
steps: mockSteps,
index: 0,
});
});
it('should find the last step position', () => {
const result = findStepPosition({
steps: mockSteps,
stepId: 'step-3',
});
expect(result).toEqual({
steps: mockSteps,
index: 2,
});
});
it('should return undefined for non-existent stepId', () => {
const result = findStepPosition({
steps: mockSteps,
stepId: 'non-existent-step',
});
expect(result).toBeUndefined();
});
it('should work with empty steps array', () => {
const result = findStepPosition({
steps: [],
stepId: 'step-1',
});
expect(result).toBeUndefined();
});
it('should work with single step array', () => {
const singleStep = [mockSteps[0]];
const result = findStepPosition({
steps: singleStep,
stepId: 'step-1',
});
expect(result).toEqual({
steps: singleStep,
index: 0,
});
});
});
@@ -0,0 +1,140 @@
import { WorkflowAction, WorkflowTrigger } from '@/workflow/types/Workflow';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { getStepDefinitionOrThrow } from '../getStepDefinitionOrThrow';
describe('getStepDefinitionOrThrow', () => {
const mockTrigger: WorkflowTrigger = {
type: 'DATABASE_EVENT',
settings: {
eventName: 'company.created',
outputSchema: {},
},
};
const mockSteps: WorkflowAction[] = [
{
id: 'step-1',
name: 'Create Record',
type: 'CREATE_RECORD',
valid: true,
settings: {
errorHandlingOptions: {
continueOnFailure: { value: false },
retryOnFailure: { value: false },
},
input: {
objectName: 'Company',
objectRecord: {},
},
outputSchema: {},
},
} as WorkflowAction,
{
id: 'step-2',
name: 'Update Record',
type: 'UPDATE_RECORD',
valid: true,
settings: {
errorHandlingOptions: {
continueOnFailure: { value: false },
retryOnFailure: { value: false },
},
input: {
objectName: 'Company',
objectRecord: {},
objectRecordId: 'test-id',
fieldsToUpdate: ['name'],
},
outputSchema: {},
},
} as WorkflowAction,
];
describe('when stepId is TRIGGER_STEP_ID', () => {
it('should return trigger definition when trigger is provided', () => {
const result = getStepDefinitionOrThrow({
stepId: TRIGGER_STEP_ID,
trigger: mockTrigger,
steps: mockSteps,
});
expect(result).toEqual({
type: 'trigger',
definition: mockTrigger,
});
});
it('should return undefined trigger definition when trigger is null', () => {
const result = getStepDefinitionOrThrow({
stepId: TRIGGER_STEP_ID,
trigger: null,
steps: mockSteps,
});
expect(result).toEqual({
type: 'trigger',
definition: undefined,
});
});
});
describe('when stepId is not TRIGGER_STEP_ID', () => {
it('should throw error when steps is null', () => {
expect(() => {
getStepDefinitionOrThrow({
stepId: 'step-1',
trigger: mockTrigger,
steps: null,
});
}).toThrow(
'Malformed workflow version: missing steps information; be sure to create at least one step before trying to edit one',
);
});
it('should return action definition for existing step', () => {
const result = getStepDefinitionOrThrow({
stepId: 'step-1',
trigger: mockTrigger,
steps: mockSteps,
});
expect(result).toEqual({
type: 'action',
definition: mockSteps[0],
});
});
it('should return action definition for second step', () => {
const result = getStepDefinitionOrThrow({
stepId: 'step-2',
trigger: mockTrigger,
steps: mockSteps,
});
expect(result).toEqual({
type: 'action',
definition: mockSteps[1],
});
});
it('should return undefined for non-existent step', () => {
const result = getStepDefinitionOrThrow({
stepId: 'non-existent-step',
trigger: mockTrigger,
steps: mockSteps,
});
expect(result).toBeUndefined();
});
it('should work with empty steps array', () => {
const result = getStepDefinitionOrThrow({
stepId: 'step-1',
trigger: mockTrigger,
steps: [],
});
expect(result).toBeUndefined();
});
});
});
@@ -0,0 +1,50 @@
import { getStepOutputSchemaFamilyStateKey } from '../getStepOutputSchemaFamilyStateKey';
describe('getStepOutputSchemaFamilyStateKey', () => {
it('should concatenate workflowVersionId and stepId with a dash', () => {
const workflowVersionId = 'workflow-version-123';
const stepId = 'step-456';
const result = getStepOutputSchemaFamilyStateKey(workflowVersionId, stepId);
expect(result).toBe('workflow-version-123-step-456');
});
it('should handle empty strings', () => {
const workflowVersionId = '';
const stepId = '';
const result = getStepOutputSchemaFamilyStateKey(workflowVersionId, stepId);
expect(result).toBe('-');
});
it('should handle UUID format IDs', () => {
const workflowVersionId = '550e8400-e29b-41d4-a716-446655440000';
const stepId = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
const result = getStepOutputSchemaFamilyStateKey(workflowVersionId, stepId);
expect(result).toBe(
'550e8400-e29b-41d4-a716-446655440000-6ba7b810-9dad-11d1-80b4-00c04fd430c8',
);
});
it('should handle special characters in IDs', () => {
const workflowVersionId = 'workflow_version.123';
const stepId = 'step@456';
const result = getStepOutputSchemaFamilyStateKey(workflowVersionId, stepId);
expect(result).toBe('workflow_version.123-step@456');
});
it('should handle numeric IDs', () => {
const workflowVersionId = '123';
const stepId = '456';
const result = getStepOutputSchemaFamilyStateKey(workflowVersionId, stepId);
expect(result).toBe('123-456');
});
});
@@ -0,0 +1,35 @@
import { getWorkflowVisualizerComponentInstanceId } from '../getWorkflowVisualizerComponentInstanceId';
describe('getWorkflowVisualizerComponentInstanceId', () => {
it('should return the same recordId that was passed in', () => {
const recordId = 'test-record-id-123';
const result = getWorkflowVisualizerComponentInstanceId({ recordId });
expect(result).toBe(recordId);
});
it('should return empty string when recordId is empty', () => {
const recordId = '';
const result = getWorkflowVisualizerComponentInstanceId({ recordId });
expect(result).toBe('');
});
it('should handle UUID format recordIds', () => {
const recordId = '550e8400-e29b-41d4-a716-446655440000';
const result = getWorkflowVisualizerComponentInstanceId({ recordId });
expect(result).toBe(recordId);
});
it('should handle special characters in recordId', () => {
const recordId = 'test-record-id_with.special@chars';
const result = getWorkflowVisualizerComponentInstanceId({ recordId });
expect(result).toBe(recordId);
});
});
@@ -0,0 +1,102 @@
import { splitWorkflowTriggerEventName } from '../splitWorkflowTriggerEventName';
describe('splitWorkflowTriggerEventName', () => {
it('should split a basic event name into objectType and event', () => {
const eventName = 'company.created';
const result = splitWorkflowTriggerEventName(eventName);
expect(result).toEqual({
objectType: 'company',
event: 'created',
});
});
it('should split event name with updated event', () => {
const eventName = 'person.updated';
const result = splitWorkflowTriggerEventName(eventName);
expect(result).toEqual({
objectType: 'person',
event: 'updated',
});
});
it('should split event name with deleted event', () => {
const eventName = 'opportunity.deleted';
const result = splitWorkflowTriggerEventName(eventName);
expect(result).toEqual({
objectType: 'opportunity',
event: 'deleted',
});
});
it('should handle camelCase object types', () => {
const eventName = 'activityTarget.created';
const result = splitWorkflowTriggerEventName(eventName);
expect(result).toEqual({
objectType: 'activityTarget',
event: 'created',
});
});
it('should handle event names with underscores', () => {
const eventName = 'custom_object.field_updated';
const result = splitWorkflowTriggerEventName(eventName);
expect(result).toEqual({
objectType: 'custom_object',
event: 'field_updated',
});
});
it('should handle event names without dots', () => {
const eventName = 'invalidEventName';
const result = splitWorkflowTriggerEventName(eventName);
expect(result).toEqual({
objectType: 'invalidEventName',
event: undefined,
});
});
it('should handle empty string', () => {
const eventName = '';
const result = splitWorkflowTriggerEventName(eventName);
expect(result).toEqual({
objectType: '',
event: undefined,
});
});
it('should handle event name starting with dot', () => {
const eventName = '.created';
const result = splitWorkflowTriggerEventName(eventName);
expect(result).toEqual({
objectType: '',
event: 'created',
});
});
it('should handle event name ending with dot', () => {
const eventName = 'company.';
const result = splitWorkflowTriggerEventName(eventName);
expect(result).toEqual({
objectType: 'company',
event: '',
});
});
});