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
parent 0656b640d7
commit 049a065307
12 changed files with 266 additions and 37 deletions

View File

@ -1,5 +1,5 @@
import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
@ -856,6 +856,7 @@ export type Mutation = {
updateOneRole: Role;
updateOneServerlessFunction: ServerlessFunction;
updatePasswordViaResetToken: InvalidatePassword;
updateWorkflowRunStep: WorkflowAction;
updateWorkflowVersionStep: WorkflowAction;
updateWorkspace: Workspace;
updateWorkspaceFeatureFlag: Scalars['Boolean'];
@ -1113,6 +1114,11 @@ export type MutationUpdatePasswordViaResetTokenArgs = {
};
export type MutationUpdateWorkflowRunStepArgs = {
input: UpdateWorkflowRunStepInput;
};
export type MutationUpdateWorkflowVersionStepArgs = {
input: UpdateWorkflowVersionStepInput;
};
@ -1954,6 +1960,13 @@ export type UpdateServerlessFunctionInput = {
timeoutSeconds?: InputMaybe<Scalars['Float']>;
};
export type UpdateWorkflowRunStepInput = {
/** Step to update in JSON format */
step: Scalars['JSON'];
/** Workflow run ID */
workflowRunId: Scalars['String'];
};
export type UpdateWorkflowVersionStepInput = {
/** Step to update in JSON format */
step: Scalars['JSON'];
@ -2662,6 +2675,13 @@ export type RunWorkflowVersionMutationVariables = Exact<{
export type RunWorkflowVersionMutation = { __typename?: 'Mutation', runWorkflowVersion: { __typename?: 'WorkflowRun', workflowRunId: any } };
export type UpdateWorkflowRunStepMutationVariables = Exact<{
input: UpdateWorkflowRunStepInput;
}>;
export type UpdateWorkflowRunStepMutation = { __typename?: 'Mutation', updateWorkflowRunStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean } };
export type UpdateWorkflowVersionStepMutationVariables = Exact<{
input: UpdateWorkflowVersionStepInput;
}>;
@ -5265,6 +5285,43 @@ export function useRunWorkflowVersionMutation(baseOptions?: Apollo.MutationHookO
export type RunWorkflowVersionMutationHookResult = ReturnType<typeof useRunWorkflowVersionMutation>;
export type RunWorkflowVersionMutationResult = Apollo.MutationResult<RunWorkflowVersionMutation>;
export type RunWorkflowVersionMutationOptions = Apollo.BaseMutationOptions<RunWorkflowVersionMutation, RunWorkflowVersionMutationVariables>;
export const UpdateWorkflowRunStepDocument = gql`
mutation UpdateWorkflowRunStep($input: UpdateWorkflowRunStepInput!) {
updateWorkflowRunStep(input: $input) {
id
name
type
settings
valid
}
}
`;
export type UpdateWorkflowRunStepMutationFn = Apollo.MutationFunction<UpdateWorkflowRunStepMutation, UpdateWorkflowRunStepMutationVariables>;
/**
* __useUpdateWorkflowRunStepMutation__
*
* To run a mutation, you first call `useUpdateWorkflowRunStepMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateWorkflowRunStepMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateWorkflowRunStepMutation, { data, loading, error }] = useUpdateWorkflowRunStepMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useUpdateWorkflowRunStepMutation(baseOptions?: Apollo.MutationHookOptions<UpdateWorkflowRunStepMutation, UpdateWorkflowRunStepMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateWorkflowRunStepMutation, UpdateWorkflowRunStepMutationVariables>(UpdateWorkflowRunStepDocument, options);
}
export type UpdateWorkflowRunStepMutationHookResult = ReturnType<typeof useUpdateWorkflowRunStepMutation>;
export type UpdateWorkflowRunStepMutationResult = Apollo.MutationResult<UpdateWorkflowRunStepMutation>;
export type UpdateWorkflowRunStepMutationOptions = Apollo.BaseMutationOptions<UpdateWorkflowRunStepMutation, UpdateWorkflowRunStepMutationVariables>;
export const UpdateWorkflowVersionStepDocument = gql`
mutation UpdateWorkflowVersionStep($input: UpdateWorkflowVersionStepInput!) {
updateWorkflowVersionStep(input: $input) {

View File

@ -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
}
}
`;

View File

@ -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(),
}),
),
});

View File

@ -171,8 +171,6 @@ export const WorkflowRunStepNodeDetail = ({
action={stepDefinition.definition}
actionOptions={{
readonly: stepExecutionStatus !== 'running',
// TODO: Implement update worklfow run flow step
onActionUpdate: () => {},
}}
/>
);

View File

@ -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 };
};

View File

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

View File

@ -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 }) => {

View File

@ -0,0 +1,20 @@
import { Field, InputType } from '@nestjs/graphql';
import graphqlTypeJson from 'graphql-type-json';
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
@InputType()
export class UpdateWorkflowRunStepInput {
@Field(() => String, {
description: 'Workflow run ID',
nullable: false,
})
workflowRunId: string;
@Field(() => graphqlTypeJson, {
description: 'Step to update in JSON format',
nullable: false,
})
step: WorkflowAction;
}

View File

@ -4,6 +4,7 @@ import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
import { DeleteWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/delete-workflow-version-step-input.dto';
import { SubmitFormStepInput } from 'src/engine/core-modules/workflow/dtos/submit-form-step-input.dto';
import { UpdateWorkflowRunStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-run-step-input.dto';
import { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-step-input.dto';
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@ -11,12 +12,14 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service';
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
@Resolver()
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
export class WorkflowVersionStepResolver {
export class WorkflowStepResolver {
constructor(
private readonly workflowVersionStepWorkspaceService: WorkflowVersionStepWorkspaceService,
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
) {}
@Mutation(() => WorkflowActionDTO)
@ -73,4 +76,17 @@ export class WorkflowVersionStepResolver {
return true;
}
@Mutation(() => WorkflowActionDTO)
async updateWorkflowRunStep(
@Args('input')
{ workflowRunId, step }: UpdateWorkflowRunStepInput,
): Promise<WorkflowActionDTO> {
await this.workflowRunWorkspaceService.updateWorkflowRunStep({
workflowRunId,
step,
});
return step;
}
}

View File

@ -1,14 +1,15 @@
import { Module } from '@nestjs/common';
import { WorkflowTriggerController } from 'src/engine/core-modules/workflow/controllers/workflow-trigger.controller';
import { WorkflowBuilderResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-builder.resolver';
import { WorkflowStepResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-step.resolver';
import { WorkflowTriggerResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver';
import { WorkflowVersionStepResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-version-step.resolver';
import { WorkflowVersionResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-version.resolver';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { WorkflowBuilderModule } from 'src/modules/workflow/workflow-builder/workflow-builder.module';
import { WorkflowVersionModule } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.module';
import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module';
import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/workflow-trigger.module';
import { WorkflowTriggerController } from 'src/engine/core-modules/workflow/controllers/workflow-trigger.controller';
@Module({
imports: [
@ -16,12 +17,13 @@ import { WorkflowTriggerController } from 'src/engine/core-modules/workflow/cont
WorkflowBuilderModule,
WorkflowCommonModule,
WorkflowVersionModule,
WorkflowRunModule,
],
controllers: [WorkflowTriggerController],
providers: [
WorkflowTriggerResolver,
WorkflowBuilderResolver,
WorkflowVersionStepResolver,
WorkflowStepResolver,
WorkflowVersionResolver,
],
})

View File

@ -1,10 +1,10 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';

View File

@ -9,6 +9,7 @@ import {
WorkflowRunWorkspaceEntity,
} from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import {
WorkflowRunException,
WorkflowRunExceptionCode,
@ -164,6 +165,54 @@ export class WorkflowRunWorkspaceService {
});
}
async updateWorkflowRunStep({
workflowRunId,
step,
}: {
workflowRunId: string;
step: WorkflowAction;
}) {
const workflowRunRepository =
await this.twentyORMManager.getRepository<WorkflowRunWorkspaceEntity>(
'workflowRun',
);
const workflowRunToUpdate = await workflowRunRepository.findOneBy({
id: workflowRunId,
});
if (!workflowRunToUpdate) {
throw new WorkflowRunException(
'No workflow run to update',
WorkflowRunExceptionCode.WORKFLOW_RUN_NOT_FOUND,
);
}
if (
workflowRunToUpdate.status === WorkflowRunStatus.COMPLETED ||
workflowRunToUpdate.status === WorkflowRunStatus.FAILED
) {
throw new WorkflowRunException(
'Cannot update steps of a completed or failed workflow run',
WorkflowRunExceptionCode.INVALID_OPERATION,
);
}
const updatedSteps = workflowRunToUpdate.output?.flow?.steps?.map(
(existingStep) => (step.id === existingStep.id ? step : existingStep),
);
return workflowRunRepository.update(workflowRunToUpdate.id, {
output: {
...(workflowRunToUpdate.output ?? {}),
flow: {
...(workflowRunToUpdate.output?.flow ?? {}),
steps: updatedSteps,
},
},
});
}
async getWorkflowRunOrFail(
workflowRunId: string,
): Promise<WorkflowRunWorkspaceEntity> {