Scaffold filters creation and deletion (#12990)
When the feature flag is activated, we can now create filters and delete them. This PR mainly updates how we generate workflow diagrams. https://github.com/user-attachments/assets/1a4aef46-7c3c-45fa-953f-0bd1908b9be7
This commit is contained in:
committed by
GitHub
parent
ba67e0d5f4
commit
e8a2d71844
@ -1,9 +1,15 @@
|
|||||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
|
import {
|
||||||
|
WorkflowActionType,
|
||||||
|
WorkflowWithCurrentVersion,
|
||||||
|
} from '@/workflow/types/Workflow';
|
||||||
import { RightDrawerStepListContainer } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepContainer';
|
import { RightDrawerStepListContainer } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepContainer';
|
||||||
import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle';
|
import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle';
|
||||||
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
|
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
|
||||||
|
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
||||||
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
|
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
|
||||||
import { useFilteredOtherActions } from '@/workflow/workflow-steps/workflow-actions/hooks/useFilteredOtherActions';
|
import { useFilteredOtherActions } from '@/workflow/workflow-steps/workflow-actions/hooks/useFilteredOtherActions';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { useIcons } from 'twenty-ui/display';
|
import { useIcons } from 'twenty-ui/display';
|
||||||
import { MenuItemCommand } from 'twenty-ui/navigation';
|
import { MenuItemCommand } from 'twenty-ui/navigation';
|
||||||
|
|
||||||
@ -13,11 +19,36 @@ export const CommandMenuWorkflowSelectActionContent = ({
|
|||||||
workflow: WorkflowWithCurrentVersion;
|
workflow: WorkflowWithCurrentVersion;
|
||||||
}) => {
|
}) => {
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
const { createStep } = useCreateStep({
|
const { createStep } = useCreateStep({
|
||||||
workflow,
|
workflow,
|
||||||
});
|
});
|
||||||
const filteredOtherActions = useFilteredOtherActions();
|
const filteredOtherActions = useFilteredOtherActions();
|
||||||
|
|
||||||
|
const [workflowInsertStepIds, setWorkflowInsertStepIds] =
|
||||||
|
useRecoilComponentStateV2(workflowInsertStepIdsComponentState);
|
||||||
|
|
||||||
|
const handleCreateStep = async (actionType: WorkflowActionType) => {
|
||||||
|
const { parentStepId, nextStepId } = workflowInsertStepIds;
|
||||||
|
|
||||||
|
if (!isDefined(parentStepId)) {
|
||||||
|
throw new Error(
|
||||||
|
'No parentStepId. Please select a parent step to create from.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await createStep({
|
||||||
|
newStepType: actionType,
|
||||||
|
parentStepId,
|
||||||
|
nextStepId,
|
||||||
|
});
|
||||||
|
|
||||||
|
setWorkflowInsertStepIds({
|
||||||
|
parentStepId: undefined,
|
||||||
|
nextStepId: undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RightDrawerStepListContainer>
|
<RightDrawerStepListContainer>
|
||||||
<RightDrawerWorkflowSelectStepTitle>
|
<RightDrawerWorkflowSelectStepTitle>
|
||||||
@ -28,7 +59,7 @@ export const CommandMenuWorkflowSelectActionContent = ({
|
|||||||
key={action.type}
|
key={action.type}
|
||||||
LeftIcon={getIcon(action.icon)}
|
LeftIcon={getIcon(action.icon)}
|
||||||
text={action.label}
|
text={action.label}
|
||||||
onClick={() => createStep(action.type)}
|
onClick={() => handleCreateStep(action.type)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<RightDrawerWorkflowSelectStepTitle>
|
<RightDrawerWorkflowSelectStepTitle>
|
||||||
@ -39,7 +70,7 @@ export const CommandMenuWorkflowSelectActionContent = ({
|
|||||||
key={action.type}
|
key={action.type}
|
||||||
LeftIcon={getIcon(action.icon)}
|
LeftIcon={getIcon(action.icon)}
|
||||||
text={action.label}
|
text={action.label}
|
||||||
onClick={() => createStep(action.type)}
|
onClick={() => handleCreateStep(action.type)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</RightDrawerStepListContainer>
|
</RightDrawerStepListContainer>
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import {
|
|||||||
workflowDeleteRecordActionSchema,
|
workflowDeleteRecordActionSchema,
|
||||||
workflowDeleteRecordActionSettingsSchema,
|
workflowDeleteRecordActionSettingsSchema,
|
||||||
workflowExecutorOutputSchema,
|
workflowExecutorOutputSchema,
|
||||||
|
workflowFilterActionSchema,
|
||||||
|
workflowFilterActionSettingsSchema,
|
||||||
workflowFindRecordsActionSchema,
|
workflowFindRecordsActionSchema,
|
||||||
workflowFindRecordsActionSettingsSchema,
|
workflowFindRecordsActionSettingsSchema,
|
||||||
workflowFormActionSchema,
|
workflowFormActionSchema,
|
||||||
@ -48,6 +50,9 @@ export type WorkflowDeleteRecordActionSettings = z.infer<
|
|||||||
export type WorkflowFindRecordsActionSettings = z.infer<
|
export type WorkflowFindRecordsActionSettings = z.infer<
|
||||||
typeof workflowFindRecordsActionSettingsSchema
|
typeof workflowFindRecordsActionSettingsSchema
|
||||||
>;
|
>;
|
||||||
|
export type WorkflowFilterActionSettings = z.infer<
|
||||||
|
typeof workflowFilterActionSettingsSchema
|
||||||
|
>;
|
||||||
export type WorkflowFormActionSettings = z.infer<
|
export type WorkflowFormActionSettings = z.infer<
|
||||||
typeof workflowFormActionSettingsSchema
|
typeof workflowFormActionSettingsSchema
|
||||||
>;
|
>;
|
||||||
@ -68,6 +73,7 @@ export type WorkflowDeleteRecordAction = z.infer<
|
|||||||
export type WorkflowFindRecordsAction = z.infer<
|
export type WorkflowFindRecordsAction = z.infer<
|
||||||
typeof workflowFindRecordsActionSchema
|
typeof workflowFindRecordsActionSchema
|
||||||
>;
|
>;
|
||||||
|
export type WorkflowFilterAction = z.infer<typeof workflowFilterActionSchema>;
|
||||||
export type WorkflowFormAction = z.infer<typeof workflowFormActionSchema>;
|
export type WorkflowFormAction = z.infer<typeof workflowFormActionSchema>;
|
||||||
export type WorkflowHttpRequestAction = z.infer<
|
export type WorkflowHttpRequestAction = z.infer<
|
||||||
typeof workflowHttpRequestActionSchema
|
typeof workflowHttpRequestActionSchema
|
||||||
@ -86,6 +92,7 @@ export type WorkflowAction =
|
|||||||
| WorkflowUpdateRecordAction
|
| WorkflowUpdateRecordAction
|
||||||
| WorkflowDeleteRecordAction
|
| WorkflowDeleteRecordAction
|
||||||
| WorkflowFindRecordsAction
|
| WorkflowFindRecordsAction
|
||||||
|
| WorkflowFilterAction
|
||||||
| WorkflowFormAction
|
| WorkflowFormAction
|
||||||
| WorkflowHttpRequestAction
|
| WorkflowHttpRequestAction
|
||||||
| WorkflowAiAgentAction;
|
| WorkflowAiAgentAction;
|
||||||
|
|||||||
@ -138,6 +138,13 @@ export const workflowAiAgentActionSettingsSchema =
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const workflowFilterActionSettingsSchema =
|
||||||
|
baseWorkflowActionSettingsSchema.extend({
|
||||||
|
input: z.object({
|
||||||
|
filter: z.record(z.any()),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
// Action schemas
|
// Action schemas
|
||||||
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
|
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
|
||||||
type: z.literal('CODE'),
|
type: z.literal('CODE'),
|
||||||
@ -190,6 +197,11 @@ export const workflowAiAgentActionSchema = baseWorkflowActionSchema.extend({
|
|||||||
settings: workflowAiAgentActionSettingsSchema,
|
settings: workflowAiAgentActionSettingsSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const workflowFilterActionSchema = baseWorkflowActionSchema.extend({
|
||||||
|
type: z.literal('FILTER'),
|
||||||
|
settings: workflowFilterActionSettingsSchema,
|
||||||
|
});
|
||||||
|
|
||||||
// Combined action schema
|
// Combined action schema
|
||||||
export const workflowActionSchema = z.discriminatedUnion('type', [
|
export const workflowActionSchema = z.discriminatedUnion('type', [
|
||||||
workflowCodeActionSchema,
|
workflowCodeActionSchema,
|
||||||
@ -201,6 +213,7 @@ export const workflowActionSchema = z.discriminatedUnion('type', [
|
|||||||
workflowFormActionSchema,
|
workflowFormActionSchema,
|
||||||
workflowHttpRequestActionSchema,
|
workflowHttpRequestActionSchema,
|
||||||
workflowAiAgentActionSchema,
|
workflowAiAgentActionSchema,
|
||||||
|
workflowFilterActionSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Trigger schemas
|
// Trigger schemas
|
||||||
|
|||||||
@ -50,8 +50,10 @@ export const WorkflowDiagramDefaultEdge = ({
|
|||||||
<WorkflowDiagramEdgeV2
|
<WorkflowDiagramEdgeV2
|
||||||
labelX={labelX}
|
labelX={labelX}
|
||||||
labelY={labelY}
|
labelY={labelY}
|
||||||
|
stepId={data.stepId}
|
||||||
parentStepId={source}
|
parentStepId={source}
|
||||||
nextStepId={target}
|
nextStepId={target}
|
||||||
|
filter={data.filter}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<WorkflowDiagramEdgeV1
|
<WorkflowDiagramEdgeV1
|
||||||
|
|||||||
@ -1,164 +1,71 @@
|
|||||||
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 { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
|
||||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
|
||||||
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
|
||||||
import { isDropdownOpenComponentStateV2 } from '@/ui/layout/dropdown/states/isDropdownOpenComponentStateV2';
|
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||||
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||||
|
import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined';
|
||||||
|
import { WorkflowDiagramEdgeV2Content } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Content';
|
||||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||||
import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramPanOnDragComponentState';
|
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
|
||||||
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
|
||||||
import { css } from '@emotion/react';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import {
|
|
||||||
IconDotsVertical,
|
|
||||||
IconFilter,
|
|
||||||
IconFilterPlus,
|
|
||||||
IconFilterX,
|
|
||||||
IconGitBranchDeleted,
|
|
||||||
IconPlus,
|
|
||||||
} from 'twenty-ui/display';
|
|
||||||
import { IconButtonGroup } from 'twenty-ui/input';
|
|
||||||
import { MenuItem } from 'twenty-ui/navigation';
|
|
||||||
|
|
||||||
const StyledIconButtonGroup = styled(IconButtonGroup)`
|
|
||||||
pointer-events: all;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledContainer = styled.div<{ labelX: number; labelY: number }>`
|
|
||||||
padding: ${({ theme }) => theme.spacing(1)};
|
|
||||||
pointer-events: all;
|
|
||||||
${({ labelX, labelY }) => css`
|
|
||||||
transform: translate(-50%, -50%) translate(${labelX}px, ${labelY}px);
|
|
||||||
`}
|
|
||||||
position: absolute;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledOpacityOverlay = styled.div<{ shouldDisplay: boolean }>`
|
|
||||||
opacity: ${({ shouldDisplay }) => (shouldDisplay ? 1 : 0)};
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type WorkflowDiagramEdgeV2Props = {
|
type WorkflowDiagramEdgeV2Props = {
|
||||||
labelX: number;
|
labelX: number;
|
||||||
labelY: number;
|
labelY: number;
|
||||||
|
stepId: string | undefined;
|
||||||
parentStepId: string;
|
parentStepId: string;
|
||||||
nextStepId: string;
|
nextStepId: string;
|
||||||
|
filter: Record<string, any> | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkflowDiagramEdgeV2 = ({
|
export const WorkflowDiagramEdgeV2 = ({
|
||||||
labelX,
|
labelX,
|
||||||
labelY,
|
labelY,
|
||||||
|
stepId,
|
||||||
parentStepId,
|
parentStepId,
|
||||||
nextStepId,
|
nextStepId,
|
||||||
|
filter,
|
||||||
}: WorkflowDiagramEdgeV2Props) => {
|
}: WorkflowDiagramEdgeV2Props) => {
|
||||||
const { openDropdown } = useOpenDropdown();
|
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
|
||||||
const { closeDropdown } = useCloseDropdown();
|
workflowVisualizerWorkflowIdComponentState,
|
||||||
|
);
|
||||||
|
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
|
||||||
|
assertWorkflowWithCurrentVersionIsDefined(workflow);
|
||||||
|
|
||||||
|
const { createStep } = useCreateStep({ workflow });
|
||||||
|
const { deleteStep } = useDeleteStep({ workflow });
|
||||||
const { startNodeCreation } = useStartNodeCreation();
|
const { startNodeCreation } = useStartNodeCreation();
|
||||||
|
|
||||||
const [hovered, setHovered] = useState(false);
|
|
||||||
|
|
||||||
const setWorkflowDiagramPanOnDrag = useSetRecoilComponentStateV2(
|
|
||||||
workflowDiagramPanOnDragComponentState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const workflowInsertStepIds = useRecoilComponentValueV2(
|
|
||||||
workflowInsertStepIdsComponentState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSelected =
|
|
||||||
workflowInsertStepIds.parentStepId === parentStepId &&
|
|
||||||
workflowInsertStepIds.nextStepId === nextStepId;
|
|
||||||
|
|
||||||
const dropdownId = `${WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}-${parentStepId}-${nextStepId}`;
|
|
||||||
|
|
||||||
const isDropdownOpen = useRecoilComponentValueV2(
|
|
||||||
isDropdownOpenComponentStateV2,
|
|
||||||
dropdownId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer
|
<WorkflowDiagramEdgeV2Content
|
||||||
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
|
||||||
labelX={labelX}
|
labelX={labelX}
|
||||||
labelY={labelY}
|
labelY={labelY}
|
||||||
onMouseEnter={() => setHovered(true)}
|
stepId={stepId}
|
||||||
onMouseLeave={() => setHovered(false)}
|
parentStepId={parentStepId}
|
||||||
>
|
nextStepId={nextStepId}
|
||||||
<StyledOpacityOverlay
|
filter={filter}
|
||||||
shouldDisplay={isSelected || hovered || isDropdownOpen}
|
onCreateFilter={() => {
|
||||||
>
|
return createStep({
|
||||||
<StyledIconButtonGroup
|
newStepType: 'FILTER',
|
||||||
className="nodrag nopan"
|
parentStepId,
|
||||||
iconButtons={[
|
nextStepId,
|
||||||
{
|
});
|
||||||
Icon: IconFilterPlus,
|
}}
|
||||||
onClick: () => {},
|
onDeleteFilter={() => {
|
||||||
},
|
if (!isDefined(stepId)) {
|
||||||
{
|
throw new Error(
|
||||||
Icon: IconDotsVertical,
|
'Step ID must be configured for the edge when rendering a filter',
|
||||||
onClick: () => {
|
);
|
||||||
openDropdown({
|
}
|
||||||
dropdownComponentInstanceIdFromProps: dropdownId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Dropdown
|
return deleteStep(stepId);
|
||||||
dropdownId={dropdownId}
|
}}
|
||||||
clickableComponent={<div></div>}
|
onCreateNode={() => {
|
||||||
data-select-disable
|
if (isDefined(filter)) {
|
||||||
dropdownPlacement="bottom-start"
|
startNodeCreation({ parentStepId: stepId, nextStepId });
|
||||||
dropdownStrategy="absolute"
|
} else {
|
||||||
dropdownOffset={{
|
startNodeCreation({ parentStepId, nextStepId });
|
||||||
x: 0,
|
}
|
||||||
y: 4,
|
}}
|
||||||
}}
|
/>
|
||||||
onOpen={() => {
|
|
||||||
setWorkflowDiagramPanOnDrag(false);
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
setWorkflowDiagramPanOnDrag(true);
|
|
||||||
}}
|
|
||||||
dropdownComponents={
|
|
||||||
<DropdownContent widthInPixels={GenericDropdownContentWidth.Narrow}>
|
|
||||||
<DropdownMenuItemsContainer>
|
|
||||||
<MenuItem
|
|
||||||
text="Filter"
|
|
||||||
LeftIcon={IconFilter}
|
|
||||||
onClick={() => {}}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
text="Remove Filter"
|
|
||||||
LeftIcon={IconFilterX}
|
|
||||||
onClick={() => {}}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
text="Add Node"
|
|
||||||
LeftIcon={IconPlus}
|
|
||||||
onClick={() => {
|
|
||||||
closeDropdown(dropdownId);
|
|
||||||
setHovered(false);
|
|
||||||
|
|
||||||
startNodeCreation({ parentStepId, nextStepId });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
text="Delete branch"
|
|
||||||
LeftIcon={IconGitBranchDeleted}
|
|
||||||
onClick={() => {}}
|
|
||||||
/>
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</DropdownContent>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledOpacityOverlay>
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,209 @@
|
|||||||
|
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 { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||||
|
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||||
|
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
||||||
|
import { isDropdownOpenComponentStateV2 } from '@/ui/layout/dropdown/states/isDropdownOpenComponentStateV2';
|
||||||
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
|
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||||
|
import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramPanOnDragComponentState';
|
||||||
|
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
||||||
|
import { css } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import {
|
||||||
|
IconDotsVertical,
|
||||||
|
IconFilter,
|
||||||
|
IconFilterPlus,
|
||||||
|
IconFilterX,
|
||||||
|
IconGitBranchDeleted,
|
||||||
|
IconPlus,
|
||||||
|
} from 'twenty-ui/display';
|
||||||
|
import { IconButtonGroup } from 'twenty-ui/input';
|
||||||
|
import { MenuItem } from 'twenty-ui/navigation';
|
||||||
|
|
||||||
|
const StyledIconButtonGroup = styled(IconButtonGroup)`
|
||||||
|
pointer-events: all;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRoundedIconButtonGroup = styled(IconButtonGroup)`
|
||||||
|
border-radius: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: all;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledContainer = styled.div<{ labelX: number; labelY: number }>`
|
||||||
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
pointer-events: all;
|
||||||
|
${({ labelX, labelY }) => css`
|
||||||
|
transform: translate(-50%, -50%) translate(${labelX}px, ${labelY}px);
|
||||||
|
`}
|
||||||
|
position: absolute;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledOpacityOverlay = styled.div<{ shouldDisplay: boolean }>`
|
||||||
|
opacity: ${({ shouldDisplay }) => (shouldDisplay ? 1 : 0)};
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type WorkflowDiagramEdgeV2ContentProps = {
|
||||||
|
labelX: number;
|
||||||
|
labelY: number;
|
||||||
|
stepId: string | undefined;
|
||||||
|
parentStepId: string;
|
||||||
|
nextStepId: string;
|
||||||
|
filter: Record<string, any> | undefined;
|
||||||
|
onCreateFilter: () => Promise<void>;
|
||||||
|
onDeleteFilter: () => Promise<void>;
|
||||||
|
onCreateNode: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkflowDiagramEdgeV2Content = ({
|
||||||
|
labelX,
|
||||||
|
labelY,
|
||||||
|
stepId,
|
||||||
|
parentStepId,
|
||||||
|
nextStepId,
|
||||||
|
filter,
|
||||||
|
onCreateFilter,
|
||||||
|
onDeleteFilter,
|
||||||
|
onCreateNode,
|
||||||
|
}: WorkflowDiagramEdgeV2ContentProps) => {
|
||||||
|
const { openDropdown } = useOpenDropdown();
|
||||||
|
const { closeDropdown } = useCloseDropdown();
|
||||||
|
|
||||||
|
const [hovered, setHovered] = useState(false);
|
||||||
|
|
||||||
|
const setWorkflowDiagramPanOnDrag = useSetRecoilComponentStateV2(
|
||||||
|
workflowDiagramPanOnDragComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const workflowInsertStepIds = useRecoilComponentValueV2(
|
||||||
|
workflowInsertStepIdsComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSelected =
|
||||||
|
workflowInsertStepIds.nextStepId === nextStepId &&
|
||||||
|
(workflowInsertStepIds.parentStepId === parentStepId ||
|
||||||
|
(isNonEmptyString(stepId) &&
|
||||||
|
workflowInsertStepIds.parentStepId === stepId));
|
||||||
|
|
||||||
|
const dropdownId = `${WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}-${parentStepId}-${nextStepId}`;
|
||||||
|
|
||||||
|
const isDropdownOpen = useRecoilComponentValueV2(
|
||||||
|
isDropdownOpenComponentStateV2,
|
||||||
|
dropdownId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreateFilter = async () => {
|
||||||
|
await onCreateFilter();
|
||||||
|
|
||||||
|
closeDropdown(dropdownId);
|
||||||
|
setHovered(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer
|
||||||
|
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||||
|
labelX={labelX}
|
||||||
|
labelY={labelY}
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
>
|
||||||
|
<StyledOpacityOverlay
|
||||||
|
shouldDisplay={
|
||||||
|
isSelected || hovered || isDropdownOpen || isDefined(filter)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isDefined(filter) && !hovered && !isDropdownOpen && !isSelected ? (
|
||||||
|
<StyledRoundedIconButtonGroup
|
||||||
|
className="nodrag nopan"
|
||||||
|
iconButtons={[
|
||||||
|
{
|
||||||
|
Icon: IconFilterPlus,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<StyledIconButtonGroup
|
||||||
|
className="nodrag nopan"
|
||||||
|
iconButtons={[
|
||||||
|
{
|
||||||
|
Icon: IconFilterPlus,
|
||||||
|
onClick: () => {
|
||||||
|
handleCreateFilter();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Icon: IconDotsVertical,
|
||||||
|
onClick: () => {
|
||||||
|
openDropdown({
|
||||||
|
dropdownComponentInstanceIdFromProps: dropdownId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
dropdownId={dropdownId}
|
||||||
|
clickableComponent={<div></div>}
|
||||||
|
data-select-disable
|
||||||
|
dropdownPlacement="bottom-start"
|
||||||
|
dropdownStrategy="absolute"
|
||||||
|
dropdownOffset={{
|
||||||
|
x: 0,
|
||||||
|
y: 4,
|
||||||
|
}}
|
||||||
|
onOpen={() => {
|
||||||
|
setWorkflowDiagramPanOnDrag(false);
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setWorkflowDiagramPanOnDrag(true);
|
||||||
|
}}
|
||||||
|
dropdownComponents={
|
||||||
|
<DropdownContent widthInPixels={GenericDropdownContentWidth.Narrow}>
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
<MenuItem
|
||||||
|
text="Filter"
|
||||||
|
LeftIcon={IconFilter}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
text="Remove Filter"
|
||||||
|
LeftIcon={IconFilterX}
|
||||||
|
onClick={() => {
|
||||||
|
closeDropdown(dropdownId);
|
||||||
|
setHovered(false);
|
||||||
|
|
||||||
|
onDeleteFilter();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
text="Add Node"
|
||||||
|
LeftIcon={IconPlus}
|
||||||
|
onClick={() => {
|
||||||
|
closeDropdown(dropdownId);
|
||||||
|
setHovered(false);
|
||||||
|
|
||||||
|
onCreateNode();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
text="Delete branch"
|
||||||
|
LeftIcon={IconGitBranchDeleted}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</DropdownContent>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledOpacityOverlay>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,17 +1,17 @@
|
|||||||
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import {
|
import {
|
||||||
ComponentDecorator,
|
ComponentDecorator,
|
||||||
getCanvasElementForDropdownTesting,
|
getCanvasElementForDropdownTesting,
|
||||||
} from 'twenty-ui/testing';
|
} from 'twenty-ui/testing';
|
||||||
import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
|
import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
|
||||||
import { WorkflowDiagramEdgeV2 } from '../WorkflowDiagramEdgeV2';
|
import { WorkflowDiagramEdgeV2Content } from '../WorkflowDiagramEdgeV2Content';
|
||||||
|
|
||||||
const meta: Meta<typeof WorkflowDiagramEdgeV2> = {
|
const meta: Meta<typeof WorkflowDiagramEdgeV2Content> = {
|
||||||
title: 'Modules/Workflow/WorkflowDiagramEdgeV2',
|
title: 'Modules/Workflow/WorkflowDiagramEdgeV2Content',
|
||||||
component: WorkflowDiagramEdgeV2,
|
component: WorkflowDiagramEdgeV2Content,
|
||||||
decorators: [
|
decorators: [
|
||||||
ComponentDecorator,
|
ComponentDecorator,
|
||||||
ReactflowDecorator,
|
ReactflowDecorator,
|
||||||
@ -35,11 +35,14 @@ const meta: Meta<typeof WorkflowDiagramEdgeV2> = {
|
|||||||
labelY: 0,
|
labelY: 0,
|
||||||
parentStepId: 'parent-step-id',
|
parentStepId: 'parent-step-id',
|
||||||
nextStepId: 'next-step-id',
|
nextStepId: 'next-step-id',
|
||||||
|
onCreateFilter: fn(),
|
||||||
|
onDeleteFilter: fn(),
|
||||||
|
onCreateNode: fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof WorkflowDiagramEdgeV2>;
|
type Story = StoryObj<typeof WorkflowDiagramEdgeV2Content>;
|
||||||
|
|
||||||
export const ButtonsAppearOnHover: Story = {
|
export const ButtonsAppearOnHover: Story = {
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
@ -57,7 +60,7 @@ export const ButtonsAppearOnHover: Story = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CreateFilter: Story = {
|
export const CreateFilter: Story = {
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement, args }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const buttons = await canvas.findAllByRole('button');
|
const buttons = await canvas.findAllByRole('button');
|
||||||
@ -71,12 +74,14 @@ export const CreateFilter: Story = {
|
|||||||
|
|
||||||
userEvent.click(filterButton);
|
userEvent.click(filterButton);
|
||||||
|
|
||||||
// TODO: Assert we created a filter
|
await waitFor(() => {
|
||||||
|
expect(args.onCreateFilter).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddNodeAction: Story = {
|
export const AddNodeAction: Story = {
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement, args }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const buttons = await canvas.findAllByRole('button');
|
const buttons = await canvas.findAllByRole('button');
|
||||||
@ -99,6 +104,10 @@ export const AddNodeAction: Story = {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(canvas.queryByText('Add Node')).not.toBeInTheDocument();
|
expect(canvas.queryByText('Add Node')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(args.onCreateNode).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,6 +69,8 @@ export type WorkflowRunDiagramNodeData = Exclude<
|
|||||||
> & { runStatus: WorkflowDiagramRunStatus };
|
> & { runStatus: WorkflowDiagramRunStatus };
|
||||||
|
|
||||||
export type EdgeData = {
|
export type EdgeData = {
|
||||||
|
stepId?: string;
|
||||||
|
filter?: Record<string, any>;
|
||||||
shouldDisplayEdgeOptions?: boolean;
|
shouldDisplayEdgeOptions?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,333 @@
|
|||||||
|
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { transformFilterNodesAsEdges } from '../transformFilterNodesAsEdges';
|
||||||
|
|
||||||
|
describe('transformFilterNodesAsEdges', () => {
|
||||||
|
it('should return the original diagram when there are no filter nodes', () => {
|
||||||
|
const diagram: WorkflowDiagram = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'A',
|
||||||
|
data: { nodeType: 'action', actionType: 'CODE', name: 'Step A' },
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'C',
|
||||||
|
data: {
|
||||||
|
nodeType: 'action',
|
||||||
|
actionType: 'SEND_EMAIL',
|
||||||
|
name: 'Step C',
|
||||||
|
},
|
||||||
|
position: { x: 0, y: 300 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: 'A-C',
|
||||||
|
source: 'A',
|
||||||
|
target: 'C',
|
||||||
|
data: { stepId: 'A', shouldDisplayEdgeOptions: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = transformFilterNodesAsEdges(diagram);
|
||||||
|
|
||||||
|
expect(result.nodes).toEqual(diagram.nodes);
|
||||||
|
expect(result.edges).toEqual(diagram.edges);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform A->B->C where B is a FILTER step', () => {
|
||||||
|
const diagram: WorkflowDiagram = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'A',
|
||||||
|
data: { nodeType: 'action', actionType: 'CODE', name: 'Step A' },
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B',
|
||||||
|
data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B' },
|
||||||
|
position: { x: 0, y: 150 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'C',
|
||||||
|
data: {
|
||||||
|
nodeType: 'action',
|
||||||
|
actionType: 'SEND_EMAIL',
|
||||||
|
name: 'Step C',
|
||||||
|
},
|
||||||
|
position: { x: 0, y: 300 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: 'A-B',
|
||||||
|
source: 'A',
|
||||||
|
target: 'B',
|
||||||
|
data: { stepId: 'A', shouldDisplayEdgeOptions: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B-C',
|
||||||
|
source: 'B',
|
||||||
|
target: 'C',
|
||||||
|
data: { stepId: 'B', shouldDisplayEdgeOptions: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = transformFilterNodesAsEdges(diagram);
|
||||||
|
|
||||||
|
// Should only have nodes A and C
|
||||||
|
expect(result.nodes).toEqual([
|
||||||
|
{
|
||||||
|
id: 'A',
|
||||||
|
data: { nodeType: 'action', actionType: 'CODE', name: 'Step A' },
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'C',
|
||||||
|
data: { nodeType: 'action', actionType: 'SEND_EMAIL', name: 'Step C' },
|
||||||
|
position: { x: 0, y: 300 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Should have one edge with filter data
|
||||||
|
expect(result.edges).toHaveLength(1);
|
||||||
|
expect(result.edges[0]).toEqual({
|
||||||
|
id: 'A-C-filter-B',
|
||||||
|
source: 'A',
|
||||||
|
target: 'C',
|
||||||
|
data: {
|
||||||
|
shouldDisplayEdgeOptions: true,
|
||||||
|
stepId: 'B',
|
||||||
|
filter: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple filter nodes', () => {
|
||||||
|
const diagram: WorkflowDiagram = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'A',
|
||||||
|
data: { nodeType: 'action', actionType: 'CODE', name: 'Step A' },
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B1',
|
||||||
|
data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B1' },
|
||||||
|
position: { x: 0, y: 150 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'C',
|
||||||
|
data: {
|
||||||
|
nodeType: 'action',
|
||||||
|
actionType: 'SEND_EMAIL',
|
||||||
|
name: 'Step C',
|
||||||
|
},
|
||||||
|
position: { x: 0, y: 300 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B2',
|
||||||
|
data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B2' },
|
||||||
|
position: { x: 0, y: 450 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'D',
|
||||||
|
data: {
|
||||||
|
nodeType: 'action',
|
||||||
|
actionType: 'CREATE_RECORD',
|
||||||
|
name: 'Step D',
|
||||||
|
},
|
||||||
|
position: { x: 0, y: 600 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: 'A-B1',
|
||||||
|
source: 'A',
|
||||||
|
target: 'B1',
|
||||||
|
data: { stepId: 'A', shouldDisplayEdgeOptions: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B1-C',
|
||||||
|
source: 'B1',
|
||||||
|
target: 'C',
|
||||||
|
data: { stepId: 'B1', shouldDisplayEdgeOptions: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'C-B2',
|
||||||
|
source: 'C',
|
||||||
|
target: 'B2',
|
||||||
|
data: { stepId: 'C', shouldDisplayEdgeOptions: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B2-D',
|
||||||
|
source: 'B2',
|
||||||
|
target: 'D',
|
||||||
|
data: { stepId: 'B2', shouldDisplayEdgeOptions: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = transformFilterNodesAsEdges(diagram);
|
||||||
|
|
||||||
|
// Should only have nodes A, C, and D
|
||||||
|
expect(result.nodes).toHaveLength(3);
|
||||||
|
expect(result.nodes.map((n) => n.id)).toEqual(
|
||||||
|
expect.arrayContaining(['A', 'C', 'D']),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should have two edges with filter data
|
||||||
|
expect(result.edges).toHaveLength(2);
|
||||||
|
|
||||||
|
const edgeAC = result.edges.find(
|
||||||
|
(e) => e.source === 'A' && e.target === 'C',
|
||||||
|
);
|
||||||
|
expect(edgeAC).toEqual({
|
||||||
|
id: 'A-C-filter-B1',
|
||||||
|
source: 'A',
|
||||||
|
target: 'C',
|
||||||
|
data: {
|
||||||
|
stepId: 'B1',
|
||||||
|
shouldDisplayEdgeOptions: true,
|
||||||
|
filter: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B1' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const edgeCD = result.edges.find(
|
||||||
|
(e) => e.source === 'C' && e.target === 'D',
|
||||||
|
);
|
||||||
|
expect(edgeCD).toEqual({
|
||||||
|
id: 'C-D-filter-B2',
|
||||||
|
source: 'C',
|
||||||
|
target: 'D',
|
||||||
|
data: {
|
||||||
|
stepId: 'B2',
|
||||||
|
shouldDisplayEdgeOptions: true,
|
||||||
|
filter: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B2' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle filter nodes that are not part of a chain', () => {
|
||||||
|
const diagram: WorkflowDiagram = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'A',
|
||||||
|
data: { nodeType: 'action', actionType: 'CODE', name: 'Step A' },
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B',
|
||||||
|
data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B' },
|
||||||
|
position: { x: 0, y: 150 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: 'A-B',
|
||||||
|
source: 'A',
|
||||||
|
target: 'B',
|
||||||
|
data: { stepId: 'A', shouldDisplayEdgeOptions: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = transformFilterNodesAsEdges(diagram);
|
||||||
|
|
||||||
|
// Should only have node A (filter node B is removed)
|
||||||
|
expect(result.nodes).toEqual([
|
||||||
|
{
|
||||||
|
id: 'A',
|
||||||
|
data: { nodeType: 'action', actionType: 'CODE', name: 'Step A' },
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Should have no edges (original edge A-B is removed, no new edges created)
|
||||||
|
expect(result.edges).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve trigger nodes', () => {
|
||||||
|
const diagram: WorkflowDiagram = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'trigger',
|
||||||
|
data: {
|
||||||
|
nodeType: 'trigger',
|
||||||
|
triggerType: 'DATABASE_EVENT',
|
||||||
|
name: 'Trigger',
|
||||||
|
},
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B',
|
||||||
|
data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B' },
|
||||||
|
position: { x: 0, y: 150 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'C',
|
||||||
|
data: {
|
||||||
|
nodeType: 'action',
|
||||||
|
actionType: 'SEND_EMAIL',
|
||||||
|
name: 'Step C',
|
||||||
|
},
|
||||||
|
position: { x: 0, y: 300 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: 'trigger-B',
|
||||||
|
source: 'trigger',
|
||||||
|
target: 'B',
|
||||||
|
data: { stepId: 'trigger', shouldDisplayEdgeOptions: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'B-C',
|
||||||
|
source: 'B',
|
||||||
|
target: 'C',
|
||||||
|
data: { stepId: 'B', shouldDisplayEdgeOptions: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = transformFilterNodesAsEdges(diagram);
|
||||||
|
|
||||||
|
// Should have trigger and C nodes
|
||||||
|
expect(result.nodes).toEqual([
|
||||||
|
{
|
||||||
|
id: 'trigger',
|
||||||
|
data: {
|
||||||
|
nodeType: 'trigger',
|
||||||
|
triggerType: 'DATABASE_EVENT',
|
||||||
|
name: 'Trigger',
|
||||||
|
},
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'C',
|
||||||
|
data: { nodeType: 'action', actionType: 'SEND_EMAIL', name: 'Step C' },
|
||||||
|
position: { x: 0, y: 300 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Should have one edge with filter data
|
||||||
|
expect(result.edges).toEqual([
|
||||||
|
{
|
||||||
|
id: 'trigger-C-filter-B',
|
||||||
|
source: 'trigger',
|
||||||
|
target: 'C',
|
||||||
|
data: {
|
||||||
|
stepId: 'B',
|
||||||
|
shouldDisplayEdgeOptions: true,
|
||||||
|
filter: {
|
||||||
|
nodeType: 'action',
|
||||||
|
actionType: 'FILTER',
|
||||||
|
name: 'Filter B',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -6,9 +6,14 @@ export const addEdgeOptions = ({
|
|||||||
}: WorkflowDiagram): WorkflowDiagram => {
|
}: WorkflowDiagram): WorkflowDiagram => {
|
||||||
return {
|
return {
|
||||||
nodes,
|
nodes,
|
||||||
edges: edges.map((edge) => ({
|
edges: edges.map((edge) => {
|
||||||
...edge,
|
return {
|
||||||
data: { shouldDisplayEdgeOptions: true },
|
...edge,
|
||||||
})),
|
data: {
|
||||||
|
...edge.data,
|
||||||
|
shouldDisplayEdgeOptions: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,9 +10,10 @@ import {
|
|||||||
WorkflowRunDiagramNode,
|
WorkflowRunDiagramNode,
|
||||||
WorkflowRunDiagramStepNodeData,
|
WorkflowRunDiagramStepNodeData,
|
||||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
|
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
|
||||||
import { isStepNode } from '@/workflow/workflow-diagram/utils/isStepNode';
|
import { isStepNode } from '@/workflow/workflow-diagram/utils/isStepNode';
|
||||||
|
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
export const generateWorkflowRunDiagram = ({
|
export const generateWorkflowRunDiagram = ({
|
||||||
trigger,
|
trigger,
|
||||||
@ -38,7 +39,9 @@ export const generateWorkflowRunDiagram = ({
|
|||||||
}
|
}
|
||||||
| undefined = undefined;
|
| undefined = undefined;
|
||||||
|
|
||||||
const workflowDiagram = generateWorkflowDiagram({ trigger, steps });
|
const workflowDiagram = transformFilterNodesAsEdges(
|
||||||
|
generateWorkflowDiagram({ trigger, steps }),
|
||||||
|
);
|
||||||
|
|
||||||
let skippedExecution = false;
|
let skippedExecution = false;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||||
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
|
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
|
||||||
|
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
const EMPTY_DIAGRAM: WorkflowDiagram = {
|
const EMPTY_DIAGRAM: WorkflowDiagram = {
|
||||||
@ -15,8 +16,10 @@ export const getWorkflowVersionDiagram = (
|
|||||||
return EMPTY_DIAGRAM;
|
return EMPTY_DIAGRAM;
|
||||||
}
|
}
|
||||||
|
|
||||||
return generateWorkflowDiagram({
|
return transformFilterNodesAsEdges(
|
||||||
trigger: workflowVersion.trigger ?? undefined,
|
generateWorkflowDiagram({
|
||||||
steps: workflowVersion.steps ?? [],
|
trigger: workflowVersion.trigger ?? undefined,
|
||||||
});
|
steps: workflowVersion.steps ?? [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,66 @@
|
|||||||
|
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
export const transformFilterNodesAsEdges = ({
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
}: WorkflowDiagram): WorkflowDiagram => {
|
||||||
|
const filterNodes = nodes.filter(
|
||||||
|
(node) =>
|
||||||
|
node.data.nodeType === 'action' &&
|
||||||
|
'actionType' in node.data &&
|
||||||
|
node.data.actionType === 'FILTER',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filterNodes.length === 0) {
|
||||||
|
return { nodes, edges };
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultNodes = nodes.filter(
|
||||||
|
(node) => !filterNodes.some((filterNode) => filterNode.id === node.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const resultEdges = [...edges];
|
||||||
|
const edgesToRemove = new Set<string>();
|
||||||
|
const edgesToAdd: typeof edges = [];
|
||||||
|
|
||||||
|
for (const filterNode of filterNodes) {
|
||||||
|
const incomingEdge = edges.find((edge) => edge.target === filterNode.id);
|
||||||
|
const outgoingEdge = edges.find((edge) => edge.source === filterNode.id);
|
||||||
|
|
||||||
|
if (isDefined(incomingEdge) && isDefined(outgoingEdge)) {
|
||||||
|
const newEdge = {
|
||||||
|
...incomingEdge,
|
||||||
|
id: `${incomingEdge.source}-${outgoingEdge.target}-filter-${filterNode.id}`,
|
||||||
|
target: outgoingEdge.target,
|
||||||
|
data: {
|
||||||
|
...incomingEdge.data,
|
||||||
|
stepId: filterNode.id,
|
||||||
|
filter: filterNode.data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
edgesToAdd.push(newEdge);
|
||||||
|
edgesToRemove.add(incomingEdge.id);
|
||||||
|
edgesToRemove.add(outgoingEdge.id);
|
||||||
|
} else {
|
||||||
|
if (isDefined(incomingEdge)) {
|
||||||
|
edgesToRemove.add(incomingEdge.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(outgoingEdge)) {
|
||||||
|
edgesToRemove.add(outgoingEdge.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalEdges = [
|
||||||
|
...resultEdges.filter((edge) => !edgesToRemove.has(edge.id)),
|
||||||
|
...edgesToAdd,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes: resultNodes,
|
||||||
|
edges: finalEdges,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -200,6 +200,11 @@ export const WorkflowRunStepNodeDetail = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case 'FILTER': {
|
||||||
|
throw new Error(
|
||||||
|
"The Filter action isn't meant to be displayed as a node.",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -184,6 +184,11 @@ export const WorkflowStepDetail = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case 'FILTER': {
|
||||||
|
throw new Error(
|
||||||
|
"The Filter action isn't meant to be displayed as a node.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return assertUnreachable(
|
return assertUnreachable(
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||||
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
|
||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
import { WorkflowVisualizerComponentInstanceContext } from '../../../workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
import { WorkflowVisualizerComponentInstanceContext } from '../../../workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
||||||
@ -30,16 +29,7 @@ const wrapper = ({ children }: { children: React.ReactNode }) => {
|
|||||||
'workflow-visualizer-instance-id';
|
'workflow-visualizer-instance-id';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecoilRoot
|
<RecoilRoot>
|
||||||
initializeState={({ set }) => {
|
|
||||||
set(
|
|
||||||
workflowInsertStepIdsComponentState.atomFamily({
|
|
||||||
instanceId: workflowVisualizerComponentInstanceId,
|
|
||||||
}),
|
|
||||||
{ parentStepId: 'parent-step-id', nextStepId: undefined },
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<WorkflowVisualizerComponentInstanceContext.Provider
|
<WorkflowVisualizerComponentInstanceContext.Provider
|
||||||
value={{
|
value={{
|
||||||
instanceId: workflowVisualizerComponentInstanceId,
|
instanceId: workflowVisualizerComponentInstanceId,
|
||||||
@ -73,7 +63,11 @@ describe('useCreateStep', () => {
|
|||||||
wrapper,
|
wrapper,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await result.current.createStep('CODE');
|
await result.current.createStep({
|
||||||
|
newStepType: 'CODE',
|
||||||
|
parentStepId: 'parent-step-id',
|
||||||
|
nextStepId: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
expect(mockCreateWorkflowVersionStep).toHaveBeenCalled();
|
expect(mockCreateWorkflowVersionStep).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,10 +7,7 @@ import {
|
|||||||
} from '@/workflow/types/Workflow';
|
} from '@/workflow/types/Workflow';
|
||||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||||
import { useCreateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep';
|
import { useCreateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep';
|
||||||
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
|
||||||
|
|
||||||
export const useCreateStep = ({
|
export const useCreateStep = ({
|
||||||
workflow,
|
workflow,
|
||||||
@ -26,12 +23,17 @@ export const useCreateStep = ({
|
|||||||
workflowLastCreatedStepIdComponentState,
|
workflowLastCreatedStepIdComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [workflowInsertStepIds, setWorkflowInsertStepIds] =
|
|
||||||
useRecoilComponentStateV2(workflowInsertStepIdsComponentState);
|
|
||||||
|
|
||||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||||
|
|
||||||
const createStep = async (newStepType: WorkflowStepType) => {
|
const createStep = async ({
|
||||||
|
newStepType,
|
||||||
|
parentStepId,
|
||||||
|
nextStepId,
|
||||||
|
}: {
|
||||||
|
newStepType: WorkflowStepType;
|
||||||
|
parentStepId: string;
|
||||||
|
nextStepId: string | undefined;
|
||||||
|
}) => {
|
||||||
if (isLoading === true) {
|
if (isLoading === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -39,20 +41,14 @@ export const useCreateStep = ({
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!isDefined(workflowInsertStepIds.parentStepId)) {
|
|
||||||
throw new Error(
|
|
||||||
'No parentStepId. Please select a parent step to create from.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowVersionId = await getUpdatableWorkflowVersion(workflow);
|
const workflowVersionId = await getUpdatableWorkflowVersion(workflow);
|
||||||
|
|
||||||
const createdStep = (
|
const createdStep = (
|
||||||
await createWorkflowVersionStep({
|
await createWorkflowVersionStep({
|
||||||
workflowVersionId,
|
workflowVersionId,
|
||||||
stepType: newStepType,
|
stepType: newStepType,
|
||||||
parentStepId: workflowInsertStepIds.parentStepId,
|
parentStepId,
|
||||||
nextStepId: workflowInsertStepIds.nextStepId,
|
nextStepId,
|
||||||
})
|
})
|
||||||
)?.data?.createWorkflowVersionStep;
|
)?.data?.createWorkflowVersionStep;
|
||||||
|
|
||||||
@ -62,10 +58,6 @@ export const useCreateStep = ({
|
|||||||
|
|
||||||
setWorkflowSelectedNode(createdStep.id);
|
setWorkflowSelectedNode(createdStep.id);
|
||||||
setWorkflowLastCreatedStepId(createdStep.id);
|
setWorkflowLastCreatedStepId(createdStep.id);
|
||||||
setWorkflowInsertStepIds({
|
|
||||||
parentStepId: undefined,
|
|
||||||
nextStepId: undefined,
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
workflowActionSchema,
|
WorkflowFormAction,
|
||||||
|
WorkflowHttpRequestAction,
|
||||||
|
WorkflowSendEmailAction,
|
||||||
|
} from '@/workflow/types/Workflow';
|
||||||
|
import {
|
||||||
workflowFormActionSettingsSchema,
|
workflowFormActionSettingsSchema,
|
||||||
workflowHttpRequestActionSettingsSchema,
|
workflowHttpRequestActionSettingsSchema,
|
||||||
workflowSendEmailActionSettingsSchema,
|
workflowSendEmailActionSettingsSchema,
|
||||||
@ -34,7 +38,7 @@ describe('useWorkflowActionHeader', () => {
|
|||||||
|
|
||||||
describe('when action name is not defined', () => {
|
describe('when action name is not defined', () => {
|
||||||
it('should return default title', () => {
|
it('should return default title', () => {
|
||||||
const action = workflowActionSchema.parse({
|
const action = {
|
||||||
id: '1',
|
id: '1',
|
||||||
name: '',
|
name: '',
|
||||||
type: 'HTTP_REQUEST',
|
type: 'HTTP_REQUEST',
|
||||||
@ -52,7 +56,7 @@ describe('useWorkflowActionHeader', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
valid: true,
|
valid: true,
|
||||||
});
|
} satisfies WorkflowHttpRequestAction;
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useWorkflowActionHeader({
|
useWorkflowActionHeader({
|
||||||
@ -71,7 +75,7 @@ describe('useWorkflowActionHeader', () => {
|
|||||||
|
|
||||||
describe('when action name is defined', () => {
|
describe('when action name is defined', () => {
|
||||||
it('should return the action name', () => {
|
it('should return the action name', () => {
|
||||||
const action = workflowActionSchema.parse({
|
const action = {
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'Test Action',
|
name: 'Test Action',
|
||||||
type: 'HTTP_REQUEST',
|
type: 'HTTP_REQUEST',
|
||||||
@ -89,7 +93,7 @@ describe('useWorkflowActionHeader', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
valid: true,
|
valid: true,
|
||||||
});
|
} satisfies WorkflowHttpRequestAction;
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useWorkflowActionHeader({
|
useWorkflowActionHeader({
|
||||||
@ -108,7 +112,7 @@ describe('useWorkflowActionHeader', () => {
|
|||||||
|
|
||||||
describe('when action type is defined', () => {
|
describe('when action type is defined', () => {
|
||||||
it('should return default title for HTTP request action', () => {
|
it('should return default title for HTTP request action', () => {
|
||||||
const action = workflowActionSchema.parse({
|
const action = {
|
||||||
id: '1',
|
id: '1',
|
||||||
name: '',
|
name: '',
|
||||||
type: 'HTTP_REQUEST',
|
type: 'HTTP_REQUEST',
|
||||||
@ -126,7 +130,7 @@ describe('useWorkflowActionHeader', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
valid: true,
|
valid: true,
|
||||||
});
|
} satisfies WorkflowHttpRequestAction;
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useWorkflowActionHeader({
|
useWorkflowActionHeader({
|
||||||
@ -143,7 +147,7 @@ describe('useWorkflowActionHeader', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return default title for form action', () => {
|
it('should return default title for form action', () => {
|
||||||
const action = workflowActionSchema.parse({
|
const action = {
|
||||||
id: '1',
|
id: '1',
|
||||||
name: '',
|
name: '',
|
||||||
type: 'FORM',
|
type: 'FORM',
|
||||||
@ -165,7 +169,7 @@ describe('useWorkflowActionHeader', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
valid: true,
|
valid: true,
|
||||||
});
|
} satisfies WorkflowFormAction;
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useWorkflowActionHeader({
|
useWorkflowActionHeader({
|
||||||
@ -182,7 +186,7 @@ describe('useWorkflowActionHeader', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return default title for email action', () => {
|
it('should return default title for email action', () => {
|
||||||
const action = workflowActionSchema.parse({
|
const action = {
|
||||||
id: '1',
|
id: '1',
|
||||||
name: '',
|
name: '',
|
||||||
type: 'SEND_EMAIL',
|
type: 'SEND_EMAIL',
|
||||||
@ -200,7 +204,7 @@ describe('useWorkflowActionHeader', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
valid: true,
|
valid: true,
|
||||||
});
|
} satisfies WorkflowSendEmailAction;
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useWorkflowActionHeader({
|
useWorkflowActionHeader({
|
||||||
|
|||||||
@ -17,6 +17,11 @@ export const getActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
|
|||||||
return msg`HTTP Request`;
|
return msg`HTTP Request`;
|
||||||
case 'AI_AGENT':
|
case 'AI_AGENT':
|
||||||
return msg`AI Agent`;
|
return msg`AI Agent`;
|
||||||
|
case 'FILTER': {
|
||||||
|
throw new Error(
|
||||||
|
"The Filter action isn't meant to be displayed as a node.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
|
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
|
||||||
|
|||||||
@ -23,6 +23,11 @@ export const getActionIconColorOrThrow = ({
|
|||||||
return theme.color.blue;
|
return theme.color.blue;
|
||||||
case 'AI_AGENT':
|
case 'AI_AGENT':
|
||||||
return theme.color.pink;
|
return theme.color.pink;
|
||||||
|
case 'FILTER': {
|
||||||
|
throw new Error(
|
||||||
|
"The Filter action isn't meant to be displayed as a node.",
|
||||||
|
);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
|
assertUnreachable(actionType, `Unsupported action type: ${actionType}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -576,6 +576,20 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case WorkflowActionType.FILTER: {
|
||||||
|
return {
|
||||||
|
id: newStepId,
|
||||||
|
name: 'Filter',
|
||||||
|
type: WorkflowActionType.FILTER,
|
||||||
|
valid: false,
|
||||||
|
settings: {
|
||||||
|
...BASE_STEP_DEFINITION,
|
||||||
|
input: {
|
||||||
|
filter: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
case WorkflowActionType.HTTP_REQUEST: {
|
case WorkflowActionType.HTTP_REQUEST: {
|
||||||
return {
|
return {
|
||||||
id: newStepId,
|
id: newStepId,
|
||||||
|
|||||||
Reference in New Issue
Block a user