Workflow runs in side panel (#11669)
Vidéo explicative : https://share.cleanshot.com/VsvWknlW Closes https://github.com/twentyhq/core-team-issues/issues/810 Closes https://github.com/twentyhq/core-team-issues/issues/806 Known issues to fix later: - https://github.com/twentyhq/core-team-issues/issues/879
This commit is contained in:
committed by
GitHub
parent
0083569606
commit
cc211550ae
@ -24,6 +24,8 @@ export class WorkflowVisualizerPage {
|
|||||||
readonly useAsDraftButton: Locator;
|
readonly useAsDraftButton: Locator;
|
||||||
readonly overrideDraftButton: Locator;
|
readonly overrideDraftButton: Locator;
|
||||||
readonly discardDraftButton: Locator;
|
readonly discardDraftButton: Locator;
|
||||||
|
readonly seeRunsButton: Locator;
|
||||||
|
readonly goBackInCommandMenu: Locator;
|
||||||
|
|
||||||
#actionNames: Record<WorkflowActionType, string> = {
|
#actionNames: Record<WorkflowActionType, string> = {
|
||||||
'create-record': 'Create Record',
|
'create-record': 'Create Record',
|
||||||
@ -31,6 +33,7 @@ export class WorkflowVisualizerPage {
|
|||||||
'delete-record': 'Delete Record',
|
'delete-record': 'Delete Record',
|
||||||
code: 'Code',
|
code: 'Code',
|
||||||
'send-email': 'Send Email',
|
'send-email': 'Send Email',
|
||||||
|
form: 'Form',
|
||||||
};
|
};
|
||||||
|
|
||||||
#createdActionNames: Record<WorkflowActionType, string> = {
|
#createdActionNames: Record<WorkflowActionType, string> = {
|
||||||
@ -39,6 +42,7 @@ export class WorkflowVisualizerPage {
|
|||||||
'delete-record': 'Delete Record',
|
'delete-record': 'Delete Record',
|
||||||
code: 'Code - Serverless Function',
|
code: 'Code - Serverless Function',
|
||||||
'send-email': 'Send Email',
|
'send-email': 'Send Email',
|
||||||
|
form: 'Form',
|
||||||
};
|
};
|
||||||
|
|
||||||
#triggerNames: Record<WorkflowTriggerType, string> = {
|
#triggerNames: Record<WorkflowTriggerType, string> = {
|
||||||
@ -84,6 +88,10 @@ export class WorkflowVisualizerPage {
|
|||||||
this.discardDraftButton = page.getByRole('button', {
|
this.discardDraftButton = page.getByRole('button', {
|
||||||
name: 'Discard Draft',
|
name: 'Discard Draft',
|
||||||
});
|
});
|
||||||
|
this.seeRunsButton = page.getByRole('link', { name: 'See runs' });
|
||||||
|
this.goBackInCommandMenu = this.commandMenu
|
||||||
|
.getByRole('button')
|
||||||
|
.and(this.commandMenu.getByTestId('command-menu-go-back-button'));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOneWorkflow() {
|
async createOneWorkflow() {
|
||||||
|
|||||||
@ -9,4 +9,5 @@ export type WorkflowActionType =
|
|||||||
| 'update-record'
|
| 'update-record'
|
||||||
| 'delete-record'
|
| 'delete-record'
|
||||||
| 'code'
|
| 'code'
|
||||||
| 'send-email';
|
| 'send-email'
|
||||||
|
| 'form';
|
||||||
|
|||||||
@ -56,3 +56,113 @@ test('The workflow run visualizer shows the executed draft version without the l
|
|||||||
'Create Record',
|
'Create Record',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Workflow Runs with a pending form step can be opened in the side panel and then in full screen', async ({
|
||||||
|
workflowVisualizer,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await workflowVisualizer.createInitialTrigger('manual');
|
||||||
|
|
||||||
|
const manualTriggerAvailabilitySelect = page.getByRole('button', {
|
||||||
|
name: 'When record(s) are selected',
|
||||||
|
});
|
||||||
|
|
||||||
|
await manualTriggerAvailabilitySelect.click();
|
||||||
|
|
||||||
|
const alwaysAvailableOption = page.getByText(
|
||||||
|
'When no record(s) are selected',
|
||||||
|
);
|
||||||
|
|
||||||
|
await alwaysAvailableOption.click();
|
||||||
|
|
||||||
|
await workflowVisualizer.closeSidePanel();
|
||||||
|
|
||||||
|
const { createdStepId: firstStepId } =
|
||||||
|
await workflowVisualizer.createStep('form');
|
||||||
|
|
||||||
|
await workflowVisualizer.closeSidePanel();
|
||||||
|
|
||||||
|
const launchTestButton = page.getByLabel(workflowVisualizer.workflowName);
|
||||||
|
|
||||||
|
await launchTestButton.click();
|
||||||
|
|
||||||
|
const goToExecutionPageLink = page.getByRole('link', {
|
||||||
|
name: 'View execution details',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(goToExecutionPageLink).toBeVisible();
|
||||||
|
|
||||||
|
await workflowVisualizer.seeRunsButton.click();
|
||||||
|
|
||||||
|
const workflowRunName = `#1 - ${workflowVisualizer.workflowName}`;
|
||||||
|
|
||||||
|
const workflowRunNameCell = page.getByRole('cell', { name: workflowRunName });
|
||||||
|
|
||||||
|
await expect(workflowRunNameCell).toBeVisible();
|
||||||
|
|
||||||
|
const recordTableOptionsButton = page.getByText('Options');
|
||||||
|
|
||||||
|
await recordTableOptionsButton.click();
|
||||||
|
|
||||||
|
const layoutButton = page.getByText('Layout');
|
||||||
|
|
||||||
|
await layoutButton.click();
|
||||||
|
|
||||||
|
const openInButton = page.getByText('Open in');
|
||||||
|
|
||||||
|
await openInButton.click();
|
||||||
|
|
||||||
|
const openInSidePanelOption = page.getByRole('option', {
|
||||||
|
name: 'Side panel',
|
||||||
|
});
|
||||||
|
|
||||||
|
await openInSidePanelOption.click();
|
||||||
|
|
||||||
|
// 1. Exit the dropdown
|
||||||
|
await workflowRunNameCell.click();
|
||||||
|
// 2. Actually open the workflow run in the side panel
|
||||||
|
await workflowRunNameCell.click();
|
||||||
|
|
||||||
|
await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
|
||||||
|
'Form',
|
||||||
|
);
|
||||||
|
|
||||||
|
await workflowVisualizer.goBackInCommandMenu.click();
|
||||||
|
|
||||||
|
const workflowRunNameInCommandMenu =
|
||||||
|
workflowVisualizer.commandMenu.getByText(workflowRunName);
|
||||||
|
|
||||||
|
await expect(workflowRunNameInCommandMenu).toBeVisible();
|
||||||
|
|
||||||
|
await workflowVisualizer.triggerNode.click();
|
||||||
|
|
||||||
|
await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
|
||||||
|
'Launch manually',
|
||||||
|
);
|
||||||
|
|
||||||
|
await workflowVisualizer.goBackInCommandMenu.click();
|
||||||
|
|
||||||
|
const formStep = workflowVisualizer.getStepNode(firstStepId);
|
||||||
|
|
||||||
|
await formStep.click();
|
||||||
|
|
||||||
|
await workflowVisualizer.goBackInCommandMenu.click();
|
||||||
|
|
||||||
|
const openInFullScreenButton = workflowVisualizer.commandMenu.getByRole(
|
||||||
|
'button',
|
||||||
|
{ name: 'Open' },
|
||||||
|
);
|
||||||
|
|
||||||
|
await openInFullScreenButton.click();
|
||||||
|
|
||||||
|
const workflowRunNameInShowPage = page
|
||||||
|
.getByText(`#1 - ${workflowVisualizer.workflowName}`)
|
||||||
|
.nth(1);
|
||||||
|
|
||||||
|
await expect(workflowRunNameInShowPage).toBeVisible();
|
||||||
|
|
||||||
|
// Expect the side panel to be opened by default on the form.
|
||||||
|
await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
|
||||||
|
'Form',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
|
|||||||
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
|
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
|
|
||||||
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowServerlessFunctionTabListComponentId';
|
import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowServerlessFunctionTabListComponentId';
|
||||||
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId';
|
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
@ -61,12 +60,6 @@ export const useCommandMenuCloseAnimationCompleteCleanup = () => {
|
|||||||
|
|
||||||
emitRightDrawerCloseEvent();
|
emitRightDrawerCloseEvent();
|
||||||
set(isCommandMenuClosingState, false);
|
set(isCommandMenuClosingState, false);
|
||||||
set(
|
|
||||||
activeTabIdComponentState.atomFamily({
|
|
||||||
instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
|
||||||
}),
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
set(
|
set(
|
||||||
activeTabIdComponentState.atomFamily({
|
activeTabIdComponentState.atomFamily({
|
||||||
instanceId: WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID,
|
instanceId: WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID,
|
||||||
|
|||||||
@ -11,21 +11,25 @@ import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-sto
|
|||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { getIconColorForObjectType } from '@/object-metadata/utils/getIconColorForObjectType';
|
import { getIconColorForObjectType } from '@/object-metadata/utils/getIconColorForObjectType';
|
||||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||||
|
import { useRunWorkflowRunOpeningInCommandMenuSideEffects } from '@/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { v4 } from 'uuid';
|
|
||||||
import { capitalize } from 'twenty-shared/utils';
|
import { capitalize } from 'twenty-shared/utils';
|
||||||
import { useIcons } from 'twenty-ui/display';
|
import { useIcons } from 'twenty-ui/display';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
export const useOpenRecordInCommandMenu = () => {
|
export const useOpenRecordInCommandMenu = () => {
|
||||||
const { navigateCommandMenu } = useCommandMenu();
|
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const { navigateCommandMenu } = useCommandMenu();
|
||||||
|
const { runWorkflowRunOpeningInCommandMenuSideEffects } =
|
||||||
|
useRunWorkflowRunOpeningInCommandMenuSideEffects();
|
||||||
|
|
||||||
const openRecordInCommandMenu = useRecoilCallback(
|
const openRecordInCommandMenu = useRecoilCallback(
|
||||||
({ set, snapshot }) => {
|
({ set, snapshot }) => {
|
||||||
return ({
|
return ({
|
||||||
@ -147,9 +151,21 @@ export const useOpenRecordInCommandMenu = () => {
|
|||||||
pageId: pageComponentInstanceId,
|
pageId: pageComponentInstanceId,
|
||||||
resetNavigationStack: false,
|
resetNavigationStack: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (objectNameSingular === CoreObjectNameSingular.WorkflowRun) {
|
||||||
|
runWorkflowRunOpeningInCommandMenuSideEffects({
|
||||||
|
objectMetadataItem,
|
||||||
|
recordId,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[getIcon, navigateCommandMenu, theme],
|
[
|
||||||
|
getIcon,
|
||||||
|
navigateCommandMenu,
|
||||||
|
runWorkflowRunOpeningInCommandMenuSideEffects,
|
||||||
|
theme,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,17 +1,21 @@
|
|||||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||||
import { workflowIdComponentState } from '@/command-menu/pages/workflow/states/workflowIdComponentState';
|
import { workflowIdComponentState } from '@/command-menu/pages/workflow/states/workflowIdComponentState';
|
||||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||||
|
import { useSetInitialWorkflowRunRightDrawerTab } from '@/workflow/workflow-diagram/hooks/useSetInitialWorkflowRunRightDrawerTab';
|
||||||
|
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { v4 } from 'uuid';
|
|
||||||
import {
|
import {
|
||||||
IconBolt,
|
IconBolt,
|
||||||
IconComponent,
|
IconComponent,
|
||||||
IconSettingsAutomation,
|
IconSettingsAutomation,
|
||||||
} from 'twenty-ui/display';
|
} from 'twenty-ui/display';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
export const useWorkflowCommandMenu = () => {
|
export const useWorkflowCommandMenu = () => {
|
||||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||||
|
const { setInitialWorkflowRunRightDrawerTab } =
|
||||||
|
useSetInitialWorkflowRunRightDrawerTab();
|
||||||
|
|
||||||
const openWorkflowTriggerTypeInCommandMenu = useRecoilCallback(
|
const openWorkflowTriggerTypeInCommandMenu = useRecoilCallback(
|
||||||
({ set }) => {
|
({ set }) => {
|
||||||
@ -99,7 +103,19 @@ export const useWorkflowCommandMenu = () => {
|
|||||||
|
|
||||||
const openWorkflowRunViewStepInCommandMenu = useRecoilCallback(
|
const openWorkflowRunViewStepInCommandMenu = useRecoilCallback(
|
||||||
({ set }) => {
|
({ set }) => {
|
||||||
return (workflowId: string, title: string, icon: IconComponent) => {
|
return ({
|
||||||
|
workflowId,
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
workflowSelectedNode,
|
||||||
|
stepExecutionStatus,
|
||||||
|
}: {
|
||||||
|
workflowId: string;
|
||||||
|
title: string;
|
||||||
|
icon: IconComponent;
|
||||||
|
workflowSelectedNode: string;
|
||||||
|
stepExecutionStatus: WorkflowDiagramRunStatus;
|
||||||
|
}) => {
|
||||||
const pageId = v4();
|
const pageId = v4();
|
||||||
|
|
||||||
set(
|
set(
|
||||||
@ -113,9 +129,14 @@ export const useWorkflowCommandMenu = () => {
|
|||||||
pageIcon: icon,
|
pageIcon: icon,
|
||||||
pageId,
|
pageId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setInitialWorkflowRunRightDrawerTab({
|
||||||
|
workflowSelectedNode,
|
||||||
|
stepExecutionStatus,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[navigateCommandMenu],
|
[navigateCommandMenu, setInitialWorkflowRunRightDrawerTab],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled';
|
import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled';
|
||||||
import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled';
|
import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled';
|
||||||
|
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
|
||||||
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
||||||
|
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||||
@ -11,13 +13,13 @@ import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hook
|
|||||||
import { WorkflowRunStepInputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepInputDetail';
|
import { WorkflowRunStepInputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepInputDetail';
|
||||||
import { WorkflowRunStepNodeDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepNodeDetail';
|
import { WorkflowRunStepNodeDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepNodeDetail';
|
||||||
import { WorkflowRunStepOutputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepOutputDetail';
|
import { WorkflowRunStepOutputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepOutputDetail';
|
||||||
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
|
|
||||||
import {
|
import {
|
||||||
WorkflowRunTabId,
|
WorkflowRunTabId,
|
||||||
WorkflowRunTabIdType,
|
WorkflowRunTabIdType,
|
||||||
} from '@/workflow/workflow-steps/types/WorkflowRunTabId';
|
} from '@/workflow/workflow-steps/types/WorkflowRunTabId';
|
||||||
import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
|
import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { isNull } from '@sniptt/guards';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui/display';
|
import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui/display';
|
||||||
|
|
||||||
@ -41,9 +43,18 @@ export const CommandMenuWorkflowRunViewStep = () => {
|
|||||||
|
|
||||||
const workflowRun = useWorkflowRun({ workflowRunId });
|
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||||
|
|
||||||
|
const commandMenuPageComponentInstance = useComponentInstanceStateContext(
|
||||||
|
CommandMenuPageComponentInstanceContext,
|
||||||
|
);
|
||||||
|
if (isNull(commandMenuPageComponentInstance)) {
|
||||||
|
throw new Error(
|
||||||
|
'CommandMenuPageComponentInstanceContext is not defined. This component should be used within CommandMenuPageComponentInstanceContext.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const activeTabId = useRecoilComponentValueV2(
|
const activeTabId = useRecoilComponentValueV2(
|
||||||
activeTabIdComponentState,
|
activeTabIdComponentState,
|
||||||
WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
commandMenuPageComponentInstance.instanceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isDefined(workflowRun)) {
|
if (!isDefined(workflowRun)) {
|
||||||
@ -90,9 +101,7 @@ export const CommandMenuWorkflowRunViewStep = () => {
|
|||||||
<StyledTabList
|
<StyledTabList
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
behaveAsLinks={false}
|
behaveAsLinks={false}
|
||||||
componentInstanceId={
|
componentInstanceId={commandMenuPageComponentInstance.instanceId}
|
||||||
WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{activeTabId === WorkflowRunTabId.OUTPUT ? (
|
{activeTabId === WorkflowRunTabId.OUTPUT ? (
|
||||||
|
|||||||
@ -145,7 +145,7 @@ export const useRecordShowContainerTabs = (
|
|||||||
tabs: {
|
tabs: {
|
||||||
workflow: {
|
workflow: {
|
||||||
title: 'Flow',
|
title: 'Flow',
|
||||||
position: 0,
|
position: 101,
|
||||||
Icon: IconSettings,
|
Icon: IconSettings,
|
||||||
cards: [{ type: CardType.WorkflowCard }],
|
cards: [{ type: CardType.WorkflowCard }],
|
||||||
hide: {
|
hide: {
|
||||||
@ -168,7 +168,7 @@ export const useRecordShowContainerTabs = (
|
|||||||
tabs: {
|
tabs: {
|
||||||
workflowVersion: {
|
workflowVersion: {
|
||||||
title: 'Flow',
|
title: 'Flow',
|
||||||
position: 0,
|
position: 101,
|
||||||
Icon: IconSettings,
|
Icon: IconSettings,
|
||||||
cards: [{ type: CardType.WorkflowVersionCard }],
|
cards: [{ type: CardType.WorkflowVersionCard }],
|
||||||
hide: {
|
hide: {
|
||||||
@ -190,7 +190,7 @@ export const useRecordShowContainerTabs = (
|
|||||||
tabs: {
|
tabs: {
|
||||||
workflowRun: {
|
workflowRun: {
|
||||||
title: 'Flow',
|
title: 'Flow',
|
||||||
position: 0,
|
position: 101,
|
||||||
Icon: IconSettings,
|
Icon: IconSettings,
|
||||||
cards: [{ type: CardType.WorkflowRunCard }],
|
cards: [{ type: CardType.WorkflowRunCard }],
|
||||||
hide: {
|
hide: {
|
||||||
|
|||||||
@ -0,0 +1,84 @@
|
|||||||
|
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
||||||
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
|
||||||
|
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||||
|
import { flowState } from '@/workflow/states/flowState';
|
||||||
|
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||||
|
import { workflowRunIdState } from '@/workflow/states/workflowRunIdState';
|
||||||
|
import { WorkflowRun } from '@/workflow/types/Workflow';
|
||||||
|
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||||
|
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
|
||||||
|
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
|
||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { useIcons } from 'twenty-ui/display';
|
||||||
|
|
||||||
|
export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu();
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const runWorkflowRunOpeningInCommandMenuSideEffects = useRecoilCallback(
|
||||||
|
({ snapshot, set }) =>
|
||||||
|
({
|
||||||
|
objectMetadataItem,
|
||||||
|
recordId,
|
||||||
|
}: {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
recordId: string;
|
||||||
|
}) => {
|
||||||
|
const objectMetadataItems = getSnapshotValue(
|
||||||
|
snapshot,
|
||||||
|
objectMetadataItemsState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const workflowRunRecord = getRecordFromCache<WorkflowRun>({
|
||||||
|
objectMetadataItem,
|
||||||
|
cache: apolloClient.cache,
|
||||||
|
recordId,
|
||||||
|
objectMetadataItems,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
!(isDefined(workflowRunRecord) && isDefined(workflowRunRecord.output))
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`No workflow run record found for record ID ${recordId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stepToOpenByDefault } = generateWorkflowRunDiagram({
|
||||||
|
steps: workflowRunRecord.output.flow.steps,
|
||||||
|
stepsOutput: workflowRunRecord.output.stepsOutput,
|
||||||
|
trigger: workflowRunRecord.output.flow.trigger,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDefined(stepToOpenByDefault)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(workflowRunIdState, workflowRunRecord.id);
|
||||||
|
set(workflowIdState, workflowRunRecord.workflowId);
|
||||||
|
set(flowState, {
|
||||||
|
workflowVersionId: workflowRunRecord.workflowVersionId,
|
||||||
|
trigger: workflowRunRecord.output.flow.trigger,
|
||||||
|
steps: workflowRunRecord.output.flow.steps,
|
||||||
|
});
|
||||||
|
set(workflowSelectedNodeState, stepToOpenByDefault.id);
|
||||||
|
|
||||||
|
openWorkflowRunViewStepInCommandMenu({
|
||||||
|
workflowId: workflowRunRecord.workflowId,
|
||||||
|
title: stepToOpenByDefault.data.name,
|
||||||
|
icon: getIcon(getWorkflowNodeIconKey(stepToOpenByDefault.data)),
|
||||||
|
workflowSelectedNode: stepToOpenByDefault.id,
|
||||||
|
stepExecutionStatus: stepToOpenByDefault.data.runStatus,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[apolloClient.cache, getIcon, openWorkflowRunViewStepInCommandMenu],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
runWorkflowRunOpeningInCommandMenuSideEffects,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -2,6 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
|||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { WorkflowRun } from '@/workflow/types/Workflow';
|
import { WorkflowRun } from '@/workflow/types/Workflow';
|
||||||
import { workflowRunSchema } from '@/workflow/validation-schemas/workflowSchema';
|
import { workflowRunSchema } from '@/workflow/validation-schemas/workflowSchema';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const useWorkflowRun = ({
|
export const useWorkflowRun = ({
|
||||||
workflowRunId,
|
workflowRunId,
|
||||||
@ -13,7 +14,10 @@ export const useWorkflowRun = ({
|
|||||||
objectRecordId: workflowRunId,
|
objectRecordId: workflowRunId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { success, data: record } = workflowRunSchema.safeParse(rawRecord);
|
const { success, data: record } = useMemo(
|
||||||
|
() => workflowRunSchema.safeParse(rawRecord),
|
||||||
|
[rawRecord],
|
||||||
|
);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { createState } from 'twenty-ui/utilities';
|
import { createState } from 'twenty-ui/utilities';
|
||||||
|
|
||||||
export const workflowRunIdState = createState<string | undefined>({
|
export const workflowRunIdState = createState<string | undefined>({
|
||||||
key: 'workflowRunIdState',
|
key: 'workflowRunIdState',
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
|
|||||||
@ -26,10 +26,10 @@ import {
|
|||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import React, { useEffect, useMemo, useRef } from 'react';
|
import React, { useEffect, useMemo, useRef } from 'react';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { THEME_COMMON } from 'twenty-ui/theme';
|
|
||||||
import { Tag, TagColor } from 'twenty-ui/components';
|
import { Tag, TagColor } from 'twenty-ui/components';
|
||||||
|
import { THEME_COMMON } from 'twenty-ui/theme';
|
||||||
|
|
||||||
const StyledResetReactflowStyles = styled.div`
|
const StyledResetReactflowStyles = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -72,7 +72,7 @@ const StyledStatusTagContainer = styled.div`
|
|||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
padding: ${({ theme }) => theme.spacing(4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const defaultFitViewOptions = {
|
const defaultFitViewOptions = {
|
||||||
@ -87,6 +87,7 @@ export const WorkflowDiagramCanvasBase = ({
|
|||||||
tagContainerTestId,
|
tagContainerTestId,
|
||||||
tagColor,
|
tagColor,
|
||||||
tagText,
|
tagText,
|
||||||
|
onInit,
|
||||||
}: {
|
}: {
|
||||||
nodeTypes: Partial<
|
nodeTypes: Partial<
|
||||||
Record<
|
Record<
|
||||||
@ -114,13 +115,11 @@ export const WorkflowDiagramCanvasBase = ({
|
|||||||
tagContainerTestId: string;
|
tagContainerTestId: string;
|
||||||
tagColor: TagColor;
|
tagColor: TagColor;
|
||||||
tagText: string;
|
tagText: string;
|
||||||
|
onInit?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const reactflow = useReactFlow();
|
const reactflow = useReactFlow();
|
||||||
const setWorkflowReactFlowRefState = useSetRecoilState(
|
|
||||||
workflowReactFlowRefState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const workflowDiagram = useRecoilValue(workflowDiagramState);
|
const workflowDiagram = useRecoilValue(workflowDiagramState);
|
||||||
|
|
||||||
@ -140,22 +139,13 @@ export const WorkflowDiagramCanvasBase = ({
|
|||||||
|
|
||||||
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
||||||
|
|
||||||
const handleNodesChange = (
|
const setWorkflowReactFlowRef = useRecoilCallback(
|
||||||
nodeChanges: Array<NodeChange<WorkflowDiagramNode>>,
|
({ set }) =>
|
||||||
) => {
|
(node: HTMLDivElement | null) => {
|
||||||
setWorkflowDiagram((diagram) => {
|
set(workflowReactFlowRefState, { current: node });
|
||||||
if (isDefined(diagram) === false) {
|
},
|
||||||
throw new Error(
|
[],
|
||||||
'It must be impossible for the nodes to be updated if the diagram is not defined yet. Be sure the diagram is rendered only when defined.',
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...diagram,
|
|
||||||
nodes: applyNodeChanges(nodeChanges, diagram.nodes),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdgesChange = (
|
const handleEdgesChange = (
|
||||||
edgeChanges: Array<EdgeChange<WorkflowDiagramEdge>>,
|
edgeChanges: Array<EdgeChange<WorkflowDiagramEdge>>,
|
||||||
@ -209,36 +199,55 @@ export const WorkflowDiagramCanvasBase = ({
|
|||||||
);
|
);
|
||||||
}, [reactflow, rightDrawerState, rightDrawerWidth]);
|
}, [reactflow, rightDrawerState, rightDrawerWidth]);
|
||||||
|
|
||||||
|
const handleNodesChanges = useRecoilCallback(
|
||||||
|
({ set }) =>
|
||||||
|
(changes: NodeChange<WorkflowDiagramNode>[]) => {
|
||||||
|
set(workflowDiagramState, (diagram) => {
|
||||||
|
if (!isDefined(diagram)) {
|
||||||
|
throw new Error(
|
||||||
|
'It must be impossible for the nodes to be updated if the diagram is not defined yet. Be sure the diagram is rendered only when defined.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...diagram,
|
||||||
|
nodes: applyNodeChanges(changes, diagram.nodes),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInit = () => {
|
||||||
|
if (!isDefined(containerRef.current)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const flowBounds = reactflow.getNodesBounds(reactflow.getNodes());
|
||||||
|
|
||||||
|
reactflow.setViewport({
|
||||||
|
x: containerRef.current.offsetWidth / 2 - flowBounds.width / 2,
|
||||||
|
y: 150,
|
||||||
|
zoom: defaultFitViewOptions.maxZoom,
|
||||||
|
});
|
||||||
|
|
||||||
|
onInit?.();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledResetReactflowStyles ref={containerRef}>
|
<StyledResetReactflowStyles ref={containerRef}>
|
||||||
<WorkflowDiagramCustomMarkers />
|
<WorkflowDiagramCustomMarkers />
|
||||||
|
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
ref={(node) => {
|
ref={setWorkflowReactFlowRef}
|
||||||
if (isDefined(node)) {
|
onInit={handleInit}
|
||||||
setWorkflowReactFlowRefState({ current: node });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onInit={() => {
|
|
||||||
if (!isDefined(containerRef.current)) {
|
|
||||||
throw new Error('Expect the container ref to be defined');
|
|
||||||
}
|
|
||||||
|
|
||||||
const flowBounds = reactflow.getNodesBounds(reactflow.getNodes());
|
|
||||||
|
|
||||||
reactflow.setViewport({
|
|
||||||
x: containerRef.current.offsetWidth / 2 - flowBounds.width / 2,
|
|
||||||
y: 150,
|
|
||||||
zoom: defaultFitViewOptions.maxZoom,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
minZoom={defaultFitViewOptions.minZoom}
|
minZoom={defaultFitViewOptions.minZoom}
|
||||||
maxZoom={defaultFitViewOptions.maxZoom}
|
maxZoom={defaultFitViewOptions.maxZoom}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
edgeTypes={edgeTypes}
|
edgeTypes={edgeTypes}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onNodesChange={handleNodesChange}
|
onNodesChange={handleNodesChanges}
|
||||||
onEdgesChange={handleEdgesChange}
|
onEdgesChange={handleEdgesChange}
|
||||||
onBeforeDelete={async () => {
|
onBeforeDelete={async () => {
|
||||||
// Abort all non-programmatic deletions
|
// Abort all non-programmatic deletions
|
||||||
@ -251,6 +260,7 @@ export const WorkflowDiagramCanvasBase = ({
|
|||||||
nodesDraggable={false}
|
nodesDraggable={false}
|
||||||
nodesConnectable={false}
|
nodesConnectable={false}
|
||||||
paneClickDistance={10} // Fix small unwanted user dragging does not select node
|
paneClickDistance={10} // Fix small unwanted user dragging does not select node
|
||||||
|
preventScrolling={false}
|
||||||
>
|
>
|
||||||
<Background color={theme.border.color.medium} size={2} />
|
<Background color={theme.border.color.medium} size={2} />
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/componen
|
|||||||
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 { 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 { getWorkflowRunStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps';
|
import { getWorkflowRunStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps';
|
||||||
import { ReactFlowProvider } from '@xyflow/react';
|
import { ReactFlowProvider } from '@xyflow/react';
|
||||||
|
|
||||||
@ -16,6 +17,9 @@ export const WorkflowRunDiagramCanvas = ({
|
|||||||
workflowRunStatus,
|
workflowRunStatus,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { handleWorkflowRunDiagramCanvasInit } =
|
||||||
|
useHandleWorkflowRunDiagramCanvasInit();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<WorkflowDiagramCanvasBase
|
<WorkflowDiagramCanvasBase
|
||||||
@ -29,6 +33,7 @@ export const WorkflowRunDiagramCanvas = ({
|
|||||||
tagContainerTestId="workflow-run-status"
|
tagContainerTestId="workflow-run-status"
|
||||||
tagColor={tagProps.color}
|
tagColor={tagProps.color}
|
||||||
tagText={tagProps.text}
|
tagText={tagProps.text}
|
||||||
|
onInit={handleWorkflowRunDiagramCanvasInit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<WorkflowRunDiagramCanvasEffect />
|
<WorkflowRunDiagramCanvasEffect />
|
||||||
|
|||||||
@ -1,121 +1,61 @@
|
|||||||
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
||||||
import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled';
|
|
||||||
import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled';
|
|
||||||
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
|
||||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||||
|
import { workflowDiagramStatusState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusState';
|
||||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||||
import {
|
import { WorkflowRunDiagramNode } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
WorkflowDiagramNode,
|
|
||||||
WorkflowDiagramRunStatus,
|
|
||||||
WorkflowRunDiagramStepNodeData,
|
|
||||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
|
||||||
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
|
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
|
||||||
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
|
|
||||||
import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId';
|
|
||||||
import { isNull } from '@sniptt/guards';
|
|
||||||
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
||||||
import { useCallback } from 'react';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { useRecoilCallback, useRecoilValue, useSetRecoilState } 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';
|
||||||
|
|
||||||
export const WorkflowRunDiagramCanvasEffect = () => {
|
export const WorkflowRunDiagramCanvasEffect = () => {
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
|
||||||
const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu();
|
const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu();
|
||||||
|
|
||||||
const workflowId = useRecoilValue(workflowIdState);
|
const handleSelectionChange = useRecoilCallback(
|
||||||
|
|
||||||
const resetWorkflowRunRightDrawerTabIfNeeded = useRecoilCallback(
|
|
||||||
({ snapshot, set }) =>
|
({ snapshot, set }) =>
|
||||||
({
|
({ nodes }: OnSelectionChangeParams) => {
|
||||||
workflowSelectedNode,
|
const workflowId = getSnapshotValue(snapshot, workflowIdState);
|
||||||
stepExecutionStatus,
|
|
||||||
}: {
|
if (!isDefined(workflowId)) {
|
||||||
workflowSelectedNode: string;
|
throw new Error('Expected the workflowId to be defined.');
|
||||||
stepExecutionStatus: WorkflowDiagramRunStatus;
|
}
|
||||||
}) => {
|
|
||||||
const activeWorkflowRunRightDrawerTab = getSnapshotValue(
|
const workflowDiagramStatus = getSnapshotValue(
|
||||||
snapshot,
|
snapshot,
|
||||||
activeTabIdComponentState.atomFamily({
|
workflowDiagramStatusState,
|
||||||
instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
);
|
||||||
}),
|
|
||||||
) as WorkflowRunTabId | null;
|
|
||||||
|
|
||||||
const isInputTabDisabled = getIsInputTabDisabled({
|
|
||||||
stepExecutionStatus,
|
|
||||||
workflowSelectedNode,
|
|
||||||
});
|
|
||||||
const isOutputTabDisabled = getIsOutputTabDisabled({
|
|
||||||
stepExecutionStatus,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isNull(activeWorkflowRunRightDrawerTab)) {
|
|
||||||
const defaultTabId = isOutputTabDisabled
|
|
||||||
? WorkflowRunTabId.NODE
|
|
||||||
: WorkflowRunTabId.OUTPUT;
|
|
||||||
|
|
||||||
set(
|
|
||||||
activeTabIdComponentState.atomFamily({
|
|
||||||
instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
|
||||||
}),
|
|
||||||
defaultTabId,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// The `handleSelectionChange` function is called when the diagram initializes and
|
||||||
|
// a node is selected. In this case, we don't want to execute the rest of this function.
|
||||||
|
// We open the Side Panel® synchronously after ReactFlow is initialized and a node is selected,
|
||||||
|
// animations perform better that way.
|
||||||
|
if (workflowDiagramStatus !== 'done') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const selectedNode = nodes[0] as WorkflowRunDiagramNode | undefined;
|
||||||
(isInputTabDisabled &&
|
|
||||||
activeWorkflowRunRightDrawerTab === WorkflowRunTabId.INPUT) ||
|
if (!isDefined(selectedNode)) {
|
||||||
(isOutputTabDisabled &&
|
return;
|
||||||
activeWorkflowRunRightDrawerTab === WorkflowRunTabId.OUTPUT)
|
|
||||||
) {
|
|
||||||
set(
|
|
||||||
activeTabIdComponentState.atomFamily({
|
|
||||||
instanceId: WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
|
||||||
}),
|
|
||||||
WorkflowRunTabId.NODE,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSelectionChange = useCallback(
|
set(workflowSelectedNodeState, selectedNode.id);
|
||||||
({ nodes }: OnSelectionChangeParams) => {
|
|
||||||
const selectedNode = nodes[0] as WorkflowDiagramNode | undefined;
|
|
||||||
|
|
||||||
if (!isDefined(selectedNode)) {
|
const selectedNodeData = selectedNode.data;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setWorkflowSelectedNode(selectedNode.id);
|
openWorkflowRunViewStepInCommandMenu({
|
||||||
|
|
||||||
const selectedNodeData =
|
|
||||||
selectedNode.data as WorkflowRunDiagramStepNodeData;
|
|
||||||
|
|
||||||
if (isDefined(workflowId)) {
|
|
||||||
openWorkflowRunViewStepInCommandMenu(
|
|
||||||
workflowId,
|
workflowId,
|
||||||
selectedNodeData.name,
|
title: selectedNodeData.name,
|
||||||
getIcon(getWorkflowNodeIconKey(selectedNodeData)),
|
icon: getIcon(getWorkflowNodeIconKey(selectedNodeData)),
|
||||||
);
|
|
||||||
|
|
||||||
resetWorkflowRunRightDrawerTabIfNeeded({
|
|
||||||
workflowSelectedNode: selectedNode.id,
|
workflowSelectedNode: selectedNode.id,
|
||||||
stepExecutionStatus: selectedNodeData.runStatus,
|
stepExecutionStatus: selectedNodeData.runStatus,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
},
|
[getIcon, openWorkflowRunViewStepInCommandMenu],
|
||||||
[
|
|
||||||
setWorkflowSelectedNode,
|
|
||||||
resetWorkflowRunRightDrawerTabIfNeeded,
|
|
||||||
workflowId,
|
|
||||||
openWorkflowRunViewStepInCommandMenu,
|
|
||||||
getIcon,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useOnSelectionChange({
|
useOnSelectionChange({
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||||
import { WorkflowRunDiagramCanvas } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas';
|
import { WorkflowRunDiagramCanvas } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas';
|
||||||
|
import { workflowDiagramStatusState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusState';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -13,8 +15,12 @@ export const WorkflowRunVisualizer = ({
|
|||||||
workflowRunId: string;
|
workflowRunId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const workflowRun = useWorkflowRun({ workflowRunId });
|
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||||
|
const workflowDiagramStatus = useRecoilValue(workflowDiagramStatusState);
|
||||||
|
|
||||||
if (!isDefined(workflowRun)) {
|
if (
|
||||||
|
!isDefined(workflowRun) ||
|
||||||
|
workflowDiagramStatus === 'computing-diagram'
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
|
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||||
import { useStepsOutputSchema } from '@/workflow/hooks/useStepsOutputSchema';
|
import { useStepsOutputSchema } from '@/workflow/hooks/useStepsOutputSchema';
|
||||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||||
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||||
import { flowState } from '@/workflow/states/flowState';
|
import { flowState } from '@/workflow/states/flowState';
|
||||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||||
import { workflowRunIdState } from '@/workflow/states/workflowRunIdState';
|
import { workflowRunIdState } from '@/workflow/states/workflowRunIdState';
|
||||||
|
import { WorkflowRunOutput } from '@/workflow/types/Workflow';
|
||||||
import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState';
|
import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState';
|
||||||
|
import { workflowDiagramStatusState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusState';
|
||||||
|
import { workflowRunStepToOpenByDefaultState } from '@/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultState';
|
||||||
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
|
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
|
||||||
import { useEffect } from 'react';
|
import { selectWorkflowDiagramNode } from '@/workflow/workflow-diagram/utils/selectWorkflowDiagramNode';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useContext, useEffect } from 'react';
|
||||||
|
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
export const WorkflowRunVisualizerEffect = ({
|
export const WorkflowRunVisualizerEffect = ({
|
||||||
@ -20,10 +25,10 @@ export const WorkflowRunVisualizerEffect = ({
|
|||||||
|
|
||||||
const setWorkflowRunId = useSetRecoilState(workflowRunIdState);
|
const setWorkflowRunId = useSetRecoilState(workflowRunIdState);
|
||||||
const setWorkflowId = useSetRecoilState(workflowIdState);
|
const setWorkflowId = useSetRecoilState(workflowIdState);
|
||||||
const setFlow = useSetRecoilState(flowState);
|
|
||||||
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
|
||||||
const { populateStepsOutputSchema } = useStepsOutputSchema();
|
const { populateStepsOutputSchema } = useStepsOutputSchema();
|
||||||
|
|
||||||
|
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setWorkflowRunId(workflowRunId);
|
setWorkflowRunId(workflowRunId);
|
||||||
}, [setWorkflowRunId, workflowRunId]);
|
}, [setWorkflowRunId, workflowRunId]);
|
||||||
@ -32,33 +37,72 @@ export const WorkflowRunVisualizerEffect = ({
|
|||||||
if (!isDefined(workflowRun)) {
|
if (!isDefined(workflowRun)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorkflowId(workflowRun.workflowId);
|
setWorkflowId(workflowRun.workflowId);
|
||||||
}, [setWorkflowId, workflowRun]);
|
}, [setWorkflowId, workflowRun]);
|
||||||
|
|
||||||
|
const handleWorkflowRunDiagramGeneration = useRecoilCallback(
|
||||||
|
({ set }) =>
|
||||||
|
({
|
||||||
|
workflowRunOutput,
|
||||||
|
workflowVersionId,
|
||||||
|
skipNodeSelection,
|
||||||
|
}: {
|
||||||
|
workflowRunOutput: WorkflowRunOutput | undefined;
|
||||||
|
workflowVersionId: string | undefined;
|
||||||
|
skipNodeSelection: boolean;
|
||||||
|
}) => {
|
||||||
|
if (!(isDefined(workflowRunOutput) && isDefined(workflowVersionId))) {
|
||||||
|
set(flowState, undefined);
|
||||||
|
set(workflowDiagramState, undefined);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(workflowDiagramStatusState, 'computing-diagram');
|
||||||
|
|
||||||
|
set(flowState, {
|
||||||
|
workflowVersionId,
|
||||||
|
trigger: workflowRunOutput.flow.trigger,
|
||||||
|
steps: workflowRunOutput.flow.steps,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { diagram: baseWorkflowRunDiagram, stepToOpenByDefault } =
|
||||||
|
generateWorkflowRunDiagram({
|
||||||
|
trigger: workflowRunOutput.flow.trigger,
|
||||||
|
steps: workflowRunOutput.flow.steps,
|
||||||
|
stepsOutput: workflowRunOutput.stepsOutput,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDefined(stepToOpenByDefault) && !skipNodeSelection) {
|
||||||
|
const workflowRunDiagram = selectWorkflowDiagramNode({
|
||||||
|
diagram: baseWorkflowRunDiagram,
|
||||||
|
nodeIdToSelect: stepToOpenByDefault.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
set(workflowDiagramState, workflowRunDiagram);
|
||||||
|
set(workflowRunStepToOpenByDefaultState, {
|
||||||
|
id: stepToOpenByDefault.id,
|
||||||
|
data: stepToOpenByDefault.data,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
set(workflowDiagramState, baseWorkflowRunDiagram);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(workflowDiagramStatusState, 'computing-dimensions');
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDefined(workflowRun?.output)) {
|
handleWorkflowRunDiagramGeneration({
|
||||||
setFlow(undefined);
|
workflowRunOutput: workflowRun?.output ?? undefined,
|
||||||
setWorkflowDiagram(undefined);
|
workflowVersionId: workflowRun?.workflowVersionId,
|
||||||
|
skipNodeSelection: isInRightDrawer,
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFlow({
|
|
||||||
workflowVersionId: workflowRun.workflowVersionId,
|
|
||||||
trigger: workflowRun.output.flow.trigger,
|
|
||||||
steps: workflowRun.output.flow.steps,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const nextWorkflowDiagram = generateWorkflowRunDiagram({
|
|
||||||
trigger: workflowRun.output.flow.trigger,
|
|
||||||
steps: workflowRun.output.flow.steps,
|
|
||||||
stepsOutput: workflowRun.output.stepsOutput,
|
|
||||||
});
|
|
||||||
|
|
||||||
setWorkflowDiagram(nextWorkflowDiagram);
|
|
||||||
}, [
|
}, [
|
||||||
setFlow,
|
handleWorkflowRunDiagramGeneration,
|
||||||
setWorkflowDiagram,
|
isInRightDrawer,
|
||||||
workflowRun?.output,
|
workflowRun?.output,
|
||||||
workflowRun?.workflowVersionId,
|
workflowRun?.workflowVersionId,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -0,0 +1,74 @@
|
|||||||
|
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||||
|
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
||||||
|
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||||
|
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||||
|
import { workflowDiagramStatusState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusState';
|
||||||
|
import { workflowRunStepToOpenByDefaultState } from '@/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultState';
|
||||||
|
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||||
|
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { useIcons } from 'twenty-ui/display';
|
||||||
|
|
||||||
|
export const useHandleWorkflowRunDiagramCanvasInit = () => {
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu();
|
||||||
|
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||||
|
|
||||||
|
const handleWorkflowRunDiagramCanvasInit = useRecoilCallback(
|
||||||
|
({ snapshot, set }) =>
|
||||||
|
() => {
|
||||||
|
const workflowDiagramStatus = getSnapshotValue(
|
||||||
|
snapshot,
|
||||||
|
workflowDiagramStatusState,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (workflowDiagramStatus !== 'computing-dimensions') {
|
||||||
|
throw new Error(
|
||||||
|
'Sequence error: reactflow should be considered initialized only when the workflow diagram status is computing-dimensions.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(workflowDiagramStatusState, 'done');
|
||||||
|
|
||||||
|
if (isInRightDrawer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflowStepToOpenByDefault = getSnapshotValue(
|
||||||
|
snapshot,
|
||||||
|
workflowRunStepToOpenByDefaultState,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDefined(workflowStepToOpenByDefault)) {
|
||||||
|
const workflowId = getSnapshotValue(snapshot, workflowIdState);
|
||||||
|
if (!isDefined(workflowId)) {
|
||||||
|
throw new Error(
|
||||||
|
'The workflow id must be set; ensure the workflow id is always set before rendering the workflow diagram.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(workflowSelectedNodeState, workflowStepToOpenByDefault.id);
|
||||||
|
|
||||||
|
openWorkflowRunViewStepInCommandMenu({
|
||||||
|
workflowId,
|
||||||
|
title: workflowStepToOpenByDefault.data.name,
|
||||||
|
icon: getIcon(
|
||||||
|
getWorkflowNodeIconKey(workflowStepToOpenByDefault.data),
|
||||||
|
),
|
||||||
|
workflowSelectedNode: workflowStepToOpenByDefault.id,
|
||||||
|
stepExecutionStatus: workflowStepToOpenByDefault.data.runStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
set(workflowRunStepToOpenByDefaultState, undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[getIcon, isInRightDrawer, openWorkflowRunViewStepInCommandMenu],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleWorkflowRunDiagramCanvasInit,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled';
|
||||||
|
import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled';
|
||||||
|
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||||
|
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
|
||||||
|
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||||
|
import { WorkflowDiagramRunStatus } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { WorkflowRunTabId } from '@/workflow/workflow-steps/types/WorkflowRunTabId';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
export const useSetInitialWorkflowRunRightDrawerTab = () => {
|
||||||
|
const setInitialWorkflowRunRightDrawerTab = useRecoilCallback(
|
||||||
|
({ snapshot, set }) =>
|
||||||
|
({
|
||||||
|
workflowSelectedNode,
|
||||||
|
stepExecutionStatus,
|
||||||
|
}: {
|
||||||
|
workflowSelectedNode: string;
|
||||||
|
stepExecutionStatus: WorkflowDiagramRunStatus;
|
||||||
|
}) => {
|
||||||
|
const commandMenuPageInfo = getSnapshotValue(
|
||||||
|
snapshot,
|
||||||
|
commandMenuPageInfoState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeWorkflowRunRightDrawerTab = getSnapshotValue(
|
||||||
|
snapshot,
|
||||||
|
activeTabIdComponentState.atomFamily({
|
||||||
|
instanceId: commandMenuPageInfo.instanceId,
|
||||||
|
}),
|
||||||
|
) as WorkflowRunTabId | null;
|
||||||
|
|
||||||
|
const isInputTabDisabled = getIsInputTabDisabled({
|
||||||
|
stepExecutionStatus,
|
||||||
|
workflowSelectedNode,
|
||||||
|
});
|
||||||
|
const isOutputTabDisabled = getIsOutputTabDisabled({
|
||||||
|
stepExecutionStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDefined(activeWorkflowRunRightDrawerTab)) {
|
||||||
|
const defaultTabId = isOutputTabDisabled
|
||||||
|
? WorkflowRunTabId.NODE
|
||||||
|
: WorkflowRunTabId.OUTPUT;
|
||||||
|
|
||||||
|
set(
|
||||||
|
activeTabIdComponentState.atomFamily({
|
||||||
|
instanceId: commandMenuPageInfo.instanceId,
|
||||||
|
}),
|
||||||
|
defaultTabId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(isInputTabDisabled &&
|
||||||
|
activeWorkflowRunRightDrawerTab === WorkflowRunTabId.INPUT) ||
|
||||||
|
(isOutputTabDisabled &&
|
||||||
|
activeWorkflowRunRightDrawerTab === WorkflowRunTabId.OUTPUT)
|
||||||
|
) {
|
||||||
|
set(
|
||||||
|
activeTabIdComponentState.atomFamily({
|
||||||
|
instanceId: commandMenuPageInfo.instanceId,
|
||||||
|
}),
|
||||||
|
WorkflowRunTabId.NODE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
setInitialWorkflowRunRightDrawerTab,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { createState } from 'twenty-ui/utilities';
|
||||||
|
|
||||||
|
export const workflowDiagramStatusState = createState<
|
||||||
|
'computing-diagram' | 'computing-dimensions' | 'done'
|
||||||
|
>({
|
||||||
|
key: 'workflowDiagramStatusState',
|
||||||
|
defaultValue: 'computing-diagram',
|
||||||
|
});
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { WorkflowRunDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { createState } from 'twenty-ui/utilities';
|
||||||
|
|
||||||
|
export const workflowRunStepToOpenByDefaultState = createState<
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
data: WorkflowRunDiagramStepNodeData;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
>({
|
||||||
|
key: 'workflowStepIdToOpenByDefaultState',
|
||||||
|
defaultValue: undefined,
|
||||||
|
});
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { createState } from 'twenty-ui/utilities';
|
import { createState } from 'twenty-ui/utilities';
|
||||||
|
|
||||||
export const workflowSelectedNodeState = createState<string | undefined>({
|
export const workflowSelectedNodeState = createState<string | undefined>({
|
||||||
key: 'workflowSelectedNodeState',
|
key: 'workflowSelectedNodeState',
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
WorkflowStep,
|
WorkflowStep,
|
||||||
WorkflowTrigger,
|
WorkflowTrigger,
|
||||||
} from '@/workflow/types/Workflow';
|
} from '@/workflow/types/Workflow';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { getUuidV4Mock } from '~/testing/utils/getUuidV4Mock';
|
import { getUuidV4Mock } from '~/testing/utils/getUuidV4Mock';
|
||||||
import { generateWorkflowRunDiagram } from '../generateWorkflowRunDiagram';
|
import { generateWorkflowRunDiagram } from '../generateWorkflowRunDiagram';
|
||||||
|
|
||||||
@ -87,94 +88,94 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"edges": [
|
"diagram": {
|
||||||
{
|
"edges": [
|
||||||
"deletable": false,
|
{
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-0",
|
"deletable": false,
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"id": "8f3b2121-f194-4ba4-9fbf-0",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
"selectable": false,
|
"markerStart": "workflow-edge-green-circle",
|
||||||
"source": "trigger",
|
"selectable": false,
|
||||||
"target": "step1",
|
"source": "trigger",
|
||||||
"type": "success",
|
"target": "step1",
|
||||||
},
|
"type": "success",
|
||||||
{
|
|
||||||
"deletable": false,
|
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-1",
|
|
||||||
"markerEnd": "workflow-edge-arrow-rounded",
|
|
||||||
"markerStart": "workflow-edge-gray-circle",
|
|
||||||
"selectable": false,
|
|
||||||
"source": "step1",
|
|
||||||
"target": "step2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deletable": false,
|
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-2",
|
|
||||||
"markerEnd": "workflow-edge-arrow-rounded",
|
|
||||||
"markerStart": "workflow-edge-gray-circle",
|
|
||||||
"selectable": false,
|
|
||||||
"source": "step2",
|
|
||||||
"target": "step3",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"icon": "IconPlaylistAdd",
|
|
||||||
"name": "Company created",
|
|
||||||
"nodeType": "trigger",
|
|
||||||
"runStatus": "success",
|
|
||||||
"triggerType": "DATABASE_EVENT",
|
|
||||||
},
|
},
|
||||||
"id": "trigger",
|
{
|
||||||
"position": {
|
"deletable": false,
|
||||||
"x": 0,
|
"id": "8f3b2121-f194-4ba4-9fbf-1",
|
||||||
"y": 0,
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step1",
|
||||||
|
"target": "step2",
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
"deletable": false,
|
||||||
"data": {
|
"id": "8f3b2121-f194-4ba4-9fbf-2",
|
||||||
"actionType": "CODE",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"name": "Step 1",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"nodeType": "action",
|
"selectable": false,
|
||||||
"runStatus": "failure",
|
"source": "step2",
|
||||||
|
"target": "step3",
|
||||||
},
|
},
|
||||||
"id": "step1",
|
],
|
||||||
"position": {
|
"nodes": [
|
||||||
"x": 0,
|
{
|
||||||
"y": 0,
|
"data": {
|
||||||
|
"icon": "IconPlaylistAdd",
|
||||||
|
"name": "Company created",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"runStatus": "success",
|
||||||
|
"triggerType": "DATABASE_EVENT",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"selected": false,
|
{
|
||||||
},
|
"data": {
|
||||||
{
|
"actionType": "CODE",
|
||||||
"data": {
|
"name": "Step 1",
|
||||||
"actionType": "CODE",
|
"nodeType": "action",
|
||||||
"name": "Step 2",
|
"runStatus": "failure",
|
||||||
"nodeType": "action",
|
},
|
||||||
"runStatus": "not-executed",
|
"id": "step1",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"id": "step2",
|
{
|
||||||
"position": {
|
"data": {
|
||||||
"x": 0,
|
"actionType": "CODE",
|
||||||
"y": 150,
|
"name": "Step 2",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "not-executed",
|
||||||
|
},
|
||||||
|
"id": "step2",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 150,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"selected": false,
|
{
|
||||||
},
|
"data": {
|
||||||
{
|
"actionType": "CODE",
|
||||||
"data": {
|
"name": "Step 3",
|
||||||
"actionType": "CODE",
|
"nodeType": "action",
|
||||||
"name": "Step 3",
|
"runStatus": "not-executed",
|
||||||
"nodeType": "action",
|
},
|
||||||
"runStatus": "not-executed",
|
"id": "step3",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 300,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"id": "step3",
|
],
|
||||||
"position": {
|
},
|
||||||
"x": 0,
|
"stepToOpenByDefault": undefined,
|
||||||
"y": 300,
|
|
||||||
},
|
|
||||||
"selected": false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -263,96 +264,96 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"edges": [
|
"diagram": {
|
||||||
{
|
"edges": [
|
||||||
"deletable": false,
|
{
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-3",
|
"deletable": false,
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"id": "8f3b2121-f194-4ba4-9fbf-3",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
"selectable": false,
|
"markerStart": "workflow-edge-green-circle",
|
||||||
"source": "trigger",
|
"selectable": false,
|
||||||
"target": "step1",
|
"source": "trigger",
|
||||||
"type": "success",
|
"target": "step1",
|
||||||
},
|
"type": "success",
|
||||||
{
|
|
||||||
"deletable": false,
|
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-4",
|
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
|
||||||
"markerStart": "workflow-edge-green-circle",
|
|
||||||
"selectable": false,
|
|
||||||
"source": "step1",
|
|
||||||
"target": "step2",
|
|
||||||
"type": "success",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deletable": false,
|
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-5",
|
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
|
||||||
"markerStart": "workflow-edge-green-circle",
|
|
||||||
"selectable": false,
|
|
||||||
"source": "step2",
|
|
||||||
"target": "step3",
|
|
||||||
"type": "success",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"icon": "IconPlaylistAdd",
|
|
||||||
"name": "Company created",
|
|
||||||
"nodeType": "trigger",
|
|
||||||
"runStatus": "success",
|
|
||||||
"triggerType": "DATABASE_EVENT",
|
|
||||||
},
|
},
|
||||||
"id": "trigger",
|
{
|
||||||
"position": {
|
"deletable": false,
|
||||||
"x": 0,
|
"id": "8f3b2121-f194-4ba4-9fbf-4",
|
||||||
"y": 0,
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-green-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step1",
|
||||||
|
"target": "step2",
|
||||||
|
"type": "success",
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
"deletable": false,
|
||||||
"data": {
|
"id": "8f3b2121-f194-4ba4-9fbf-5",
|
||||||
"actionType": "CODE",
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
"name": "Step 1",
|
"markerStart": "workflow-edge-green-circle",
|
||||||
"nodeType": "action",
|
"selectable": false,
|
||||||
"runStatus": "success",
|
"source": "step2",
|
||||||
|
"target": "step3",
|
||||||
|
"type": "success",
|
||||||
},
|
},
|
||||||
"id": "step1",
|
],
|
||||||
"position": {
|
"nodes": [
|
||||||
"x": 0,
|
{
|
||||||
"y": 0,
|
"data": {
|
||||||
|
"icon": "IconPlaylistAdd",
|
||||||
|
"name": "Company created",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"runStatus": "success",
|
||||||
|
"triggerType": "DATABASE_EVENT",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"selected": false,
|
{
|
||||||
},
|
"data": {
|
||||||
{
|
"actionType": "CODE",
|
||||||
"data": {
|
"name": "Step 1",
|
||||||
"actionType": "CODE",
|
"nodeType": "action",
|
||||||
"name": "Step 2",
|
"runStatus": "success",
|
||||||
"nodeType": "action",
|
},
|
||||||
"runStatus": "success",
|
"id": "step1",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"id": "step2",
|
{
|
||||||
"position": {
|
"data": {
|
||||||
"x": 0,
|
"actionType": "CODE",
|
||||||
"y": 150,
|
"name": "Step 2",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "success",
|
||||||
|
},
|
||||||
|
"id": "step2",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 150,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"selected": false,
|
{
|
||||||
},
|
"data": {
|
||||||
{
|
"actionType": "CODE",
|
||||||
"data": {
|
"name": "Step 3",
|
||||||
"actionType": "CODE",
|
"nodeType": "action",
|
||||||
"name": "Step 3",
|
"runStatus": "success",
|
||||||
"nodeType": "action",
|
},
|
||||||
"runStatus": "success",
|
"id": "step3",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 300,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"id": "step3",
|
],
|
||||||
"position": {
|
},
|
||||||
"x": 0,
|
"stepToOpenByDefault": undefined,
|
||||||
"y": 300,
|
|
||||||
},
|
|
||||||
"selected": false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -428,94 +429,94 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"edges": [
|
"diagram": {
|
||||||
{
|
"edges": [
|
||||||
"deletable": false,
|
{
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-6",
|
"deletable": false,
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"id": "8f3b2121-f194-4ba4-9fbf-6",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
"selectable": false,
|
"markerStart": "workflow-edge-green-circle",
|
||||||
"source": "trigger",
|
"selectable": false,
|
||||||
"target": "step1",
|
"source": "trigger",
|
||||||
"type": "success",
|
"target": "step1",
|
||||||
},
|
"type": "success",
|
||||||
{
|
|
||||||
"deletable": false,
|
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-7",
|
|
||||||
"markerEnd": "workflow-edge-arrow-rounded",
|
|
||||||
"markerStart": "workflow-edge-gray-circle",
|
|
||||||
"selectable": false,
|
|
||||||
"source": "step1",
|
|
||||||
"target": "step2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deletable": false,
|
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-8",
|
|
||||||
"markerEnd": "workflow-edge-arrow-rounded",
|
|
||||||
"markerStart": "workflow-edge-gray-circle",
|
|
||||||
"selectable": false,
|
|
||||||
"source": "step2",
|
|
||||||
"target": "step3",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"icon": "IconPlaylistAdd",
|
|
||||||
"name": "Company created",
|
|
||||||
"nodeType": "trigger",
|
|
||||||
"runStatus": "success",
|
|
||||||
"triggerType": "DATABASE_EVENT",
|
|
||||||
},
|
},
|
||||||
"id": "trigger",
|
{
|
||||||
"position": {
|
"deletable": false,
|
||||||
"x": 0,
|
"id": "8f3b2121-f194-4ba4-9fbf-7",
|
||||||
"y": 0,
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step1",
|
||||||
|
"target": "step2",
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
"deletable": false,
|
||||||
"data": {
|
"id": "8f3b2121-f194-4ba4-9fbf-8",
|
||||||
"actionType": "CODE",
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
"name": "Step 1",
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
"nodeType": "action",
|
"selectable": false,
|
||||||
"runStatus": "running",
|
"source": "step2",
|
||||||
|
"target": "step3",
|
||||||
},
|
},
|
||||||
"id": "step1",
|
],
|
||||||
"position": {
|
"nodes": [
|
||||||
"x": 0,
|
{
|
||||||
"y": 0,
|
"data": {
|
||||||
|
"icon": "IconPlaylistAdd",
|
||||||
|
"name": "Company created",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"runStatus": "success",
|
||||||
|
"triggerType": "DATABASE_EVENT",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"selected": false,
|
{
|
||||||
},
|
"data": {
|
||||||
{
|
"actionType": "CODE",
|
||||||
"data": {
|
"name": "Step 1",
|
||||||
"actionType": "CODE",
|
"nodeType": "action",
|
||||||
"name": "Step 2",
|
"runStatus": "running",
|
||||||
"nodeType": "action",
|
},
|
||||||
"runStatus": "not-executed",
|
"id": "step1",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"id": "step2",
|
{
|
||||||
"position": {
|
"data": {
|
||||||
"x": 0,
|
"actionType": "CODE",
|
||||||
"y": 150,
|
"name": "Step 2",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "not-executed",
|
||||||
|
},
|
||||||
|
"id": "step2",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 150,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"selected": false,
|
{
|
||||||
},
|
"data": {
|
||||||
{
|
"actionType": "CODE",
|
||||||
"data": {
|
"name": "Step 3",
|
||||||
"actionType": "CODE",
|
"nodeType": "action",
|
||||||
"name": "Step 3",
|
"runStatus": "not-executed",
|
||||||
"nodeType": "action",
|
},
|
||||||
"runStatus": "not-executed",
|
"id": "step3",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 300,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"id": "step3",
|
],
|
||||||
"position": {
|
},
|
||||||
"x": 0,
|
"stepToOpenByDefault": undefined,
|
||||||
"y": 300,
|
|
||||||
},
|
|
||||||
"selected": false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@ -614,118 +615,219 @@ describe('generateWorkflowRunDiagram', () => {
|
|||||||
|
|
||||||
expect(result).toMatchInlineSnapshot(`
|
expect(result).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"edges": [
|
"diagram": {
|
||||||
{
|
"edges": [
|
||||||
"deletable": false,
|
{
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-9",
|
"deletable": false,
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
"id": "8f3b2121-f194-4ba4-9fbf-9",
|
||||||
"markerStart": "workflow-edge-green-circle",
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
"selectable": false,
|
"markerStart": "workflow-edge-green-circle",
|
||||||
"source": "trigger",
|
"selectable": false,
|
||||||
"target": "step1",
|
"source": "trigger",
|
||||||
"type": "success",
|
"target": "step1",
|
||||||
|
"type": "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-10",
|
||||||
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-green-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step1",
|
||||||
|
"target": "step2",
|
||||||
|
"type": "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-11",
|
||||||
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step2",
|
||||||
|
"target": "step3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-12",
|
||||||
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step3",
|
||||||
|
"target": "step4",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"icon": "IconPlaylistAdd",
|
||||||
|
"name": "Company created",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"runStatus": "success",
|
||||||
|
"triggerType": "DATABASE_EVENT",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"name": "Step 1",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "success",
|
||||||
|
},
|
||||||
|
"id": "step1",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"name": "Step 2",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "running",
|
||||||
|
},
|
||||||
|
"id": "step2",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 150,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"name": "Step 3",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "not-executed",
|
||||||
|
},
|
||||||
|
"id": "step3",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"name": "Step 4",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "not-executed",
|
||||||
|
},
|
||||||
|
"id": "step4",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 450,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"stepToOpenByDefault": undefined,
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks node as running when a Form step is pending and return its data as the stepToOpenByDefault object', () => {
|
||||||
|
const trigger: WorkflowTrigger = {
|
||||||
|
name: 'Company created',
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
settings: {
|
||||||
|
eventName: 'company.created',
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const steps: WorkflowStep[] = [
|
||||||
|
{
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Step 1',
|
||||||
|
type: 'FORM',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
id: 'field-1',
|
||||||
|
name: 'text',
|
||||||
|
label: 'Text Field',
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
placeholder: 'Enter text',
|
||||||
|
settings: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const stepsOutput = {
|
||||||
|
step1: {
|
||||||
|
result: undefined,
|
||||||
|
error: undefined,
|
||||||
|
pendingEvent: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput });
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"diagram": {
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-13",
|
||||||
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-green-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "trigger",
|
||||||
|
"target": "step1",
|
||||||
|
"type": "success",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"icon": "IconPlaylistAdd",
|
||||||
|
"name": "Company created",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"runStatus": "success",
|
||||||
|
"triggerType": "DATABASE_EVENT",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "FORM",
|
||||||
|
"name": "Step 1",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "running",
|
||||||
|
},
|
||||||
|
"id": "step1",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"stepToOpenByDefault": {
|
||||||
|
"data": {
|
||||||
|
"actionType": "FORM",
|
||||||
|
"name": "Step 1",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "running",
|
||||||
},
|
},
|
||||||
{
|
"id": "step1",
|
||||||
"deletable": false,
|
},
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-10",
|
|
||||||
"markerEnd": "workflow-edge-green-arrow-rounded",
|
|
||||||
"markerStart": "workflow-edge-green-circle",
|
|
||||||
"selectable": false,
|
|
||||||
"source": "step1",
|
|
||||||
"target": "step2",
|
|
||||||
"type": "success",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deletable": false,
|
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-11",
|
|
||||||
"markerEnd": "workflow-edge-arrow-rounded",
|
|
||||||
"markerStart": "workflow-edge-gray-circle",
|
|
||||||
"selectable": false,
|
|
||||||
"source": "step2",
|
|
||||||
"target": "step3",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"deletable": false,
|
|
||||||
"id": "8f3b2121-f194-4ba4-9fbf-12",
|
|
||||||
"markerEnd": "workflow-edge-arrow-rounded",
|
|
||||||
"markerStart": "workflow-edge-gray-circle",
|
|
||||||
"selectable": false,
|
|
||||||
"source": "step3",
|
|
||||||
"target": "step4",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"icon": "IconPlaylistAdd",
|
|
||||||
"name": "Company created",
|
|
||||||
"nodeType": "trigger",
|
|
||||||
"runStatus": "success",
|
|
||||||
"triggerType": "DATABASE_EVENT",
|
|
||||||
},
|
|
||||||
"id": "trigger",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"actionType": "CODE",
|
|
||||||
"name": "Step 1",
|
|
||||||
"nodeType": "action",
|
|
||||||
"runStatus": "success",
|
|
||||||
},
|
|
||||||
"id": "step1",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
},
|
|
||||||
"selected": false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"actionType": "CODE",
|
|
||||||
"name": "Step 2",
|
|
||||||
"nodeType": "action",
|
|
||||||
"runStatus": "running",
|
|
||||||
},
|
|
||||||
"id": "step2",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 150,
|
|
||||||
},
|
|
||||||
"selected": false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"actionType": "CODE",
|
|
||||||
"name": "Step 3",
|
|
||||||
"nodeType": "action",
|
|
||||||
"runStatus": "not-executed",
|
|
||||||
},
|
|
||||||
"id": "step3",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 300,
|
|
||||||
},
|
|
||||||
"selected": false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"actionType": "CODE",
|
|
||||||
"name": "Step 4",
|
|
||||||
"nodeType": "action",
|
|
||||||
"runStatus": "not-executed",
|
|
||||||
},
|
|
||||||
"id": "step4",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 450,
|
|
||||||
},
|
|
||||||
"selected": false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,91 @@
|
|||||||
|
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { selectWorkflowDiagramNode } from '../selectWorkflowDiagramNode';
|
||||||
|
|
||||||
|
describe('selectWorkflowDiagramNode', () => {
|
||||||
|
it('should select the specified node', () => {
|
||||||
|
const diagram: WorkflowDiagram = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
selected: false,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {
|
||||||
|
name: 'Node 1',
|
||||||
|
nodeType: 'action',
|
||||||
|
actionType: 'CODE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
selected: false,
|
||||||
|
position: { x: 0, y: 150 },
|
||||||
|
data: {
|
||||||
|
name: 'Node 2',
|
||||||
|
nodeType: 'action',
|
||||||
|
actionType: 'CODE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = selectWorkflowDiagramNode({
|
||||||
|
diagram,
|
||||||
|
nodeIdToSelect: '1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.nodes[0].selected).toBe(true);
|
||||||
|
expect(result.nodes[1].selected).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return same diagram when node is not found', () => {
|
||||||
|
const diagram: WorkflowDiagram = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
selected: false,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {
|
||||||
|
name: 'Node 1',
|
||||||
|
nodeType: 'action',
|
||||||
|
actionType: 'CODE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = selectWorkflowDiagramNode({
|
||||||
|
diagram,
|
||||||
|
nodeIdToSelect: 'non-existent',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(diagram);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not mutate original diagram', () => {
|
||||||
|
const diagram: WorkflowDiagram = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
selected: false,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {
|
||||||
|
name: 'Node 1',
|
||||||
|
nodeType: 'action',
|
||||||
|
actionType: 'CODE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [],
|
||||||
|
};
|
||||||
|
const originalDiagram = JSON.parse(JSON.stringify(diagram));
|
||||||
|
|
||||||
|
selectWorkflowDiagramNode({
|
||||||
|
diagram,
|
||||||
|
nodeIdToSelect: '1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(diagram).toEqual(originalDiagram);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -12,6 +12,8 @@ import {
|
|||||||
WorkflowRunDiagram,
|
WorkflowRunDiagram,
|
||||||
WorkflowRunDiagramEdge,
|
WorkflowRunDiagramEdge,
|
||||||
WorkflowRunDiagramNode,
|
WorkflowRunDiagramNode,
|
||||||
|
WorkflowRunDiagramNodeData,
|
||||||
|
WorkflowRunDiagramStepNodeData,
|
||||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode';
|
import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode';
|
||||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||||
@ -26,7 +28,22 @@ export const generateWorkflowRunDiagram = ({
|
|||||||
trigger: WorkflowTrigger;
|
trigger: WorkflowTrigger;
|
||||||
steps: Array<WorkflowStep>;
|
steps: Array<WorkflowStep>;
|
||||||
stepsOutput: WorkflowRunOutputStepsOutput | undefined;
|
stepsOutput: WorkflowRunOutputStepsOutput | undefined;
|
||||||
}): WorkflowRunDiagram => {
|
}): {
|
||||||
|
diagram: WorkflowRunDiagram;
|
||||||
|
stepToOpenByDefault:
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
data: WorkflowRunDiagramStepNodeData;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
} => {
|
||||||
|
let stepToOpenByDefault:
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
data: WorkflowRunDiagramStepNodeData;
|
||||||
|
}
|
||||||
|
| undefined = undefined;
|
||||||
|
|
||||||
const triggerBase = getWorkflowDiagramTriggerNode({ trigger });
|
const triggerBase = getWorkflowDiagramTriggerNode({ trigger });
|
||||||
|
|
||||||
const nodes: Array<WorkflowRunDiagramNode> = [
|
const nodes: Array<WorkflowRunDiagramNode> = [
|
||||||
@ -97,21 +114,29 @@ export const generateWorkflowRunDiagram = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nodeData: WorkflowRunDiagramNodeData = {
|
||||||
|
nodeType: 'action',
|
||||||
|
actionType: step.type,
|
||||||
|
name: step.name,
|
||||||
|
runStatus,
|
||||||
|
};
|
||||||
|
|
||||||
nodes.push({
|
nodes.push({
|
||||||
id: nodeId,
|
id: nodeId,
|
||||||
data: {
|
data: nodeData,
|
||||||
nodeType: 'action',
|
|
||||||
actionType: step.type,
|
|
||||||
name: step.name,
|
|
||||||
runStatus,
|
|
||||||
},
|
|
||||||
position: {
|
position: {
|
||||||
x: xPos,
|
x: xPos,
|
||||||
y: yPos,
|
y: yPos,
|
||||||
},
|
},
|
||||||
selected: isPendingFormAction,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isPendingFormAction) {
|
||||||
|
stepToOpenByDefault = {
|
||||||
|
id: nodeId,
|
||||||
|
data: nodeData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
processNode({
|
processNode({
|
||||||
stepIndex: stepIndex + 1,
|
stepIndex: stepIndex + 1,
|
||||||
parentNodeId: nodeId,
|
parentNodeId: nodeId,
|
||||||
@ -134,7 +159,10 @@ export const generateWorkflowRunDiagram = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodes,
|
diagram: {
|
||||||
edges,
|
nodes,
|
||||||
|
edges,
|
||||||
|
},
|
||||||
|
stepToOpenByDefault,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
|
||||||
|
export const selectWorkflowDiagramNode = <T extends WorkflowDiagram>({
|
||||||
|
diagram,
|
||||||
|
nodeIdToSelect,
|
||||||
|
}: {
|
||||||
|
diagram: T;
|
||||||
|
nodeIdToSelect: string;
|
||||||
|
}): T => {
|
||||||
|
return {
|
||||||
|
...diagram,
|
||||||
|
nodes: diagram.nodes.map((node) => {
|
||||||
|
if (node.id === nodeIdToSelect) {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
selected: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export const WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID =
|
|
||||||
'workflow-run-step-side-panel-tab-list';
|
|
||||||
Reference in New Issue
Block a user