Update workflow run step (#11125)

Currently, when filling the form, values are not saved in the action
settings. This is an issue because we do not see the response in the
node settings, only in the output of the step.

This PR:
- adds a new endpoint to update a step in the run flow output
- updates this flow when a step is updated



https://github.com/user-attachments/assets/2e74a010-a0d2-4b87-bd1f-1c91f7ca6b60

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Thomas Trompette
2025-03-24 17:42:15 +01:00
committed by GitHub
co-authored by greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
parent 0656b640d7
commit 049a065307
12 changed files with 266 additions and 37 deletions
@@ -0,0 +1,13 @@
import { gql } from '@apollo/client';
export const UPDATE_WORKFLOW_RUN_STEP = gql`
mutation UpdateWorkflowRunStep($input: UpdateWorkflowRunStepInput!) {
updateWorkflowRunStep(input: $input) {
id
name
type
settings
valid
}
}
`;
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { FieldMetadataType } from 'twenty-shared/types';
import { z } from 'zod';
// Base schemas
export const objectRecordSchema = z.record(z.any());
@@ -95,6 +95,7 @@ export const workflowFormActionSettingsSchema =
]),
placeholder: z.string().optional(),
settings: z.record(z.any()).optional(),
value: z.any().optional(),
}),
),
});
@@ -171,8 +171,6 @@ export const WorkflowRunStepNodeDetail = ({
action={stepDefinition.definition}
actionOptions={{
readonly: stepExecutionStatus !== 'running',
// TODO: Implement update worklfow run flow step
onActionUpdate: () => {},
}}
/>
);
@@ -0,0 +1,86 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { UPDATE_WORKFLOW_RUN_STEP } from '@/workflow/graphql/mutations/updateWorkflowRunStep';
import { WorkflowRun } from '@/workflow/types/Workflow';
import { useApolloClient, useMutation } from '@apollo/client';
import { isDefined } from 'twenty-shared/utils';
import {
UpdateWorkflowRunStepInput,
UpdateWorkflowRunStepMutation,
UpdateWorkflowRunStepMutationVariables,
WorkflowAction,
} from '~/generated/graphql';
export const useUpdateWorkflowRunStep = () => {
const apolloClient = useApolloClient();
const { objectMetadataItems } = useObjectMetadataItems();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.WorkflowRun,
});
const [mutate] = useMutation<
UpdateWorkflowRunStepMutation,
UpdateWorkflowRunStepMutationVariables
>(UPDATE_WORKFLOW_RUN_STEP, {
client: apolloClient,
});
const getRecordFromCache = useGetRecordFromCache({
objectNameSingular: CoreObjectNameSingular.WorkflowRun,
});
const updateWorkflowRunStep = async (input: UpdateWorkflowRunStepInput) => {
const result = await mutate({
variables: {
input: { workflowRunId: input.workflowRunId, step: input.step },
},
});
const updatedStep = result?.data?.updateWorkflowRunStep;
if (!isDefined(updatedStep)) {
return;
}
const cachedRecord = getRecordFromCache<WorkflowRun>(input.workflowRunId);
if (
!isDefined(cachedRecord) ||
!isDefined(cachedRecord?.output?.flow?.steps)
) {
return;
}
const newCachedRecord = {
...cachedRecord,
output: {
...cachedRecord.output,
flow: {
...cachedRecord.output.flow,
steps: cachedRecord.output.flow.steps.map((step: WorkflowAction) => {
if (step.id === updatedStep.id) {
return updatedStep;
}
return step;
}),
},
},
};
const recordGqlFields = {
output: true,
};
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
record: newCachedRecord,
recordGqlFields,
});
return updatedStep;
};
return { updateWorkflowRunStep };
};
@@ -7,25 +7,21 @@ import { useWorkflowStepContextOrThrow } from '@/workflow/states/context/Workflo
import { WorkflowFormAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { useUpdateWorkflowRunStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowRunStep';
import { useSubmitFormStep } from '@/workflow/workflow-steps/workflow-actions/form-action/hooks/useSubmitFormStep';
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { useTheme } from '@emotion/react';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui';
import { useDebouncedCallback } from 'use-debounce';
import { isDefined } from 'twenty-shared/utils';
export type WorkflowEditActionFormFillerProps = {
action: WorkflowFormAction;
actionOptions:
| {
readonly: true;
}
| {
readonly?: false;
onActionUpdate: (action: WorkflowFormAction) => void;
};
actionOptions: {
readonly: boolean;
};
};
type FormData = WorkflowFormActionField[];
@@ -40,6 +36,7 @@ export const WorkflowEditActionFormFiller = ({
const [formData, setFormData] = useState<FormData>(action.settings.input);
const { workflowRunId } = useWorkflowStepContextOrThrow();
const { closeCommandMenu } = useCommandMenu();
const { updateWorkflowRunStep } = useUpdateWorkflowRunStep();
if (!isDefined(workflowRunId)) {
throw new Error('Form filler action must be used in a workflow run');
@@ -73,11 +70,11 @@ export const WorkflowEditActionFormFiller = ({
return;
}
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: updatedFormData,
await updateWorkflowRunStep({
workflowRunId,
step: {
...action,
settings: { ...action.settings, input: updatedFormData },
},
});
}, 1_000);
@@ -109,16 +106,6 @@ export const WorkflowEditActionFormFiller = ({
return (
<>
<WorkflowStepHeader
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={theme.font.color.tertiary}
initialTitle={headerTitle}
@@ -1,6 +1,7 @@
import { WorkflowFormAction } from '@/workflow/types/Workflow';
import { Meta, StoryObj } from '@storybook/react';
import { expect, fn, within } from '@storybook/test';
import { expect, within } from '@storybook/test';
import { FieldMetadataType } from 'twenty-shared/types';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
@@ -9,7 +10,6 @@ import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorato
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { WorkflowEditActionFormFiller } from '../WorkflowEditActionFormFiller';
import { FieldMetadataType } from 'twenty-shared/types';
const meta: Meta<typeof WorkflowEditActionFormFiller> = {
title: 'Modules/Workflow/Actions/Form/WorkflowEditActionFormFiller',
@@ -67,7 +67,7 @@ export const Default: Story = {
args: {
action: mockAction,
actionOptions: {
onActionUpdate: fn(),
readonly: false,
},
},
play: async ({ canvasElement }) => {