Display + edge icon on hover (#12635)

This commit is contained in:
martmull
2025-06-17 10:17:58 +02:00
committed by GitHub
parent 15c703c01e
commit ccd16fb27f
5 changed files with 79 additions and 28 deletions

View File

@ -39,6 +39,7 @@ import React, {
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { Tag, TagColor } from 'twenty-ui/components'; import { Tag, TagColor } from 'twenty-ui/components';
import { THEME_COMMON } from 'twenty-ui/theme'; import { THEME_COMMON } from 'twenty-ui/theme';
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
const StyledResetReactflowStyles = styled.div` const StyledResetReactflowStyles = styled.div`
height: 100%; height: 100%;
@ -133,6 +134,11 @@ export const WorkflowDiagramCanvasBase = ({
const workflowDiagram = useRecoilComponentValueV2( const workflowDiagram = useRecoilComponentValueV2(
workflowDiagramComponentState, workflowDiagramComponentState,
); );
const setWorkflowInsertStepIds = useSetRecoilComponentStateV2(
workflowInsertStepIdsComponentState,
);
const [ const [
workflowDiagramFlowInitializationStatus, workflowDiagramFlowInitializationStatus,
setWorkflowDiagramFlowInitializationStatus, setWorkflowDiagramFlowInitializationStatus,
@ -174,6 +180,10 @@ export const WorkflowDiagramCanvasBase = ({
reactflow.setNodes((nodes) => reactflow.setNodes((nodes) =>
nodes.map((node) => ({ ...node, selected: false })), nodes.map((node) => ({ ...node, selected: false })),
); );
setWorkflowInsertStepIds({
parentStepId: undefined,
nextStepId: undefined,
});
}); });
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);

View File

@ -1,25 +1,36 @@
import { STEP_ICON_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
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 styled from '@emotion/styled'; import styled from '@emotion/styled';
import { EdgeLabelRenderer } from '@xyflow/react'; import { EdgeLabelRenderer } from '@xyflow/react';
import { isDefined } from 'twenty-shared/utils';
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';
const EDGE_OPTION_BUTTON_LEFT_MARGIN = 8; 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)`
border: 1px solid ${({ theme }) => theme.border.color.strong};
pointer-events: all; pointer-events: all;
`; `;
const StyledContainer = styled.div<{ const StyledContainer = styled.div<{
labelX?: number;
labelY?: number; labelY?: number;
}>` }>`
position: absolute; position: absolute;
transform: ${({ labelX, labelY }) => transform: ${({ labelY }) => `translate(${21}px, ${(labelY || 0) - 14}px)`};
`translate(${labelX || 0}px, ${isDefined(labelY) ? labelY - STEP_ICON_WIDTH / 2 : 0}px) translateX(${EDGE_OPTION_BUTTON_LEFT_MARGIN}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 WorkflowDiagramEdgeOptionsProps = { type WorkflowDiagramEdgeOptionsProps = {
@ -30,31 +41,47 @@ type WorkflowDiagramEdgeOptionsProps = {
}; };
export const WorkflowDiagramEdgeOptions = ({ export const WorkflowDiagramEdgeOptions = ({
labelX,
labelY, labelY,
parentStepId, parentStepId,
nextStepId, nextStepId,
}: WorkflowDiagramEdgeOptionsProps) => { }: WorkflowDiagramEdgeOptionsProps) => {
const [hovered, setHovered] = useState(false);
const { startNodeCreation } = useStartNodeCreation(); const { startNodeCreation } = useStartNodeCreation();
const workflowInsertStepIds = useRecoilComponentValueV2(
workflowInsertStepIdsComponentState,
);
const isSelected =
workflowInsertStepIds.parentStepId === parentStepId &&
workflowInsertStepIds.nextStepId === nextStepId;
return ( return (
<EdgeLabelRenderer> <EdgeLabelRenderer>
<StyledContainer <StyledContainer
labelX={labelX}
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}
> >
<StyledIconButtonGroup <StyledWrapper
className="nodrag nopan" onMouseEnter={() => setHovered(true)}
iconButtons={[ onMouseLeave={() => setHovered(false)}
{ >
Icon: IconPlus, <StyledHoverZone />
onClick: () => { {(hovered || isSelected) && (
startNodeCreation({ parentStepId, nextStepId }); <StyledIconButtonGroup
}, className="nodrag nopan"
}, iconButtons={[
]} {
/> Icon: IconPlus,
onClick: () => {
startNodeCreation({ parentStepId, nextStepId });
},
},
]}
/>
)}
</StyledWrapper>
</StyledContainer> </StyledContainer>
</EdgeLabelRenderer> </EdgeLabelRenderer>
); );

View File

@ -11,6 +11,7 @@ export const useStartNodeCreation = () => {
const setWorkflowInsertStepIds = useSetRecoilComponentStateV2( const setWorkflowInsertStepIds = useSetRecoilComponentStateV2(
workflowInsertStepIdsComponentState, workflowInsertStepIdsComponentState,
); );
const { openStepSelectInCommandMenu } = useWorkflowCommandMenu(); const { openStepSelectInCommandMenu } = useWorkflowCommandMenu();
const workflowVisualizerWorkflowId = useRecoilComponentValueV2( const workflowVisualizerWorkflowId = useRecoilComponentValueV2(

View File

@ -1,4 +1,3 @@
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion'; import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
import { workflowLastCreatedStepIdComponentState } from '@/workflow/states/workflowLastCreatedStepIdComponentState'; import { workflowLastCreatedStepIdComponentState } from '@/workflow/states/workflowLastCreatedStepIdComponentState';
@ -11,6 +10,7 @@ import { useCreateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/us
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState'; import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { useState } from 'react'; import { useState } from 'react';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
export const useCreateStep = ({ export const useCreateStep = ({
workflow, workflow,
@ -26,9 +26,8 @@ export const useCreateStep = ({
workflowLastCreatedStepIdComponentState, workflowLastCreatedStepIdComponentState,
); );
const workflowInsertStepIds = useRecoilComponentValueV2( const [workflowInsertStepIds, setWorkflowInsertStepIds] =
workflowInsertStepIdsComponentState, useRecoilComponentStateV2(workflowInsertStepIdsComponentState);
);
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion(); const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
@ -63,9 +62,10 @@ export const useCreateStep = ({
setWorkflowSelectedNode(createdStep.id); setWorkflowSelectedNode(createdStep.id);
setWorkflowLastCreatedStepId(createdStep.id); setWorkflowLastCreatedStepId(createdStep.id);
} catch (error) { setWorkflowInsertStepIds({
setIsLoading(false); parentStepId: undefined,
throw error; nextStepId: undefined,
});
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }

View File

@ -31,6 +31,15 @@ const StyledButton = styled.button`
} }
`; `;
const StyledAnimatedIconWrapper = styled.span`
display: flex;
transition: transform 0.1s ease;
&:hover {
transform: translateY(-3%);
}
`;
export const InsideButton = ({ export const InsideButton = ({
className, className,
Icon, Icon,
@ -41,7 +50,11 @@ export const InsideButton = ({
return ( return (
<StyledButton className={className} onClick={onClick} disabled={disabled}> <StyledButton className={className} onClick={onClick} disabled={disabled}>
{Icon && <Icon size={theme.icon.size.sm} />} {Icon && (
<StyledAnimatedIconWrapper>
<Icon size={theme.icon.size.sm} />
</StyledAnimatedIconWrapper>
)}
</StyledButton> </StyledButton>
); );
}; };