From 1115f6fc577995cf7bc6a4f4d98fcd74d81e17e4 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Wed, 28 May 2025 18:17:22 +0200 Subject: [PATCH] Automatically open pending form nodes in the side panel (#12332) - Keep the side panel open when submitting a pending form node if the history isn't empty - Prevent the same pending Form step from being automatically opened several times - Prevent duplicated views of the same pending form in the side panel > [!WARNING] Switching from the side panel view to the full-screen view isn't robust, and I used a hack to make one use case work here. We'll have to improve that part later. ## Before https://github.com/user-attachments/assets/0f830e87-7681-49e9-acc8-a08178f631a2 ## After https://github.com/user-attachments/assets/bfd742d6-e38e-4981-a93b-8e895d3aa38a --- ...kflowRunOpeningInCommandMenuSideEffects.ts | 9 ++ .../WorkflowRunVisualizerEffect.tsx | 94 ++++++++++++++++--- .../useHandleWorkflowRunDiagramCanvasInit.ts | 40 ++++++-- ...mAutomaticallyOpenedStepsComponentState.ts | 9 ++ .../WorkflowEditActionFormFiller.tsx | 6 +- 5 files changed, 132 insertions(+), 26 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState.ts diff --git a/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts b/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts index e34106778..d3d96ed4c 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useRunWorkflowRunOpeningInCommandMenuSideEffects.ts @@ -8,6 +8,7 @@ import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/wo import { workflowVisualizerWorkflowRunIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowRunIdComponentState'; import { WorkflowRun } from '@/workflow/types/Workflow'; import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId'; +import { workflowRunDiagramAutomaticallyOpenedStepsComponentState } from '@/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState'; import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram'; import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; @@ -96,6 +97,14 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => { stepToOpenByDefault.id, ); + set( + workflowRunDiagramAutomaticallyOpenedStepsComponentState.atomFamily({ + instanceId: getWorkflowVisualizerComponentInstanceId({ + recordId, + }), + }), + (steps) => [...steps, stepToOpenByDefault.id], + ); openWorkflowRunViewStepInCommandMenu({ workflowId: workflowRunRecord.workflowId, workflowRunId: workflowRunRecord.id, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx index a404d8bae..a5de43a1f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect.tsx @@ -1,4 +1,5 @@ import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; @@ -11,24 +12,33 @@ import { workflowVisualizerWorkflowRunIdComponentState } from '@/workflow/states import { WorkflowRunOutput } from '@/workflow/types/Workflow'; import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; import { workflowDiagramStatusComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusComponentState'; +import { workflowRunDiagramAutomaticallyOpenedStepsComponentState } from '@/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState'; import { workflowRunStepToOpenByDefaultComponentState } from '@/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultComponentState'; +import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram'; +import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; import { selectWorkflowDiagramNode } from '@/workflow/workflow-diagram/utils/selectWorkflowDiagramNode'; import { useContext, useEffect } from 'react'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; export const WorkflowRunVisualizerEffect = ({ workflowRunId, }: { workflowRunId: string; }) => { + const { getIcon } = useIcons(); + const workflowRun = useWorkflowRun({ workflowRunId }); const workflowVersion = useWorkflowVersion(workflowRun?.workflowVersionId); const setWorkflowRunId = useSetRecoilComponentStateV2( workflowVisualizerWorkflowRunIdComponentState, ); + const workflowVisualizerWorkflowIdState = useRecoilComponentCallbackStateV2( + workflowVisualizerWorkflowIdComponentState, + ); const setWorkflowVisualizerWorkflowId = useSetRecoilComponentStateV2( workflowVisualizerWorkflowIdComponentState, ); @@ -43,6 +53,15 @@ export const WorkflowRunVisualizerEffect = ({ const workflowRunStepToOpenByDefaultState = useRecoilComponentCallbackStateV2( workflowRunStepToOpenByDefaultComponentState, ); + const workflowSelectedNodeState = useRecoilComponentCallbackStateV2( + workflowSelectedNodeComponentState, + ); + const workflowRunDiagramAutomaticallyOpenedStepsState = + useRecoilComponentCallbackStateV2( + workflowRunDiagramAutomaticallyOpenedStepsComponentState, + ); + + const { openWorkflowRunViewStepInCommandMenu } = useWorkflowCommandMenu(); const { populateStepsOutputSchema } = useStepsOutputSchema(); @@ -65,11 +84,11 @@ export const WorkflowRunVisualizerEffect = ({ ({ workflowRunOutput, workflowVersionId, - skipNodeSelection, + isInRightDrawer, }: { workflowRunOutput: WorkflowRunOutput | undefined; workflowVersionId: string | undefined; - skipNodeSelection: boolean; + isInRightDrawer: boolean; }) => { if (!(isDefined(workflowRunOutput) && isDefined(workflowVersionId))) { set(flowState, undefined); @@ -100,17 +119,60 @@ export const WorkflowRunVisualizerEffect = ({ stepsOutput: workflowRunOutput.stepsOutput, }); - if (isDefined(stepToOpenByDefault) && !skipNodeSelection) { - const workflowRunDiagram = selectWorkflowDiagramNode({ - diagram: baseWorkflowRunDiagram, - nodeIdToSelect: stepToOpenByDefault.id, - }); + if (isDefined(stepToOpenByDefault)) { + if (isInRightDrawer) { + set(workflowDiagramState, baseWorkflowRunDiagram); - set(workflowDiagramState, workflowRunDiagram); - set(workflowRunStepToOpenByDefaultState, { - id: stepToOpenByDefault.id, - data: stepToOpenByDefault.data, - }); + const workflowRunDiagramAutomaticallyOpenedSteps = getSnapshotValue( + snapshot, + workflowRunDiagramAutomaticallyOpenedStepsState, + ); + const hasStepAlreadyBeenOpenedAutomatically = + workflowRunDiagramAutomaticallyOpenedSteps.includes( + stepToOpenByDefault.id, + ); + + if ( + workflowDiagramStatus === 'done' && + !hasStepAlreadyBeenOpenedAutomatically + ) { + set(workflowSelectedNodeState, stepToOpenByDefault.id); + + const workflowVisualizerWorkflowId = getSnapshotValue( + snapshot, + workflowVisualizerWorkflowIdState, + ); + if (!isDefined(workflowVisualizerWorkflowId)) { + throw new Error( + 'The workflow id must be set; ensure the workflow id is always set before rendering the workflow diagram.', + ); + } + + set(workflowRunDiagramAutomaticallyOpenedStepsState, [ + ...workflowRunDiagramAutomaticallyOpenedSteps, + stepToOpenByDefault.id, + ]); + openWorkflowRunViewStepInCommandMenu({ + workflowId: workflowVisualizerWorkflowId, + workflowRunId, + title: stepToOpenByDefault.data.name, + icon: getIcon(getWorkflowNodeIconKey(stepToOpenByDefault.data)), + workflowSelectedNode: stepToOpenByDefault.id, + stepExecutionStatus: stepToOpenByDefault.data.runStatus, + }); + } + } else { + const workflowRunDiagram = selectWorkflowDiagramNode({ + diagram: baseWorkflowRunDiagram, + nodeIdToSelect: stepToOpenByDefault.id, + }); + + set(workflowDiagramState, workflowRunDiagram); + set(workflowRunStepToOpenByDefaultState, { + id: stepToOpenByDefault.id, + data: stepToOpenByDefault.data, + }); + } } else { set(workflowDiagramState, baseWorkflowRunDiagram); } @@ -121,9 +183,15 @@ export const WorkflowRunVisualizerEffect = ({ }, [ flowState, + getIcon, + openWorkflowRunViewStepInCommandMenu, workflowDiagramState, workflowDiagramStatusState, + workflowRunDiagramAutomaticallyOpenedStepsState, + workflowRunId, workflowRunStepToOpenByDefaultState, + workflowSelectedNodeState, + workflowVisualizerWorkflowIdState, ], ); @@ -131,7 +199,7 @@ export const WorkflowRunVisualizerEffect = ({ handleWorkflowRunDiagramGeneration({ workflowRunOutput: workflowRun?.output ?? undefined, workflowVersionId: workflowRun?.workflowVersionId, - skipNodeSelection: isInRightDrawer, + isInRightDrawer, }); }, [ handleWorkflowRunDiagramGeneration, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit.ts index 4b52dd43c..a2a07e58d 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useHandleWorkflowRunDiagramCanvasInit.ts @@ -5,6 +5,7 @@ import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/componen import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; import { workflowDiagramStatusComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramStatusComponentState'; +import { workflowRunDiagramAutomaticallyOpenedStepsComponentState } from '@/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState'; import { workflowRunStepToOpenByDefaultComponentState } from '@/workflow/workflow-diagram/states/workflowRunStepToOpenByDefaultComponentState'; import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey'; @@ -33,6 +34,10 @@ export const useHandleWorkflowRunDiagramCanvasInit = () => { const workflowSelectedNodeState = useRecoilComponentCallbackStateV2( workflowSelectedNodeComponentState, ); + const workflowRunDiagramAutomaticallyOpenedStepsState = + useRecoilComponentCallbackStateV2( + workflowRunDiagramAutomaticallyOpenedStepsComponentState, + ); const handleWorkflowRunDiagramCanvasInit = useRecoilCallback( ({ snapshot, set }) => @@ -72,16 +77,30 @@ export const useHandleWorkflowRunDiagramCanvasInit = () => { set(workflowSelectedNodeState, workflowStepToOpenByDefault.id); - openWorkflowRunViewStepInCommandMenu({ - workflowId: workflowVisualizerWorkflowId, - workflowRunId, - title: workflowStepToOpenByDefault.data.name, - icon: getIcon( - getWorkflowNodeIconKey(workflowStepToOpenByDefault.data), - ), - workflowSelectedNode: workflowStepToOpenByDefault.id, - stepExecutionStatus: workflowStepToOpenByDefault.data.runStatus, - }); + const workflowRunDiagramAutomaticallyOpenedSteps = getSnapshotValue( + snapshot, + workflowRunDiagramAutomaticallyOpenedStepsState, + ); + const hasStepAlreadyBeenOpenedAutomatically = + workflowRunDiagramAutomaticallyOpenedSteps.includes( + workflowStepToOpenByDefault.id, + ); + + // FIXME: This is a workaround to avoid opening a workflow run step twice when going from the side panel to the fullscreen show page. + // The step is opened in the `handleSelectionChange` function of `WorkflowRunDiagramCanvasEffect`. I think it shouldn't be opened there but + // we should keep opening the step here, in `handleWorkflowRunDiagramCanvasInit`. + if (!hasStepAlreadyBeenOpenedAutomatically) { + openWorkflowRunViewStepInCommandMenu({ + workflowId: workflowVisualizerWorkflowId, + workflowRunId, + title: workflowStepToOpenByDefault.data.name, + icon: getIcon( + getWorkflowNodeIconKey(workflowStepToOpenByDefault.data), + ), + workflowSelectedNode: workflowStepToOpenByDefault.id, + stepExecutionStatus: workflowStepToOpenByDefault.data.runStatus, + }); + } set(workflowRunStepToOpenByDefaultState, undefined); } @@ -92,6 +111,7 @@ export const useHandleWorkflowRunDiagramCanvasInit = () => { workflowRunStepToOpenByDefaultState, workflowVisualizerWorkflowIdState, workflowSelectedNodeState, + workflowRunDiagramAutomaticallyOpenedStepsState, openWorkflowRunViewStepInCommandMenu, workflowRunId, getIcon, diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState.ts new file mode 100644 index 000000000..41685e046 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/states/workflowRunDiagramAutomaticallyOpenedStepsComponentState.ts @@ -0,0 +1,9 @@ +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; +import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; + +export const workflowRunDiagramAutomaticallyOpenedStepsComponentState = + createComponentStateV2({ + key: 'workflowRunDiagramAutomaticallyOpenedStepsComponentState', + defaultValue: [], + componentInstanceContext: WorkflowVisualizerComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller.tsx index f00630467..57e3b2750 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller.tsx @@ -1,5 +1,5 @@ import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory'; import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput'; import { FormSingleRecordPicker } from '@/object-record/record-field/form-types/components/FormSingleRecordPicker'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; @@ -37,7 +37,7 @@ export const WorkflowEditActionFormFiller = ({ const { submitFormStep } = useSubmitFormStep(); const [formData, setFormData] = useState(action.settings.input); const { workflowRunId } = useWorkflowStepContextOrThrow(); - const { closeCommandMenu } = useCommandMenu(); + const { goBackFromCommandMenu } = useCommandMenuHistory(); const { updateWorkflowRunStep } = useUpdateWorkflowRunStep(); const [error, setError] = useState(undefined); const canSubmit = !actionOptions.readonly && !isDefined(error); @@ -98,7 +98,7 @@ export const WorkflowEditActionFormFiller = ({ response, }); - closeCommandMenu(); + goBackFromCommandMenu(); }; useEffect(() => {