Open filters in side panel (#13304)
In this PR: - Open filters in the side panel for **workflows** - Open filters in the side panel for **workflow versions** - Preparation for opening filters in the side panel for **workflow runs** - Add many tests to increase the coverage Remaining to do: - Open filters in the side panel for **workflow runs** - Upon filter creation, open it in the side panel --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
eeade6e94c
commit
924e599cba
@ -0,0 +1,21 @@
|
|||||||
|
import { checkIfItsAViteStaleChunkLazyLoadingError } from '@/error-handler/utils/checkIfItsAViteStaleChunkLazyLoadingError';
|
||||||
|
|
||||||
|
describe('checkIfItsAViteStaleChunkLazyLoadingError', () => {
|
||||||
|
it('should return true when error message contains the Vite stale chunk error text', () => {
|
||||||
|
const error = new Error(
|
||||||
|
'Failed to fetch dynamically imported module: /some/module.js',
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = checkIfItsAViteStaleChunkLazyLoadingError(error);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when error message does not contain the Vite stale chunk error text', () => {
|
||||||
|
const error = new Error('Some other error message');
|
||||||
|
|
||||||
|
const result = checkIfItsAViteStaleChunkLazyLoadingError(error);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { isWorkflowRelatedObjectMetadata } from '@/object-metadata/utils/isWorkflowRelatedObjectMetadata';
|
||||||
|
|
||||||
|
describe('isWorkflowRelatedObjectMetadata', () => {
|
||||||
|
it('should return true for Workflow object', () => {
|
||||||
|
const result = isWorkflowRelatedObjectMetadata(
|
||||||
|
CoreObjectNameSingular.Workflow,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for WorkflowVersion object', () => {
|
||||||
|
const result = isWorkflowRelatedObjectMetadata(
|
||||||
|
CoreObjectNameSingular.WorkflowVersion,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for WorkflowRun object', () => {
|
||||||
|
const result = isWorkflowRelatedObjectMetadata(
|
||||||
|
CoreObjectNameSingular.WorkflowRun,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-workflow related objects', () => {
|
||||||
|
const result = isWorkflowRelatedObjectMetadata(
|
||||||
|
CoreObjectNameSingular.Company,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for unknown object names', () => {
|
||||||
|
const result = isWorkflowRelatedObjectMetadata('unknownObject');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for Person object', () => {
|
||||||
|
const result = isWorkflowRelatedObjectMetadata(
|
||||||
|
CoreObjectNameSingular.Person,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,5 +1,5 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
|
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
|
||||||
import { CoreObjectNameSingular } from '../types/CoreObjectNameSingular';
|
|
||||||
|
|
||||||
export const isWorkflowRelatedObjectMetadata = (objectNameSingular: string) => {
|
export const isWorkflowRelatedObjectMetadata = (objectNameSingular: string) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -14,9 +14,11 @@ import { workflowRunDiagramAutomaticallyOpenedStepsComponentState } from '@/work
|
|||||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||||
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
|
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
|
||||||
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
|
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { useIcons } from 'twenty-ui/display';
|
import { useIcons } from 'twenty-ui/display';
|
||||||
|
import { FeatureFlagKey } from '~/generated/graphql';
|
||||||
|
|
||||||
export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
|
export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
|
||||||
const apolloCoreClient = useApolloCoreClient();
|
const apolloCoreClient = useApolloCoreClient();
|
||||||
@ -25,6 +27,10 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
|
|||||||
|
|
||||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||||
|
|
||||||
|
const isWorkflowFilteringEnabled = useIsFeatureEnabled(
|
||||||
|
FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED,
|
||||||
|
);
|
||||||
|
|
||||||
const runWorkflowRunOpeningInCommandMenuSideEffects = useRecoilCallback(
|
const runWorkflowRunOpeningInCommandMenuSideEffects = useRecoilCallback(
|
||||||
({ snapshot, set }) =>
|
({ snapshot, set }) =>
|
||||||
({
|
({
|
||||||
@ -56,6 +62,7 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
|
|||||||
steps: workflowRunRecord.state.flow.steps,
|
steps: workflowRunRecord.state.flow.steps,
|
||||||
stepInfos: workflowRunRecord.state.stepInfos,
|
stepInfos: workflowRunRecord.state.stepInfos,
|
||||||
trigger: workflowRunRecord.state.flow.trigger,
|
trigger: workflowRunRecord.state.flow.trigger,
|
||||||
|
isWorkflowFilteringEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isDefined(stepToOpenByDefault)) {
|
if (!isDefined(stepToOpenByDefault)) {
|
||||||
@ -118,9 +125,10 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
apolloCoreClient.cache,
|
apolloCoreClient.cache,
|
||||||
getIcon,
|
|
||||||
openWorkflowRunViewStepInCommandMenu,
|
|
||||||
objectPermissionsByObjectMetadataId,
|
objectPermissionsByObjectMetadataId,
|
||||||
|
isWorkflowFilteringEnabled,
|
||||||
|
openWorkflowRunViewStepInCommandMenu,
|
||||||
|
getIcon,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import { getStepOutputSchemaFamilyStateKey } from '@/workflow/utils/getStepOutpu
|
|||||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||||
|
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
||||||
import {
|
import {
|
||||||
OutputSchema,
|
OutputSchema,
|
||||||
StepOutputSchema,
|
StepOutputSchema,
|
||||||
} from '@/workflow/workflow-variables/types/StepOutputSchema';
|
} from '@/workflow/workflow-variables/types/StepOutputSchema';
|
||||||
import { getTriggerStepName } from '@/workflow/workflow-variables/utils/getTriggerStepName';
|
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ export const useStepsOutputSchema = () => {
|
|||||||
id: TRIGGER_STEP_ID,
|
id: TRIGGER_STEP_ID,
|
||||||
name: isDefined(trigger.name)
|
name: isDefined(trigger.name)
|
||||||
? trigger.name
|
? trigger.name
|
||||||
: getTriggerStepName(trigger),
|
: getTriggerDefaultLabel(trigger),
|
||||||
icon: triggerIconKey,
|
icon: triggerIconKey,
|
||||||
outputSchema: trigger.settings?.outputSchema as OutputSchema,
|
outputSchema: trigger.settings?.outputSchema as OutputSchema,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||||
|
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react';
|
||||||
|
|
||||||
|
type WorkflowDiagramBlankEdgeProps = EdgeProps<WorkflowDiagramEdge>;
|
||||||
|
|
||||||
|
export const WorkflowDiagramBlankEdge = ({
|
||||||
|
markerStart,
|
||||||
|
markerEnd,
|
||||||
|
sourceY,
|
||||||
|
targetY,
|
||||||
|
}: WorkflowDiagramBlankEdgeProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [edgePath] = getStraightPath({
|
||||||
|
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
sourceY,
|
||||||
|
targetX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseEdge
|
||||||
|
markerStart={markerStart}
|
||||||
|
markerEnd={markerEnd}
|
||||||
|
path={edgePath}
|
||||||
|
style={{ stroke: theme.border.color.strong }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,9 +1,12 @@
|
|||||||
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
|
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
|
||||||
|
import { WorkflowDiagramBlankEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramBlankEdge';
|
||||||
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
|
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
|
||||||
import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect';
|
import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect';
|
||||||
import { WorkflowDiagramCreateStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepNode';
|
import { WorkflowDiagramCreateStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepNode';
|
||||||
import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge';
|
import { WorkflowDiagramDefaultEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable';
|
||||||
import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger';
|
import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger';
|
||||||
|
import { WorkflowDiagramFilterEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable';
|
||||||
|
import { WorkflowDiagramFilteringDisabledEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeEditable';
|
||||||
import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable';
|
import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable';
|
||||||
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
|
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
|
||||||
import { ReactFlowProvider } from '@xyflow/react';
|
import { ReactFlowProvider } from '@xyflow/react';
|
||||||
@ -26,7 +29,11 @@ export const WorkflowDiagramCanvasEditable = ({
|
|||||||
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
||||||
}}
|
}}
|
||||||
edgeTypes={{
|
edgeTypes={{
|
||||||
default: WorkflowDiagramDefaultEdge,
|
blank: WorkflowDiagramBlankEdge,
|
||||||
|
'filtering-disabled--editable':
|
||||||
|
WorkflowDiagramFilteringDisabledEdgeEditable,
|
||||||
|
'empty-filter--editable': WorkflowDiagramDefaultEdgeEditable,
|
||||||
|
'filter--editable': WorkflowDiagramFilterEdgeEditable,
|
||||||
}}
|
}}
|
||||||
tagContainerTestId="workflow-visualizer-status"
|
tagContainerTestId="workflow-visualizer-status"
|
||||||
tagColor={tagProps.color}
|
tagColor={tagProps.color}
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
|
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
|
||||||
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
|
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
|
||||||
import { WorkflowDiagramCanvasReadonlyEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect';
|
import { WorkflowDiagramCanvasReadonlyEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect';
|
||||||
import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge';
|
import { WorkflowDiagramDefaultEdgeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeReadonly';
|
||||||
import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger';
|
import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger';
|
||||||
|
import { WorkflowDiagramFilterEdgeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeReadonly';
|
||||||
|
import { WorkflowDiagramFilteringDisabledEdgeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeReadonly';
|
||||||
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly';
|
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly';
|
||||||
import { WorkflowDiagramSuccessEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge';
|
|
||||||
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
|
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
|
||||||
import { ReactFlowProvider } from '@xyflow/react';
|
import { ReactFlowProvider } from '@xyflow/react';
|
||||||
|
|
||||||
@ -25,8 +26,10 @@ export const WorkflowDiagramCanvasReadonly = ({
|
|||||||
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
||||||
}}
|
}}
|
||||||
edgeTypes={{
|
edgeTypes={{
|
||||||
default: WorkflowDiagramDefaultEdge,
|
'filtering-disabled--readonly':
|
||||||
success: WorkflowDiagramSuccessEdge,
|
WorkflowDiagramFilteringDisabledEdgeReadonly,
|
||||||
|
'empty-filter--readonly': WorkflowDiagramDefaultEdgeReadonly,
|
||||||
|
'filter--readonly': WorkflowDiagramFilterEdgeReadonly,
|
||||||
}}
|
}}
|
||||||
tagContainerTestId="workflow-visualizer-status"
|
tagContainerTestId="workflow-visualizer-status"
|
||||||
tagColor={tagProps.color}
|
tagColor={tagProps.color}
|
||||||
|
|||||||
@ -1,92 +0,0 @@
|
|||||||
import { WorkflowDiagramEdgeV1 } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV1';
|
|
||||||
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';
|
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import {
|
|
||||||
BaseEdge,
|
|
||||||
EdgeLabelRenderer,
|
|
||||||
EdgeProps,
|
|
||||||
getStraightPath,
|
|
||||||
} from '@xyflow/react';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
import { FeatureFlagKey } from '~/generated/graphql';
|
|
||||||
|
|
||||||
type WorkflowDiagramDefaultEdgeProps = EdgeProps<WorkflowDiagramEdge>;
|
|
||||||
|
|
||||||
export const WorkflowDiagramDefaultEdge = ({
|
|
||||||
source,
|
|
||||||
target,
|
|
||||||
sourceY,
|
|
||||||
targetY,
|
|
||||||
markerStart,
|
|
||||||
markerEnd,
|
|
||||||
data,
|
|
||||||
}: WorkflowDiagramDefaultEdgeProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const isWorkflowFilteringEnabled = useIsFeatureEnabled(
|
|
||||||
FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [edgePath, labelX, labelY] = getStraightPath({
|
|
||||||
sourceX: CREATE_STEP_NODE_WIDTH,
|
|
||||||
sourceY,
|
|
||||||
targetX: CREATE_STEP_NODE_WIDTH,
|
|
||||||
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
|
|
||||||
markerStart={markerStart}
|
|
||||||
markerEnd={markerEnd}
|
|
||||||
path={edgePath}
|
|
||||||
style={{ stroke: theme.border.color.strong }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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,134 @@
|
|||||||
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
|
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||||
|
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||||
|
import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined';
|
||||||
|
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
|
||||||
|
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
|
||||||
|
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||||
|
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||||
|
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||||
|
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||||
|
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
|
||||||
|
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import {
|
||||||
|
BaseEdge,
|
||||||
|
EdgeLabelRenderer,
|
||||||
|
EdgeProps,
|
||||||
|
getStraightPath,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { IconFilter, IconPlus } from 'twenty-ui/display';
|
||||||
|
import { IconButtonGroup } from 'twenty-ui/input';
|
||||||
|
|
||||||
|
type WorkflowDiagramDefaultEdgeEditableProps = EdgeProps<WorkflowDiagramEdge>;
|
||||||
|
|
||||||
|
const StyledIconButtonGroup = styled(IconButtonGroup)`
|
||||||
|
pointer-events: all;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkflowDiagramDefaultEdgeEditable = ({
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
sourceY,
|
||||||
|
targetY,
|
||||||
|
markerStart,
|
||||||
|
markerEnd,
|
||||||
|
}: WorkflowDiagramDefaultEdgeEditableProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [edgePath, labelX, labelY] = getStraightPath({
|
||||||
|
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
sourceY,
|
||||||
|
targetX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
|
||||||
|
workflowVisualizerWorkflowIdComponentState,
|
||||||
|
);
|
||||||
|
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
|
||||||
|
assertWorkflowWithCurrentVersionIsDefined(workflow);
|
||||||
|
|
||||||
|
const { createStep } = useCreateStep({ workflow });
|
||||||
|
const { startNodeCreation } = useStartNodeCreation();
|
||||||
|
|
||||||
|
const [hovered, setHovered] = useState(false);
|
||||||
|
|
||||||
|
const workflowInsertStepIds = useRecoilComponentValueV2(
|
||||||
|
workflowInsertStepIdsComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSelected =
|
||||||
|
workflowInsertStepIds.nextStepId === target &&
|
||||||
|
workflowInsertStepIds.parentStepId === source;
|
||||||
|
|
||||||
|
const handleCreateFilter = async () => {
|
||||||
|
await createStep({
|
||||||
|
newStepType: 'FILTER',
|
||||||
|
parentStepId: source,
|
||||||
|
nextStepId: target,
|
||||||
|
});
|
||||||
|
|
||||||
|
setHovered(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setWorkflowSelectedNode = useSetRecoilComponentStateV2(
|
||||||
|
workflowSelectedNodeComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterButtonClick = () => {
|
||||||
|
setWorkflowSelectedNode(source);
|
||||||
|
|
||||||
|
handleCreateFilter();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodeButtonClick = () => {
|
||||||
|
startNodeCreation({
|
||||||
|
parentStepId: source,
|
||||||
|
nextStepId: target,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseEdge
|
||||||
|
markerStart={markerStart}
|
||||||
|
markerEnd={markerEnd}
|
||||||
|
path={edgePath}
|
||||||
|
style={{ stroke: theme.border.color.strong }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<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: handleNodeButtonClick,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</WorkflowDiagramEdgeV2VisibilityContainer>
|
||||||
|
</WorkflowDiagramEdgeV2Container>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||||
|
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react';
|
||||||
|
|
||||||
|
type WorkflowDiagramDefaultEdgeReadonlyProps = EdgeProps<WorkflowDiagramEdge>;
|
||||||
|
|
||||||
|
export const WorkflowDiagramDefaultEdgeReadonly = ({
|
||||||
|
sourceY,
|
||||||
|
targetY,
|
||||||
|
markerStart,
|
||||||
|
markerEnd,
|
||||||
|
}: WorkflowDiagramDefaultEdgeReadonlyProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [edgePath] = getStraightPath({
|
||||||
|
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
sourceY,
|
||||||
|
targetX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseEdge
|
||||||
|
markerStart={markerStart}
|
||||||
|
markerEnd={markerEnd}
|
||||||
|
path={edgePath}
|
||||||
|
style={{ stroke: theme.border.color.strong }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge';
|
||||||
|
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||||
|
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { EdgeProps, getStraightPath } from '@xyflow/react';
|
||||||
|
|
||||||
|
type WorkflowDiagramDefaultEdgeRunProps = EdgeProps<WorkflowDiagramEdge>;
|
||||||
|
|
||||||
|
export const WorkflowDiagramDefaultEdgeRun = ({
|
||||||
|
sourceY,
|
||||||
|
targetY,
|
||||||
|
data,
|
||||||
|
}: WorkflowDiagramDefaultEdgeRunProps) => {
|
||||||
|
const [edgePath] = getStraightPath({
|
||||||
|
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
sourceY,
|
||||||
|
targetX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorkflowRunDiagramBaseEdge
|
||||||
|
edgePath={edgePath}
|
||||||
|
edgeExecutionStatus={data?.edgeExecutionStatus}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,83 +0,0 @@
|
|||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
|
||||||
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
|
||||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
|
||||||
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { IconPlus } from 'twenty-ui/display';
|
|
||||||
import { IconButtonGroup } from 'twenty-ui/input';
|
|
||||||
|
|
||||||
const StyledIconButtonGroup = styled(IconButtonGroup)`
|
|
||||||
pointer-events: all;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledContainer = styled.div<{
|
|
||||||
labelY?: number;
|
|
||||||
}>`
|
|
||||||
position: absolute;
|
|
||||||
transform: ${({ labelY }) => `translate(${21}px, ${(labelY || 0) - 14}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 WorkflowDiagramEdgeV1Props = {
|
|
||||||
labelY?: number;
|
|
||||||
parentStepId: string;
|
|
||||||
nextStepId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WorkflowDiagramEdgeV1 = ({
|
|
||||||
labelY,
|
|
||||||
parentStepId,
|
|
||||||
nextStepId,
|
|
||||||
}: WorkflowDiagramEdgeV1Props) => {
|
|
||||||
const [hovered, setHovered] = useState(false);
|
|
||||||
|
|
||||||
const { startNodeCreation } = useStartNodeCreation();
|
|
||||||
|
|
||||||
const workflowInsertStepIds = useRecoilComponentValueV2(
|
|
||||||
workflowInsertStepIdsComponentState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSelected =
|
|
||||||
workflowInsertStepIds.parentStepId === parentStepId &&
|
|
||||||
workflowInsertStepIds.nextStepId === nextStepId;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledContainer
|
|
||||||
labelY={labelY}
|
|
||||||
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
|
||||||
>
|
|
||||||
<StyledWrapper
|
|
||||||
onMouseEnter={() => setHovered(true)}
|
|
||||||
onMouseLeave={() => setHovered(false)}
|
|
||||||
>
|
|
||||||
<StyledHoverZone />
|
|
||||||
{(hovered || isSelected) && (
|
|
||||||
<StyledIconButtonGroup
|
|
||||||
className="nodrag nopan"
|
|
||||||
iconButtons={[
|
|
||||||
{
|
|
||||||
Icon: IconPlus,
|
|
||||||
onClick: () => {
|
|
||||||
startNodeCreation({ parentStepId, nextStepId });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</StyledWrapper>
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
|
||||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
|
||||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
|
||||||
import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined';
|
|
||||||
import { WorkflowDiagramEdgeV2EmptyContent } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2EmptyContent';
|
|
||||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
|
||||||
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
|
|
||||||
|
|
||||||
type WorkflowDiagramEdgeV2EmptyProps = {
|
|
||||||
labelX: number;
|
|
||||||
labelY: number;
|
|
||||||
parentStepId: string;
|
|
||||||
nextStepId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WorkflowDiagramEdgeV2Empty = ({
|
|
||||||
labelX,
|
|
||||||
labelY,
|
|
||||||
parentStepId,
|
|
||||||
nextStepId,
|
|
||||||
}: WorkflowDiagramEdgeV2EmptyProps) => {
|
|
||||||
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
|
|
||||||
workflowVisualizerWorkflowIdComponentState,
|
|
||||||
);
|
|
||||||
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
|
|
||||||
assertWorkflowWithCurrentVersionIsDefined(workflow);
|
|
||||||
|
|
||||||
const { createStep } = useCreateStep({ workflow });
|
|
||||||
const { startNodeCreation } = useStartNodeCreation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WorkflowDiagramEdgeV2EmptyContent
|
|
||||||
labelX={labelX}
|
|
||||||
labelY={labelY}
|
|
||||||
parentStepId={parentStepId}
|
|
||||||
nextStepId={nextStepId}
|
|
||||||
onCreateFilter={() => {
|
|
||||||
return createStep({
|
|
||||||
newStepType: 'FILTER',
|
|
||||||
parentStepId,
|
|
||||||
nextStepId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onCreateNode={() => {
|
|
||||||
startNodeCreation({ parentStepId, nextStepId });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
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 });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,220 +0,0 @@
|
|||||||
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
|
||||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
|
||||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
|
||||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
|
||||||
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
|
||||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
|
||||||
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 { 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';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
import {
|
|
||||||
IconDotsVertical,
|
|
||||||
IconFilter,
|
|
||||||
IconFilterX,
|
|
||||||
IconPlus,
|
|
||||||
} from 'twenty-ui/display';
|
|
||||||
import { IconButtonGroup } from 'twenty-ui/input';
|
|
||||||
import { MenuItem } from 'twenty-ui/navigation';
|
|
||||||
|
|
||||||
const StyledIconButtonGroup = styled(IconButtonGroup)`
|
|
||||||
pointer-events: all;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledConfiguredFilterContainer = styled.div`
|
|
||||||
height: 26px;
|
|
||||||
width: 26px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type WorkflowDiagramEdgeV2FilterContentProps = {
|
|
||||||
labelX: number;
|
|
||||||
labelY: number;
|
|
||||||
stepId: string;
|
|
||||||
parentStepId: string;
|
|
||||||
nextStepId: string;
|
|
||||||
filterSettings: FilterSettings;
|
|
||||||
onDeleteFilter: () => Promise<void>;
|
|
||||||
onCreateNode: () => void;
|
|
||||||
isEdgeEditable: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WorkflowDiagramEdgeV2FilterContent = ({
|
|
||||||
labelX,
|
|
||||||
labelY,
|
|
||||||
stepId,
|
|
||||||
parentStepId,
|
|
||||||
nextStepId,
|
|
||||||
onDeleteFilter,
|
|
||||||
onCreateNode,
|
|
||||||
isEdgeEditable,
|
|
||||||
}: WorkflowDiagramEdgeV2FilterContentProps) => {
|
|
||||||
const { openDropdown } = useOpenDropdown();
|
|
||||||
const { closeDropdown } = useCloseDropdown();
|
|
||||||
|
|
||||||
const [hovered, setHovered] = useState(false);
|
|
||||||
|
|
||||||
const setWorkflowDiagramPanOnDrag = useSetRecoilComponentStateV2(
|
|
||||||
workflowDiagramPanOnDragComponentState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const workflowInsertStepIds = useRecoilComponentValueV2(
|
|
||||||
workflowInsertStepIdsComponentState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSelected =
|
|
||||||
workflowInsertStepIds.nextStepId === nextStepId &&
|
|
||||||
(workflowInsertStepIds.parentStepId === parentStepId ||
|
|
||||||
(isNonEmptyString(stepId) &&
|
|
||||||
workflowInsertStepIds.parentStepId === stepId));
|
|
||||||
|
|
||||||
const dropdownId = `${WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}-${parentStepId}-${nextStepId}`;
|
|
||||||
|
|
||||||
const isDropdownOpen = useRecoilComponentValueV2(
|
|
||||||
isDropdownOpenComponentState,
|
|
||||||
dropdownId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
|
|
||||||
workflowVisualizerWorkflowIdComponentState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu();
|
|
||||||
|
|
||||||
const setWorkflowSelectedNode = useSetRecoilComponentStateV2(
|
|
||||||
workflowSelectedNodeComponentState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
|
||||||
if (!isEdgeEditable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setHovered(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
setHovered(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterButtonClick = () => {
|
|
||||||
setWorkflowSelectedNode(stepId);
|
|
||||||
|
|
||||||
if (isDefined(workflowVisualizerWorkflowId)) {
|
|
||||||
openWorkflowEditStepInCommandMenu(
|
|
||||||
workflowVisualizerWorkflowId,
|
|
||||||
'Filter',
|
|
||||||
IconFilter,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WorkflowDiagramEdgeV2Container
|
|
||||||
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
|
||||||
labelX={labelX}
|
|
||||||
labelY={labelY}
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
>
|
|
||||||
<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}
|
|
||||||
clickableComponent={<div></div>}
|
|
||||||
data-select-disable
|
|
||||||
dropdownPlacement="bottom-start"
|
|
||||||
dropdownStrategy="absolute"
|
|
||||||
dropdownOffset={{
|
|
||||||
x: 24,
|
|
||||||
y: 4,
|
|
||||||
}}
|
|
||||||
onOpen={() => {
|
|
||||||
setWorkflowDiagramPanOnDrag(false);
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
setWorkflowDiagramPanOnDrag(true);
|
|
||||||
}}
|
|
||||||
dropdownComponents={
|
|
||||||
<DropdownContent widthInPixels={GenericDropdownContentWidth.Narrow}>
|
|
||||||
<DropdownMenuItemsContainer>
|
|
||||||
<MenuItem
|
|
||||||
text="Filter"
|
|
||||||
LeftIcon={IconFilter}
|
|
||||||
onClick={() => {
|
|
||||||
closeDropdown(dropdownId);
|
|
||||||
setHovered(false);
|
|
||||||
|
|
||||||
handleFilterButtonClick();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
text="Remove Filter"
|
|
||||||
LeftIcon={IconFilterX}
|
|
||||||
onClick={() => {
|
|
||||||
closeDropdown(dropdownId);
|
|
||||||
setHovered(false);
|
|
||||||
|
|
||||||
onDeleteFilter();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
text="Add Node"
|
|
||||||
LeftIcon={IconPlus}
|
|
||||||
onClick={() => {
|
|
||||||
closeDropdown(dropdownId);
|
|
||||||
setHovered(false);
|
|
||||||
|
|
||||||
onCreateNode();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</DropdownContent>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</WorkflowDiagramEdgeV2VisibilityContainer>
|
|
||||||
</WorkflowDiagramEdgeV2Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -13,10 +13,11 @@ import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/state
|
|||||||
import { addCreateStepNodes } from '@/workflow/workflow-diagram/utils/addCreateStepNodes';
|
import { addCreateStepNodes } from '@/workflow/workflow-diagram/utils/addCreateStepNodes';
|
||||||
import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram';
|
import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram';
|
||||||
import { mergeWorkflowDiagrams } from '@/workflow/workflow-diagram/utils/mergeWorkflowDiagrams';
|
import { mergeWorkflowDiagrams } from '@/workflow/workflow-diagram/utils/mergeWorkflowDiagrams';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { addEdgeOptions } from '@/workflow/workflow-diagram/utils/addEdgeOptions';
|
import { FeatureFlagKey } from '~/generated/graphql';
|
||||||
|
|
||||||
export const WorkflowDiagramEffect = ({
|
export const WorkflowDiagramEffect = ({
|
||||||
workflowWithCurrentVersion,
|
workflowWithCurrentVersion,
|
||||||
@ -36,6 +37,10 @@ export const WorkflowDiagramEffect = ({
|
|||||||
workflowLastCreatedStepIdComponentState,
|
workflowLastCreatedStepIdComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isWorkflowFilteringEnabled = useIsFeatureEnabled(
|
||||||
|
FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED,
|
||||||
|
);
|
||||||
|
|
||||||
const computeAndMergeNewWorkflowDiagram = useRecoilCallback(
|
const computeAndMergeNewWorkflowDiagram = useRecoilCallback(
|
||||||
({ snapshot, set }) => {
|
({ snapshot, set }) => {
|
||||||
return (currentVersion: WorkflowVersion) => {
|
return (currentVersion: WorkflowVersion) => {
|
||||||
@ -45,7 +50,11 @@ export const WorkflowDiagramEffect = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const nextWorkflowDiagram = addCreateStepNodes(
|
const nextWorkflowDiagram = addCreateStepNodes(
|
||||||
addEdgeOptions(getWorkflowVersionDiagram(currentVersion)),
|
getWorkflowVersionDiagram({
|
||||||
|
workflowVersion: currentVersion,
|
||||||
|
isWorkflowFilteringEnabled,
|
||||||
|
isEditable: true,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mergedWorkflowDiagram = nextWorkflowDiagram;
|
let mergedWorkflowDiagram = nextWorkflowDiagram;
|
||||||
@ -78,7 +87,11 @@ export const WorkflowDiagramEffect = ({
|
|||||||
set(workflowDiagramState, mergedWorkflowDiagram);
|
set(workflowDiagramState, mergedWorkflowDiagram);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[workflowLastCreatedStepIdState, workflowDiagramState],
|
[
|
||||||
|
workflowDiagramState,
|
||||||
|
isWorkflowFilteringEnabled,
|
||||||
|
workflowLastCreatedStepIdState,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentVersion = workflowWithCurrentVersion?.currentVersion;
|
const currentVersion = workflowWithCurrentVersion?.currentVersion;
|
||||||
|
|||||||
@ -0,0 +1,257 @@
|
|||||||
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
|
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||||
|
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||||
|
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
||||||
|
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||||
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
|
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||||
|
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||||
|
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
|
||||||
|
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
|
||||||
|
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||||
|
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/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';
|
||||||
|
import {
|
||||||
|
WorkflowDiagramEdge,
|
||||||
|
WorkflowDiagramEdgeData,
|
||||||
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
|
||||||
|
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
import {
|
||||||
|
BaseEdge,
|
||||||
|
EdgeLabelRenderer,
|
||||||
|
EdgeProps,
|
||||||
|
getStraightPath,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import {
|
||||||
|
IconDotsVertical,
|
||||||
|
IconFilter,
|
||||||
|
IconFilterX,
|
||||||
|
IconPlus,
|
||||||
|
} from 'twenty-ui/display';
|
||||||
|
import { IconButtonGroup } from 'twenty-ui/input';
|
||||||
|
import { MenuItem } from 'twenty-ui/navigation';
|
||||||
|
|
||||||
|
type WorkflowDiagramFilterEdgeEditableProps = EdgeProps<WorkflowDiagramEdge>;
|
||||||
|
|
||||||
|
const assertFilterEdgeDataOrThrow: (
|
||||||
|
data: WorkflowDiagramEdgeData | undefined,
|
||||||
|
) => asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = (
|
||||||
|
data: WorkflowDiagramEdgeData | undefined,
|
||||||
|
) => {
|
||||||
|
if (data?.edgeType !== 'filter') {
|
||||||
|
throw new Error('Edge data must be of type "filter"');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledIconButtonGroup = styled(IconButtonGroup)`
|
||||||
|
pointer-events: all;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledConfiguredFilterContainer = styled.div`
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkflowDiagramFilterEdgeEditable = ({
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
sourceY,
|
||||||
|
targetY,
|
||||||
|
markerStart,
|
||||||
|
markerEnd,
|
||||||
|
data,
|
||||||
|
}: WorkflowDiagramFilterEdgeEditableProps) => {
|
||||||
|
assertFilterEdgeDataOrThrow(data);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [edgePath, labelX, labelY] = getStraightPath({
|
||||||
|
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
sourceY,
|
||||||
|
targetX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
|
||||||
|
workflowVisualizerWorkflowIdComponentState,
|
||||||
|
);
|
||||||
|
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
|
||||||
|
|
||||||
|
const { deleteStep } = useDeleteStep({ workflow });
|
||||||
|
const { startNodeCreation } = useStartNodeCreation();
|
||||||
|
|
||||||
|
const { openDropdown } = useOpenDropdown();
|
||||||
|
const { closeDropdown } = useCloseDropdown();
|
||||||
|
|
||||||
|
const [hovered, setHovered] = useState(false);
|
||||||
|
|
||||||
|
const setWorkflowDiagramPanOnDrag = useSetRecoilComponentStateV2(
|
||||||
|
workflowDiagramPanOnDragComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const workflowInsertStepIds = useRecoilComponentValueV2(
|
||||||
|
workflowInsertStepIdsComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSelected =
|
||||||
|
workflowInsertStepIds.nextStepId === source &&
|
||||||
|
(workflowInsertStepIds.parentStepId === target ||
|
||||||
|
(isNonEmptyString(data.stepId) &&
|
||||||
|
workflowInsertStepIds.parentStepId === data.stepId));
|
||||||
|
|
||||||
|
const dropdownId = `${WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}-${source}-${target}`;
|
||||||
|
|
||||||
|
const isDropdownOpen = useRecoilComponentValueV2(
|
||||||
|
isDropdownOpenComponentState,
|
||||||
|
dropdownId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { openWorkflowEditFilterInCommandMenu } =
|
||||||
|
useOpenWorkflowEditFilterInCommandMenu();
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
setHovered(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
setHovered(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterButtonClick = () => {
|
||||||
|
openWorkflowEditFilterInCommandMenu({
|
||||||
|
stepId: data.stepId,
|
||||||
|
stepName: data.name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseEdge
|
||||||
|
markerStart={markerStart}
|
||||||
|
markerEnd={markerEnd}
|
||||||
|
path={edgePath}
|
||||||
|
style={{ stroke: theme.border.color.strong }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<WorkflowDiagramEdgeV2Container
|
||||||
|
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||||
|
labelX={labelX}
|
||||||
|
labelY={labelY}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
<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}
|
||||||
|
clickableComponent={<div></div>}
|
||||||
|
data-select-disable
|
||||||
|
dropdownPlacement="bottom-start"
|
||||||
|
dropdownStrategy="absolute"
|
||||||
|
dropdownOffset={{
|
||||||
|
x: 24,
|
||||||
|
y: 4,
|
||||||
|
}}
|
||||||
|
onOpen={() => {
|
||||||
|
setWorkflowDiagramPanOnDrag(false);
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setWorkflowDiagramPanOnDrag(true);
|
||||||
|
}}
|
||||||
|
dropdownComponents={
|
||||||
|
<DropdownContent
|
||||||
|
widthInPixels={GenericDropdownContentWidth.Narrow}
|
||||||
|
>
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
<MenuItem
|
||||||
|
text="Filter"
|
||||||
|
LeftIcon={IconFilter}
|
||||||
|
onClick={() => {
|
||||||
|
closeDropdown(dropdownId);
|
||||||
|
setHovered(false);
|
||||||
|
|
||||||
|
handleFilterButtonClick();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
text="Remove Filter"
|
||||||
|
LeftIcon={IconFilterX}
|
||||||
|
onClick={() => {
|
||||||
|
closeDropdown(dropdownId);
|
||||||
|
setHovered(false);
|
||||||
|
|
||||||
|
if (!isDefined(data.stepId)) {
|
||||||
|
throw new Error(
|
||||||
|
'Step ID must be configured for the edge when rendering a filter',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleteStep(data.stepId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
text="Add Node"
|
||||||
|
LeftIcon={IconPlus}
|
||||||
|
onClick={() => {
|
||||||
|
closeDropdown(dropdownId);
|
||||||
|
setHovered(false);
|
||||||
|
|
||||||
|
startNodeCreation({
|
||||||
|
parentStepId: data.stepId,
|
||||||
|
nextStepId: target,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</DropdownContent>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</WorkflowDiagramEdgeV2VisibilityContainer>
|
||||||
|
</WorkflowDiagramEdgeV2Container>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,102 @@
|
|||||||
|
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
|
||||||
|
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
|
||||||
|
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||||
|
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||||
|
import { useOpenWorkflowViewFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu';
|
||||||
|
import {
|
||||||
|
WorkflowDiagramEdge,
|
||||||
|
WorkflowDiagramEdgeData,
|
||||||
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import {
|
||||||
|
BaseEdge,
|
||||||
|
EdgeLabelRenderer,
|
||||||
|
EdgeProps,
|
||||||
|
getStraightPath,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
import { IconFilter } from 'twenty-ui/display';
|
||||||
|
import { IconButtonGroup } from 'twenty-ui/input';
|
||||||
|
|
||||||
|
type WorkflowDiagramFilterEdgeReadonlyProps = EdgeProps<WorkflowDiagramEdge>;
|
||||||
|
|
||||||
|
const assertFilterEdgeDataOrThrow: (
|
||||||
|
data: WorkflowDiagramEdgeData | undefined,
|
||||||
|
) => asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = (
|
||||||
|
data: WorkflowDiagramEdgeData | undefined,
|
||||||
|
) => {
|
||||||
|
if (data?.edgeType !== 'filter') {
|
||||||
|
throw new Error('Edge data must be of type "filter"');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledIconButtonGroup = styled(IconButtonGroup)`
|
||||||
|
pointer-events: all;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledConfiguredFilterContainer = styled.div`
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkflowDiagramFilterEdgeReadonly = ({
|
||||||
|
sourceY,
|
||||||
|
targetY,
|
||||||
|
markerStart,
|
||||||
|
markerEnd,
|
||||||
|
data,
|
||||||
|
}: WorkflowDiagramFilterEdgeReadonlyProps) => {
|
||||||
|
assertFilterEdgeDataOrThrow(data);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [edgePath, labelX, labelY] = getStraightPath({
|
||||||
|
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
sourceY,
|
||||||
|
targetX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { openWorkflowViewFilterInCommandMenu } =
|
||||||
|
useOpenWorkflowViewFilterInCommandMenu();
|
||||||
|
|
||||||
|
const handleFilterButtonClick = () => {
|
||||||
|
openWorkflowViewFilterInCommandMenu({
|
||||||
|
stepId: data.stepId,
|
||||||
|
stepName: data.name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseEdge
|
||||||
|
markerStart={markerStart}
|
||||||
|
markerEnd={markerEnd}
|
||||||
|
path={edgePath}
|
||||||
|
style={{ stroke: theme.border.color.strong }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<WorkflowDiagramEdgeV2Container
|
||||||
|
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||||
|
labelX={labelX}
|
||||||
|
labelY={labelY}
|
||||||
|
>
|
||||||
|
<WorkflowDiagramEdgeV2VisibilityContainer shouldDisplay>
|
||||||
|
<StyledConfiguredFilterContainer>
|
||||||
|
<StyledIconButtonGroup
|
||||||
|
className="nodrag nopan"
|
||||||
|
iconButtons={[
|
||||||
|
{
|
||||||
|
Icon: IconFilter,
|
||||||
|
onClick: handleFilterButtonClick,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</StyledConfiguredFilterContainer>
|
||||||
|
</WorkflowDiagramEdgeV2VisibilityContainer>
|
||||||
|
</WorkflowDiagramEdgeV2Container>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
|
||||||
|
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
|
||||||
|
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge';
|
||||||
|
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||||
|
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||||
|
import { useOpenWorkflowRunFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu';
|
||||||
|
import {
|
||||||
|
WorkflowDiagramEdge,
|
||||||
|
WorkflowDiagramEdgeData,
|
||||||
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { EdgeLabelRenderer, EdgeProps, getStraightPath } from '@xyflow/react';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { IconFilter } from 'twenty-ui/display';
|
||||||
|
import { IconButtonGroup } from 'twenty-ui/input';
|
||||||
|
|
||||||
|
type WorkflowDiagramFilterEdgeRunProps = EdgeProps<WorkflowDiagramEdge>;
|
||||||
|
|
||||||
|
const assertFilterEdgeDataOrThrow: (
|
||||||
|
data: WorkflowDiagramEdgeData | undefined,
|
||||||
|
) => asserts data is WorkflowDiagramEdgeData & { edgeType: 'filter' } = (
|
||||||
|
data: WorkflowDiagramEdgeData | undefined,
|
||||||
|
) => {
|
||||||
|
if (data?.edgeType !== 'filter') {
|
||||||
|
throw new Error('Edge data must be of type "filter"');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledIconButtonGroup = styled(IconButtonGroup)`
|
||||||
|
pointer-events: all;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledConfiguredFilterContainer = styled.div`
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkflowDiagramFilterEdgeRun = ({
|
||||||
|
sourceY,
|
||||||
|
targetY,
|
||||||
|
data,
|
||||||
|
}: WorkflowDiagramFilterEdgeRunProps) => {
|
||||||
|
assertFilterEdgeDataOrThrow(data);
|
||||||
|
|
||||||
|
const [edgePath, labelX, labelY] = getStraightPath({
|
||||||
|
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
sourceY,
|
||||||
|
targetX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { openWorkflowRunFilterInCommandMenu } =
|
||||||
|
useOpenWorkflowRunFilterInCommandMenu();
|
||||||
|
|
||||||
|
const handleFilterButtonClick = () => {
|
||||||
|
if (!isDefined(data.runStatus)) {
|
||||||
|
throw new Error('Run status must be set on edge data for workflow runs');
|
||||||
|
}
|
||||||
|
|
||||||
|
openWorkflowRunFilterInCommandMenu({
|
||||||
|
stepId: data.stepId,
|
||||||
|
stepName: data.name,
|
||||||
|
stepExecutionStatus: data.runStatus,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WorkflowRunDiagramBaseEdge
|
||||||
|
edgePath={edgePath}
|
||||||
|
edgeExecutionStatus={data.edgeExecutionStatus}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<WorkflowDiagramEdgeV2Container
|
||||||
|
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||||
|
labelX={labelX}
|
||||||
|
labelY={labelY}
|
||||||
|
>
|
||||||
|
<WorkflowDiagramEdgeV2VisibilityContainer shouldDisplay>
|
||||||
|
<StyledConfiguredFilterContainer>
|
||||||
|
<StyledIconButtonGroup
|
||||||
|
className="nodrag nopan"
|
||||||
|
iconButtons={[
|
||||||
|
{
|
||||||
|
Icon: IconFilter,
|
||||||
|
onClick: handleFilterButtonClick,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</StyledConfiguredFilterContainer>
|
||||||
|
</WorkflowDiagramEdgeV2VisibilityContainer>
|
||||||
|
</WorkflowDiagramEdgeV2Container>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||||
|
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||||
|
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||||
|
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { IconPlus } from 'twenty-ui/display';
|
||||||
|
import { IconButtonGroup } from 'twenty-ui/input';
|
||||||
|
|
||||||
|
const StyledIconButtonGroup = styled(IconButtonGroup)`
|
||||||
|
pointer-events: all;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledContainer = styled.div<{
|
||||||
|
labelY?: number;
|
||||||
|
}>`
|
||||||
|
position: absolute;
|
||||||
|
transform: ${({ labelY }) => `translate(${21}px, ${(labelY || 0) - 14}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 WorkflowDiagramFilteringDisabledEdgeEditableProps =
|
||||||
|
EdgeProps<WorkflowDiagramEdge>;
|
||||||
|
|
||||||
|
export const WorkflowDiagramFilteringDisabledEdgeEditable = ({
|
||||||
|
markerStart,
|
||||||
|
markerEnd,
|
||||||
|
source,
|
||||||
|
sourceY,
|
||||||
|
target,
|
||||||
|
targetY,
|
||||||
|
}: WorkflowDiagramFilteringDisabledEdgeEditableProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [edgePath, , labelY] = getStraightPath({
|
||||||
|
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
sourceY,
|
||||||
|
targetX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [hovered, setHovered] = useState(false);
|
||||||
|
|
||||||
|
const { startNodeCreation } = useStartNodeCreation();
|
||||||
|
|
||||||
|
const workflowInsertStepIds = useRecoilComponentValueV2(
|
||||||
|
workflowInsertStepIdsComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSelected =
|
||||||
|
workflowInsertStepIds.parentStepId === source &&
|
||||||
|
workflowInsertStepIds.nextStepId === target;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseEdge
|
||||||
|
markerStart={markerStart}
|
||||||
|
markerEnd={markerEnd}
|
||||||
|
path={edgePath}
|
||||||
|
style={{ stroke: theme.border.color.strong }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StyledContainer
|
||||||
|
labelY={labelY}
|
||||||
|
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||||
|
>
|
||||||
|
<StyledWrapper
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
>
|
||||||
|
<StyledHoverZone />
|
||||||
|
{(hovered || isSelected) && (
|
||||||
|
<StyledIconButtonGroup
|
||||||
|
className="nodrag nopan"
|
||||||
|
iconButtons={[
|
||||||
|
{
|
||||||
|
Icon: IconPlus,
|
||||||
|
onClick: () => {
|
||||||
|
startNodeCreation({
|
||||||
|
parentStepId: source,
|
||||||
|
nextStepId: target,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</StyledWrapper>
|
||||||
|
</StyledContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||||
|
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react';
|
||||||
|
|
||||||
|
type WorkflowDiagramFilteringDisabledEdgeReadonlyProps =
|
||||||
|
EdgeProps<WorkflowDiagramEdge>;
|
||||||
|
|
||||||
|
export const WorkflowDiagramFilteringDisabledEdgeReadonly = ({
|
||||||
|
markerStart,
|
||||||
|
markerEnd,
|
||||||
|
sourceY,
|
||||||
|
targetY,
|
||||||
|
}: WorkflowDiagramFilteringDisabledEdgeReadonlyProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [edgePath] = getStraightPath({
|
||||||
|
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
sourceY,
|
||||||
|
targetX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseEdge
|
||||||
|
markerStart={markerStart}
|
||||||
|
markerEnd={markerEnd}
|
||||||
|
path={edgePath}
|
||||||
|
style={{ stroke: theme.border.color.strong }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge';
|
||||||
|
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||||
|
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { EdgeProps, getStraightPath } from '@xyflow/react';
|
||||||
|
|
||||||
|
type WorkflowDiagramFilteringDisabledEdgeRunProps =
|
||||||
|
EdgeProps<WorkflowDiagramEdge>;
|
||||||
|
|
||||||
|
export const WorkflowDiagramFilteringDisabledEdgeRun = ({
|
||||||
|
sourceY,
|
||||||
|
targetY,
|
||||||
|
data,
|
||||||
|
}: WorkflowDiagramFilteringDisabledEdgeRunProps) => {
|
||||||
|
const [edgePath] = getStraightPath({
|
||||||
|
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
sourceY,
|
||||||
|
targetX: CREATE_STEP_NODE_WIDTH,
|
||||||
|
targetY,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorkflowRunDiagramBaseEdge
|
||||||
|
edgePath={edgePath}
|
||||||
|
edgeExecutionStatus={data?.edgeExecutionStatus}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import { EDGE_GREEN_ROUNDED_ARROW_MARKER_WIDTH_PX } from '@/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerWidthPx';
|
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import {
|
|
||||||
BaseEdge,
|
|
||||||
EdgeLabelRenderer,
|
|
||||||
EdgeProps,
|
|
||||||
getStraightPath,
|
|
||||||
} from '@xyflow/react';
|
|
||||||
import { Label } from 'twenty-ui/display';
|
|
||||||
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
|
||||||
|
|
||||||
const StyledLabel = styled(Label)`
|
|
||||||
color: ${({ theme }) => theme.tag.text.turquoise};
|
|
||||||
`;
|
|
||||||
|
|
||||||
type WorkflowDiagramSuccessEdgeProps = EdgeProps;
|
|
||||||
|
|
||||||
export const WorkflowDiagramSuccessEdge = ({
|
|
||||||
sourceY,
|
|
||||||
targetY,
|
|
||||||
markerStart,
|
|
||||||
markerEnd,
|
|
||||||
label,
|
|
||||||
}: WorkflowDiagramSuccessEdgeProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const [edgePath, labelX, labelY] = getStraightPath({
|
|
||||||
sourceX: CREATE_STEP_NODE_WIDTH,
|
|
||||||
sourceY,
|
|
||||||
targetX: CREATE_STEP_NODE_WIDTH,
|
|
||||||
targetY,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<BaseEdge
|
|
||||||
markerStart={markerStart}
|
|
||||||
markerEnd={markerEnd}
|
|
||||||
path={edgePath}
|
|
||||||
style={{ stroke: theme.tag.text.turquoise }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EdgeLabelRenderer>
|
|
||||||
<StyledLabel
|
|
||||||
variant="small"
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
transform: `translate(0, -50%) translate(${labelX}px, ${labelY}px) translateX(${EDGE_GREEN_ROUNDED_ARROW_MARKER_WIDTH_PX / 2 + 3}px)`,
|
|
||||||
pointerEvents: 'all',
|
|
||||||
}}
|
|
||||||
className="nodrag nopan"
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</StyledLabel>
|
|
||||||
</EdgeLabelRenderer>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import { EDGE_GRAY_CIRCLE_MARKED_ID } from '@/workflow/workflow-diagram/constants/EdgeGrayCircleMarkedId';
|
||||||
|
import { EDGE_GREEN_CIRCLE_MARKED_ID } from '@/workflow/workflow-diagram/constants/EdgeGreenCircleMarkedId';
|
||||||
|
import { EDGE_GREEN_ROUNDED_ARROW_MARKER_ID } from '@/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerId';
|
||||||
|
import { EDGE_ROUNDED_ARROW_MARKER_ID } from '@/workflow/workflow-diagram/constants/EdgeRoundedArrowMarkerId';
|
||||||
|
import { Theme, useTheme } from '@emotion/react';
|
||||||
|
import { BaseEdge } from '@xyflow/react';
|
||||||
|
import { StepStatus } from 'twenty-shared/workflow';
|
||||||
|
|
||||||
|
const toMarkerId = (id: string) => `url(#${id})`;
|
||||||
|
|
||||||
|
const getMarkerStart = (edgeExecutionStatus: StepStatus | undefined) => {
|
||||||
|
if (edgeExecutionStatus === StepStatus.SUCCESS) {
|
||||||
|
return EDGE_GREEN_CIRCLE_MARKED_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EDGE_GRAY_CIRCLE_MARKED_ID;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMarkerEnd = (edgeExecutionStatus: StepStatus | undefined) => {
|
||||||
|
if (edgeExecutionStatus === StepStatus.SUCCESS) {
|
||||||
|
return EDGE_GREEN_ROUNDED_ARROW_MARKER_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EDGE_ROUNDED_ARROW_MARKER_ID;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStrokeColor = ({
|
||||||
|
theme,
|
||||||
|
edgeExecutionStatus,
|
||||||
|
}: {
|
||||||
|
theme: Theme;
|
||||||
|
edgeExecutionStatus: StepStatus | undefined;
|
||||||
|
}) => {
|
||||||
|
if (edgeExecutionStatus === StepStatus.SUCCESS) {
|
||||||
|
return theme.tag.text.turquoise;
|
||||||
|
}
|
||||||
|
|
||||||
|
return theme.border.color.strong;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkflowRunDiagramBaseEdgeProps = {
|
||||||
|
edgePath: string;
|
||||||
|
edgeExecutionStatus: StepStatus | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkflowRunDiagramBaseEdge = ({
|
||||||
|
edgePath,
|
||||||
|
edgeExecutionStatus,
|
||||||
|
}: WorkflowRunDiagramBaseEdgeProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseEdge
|
||||||
|
markerStart={toMarkerId(getMarkerStart(edgeExecutionStatus))}
|
||||||
|
markerEnd={toMarkerId(getMarkerEnd(edgeExecutionStatus))}
|
||||||
|
path={edgePath}
|
||||||
|
style={{
|
||||||
|
stroke: getStrokeColor({
|
||||||
|
theme,
|
||||||
|
edgeExecutionStatus,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import { WorkflowRunStatus } from '@/workflow/types/Workflow';
|
import { WorkflowRunStatus } from '@/workflow/types/Workflow';
|
||||||
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
|
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
|
||||||
import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge';
|
import { WorkflowDiagramDefaultEdgeRun } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeRun';
|
||||||
|
import { WorkflowDiagramFilterEdgeRun } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeRun';
|
||||||
|
import { WorkflowDiagramFilteringDisabledEdgeRun } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeRun';
|
||||||
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly';
|
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly';
|
||||||
import { WorkflowDiagramSuccessEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge';
|
|
||||||
import { WorkflowRunDiagramCanvasEffect } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect';
|
import { WorkflowRunDiagramCanvasEffect } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect';
|
||||||
import { useHandleWorkflowRunDiagramCanvasInit } from '@/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit';
|
import { useHandleWorkflowRunDiagramCanvasInit } from '@/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit';
|
||||||
import { getWorkflowRunStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps';
|
import { getWorkflowRunStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps';
|
||||||
@ -27,8 +28,9 @@ export const WorkflowRunDiagramCanvas = ({
|
|||||||
default: WorkflowDiagramStepNodeReadonly,
|
default: WorkflowDiagramStepNodeReadonly,
|
||||||
}}
|
}}
|
||||||
edgeTypes={{
|
edgeTypes={{
|
||||||
default: WorkflowDiagramDefaultEdge,
|
'filtering-disabled--run': WorkflowDiagramFilteringDisabledEdgeRun,
|
||||||
success: WorkflowDiagramSuccessEdge,
|
'empty-filter--run': WorkflowDiagramDefaultEdgeRun,
|
||||||
|
'filter--run': WorkflowDiagramFilterEdgeRun,
|
||||||
}}
|
}}
|
||||||
tagContainerTestId="workflow-run-status"
|
tagContainerTestId="workflow-run-status"
|
||||||
tagColor={tagProps.color}
|
tagColor={tagProps.color}
|
||||||
|
|||||||
@ -18,10 +18,12 @@ import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/
|
|||||||
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
|
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
|
||||||
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
|
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
|
||||||
import { selectWorkflowDiagramNode } from '@/workflow/workflow-diagram/utils/selectWorkflowDiagramNode';
|
import { selectWorkflowDiagramNode } from '@/workflow/workflow-diagram/utils/selectWorkflowDiagramNode';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { useContext, useEffect } from 'react';
|
import { useContext, useEffect } from 'react';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { useIcons } from 'twenty-ui/display';
|
import { useIcons } from 'twenty-ui/display';
|
||||||
|
import { FeatureFlagKey } from '~/generated/graphql';
|
||||||
|
|
||||||
export const WorkflowRunVisualizerEffect = ({
|
export const WorkflowRunVisualizerEffect = ({
|
||||||
workflowRunId,
|
workflowRunId,
|
||||||
@ -67,6 +69,10 @@ export const WorkflowRunVisualizerEffect = ({
|
|||||||
|
|
||||||
const { isInRightDrawer } = useContext(ActionMenuContext);
|
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||||
|
|
||||||
|
const isWorkflowFilteringEnabled = useIsFeatureEnabled(
|
||||||
|
FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setWorkflowRunId(workflowRunId);
|
setWorkflowRunId(workflowRunId);
|
||||||
}, [setWorkflowRunId, workflowRunId]);
|
}, [setWorkflowRunId, workflowRunId]);
|
||||||
@ -117,6 +123,7 @@ export const WorkflowRunVisualizerEffect = ({
|
|||||||
trigger: workflowRunState.flow.trigger,
|
trigger: workflowRunState.flow.trigger,
|
||||||
steps: workflowRunState.flow.steps,
|
steps: workflowRunState.flow.steps,
|
||||||
stepInfos: workflowRunState.stepInfos,
|
stepInfos: workflowRunState.stepInfos,
|
||||||
|
isWorkflowFilteringEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isDefined(stepToOpenByDefault)) {
|
if (isDefined(stepToOpenByDefault)) {
|
||||||
@ -184,6 +191,7 @@ export const WorkflowRunVisualizerEffect = ({
|
|||||||
[
|
[
|
||||||
flowState,
|
flowState,
|
||||||
getIcon,
|
getIcon,
|
||||||
|
isWorkflowFilteringEnabled,
|
||||||
openWorkflowRunViewStepInCommandMenu,
|
openWorkflowRunViewStepInCommandMenu,
|
||||||
workflowDiagramState,
|
workflowDiagramState,
|
||||||
workflowDiagramStatusState,
|
workflowDiagramStatusState,
|
||||||
|
|||||||
@ -6,8 +6,10 @@ import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/wo
|
|||||||
import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState';
|
import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState';
|
||||||
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||||
import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram';
|
import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { FeatureFlagKey } from '~/generated/graphql';
|
||||||
|
|
||||||
export const WorkflowVersionVisualizerEffect = ({
|
export const WorkflowVersionVisualizerEffect = ({
|
||||||
workflowVersionId,
|
workflowVersionId,
|
||||||
@ -29,6 +31,10 @@ export const WorkflowVersionVisualizerEffect = ({
|
|||||||
|
|
||||||
const { populateStepsOutputSchema } = useStepsOutputSchema();
|
const { populateStepsOutputSchema } = useStepsOutputSchema();
|
||||||
|
|
||||||
|
const isWorkflowFilteringEnabled = useIsFeatureEnabled(
|
||||||
|
FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDefined(workflowVersion)) {
|
if (!isDefined(workflowVersion)) {
|
||||||
setFlow(undefined);
|
setFlow(undefined);
|
||||||
@ -58,10 +64,14 @@ export const WorkflowVersionVisualizerEffect = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextWorkflowDiagram = getWorkflowVersionDiagram(workflowVersion);
|
const nextWorkflowDiagram = getWorkflowVersionDiagram({
|
||||||
|
workflowVersion,
|
||||||
|
isWorkflowFilteringEnabled,
|
||||||
|
isEditable: false,
|
||||||
|
});
|
||||||
|
|
||||||
setWorkflowDiagram(nextWorkflowDiagram);
|
setWorkflowDiagram(nextWorkflowDiagram);
|
||||||
}, [setWorkflowDiagram, workflowVersion]);
|
}, [isWorkflowFilteringEnabled, setWorkflowDiagram, workflowVersion]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDefined(workflowVersion)) {
|
if (!isDefined(workflowVersion)) {
|
||||||
|
|||||||
@ -1,205 +0,0 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import { RecoilRoot } from 'recoil';
|
|
||||||
|
|
||||||
import { WorkflowDiagramCreateStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepNode';
|
|
||||||
import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge';
|
|
||||||
import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger';
|
|
||||||
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly';
|
|
||||||
import { WorkflowDiagramSuccessEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge';
|
|
||||||
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeDefaultConfiguration';
|
|
||||||
import { WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration';
|
|
||||||
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
|
||||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
|
||||||
import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
|
|
||||||
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
|
||||||
import { workflowDiagramComponentState } from '../../states/workflowDiagramComponentState';
|
|
||||||
import { WorkflowDiagramCanvasBase } from '../WorkflowDiagramCanvasBase';
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
|
||||||
height: 400px;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const meta: Meta = {
|
|
||||||
title: 'Modules/Workflow/WorkflowDiagram/WorkflowDiagramCustomMarkers',
|
|
||||||
component: WorkflowDiagramCanvasBase,
|
|
||||||
parameters: {
|
|
||||||
msw: graphqlMocks,
|
|
||||||
},
|
|
||||||
decorators: [
|
|
||||||
WorkspaceDecorator,
|
|
||||||
ObjectMetadataItemsDecorator,
|
|
||||||
ReactflowDecorator,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
|
|
||||||
type Story = StoryObj<typeof WorkflowDiagramCanvasBase>;
|
|
||||||
|
|
||||||
export const DefaultEdge: Story = {
|
|
||||||
args: {
|
|
||||||
nodeTypes: {
|
|
||||||
default: WorkflowDiagramStepNodeReadonly,
|
|
||||||
'create-step': WorkflowDiagramCreateStepNode,
|
|
||||||
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
|
||||||
},
|
|
||||||
edgeTypes: {
|
|
||||||
default: WorkflowDiagramDefaultEdge,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
decorators: [
|
|
||||||
(Story) => {
|
|
||||||
const workflowVisualizerComponentInstanceId =
|
|
||||||
'workflow-visualizer-test-id';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RecoilRoot
|
|
||||||
initializeState={({ set }) => {
|
|
||||||
set(
|
|
||||||
workflowDiagramComponentState.atomFamily({
|
|
||||||
instanceId: workflowVisualizerComponentInstanceId,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
id: 'trigger-1',
|
|
||||||
type: 'default',
|
|
||||||
position: { x: 100, y: 100 },
|
|
||||||
data: {
|
|
||||||
nodeType: 'trigger',
|
|
||||||
triggerType: 'DATABASE_EVENT',
|
|
||||||
name: 'When record is created',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'action-1',
|
|
||||||
type: 'default',
|
|
||||||
position: { x: 300, y: 100 },
|
|
||||||
data: {
|
|
||||||
nodeType: 'action',
|
|
||||||
actionType: 'CREATE_RECORD',
|
|
||||||
name: 'Create record',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'create-step-1',
|
|
||||||
type: 'create-step',
|
|
||||||
position: { x: 500, y: 100 },
|
|
||||||
data: {
|
|
||||||
nodeType: 'create-step',
|
|
||||||
parentNodeId: 'action-1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
edges: [
|
|
||||||
{
|
|
||||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
|
||||||
id: 'edge-1',
|
|
||||||
source: 'trigger-1',
|
|
||||||
target: 'action-1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
|
||||||
id: 'edge-2',
|
|
||||||
source: 'action-1',
|
|
||||||
target: 'create-step-1',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<WorkflowVisualizerComponentInstanceContext.Provider
|
|
||||||
value={{
|
|
||||||
instanceId: workflowVisualizerComponentInstanceId,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyledContainer>
|
|
||||||
<Story />
|
|
||||||
</StyledContainer>
|
|
||||||
</WorkflowVisualizerComponentInstanceContext.Provider>
|
|
||||||
</RecoilRoot>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SuccessEdge: Story = {
|
|
||||||
args: {
|
|
||||||
nodeTypes: {
|
|
||||||
default: WorkflowDiagramStepNodeReadonly,
|
|
||||||
'create-step': WorkflowDiagramCreateStepNode,
|
|
||||||
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
|
||||||
},
|
|
||||||
edgeTypes: {
|
|
||||||
default: WorkflowDiagramDefaultEdge,
|
|
||||||
success: WorkflowDiagramSuccessEdge,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
decorators: [
|
|
||||||
(Story) => {
|
|
||||||
const workflowVisualizerComponentInstanceId =
|
|
||||||
'workflow-visualizer-test-id';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RecoilRoot
|
|
||||||
initializeState={({ set }) => {
|
|
||||||
set(
|
|
||||||
workflowDiagramComponentState.atomFamily({
|
|
||||||
instanceId: workflowVisualizerComponentInstanceId,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
id: 'trigger-1',
|
|
||||||
type: 'default',
|
|
||||||
position: { x: 100, y: 100 },
|
|
||||||
data: {
|
|
||||||
nodeType: 'trigger',
|
|
||||||
triggerType: 'DATABASE_EVENT',
|
|
||||||
name: 'When record is created',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'action-1',
|
|
||||||
type: 'default',
|
|
||||||
position: { x: 300, y: 100 },
|
|
||||||
data: {
|
|
||||||
nodeType: 'action',
|
|
||||||
actionType: 'CREATE_RECORD',
|
|
||||||
name: 'Create record',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
edges: [
|
|
||||||
{
|
|
||||||
...WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION,
|
|
||||||
id: 'edge-1',
|
|
||||||
source: 'trigger-1',
|
|
||||||
target: 'action-1',
|
|
||||||
type: 'success',
|
|
||||||
label: '1 item',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<WorkflowVisualizerComponentInstanceContext.Provider
|
|
||||||
value={{
|
|
||||||
instanceId: workflowVisualizerComponentInstanceId,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyledContainer>
|
|
||||||
<Story />
|
|
||||||
</StyledContainer>
|
|
||||||
</WorkflowVisualizerComponentInstanceContext.Provider>
|
|
||||||
</RecoilRoot>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,125 +0,0 @@
|
|||||||
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,
|
|
||||||
getCanvasElementForDropdownTesting,
|
|
||||||
} from 'twenty-ui/testing';
|
|
||||||
import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
|
|
||||||
import { WorkflowDiagramEdgeV2FilterContent } from '../WorkflowDiagramEdgeV2FilterContent';
|
|
||||||
|
|
||||||
const meta: Meta<typeof WorkflowDiagramEdgeV2FilterContent> = {
|
|
||||||
title: 'Modules/Workflow/WorkflowDiagramEdgeV2FilterContent',
|
|
||||||
component: WorkflowDiagramEdgeV2FilterContent,
|
|
||||||
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',
|
|
||||||
onDeleteFilter: fn(),
|
|
||||||
onCreateNode: fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof WorkflowDiagramEdgeV2FilterContent>;
|
|
||||||
|
|
||||||
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 AddNodeAction: Story = {
|
|
||||||
play: async ({ canvasElement, args }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
|
|
||||||
const buttons = await canvas.findAllByRole('button');
|
|
||||||
const dotsButton = buttons[1];
|
|
||||||
|
|
||||||
userEvent.hover(dotsButton);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(dotsButton).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
userEvent.click(dotsButton);
|
|
||||||
|
|
||||||
const addNodeButton = await within(
|
|
||||||
getCanvasElementForDropdownTesting(),
|
|
||||||
).findByText('Add Node');
|
|
||||||
|
|
||||||
userEvent.click(addNodeButton);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(canvas.queryByText('Add Node')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(args.onCreateNode).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DropdownInteractions: Story = {
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
|
|
||||||
const buttons = await canvas.findAllByRole('button');
|
|
||||||
const dotsButton = buttons[1];
|
|
||||||
|
|
||||||
userEvent.hover(dotsButton);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(dotsButton).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
userEvent.click(dotsButton);
|
|
||||||
|
|
||||||
const dropdownCanvas = within(getCanvasElementForDropdownTesting());
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(dropdownCanvas.getByText('Filter')).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
userEvent.click(canvasElement);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(dropdownCanvas.queryByText('Filter')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
userEvent.click(dotsButton);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(dropdownCanvas.getByText('Filter')).toBeVisible();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import { WorkflowDiagramEdgeType } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
|
||||||
|
|
||||||
export const WORKFLOW_DIAGRAM_SUCCESS_EDGE_TYPE =
|
|
||||||
'success' satisfies WorkflowDiagramEdgeType;
|
|
||||||
@ -1,14 +1,17 @@
|
|||||||
import { EDGE_GRAY_CIRCLE_MARKED_ID } from '@/workflow/workflow-diagram/constants/EdgeGrayCircleMarkedId';
|
import { EDGE_GRAY_CIRCLE_MARKED_ID } from '@/workflow/workflow-diagram/constants/EdgeGrayCircleMarkedId';
|
||||||
import { EDGE_ROUNDED_ARROW_MARKER_ID } from '@/workflow/workflow-diagram/constants/EdgeRoundedArrowMarkerId';
|
import { EDGE_ROUNDED_ARROW_MARKER_ID } from '@/workflow/workflow-diagram/constants/EdgeRoundedArrowMarkerId';
|
||||||
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
import {
|
||||||
|
WorkflowDiagramEdge,
|
||||||
|
WorkflowDiagramEdgeType,
|
||||||
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
|
||||||
export const WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION = {
|
export const WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION = {
|
||||||
|
type: 'empty-filter--readonly' satisfies WorkflowDiagramEdgeType,
|
||||||
markerStart: EDGE_GRAY_CIRCLE_MARKED_ID,
|
markerStart: EDGE_GRAY_CIRCLE_MARKED_ID,
|
||||||
markerEnd: EDGE_ROUNDED_ARROW_MARKER_ID,
|
markerEnd: EDGE_ROUNDED_ARROW_MARKER_ID,
|
||||||
deletable: false,
|
deletable: false,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
data: {
|
data: {
|
||||||
edgeType: 'default',
|
edgeType: 'default',
|
||||||
isEdgeEditable: false,
|
|
||||||
},
|
},
|
||||||
} satisfies Partial<WorkflowDiagramEdge>;
|
} satisfies Partial<WorkflowDiagramEdge>;
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
import { EDGE_GREEN_CIRCLE_MARKED_ID } from '@/workflow/workflow-diagram/constants/EdgeGreenCircleMarkedId';
|
|
||||||
import { EDGE_GREEN_ROUNDED_ARROW_MARKER_ID } from '@/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerId';
|
|
||||||
import { WORKFLOW_DIAGRAM_SUCCESS_EDGE_TYPE } from '@/workflow/workflow-diagram/constants/WorkflowDiagramSuccessEdgeType';
|
|
||||||
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
|
||||||
|
|
||||||
export const WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION = {
|
|
||||||
type: WORKFLOW_DIAGRAM_SUCCESS_EDGE_TYPE,
|
|
||||||
markerStart: EDGE_GREEN_CIRCLE_MARKED_ID,
|
|
||||||
markerEnd: EDGE_GREEN_ROUNDED_ARROW_MARKER_ID,
|
|
||||||
deletable: false,
|
|
||||||
selectable: false,
|
|
||||||
} satisfies Partial<WorkflowDiagramEdge>;
|
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
||||||
|
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 { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||||
|
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||||
|
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { useIcons } from 'twenty-ui/display';
|
||||||
|
|
||||||
|
export const useOpenWorkflowEditFilterInCommandMenu = () => {
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
|
||||||
|
workflowVisualizerWorkflowIdComponentState,
|
||||||
|
);
|
||||||
|
const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu();
|
||||||
|
|
||||||
|
const setWorkflowSelectedNode = useSetRecoilComponentStateV2(
|
||||||
|
workflowSelectedNodeComponentState,
|
||||||
|
);
|
||||||
|
const setWorkflowDiagram = useSetRecoilComponentStateV2(
|
||||||
|
workflowDiagramComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const openWorkflowEditFilterInCommandMenu = ({
|
||||||
|
stepId,
|
||||||
|
stepName,
|
||||||
|
}: {
|
||||||
|
stepId: string;
|
||||||
|
stepName: string;
|
||||||
|
}) => {
|
||||||
|
if (!isDefined(workflowVisualizerWorkflowId)) {
|
||||||
|
throw new Error(
|
||||||
|
'Workflow ID must be configured for the edge when opening a filter in command menu',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setWorkflowSelectedNode(stepId);
|
||||||
|
|
||||||
|
setWorkflowDiagram((diagram) => {
|
||||||
|
if (!isDefined(diagram)) {
|
||||||
|
throw new Error('Workflow diagram must be defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...diagram,
|
||||||
|
nodes: diagram.nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
selected: false,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
openWorkflowEditStepInCommandMenu(
|
||||||
|
workflowVisualizerWorkflowId,
|
||||||
|
stepName,
|
||||||
|
getIcon(getActionIcon('FILTER')),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
openWorkflowEditFilterInCommandMenu,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
||||||
|
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 { workflowVisualizerWorkflowRunIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowRunIdComponentState';
|
||||||
|
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||||
|
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||||
|
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { StepStatus } from 'twenty-shared/workflow';
|
||||||
|
import { useIcons } from 'twenty-ui/display';
|
||||||
|
|
||||||
|
export const useOpenWorkflowRunFilterInCommandMenu = () => {
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
|
||||||
|
workflowVisualizerWorkflowIdComponentState,
|
||||||
|
);
|
||||||
|
const workflowVisualizerWorkflowRunId = useRecoilComponentValueV2(
|
||||||
|
workflowVisualizerWorkflowRunIdComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const setWorkflowSelectedNode = useSetRecoilComponentStateV2(
|
||||||
|
workflowSelectedNodeComponentState,
|
||||||
|
);
|
||||||
|
const setWorkflowDiagram = useSetRecoilComponentStateV2(
|
||||||
|
workflowDiagramComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu();
|
||||||
|
|
||||||
|
const openWorkflowRunFilterInCommandMenu = ({
|
||||||
|
stepId,
|
||||||
|
stepName,
|
||||||
|
stepExecutionStatus,
|
||||||
|
}: {
|
||||||
|
stepId: string;
|
||||||
|
stepName: string;
|
||||||
|
stepExecutionStatus: StepStatus;
|
||||||
|
}) => {
|
||||||
|
if (!isDefined(workflowVisualizerWorkflowId)) {
|
||||||
|
throw new Error('Workflow ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDefined(workflowVisualizerWorkflowRunId)) {
|
||||||
|
throw new Error('Workflow run ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
setWorkflowSelectedNode(stepId);
|
||||||
|
|
||||||
|
setWorkflowDiagram((diagram) => {
|
||||||
|
if (!isDefined(diagram)) {
|
||||||
|
throw new Error('Workflow diagram must be defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...diagram,
|
||||||
|
nodes: diagram.nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
selected: false,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
openWorkflowRunViewStepInCommandMenu({
|
||||||
|
workflowId: workflowVisualizerWorkflowId,
|
||||||
|
workflowRunId: workflowVisualizerWorkflowRunId,
|
||||||
|
title: stepName,
|
||||||
|
icon: getIcon(getActionIcon('FILTER')),
|
||||||
|
workflowSelectedNode: stepId,
|
||||||
|
stepExecutionStatus,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
openWorkflowRunFilterInCommandMenu,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
||||||
|
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 { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState';
|
||||||
|
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||||
|
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||||
|
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { useIcons } from 'twenty-ui/display';
|
||||||
|
|
||||||
|
export const useOpenWorkflowViewFilterInCommandMenu = () => {
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
|
||||||
|
workflowVisualizerWorkflowIdComponentState,
|
||||||
|
);
|
||||||
|
const workflowVisualizerWorkflowVersionId = useRecoilComponentValueV2(
|
||||||
|
workflowVisualizerWorkflowVersionIdComponentState,
|
||||||
|
);
|
||||||
|
const { openWorkflowViewStepInCommandMenu } = useWorkflowCommandMenu();
|
||||||
|
|
||||||
|
const setWorkflowSelectedNode = useSetRecoilComponentStateV2(
|
||||||
|
workflowSelectedNodeComponentState,
|
||||||
|
);
|
||||||
|
const setWorkflowDiagram = useSetRecoilComponentStateV2(
|
||||||
|
workflowDiagramComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const openWorkflowViewFilterInCommandMenu = ({
|
||||||
|
stepId,
|
||||||
|
stepName,
|
||||||
|
}: {
|
||||||
|
stepId: string;
|
||||||
|
stepName: string;
|
||||||
|
}) => {
|
||||||
|
if (!workflowVisualizerWorkflowId) {
|
||||||
|
throw new Error('Workflow ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!workflowVisualizerWorkflowVersionId) {
|
||||||
|
throw new Error('Workflow version ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
setWorkflowSelectedNode(stepId);
|
||||||
|
|
||||||
|
setWorkflowDiagram((diagram) => {
|
||||||
|
if (!isDefined(diagram)) {
|
||||||
|
throw new Error('Workflow diagram must be defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...diagram,
|
||||||
|
nodes: diagram.nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
selected: false,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
openWorkflowViewStepInCommandMenu({
|
||||||
|
workflowId: workflowVisualizerWorkflowId,
|
||||||
|
workflowVersionId: workflowVisualizerWorkflowVersionId,
|
||||||
|
title: stepName,
|
||||||
|
icon: getIcon(getActionIcon('FILTER')),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
openWorkflowViewFilterInCommandMenu,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -5,6 +5,7 @@ import {
|
|||||||
} from '@/workflow/types/Workflow';
|
} from '@/workflow/types/Workflow';
|
||||||
import { FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
|
import { FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
|
||||||
import { Edge, Node } from '@xyflow/react';
|
import { Edge, Node } from '@xyflow/react';
|
||||||
|
import { StepStatus } from 'twenty-shared/workflow';
|
||||||
|
|
||||||
export type WorkflowDiagramStepNode = Node<WorkflowDiagramStepNodeData>;
|
export type WorkflowDiagramStepNode = Node<WorkflowDiagramStepNodeData>;
|
||||||
export type WorkflowDiagramNode = Node<WorkflowDiagramNodeData>;
|
export type WorkflowDiagramNode = Node<WorkflowDiagramNodeData>;
|
||||||
@ -69,12 +70,12 @@ export type WorkflowDiagramFilterEdgeData = {
|
|||||||
filterSettings: FilterSettings;
|
filterSettings: FilterSettings;
|
||||||
name: string;
|
name: string;
|
||||||
runStatus?: WorkflowRunStepStatus;
|
runStatus?: WorkflowRunStepStatus;
|
||||||
isEdgeEditable: boolean;
|
edgeExecutionStatus?: StepStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowDiagramDefaultEdgeData = {
|
export type WorkflowDiagramDefaultEdgeData = {
|
||||||
edgeType: 'default';
|
edgeType: 'default';
|
||||||
isEdgeEditable: boolean;
|
edgeExecutionStatus?: StepStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowDiagramEdgeData =
|
export type WorkflowDiagramEdgeData =
|
||||||
@ -86,4 +87,14 @@ export type WorkflowDiagramNodeType =
|
|||||||
| 'empty-trigger'
|
| 'empty-trigger'
|
||||||
| 'create-step';
|
| 'create-step';
|
||||||
|
|
||||||
export type WorkflowDiagramEdgeType = 'default' | 'success';
|
export type WorkflowDiagramEdgeType =
|
||||||
|
| 'blank'
|
||||||
|
| 'filtering-disabled--editable'
|
||||||
|
| 'filtering-disabled--readonly'
|
||||||
|
| 'filtering-disabled--run'
|
||||||
|
| 'empty-filter--editable'
|
||||||
|
| 'empty-filter--readonly'
|
||||||
|
| 'empty-filter--run'
|
||||||
|
| 'filter--editable'
|
||||||
|
| 'filter--readonly'
|
||||||
|
| 'filter--run';
|
||||||
|
|||||||
@ -53,7 +53,11 @@ describe('addCreateStepNodes', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const diagramInitial = generateWorkflowDiagram({ trigger, steps });
|
const diagramInitial = generateWorkflowDiagram({
|
||||||
|
trigger,
|
||||||
|
steps,
|
||||||
|
defaultEdgeType: 'empty-filter--editable',
|
||||||
|
});
|
||||||
|
|
||||||
expect(diagramInitial.nodes).toHaveLength(3);
|
expect(diagramInitial.nodes).toHaveLength(3);
|
||||||
expect(diagramInitial.edges).toHaveLength(2);
|
expect(diagramInitial.edges).toHaveLength(2);
|
||||||
|
|||||||
@ -1,133 +0,0 @@
|
|||||||
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
|
||||||
import { addEdgeOptions } from '../addEdgeOptions';
|
|
||||||
|
|
||||||
describe('addEdgeOptions', () => {
|
|
||||||
it('should add isEdgeEditable to all edges', () => {
|
|
||||||
const diagram: WorkflowDiagram = {
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
id: 'trigger',
|
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
data: {
|
|
||||||
nodeType: 'trigger',
|
|
||||||
triggerType: 'DATABASE_EVENT',
|
|
||||||
name: 'Company Created',
|
|
||||||
icon: 'IconPlus',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'action-1',
|
|
||||||
position: { x: 0, y: 100 },
|
|
||||||
data: {
|
|
||||||
nodeType: 'action',
|
|
||||||
actionType: 'CREATE_RECORD',
|
|
||||||
name: 'Create Company',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
edges: [
|
|
||||||
{
|
|
||||||
id: 'edge-1',
|
|
||||||
source: 'trigger',
|
|
||||||
target: 'action-1',
|
|
||||||
data: {
|
|
||||||
edgeType: 'default',
|
|
||||||
isEdgeEditable: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'edge-2',
|
|
||||||
source: 'action-1',
|
|
||||||
target: 'action-2',
|
|
||||||
data: {
|
|
||||||
edgeType: 'default',
|
|
||||||
isEdgeEditable: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = addEdgeOptions(diagram);
|
|
||||||
|
|
||||||
expect(result.nodes).toEqual(diagram.nodes);
|
|
||||||
expect(result.edges).toHaveLength(2);
|
|
||||||
|
|
||||||
expect(result.edges[0]).toEqual({
|
|
||||||
id: 'edge-1',
|
|
||||||
source: 'trigger',
|
|
||||||
target: 'action-1',
|
|
||||||
data: {
|
|
||||||
edgeType: 'default',
|
|
||||||
isEdgeEditable: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.edges[1]).toEqual({
|
|
||||||
id: 'edge-2',
|
|
||||||
source: 'action-1',
|
|
||||||
target: 'action-2',
|
|
||||||
data: {
|
|
||||||
edgeType: 'default',
|
|
||||||
isEdgeEditable: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle empty edges array', () => {
|
|
||||||
const diagram: WorkflowDiagram = {
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
id: 'trigger',
|
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
data: {
|
|
||||||
nodeType: 'trigger',
|
|
||||||
triggerType: 'DATABASE_EVENT',
|
|
||||||
name: 'Company Created',
|
|
||||||
icon: 'IconPlus',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
edges: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = addEdgeOptions(diagram);
|
|
||||||
|
|
||||||
expect(result.nodes).toEqual(diagram.nodes);
|
|
||||||
expect(result.edges).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle edges without existing data property', () => {
|
|
||||||
const diagram: WorkflowDiagram = {
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
id: 'trigger',
|
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
data: {
|
|
||||||
nodeType: 'trigger',
|
|
||||||
triggerType: 'MANUAL',
|
|
||||||
name: 'Manual Trigger',
|
|
||||||
icon: 'IconClick',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'action-1',
|
|
||||||
position: { x: 0, y: 100 },
|
|
||||||
data: {
|
|
||||||
nodeType: 'action',
|
|
||||||
actionType: 'SEND_EMAIL',
|
|
||||||
name: 'Send Email',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
edges: [
|
|
||||||
{
|
|
||||||
id: 'edge-1',
|
|
||||||
source: 'trigger',
|
|
||||||
target: 'action-1',
|
|
||||||
} as any,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(() => addEdgeOptions(diagram)).toThrow('Edge data must be defined');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -13,7 +13,11 @@ describe('generateWorkflowDiagram', () => {
|
|||||||
};
|
};
|
||||||
const steps: WorkflowStep[] = [];
|
const steps: WorkflowStep[] = [];
|
||||||
|
|
||||||
const result = generateWorkflowDiagram({ trigger, steps });
|
const result = generateWorkflowDiagram({
|
||||||
|
trigger,
|
||||||
|
steps,
|
||||||
|
defaultEdgeType: 'empty-filter--editable',
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.nodes).toHaveLength(1);
|
expect(result.nodes).toHaveLength(1);
|
||||||
expect(result.edges).toHaveLength(0);
|
expect(result.edges).toHaveLength(0);
|
||||||
@ -75,7 +79,11 @@ describe('generateWorkflowDiagram', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = generateWorkflowDiagram({ trigger, steps });
|
const result = generateWorkflowDiagram({
|
||||||
|
trigger,
|
||||||
|
steps,
|
||||||
|
defaultEdgeType: 'empty-filter--editable',
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.nodes).toHaveLength(steps.length + 1); // All steps + trigger
|
expect(result.nodes).toHaveLength(steps.length + 1); // All steps + trigger
|
||||||
expect(result.edges).toHaveLength(steps.length - 1 + 1); // Edges are one less than nodes + the edge from the trigger to the first node
|
expect(result.edges).toHaveLength(steps.length - 1 + 1); // Edges are one less than nodes + the edge from the trigger to the first node
|
||||||
@ -143,7 +151,11 @@ describe('generateWorkflowDiagram', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = generateWorkflowDiagram({ trigger, steps });
|
const result = generateWorkflowDiagram({
|
||||||
|
trigger,
|
||||||
|
steps,
|
||||||
|
defaultEdgeType: 'empty-filter--editable',
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.edges.length).toEqual(2);
|
expect(result.edges.length).toEqual(2);
|
||||||
expect(result.nodes.length).toEqual(3);
|
expect(result.nodes.length).toEqual(3);
|
||||||
@ -205,7 +217,11 @@ describe('generateWorkflowDiagram', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = generateWorkflowDiagram({ trigger, steps });
|
const result = generateWorkflowDiagram({
|
||||||
|
trigger,
|
||||||
|
steps,
|
||||||
|
defaultEdgeType: 'empty-filter--editable',
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.edges.length).toEqual(2);
|
expect(result.edges.length).toEqual(2);
|
||||||
expect(result.nodes.length).toEqual(3);
|
expect(result.nodes.length).toEqual(3);
|
||||||
@ -286,7 +302,11 @@ describe('generateWorkflowDiagram', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = generateWorkflowDiagram({ trigger, steps });
|
const result = generateWorkflowDiagram({
|
||||||
|
trigger,
|
||||||
|
steps,
|
||||||
|
defaultEdgeType: 'empty-filter--editable',
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.edges.length).toEqual(4);
|
expect(result.edges.length).toEqual(4);
|
||||||
expect(result.nodes.length).toEqual(4);
|
expect(result.nodes.length).toEqual(4);
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
|
import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||||
import { getUuidV4Mock } from '~/testing/utils/getUuidV4Mock';
|
import { getUuidV4Mock } from '~/testing/utils/getUuidV4Mock';
|
||||||
import { generateWorkflowRunDiagram } from '../generateWorkflowRunDiagram';
|
import { generateWorkflowRunDiagram } from '../generateWorkflowRunDiagram';
|
||||||
import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
|
||||||
|
|
||||||
jest.mock('uuid', () => ({
|
jest.mock('uuid', () => ({
|
||||||
v4: getUuidV4Mock(),
|
v4: getUuidV4Mock(),
|
||||||
@ -100,6 +100,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
trigger,
|
trigger,
|
||||||
steps,
|
steps,
|
||||||
stepInfos,
|
stepInfos,
|
||||||
|
isWorkflowFilteringEnabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
@ -108,22 +109,22 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"edges": [
|
"edges": [
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
|
"edgeExecutionStatus": "SUCCESS",
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-0",
|
"id": "8f3b2121-f194-4ba4-9fbf-0",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "trigger",
|
"source": "trigger",
|
||||||
"target": "step1",
|
"target": "step1",
|
||||||
"type": "success",
|
"type": "empty-filter--run",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
|
"edgeExecutionStatus": "FAILED",
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-1",
|
"id": "8f3b2121-f194-4ba4-9fbf-1",
|
||||||
@ -132,11 +133,12 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "step1",
|
"source": "step1",
|
||||||
"target": "step2",
|
"target": "step2",
|
||||||
|
"type": "empty-filter--run",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
|
"edgeExecutionStatus": "NOT_STARTED",
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-2",
|
"id": "8f3b2121-f194-4ba4-9fbf-2",
|
||||||
@ -145,6 +147,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "step2",
|
"source": "step2",
|
||||||
"target": "step3",
|
"target": "step3",
|
||||||
|
"type": "empty-filter--run",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"nodes": [
|
"nodes": [
|
||||||
@ -301,6 +304,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
trigger,
|
trigger,
|
||||||
steps,
|
steps,
|
||||||
stepInfos,
|
stepInfos,
|
||||||
|
isWorkflowFilteringEnabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
@ -309,45 +313,45 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"edges": [
|
"edges": [
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
|
"edgeExecutionStatus": "SUCCESS",
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-3",
|
"id": "8f3b2121-f194-4ba4-9fbf-3",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "trigger",
|
"source": "trigger",
|
||||||
"target": "step1",
|
"target": "step1",
|
||||||
"type": "success",
|
"type": "empty-filter--run",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
|
"edgeExecutionStatus": "SUCCESS",
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-4",
|
"id": "8f3b2121-f194-4ba4-9fbf-4",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "step1",
|
"source": "step1",
|
||||||
"target": "step2",
|
"target": "step2",
|
||||||
"type": "success",
|
"type": "empty-filter--run",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
|
"edgeExecutionStatus": "SUCCESS",
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-5",
|
"id": "8f3b2121-f194-4ba4-9fbf-5",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "step2",
|
"source": "step2",
|
||||||
"target": "step3",
|
"target": "step3",
|
||||||
"type": "success",
|
"type": "empty-filter--run",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"nodes": [
|
"nodes": [
|
||||||
@ -504,6 +508,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
trigger,
|
trigger,
|
||||||
steps,
|
steps,
|
||||||
stepInfos,
|
stepInfos,
|
||||||
|
isWorkflowFilteringEnabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
@ -512,22 +517,22 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"edges": [
|
"edges": [
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
|
"edgeExecutionStatus": "SUCCESS",
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-6",
|
"id": "8f3b2121-f194-4ba4-9fbf-6",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "trigger",
|
"source": "trigger",
|
||||||
"target": "step1",
|
"target": "step1",
|
||||||
"type": "success",
|
"type": "empty-filter--run",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
|
"edgeExecutionStatus": "RUNNING",
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-7",
|
"id": "8f3b2121-f194-4ba4-9fbf-7",
|
||||||
@ -536,11 +541,12 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "step1",
|
"source": "step1",
|
||||||
"target": "step2",
|
"target": "step2",
|
||||||
|
"type": "empty-filter--run",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
|
"edgeExecutionStatus": "NOT_STARTED",
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-8",
|
"id": "8f3b2121-f194-4ba4-9fbf-8",
|
||||||
@ -549,6 +555,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "step2",
|
"source": "step2",
|
||||||
"target": "step3",
|
"target": "step3",
|
||||||
|
"type": "empty-filter--run",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"nodes": [
|
"nodes": [
|
||||||
@ -724,6 +731,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
trigger,
|
trigger,
|
||||||
steps,
|
steps,
|
||||||
stepInfos,
|
stepInfos,
|
||||||
|
isWorkflowFilteringEnabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
@ -732,36 +740,36 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"edges": [
|
"edges": [
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
|
"edgeExecutionStatus": "SUCCESS",
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-9",
|
"id": "8f3b2121-f194-4ba4-9fbf-9",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "trigger",
|
"source": "trigger",
|
||||||
"target": "step1",
|
"target": "step1",
|
||||||
"type": "success",
|
"type": "empty-filter--run",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
|
"edgeExecutionStatus": "SUCCESS",
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-10",
|
"id": "8f3b2121-f194-4ba4-9fbf-10",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "step1",
|
"source": "step1",
|
||||||
"target": "step2",
|
"target": "step2",
|
||||||
"type": "success",
|
"type": "empty-filter--run",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
|
"edgeExecutionStatus": "RUNNING",
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-11",
|
"id": "8f3b2121-f194-4ba4-9fbf-11",
|
||||||
@ -770,11 +778,12 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "step2",
|
"source": "step2",
|
||||||
"target": "step3",
|
"target": "step3",
|
||||||
|
"type": "empty-filter--run",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
|
"edgeExecutionStatus": "NOT_STARTED",
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-12",
|
"id": "8f3b2121-f194-4ba4-9fbf-12",
|
||||||
@ -783,6 +792,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "step3",
|
"source": "step3",
|
||||||
"target": "step4",
|
"target": "step4",
|
||||||
|
"type": "empty-filter--run",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"nodes": [
|
"nodes": [
|
||||||
@ -918,6 +928,7 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
trigger,
|
trigger,
|
||||||
steps,
|
steps,
|
||||||
stepInfos,
|
stepInfos,
|
||||||
|
isWorkflowFilteringEnabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
@ -926,17 +937,17 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
"edges": [
|
"edges": [
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
|
"edgeExecutionStatus": "SUCCESS",
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-13",
|
"id": "8f3b2121-f194-4ba4-9fbf-13",
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "trigger",
|
"source": "trigger",
|
||||||
"target": "step1",
|
"target": "step1",
|
||||||
"type": "success",
|
"type": "empty-filter--run",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"nodes": [
|
"nodes": [
|
||||||
|
|||||||
@ -0,0 +1,207 @@
|
|||||||
|
import { WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||||
|
import { getWorkflowDiagramTriggerNode } from '../getWorkflowDiagramTriggerNode';
|
||||||
|
|
||||||
|
describe('getWorkflowDiagramTriggerNode', () => {
|
||||||
|
describe('MANUAL trigger type', () => {
|
||||||
|
it('should create trigger node with default label when trigger name is not provided', () => {
|
||||||
|
const trigger: WorkflowTrigger = {
|
||||||
|
type: 'MANUAL',
|
||||||
|
settings: {
|
||||||
|
objectType: 'person',
|
||||||
|
outputSchema: {},
|
||||||
|
icon: 'IconUser',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkflowDiagramTriggerNode({ trigger });
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"icon": "IconHandMove",
|
||||||
|
"name": "Manual trigger",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"triggerType": "MANUAL",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CRON trigger type', () => {
|
||||||
|
it('should create trigger node for CRON trigger', () => {
|
||||||
|
const trigger: WorkflowTrigger = {
|
||||||
|
type: 'CRON',
|
||||||
|
settings: {
|
||||||
|
type: 'DAYS',
|
||||||
|
schedule: {
|
||||||
|
day: 1,
|
||||||
|
hour: 9,
|
||||||
|
minute: 0,
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkflowDiagramTriggerNode({ trigger });
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"icon": "IconClock",
|
||||||
|
"name": "On a schedule",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"triggerType": "CRON",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('WEBHOOK trigger type', () => {
|
||||||
|
it('should create trigger node for WEBHOOK trigger', () => {
|
||||||
|
const trigger: WorkflowTrigger = {
|
||||||
|
type: 'WEBHOOK',
|
||||||
|
settings: {
|
||||||
|
httpMethod: 'POST',
|
||||||
|
outputSchema: {},
|
||||||
|
expectedBody: {},
|
||||||
|
authentication: 'API_KEY',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkflowDiagramTriggerNode({ trigger });
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"icon": "IconWebhook",
|
||||||
|
"name": "Webhook",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"triggerType": "WEBHOOK",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DATABASE_EVENT trigger type', () => {
|
||||||
|
it('should create trigger node for DATABASE_EVENT trigger with created event', () => {
|
||||||
|
const trigger: WorkflowTrigger = {
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
settings: {
|
||||||
|
eventName: 'company.created',
|
||||||
|
outputSchema: {},
|
||||||
|
objectType: 'company',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkflowDiagramTriggerNode({ trigger });
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"icon": "IconPlaylistAdd",
|
||||||
|
"name": "Record is created",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"triggerType": "DATABASE_EVENT",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create trigger node with empty label for DATABASE_EVENT trigger with unknown event', () => {
|
||||||
|
const trigger: WorkflowTrigger = {
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
settings: {
|
||||||
|
eventName: 'company.unknownEvent',
|
||||||
|
outputSchema: {},
|
||||||
|
objectType: 'company',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkflowDiagramTriggerNode({ trigger });
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"icon": undefined,
|
||||||
|
"name": "",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"triggerType": "DATABASE_EVENT",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('custom trigger name', () => {
|
||||||
|
it('should use custom name when trigger name is provided', () => {
|
||||||
|
const trigger: WorkflowTrigger = {
|
||||||
|
type: 'MANUAL',
|
||||||
|
name: 'Custom Trigger Name',
|
||||||
|
settings: {
|
||||||
|
objectType: 'person',
|
||||||
|
outputSchema: {},
|
||||||
|
icon: 'IconUser',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkflowDiagramTriggerNode({ trigger });
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"icon": "IconHandMove",
|
||||||
|
"name": "Custom Trigger Name",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"triggerType": "MANUAL",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('default case', () => {
|
||||||
|
it('should throw error for unsupported trigger type', () => {
|
||||||
|
const trigger = {
|
||||||
|
type: 'UNSUPPORTED_TYPE',
|
||||||
|
settings: {},
|
||||||
|
} as unknown as WorkflowTrigger;
|
||||||
|
|
||||||
|
expect(() => getWorkflowDiagramTriggerNode({ trigger })).toThrow(
|
||||||
|
'Expected the trigger "{"type":"UNSUPPORTED_TYPE","settings":{}}" to be supported.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import { WorkflowRunStatus } from '@/workflow/types/Workflow';
|
||||||
|
import { getWorkflowRunStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps';
|
||||||
|
|
||||||
|
describe('getWorkflowRunStatusTagProps', () => {
|
||||||
|
it('should return gray color and "Not started" text for NOT_STARTED status', () => {
|
||||||
|
const result = getWorkflowRunStatusTagProps({
|
||||||
|
workflowRunStatus: 'NOT_STARTED',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"color": "gray",
|
||||||
|
"text": "Not started",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return yellow color and "Running" text for RUNNING status', () => {
|
||||||
|
const result = getWorkflowRunStatusTagProps({
|
||||||
|
workflowRunStatus: 'RUNNING',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"color": "yellow",
|
||||||
|
"text": "Running",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return green color and "Completed" text for COMPLETED status', () => {
|
||||||
|
const result = getWorkflowRunStatusTagProps({
|
||||||
|
workflowRunStatus: 'COMPLETED',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"text": "Completed",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return blue color and "Enqueued" text for ENQUEUED status', () => {
|
||||||
|
const result = getWorkflowRunStatusTagProps({
|
||||||
|
workflowRunStatus: 'ENQUEUED',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"color": "blue",
|
||||||
|
"text": "Enqueued",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return red color and "Failed" text for FAILED status', () => {
|
||||||
|
const result = getWorkflowRunStatusTagProps({
|
||||||
|
workflowRunStatus: 'FAILED',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"text": "Failed",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return red color and "Failed" text for any unknown status (default case)', () => {
|
||||||
|
const result = getWorkflowRunStatusTagProps({
|
||||||
|
workflowRunStatus: 'UNKNOWN_STATUS' as WorkflowRunStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"text": "Failed",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -7,7 +7,11 @@ jest.mock('uuid', () => ({
|
|||||||
|
|
||||||
describe('getWorkflowVersionDiagram', () => {
|
describe('getWorkflowVersionDiagram', () => {
|
||||||
it('returns an empty diagram if the provided workflow version', () => {
|
it('returns an empty diagram if the provided workflow version', () => {
|
||||||
const result = getWorkflowVersionDiagram(undefined);
|
const result = getWorkflowVersionDiagram({
|
||||||
|
workflowVersion: undefined,
|
||||||
|
isEditable: true,
|
||||||
|
isWorkflowFilteringEnabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
@ -19,15 +23,19 @@ describe('getWorkflowVersionDiagram', () => {
|
|||||||
|
|
||||||
it('returns a diagram with an empty-trigger node if the provided workflow version has no trigger', () => {
|
it('returns a diagram with an empty-trigger node if the provided workflow version has no trigger', () => {
|
||||||
const result = getWorkflowVersionDiagram({
|
const result = getWorkflowVersionDiagram({
|
||||||
__typename: 'WorkflowVersion',
|
workflowVersion: {
|
||||||
status: 'ACTIVE',
|
__typename: 'WorkflowVersion',
|
||||||
createdAt: '',
|
status: 'ACTIVE',
|
||||||
id: '1',
|
createdAt: '',
|
||||||
name: '',
|
id: '1',
|
||||||
steps: [],
|
name: '',
|
||||||
trigger: null,
|
steps: [],
|
||||||
updatedAt: '',
|
trigger: null,
|
||||||
workflowId: '',
|
updatedAt: '',
|
||||||
|
workflowId: '',
|
||||||
|
},
|
||||||
|
isEditable: true,
|
||||||
|
isWorkflowFilteringEnabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
@ -52,19 +60,23 @@ describe('getWorkflowVersionDiagram', () => {
|
|||||||
|
|
||||||
it('returns a diagram with only a trigger node if the provided workflow version has no steps', () => {
|
it('returns a diagram with only a trigger node if the provided workflow version has no steps', () => {
|
||||||
const result = getWorkflowVersionDiagram({
|
const result = getWorkflowVersionDiagram({
|
||||||
__typename: 'WorkflowVersion',
|
workflowVersion: {
|
||||||
status: 'ACTIVE',
|
__typename: 'WorkflowVersion',
|
||||||
createdAt: '',
|
status: 'ACTIVE',
|
||||||
id: '1',
|
createdAt: '',
|
||||||
name: '',
|
id: '1',
|
||||||
steps: null,
|
name: '',
|
||||||
trigger: {
|
steps: null,
|
||||||
name: 'Record is created',
|
trigger: {
|
||||||
settings: { eventName: 'company.created', outputSchema: {} },
|
name: 'Record is created',
|
||||||
type: 'DATABASE_EVENT',
|
settings: { eventName: 'company.created', outputSchema: {} },
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
},
|
||||||
|
updatedAt: '',
|
||||||
|
workflowId: '',
|
||||||
},
|
},
|
||||||
updatedAt: '',
|
isEditable: true,
|
||||||
workflowId: '',
|
isWorkflowFilteringEnabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
@ -91,38 +103,42 @@ describe('getWorkflowVersionDiagram', () => {
|
|||||||
|
|
||||||
it('returns the diagram for the last version', () => {
|
it('returns the diagram for the last version', () => {
|
||||||
const result = getWorkflowVersionDiagram({
|
const result = getWorkflowVersionDiagram({
|
||||||
__typename: 'WorkflowVersion',
|
workflowVersion: {
|
||||||
status: 'ACTIVE',
|
__typename: 'WorkflowVersion',
|
||||||
createdAt: '',
|
status: 'ACTIVE',
|
||||||
id: '1',
|
createdAt: '',
|
||||||
name: '',
|
id: '1',
|
||||||
steps: [
|
name: '',
|
||||||
{
|
steps: [
|
||||||
id: 'step-1',
|
{
|
||||||
name: '',
|
id: 'step-1',
|
||||||
settings: {
|
name: '',
|
||||||
errorHandlingOptions: {
|
settings: {
|
||||||
retryOnFailure: { value: true },
|
errorHandlingOptions: {
|
||||||
continueOnFailure: { value: false },
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
},
|
},
|
||||||
input: {
|
type: 'CODE',
|
||||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
valid: true,
|
||||||
serverlessFunctionVersion: '1',
|
|
||||||
serverlessFunctionInput: {},
|
|
||||||
},
|
|
||||||
outputSchema: {},
|
|
||||||
},
|
},
|
||||||
type: 'CODE',
|
],
|
||||||
valid: true,
|
trigger: {
|
||||||
|
name: 'Company created',
|
||||||
|
settings: { eventName: 'company.created', outputSchema: {} },
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
},
|
},
|
||||||
],
|
updatedAt: '',
|
||||||
trigger: {
|
workflowId: '',
|
||||||
name: 'Company created',
|
|
||||||
settings: { eventName: 'company.created', outputSchema: {} },
|
|
||||||
type: 'DATABASE_EVENT',
|
|
||||||
},
|
},
|
||||||
updatedAt: '',
|
isEditable: true,
|
||||||
workflowId: '',
|
isWorkflowFilteringEnabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
@ -131,7 +147,6 @@ describe('getWorkflowVersionDiagram', () => {
|
|||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"edgeType": "default",
|
"edgeType": "default",
|
||||||
"isEdgeEditable": false,
|
|
||||||
},
|
},
|
||||||
"deletable": false,
|
"deletable": false,
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-0",
|
"id": "8f3b2121-f194-4ba4-9fbf-0",
|
||||||
@ -140,6 +155,7 @@ describe('getWorkflowVersionDiagram', () => {
|
|||||||
"selectable": false,
|
"selectable": false,
|
||||||
"source": "trigger",
|
"source": "trigger",
|
||||||
"target": "step-1",
|
"target": "step-1",
|
||||||
|
"type": "empty-filter--editable",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"nodes": [
|
"nodes": [
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
|
||||||
|
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
|
||||||
|
|
||||||
|
describe('getWorkflowVersionStatusTagProps', () => {
|
||||||
|
it('should return gray color and "Archived" text for ARCHIVED status', () => {
|
||||||
|
const result = getWorkflowVersionStatusTagProps({
|
||||||
|
workflowVersionStatus: 'ARCHIVED',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"color": "gray",
|
||||||
|
"text": "Archived",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return yellow color and "Draft" text for DRAFT status', () => {
|
||||||
|
const result = getWorkflowVersionStatusTagProps({
|
||||||
|
workflowVersionStatus: 'DRAFT',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"color": "yellow",
|
||||||
|
"text": "Draft",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return green color and "Active" text for ACTIVE status', () => {
|
||||||
|
const result = getWorkflowVersionStatusTagProps({
|
||||||
|
workflowVersionStatus: 'ACTIVE',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"text": "Active",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return gray color and "Deactivated" text for DEACTIVATED status', () => {
|
||||||
|
const result = getWorkflowVersionStatusTagProps({
|
||||||
|
workflowVersionStatus: 'DEACTIVATED',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"color": "gray",
|
||||||
|
"text": "Deactivated",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return gray color and "Deactivated" text for any unknown status (default case)', () => {
|
||||||
|
const result = getWorkflowVersionStatusTagProps({
|
||||||
|
workflowVersionStatus: 'UNKNOWN_STATUS' as WorkflowVersionStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"color": "gray",
|
||||||
|
"text": "Deactivated",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -27,13 +27,16 @@ describe('transformFilterNodesAsEdges', () => {
|
|||||||
target: 'C',
|
target: 'C',
|
||||||
data: {
|
data: {
|
||||||
edgeType: 'default',
|
edgeType: 'default',
|
||||||
isEdgeEditable: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = transformFilterNodesAsEdges(diagram);
|
const result = transformFilterNodesAsEdges({
|
||||||
|
nodes: diagram.nodes,
|
||||||
|
edges: diagram.edges,
|
||||||
|
defaultFilterEdgeType: 'filter--editable',
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.nodes).toEqual(diagram.nodes);
|
expect(result.nodes).toEqual(diagram.nodes);
|
||||||
expect(result.edges).toEqual(diagram.edges);
|
expect(result.edges).toEqual(diagram.edges);
|
||||||
@ -67,18 +70,22 @@ describe('transformFilterNodesAsEdges', () => {
|
|||||||
id: 'A-B',
|
id: 'A-B',
|
||||||
source: 'A',
|
source: 'A',
|
||||||
target: 'B',
|
target: 'B',
|
||||||
data: { edgeType: 'default', isEdgeEditable: true },
|
data: { edgeType: 'default' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'B-C',
|
id: 'B-C',
|
||||||
source: 'B',
|
source: 'B',
|
||||||
target: 'C',
|
target: 'C',
|
||||||
data: { edgeType: 'default', isEdgeEditable: true },
|
data: { edgeType: 'default' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = transformFilterNodesAsEdges(diagram);
|
const result = transformFilterNodesAsEdges({
|
||||||
|
nodes: diagram.nodes,
|
||||||
|
edges: diagram.edges,
|
||||||
|
defaultFilterEdgeType: 'filter--editable',
|
||||||
|
});
|
||||||
|
|
||||||
// Should only have nodes A and C
|
// Should only have nodes A and C
|
||||||
expect(result.nodes).toEqual([
|
expect(result.nodes).toEqual([
|
||||||
@ -98,6 +105,7 @@ describe('transformFilterNodesAsEdges', () => {
|
|||||||
expect(result.edges).toHaveLength(1);
|
expect(result.edges).toHaveLength(1);
|
||||||
expect(result.edges[0]).toEqual({
|
expect(result.edges[0]).toEqual({
|
||||||
id: 'A-C-filter-B',
|
id: 'A-C-filter-B',
|
||||||
|
type: 'filter--editable',
|
||||||
source: 'A',
|
source: 'A',
|
||||||
target: 'C',
|
target: 'C',
|
||||||
data: {
|
data: {
|
||||||
@ -106,7 +114,6 @@ describe('transformFilterNodesAsEdges', () => {
|
|||||||
name: 'Filter B',
|
name: 'Filter B',
|
||||||
runStatus: undefined,
|
runStatus: undefined,
|
||||||
filterSettings: {},
|
filterSettings: {},
|
||||||
isEdgeEditable: false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -153,30 +160,34 @@ describe('transformFilterNodesAsEdges', () => {
|
|||||||
id: 'A-B1',
|
id: 'A-B1',
|
||||||
source: 'A',
|
source: 'A',
|
||||||
target: 'B1',
|
target: 'B1',
|
||||||
data: { edgeType: 'default', isEdgeEditable: true },
|
data: { edgeType: 'default' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'B1-C',
|
id: 'B1-C',
|
||||||
source: 'B1',
|
source: 'B1',
|
||||||
target: 'C',
|
target: 'C',
|
||||||
data: { edgeType: 'default', isEdgeEditable: true },
|
data: { edgeType: 'default' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'C-B2',
|
id: 'C-B2',
|
||||||
source: 'C',
|
source: 'C',
|
||||||
target: 'B2',
|
target: 'B2',
|
||||||
data: { edgeType: 'default', isEdgeEditable: true },
|
data: { edgeType: 'default' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'B2-D',
|
id: 'B2-D',
|
||||||
source: 'B2',
|
source: 'B2',
|
||||||
target: 'D',
|
target: 'D',
|
||||||
data: { edgeType: 'default', isEdgeEditable: true },
|
data: { edgeType: 'default' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = transformFilterNodesAsEdges(diagram);
|
const result = transformFilterNodesAsEdges({
|
||||||
|
nodes: diagram.nodes,
|
||||||
|
edges: diagram.edges,
|
||||||
|
defaultFilterEdgeType: 'filter--editable',
|
||||||
|
});
|
||||||
|
|
||||||
// Should only have nodes A, C, and D
|
// Should only have nodes A, C, and D
|
||||||
expect(result.nodes).toHaveLength(3);
|
expect(result.nodes).toHaveLength(3);
|
||||||
@ -192,6 +203,7 @@ describe('transformFilterNodesAsEdges', () => {
|
|||||||
);
|
);
|
||||||
expect(edgeAC).toEqual({
|
expect(edgeAC).toEqual({
|
||||||
id: 'A-C-filter-B1',
|
id: 'A-C-filter-B1',
|
||||||
|
type: 'filter--editable',
|
||||||
source: 'A',
|
source: 'A',
|
||||||
target: 'C',
|
target: 'C',
|
||||||
data: {
|
data: {
|
||||||
@ -200,7 +212,6 @@ describe('transformFilterNodesAsEdges', () => {
|
|||||||
runStatus: undefined,
|
runStatus: undefined,
|
||||||
stepId: 'B1',
|
stepId: 'B1',
|
||||||
filterSettings: {},
|
filterSettings: {},
|
||||||
isEdgeEditable: false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -209,6 +220,7 @@ describe('transformFilterNodesAsEdges', () => {
|
|||||||
);
|
);
|
||||||
expect(edgeCD).toEqual({
|
expect(edgeCD).toEqual({
|
||||||
id: 'C-D-filter-B2',
|
id: 'C-D-filter-B2',
|
||||||
|
type: 'filter--editable',
|
||||||
source: 'C',
|
source: 'C',
|
||||||
target: 'D',
|
target: 'D',
|
||||||
data: {
|
data: {
|
||||||
@ -217,7 +229,6 @@ describe('transformFilterNodesAsEdges', () => {
|
|||||||
runStatus: undefined,
|
runStatus: undefined,
|
||||||
stepId: 'B2',
|
stepId: 'B2',
|
||||||
filterSettings: {},
|
filterSettings: {},
|
||||||
isEdgeEditable: false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -241,12 +252,16 @@ describe('transformFilterNodesAsEdges', () => {
|
|||||||
id: 'A-B',
|
id: 'A-B',
|
||||||
source: 'A',
|
source: 'A',
|
||||||
target: 'B',
|
target: 'B',
|
||||||
data: { edgeType: 'default', isEdgeEditable: true },
|
data: { edgeType: 'default' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = transformFilterNodesAsEdges(diagram);
|
const result = transformFilterNodesAsEdges({
|
||||||
|
nodes: diagram.nodes,
|
||||||
|
edges: diagram.edges,
|
||||||
|
defaultFilterEdgeType: 'filter--editable',
|
||||||
|
});
|
||||||
|
|
||||||
// Should only have node A (filter node B is removed)
|
// Should only have node A (filter node B is removed)
|
||||||
expect(result.nodes).toEqual([
|
expect(result.nodes).toEqual([
|
||||||
@ -293,18 +308,22 @@ describe('transformFilterNodesAsEdges', () => {
|
|||||||
id: 'trigger-B',
|
id: 'trigger-B',
|
||||||
source: 'trigger',
|
source: 'trigger',
|
||||||
target: 'B',
|
target: 'B',
|
||||||
data: { edgeType: 'default', isEdgeEditable: true },
|
data: { edgeType: 'default' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'B-C',
|
id: 'B-C',
|
||||||
source: 'B',
|
source: 'B',
|
||||||
target: 'C',
|
target: 'C',
|
||||||
data: { edgeType: 'default', isEdgeEditable: true },
|
data: { edgeType: 'default' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = transformFilterNodesAsEdges(diagram);
|
const result = transformFilterNodesAsEdges({
|
||||||
|
nodes: diagram.nodes,
|
||||||
|
edges: diagram.edges,
|
||||||
|
defaultFilterEdgeType: 'filter--editable',
|
||||||
|
});
|
||||||
|
|
||||||
// Should have trigger and C nodes
|
// Should have trigger and C nodes
|
||||||
expect(result.nodes).toEqual([
|
expect(result.nodes).toEqual([
|
||||||
@ -328,6 +347,7 @@ describe('transformFilterNodesAsEdges', () => {
|
|||||||
expect(result.edges).toEqual([
|
expect(result.edges).toEqual([
|
||||||
{
|
{
|
||||||
id: 'trigger-C-filter-B',
|
id: 'trigger-C-filter-B',
|
||||||
|
type: 'filter--editable',
|
||||||
source: 'trigger',
|
source: 'trigger',
|
||||||
target: 'C',
|
target: 'C',
|
||||||
data: {
|
data: {
|
||||||
@ -336,7 +356,6 @@ describe('transformFilterNodesAsEdges', () => {
|
|||||||
runStatus: undefined,
|
runStatus: undefined,
|
||||||
stepId: 'B',
|
stepId: 'B',
|
||||||
filterSettings: {},
|
filterSettings: {},
|
||||||
isEdgeEditable: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workf
|
|||||||
import {
|
import {
|
||||||
WorkflowDiagram,
|
WorkflowDiagram,
|
||||||
WorkflowDiagramEdge,
|
WorkflowDiagramEdge,
|
||||||
|
WorkflowDiagramEdgeType,
|
||||||
WorkflowDiagramNode,
|
WorkflowDiagramNode,
|
||||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
@ -31,6 +32,7 @@ export const addCreateStepNodes = ({ nodes, edges }: WorkflowDiagram) => {
|
|||||||
|
|
||||||
updatedEdges.push({
|
updatedEdges.push({
|
||||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||||
|
type: 'blank' as WorkflowDiagramEdgeType,
|
||||||
id: v4(),
|
id: v4(),
|
||||||
source: node.id,
|
source: node.id,
|
||||||
target: newCreateStepNode.id,
|
target: newCreateStepNode.id,
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
|
|
||||||
export const addEdgeOptions = ({
|
|
||||||
nodes,
|
|
||||||
edges,
|
|
||||||
}: WorkflowDiagram): WorkflowDiagram => {
|
|
||||||
return {
|
|
||||||
nodes,
|
|
||||||
edges: edges.map((edge) => {
|
|
||||||
if (!isDefined(edge.data)) {
|
|
||||||
throw new Error('Edge data must be defined');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...edge,
|
|
||||||
data: {
|
|
||||||
...edge.data,
|
|
||||||
isEdgeEditable: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -6,6 +6,7 @@ import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workf
|
|||||||
import {
|
import {
|
||||||
WorkflowDiagram,
|
WorkflowDiagram,
|
||||||
WorkflowDiagramEdge,
|
WorkflowDiagramEdge,
|
||||||
|
WorkflowDiagramEdgeType,
|
||||||
WorkflowDiagramNode,
|
WorkflowDiagramNode,
|
||||||
WorkflowDiagramStepNodeData,
|
WorkflowDiagramStepNodeData,
|
||||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
@ -70,12 +71,13 @@ const groupStepsByLevel = (steps: WorkflowStep[]): WorkflowStep[][] => {
|
|||||||
export const generateWorkflowDiagram = ({
|
export const generateWorkflowDiagram = ({
|
||||||
trigger,
|
trigger,
|
||||||
steps,
|
steps,
|
||||||
|
defaultEdgeType,
|
||||||
}: {
|
}: {
|
||||||
trigger: WorkflowTrigger | undefined;
|
trigger: WorkflowTrigger | undefined;
|
||||||
steps: Array<WorkflowStep>;
|
steps: Array<WorkflowStep>;
|
||||||
|
defaultEdgeType: WorkflowDiagramEdgeType;
|
||||||
}): WorkflowDiagram => {
|
}): WorkflowDiagram => {
|
||||||
const nodes: Array<WorkflowDiagramNode> = [];
|
const nodes: Array<WorkflowDiagramNode> = [];
|
||||||
|
|
||||||
const edges: Array<WorkflowDiagramEdge> = [];
|
const edges: Array<WorkflowDiagramEdge> = [];
|
||||||
|
|
||||||
if (isDefined(trigger)) {
|
if (isDefined(trigger)) {
|
||||||
@ -112,6 +114,7 @@ export const generateWorkflowDiagram = ({
|
|||||||
for (const firstLevelStep of stepsGroupedByLevel[0] || []) {
|
for (const firstLevelStep of stepsGroupedByLevel[0] || []) {
|
||||||
edges.push({
|
edges.push({
|
||||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||||
|
type: defaultEdgeType,
|
||||||
id: v4(),
|
id: v4(),
|
||||||
source: TRIGGER_STEP_ID,
|
source: TRIGGER_STEP_ID,
|
||||||
target: firstLevelStep.id,
|
target: firstLevelStep.id,
|
||||||
@ -122,6 +125,7 @@ export const generateWorkflowDiagram = ({
|
|||||||
step.nextStepIds?.forEach((child) => {
|
step.nextStepIds?.forEach((child) => {
|
||||||
edges.push({
|
edges.push({
|
||||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||||
|
type: defaultEdgeType,
|
||||||
id: v4(),
|
id: v4(),
|
||||||
source: step.id,
|
source: step.id,
|
||||||
target: child,
|
target: child,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
|
import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||||
import { WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration';
|
|
||||||
import {
|
import {
|
||||||
|
WorkflowDiagramEdgeData,
|
||||||
|
WorkflowDiagramEdgeType,
|
||||||
WorkflowRunDiagram,
|
WorkflowRunDiagram,
|
||||||
WorkflowRunDiagramNode,
|
WorkflowRunDiagramNode,
|
||||||
WorkflowRunDiagramStepNodeData,
|
WorkflowRunDiagramStepNodeData,
|
||||||
@ -9,16 +10,18 @@ import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/gener
|
|||||||
import { isStepNode } from '@/workflow/workflow-diagram/utils/isStepNode';
|
import { isStepNode } from '@/workflow/workflow-diagram/utils/isStepNode';
|
||||||
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
|
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { WorkflowRunStepInfos, StepStatus } from 'twenty-shared/workflow';
|
import { StepStatus, WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||||
|
|
||||||
export const generateWorkflowRunDiagram = ({
|
export const generateWorkflowRunDiagram = ({
|
||||||
trigger,
|
trigger,
|
||||||
steps,
|
steps,
|
||||||
stepInfos,
|
stepInfos,
|
||||||
|
isWorkflowFilteringEnabled,
|
||||||
}: {
|
}: {
|
||||||
trigger: WorkflowTrigger;
|
trigger: WorkflowTrigger;
|
||||||
steps: Array<WorkflowStep>;
|
steps: Array<WorkflowStep>;
|
||||||
stepInfos: WorkflowRunStepInfos | undefined;
|
stepInfos: WorkflowRunStepInfos | undefined;
|
||||||
|
isWorkflowFilteringEnabled: boolean;
|
||||||
}): {
|
}): {
|
||||||
diagram: WorkflowRunDiagram;
|
diagram: WorkflowRunDiagram;
|
||||||
stepToOpenByDefault:
|
stepToOpenByDefault:
|
||||||
@ -35,9 +38,11 @@ export const generateWorkflowRunDiagram = ({
|
|||||||
}
|
}
|
||||||
| undefined = undefined;
|
| undefined = undefined;
|
||||||
|
|
||||||
const workflowDiagram = transformFilterNodesAsEdges(
|
const workflowDiagram = generateWorkflowDiagram({
|
||||||
generateWorkflowDiagram({ trigger, steps }),
|
trigger,
|
||||||
);
|
steps,
|
||||||
|
defaultEdgeType: 'filtering-disabled--readonly',
|
||||||
|
});
|
||||||
|
|
||||||
const workflowRunDiagramNodes: WorkflowRunDiagramNode[] =
|
const workflowRunDiagramNodes: WorkflowRunDiagramNode[] =
|
||||||
workflowDiagram.nodes.filter(isStepNode).map((node) => {
|
workflowDiagram.nodes.filter(isStepNode).map((node) => {
|
||||||
@ -76,30 +81,42 @@ export const generateWorkflowRunDiagram = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!isDefined(parentNode)) {
|
if (!isDefined(parentNode)) {
|
||||||
return edge;
|
throw new Error('Expected the edge to have a parent node');
|
||||||
}
|
}
|
||||||
|
|
||||||
const stepInfo = stepInfos?.[parentNode.id];
|
const stepInfo = stepInfos?.[parentNode.id];
|
||||||
|
|
||||||
if (!isDefined(stepInfo)) {
|
const edgeType: WorkflowDiagramEdgeType = isWorkflowFilteringEnabled
|
||||||
return edge;
|
? 'empty-filter--run'
|
||||||
}
|
: 'filtering-disabled--run';
|
||||||
|
|
||||||
if (stepInfo.status === 'SUCCESS') {
|
return {
|
||||||
return {
|
...edge,
|
||||||
...edge,
|
type: edgeType,
|
||||||
...WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION,
|
data: {
|
||||||
};
|
...edge.data,
|
||||||
}
|
edgeType: 'default',
|
||||||
|
edgeExecutionStatus: stepInfo?.status ?? StepStatus.NOT_STARTED,
|
||||||
return edge;
|
} satisfies WorkflowDiagramEdgeData,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!isWorkflowFilteringEnabled) {
|
||||||
|
return {
|
||||||
|
diagram: {
|
||||||
|
nodes: workflowRunDiagramNodes,
|
||||||
|
edges: workflowRunDiagramEdges,
|
||||||
|
},
|
||||||
|
stepToOpenByDefault,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
diagram: {
|
diagram: transformFilterNodesAsEdges({
|
||||||
nodes: workflowRunDiagramNodes,
|
nodes: workflowRunDiagramNodes,
|
||||||
edges: workflowRunDiagramEdges,
|
edges: workflowRunDiagramEdges,
|
||||||
},
|
defaultFilterEdgeType: 'filter--run',
|
||||||
|
}),
|
||||||
stepToOpenByDefault,
|
stepToOpenByDefault,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||||
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
import {
|
||||||
|
WorkflowDiagram,
|
||||||
|
WorkflowDiagramEdgeType,
|
||||||
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
|
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
|
||||||
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
|
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
@ -9,17 +12,47 @@ const EMPTY_DIAGRAM: WorkflowDiagram = {
|
|||||||
edges: [],
|
edges: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWorkflowVersionDiagram = (
|
const getEdgeTypeToCreateByDefault = ({
|
||||||
workflowVersion: WorkflowVersion | undefined,
|
isWorkflowFilteringEnabled,
|
||||||
): WorkflowDiagram => {
|
isEditable,
|
||||||
|
}: {
|
||||||
|
isWorkflowFilteringEnabled: boolean;
|
||||||
|
isEditable: boolean;
|
||||||
|
}): WorkflowDiagramEdgeType => {
|
||||||
|
if (isWorkflowFilteringEnabled) {
|
||||||
|
return isEditable ? 'empty-filter--editable' : 'empty-filter--readonly';
|
||||||
|
}
|
||||||
|
|
||||||
|
return isEditable
|
||||||
|
? 'filtering-disabled--editable'
|
||||||
|
: 'filtering-disabled--readonly';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWorkflowVersionDiagram = ({
|
||||||
|
workflowVersion,
|
||||||
|
isWorkflowFilteringEnabled,
|
||||||
|
isEditable,
|
||||||
|
}: {
|
||||||
|
workflowVersion: WorkflowVersion | undefined;
|
||||||
|
isWorkflowFilteringEnabled: boolean;
|
||||||
|
isEditable: boolean;
|
||||||
|
}): WorkflowDiagram => {
|
||||||
if (!isDefined(workflowVersion)) {
|
if (!isDefined(workflowVersion)) {
|
||||||
return EMPTY_DIAGRAM;
|
return EMPTY_DIAGRAM;
|
||||||
}
|
}
|
||||||
|
|
||||||
return transformFilterNodesAsEdges(
|
const diagram = generateWorkflowDiagram({
|
||||||
generateWorkflowDiagram({
|
trigger: workflowVersion.trigger ?? undefined,
|
||||||
trigger: workflowVersion.trigger ?? undefined,
|
steps: workflowVersion.steps ?? [],
|
||||||
steps: workflowVersion.steps ?? [],
|
defaultEdgeType: getEdgeTypeToCreateByDefault({
|
||||||
|
isWorkflowFilteringEnabled,
|
||||||
|
isEditable,
|
||||||
}),
|
}),
|
||||||
);
|
});
|
||||||
|
|
||||||
|
return transformFilterNodesAsEdges({
|
||||||
|
nodes: diagram.nodes,
|
||||||
|
edges: diagram.edges,
|
||||||
|
defaultFilterEdgeType: isEditable ? 'filter--editable' : 'filter--readonly',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,13 +1,22 @@
|
|||||||
import {
|
import {
|
||||||
WorkflowDiagram,
|
|
||||||
WorkflowDiagramEdge,
|
WorkflowDiagramEdge,
|
||||||
|
WorkflowDiagramEdgeType,
|
||||||
|
WorkflowDiagramNode,
|
||||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
export const transformFilterNodesAsEdges = ({
|
export const transformFilterNodesAsEdges = <
|
||||||
|
T extends WorkflowDiagramNode,
|
||||||
|
U extends WorkflowDiagramEdge,
|
||||||
|
>({
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
}: WorkflowDiagram): WorkflowDiagram => {
|
defaultFilterEdgeType,
|
||||||
|
}: {
|
||||||
|
nodes: T[];
|
||||||
|
edges: U[];
|
||||||
|
defaultFilterEdgeType: WorkflowDiagramEdgeType;
|
||||||
|
}): { nodes: T[]; edges: U[] } => {
|
||||||
const filterNodes = nodes.filter(
|
const filterNodes = nodes.filter(
|
||||||
(node) =>
|
(node) =>
|
||||||
node.data.nodeType === 'action' &&
|
node.data.nodeType === 'action' &&
|
||||||
@ -39,18 +48,19 @@ export const transformFilterNodesAsEdges = ({
|
|||||||
throw new Error('Expected the filter node to be of action type');
|
throw new Error('Expected the filter node to be of action type');
|
||||||
}
|
}
|
||||||
|
|
||||||
const newEdge: WorkflowDiagramEdge = {
|
const newEdge: U = {
|
||||||
...incomingEdge,
|
...incomingEdge,
|
||||||
|
type: defaultFilterEdgeType,
|
||||||
id: `${incomingEdge.source}-${outgoingEdge.target}-filter-${filterNode.id}`,
|
id: `${incomingEdge.source}-${outgoingEdge.target}-filter-${filterNode.id}`,
|
||||||
target: outgoingEdge.target,
|
target: outgoingEdge.target,
|
||||||
data: {
|
data: {
|
||||||
|
...incomingEdge.data,
|
||||||
edgeType: 'filter',
|
edgeType: 'filter',
|
||||||
stepId: filterNode.id,
|
stepId: filterNode.id,
|
||||||
// TODO: Get the filter settings from the filter node
|
// TODO: Get the filter settings from the filter node
|
||||||
filterSettings: {},
|
filterSettings: {},
|
||||||
name: filterNode.data.name,
|
name: filterNode.data.name,
|
||||||
runStatus: filterNode.data.runStatus,
|
runStatus: filterNode.data.runStatus,
|
||||||
isEdgeEditable: false,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,15 @@
|
|||||||
import { getCronTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getCronTriggerDefaultSettings';
|
import { getCronTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getCronTriggerDefaultSettings';
|
||||||
|
|
||||||
describe('getCronTriggerDefaultSettings', () => {
|
describe('getCronTriggerDefaultSettings', () => {
|
||||||
|
it('returns correct settings for DAYS interval', () => {
|
||||||
|
const result = getCronTriggerDefaultSettings('DAYS');
|
||||||
|
expect(result).toEqual({
|
||||||
|
schedule: { day: 1, hour: 0, minute: 0 },
|
||||||
|
type: 'DAYS',
|
||||||
|
outputSchema: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('returns correct settings for HOURS interval', () => {
|
it('returns correct settings for HOURS interval', () => {
|
||||||
const result = getCronTriggerDefaultSettings('HOURS');
|
const result = getCronTriggerDefaultSettings('HOURS');
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
import { WorkflowManualTriggerAvailability } from '@/workflow/types/Workflow';
|
||||||
|
import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
|
||||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||||
import { getManualTriggerDefaultSettings } from '../getManualTriggerDefaultSettings';
|
import { getManualTriggerDefaultSettings } from '../getManualTriggerDefaultSettings';
|
||||||
import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
|
|
||||||
|
|
||||||
it('returns settings for a manual trigger that can be activated from any where', () => {
|
it('returns settings for a manual trigger that can be activated from any where', () => {
|
||||||
expect(
|
expect(
|
||||||
@ -28,3 +29,28 @@ it('returns settings for a manual trigger that can be activated from any where',
|
|||||||
icon: 'IconTest',
|
icon: 'IconTest',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns settings for WHEN_RECORD_SELECTED with default icon when no custom icon provided', () => {
|
||||||
|
expect(
|
||||||
|
getManualTriggerDefaultSettings({
|
||||||
|
availability: 'WHEN_RECORD_SELECTED',
|
||||||
|
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||||
|
}),
|
||||||
|
).toStrictEqual({
|
||||||
|
objectType: generatedMockObjectMetadataItems[0].nameSingular,
|
||||||
|
outputSchema: {},
|
||||||
|
icon: COMMAND_MENU_DEFAULT_ICON,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error for unsupported availability type', () => {
|
||||||
|
const invalidAvailability =
|
||||||
|
'INVALID_AVAILABILITY' as WorkflowManualTriggerAvailability;
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
getManualTriggerDefaultSettings({
|
||||||
|
availability: invalidAvailability,
|
||||||
|
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||||
|
}),
|
||||||
|
).toThrow("Didn't expect to get here.");
|
||||||
|
});
|
||||||
|
|||||||
@ -100,6 +100,42 @@ describe('getTriggerDefaultDefinition', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns a valid configuration for CRON trigger type', () => {
|
||||||
|
expect(
|
||||||
|
getTriggerDefaultDefinition({
|
||||||
|
defaultLabel: 'On a schedule',
|
||||||
|
type: 'CRON',
|
||||||
|
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||||
|
}),
|
||||||
|
).toStrictEqual({
|
||||||
|
type: 'CRON',
|
||||||
|
name: 'On a schedule',
|
||||||
|
settings: {
|
||||||
|
type: 'DAYS',
|
||||||
|
schedule: { day: 1, hour: 0, minute: 0 },
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a valid configuration for WEBHOOK trigger type', () => {
|
||||||
|
expect(
|
||||||
|
getTriggerDefaultDefinition({
|
||||||
|
defaultLabel: 'Webhook',
|
||||||
|
type: 'WEBHOOK',
|
||||||
|
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||||
|
}),
|
||||||
|
).toStrictEqual({
|
||||||
|
type: 'WEBHOOK',
|
||||||
|
name: 'Webhook',
|
||||||
|
settings: {
|
||||||
|
outputSchema: {},
|
||||||
|
httpMethod: 'GET',
|
||||||
|
authentication: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('throws when providing an unknown trigger type', () => {
|
it('throws when providing an unknown trigger type', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
getTriggerDefaultDefinition({
|
getTriggerDefaultDefinition({
|
||||||
|
|||||||
@ -1,53 +0,0 @@
|
|||||||
import { getTriggerStepName } from '../getTriggerStepName';
|
|
||||||
|
|
||||||
it('returns the expected name for a DATABASE_EVENT trigger', () => {
|
|
||||||
expect(
|
|
||||||
getTriggerStepName({
|
|
||||||
type: 'DATABASE_EVENT',
|
|
||||||
name: '',
|
|
||||||
settings: {
|
|
||||||
eventName: 'company.created',
|
|
||||||
outputSchema: {},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toBe('Record is created');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the expected name for a MANUAL trigger without a defined objectType', () => {
|
|
||||||
expect(
|
|
||||||
getTriggerStepName({
|
|
||||||
type: 'MANUAL',
|
|
||||||
name: '',
|
|
||||||
settings: {
|
|
||||||
objectType: undefined,
|
|
||||||
outputSchema: {},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toBe('Manual trigger');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the expected name for a MANUAL trigger with a defined objectType', () => {
|
|
||||||
expect(
|
|
||||||
getTriggerStepName({
|
|
||||||
type: 'MANUAL',
|
|
||||||
name: '',
|
|
||||||
settings: {
|
|
||||||
objectType: 'company',
|
|
||||||
outputSchema: {},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toBe('Manual trigger for Company');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws when an unknown trigger type is provided', () => {
|
|
||||||
expect(() => {
|
|
||||||
getTriggerStepName({
|
|
||||||
type: 'unknown' as any,
|
|
||||||
name: '',
|
|
||||||
settings: {
|
|
||||||
objectType: 'company',
|
|
||||||
outputSchema: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import {
|
|
||||||
WorkflowDatabaseEventTrigger,
|
|
||||||
WorkflowTrigger,
|
|
||||||
} from '@/workflow/types/Workflow';
|
|
||||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
|
||||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
|
||||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
|
||||||
|
|
||||||
export const getTriggerStepName = (trigger: WorkflowTrigger): string => {
|
|
||||||
switch (trigger.type) {
|
|
||||||
case 'DATABASE_EVENT':
|
|
||||||
return getDatabaseEventTriggerStepName(trigger);
|
|
||||||
case 'CRON':
|
|
||||||
return 'On a schedule';
|
|
||||||
case 'WEBHOOK':
|
|
||||||
return 'Webhook';
|
|
||||||
case 'MANUAL':
|
|
||||||
if (!isDefined(trigger.settings.objectType)) {
|
|
||||||
return 'Manual trigger';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Manual trigger for ' + capitalize(trigger.settings.objectType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return assertUnreachable(trigger);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDatabaseEventTriggerStepName = (
|
|
||||||
trigger: WorkflowDatabaseEventTrigger,
|
|
||||||
): string => {
|
|
||||||
const defaultLabel = getTriggerDefaultLabel(trigger);
|
|
||||||
|
|
||||||
return defaultLabel ?? '';
|
|
||||||
};
|
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { WorkspaceUrls } from '~/generated/graphql';
|
||||||
|
import { getWorkspaceUrl } from '../getWorkspaceUrl';
|
||||||
|
|
||||||
|
describe('getWorkspaceUrl', () => {
|
||||||
|
it('should return customUrl when it is defined', () => {
|
||||||
|
const workspaceUrls: WorkspaceUrls = {
|
||||||
|
customUrl: 'https://custom.example.com',
|
||||||
|
subdomainUrl: 'https://subdomain.twenty.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkspaceUrl(workspaceUrls);
|
||||||
|
|
||||||
|
expect(result).toBe('https://custom.example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return subdomainUrl when customUrl is null', () => {
|
||||||
|
const workspaceUrls: WorkspaceUrls = {
|
||||||
|
customUrl: null,
|
||||||
|
subdomainUrl: 'https://subdomain.twenty.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkspaceUrl(workspaceUrls);
|
||||||
|
|
||||||
|
expect(result).toBe('https://subdomain.twenty.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return subdomainUrl when customUrl is undefined', () => {
|
||||||
|
const workspaceUrls: WorkspaceUrls = {
|
||||||
|
customUrl: undefined,
|
||||||
|
subdomainUrl: 'https://subdomain.twenty.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkspaceUrl(workspaceUrls);
|
||||||
|
|
||||||
|
expect(result).toBe('https://subdomain.twenty.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return customUrl when both customUrl and subdomainUrl are defined', () => {
|
||||||
|
const workspaceUrls: WorkspaceUrls = {
|
||||||
|
customUrl: 'https://my-company.com',
|
||||||
|
subdomainUrl: 'https://mycompany.twenty.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkspaceUrl(workspaceUrls);
|
||||||
|
|
||||||
|
expect(result).toBe('https://my-company.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty string when customUrl is empty string', () => {
|
||||||
|
const workspaceUrls: WorkspaceUrls = {
|
||||||
|
customUrl: '',
|
||||||
|
subdomainUrl: 'https://subdomain.twenty.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkspaceUrl(workspaceUrls);
|
||||||
|
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return subdomainUrl when customUrl is explicitly undefined', () => {
|
||||||
|
const workspaceUrls: WorkspaceUrls = {
|
||||||
|
customUrl: undefined,
|
||||||
|
subdomainUrl: 'https://subdomain.twenty.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getWorkspaceUrl(workspaceUrls);
|
||||||
|
|
||||||
|
expect(result).toBe('https://subdomain.twenty.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user