Open filters in side panel (#13304)

In this PR:

- Open filters in the side panel for **workflows**
- Open filters in the side panel for **workflow versions**
- Preparation for opening filters in the side panel for **workflow
runs**
- Add many tests to increase the coverage

Remaining to do:

- Open filters in the side panel for **workflow runs**
- Upon filter creation, open it in the side panel

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Baptiste Devessier
2025-07-23 10:30:08 +02:00
committed by GitHub
parent eeade6e94c
commit 924e599cba
60 changed files with 2071 additions and 1509 deletions
@@ -0,0 +1,31 @@
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { useTheme } from '@emotion/react';
import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react';
type WorkflowDiagramBlankEdgeProps = EdgeProps<WorkflowDiagramEdge>;
export const WorkflowDiagramBlankEdge = ({
markerStart,
markerEnd,
sourceY,
targetY,
}: WorkflowDiagramBlankEdgeProps) => {
const theme = useTheme();
const [edgePath] = getStraightPath({
sourceX: CREATE_STEP_NODE_WIDTH,
sourceY,
targetX: CREATE_STEP_NODE_WIDTH,
targetY,
});
return (
<BaseEdge
markerStart={markerStart}
markerEnd={markerEnd}
path={edgePath}
style={{ stroke: theme.border.color.strong }}
/>
);
};
@@ -1,9 +1,12 @@
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
import { WorkflowDiagramBlankEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramBlankEdge';
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect';
import { WorkflowDiagramCreateStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepNode';
import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge';
import { WorkflowDiagramDefaultEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable';
import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger';
import { WorkflowDiagramFilterEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable';
import { WorkflowDiagramFilteringDisabledEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeEditable';
import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable';
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
import { ReactFlowProvider } from '@xyflow/react';
@@ -26,7 +29,11 @@ export const WorkflowDiagramCanvasEditable = ({
'empty-trigger': WorkflowDiagramEmptyTrigger,
}}
edgeTypes={{
default: WorkflowDiagramDefaultEdge,
blank: WorkflowDiagramBlankEdge,
'filtering-disabled--editable':
WorkflowDiagramFilteringDisabledEdgeEditable,
'empty-filter--editable': WorkflowDiagramDefaultEdgeEditable,
'filter--editable': WorkflowDiagramFilterEdgeEditable,
}}
tagContainerTestId="workflow-visualizer-status"
tagColor={tagProps.color}
@@ -1,10 +1,11 @@
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
import { WorkflowDiagramCanvasReadonlyEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect';
import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge';
import { WorkflowDiagramDefaultEdgeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeReadonly';
import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger';
import { WorkflowDiagramFilterEdgeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeReadonly';
import { WorkflowDiagramFilteringDisabledEdgeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeReadonly';
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly';
import { WorkflowDiagramSuccessEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge';
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
import { ReactFlowProvider } from '@xyflow/react';
@@ -25,8 +26,10 @@ export const WorkflowDiagramCanvasReadonly = ({
'empty-trigger': WorkflowDiagramEmptyTrigger,
}}
edgeTypes={{
default: WorkflowDiagramDefaultEdge,
success: WorkflowDiagramSuccessEdge,
'filtering-disabled--readonly':
WorkflowDiagramFilteringDisabledEdgeReadonly,
'empty-filter--readonly': WorkflowDiagramDefaultEdgeReadonly,
'filter--readonly': WorkflowDiagramFilterEdgeReadonly,
}}
tagContainerTestId="workflow-visualizer-status"
tagColor={tagProps.color}
@@ -1,92 +0,0 @@
import { WorkflowDiagramEdgeV1 } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV1';
import { WorkflowDiagramEdgeV2Empty } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Empty';
import { WorkflowDiagramEdgeV2Filter } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Filter';
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getStraightPath,
} from '@xyflow/react';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
type WorkflowDiagramDefaultEdgeProps = EdgeProps<WorkflowDiagramEdge>;
export const WorkflowDiagramDefaultEdge = ({
source,
target,
sourceY,
targetY,
markerStart,
markerEnd,
data,
}: WorkflowDiagramDefaultEdgeProps) => {
const theme = useTheme();
const isWorkflowFilteringEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED,
);
const [edgePath, labelX, labelY] = getStraightPath({
sourceX: CREATE_STEP_NODE_WIDTH,
sourceY,
targetX: CREATE_STEP_NODE_WIDTH,
targetY,
});
if (!isDefined(data)) {
throw new Error('Edge data is not defined');
}
const displayEdgeV1 = !isWorkflowFilteringEnabled && data.isEdgeEditable;
const displayEmptyFilters =
isWorkflowFilteringEnabled &&
data.edgeType === 'default' &&
data.isEdgeEditable;
const displayFilters =
isWorkflowFilteringEnabled && data.edgeType === 'filter';
return (
<>
<BaseEdge
markerStart={markerStart}
markerEnd={markerEnd}
path={edgePath}
style={{ stroke: theme.border.color.strong }}
/>
<EdgeLabelRenderer>
{displayEdgeV1 && (
<WorkflowDiagramEdgeV1
labelY={labelY}
parentStepId={source}
nextStepId={target}
/>
)}
{displayEmptyFilters && (
<WorkflowDiagramEdgeV2Empty
labelX={labelX}
labelY={labelY}
parentStepId={source}
nextStepId={target}
/>
)}
{displayFilters && (
<WorkflowDiagramEdgeV2Filter
labelX={labelX}
labelY={labelY}
stepId={data.stepId}
parentStepId={source}
nextStepId={target}
filterSettings={data.filterSettings}
isEdgeEditable={data.isEdgeEditable}
/>
)}
</EdgeLabelRenderer>
</>
);
};
@@ -0,0 +1,134 @@
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 { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getStraightPath,
} from '@xyflow/react';
import { useState } from 'react';
import { IconFilter, IconPlus } from 'twenty-ui/display';
import { IconButtonGroup } from 'twenty-ui/input';
type WorkflowDiagramDefaultEdgeEditableProps = EdgeProps<WorkflowDiagramEdge>;
const StyledIconButtonGroup = styled(IconButtonGroup)`
pointer-events: all;
`;
export const WorkflowDiagramDefaultEdgeEditable = ({
source,
target,
sourceY,
targetY,
markerStart,
markerEnd,
}: WorkflowDiagramDefaultEdgeEditableProps) => {
const theme = useTheme();
const [edgePath, labelX, labelY] = getStraightPath({
sourceX: CREATE_STEP_NODE_WIDTH,
sourceY,
targetX: CREATE_STEP_NODE_WIDTH,
targetY,
});
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
workflowVisualizerWorkflowIdComponentState,
);
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
assertWorkflowWithCurrentVersionIsDefined(workflow);
const { createStep } = useCreateStep({ workflow });
const { startNodeCreation } = useStartNodeCreation();
const [hovered, setHovered] = useState(false);
const workflowInsertStepIds = useRecoilComponentValueV2(
workflowInsertStepIdsComponentState,
);
const isSelected =
workflowInsertStepIds.nextStepId === target &&
workflowInsertStepIds.parentStepId === source;
const handleCreateFilter = async () => {
await createStep({
newStepType: 'FILTER',
parentStepId: source,
nextStepId: target,
});
setHovered(false);
};
const setWorkflowSelectedNode = useSetRecoilComponentStateV2(
workflowSelectedNodeComponentState,
);
const handleFilterButtonClick = () => {
setWorkflowSelectedNode(source);
handleCreateFilter();
};
const handleNodeButtonClick = () => {
startNodeCreation({
parentStepId: source,
nextStepId: target,
});
};
return (
<>
<BaseEdge
markerStart={markerStart}
markerEnd={markerEnd}
path={edgePath}
style={{ stroke: theme.border.color.strong }}
/>
<EdgeLabelRenderer>
<WorkflowDiagramEdgeV2Container
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
labelX={labelX}
labelY={labelY}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<WorkflowDiagramEdgeV2VisibilityContainer
shouldDisplay={isSelected || hovered}
>
<StyledIconButtonGroup
className="nodrag nopan"
iconButtons={[
{
Icon: IconFilter,
onClick: handleFilterButtonClick,
},
{
Icon: IconPlus,
onClick: handleNodeButtonClick,
},
]}
/>
</WorkflowDiagramEdgeV2VisibilityContainer>
</WorkflowDiagramEdgeV2Container>
</EdgeLabelRenderer>
</>
);
};
@@ -0,0 +1,31 @@
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { useTheme } from '@emotion/react';
import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react';
type WorkflowDiagramDefaultEdgeReadonlyProps = EdgeProps<WorkflowDiagramEdge>;
export const WorkflowDiagramDefaultEdgeReadonly = ({
sourceY,
targetY,
markerStart,
markerEnd,
}: WorkflowDiagramDefaultEdgeReadonlyProps) => {
const theme = useTheme();
const [edgePath] = getStraightPath({
sourceX: CREATE_STEP_NODE_WIDTH,
sourceY,
targetX: CREATE_STEP_NODE_WIDTH,
targetY,
});
return (
<BaseEdge
markerStart={markerStart}
markerEnd={markerEnd}
path={edgePath}
style={{ stroke: theme.border.color.strong }}
/>
);
};
@@ -0,0 +1,26 @@
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge';
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { EdgeProps, getStraightPath } from '@xyflow/react';
type WorkflowDiagramDefaultEdgeRunProps = EdgeProps<WorkflowDiagramEdge>;
export const WorkflowDiagramDefaultEdgeRun = ({
sourceY,
targetY,
data,
}: WorkflowDiagramDefaultEdgeRunProps) => {
const [edgePath] = getStraightPath({
sourceX: CREATE_STEP_NODE_WIDTH,
sourceY,
targetX: CREATE_STEP_NODE_WIDTH,
targetY,
});
return (
<WorkflowRunDiagramBaseEdge
edgePath={edgePath}
edgeExecutionStatus={data?.edgeExecutionStatus}
/>
);
};
@@ -1,83 +0,0 @@
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
import styled from '@emotion/styled';
import { useState } from 'react';
import { IconPlus } from 'twenty-ui/display';
import { IconButtonGroup } from 'twenty-ui/input';
const StyledIconButtonGroup = styled(IconButtonGroup)`
pointer-events: all;
`;
const StyledContainer = styled.div<{
labelY?: number;
}>`
position: absolute;
transform: ${({ labelY }) => `translate(${21}px, ${(labelY || 0) - 14}px)`};
`;
const StyledHoverZone = styled.div`
position: absolute;
width: 48px;
height: 52px;
transform: translate(-13px, -16px);
background: transparent;
`;
const StyledWrapper = styled.div`
pointer-events: all;
position: relative;
`;
type WorkflowDiagramEdgeV1Props = {
labelY?: number;
parentStepId: string;
nextStepId: string;
};
export const WorkflowDiagramEdgeV1 = ({
labelY,
parentStepId,
nextStepId,
}: WorkflowDiagramEdgeV1Props) => {
const [hovered, setHovered] = useState(false);
const { startNodeCreation } = useStartNodeCreation();
const workflowInsertStepIds = useRecoilComponentValueV2(
workflowInsertStepIdsComponentState,
);
const isSelected =
workflowInsertStepIds.parentStepId === parentStepId &&
workflowInsertStepIds.nextStepId === nextStepId;
return (
<StyledContainer
labelY={labelY}
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
>
<StyledWrapper
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<StyledHoverZone />
{(hovered || isSelected) && (
<StyledIconButtonGroup
className="nodrag nopan"
iconButtons={[
{
Icon: IconPlus,
onClick: () => {
startNodeCreation({ parentStepId, nextStepId });
},
},
]}
/>
)}
</StyledWrapper>
</StyledContainer>
);
};
@@ -1,49 +0,0 @@
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined';
import { WorkflowDiagramEdgeV2EmptyContent } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2EmptyContent';
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
type WorkflowDiagramEdgeV2EmptyProps = {
labelX: number;
labelY: number;
parentStepId: string;
nextStepId: string;
};
export const WorkflowDiagramEdgeV2Empty = ({
labelX,
labelY,
parentStepId,
nextStepId,
}: WorkflowDiagramEdgeV2EmptyProps) => {
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
workflowVisualizerWorkflowIdComponentState,
);
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
assertWorkflowWithCurrentVersionIsDefined(workflow);
const { createStep } = useCreateStep({ workflow });
const { startNodeCreation } = useStartNodeCreation();
return (
<WorkflowDiagramEdgeV2EmptyContent
labelX={labelX}
labelY={labelY}
parentStepId={parentStepId}
nextStepId={nextStepId}
onCreateFilter={() => {
return createStep({
newStepType: 'FILTER',
parentStepId,
nextStepId,
});
}}
onCreateNode={() => {
startNodeCreation({ parentStepId, nextStepId });
}}
/>
);
};
@@ -1,87 +0,0 @@
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
import styled from '@emotion/styled';
import { useState } from 'react';
import { IconFilter, IconPlus } from 'twenty-ui/display';
import { IconButtonGroup } from 'twenty-ui/input';
const StyledIconButtonGroup = styled(IconButtonGroup)`
pointer-events: all;
`;
type WorkflowDiagramEdgeV2EmptyContentProps = {
labelX: number;
labelY: number;
parentStepId: string;
nextStepId: string;
onCreateFilter: () => Promise<void>;
onCreateNode: () => void;
};
export const WorkflowDiagramEdgeV2EmptyContent = ({
labelX,
labelY,
parentStepId,
nextStepId,
onCreateFilter,
onCreateNode,
}: WorkflowDiagramEdgeV2EmptyContentProps) => {
const [hovered, setHovered] = useState(false);
const workflowInsertStepIds = useRecoilComponentValueV2(
workflowInsertStepIdsComponentState,
);
const isSelected =
workflowInsertStepIds.nextStepId === nextStepId &&
workflowInsertStepIds.parentStepId === parentStepId;
const handleCreateFilter = async () => {
await onCreateFilter();
setHovered(false);
};
const setWorkflowSelectedNode = useSetRecoilComponentStateV2(
workflowSelectedNodeComponentState,
);
const handleFilterButtonClick = () => {
setWorkflowSelectedNode(parentStepId);
handleCreateFilter();
};
return (
<WorkflowDiagramEdgeV2Container
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
labelX={labelX}
labelY={labelY}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<WorkflowDiagramEdgeV2VisibilityContainer
shouldDisplay={isSelected || hovered}
>
<StyledIconButtonGroup
className="nodrag nopan"
iconButtons={[
{
Icon: IconFilter,
onClick: handleFilterButtonClick,
},
{
Icon: IconPlus,
onClick: onCreateNode,
},
]}
/>
</WorkflowDiagramEdgeV2VisibilityContainer>
</WorkflowDiagramEdgeV2Container>
);
};
@@ -1,60 +0,0 @@
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { WorkflowDiagramEdgeV2FilterContent } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2FilterContent';
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
import { FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
import { isDefined } from 'twenty-shared/utils';
type WorkflowDiagramEdgeV2FilterProps = {
labelX: number;
labelY: number;
stepId: string;
parentStepId: string;
nextStepId: string;
filterSettings: FilterSettings;
isEdgeEditable: boolean;
};
export const WorkflowDiagramEdgeV2Filter = ({
labelX,
labelY,
stepId,
parentStepId,
nextStepId,
filterSettings,
isEdgeEditable,
}: WorkflowDiagramEdgeV2FilterProps) => {
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
workflowVisualizerWorkflowIdComponentState,
);
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
const { deleteStep } = useDeleteStep({ workflow });
const { startNodeCreation } = useStartNodeCreation();
return (
<WorkflowDiagramEdgeV2FilterContent
labelX={labelX}
labelY={labelY}
stepId={stepId}
parentStepId={parentStepId}
nextStepId={nextStepId}
filterSettings={filterSettings}
isEdgeEditable={isEdgeEditable}
onDeleteFilter={() => {
if (!isDefined(stepId)) {
throw new Error(
'Step ID must be configured for the edge when rendering a filter',
);
}
return deleteStep(stepId);
}}
onCreateNode={() => {
startNodeCreation({ parentStepId: stepId, nextStepId });
}}
/>
);
};
@@ -1,220 +0,0 @@
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';
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 { 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 { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
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 { FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
IconDotsVertical,
IconFilter,
IconFilterX,
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 StyledConfiguredFilterContainer = styled.div`
height: 26px;
width: 26px;
`;
type WorkflowDiagramEdgeV2FilterContentProps = {
labelX: number;
labelY: number;
stepId: string;
parentStepId: string;
nextStepId: string;
filterSettings: FilterSettings;
onDeleteFilter: () => Promise<void>;
onCreateNode: () => void;
isEdgeEditable: boolean;
};
export const WorkflowDiagramEdgeV2FilterContent = ({
labelX,
labelY,
stepId,
parentStepId,
nextStepId,
onDeleteFilter,
onCreateNode,
isEdgeEditable,
}: WorkflowDiagramEdgeV2FilterContentProps) => {
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(
isDropdownOpenComponentState,
dropdownId,
);
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
workflowVisualizerWorkflowIdComponentState,
);
const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu();
const setWorkflowSelectedNode = useSetRecoilComponentStateV2(
workflowSelectedNodeComponentState,
);
const handleMouseEnter = () => {
if (!isEdgeEditable) {
return;
}
setHovered(true);
};
const handleMouseLeave = () => {
setHovered(false);
};
const handleFilterButtonClick = () => {
setWorkflowSelectedNode(stepId);
if (isDefined(workflowVisualizerWorkflowId)) {
openWorkflowEditStepInCommandMenu(
workflowVisualizerWorkflowId,
'Filter',
IconFilter,
);
}
};
return (
<WorkflowDiagramEdgeV2Container
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
labelX={labelX}
labelY={labelY}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<WorkflowDiagramEdgeV2VisibilityContainer shouldDisplay>
<StyledConfiguredFilterContainer>
{hovered || isDropdownOpen || isSelected ? (
<StyledIconButtonGroup
className="nodrag nopan"
iconButtons={[
{
Icon: IconFilter,
onClick: handleFilterButtonClick,
},
{
Icon: IconDotsVertical,
onClick: () => {
openDropdown({
dropdownComponentInstanceIdFromProps: dropdownId,
});
},
},
]}
/>
) : (
<StyledIconButtonGroup
className="nodrag nopan"
iconButtons={[
{
Icon: IconFilter,
onClick: handleFilterButtonClick,
},
]}
/>
)}
</StyledConfiguredFilterContainer>
<Dropdown
dropdownId={dropdownId}
clickableComponent={<div></div>}
data-select-disable
dropdownPlacement="bottom-start"
dropdownStrategy="absolute"
dropdownOffset={{
x: 24,
y: 4,
}}
onOpen={() => {
setWorkflowDiagramPanOnDrag(false);
}}
onClose={() => {
setWorkflowDiagramPanOnDrag(true);
}}
dropdownComponents={
<DropdownContent widthInPixels={GenericDropdownContentWidth.Narrow}>
<DropdownMenuItemsContainer>
<MenuItem
text="Filter"
LeftIcon={IconFilter}
onClick={() => {
closeDropdown(dropdownId);
setHovered(false);
handleFilterButtonClick();
}}
/>
<MenuItem
text="Remove Filter"
LeftIcon={IconFilterX}
onClick={() => {
closeDropdown(dropdownId);
setHovered(false);
onDeleteFilter();
}}
/>
<MenuItem
text="Add Node"
LeftIcon={IconPlus}
onClick={() => {
closeDropdown(dropdownId);
setHovered(false);
onCreateNode();
}}
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
</WorkflowDiagramEdgeV2VisibilityContainer>
</WorkflowDiagramEdgeV2Container>
);
};
@@ -13,10 +13,11 @@ import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/state
import { addCreateStepNodes } from '@/workflow/workflow-diagram/utils/addCreateStepNodes';
import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram';
import { mergeWorkflowDiagrams } from '@/workflow/workflow-diagram/utils/mergeWorkflowDiagrams';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { addEdgeOptions } from '@/workflow/workflow-diagram/utils/addEdgeOptions';
import { FeatureFlagKey } from '~/generated/graphql';
export const WorkflowDiagramEffect = ({
workflowWithCurrentVersion,
@@ -36,6 +37,10 @@ export const WorkflowDiagramEffect = ({
workflowLastCreatedStepIdComponentState,
);
const isWorkflowFilteringEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED,
);
const computeAndMergeNewWorkflowDiagram = useRecoilCallback(
({ snapshot, set }) => {
return (currentVersion: WorkflowVersion) => {
@@ -45,7 +50,11 @@ export const WorkflowDiagramEffect = ({
);
const nextWorkflowDiagram = addCreateStepNodes(
addEdgeOptions(getWorkflowVersionDiagram(currentVersion)),
getWorkflowVersionDiagram({
workflowVersion: currentVersion,
isWorkflowFilteringEnabled,
isEditable: true,
}),
);
let mergedWorkflowDiagram = nextWorkflowDiagram;
@@ -78,7 +87,11 @@ export const WorkflowDiagramEffect = ({
set(workflowDiagramState, mergedWorkflowDiagram);
};
},
[workflowLastCreatedStepIdState, workflowDiagramState],
[
workflowDiagramState,
isWorkflowFilteringEnabled,
workflowLastCreatedStepIdState,
],
);
const currentVersion = workflowWithCurrentVersion?.currentVersion;
@@ -0,0 +1,257 @@
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 { 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 { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu';
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramPanOnDragComponentState';
import {
WorkflowDiagramEdge,
WorkflowDiagramEdgeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getStraightPath,
} from '@xyflow/react';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
IconDotsVertical,
IconFilter,
IconFilterX,
IconPlus,
} from 'twenty-ui/display';
import { IconButtonGroup } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
type WorkflowDiagramFilterEdgeEditableProps = EdgeProps<WorkflowDiagramEdge>;
const assertFilterEdgeDataOrThrow: (
data: WorkflowDiagramEdgeData | undefined,
) => asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = (
data: WorkflowDiagramEdgeData | undefined,
) => {
if (data?.edgeType !== 'filter') {
throw new Error('Edge data must be of type "filter"');
}
};
const StyledIconButtonGroup = styled(IconButtonGroup)`
pointer-events: all;
`;
const StyledConfiguredFilterContainer = styled.div`
height: 26px;
width: 26px;
`;
export const WorkflowDiagramFilterEdgeEditable = ({
source,
target,
sourceY,
targetY,
markerStart,
markerEnd,
data,
}: WorkflowDiagramFilterEdgeEditableProps) => {
assertFilterEdgeDataOrThrow(data);
const theme = useTheme();
const [edgePath, labelX, labelY] = getStraightPath({
sourceX: CREATE_STEP_NODE_WIDTH,
sourceY,
targetX: CREATE_STEP_NODE_WIDTH,
targetY,
});
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
workflowVisualizerWorkflowIdComponentState,
);
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
const { deleteStep } = useDeleteStep({ workflow });
const { startNodeCreation } = useStartNodeCreation();
const { openDropdown } = useOpenDropdown();
const { closeDropdown } = useCloseDropdown();
const [hovered, setHovered] = useState(false);
const setWorkflowDiagramPanOnDrag = useSetRecoilComponentStateV2(
workflowDiagramPanOnDragComponentState,
);
const workflowInsertStepIds = useRecoilComponentValueV2(
workflowInsertStepIdsComponentState,
);
const isSelected =
workflowInsertStepIds.nextStepId === source &&
(workflowInsertStepIds.parentStepId === target ||
(isNonEmptyString(data.stepId) &&
workflowInsertStepIds.parentStepId === data.stepId));
const dropdownId = `${WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}-${source}-${target}`;
const isDropdownOpen = useRecoilComponentValueV2(
isDropdownOpenComponentState,
dropdownId,
);
const { openWorkflowEditFilterInCommandMenu } =
useOpenWorkflowEditFilterInCommandMenu();
const handleMouseEnter = () => {
setHovered(true);
};
const handleMouseLeave = () => {
setHovered(false);
};
const handleFilterButtonClick = () => {
openWorkflowEditFilterInCommandMenu({
stepId: data.stepId,
stepName: data.name,
});
};
return (
<>
<BaseEdge
markerStart={markerStart}
markerEnd={markerEnd}
path={edgePath}
style={{ stroke: theme.border.color.strong }}
/>
<EdgeLabelRenderer>
<WorkflowDiagramEdgeV2Container
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
labelX={labelX}
labelY={labelY}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<WorkflowDiagramEdgeV2VisibilityContainer shouldDisplay>
<StyledConfiguredFilterContainer>
{hovered || isDropdownOpen || isSelected ? (
<StyledIconButtonGroup
className="nodrag nopan"
iconButtons={[
{
Icon: IconFilter,
onClick: handleFilterButtonClick,
},
{
Icon: IconDotsVertical,
onClick: () => {
openDropdown({
dropdownComponentInstanceIdFromProps: dropdownId,
});
},
},
]}
/>
) : (
<StyledIconButtonGroup
className="nodrag nopan"
iconButtons={[
{
Icon: IconFilter,
onClick: handleFilterButtonClick,
},
]}
/>
)}
</StyledConfiguredFilterContainer>
<Dropdown
dropdownId={dropdownId}
clickableComponent={<div></div>}
data-select-disable
dropdownPlacement="bottom-start"
dropdownStrategy="absolute"
dropdownOffset={{
x: 24,
y: 4,
}}
onOpen={() => {
setWorkflowDiagramPanOnDrag(false);
}}
onClose={() => {
setWorkflowDiagramPanOnDrag(true);
}}
dropdownComponents={
<DropdownContent
widthInPixels={GenericDropdownContentWidth.Narrow}
>
<DropdownMenuItemsContainer>
<MenuItem
text="Filter"
LeftIcon={IconFilter}
onClick={() => {
closeDropdown(dropdownId);
setHovered(false);
handleFilterButtonClick();
}}
/>
<MenuItem
text="Remove Filter"
LeftIcon={IconFilterX}
onClick={() => {
closeDropdown(dropdownId);
setHovered(false);
if (!isDefined(data.stepId)) {
throw new Error(
'Step ID must be configured for the edge when rendering a filter',
);
}
return deleteStep(data.stepId);
}}
/>
<MenuItem
text="Add Node"
LeftIcon={IconPlus}
onClick={() => {
closeDropdown(dropdownId);
setHovered(false);
startNodeCreation({
parentStepId: data.stepId,
nextStepId: target,
});
}}
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
</WorkflowDiagramEdgeV2VisibilityContainer>
</WorkflowDiagramEdgeV2Container>
</EdgeLabelRenderer>
</>
);
};
@@ -0,0 +1,102 @@
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useOpenWorkflowViewFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu';
import {
WorkflowDiagramEdge,
WorkflowDiagramEdgeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getStraightPath,
} from '@xyflow/react';
import { IconFilter } from 'twenty-ui/display';
import { IconButtonGroup } from 'twenty-ui/input';
type WorkflowDiagramFilterEdgeReadonlyProps = EdgeProps<WorkflowDiagramEdge>;
const assertFilterEdgeDataOrThrow: (
data: WorkflowDiagramEdgeData | undefined,
) => asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = (
data: WorkflowDiagramEdgeData | undefined,
) => {
if (data?.edgeType !== 'filter') {
throw new Error('Edge data must be of type "filter"');
}
};
const StyledIconButtonGroup = styled(IconButtonGroup)`
pointer-events: all;
`;
const StyledConfiguredFilterContainer = styled.div`
height: 26px;
width: 26px;
`;
export const WorkflowDiagramFilterEdgeReadonly = ({
sourceY,
targetY,
markerStart,
markerEnd,
data,
}: WorkflowDiagramFilterEdgeReadonlyProps) => {
assertFilterEdgeDataOrThrow(data);
const theme = useTheme();
const [edgePath, labelX, labelY] = getStraightPath({
sourceX: CREATE_STEP_NODE_WIDTH,
sourceY,
targetX: CREATE_STEP_NODE_WIDTH,
targetY,
});
const { openWorkflowViewFilterInCommandMenu } =
useOpenWorkflowViewFilterInCommandMenu();
const handleFilterButtonClick = () => {
openWorkflowViewFilterInCommandMenu({
stepId: data.stepId,
stepName: data.name,
});
};
return (
<>
<BaseEdge
markerStart={markerStart}
markerEnd={markerEnd}
path={edgePath}
style={{ stroke: theme.border.color.strong }}
/>
<EdgeLabelRenderer>
<WorkflowDiagramEdgeV2Container
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
labelX={labelX}
labelY={labelY}
>
<WorkflowDiagramEdgeV2VisibilityContainer shouldDisplay>
<StyledConfiguredFilterContainer>
<StyledIconButtonGroup
className="nodrag nopan"
iconButtons={[
{
Icon: IconFilter,
onClick: handleFilterButtonClick,
},
]}
/>
</StyledConfiguredFilterContainer>
</WorkflowDiagramEdgeV2VisibilityContainer>
</WorkflowDiagramEdgeV2Container>
</EdgeLabelRenderer>
</>
);
};
@@ -0,0 +1,97 @@
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge';
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useOpenWorkflowRunFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu';
import {
WorkflowDiagramEdge,
WorkflowDiagramEdgeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import styled from '@emotion/styled';
import { EdgeLabelRenderer, EdgeProps, getStraightPath } from '@xyflow/react';
import { isDefined } from 'twenty-shared/utils';
import { IconFilter } from 'twenty-ui/display';
import { IconButtonGroup } from 'twenty-ui/input';
type WorkflowDiagramFilterEdgeRunProps = EdgeProps<WorkflowDiagramEdge>;
const assertFilterEdgeDataOrThrow: (
data: WorkflowDiagramEdgeData | undefined,
) => asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = (
data: WorkflowDiagramEdgeData | undefined,
) => {
if (data?.edgeType !== 'filter') {
throw new Error('Edge data must be of type "filter"');
}
};
const StyledIconButtonGroup = styled(IconButtonGroup)`
pointer-events: all;
`;
const StyledConfiguredFilterContainer = styled.div`
height: 26px;
width: 26px;
`;
export const WorkflowDiagramFilterEdgeRun = ({
sourceY,
targetY,
data,
}: WorkflowDiagramFilterEdgeRunProps) => {
assertFilterEdgeDataOrThrow(data);
const [edgePath, labelX, labelY] = getStraightPath({
sourceX: CREATE_STEP_NODE_WIDTH,
sourceY,
targetX: CREATE_STEP_NODE_WIDTH,
targetY,
});
const { openWorkflowRunFilterInCommandMenu } =
useOpenWorkflowRunFilterInCommandMenu();
const handleFilterButtonClick = () => {
if (!isDefined(data.runStatus)) {
throw new Error('Run status must be set on edge data for workflow runs');
}
openWorkflowRunFilterInCommandMenu({
stepId: data.stepId,
stepName: data.name,
stepExecutionStatus: data.runStatus,
});
};
return (
<>
<WorkflowRunDiagramBaseEdge
edgePath={edgePath}
edgeExecutionStatus={data.edgeExecutionStatus}
/>
<EdgeLabelRenderer>
<WorkflowDiagramEdgeV2Container
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
labelX={labelX}
labelY={labelY}
>
<WorkflowDiagramEdgeV2VisibilityContainer shouldDisplay>
<StyledConfiguredFilterContainer>
<StyledIconButtonGroup
className="nodrag nopan"
iconButtons={[
{
Icon: IconFilter,
onClick: handleFilterButtonClick,
},
]}
/>
</StyledConfiguredFilterContainer>
</WorkflowDiagramEdgeV2VisibilityContainer>
</WorkflowDiagramEdgeV2Container>
</EdgeLabelRenderer>
</>
);
};
@@ -0,0 +1,108 @@
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react';
import { useState } from 'react';
import { IconPlus } from 'twenty-ui/display';
import { IconButtonGroup } from 'twenty-ui/input';
const StyledIconButtonGroup = styled(IconButtonGroup)`
pointer-events: all;
`;
const StyledContainer = styled.div<{
labelY?: number;
}>`
position: absolute;
transform: ${({ labelY }) => `translate(${21}px, ${(labelY || 0) - 14}px)`};
`;
const StyledHoverZone = styled.div`
position: absolute;
width: 48px;
height: 52px;
transform: translate(-13px, -16px);
background: transparent;
`;
const StyledWrapper = styled.div`
pointer-events: all;
position: relative;
`;
type WorkflowDiagramFilteringDisabledEdgeEditableProps =
EdgeProps<WorkflowDiagramEdge>;
export const WorkflowDiagramFilteringDisabledEdgeEditable = ({
markerStart,
markerEnd,
source,
sourceY,
target,
targetY,
}: WorkflowDiagramFilteringDisabledEdgeEditableProps) => {
const theme = useTheme();
const [edgePath, , labelY] = getStraightPath({
sourceX: CREATE_STEP_NODE_WIDTH,
sourceY,
targetX: CREATE_STEP_NODE_WIDTH,
targetY,
});
const [hovered, setHovered] = useState(false);
const { startNodeCreation } = useStartNodeCreation();
const workflowInsertStepIds = useRecoilComponentValueV2(
workflowInsertStepIdsComponentState,
);
const isSelected =
workflowInsertStepIds.parentStepId === source &&
workflowInsertStepIds.nextStepId === target;
return (
<>
<BaseEdge
markerStart={markerStart}
markerEnd={markerEnd}
path={edgePath}
style={{ stroke: theme.border.color.strong }}
/>
<StyledContainer
labelY={labelY}
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
>
<StyledWrapper
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<StyledHoverZone />
{(hovered || isSelected) && (
<StyledIconButtonGroup
className="nodrag nopan"
iconButtons={[
{
Icon: IconPlus,
onClick: () => {
startNodeCreation({
parentStepId: source,
nextStepId: target,
});
},
},
]}
/>
)}
</StyledWrapper>
</StyledContainer>
</>
);
};
@@ -0,0 +1,32 @@
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { useTheme } from '@emotion/react';
import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react';
type WorkflowDiagramFilteringDisabledEdgeReadonlyProps =
EdgeProps<WorkflowDiagramEdge>;
export const WorkflowDiagramFilteringDisabledEdgeReadonly = ({
markerStart,
markerEnd,
sourceY,
targetY,
}: WorkflowDiagramFilteringDisabledEdgeReadonlyProps) => {
const theme = useTheme();
const [edgePath] = getStraightPath({
sourceX: CREATE_STEP_NODE_WIDTH,
sourceY,
targetX: CREATE_STEP_NODE_WIDTH,
targetY,
});
return (
<BaseEdge
markerStart={markerStart}
markerEnd={markerEnd}
path={edgePath}
style={{ stroke: theme.border.color.strong }}
/>
);
};
@@ -0,0 +1,27 @@
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge';
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { EdgeProps, getStraightPath } from '@xyflow/react';
type WorkflowDiagramFilteringDisabledEdgeRunProps =
EdgeProps<WorkflowDiagramEdge>;
export const WorkflowDiagramFilteringDisabledEdgeRun = ({
sourceY,
targetY,
data,
}: WorkflowDiagramFilteringDisabledEdgeRunProps) => {
const [edgePath] = getStraightPath({
sourceX: CREATE_STEP_NODE_WIDTH,
sourceY,
targetX: CREATE_STEP_NODE_WIDTH,
targetY,
});
return (
<WorkflowRunDiagramBaseEdge
edgePath={edgePath}
edgeExecutionStatus={data?.edgeExecutionStatus}
/>
);
};
@@ -1,59 +0,0 @@
import { EDGE_GREEN_ROUNDED_ARROW_MARKER_WIDTH_PX } from '@/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerWidthPx';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getStraightPath,
} from '@xyflow/react';
import { Label } from 'twenty-ui/display';
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
const StyledLabel = styled(Label)`
color: ${({ theme }) => theme.tag.text.turquoise};
`;
type WorkflowDiagramSuccessEdgeProps = EdgeProps;
export const WorkflowDiagramSuccessEdge = ({
sourceY,
targetY,
markerStart,
markerEnd,
label,
}: WorkflowDiagramSuccessEdgeProps) => {
const theme = useTheme();
const [edgePath, labelX, labelY] = getStraightPath({
sourceX: CREATE_STEP_NODE_WIDTH,
sourceY,
targetX: CREATE_STEP_NODE_WIDTH,
targetY,
});
return (
<>
<BaseEdge
markerStart={markerStart}
markerEnd={markerEnd}
path={edgePath}
style={{ stroke: theme.tag.text.turquoise }}
/>
<EdgeLabelRenderer>
<StyledLabel
variant="small"
style={{
position: 'absolute',
transform: `translate(0, -50%) translate(${labelX}px, ${labelY}px) translateX(${EDGE_GREEN_ROUNDED_ARROW_MARKER_WIDTH_PX / 2 + 3}px)`,
pointerEvents: 'all',
}}
className="nodrag nopan"
>
{label}
</StyledLabel>
</EdgeLabelRenderer>
</>
);
};
@@ -0,0 +1,65 @@
import { EDGE_GRAY_CIRCLE_MARKED_ID } from '@/workflow/workflow-diagram/constants/EdgeGrayCircleMarkedId';
import { EDGE_GREEN_CIRCLE_MARKED_ID } from '@/workflow/workflow-diagram/constants/EdgeGreenCircleMarkedId';
import { EDGE_GREEN_ROUNDED_ARROW_MARKER_ID } from '@/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerId';
import { EDGE_ROUNDED_ARROW_MARKER_ID } from '@/workflow/workflow-diagram/constants/EdgeRoundedArrowMarkerId';
import { Theme, useTheme } from '@emotion/react';
import { BaseEdge } from '@xyflow/react';
import { StepStatus } from 'twenty-shared/workflow';
const toMarkerId = (id: string) => `url(#${id})`;
const getMarkerStart = (edgeExecutionStatus: StepStatus | undefined) => {
if (edgeExecutionStatus === StepStatus.SUCCESS) {
return EDGE_GREEN_CIRCLE_MARKED_ID;
}
return EDGE_GRAY_CIRCLE_MARKED_ID;
};
const getMarkerEnd = (edgeExecutionStatus: StepStatus | undefined) => {
if (edgeExecutionStatus === StepStatus.SUCCESS) {
return EDGE_GREEN_ROUNDED_ARROW_MARKER_ID;
}
return EDGE_ROUNDED_ARROW_MARKER_ID;
};
const getStrokeColor = ({
theme,
edgeExecutionStatus,
}: {
theme: Theme;
edgeExecutionStatus: StepStatus | undefined;
}) => {
if (edgeExecutionStatus === StepStatus.SUCCESS) {
return theme.tag.text.turquoise;
}
return theme.border.color.strong;
};
type WorkflowRunDiagramBaseEdgeProps = {
edgePath: string;
edgeExecutionStatus: StepStatus | undefined;
};
export const WorkflowRunDiagramBaseEdge = ({
edgePath,
edgeExecutionStatus,
}: WorkflowRunDiagramBaseEdgeProps) => {
const theme = useTheme();
return (
<BaseEdge
markerStart={toMarkerId(getMarkerStart(edgeExecutionStatus))}
markerEnd={toMarkerId(getMarkerEnd(edgeExecutionStatus))}
path={edgePath}
style={{
stroke: getStrokeColor({
theme,
edgeExecutionStatus,
}),
}}
/>
);
};
@@ -1,8 +1,9 @@
import { WorkflowRunStatus } from '@/workflow/types/Workflow';
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge';
import { WorkflowDiagramDefaultEdgeRun } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeRun';
import { WorkflowDiagramFilterEdgeRun } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeRun';
import { WorkflowDiagramFilteringDisabledEdgeRun } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeRun';
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly';
import { WorkflowDiagramSuccessEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge';
import { WorkflowRunDiagramCanvasEffect } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect';
import { useHandleWorkflowRunDiagramCanvasInit } from '@/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit';
import { getWorkflowRunStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps';
@@ -27,8 +28,9 @@ export const WorkflowRunDiagramCanvas = ({
default: WorkflowDiagramStepNodeReadonly,
}}
edgeTypes={{
default: WorkflowDiagramDefaultEdge,
success: WorkflowDiagramSuccessEdge,
'filtering-disabled--run': WorkflowDiagramFilteringDisabledEdgeRun,
'empty-filter--run': WorkflowDiagramDefaultEdgeRun,
'filter--run': WorkflowDiagramFilterEdgeRun,
}}
tagContainerTestId="workflow-run-status"
tagColor={tagProps.color}
@@ -18,10 +18,12 @@ import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
import { selectWorkflowDiagramNode } from '@/workflow/workflow-diagram/utils/selectWorkflowDiagramNode';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useContext, useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { FeatureFlagKey } from '~/generated/graphql';
export const WorkflowRunVisualizerEffect = ({
workflowRunId,
@@ -67,6 +69,10 @@ export const WorkflowRunVisualizerEffect = ({
const { isInRightDrawer } = useContext(ActionMenuContext);
const isWorkflowFilteringEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED,
);
useEffect(() => {
setWorkflowRunId(workflowRunId);
}, [setWorkflowRunId, workflowRunId]);
@@ -117,6 +123,7 @@ export const WorkflowRunVisualizerEffect = ({
trigger: workflowRunState.flow.trigger,
steps: workflowRunState.flow.steps,
stepInfos: workflowRunState.stepInfos,
isWorkflowFilteringEnabled,
});
if (isDefined(stepToOpenByDefault)) {
@@ -184,6 +191,7 @@ export const WorkflowRunVisualizerEffect = ({
[
flowState,
getIcon,
isWorkflowFilteringEnabled,
openWorkflowRunViewStepInCommandMenu,
workflowDiagramState,
workflowDiagramStatusState,
@@ -6,8 +6,10 @@ import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/wo
import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState';
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
export const WorkflowVersionVisualizerEffect = ({
workflowVersionId,
@@ -29,6 +31,10 @@ export const WorkflowVersionVisualizerEffect = ({
const { populateStepsOutputSchema } = useStepsOutputSchema();
const isWorkflowFilteringEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED,
);
useEffect(() => {
if (!isDefined(workflowVersion)) {
setFlow(undefined);
@@ -58,10 +64,14 @@ export const WorkflowVersionVisualizerEffect = ({
return;
}
const nextWorkflowDiagram = getWorkflowVersionDiagram(workflowVersion);
const nextWorkflowDiagram = getWorkflowVersionDiagram({
workflowVersion,
isWorkflowFilteringEnabled,
isEditable: false,
});
setWorkflowDiagram(nextWorkflowDiagram);
}, [setWorkflowDiagram, workflowVersion]);
}, [isWorkflowFilteringEnabled, setWorkflowDiagram, workflowVersion]);
useEffect(() => {
if (!isDefined(workflowVersion)) {
@@ -1,205 +0,0 @@
import styled from '@emotion/styled';
import { Meta, StoryObj } from '@storybook/react';
import { RecoilRoot } from 'recoil';
import { WorkflowDiagramCreateStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepNode';
import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge';
import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger';
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly';
import { WorkflowDiagramSuccessEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge';
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeDefaultConfiguration';
import { WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration';
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { workflowDiagramComponentState } from '../../states/workflowDiagramComponentState';
import { WorkflowDiagramCanvasBase } from '../WorkflowDiagramCanvasBase';
const StyledContainer = styled.div`
height: 400px;
width: 100%;
position: relative;
`;
const meta: Meta = {
title: 'Modules/Workflow/WorkflowDiagram/WorkflowDiagramCustomMarkers',
component: WorkflowDiagramCanvasBase,
parameters: {
msw: graphqlMocks,
},
decorators: [
WorkspaceDecorator,
ObjectMetadataItemsDecorator,
ReactflowDecorator,
],
};
export default meta;
type Story = StoryObj<typeof WorkflowDiagramCanvasBase>;
export const DefaultEdge: Story = {
args: {
nodeTypes: {
default: WorkflowDiagramStepNodeReadonly,
'create-step': WorkflowDiagramCreateStepNode,
'empty-trigger': WorkflowDiagramEmptyTrigger,
},
edgeTypes: {
default: WorkflowDiagramDefaultEdge,
},
},
decorators: [
(Story) => {
const workflowVisualizerComponentInstanceId =
'workflow-visualizer-test-id';
return (
<RecoilRoot
initializeState={({ set }) => {
set(
workflowDiagramComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
{
nodes: [
{
id: 'trigger-1',
type: 'default',
position: { x: 100, y: 100 },
data: {
nodeType: 'trigger',
triggerType: 'DATABASE_EVENT',
name: 'When record is created',
},
},
{
id: 'action-1',
type: 'default',
position: { x: 300, y: 100 },
data: {
nodeType: 'action',
actionType: 'CREATE_RECORD',
name: 'Create record',
},
},
{
id: 'create-step-1',
type: 'create-step',
position: { x: 500, y: 100 },
data: {
nodeType: 'create-step',
parentNodeId: 'action-1',
},
},
],
edges: [
{
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
id: 'edge-1',
source: 'trigger-1',
target: 'action-1',
},
{
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
id: 'edge-2',
source: 'action-1',
target: 'create-step-1',
},
],
},
);
}}
>
<WorkflowVisualizerComponentInstanceContext.Provider
value={{
instanceId: workflowVisualizerComponentInstanceId,
}}
>
<StyledContainer>
<Story />
</StyledContainer>
</WorkflowVisualizerComponentInstanceContext.Provider>
</RecoilRoot>
);
},
],
};
export const SuccessEdge: Story = {
args: {
nodeTypes: {
default: WorkflowDiagramStepNodeReadonly,
'create-step': WorkflowDiagramCreateStepNode,
'empty-trigger': WorkflowDiagramEmptyTrigger,
},
edgeTypes: {
default: WorkflowDiagramDefaultEdge,
success: WorkflowDiagramSuccessEdge,
},
},
decorators: [
(Story) => {
const workflowVisualizerComponentInstanceId =
'workflow-visualizer-test-id';
return (
<RecoilRoot
initializeState={({ set }) => {
set(
workflowDiagramComponentState.atomFamily({
instanceId: workflowVisualizerComponentInstanceId,
}),
{
nodes: [
{
id: 'trigger-1',
type: 'default',
position: { x: 100, y: 100 },
data: {
nodeType: 'trigger',
triggerType: 'DATABASE_EVENT',
name: 'When record is created',
},
},
{
id: 'action-1',
type: 'default',
position: { x: 300, y: 100 },
data: {
nodeType: 'action',
actionType: 'CREATE_RECORD',
name: 'Create record',
},
},
],
edges: [
{
...WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION,
id: 'edge-1',
source: 'trigger-1',
target: 'action-1',
type: 'success',
label: '1 item',
},
],
},
);
}}
>
<WorkflowVisualizerComponentInstanceContext.Provider
value={{
instanceId: workflowVisualizerComponentInstanceId,
}}
>
<StyledContainer>
<Story />
</StyledContainer>
</WorkflowVisualizerComponentInstanceContext.Provider>
</RecoilRoot>
);
},
],
};
@@ -1,94 +0,0 @@
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
import { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
import '@xyflow/react/dist/style.css';
import { ComponentDecorator } from 'twenty-ui/testing';
import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
import { WorkflowDiagramEdgeV2EmptyContent } from '../WorkflowDiagramEdgeV2EmptyContent';
const meta: Meta<typeof WorkflowDiagramEdgeV2EmptyContent> = {
title: 'Modules/Workflow/WorkflowDiagramEdgeV2EmptyContent',
component: WorkflowDiagramEdgeV2EmptyContent,
decorators: [
ComponentDecorator,
ReactflowDecorator,
(Story) => {
const workflowVisualizerComponentInstanceId =
'workflow-visualizer-test-id';
return (
<WorkflowVisualizerComponentInstanceContext.Provider
value={{
instanceId: workflowVisualizerComponentInstanceId,
}}
>
<Story />
</WorkflowVisualizerComponentInstanceContext.Provider>
);
},
],
args: {
labelX: 0,
labelY: 0,
parentStepId: 'parent-step-id',
nextStepId: 'next-step-id',
onCreateFilter: fn(),
onCreateNode: fn(),
},
};
export default meta;
type Story = StoryObj<typeof WorkflowDiagramEdgeV2EmptyContent>;
export const ButtonsAppearOnHover: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const buttons = await canvas.findAllByRole('button');
const filterButton = buttons[0];
userEvent.hover(filterButton);
await waitFor(() => {
expect(filterButton).toBeVisible();
});
},
};
export const CreateFilter: Story = {
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const buttons = await canvas.findAllByRole('button');
const filterButton = buttons[0];
userEvent.hover(filterButton);
await waitFor(() => {
expect(filterButton).toBeVisible();
});
userEvent.click(filterButton);
await waitFor(() => {
expect(args.onCreateFilter).toHaveBeenCalledTimes(1);
});
},
};
export const AddNodeAction: Story = {
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const buttons = await canvas.findAllByRole('button');
const addNodeButton = buttons[1];
userEvent.hover(addNodeButton);
userEvent.click(addNodeButton);
await waitFor(() => {
expect(args.onCreateNode).toHaveBeenCalledTimes(1);
});
},
};
@@ -1,125 +0,0 @@
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
import { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
import '@xyflow/react/dist/style.css';
import {
ComponentDecorator,
getCanvasElementForDropdownTesting,
} from 'twenty-ui/testing';
import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
import { WorkflowDiagramEdgeV2FilterContent } from '../WorkflowDiagramEdgeV2FilterContent';
const meta: Meta<typeof WorkflowDiagramEdgeV2FilterContent> = {
title: 'Modules/Workflow/WorkflowDiagramEdgeV2FilterContent',
component: WorkflowDiagramEdgeV2FilterContent,
decorators: [
ComponentDecorator,
ReactflowDecorator,
(Story) => {
const workflowVisualizerComponentInstanceId =
'workflow-visualizer-test-id';
return (
<WorkflowVisualizerComponentInstanceContext.Provider
value={{
instanceId: workflowVisualizerComponentInstanceId,
}}
>
<Story />
</WorkflowVisualizerComponentInstanceContext.Provider>
);
},
],
args: {
labelX: 0,
labelY: 0,
parentStepId: 'parent-step-id',
nextStepId: 'next-step-id',
onDeleteFilter: fn(),
onCreateNode: fn(),
},
};
export default meta;
type Story = StoryObj<typeof WorkflowDiagramEdgeV2FilterContent>;
export const ButtonsAppearOnHover: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const buttons = await canvas.findAllByRole('button');
const filterButton = buttons[0];
userEvent.hover(filterButton);
await waitFor(() => {
expect(filterButton).toBeVisible();
});
},
};
export const AddNodeAction: Story = {
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const buttons = await canvas.findAllByRole('button');
const dotsButton = buttons[1];
userEvent.hover(dotsButton);
await waitFor(() => {
expect(dotsButton).toBeVisible();
});
userEvent.click(dotsButton);
const addNodeButton = await within(
getCanvasElementForDropdownTesting(),
).findByText('Add Node');
userEvent.click(addNodeButton);
await waitFor(() => {
expect(canvas.queryByText('Add Node')).not.toBeInTheDocument();
});
await waitFor(() => {
expect(args.onCreateNode).toHaveBeenCalledTimes(1);
});
},
};
export const DropdownInteractions: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const buttons = await canvas.findAllByRole('button');
const dotsButton = buttons[1];
userEvent.hover(dotsButton);
await waitFor(() => {
expect(dotsButton).toBeVisible();
});
userEvent.click(dotsButton);
const dropdownCanvas = within(getCanvasElementForDropdownTesting());
await waitFor(() => {
expect(dropdownCanvas.getByText('Filter')).toBeVisible();
});
userEvent.click(canvasElement);
await waitFor(() => {
expect(dropdownCanvas.queryByText('Filter')).not.toBeInTheDocument();
});
userEvent.click(dotsButton);
await waitFor(() => {
expect(dropdownCanvas.getByText('Filter')).toBeVisible();
});
},
};