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:
@ -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: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -141,7 +141,8 @@ export const workflowAiAgentActionSettingsSchema =
|
||||
export const workflowFilterActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
filter: z.record(z.any()),
|
||||
stepFilterGroups: z.array(z.any()),
|
||||
stepFilters: z.array(z.any()),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
@ -7,8 +8,10 @@ import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||
import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramPanOnDragComponentState';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
@ -99,6 +102,12 @@ export const WorkflowDiagramEdgeV2Content = ({
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
|
||||
workflowVisualizerWorkflowIdComponentState,
|
||||
);
|
||||
|
||||
const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu();
|
||||
|
||||
const handleCreateFilter = async () => {
|
||||
await onCreateFilter();
|
||||
|
||||
@ -106,6 +115,23 @@ export const WorkflowDiagramEdgeV2Content = ({
|
||||
setHovered(false);
|
||||
};
|
||||
|
||||
const setWorkflowSelectedNode = useSetRecoilComponentStateV2(
|
||||
workflowSelectedNodeComponentState,
|
||||
);
|
||||
|
||||
const handleFilterButtonClick = () => {
|
||||
setWorkflowSelectedNode(stepId);
|
||||
if (isDefined(filter) && isDefined(workflowVisualizerWorkflowId)) {
|
||||
openWorkflowEditStepInCommandMenu(
|
||||
workflowVisualizerWorkflowId,
|
||||
'Filter',
|
||||
IconFilter,
|
||||
);
|
||||
} else {
|
||||
handleCreateFilter();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||
@ -134,9 +160,7 @@ export const WorkflowDiagramEdgeV2Content = ({
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconFilterPlus,
|
||||
onClick: () => {
|
||||
handleCreateFilter();
|
||||
},
|
||||
onClick: handleFilterButtonClick,
|
||||
},
|
||||
{
|
||||
Icon: IconDotsVertical,
|
||||
|
||||
@ -0,0 +1,136 @@
|
||||
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { addEdgeOptions } from '../addEdgeOptions';
|
||||
|
||||
describe('addEdgeOptions', () => {
|
||||
it('should add shouldDisplayEdgeOptions to all edges', () => {
|
||||
const diagram: WorkflowDiagram = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
name: 'Company Created',
|
||||
icon: 'IconPlus',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'action-1',
|
||||
position: { x: 0, y: 100 },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CREATE_RECORD',
|
||||
name: 'Create Company',
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'edge-1',
|
||||
source: 'trigger',
|
||||
target: 'action-1',
|
||||
data: {
|
||||
shouldDisplayEdgeOptions: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'edge-2',
|
||||
source: 'action-1',
|
||||
target: 'action-2',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = addEdgeOptions(diagram);
|
||||
|
||||
expect(result.nodes).toEqual(diagram.nodes);
|
||||
expect(result.edges).toHaveLength(2);
|
||||
|
||||
expect(result.edges[0]).toEqual({
|
||||
id: 'edge-1',
|
||||
source: 'trigger',
|
||||
target: 'action-1',
|
||||
data: {
|
||||
shouldDisplayEdgeOptions: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.edges[1]).toEqual({
|
||||
id: 'edge-2',
|
||||
source: 'action-1',
|
||||
target: 'action-2',
|
||||
data: {
|
||||
shouldDisplayEdgeOptions: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty edges array', () => {
|
||||
const diagram: WorkflowDiagram = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
name: 'Company Created',
|
||||
icon: 'IconPlus',
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
};
|
||||
|
||||
const result = addEdgeOptions(diagram);
|
||||
|
||||
expect(result.nodes).toEqual(diagram.nodes);
|
||||
expect(result.edges).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle edges without existing data property', () => {
|
||||
const diagram: WorkflowDiagram = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'MANUAL',
|
||||
name: 'Manual Trigger',
|
||||
icon: 'IconClick',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'action-1',
|
||||
position: { x: 0, y: 100 },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Send Email',
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'edge-1',
|
||||
source: 'trigger',
|
||||
target: 'action-1',
|
||||
} as any,
|
||||
],
|
||||
};
|
||||
|
||||
const result = addEdgeOptions(diagram);
|
||||
|
||||
expect(result.edges[0]).toEqual({
|
||||
id: 'edge-1',
|
||||
source: 'trigger',
|
||||
target: 'action-1',
|
||||
data: {
|
||||
shouldDisplayEdgeOptions: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,136 @@
|
||||
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getWorkflowNodeIconKey } from '../getWorkflowNodeIconKey';
|
||||
|
||||
// Mock the getActionIcon function
|
||||
jest.mock(
|
||||
'@/workflow/workflow-steps/workflow-actions/utils/getActionIcon',
|
||||
() => ({
|
||||
getActionIcon: jest.fn((actionType) => {
|
||||
const mockIcons = {
|
||||
CREATE_RECORD: 'IconPlus',
|
||||
UPDATE_RECORD: 'IconEdit',
|
||||
DELETE_RECORD: 'IconTrash',
|
||||
SEND_EMAIL: 'IconMail',
|
||||
FILTER: 'IconFilter',
|
||||
};
|
||||
return mockIcons[actionType as keyof typeof mockIcons] || 'IconQuestion';
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
describe('getWorkflowNodeIconKey', () => {
|
||||
describe('trigger nodes', () => {
|
||||
it('should return the icon from trigger node data', () => {
|
||||
const triggerNodeData: WorkflowDiagramStepNodeData = {
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
name: 'Company Created',
|
||||
icon: 'IconDatabase',
|
||||
};
|
||||
|
||||
const result = getWorkflowNodeIconKey(triggerNodeData);
|
||||
|
||||
expect(result).toBe('IconDatabase');
|
||||
});
|
||||
|
||||
it('should return icon for manual trigger', () => {
|
||||
const manualTriggerData: WorkflowDiagramStepNodeData = {
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'MANUAL',
|
||||
name: 'Manual Trigger',
|
||||
icon: 'IconClick',
|
||||
};
|
||||
|
||||
const result = getWorkflowNodeIconKey(manualTriggerData);
|
||||
|
||||
expect(result).toBe('IconClick');
|
||||
});
|
||||
|
||||
it('should return icon for webhook trigger', () => {
|
||||
const webhookTriggerData: WorkflowDiagramStepNodeData = {
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'WEBHOOK',
|
||||
name: 'Webhook Trigger',
|
||||
icon: 'IconWebhook',
|
||||
};
|
||||
|
||||
const result = getWorkflowNodeIconKey(webhookTriggerData);
|
||||
|
||||
expect(result).toBe('IconWebhook');
|
||||
});
|
||||
});
|
||||
|
||||
describe('action nodes', () => {
|
||||
it('should return icon for CREATE_RECORD action', () => {
|
||||
const createActionData: WorkflowDiagramStepNodeData = {
|
||||
nodeType: 'action',
|
||||
actionType: 'CREATE_RECORD',
|
||||
name: 'Create Company',
|
||||
};
|
||||
|
||||
const result = getWorkflowNodeIconKey(createActionData);
|
||||
|
||||
expect(result).toBe('IconPlus');
|
||||
});
|
||||
|
||||
it('should return icon for UPDATE_RECORD action', () => {
|
||||
const updateActionData: WorkflowDiagramStepNodeData = {
|
||||
nodeType: 'action',
|
||||
actionType: 'UPDATE_RECORD',
|
||||
name: 'Update Company',
|
||||
};
|
||||
|
||||
const result = getWorkflowNodeIconKey(updateActionData);
|
||||
|
||||
expect(result).toBe('IconEdit');
|
||||
});
|
||||
|
||||
it('should return icon for DELETE_RECORD action', () => {
|
||||
const deleteActionData: WorkflowDiagramStepNodeData = {
|
||||
nodeType: 'action',
|
||||
actionType: 'DELETE_RECORD',
|
||||
name: 'Delete Company',
|
||||
};
|
||||
|
||||
const result = getWorkflowNodeIconKey(deleteActionData);
|
||||
|
||||
expect(result).toBe('IconTrash');
|
||||
});
|
||||
|
||||
it('should return icon for SEND_EMAIL action', () => {
|
||||
const emailActionData: WorkflowDiagramStepNodeData = {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Send Email',
|
||||
};
|
||||
|
||||
const result = getWorkflowNodeIconKey(emailActionData);
|
||||
|
||||
expect(result).toBe('IconMail');
|
||||
});
|
||||
|
||||
it('should return icon for FILTER action', () => {
|
||||
const filterActionData: WorkflowDiagramStepNodeData = {
|
||||
nodeType: 'action',
|
||||
actionType: 'FILTER',
|
||||
name: 'Filter Records',
|
||||
};
|
||||
|
||||
const result = getWorkflowNodeIconKey(filterActionData);
|
||||
|
||||
expect(result).toBe('IconFilter');
|
||||
});
|
||||
|
||||
it('should handle unknown action types', () => {
|
||||
const unknownActionData: WorkflowDiagramStepNodeData = {
|
||||
nodeType: 'action',
|
||||
actionType: 'UNKNOWN_ACTION' as any,
|
||||
name: 'Unknown Action',
|
||||
};
|
||||
|
||||
const result = getWorkflowNodeIconKey(unknownActionData);
|
||||
|
||||
expect(result).toBe('IconQuestion');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,132 @@
|
||||
import { WorkflowDiagramNode } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { isCreateStepNode } from '../isCreateStepNode';
|
||||
|
||||
describe('isCreateStepNode', () => {
|
||||
it('should return true for create-step node with correct type and nodeType', () => {
|
||||
const createStepNode: WorkflowDiagramNode = {
|
||||
id: 'create-step-1',
|
||||
type: 'create-step',
|
||||
position: { x: 0, y: 200 },
|
||||
data: {
|
||||
nodeType: 'create-step',
|
||||
parentNodeId: 'action-1',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(createStepNode);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for node with create-step type but wrong nodeType', () => {
|
||||
const node: WorkflowDiagramNode = {
|
||||
id: 'fake-create-step',
|
||||
type: 'create-step',
|
||||
position: { x: 0, y: 200 },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CREATE_RECORD',
|
||||
name: 'Create Company',
|
||||
} as any,
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(node);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for node with correct nodeType but wrong type', () => {
|
||||
const node: WorkflowDiagramNode = {
|
||||
id: 'fake-create-step',
|
||||
type: 'action',
|
||||
position: { x: 0, y: 200 },
|
||||
data: {
|
||||
nodeType: 'create-step',
|
||||
parentNodeId: 'action-1',
|
||||
} as any,
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(node);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for action node', () => {
|
||||
const actionNode: WorkflowDiagramNode = {
|
||||
id: 'action-1',
|
||||
position: { x: 0, y: 100 },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CREATE_RECORD',
|
||||
name: 'Create Company',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(actionNode);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for trigger node', () => {
|
||||
const triggerNode: WorkflowDiagramNode = {
|
||||
id: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
name: 'Company Created',
|
||||
icon: 'IconPlus',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(triggerNode);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty-trigger node', () => {
|
||||
const emptyTriggerNode: WorkflowDiagramNode = {
|
||||
id: 'empty-trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
nodeType: 'empty-trigger',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(emptyTriggerNode);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle create-step node with additional properties', () => {
|
||||
const createStepNodeWithExtras: WorkflowDiagramNode = {
|
||||
id: 'create-step-with-extras',
|
||||
type: 'create-step',
|
||||
position: { x: 50, y: 250 },
|
||||
selected: true,
|
||||
data: {
|
||||
nodeType: 'create-step',
|
||||
parentNodeId: 'action-2',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(createStepNodeWithExtras);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for node without type property', () => {
|
||||
const nodeWithoutType: WorkflowDiagramNode = {
|
||||
id: 'node-without-type',
|
||||
position: { x: 0, y: 200 },
|
||||
data: {
|
||||
nodeType: 'create-step',
|
||||
parentNodeId: 'action-1',
|
||||
} as any,
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(nodeWithoutType);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,84 @@
|
||||
import { WorkflowDiagramNode } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { isStepNode } from '../isStepNode';
|
||||
|
||||
describe('isStepNode', () => {
|
||||
it('should return true for trigger node', () => {
|
||||
const triggerNode: WorkflowDiagramNode = {
|
||||
id: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
name: 'Company Created',
|
||||
icon: 'IconPlus',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isStepNode(triggerNode);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for action node', () => {
|
||||
const actionNode: WorkflowDiagramNode = {
|
||||
id: 'action-1',
|
||||
position: { x: 0, y: 100 },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CREATE_RECORD',
|
||||
name: 'Create Company',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isStepNode(actionNode);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for create-step node', () => {
|
||||
const createStepNode: WorkflowDiagramNode = {
|
||||
id: 'create-step-1',
|
||||
position: { x: 0, y: 200 },
|
||||
data: {
|
||||
nodeType: 'create-step',
|
||||
parentNodeId: 'action-1',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isStepNode(createStepNode);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty-trigger node', () => {
|
||||
const emptyTriggerNode: WorkflowDiagramNode = {
|
||||
id: 'empty-trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
nodeType: 'empty-trigger',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isStepNode(emptyTriggerNode);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle nodes with additional properties', () => {
|
||||
const nodeWithExtra: WorkflowDiagramNode = {
|
||||
id: 'trigger-with-extra',
|
||||
position: { x: 50, y: 50 },
|
||||
selected: true,
|
||||
data: {
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'MANUAL',
|
||||
name: 'Manual Trigger',
|
||||
icon: 'IconClick',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isStepNode(nodeWithExtra);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
@ -7,6 +7,7 @@ import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workfl
|
||||
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
||||
import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
|
||||
import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord';
|
||||
import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
|
||||
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords';
|
||||
import { WorkflowEditActionFormBuilder } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder';
|
||||
import { WorkflowEditActionHttpRequest } from '@/workflow/workflow-steps/workflow-actions/http-request-action/components/WorkflowEditActionHttpRequest';
|
||||
@ -185,8 +186,12 @@ export const WorkflowStepDetail = ({
|
||||
);
|
||||
}
|
||||
case 'FILTER': {
|
||||
throw new Error(
|
||||
"The Filter action isn't meant to be displayed as a node.",
|
||||
return (
|
||||
<WorkflowEditActionFilter
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,239 @@
|
||||
import { WorkflowStep } from '@/workflow/types/Workflow';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import { getWorkflowPreviousStepId } from '../getWorkflowPreviousStepId';
|
||||
|
||||
describe('getWorkflowPreviousStepId', () => {
|
||||
const mockSteps: WorkflowStep[] = [
|
||||
{
|
||||
id: 'step-1',
|
||||
name: 'First Step',
|
||||
type: 'CREATE_RECORD',
|
||||
valid: true,
|
||||
nextStepIds: ['step-2'],
|
||||
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,
|
||||
nextStepIds: ['step-3'],
|
||||
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: 'SEND_EMAIL',
|
||||
valid: true,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
connectedAccountId: 'account-id',
|
||||
email: 'test@example.com',
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
} as WorkflowStep,
|
||||
];
|
||||
|
||||
it('should return undefined for TRIGGER_STEP_ID', () => {
|
||||
const result = getWorkflowPreviousStepId({
|
||||
stepId: TRIGGER_STEP_ID,
|
||||
steps: mockSteps,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return TRIGGER_STEP_ID for first step', () => {
|
||||
const result = getWorkflowPreviousStepId({
|
||||
stepId: 'step-1',
|
||||
steps: mockSteps,
|
||||
});
|
||||
|
||||
expect(result).toBe(TRIGGER_STEP_ID);
|
||||
});
|
||||
|
||||
it('should return previous step ID for middle step', () => {
|
||||
const result = getWorkflowPreviousStepId({
|
||||
stepId: 'step-2',
|
||||
steps: mockSteps,
|
||||
});
|
||||
|
||||
expect(result).toBe('step-1');
|
||||
});
|
||||
|
||||
it('should return previous step ID for last step', () => {
|
||||
const result = getWorkflowPreviousStepId({
|
||||
stepId: 'step-3',
|
||||
steps: mockSteps,
|
||||
});
|
||||
|
||||
expect(result).toBe('step-2');
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent step', () => {
|
||||
const result = getWorkflowPreviousStepId({
|
||||
stepId: 'non-existent-step',
|
||||
steps: mockSteps,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should work with single step', () => {
|
||||
const singleStep = [mockSteps[0]];
|
||||
|
||||
const result = getWorkflowPreviousStepId({
|
||||
stepId: 'step-1',
|
||||
steps: singleStep,
|
||||
});
|
||||
|
||||
expect(result).toBe(TRIGGER_STEP_ID);
|
||||
});
|
||||
|
||||
it('should handle branching workflow with multiple next steps', () => {
|
||||
const branchingSteps: WorkflowStep[] = [
|
||||
{
|
||||
id: 'step-1',
|
||||
name: 'First Step',
|
||||
type: 'CREATE_RECORD',
|
||||
valid: true,
|
||||
nextStepIds: ['step-2a', 'step-2b'],
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
objectName: 'Company',
|
||||
objectRecord: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
} as WorkflowStep,
|
||||
{
|
||||
id: 'step-2a',
|
||||
name: 'Second Step A',
|
||||
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-2b',
|
||||
name: 'Second Step B',
|
||||
type: 'SEND_EMAIL',
|
||||
valid: true,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: { value: false },
|
||||
retryOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
connectedAccountId: 'account-id',
|
||||
email: 'test@example.com',
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
} as WorkflowStep,
|
||||
];
|
||||
|
||||
const resultA = getWorkflowPreviousStepId({
|
||||
stepId: 'step-2a',
|
||||
steps: branchingSteps,
|
||||
});
|
||||
|
||||
const resultB = getWorkflowPreviousStepId({
|
||||
stepId: 'step-2b',
|
||||
steps: branchingSteps,
|
||||
});
|
||||
|
||||
expect(resultA).toBe('step-1');
|
||||
expect(resultB).toBe('step-1');
|
||||
});
|
||||
|
||||
it('should handle workflow where step is not connected to previous steps', () => {
|
||||
const disconnectedSteps: 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: 'Disconnected 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,
|
||||
];
|
||||
|
||||
const result = getWorkflowPreviousStepId({
|
||||
stepId: 'step-2',
|
||||
steps: disconnectedSteps,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,87 @@
|
||||
import { WorkflowFilterAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { WorkflowEditActionFilterBody } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBody';
|
||||
import { WorkflowEditActionFilterBodyEffect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBodyEffect';
|
||||
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFilterGroupsComponentInstanceContext';
|
||||
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFiltersComponentInstanceContext';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import {
|
||||
StepFilter,
|
||||
StepFilterGroup,
|
||||
} from 'twenty-shared/src/types/StepFilters';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
type WorkflowEditActionFilterProps = {
|
||||
action: WorkflowFilterAction;
|
||||
actionOptions:
|
||||
| {
|
||||
readonly: true;
|
||||
}
|
||||
| {
|
||||
readonly?: false;
|
||||
onActionUpdate: (action: WorkflowFilterAction) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export type FilterSettings = {
|
||||
stepFilterGroups?: StepFilterGroup[];
|
||||
stepFilters?: StepFilter[];
|
||||
};
|
||||
|
||||
export const WorkflowEditActionFilter = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionFilterProps) => {
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: 'Filter',
|
||||
});
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={actionOptions.readonly}
|
||||
/>
|
||||
<StepFiltersComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: action.id,
|
||||
}}
|
||||
>
|
||||
<StepFilterGroupsComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: action.id,
|
||||
}}
|
||||
>
|
||||
<WorkflowEditActionFilterBody
|
||||
action={action}
|
||||
actionOptions={actionOptions}
|
||||
/>
|
||||
<WorkflowEditActionFilterBodyEffect
|
||||
stepId={action.id}
|
||||
defaultValue={{
|
||||
stepFilterGroups: action.settings.input.stepFilterGroups,
|
||||
stepFilters: action.settings.input.stepFilters,
|
||||
}}
|
||||
/>
|
||||
</StepFilterGroupsComponentInstanceContext.Provider>
|
||||
</StepFiltersComponentInstanceContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,117 @@
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { WorkflowFilterAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
|
||||
import { WorkflowStepFilterAddFilterRuleSelect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterAddFilterRuleSelect';
|
||||
import { WorkflowStepFilterAddRootStepFilterButton } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterAddRootStepFilterButton';
|
||||
import { WorkflowStepFilterColumn } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterColumn';
|
||||
import { WorkflowStepFilterGroupColumn } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupColumn';
|
||||
import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useChildStepFiltersAndChildStepFilterGroups';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { rootLevelStepFilterGroupComponentSelector } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/rootLevelStepFilterGroupComponentSelector';
|
||||
import { isStepFilterGroupChildAStepFilterGroup } from '@/workflow/workflow-steps/workflow-actions/filter-action/utils/isStepFilterGroupChildAStepFilterGroup';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledChildContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(6)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type WorkflowEditActionFilterBodyProps = {
|
||||
action: WorkflowFilterAction;
|
||||
actionOptions:
|
||||
| {
|
||||
readonly: true;
|
||||
}
|
||||
| {
|
||||
readonly?: false;
|
||||
onActionUpdate: (action: WorkflowFilterAction) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export const WorkflowEditActionFilterBody = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionFilterBodyProps) => {
|
||||
const rootStepFilterGroup = useRecoilComponentValueV2(
|
||||
rootLevelStepFilterGroupComponentSelector,
|
||||
);
|
||||
|
||||
const { childStepFiltersAndChildStepFilterGroups } =
|
||||
useChildStepFiltersAndChildStepFilterGroups({
|
||||
stepFilterGroupId: rootStepFilterGroup?.id ?? '',
|
||||
});
|
||||
|
||||
const onFilterSettingsUpdate = (newFilterSettings: FilterSettings) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: {
|
||||
stepFilterGroups: newFilterSettings.stepFilterGroups ?? [],
|
||||
stepFilters: newFilterSettings.stepFilters ?? [],
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<WorkflowStepFilterContext.Provider
|
||||
value={{
|
||||
stepId: action.id,
|
||||
readonly: actionOptions.readonly,
|
||||
onFilterSettingsUpdate,
|
||||
}}
|
||||
>
|
||||
<WorkflowStepBody>
|
||||
{isDefined(rootStepFilterGroup) ? (
|
||||
<StyledContainer>
|
||||
<StyledChildContainer>
|
||||
{childStepFiltersAndChildStepFilterGroups.map(
|
||||
(stepFilterGroupChild, stepFilterGroupChildIndex) =>
|
||||
isStepFilterGroupChildAStepFilterGroup(
|
||||
stepFilterGroupChild,
|
||||
) ? (
|
||||
<WorkflowStepFilterGroupColumn
|
||||
key={stepFilterGroupChild.id}
|
||||
parentStepFilterGroup={rootStepFilterGroup}
|
||||
stepFilterGroup={stepFilterGroupChild}
|
||||
stepFilterGroupIndex={stepFilterGroupChildIndex}
|
||||
/>
|
||||
) : (
|
||||
<WorkflowStepFilterColumn
|
||||
key={stepFilterGroupChild.id}
|
||||
stepFilterGroup={rootStepFilterGroup}
|
||||
stepFilter={stepFilterGroupChild}
|
||||
stepFilterIndex={stepFilterGroupChildIndex}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</StyledChildContainer>
|
||||
{!actionOptions.readonly && (
|
||||
<WorkflowStepFilterAddFilterRuleSelect
|
||||
stepFilterGroup={rootStepFilterGroup}
|
||||
/>
|
||||
)}
|
||||
</StyledContainer>
|
||||
) : (
|
||||
<WorkflowStepFilterAddRootStepFilterButton />
|
||||
)}
|
||||
</WorkflowStepBody>
|
||||
</WorkflowStepFilterContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,74 @@
|
||||
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
|
||||
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFilterGroupsComponentState';
|
||||
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFiltersComponentState';
|
||||
import { hasInitializedCurrentStepFilterGroupsComponentFamilyState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/hasInitializedCurrentStepFilterGroupsComponentFamilyState';
|
||||
import { hasInitializedCurrentStepFiltersComponentFamilyState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/hasInitializedCurrentStepFiltersComponentFamilyState';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const WorkflowEditActionFilterBodyEffect = ({
|
||||
stepId,
|
||||
defaultValue,
|
||||
}: {
|
||||
stepId: string;
|
||||
defaultValue?: FilterSettings;
|
||||
}) => {
|
||||
const [
|
||||
hasInitializedCurrentStepFilters,
|
||||
setHasInitializedCurrentStepFilters,
|
||||
] = useRecoilComponentFamilyStateV2(
|
||||
hasInitializedCurrentStepFiltersComponentFamilyState,
|
||||
{ stepId },
|
||||
);
|
||||
|
||||
const [
|
||||
hasInitializedCurrentStepFilterGroups,
|
||||
setHasInitializedCurrentStepFilterGroups,
|
||||
] = useRecoilComponentFamilyStateV2(
|
||||
hasInitializedCurrentStepFilterGroupsComponentFamilyState,
|
||||
{ stepId },
|
||||
);
|
||||
|
||||
const setCurrentStepFilters = useSetRecoilComponentStateV2(
|
||||
currentStepFiltersComponentState,
|
||||
);
|
||||
|
||||
const setCurrentStepFilterGroups = useSetRecoilComponentStateV2(
|
||||
currentStepFilterGroupsComponentState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!hasInitializedCurrentStepFilters &&
|
||||
isDefined(defaultValue?.stepFilters)
|
||||
) {
|
||||
setCurrentStepFilters(defaultValue.stepFilters ?? []);
|
||||
setHasInitializedCurrentStepFilters(true);
|
||||
}
|
||||
}, [
|
||||
setCurrentStepFilters,
|
||||
hasInitializedCurrentStepFilters,
|
||||
setHasInitializedCurrentStepFilters,
|
||||
defaultValue?.stepFilters,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!hasInitializedCurrentStepFilterGroups &&
|
||||
isDefined(defaultValue?.stepFilterGroups) &&
|
||||
defaultValue.stepFilterGroups.length > 0
|
||||
) {
|
||||
setCurrentStepFilterGroups(defaultValue.stepFilterGroups ?? []);
|
||||
setHasInitializedCurrentStepFilterGroups(true);
|
||||
}
|
||||
}, [
|
||||
setCurrentStepFilterGroups,
|
||||
hasInitializedCurrentStepFilterGroups,
|
||||
setHasInitializedCurrentStepFilterGroups,
|
||||
defaultValue?.stepFilterGroups,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -0,0 +1,144 @@
|
||||
import { ActionButton } from '@/action-menu/actions/display/components/ActionButton';
|
||||
import { getAdvancedFilterAddFilterRuleSelectDropdownId } from '@/object-record/advanced-filter/utils/getAdvancedFilterAddFilterRuleSelectDropdownId';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useChildStepFiltersAndChildStepFilterGroups';
|
||||
import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useUpsertStepFilterSettings';
|
||||
import {
|
||||
StepFilter,
|
||||
StepFilterGroup,
|
||||
StepLogicalOperator,
|
||||
StepOperand,
|
||||
} from 'twenty-shared/src/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconLibraryPlus, IconPlus } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
type WorkflowStepFilterAddFilterRuleSelectProps = {
|
||||
stepFilterGroup: StepFilterGroup;
|
||||
};
|
||||
|
||||
export const WorkflowStepFilterAddFilterRuleSelect = ({
|
||||
stepFilterGroup,
|
||||
}: WorkflowStepFilterAddFilterRuleSelectProps) => {
|
||||
const { upsertStepFilterSettings } = useUpsertStepFilterSettings();
|
||||
|
||||
const dropdownId = getAdvancedFilterAddFilterRuleSelectDropdownId(
|
||||
stepFilterGroup.id,
|
||||
);
|
||||
|
||||
const { lastChildPosition } = useChildStepFiltersAndChildStepFilterGroups({
|
||||
stepFilterGroupId: stepFilterGroup.id,
|
||||
});
|
||||
|
||||
const newPositionInStepFilterGroup = lastChildPosition + 1;
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const handleAddFilter = () => {
|
||||
closeDropdown(dropdownId);
|
||||
|
||||
const newStepFilter = {
|
||||
id: v4(),
|
||||
type: 'text',
|
||||
label: 'New Filter',
|
||||
value: '',
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: '',
|
||||
stepFilterGroupId: stepFilterGroup.id,
|
||||
stepOutputKey: '',
|
||||
positionInStepFilterGroup: newPositionInStepFilterGroup,
|
||||
};
|
||||
|
||||
upsertStepFilterSettings({
|
||||
stepFilterToUpsert: newStepFilter,
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddFilterGroup = () => {
|
||||
closeDropdown(dropdownId);
|
||||
|
||||
const newStepFilterGroupId = v4();
|
||||
|
||||
const newStepFilterGroup: StepFilterGroup = {
|
||||
id: newStepFilterGroupId,
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
parentStepFilterGroupId: stepFilterGroup.id,
|
||||
positionInStepFilterGroup: newPositionInStepFilterGroup,
|
||||
};
|
||||
|
||||
const newStepFilter: StepFilter = {
|
||||
id: v4(),
|
||||
type: 'text',
|
||||
operand: StepOperand.EQ,
|
||||
value: '',
|
||||
displayValue: '',
|
||||
stepFilterGroupId: newStepFilterGroupId,
|
||||
positionInStepFilterGroup: 1,
|
||||
label: 'New Filter',
|
||||
stepOutputKey: '',
|
||||
};
|
||||
|
||||
upsertStepFilterSettings({
|
||||
stepFilterToUpsert: newStepFilter,
|
||||
stepFilterGroupToUpsert: newStepFilterGroup,
|
||||
});
|
||||
};
|
||||
|
||||
const isFilterRuleGroupOptionVisible = !isDefined(
|
||||
stepFilterGroup.parentStepFilterGroupId,
|
||||
);
|
||||
|
||||
if (!isFilterRuleGroupOptionVisible) {
|
||||
return (
|
||||
<ActionButton
|
||||
action={{
|
||||
Icon: IconPlus,
|
||||
label: 'Add rule',
|
||||
shortLabel: 'Add rule',
|
||||
key: 'add-rule',
|
||||
}}
|
||||
onClick={handleAddFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
clickableComponent={
|
||||
<ActionButton
|
||||
action={{
|
||||
Icon: IconPlus,
|
||||
label: 'Add filter rule',
|
||||
shortLabel: 'Add filter rule',
|
||||
key: 'add-filter-rule',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
LeftIcon={IconPlus}
|
||||
text="Add rule"
|
||||
onClick={handleAddFilter}
|
||||
/>
|
||||
{isFilterRuleGroupOptionVisible && (
|
||||
<MenuItem
|
||||
LeftIcon={IconLibraryPlus}
|
||||
text="Add rule group"
|
||||
onClick={handleAddFilterGroup}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
dropdownOffset={{ y: 8, x: 0 }}
|
||||
dropdownPlacement="bottom-start"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,25 @@
|
||||
import { useAddRootStepFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useAddRootStepFilter';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext } from 'react';
|
||||
import { IconFilter } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
|
||||
export const WorkflowStepFilterAddRootStepFilterButton = () => {
|
||||
const { t } = useLingui();
|
||||
const { readonly } = useContext(WorkflowStepFilterContext);
|
||||
const { addRootStepFilter } = useAddRootStepFilter();
|
||||
|
||||
return (
|
||||
<Button
|
||||
Icon={IconFilter}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
onClick={addRootStepFilter}
|
||||
ariaLabel={t`Add first filter`}
|
||||
title={t`Add first filter`}
|
||||
disabled={readonly}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,48 @@
|
||||
import { WorkflowStepFilterFieldSelect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterFieldSelect';
|
||||
import { WorkflowStepFilterLogicalOperatorCell } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterLogicalOperatorCell';
|
||||
import { WorkflowStepFilterOperandSelect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterOperandSelect';
|
||||
import { WorkflowStepFilterOptionsDropdown } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterOptionsDropdown';
|
||||
import { WorkflowStepFilterValueInput } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { WorkflowAdvancedFilterDropdownColumn } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterDropdownColumn';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
import { StepFilter, StepFilterGroup } from 'twenty-shared/src/types';
|
||||
|
||||
type WorkflowStepFilterColumnProps = {
|
||||
stepFilterGroup: StepFilterGroup;
|
||||
stepFilter: StepFilter;
|
||||
stepFilterIndex: number;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const WorkflowStepFilterColumn = ({
|
||||
stepFilterGroup,
|
||||
stepFilter,
|
||||
stepFilterIndex,
|
||||
}: WorkflowStepFilterColumnProps) => {
|
||||
const { readonly } = useContext(WorkflowStepFilterContext);
|
||||
|
||||
return (
|
||||
<WorkflowAdvancedFilterDropdownColumn>
|
||||
<StyledContainer>
|
||||
<WorkflowStepFilterLogicalOperatorCell
|
||||
index={stepFilterIndex}
|
||||
stepFilterGroup={stepFilterGroup}
|
||||
/>
|
||||
{!readonly && (
|
||||
<WorkflowStepFilterOptionsDropdown stepFilterId={stepFilter.id} />
|
||||
)}
|
||||
</StyledContainer>
|
||||
<WorkflowStepFilterFieldSelect stepFilter={stepFilter} />
|
||||
<WorkflowStepFilterOperandSelect stepFilter={stepFilter} />
|
||||
<WorkflowStepFilterValueInput stepFilter={stepFilter} />
|
||||
</WorkflowAdvancedFilterDropdownColumn>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,79 @@
|
||||
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||
import { useWorkflowStepContextOrThrow } from '@/workflow/states/context/WorkflowStepContext';
|
||||
import { stepsOutputSchemaFamilySelector } from '@/workflow/states/selectors/stepsOutputSchemaFamilySelector';
|
||||
import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useUpsertStepFilterSettings';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { WorkflowVariablesDropdown } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdown';
|
||||
import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/extractRawVariableNamePart';
|
||||
import { searchVariableThroughOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughOutputSchema';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { StepFilter } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type WorkflowStepFilterFieldSelectProps = {
|
||||
stepFilter: StepFilter;
|
||||
};
|
||||
|
||||
export const WorkflowStepFilterFieldSelect = ({
|
||||
stepFilter,
|
||||
}: WorkflowStepFilterFieldSelectProps) => {
|
||||
const { readonly } = useContext(WorkflowStepFilterContext);
|
||||
|
||||
const { upsertStepFilterSettings } = useUpsertStepFilterSettings();
|
||||
|
||||
const { t } = useLingui();
|
||||
const { workflowVersionId } = useWorkflowStepContextOrThrow();
|
||||
|
||||
const stepId = extractRawVariableNamePart({
|
||||
rawVariableName: stepFilter.stepOutputKey,
|
||||
part: 'stepId',
|
||||
});
|
||||
const stepsOutputSchema = useRecoilValue(
|
||||
stepsOutputSchemaFamilySelector({
|
||||
workflowVersionId,
|
||||
stepIds: [stepId],
|
||||
}),
|
||||
);
|
||||
|
||||
if (!isDefined(stepId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { variableLabel } = searchVariableThroughOutputSchema({
|
||||
stepOutputSchema: stepsOutputSchema?.[0],
|
||||
rawVariableName: stepFilter.stepOutputKey,
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
const isSelectedFieldNotFound = !isDefined(variableLabel);
|
||||
const label = isSelectedFieldNotFound ? t`No Field Selected` : variableLabel;
|
||||
|
||||
const handleChange = (variableName: string) => {
|
||||
upsertStepFilterSettings({
|
||||
stepFilterToUpsert: {
|
||||
...stepFilter,
|
||||
stepOutputKey: variableName,
|
||||
displayValue: label,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<WorkflowVariablesDropdown
|
||||
instanceId={`step-filter-field-${stepFilter.id}`}
|
||||
onVariableSelect={handleChange}
|
||||
disabled={readonly}
|
||||
clickableComponent={
|
||||
<SelectControl
|
||||
selectedOption={{
|
||||
value: stepFilter.stepOutputKey,
|
||||
label,
|
||||
}}
|
||||
isDisabled={readonly}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,61 @@
|
||||
import { WorkflowStepFilterAddFilterRuleSelect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterAddFilterRuleSelect';
|
||||
import { WorkflowStepFilterColumn } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterColumn';
|
||||
import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useChildStepFiltersAndChildStepFilterGroups';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledContainer = styled.div<{ isGrayBackground?: boolean }>`
|
||||
align-items: start;
|
||||
background-color: ${({ theme, isGrayBackground }) =>
|
||||
isGrayBackground ? theme.background.transparent.lighter : 'transparent'};
|
||||
border: ${({ theme }) => `1px solid ${theme.border.color.medium}`};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(6)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type WorkflowStepFilterGroupChildrenProps = {
|
||||
stepFilterGroupId: string;
|
||||
};
|
||||
|
||||
export const WorkflowStepFilterGroupChildren = ({
|
||||
stepFilterGroupId,
|
||||
}: WorkflowStepFilterGroupChildrenProps) => {
|
||||
const { readonly } = useContext(WorkflowStepFilterContext);
|
||||
|
||||
const { currentStepFilterGroup, childStepFilters } =
|
||||
useChildStepFiltersAndChildStepFilterGroups({
|
||||
stepFilterGroupId,
|
||||
});
|
||||
|
||||
if (!currentStepFilterGroup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasParentStepFilterGroup = isDefined(
|
||||
currentStepFilterGroup.parentStepFilterGroupId,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer isGrayBackground={hasParentStepFilterGroup}>
|
||||
{(childStepFilters ?? []).map((childStepFilter, childStepFilterIndex) => (
|
||||
<WorkflowStepFilterColumn
|
||||
key={childStepFilter.id}
|
||||
stepFilter={childStepFilter}
|
||||
stepFilterIndex={childStepFilterIndex}
|
||||
stepFilterGroup={currentStepFilterGroup}
|
||||
/>
|
||||
))}
|
||||
{!readonly && (
|
||||
<WorkflowStepFilterAddFilterRuleSelect
|
||||
stepFilterGroup={currentStepFilterGroup}
|
||||
/>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,46 @@
|
||||
import { WorkflowStepFilterGroupChildren } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupChildren';
|
||||
import { WorkflowStepFilterGroupOptionsDropdown } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupOptionsDropdown';
|
||||
import { WorkflowStepFilterLogicalOperatorCell } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterLogicalOperatorCell';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { WorkflowAdvancedFilterDropdownColumn } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowAdvancedFilterDropdownColumn';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
import { StepFilterGroup } from 'twenty-shared/src/types';
|
||||
|
||||
type WorkflowStepFilterGroupColumnProps = {
|
||||
parentStepFilterGroup: StepFilterGroup;
|
||||
stepFilterGroup: StepFilterGroup;
|
||||
stepFilterGroupIndex: number;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const WorkflowStepFilterGroupColumn = ({
|
||||
parentStepFilterGroup,
|
||||
stepFilterGroup,
|
||||
stepFilterGroupIndex,
|
||||
}: WorkflowStepFilterGroupColumnProps) => {
|
||||
const { readonly } = useContext(WorkflowStepFilterContext);
|
||||
|
||||
return (
|
||||
<WorkflowAdvancedFilterDropdownColumn>
|
||||
<StyledContainer>
|
||||
<WorkflowStepFilterLogicalOperatorCell
|
||||
index={stepFilterGroupIndex}
|
||||
stepFilterGroup={parentStepFilterGroup}
|
||||
/>
|
||||
{!readonly && (
|
||||
<WorkflowStepFilterGroupOptionsDropdown
|
||||
stepFilterGroupId={stepFilterGroup.id}
|
||||
/>
|
||||
)}
|
||||
</StyledContainer>
|
||||
<WorkflowStepFilterGroupChildren stepFilterGroupId={stepFilterGroup.id} />
|
||||
</WorkflowAdvancedFilterDropdownColumn>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,49 @@
|
||||
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useRemoveStepFilterGroup } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useRemoveStepFilterGroup';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { useContext } from 'react';
|
||||
import { IconDotsVertical, IconTrash } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
type WorkflowStepFilterGroupOptionsDropdownProps = {
|
||||
stepFilterGroupId: string;
|
||||
};
|
||||
|
||||
export const WorkflowStepFilterGroupOptionsDropdown = ({
|
||||
stepFilterGroupId,
|
||||
}: WorkflowStepFilterGroupOptionsDropdownProps) => {
|
||||
const { readonly } = useContext(WorkflowStepFilterContext);
|
||||
|
||||
const { removeStepFilterGroup } = useRemoveStepFilterGroup();
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={`step-filter-group-options-${stepFilterGroupId}`}
|
||||
clickableComponent={
|
||||
<IconButton
|
||||
aria-label="Step filter group options"
|
||||
variant="tertiary"
|
||||
Icon={IconDotsVertical}
|
||||
disabled={readonly}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
LeftIcon={IconTrash}
|
||||
text="Delete group"
|
||||
onClick={() => removeStepFilterGroup(stepFilterGroupId)}
|
||||
accent="danger"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
dropdownOffset={DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,93 @@
|
||||
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useUpsertStepFilterSettings';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
import { StepFilterGroup, StepLogicalOperator } from 'twenty-shared/src/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
const StyledText = styled.div`
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: start;
|
||||
display: flex;
|
||||
min-width: ${({ theme }) => theme.spacing(20)};
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
type WorkflowStepFilterLogicalOperatorCellProps = {
|
||||
index: number;
|
||||
stepFilterGroup: StepFilterGroup;
|
||||
};
|
||||
|
||||
const STEP_FILTER_LOGICAL_OPERATOR_OPTIONS = [
|
||||
{
|
||||
value: StepLogicalOperator.AND,
|
||||
label: 'And',
|
||||
},
|
||||
{
|
||||
value: StepLogicalOperator.OR,
|
||||
label: 'Or',
|
||||
},
|
||||
];
|
||||
|
||||
export const WorkflowStepFilterLogicalOperatorCell = ({
|
||||
index,
|
||||
stepFilterGroup,
|
||||
}: WorkflowStepFilterLogicalOperatorCellProps) => {
|
||||
const { readonly } = useContext(WorkflowStepFilterContext);
|
||||
|
||||
const { upsertStepFilterSettings } = useUpsertStepFilterSettings();
|
||||
|
||||
const handleChange = (value: StepLogicalOperator) => {
|
||||
upsertStepFilterSettings({
|
||||
stepFilterGroupToUpsert: {
|
||||
id: stepFilterGroup.id,
|
||||
parentStepFilterGroupId: stepFilterGroup.parentStepFilterGroupId,
|
||||
positionInStepFilterGroup: stepFilterGroup.positionInStepFilterGroup,
|
||||
logicalOperator: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{index === 0 ? (
|
||||
<StyledText>Where</StyledText>
|
||||
) : index === 1 ? (
|
||||
readonly ? (
|
||||
<Select
|
||||
fullWidth
|
||||
dropdownWidth={GenericDropdownContentWidth.Narrow}
|
||||
dropdownId={`advanced-filter-logical-operator-${stepFilterGroup.id}`}
|
||||
value={stepFilterGroup.logicalOperator}
|
||||
options={STEP_FILTER_LOGICAL_OPERATOR_OPTIONS}
|
||||
dropdownOffset={DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET}
|
||||
disabled
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
fullWidth
|
||||
dropdownWidth={GenericDropdownContentWidth.Narrow}
|
||||
dropdownId={`advanced-filter-logical-operator-${stepFilterGroup.id}`}
|
||||
value={stepFilterGroup.logicalOperator}
|
||||
onChange={handleChange}
|
||||
options={STEP_FILTER_LOGICAL_OPERATOR_OPTIONS}
|
||||
dropdownOffset={DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<StyledText>
|
||||
{capitalize(stepFilterGroup.logicalOperator.toLowerCase())}
|
||||
</StyledText>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,54 @@
|
||||
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useUpsertStepFilterSettings';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { useContext } from 'react';
|
||||
import { StepFilter, StepOperand } from 'twenty-shared/src/types';
|
||||
|
||||
type WorkflowStepFilterOperandSelectProps = {
|
||||
stepFilter: StepFilter;
|
||||
};
|
||||
|
||||
const STEP_OPERAND_OPTIONS = [
|
||||
{ value: StepOperand.EQ, label: 'Equals' },
|
||||
{ value: StepOperand.NE, label: 'Not equals' },
|
||||
{ value: StepOperand.GT, label: 'Greater than' },
|
||||
{ value: StepOperand.GTE, label: 'Greater than or equal' },
|
||||
{ value: StepOperand.LT, label: 'Less than' },
|
||||
{ value: StepOperand.LTE, label: 'Less than or equal' },
|
||||
{ value: StepOperand.LIKE, label: 'Contains' },
|
||||
{ value: StepOperand.ILIKE, label: 'Contains (case insensitive)' },
|
||||
{ value: StepOperand.IN, label: 'In' },
|
||||
{ value: StepOperand.IS, label: 'Is' },
|
||||
];
|
||||
|
||||
export const WorkflowStepFilterOperandSelect = ({
|
||||
stepFilter,
|
||||
}: WorkflowStepFilterOperandSelectProps) => {
|
||||
const { readonly } = useContext(WorkflowStepFilterContext);
|
||||
|
||||
const { upsertStepFilterSettings } = useUpsertStepFilterSettings();
|
||||
|
||||
const handleChange = (operand: StepOperand) => {
|
||||
upsertStepFilterSettings({
|
||||
stepFilterToUpsert: {
|
||||
...stepFilter,
|
||||
operand,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
fullWidth
|
||||
dropdownWidth={GenericDropdownContentWidth.Medium}
|
||||
dropdownId={`step-filter-operand-${stepFilter.id}`}
|
||||
value={stepFilter.operand}
|
||||
options={STEP_OPERAND_OPTIONS}
|
||||
onChange={handleChange}
|
||||
disabled={readonly}
|
||||
dropdownOffset={DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,50 @@
|
||||
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useRemoveStepFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useRemoveStepFilter';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { useContext } from 'react';
|
||||
import { IconDotsVertical, IconTrash } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
type WorkflowStepFilterOptionsDropdownProps = {
|
||||
stepFilterId: string;
|
||||
};
|
||||
|
||||
export const WorkflowStepFilterOptionsDropdown = ({
|
||||
stepFilterId,
|
||||
}: WorkflowStepFilterOptionsDropdownProps) => {
|
||||
const { readonly } = useContext(WorkflowStepFilterContext);
|
||||
|
||||
const { removeStepFilter } = useRemoveStepFilter();
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={`step-filter-options-${stepFilterId}`}
|
||||
clickableComponent={
|
||||
<IconButton
|
||||
aria-label="Step filter options"
|
||||
variant="tertiary"
|
||||
Icon={IconDotsVertical}
|
||||
disabled={readonly}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
LeftIcon={IconTrash}
|
||||
text="Delete"
|
||||
onClick={() => removeStepFilter(stepFilterId)}
|
||||
accent="danger"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
dropdownOffset={DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET}
|
||||
dropdownPlacement="bottom-start"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||
import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useUpsertStepFilterSettings';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext } from 'react';
|
||||
import { StepFilter } from 'twenty-shared/src/types';
|
||||
|
||||
type WorkflowStepFilterValueInputProps = {
|
||||
stepFilter: StepFilter;
|
||||
};
|
||||
|
||||
export const WorkflowStepFilterValueInput = ({
|
||||
stepFilter,
|
||||
}: WorkflowStepFilterValueInputProps) => {
|
||||
const { t } = useLingui();
|
||||
const { readonly } = useContext(WorkflowStepFilterContext);
|
||||
|
||||
const { upsertStepFilterSettings } = useUpsertStepFilterSettings();
|
||||
|
||||
const handleValueChange = (value: string) => {
|
||||
upsertStepFilterSettings({
|
||||
stepFilterToUpsert: {
|
||||
...stepFilter,
|
||||
value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<FormTextFieldInput
|
||||
defaultValue={stepFilter.value}
|
||||
onChange={handleValueChange}
|
||||
readonly={readonly}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
placeholder={t`Enter value`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,108 @@
|
||||
import { WorkflowFilterAction } from '@/workflow/types/Workflow';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
|
||||
import { WorkflowEditActionFilter } from '../WorkflowEditActionFilter';
|
||||
|
||||
const DEFAULT_ACTION: WorkflowFilterAction = {
|
||||
id: getWorkflowNodeIdMock(),
|
||||
name: 'Filter Records',
|
||||
type: 'FILTER',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
stepFilterGroups: [],
|
||||
stepFilters: [],
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const CONFIGURED_ACTION: WorkflowFilterAction = {
|
||||
id: getWorkflowNodeIdMock(),
|
||||
name: 'Filter Companies',
|
||||
type: 'FILTER',
|
||||
valid: true,
|
||||
settings: {
|
||||
input: {
|
||||
stepFilterGroups: [
|
||||
{
|
||||
id: 'filter-group-1',
|
||||
parentStepFilterGroupId: null,
|
||||
logicalOperator: 'AND',
|
||||
stepFilterGroupChildren: [],
|
||||
},
|
||||
],
|
||||
stepFilters: [
|
||||
{
|
||||
id: 'filter-1',
|
||||
stepFilterGroupId: 'filter-group-1',
|
||||
stepOutputKey: 'company.name',
|
||||
displayValue: 'Company Name',
|
||||
operandType: 'LITERAL',
|
||||
operand: 'contains',
|
||||
value: 'Acme',
|
||||
},
|
||||
],
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const meta: Meta<typeof WorkflowEditActionFilter> = {
|
||||
title: 'Modules/Workflow/Actions/Filter/WorkflowEditActionFilter',
|
||||
component: WorkflowEditActionFilter,
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
args: {
|
||||
action: DEFAULT_ACTION,
|
||||
actionOptions: {
|
||||
readonly: false,
|
||||
onActionUpdate: fn(),
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
WorkflowStepActionDrawerDecorator,
|
||||
WorkflowStepDecorator,
|
||||
ComponentDecorator,
|
||||
WorkspaceDecorator,
|
||||
I18nFrontDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkflowEditActionFilter>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const ReadOnly: Story = {
|
||||
args: {
|
||||
action: CONFIGURED_ACTION,
|
||||
actionOptions: {
|
||||
readonly: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,71 @@
|
||||
import { WorkflowFilterAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
|
||||
import { WorkflowEditActionFilterBody } from '../WorkflowEditActionFilterBody';
|
||||
|
||||
const DEFAULT_ACTION: WorkflowFilterAction = {
|
||||
id: getWorkflowNodeIdMock(),
|
||||
name: 'Filter Records',
|
||||
type: 'FILTER',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
stepFilterGroups: [],
|
||||
stepFilters: [],
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const meta: Meta<typeof WorkflowEditActionFilterBody> = {
|
||||
title: 'Modules/Workflow/Actions/Filter/WorkflowEditActionFilterBody',
|
||||
component: WorkflowEditActionFilterBody,
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
args: {
|
||||
action: DEFAULT_ACTION,
|
||||
actionOptions: {
|
||||
readonly: false,
|
||||
onActionUpdate: fn(),
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
WorkflowStepActionDrawerDecorator,
|
||||
WorkflowStepDecorator,
|
||||
ComponentDecorator,
|
||||
WorkspaceDecorator,
|
||||
I18nFrontDecorator,
|
||||
WorkflowStepFilterDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkflowEditActionFilterBody>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const ReadOnly: Story = {
|
||||
args: {
|
||||
action: DEFAULT_ACTION,
|
||||
actionOptions: {
|
||||
readonly: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,48 @@
|
||||
import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, within } from '@storybook/test';
|
||||
import { StepFilterGroup, StepLogicalOperator } from 'twenty-shared/types';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { WorkflowStepFilterAddFilterRuleSelect } from '../WorkflowStepFilterAddFilterRuleSelect';
|
||||
|
||||
const STEP_FILTER_GROUP: StepFilterGroup = {
|
||||
id: 'filter-group-1',
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
const meta: Meta<typeof WorkflowStepFilterAddFilterRuleSelect> = {
|
||||
title:
|
||||
'Modules/Workflow/Actions/Filter/WorkflowStepFilterAddFilterRuleSelect',
|
||||
component: WorkflowStepFilterAddFilterRuleSelect,
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
args: {
|
||||
stepFilterGroup: STEP_FILTER_GROUP,
|
||||
},
|
||||
decorators: [
|
||||
WorkflowStepActionDrawerDecorator,
|
||||
WorkflowStepDecorator,
|
||||
ComponentDecorator,
|
||||
WorkspaceDecorator,
|
||||
I18nFrontDecorator,
|
||||
WorkflowStepFilterDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkflowStepFilterAddFilterRuleSelect>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(await canvas.findByText('Add filter rule')).toBeVisible();
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, within } from '@storybook/test';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { WorkflowStepFilterAddRootStepFilterButton } from '../WorkflowStepFilterAddRootStepFilterButton';
|
||||
|
||||
const meta: Meta<typeof WorkflowStepFilterAddRootStepFilterButton> = {
|
||||
title:
|
||||
'Modules/Workflow/Actions/Filter/WorkflowStepFilterAddRootStepFilterButton',
|
||||
component: WorkflowStepFilterAddRootStepFilterButton,
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
decorators: [
|
||||
WorkflowStepActionDrawerDecorator,
|
||||
WorkflowStepDecorator,
|
||||
ComponentDecorator,
|
||||
WorkspaceDecorator,
|
||||
I18nFrontDecorator,
|
||||
WorkflowStepFilterDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkflowStepFilterAddRootStepFilterButton>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const addButton = await canvas.findByRole('button');
|
||||
await expect(addButton).toBeVisible();
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,58 @@
|
||||
import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import {
|
||||
StepFilter,
|
||||
StepFilterGroup,
|
||||
StepLogicalOperator,
|
||||
StepOperand,
|
||||
} from 'twenty-shared/types';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { WorkflowStepFilterColumn } from '../WorkflowStepFilterColumn';
|
||||
|
||||
const STEP_FILTER_GROUP: StepFilterGroup = {
|
||||
id: 'filter-group-1',
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
const TEXT_STEP_FILTER: StepFilter = {
|
||||
id: 'filter-1',
|
||||
stepFilterGroupId: 'filter-group-1',
|
||||
stepOutputKey: 'company.name',
|
||||
displayValue: 'Company Name',
|
||||
type: 'text',
|
||||
label: 'Company Name',
|
||||
value: 'Acme',
|
||||
operand: StepOperand.LIKE,
|
||||
};
|
||||
|
||||
const meta: Meta<typeof WorkflowStepFilterColumn> = {
|
||||
title: 'Modules/Workflow/Actions/Filter/WorkflowStepFilterColumn',
|
||||
component: WorkflowStepFilterColumn,
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
args: {
|
||||
stepFilterGroup: STEP_FILTER_GROUP,
|
||||
stepFilter: TEXT_STEP_FILTER,
|
||||
stepFilterIndex: 0,
|
||||
},
|
||||
decorators: [
|
||||
WorkflowStepActionDrawerDecorator,
|
||||
WorkflowStepDecorator,
|
||||
ComponentDecorator,
|
||||
WorkspaceDecorator,
|
||||
I18nFrontDecorator,
|
||||
WorkflowStepFilterDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkflowStepFilterColumn>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@ -0,0 +1,46 @@
|
||||
import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { StepFilter, StepOperand } from 'twenty-shared/types';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { WorkflowStepFilterFieldSelect } from '../WorkflowStepFilterFieldSelect';
|
||||
|
||||
const DEFAULT_STEP_FILTER: StepFilter = {
|
||||
id: 'filter-1',
|
||||
stepFilterGroupId: 'filter-group-1',
|
||||
stepOutputKey: '',
|
||||
displayValue: '',
|
||||
type: 'text',
|
||||
label: 'New Filter',
|
||||
operand: StepOperand.EQ,
|
||||
value: '',
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
const meta: Meta<typeof WorkflowStepFilterFieldSelect> = {
|
||||
title: 'Modules/Workflow/Actions/Filter/WorkflowStepFilterFieldSelect',
|
||||
component: WorkflowStepFilterFieldSelect,
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
args: {
|
||||
stepFilter: DEFAULT_STEP_FILTER,
|
||||
},
|
||||
decorators: [
|
||||
WorkflowStepActionDrawerDecorator,
|
||||
WorkflowStepDecorator,
|
||||
ComponentDecorator,
|
||||
WorkspaceDecorator,
|
||||
I18nFrontDecorator,
|
||||
WorkflowStepFilterDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkflowStepFilterFieldSelect>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@ -0,0 +1,67 @@
|
||||
import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, within } from '@storybook/test';
|
||||
import { StepFilterGroup, StepLogicalOperator } from 'twenty-shared/types';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { WorkflowStepFilterLogicalOperatorCell } from '../WorkflowStepFilterLogicalOperatorCell';
|
||||
|
||||
const AND_STEP_FILTER_GROUP: StepFilterGroup = {
|
||||
id: 'filter-group-1',
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
const OR_STEP_FILTER_GROUP: StepFilterGroup = {
|
||||
id: 'filter-group-2',
|
||||
logicalOperator: StepLogicalOperator.OR,
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
const meta: Meta<typeof WorkflowStepFilterLogicalOperatorCell> = {
|
||||
title:
|
||||
'Modules/Workflow/Actions/Filter/WorkflowStepFilterLogicalOperatorCell',
|
||||
component: WorkflowStepFilterLogicalOperatorCell,
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
args: {
|
||||
stepFilterGroup: AND_STEP_FILTER_GROUP,
|
||||
index: 1,
|
||||
},
|
||||
decorators: [
|
||||
WorkflowStepActionDrawerDecorator,
|
||||
WorkflowStepDecorator,
|
||||
ComponentDecorator,
|
||||
WorkspaceDecorator,
|
||||
I18nFrontDecorator,
|
||||
WorkflowStepFilterDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkflowStepFilterLogicalOperatorCell>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(await canvas.findByText('And')).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const OrOperator: Story = {
|
||||
args: {
|
||||
stepFilterGroup: OR_STEP_FILTER_GROUP,
|
||||
index: 1,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(await canvas.findByText('Or')).toBeVisible();
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,99 @@
|
||||
import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, within } from '@storybook/test';
|
||||
import { StepFilter, StepOperand } from 'twenty-shared/types';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { WorkflowStepFilterOperandSelect } from '../WorkflowStepFilterOperandSelect';
|
||||
|
||||
const DEFAULT_STEP_FILTER: StepFilter = {
|
||||
id: 'filter-1',
|
||||
stepFilterGroupId: 'filter-group-1',
|
||||
stepOutputKey: 'company.name',
|
||||
displayValue: 'Company Name',
|
||||
type: 'text',
|
||||
label: 'Company Name',
|
||||
operand: StepOperand.EQ,
|
||||
value: '',
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
const LIKE_OPERAND_FILTER: StepFilter = {
|
||||
id: 'filter-1',
|
||||
stepFilterGroupId: 'filter-group-1',
|
||||
stepOutputKey: 'company.name',
|
||||
displayValue: 'Company Name',
|
||||
type: 'text',
|
||||
label: 'Company Name',
|
||||
operand: StepOperand.LIKE,
|
||||
value: 'Acme',
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
const GREATER_THAN_FILTER: StepFilter = {
|
||||
id: 'filter-1',
|
||||
stepFilterGroupId: 'filter-group-1',
|
||||
stepOutputKey: 'company.employees',
|
||||
displayValue: 'Employee Count',
|
||||
type: 'number',
|
||||
label: 'Employee Count',
|
||||
operand: StepOperand.GT,
|
||||
value: '100',
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
const meta: Meta<typeof WorkflowStepFilterOperandSelect> = {
|
||||
title: 'Modules/Workflow/Actions/Filter/WorkflowStepFilterOperandSelect',
|
||||
component: WorkflowStepFilterOperandSelect,
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
args: {
|
||||
stepFilter: DEFAULT_STEP_FILTER,
|
||||
},
|
||||
decorators: [
|
||||
WorkflowStepActionDrawerDecorator,
|
||||
WorkflowStepDecorator,
|
||||
ComponentDecorator,
|
||||
WorkspaceDecorator,
|
||||
I18nFrontDecorator,
|
||||
WorkflowStepFilterDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkflowStepFilterOperandSelect>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(await canvas.findByText('Equals')).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLikeOperand: Story = {
|
||||
args: {
|
||||
stepFilter: LIKE_OPERAND_FILTER,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(await canvas.findByText('Contains')).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const WithGreaterThanOperand: Story = {
|
||||
args: {
|
||||
stepFilter: GREATER_THAN_FILTER,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(await canvas.findByText('Greater than')).toBeVisible();
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,76 @@
|
||||
import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, within } from '@storybook/test';
|
||||
import { StepFilter, StepOperand } from 'twenty-shared/types';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { WorkflowStepFilterValueInput } from '../WorkflowStepFilterValueInput';
|
||||
|
||||
const TEXT_FILTER: StepFilter = {
|
||||
id: 'filter-1',
|
||||
stepFilterGroupId: 'filter-group-1',
|
||||
stepOutputKey: 'company.name',
|
||||
displayValue: 'Company Name',
|
||||
type: 'text',
|
||||
label: 'Company Name',
|
||||
operand: StepOperand.LIKE,
|
||||
value: 'Acme',
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
const NUMBER_FILTER: StepFilter = {
|
||||
id: 'filter-2',
|
||||
stepFilterGroupId: 'filter-group-1',
|
||||
stepOutputKey: 'company.employees',
|
||||
displayValue: 'Employee Count',
|
||||
type: 'number',
|
||||
label: 'Employee Count',
|
||||
operand: StepOperand.GT,
|
||||
value: '100',
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
const meta: Meta<typeof WorkflowStepFilterValueInput> = {
|
||||
title: 'Modules/Workflow/Actions/Filter/WorkflowStepFilterValueInput',
|
||||
component: WorkflowStepFilterValueInput,
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
args: {
|
||||
stepFilter: TEXT_FILTER,
|
||||
},
|
||||
decorators: [
|
||||
WorkflowStepActionDrawerDecorator,
|
||||
WorkflowStepDecorator,
|
||||
ComponentDecorator,
|
||||
WorkspaceDecorator,
|
||||
I18nFrontDecorator,
|
||||
WorkflowStepFilterDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkflowStepFilterValueInput>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(await canvas.findByText('Acme')).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const NumberInput: Story = {
|
||||
args: {
|
||||
stepFilter: NUMBER_FILTER,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(await canvas.findByText('100')).toBeVisible();
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFilterGroupsComponentInstanceContext';
|
||||
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFiltersComponentInstanceContext';
|
||||
import { Decorator } from '@storybook/react';
|
||||
|
||||
export const WorkflowStepFilterDecorator: Decorator = (Story) => {
|
||||
const stepId = 'step-id';
|
||||
|
||||
return (
|
||||
<StepFilterGroupsComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: stepId,
|
||||
}}
|
||||
>
|
||||
<StepFiltersComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: stepId,
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</StepFiltersComponentInstanceContext.Provider>
|
||||
</StepFilterGroupsComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,84 @@
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFilterGroupsComponentState';
|
||||
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFiltersComponentState';
|
||||
import { hasInitializedCurrentStepFilterGroupsComponentFamilyState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/hasInitializedCurrentStepFilterGroupsComponentFamilyState';
|
||||
import { hasInitializedCurrentStepFiltersComponentFamilyState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/hasInitializedCurrentStepFiltersComponentFamilyState';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import {
|
||||
StepFilter,
|
||||
StepFilterGroup,
|
||||
StepLogicalOperator,
|
||||
StepOperand,
|
||||
} from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const useAddRootStepFilter = () => {
|
||||
const { stepId, onFilterSettingsUpdate } = useContext(
|
||||
WorkflowStepFilterContext,
|
||||
);
|
||||
const currentStepFilterGroupsCallbackState =
|
||||
useRecoilComponentCallbackStateV2(currentStepFilterGroupsComponentState);
|
||||
|
||||
const currentStepFiltersCallbackState = useRecoilComponentCallbackStateV2(
|
||||
currentStepFiltersComponentState,
|
||||
);
|
||||
|
||||
const setHasInitializedCurrentStepFilters =
|
||||
useSetRecoilComponentFamilyStateV2(
|
||||
hasInitializedCurrentStepFiltersComponentFamilyState,
|
||||
{ stepId },
|
||||
);
|
||||
|
||||
const setHasInitializedCurrentStepFilterGroups =
|
||||
useSetRecoilComponentFamilyStateV2(
|
||||
hasInitializedCurrentStepFilterGroupsComponentFamilyState,
|
||||
{ stepId },
|
||||
);
|
||||
|
||||
const addRootStepFilterRecoilCallback = useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
const newStepFilterGroup: StepFilterGroup = {
|
||||
id: v4(),
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
};
|
||||
|
||||
const newStepFilter: StepFilter = {
|
||||
id: v4(),
|
||||
type: 'text',
|
||||
label: 'New Filter',
|
||||
value: '',
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: '',
|
||||
stepFilterGroupId: newStepFilterGroup.id,
|
||||
stepOutputKey: '',
|
||||
positionInStepFilterGroup: 0,
|
||||
};
|
||||
|
||||
set(currentStepFilterGroupsCallbackState, [newStepFilterGroup]);
|
||||
set(currentStepFiltersCallbackState, [newStepFilter]);
|
||||
|
||||
setHasInitializedCurrentStepFilters(true);
|
||||
setHasInitializedCurrentStepFilterGroups(true);
|
||||
|
||||
onFilterSettingsUpdate({
|
||||
stepFilterGroups: [newStepFilterGroup],
|
||||
stepFilters: [newStepFilter],
|
||||
});
|
||||
},
|
||||
[
|
||||
onFilterSettingsUpdate,
|
||||
currentStepFilterGroupsCallbackState,
|
||||
currentStepFiltersCallbackState,
|
||||
setHasInitializedCurrentStepFilters,
|
||||
setHasInitializedCurrentStepFilterGroups,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
addRootStepFilter: addRootStepFilterRecoilCallback,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,66 @@
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFilterGroupsComponentState';
|
||||
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFiltersComponentState';
|
||||
import { StepFilter, StepFilterGroup } from 'twenty-shared/src/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useChildStepFiltersAndChildStepFilterGroups = ({
|
||||
stepFilterGroupId,
|
||||
}: {
|
||||
stepFilterGroupId: string;
|
||||
}) => {
|
||||
const stepFilterGroups = useRecoilComponentValueV2(
|
||||
currentStepFilterGroupsComponentState,
|
||||
);
|
||||
|
||||
const stepFilters = useRecoilComponentValueV2(
|
||||
currentStepFiltersComponentState,
|
||||
);
|
||||
|
||||
const currentStepFilterGroup = stepFilterGroups?.find(
|
||||
(stepFilterGroup) => stepFilterGroup.id === stepFilterGroupId,
|
||||
);
|
||||
|
||||
if (!isDefined(currentStepFilterGroup)) {
|
||||
return {
|
||||
currentStepFilterGroup: undefined,
|
||||
childStepFiltersAndChildStepFilterGroups: [] as Array<
|
||||
StepFilter | StepFilterGroup
|
||||
>,
|
||||
childStepFilters: [] as StepFilter[],
|
||||
childStepFilterGroups: [] as StepFilterGroup[],
|
||||
lastChildPosition: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const childStepFilters = stepFilters?.filter(
|
||||
(filter) => filter.stepFilterGroupId === currentStepFilterGroup.id,
|
||||
);
|
||||
|
||||
const childStepFilterGroups = stepFilterGroups?.filter(
|
||||
(filterGroup) =>
|
||||
filterGroup.parentStepFilterGroupId === currentStepFilterGroup.id,
|
||||
);
|
||||
|
||||
const childStepFiltersAndChildStepFilterGroups = [
|
||||
...(childStepFilterGroups ?? []),
|
||||
...(childStepFilters ?? []),
|
||||
].sort((a, b) => {
|
||||
const positionA = a.positionInStepFilterGroup ?? 0;
|
||||
const positionB = b.positionInStepFilterGroup ?? 0;
|
||||
return positionA - positionB;
|
||||
});
|
||||
|
||||
const lastChildPosition =
|
||||
childStepFiltersAndChildStepFilterGroups[
|
||||
childStepFiltersAndChildStepFilterGroups.length - 1
|
||||
]?.positionInStepFilterGroup ?? 0;
|
||||
|
||||
return {
|
||||
currentStepFilterGroup,
|
||||
childStepFiltersAndChildStepFilterGroups,
|
||||
childStepFilters,
|
||||
childStepFilterGroups,
|
||||
lastChildPosition,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,106 @@
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFilterGroupsComponentState';
|
||||
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFiltersComponentState';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkflowStepFilterContext } from '../states/context/WorkflowStepFilterContext';
|
||||
|
||||
export const useRemoveStepFilter = () => {
|
||||
const { onFilterSettingsUpdate } = useContext(WorkflowStepFilterContext);
|
||||
|
||||
const currentStepFiltersCallbackState = useRecoilComponentCallbackStateV2(
|
||||
currentStepFiltersComponentState,
|
||||
);
|
||||
|
||||
const currentStepFilterGroupsCallbackState =
|
||||
useRecoilComponentCallbackStateV2(currentStepFilterGroupsComponentState);
|
||||
|
||||
const removeStepFilterRecoilCallback = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(stepFilterId: string) => {
|
||||
const stepFilters = getSnapshotValue(
|
||||
snapshot,
|
||||
currentStepFiltersCallbackState,
|
||||
);
|
||||
|
||||
const stepFilterGroups = getSnapshotValue(
|
||||
snapshot,
|
||||
currentStepFilterGroupsCallbackState,
|
||||
);
|
||||
|
||||
const rootStepFilterGroup = stepFilterGroups?.find(
|
||||
(filterGroup) => !isDefined(filterGroup.parentStepFilterGroupId),
|
||||
);
|
||||
|
||||
if (!isDefined(rootStepFilterGroup)) return;
|
||||
|
||||
const stepFilterToRemove = stepFilters?.find(
|
||||
(filter) => filter.id === stepFilterId,
|
||||
);
|
||||
|
||||
if (!isDefined(stepFilterToRemove)) return;
|
||||
|
||||
const updatedStepFilters = (stepFilters ?? []).filter(
|
||||
(filter) => filter.id !== stepFilterId,
|
||||
);
|
||||
|
||||
const parentStepFilterGroup = stepFilterGroups?.find(
|
||||
(filterGroup) =>
|
||||
filterGroup.id === stepFilterToRemove.stepFilterGroupId,
|
||||
);
|
||||
|
||||
const stepFiltersInParentStepFilterGroup = updatedStepFilters?.filter(
|
||||
(filter) => filter.stepFilterGroupId === parentStepFilterGroup?.id,
|
||||
);
|
||||
|
||||
const stepFilterGroupsInParentStepFilterGroup =
|
||||
stepFilterGroups?.filter(
|
||||
(g) => g.parentStepFilterGroupId === parentStepFilterGroup?.id,
|
||||
);
|
||||
|
||||
const shouldDeleteParentStepFilterGroup =
|
||||
stepFiltersInParentStepFilterGroup?.length === 0 &&
|
||||
stepFilterGroupsInParentStepFilterGroup?.length === 0;
|
||||
|
||||
const updatedStepFilterGroups = shouldDeleteParentStepFilterGroup
|
||||
? (stepFilterGroups ?? []).filter(
|
||||
(filterGroup) => filterGroup.id !== parentStepFilterGroup?.id,
|
||||
)
|
||||
: stepFilterGroups;
|
||||
|
||||
const shouldResetStepFilterSettings =
|
||||
updatedStepFilterGroups.length === 1 &&
|
||||
updatedStepFilterGroups[0].id === rootStepFilterGroup?.id &&
|
||||
updatedStepFilters.length === 0;
|
||||
|
||||
if (shouldResetStepFilterSettings) {
|
||||
set(currentStepFilterGroupsCallbackState, []);
|
||||
set(currentStepFiltersCallbackState, []);
|
||||
|
||||
onFilterSettingsUpdate({
|
||||
stepFilterGroups: [],
|
||||
stepFilters: [],
|
||||
});
|
||||
} else {
|
||||
set(currentStepFilterGroupsCallbackState, updatedStepFilterGroups);
|
||||
set(currentStepFiltersCallbackState, updatedStepFilters);
|
||||
|
||||
onFilterSettingsUpdate({
|
||||
stepFilters: updatedStepFilters,
|
||||
stepFilterGroups: updatedStepFilterGroups,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
onFilterSettingsUpdate,
|
||||
currentStepFilterGroupsCallbackState,
|
||||
currentStepFiltersCallbackState,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
removeStepFilter: removeStepFilterRecoilCallback,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,78 @@
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFilterGroupsComponentState';
|
||||
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFiltersComponentState';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useRemoveStepFilterGroup = () => {
|
||||
const { onFilterSettingsUpdate } = useContext(WorkflowStepFilterContext);
|
||||
|
||||
const currentStepFilterGroupsCallbackState =
|
||||
useRecoilComponentCallbackStateV2(currentStepFilterGroupsComponentState);
|
||||
|
||||
const currentStepFiltersCallbackState = useRecoilComponentCallbackStateV2(
|
||||
currentStepFiltersComponentState,
|
||||
);
|
||||
|
||||
const removeStepFilterGroupRecoilCallback = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(stepFilterGroupId: string) => {
|
||||
const stepFilterGroups = getSnapshotValue(
|
||||
snapshot,
|
||||
currentStepFilterGroupsCallbackState,
|
||||
);
|
||||
|
||||
const stepFilters = getSnapshotValue(
|
||||
snapshot,
|
||||
currentStepFiltersCallbackState,
|
||||
);
|
||||
|
||||
const rootStepFilterGroup = stepFilterGroups?.find(
|
||||
(filterGroup) => !isDefined(filterGroup.parentStepFilterGroupId),
|
||||
);
|
||||
|
||||
const updatedStepFilterGroups = (stepFilterGroups ?? []).filter(
|
||||
(filterGroup) => filterGroup.id !== stepFilterGroupId,
|
||||
);
|
||||
|
||||
const updatedStepFilters = (stepFilters ?? []).filter(
|
||||
(filter) => filter.stepFilterGroupId !== stepFilterGroupId,
|
||||
);
|
||||
|
||||
const shouldResetStepFilterSettings =
|
||||
updatedStepFilterGroups.length === 1 &&
|
||||
updatedStepFilterGroups[0].id === rootStepFilterGroup?.id &&
|
||||
updatedStepFilters.length === 0;
|
||||
|
||||
if (shouldResetStepFilterSettings) {
|
||||
set(currentStepFilterGroupsCallbackState, []);
|
||||
set(currentStepFiltersCallbackState, []);
|
||||
|
||||
onFilterSettingsUpdate({
|
||||
stepFilterGroups: [],
|
||||
stepFilters: [],
|
||||
});
|
||||
} else {
|
||||
set(currentStepFilterGroupsCallbackState, updatedStepFilterGroups);
|
||||
set(currentStepFiltersCallbackState, updatedStepFilters);
|
||||
|
||||
onFilterSettingsUpdate({
|
||||
stepFilterGroups: updatedStepFilterGroups,
|
||||
stepFilters: updatedStepFilters,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
onFilterSettingsUpdate,
|
||||
currentStepFilterGroupsCallbackState,
|
||||
currentStepFiltersCallbackState,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
removeStepFilterGroup: removeStepFilterGroupRecoilCallback,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,83 @@
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
|
||||
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFilterGroupsComponentState';
|
||||
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFiltersComponentState';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { StepFilter, StepFilterGroup } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useUpsertStepFilterSettings = () => {
|
||||
const currentStepFilterGroupsCallbackState =
|
||||
useRecoilComponentCallbackStateV2(currentStepFilterGroupsComponentState);
|
||||
|
||||
const currentStepFiltersCallbackState = useRecoilComponentCallbackStateV2(
|
||||
currentStepFiltersComponentState,
|
||||
);
|
||||
const { onFilterSettingsUpdate } = useContext(WorkflowStepFilterContext);
|
||||
|
||||
const upsertStepFilterSettingsRecoilCallback = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
({
|
||||
stepFilterGroupToUpsert,
|
||||
stepFilterToUpsert,
|
||||
}: {
|
||||
stepFilterGroupToUpsert?: StepFilterGroup;
|
||||
stepFilterToUpsert?: StepFilter;
|
||||
}) => {
|
||||
const stepFilterGroups = getSnapshotValue(
|
||||
snapshot,
|
||||
currentStepFilterGroupsCallbackState,
|
||||
);
|
||||
|
||||
const stepFilters = getSnapshotValue(
|
||||
snapshot,
|
||||
currentStepFiltersCallbackState,
|
||||
);
|
||||
|
||||
const updatedStepFilterGroups = [...(stepFilterGroups ?? [])];
|
||||
const updatedStepFilters = [...(stepFilters ?? [])];
|
||||
|
||||
if (isDefined(stepFilterGroupToUpsert)) {
|
||||
const existingIndex = updatedStepFilterGroups.findIndex(
|
||||
(filterGroup) => filterGroup.id === stepFilterGroupToUpsert.id,
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
updatedStepFilterGroups[existingIndex] = stepFilterGroupToUpsert;
|
||||
} else {
|
||||
updatedStepFilterGroups.push(stepFilterGroupToUpsert);
|
||||
}
|
||||
set(currentStepFilterGroupsCallbackState, updatedStepFilterGroups);
|
||||
}
|
||||
|
||||
if (isDefined(stepFilterToUpsert)) {
|
||||
const existingIndex = updatedStepFilters.findIndex(
|
||||
(filter) => filter.id === stepFilterToUpsert.id,
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
updatedStepFilters[existingIndex] = stepFilterToUpsert;
|
||||
} else {
|
||||
updatedStepFilters.push(stepFilterToUpsert);
|
||||
}
|
||||
set(currentStepFiltersCallbackState, updatedStepFilters);
|
||||
}
|
||||
|
||||
onFilterSettingsUpdate({
|
||||
stepFilterGroups: updatedStepFilterGroups,
|
||||
stepFilters: updatedStepFilters,
|
||||
});
|
||||
},
|
||||
[
|
||||
onFilterSettingsUpdate,
|
||||
currentStepFilterGroupsCallbackState,
|
||||
currentStepFiltersCallbackState,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
upsertStepFilterSettings: upsertStepFilterSettingsRecoilCallback,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const StepFilterGroupsComponentInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
@ -0,0 +1,4 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const StepFiltersComponentInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
@ -0,0 +1,13 @@
|
||||
import { FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
|
||||
import { createContext } from 'react';
|
||||
|
||||
type WorkflowStepFilterContextType = {
|
||||
stepId: string;
|
||||
onFilterSettingsUpdate: (filterSettings: FilterSettings) => void;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export const WorkflowStepFilterContext =
|
||||
createContext<WorkflowStepFilterContextType>(
|
||||
{} as WorkflowStepFilterContextType,
|
||||
);
|
||||
@ -0,0 +1,11 @@
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFilterGroupsComponentInstanceContext';
|
||||
import { StepFilterGroup } from 'twenty-shared/types';
|
||||
|
||||
export const currentStepFilterGroupsComponentState = createComponentStateV2<
|
||||
StepFilterGroup[]
|
||||
>({
|
||||
key: 'currentStepFilterGroupsComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: StepFilterGroupsComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFiltersComponentInstanceContext';
|
||||
import { StepFilter } from 'twenty-shared/types';
|
||||
|
||||
export const currentStepFiltersComponentState = createComponentStateV2<
|
||||
StepFilter[]
|
||||
>({
|
||||
key: 'currentStepFiltersComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: StepFiltersComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
|
||||
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFilterGroupsComponentInstanceContext';
|
||||
|
||||
export const hasInitializedCurrentStepFilterGroupsComponentFamilyState =
|
||||
createComponentFamilyStateV2<boolean, { stepId: string }>({
|
||||
key: 'hasInitializedCurrentStepFilterGroupsComponentFamilyState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: StepFilterGroupsComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
|
||||
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFiltersComponentInstanceContext';
|
||||
|
||||
export const hasInitializedCurrentStepFiltersComponentFamilyState =
|
||||
createComponentFamilyStateV2<boolean, { stepId: string }>({
|
||||
key: 'hasInitializedCurrentStepFiltersComponentFamilyState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: StepFiltersComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
|
||||
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFilterGroupsComponentInstanceContext';
|
||||
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFilterGroupsComponentState';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const rootLevelStepFilterGroupComponentSelector =
|
||||
createComponentSelectorV2({
|
||||
key: 'rootLevelStepFilterGroupComponentSelector',
|
||||
get:
|
||||
({ instanceId }) =>
|
||||
({ get }) => {
|
||||
const currentStepFilterGroups = get(
|
||||
currentStepFilterGroupsComponentState.atomFamily({ instanceId }),
|
||||
);
|
||||
|
||||
const rootLevelStepFilterGroup = currentStepFilterGroups.find(
|
||||
(stepFilterGroup) =>
|
||||
!isDefined(stepFilterGroup.parentStepFilterGroupId),
|
||||
);
|
||||
|
||||
return rootLevelStepFilterGroup;
|
||||
},
|
||||
componentInstanceContext: StepFilterGroupsComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import {
|
||||
StepFilter,
|
||||
StepFilterGroup,
|
||||
} from 'twenty-shared/src/types/StepFilters';
|
||||
|
||||
export const isStepFilterGroupChildAStepFilterGroup = (
|
||||
child: StepFilter | StepFilterGroup,
|
||||
): child is StepFilterGroup => {
|
||||
return ('logicalOperator' satisfies keyof StepFilterGroup) in child;
|
||||
};
|
||||
@ -1,3 +1,4 @@
|
||||
import { WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
import { Theme } from '@emotion/react';
|
||||
import { COLOR, GRAY_SCALE } from 'twenty-ui/theme';
|
||||
import { getActionIconColorOrThrow } from '../getActionIconColorOrThrow';
|
||||
@ -22,106 +23,277 @@ describe('getActionIconColorOrThrow', () => {
|
||||
).toBe(mockTheme.color.orange);
|
||||
});
|
||||
|
||||
it('should return tertiary font color for CREATE_RECORD action type', () => {
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
describe('action types that return tertiary font color', () => {
|
||||
const recordActionTypes: WorkflowActionType[] = [
|
||||
'CREATE_RECORD',
|
||||
'UPDATE_RECORD',
|
||||
'DELETE_RECORD',
|
||||
'FIND_RECORDS',
|
||||
'FORM',
|
||||
];
|
||||
|
||||
recordActionTypes.forEach((actionType) => {
|
||||
it(`should return tertiary font color for ${actionType} action type`, () => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
|
||||
expect(result).toBe(mockTheme.font.color.tertiary);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('action types that return blue color', () => {
|
||||
it('should return blue color for SEND_EMAIL action type', () => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'SEND_EMAIL',
|
||||
});
|
||||
|
||||
expect(result).toBe(mockTheme.color.blue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('action types that return pink color', () => {
|
||||
it('should return pink color for AI_AGENT action type', () => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'AI_AGENT',
|
||||
});
|
||||
|
||||
expect(result).toBe(mockTheme.color.pink);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FILTER action type', () => {
|
||||
it('should throw an error for FILTER action type', () => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'FILTER',
|
||||
});
|
||||
|
||||
expect(result).toBe(mockTheme.font.color.tertiary);
|
||||
});
|
||||
});
|
||||
|
||||
describe('theme object handling', () => {
|
||||
it('should use the provided theme colors correctly', () => {
|
||||
const customTheme: Theme = {
|
||||
color: {
|
||||
orange: COLOR.red,
|
||||
blue: COLOR.purple,
|
||||
pink: COLOR.turquoise,
|
||||
},
|
||||
font: {
|
||||
color: {
|
||||
tertiary: GRAY_SCALE.gray50,
|
||||
},
|
||||
},
|
||||
} as Theme;
|
||||
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'CODE',
|
||||
}),
|
||||
).toBe(COLOR.red);
|
||||
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'SEND_EMAIL',
|
||||
}),
|
||||
).toBe(COLOR.purple);
|
||||
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'AI_AGENT',
|
||||
}),
|
||||
).toBe(COLOR.turquoise);
|
||||
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'CREATE_RECORD',
|
||||
}),
|
||||
).toBe(GRAY_SCALE.gray50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('type safety and exhaustive checking', () => {
|
||||
it('should handle all valid action types without throwing unreachable errors', () => {
|
||||
const validActionTypes: WorkflowActionType[] = [
|
||||
'CODE',
|
||||
'HTTP_REQUEST',
|
||||
'CREATE_RECORD',
|
||||
'UPDATE_RECORD',
|
||||
'DELETE_RECORD',
|
||||
'FIND_RECORDS',
|
||||
'FORM',
|
||||
'SEND_EMAIL',
|
||||
'AI_AGENT',
|
||||
];
|
||||
|
||||
validActionTypes.forEach((actionType) => {
|
||||
expect(() => {
|
||||
getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return consistent color values for the same action type', () => {
|
||||
const actionType: WorkflowActionType = 'CODE';
|
||||
const result1 = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
const result2 = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
|
||||
expect(result1).toBe(result2);
|
||||
expect(result1).toBe(mockTheme.color.orange);
|
||||
});
|
||||
});
|
||||
|
||||
describe('color grouping logic', () => {
|
||||
it('should group CODE and HTTP_REQUEST actions with orange color', () => {
|
||||
const orangeActions: WorkflowActionType[] = ['CODE', 'HTTP_REQUEST'];
|
||||
|
||||
orangeActions.forEach((actionType) => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
expect(result).toBe(mockTheme.color.orange);
|
||||
});
|
||||
});
|
||||
|
||||
it('should group record-related actions with tertiary font color', () => {
|
||||
const recordActions: WorkflowActionType[] = [
|
||||
'CREATE_RECORD',
|
||||
'UPDATE_RECORD',
|
||||
'DELETE_RECORD',
|
||||
'FIND_RECORDS',
|
||||
'FORM',
|
||||
];
|
||||
|
||||
recordActions.forEach((actionType) => {
|
||||
const result = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType,
|
||||
});
|
||||
expect(result).toBe(mockTheme.font.color.tertiary);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have unique colors for different action categories', () => {
|
||||
const tertiaryResult = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'CREATE_RECORD',
|
||||
}),
|
||||
).toBe(mockTheme.font.color.tertiary);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return blue color for SEND_EMAIL action type', () => {
|
||||
expect(
|
||||
getActionIconColorOrThrow({ theme: mockTheme, actionType: 'SEND_EMAIL' }),
|
||||
).toBe(mockTheme.color.blue);
|
||||
});
|
||||
expect(tertiaryResult).toBe(mockTheme.font.color.tertiary);
|
||||
});
|
||||
|
||||
it('should return pink color for AI_AGENT action type', () => {
|
||||
expect(
|
||||
getActionIconColorOrThrow({ theme: mockTheme, actionType: 'AI_AGENT' }),
|
||||
).toBe(mockTheme.color.pink);
|
||||
});
|
||||
it('should return blue color for SEND_EMAIL action type', () => {
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'SEND_EMAIL',
|
||||
}),
|
||||
).toBe(mockTheme.color.blue);
|
||||
});
|
||||
|
||||
it('should throw an error for FILTER action type', () => {
|
||||
expect(() => {
|
||||
getActionIconColorOrThrow({ theme: mockTheme, actionType: 'FILTER' });
|
||||
}).toThrow("The Filter action isn't meant to be displayed as a node.");
|
||||
});
|
||||
it('should return pink color for AI_AGENT action type', () => {
|
||||
expect(
|
||||
getActionIconColorOrThrow({ theme: mockTheme, actionType: 'AI_AGENT' }),
|
||||
).toBe(mockTheme.color.pink);
|
||||
});
|
||||
|
||||
it('should use the provided theme colors correctly', () => {
|
||||
const customTheme: Theme = {
|
||||
color: {
|
||||
orange: COLOR.red,
|
||||
blue: COLOR.purple,
|
||||
pink: COLOR.turquoise,
|
||||
},
|
||||
font: {
|
||||
it('should use the provided theme colors correctly', () => {
|
||||
const customTheme: Theme = {
|
||||
color: {
|
||||
tertiary: GRAY_SCALE.gray50,
|
||||
orange: COLOR.red,
|
||||
blue: COLOR.purple,
|
||||
pink: COLOR.turquoise,
|
||||
},
|
||||
},
|
||||
} as Theme;
|
||||
font: {
|
||||
color: {
|
||||
tertiary: GRAY_SCALE.gray50,
|
||||
},
|
||||
},
|
||||
} as Theme;
|
||||
|
||||
expect(
|
||||
getActionIconColorOrThrow({ theme: customTheme, actionType: 'CODE' }),
|
||||
).toBe(COLOR.red);
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'SEND_EMAIL',
|
||||
}),
|
||||
).toBe(COLOR.purple);
|
||||
expect(
|
||||
getActionIconColorOrThrow({ theme: customTheme, actionType: 'AI_AGENT' }),
|
||||
).toBe(COLOR.turquoise);
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'CREATE_RECORD',
|
||||
}),
|
||||
).toBe(GRAY_SCALE.gray50);
|
||||
});
|
||||
expect(
|
||||
getActionIconColorOrThrow({ theme: customTheme, actionType: 'CODE' }),
|
||||
).toBe(COLOR.red);
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'SEND_EMAIL',
|
||||
}),
|
||||
).toBe(COLOR.purple);
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'AI_AGENT',
|
||||
}),
|
||||
).toBe(COLOR.turquoise);
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: customTheme,
|
||||
actionType: 'CREATE_RECORD',
|
||||
}),
|
||||
).toBe(GRAY_SCALE.gray50);
|
||||
});
|
||||
|
||||
it('should return undefined when blue color is missing for SEND_EMAIL action', () => {
|
||||
const themeWithoutBlue: Theme = {
|
||||
color: {
|
||||
orange: COLOR.orange,
|
||||
pink: COLOR.pink,
|
||||
},
|
||||
font: {
|
||||
it('should return undefined when blue color is missing for SEND_EMAIL action', () => {
|
||||
const themeWithoutBlue: Theme = {
|
||||
color: {
|
||||
tertiary: GRAY_SCALE.gray40,
|
||||
orange: COLOR.orange,
|
||||
pink: COLOR.pink,
|
||||
},
|
||||
},
|
||||
} as Theme;
|
||||
font: {
|
||||
color: {
|
||||
tertiary: GRAY_SCALE.gray40,
|
||||
},
|
||||
},
|
||||
} as Theme;
|
||||
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: themeWithoutBlue,
|
||||
actionType: 'SEND_EMAIL',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
expect(
|
||||
getActionIconColorOrThrow({
|
||||
theme: themeWithoutBlue,
|
||||
actionType: 'SEND_EMAIL',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle null theme gracefully', () => {
|
||||
expect(() => {
|
||||
getActionIconColorOrThrow({
|
||||
theme: null as unknown as Theme,
|
||||
it('should handle null theme gracefully', () => {
|
||||
expect(() => {
|
||||
getActionIconColorOrThrow({
|
||||
theme: null as unknown as Theme,
|
||||
actionType: 'CODE',
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should return the same color for the same action type', () => {
|
||||
const result1 = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'CODE',
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('should return the same color for the same action type', () => {
|
||||
const result1 = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'CODE',
|
||||
const result2 = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'CODE',
|
||||
});
|
||||
expect(result1).toBe(result2);
|
||||
});
|
||||
const result2 = getActionIconColorOrThrow({
|
||||
theme: mockTheme,
|
||||
actionType: 'CODE',
|
||||
});
|
||||
expect(result1).toBe(result2);
|
||||
});
|
||||
});
|
||||
|
||||
@ -18,9 +18,7 @@ export const getActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
|
||||
case 'AI_AGENT':
|
||||
return msg`AI Agent`;
|
||||
case 'FILTER': {
|
||||
throw new Error(
|
||||
"The Filter action isn't meant to be displayed as a node.",
|
||||
);
|
||||
return msg`Filter`;
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
@ -9,6 +9,8 @@ export const getActionIcon = (actionType: WorkflowActionType) => {
|
||||
case 'DELETE_RECORD':
|
||||
case 'FIND_RECORDS':
|
||||
return RECORD_ACTIONS.find((item) => item.type === actionType)?.icon;
|
||||
case 'FILTER':
|
||||
return 'IconFilter';
|
||||
default:
|
||||
return OTHER_ACTIONS.find((item) => item.type === actionType)?.icon;
|
||||
}
|
||||
|
||||
@ -18,16 +18,12 @@ export const getActionIconColorOrThrow = ({
|
||||
case 'DELETE_RECORD':
|
||||
case 'FIND_RECORDS':
|
||||
case 'FORM':
|
||||
case 'FILTER':
|
||||
return theme.font.color.tertiary;
|
||||
case 'SEND_EMAIL':
|
||||
return theme.color.blue;
|
||||
case 'AI_AGENT':
|
||||
return theme.color.pink;
|
||||
case 'FILTER': {
|
||||
throw new Error(
|
||||
"The Filter action isn't meant to be displayed as a node.",
|
||||
);
|
||||
}
|
||||
default:
|
||||
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
|
||||
}
|
||||
|
||||
@ -37,12 +37,14 @@ export const WorkflowVariablesDropdown = ({
|
||||
disabled,
|
||||
objectNameSingularToSelect,
|
||||
multiline,
|
||||
clickableComponent,
|
||||
}: {
|
||||
instanceId: string;
|
||||
onVariableSelect: (variableName: string) => void;
|
||||
disabled?: boolean;
|
||||
objectNameSingularToSelect?: string;
|
||||
multiline?: boolean;
|
||||
clickableComponent?: React.ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@ -102,12 +104,14 @@ export const WorkflowVariablesDropdown = ({
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
clickableComponent={
|
||||
<StyledDropdownVariableButtonContainer
|
||||
isUnfolded={isDropdownOpen}
|
||||
transparentBackground
|
||||
>
|
||||
<IconVariablePlus size={theme.icon.size.sm} />
|
||||
</StyledDropdownVariableButtonContainer>
|
||||
clickableComponent ?? (
|
||||
<StyledDropdownVariableButtonContainer
|
||||
isUnfolded={isDropdownOpen}
|
||||
transparentBackground
|
||||
>
|
||||
<IconVariablePlus size={theme.icon.size.sm} />
|
||||
</StyledDropdownVariableButtonContainer>
|
||||
)
|
||||
}
|
||||
dropdownComponents={
|
||||
!isDefined(selectedStep) ? (
|
||||
|
||||
@ -592,8 +592,8 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
settings: {
|
||||
...BASE_STEP_DEFINITION,
|
||||
input: {
|
||||
filterGroups: [],
|
||||
filters: [],
|
||||
stepFilterGroups: [],
|
||||
stepFilters: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -2,4 +2,5 @@ export type WorkflowActionOutput = {
|
||||
result?: object;
|
||||
error?: string;
|
||||
pendingEvent?: boolean;
|
||||
shouldEndWorkflowRun?: boolean;
|
||||
};
|
||||
|
||||
@ -33,23 +33,24 @@ export class FilterWorkflowAction implements WorkflowAction {
|
||||
);
|
||||
}
|
||||
|
||||
const { filterGroups, filters } = step.settings.input;
|
||||
const { stepFilterGroups, stepFilters } = step.settings.input;
|
||||
|
||||
if (!filterGroups || !filters) {
|
||||
throw new WorkflowStepExecutorException(
|
||||
'Filter is not defined',
|
||||
WorkflowStepExecutorExceptionCode.INVALID_STEP_SETTINGS,
|
||||
);
|
||||
if (!stepFilterGroups || !stepFilters) {
|
||||
return {
|
||||
result: {
|
||||
shouldEndWorkflowRun: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedFilters = filters.map((filter) => ({
|
||||
const resolvedFilters = stepFilters.map((filter) => ({
|
||||
...filter,
|
||||
rightOperand: resolveInput(filter.value, context),
|
||||
leftOperand: resolveInput(filter.stepOutputKey, context),
|
||||
}));
|
||||
|
||||
const matchesFilter = evaluateFilterConditions({
|
||||
filterGroups,
|
||||
filterGroups: stepFilterGroups,
|
||||
filters: resolvedFilters,
|
||||
});
|
||||
|
||||
@ -57,6 +58,7 @@ export class FilterWorkflowAction implements WorkflowAction {
|
||||
result: {
|
||||
matchesFilter,
|
||||
},
|
||||
shouldEndWorkflowRun: !matchesFilter,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,44 +1,10 @@
|
||||
import { StepFilter, StepFilterGroup } from 'twenty-shared/types';
|
||||
|
||||
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
||||
|
||||
export enum LogicalOperator {
|
||||
AND = 'AND',
|
||||
OR = 'OR',
|
||||
}
|
||||
|
||||
export enum Operand {
|
||||
EQ = 'eq',
|
||||
NE = 'ne',
|
||||
GT = 'gt',
|
||||
GTE = 'gte',
|
||||
LT = 'lt',
|
||||
LTE = 'lte',
|
||||
LIKE = 'like',
|
||||
ILIKE = 'ilike',
|
||||
IN = 'in',
|
||||
IS = 'is',
|
||||
}
|
||||
|
||||
export type FilterGroup = {
|
||||
id: string;
|
||||
logicalOperator: LogicalOperator;
|
||||
parentRecordFilterGroupId?: string;
|
||||
positionInRecordFilterGroup?: number;
|
||||
};
|
||||
|
||||
export type Filter = {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
value: string;
|
||||
operand: Operand;
|
||||
displayValue: string;
|
||||
recordFilterGroupId: string;
|
||||
stepOutputKey: string;
|
||||
};
|
||||
|
||||
export type WorkflowFilterActionSettings = BaseWorkflowActionSettings & {
|
||||
input: {
|
||||
filterGroups?: FilterGroup[];
|
||||
filters?: Filter[];
|
||||
stepFilterGroups?: StepFilterGroup[];
|
||||
stepFilters?: StepFilter[];
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,21 +1,11 @@
|
||||
import {
|
||||
FilterGroup,
|
||||
LogicalOperator,
|
||||
Operand,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/workflow-filter-action-settings.type';
|
||||
import { evaluateFilterConditions } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util';
|
||||
StepFilter,
|
||||
StepFilterGroup,
|
||||
StepLogicalOperator,
|
||||
StepOperand,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
type ResolvedFilter = {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
rightOperand: unknown;
|
||||
operand: Operand;
|
||||
displayValue: string;
|
||||
fieldMetadataId: string;
|
||||
recordFilterGroupId: string;
|
||||
leftOperand: unknown;
|
||||
};
|
||||
import { evaluateFilterConditions } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util';
|
||||
|
||||
describe('evaluateFilterConditions', () => {
|
||||
describe('empty inputs', () => {
|
||||
@ -36,8 +26,13 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
describe('single filter operands', () => {
|
||||
type ResolvedFilter = Omit<StepFilter, 'value' | 'stepOutputKey'> & {
|
||||
rightOperand: unknown;
|
||||
leftOperand: unknown;
|
||||
};
|
||||
|
||||
const createFilter = (
|
||||
operand: Operand,
|
||||
operand: StepOperand,
|
||||
leftOperand: unknown,
|
||||
rightOperand: unknown,
|
||||
): ResolvedFilter => ({
|
||||
@ -47,28 +42,27 @@ describe('evaluateFilterConditions', () => {
|
||||
rightOperand,
|
||||
operand,
|
||||
displayValue: String(rightOperand),
|
||||
fieldMetadataId: 'field1',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand,
|
||||
});
|
||||
|
||||
describe('eq operand', () => {
|
||||
it('should return true when values are equal', () => {
|
||||
const filter = createFilter(Operand.EQ, 'John', 'John');
|
||||
const filter = createFilter(StepOperand.EQ, 'John', 'John');
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when values are loosely equal', () => {
|
||||
const filter = createFilter(Operand.EQ, '123', 123);
|
||||
const filter = createFilter(StepOperand.EQ, '123', 123);
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when values are not equal', () => {
|
||||
const filter = createFilter(Operand.EQ, 'John', 'Jane');
|
||||
const filter = createFilter(StepOperand.EQ, 'John', 'Jane');
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(false);
|
||||
@ -77,14 +71,14 @@ describe('evaluateFilterConditions', () => {
|
||||
|
||||
describe('ne operand', () => {
|
||||
it('should return false when values are equal', () => {
|
||||
const filter = createFilter(Operand.NE, 'John', 'John');
|
||||
const filter = createFilter(StepOperand.NE, 'John', 'John');
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when values are not equal', () => {
|
||||
const filter = createFilter(Operand.NE, 'John', 'Jane');
|
||||
const filter = createFilter(StepOperand.NE, 'John', 'Jane');
|
||||
const result = evaluateFilterConditions({ filters: [filter] });
|
||||
|
||||
expect(result).toBe(true);
|
||||
@ -93,17 +87,17 @@ describe('evaluateFilterConditions', () => {
|
||||
|
||||
describe('numeric operands', () => {
|
||||
it('should handle gt operand correctly', () => {
|
||||
const filter1 = createFilter(Operand.GT, 30, 25);
|
||||
const filter2 = createFilter(Operand.GT, 20, 25);
|
||||
const filter1 = createFilter(StepOperand.GT, 30, 25);
|
||||
const filter2 = createFilter(StepOperand.GT, 20, 25);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle gte operand correctly', () => {
|
||||
const filter1 = createFilter(Operand.GTE, 25, 25);
|
||||
const filter2 = createFilter(Operand.GTE, 30, 25);
|
||||
const filter3 = createFilter(Operand.GTE, 20, 25);
|
||||
const filter1 = createFilter(StepOperand.GTE, 25, 25);
|
||||
const filter2 = createFilter(StepOperand.GTE, 30, 25);
|
||||
const filter3 = createFilter(StepOperand.GTE, 20, 25);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
@ -111,17 +105,17 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
it('should handle lt operand correctly', () => {
|
||||
const filter1 = createFilter(Operand.LT, 20, 25);
|
||||
const filter2 = createFilter(Operand.LT, 30, 25);
|
||||
const filter1 = createFilter(StepOperand.LT, 20, 25);
|
||||
const filter2 = createFilter(StepOperand.LT, 30, 25);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle lte operand correctly', () => {
|
||||
const filter1 = createFilter(Operand.LTE, 25, 25);
|
||||
const filter2 = createFilter(Operand.LTE, 20, 25);
|
||||
const filter3 = createFilter(Operand.LTE, 30, 25);
|
||||
const filter1 = createFilter(StepOperand.LTE, 25, 25);
|
||||
const filter2 = createFilter(StepOperand.LTE, 20, 25);
|
||||
const filter3 = createFilter(StepOperand.LTE, 30, 25);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
@ -129,8 +123,8 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
it('should convert string numbers for numeric comparisons', () => {
|
||||
const filter1 = createFilter(Operand.GT, '30', '25');
|
||||
const filter2 = createFilter(Operand.LT, '20', '25');
|
||||
const filter1 = createFilter(StepOperand.GT, '30', '25');
|
||||
const filter2 = createFilter(StepOperand.LT, '20', '25');
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
@ -139,17 +133,17 @@ describe('evaluateFilterConditions', () => {
|
||||
|
||||
describe('string operands', () => {
|
||||
it('should handle like operand correctly', () => {
|
||||
const filter1 = createFilter(Operand.LIKE, 'Hello World', 'World');
|
||||
const filter2 = createFilter(Operand.LIKE, 'Hello', 'World');
|
||||
const filter1 = createFilter(StepOperand.LIKE, 'Hello World', 'World');
|
||||
const filter2 = createFilter(StepOperand.LIKE, 'Hello', 'World');
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle ilike operand correctly (case insensitive)', () => {
|
||||
const filter1 = createFilter(Operand.ILIKE, 'Hello World', 'WORLD');
|
||||
const filter2 = createFilter(Operand.ILIKE, 'Hello World', 'world');
|
||||
const filter3 = createFilter(Operand.ILIKE, 'Hello', 'WORLD');
|
||||
const filter1 = createFilter(StepOperand.ILIKE, 'Hello World', 'WORLD');
|
||||
const filter2 = createFilter(StepOperand.ILIKE, 'Hello World', 'world');
|
||||
const filter3 = createFilter(StepOperand.ILIKE, 'Hello', 'WORLD');
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
@ -160,12 +154,12 @@ describe('evaluateFilterConditions', () => {
|
||||
describe('in operand', () => {
|
||||
it('should handle JSON array values', () => {
|
||||
const filter1 = createFilter(
|
||||
Operand.IN,
|
||||
StepOperand.IN,
|
||||
'apple',
|
||||
'["apple", "banana", "cherry"]',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
Operand.IN,
|
||||
StepOperand.IN,
|
||||
'grape',
|
||||
'["apple", "banana", "cherry"]',
|
||||
);
|
||||
@ -176,12 +170,12 @@ describe('evaluateFilterConditions', () => {
|
||||
|
||||
it('should handle comma-separated string values when JSON parsing fails', () => {
|
||||
const filter1 = createFilter(
|
||||
Operand.IN,
|
||||
StepOperand.IN,
|
||||
'apple',
|
||||
'apple, banana, cherry',
|
||||
);
|
||||
const filter2 = createFilter(
|
||||
Operand.IN,
|
||||
StepOperand.IN,
|
||||
'grape',
|
||||
'apple, banana, cherry',
|
||||
);
|
||||
@ -192,7 +186,7 @@ describe('evaluateFilterConditions', () => {
|
||||
|
||||
it('should handle comma-separated values with whitespace', () => {
|
||||
const filter = createFilter(
|
||||
Operand.IN,
|
||||
StepOperand.IN,
|
||||
'apple',
|
||||
' apple , banana , cherry ',
|
||||
);
|
||||
@ -201,7 +195,11 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
it('should return false for non-array JSON values', () => {
|
||||
const filter = createFilter(Operand.IN, 'apple', '{"key": "value"}');
|
||||
const filter = createFilter(
|
||||
StepOperand.IN,
|
||||
'apple',
|
||||
'{"key": "value"}',
|
||||
);
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter] })).toBe(false);
|
||||
});
|
||||
@ -209,9 +207,9 @@ describe('evaluateFilterConditions', () => {
|
||||
|
||||
describe('is operand', () => {
|
||||
it('should handle null checks', () => {
|
||||
const filter1 = createFilter(Operand.IS, null, 'null');
|
||||
const filter2 = createFilter(Operand.IS, undefined, 'NULL');
|
||||
const filter3 = createFilter(Operand.IS, 'value', 'null');
|
||||
const filter1 = createFilter(StepOperand.IS, null, 'null');
|
||||
const filter2 = createFilter(StepOperand.IS, undefined, 'NULL');
|
||||
const filter3 = createFilter(StepOperand.IS, 'value', 'null');
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
@ -219,10 +217,10 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
it('should handle not null checks', () => {
|
||||
const filter1 = createFilter(Operand.IS, 'value', 'not null');
|
||||
const filter2 = createFilter(Operand.IS, 'value', 'NOT NULL');
|
||||
const filter3 = createFilter(Operand.IS, null, 'not null');
|
||||
const filter4 = createFilter(Operand.IS, undefined, 'not null');
|
||||
const filter1 = createFilter(StepOperand.IS, 'value', 'not null');
|
||||
const filter2 = createFilter(StepOperand.IS, 'value', 'NOT NULL');
|
||||
const filter3 = createFilter(StepOperand.IS, null, 'not null');
|
||||
const filter4 = createFilter(StepOperand.IS, undefined, 'not null');
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(true);
|
||||
@ -231,8 +229,8 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
it('should handle exact value comparisons for non-null/not-null cases', () => {
|
||||
const filter1 = createFilter(Operand.IS, 'exact', 'exact');
|
||||
const filter2 = createFilter(Operand.IS, 'value', 'different');
|
||||
const filter1 = createFilter(StepOperand.IS, 'exact', 'exact');
|
||||
const filter2 = createFilter(StepOperand.IS, 'value', 'different');
|
||||
|
||||
expect(evaluateFilterConditions({ filters: [filter1] })).toBe(true);
|
||||
expect(evaluateFilterConditions({ filters: [filter2] })).toBe(false);
|
||||
@ -241,7 +239,7 @@ describe('evaluateFilterConditions', () => {
|
||||
|
||||
describe('error cases', () => {
|
||||
it('should throw error for unknown operand', () => {
|
||||
const filter = createFilter('unknown' as Operand, 'value', 'value');
|
||||
const filter = createFilter('unknown' as StepOperand, 'value', 'value');
|
||||
|
||||
expect(() => evaluateFilterConditions({ filters: [filter] })).toThrow(
|
||||
'Unknown operand: unknown',
|
||||
@ -251,6 +249,11 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
describe('multiple filters without groups', () => {
|
||||
type ResolvedFilter = Omit<StepFilter, 'value' | 'stepOutputKey'> & {
|
||||
rightOperand: unknown;
|
||||
leftOperand: unknown;
|
||||
};
|
||||
|
||||
it('should apply AND logic by default for multiple filters', () => {
|
||||
const filters: ResolvedFilter[] = [
|
||||
{
|
||||
@ -258,10 +261,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
fieldMetadataId: 'field1',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John',
|
||||
},
|
||||
{
|
||||
@ -269,10 +271,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: Operand.GT,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '25',
|
||||
fieldMetadataId: 'field2',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 30,
|
||||
},
|
||||
];
|
||||
@ -289,10 +290,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
fieldMetadataId: 'field1',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John',
|
||||
},
|
||||
{
|
||||
@ -300,10 +300,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: Operand.GT,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '25',
|
||||
fieldMetadataId: 'field2',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 20, // This will fail
|
||||
},
|
||||
];
|
||||
@ -315,12 +314,17 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
describe('filter groups', () => {
|
||||
type ResolvedFilter = Omit<StepFilter, 'value' | 'stepOutputKey'> & {
|
||||
rightOperand: unknown;
|
||||
leftOperand: unknown;
|
||||
};
|
||||
|
||||
describe('single group with AND logic', () => {
|
||||
it('should return true when all filters pass', () => {
|
||||
const filterGroups: FilterGroup[] = [
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: LogicalOperator.AND,
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
];
|
||||
|
||||
@ -330,10 +334,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
fieldMetadataId: 'field1',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John',
|
||||
},
|
||||
{
|
||||
@ -341,10 +344,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: Operand.GT,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '25',
|
||||
fieldMetadataId: 'field2',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 30,
|
||||
},
|
||||
];
|
||||
@ -355,10 +357,10 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
it('should return false when one filter fails', () => {
|
||||
const filterGroups: FilterGroup[] = [
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: LogicalOperator.AND,
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
];
|
||||
|
||||
@ -368,10 +370,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
fieldMetadataId: 'field1',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'Jane', // This will fail
|
||||
},
|
||||
{
|
||||
@ -379,10 +380,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: Operand.GT,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '25',
|
||||
fieldMetadataId: 'field2',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 30,
|
||||
},
|
||||
];
|
||||
@ -395,10 +395,10 @@ describe('evaluateFilterConditions', () => {
|
||||
|
||||
describe('single group with OR logic', () => {
|
||||
it('should return true when at least one filter passes', () => {
|
||||
const filterGroups: FilterGroup[] = [
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: LogicalOperator.OR,
|
||||
logicalOperator: StepLogicalOperator.OR,
|
||||
},
|
||||
];
|
||||
|
||||
@ -408,10 +408,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
fieldMetadataId: 'field1',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'Jane', // This will fail
|
||||
},
|
||||
{
|
||||
@ -419,10 +418,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: Operand.GT,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '25',
|
||||
fieldMetadataId: 'field2',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 30, // This will pass
|
||||
},
|
||||
];
|
||||
@ -433,10 +431,10 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
it('should return false when all filters fail', () => {
|
||||
const filterGroups: FilterGroup[] = [
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: LogicalOperator.OR,
|
||||
logicalOperator: StepLogicalOperator.OR,
|
||||
},
|
||||
];
|
||||
|
||||
@ -446,10 +444,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
fieldMetadataId: 'field1',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'Jane', // This will fail
|
||||
},
|
||||
{
|
||||
@ -457,10 +454,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: Operand.GT,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '25',
|
||||
fieldMetadataId: 'field2',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 20, // This will fail
|
||||
},
|
||||
];
|
||||
@ -473,10 +469,10 @@ describe('evaluateFilterConditions', () => {
|
||||
|
||||
describe('empty groups', () => {
|
||||
it('should return true for empty group', () => {
|
||||
const filterGroups: FilterGroup[] = [
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: LogicalOperator.AND,
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
];
|
||||
|
||||
@ -488,16 +484,16 @@ describe('evaluateFilterConditions', () => {
|
||||
|
||||
describe('nested groups', () => {
|
||||
it('should handle nested groups correctly', () => {
|
||||
const filterGroups: FilterGroup[] = [
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: LogicalOperator.AND,
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
logicalOperator: LogicalOperator.OR,
|
||||
parentRecordFilterGroupId: 'group1',
|
||||
positionInRecordFilterGroup: 1,
|
||||
logicalOperator: StepLogicalOperator.OR,
|
||||
parentStepFilterGroupId: 'group1',
|
||||
positionInStepFilterGroup: 1,
|
||||
},
|
||||
];
|
||||
|
||||
@ -508,10 +504,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
fieldMetadataId: 'field1',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John', // This will pass
|
||||
},
|
||||
// Filters in child group with OR logic
|
||||
@ -520,10 +515,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 25,
|
||||
operand: Operand.GT,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '25',
|
||||
fieldMetadataId: 'field2',
|
||||
recordFilterGroupId: 'group2',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 20, // This will fail
|
||||
},
|
||||
{
|
||||
@ -531,10 +525,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'City Filter',
|
||||
rightOperand: 'NYC',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'NYC',
|
||||
fieldMetadataId: 'field3',
|
||||
recordFilterGroupId: 'group2',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 'NYC', // This will pass
|
||||
},
|
||||
];
|
||||
@ -546,22 +539,22 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
it('should handle multiple child groups with correct positioning', () => {
|
||||
const filterGroups: FilterGroup[] = [
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: LogicalOperator.AND,
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
logicalOperator: LogicalOperator.OR,
|
||||
parentRecordFilterGroupId: 'group1',
|
||||
positionInRecordFilterGroup: 1,
|
||||
logicalOperator: StepLogicalOperator.OR,
|
||||
parentStepFilterGroupId: 'group1',
|
||||
positionInStepFilterGroup: 1,
|
||||
},
|
||||
{
|
||||
id: 'group3',
|
||||
logicalOperator: LogicalOperator.AND,
|
||||
parentRecordFilterGroupId: 'group1',
|
||||
positionInRecordFilterGroup: 2,
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
parentStepFilterGroupId: 'group1',
|
||||
positionInStepFilterGroup: 2,
|
||||
},
|
||||
];
|
||||
|
||||
@ -572,10 +565,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
fieldMetadataId: 'field1',
|
||||
recordFilterGroupId: 'group2',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 'Jane', // This will fail
|
||||
},
|
||||
{
|
||||
@ -583,10 +575,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Status Filter',
|
||||
rightOperand: 'active',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'active',
|
||||
fieldMetadataId: 'field2',
|
||||
recordFilterGroupId: 'group2',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 'active', // This will pass
|
||||
},
|
||||
// Group3 filters (AND logic)
|
||||
@ -595,10 +586,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 18,
|
||||
operand: Operand.GTE,
|
||||
operand: StepOperand.GTE,
|
||||
displayValue: '18',
|
||||
fieldMetadataId: 'field3',
|
||||
recordFilterGroupId: 'group3',
|
||||
stepFilterGroupId: 'group3',
|
||||
leftOperand: 25, // This will pass
|
||||
},
|
||||
{
|
||||
@ -606,10 +596,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Score Filter',
|
||||
rightOperand: 80,
|
||||
operand: Operand.GT,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '80',
|
||||
fieldMetadataId: 'field4',
|
||||
recordFilterGroupId: 'group3',
|
||||
stepFilterGroupId: 'group3',
|
||||
leftOperand: 85, // This will pass
|
||||
},
|
||||
];
|
||||
@ -623,14 +612,14 @@ describe('evaluateFilterConditions', () => {
|
||||
|
||||
describe('multiple root groups', () => {
|
||||
it('should combine multiple root groups with AND logic', () => {
|
||||
const filterGroups: FilterGroup[] = [
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: LogicalOperator.OR,
|
||||
logicalOperator: StepLogicalOperator.OR,
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
logicalOperator: LogicalOperator.AND,
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
];
|
||||
|
||||
@ -641,10 +630,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
fieldMetadataId: 'field1',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John', // This will pass
|
||||
},
|
||||
{
|
||||
@ -652,10 +640,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Status Filter',
|
||||
rightOperand: 'inactive',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'inactive',
|
||||
fieldMetadataId: 'field2',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'active', // This will fail
|
||||
},
|
||||
// Group2 filters (AND logic)
|
||||
@ -664,10 +651,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 18,
|
||||
operand: Operand.GTE,
|
||||
operand: StepOperand.GTE,
|
||||
displayValue: '18',
|
||||
fieldMetadataId: 'field3',
|
||||
recordFilterGroupId: 'group2',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 25, // This will pass
|
||||
},
|
||||
{
|
||||
@ -675,10 +661,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Score Filter',
|
||||
rightOperand: 80,
|
||||
operand: Operand.GT,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '80',
|
||||
fieldMetadataId: 'field4',
|
||||
recordFilterGroupId: 'group2',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 85, // This will pass
|
||||
},
|
||||
];
|
||||
@ -690,14 +675,14 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
it('should return false when one root group fails', () => {
|
||||
const filterGroups: FilterGroup[] = [
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: LogicalOperator.OR,
|
||||
logicalOperator: StepLogicalOperator.OR,
|
||||
},
|
||||
{
|
||||
id: 'group2',
|
||||
logicalOperator: LogicalOperator.AND,
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
];
|
||||
|
||||
@ -708,10 +693,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
fieldMetadataId: 'field1',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John', // This will pass
|
||||
},
|
||||
// Group2 filters (AND logic) - will fail
|
||||
@ -720,10 +704,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Age Filter',
|
||||
rightOperand: 30,
|
||||
operand: Operand.GT,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '30',
|
||||
fieldMetadataId: 'field2',
|
||||
recordFilterGroupId: 'group2',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 25, // This will fail
|
||||
},
|
||||
{
|
||||
@ -731,10 +714,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'number',
|
||||
label: 'Score Filter',
|
||||
rightOperand: 80,
|
||||
operand: Operand.GT,
|
||||
operand: StepOperand.GT,
|
||||
displayValue: '80',
|
||||
fieldMetadataId: 'field3',
|
||||
recordFilterGroupId: 'group2',
|
||||
stepFilterGroupId: 'group2',
|
||||
leftOperand: 85, // This will pass
|
||||
},
|
||||
];
|
||||
@ -748,10 +730,10 @@ describe('evaluateFilterConditions', () => {
|
||||
|
||||
describe('error cases', () => {
|
||||
it('should throw error when filter group is not found', () => {
|
||||
const filterGroups: FilterGroup[] = [
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: LogicalOperator.AND,
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
];
|
||||
|
||||
@ -761,10 +743,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
fieldMetadataId: 'field1',
|
||||
recordFilterGroupId: 'nonexistent-group',
|
||||
stepFilterGroupId: 'nonexistent-group',
|
||||
leftOperand: 'John',
|
||||
},
|
||||
];
|
||||
@ -775,10 +756,10 @@ describe('evaluateFilterConditions', () => {
|
||||
});
|
||||
|
||||
it('should throw error for unknown logical operator', () => {
|
||||
const filterGroups: FilterGroup[] = [
|
||||
const filterGroups: StepFilterGroup[] = [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: 'UNKNOWN' as LogicalOperator,
|
||||
logicalOperator: 'UNKNOWN' as StepLogicalOperator,
|
||||
},
|
||||
];
|
||||
|
||||
@ -788,10 +769,9 @@ describe('evaluateFilterConditions', () => {
|
||||
type: 'text',
|
||||
label: 'Name Filter',
|
||||
rightOperand: 'John',
|
||||
operand: Operand.EQ,
|
||||
operand: StepOperand.EQ,
|
||||
displayValue: 'John',
|
||||
fieldMetadataId: 'field1',
|
||||
recordFilterGroupId: 'group1',
|
||||
stepFilterGroupId: 'group1',
|
||||
leftOperand: 'John',
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
import {
|
||||
Filter,
|
||||
FilterGroup,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/workflow-filter-action-settings.type';
|
||||
import { StepFilter, StepFilterGroup } from 'twenty-shared/types';
|
||||
|
||||
type ResolvedFilter = Omit<Filter, 'value' | 'stepOutputKey'> & {
|
||||
type ResolvedFilter = Omit<StepFilter, 'value' | 'stepOutputKey'> & {
|
||||
rightOperand: unknown;
|
||||
leftOperand: unknown;
|
||||
};
|
||||
@ -72,7 +69,7 @@ function evaluateFilter(filter: ResolvedFilter): boolean {
|
||||
*/
|
||||
function evaluateFilterGroup(
|
||||
groupId: string,
|
||||
filterGroups: FilterGroup[],
|
||||
filterGroups: StepFilterGroup[],
|
||||
filters: ResolvedFilter[],
|
||||
): boolean {
|
||||
const group = filterGroups.find((g) => g.id === groupId);
|
||||
@ -83,14 +80,13 @@ function evaluateFilterGroup(
|
||||
|
||||
// Get all direct child groups
|
||||
const childGroups = filterGroups
|
||||
.filter((g) => g.parentRecordFilterGroupId === groupId)
|
||||
.filter((g) => g.parentStepFilterGroupId === groupId)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(a.positionInRecordFilterGroup || 0) -
|
||||
(b.positionInRecordFilterGroup || 0),
|
||||
(a.positionInStepFilterGroup || 0) - (b.positionInStepFilterGroup || 0),
|
||||
);
|
||||
|
||||
const groupFilters = filters.filter((f) => f.recordFilterGroupId === groupId);
|
||||
const groupFilters = filters.filter((f) => f.stepFilterGroupId === groupId);
|
||||
|
||||
const filterResults = groupFilters.map((filter) => evaluateFilter(filter));
|
||||
|
||||
@ -120,7 +116,7 @@ export function evaluateFilterConditions({
|
||||
filterGroups = [],
|
||||
filters = [],
|
||||
}: {
|
||||
filterGroups?: FilterGroup[];
|
||||
filterGroups?: StepFilterGroup[];
|
||||
filters?: ResolvedFilter[];
|
||||
}): boolean {
|
||||
if (filterGroups.length === 0 && filters.length === 0) {
|
||||
@ -131,15 +127,15 @@ export function evaluateFilterConditions({
|
||||
const groupIds = new Set(filterGroups.map((g) => g.id));
|
||||
|
||||
for (const filter of filters) {
|
||||
if (!groupIds.has(filter.recordFilterGroupId)) {
|
||||
if (!groupIds.has(filter.stepFilterGroupId)) {
|
||||
throw new Error(
|
||||
`Filter group with id ${filter.recordFilterGroupId} not found`,
|
||||
`Filter group with id ${filter.stepFilterGroupId} not found`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootGroups = filterGroups.filter((g) => !g.parentRecordFilterGroupId);
|
||||
const rootGroups = filterGroups.filter((g) => !g.parentStepFilterGroupId);
|
||||
|
||||
if (rootGroups.length === 0 && filters.length > 0) {
|
||||
const filterResults = filters.map((filter) => evaluateFilter(filter));
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
WorkflowStepExecutorException,
|
||||
WorkflowStepExecutorExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const getPreviousStepOutput = (
|
||||
steps: WorkflowAction[],
|
||||
currentStepId: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
context: Record<string, any>,
|
||||
) => {
|
||||
const previousSteps = steps.filter((step) =>
|
||||
step?.nextStepIds?.includes(currentStepId),
|
||||
);
|
||||
|
||||
if (previousSteps.length === 0) {
|
||||
throw new WorkflowStepExecutorException(
|
||||
'Filter action must have a previous step',
|
||||
WorkflowStepExecutorExceptionCode.FAILED_TO_EXECUTE_STEP,
|
||||
{
|
||||
userFriendlyMessage: t`Filter action must have a previous step`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (previousSteps.length > 1) {
|
||||
throw new WorkflowStepExecutorException(
|
||||
'Filter action must have only one previous step',
|
||||
WorkflowStepExecutorExceptionCode.FAILED_TO_EXECUTE_STEP,
|
||||
{
|
||||
userFriendlyMessage: t`Filter action must have only one previous step`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const previousStep = previousSteps[0];
|
||||
const previousStepOutput = context[previousStep.id];
|
||||
|
||||
if (!previousStepOutput) {
|
||||
throw new WorkflowStepExecutorException(
|
||||
'Previous step output not found',
|
||||
WorkflowStepExecutorExceptionCode.FAILED_TO_EXECUTE_STEP,
|
||||
);
|
||||
}
|
||||
|
||||
return previousStepOutput;
|
||||
};
|
||||
@ -15,13 +15,13 @@ import {
|
||||
} from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory';
|
||||
import { WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
||||
import { StepStatus } from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
|
||||
import {
|
||||
WorkflowBranchExecutorInput,
|
||||
WorkflowExecutorInput,
|
||||
} from 'src/modules/workflow/workflow-executor/types/workflow-executor-input';
|
||||
import { StepStatus } from 'src/modules/workflow/workflow-executor/types/workflow-run-step-info.type';
|
||||
import { canExecuteStep } from 'src/modules/workflow/workflow-executor/utils/can-execute-step.utils';
|
||||
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
||||
|
||||
const MAX_RETRIES_ON_FAILURE = 3;
|
||||
|
||||
@ -128,11 +128,11 @@ export class WorkflowExecutorWorkspaceService {
|
||||
|
||||
const actionOutputSuccess = isDefined(actionOutput.result);
|
||||
|
||||
const shouldContinue =
|
||||
const isValidActionOutput =
|
||||
actionOutputSuccess ||
|
||||
stepToExecute.settings.errorHandlingOptions.continueOnFailure.value;
|
||||
|
||||
if (shouldContinue) {
|
||||
if (isValidActionOutput) {
|
||||
await this.workflowRunWorkspaceService.saveWorkflowRunState({
|
||||
workflowRunId,
|
||||
stepOutput,
|
||||
@ -144,7 +144,8 @@ export class WorkflowExecutorWorkspaceService {
|
||||
|
||||
if (
|
||||
!isDefined(stepToExecute.nextStepIds) ||
|
||||
stepToExecute.nextStepIds.length === 0
|
||||
stepToExecute.nextStepIds.length === 0 ||
|
||||
actionOutput.shouldEndWorkflowRun === true
|
||||
) {
|
||||
await this.workflowRunWorkspaceService.endWorkflowRun({
|
||||
workflowRunId,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ObjectRecordsPermissions } from '@/types/ObjectRecordsPermissions';
|
||||
import { ObjectRecordsPermissions } from './ObjectRecordsPermissions';
|
||||
|
||||
type RoleId = string;
|
||||
export type ObjectRecordsPermissionsByRoleId = Record<
|
||||
|
||||
36
packages/twenty-shared/src/types/StepFilters.ts
Normal file
36
packages/twenty-shared/src/types/StepFilters.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export enum StepLogicalOperator {
|
||||
AND = 'AND',
|
||||
OR = 'OR',
|
||||
}
|
||||
|
||||
export enum StepOperand {
|
||||
EQ = 'eq',
|
||||
NE = 'ne',
|
||||
GT = 'gt',
|
||||
GTE = 'gte',
|
||||
LT = 'lt',
|
||||
LTE = 'lte',
|
||||
LIKE = 'like',
|
||||
ILIKE = 'ilike',
|
||||
IN = 'in',
|
||||
IS = 'is',
|
||||
}
|
||||
|
||||
export type StepFilterGroup = {
|
||||
id: string;
|
||||
logicalOperator: StepLogicalOperator;
|
||||
parentStepFilterGroupId?: string;
|
||||
positionInStepFilterGroup?: number;
|
||||
};
|
||||
|
||||
export type StepFilter = {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
value: string;
|
||||
operand: StepOperand;
|
||||
displayValue: string;
|
||||
stepFilterGroupId: string;
|
||||
stepOutputKey: string;
|
||||
positionInStepFilterGroup?: number;
|
||||
};
|
||||
@ -13,3 +13,5 @@ export { FieldMetadataType } from './FieldMetadataType';
|
||||
export type { IsExactly } from './IsExactly';
|
||||
export type { ObjectRecordsPermissions } from './ObjectRecordsPermissions';
|
||||
export type { ObjectRecordsPermissionsByRoleId } from './ObjectRecordsPermissionsByRoleId';
|
||||
export type { StepFilterGroup, StepFilter } from './StepFilters';
|
||||
export { StepLogicalOperator, StepOperand } from './StepFilters';
|
||||
|
||||
Reference in New Issue
Block a user