Add counter to filter (#14108)

See figma
https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=70822-35001&m=dev

<img width="75" height="61" alt="Capture d’écran 2025-08-27 à 16 57
46"
src="https://github.com/user-attachments/assets/e41290c8-3ec5-4d64-a5d2-d52bd14ccd57"
/>
<img width="75" height="61" alt="Capture d’écran 2025-08-27 à 16 57
58"
src="https://github.com/user-attachments/assets/710d5735-c256-47fc-ad2f-93e4f72b8002"
/>
<img width="121" height="89" alt="Capture d’écran 2025-08-27 à 16 16
33"
src="https://github.com/user-attachments/assets/0c8216b2-f251-4f0e-ad9f-f3a25229df40"
/>



https://github.com/user-attachments/assets/43156c0c-91b8-4386-a971-e37577e3c199
This commit is contained in:
Thomas Trompette
2025-08-27 15:25:18 +00:00
committed by GitHub
parent 58db0c9b0a
commit e449e69f4c
10 changed files with 198 additions and 24 deletions
@@ -0,0 +1,26 @@
import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector';
import { flowComponentState } from '@/workflow/states/flowComponentState';
import type { WorkflowAction } from '@/workflow/types/Workflow';
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
import { isDefined } from 'twenty-shared/utils';
export const createStepSelector = (stepId: string) =>
createComponentSelector<WorkflowAction | null>({
key: `stepSelector-${stepId}`,
get:
({ instanceId }) =>
({ get }) => {
const flowState = get(flowComponentState.atomFamily({ instanceId }));
if (!isDefined(flowState) || !isDefined(flowState.steps)) {
return null;
}
const step = flowState.steps.find(
(step: WorkflowAction) => step.id === stepId,
);
return step ?? null;
},
componentInstanceContext: WorkflowVisualizerComponentInstanceContext,
});
@@ -1,11 +1,12 @@
import { type Theme } from '@emotion/react';
import type { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
import { type Theme } from '@emotion/react';
type WorkflowDiagramColors = {
background: string;
borderColor: string;
color: string;
titleColor: string;
tagBackground: string;
};
export type WorkflowDiagramNodeColors = {
@@ -29,28 +30,33 @@ export const getWorkflowDiagramColors = ({
borderColor: theme.color.yellow,
color: theme.tag.text.yellow,
titleColor: theme.font.color.primary,
tagBackground: theme.tag.background.yellow,
},
unselected: {
background: theme.background.secondary,
borderColor: theme.border.color.strong,
color: theme.tag.text.yellow,
titleColor: theme.font.color.primary,
tagBackground: theme.tag.background.yellow,
},
};
}
case 'FAILED': {
case 'FAILED':
case 'STOPPED': {
return {
selected: {
background: theme.adaptiveColors.red1,
borderColor: theme.color.red,
color: theme.tag.text.red,
titleColor: theme.font.color.primary,
tagBackground: theme.tag.background.red,
},
unselected: {
background: theme.background.secondary,
borderColor: theme.border.color.strong,
color: theme.tag.text.red,
titleColor: theme.font.color.primary,
tagBackground: theme.tag.background.red,
},
};
}
@@ -61,12 +67,14 @@ export const getWorkflowDiagramColors = ({
borderColor: theme.color.turquoise,
color: theme.tag.text.green,
titleColor: theme.font.color.primary,
tagBackground: theme.tag.background.turquoise,
},
unselected: {
background: theme.background.secondary,
borderColor: theme.border.color.strong,
color: theme.tag.text.green,
titleColor: theme.font.color.primary,
tagBackground: theme.tag.background.turquoise,
},
};
}
@@ -77,12 +85,14 @@ export const getWorkflowDiagramColors = ({
borderColor: theme.color.blue,
color: theme.tag.text.blue,
titleColor: theme.font.color.primary,
tagBackground: theme.border.color.strong,
},
unselected: {
background: theme.background.secondary,
borderColor: theme.border.color.strong,
color: theme.font.color.tertiary,
titleColor: theme.font.color.light,
tagBackground: theme.border.color.strong,
},
};
}
@@ -9,12 +9,6 @@ import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge';
import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeButtonGroup';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/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';
@@ -24,10 +18,18 @@ import {
type WorkflowDiagramEdgeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowDiagramColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramColors';
import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge';
import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeButtonGroup';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge';
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
import { WorkflowStepFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter';
import { useFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterCounter';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { css } from '@emotion/react';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
@@ -38,6 +40,7 @@ import {
} from '@xyflow/react';
import { useContext } from 'react';
import { useSetRecoilState } from 'recoil';
import {
IconDotsVertical,
IconFilter,
@@ -61,6 +64,12 @@ const assertFilterEdgeDataOrThrow: (
}
};
const StyledConfiguredFilterContainer = styled.div`
height: 26px;
width: 26px;
position: relative;
`;
const StyledIconButtonGroup = styled(IconButtonGroup)<{ selected?: boolean }>`
pointer-events: all;
@@ -74,11 +83,6 @@ const StyledIconButtonGroup = styled(IconButtonGroup)<{ selected?: boolean }>`
}}
`;
const StyledConfiguredFilterContainer = styled.div`
height: 26px;
width: 26px;
`;
export const WorkflowDiagramFilterEdgeEditable = ({
source,
target,
@@ -93,6 +97,7 @@ export const WorkflowDiagramFilterEdgeEditable = ({
assertFilterEdgeDataOrThrow(data);
const { t } = useLingui();
const theme = useTheme();
const isWorkflowBranchEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
);
@@ -178,6 +183,9 @@ export const WorkflowDiagramFilterEdgeEditable = ({
await deleteEdge({ source, target });
};
const { filterCounter } = useFilterCounter({ stepId: data.stepId });
const { unselected } = getWorkflowDiagramColors({ theme });
return (
<>
<WorkflowDiagramBaseEdge
@@ -196,6 +204,11 @@ export const WorkflowDiagramFilterEdgeEditable = ({
>
<WorkflowDiagramEdgeV2VisibilityContainer shouldDisplay>
<StyledConfiguredFilterContainer>
<WorkflowStepFilterCounter
backgroundColor={unselected.tagBackground}
textColor={theme.font.color.inverted}
counter={filterCounter}
/>
{isEdgeHovered({ source, target }) ||
isDropdownOpen ||
nodeCreationStarted ? (
@@ -1,9 +1,6 @@
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useOpenWorkflowViewFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu';
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import {
@@ -11,6 +8,11 @@ import {
type WorkflowDiagramEdgeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowDiagramColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramColors';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { WorkflowStepFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter';
import { useFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterCounter';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
@@ -102,6 +104,11 @@ export const WorkflowDiagramFilterEdgeReadonly = ({
});
};
const { filterCounter } = useFilterCounter({
stepId: data.stepId,
});
const { unselected } = getWorkflowDiagramColors({ theme });
return (
<>
<BaseEdge
@@ -119,6 +126,11 @@ export const WorkflowDiagramFilterEdgeReadonly = ({
>
<WorkflowDiagramEdgeV2VisibilityContainer shouldDisplay>
<StyledConfiguredFilterContainer>
<WorkflowStepFilterCounter
backgroundColor={unselected.tagBackground}
textColor={theme.font.color.inverted}
counter={filterCounter}
/>
<StyledIconButtonGroup
className="nodrag nopan"
iconButtons={[
@@ -1,8 +1,5 @@
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowRunDiagramBaseEdge';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import type { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
import { useOpenWorkflowRunFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu';
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import {
@@ -10,7 +7,13 @@ import {
type WorkflowDiagramEdgeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowDiagramColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramColors';
import { css } from '@emotion/react';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowRunDiagramBaseEdge';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { WorkflowStepFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter';
import { useFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterCounter';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import {
@@ -21,7 +24,6 @@ import {
import { isDefined } from 'twenty-shared/utils';
import { IconFilter } from 'twenty-ui/display';
import { IconButtonGroup } from 'twenty-ui/input';
import type { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
type WorkflowDiagramFilterEdgeRunProps = EdgeProps<WorkflowDiagramEdge>;
@@ -67,6 +69,8 @@ export const WorkflowDiagramFilterEdgeRun = ({
}: WorkflowDiagramFilterEdgeRunProps) => {
assertFilterEdgeDataOrThrow(data);
const theme = useTheme();
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
@@ -96,6 +100,12 @@ export const WorkflowDiagramFilterEdgeRun = ({
});
};
const { filterCounter } = useFilterCounter({ stepId: data.stepId });
const { selected } = getWorkflowDiagramColors({
theme,
runStatus: data.runStatus,
});
return (
<>
<WorkflowRunDiagramBaseEdge
@@ -112,6 +122,11 @@ export const WorkflowDiagramFilterEdgeRun = ({
>
<WorkflowDiagramEdgeV2VisibilityContainer shouldDisplay>
<StyledConfiguredFilterContainer>
<WorkflowStepFilterCounter
backgroundColor={selected.tagBackground}
textColor={selected.color}
counter={filterCounter}
/>
<StyledIconButtonGroup
className="nodrag nopan"
iconButtons={[
@@ -0,0 +1,37 @@
import styled from '@emotion/styled';
const StyledFilterCounter = styled.div<{
backgroundColor: string;
textColor: string;
}>`
align-items: center;
background: ${({ backgroundColor }) => backgroundColor};
border-radius: 50%;
color: ${({ textColor }) => textColor};
display: flex;
font-size: ${({ theme }) => theme.font.size.xxs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
height: 12px;
justify-content: center;
width: 12px;
left: -3px;
position: absolute;
top: -3px;
z-index: 1;
`;
type WorkflowStepFilterCounterProps = {
backgroundColor: string;
textColor: string;
counter: number;
};
export const WorkflowStepFilterCounter = ({
backgroundColor,
textColor,
counter,
}: WorkflowStepFilterCounterProps) => (
<StyledFilterCounter backgroundColor={backgroundColor} textColor={textColor}>
{counter}
</StyledFilterCounter>
);
@@ -0,0 +1,28 @@
import { WorkflowStepFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter';
import { type Meta, type StoryObj } from '@storybook/react';
import { expect, within } from '@storybook/test';
import { ComponentDecorator } from 'twenty-ui/testing';
import { THEME_LIGHT } from 'twenty-ui/theme';
const meta: Meta<typeof WorkflowStepFilterCounter> = {
title: 'Modules/Workflow/Actions/Filter/WorkflowStepFilterCounter',
component: WorkflowStepFilterCounter,
decorators: [ComponentDecorator],
};
export default meta;
type Story = StoryObj<typeof WorkflowStepFilterCounter>;
export const Default: Story = {
args: {
counter: 1,
backgroundColor: THEME_LIGHT.border.color.strong,
textColor: THEME_LIGHT.font.color.inverted,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('1')).toBeVisible();
},
};
@@ -0,0 +1,25 @@
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { createStepSelector } from '@/workflow/states/selectors/stepSelector';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useFilterCounter = ({ stepId }: { stepId: string }) => {
const stepSelector = useMemo(() => createStepSelector(stepId), [stepId]);
const step = useRecoilComponentValue(stepSelector);
if (!isDefined(step)) {
return {
filterCounter: 0,
};
}
if (step.type === 'FILTER' && isDefined(step.settings?.input?.stepFilters)) {
return {
filterCounter: step.settings.input.stepFilters.length,
};
}
return {
filterCounter: 0,
};
};
@@ -42,6 +42,14 @@ export const useSearchVariable = ({
stepIds: [stepId],
}),
);
if (!isDefined(stepOutputSchema)) {
return {
variableLabel: undefined,
variablePathLabel: undefined,
};
}
const stepType =
stepId === TRIGGER_STEP_ID
? flow.trigger?.type
+1 -1
View File
@@ -46,8 +46,8 @@
},
"files": [
"dist",
"assets",
"accessibility",
"assets",
"components",
"display",
"feedback",