Update filter design (#13243)
In this PR: - Adjust the edges to match the new Figma design - Properly display the filters for workflows and workflow versions (replaced shouldDisplayEdgeOptions with isEdgeEditable as we want to display configured filters on workflow versions, but want to disallow editing them) - Wrote a few tests to make coverage pass https://github.com/user-attachments/assets/d303d338-1938-4efe-b489-5a530d65fb30
This commit is contained in:
committed by
GitHub
parent
fca39d317f
commit
0752f24638
@ -1,5 +1,6 @@
|
||||
import { WorkflowDiagramEdgeV1 } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV1';
|
||||
import { WorkflowDiagramEdgeV2 } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2';
|
||||
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';
|
||||
@ -10,6 +11,7 @@ import {
|
||||
EdgeProps,
|
||||
getStraightPath,
|
||||
} from '@xyflow/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
type WorkflowDiagramDefaultEdgeProps = EdgeProps<WorkflowDiagramEdge>;
|
||||
@ -36,6 +38,18 @@ export const WorkflowDiagramDefaultEdge = ({
|
||||
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
|
||||
@ -44,26 +58,35 @@ export const WorkflowDiagramDefaultEdge = ({
|
||||
path={edgePath}
|
||||
style={{ stroke: theme.border.color.strong }}
|
||||
/>
|
||||
{data?.shouldDisplayEdgeOptions && (
|
||||
<EdgeLabelRenderer>
|
||||
{isWorkflowFilteringEnabled ? (
|
||||
<WorkflowDiagramEdgeV2
|
||||
labelX={labelX}
|
||||
labelY={labelY}
|
||||
stepId={data.stepId}
|
||||
parentStepId={source}
|
||||
nextStepId={target}
|
||||
filter={data.filter}
|
||||
/>
|
||||
) : (
|
||||
<WorkflowDiagramEdgeV1
|
||||
labelY={labelY}
|
||||
parentStepId={source}
|
||||
nextStepId={target}
|
||||
/>
|
||||
)}
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
|
||||
<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,13 @@
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
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;
|
||||
`;
|
||||
|
||||
export { StyledContainer as WorkflowDiagramEdgeV2Container };
|
||||
@ -2,29 +2,23 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined';
|
||||
import { WorkflowDiagramEdgeV2Content } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Content';
|
||||
import { WorkflowDiagramEdgeV2EmptyContent } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2EmptyContent';
|
||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
|
||||
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type WorkflowDiagramEdgeV2Props = {
|
||||
type WorkflowDiagramEdgeV2EmptyProps = {
|
||||
labelX: number;
|
||||
labelY: number;
|
||||
stepId: string | undefined;
|
||||
parentStepId: string;
|
||||
nextStepId: string;
|
||||
filter: Record<string, any> | undefined;
|
||||
};
|
||||
|
||||
export const WorkflowDiagramEdgeV2 = ({
|
||||
export const WorkflowDiagramEdgeV2Empty = ({
|
||||
labelX,
|
||||
labelY,
|
||||
stepId,
|
||||
parentStepId,
|
||||
nextStepId,
|
||||
filter,
|
||||
}: WorkflowDiagramEdgeV2Props) => {
|
||||
}: WorkflowDiagramEdgeV2EmptyProps) => {
|
||||
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
|
||||
workflowVisualizerWorkflowIdComponentState,
|
||||
);
|
||||
@ -32,17 +26,14 @@ export const WorkflowDiagramEdgeV2 = ({
|
||||
assertWorkflowWithCurrentVersionIsDefined(workflow);
|
||||
|
||||
const { createStep } = useCreateStep({ workflow });
|
||||
const { deleteStep } = useDeleteStep({ workflow });
|
||||
const { startNodeCreation } = useStartNodeCreation();
|
||||
|
||||
return (
|
||||
<WorkflowDiagramEdgeV2Content
|
||||
<WorkflowDiagramEdgeV2EmptyContent
|
||||
labelX={labelX}
|
||||
labelY={labelY}
|
||||
stepId={stepId}
|
||||
parentStepId={parentStepId}
|
||||
nextStepId={nextStepId}
|
||||
filter={filter}
|
||||
onCreateFilter={() => {
|
||||
return createStep({
|
||||
newStepType: 'FILTER',
|
||||
@ -50,21 +41,8 @@ export const WorkflowDiagramEdgeV2 = ({
|
||||
nextStepId,
|
||||
});
|
||||
}}
|
||||
onDeleteFilter={() => {
|
||||
if (!isDefined(stepId)) {
|
||||
throw new Error(
|
||||
'Step ID must be configured for the edge when rendering a filter',
|
||||
);
|
||||
}
|
||||
|
||||
return deleteStep(stepId);
|
||||
}}
|
||||
onCreateNode={() => {
|
||||
if (isDefined(filter)) {
|
||||
startNodeCreation({ parentStepId: stepId, nextStepId });
|
||||
} else {
|
||||
startNodeCreation({ parentStepId, nextStepId });
|
||||
}
|
||||
startNodeCreation({ parentStepId, nextStepId });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -0,0 +1,87 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,60 @@
|
||||
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 });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -9,11 +9,13 @@ import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDrop
|
||||
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 { css } from '@emotion/react';
|
||||
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';
|
||||
@ -21,9 +23,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconDotsVertical,
|
||||
IconFilter,
|
||||
IconFilterPlus,
|
||||
IconFilterX,
|
||||
IconGitBranchDeleted,
|
||||
IconPlus,
|
||||
} from 'twenty-ui/display';
|
||||
import { IconButtonGroup } from 'twenty-ui/input';
|
||||
@ -33,49 +33,33 @@ const StyledIconButtonGroup = styled(IconButtonGroup)`
|
||||
pointer-events: all;
|
||||
`;
|
||||
|
||||
const StyledRoundedIconButtonGroup = styled(IconButtonGroup)`
|
||||
border-radius: 50px;
|
||||
overflow: hidden;
|
||||
pointer-events: all;
|
||||
const StyledConfiguredFilterContainer = styled.div`
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
`;
|
||||
|
||||
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 = {
|
||||
type WorkflowDiagramEdgeV2FilterContentProps = {
|
||||
labelX: number;
|
||||
labelY: number;
|
||||
stepId: string | undefined;
|
||||
stepId: string;
|
||||
parentStepId: string;
|
||||
nextStepId: string;
|
||||
filter: Record<string, any> | undefined;
|
||||
onCreateFilter: () => Promise<void>;
|
||||
filterSettings: FilterSettings;
|
||||
onDeleteFilter: () => Promise<void>;
|
||||
onCreateNode: () => void;
|
||||
isEdgeEditable: boolean;
|
||||
};
|
||||
|
||||
export const WorkflowDiagramEdgeV2Content = ({
|
||||
export const WorkflowDiagramEdgeV2FilterContent = ({
|
||||
labelX,
|
||||
labelY,
|
||||
stepId,
|
||||
parentStepId,
|
||||
nextStepId,
|
||||
filter,
|
||||
onCreateFilter,
|
||||
onDeleteFilter,
|
||||
onCreateNode,
|
||||
}: WorkflowDiagramEdgeV2ContentProps) => {
|
||||
isEdgeEditable,
|
||||
}: WorkflowDiagramEdgeV2FilterContentProps) => {
|
||||
const { openDropdown } = useOpenDropdown();
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
@ -108,71 +92,74 @@ export const WorkflowDiagramEdgeV2Content = ({
|
||||
|
||||
const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu();
|
||||
|
||||
const handleCreateFilter = async () => {
|
||||
await onCreateFilter();
|
||||
|
||||
closeDropdown(dropdownId);
|
||||
setHovered(false);
|
||||
};
|
||||
|
||||
const setWorkflowSelectedNode = useSetRecoilComponentStateV2(
|
||||
workflowSelectedNodeComponentState,
|
||||
);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!isEdgeEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setHovered(false);
|
||||
};
|
||||
|
||||
const handleFilterButtonClick = () => {
|
||||
setWorkflowSelectedNode(stepId);
|
||||
if (isDefined(filter) && isDefined(workflowVisualizerWorkflowId)) {
|
||||
|
||||
if (isDefined(workflowVisualizerWorkflowId)) {
|
||||
openWorkflowEditStepInCommandMenu(
|
||||
workflowVisualizerWorkflowId,
|
||||
'Filter',
|
||||
IconFilter,
|
||||
);
|
||||
} else {
|
||||
handleCreateFilter();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
<WorkflowDiagramEdgeV2Container
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||
labelX={labelX}
|
||||
labelY={labelY}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<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: handleFilterButtonClick,
|
||||
},
|
||||
{
|
||||
Icon: IconDotsVertical,
|
||||
onClick: () => {
|
||||
openDropdown({
|
||||
dropdownComponentInstanceIdFromProps: dropdownId,
|
||||
});
|
||||
<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}
|
||||
@ -181,7 +168,7 @@ export const WorkflowDiagramEdgeV2Content = ({
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownStrategy="absolute"
|
||||
dropdownOffset={{
|
||||
x: 0,
|
||||
x: 24,
|
||||
y: 4,
|
||||
}}
|
||||
onOpen={() => {
|
||||
@ -196,7 +183,12 @@ export const WorkflowDiagramEdgeV2Content = ({
|
||||
<MenuItem
|
||||
text="Filter"
|
||||
LeftIcon={IconFilter}
|
||||
onClick={() => {}}
|
||||
onClick={() => {
|
||||
closeDropdown(dropdownId);
|
||||
setHovered(false);
|
||||
|
||||
handleFilterButtonClick();
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
text="Remove Filter"
|
||||
@ -218,16 +210,11 @@ export const WorkflowDiagramEdgeV2Content = ({
|
||||
onCreateNode();
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
text="Delete branch"
|
||||
LeftIcon={IconGitBranchDeleted}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
</StyledOpacityOverlay>
|
||||
</StyledContainer>
|
||||
</WorkflowDiagramEdgeV2VisibilityContainer>
|
||||
</WorkflowDiagramEdgeV2Container>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledContainer = styled.div<{ shouldDisplay: boolean }>`
|
||||
opacity: ${({ shouldDisplay }) => (shouldDisplay ? 1 : 0)};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export { StyledContainer as WorkflowDiagramEdgeV2VisibilityContainer };
|
||||
@ -0,0 +1,94 @@
|
||||
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);
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -7,11 +7,11 @@ import {
|
||||
getCanvasElementForDropdownTesting,
|
||||
} from 'twenty-ui/testing';
|
||||
import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
|
||||
import { WorkflowDiagramEdgeV2Content } from '../WorkflowDiagramEdgeV2Content';
|
||||
import { WorkflowDiagramEdgeV2FilterContent } from '../WorkflowDiagramEdgeV2FilterContent';
|
||||
|
||||
const meta: Meta<typeof WorkflowDiagramEdgeV2Content> = {
|
||||
title: 'Modules/Workflow/WorkflowDiagramEdgeV2Content',
|
||||
component: WorkflowDiagramEdgeV2Content,
|
||||
const meta: Meta<typeof WorkflowDiagramEdgeV2FilterContent> = {
|
||||
title: 'Modules/Workflow/WorkflowDiagramEdgeV2FilterContent',
|
||||
component: WorkflowDiagramEdgeV2FilterContent,
|
||||
decorators: [
|
||||
ComponentDecorator,
|
||||
ReactflowDecorator,
|
||||
@ -35,14 +35,13 @@ const meta: Meta<typeof WorkflowDiagramEdgeV2Content> = {
|
||||
labelY: 0,
|
||||
parentStepId: 'parent-step-id',
|
||||
nextStepId: 'next-step-id',
|
||||
onCreateFilter: fn(),
|
||||
onDeleteFilter: fn(),
|
||||
onCreateNode: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkflowDiagramEdgeV2Content>;
|
||||
type Story = StoryObj<typeof WorkflowDiagramEdgeV2FilterContent>;
|
||||
|
||||
export const ButtonsAppearOnHover: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
@ -59,27 +58,6 @@ export const ButtonsAppearOnHover: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
@ -7,4 +7,8 @@ export const WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION = {
|
||||
markerEnd: EDGE_ROUNDED_ARROW_MARKER_ID,
|
||||
deletable: false,
|
||||
selectable: false,
|
||||
data: {
|
||||
edgeType: 'default',
|
||||
isEdgeEditable: false,
|
||||
},
|
||||
} satisfies Partial<WorkflowDiagramEdge>;
|
||||
|
||||
@ -3,18 +3,18 @@ import {
|
||||
WorkflowRunStepStatus,
|
||||
WorkflowTriggerType,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
|
||||
import { Edge, Node } from '@xyflow/react';
|
||||
|
||||
export type WorkflowDiagramStepNode = Node<WorkflowDiagramStepNodeData>;
|
||||
export type WorkflowDiagramNode = Node<WorkflowDiagramNodeData>;
|
||||
export type WorkflowDiagramEdge = Edge<EdgeData>;
|
||||
export type WorkflowDiagramEdge = Edge<WorkflowDiagramEdgeData>;
|
||||
|
||||
export type WorkflowRunDiagramNode = Node<WorkflowRunDiagramNodeData>;
|
||||
export type WorkflowRunDiagramEdge = Edge<EdgeData>;
|
||||
|
||||
export type WorkflowRunDiagram = {
|
||||
nodes: Array<WorkflowRunDiagramNode>;
|
||||
edges: Array<WorkflowRunDiagramEdge>;
|
||||
edges: Array<WorkflowDiagramEdge>;
|
||||
};
|
||||
|
||||
export type WorkflowDiagram = {
|
||||
@ -63,12 +63,24 @@ export type WorkflowRunDiagramNodeData = Exclude<
|
||||
'runStatus'
|
||||
> & { runStatus: WorkflowRunStepStatus };
|
||||
|
||||
export type EdgeData = {
|
||||
stepId?: string;
|
||||
filter?: Record<string, any>;
|
||||
shouldDisplayEdgeOptions?: boolean;
|
||||
export type WorkflowDiagramFilterEdgeData = {
|
||||
edgeType: 'filter';
|
||||
stepId: string;
|
||||
filterSettings: FilterSettings;
|
||||
name: string;
|
||||
runStatus?: WorkflowRunStepStatus;
|
||||
isEdgeEditable: boolean;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramDefaultEdgeData = {
|
||||
edgeType: 'default';
|
||||
isEdgeEditable: boolean;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramEdgeData =
|
||||
| WorkflowDiagramFilterEdgeData
|
||||
| WorkflowDiagramDefaultEdgeData;
|
||||
|
||||
export type WorkflowDiagramNodeType =
|
||||
| 'default'
|
||||
| 'empty-trigger'
|
||||
|
||||
@ -2,7 +2,7 @@ import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagr
|
||||
import { addEdgeOptions } from '../addEdgeOptions';
|
||||
|
||||
describe('addEdgeOptions', () => {
|
||||
it('should add shouldDisplayEdgeOptions to all edges', () => {
|
||||
it('should add isEdgeEditable to all edges', () => {
|
||||
const diagram: WorkflowDiagram = {
|
||||
nodes: [
|
||||
{
|
||||
@ -31,14 +31,18 @@ describe('addEdgeOptions', () => {
|
||||
source: 'trigger',
|
||||
target: 'action-1',
|
||||
data: {
|
||||
shouldDisplayEdgeOptions: true,
|
||||
edgeType: 'default',
|
||||
isEdgeEditable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'edge-2',
|
||||
source: 'action-1',
|
||||
target: 'action-2',
|
||||
data: {},
|
||||
data: {
|
||||
edgeType: 'default',
|
||||
isEdgeEditable: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -53,7 +57,8 @@ describe('addEdgeOptions', () => {
|
||||
source: 'trigger',
|
||||
target: 'action-1',
|
||||
data: {
|
||||
shouldDisplayEdgeOptions: true,
|
||||
edgeType: 'default',
|
||||
isEdgeEditable: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -62,7 +67,8 @@ describe('addEdgeOptions', () => {
|
||||
source: 'action-1',
|
||||
target: 'action-2',
|
||||
data: {
|
||||
shouldDisplayEdgeOptions: true,
|
||||
edgeType: 'default',
|
||||
isEdgeEditable: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -122,15 +128,6 @@ describe('addEdgeOptions', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const result = addEdgeOptions(diagram);
|
||||
|
||||
expect(result.edges[0]).toEqual({
|
||||
id: 'edge-1',
|
||||
source: 'trigger',
|
||||
target: 'action-1',
|
||||
data: {
|
||||
shouldDisplayEdgeOptions: true,
|
||||
},
|
||||
});
|
||||
expect(() => addEdgeOptions(diagram)).toThrow('Edge data must be defined');
|
||||
});
|
||||
});
|
||||
|
||||
@ -107,6 +107,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"diagram": {
|
||||
"edges": [
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-0",
|
||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||
@ -117,6 +121,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"type": "success",
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-1",
|
||||
"markerEnd": "workflow-edge-arrow-rounded",
|
||||
@ -126,6 +134,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"target": "step2",
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-2",
|
||||
"markerEnd": "workflow-edge-arrow-rounded",
|
||||
@ -296,6 +308,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"diagram": {
|
||||
"edges": [
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-3",
|
||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||
@ -306,6 +322,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"type": "success",
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-4",
|
||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||
@ -316,6 +336,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"type": "success",
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-5",
|
||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||
@ -487,6 +511,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"diagram": {
|
||||
"edges": [
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-6",
|
||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||
@ -497,6 +525,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"type": "success",
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-7",
|
||||
"markerEnd": "workflow-edge-arrow-rounded",
|
||||
@ -506,6 +538,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"target": "step2",
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-8",
|
||||
"markerEnd": "workflow-edge-arrow-rounded",
|
||||
@ -695,6 +731,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"diagram": {
|
||||
"edges": [
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-9",
|
||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||
@ -705,6 +745,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"type": "success",
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-10",
|
||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||
@ -715,6 +759,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"type": "success",
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-11",
|
||||
"markerEnd": "workflow-edge-arrow-rounded",
|
||||
@ -724,6 +772,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"target": "step3",
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-12",
|
||||
"markerEnd": "workflow-edge-arrow-rounded",
|
||||
@ -873,6 +925,10 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"diagram": {
|
||||
"edges": [
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-13",
|
||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||
|
||||
@ -129,6 +129,10 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
{
|
||||
"edges": [
|
||||
{
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
"isEdgeEditable": false,
|
||||
},
|
||||
"deletable": false,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-0",
|
||||
"markerEnd": "workflow-edge-arrow-rounded",
|
||||
|
||||
@ -25,7 +25,10 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
id: 'A-C',
|
||||
source: 'A',
|
||||
target: 'C',
|
||||
data: { stepId: 'A', shouldDisplayEdgeOptions: true },
|
||||
data: {
|
||||
edgeType: 'default',
|
||||
isEdgeEditable: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -64,13 +67,13 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
id: 'A-B',
|
||||
source: 'A',
|
||||
target: 'B',
|
||||
data: { stepId: 'A', shouldDisplayEdgeOptions: true },
|
||||
data: { edgeType: 'default', isEdgeEditable: true },
|
||||
},
|
||||
{
|
||||
id: 'B-C',
|
||||
source: 'B',
|
||||
target: 'C',
|
||||
data: { stepId: 'B', shouldDisplayEdgeOptions: true },
|
||||
data: { edgeType: 'default', isEdgeEditable: true },
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -98,9 +101,12 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
source: 'A',
|
||||
target: 'C',
|
||||
data: {
|
||||
shouldDisplayEdgeOptions: true,
|
||||
edgeType: 'filter',
|
||||
stepId: 'B',
|
||||
filter: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B' },
|
||||
name: 'Filter B',
|
||||
runStatus: undefined,
|
||||
filterSettings: {},
|
||||
isEdgeEditable: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -147,25 +153,25 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
id: 'A-B1',
|
||||
source: 'A',
|
||||
target: 'B1',
|
||||
data: { stepId: 'A', shouldDisplayEdgeOptions: true },
|
||||
data: { edgeType: 'default', isEdgeEditable: true },
|
||||
},
|
||||
{
|
||||
id: 'B1-C',
|
||||
source: 'B1',
|
||||
target: 'C',
|
||||
data: { stepId: 'B1', shouldDisplayEdgeOptions: true },
|
||||
data: { edgeType: 'default', isEdgeEditable: true },
|
||||
},
|
||||
{
|
||||
id: 'C-B2',
|
||||
source: 'C',
|
||||
target: 'B2',
|
||||
data: { stepId: 'C', shouldDisplayEdgeOptions: true },
|
||||
data: { edgeType: 'default', isEdgeEditable: true },
|
||||
},
|
||||
{
|
||||
id: 'B2-D',
|
||||
source: 'B2',
|
||||
target: 'D',
|
||||
data: { stepId: 'B2', shouldDisplayEdgeOptions: true },
|
||||
data: { edgeType: 'default', isEdgeEditable: true },
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -189,9 +195,12 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
source: 'A',
|
||||
target: 'C',
|
||||
data: {
|
||||
edgeType: 'filter',
|
||||
name: 'Filter B1',
|
||||
runStatus: undefined,
|
||||
stepId: 'B1',
|
||||
shouldDisplayEdgeOptions: true,
|
||||
filter: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B1' },
|
||||
filterSettings: {},
|
||||
isEdgeEditable: false,
|
||||
},
|
||||
});
|
||||
|
||||
@ -203,9 +212,12 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
source: 'C',
|
||||
target: 'D',
|
||||
data: {
|
||||
edgeType: 'filter',
|
||||
name: 'Filter B2',
|
||||
runStatus: undefined,
|
||||
stepId: 'B2',
|
||||
shouldDisplayEdgeOptions: true,
|
||||
filter: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B2' },
|
||||
filterSettings: {},
|
||||
isEdgeEditable: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -229,7 +241,7 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
id: 'A-B',
|
||||
source: 'A',
|
||||
target: 'B',
|
||||
data: { stepId: 'A', shouldDisplayEdgeOptions: true },
|
||||
data: { edgeType: 'default', isEdgeEditable: true },
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -281,13 +293,13 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
id: 'trigger-B',
|
||||
source: 'trigger',
|
||||
target: 'B',
|
||||
data: { stepId: 'trigger', shouldDisplayEdgeOptions: true },
|
||||
data: { edgeType: 'default', isEdgeEditable: true },
|
||||
},
|
||||
{
|
||||
id: 'B-C',
|
||||
source: 'B',
|
||||
target: 'C',
|
||||
data: { stepId: 'B', shouldDisplayEdgeOptions: true },
|
||||
data: { edgeType: 'default', isEdgeEditable: true },
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -319,13 +331,12 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
source: 'trigger',
|
||||
target: 'C',
|
||||
data: {
|
||||
edgeType: 'filter',
|
||||
name: 'Filter B',
|
||||
runStatus: undefined,
|
||||
stepId: 'B',
|
||||
shouldDisplayEdgeOptions: true,
|
||||
filter: {
|
||||
nodeType: 'action',
|
||||
actionType: 'FILTER',
|
||||
name: 'Filter B',
|
||||
},
|
||||
filterSettings: {},
|
||||
isEdgeEditable: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const addEdgeOptions = ({
|
||||
nodes,
|
||||
@ -7,11 +8,15 @@ export const addEdgeOptions = ({
|
||||
return {
|
||||
nodes,
|
||||
edges: edges.map((edge) => {
|
||||
if (!isDefined(edge.data)) {
|
||||
throw new Error('Edge data must be defined');
|
||||
}
|
||||
|
||||
return {
|
||||
...edge,
|
||||
data: {
|
||||
...edge.data,
|
||||
shouldDisplayEdgeOptions: true,
|
||||
isEdgeEditable: true,
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import {
|
||||
WorkflowDiagram,
|
||||
WorkflowDiagramEdge,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const transformFilterNodesAsEdges = ({
|
||||
@ -29,14 +32,25 @@ export const transformFilterNodesAsEdges = ({
|
||||
const outgoingEdge = edges.find((edge) => edge.source === filterNode.id);
|
||||
|
||||
if (isDefined(incomingEdge) && isDefined(outgoingEdge)) {
|
||||
const newEdge = {
|
||||
if (
|
||||
filterNode.data.nodeType !== 'action' ||
|
||||
filterNode.data.actionType !== 'FILTER'
|
||||
) {
|
||||
throw new Error('Expected the filter node to be of action type');
|
||||
}
|
||||
|
||||
const newEdge: WorkflowDiagramEdge = {
|
||||
...incomingEdge,
|
||||
id: `${incomingEdge.source}-${outgoingEdge.target}-filter-${filterNode.id}`,
|
||||
target: outgoingEdge.target,
|
||||
data: {
|
||||
...incomingEdge.data,
|
||||
edgeType: 'filter',
|
||||
stepId: filterNode.id,
|
||||
filter: filterNode.data,
|
||||
// TODO: Get the filter settings from the filter node
|
||||
filterSettings: {},
|
||||
name: filterNode.data.name,
|
||||
runStatus: filterNode.data.runStatus,
|
||||
isEdgeEditable: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -8,12 +8,13 @@ import {
|
||||
WorkflowVersion,
|
||||
WorkflowWithCurrentVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
|
||||
export const useDeleteStep = ({
|
||||
workflow,
|
||||
}: {
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
workflow: WorkflowWithCurrentVersion | undefined;
|
||||
}) => {
|
||||
const { deleteWorkflowVersionStep } = useDeleteWorkflowVersionStep();
|
||||
const { updateOneRecord: updateOneWorkflowVersion } =
|
||||
@ -26,8 +27,12 @@ export const useDeleteStep = ({
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
|
||||
const deleteStep = async (stepId: string) => {
|
||||
assertWorkflowWithCurrentVersionIsDefined(workflow);
|
||||
|
||||
closeCommandMenu();
|
||||
|
||||
const workflowVersionId = await getUpdatableWorkflowVersion(workflow);
|
||||
|
||||
if (stepId === TRIGGER_STEP_ID) {
|
||||
await updateOneWorkflowVersion({
|
||||
idToUpdate: workflowVersionId,
|
||||
|
||||
@ -0,0 +1,251 @@
|
||||
import { WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import { getTriggerHeaderType } from '../getTriggerHeaderType';
|
||||
|
||||
describe('getTriggerHeaderType', () => {
|
||||
describe('DATABASE_EVENT triggers', () => {
|
||||
it('returns "Trigger · Record is created" for created event', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
type: 'DATABASE_EVENT',
|
||||
name: 'Company Created',
|
||||
settings: {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getTriggerHeaderType(trigger);
|
||||
|
||||
expect(result).toBe('Trigger · Record is created');
|
||||
});
|
||||
|
||||
it('returns "Trigger · Record is updated" for updated event', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
type: 'DATABASE_EVENT',
|
||||
name: 'Company Updated',
|
||||
settings: {
|
||||
eventName: 'company.updated',
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getTriggerHeaderType(trigger);
|
||||
|
||||
expect(result).toBe('Trigger · Record is updated');
|
||||
});
|
||||
|
||||
it('returns "Trigger · Record is deleted" for deleted event', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
type: 'DATABASE_EVENT',
|
||||
name: 'Company Deleted',
|
||||
settings: {
|
||||
eventName: 'company.deleted',
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getTriggerHeaderType(trigger);
|
||||
|
||||
expect(result).toBe('Trigger · Record is deleted');
|
||||
});
|
||||
|
||||
it('works with different object types', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
type: 'DATABASE_EVENT',
|
||||
name: 'Person Created',
|
||||
settings: {
|
||||
eventName: 'person.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getTriggerHeaderType(trigger);
|
||||
|
||||
expect(result).toBe('Trigger · Record is created');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MANUAL triggers', () => {
|
||||
it('returns "Trigger · Manual" for manual trigger', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
type: 'MANUAL',
|
||||
name: 'Manual Trigger',
|
||||
settings: {
|
||||
objectType: 'company',
|
||||
outputSchema: {},
|
||||
icon: 'IconHandMove',
|
||||
},
|
||||
};
|
||||
|
||||
const result = getTriggerHeaderType(trigger);
|
||||
|
||||
expect(result).toBe('Trigger · Manual');
|
||||
});
|
||||
|
||||
it('returns "Trigger · Manual" for manual trigger without objectType', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
type: 'MANUAL',
|
||||
name: 'Manual Trigger',
|
||||
settings: {
|
||||
outputSchema: {},
|
||||
icon: 'IconHandMove',
|
||||
},
|
||||
};
|
||||
|
||||
const result = getTriggerHeaderType(trigger);
|
||||
|
||||
expect(result).toBe('Trigger · Manual');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CRON triggers', () => {
|
||||
it('returns "Trigger" for cron trigger with DAYS schedule', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
type: 'CRON',
|
||||
name: 'Scheduled Trigger',
|
||||
settings: {
|
||||
type: 'DAYS',
|
||||
schedule: {
|
||||
day: 1,
|
||||
hour: 9,
|
||||
minute: 0,
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getTriggerHeaderType(trigger);
|
||||
|
||||
expect(result).toBe('Trigger');
|
||||
});
|
||||
|
||||
it('returns "Trigger" for cron trigger with HOURS schedule', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
type: 'CRON',
|
||||
name: 'Hourly Trigger',
|
||||
settings: {
|
||||
type: 'HOURS',
|
||||
schedule: {
|
||||
hour: 2,
|
||||
minute: 30,
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getTriggerHeaderType(trigger);
|
||||
|
||||
expect(result).toBe('Trigger');
|
||||
});
|
||||
|
||||
it('returns "Trigger" for cron trigger with MINUTES schedule', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
type: 'CRON',
|
||||
name: 'Minutely Trigger',
|
||||
settings: {
|
||||
type: 'MINUTES',
|
||||
schedule: {
|
||||
minute: 15,
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getTriggerHeaderType(trigger);
|
||||
|
||||
expect(result).toBe('Trigger');
|
||||
});
|
||||
|
||||
it('returns "Trigger" for cron trigger with CUSTOM schedule', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
type: 'CRON',
|
||||
name: 'Custom Trigger',
|
||||
settings: {
|
||||
type: 'CUSTOM',
|
||||
pattern: '0 9 * * 1',
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getTriggerHeaderType(trigger);
|
||||
|
||||
expect(result).toBe('Trigger');
|
||||
});
|
||||
});
|
||||
|
||||
describe('WEBHOOK triggers', () => {
|
||||
it('returns "Trigger · Webhook" for webhook trigger with GET method', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
type: 'WEBHOOK',
|
||||
name: 'Webhook Trigger',
|
||||
settings: {
|
||||
httpMethod: 'GET',
|
||||
authentication: null,
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getTriggerHeaderType(trigger);
|
||||
|
||||
expect(result).toBe('Trigger · Webhook');
|
||||
});
|
||||
|
||||
it('returns "Trigger · Webhook" for webhook trigger with POST method', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
type: 'WEBHOOK',
|
||||
name: 'Webhook Trigger',
|
||||
settings: {
|
||||
httpMethod: 'POST',
|
||||
expectedBody: {
|
||||
message: 'Workflow was started',
|
||||
},
|
||||
authentication: null,
|
||||
outputSchema: {
|
||||
message: {
|
||||
icon: 'IconVariable',
|
||||
isLeaf: true,
|
||||
label: 'message',
|
||||
type: 'string',
|
||||
value: 'Workflow was started',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getTriggerHeaderType(trigger);
|
||||
|
||||
expect(result).toBe('Trigger · Webhook');
|
||||
});
|
||||
|
||||
it('returns "Trigger · Webhook" for webhook trigger with API_KEY authentication', () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
type: 'WEBHOOK',
|
||||
name: 'Secure Webhook Trigger',
|
||||
settings: {
|
||||
httpMethod: 'GET',
|
||||
authentication: 'API_KEY',
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getTriggerHeaderType(trigger);
|
||||
|
||||
expect(result).toBe('Trigger · Webhook');
|
||||
});
|
||||
});
|
||||
|
||||
describe('error cases', () => {
|
||||
it('throws error for unknown trigger type', () => {
|
||||
const trigger = {
|
||||
type: 'UNKNOWN_TYPE',
|
||||
name: 'Unknown Trigger',
|
||||
settings: {
|
||||
outputSchema: {},
|
||||
},
|
||||
} as unknown as WorkflowTrigger;
|
||||
|
||||
expect(() => getTriggerHeaderType(trigger)).toThrow(
|
||||
'Unknown trigger type',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,56 @@
|
||||
/* eslint-disable @nx/workspace-no-hardcoded-colors */
|
||||
import { Theme } from '@emotion/react';
|
||||
import { getTriggerIconColor } from '../getTriggerIconColor';
|
||||
|
||||
describe('getTriggerIconColor', () => {
|
||||
const mockTheme: Theme = {
|
||||
font: {
|
||||
color: {
|
||||
primary: '#2c2c2c',
|
||||
secondary: '#666666',
|
||||
tertiary: '#999999',
|
||||
light: '#cccccc',
|
||||
},
|
||||
},
|
||||
} as unknown as Theme;
|
||||
|
||||
it('returns the tertiary font color from theme', () => {
|
||||
const result = getTriggerIconColor({ theme: mockTheme });
|
||||
|
||||
expect(result).toBe('#999999');
|
||||
});
|
||||
|
||||
it('works with different theme configurations', () => {
|
||||
const differentTheme: Theme = {
|
||||
font: {
|
||||
color: {
|
||||
primary: '#000000',
|
||||
secondary: '#444444',
|
||||
tertiary: '#888888',
|
||||
light: '#ffffff',
|
||||
},
|
||||
},
|
||||
} as unknown as Theme;
|
||||
|
||||
const result = getTriggerIconColor({ theme: differentTheme });
|
||||
|
||||
expect(result).toBe('#888888');
|
||||
});
|
||||
|
||||
it('maintains reference to theme.font.color.tertiary', () => {
|
||||
const customTheme: Theme = {
|
||||
font: {
|
||||
color: {
|
||||
primary: '#111111',
|
||||
secondary: '#333333',
|
||||
tertiary: '#custom-tertiary-color',
|
||||
light: '#eeeeee',
|
||||
},
|
||||
},
|
||||
} as unknown as Theme;
|
||||
|
||||
const result = getTriggerIconColor({ theme: customTheme });
|
||||
|
||||
expect(result).toBe('#custom-tertiary-color');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user