From f74e4bedc4b67d682e46e4ed9636f61ad702abf5 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Wed, 26 Feb 2025 16:48:24 +0100 Subject: [PATCH] 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 --- .../constants/CommandMenuPagesConfig.tsx | 2 + .../command-menu/types/CommandMenuPages.ts | 1 + .../components/RightDrawerRouter.tsx | 7 +- .../constants/RightDrawerPageIcons.ts | 3 +- .../constants/RightDrawerPageTitles.ts | 3 +- .../types/ComponentByRightDrawerPage.ts | 5 - .../right-drawer/types/RightDrawerPages.ts | 1 + .../mapRightDrawerPageToCommandMenuPage.ts | 41 +++---- .../components/ShowPageSubContainer.tsx | 9 +- .../ShowPageSubContainerTabListContainer.tsx | 13 +++ .../ui/layout/tab/components/TabList.tsx | 4 +- .../modules/ui/layout/tab/hooks/useTabList.ts | 8 +- .../WorkflowRunVisualizerContent.tsx | 4 +- .../components/WorkflowRunDiagramCanvas.tsx | 30 +++++ .../WorkflowRunDiagramCanvasEffect.tsx | 61 ++++++++++ .../hooks/useWorkflowSelectedNodeOrThrow.ts | 15 +++ .../RightDrawerWorkflowEditStepContent.tsx | 12 +- .../RightDrawerWorkflowRunViewStep.tsx | 51 +++++++++ .../RightDrawerWorkflowViewStep.tsx | 12 +- .../components/WorkflowStepBody.tsx | 5 +- .../components/WorkflowStepDetail.tsx | 31 ++++-- .../components/WorkflowStepHeader.tsx | 34 ++++-- ...kflowRunStepSidePanelTabListComponentId.ts | 2 + ...rkflowEditActionFormServerlessFunction.tsx | 10 +- ...EditActionFormServerlessFunctionFields.tsx | 89 ++++++--------- ...owReadonlyActionFormServerlessFunction.tsx | 104 ++++++++++++++++++ .../useAvailableVariablesInWorkflowStep.ts | 6 +- .../display/icon/components/TablerIcons.ts | 3 + 28 files changed, 418 insertions(+), 148 deletions(-) delete mode 100644 packages/twenty-front/src/modules/ui/layout/right-drawer/types/ComponentByRightDrawerPage.ts create mode 100644 packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainerTabListContainer.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowRunViewStep.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowReadonlyActionFormServerlessFunction.tsx diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx index 16832c625..8426c0a33 100644 --- a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx +++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx @@ -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, ], [CommandMenuPages.WorkflowStepView, ], + [CommandMenuPages.WorkflowRunStepView, ], [CommandMenuPages.SearchRecords, ], ]); diff --git a/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts b/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts index 4911abc81..57bf842fd 100644 --- a/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts +++ b/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts @@ -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', } diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx index a2d5dbcc1..957c01de0 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx @@ -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]: , [RightDrawerPages.ViewCalendarEvent]: , [RightDrawerPages.ViewRecord]: , @@ -41,7 +41,8 @@ const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = { ), [RightDrawerPages.WorkflowStepEdit]: , [RightDrawerPages.WorkflowStepView]: , -}; + [RightDrawerPages.WorkflowRunStepView]: , +} satisfies Record; export const RightDrawerRouter = () => { const [rightDrawerPage] = useRecoilState(rightDrawerPageState); diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts index 64f594a5f..51fcc5264 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts @@ -9,4 +9,5 @@ export const RIGHT_DRAWER_PAGE_ICONS = { [RightDrawerPages.WorkflowStepSelectAction]: 'IconSparkles', [RightDrawerPages.WorkflowStepEdit]: 'IconSparkles', [RightDrawerPages.WorkflowStepView]: 'IconSparkles', -}; + [RightDrawerPages.WorkflowRunStepView]: 'IconSparkles', +} satisfies Record; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts index 55e2f8899..fb918821b 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts @@ -9,4 +9,5 @@ export const RIGHT_DRAWER_PAGE_TITLES = { [RightDrawerPages.WorkflowStepSelectAction]: 'Workflow', [RightDrawerPages.WorkflowStepEdit]: 'Workflow', [RightDrawerPages.WorkflowStepView]: 'Workflow', -}; + [RightDrawerPages.WorkflowRunStepView]: 'Workflow', +} satisfies Record; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/ComponentByRightDrawerPage.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/ComponentByRightDrawerPage.ts deleted file mode 100644 index bcef77e8c..000000000 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/ComponentByRightDrawerPage.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; - -export type ComponentByRightDrawerPage = { - [componentName in RightDrawerPages]?: JSX.Element; -}; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts index 1ca51cb74..764b410a8 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts @@ -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', } diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage.ts index 6a42ddc38..8a22d1e25 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage.ts @@ -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 + ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx index eaeb6986e..6efae7665 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainerTabListContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainerTabListContainer.tsx new file mode 100644 index 000000000..6ba063dc9 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainerTabListContainer.tsx @@ -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 }; diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx index ec3c6d965..f1794d7ea 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx @@ -9,10 +9,10 @@ import { useEffect } from 'react'; import { IconComponent } from 'twenty-ui'; import { Tab } from './Tab'; -export type SingleTabProps = { +export type SingleTabProps = { title: string; Icon?: IconComponent; - id: string; + id: T; hide?: boolean; disabled?: boolean; pill?: string | React.ReactElement; diff --git a/packages/twenty-front/src/modules/ui/layout/tab/hooks/useTabList.ts b/packages/twenty-front/src/modules/ui/layout/tab/hooks/useTabList.ts index 06c0d3ad7..f71b0c0d8 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/hooks/useTabList.ts +++ b/packages/twenty-front/src/modules/ui/layout/tab/hooks/useTabList.ts @@ -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 = (tabListId?: string) => { const { activeTabIdState } = useTabListStates({ tabListScopeId: tabListId, }); - const [activeTabId, setActiveTabId] = useRecoilState(activeTabIdState); + const [activeTabId, setActiveTabId] = useRecoilState( + activeTabIdState as RecoilState, + ); return { activeTabId, diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowRunVisualizerContent.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowRunVisualizerContent.tsx index 49d4e58bf..dd0623d84 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowRunVisualizerContent.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowRunVisualizerContent.tsx @@ -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 = ({ <> - + ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx new file mode 100644 index 000000000..348c5c8d9 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas.tsx @@ -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 ( + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx new file mode 100644 index 000000000..2aa382275 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect.tsx @@ -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; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow.ts new file mode 100644 index 000000000..dcc840e55 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow.ts @@ -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; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowEditStepContent.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowEditStepContent.tsx index 4dfd8183b..a04dbeccb 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowEditStepContent.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowEditStepContent.tsx @@ -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({ diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowRunViewStep.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowRunViewStep.tsx new file mode 100644 index 000000000..c66a4d4cd --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowRunViewStep.tsx @@ -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( + WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID, + ); + + const tabs: SingleTabProps[] = [ + { id: 'node', title: 'Node', Icon: IconStepInto }, + { id: 'input', title: 'Input', Icon: IconLogin2 }, + { id: 'output', title: 'Output', Icon: IconLogout }, + ]; + + return ( + <> + + + + + {activeTabId === 'node' ? ( + + ) : null} + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowViewStep.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowViewStep.tsx index a029adf75..8b7d76b1e 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowViewStep.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/RightDrawerWorkflowViewStep.tsx @@ -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 ( 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 }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx index ed9b8af02..f56d759fd 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepDetail.tsx @@ -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 ( @@ -68,6 +78,7 @@ export const WorkflowStepDetail = ({ case 'MANUAL': { return ( @@ -76,6 +87,7 @@ export const WorkflowStepDetail = ({ case 'CRON': { return ( @@ -93,11 +105,18 @@ export const WorkflowStepDetail = ({ case 'CODE': { return ( }> - + {props.readonly ? ( + + ) : ( + + )} ); } @@ -150,8 +169,6 @@ export const WorkflowStepDetail = ({ ); } } - - return null; } } diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepHeader.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepHeader.tsx index ffb92117f..6e17ad94b 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepHeader.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepHeader.tsx @@ -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); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId.ts new file mode 100644 index 000000000..72a76a5bc --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId.ts @@ -0,0 +1,2 @@ +export const WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID = + 'workflow-run-step-side-panel-tab-list'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx index 2032b4cd9..8b757c76c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx @@ -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}; `; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunctionFields.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunctionFields.tsx index e09c32b1c..6069b4492 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunctionFields.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunctionFields.tsx @@ -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 ( - - {inputKey} - - {renderFields({ - functionInput: inputValue, - path: currentPath, - VariablePicker, - onInputChange, - })} - - - ); - } else { + VariablePicker, +}: WorkflowEditActionFormServerlessFunctionFieldsProps) => { + return ( + <> + {Object.entries(functionInput).map(([inputKey, inputValue]) => { + const currentPath = [...path, inputKey]; + const pathKey = currentPath.join('.'); + + if (inputValue !== null && isObject(inputValue)) { + return ( + + {inputKey} + + + + + ); + } + return ( onInputChange(value, currentPath)} + onPersist={(value) => onInputChange?.(value, currentPath)} VariablePicker={VariablePicker} /> ); - } - }); - }; - - return ( - <> - {renderFields({ - functionInput, - path, - VariablePicker, - onInputChange, - readonly, })} ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowReadonlyActionFormServerlessFunction.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowReadonlyActionFormServerlessFunction.tsx new file mode 100644 index 000000000..890a89606 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowReadonlyActionFormServerlessFunction.tsx @@ -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 ( + + + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts index 9a1de459e..be4909ace 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts @@ -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 []; } diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index ae5b0c30c..875338b0b 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -52,6 +52,9 @@ export { IconClockPlay, IconClockShare, IconCode, + IconStepInto, + IconLogin2, + IconLogout, IconCodeCircle, IconCoins, IconColorSwatch,