Create a right drawer for viewing steps in workflow runs (#10366)
- Improve the type-safety of the objects mapping the id of a right drawer or side panel view to a React component - Improve the types of the `useTabList` hook to type the available tab identifiers strictly - Create a specialized `WorkflowRunDiagramCanvas` component to render a `WorkflowRunDiagramCanvasEffect` component that opens `RightDrawerPages.WorkflowRunStepView` when a step is selected - Create a new side panel view specifically for workflow run step details - Create tab list in the new side panel; all the tabs are `Node`, `Input` and `Output` - Create a hook `useWorkflowSelectedNodeOrThrow` not to duplicate throwing mechanisms Closes https://github.com/twentyhq/core-team-issues/issues/432 ## Demo https://github.com/user-attachments/assets/8d5df7dc-0b99-49a2-9a54-d3eaee80a8e6
This commit is contained in:
committed by
GitHub
parent
694553608b
commit
f74e4bedc4
@ -6,6 +6,7 @@ import { CommandMenuSearchRecordsPage } from '@/command-menu/pages/components/Co
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { RightDrawerRecord } from '@/object-record/record-right-drawer/components/RightDrawerRecord';
|
||||
import { RightDrawerWorkflowEditStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowEditStep';
|
||||
import { RightDrawerWorkflowRunViewStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowRunViewStep';
|
||||
import { RightDrawerWorkflowViewStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowViewStep';
|
||||
import { RightDrawerWorkflowSelectAction } from '@/workflow/workflow-steps/workflow-actions/components/RightDrawerWorkflowSelectAction';
|
||||
import { RightDrawerWorkflowSelectTriggerType } from '@/workflow/workflow-trigger/components/RightDrawerWorkflowSelectTriggerType';
|
||||
@ -29,5 +30,6 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
|
||||
],
|
||||
[CommandMenuPages.WorkflowStepEdit, <RightDrawerWorkflowEditStep />],
|
||||
[CommandMenuPages.WorkflowStepView, <RightDrawerWorkflowViewStep />],
|
||||
[CommandMenuPages.WorkflowRunStepView, <RightDrawerWorkflowRunViewStep />],
|
||||
[CommandMenuPages.SearchRecords, <CommandMenuSearchRecordsPage />],
|
||||
]);
|
||||
|
||||
@ -8,5 +8,6 @@ export enum CommandMenuPages {
|
||||
WorkflowStepSelectAction = 'workflow-step-select-action',
|
||||
WorkflowStepView = 'workflow-step-view',
|
||||
WorkflowStepEdit = 'workflow-step-edit',
|
||||
WorkflowRunStepView = 'workflow-run-step-view',
|
||||
SearchRecords = 'search-records',
|
||||
}
|
||||
|
||||
@ -9,8 +9,8 @@ import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isR
|
||||
|
||||
import { RightDrawerContainer } from '@/ui/layout/right-drawer/components/RightDrawerContainer';
|
||||
import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar';
|
||||
import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage';
|
||||
import { RightDrawerWorkflowEditStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowEditStep';
|
||||
import { RightDrawerWorkflowRunViewStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowRunViewStep';
|
||||
import { RightDrawerWorkflowViewStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowViewStep';
|
||||
import { RightDrawerWorkflowSelectAction } from '@/workflow/workflow-steps/workflow-actions/components/RightDrawerWorkflowSelectAction';
|
||||
import { RightDrawerWorkflowSelectTriggerType } from '@/workflow/workflow-trigger/components/RightDrawerWorkflowSelectTriggerType';
|
||||
@ -28,7 +28,7 @@ const StyledRightDrawerBody = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = {
|
||||
const RIGHT_DRAWER_PAGES_CONFIG = {
|
||||
[RightDrawerPages.ViewEmailThread]: <RightDrawerEmailThread />,
|
||||
[RightDrawerPages.ViewCalendarEvent]: <RightDrawerCalendarEvent />,
|
||||
[RightDrawerPages.ViewRecord]: <RightDrawerRecord />,
|
||||
@ -41,7 +41,8 @@ const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = {
|
||||
),
|
||||
[RightDrawerPages.WorkflowStepEdit]: <RightDrawerWorkflowEditStep />,
|
||||
[RightDrawerPages.WorkflowStepView]: <RightDrawerWorkflowViewStep />,
|
||||
};
|
||||
[RightDrawerPages.WorkflowRunStepView]: <RightDrawerWorkflowRunViewStep />,
|
||||
} satisfies Record<RightDrawerPages, JSX.Element>;
|
||||
|
||||
export const RightDrawerRouter = () => {
|
||||
const [rightDrawerPage] = useRecoilState(rightDrawerPageState);
|
||||
|
||||
@ -9,4 +9,5 @@ export const RIGHT_DRAWER_PAGE_ICONS = {
|
||||
[RightDrawerPages.WorkflowStepSelectAction]: 'IconSparkles',
|
||||
[RightDrawerPages.WorkflowStepEdit]: 'IconSparkles',
|
||||
[RightDrawerPages.WorkflowStepView]: 'IconSparkles',
|
||||
};
|
||||
[RightDrawerPages.WorkflowRunStepView]: 'IconSparkles',
|
||||
} satisfies Record<RightDrawerPages, string>;
|
||||
|
||||
@ -9,4 +9,5 @@ export const RIGHT_DRAWER_PAGE_TITLES = {
|
||||
[RightDrawerPages.WorkflowStepSelectAction]: 'Workflow',
|
||||
[RightDrawerPages.WorkflowStepEdit]: 'Workflow',
|
||||
[RightDrawerPages.WorkflowStepView]: 'Workflow',
|
||||
};
|
||||
[RightDrawerPages.WorkflowRunStepView]: 'Workflow',
|
||||
} satisfies Record<RightDrawerPages, string>;
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
|
||||
export type ComponentByRightDrawerPage = {
|
||||
[componentName in RightDrawerPages]?: JSX.Element;
|
||||
};
|
||||
@ -7,4 +7,5 @@ export enum RightDrawerPages {
|
||||
WorkflowStepSelectAction = 'workflow-step-select-action',
|
||||
WorkflowStepView = 'workflow-step-view',
|
||||
WorkflowStepEdit = 'workflow-step-edit',
|
||||
WorkflowRunStepView = 'workflow-run-step-view',
|
||||
}
|
||||
|
||||
@ -4,24 +4,25 @@ import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPage
|
||||
export const mapRightDrawerPageToCommandMenuPage = (
|
||||
rightDrawerPage: RightDrawerPages,
|
||||
) => {
|
||||
switch (rightDrawerPage) {
|
||||
case RightDrawerPages.ViewRecord:
|
||||
return CommandMenuPages.ViewRecord;
|
||||
case RightDrawerPages.ViewEmailThread:
|
||||
return CommandMenuPages.ViewEmailThread;
|
||||
case RightDrawerPages.ViewCalendarEvent:
|
||||
return CommandMenuPages.ViewCalendarEvent;
|
||||
case RightDrawerPages.Copilot:
|
||||
return CommandMenuPages.Copilot;
|
||||
case RightDrawerPages.WorkflowStepSelectTriggerType:
|
||||
return CommandMenuPages.WorkflowStepSelectTriggerType;
|
||||
case RightDrawerPages.WorkflowStepSelectAction:
|
||||
return CommandMenuPages.WorkflowStepSelectAction;
|
||||
case RightDrawerPages.WorkflowStepView:
|
||||
return CommandMenuPages.WorkflowStepView;
|
||||
case RightDrawerPages.WorkflowStepEdit:
|
||||
return CommandMenuPages.WorkflowStepEdit;
|
||||
default:
|
||||
return CommandMenuPages.Root;
|
||||
}
|
||||
const rightDrawerPagesToCommandMenuPages: Record<
|
||||
RightDrawerPages,
|
||||
CommandMenuPages
|
||||
> = {
|
||||
[RightDrawerPages.ViewRecord]: CommandMenuPages.ViewRecord,
|
||||
[RightDrawerPages.ViewEmailThread]: CommandMenuPages.ViewEmailThread,
|
||||
[RightDrawerPages.ViewCalendarEvent]: CommandMenuPages.ViewCalendarEvent,
|
||||
[RightDrawerPages.Copilot]: CommandMenuPages.Copilot,
|
||||
[RightDrawerPages.WorkflowStepSelectTriggerType]:
|
||||
CommandMenuPages.WorkflowStepSelectTriggerType,
|
||||
[RightDrawerPages.WorkflowStepSelectAction]:
|
||||
CommandMenuPages.WorkflowStepSelectAction,
|
||||
[RightDrawerPages.WorkflowStepView]: CommandMenuPages.WorkflowStepView,
|
||||
[RightDrawerPages.WorkflowRunStepView]:
|
||||
CommandMenuPages.WorkflowRunStepView,
|
||||
[RightDrawerPages.WorkflowStepEdit]: CommandMenuPages.WorkflowStepEdit,
|
||||
};
|
||||
|
||||
return (
|
||||
rightDrawerPagesToCommandMenuPages[rightDrawerPage] ?? CommandMenuPages.Root
|
||||
);
|
||||
};
|
||||
|
||||
@ -9,6 +9,7 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
||||
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
|
||||
import { ShowPageSubContainerTabListContainer } from '@/ui/layout/show-page/components/ShowPageSubContainerTabListContainer';
|
||||
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
@ -26,14 +27,8 @@ const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>`
|
||||
`;
|
||||
|
||||
const StyledTabListContainer = styled.div<{ shouldDisplay: boolean }>`
|
||||
align-items: center;
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
||||
box-sizing: border-box;
|
||||
display: ${({ shouldDisplay }) => (shouldDisplay ? 'flex' : 'none')};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: 40px;
|
||||
`;
|
||||
`.withComponent(ShowPageSubContainerTabListContainer);
|
||||
|
||||
const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>`
|
||||
flex: 1;
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledTabListContainer = styled.div`
|
||||
align-items: center;
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: 40px;
|
||||
`;
|
||||
|
||||
export { StyledTabListContainer as ShowPageSubContainerTabListContainer };
|
||||
@ -9,10 +9,10 @@ import { useEffect } from 'react';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
import { Tab } from './Tab';
|
||||
|
||||
export type SingleTabProps = {
|
||||
export type SingleTabProps<T extends string = string> = {
|
||||
title: string;
|
||||
Icon?: IconComponent;
|
||||
id: string;
|
||||
id: T;
|
||||
hide?: boolean;
|
||||
disabled?: boolean;
|
||||
pill?: string | React.ReactElement;
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { RecoilState, useRecoilState } from 'recoil';
|
||||
|
||||
import { useTabListStates } from '@/ui/layout/tab/hooks/internal/useTabListStates';
|
||||
|
||||
export const useTabList = (tabListId?: string) => {
|
||||
export const useTabList = <T extends string>(tabListId?: string) => {
|
||||
const { activeTabIdState } = useTabListStates({
|
||||
tabListScopeId: tabListId,
|
||||
});
|
||||
|
||||
const [activeTabId, setActiveTabId] = useRecoilState(activeTabIdState);
|
||||
const [activeTabId, setActiveTabId] = useRecoilState(
|
||||
activeTabIdState as RecoilState<T | null>,
|
||||
);
|
||||
|
||||
return {
|
||||
activeTabId,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||
import { WorkflowRun } from '@/workflow/types/Workflow';
|
||||
import { WorkflowDiagramCanvasReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly';
|
||||
import { WorkflowRunDiagramCanvas } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas';
|
||||
import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
@ -18,7 +18,7 @@ export const WorkflowRunVisualizerContent = ({
|
||||
<>
|
||||
<WorkflowRunVisualizerEffect workflowRun={workflowRun} />
|
||||
|
||||
<WorkflowDiagramCanvasReadonly versionStatus={workflowVersion.status} />
|
||||
<WorkflowRunDiagramCanvas versionStatus={workflowVersion.status} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
|
||||
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
|
||||
import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge';
|
||||
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 { ReactFlowProvider } from '@xyflow/react';
|
||||
|
||||
export const WorkflowRunDiagramCanvas = ({
|
||||
versionStatus,
|
||||
}: {
|
||||
versionStatus: WorkflowVersionStatus;
|
||||
}) => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<WorkflowDiagramCanvasBase
|
||||
status={versionStatus}
|
||||
nodeTypes={{
|
||||
default: WorkflowDiagramStepNodeReadonly,
|
||||
}}
|
||||
edgeTypes={{
|
||||
default: WorkflowDiagramDefaultEdge,
|
||||
success: WorkflowDiagramSuccessEdge,
|
||||
}}
|
||||
/>
|
||||
|
||||
<WorkflowRunDiagramCanvasEffect />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,61 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||
import {
|
||||
WorkflowDiagramNode,
|
||||
WorkflowDiagramStepNodeData,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
|
||||
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
||||
import { useCallback } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { useIcons } from 'twenty-ui';
|
||||
|
||||
export const WorkflowRunDiagramCanvasEffect = () => {
|
||||
const { getIcon } = useIcons();
|
||||
const { openRightDrawer, closeRightDrawer } = useRightDrawer();
|
||||
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
|
||||
const handleSelectionChange = useCallback(
|
||||
({ nodes }: OnSelectionChangeParams) => {
|
||||
const selectedNode = nodes[0] as WorkflowDiagramNode;
|
||||
const isClosingStep = isDefined(selectedNode) === false;
|
||||
|
||||
if (isClosingStep) {
|
||||
closeRightDrawer();
|
||||
closeCommandMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
setWorkflowSelectedNode(selectedNode.id);
|
||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||
|
||||
const selectedNodeData = selectedNode.data as WorkflowDiagramStepNodeData;
|
||||
|
||||
openRightDrawer(RightDrawerPages.WorkflowRunStepView, {
|
||||
title: selectedNodeData.name,
|
||||
Icon: getIcon(getWorkflowNodeIconKey(selectedNodeData)),
|
||||
});
|
||||
},
|
||||
[
|
||||
setWorkflowSelectedNode,
|
||||
setHotkeyScope,
|
||||
openRightDrawer,
|
||||
closeRightDrawer,
|
||||
closeCommandMenu,
|
||||
getIcon,
|
||||
],
|
||||
);
|
||||
|
||||
useOnSelectionChange({
|
||||
onChange: handleSelectionChange,
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const useWorkflowSelectedNodeOrThrow = () => {
|
||||
const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
|
||||
|
||||
if (!isDefined(workflowSelectedNode)) {
|
||||
throw new Error(
|
||||
'Expected a node to be selected. A node must have been selected before running this code.',
|
||||
);
|
||||
}
|
||||
|
||||
return workflowSelectedNode;
|
||||
};
|
||||
@ -1,11 +1,9 @@
|
||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
|
||||
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
|
||||
import { useUpdateStep } from '@/workflow/workflow-steps/hooks/useUpdateStep';
|
||||
import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const RightDrawerWorkflowEditStepContent = ({
|
||||
workflow,
|
||||
@ -13,13 +11,7 @@ export const RightDrawerWorkflowEditStepContent = ({
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const flow = useFlowOrThrow();
|
||||
|
||||
const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
|
||||
if (!isDefined(workflowSelectedNode)) {
|
||||
throw new Error(
|
||||
'Expected a node to be selected. Selecting a node is mandatory to edit it.',
|
||||
);
|
||||
}
|
||||
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
|
||||
|
||||
const { updateTrigger } = useUpdateWorkflowVersionTrigger({ workflow });
|
||||
const { updateStep } = useUpdateStep({
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
import { ShowPageSubContainerTabListContainer } from '@/ui/layout/show-page/components/ShowPageSubContainerTabListContainer';
|
||||
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
|
||||
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
|
||||
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui';
|
||||
|
||||
const StyledTabListContainer = styled(ShowPageSubContainerTabListContainer)`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
`;
|
||||
|
||||
type TabId = 'node' | 'input' | 'output';
|
||||
|
||||
export const RightDrawerWorkflowRunViewStep = () => {
|
||||
const flow = useFlowOrThrow();
|
||||
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
|
||||
|
||||
const { activeTabId } = useTabList<TabId>(
|
||||
WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID,
|
||||
);
|
||||
|
||||
const tabs: SingleTabProps<TabId>[] = [
|
||||
{ id: 'node', title: 'Node', Icon: IconStepInto },
|
||||
{ id: 'input', title: 'Input', Icon: IconLogin2 },
|
||||
{ id: 'output', title: 'Output', Icon: IconLogout },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTabListContainer>
|
||||
<TabList
|
||||
tabListInstanceId={WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID}
|
||||
tabs={tabs}
|
||||
behaveAsLinks={false}
|
||||
/>
|
||||
</StyledTabListContainer>
|
||||
|
||||
{activeTabId === 'node' ? (
|
||||
<WorkflowStepDetail
|
||||
readonly
|
||||
stepId={workflowSelectedNode}
|
||||
trigger={flow.trigger}
|
||||
steps={flow.steps}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,18 +1,10 @@
|
||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
|
||||
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const RightDrawerWorkflowViewStep = () => {
|
||||
const flow = useFlowOrThrow();
|
||||
|
||||
const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
|
||||
if (!isDefined(workflowSelectedNode)) {
|
||||
throw new Error(
|
||||
'Expected a node to be selected. Selecting a node is mandatory to view its details.',
|
||||
);
|
||||
}
|
||||
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
|
||||
|
||||
return (
|
||||
<WorkflowStepDetail
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledWorkflowStepBody = styled.div`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
row-gap: ${({ theme }) => theme.spacing(6)};
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export { StyledWorkflowStepBody as WorkflowStepBody };
|
||||
|
||||
@ -21,6 +21,14 @@ const WorkflowEditActionFormServerlessFunction = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const WorkflowReadonlyActionFormServerlessFunction = lazy(() =>
|
||||
import(
|
||||
'@/workflow/workflow-steps/workflow-actions/components/WorkflowReadonlyActionFormServerlessFunction'
|
||||
).then((module) => ({
|
||||
default: module.WorkflowReadonlyActionFormServerlessFunction,
|
||||
})),
|
||||
);
|
||||
|
||||
type WorkflowStepDetailProps = {
|
||||
stepId: string;
|
||||
trigger: WorkflowTrigger | null;
|
||||
@ -50,6 +58,7 @@ export const WorkflowStepDetail = ({
|
||||
trigger,
|
||||
steps,
|
||||
});
|
||||
|
||||
if (!isDefined(stepDefinition) || !isDefined(stepDefinition.definition)) {
|
||||
return null;
|
||||
}
|
||||
@ -60,6 +69,7 @@ export const WorkflowStepDetail = ({
|
||||
case 'DATABASE_EVENT': {
|
||||
return (
|
||||
<WorkflowEditTriggerDatabaseEventForm
|
||||
key={stepId}
|
||||
trigger={stepDefinition.definition}
|
||||
triggerOptions={props}
|
||||
/>
|
||||
@ -68,6 +78,7 @@ export const WorkflowStepDetail = ({
|
||||
case 'MANUAL': {
|
||||
return (
|
||||
<WorkflowEditTriggerManualForm
|
||||
key={stepId}
|
||||
trigger={stepDefinition.definition}
|
||||
triggerOptions={props}
|
||||
/>
|
||||
@ -76,6 +87,7 @@ export const WorkflowStepDetail = ({
|
||||
case 'CRON': {
|
||||
return (
|
||||
<WorkflowEditTriggerCronForm
|
||||
key={stepId}
|
||||
trigger={stepDefinition.definition}
|
||||
triggerOptions={props}
|
||||
/>
|
||||
@ -93,11 +105,18 @@ export const WorkflowStepDetail = ({
|
||||
case 'CODE': {
|
||||
return (
|
||||
<Suspense fallback={<RightDrawerSkeletonLoader />}>
|
||||
<WorkflowEditActionFormServerlessFunction
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
{props.readonly ? (
|
||||
<WorkflowReadonlyActionFormServerlessFunction
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
/>
|
||||
) : (
|
||||
<WorkflowEditActionFormServerlessFunction
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@ -150,8 +169,6 @@ export const WorkflowStepDetail = ({
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -43,24 +43,38 @@ const StyledHeaderIconContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type WorkflowStepHeaderProps = {
|
||||
Icon: IconComponent;
|
||||
iconColor: string;
|
||||
initialTitle: string;
|
||||
headerType: string;
|
||||
} & (
|
||||
| {
|
||||
disabled: true;
|
||||
onTitleChange?: never;
|
||||
}
|
||||
| {
|
||||
disabled?: boolean;
|
||||
onTitleChange: (newTitle: string) => void;
|
||||
}
|
||||
);
|
||||
|
||||
export const WorkflowStepHeader = ({
|
||||
onTitleChange,
|
||||
Icon,
|
||||
iconColor,
|
||||
initialTitle,
|
||||
headerType,
|
||||
disabled,
|
||||
}: {
|
||||
onTitleChange: (newTitle: string) => void;
|
||||
Icon: IconComponent;
|
||||
iconColor: string;
|
||||
initialTitle: string;
|
||||
headerType: string;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
onTitleChange,
|
||||
}: WorkflowStepHeaderProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [title, setTitle] = useState(initialTitle);
|
||||
const debouncedOnTitleChange = useDebouncedCallback(onTitleChange, 100);
|
||||
|
||||
const debouncedOnTitleChange = useDebouncedCallback((newTitle: string) => {
|
||||
onTitleChange?.(newTitle);
|
||||
}, 100);
|
||||
|
||||
const handleChange = (newTitle: string) => {
|
||||
setTitle(newTitle);
|
||||
debouncedOnTitleChange(newTitle);
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
export const WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID =
|
||||
'workflow-run-step-side-panel-tab-list';
|
||||
@ -17,6 +17,7 @@ import { getFunctionOutputSchema } from '@/serverless-functions/utils/getFunctio
|
||||
import { mergeDefaultFunctionInputAndFunctionInput } from '@/serverless-functions/utils/mergeDefaultFunctionInputAndFunctionInput';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
||||
import { ShowPageSubContainerTabListContainer } from '@/ui/layout/show-page/components/ShowPageSubContainerTabListContainer';
|
||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
||||
@ -48,14 +49,7 @@ const StyledCodeEditorContainer = styled.div`
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledTabListContainer = styled.div`
|
||||
align-items: center;
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: ${({ theme }) => theme.spacing(10)};
|
||||
const StyledTabListContainer = styled(ShowPageSubContainerTabListContainer)`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
`;
|
||||
|
||||
|
||||
@ -5,57 +5,50 @@ import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { FunctionInput } from '@/workflow/workflow-steps/workflow-actions/types/FunctionInput';
|
||||
import styled from '@emotion/styled';
|
||||
import { isObject } from '@sniptt/guards';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
type WorkflowEditActionFormServerlessFunctionFieldsProps = {
|
||||
functionInput: FunctionInput;
|
||||
path?: string[];
|
||||
readonly?: boolean;
|
||||
onInputChange?: (value: any, path: string[]) => void;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
};
|
||||
|
||||
export const WorkflowEditActionFormServerlessFunctionFields = ({
|
||||
functionInput,
|
||||
path = [],
|
||||
VariablePicker,
|
||||
readonly,
|
||||
onInputChange,
|
||||
readonly = false,
|
||||
}: {
|
||||
functionInput: FunctionInput;
|
||||
path?: string[];
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
onInputChange: (value: any, path: string[]) => void;
|
||||
readonly?: boolean;
|
||||
}) => {
|
||||
const renderFields = ({
|
||||
functionInput,
|
||||
path = [],
|
||||
VariablePicker,
|
||||
onInputChange,
|
||||
readonly = false,
|
||||
}: {
|
||||
functionInput: FunctionInput;
|
||||
path?: string[];
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
onInputChange: (value: any, path: string[]) => void;
|
||||
readonly?: boolean;
|
||||
}): ReactNode[] => {
|
||||
return Object.entries(functionInput).map(([inputKey, inputValue]) => {
|
||||
const currentPath = [...path, inputKey];
|
||||
const pathKey = currentPath.join('.');
|
||||
if (inputValue !== null && isObject(inputValue)) {
|
||||
return (
|
||||
<StyledContainer key={pathKey}>
|
||||
<InputLabel>{inputKey}</InputLabel>
|
||||
<FormNestedFieldInputContainer>
|
||||
{renderFields({
|
||||
functionInput: inputValue,
|
||||
path: currentPath,
|
||||
VariablePicker,
|
||||
onInputChange,
|
||||
})}
|
||||
</FormNestedFieldInputContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
} else {
|
||||
VariablePicker,
|
||||
}: WorkflowEditActionFormServerlessFunctionFieldsProps) => {
|
||||
return (
|
||||
<>
|
||||
{Object.entries(functionInput).map(([inputKey, inputValue]) => {
|
||||
const currentPath = [...path, inputKey];
|
||||
const pathKey = currentPath.join('.');
|
||||
|
||||
if (inputValue !== null && isObject(inputValue)) {
|
||||
return (
|
||||
<StyledContainer key={pathKey}>
|
||||
<InputLabel>{inputKey}</InputLabel>
|
||||
<FormNestedFieldInputContainer>
|
||||
<WorkflowEditActionFormServerlessFunctionFields
|
||||
functionInput={inputValue}
|
||||
path={currentPath}
|
||||
readonly={readonly}
|
||||
onInputChange={onInputChange}
|
||||
VariablePicker={VariablePicker}
|
||||
/>
|
||||
</FormNestedFieldInputContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormTextFieldInput
|
||||
key={pathKey}
|
||||
@ -63,22 +56,10 @@ export const WorkflowEditActionFormServerlessFunctionFields = ({
|
||||
placeholder="Enter value"
|
||||
defaultValue={inputValue ? `${inputValue}` : ''}
|
||||
readonly={readonly}
|
||||
onPersist={(value) => onInputChange(value, currentPath)}
|
||||
onPersist={(value) => onInputChange?.(value, currentPath)}
|
||||
VariablePicker={VariablePicker}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderFields({
|
||||
functionInput,
|
||||
path,
|
||||
VariablePicker,
|
||||
onInputChange,
|
||||
readonly,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -0,0 +1,104 @@
|
||||
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
|
||||
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { WorkflowCodeAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
|
||||
import { INDEX_FILE_PATH } from '@/serverless-functions/constants/IndexFilePath';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowEditActionFormServerlessFunctionFields } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunctionFields';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Monaco } from '@monaco-editor/react';
|
||||
import { editor } from 'monaco-editor';
|
||||
import { AutoTypings } from 'monaco-editor-auto-typings';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { CodeEditor, useIcons } from 'twenty-ui';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledCodeEditorContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
type WorkflowReadonlyActionFormServerlessFunctionProps = {
|
||||
action: WorkflowCodeAction;
|
||||
};
|
||||
|
||||
export const WorkflowReadonlyActionFormServerlessFunction = ({
|
||||
action,
|
||||
}: WorkflowReadonlyActionFormServerlessFunctionProps) => {
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
const serverlessFunctionId = action.settings.input.serverlessFunctionId;
|
||||
const serverlessFunctionVersion =
|
||||
action.settings.input.serverlessFunctionVersion;
|
||||
|
||||
const { availablePackages } = useGetAvailablePackages({
|
||||
id: serverlessFunctionId,
|
||||
});
|
||||
|
||||
const { formValues, loading } = useServerlessFunctionUpdateFormState({
|
||||
serverlessFunctionId,
|
||||
serverlessFunctionVersion,
|
||||
});
|
||||
|
||||
const handleEditorDidMount = async (
|
||||
editor: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
) => {
|
||||
await AutoTypings.create(editor, {
|
||||
monaco,
|
||||
preloadPackages: true,
|
||||
onlySpecifiedPackages: true,
|
||||
versions: availablePackages,
|
||||
debounceDuration: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const headerTitle = isDefined(action.name)
|
||||
? action.name
|
||||
: 'Code - Serverless Function';
|
||||
const headerIcon = getActionIcon(action.type);
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<WorkflowStepHeader
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={theme.color.orange}
|
||||
initialTitle={headerTitle}
|
||||
headerType="Code"
|
||||
disabled
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<WorkflowEditActionFormServerlessFunctionFields
|
||||
functionInput={action.settings.input.serverlessFunctionInput}
|
||||
readonly
|
||||
/>
|
||||
<StyledCodeEditorContainer>
|
||||
<CodeEditor
|
||||
height={343}
|
||||
value={formValues.code?.[INDEX_FILE_PATH]}
|
||||
language={'typescript'}
|
||||
onMount={handleEditorDidMount}
|
||||
setMarkers={getWrongExportedFunctionMarkers}
|
||||
options={{
|
||||
readOnly: true,
|
||||
domReadOnly: true,
|
||||
}}
|
||||
/>
|
||||
</StyledCodeEditorContainer>
|
||||
</WorkflowStepBody>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -3,7 +3,7 @@ import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithC
|
||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import {
|
||||
@ -24,10 +24,10 @@ export const useAvailableVariablesInWorkflowStep = ({
|
||||
}): StepOutputSchema[] => {
|
||||
const workflowId = useRecoilValue(workflowIdState);
|
||||
const workflow = useWorkflowWithCurrentVersion(workflowId);
|
||||
const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState);
|
||||
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
|
||||
const flow = useFlowOrThrow();
|
||||
|
||||
if (!isDefined(workflowSelectedNode) || !isDefined(workflow)) {
|
||||
if (!isDefined(workflow)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@ -52,6 +52,9 @@ export {
|
||||
IconClockPlay,
|
||||
IconClockShare,
|
||||
IconCode,
|
||||
IconStepInto,
|
||||
IconLogin2,
|
||||
IconLogout,
|
||||
IconCodeCircle,
|
||||
IconCoins,
|
||||
IconColorSwatch,
|
||||
|
||||
Reference in New Issue
Block a user