Add workflow filters on diagram (#12974)
> [!NOTE] > The new behavior is hidden behind a feature flag. ## Before https://github.com/user-attachments/assets/30c6d001-d9c8-4006-b577-e4e450d58b12 ## After https://github.com/user-attachments/assets/79446976-4508-41d2-8044-4078f67c02e0
This commit is contained in:
committed by
GitHub
parent
7756b472a4
commit
34e9e7d836
@ -697,7 +697,8 @@ export enum FeatureFlagKey {
|
|||||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||||
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
|
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
|
||||||
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
|
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
|
||||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
|
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
|
||||||
|
IS_WORKFLOW_FILTERING_ENABLED = 'IS_WORKFLOW_FILTERING_ENABLED'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Field = {
|
export type Field = {
|
||||||
|
|||||||
@ -653,7 +653,8 @@ export enum FeatureFlagKey {
|
|||||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||||
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
|
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
|
||||||
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
|
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
|
||||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
|
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
|
||||||
|
IS_WORKFLOW_FILTERING_ENABLED = 'IS_WORKFLOW_FILTERING_ENABLED'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Field = {
|
export type Field = {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta
|
|||||||
import { WorkflowDiagramCustomMarkers } from '@/workflow/workflow-diagram/components/WorkflowDiagramCustomMarkers';
|
import { WorkflowDiagramCustomMarkers } from '@/workflow/workflow-diagram/components/WorkflowDiagramCustomMarkers';
|
||||||
import { useRightDrawerState } from '@/workflow/workflow-diagram/hooks/useRightDrawerState';
|
import { useRightDrawerState } from '@/workflow/workflow-diagram/hooks/useRightDrawerState';
|
||||||
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||||
|
import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramPanOnDragComponentState';
|
||||||
import { workflowDiagramWaitingNodesDimensionsComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramWaitingNodesDimensionsComponentState';
|
import { workflowDiagramWaitingNodesDimensionsComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramWaitingNodesDimensionsComponentState';
|
||||||
import {
|
import {
|
||||||
WorkflowDiagram,
|
WorkflowDiagram,
|
||||||
@ -132,6 +133,9 @@ export const WorkflowDiagramCanvasBase = ({
|
|||||||
const workflowDiagram = useRecoilComponentValueV2(
|
const workflowDiagram = useRecoilComponentValueV2(
|
||||||
workflowDiagramComponentState,
|
workflowDiagramComponentState,
|
||||||
);
|
);
|
||||||
|
const workflowDiagramPanOnDrag = useRecoilComponentValueV2(
|
||||||
|
workflowDiagramPanOnDragComponentState,
|
||||||
|
);
|
||||||
const workflowDiagramState = useRecoilComponentCallbackStateV2(
|
const workflowDiagramState = useRecoilComponentCallbackStateV2(
|
||||||
workflowDiagramComponentState,
|
workflowDiagramComponentState,
|
||||||
);
|
);
|
||||||
@ -383,6 +387,7 @@ export const WorkflowDiagramCanvasBase = ({
|
|||||||
nodesFocusable={false}
|
nodesFocusable={false}
|
||||||
edgesFocusable={false}
|
edgesFocusable={false}
|
||||||
nodesDraggable={false}
|
nodesDraggable={false}
|
||||||
|
panOnDrag={workflowDiagramPanOnDrag}
|
||||||
nodesConnectable={false}
|
nodesConnectable={false}
|
||||||
paneClickDistance={10} // Fix small unwanted user dragging does not select node
|
paneClickDistance={10} // Fix small unwanted user dragging does not select node
|
||||||
preventScrolling={false}
|
preventScrolling={false}
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
import { WorkflowDiagramEdgeV1 } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV1';
|
||||||
import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react';
|
import { WorkflowDiagramEdgeV2 } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2';
|
||||||
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||||
import { WorkflowDiagramEdgeOptions } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeOptions';
|
|
||||||
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
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 { FeatureFlagKey } from '~/generated/graphql';
|
||||||
|
|
||||||
type WorkflowDiagramDefaultEdgeProps = EdgeProps<WorkflowDiagramEdge>;
|
type WorkflowDiagramDefaultEdgeProps = EdgeProps<WorkflowDiagramEdge>;
|
||||||
|
|
||||||
@ -17,6 +25,10 @@ export const WorkflowDiagramDefaultEdge = ({
|
|||||||
}: WorkflowDiagramDefaultEdgeProps) => {
|
}: WorkflowDiagramDefaultEdgeProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const isWorkflowFilteringEnabled = useIsFeatureEnabled(
|
||||||
|
FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED,
|
||||||
|
);
|
||||||
|
|
||||||
const [edgePath, labelX, labelY] = getStraightPath({
|
const [edgePath, labelX, labelY] = getStraightPath({
|
||||||
sourceX: CREATE_STEP_NODE_WIDTH,
|
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||||
sourceY,
|
sourceY,
|
||||||
@ -33,12 +45,22 @@ export const WorkflowDiagramDefaultEdge = ({
|
|||||||
style={{ stroke: theme.border.color.strong }}
|
style={{ stroke: theme.border.color.strong }}
|
||||||
/>
|
/>
|
||||||
{data?.shouldDisplayEdgeOptions && (
|
{data?.shouldDisplayEdgeOptions && (
|
||||||
<WorkflowDiagramEdgeOptions
|
<EdgeLabelRenderer>
|
||||||
labelX={labelX}
|
{isWorkflowFilteringEnabled ? (
|
||||||
labelY={labelY}
|
<WorkflowDiagramEdgeV2
|
||||||
parentStepId={source}
|
labelX={labelX}
|
||||||
nextStepId={target}
|
labelY={labelY}
|
||||||
/>
|
parentStepId={source}
|
||||||
|
nextStepId={target}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<WorkflowDiagramEdgeV1
|
||||||
|
labelY={labelY}
|
||||||
|
parentStepId={source}
|
||||||
|
nextStepId={target}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</EdgeLabelRenderer>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
|
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 { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||||
|
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { EdgeLabelRenderer } from '@xyflow/react';
|
import { useState } from 'react';
|
||||||
import { IconPlus } from 'twenty-ui/display';
|
import { IconPlus } from 'twenty-ui/display';
|
||||||
import { IconButtonGroup } from 'twenty-ui/input';
|
import { IconButtonGroup } from 'twenty-ui/input';
|
||||||
import { useState } from 'react';
|
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
|
||||||
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
|
||||||
|
|
||||||
const StyledIconButtonGroup = styled(IconButtonGroup)`
|
const StyledIconButtonGroup = styled(IconButtonGroup)`
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
@ -32,18 +31,17 @@ const StyledWrapper = styled.div`
|
|||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type WorkflowDiagramEdgeOptionsProps = {
|
type WorkflowDiagramEdgeV1Props = {
|
||||||
labelX?: number;
|
|
||||||
labelY?: number;
|
labelY?: number;
|
||||||
parentStepId: string;
|
parentStepId: string;
|
||||||
nextStepId: string;
|
nextStepId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkflowDiagramEdgeOptions = ({
|
export const WorkflowDiagramEdgeV1 = ({
|
||||||
labelY,
|
labelY,
|
||||||
parentStepId,
|
parentStepId,
|
||||||
nextStepId,
|
nextStepId,
|
||||||
}: WorkflowDiagramEdgeOptionsProps) => {
|
}: WorkflowDiagramEdgeV1Props) => {
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
|
|
||||||
const { startNodeCreation } = useStartNodeCreation();
|
const { startNodeCreation } = useStartNodeCreation();
|
||||||
@ -57,31 +55,29 @@ export const WorkflowDiagramEdgeOptions = ({
|
|||||||
workflowInsertStepIds.nextStepId === nextStepId;
|
workflowInsertStepIds.nextStepId === nextStepId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EdgeLabelRenderer>
|
<StyledContainer
|
||||||
<StyledContainer
|
labelY={labelY}
|
||||||
labelY={labelY}
|
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||||
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
>
|
||||||
|
<StyledWrapper
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
>
|
>
|
||||||
<StyledWrapper
|
<StyledHoverZone />
|
||||||
onMouseEnter={() => setHovered(true)}
|
{(hovered || isSelected) && (
|
||||||
onMouseLeave={() => setHovered(false)}
|
<StyledIconButtonGroup
|
||||||
>
|
className="nodrag nopan"
|
||||||
<StyledHoverZone />
|
iconButtons={[
|
||||||
{(hovered || isSelected) && (
|
{
|
||||||
<StyledIconButtonGroup
|
Icon: IconPlus,
|
||||||
className="nodrag nopan"
|
onClick: () => {
|
||||||
iconButtons={[
|
startNodeCreation({ parentStepId, nextStepId });
|
||||||
{
|
|
||||||
Icon: IconPlus,
|
|
||||||
onClick: () => {
|
|
||||||
startNodeCreation({ parentStepId, nextStepId });
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]}
|
},
|
||||||
/>
|
]}
|
||||||
)}
|
/>
|
||||||
</StyledWrapper>
|
)}
|
||||||
</StyledContainer>
|
</StyledWrapper>
|
||||||
</EdgeLabelRenderer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -0,0 +1,164 @@
|
|||||||
|
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 { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||||
|
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 { 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 = {
|
||||||
|
labelX: number;
|
||||||
|
labelY: number;
|
||||||
|
parentStepId: string;
|
||||||
|
nextStepId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkflowDiagramEdgeV2 = ({
|
||||||
|
labelX,
|
||||||
|
labelY,
|
||||||
|
parentStepId,
|
||||||
|
nextStepId,
|
||||||
|
}: WorkflowDiagramEdgeV2Props) => {
|
||||||
|
const { openDropdown } = useOpenDropdown();
|
||||||
|
const { closeDropdown } = useCloseDropdown();
|
||||||
|
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 (
|
||||||
|
<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}
|
||||||
|
>
|
||||||
|
<StyledIconButtonGroup
|
||||||
|
className="nodrag nopan"
|
||||||
|
iconButtons={[
|
||||||
|
{
|
||||||
|
Icon: IconFilterPlus,
|
||||||
|
onClick: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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={() => {}}
|
||||||
|
/>
|
||||||
|
<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,138 @@
|
|||||||
|
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { expect, 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 { WorkflowDiagramEdgeV2 } from '../WorkflowDiagramEdgeV2';
|
||||||
|
|
||||||
|
const meta: Meta<typeof WorkflowDiagramEdgeV2> = {
|
||||||
|
title: 'Modules/Workflow/WorkflowDiagramEdgeV2',
|
||||||
|
component: WorkflowDiagramEdgeV2,
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof WorkflowDiagramEdgeV2>;
|
||||||
|
|
||||||
|
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 }) => {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// TODO: Assert we created a filter
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddNodeAction: 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 addNodeButton = await within(
|
||||||
|
getCanvasElementForDropdownTesting(),
|
||||||
|
).findByText('Add Node');
|
||||||
|
|
||||||
|
userEvent.click(addNodeButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(canvas.queryByText('Add Node')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||||
|
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
||||||
|
|
||||||
|
export const workflowDiagramPanOnDragComponentState =
|
||||||
|
createComponentStateV2<boolean>({
|
||||||
|
key: 'workflowDiagramPanOnDragComponentState',
|
||||||
|
defaultValue: true,
|
||||||
|
componentInstanceContext: WorkflowVisualizerComponentInstanceContext,
|
||||||
|
});
|
||||||
@ -6,4 +6,5 @@ export enum FeatureFlagKey {
|
|||||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||||
IS_IMAP_ENABLED = 'IS_IMAP_ENABLED',
|
IS_IMAP_ENABLED = 'IS_IMAP_ENABLED',
|
||||||
|
IS_WORKFLOW_FILTERING_ENABLED = 'IS_WORKFLOW_FILTERING_ENABLED',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,15 @@ describe('featureFlagValidator', () => {
|
|||||||
).not.toThrow();
|
).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not throw error for new workflow filtering feature flag', () => {
|
||||||
|
expect(() =>
|
||||||
|
featureFlagValidator.assertIsFeatureFlagKey(
|
||||||
|
'IS_WORKFLOW_FILTERING_ENABLED',
|
||||||
|
new CustomException('Error', 'Error'),
|
||||||
|
),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw error if featureFlagKey is invalid', () => {
|
it('should throw error if featureFlagKey is invalid', () => {
|
||||||
const invalidKey = 'InvalidKey';
|
const invalidKey = 'InvalidKey';
|
||||||
const exception = new CustomException('Error', 'Error');
|
const exception = new CustomException('Error', 'Error');
|
||||||
|
|||||||
@ -40,6 +40,11 @@ export const seedFeatureFlags = async (
|
|||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: FeatureFlagKey.IS_IMAP_ENABLED,
|
key: FeatureFlagKey.IS_IMAP_ENABLED,
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
|
|||||||
@ -142,6 +142,8 @@ export {
|
|||||||
IconFilter,
|
IconFilter,
|
||||||
IconFilterCog,
|
IconFilterCog,
|
||||||
IconFilterOff,
|
IconFilterOff,
|
||||||
|
IconFilterPlus,
|
||||||
|
IconFilterX,
|
||||||
IconFlag,
|
IconFlag,
|
||||||
IconFlask,
|
IconFlask,
|
||||||
IconFocusCentered,
|
IconFocusCentered,
|
||||||
@ -152,6 +154,7 @@ export {
|
|||||||
IconForbid,
|
IconForbid,
|
||||||
IconFunction,
|
IconFunction,
|
||||||
IconGauge,
|
IconGauge,
|
||||||
|
IconGitBranchDeleted,
|
||||||
IconGitCommit,
|
IconGitCommit,
|
||||||
IconGripVertical,
|
IconGripVertical,
|
||||||
IconH1,
|
IconH1,
|
||||||
|
|||||||
@ -204,6 +204,8 @@ export {
|
|||||||
IconFilter,
|
IconFilter,
|
||||||
IconFilterCog,
|
IconFilterCog,
|
||||||
IconFilterOff,
|
IconFilterOff,
|
||||||
|
IconFilterPlus,
|
||||||
|
IconFilterX,
|
||||||
IconFlag,
|
IconFlag,
|
||||||
IconFlask,
|
IconFlask,
|
||||||
IconFocusCentered,
|
IconFocusCentered,
|
||||||
@ -214,6 +216,7 @@ export {
|
|||||||
IconForbid,
|
IconForbid,
|
||||||
IconFunction,
|
IconFunction,
|
||||||
IconGauge,
|
IconGauge,
|
||||||
|
IconGitBranchDeleted,
|
||||||
IconGitCommit,
|
IconGitCommit,
|
||||||
IconGripVertical,
|
IconGripVertical,
|
||||||
IconH1,
|
IconH1,
|
||||||
|
|||||||
Reference in New Issue
Block a user