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
This commit is contained in:
Baptiste Devessier
2025-05-28 18:17:22 +02:00
committed by GitHub
parent b90cb3e1f9
commit 1115f6fc57
5 changed files with 132 additions and 26 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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<string[]>({
key: 'workflowRunDiagramAutomaticallyOpenedStepsComponentState',
defaultValue: [],
componentInstanceContext: WorkflowVisualizerComponentInstanceContext,
});

View File

@ -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<FormData>(action.settings.input);
const { workflowRunId } = useWorkflowStepContextOrThrow();
const { closeCommandMenu } = useCommandMenu();
const { goBackFromCommandMenu } = useCommandMenuHistory();
const { updateWorkflowRunStep } = useUpdateWorkflowRunStep();
const [error, setError] = useState<string | undefined>(undefined);
const canSubmit = !actionOptions.readonly && !isDefined(error);
@ -98,7 +98,7 @@ export const WorkflowEditActionFormFiller = ({
response,
});
closeCommandMenu();
goBackFromCommandMenu();
};
useEffect(() => {