diff --git a/packages/twenty-front/src/modules/workflow/states/selectors/stepSelector.ts b/packages/twenty-front/src/modules/workflow/states/selectors/stepSelector.ts new file mode 100644 index 00000000000..334747cb35a --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/states/selectors/stepSelector.ts @@ -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({ + 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, + }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowDiagramColors.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowDiagramColors.ts index 4bcf3ce20a5..f02ca4c1e65 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowDiagramColors.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/utils/getWorkflowDiagramColors.ts @@ -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, }, }; } diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable.tsx index 1351d885922..bf07b6d357a 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable.tsx @@ -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 ( <> + {isEdgeHovered({ source, target }) || isDropdownOpen || nodeCreationStarted ? ( diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeReadonly.tsx index 5a1b48da0ee..9fe7f66269b 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeReadonly.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeReadonly.tsx @@ -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 ( <> + ; @@ -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 ( <> + ` + 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) => ( + + {counter} + +); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/__stories__/WorkflowStepFilterCounter.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/__stories__/WorkflowStepFilterCounter.stories.tsx new file mode 100644 index 00000000000..1ca44ccf46b --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/components/__stories__/WorkflowStepFilterCounter.stories.tsx @@ -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 = { + title: 'Modules/Workflow/Actions/Filter/WorkflowStepFilterCounter', + component: WorkflowStepFilterCounter, + decorators: [ComponentDecorator], +}; + +export default meta; + +type Story = StoryObj; + +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(); + }, +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterCounter.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterCounter.ts new file mode 100644 index 00000000000..a5c9632f600 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterCounter.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useSearchVariable.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useSearchVariable.ts index 71d26888896..e1d1cd815db 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useSearchVariable.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useSearchVariable.ts @@ -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 diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json index 3867bba9ee1..d36989f015c 100644 --- a/packages/twenty-ui/package.json +++ b/packages/twenty-ui/package.json @@ -46,8 +46,8 @@ }, "files": [ "dist", - "assets", "accessibility", + "assets", "components", "display", "feedback",