Implement branch front end (#13489)
First PR to support workflow branches on frontend
This commit is contained in:
@@ -608,11 +608,22 @@ export type CreateWebhookDto = {
|
||||
targetUrl: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateWorkflowVersionEdgeInput = {
|
||||
/** Workflow version source step ID */
|
||||
source: Scalars['String'];
|
||||
/** Workflow version target step ID */
|
||||
target: Scalars['String'];
|
||||
/** Workflow version ID */
|
||||
workflowVersionId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateWorkflowVersionStepInput = {
|
||||
/** Next step ID */
|
||||
nextStepId?: InputMaybe<Scalars['UUID']>;
|
||||
/** Parent step ID */
|
||||
parentStepId?: InputMaybe<Scalars['UUID']>;
|
||||
parentStepId?: InputMaybe<Scalars['String']>;
|
||||
/** Step position */
|
||||
position?: InputMaybe<WorkflowStepPositionInput>;
|
||||
/** New step type */
|
||||
stepType: Scalars['String'];
|
||||
/** Workflow version ID */
|
||||
@@ -786,6 +797,7 @@ export enum FeatureFlagKey {
|
||||
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
|
||||
IS_TWO_FACTOR_AUTHENTICATION_ENABLED = 'IS_TWO_FACTOR_AUTHENTICATION_ENABLED',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
|
||||
IS_WORKFLOW_BRANCH_ENABLED = 'IS_WORKFLOW_BRANCH_ENABLED',
|
||||
IS_WORKFLOW_FILTERING_ENABLED = 'IS_WORKFLOW_FILTERING_ENABLED',
|
||||
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED = 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED',
|
||||
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED'
|
||||
@@ -1171,7 +1183,8 @@ export type Mutation = {
|
||||
createOneServerlessFunction: ServerlessFunction;
|
||||
createSAMLIdentityProvider: SetupSsoOutput;
|
||||
createWebhook: Webhook;
|
||||
createWorkflowVersionStep: WorkflowAction;
|
||||
createWorkflowVersionEdge: WorkflowVersionStepChanges;
|
||||
createWorkflowVersionStep: WorkflowVersionStepChanges;
|
||||
deactivateWorkflowVersion: Scalars['Boolean'];
|
||||
deleteApprovedAccessDomain: Scalars['Boolean'];
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
@@ -1187,7 +1200,8 @@ export type Mutation = {
|
||||
deleteTwoFactorAuthenticationMethod: DeleteTwoFactorAuthenticationMethodOutput;
|
||||
deleteUser: User;
|
||||
deleteWebhook: Scalars['Boolean'];
|
||||
deleteWorkflowVersionStep: WorkflowAction;
|
||||
deleteWorkflowVersionEdge: WorkflowVersionStepChanges;
|
||||
deleteWorkflowVersionStep: WorkflowVersionStepChanges;
|
||||
deleteWorkspaceInvitation: Scalars['String'];
|
||||
disablePostgresProxy: PostgresCredentials;
|
||||
editSSOIdentityProvider: EditSsoOutput;
|
||||
@@ -1241,6 +1255,7 @@ export type Mutation = {
|
||||
updatePasswordViaResetToken: InvalidatePassword;
|
||||
updateWebhook?: Maybe<Webhook>;
|
||||
updateWorkflowRunStep: WorkflowAction;
|
||||
updateWorkflowVersionPositions: Scalars['Boolean'];
|
||||
updateWorkflowVersionStep: WorkflowAction;
|
||||
updateWorkspace: Workspace;
|
||||
updateWorkspaceFeatureFlag: Scalars['Boolean'];
|
||||
@@ -1394,6 +1409,11 @@ export type MutationCreateWebhookArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateWorkflowVersionEdgeArgs = {
|
||||
input: CreateWorkflowVersionEdgeInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateWorkflowVersionStepArgs = {
|
||||
input: CreateWorkflowVersionStepInput;
|
||||
};
|
||||
@@ -1464,6 +1484,11 @@ export type MutationDeleteWebhookArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteWorkflowVersionEdgeArgs = {
|
||||
input: CreateWorkflowVersionEdgeInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteWorkflowVersionStepArgs = {
|
||||
input: DeleteWorkflowVersionStepInput;
|
||||
};
|
||||
@@ -1726,6 +1751,11 @@ export type MutationUpdateWorkflowRunStepArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateWorkflowVersionPositionsArgs = {
|
||||
input: UpdateWorkflowVersionPositionsInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateWorkflowVersionStepArgs = {
|
||||
input: UpdateWorkflowVersionStepInput;
|
||||
};
|
||||
@@ -2860,6 +2890,13 @@ export type UpdateWorkflowRunStepInput = {
|
||||
workflowRunId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type UpdateWorkflowVersionPositionsInput = {
|
||||
/** Workflow version updated positions */
|
||||
positions: Array<WorkflowStepPositionUpdateInput>;
|
||||
/** Workflow version ID */
|
||||
workflowVersionId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type UpdateWorkflowVersionStepInput = {
|
||||
/** Step to update in JSON format */
|
||||
step: Scalars['JSON'];
|
||||
@@ -3034,6 +3071,7 @@ export type WorkflowAction = {
|
||||
id: Scalars['UUID'];
|
||||
name: Scalars['String'];
|
||||
nextStepIds?: Maybe<Array<Scalars['UUID']>>;
|
||||
position?: Maybe<WorkflowStepPosition>;
|
||||
settings: Scalars['JSON'];
|
||||
type: Scalars['String'];
|
||||
valid: Scalars['Boolean'];
|
||||
@@ -3044,11 +3082,37 @@ export type WorkflowRun = {
|
||||
workflowRunId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type WorkflowStepPosition = {
|
||||
__typename?: 'WorkflowStepPosition';
|
||||
x: Scalars['Float'];
|
||||
y: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type WorkflowStepPositionInput = {
|
||||
x: Scalars['Float'];
|
||||
y: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type WorkflowStepPositionUpdateInput = {
|
||||
/** Step or trigger ID */
|
||||
id: Scalars['String'];
|
||||
/** Position of the step or trigger */
|
||||
position: WorkflowStepPositionInput;
|
||||
};
|
||||
|
||||
export type WorkflowVersion = {
|
||||
__typename?: 'WorkflowVersion';
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type WorkflowVersionStepChanges = {
|
||||
__typename?: 'WorkflowVersionStepChanges';
|
||||
createdStep?: Maybe<WorkflowAction>;
|
||||
deletedStepId?: Maybe<Scalars['String']>;
|
||||
stepsNextStepIds?: Maybe<Scalars['JSON']>;
|
||||
triggerNextStepIds?: Maybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
export type Workspace = {
|
||||
__typename?: 'Workspace';
|
||||
activationStatus: WorkspaceActivationStatus;
|
||||
@@ -4086,12 +4150,19 @@ export type CreateDraftFromWorkflowVersionMutationVariables = Exact<{
|
||||
|
||||
export type CreateDraftFromWorkflowVersionMutation = { __typename?: 'Mutation', createDraftFromWorkflowVersion: { __typename?: 'WorkflowVersion', id: any } };
|
||||
|
||||
export type CreateWorkflowVersionEdgeMutationVariables = Exact<{
|
||||
input: CreateWorkflowVersionEdgeInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateWorkflowVersionEdgeMutation = { __typename?: 'Mutation', createWorkflowVersionEdge: { __typename?: 'WorkflowVersionStepChanges', triggerNextStepIds?: Array<string> | null, stepsNextStepIds?: any | null } };
|
||||
|
||||
export type CreateWorkflowVersionStepMutationVariables = Exact<{
|
||||
input: CreateWorkflowVersionStepInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateWorkflowVersionStepMutation = { __typename?: 'Mutation', createWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array<any> | null } };
|
||||
export type CreateWorkflowVersionStepMutation = { __typename?: 'Mutation', createWorkflowVersionStep: { __typename?: 'WorkflowVersionStepChanges', triggerNextStepIds?: Array<string> | null, stepsNextStepIds?: any | null, createdStep?: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array<any> | null, position?: { __typename?: 'WorkflowStepPosition', x: number, y: number } | null } | null } };
|
||||
|
||||
export type DeactivateWorkflowVersionMutationVariables = Exact<{
|
||||
workflowVersionId: Scalars['UUID'];
|
||||
@@ -4100,12 +4171,19 @@ export type DeactivateWorkflowVersionMutationVariables = Exact<{
|
||||
|
||||
export type DeactivateWorkflowVersionMutation = { __typename?: 'Mutation', deactivateWorkflowVersion: boolean };
|
||||
|
||||
export type DeleteWorkflowVersionEdgeMutationVariables = Exact<{
|
||||
input: CreateWorkflowVersionEdgeInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteWorkflowVersionEdgeMutation = { __typename?: 'Mutation', deleteWorkflowVersionEdge: { __typename?: 'WorkflowVersionStepChanges', triggerNextStepIds?: Array<string> | null, stepsNextStepIds?: any | null } };
|
||||
|
||||
export type DeleteWorkflowVersionStepMutationVariables = Exact<{
|
||||
input: DeleteWorkflowVersionStepInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteWorkflowVersionStepMutation = { __typename?: 'Mutation', deleteWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array<any> | null } };
|
||||
export type DeleteWorkflowVersionStepMutation = { __typename?: 'Mutation', deleteWorkflowVersionStep: { __typename?: 'WorkflowVersionStepChanges', triggerNextStepIds?: Array<string> | null, stepsNextStepIds?: any | null, deletedStepId?: string | null } };
|
||||
|
||||
export type RunWorkflowVersionMutationVariables = Exact<{
|
||||
input: RunWorkflowVersionInput;
|
||||
@@ -4119,14 +4197,14 @@ export type UpdateWorkflowRunStepMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateWorkflowRunStepMutation = { __typename?: 'Mutation', updateWorkflowRunStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array<any> | null } };
|
||||
export type UpdateWorkflowRunStepMutation = { __typename?: 'Mutation', updateWorkflowRunStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array<any> | null, position?: { __typename?: 'WorkflowStepPosition', x: number, y: number } | null } };
|
||||
|
||||
export type UpdateWorkflowVersionStepMutationVariables = Exact<{
|
||||
input: UpdateWorkflowVersionStepInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateWorkflowVersionStepMutation = { __typename?: 'Mutation', updateWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array<any> | null } };
|
||||
export type UpdateWorkflowVersionStepMutation = { __typename?: 'Mutation', updateWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array<any> | null, position?: { __typename?: 'WorkflowStepPosition', x: number, y: number } | null } };
|
||||
|
||||
export type SubmitFormStepMutationVariables = Exact<{
|
||||
input: SubmitFormStepInput;
|
||||
@@ -4135,6 +4213,13 @@ export type SubmitFormStepMutationVariables = Exact<{
|
||||
|
||||
export type SubmitFormStepMutation = { __typename?: 'Mutation', submitFormStep: boolean };
|
||||
|
||||
export type UpdateWorkflowVersionPositionsMutationVariables = Exact<{
|
||||
input: UpdateWorkflowVersionPositionsInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateWorkflowVersionPositionsMutation = { __typename?: 'Mutation', updateWorkflowVersionPositions: boolean };
|
||||
|
||||
export type DeleteWorkspaceInvitationMutationVariables = Exact<{
|
||||
appTokenId: Scalars['String'];
|
||||
}>;
|
||||
@@ -9226,15 +9311,57 @@ export function useCreateDraftFromWorkflowVersionMutation(baseOptions?: Apollo.M
|
||||
export type CreateDraftFromWorkflowVersionMutationHookResult = ReturnType<typeof useCreateDraftFromWorkflowVersionMutation>;
|
||||
export type CreateDraftFromWorkflowVersionMutationResult = Apollo.MutationResult<CreateDraftFromWorkflowVersionMutation>;
|
||||
export type CreateDraftFromWorkflowVersionMutationOptions = Apollo.BaseMutationOptions<CreateDraftFromWorkflowVersionMutation, CreateDraftFromWorkflowVersionMutationVariables>;
|
||||
export const CreateWorkflowVersionEdgeDocument = gql`
|
||||
mutation CreateWorkflowVersionEdge($input: CreateWorkflowVersionEdgeInput!) {
|
||||
createWorkflowVersionEdge(input: $input) {
|
||||
triggerNextStepIds
|
||||
stepsNextStepIds
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type CreateWorkflowVersionEdgeMutationFn = Apollo.MutationFunction<CreateWorkflowVersionEdgeMutation, CreateWorkflowVersionEdgeMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useCreateWorkflowVersionEdgeMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useCreateWorkflowVersionEdgeMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCreateWorkflowVersionEdgeMutation` 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 [createWorkflowVersionEdgeMutation, { data, loading, error }] = useCreateWorkflowVersionEdgeMutation({
|
||||
* variables: {
|
||||
* input: // value for 'input'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCreateWorkflowVersionEdgeMutation(baseOptions?: Apollo.MutationHookOptions<CreateWorkflowVersionEdgeMutation, CreateWorkflowVersionEdgeMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CreateWorkflowVersionEdgeMutation, CreateWorkflowVersionEdgeMutationVariables>(CreateWorkflowVersionEdgeDocument, options);
|
||||
}
|
||||
export type CreateWorkflowVersionEdgeMutationHookResult = ReturnType<typeof useCreateWorkflowVersionEdgeMutation>;
|
||||
export type CreateWorkflowVersionEdgeMutationResult = Apollo.MutationResult<CreateWorkflowVersionEdgeMutation>;
|
||||
export type CreateWorkflowVersionEdgeMutationOptions = Apollo.BaseMutationOptions<CreateWorkflowVersionEdgeMutation, CreateWorkflowVersionEdgeMutationVariables>;
|
||||
export const CreateWorkflowVersionStepDocument = gql`
|
||||
mutation CreateWorkflowVersionStep($input: CreateWorkflowVersionStepInput!) {
|
||||
createWorkflowVersionStep(input: $input) {
|
||||
id
|
||||
name
|
||||
type
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
triggerNextStepIds
|
||||
stepsNextStepIds
|
||||
createdStep {
|
||||
id
|
||||
name
|
||||
type
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
position {
|
||||
x
|
||||
y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -9295,15 +9422,46 @@ export function useDeactivateWorkflowVersionMutation(baseOptions?: Apollo.Mutati
|
||||
export type DeactivateWorkflowVersionMutationHookResult = ReturnType<typeof useDeactivateWorkflowVersionMutation>;
|
||||
export type DeactivateWorkflowVersionMutationResult = Apollo.MutationResult<DeactivateWorkflowVersionMutation>;
|
||||
export type DeactivateWorkflowVersionMutationOptions = Apollo.BaseMutationOptions<DeactivateWorkflowVersionMutation, DeactivateWorkflowVersionMutationVariables>;
|
||||
export const DeleteWorkflowVersionEdgeDocument = gql`
|
||||
mutation DeleteWorkflowVersionEdge($input: CreateWorkflowVersionEdgeInput!) {
|
||||
deleteWorkflowVersionEdge(input: $input) {
|
||||
triggerNextStepIds
|
||||
stepsNextStepIds
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type DeleteWorkflowVersionEdgeMutationFn = Apollo.MutationFunction<DeleteWorkflowVersionEdgeMutation, DeleteWorkflowVersionEdgeMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useDeleteWorkflowVersionEdgeMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useDeleteWorkflowVersionEdgeMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useDeleteWorkflowVersionEdgeMutation` 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 [deleteWorkflowVersionEdgeMutation, { data, loading, error }] = useDeleteWorkflowVersionEdgeMutation({
|
||||
* variables: {
|
||||
* input: // value for 'input'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDeleteWorkflowVersionEdgeMutation(baseOptions?: Apollo.MutationHookOptions<DeleteWorkflowVersionEdgeMutation, DeleteWorkflowVersionEdgeMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<DeleteWorkflowVersionEdgeMutation, DeleteWorkflowVersionEdgeMutationVariables>(DeleteWorkflowVersionEdgeDocument, options);
|
||||
}
|
||||
export type DeleteWorkflowVersionEdgeMutationHookResult = ReturnType<typeof useDeleteWorkflowVersionEdgeMutation>;
|
||||
export type DeleteWorkflowVersionEdgeMutationResult = Apollo.MutationResult<DeleteWorkflowVersionEdgeMutation>;
|
||||
export type DeleteWorkflowVersionEdgeMutationOptions = Apollo.BaseMutationOptions<DeleteWorkflowVersionEdgeMutation, DeleteWorkflowVersionEdgeMutationVariables>;
|
||||
export const DeleteWorkflowVersionStepDocument = gql`
|
||||
mutation DeleteWorkflowVersionStep($input: DeleteWorkflowVersionStepInput!) {
|
||||
deleteWorkflowVersionStep(input: $input) {
|
||||
id
|
||||
name
|
||||
type
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
triggerNextStepIds
|
||||
stepsNextStepIds
|
||||
deletedStepId
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -9375,6 +9533,10 @@ export const UpdateWorkflowRunStepDocument = gql`
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
position {
|
||||
x
|
||||
y
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -9413,6 +9575,10 @@ export const UpdateWorkflowVersionStepDocument = gql`
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
position {
|
||||
x
|
||||
y
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -9473,6 +9639,37 @@ export function useSubmitFormStepMutation(baseOptions?: Apollo.MutationHookOptio
|
||||
export type SubmitFormStepMutationHookResult = ReturnType<typeof useSubmitFormStepMutation>;
|
||||
export type SubmitFormStepMutationResult = Apollo.MutationResult<SubmitFormStepMutation>;
|
||||
export type SubmitFormStepMutationOptions = Apollo.BaseMutationOptions<SubmitFormStepMutation, SubmitFormStepMutationVariables>;
|
||||
export const UpdateWorkflowVersionPositionsDocument = gql`
|
||||
mutation UpdateWorkflowVersionPositions($input: UpdateWorkflowVersionPositionsInput!) {
|
||||
updateWorkflowVersionPositions(input: $input)
|
||||
}
|
||||
`;
|
||||
export type UpdateWorkflowVersionPositionsMutationFn = Apollo.MutationFunction<UpdateWorkflowVersionPositionsMutation, UpdateWorkflowVersionPositionsMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useUpdateWorkflowVersionPositionsMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useUpdateWorkflowVersionPositionsMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useUpdateWorkflowVersionPositionsMutation` 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 [updateWorkflowVersionPositionsMutation, { data, loading, error }] = useUpdateWorkflowVersionPositionsMutation({
|
||||
* variables: {
|
||||
* input: // value for 'input'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useUpdateWorkflowVersionPositionsMutation(baseOptions?: Apollo.MutationHookOptions<UpdateWorkflowVersionPositionsMutation, UpdateWorkflowVersionPositionsMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<UpdateWorkflowVersionPositionsMutation, UpdateWorkflowVersionPositionsMutationVariables>(UpdateWorkflowVersionPositionsDocument, options);
|
||||
}
|
||||
export type UpdateWorkflowVersionPositionsMutationHookResult = ReturnType<typeof useUpdateWorkflowVersionPositionsMutation>;
|
||||
export type UpdateWorkflowVersionPositionsMutationResult = Apollo.MutationResult<UpdateWorkflowVersionPositionsMutation>;
|
||||
export type UpdateWorkflowVersionPositionsMutationOptions = Apollo.BaseMutationOptions<UpdateWorkflowVersionPositionsMutation, UpdateWorkflowVersionPositionsMutationVariables>;
|
||||
export const DeleteWorkspaceInvitationDocument = gql`
|
||||
mutation DeleteWorkspaceInvitation($appTokenId: String!) {
|
||||
deleteWorkspaceInvitation(appTokenId: $appTokenId)
|
||||
|
||||
@@ -572,11 +572,22 @@ export type CreateWebhookDto = {
|
||||
targetUrl: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateWorkflowVersionEdgeInput = {
|
||||
/** Workflow version source step ID */
|
||||
source: Scalars['String'];
|
||||
/** Workflow version target step ID */
|
||||
target: Scalars['String'];
|
||||
/** Workflow version ID */
|
||||
workflowVersionId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateWorkflowVersionStepInput = {
|
||||
/** Next step ID */
|
||||
nextStepId?: InputMaybe<Scalars['UUID']>;
|
||||
/** Parent step ID */
|
||||
parentStepId?: InputMaybe<Scalars['UUID']>;
|
||||
parentStepId?: InputMaybe<Scalars['String']>;
|
||||
/** Step position */
|
||||
position?: InputMaybe<WorkflowStepPositionInput>;
|
||||
/** New step type */
|
||||
stepType: Scalars['String'];
|
||||
/** Workflow version ID */
|
||||
@@ -750,6 +761,7 @@ export enum FeatureFlagKey {
|
||||
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
|
||||
IS_TWO_FACTOR_AUTHENTICATION_ENABLED = 'IS_TWO_FACTOR_AUTHENTICATION_ENABLED',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
|
||||
IS_WORKFLOW_BRANCH_ENABLED = 'IS_WORKFLOW_BRANCH_ENABLED',
|
||||
IS_WORKFLOW_FILTERING_ENABLED = 'IS_WORKFLOW_FILTERING_ENABLED',
|
||||
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED = 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED',
|
||||
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED'
|
||||
@@ -1127,7 +1139,8 @@ export type Mutation = {
|
||||
createOneServerlessFunction: ServerlessFunction;
|
||||
createSAMLIdentityProvider: SetupSsoOutput;
|
||||
createWebhook: Webhook;
|
||||
createWorkflowVersionStep: WorkflowAction;
|
||||
createWorkflowVersionEdge: WorkflowVersionStepChanges;
|
||||
createWorkflowVersionStep: WorkflowVersionStepChanges;
|
||||
deactivateWorkflowVersion: Scalars['Boolean'];
|
||||
deleteApprovedAccessDomain: Scalars['Boolean'];
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
@@ -1142,7 +1155,8 @@ export type Mutation = {
|
||||
deleteTwoFactorAuthenticationMethod: DeleteTwoFactorAuthenticationMethodOutput;
|
||||
deleteUser: User;
|
||||
deleteWebhook: Scalars['Boolean'];
|
||||
deleteWorkflowVersionStep: WorkflowAction;
|
||||
deleteWorkflowVersionEdge: WorkflowVersionStepChanges;
|
||||
deleteWorkflowVersionStep: WorkflowVersionStepChanges;
|
||||
deleteWorkspaceInvitation: Scalars['String'];
|
||||
disablePostgresProxy: PostgresCredentials;
|
||||
editSSOIdentityProvider: EditSsoOutput;
|
||||
@@ -1192,6 +1206,7 @@ export type Mutation = {
|
||||
updatePasswordViaResetToken: InvalidatePassword;
|
||||
updateWebhook?: Maybe<Webhook>;
|
||||
updateWorkflowRunStep: WorkflowAction;
|
||||
updateWorkflowVersionPositions: Scalars['Boolean'];
|
||||
updateWorkflowVersionStep: WorkflowAction;
|
||||
updateWorkspace: Workspace;
|
||||
updateWorkspaceFeatureFlag: Scalars['Boolean'];
|
||||
@@ -1330,6 +1345,11 @@ export type MutationCreateWebhookArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateWorkflowVersionEdgeArgs = {
|
||||
input: CreateWorkflowVersionEdgeInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateWorkflowVersionStepArgs = {
|
||||
input: CreateWorkflowVersionStepInput;
|
||||
};
|
||||
@@ -1395,6 +1415,11 @@ export type MutationDeleteWebhookArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteWorkflowVersionEdgeArgs = {
|
||||
input: CreateWorkflowVersionEdgeInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteWorkflowVersionStepArgs = {
|
||||
input: DeleteWorkflowVersionStepInput;
|
||||
};
|
||||
@@ -1637,6 +1662,11 @@ export type MutationUpdateWorkflowRunStepArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateWorkflowVersionPositionsArgs = {
|
||||
input: UpdateWorkflowVersionPositionsInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateWorkflowVersionStepArgs = {
|
||||
input: UpdateWorkflowVersionStepInput;
|
||||
};
|
||||
@@ -2698,6 +2728,13 @@ export type UpdateWorkflowRunStepInput = {
|
||||
workflowRunId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type UpdateWorkflowVersionPositionsInput = {
|
||||
/** Workflow version updated positions */
|
||||
positions: Array<WorkflowStepPositionUpdateInput>;
|
||||
/** Workflow version ID */
|
||||
workflowVersionId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type UpdateWorkflowVersionStepInput = {
|
||||
/** Step to update in JSON format */
|
||||
step: Scalars['JSON'];
|
||||
@@ -2862,6 +2899,7 @@ export type WorkflowAction = {
|
||||
id: Scalars['UUID'];
|
||||
name: Scalars['String'];
|
||||
nextStepIds?: Maybe<Array<Scalars['UUID']>>;
|
||||
position?: Maybe<WorkflowStepPosition>;
|
||||
settings: Scalars['JSON'];
|
||||
type: Scalars['String'];
|
||||
valid: Scalars['Boolean'];
|
||||
@@ -2872,11 +2910,37 @@ export type WorkflowRun = {
|
||||
workflowRunId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type WorkflowStepPosition = {
|
||||
__typename?: 'WorkflowStepPosition';
|
||||
x: Scalars['Float'];
|
||||
y: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type WorkflowStepPositionInput = {
|
||||
x: Scalars['Float'];
|
||||
y: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type WorkflowStepPositionUpdateInput = {
|
||||
/** Step or trigger ID */
|
||||
id: Scalars['String'];
|
||||
/** Position of the step or trigger */
|
||||
position: WorkflowStepPositionInput;
|
||||
};
|
||||
|
||||
export type WorkflowVersion = {
|
||||
__typename?: 'WorkflowVersion';
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type WorkflowVersionStepChanges = {
|
||||
__typename?: 'WorkflowVersionStepChanges';
|
||||
createdStep?: Maybe<WorkflowAction>;
|
||||
deletedStepId?: Maybe<Scalars['String']>;
|
||||
stepsNextStepIds?: Maybe<Scalars['JSON']>;
|
||||
triggerNextStepIds?: Maybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
export type Workspace = {
|
||||
__typename?: 'Workspace';
|
||||
activationStatus: WorkspaceActivationStatus;
|
||||
|
||||
+7
-8
@@ -9,9 +9,9 @@ import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
|
||||
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
||||
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
|
||||
import { useFilteredOtherActions } from '@/workflow/workflow-steps/workflow-actions/hooks/useFilteredOtherActions';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemCommand } from 'twenty-ui/navigation';
|
||||
import { useCloseRightClickMenu } from '@/workflow/workflow-diagram/hooks/useCloseRightClickMenu';
|
||||
|
||||
export const CommandMenuWorkflowSelectActionContent = ({
|
||||
workflow,
|
||||
@@ -25,28 +25,27 @@ export const CommandMenuWorkflowSelectActionContent = ({
|
||||
});
|
||||
const filteredOtherActions = useFilteredOtherActions();
|
||||
|
||||
const { closeRightClickMenu } = useCloseRightClickMenu();
|
||||
|
||||
const [workflowInsertStepIds, setWorkflowInsertStepIds] =
|
||||
useRecoilComponentStateV2(workflowInsertStepIdsComponentState);
|
||||
|
||||
const handleCreateStep = async (actionType: WorkflowActionType) => {
|
||||
const { parentStepId, nextStepId } = workflowInsertStepIds;
|
||||
|
||||
if (!isDefined(parentStepId)) {
|
||||
throw new Error(
|
||||
'No parentStepId. Please select a parent step to create from.',
|
||||
);
|
||||
}
|
||||
const { parentStepId, nextStepId, position } = workflowInsertStepIds;
|
||||
|
||||
await createStep({
|
||||
newStepType: actionType,
|
||||
parentStepId,
|
||||
nextStepId,
|
||||
position,
|
||||
});
|
||||
|
||||
setWorkflowInsertStepIds({
|
||||
parentStepId: undefined,
|
||||
nextStepId: undefined,
|
||||
position: undefined,
|
||||
});
|
||||
closeRightClickMenu();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+7
@@ -15,6 +15,8 @@ import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hoo
|
||||
import { getTriggerDefaultDefinition } from '@/workflow/workflow-trigger/utils/getTriggerDefaultDefinition';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItemCommand } from 'twenty-ui/navigation';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
export const CommandMenuWorkflowSelectTriggerTypeContent = ({
|
||||
workflow,
|
||||
@@ -32,6 +34,10 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = ({
|
||||
);
|
||||
const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu();
|
||||
|
||||
const isWorkflowBranchEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
|
||||
);
|
||||
|
||||
const handleTriggerTypeClick = ({
|
||||
type,
|
||||
defaultLabel,
|
||||
@@ -47,6 +53,7 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = ({
|
||||
defaultLabel,
|
||||
type,
|
||||
activeNonSystemObjectMetadataItems,
|
||||
steps: !isWorkflowBranchEnabled ? workflow.currentVersion.steps : [],
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_WORKFLOW_VERSION_EDGE = gql`
|
||||
mutation CreateWorkflowVersionEdge($input: CreateWorkflowVersionEdgeInput!) {
|
||||
createWorkflowVersionEdge(input: $input) {
|
||||
triggerNextStepIds
|
||||
stepsNextStepIds
|
||||
}
|
||||
}
|
||||
`;
|
||||
+14
-6
@@ -3,12 +3,20 @@ import { gql } from '@apollo/client';
|
||||
export const CREATE_WORKFLOW_VERSION_STEP = gql`
|
||||
mutation CreateWorkflowVersionStep($input: CreateWorkflowVersionStepInput!) {
|
||||
createWorkflowVersionStep(input: $input) {
|
||||
id
|
||||
name
|
||||
type
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
triggerNextStepIds
|
||||
stepsNextStepIds
|
||||
createdStep {
|
||||
id
|
||||
name
|
||||
type
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
position {
|
||||
x
|
||||
y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_WORKFLOW_VERSION_EDGE = gql`
|
||||
mutation DeleteWorkflowVersionEdge($input: CreateWorkflowVersionEdgeInput!) {
|
||||
deleteWorkflowVersionEdge(input: $input) {
|
||||
triggerNextStepIds
|
||||
stepsNextStepIds
|
||||
}
|
||||
}
|
||||
`;
|
||||
+3
-6
@@ -3,12 +3,9 @@ import { gql } from '@apollo/client';
|
||||
export const DELETE_WORKFLOW_VERSION_STEP = gql`
|
||||
mutation DeleteWorkflowVersionStep($input: DeleteWorkflowVersionStepInput!) {
|
||||
deleteWorkflowVersionStep(input: $input) {
|
||||
id
|
||||
name
|
||||
type
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
triggerNextStepIds
|
||||
stepsNextStepIds
|
||||
deletedStepId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -9,6 +9,10 @@ export const UPDATE_WORKFLOW_RUN_STEP = gql`
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
position {
|
||||
x
|
||||
y
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
+4
@@ -9,6 +9,10 @@ export const UPDATE_WORKFLOW_VERSION_STEP = gql`
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
position {
|
||||
x
|
||||
y
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,90 +1,35 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
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 { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { DELETE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/deleteWorkflowVersionStep';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
DeleteWorkflowVersionStepInput,
|
||||
DeleteWorkflowVersionStepMutation,
|
||||
DeleteWorkflowVersionStepMutationVariables,
|
||||
WorkflowAction,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useUpdateWorkflowVersionCache } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache';
|
||||
|
||||
export const useDeleteWorkflowVersionStep = () => {
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const { updateWorkflowVersionCache } = useUpdateWorkflowVersionCache();
|
||||
|
||||
const [mutate] = useMutation<
|
||||
DeleteWorkflowVersionStepMutation,
|
||||
DeleteWorkflowVersionStepMutationVariables
|
||||
>(DELETE_WORKFLOW_VERSION_STEP, {
|
||||
client: apolloCoreClient,
|
||||
});
|
||||
|
||||
const deleteWorkflowVersionStep = async (
|
||||
input: DeleteWorkflowVersionStepInput,
|
||||
) => {
|
||||
const result = await mutate({ variables: { input } });
|
||||
|
||||
const deletedStep = result?.data?.deleteWorkflowVersionStep;
|
||||
const workflowVersionStepChanges = result?.data?.deleteWorkflowVersionStep;
|
||||
|
||||
if (!isDefined(deletedStep)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedRecord = getRecordFromCache<WorkflowVersion>(
|
||||
input.workflowVersionId,
|
||||
);
|
||||
|
||||
if (!isDefined(cachedRecord)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newCachedRecord = {
|
||||
...cachedRecord,
|
||||
steps: (cachedRecord.steps || [])
|
||||
.filter((step: WorkflowAction) => step.id !== deletedStep.id)
|
||||
.map((step: WorkflowAction) => {
|
||||
if (!step.nextStepIds?.includes(deletedStep.id)) {
|
||||
return step;
|
||||
}
|
||||
|
||||
return {
|
||||
...step,
|
||||
nextStepIds: [
|
||||
...new Set([
|
||||
...(step.nextStepIds?.filter(
|
||||
(nextStepId) => nextStepId !== deletedStep.id,
|
||||
) || []),
|
||||
...(deletedStep.nextStepIds || []),
|
||||
]),
|
||||
],
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
steps: true,
|
||||
};
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloCoreClient.cache,
|
||||
record: newCachedRecord,
|
||||
recordGqlFields,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
updateWorkflowVersionCache({
|
||||
workflowVersionStepChanges,
|
||||
workflowVersionId: input.workflowVersionId,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getStepOutputSchemaFamilyStateKey } from '@/workflow/utils/getStepOutpu
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
import {
|
||||
OutputSchema,
|
||||
StepOutputSchema,
|
||||
|
||||
@@ -23,11 +23,14 @@ export const baseWorkflowActionSchema = z.object({
|
||||
name: z.string(),
|
||||
valid: z.boolean(),
|
||||
nextStepIds: z.array(z.string()).optional().nullable(),
|
||||
position: z.object({ x: z.number(), y: z.number() }).optional().nullable(),
|
||||
});
|
||||
|
||||
export const baseTriggerSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
type: z.string(),
|
||||
position: z.object({ x: z.number(), y: z.number() }).optional().nullable(),
|
||||
nextStepIds: z.array(z.string()).optional().nullable(),
|
||||
});
|
||||
|
||||
// Action settings schemas
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Handle } from '@xyflow/react';
|
||||
import { NODE_HANDLE_HEIGHT_PX } from '@/workflow/workflow-diagram/constants/NodeHandleHeightPx';
|
||||
import { NODE_HANDLE_WIDTH_PX } from '@/workflow/workflow-diagram/constants/NodeHandleWidthPx';
|
||||
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||
|
||||
export const StyledHandle = styled(Handle)`
|
||||
height: ${NODE_HANDLE_HEIGHT_PX}px;
|
||||
width: ${NODE_HANDLE_WIDTH_PX}px;
|
||||
left: ${CREATE_STEP_NODE_WIDTH}px;
|
||||
visibility: hidden;
|
||||
`;
|
||||
|
||||
export { StyledHandle as WorkflowDiagramBaseHandle };
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { Handle, HandleProps } from '@xyflow/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { NODE_HANDLE_HEIGHT_PX } from '@/workflow/workflow-diagram/constants/NodeHandleHeightPx';
|
||||
import { NODE_HANDLE_WIDTH_PX } from '@/workflow/workflow-diagram/constants/NodeHandleWidthPx';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
type WorkflowDiagramBaseHandleProps = HandleProps & {
|
||||
isVisible?: boolean;
|
||||
disableHoverEffect?: boolean;
|
||||
};
|
||||
|
||||
const HANDLE_SCALE_ON_HOVER = 1.5;
|
||||
|
||||
const TRANSLATE_PERCENT = 33.33;
|
||||
|
||||
const StyledHandle = styled(Handle, {
|
||||
shouldForwardProp: (prop) => !['disableHoverEffect'].includes(prop),
|
||||
})<WorkflowDiagramBaseHandleProps>`
|
||||
// We need !important to avoid passing style with the Handle.style property
|
||||
height: ${NODE_HANDLE_HEIGHT_PX}px !important;
|
||||
width: ${NODE_HANDLE_WIDTH_PX}px !important;
|
||||
border-color: ${({ theme }) => theme.border.color.strong} !important;
|
||||
background: ${({ theme }) => theme.background.primary} !important;
|
||||
transition:
|
||||
transform 0.1s ease-out,
|
||||
background 0.1s,
|
||||
border-color 0.1s !important;
|
||||
z-index: 1 !important;
|
||||
|
||||
${({ position }) => {
|
||||
switch (position) {
|
||||
case 'top':
|
||||
return `top: 19px !important;`;
|
||||
case 'bottom':
|
||||
return `top: 47px !important;`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}}
|
||||
|
||||
${({ disableHoverEffect, theme, position }) => {
|
||||
if (disableHoverEffect === true) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let transform = `transform: scale(${HANDLE_SCALE_ON_HOVER}) !important;`;
|
||||
|
||||
if (position === 'top') {
|
||||
transform = `transform: scale(${HANDLE_SCALE_ON_HOVER}) translate(${-TRANSLATE_PERCENT}%, ${-TRANSLATE_PERCENT}%) !important;`;
|
||||
} else if (position === 'bottom') {
|
||||
transform = `transform: scale(${HANDLE_SCALE_ON_HOVER}) translate(${-TRANSLATE_PERCENT}%, ${TRANSLATE_PERCENT * 2}%) !important;`;
|
||||
}
|
||||
|
||||
return `
|
||||
&:hover {
|
||||
border-color: ${theme.font.color.light} !important;
|
||||
background: ${theme.background.secondary} !important;
|
||||
${transform}
|
||||
}
|
||||
`;
|
||||
}}
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramBaseHandle = ({
|
||||
type,
|
||||
position,
|
||||
isVisible = true,
|
||||
}: WorkflowDiagramBaseHandleProps) => {
|
||||
const isWorkflowBranchEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledHandle
|
||||
type={type}
|
||||
position={position}
|
||||
style={{ opacity: isVisible ? 1 : 0 }}
|
||||
disableHoverEffect={!isWorkflowBranchEnabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+6
-5
@@ -1,7 +1,6 @@
|
||||
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react';
|
||||
import { BaseEdge, EdgeProps, getBezierPath } from '@xyflow/react';
|
||||
|
||||
type WorkflowDiagramBlankEdgeProps = EdgeProps<WorkflowDiagramEdge>;
|
||||
|
||||
@@ -9,14 +8,16 @@ export const WorkflowDiagramBlankEdge = ({
|
||||
markerStart,
|
||||
markerEnd,
|
||||
sourceY,
|
||||
sourceX,
|
||||
targetY,
|
||||
targetX,
|
||||
}: WorkflowDiagramBlankEdgeProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [edgePath] = getStraightPath({
|
||||
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||
const [edgePath] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX: CREATE_STEP_NODE_WIDTH,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
|
||||
+94
-16
@@ -18,7 +18,6 @@ import {
|
||||
WorkflowDiagramNode,
|
||||
WorkflowDiagramNodeType,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getOrganizedDiagram } from '@/workflow/workflow-diagram/utils/getOrganizedDiagram';
|
||||
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
@@ -33,13 +32,27 @@ import {
|
||||
applyEdgeChanges,
|
||||
applyNodeChanges,
|
||||
useReactFlow,
|
||||
Connection,
|
||||
OnNodeDrag,
|
||||
OnBeforeDelete,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Tag, TagColor } from 'twenty-ui/components';
|
||||
import { THEME_COMMON } from 'twenty-ui/theme';
|
||||
import { WorkflowDiagramRightClickCommandMenu } from '@/workflow/workflow-diagram/components/WorkflowDiagramRightClickCommandMenu';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { getOrganizedDiagram } from '@/workflow/workflow-diagram/utils/getOrganizedDiagram';
|
||||
|
||||
const StyledResetReactflowStyles = styled.div`
|
||||
height: 100%;
|
||||
@@ -98,6 +111,11 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
tagColor,
|
||||
tagText,
|
||||
onInit,
|
||||
onConnect,
|
||||
onDeleteEdge,
|
||||
onNodeDragStop,
|
||||
handlePaneContextMenu,
|
||||
nodesConnectable = false,
|
||||
}: {
|
||||
nodeTypes: Partial<
|
||||
Record<
|
||||
@@ -126,6 +144,19 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
tagColor: TagColor;
|
||||
tagText: string;
|
||||
onInit?: () => void;
|
||||
onConnect?: (params: Connection) => void;
|
||||
onDeleteEdge?: (edge: WorkflowDiagramEdge) => void;
|
||||
onNodeDragStop?: OnNodeDrag<WorkflowDiagramNode>;
|
||||
nodesConnectable?: boolean;
|
||||
handlePaneContextMenu?: ({
|
||||
x,
|
||||
y,
|
||||
event,
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
event: MouseEvent | React.MouseEvent<Element, MouseEvent>;
|
||||
}) => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -157,16 +188,22 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
workflowDiagramWaitingNodesDimensionsComponentState,
|
||||
);
|
||||
|
||||
const isWorkflowBranchEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
|
||||
);
|
||||
|
||||
const [workflowDiagramFlowInitialized, setWorkflowDiagramFlowInitialized] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const { nodes, edges } = useMemo(
|
||||
() =>
|
||||
isDefined(workflowDiagram)
|
||||
? getOrganizedDiagram(workflowDiagram)
|
||||
: { nodes: [], edges: [] },
|
||||
[workflowDiagram],
|
||||
);
|
||||
const { nodes, edges } = useMemo(() => {
|
||||
if (isDefined(workflowDiagram)) {
|
||||
if (isWorkflowBranchEnabled) {
|
||||
return workflowDiagram;
|
||||
}
|
||||
return getOrganizedDiagram(workflowDiagram);
|
||||
}
|
||||
return { nodes: [], edges: [] };
|
||||
}, [workflowDiagram, isWorkflowBranchEnabled]);
|
||||
|
||||
const { rightDrawerState } = useRightDrawerState();
|
||||
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||
@@ -195,6 +232,7 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
setWorkflowInsertStepIds({
|
||||
parentStepId: undefined,
|
||||
nextStepId: undefined,
|
||||
position: undefined,
|
||||
});
|
||||
setWorkflowSelectedNode(undefined);
|
||||
});
|
||||
@@ -368,6 +406,36 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
],
|
||||
);
|
||||
|
||||
const onBeforeDelete: OnBeforeDelete<
|
||||
WorkflowDiagramNode,
|
||||
WorkflowDiagramEdge
|
||||
> = async (diagram) => {
|
||||
if (
|
||||
diagram.nodes.length === 0 // We don't call deleteEdge when node diagram deletion is called
|
||||
) {
|
||||
for (const edge of diagram.edges) {
|
||||
onDeleteEdge?.(edge);
|
||||
}
|
||||
return diagram;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const onPaneContextMenu = useCallback(
|
||||
(event: MouseEvent | React.MouseEvent<Element, MouseEvent>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const bounds = containerRef.current?.getBoundingClientRect();
|
||||
if (!bounds) return;
|
||||
|
||||
const x = event.clientX - bounds.left;
|
||||
const y = event.clientY - bounds.top;
|
||||
|
||||
handlePaneContextMenu?.({ x, y, event });
|
||||
},
|
||||
[handlePaneContextMenu],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledResetReactflowStyles ref={containerRef}>
|
||||
<WorkflowDiagramCustomMarkers />
|
||||
@@ -383,17 +451,23 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
edges={edges}
|
||||
onNodesChange={handleNodesChanges}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
onBeforeDelete={async () => {
|
||||
// Abort all non-programmatic deletions
|
||||
return false;
|
||||
}}
|
||||
onConnect={isWorkflowBranchEnabled ? onConnect : undefined}
|
||||
onNodeDragStop={isWorkflowBranchEnabled ? onNodeDragStop : undefined}
|
||||
onBeforeDelete={
|
||||
isWorkflowBranchEnabled ? onBeforeDelete : async () => false
|
||||
}
|
||||
selectNodesOnDrag={false}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
multiSelectionKeyCode={null}
|
||||
nodesFocusable={false}
|
||||
edgesFocusable={false}
|
||||
nodesDraggable={false}
|
||||
edgesFocusable={
|
||||
isWorkflowBranchEnabled ? isDefined(onDeleteEdge) : false
|
||||
}
|
||||
panOnDrag={workflowDiagramPanOnDrag}
|
||||
nodesConnectable={false}
|
||||
onPaneContextMenu={
|
||||
isWorkflowBranchEnabled ? onPaneContextMenu : undefined
|
||||
}
|
||||
nodesConnectable={isWorkflowBranchEnabled ? nodesConnectable : false}
|
||||
paneClickDistance={10} // Fix small unwanted user dragging does not select node
|
||||
preventScrolling={false}
|
||||
>
|
||||
@@ -402,6 +476,10 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
{children}
|
||||
</ReactFlow>
|
||||
|
||||
{isDefined(handlePaneContextMenu) && isWorkflowBranchEnabled && (
|
||||
<WorkflowDiagramRightClickCommandMenu />
|
||||
)}
|
||||
|
||||
<StyledStatusTagContainer data-testid={tagContainerTestId}>
|
||||
<Tag color={tagColor} text={tagText} />
|
||||
</StyledStatusTagContainer>
|
||||
|
||||
+104
-7
@@ -1,31 +1,123 @@
|
||||
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { WorkflowDiagramBlankEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramBlankEdge';
|
||||
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
|
||||
import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect';
|
||||
import { WorkflowDiagramCreateStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepNode';
|
||||
import { WorkflowDiagramDefaultEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdgeEditable';
|
||||
import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger';
|
||||
import { WorkflowDiagramFilterEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilterEdgeEditable';
|
||||
import { WorkflowDiagramFilteringDisabledEdgeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramFilteringDisabledEdgeEditable';
|
||||
import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable';
|
||||
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
|
||||
import { ReactFlowProvider } from '@xyflow/react';
|
||||
import { addEdge, Connection, ReactFlowProvider } from '@xyflow/react';
|
||||
import { useCreateEdge } from '@/workflow/workflow-steps/hooks/useCreateEdge';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||
import React from 'react';
|
||||
import { useUpdateStep } from '@/workflow/workflow-steps/hooks/useUpdateStep';
|
||||
import {
|
||||
WorkflowDiagramEdge,
|
||||
WorkflowDiagramNode,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger';
|
||||
import { workflowDiagramRightClickMenuPositionState } from '@/workflow/workflow-diagram/states/workflowDiagramRightClickMenuPositionState';
|
||||
import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge';
|
||||
|
||||
export const WorkflowDiagramCanvasEditable = ({
|
||||
versionStatus,
|
||||
workflowWithCurrentVersion,
|
||||
}: {
|
||||
versionStatus: WorkflowVersionStatus;
|
||||
workflowWithCurrentVersion: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const tagProps = getWorkflowVersionStatusTagProps({
|
||||
workflowVersionStatus: versionStatus,
|
||||
workflowVersionStatus: workflowWithCurrentVersion.currentVersion.status,
|
||||
});
|
||||
|
||||
const setWorkflowDiagram = useSetRecoilComponentStateV2(
|
||||
workflowDiagramComponentState,
|
||||
);
|
||||
|
||||
const setWorkflowDiagramRightClickMenuPosition = useSetRecoilComponentStateV2(
|
||||
workflowDiagramRightClickMenuPositionState,
|
||||
);
|
||||
|
||||
const { createEdge } = useCreateEdge({
|
||||
workflow: workflowWithCurrentVersion,
|
||||
});
|
||||
|
||||
const { deleteEdge } = useDeleteEdge({
|
||||
workflow: workflowWithCurrentVersion,
|
||||
});
|
||||
|
||||
const { updateStep } = useUpdateStep({
|
||||
workflow: workflowWithCurrentVersion,
|
||||
});
|
||||
|
||||
const { updateTrigger } = useUpdateWorkflowVersionTrigger({
|
||||
workflow: workflowWithCurrentVersion,
|
||||
});
|
||||
|
||||
const onConnect = (edgeConnect: Connection) => {
|
||||
setWorkflowDiagram((diagram) => {
|
||||
if (isDefined(diagram) === false) {
|
||||
throw new Error(
|
||||
'It must be impossible for the edges to be updated if the diagram is not defined yet. Be sure the diagram is rendered only when defined.',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...diagram,
|
||||
edges: addEdge(edgeConnect, diagram.edges),
|
||||
};
|
||||
});
|
||||
createEdge?.(edgeConnect);
|
||||
};
|
||||
|
||||
const onDeleteEdge = async (edge: WorkflowDiagramEdge) => {
|
||||
await deleteEdge(edge);
|
||||
};
|
||||
|
||||
const onNodeDragStop = async (
|
||||
_: React.MouseEvent<Element>,
|
||||
node: WorkflowDiagramNode,
|
||||
) => {
|
||||
const stepToUpdate =
|
||||
workflowWithCurrentVersion?.currentVersion?.steps?.find(
|
||||
(step) => step.id === node.id,
|
||||
);
|
||||
|
||||
if (isDefined(stepToUpdate)) {
|
||||
await updateStep({
|
||||
...stepToUpdate,
|
||||
position: node.position,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const triggerToUpdate = workflowWithCurrentVersion?.currentVersion?.trigger;
|
||||
|
||||
if (isDefined(triggerToUpdate)) {
|
||||
await updateTrigger({
|
||||
...triggerToUpdate,
|
||||
position: node.position,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaneContextMenu = ({ x, y }: { x: number; y: number }) => {
|
||||
setWorkflowDiagramRightClickMenuPosition({
|
||||
x,
|
||||
y,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<WorkflowDiagramCanvasBase
|
||||
nodeTypes={{
|
||||
default: WorkflowDiagramStepNodeEditable,
|
||||
'create-step': WorkflowDiagramCreateStepNode,
|
||||
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
||||
}}
|
||||
edgeTypes={{
|
||||
@@ -38,6 +130,11 @@ export const WorkflowDiagramCanvasEditable = ({
|
||||
tagContainerTestId="workflow-visualizer-status"
|
||||
tagColor={tagProps.color}
|
||||
tagText={tagProps.text}
|
||||
onConnect={onConnect}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
handlePaneContextMenu={handlePaneContextMenu}
|
||||
nodesConnectable
|
||||
onDeleteEdge={onDeleteEdge}
|
||||
/>
|
||||
|
||||
<WorkflowDiagramCanvasEditableEffect />
|
||||
|
||||
-13
@@ -9,7 +9,6 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { EMPTY_TRIGGER_STEP_ID } from '@/workflow/workflow-diagram/constants/EmptyTriggerStepId';
|
||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||
import { useTriggerNodeSelection } from '@/workflow/workflow-diagram/hooks/useTriggerNodeSelection';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import {
|
||||
@@ -17,14 +16,12 @@ import {
|
||||
WorkflowDiagramStepNodeData,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
|
||||
import { isCreateStepNode } from '@/workflow/workflow-diagram/utils/isCreateStepNode';
|
||||
import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
export const WorkflowDiagramCanvasEditableEffect = () => {
|
||||
const { getIcon } = useIcons();
|
||||
const { startNodeCreation } = useStartNodeCreation();
|
||||
|
||||
const {
|
||||
openWorkflowTriggerTypeInCommandMenu,
|
||||
@@ -67,15 +64,6 @@ export const WorkflowDiagramCanvasEditableEffect = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCreateStepNode(selectedNode)) {
|
||||
startNodeCreation({
|
||||
parentStepId: selectedNode.data.parentNodeId,
|
||||
nextStepId: undefined,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedNodeData = selectedNode.data as WorkflowDiagramStepNodeData;
|
||||
|
||||
setWorkflowSelectedNode(selectedNode.id);
|
||||
@@ -95,7 +83,6 @@ export const WorkflowDiagramCanvasEditableEffect = () => {
|
||||
setCommandMenuNavigationStack,
|
||||
workflowVisualizerWorkflowId,
|
||||
openWorkflowTriggerTypeInCommandMenu,
|
||||
startNodeCreation,
|
||||
openWorkflowEditStepInCommandMenu,
|
||||
getIcon,
|
||||
setWorkflowSelectedNode,
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
export const WorkflowDiagramConnector = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="2"
|
||||
height="56"
|
||||
viewBox="0 0 2 56"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M1 0V28V56"
|
||||
stroke={theme.border.color.strong}
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { WorkflowDiagramConnector } from '@/workflow/workflow-diagram/components/WorkflowDiagramConnector';
|
||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||
import React from 'react';
|
||||
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { VERTICAL_DISTANCE_BETWEEN_TWO_NODES } from '@/workflow/workflow-diagram/constants/VerticalDistanceBetweenTwoNodes';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledVerticalLineContainer = styled.div`
|
||||
display: flex;
|
||||
height: 56px;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
`;
|
||||
|
||||
type WorkflowDiagramCreateStepElementProps = {
|
||||
data: WorkflowDiagramStepNodeData;
|
||||
};
|
||||
|
||||
export const WorkflowDiagramCreateStepElement = ({
|
||||
data,
|
||||
}: WorkflowDiagramCreateStepElementProps) => {
|
||||
const { startNodeCreation } = useStartNodeCreation();
|
||||
|
||||
const addNode = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
startNodeCreation({
|
||||
parentStepId: data.stepId,
|
||||
nextStepId: undefined,
|
||||
position: {
|
||||
x: data.position.x,
|
||||
y: data.position.y + VERTICAL_DISTANCE_BETWEEN_TWO_NODES,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledVerticalLineContainer>
|
||||
<WorkflowDiagramConnector />
|
||||
</StyledVerticalLineContainer>
|
||||
<IconButton
|
||||
Icon={IconPlus}
|
||||
size="small"
|
||||
ariaLabel="Add a step"
|
||||
onClick={addNode}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import { WorkflowDiagramBaseHandle } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseHandle';
|
||||
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||
import { WORKFLOW_DIAGRAM_CREATE_STEP_NODE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramCreateStepNodeClickOutsideId';
|
||||
import styled from '@emotion/styled';
|
||||
import { Position } from '@xyflow/react';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
left: ${CREATE_STEP_NODE_WIDTH / 2}px;
|
||||
padding-top: ${({ theme }) => theme.spacing(3)};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramCreateStepNode = () => {
|
||||
return (
|
||||
<StyledContainer
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_CREATE_STEP_NODE_CLICK_OUTSIDE_ID}
|
||||
>
|
||||
<WorkflowDiagramBaseHandle type="target" position={Position.Top} />
|
||||
|
||||
<IconButton Icon={IconPlus} size="medium" ariaLabel="Add a step" />
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+6
-5
@@ -3,7 +3,6 @@ import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithC
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
|
||||
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
|
||||
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||
import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu';
|
||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||
@@ -16,7 +15,7 @@ import {
|
||||
BaseEdge,
|
||||
EdgeLabelRenderer,
|
||||
EdgeProps,
|
||||
getStraightPath,
|
||||
getBezierPath,
|
||||
} from '@xyflow/react';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -32,17 +31,19 @@ const StyledIconButtonGroup = styled(IconButtonGroup)`
|
||||
export const WorkflowDiagramDefaultEdgeEditable = ({
|
||||
source,
|
||||
target,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
}: WorkflowDiagramDefaultEdgeEditableProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [edgePath, labelX, labelY] = getStraightPath({
|
||||
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX: CREATE_STEP_NODE_WIDTH,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
|
||||
+6
-5
@@ -1,22 +1,23 @@
|
||||
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react';
|
||||
import { BaseEdge, EdgeProps, getBezierPath } from '@xyflow/react';
|
||||
|
||||
type WorkflowDiagramDefaultEdgeReadonlyProps = EdgeProps<WorkflowDiagramEdge>;
|
||||
|
||||
export const WorkflowDiagramDefaultEdgeReadonly = ({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
}: WorkflowDiagramDefaultEdgeReadonlyProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [edgePath] = getStraightPath({
|
||||
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||
const [edgePath] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX: CREATE_STEP_NODE_WIDTH,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
|
||||
+6
-5
@@ -1,19 +1,20 @@
|
||||
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge';
|
||||
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { EdgeProps, getStraightPath } from '@xyflow/react';
|
||||
import { EdgeProps, getBezierPath } from '@xyflow/react';
|
||||
|
||||
type WorkflowDiagramDefaultEdgeRunProps = EdgeProps<WorkflowDiagramEdge>;
|
||||
|
||||
export const WorkflowDiagramDefaultEdgeRun = ({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
data,
|
||||
}: WorkflowDiagramDefaultEdgeRunProps) => {
|
||||
const [edgePath] = getStraightPath({
|
||||
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||
const [edgePath] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX: CREATE_STEP_NODE_WIDTH,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
|
||||
+5
-8
@@ -10,7 +10,6 @@ import {
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||
|
||||
import { addCreateStepNodes } from '@/workflow/workflow-diagram/utils/addCreateStepNodes';
|
||||
import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram';
|
||||
import { mergeWorkflowDiagrams } from '@/workflow/workflow-diagram/utils/mergeWorkflowDiagrams';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
@@ -49,13 +48,11 @@ export const WorkflowDiagramEffect = ({
|
||||
workflowDiagramState,
|
||||
);
|
||||
|
||||
const nextWorkflowDiagram = addCreateStepNodes(
|
||||
getWorkflowVersionDiagram({
|
||||
workflowVersion: currentVersion,
|
||||
isWorkflowFilteringEnabled,
|
||||
isEditable: true,
|
||||
}),
|
||||
);
|
||||
const nextWorkflowDiagram = getWorkflowVersionDiagram({
|
||||
workflowVersion: currentVersion,
|
||||
isWorkflowFilteringEnabled,
|
||||
isEditable: true,
|
||||
});
|
||||
|
||||
let mergedWorkflowDiagram = nextWorkflowDiagram;
|
||||
|
||||
|
||||
+11
-16
@@ -11,7 +11,6 @@ import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithC
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
|
||||
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
|
||||
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||
import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu';
|
||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||
@@ -23,7 +22,6 @@ import {
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getWorkflowDiagramNodeSelectedColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramNodeSelectedColors';
|
||||
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
|
||||
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
@@ -31,7 +29,7 @@ import {
|
||||
BaseEdge,
|
||||
EdgeLabelRenderer,
|
||||
EdgeProps,
|
||||
getStraightPath,
|
||||
getBezierPath,
|
||||
} from '@xyflow/react';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -78,7 +76,9 @@ export const WorkflowDiagramFilterEdgeEditable = ({
|
||||
source,
|
||||
target,
|
||||
sourceY,
|
||||
sourceX,
|
||||
targetY,
|
||||
targetX,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
data,
|
||||
@@ -87,10 +87,10 @@ export const WorkflowDiagramFilterEdgeEditable = ({
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const [edgePath, labelX, labelY] = getStraightPath({
|
||||
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX: CREATE_STEP_NODE_WIDTH,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
@@ -100,7 +100,7 @@ export const WorkflowDiagramFilterEdgeEditable = ({
|
||||
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
|
||||
|
||||
const { deleteStep } = useDeleteStep({ workflow });
|
||||
const { startNodeCreation } = useStartNodeCreation();
|
||||
const { startNodeCreation, isNodeCreationStarted } = useStartNodeCreation();
|
||||
|
||||
const { openDropdown } = useOpenDropdown();
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
@@ -111,15 +111,10 @@ export const WorkflowDiagramFilterEdgeEditable = ({
|
||||
workflowDiagramPanOnDragComponentState,
|
||||
);
|
||||
|
||||
const workflowInsertStepIds = useRecoilComponentValueV2(
|
||||
workflowInsertStepIdsComponentState,
|
||||
);
|
||||
|
||||
const isEdgeSelected =
|
||||
(workflowInsertStepIds.nextStepId === source &&
|
||||
workflowInsertStepIds.parentStepId === target) ||
|
||||
(isNonEmptyString(data.stepId) &&
|
||||
workflowInsertStepIds.parentStepId === data.stepId);
|
||||
const isEdgeSelected = isNodeCreationStarted({
|
||||
parentStepId: data.stepId,
|
||||
nextStepId: target,
|
||||
});
|
||||
|
||||
const workflowSelectedNode = useRecoilComponentValueV2(
|
||||
workflowSelectedNodeComponentState,
|
||||
|
||||
+6
-5
@@ -1,7 +1,6 @@
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
|
||||
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
|
||||
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||
import { useOpenWorkflowViewFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowViewFilterInCommandMenu';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
@@ -17,7 +16,7 @@ import {
|
||||
BaseEdge,
|
||||
EdgeLabelRenderer,
|
||||
EdgeProps,
|
||||
getStraightPath,
|
||||
getBezierPath,
|
||||
} from '@xyflow/react';
|
||||
import { IconFilter } from 'twenty-ui/display';
|
||||
import { IconButtonGroup } from 'twenty-ui/input';
|
||||
@@ -53,7 +52,9 @@ const StyledConfiguredFilterContainer = styled.div`
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramFilterEdgeReadonly = ({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
@@ -63,10 +64,10 @@ export const WorkflowDiagramFilterEdgeReadonly = ({
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const [edgePath, labelX, labelY] = getStraightPath({
|
||||
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX: CREATE_STEP_NODE_WIDTH,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
|
||||
+6
-5
@@ -2,7 +2,6 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
|
||||
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
|
||||
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
|
||||
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge';
|
||||
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||
import { useOpenWorkflowRunFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowRunFilterInCommandMenu';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
@@ -16,7 +15,7 @@ import { getWorkflowDiagramNodeSelectedColors } from '@/workflow/workflow-diagra
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { EdgeLabelRenderer, EdgeProps, getStraightPath } from '@xyflow/react';
|
||||
import { EdgeLabelRenderer, EdgeProps, getBezierPath } from '@xyflow/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconFilter } from 'twenty-ui/display';
|
||||
import { IconButtonGroup } from 'twenty-ui/input';
|
||||
@@ -55,16 +54,18 @@ const StyledConfiguredFilterContainer = styled.div`
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramFilterEdgeRun = ({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
data,
|
||||
}: WorkflowDiagramFilterEdgeRunProps) => {
|
||||
assertFilterEdgeDataOrThrow(data);
|
||||
|
||||
const [edgePath, labelX, labelY] = getStraightPath({
|
||||
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX: CREATE_STEP_NODE_WIDTH,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
|
||||
+42
-57
@@ -1,45 +1,24 @@
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
BaseEdge,
|
||||
EdgeLabelRenderer,
|
||||
EdgeProps,
|
||||
getStraightPath,
|
||||
getBezierPath,
|
||||
} from '@xyflow/react';
|
||||
import { useState } from 'react';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { IconButtonGroup } from 'twenty-ui/input';
|
||||
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2Container';
|
||||
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/components/WorkflowDiagramEdgeV2VisibilityContainer';
|
||||
|
||||
const StyledIconButtonGroup = styled(IconButtonGroup)`
|
||||
pointer-events: all;
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div<{
|
||||
labelY?: number;
|
||||
}>`
|
||||
position: absolute;
|
||||
transform: ${({ labelY }) => `translate(${21}px, ${(labelY || 0) - 14}px)`};
|
||||
`;
|
||||
|
||||
const StyledHoverZone = styled.div`
|
||||
position: absolute;
|
||||
width: 48px;
|
||||
height: 52px;
|
||||
transform: translate(-13px, -16px);
|
||||
background: transparent;
|
||||
`;
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
pointer-events: all;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
type WorkflowDiagramFilteringDisabledEdgeEditableProps =
|
||||
EdgeProps<WorkflowDiagramEdge>;
|
||||
|
||||
@@ -48,29 +27,36 @@ export const WorkflowDiagramFilteringDisabledEdgeEditable = ({
|
||||
markerEnd,
|
||||
source,
|
||||
sourceY,
|
||||
sourceX,
|
||||
target,
|
||||
targetX,
|
||||
targetY,
|
||||
}: WorkflowDiagramFilteringDisabledEdgeEditableProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [edgePath, , labelY] = getStraightPath({
|
||||
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX: CREATE_STEP_NODE_WIDTH,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const { startNodeCreation } = useStartNodeCreation();
|
||||
const handleMouseEnter = () => {
|
||||
setHovered(true);
|
||||
};
|
||||
|
||||
const workflowInsertStepIds = useRecoilComponentValueV2(
|
||||
workflowInsertStepIdsComponentState,
|
||||
);
|
||||
const handleMouseLeave = () => {
|
||||
setHovered(false);
|
||||
};
|
||||
|
||||
const isSelected =
|
||||
workflowInsertStepIds.parentStepId === source &&
|
||||
workflowInsertStepIds.nextStepId === target;
|
||||
const { startNodeCreation, isNodeCreationStarted } = useStartNodeCreation();
|
||||
|
||||
const forceDisplayAddButton = isNodeCreationStarted({
|
||||
parentStepId: source,
|
||||
nextStepId: target,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -82,33 +68,32 @@ export const WorkflowDiagramFilteringDisabledEdgeEditable = ({
|
||||
/>
|
||||
|
||||
<EdgeLabelRenderer>
|
||||
<StyledContainer
|
||||
labelY={labelY}
|
||||
<WorkflowDiagramEdgeV2Container
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||
labelX={labelX}
|
||||
labelY={labelY}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<StyledWrapper
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
<WorkflowDiagramEdgeV2VisibilityContainer
|
||||
shouldDisplay={hovered || forceDisplayAddButton}
|
||||
>
|
||||
<StyledHoverZone />
|
||||
{(hovered || isSelected) && (
|
||||
<StyledIconButtonGroup
|
||||
className="nodrag nopan"
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconPlus,
|
||||
onClick: () => {
|
||||
startNodeCreation({
|
||||
parentStepId: source,
|
||||
nextStepId: target,
|
||||
});
|
||||
},
|
||||
<StyledIconButtonGroup
|
||||
className="nodrag nopan"
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconPlus,
|
||||
onClick: () => {
|
||||
startNodeCreation({
|
||||
parentStepId: source,
|
||||
nextStepId: target,
|
||||
});
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
</StyledContainer>
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</WorkflowDiagramEdgeV2VisibilityContainer>
|
||||
</WorkflowDiagramEdgeV2Container>
|
||||
</EdgeLabelRenderer>
|
||||
</>
|
||||
);
|
||||
|
||||
+6
-5
@@ -1,7 +1,6 @@
|
||||
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { BaseEdge, EdgeProps, getStraightPath } from '@xyflow/react';
|
||||
import { BaseEdge, EdgeProps, getBezierPath } from '@xyflow/react';
|
||||
|
||||
type WorkflowDiagramFilteringDisabledEdgeReadonlyProps =
|
||||
EdgeProps<WorkflowDiagramEdge>;
|
||||
@@ -9,15 +8,17 @@ type WorkflowDiagramFilteringDisabledEdgeReadonlyProps =
|
||||
export const WorkflowDiagramFilteringDisabledEdgeReadonly = ({
|
||||
markerStart,
|
||||
markerEnd,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
}: WorkflowDiagramFilteringDisabledEdgeReadonlyProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [edgePath] = getStraightPath({
|
||||
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||
const [edgePath] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX: CREATE_STEP_NODE_WIDTH,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
|
||||
+6
-5
@@ -1,20 +1,21 @@
|
||||
import { WorkflowRunDiagramBaseEdge } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramBaseEdge';
|
||||
import { CREATE_STEP_NODE_WIDTH } from '@/workflow/workflow-diagram/constants/CreateStepNodeWidth';
|
||||
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { EdgeProps, getStraightPath } from '@xyflow/react';
|
||||
import { EdgeProps, getBezierPath } from '@xyflow/react';
|
||||
|
||||
type WorkflowDiagramFilteringDisabledEdgeRunProps =
|
||||
EdgeProps<WorkflowDiagramEdge>;
|
||||
|
||||
export const WorkflowDiagramFilteringDisabledEdgeRun = ({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
data,
|
||||
}: WorkflowDiagramFilteringDisabledEdgeRunProps) => {
|
||||
const [edgePath] = getStraightPath({
|
||||
sourceX: CREATE_STEP_NODE_WIDTH,
|
||||
const [edgePath] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX: CREATE_STEP_NODE_WIDTH,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { workflowDiagramRightClickMenuPositionState } from '@/workflow/workflow-diagram/states/workflowDiagramRightClickMenuPositionState';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useRef } from 'react';
|
||||
import { WorkflowDiagramRightClickCommandMenuClickOutsideEffect } from './WorkflowDiagramRightClickCommandMenuClickOutsideEffect';
|
||||
import { IconPlus, IconReorder } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||
import { useCloseRightClickMenu } from '@/workflow/workflow-diagram/hooks/useCloseRightClickMenu';
|
||||
import { useTidyUpWorkflowVersion } from '@/workflow/workflow-version/hooks/useTidyUpWorkflowVersion';
|
||||
|
||||
const StyledContainer = styled.div<{ x: number; y: number }>`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||
border-radius: ${({ theme }) => theme.spacing(2)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(0.5)};
|
||||
left: ${({ x }) => `${x}px`};
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
position: absolute;
|
||||
top: ${({ y }) => `${y}px`};
|
||||
width: 200px;
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramRightClickCommandMenu = () => {
|
||||
const { t } = useLingui();
|
||||
const rightClickCommandMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { startNodeCreation } = useStartNodeCreation();
|
||||
|
||||
const { closeRightClickMenu } = useCloseRightClickMenu();
|
||||
|
||||
const workflowDiagramRightClickMenuPosition = useRecoilComponentValueV2(
|
||||
workflowDiagramRightClickMenuPositionState,
|
||||
);
|
||||
|
||||
const workflowVisualizerWorkflowId = useRecoilComponentValueV2(
|
||||
workflowVisualizerWorkflowIdComponentState,
|
||||
);
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
|
||||
workflowVisualizerWorkflowId,
|
||||
);
|
||||
|
||||
const { tidyUpWorkflowVersion } = useTidyUpWorkflowVersion({
|
||||
workflow: workflowWithCurrentVersion,
|
||||
});
|
||||
|
||||
const handleReorderWorkflowDiagram = async () => {
|
||||
await tidyUpWorkflowVersion();
|
||||
closeRightClickMenu();
|
||||
};
|
||||
|
||||
const addNode = () => {
|
||||
startNodeCreation({
|
||||
parentStepId: undefined,
|
||||
nextStepId: undefined,
|
||||
position: workflowDiagramRightClickMenuPosition,
|
||||
});
|
||||
};
|
||||
|
||||
if (!isDefined(workflowDiagramRightClickMenuPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContainer
|
||||
ref={rightClickCommandMenuRef}
|
||||
x={workflowDiagramRightClickMenuPosition.x}
|
||||
y={workflowDiagramRightClickMenuPosition.y}
|
||||
>
|
||||
<MenuItem text={t`Add node`} LeftIcon={IconPlus} onClick={addNode} />
|
||||
<MenuItem
|
||||
text={t`Tidy up workflow`}
|
||||
LeftIcon={IconReorder}
|
||||
onClick={handleReorderWorkflowDiagram}
|
||||
/>
|
||||
</StyledContainer>
|
||||
<WorkflowDiagramRightClickCommandMenuClickOutsideEffect
|
||||
rightClickCommandMenuRef={rightClickCommandMenuRef}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { WORKFLOW_DIAGRAM_RIGHT_CLICK_MENU_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramRightClickMenuClickOutsideId';
|
||||
import { workflowDiagramRightClickMenuPositionState } from '@/workflow/workflow-diagram/states/workflowDiagramRightClickMenuPositionState';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { COMMAND_MENU_CLICK_OUTSIDE_ID } from '@/command-menu/constants/CommandMenuClickOutsideId';
|
||||
|
||||
export const WorkflowDiagramRightClickCommandMenuClickOutsideEffect = ({
|
||||
rightClickCommandMenuRef,
|
||||
}: {
|
||||
rightClickCommandMenuRef: React.RefObject<HTMLDivElement>;
|
||||
}) => {
|
||||
const setWorkflowDiagramRightClickMenuPosition = useSetRecoilComponentStateV2(
|
||||
workflowDiagramRightClickMenuPositionState,
|
||||
);
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [rightClickCommandMenuRef],
|
||||
excludedClickOutsideIds: [COMMAND_MENU_CLICK_OUTSIDE_ID],
|
||||
callback: (event) => {
|
||||
event.stopImmediatePropagation();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
setWorkflowDiagramRightClickMenuPosition(undefined);
|
||||
},
|
||||
listenerId: WORKFLOW_DIAGRAM_RIGHT_CLICK_MENU_CLICK_OUTSIDE_ID,
|
||||
});
|
||||
|
||||
return <></>;
|
||||
};
|
||||
+30
-26
@@ -1,4 +1,3 @@
|
||||
import { WorkflowDiagramBaseHandle } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseHandle';
|
||||
import { NODE_BORDER_WIDTH } from '@/workflow/workflow-diagram/constants/NodeBorderWidth';
|
||||
import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId';
|
||||
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
@@ -6,17 +5,16 @@ import { WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/Wo
|
||||
import { getWorkflowDiagramNodeSelectedColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramNodeSelectedColors';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Position } from '@xyflow/react';
|
||||
import React from 'react';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import React, { MouseEvent } from 'react';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { Label, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { Loader } from 'twenty-ui/feedback';
|
||||
import { WorkflowDiagramBaseHandle } from '@/workflow/workflow-diagram/components/WorkflowDiagramBaseHandle';
|
||||
import { Position } from '@xyflow/react';
|
||||
|
||||
const StyledStepNodeContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding-block: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledStepNodeType = styled.div<{
|
||||
@@ -142,37 +140,41 @@ const StyledStepNodeLabel = styled.div<{
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRightFloatingElementContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: ${({ theme }) => theme.spacing(-4)};
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
transform: translateX(100%);
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramStepNodeBase = ({
|
||||
nodeType,
|
||||
name,
|
||||
variant,
|
||||
Icon,
|
||||
RightFloatingElement,
|
||||
BottomHoverFloatingElement,
|
||||
displayHandle = true,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}: {
|
||||
nodeType: WorkflowDiagramStepNodeData['nodeType'];
|
||||
name: string;
|
||||
variant: WorkflowDiagramNodeVariant;
|
||||
Icon?: React.ReactNode;
|
||||
RightFloatingElement?: React.ReactNode;
|
||||
BottomHoverFloatingElement?: React.ReactNode;
|
||||
displayHandle?: boolean;
|
||||
onMouseEnter?: (event: MouseEvent<HTMLDivElement>) => void;
|
||||
onMouseLeave?: (event: MouseEvent<HTMLDivElement>) => void;
|
||||
}) => {
|
||||
return (
|
||||
<StyledStepNodeContainer
|
||||
className="workflow-node-container"
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{nodeType !== 'trigger' ? (
|
||||
<WorkflowDiagramBaseHandle type="target" position={Position.Top} />
|
||||
) : null}
|
||||
{nodeType !== 'trigger' && (
|
||||
<WorkflowDiagramBaseHandle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
isVisible={displayHandle}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StyledStepNodeType variant="small" nodeVariant={variant}>
|
||||
{capitalize(nodeType)}
|
||||
@@ -182,17 +184,19 @@ export const WorkflowDiagramStepNodeBase = ({
|
||||
<StyledStepNodeLabel variant={variant}>
|
||||
{variant === 'running' ? <Loader /> : Icon}
|
||||
|
||||
<OverflowingTextWithTooltip text={name} />
|
||||
<OverflowingTextWithTooltip text={name} hideTooltip={true} />
|
||||
</StyledStepNodeLabel>
|
||||
|
||||
{isDefined(RightFloatingElement) ? (
|
||||
<StyledRightFloatingElementContainer>
|
||||
{RightFloatingElement}
|
||||
</StyledRightFloatingElementContainer>
|
||||
) : null}
|
||||
{RightFloatingElement}
|
||||
|
||||
{BottomHoverFloatingElement}
|
||||
</StyledStepNodeInnerContainer>
|
||||
|
||||
<WorkflowDiagramBaseHandle type="source" position={Position.Bottom} />
|
||||
<WorkflowDiagramBaseHandle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
isVisible={displayHandle}
|
||||
/>
|
||||
</StyledStepNodeContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+64
-7
@@ -4,6 +4,34 @@ import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/W
|
||||
import { WorkflowDiagramNodeVariant } from '@/workflow/workflow-diagram/types/WorkflowDiagramNodeVariant';
|
||||
import { FloatingIconButton } from 'twenty-ui/input';
|
||||
import { IconTrash } from 'twenty-ui/display';
|
||||
import { WorkflowDiagramCreateStepElement } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepElement';
|
||||
import React, { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||
|
||||
const StyledDeleteButtonContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: ${({ theme }) => theme.spacing(-4)};
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
transform: translateX(100%);
|
||||
`;
|
||||
|
||||
const StyledAddStepButtonContainer = styled.div<{
|
||||
shouldDisplay: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
opacity: ${({ shouldDisplay }) => (shouldDisplay ? 1 : 0)};
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translateX(-50%) translateY(100%);
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramStepNodeEditableContent = ({
|
||||
data,
|
||||
@@ -16,20 +44,49 @@ export const WorkflowDiagramStepNodeEditableContent = ({
|
||||
selected: boolean;
|
||||
onDelete: () => void;
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleMouseEnter = () => setIsHovered(true);
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
const { isNodeCreationStarted } = useStartNodeCreation();
|
||||
|
||||
return (
|
||||
<WorkflowDiagramStepNodeBase
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
name={data.name}
|
||||
variant={variant}
|
||||
nodeType={data.nodeType}
|
||||
Icon={<WorkflowDiagramStepNodeIcon data={data} />}
|
||||
RightFloatingElement={
|
||||
selected ? (
|
||||
<FloatingIconButton
|
||||
size="medium"
|
||||
Icon={IconTrash}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
) : undefined
|
||||
selected && (
|
||||
<StyledDeleteButtonContainer>
|
||||
<FloatingIconButton
|
||||
size="medium"
|
||||
Icon={IconTrash}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
</StyledDeleteButtonContainer>
|
||||
)
|
||||
}
|
||||
BottomHoverFloatingElement={
|
||||
!data.hasNextStepIds && (
|
||||
<StyledAddStepButtonContainer
|
||||
shouldDisplay={
|
||||
isHovered ||
|
||||
selected ||
|
||||
isNodeCreationStarted({ parentStepId: data.stepId })
|
||||
}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<WorkflowDiagramCreateStepElement data={data} />
|
||||
</StyledAddStepButtonContainer>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
+2
@@ -2,6 +2,7 @@ import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/compone
|
||||
import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon';
|
||||
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getNodeVariantFromStepRunStatus } from '@/workflow/workflow-diagram/utils/getNodeVariantFromStepRunStatus';
|
||||
import React from 'react';
|
||||
|
||||
export const WorkflowDiagramStepNodeReadonly = ({
|
||||
data,
|
||||
@@ -14,6 +15,7 @@ export const WorkflowDiagramStepNodeReadonly = ({
|
||||
variant={getNodeVariantFromStepRunStatus(data.runStatus)}
|
||||
nodeType={data.nodeType}
|
||||
Icon={<WorkflowDiagramStepNodeIcon data={data} />}
|
||||
displayHandle={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ export const WorkflowVisualizer = ({ workflowId }: { workflowId: string }) => {
|
||||
workflowWithCurrentVersion={workflowWithCurrentVersion}
|
||||
/>
|
||||
<WorkflowDiagramCanvasEditable
|
||||
versionStatus={workflowWithCurrentVersion.currentVersion.status}
|
||||
workflowWithCurrentVersion={workflowWithCurrentVersion}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ReactFlowProvider } from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { WorkflowDiagramCreateStepNode } from '../WorkflowDiagramCreateStepNode';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
|
||||
const meta: Meta<typeof WorkflowDiagramCreateStepNode> = {
|
||||
title: 'Modules/Workflow/WorkflowDiagramCreateStepNode',
|
||||
component: WorkflowDiagramCreateStepNode,
|
||||
decorators: [
|
||||
ComponentDecorator,
|
||||
(Story) => (
|
||||
<ReactFlowProvider>
|
||||
<Story />
|
||||
</ReactFlowProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkflowDiagramCreateStepNode>;
|
||||
|
||||
export const Default: Story = {
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const Selected: Story = {
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="selectable selected" style={{ position: 'relative' }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
+57
-1
@@ -9,6 +9,8 @@ import { CatalogDecorator, CatalogStory } from 'twenty-ui/testing';
|
||||
import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { WorkflowDiagramStepNodeEditableContent } from '../WorkflowDiagramStepNodeEditableContent';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
||||
|
||||
type ComponentState = 'default' | 'hover' | 'selected';
|
||||
|
||||
@@ -37,41 +39,89 @@ const ALL_STEPS = [
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
name: 'Record is Created',
|
||||
hasNextStepIds: true,
|
||||
stepId: 'trigger',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'MANUAL',
|
||||
name: 'Manual',
|
||||
hasNextStepIds: true,
|
||||
stepId: 'step1',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 150,
|
||||
},
|
||||
},
|
||||
{
|
||||
nodeType: 'action',
|
||||
actionType: 'CREATE_RECORD',
|
||||
name: 'Create Record',
|
||||
hasNextStepIds: true,
|
||||
stepId: 'step2',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 300,
|
||||
},
|
||||
},
|
||||
{
|
||||
nodeType: 'action',
|
||||
actionType: 'UPDATE_RECORD',
|
||||
name: 'Update Record',
|
||||
hasNextStepIds: true,
|
||||
stepId: 'step3',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 450,
|
||||
},
|
||||
},
|
||||
{
|
||||
nodeType: 'action',
|
||||
actionType: 'DELETE_RECORD',
|
||||
name: 'Delete Record',
|
||||
hasNextStepIds: true,
|
||||
stepId: 'step4',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 600,
|
||||
},
|
||||
},
|
||||
{
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Send Email',
|
||||
hasNextStepIds: true,
|
||||
stepId: 'step5',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 750,
|
||||
},
|
||||
},
|
||||
{
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: 'Code',
|
||||
hasNextStepIds: true,
|
||||
stepId: 'step6',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 900,
|
||||
},
|
||||
},
|
||||
{
|
||||
nodeType: 'action',
|
||||
actionType: 'HTTP_REQUEST',
|
||||
name: 'HTTP Request',
|
||||
hasNextStepIds: true,
|
||||
stepId: 'step7',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 1050,
|
||||
},
|
||||
},
|
||||
] satisfies WorkflowDiagramStepNodeData[];
|
||||
|
||||
@@ -120,7 +170,13 @@ export const Catalog: CatalogStory<Story, typeof Wrapper> = {
|
||||
<div
|
||||
className={`selectable ${args.state === 'selected' ? 'selected' : args.state === 'hover' ? 'workflow-node-container hover' : ''}`}
|
||||
>
|
||||
<Story />
|
||||
<RecoilRoot>
|
||||
<WorkflowVisualizerComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'workflow-visualizer-instance-id' }}
|
||||
>
|
||||
<Story />
|
||||
</WorkflowVisualizerComponentInstanceContext.Provider>
|
||||
</RecoilRoot>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const CREATE_STEP_STEP_ID = 'create-step';
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const NODE_HANDLE_WIDTH_PX = 4;
|
||||
export const NODE_HANDLE_WIDTH_PX = 7;
|
||||
|
||||
+4
@@ -7,6 +7,10 @@ export const WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION = {
|
||||
type: 'empty-trigger',
|
||||
data: {
|
||||
nodeType: 'empty-trigger',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
} satisfies WorkflowDiagramEmptyTriggerNodeData,
|
||||
position: {
|
||||
x: 0,
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const WORKFLOW_DIAGRAM_RIGHT_CLICK_MENU_CLICK_OUTSIDE_ID =
|
||||
'workflow-diagram-right-click-menu-click-outside';
|
||||
+2
@@ -64,6 +64,8 @@ describe('useTriggerNodeSelection', () => {
|
||||
});
|
||||
|
||||
expect(result.current.workflowDiagramTriggerNodeSelection).toBeUndefined();
|
||||
|
||||
expect(mockSetNodes).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not trigger update if state is not defined', () => {
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { workflowDiagramRightClickMenuPositionState } from '@/workflow/workflow-diagram/states/workflowDiagramRightClickMenuPositionState';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
|
||||
export const useCloseRightClickMenu = () => {
|
||||
const setWorkflowDiagramRightClickMenuPosition = useSetRecoilComponentStateV2(
|
||||
workflowDiagramRightClickMenuPositionState,
|
||||
);
|
||||
|
||||
const closeRightClickMenu = () => {
|
||||
setWorkflowDiagramRightClickMenuPosition(undefined);
|
||||
};
|
||||
|
||||
return { closeRightClickMenu };
|
||||
};
|
||||
+20
-5
@@ -2,15 +2,14 @@ import { useCallback } from 'react';
|
||||
|
||||
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
|
||||
export const useStartNodeCreation = () => {
|
||||
const setWorkflowInsertStepIds = useSetRecoilComponentStateV2(
|
||||
workflowInsertStepIdsComponentState,
|
||||
);
|
||||
const [workflowInsertStepIds, setWorkflowInsertStepIds] =
|
||||
useRecoilComponentStateV2(workflowInsertStepIdsComponentState);
|
||||
|
||||
const { openStepSelectInCommandMenu } = useWorkflowCommandMenu();
|
||||
|
||||
@@ -26,11 +25,13 @@ export const useStartNodeCreation = () => {
|
||||
({
|
||||
parentStepId,
|
||||
nextStepId,
|
||||
position,
|
||||
}: {
|
||||
parentStepId: string | undefined;
|
||||
nextStepId: string | undefined;
|
||||
position?: { x: number; y: number };
|
||||
}) => {
|
||||
setWorkflowInsertStepIds({ parentStepId, nextStepId });
|
||||
setWorkflowInsertStepIds({ parentStepId, nextStepId, position });
|
||||
|
||||
if (isDefined(workflowVisualizerWorkflowId)) {
|
||||
openStepSelectInCommandMenu(workflowVisualizerWorkflowId);
|
||||
@@ -44,7 +45,21 @@ export const useStartNodeCreation = () => {
|
||||
],
|
||||
);
|
||||
|
||||
const isNodeCreationStarted = ({
|
||||
parentStepId,
|
||||
nextStepId,
|
||||
}: {
|
||||
parentStepId?: string;
|
||||
nextStepId?: string;
|
||||
}) => {
|
||||
return (
|
||||
workflowInsertStepIds.parentStepId === parentStepId &&
|
||||
workflowInsertStepIds.nextStepId === nextStepId
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
startNodeCreation,
|
||||
isNodeCreationStarted,
|
||||
};
|
||||
};
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
||||
|
||||
type WorkflowDiagramRightClickMenuPositionState =
|
||||
| {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
export const workflowDiagramRightClickMenuPositionState =
|
||||
createComponentStateV2<WorkflowDiagramRightClickMenuPositionState>({
|
||||
key: 'workflowDiagramRightClickMenuPositionState',
|
||||
defaultValue: undefined,
|
||||
componentInstanceContext: WorkflowVisualizerComponentInstanceContext,
|
||||
});
|
||||
+17
-10
@@ -30,12 +30,24 @@ export type WorkflowDiagramStepNodeData =
|
||||
name: string;
|
||||
icon?: string;
|
||||
runStatus?: WorkflowRunStepStatus;
|
||||
hasNextStepIds: boolean;
|
||||
stepId: string;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
nodeType: 'action';
|
||||
actionType: WorkflowActionType;
|
||||
name: string;
|
||||
runStatus?: WorkflowRunStepStatus;
|
||||
hasNextStepIds: boolean;
|
||||
stepId: string;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkflowRunDiagramStepNodeData = Exclude<
|
||||
@@ -45,18 +57,16 @@ export type WorkflowRunDiagramStepNodeData = Exclude<
|
||||
runStatus: WorkflowRunStepStatus;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramCreateStepNodeData = {
|
||||
nodeType: 'create-step';
|
||||
parentNodeId: string;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramEmptyTriggerNodeData = {
|
||||
nodeType: 'empty-trigger';
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkflowDiagramNodeData =
|
||||
| WorkflowDiagramStepNodeData
|
||||
| WorkflowDiagramCreateStepNodeData
|
||||
| WorkflowDiagramEmptyTriggerNodeData;
|
||||
|
||||
export type WorkflowRunDiagramNodeData = Exclude<
|
||||
@@ -82,10 +92,7 @@ export type WorkflowDiagramEdgeData =
|
||||
| WorkflowDiagramFilterEdgeData
|
||||
| WorkflowDiagramDefaultEdgeData;
|
||||
|
||||
export type WorkflowDiagramNodeType =
|
||||
| 'default'
|
||||
| 'empty-trigger'
|
||||
| 'create-step';
|
||||
export type WorkflowDiagramNodeType = 'default' | 'empty-trigger';
|
||||
|
||||
export type WorkflowDiagramEdgeType =
|
||||
| 'blank'
|
||||
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
|
||||
import { addCreateStepNodes } from '../addCreateStepNodes';
|
||||
|
||||
describe('addCreateStepNodes', () => {
|
||||
it("adds a create step node to the end of a single-branch flow and doesn't change the shape of other nodes", () => {
|
||||
const trigger: WorkflowTrigger = {
|
||||
name: 'Company created',
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
const steps: WorkflowStep[] = [
|
||||
{
|
||||
id: 'step1',
|
||||
name: 'Step 1',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step2'],
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
name: 'Step 2',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
settings: {
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
input: {
|
||||
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||
serverlessFunctionVersion: '1',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const diagramInitial = generateWorkflowDiagram({
|
||||
trigger,
|
||||
steps,
|
||||
defaultEdgeType: 'empty-filter--editable',
|
||||
});
|
||||
|
||||
expect(diagramInitial.nodes).toHaveLength(3);
|
||||
expect(diagramInitial.edges).toHaveLength(2);
|
||||
|
||||
const diagramWithCreateStepNodes = addCreateStepNodes(diagramInitial);
|
||||
|
||||
expect(diagramWithCreateStepNodes.nodes).toHaveLength(4);
|
||||
expect(diagramWithCreateStepNodes.edges).toHaveLength(3);
|
||||
|
||||
expect(diagramWithCreateStepNodes.nodes[0].type).toBe(undefined);
|
||||
expect(diagramWithCreateStepNodes.nodes[0].data.nodeType).toBe('trigger');
|
||||
|
||||
expect(diagramWithCreateStepNodes.nodes[1].type).toBe(undefined);
|
||||
expect(diagramWithCreateStepNodes.nodes[1].data.nodeType).toBe('action');
|
||||
|
||||
expect(diagramWithCreateStepNodes.nodes[2].type).toBe(undefined);
|
||||
expect(diagramWithCreateStepNodes.nodes[2].data.nodeType).toBe('action');
|
||||
|
||||
expect(diagramWithCreateStepNodes.nodes[3].type).toBe('create-step');
|
||||
});
|
||||
});
|
||||
+10
@@ -37,6 +37,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step1'],
|
||||
};
|
||||
const steps: WorkflowStep[] = [
|
||||
{
|
||||
@@ -97,6 +98,12 @@ describe('generateWorkflowDiagram', () => {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: step.name,
|
||||
hasNextStepIds: step.id !== 'step2',
|
||||
stepId: step.id,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 150 * (index + 1),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -109,6 +116,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step1'],
|
||||
};
|
||||
const steps: WorkflowStep[] = [
|
||||
{
|
||||
@@ -175,6 +183,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step2'],
|
||||
};
|
||||
const steps: WorkflowStep[] = [
|
||||
{
|
||||
@@ -241,6 +250,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step2', 'step3'],
|
||||
};
|
||||
const steps: WorkflowStep[] = [
|
||||
{
|
||||
|
||||
+125
@@ -17,6 +17,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step1'],
|
||||
};
|
||||
|
||||
const steps: WorkflowStep[] = [
|
||||
@@ -153,10 +154,16 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"hasNextStepIds": true,
|
||||
"icon": "IconPlaylistAdd",
|
||||
"name": "Company created",
|
||||
"nodeType": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"runStatus": "SUCCESS",
|
||||
"stepId": "trigger",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
@@ -168,9 +175,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": true,
|
||||
"name": "Step 1",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 150,
|
||||
},
|
||||
"runStatus": "FAILED",
|
||||
"stepId": "step1",
|
||||
},
|
||||
"id": "step1",
|
||||
"position": {
|
||||
@@ -181,9 +194,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": true,
|
||||
"name": "Step 2",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 300,
|
||||
},
|
||||
"runStatus": "NOT_STARTED",
|
||||
"stepId": "step2",
|
||||
},
|
||||
"id": "step2",
|
||||
"position": {
|
||||
@@ -194,9 +213,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": false,
|
||||
"name": "Step 3",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 450,
|
||||
},
|
||||
"runStatus": "NOT_STARTED",
|
||||
"stepId": "step3",
|
||||
},
|
||||
"id": "step3",
|
||||
"position": {
|
||||
@@ -219,6 +244,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step1'],
|
||||
};
|
||||
|
||||
const steps: WorkflowStep[] = [
|
||||
@@ -357,10 +383,16 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"hasNextStepIds": true,
|
||||
"icon": "IconPlaylistAdd",
|
||||
"name": "Company created",
|
||||
"nodeType": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"runStatus": "SUCCESS",
|
||||
"stepId": "trigger",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
@@ -372,9 +404,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": true,
|
||||
"name": "Step 1",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 150,
|
||||
},
|
||||
"runStatus": "SUCCESS",
|
||||
"stepId": "step1",
|
||||
},
|
||||
"id": "step1",
|
||||
"position": {
|
||||
@@ -385,9 +423,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": true,
|
||||
"name": "Step 2",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 300,
|
||||
},
|
||||
"runStatus": "SUCCESS",
|
||||
"stepId": "step2",
|
||||
},
|
||||
"id": "step2",
|
||||
"position": {
|
||||
@@ -398,9 +442,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": false,
|
||||
"name": "Step 3",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 450,
|
||||
},
|
||||
"runStatus": "SUCCESS",
|
||||
"stepId": "step3",
|
||||
},
|
||||
"id": "step3",
|
||||
"position": {
|
||||
@@ -423,6 +473,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step1'],
|
||||
};
|
||||
|
||||
const steps: WorkflowStep[] = [
|
||||
@@ -561,10 +612,16 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"hasNextStepIds": true,
|
||||
"icon": "IconPlaylistAdd",
|
||||
"name": "Company created",
|
||||
"nodeType": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"runStatus": "SUCCESS",
|
||||
"stepId": "trigger",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
@@ -576,9 +633,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": true,
|
||||
"name": "Step 1",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 150,
|
||||
},
|
||||
"runStatus": "RUNNING",
|
||||
"stepId": "step1",
|
||||
},
|
||||
"id": "step1",
|
||||
"position": {
|
||||
@@ -589,9 +652,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": true,
|
||||
"name": "Step 2",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 300,
|
||||
},
|
||||
"runStatus": "NOT_STARTED",
|
||||
"stepId": "step2",
|
||||
},
|
||||
"id": "step2",
|
||||
"position": {
|
||||
@@ -602,9 +671,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": false,
|
||||
"name": "Step 3",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 450,
|
||||
},
|
||||
"runStatus": "NOT_STARTED",
|
||||
"stepId": "step3",
|
||||
},
|
||||
"id": "step3",
|
||||
"position": {
|
||||
@@ -627,6 +702,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step1'],
|
||||
};
|
||||
|
||||
const steps: WorkflowStep[] = [
|
||||
@@ -798,10 +874,16 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"hasNextStepIds": true,
|
||||
"icon": "IconPlaylistAdd",
|
||||
"name": "Company created",
|
||||
"nodeType": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"runStatus": "SUCCESS",
|
||||
"stepId": "trigger",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
@@ -813,9 +895,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": true,
|
||||
"name": "Step 1",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 150,
|
||||
},
|
||||
"runStatus": "SUCCESS",
|
||||
"stepId": "step1",
|
||||
},
|
||||
"id": "step1",
|
||||
"position": {
|
||||
@@ -826,9 +914,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": true,
|
||||
"name": "Step 2",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 300,
|
||||
},
|
||||
"runStatus": "RUNNING",
|
||||
"stepId": "step2",
|
||||
},
|
||||
"id": "step2",
|
||||
"position": {
|
||||
@@ -839,9 +933,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": true,
|
||||
"name": "Step 3",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 450,
|
||||
},
|
||||
"runStatus": "NOT_STARTED",
|
||||
"stepId": "step3",
|
||||
},
|
||||
"id": "step3",
|
||||
"position": {
|
||||
@@ -852,9 +952,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": false,
|
||||
"name": "Step 4",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 600,
|
||||
},
|
||||
"runStatus": "NOT_STARTED",
|
||||
"stepId": "step4",
|
||||
},
|
||||
"id": "step4",
|
||||
"position": {
|
||||
@@ -877,6 +983,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step1'],
|
||||
};
|
||||
const steps: WorkflowStep[] = [
|
||||
{
|
||||
@@ -953,10 +1060,16 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"hasNextStepIds": true,
|
||||
"icon": "IconPlaylistAdd",
|
||||
"name": "Company created",
|
||||
"nodeType": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"runStatus": "SUCCESS",
|
||||
"stepId": "trigger",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
@@ -968,9 +1081,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "FORM",
|
||||
"hasNextStepIds": false,
|
||||
"name": "Step 1",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 150,
|
||||
},
|
||||
"runStatus": "PENDING",
|
||||
"stepId": "step1",
|
||||
},
|
||||
"id": "step1",
|
||||
"position": {
|
||||
@@ -983,9 +1102,15 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
"stepToOpenByDefault": {
|
||||
"data": {
|
||||
"actionType": "FORM",
|
||||
"hasNextStepIds": false,
|
||||
"name": "Step 1",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 150,
|
||||
},
|
||||
"runStatus": "PENDING",
|
||||
"stepId": "step1",
|
||||
},
|
||||
"id": "step1",
|
||||
},
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { getNodeVariantFromStepRunStatus } from '@/workflow/workflow-diagram/utils/getNodeVariantFromStepRunStatus';
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
describe('getNodeVariantFromRunStatus', () => {
|
||||
it('should return proper variant', () => {
|
||||
expect(getNodeVariantFromStepRunStatus(StepStatus.SUCCESS)).toBe('success');
|
||||
expect(getNodeVariantFromStepRunStatus(StepStatus.STOPPED)).toBe('success');
|
||||
expect(getNodeVariantFromStepRunStatus(StepStatus.FAILED)).toBe('failure');
|
||||
expect(getNodeVariantFromStepRunStatus(StepStatus.RUNNING)).toBe('running');
|
||||
expect(getNodeVariantFromStepRunStatus(StepStatus.PENDING)).toBe('running');
|
||||
expect(getNodeVariantFromStepRunStatus(StepStatus.NOT_STARTED)).toBe(
|
||||
'not-executed',
|
||||
);
|
||||
expect(getNodeVariantFromStepRunStatus(undefined)).toBe('default');
|
||||
});
|
||||
});
|
||||
+36
@@ -18,9 +18,15 @@ describe('getWorkflowDiagramTriggerNode', () => {
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"data": {
|
||||
"hasNextStepIds": false,
|
||||
"icon": "IconHandMove",
|
||||
"name": "Manual trigger",
|
||||
"nodeType": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"stepId": "trigger",
|
||||
"triggerType": "MANUAL",
|
||||
},
|
||||
"id": "trigger",
|
||||
@@ -53,9 +59,15 @@ describe('getWorkflowDiagramTriggerNode', () => {
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"data": {
|
||||
"hasNextStepIds": false,
|
||||
"icon": "IconClock",
|
||||
"name": "On a schedule",
|
||||
"nodeType": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"stepId": "trigger",
|
||||
"triggerType": "CRON",
|
||||
},
|
||||
"id": "trigger",
|
||||
@@ -85,9 +97,15 @@ describe('getWorkflowDiagramTriggerNode', () => {
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"data": {
|
||||
"hasNextStepIds": false,
|
||||
"icon": "IconWebhook",
|
||||
"name": "Webhook",
|
||||
"nodeType": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"stepId": "trigger",
|
||||
"triggerType": "WEBHOOK",
|
||||
},
|
||||
"id": "trigger",
|
||||
@@ -116,9 +134,15 @@ describe('getWorkflowDiagramTriggerNode', () => {
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"data": {
|
||||
"hasNextStepIds": false,
|
||||
"icon": "IconPlaylistAdd",
|
||||
"name": "Record is created",
|
||||
"nodeType": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"stepId": "trigger",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
@@ -145,9 +169,15 @@ describe('getWorkflowDiagramTriggerNode', () => {
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"data": {
|
||||
"hasNextStepIds": false,
|
||||
"icon": undefined,
|
||||
"name": "",
|
||||
"nodeType": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"stepId": "trigger",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
@@ -177,9 +207,15 @@ describe('getWorkflowDiagramTriggerNode', () => {
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"data": {
|
||||
"hasNextStepIds": false,
|
||||
"icon": "IconHandMove",
|
||||
"name": "Custom Trigger Name",
|
||||
"nodeType": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"stepId": "trigger",
|
||||
"triggerType": "MANUAL",
|
||||
},
|
||||
"id": "trigger",
|
||||
|
||||
+15
@@ -18,10 +18,17 @@ jest.mock(
|
||||
}),
|
||||
);
|
||||
|
||||
const baseNodeData = {
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: '',
|
||||
};
|
||||
|
||||
describe('getWorkflowNodeIconKey', () => {
|
||||
describe('trigger nodes', () => {
|
||||
it('should return the icon from trigger node data', () => {
|
||||
const triggerNodeData: WorkflowDiagramStepNodeData = {
|
||||
...baseNodeData,
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
name: 'Company Created',
|
||||
@@ -35,6 +42,7 @@ describe('getWorkflowNodeIconKey', () => {
|
||||
|
||||
it('should return icon for manual trigger', () => {
|
||||
const manualTriggerData: WorkflowDiagramStepNodeData = {
|
||||
...baseNodeData,
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'MANUAL',
|
||||
name: 'Manual Trigger',
|
||||
@@ -48,6 +56,7 @@ describe('getWorkflowNodeIconKey', () => {
|
||||
|
||||
it('should return icon for webhook trigger', () => {
|
||||
const webhookTriggerData: WorkflowDiagramStepNodeData = {
|
||||
...baseNodeData,
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'WEBHOOK',
|
||||
name: 'Webhook Trigger',
|
||||
@@ -63,6 +72,7 @@ describe('getWorkflowNodeIconKey', () => {
|
||||
describe('action nodes', () => {
|
||||
it('should return icon for CREATE_RECORD action', () => {
|
||||
const createActionData: WorkflowDiagramStepNodeData = {
|
||||
...baseNodeData,
|
||||
nodeType: 'action',
|
||||
actionType: 'CREATE_RECORD',
|
||||
name: 'Create Company',
|
||||
@@ -75,6 +85,7 @@ describe('getWorkflowNodeIconKey', () => {
|
||||
|
||||
it('should return icon for UPDATE_RECORD action', () => {
|
||||
const updateActionData: WorkflowDiagramStepNodeData = {
|
||||
...baseNodeData,
|
||||
nodeType: 'action',
|
||||
actionType: 'UPDATE_RECORD',
|
||||
name: 'Update Company',
|
||||
@@ -87,6 +98,7 @@ describe('getWorkflowNodeIconKey', () => {
|
||||
|
||||
it('should return icon for DELETE_RECORD action', () => {
|
||||
const deleteActionData: WorkflowDiagramStepNodeData = {
|
||||
...baseNodeData,
|
||||
nodeType: 'action',
|
||||
actionType: 'DELETE_RECORD',
|
||||
name: 'Delete Company',
|
||||
@@ -99,6 +111,7 @@ describe('getWorkflowNodeIconKey', () => {
|
||||
|
||||
it('should return icon for SEND_EMAIL action', () => {
|
||||
const emailActionData: WorkflowDiagramStepNodeData = {
|
||||
...baseNodeData,
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Send Email',
|
||||
@@ -111,6 +124,7 @@ describe('getWorkflowNodeIconKey', () => {
|
||||
|
||||
it('should return icon for FILTER action', () => {
|
||||
const filterActionData: WorkflowDiagramStepNodeData = {
|
||||
...baseNodeData,
|
||||
nodeType: 'action',
|
||||
actionType: 'FILTER',
|
||||
name: 'Filter Records',
|
||||
@@ -123,6 +137,7 @@ describe('getWorkflowNodeIconKey', () => {
|
||||
|
||||
it('should handle unknown action types', () => {
|
||||
const unknownActionData: WorkflowDiagramStepNodeData = {
|
||||
...baseNodeData,
|
||||
nodeType: 'action',
|
||||
actionType: 'UNKNOWN_ACTION' as any,
|
||||
name: 'Unknown Action',
|
||||
|
||||
+25
-2
@@ -45,6 +45,10 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"nodeType": "empty-trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
"id": "trigger",
|
||||
"position": {
|
||||
@@ -85,9 +89,15 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"hasNextStepIds": false,
|
||||
"icon": "IconPlaylistAdd",
|
||||
"name": "Record is created",
|
||||
"nodeType": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"stepId": "trigger",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
@@ -133,6 +143,7 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
name: 'Company created',
|
||||
settings: { eventName: 'company.created', outputSchema: {} },
|
||||
type: 'DATABASE_EVENT',
|
||||
nextStepIds: ['step-1'],
|
||||
},
|
||||
updatedAt: '',
|
||||
workflowId: '',
|
||||
@@ -148,11 +159,11 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
"data": {
|
||||
"edgeType": "default",
|
||||
},
|
||||
"deletable": false,
|
||||
"deletable": true,
|
||||
"id": "8f3b2121-f194-4ba4-9fbf-0",
|
||||
"markerEnd": "workflow-edge-arrow-rounded",
|
||||
"markerStart": "workflow-edge-gray-circle",
|
||||
"selectable": false,
|
||||
"selectable": true,
|
||||
"source": "trigger",
|
||||
"target": "step-1",
|
||||
"type": "empty-filter--editable",
|
||||
@@ -161,9 +172,15 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
"nodes": [
|
||||
{
|
||||
"data": {
|
||||
"hasNextStepIds": true,
|
||||
"icon": "IconPlaylistAdd",
|
||||
"name": "Company created",
|
||||
"nodeType": "trigger",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"stepId": "trigger",
|
||||
"triggerType": "DATABASE_EVENT",
|
||||
},
|
||||
"id": "trigger",
|
||||
@@ -175,8 +192,14 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": false,
|
||||
"name": "",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 150,
|
||||
},
|
||||
"stepId": "step-1",
|
||||
},
|
||||
"id": "step-1",
|
||||
"position": {
|
||||
|
||||
-132
@@ -1,132 +0,0 @@
|
||||
import { WorkflowDiagramNode } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { isCreateStepNode } from '../isCreateStepNode';
|
||||
|
||||
describe('isCreateStepNode', () => {
|
||||
it('should return true for create-step node with correct type and nodeType', () => {
|
||||
const createStepNode: WorkflowDiagramNode = {
|
||||
id: 'create-step-1',
|
||||
type: 'create-step',
|
||||
position: { x: 0, y: 200 },
|
||||
data: {
|
||||
nodeType: 'create-step',
|
||||
parentNodeId: 'action-1',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(createStepNode);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for node with create-step type but wrong nodeType', () => {
|
||||
const node: WorkflowDiagramNode = {
|
||||
id: 'fake-create-step',
|
||||
type: 'create-step',
|
||||
position: { x: 0, y: 200 },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CREATE_RECORD',
|
||||
name: 'Create Company',
|
||||
} as any,
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(node);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for node with correct nodeType but wrong type', () => {
|
||||
const node: WorkflowDiagramNode = {
|
||||
id: 'fake-create-step',
|
||||
type: 'action',
|
||||
position: { x: 0, y: 200 },
|
||||
data: {
|
||||
nodeType: 'create-step',
|
||||
parentNodeId: 'action-1',
|
||||
} as any,
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(node);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for action node', () => {
|
||||
const actionNode: WorkflowDiagramNode = {
|
||||
id: 'action-1',
|
||||
position: { x: 0, y: 100 },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CREATE_RECORD',
|
||||
name: 'Create Company',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(actionNode);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for trigger node', () => {
|
||||
const triggerNode: WorkflowDiagramNode = {
|
||||
id: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
name: 'Company Created',
|
||||
icon: 'IconPlus',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(triggerNode);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty-trigger node', () => {
|
||||
const emptyTriggerNode: WorkflowDiagramNode = {
|
||||
id: 'empty-trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
nodeType: 'empty-trigger',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(emptyTriggerNode);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle create-step node with additional properties', () => {
|
||||
const createStepNodeWithExtras: WorkflowDiagramNode = {
|
||||
id: 'create-step-with-extras',
|
||||
type: 'create-step',
|
||||
position: { x: 50, y: 250 },
|
||||
selected: true,
|
||||
data: {
|
||||
nodeType: 'create-step',
|
||||
parentNodeId: 'action-2',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(createStepNodeWithExtras);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for node without type property', () => {
|
||||
const nodeWithoutType: WorkflowDiagramNode = {
|
||||
id: 'node-without-type',
|
||||
position: { x: 0, y: 200 },
|
||||
data: {
|
||||
nodeType: 'create-step',
|
||||
parentNodeId: 'action-1',
|
||||
} as any,
|
||||
};
|
||||
|
||||
const result = isCreateStepNode(nodeWithoutType);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
+10
-15
@@ -11,6 +11,9 @@ describe('isStepNode', () => {
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
name: 'Company Created',
|
||||
icon: 'IconPlus',
|
||||
stepId: 'trigger',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -27,6 +30,9 @@ describe('isStepNode', () => {
|
||||
nodeType: 'action',
|
||||
actionType: 'CREATE_RECORD',
|
||||
name: 'Create Company',
|
||||
stepId: 'action-1',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -35,27 +41,13 @@ describe('isStepNode', () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for create-step node', () => {
|
||||
const createStepNode: WorkflowDiagramNode = {
|
||||
id: 'create-step-1',
|
||||
position: { x: 0, y: 200 },
|
||||
data: {
|
||||
nodeType: 'create-step',
|
||||
parentNodeId: 'action-1',
|
||||
},
|
||||
};
|
||||
|
||||
const result = isStepNode(createStepNode);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty-trigger node', () => {
|
||||
const emptyTriggerNode: WorkflowDiagramNode = {
|
||||
id: 'empty-trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
nodeType: 'empty-trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -74,6 +66,9 @@ describe('isStepNode', () => {
|
||||
triggerType: 'MANUAL',
|
||||
name: 'Manual Trigger',
|
||||
icon: 'IconClick',
|
||||
stepId: 'trigger-with-extra',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+24
@@ -9,6 +9,9 @@ it('Preserves the properties defined in the previous version but not in the next
|
||||
nodeType: 'action',
|
||||
name: '',
|
||||
actionType: 'CODE',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: '',
|
||||
},
|
||||
id: '1',
|
||||
position: { x: 0, y: 0 },
|
||||
@@ -24,6 +27,9 @@ it('Preserves the properties defined in the previous version but not in the next
|
||||
nodeType: 'action',
|
||||
name: '',
|
||||
actionType: 'CODE',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: '',
|
||||
},
|
||||
id: '1',
|
||||
position: { x: 0, y: 0 },
|
||||
@@ -40,8 +46,14 @@ it('Preserves the properties defined in the previous version but not in the next
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": false,
|
||||
"name": "",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"stepId": "",
|
||||
},
|
||||
"id": "1",
|
||||
"position": {
|
||||
@@ -63,6 +75,9 @@ it('Replaces duplicated properties with the next value', () => {
|
||||
nodeType: 'action',
|
||||
name: '',
|
||||
actionType: 'CODE',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: '',
|
||||
},
|
||||
id: '1',
|
||||
position: { x: 0, y: 0 },
|
||||
@@ -77,6 +92,9 @@ it('Replaces duplicated properties with the next value', () => {
|
||||
nodeType: 'action',
|
||||
name: '2',
|
||||
actionType: 'CODE',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: '',
|
||||
},
|
||||
id: '1',
|
||||
position: { x: 0, y: 0 },
|
||||
@@ -93,8 +111,14 @@ it('Replaces duplicated properties with the next value', () => {
|
||||
{
|
||||
"data": {
|
||||
"actionType": "CODE",
|
||||
"hasNextStepIds": false,
|
||||
"name": "2",
|
||||
"nodeType": "action",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"stepId": "",
|
||||
},
|
||||
"id": "1",
|
||||
"position": {
|
||||
|
||||
+12
@@ -13,6 +13,9 @@ describe('selectWorkflowDiagramNode', () => {
|
||||
name: 'Node 1',
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -23,6 +26,9 @@ describe('selectWorkflowDiagramNode', () => {
|
||||
name: 'Node 2',
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 150 },
|
||||
stepId: '2',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -49,6 +55,9 @@ describe('selectWorkflowDiagramNode', () => {
|
||||
name: 'Node 1',
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: '1',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -74,6 +83,9 @@ describe('selectWorkflowDiagramNode', () => {
|
||||
name: 'Node 1',
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: '1',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
+125
-13
@@ -7,7 +7,14 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
data: { nodeType: 'action', actionType: 'CODE', name: 'Step A' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: 'Step A',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'A',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
@@ -16,6 +23,9 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Step C',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'C',
|
||||
},
|
||||
position: { x: 0, y: 300 },
|
||||
},
|
||||
@@ -47,12 +57,26 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
data: { nodeType: 'action', actionType: 'CODE', name: 'Step A' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: 'Step A',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'A',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'FILTER',
|
||||
name: 'Filter B',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 150 },
|
||||
stepId: 'B',
|
||||
},
|
||||
position: { x: 0, y: 150 },
|
||||
},
|
||||
{
|
||||
@@ -61,6 +85,9 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Step C',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 300 },
|
||||
stepId: 'C',
|
||||
},
|
||||
position: { x: 0, y: 300 },
|
||||
},
|
||||
@@ -91,12 +118,26 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
expect(result.nodes).toEqual([
|
||||
{
|
||||
id: 'A',
|
||||
data: { nodeType: 'action', actionType: 'CODE', name: 'Step A' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: 'Step A',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'A',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
data: { nodeType: 'action', actionType: 'SEND_EMAIL', name: 'Step C' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Step C',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 300 },
|
||||
stepId: 'C',
|
||||
},
|
||||
position: { x: 0, y: 300 },
|
||||
},
|
||||
]);
|
||||
@@ -123,12 +164,26 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
data: { nodeType: 'action', actionType: 'CODE', name: 'Step A' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: 'Step A',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'A',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: 'B1',
|
||||
data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B1' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'FILTER',
|
||||
name: 'Filter B1',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 150 },
|
||||
stepId: 'B1',
|
||||
},
|
||||
position: { x: 0, y: 150 },
|
||||
},
|
||||
{
|
||||
@@ -137,12 +192,22 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Step C',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 300 },
|
||||
stepId: 'C',
|
||||
},
|
||||
position: { x: 0, y: 300 },
|
||||
},
|
||||
{
|
||||
id: 'B2',
|
||||
data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B2' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'FILTER',
|
||||
name: 'Filter B2',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 450 },
|
||||
stepId: 'B2',
|
||||
},
|
||||
position: { x: 0, y: 450 },
|
||||
},
|
||||
{
|
||||
@@ -151,6 +216,9 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodeType: 'action',
|
||||
actionType: 'CREATE_RECORD',
|
||||
name: 'Step D',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 600 },
|
||||
stepId: 'D',
|
||||
},
|
||||
position: { x: 0, y: 600 },
|
||||
},
|
||||
@@ -238,12 +306,26 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
data: { nodeType: 'action', actionType: 'CODE', name: 'Step A' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: 'Step A',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'A',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'FILTER',
|
||||
name: 'Filter B',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 150 },
|
||||
stepId: 'B',
|
||||
},
|
||||
position: { x: 0, y: 150 },
|
||||
},
|
||||
],
|
||||
@@ -267,7 +349,14 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
expect(result.nodes).toEqual([
|
||||
{
|
||||
id: 'A',
|
||||
data: { nodeType: 'action', actionType: 'CODE', name: 'Step A' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: 'Step A',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'A',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
]);
|
||||
@@ -285,12 +374,22 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
name: 'Trigger',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'trigger',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
data: { nodeType: 'action', actionType: 'FILTER', name: 'Filter B' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'FILTER',
|
||||
name: 'Filter B',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 150 },
|
||||
stepId: 'B',
|
||||
},
|
||||
position: { x: 0, y: 150 },
|
||||
},
|
||||
{
|
||||
@@ -299,6 +398,9 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Step C',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 300 },
|
||||
stepId: 'C',
|
||||
},
|
||||
position: { x: 0, y: 300 },
|
||||
},
|
||||
@@ -333,12 +435,22 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodeType: 'trigger',
|
||||
triggerType: 'DATABASE_EVENT',
|
||||
name: 'Trigger',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'trigger',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
data: { nodeType: 'action', actionType: 'SEND_EMAIL', name: 'Step C' },
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Step C',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 300 },
|
||||
stepId: 'C',
|
||||
},
|
||||
position: { x: 0, y: 300 },
|
||||
},
|
||||
]);
|
||||
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeDefaultConfiguration';
|
||||
import {
|
||||
WorkflowDiagram,
|
||||
WorkflowDiagramEdge,
|
||||
WorkflowDiagramEdgeType,
|
||||
WorkflowDiagramNode,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const addCreateStepNodes = ({ nodes, edges }: WorkflowDiagram) => {
|
||||
const nodesWithoutTargets = nodes.filter((node) =>
|
||||
edges.every((edge) => edge.source !== node.id),
|
||||
);
|
||||
|
||||
const updatedNodes: Array<WorkflowDiagramNode> = nodes.slice();
|
||||
const updatedEdges: Array<WorkflowDiagramEdge> = edges.slice();
|
||||
|
||||
for (const node of nodesWithoutTargets) {
|
||||
const newCreateStepNode: WorkflowDiagramNode = {
|
||||
// FIXME: We need a stable id for create step nodes to be able to preserve their selected status.
|
||||
// FIXME: In the future, we'll have conditions and loops. We'll have to set an id to each branch so we can have this stable id.
|
||||
id: 'branch-1__create-step',
|
||||
type: 'create-step',
|
||||
data: {
|
||||
nodeType: 'create-step',
|
||||
parentNodeId: node.id,
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
updatedNodes.push(newCreateStepNode);
|
||||
|
||||
updatedEdges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
type: 'blank' as WorkflowDiagramEdgeType,
|
||||
id: v4(),
|
||||
source: node.id,
|
||||
target: newCreateStepNode.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: updatedNodes,
|
||||
edges: updatedEdges,
|
||||
};
|
||||
};
|
||||
+42
-69
@@ -1,7 +1,6 @@
|
||||
import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import { FIRST_NODE_POSITION } from '@/workflow/workflow-diagram/constants/FirstNodePosition';
|
||||
import { VERTICAL_DISTANCE_BETWEEN_TWO_NODES } from '@/workflow/workflow-diagram/constants/VerticalDistanceBetweenTwoNodes';
|
||||
import { WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEmptyTriggerNodeDefinition';
|
||||
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeDefaultConfiguration';
|
||||
import {
|
||||
WorkflowDiagram,
|
||||
@@ -15,58 +14,8 @@ import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils
|
||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
/**
|
||||
* Groups workflow steps into levels based on their distance from root nodes.
|
||||
*
|
||||
* A root node is one that is not referenced as a `nextStepId` by any other step.
|
||||
* The function performs a breadth-first traversal from all roots and assigns
|
||||
* each step to a level indicating its depth in the graph.
|
||||
*
|
||||
* Returns an array where each sub-array contains all steps at the same level.
|
||||
*/
|
||||
const groupStepsByLevel = (steps: WorkflowStep[]): WorkflowStep[][] => {
|
||||
const stepMap = new Map<string, WorkflowStep>();
|
||||
|
||||
const childIds = new Set<string>();
|
||||
|
||||
for (const step of steps) {
|
||||
stepMap.set(step.id, step);
|
||||
step.nextStepIds?.forEach((id) => childIds.add(id));
|
||||
}
|
||||
|
||||
const rootSteps = steps.filter((step) => !childIds.has(step.id));
|
||||
|
||||
const stepsByLevel: WorkflowStep[][] = [];
|
||||
|
||||
const visited = new Set<string>();
|
||||
|
||||
const visit = ({ step, level }: { step: WorkflowStep; level: number }) => {
|
||||
if (visited.has(step.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visited.add(step.id);
|
||||
|
||||
if (!isDefined(stepsByLevel[level])) {
|
||||
stepsByLevel[level] = [];
|
||||
}
|
||||
|
||||
stepsByLevel[level].push(step);
|
||||
|
||||
step.nextStepIds?.forEach((childId) => {
|
||||
const child = stepMap.get(childId);
|
||||
|
||||
if (isDefined(child)) {
|
||||
visit({ step: child, level: level + 1 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
rootSteps.forEach((root) => visit({ step: root, level: 0 }));
|
||||
|
||||
return stepsByLevel;
|
||||
};
|
||||
import { WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEmptyTriggerNodeDefinition';
|
||||
import { getRootStepIds } from '@/workflow/workflow-trigger/utils/getRootStepIds';
|
||||
|
||||
export const generateWorkflowDiagram = ({
|
||||
trigger,
|
||||
@@ -84,40 +33,61 @@ export const generateWorkflowDiagram = ({
|
||||
nodes.push(getWorkflowDiagramTriggerNode({ trigger }));
|
||||
} else {
|
||||
nodes.push(WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION);
|
||||
}
|
||||
|
||||
const stepsGroupedByLevel = groupStepsByLevel(steps);
|
||||
const triggerNextStepIds = isDefined(steps) ? getRootStepIds(steps) : [];
|
||||
|
||||
triggerNextStepIds.forEach((stepId) => {
|
||||
edges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
type: defaultEdgeType,
|
||||
...(defaultEdgeType.includes('editable')
|
||||
? { deletable: true, selectable: true }
|
||||
: {}),
|
||||
id: v4(),
|
||||
source: 'trigger',
|
||||
target: stepId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let levelYPos = FIRST_NODE_POSITION.y;
|
||||
|
||||
const xPos = FIRST_NODE_POSITION.x;
|
||||
|
||||
for (const stepsByLevel of stepsGroupedByLevel) {
|
||||
for (const step of steps) {
|
||||
levelYPos += VERTICAL_DISTANCE_BETWEEN_TWO_NODES;
|
||||
|
||||
for (const step of stepsByLevel) {
|
||||
nodes.push({
|
||||
id: step.id,
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: step.type,
|
||||
name: step.name,
|
||||
} satisfies WorkflowDiagramStepNodeData,
|
||||
position: {
|
||||
nodes.push({
|
||||
id: step.id,
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: step.type,
|
||||
name: step.name,
|
||||
hasNextStepIds:
|
||||
isDefined(step.nextStepIds) && step.nextStepIds.length > 0,
|
||||
stepId: step.id,
|
||||
position: step.position ?? {
|
||||
x: xPos,
|
||||
y: levelYPos,
|
||||
},
|
||||
});
|
||||
}
|
||||
} satisfies WorkflowDiagramStepNodeData,
|
||||
position: step.position ?? {
|
||||
x: xPos,
|
||||
y: levelYPos,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const firstLevelStep of stepsGroupedByLevel[0] || []) {
|
||||
for (const stepLinkToTriggerId of trigger?.nextStepIds ?? []) {
|
||||
edges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
type: defaultEdgeType,
|
||||
...(defaultEdgeType.includes('editable')
|
||||
? { deletable: true, selectable: true }
|
||||
: {}),
|
||||
id: v4(),
|
||||
source: TRIGGER_STEP_ID,
|
||||
target: firstLevelStep.id,
|
||||
target: stepLinkToTriggerId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,6 +96,9 @@ export const generateWorkflowDiagram = ({
|
||||
edges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
type: defaultEdgeType,
|
||||
...(defaultEdgeType.includes('editable')
|
||||
? { deletable: true, selectable: true }
|
||||
: {}),
|
||||
id: v4(),
|
||||
source: step.id,
|
||||
target: child,
|
||||
|
||||
+6
-7
@@ -5,17 +5,16 @@ export const getOrganizedDiagram = (
|
||||
diagram: WorkflowDiagram,
|
||||
): WorkflowDiagram => {
|
||||
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||
graph.setGraph({ rankdir: 'TB' });
|
||||
|
||||
const biggestNodeWidth = diagram.nodes.reduce(
|
||||
(acc, node) => Math.max(acc, node.measured?.width ?? 0),
|
||||
0,
|
||||
);
|
||||
graph.setGraph({
|
||||
ranksep: 80, // Vertical distance between 2 nodes
|
||||
nodesep: 200, // Horizontal distance between 2 nodes
|
||||
rankdir: 'TB',
|
||||
});
|
||||
|
||||
diagram.edges.forEach((edge) => graph.setEdge(edge.source, edge.target));
|
||||
diagram.nodes.forEach((node) =>
|
||||
graph.setNode(node.id, {
|
||||
width: biggestNodeWidth,
|
||||
width: node.measured?.width ?? 0,
|
||||
height: node.measured?.height ?? 0,
|
||||
}),
|
||||
);
|
||||
|
||||
+8
-1
@@ -63,8 +63,15 @@ export const getWorkflowDiagramTriggerNode = ({
|
||||
triggerType: trigger.type,
|
||||
name: isDefined(trigger.name) ? trigger.name : triggerDefaultLabel,
|
||||
icon: triggerIcon,
|
||||
stepId: 'trigger',
|
||||
hasNextStepIds:
|
||||
isDefined(trigger.nextStepIds) && trigger.nextStepIds.length > 0,
|
||||
position: trigger.position ?? {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
} satisfies WorkflowDiagramStepNodeData,
|
||||
position: {
|
||||
position: trigger.position ?? {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,8 +4,8 @@ import {
|
||||
WorkflowDiagramEdgeType,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
|
||||
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
|
||||
|
||||
const EMPTY_DIAGRAM: WorkflowDiagram = {
|
||||
nodes: [],
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { CREATE_STEP_STEP_ID } from '@/workflow/workflow-diagram/constants/CreateStepStepId';
|
||||
import {
|
||||
WorkflowDiagramCreateStepNodeData,
|
||||
WorkflowDiagramNode,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
|
||||
export const isCreateStepNode = (
|
||||
node: WorkflowDiagramNode,
|
||||
): node is WorkflowDiagramNode & {
|
||||
data: WorkflowDiagramCreateStepNodeData;
|
||||
} => {
|
||||
return (
|
||||
node.type === CREATE_STEP_STEP_ID && node.data.nodeType === 'create-step'
|
||||
);
|
||||
};
|
||||
+3
-1
@@ -6,7 +6,9 @@ import { useCreateStep } from '../useCreateStep';
|
||||
|
||||
const mockCreateDraftFromWorkflowVersion = jest.fn().mockResolvedValue('457');
|
||||
const mockCreateWorkflowVersionStep = jest.fn().mockResolvedValue({
|
||||
data: { createWorkflowVersionStep: { id: '1', type: 'CODE' } },
|
||||
data: {
|
||||
createWorkflowVersionStep: { createdStep: { id: '1', type: 'CODE' } },
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { useCreateWorkflowVersionEdge } from '@/workflow/workflow-steps/hooks/useCreateWorkflowVersionEdge';
|
||||
import { useState } from 'react';
|
||||
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
|
||||
|
||||
export const useCreateEdge = ({
|
||||
workflow,
|
||||
}: {
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { createWorkflowVersionEdge } = useCreateWorkflowVersionEdge();
|
||||
|
||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||
|
||||
const createEdge = async ({
|
||||
source,
|
||||
target,
|
||||
}: {
|
||||
source: string;
|
||||
target: string;
|
||||
}) => {
|
||||
if (isLoading === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const workflowVersionId = await getUpdatableWorkflowVersion(workflow);
|
||||
|
||||
const createdEdge = (
|
||||
await createWorkflowVersionEdge({
|
||||
workflowVersionId,
|
||||
source,
|
||||
target,
|
||||
})
|
||||
)?.data?.createWorkflowVersionEdge;
|
||||
|
||||
if (!createdEdge) {
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { createEdge };
|
||||
};
|
||||
@@ -36,10 +36,12 @@ export const useCreateStep = ({
|
||||
newStepType,
|
||||
parentStepId,
|
||||
nextStepId,
|
||||
position,
|
||||
}: {
|
||||
newStepType: WorkflowStepType;
|
||||
parentStepId: string;
|
||||
parentStepId: string | undefined;
|
||||
nextStepId: string | undefined;
|
||||
position?: { x: number; y: number };
|
||||
}) => {
|
||||
if (isLoading === true) {
|
||||
return;
|
||||
@@ -50,15 +52,18 @@ export const useCreateStep = ({
|
||||
try {
|
||||
const workflowVersionId = await getUpdatableWorkflowVersion(workflow);
|
||||
|
||||
const createdStep = (
|
||||
const workflowVersionStepChanges = (
|
||||
await createWorkflowVersionStep({
|
||||
workflowVersionId,
|
||||
stepType: newStepType,
|
||||
parentStepId,
|
||||
nextStepId,
|
||||
position,
|
||||
})
|
||||
)?.data?.createWorkflowVersionStep;
|
||||
|
||||
const createdStep = workflowVersionStepChanges?.createdStep;
|
||||
|
||||
if (!isDefined(createdStep)) {
|
||||
throw new Error("Couldn't create step");
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import {
|
||||
CreateWorkflowVersionEdgeMutation,
|
||||
CreateWorkflowVersionEdgeMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { CREATE_WORKFLOW_VERSION_EDGE } from '@/workflow/graphql/mutations/createWorkflowVersionEdge';
|
||||
import { CreateWorkflowVersionEdgeInput } from '~/generated/graphql';
|
||||
import { useUpdateWorkflowVersionCache } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache';
|
||||
|
||||
export const useCreateWorkflowVersionEdge = () => {
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const { updateWorkflowVersionCache } = useUpdateWorkflowVersionCache();
|
||||
|
||||
const [mutate] = useMutation<
|
||||
CreateWorkflowVersionEdgeMutation,
|
||||
CreateWorkflowVersionEdgeMutationVariables
|
||||
>(CREATE_WORKFLOW_VERSION_EDGE, { client: apolloCoreClient });
|
||||
|
||||
const createWorkflowVersionEdge = async (
|
||||
input: CreateWorkflowVersionEdgeInput,
|
||||
) => {
|
||||
const result = await mutate({ variables: { input } });
|
||||
|
||||
const workflowVersionStepChanges = result?.data?.createWorkflowVersionEdge;
|
||||
|
||||
updateWorkflowVersionCache({
|
||||
workflowVersionStepChanges,
|
||||
workflowVersionId: input.workflowVersionId,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return { createWorkflowVersionEdge };
|
||||
};
|
||||
+10
-65
@@ -1,36 +1,25 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
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 { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { CREATE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/createWorkflowVersionStep';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
CreateWorkflowVersionStepInput,
|
||||
CreateWorkflowVersionStepMutation,
|
||||
CreateWorkflowVersionStepMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useUpdateWorkflowVersionCache } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache';
|
||||
|
||||
export const useCreateWorkflowVersionStep = () => {
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const { updateWorkflowVersionCache } = useUpdateWorkflowVersionCache();
|
||||
|
||||
const [mutate] = useMutation<
|
||||
CreateWorkflowVersionStepMutation,
|
||||
CreateWorkflowVersionStepMutationVariables
|
||||
>(CREATE_WORKFLOW_VERSION_STEP, {
|
||||
client: apolloCoreClient,
|
||||
});
|
||||
|
||||
const createWorkflowVersionStep = async (
|
||||
input: CreateWorkflowVersionStepInput,
|
||||
) => {
|
||||
@@ -38,57 +27,13 @@ export const useCreateWorkflowVersionStep = () => {
|
||||
variables: { input },
|
||||
});
|
||||
|
||||
const insertedStep = result?.data?.createWorkflowVersionStep;
|
||||
const workflowVersionStepChanges = result?.data?.createWorkflowVersionStep;
|
||||
|
||||
if (!isDefined(insertedStep)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedRecord = getRecordFromCache<WorkflowVersion>(
|
||||
input.workflowVersionId,
|
||||
);
|
||||
|
||||
if (!isDefined(cachedRecord)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { parentStepId, nextStepId } = input;
|
||||
|
||||
const updatedExistingSteps =
|
||||
cachedRecord.steps?.map((existingStep) => {
|
||||
if (existingStep.id === parentStepId) {
|
||||
return {
|
||||
...existingStep,
|
||||
nextStepIds: [
|
||||
...new Set([
|
||||
...(existingStep.nextStepIds?.filter(
|
||||
(id) => id !== nextStepId,
|
||||
) || []),
|
||||
insertedStep.id,
|
||||
]),
|
||||
],
|
||||
};
|
||||
}
|
||||
return existingStep;
|
||||
}) ?? [];
|
||||
|
||||
const newCachedRecord = {
|
||||
...cachedRecord,
|
||||
steps: [...updatedExistingSteps, insertedStep],
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
steps: true,
|
||||
};
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloCoreClient.cache,
|
||||
record: newCachedRecord,
|
||||
recordGqlFields,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
updateWorkflowVersionCache({
|
||||
workflowVersionStepChanges,
|
||||
workflowVersionId: input.workflowVersionId,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { useDeleteWorkflowVersionEdge } from '@/workflow/workflow-steps/hooks/useDeleteWorkflowVersionEdge';
|
||||
import { useState } from 'react';
|
||||
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
|
||||
|
||||
export const useDeleteEdge = ({
|
||||
workflow,
|
||||
}: {
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { deleteWorkflowVersionEdge } = useDeleteWorkflowVersionEdge();
|
||||
|
||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||
|
||||
const deleteEdge = async ({
|
||||
source,
|
||||
target,
|
||||
}: {
|
||||
source: string;
|
||||
target: string;
|
||||
}) => {
|
||||
if (isLoading === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const workflowVersionId = await getUpdatableWorkflowVersion(workflow);
|
||||
|
||||
const deletedEdge = (
|
||||
await deleteWorkflowVersionEdge({
|
||||
workflowVersionId,
|
||||
source,
|
||||
target,
|
||||
})
|
||||
)?.data?.deleteWorkflowVersionEdge;
|
||||
|
||||
if (!deletedEdge) {
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { deleteEdge };
|
||||
};
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { CreateWorkflowVersionEdgeInput } from '~/generated/graphql';
|
||||
import { DELETE_WORKFLOW_VERSION_EDGE } from '@/workflow/graphql/mutations/deleteWorkflowVersionEdge';
|
||||
import {
|
||||
DeleteWorkflowVersionEdgeMutation,
|
||||
DeleteWorkflowVersionEdgeMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useUpdateWorkflowVersionCache } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache';
|
||||
|
||||
export const useDeleteWorkflowVersionEdge = () => {
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const { updateWorkflowVersionCache } = useUpdateWorkflowVersionCache();
|
||||
|
||||
const [mutate] = useMutation<
|
||||
DeleteWorkflowVersionEdgeMutation,
|
||||
DeleteWorkflowVersionEdgeMutationVariables
|
||||
>(DELETE_WORKFLOW_VERSION_EDGE, { client: apolloCoreClient });
|
||||
|
||||
const deleteWorkflowVersionEdge = async (
|
||||
input: CreateWorkflowVersionEdgeInput,
|
||||
) => {
|
||||
const result = await mutate({ variables: { input } });
|
||||
|
||||
const workflowVersionStepChanges = result?.data?.deleteWorkflowVersionEdge;
|
||||
|
||||
updateWorkflowVersionCache({
|
||||
workflowVersionStepChanges,
|
||||
workflowVersionId: input.workflowVersionId,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return { deleteWorkflowVersionEdge };
|
||||
};
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowVersionStepChanges,
|
||||
} from '~/generated/graphql';
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
|
||||
export const useUpdateWorkflowVersionCache = () => {
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const updateWorkflowVersionCache = ({
|
||||
workflowVersionStepChanges,
|
||||
workflowVersionId,
|
||||
}: {
|
||||
workflowVersionStepChanges: WorkflowVersionStepChanges | undefined;
|
||||
workflowVersionId: string;
|
||||
}) => {
|
||||
if (!isDefined(workflowVersionStepChanges)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedRecord = getRecordFromCache<WorkflowVersion>(workflowVersionId);
|
||||
|
||||
if (!isDefined(cachedRecord)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { triggerNextStepIds, stepsNextStepIds, createdStep, deletedStepId } =
|
||||
workflowVersionStepChanges;
|
||||
|
||||
const newCachedRecord = {
|
||||
...cachedRecord,
|
||||
trigger: isDefined(cachedRecord.trigger)
|
||||
? {
|
||||
...cachedRecord.trigger,
|
||||
nextStepIds: triggerNextStepIds,
|
||||
}
|
||||
: cachedRecord.trigger,
|
||||
steps: (cachedRecord.steps || []).map((step: WorkflowAction) => ({
|
||||
...step,
|
||||
nextStepIds: stepsNextStepIds[step.id] ?? step.nextStepIds,
|
||||
})),
|
||||
};
|
||||
|
||||
if (isDefined(createdStep)) {
|
||||
const formattedCreatedStep = {
|
||||
...createdStep,
|
||||
nextStepIds: createdStep.nextStepIds || [],
|
||||
};
|
||||
|
||||
newCachedRecord.steps.push(formattedCreatedStep);
|
||||
}
|
||||
|
||||
if (isDefined(deletedStepId)) {
|
||||
newCachedRecord.steps = newCachedRecord.steps.filter(
|
||||
(step) => step.id !== deletedStepId,
|
||||
);
|
||||
}
|
||||
|
||||
const recordGqlFields = {
|
||||
steps: true,
|
||||
trigger: true,
|
||||
};
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloCoreClient.cache,
|
||||
record: newCachedRecord,
|
||||
recordGqlFields,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
});
|
||||
};
|
||||
|
||||
return { updateWorkflowVersionCache };
|
||||
};
|
||||
+6
-1
@@ -4,11 +4,16 @@ import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-
|
||||
type WorkflowInsertStepIdsState = {
|
||||
parentStepId: string | undefined;
|
||||
nextStepId: string | undefined;
|
||||
position?: { x: number; y: number };
|
||||
};
|
||||
|
||||
export const workflowInsertStepIdsComponentState =
|
||||
createComponentStateV2<WorkflowInsertStepIdsState>({
|
||||
key: 'workflowInsertStepIdsComponentState',
|
||||
defaultValue: { parentStepId: undefined, nextStepId: undefined },
|
||||
defaultValue: {
|
||||
parentStepId: undefined,
|
||||
nextStepId: undefined,
|
||||
position: undefined,
|
||||
},
|
||||
componentInstanceContext: WorkflowVisualizerComponentInstanceContext,
|
||||
});
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { CRON_TRIGGER_INTERVAL_OPTIONS } from '@/workflow/workflow-trigger/const
|
||||
import { getCronTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getCronTriggerDefaultSettings';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNumber } from '@sniptt/guards';
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowS
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import { MANUAL_TRIGGER_AVAILABILITY_OPTIONS } from '@/workflow/workflow-trigger
|
||||
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import { WEBHOOK_TRIGGER_AUTHENTICATION_OPTIONS } from '@/workflow/workflow-trig
|
||||
import { WEBHOOK_TRIGGER_HTTP_METHOD_OPTIONS } from '@/workflow/workflow-trigger/constants/WebhookTriggerHttpMethodOptions';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
import { getWebhookTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getWebhookTriggerDefaultSettings';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger';
|
||||
import {
|
||||
WorkflowTrigger,
|
||||
WorkflowWithCurrentVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
|
||||
const mockUpdateOneRecord = jest.fn();
|
||||
const mockGetUpdatableWorkflowVersion = jest.fn();
|
||||
const mockComputeStepOutputSchema = jest.fn();
|
||||
|
||||
jest.mock('@/object-record/hooks/useUpdateOneRecord', () => ({
|
||||
useUpdateOneRecord: jest.fn(() => ({
|
||||
updateOneRecord: mockUpdateOneRecord,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@/workflow/hooks/useGetUpdatableWorkflowVersion', () => ({
|
||||
useGetUpdatableWorkflowVersion: jest.fn(() => ({
|
||||
getUpdatableWorkflowVersion: mockGetUpdatableWorkflowVersion,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@/workflow/hooks/useComputeStepOutputSchema', () => ({
|
||||
useComputeStepOutputSchema: jest.fn(() => ({
|
||||
computeStepOutputSchema: mockComputeStepOutputSchema,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('useUpdateWorkflowVersionTrigger', () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-id',
|
||||
currentVersion: { id: 'version-id' },
|
||||
} as WorkflowWithCurrentVersion;
|
||||
|
||||
const trigger: WorkflowTrigger = {
|
||||
name: 'Company created',
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'company.created',
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step1'],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('updates the trigger with computed output schema', async () => {
|
||||
mockGetUpdatableWorkflowVersion.mockResolvedValue('version-id');
|
||||
mockComputeStepOutputSchema.mockResolvedValue({
|
||||
data: { computeStepOutputSchema: { field1: 'string' } },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useUpdateWorkflowVersionTrigger({ workflow: mockWorkflow }),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.updateTrigger(trigger);
|
||||
});
|
||||
|
||||
expect(mockGetUpdatableWorkflowVersion).toHaveBeenCalledWith(mockWorkflow);
|
||||
expect(mockComputeStepOutputSchema).toHaveBeenCalledWith({ step: trigger });
|
||||
expect(mockUpdateOneRecord).toHaveBeenCalledWith({
|
||||
idToUpdate: 'version-id',
|
||||
updateOneRecordInput: {
|
||||
trigger: {
|
||||
...trigger,
|
||||
settings: { ...trigger.settings, outputSchema: { field1: 'string' } },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('skips output schema computation when disabled', async () => {
|
||||
mockGetUpdatableWorkflowVersion.mockResolvedValue('version-id');
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useUpdateWorkflowVersionTrigger({ workflow: mockWorkflow }),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.updateTrigger(trigger, {
|
||||
computeOutputSchema: false,
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockComputeStepOutputSchema).not.toHaveBeenCalled();
|
||||
expect(mockUpdateOneRecord).toHaveBeenCalledWith({
|
||||
idToUpdate: 'version-id',
|
||||
updateOneRecordInput: {
|
||||
trigger,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
+35
@@ -28,6 +28,11 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
eventName: `${generatedMockObjectMetadataItems[0].nameSingular}.created`,
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: [],
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,6 +50,11 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
eventName: `${generatedMockObjectMetadataItems[0].nameSingular}.updated`,
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: [],
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,6 +72,11 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
eventName: `${generatedMockObjectMetadataItems[0].nameSingular}.deleted`,
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: [],
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,6 +94,11 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
eventName: `${generatedMockObjectMetadataItems[0].nameSingular}.created`,
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: [],
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -97,6 +117,11 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
outputSchema: {},
|
||||
icon: COMMAND_MENU_DEFAULT_ICON,
|
||||
},
|
||||
nextStepIds: [],
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,6 +140,11 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
schedule: { day: 1, hour: 0, minute: 0 },
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: [],
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,6 +163,11 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
httpMethod: 'GET',
|
||||
authentication: null,
|
||||
},
|
||||
nextStepIds: [],
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import { DatabaseTriggerDefaultLabel } from '@/workflow/workflow-trigger/constants/DatabaseTriggerDefaultLabel';
|
||||
import { getTriggerDefaultLabel } from '../getTriggerLabel';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
|
||||
describe('getTriggerDefaultLabel', () => {
|
||||
describe('DATABASE_EVENT triggers', () => {
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
|
||||
describe('getTriggerIcon', () => {
|
||||
it('should return proper Icon for DATABASE_EVENT', () => {
|
||||
expect(
|
||||
getTriggerIcon({
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: { eventName: 'company.created', outputSchema: {} },
|
||||
}),
|
||||
).toBe('IconPlaylistAdd');
|
||||
});
|
||||
|
||||
it('should return proper Icon for MANUAL', () => {
|
||||
expect(
|
||||
getTriggerIcon({
|
||||
type: 'MANUAL',
|
||||
settings: { outputSchema: {} },
|
||||
}),
|
||||
).toBe('IconHandMove');
|
||||
});
|
||||
|
||||
it('should return proper Icon for CRON', () => {
|
||||
expect(
|
||||
getTriggerIcon({
|
||||
type: 'CRON',
|
||||
settings: { outputSchema: {}, type: 'CUSTOM', pattern: '' },
|
||||
}),
|
||||
).toBe('IconClock');
|
||||
});
|
||||
|
||||
it('should return proper Icon for CRON', () => {
|
||||
expect(
|
||||
getTriggerIcon({
|
||||
type: 'WEBHOOK',
|
||||
settings: { outputSchema: {}, httpMethod: 'GET', authentication: null },
|
||||
}),
|
||||
).toBe('IconWebhook');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { WorkflowAction } from '@/workflow/types/Workflow';
|
||||
|
||||
export const getRootStepIds = (steps: WorkflowAction[]): string[] => {
|
||||
const childIds = new Set<string>();
|
||||
|
||||
for (const step of steps) {
|
||||
step.nextStepIds?.forEach((id) => childIds.add(id));
|
||||
}
|
||||
|
||||
return steps.filter((step) => !childIds.has(step.id)).map((step) => step.id);
|
||||
};
|
||||
+18
-4
@@ -1,20 +1,26 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowTrigger,
|
||||
WorkflowTriggerType,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||
import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes';
|
||||
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { getRootStepIds } from '@/workflow/workflow-trigger/utils/getRootStepIds';
|
||||
|
||||
// TODO: This needs to be migrated to the server
|
||||
export const getTriggerDefaultDefinition = ({
|
||||
defaultLabel,
|
||||
type,
|
||||
activeNonSystemObjectMetadataItems,
|
||||
steps,
|
||||
}: {
|
||||
defaultLabel: string;
|
||||
type: WorkflowTriggerType;
|
||||
activeNonSystemObjectMetadataItems: ObjectMetadataItem[];
|
||||
steps?: WorkflowAction[] | null;
|
||||
}): WorkflowTrigger => {
|
||||
if (activeNonSystemObjectMetadataItems.length === 0) {
|
||||
throw new Error(
|
||||
@@ -22,11 +28,19 @@ export const getTriggerDefaultDefinition = ({
|
||||
);
|
||||
}
|
||||
|
||||
const nextStepIds = isDefined(steps) ? getRootStepIds(steps) : [];
|
||||
|
||||
const baseTriggerDefinition = {
|
||||
name: defaultLabel,
|
||||
position: { x: 0, y: 0 },
|
||||
nextStepIds,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'DATABASE_EVENT': {
|
||||
return {
|
||||
...baseTriggerDefinition,
|
||||
type,
|
||||
name: defaultLabel,
|
||||
settings: {
|
||||
eventName: `${activeNonSystemObjectMetadataItems[0].nameSingular}.${
|
||||
DATABASE_TRIGGER_TYPES.find(
|
||||
@@ -39,8 +53,8 @@ export const getTriggerDefaultDefinition = ({
|
||||
}
|
||||
case 'MANUAL': {
|
||||
return {
|
||||
...baseTriggerDefinition,
|
||||
type,
|
||||
name: defaultLabel,
|
||||
settings: getManualTriggerDefaultSettings({
|
||||
availability: 'WHEN_RECORD_SELECTED',
|
||||
activeNonSystemObjectMetadataItems,
|
||||
@@ -49,8 +63,8 @@ export const getTriggerDefaultDefinition = ({
|
||||
}
|
||||
case 'CRON': {
|
||||
return {
|
||||
...baseTriggerDefinition,
|
||||
type,
|
||||
name: defaultLabel,
|
||||
settings: {
|
||||
type: 'DAYS',
|
||||
schedule: { day: 1, hour: 0, minute: 0 },
|
||||
@@ -60,8 +74,8 @@ export const getTriggerDefaultDefinition = ({
|
||||
}
|
||||
case 'WEBHOOK': {
|
||||
return {
|
||||
...baseTriggerDefinition,
|
||||
type,
|
||||
name: defaultLabel,
|
||||
settings: {
|
||||
outputSchema: {},
|
||||
httpMethod: 'GET',
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
export const getTriggerHeaderType = (trigger: WorkflowTrigger) => {
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { isBaseOutputSchema } from '@/workflow/workflow-variables/utils/isBaseOutputSchema';
|
||||
|
||||
describe('isBaseOutputSchema', () => {
|
||||
// This looks weird, but that is the way this method was built
|
||||
it('should return false for base output schema', () => {
|
||||
expect(
|
||||
isBaseOutputSchema({ _outputSchemaType: 'LINK', link: { isLeaf: true } }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true in other cases', () => {
|
||||
expect(isBaseOutputSchema({})).toBe(true);
|
||||
});
|
||||
});
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { isFieldTypeCompatibleWithRecordId } from '@/workflow/workflow-variables/utils/isFieldTypeCompatibleWithRecordId';
|
||||
|
||||
describe('isFieldTypeCompatibleWithRecordId', () => {
|
||||
it('should return true', () => {
|
||||
expect(isFieldTypeCompatibleWithRecordId('string')).toBe(true);
|
||||
expect(isFieldTypeCompatibleWithRecordId('unknown')).toBe(true);
|
||||
expect(isFieldTypeCompatibleWithRecordId()).toBe(true);
|
||||
expect(isFieldTypeCompatibleWithRecordId(undefined)).toBe(true);
|
||||
});
|
||||
it('should return false', () => {
|
||||
expect(isFieldTypeCompatibleWithRecordId('number')).toBe(false);
|
||||
expect(isFieldTypeCompatibleWithRecordId('boolean')).toBe(false);
|
||||
expect(isFieldTypeCompatibleWithRecordId('object')).toBe(false);
|
||||
expect(isFieldTypeCompatibleWithRecordId('array')).toBe(false);
|
||||
});
|
||||
});
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_WORKFLOW_VERSION_POSITIONS = gql`
|
||||
mutation UpdateWorkflowVersionPositions(
|
||||
$input: UpdateWorkflowVersionPositionsInput!
|
||||
) {
|
||||
updateWorkflowVersionPositions(input: $input)
|
||||
}
|
||||
`;
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
WorkflowVersion,
|
||||
WorkflowWithCurrentVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { useGetUpdatableWorkflowVersion } from '@/workflow/hooks/useGetUpdatableWorkflowVersion';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import {
|
||||
UpdateWorkflowVersionPositionsMutation,
|
||||
UpdateWorkflowVersionPositionsMutationVariables,
|
||||
WorkflowAction,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { UPDATE_WORKFLOW_VERSION_POSITIONS } from '@/workflow/workflow-version/graphql/mutations/updateWorkflowVersionPositions';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||
import { getOrganizedDiagram } from '@/workflow/workflow-diagram/utils/getOrganizedDiagram';
|
||||
|
||||
export const useTidyUpWorkflowVersion = ({
|
||||
workflow,
|
||||
}: {
|
||||
workflow?: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
const [workflowDiagram, setWorkflowDiagram] = useRecoilComponentStateV2(
|
||||
workflowDiagramComponentState,
|
||||
);
|
||||
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const [mutate] = useMutation<
|
||||
UpdateWorkflowVersionPositionsMutation,
|
||||
UpdateWorkflowVersionPositionsMutationVariables
|
||||
>(UPDATE_WORKFLOW_VERSION_POSITIONS, { client: apolloCoreClient });
|
||||
|
||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||
|
||||
const updateWorkflowVersionPosition = async (
|
||||
positions: { id: string; position: { x: number; y: number } }[],
|
||||
) => {
|
||||
if (!isDefined(workflow?.currentVersion)) {
|
||||
throw new Error('Can not update an undefined workflow version.');
|
||||
}
|
||||
|
||||
const workflowVersionId = await getUpdatableWorkflowVersion(workflow);
|
||||
|
||||
await mutate({ variables: { input: { workflowVersionId, positions } } });
|
||||
|
||||
const cachedRecord = getRecordFromCache<WorkflowVersion>(workflowVersionId);
|
||||
|
||||
if (!isDefined(cachedRecord)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const triggerPosition = positions.find(
|
||||
(position) => position.id === 'trigger',
|
||||
);
|
||||
|
||||
const updatedTrigger =
|
||||
isDefined(triggerPosition) && isDefined(cachedRecord.trigger)
|
||||
? { ...cachedRecord.trigger, position: triggerPosition.position }
|
||||
: cachedRecord.trigger;
|
||||
|
||||
const updatedSteps = cachedRecord.steps?.map((step: WorkflowAction) => {
|
||||
const stepPosition = positions.find(
|
||||
(position) => position.id === step.id,
|
||||
);
|
||||
|
||||
if (!isDefined(stepPosition)) {
|
||||
return step;
|
||||
}
|
||||
|
||||
return { ...step, position: stepPosition.position };
|
||||
});
|
||||
|
||||
const newCachedRecord = {
|
||||
...cachedRecord,
|
||||
trigger: updatedTrigger,
|
||||
steps: updatedSteps,
|
||||
};
|
||||
|
||||
const recordGqlFields = {
|
||||
trigger: true,
|
||||
steps: true,
|
||||
};
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloCoreClient.cache,
|
||||
record: newCachedRecord,
|
||||
recordGqlFields,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
});
|
||||
};
|
||||
|
||||
const tidyUpWorkflowVersion = async () => {
|
||||
if (!isDefined(workflowDiagram) || !isDefined(workflow?.currentVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tidiedUpDiagram = getOrganizedDiagram(workflowDiagram);
|
||||
|
||||
const positions = tidiedUpDiagram.nodes.map((node) => ({
|
||||
id: node.id,
|
||||
position: node.position,
|
||||
}));
|
||||
|
||||
await updateWorkflowVersionPosition(positions);
|
||||
|
||||
setWorkflowDiagram(tidiedUpDiagram);
|
||||
};
|
||||
|
||||
return { tidyUpWorkflowVersion };
|
||||
};
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-3:add-next-step-ids-to-workflow-runs-trigger',
|
||||
description: 'Add next step ids to workflow runs trigger',
|
||||
})
|
||||
export class AddNextStepIdsToWorkflowRunsTrigger extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const mainDataSource =
|
||||
await this.workspaceDataSourceService.connectToMainDataSource();
|
||||
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
|
||||
const workflowRuns = await mainDataSource.query(
|
||||
`SELECT id, state FROM ${schemaName}."workflowRun"`,
|
||||
);
|
||||
|
||||
let updatedWorkflowRunCount = 0;
|
||||
|
||||
for (const workflowRun of workflowRuns) {
|
||||
try {
|
||||
if (isDefined(workflowRun.state.flow.trigger.nextStepIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rootSteps = this.getRootSteps(workflowRun.state.flow.steps || []);
|
||||
|
||||
const nextStepIds = rootSteps.map((step) => step.id);
|
||||
|
||||
const updatedState = {
|
||||
...workflowRun.state,
|
||||
flow: {
|
||||
...workflowRun.state.flow,
|
||||
trigger: {
|
||||
...workflowRun.state.flow.trigger,
|
||||
nextStepIds,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await mainDataSource.query(
|
||||
`UPDATE ${schemaName}."workflowRun" SET state = '${JSON.stringify(updatedState)}'::jsonb`,
|
||||
);
|
||||
|
||||
updatedWorkflowRunCount += 1;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error while adding nextStepIds to workflowRun state '${workflowRun.id}'`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.logger.log(
|
||||
`${updatedWorkflowRunCount}/${workflowRuns.length} triggers updated`,
|
||||
);
|
||||
}
|
||||
|
||||
private getRootSteps(steps: WorkflowAction[]): WorkflowAction[] {
|
||||
const childIds = new Set<string>();
|
||||
|
||||
for (const step of steps) {
|
||||
step.nextStepIds?.forEach((id) => childIds.add(id));
|
||||
}
|
||||
|
||||
return steps.filter((step) => !childIds.has(step.id));
|
||||
}
|
||||
}
|
||||
+11
-2
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AddNextStepIdsToWorkflowRunsTrigger } from 'src/database/commands/upgrade-version-command/1-3/1-3-add-next-step-ids-to-workflow-runs-trigger.command';
|
||||
import { AssignRolesToExistingApiKeysCommand } from 'src/database/commands/upgrade-version-command/1-3/1-3-assign-roles-to-existing-api-keys.command';
|
||||
import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
|
||||
@@ -14,6 +15,7 @@ import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
|
||||
import { WorkspaceFeatureFlagsMapCacheModule } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -28,6 +30,7 @@ import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
],
|
||||
'core',
|
||||
),
|
||||
WorkspaceDataSourceModule,
|
||||
ApiKeyModule,
|
||||
FeatureFlagModule,
|
||||
TwentyORMModule,
|
||||
@@ -35,7 +38,13 @@ import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
WorkspacePermissionsCacheModule,
|
||||
WorkspaceFeatureFlagsMapCacheModule,
|
||||
],
|
||||
providers: [AssignRolesToExistingApiKeysCommand],
|
||||
exports: [AssignRolesToExistingApiKeysCommand],
|
||||
providers: [
|
||||
AssignRolesToExistingApiKeysCommand,
|
||||
AddNextStepIdsToWorkflowRunsTrigger,
|
||||
],
|
||||
exports: [
|
||||
AssignRolesToExistingApiKeysCommand,
|
||||
AddNextStepIdsToWorkflowRunsTrigger,
|
||||
],
|
||||
})
|
||||
export class V1_3_UpgradeVersionCommandModule {}
|
||||
|
||||
+7
-1
@@ -34,6 +34,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
import { compareVersionMajorAndMinor } from 'src/utils/version/compare-version-minor-and-major';
|
||||
import { AddNextStepIdsToWorkflowRunsTrigger } from 'src/database/commands/upgrade-version-command/1-3/1-3-add-next-step-ids-to-workflow-runs-trigger.command';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
@@ -157,6 +158,8 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
|
||||
// 1.3 Commands
|
||||
protected readonly assignRolesToExistingApiKeysCommand: AssignRolesToExistingApiKeysCommand,
|
||||
// protected readonly addNextStepIdsToWorkflowVersionTriggers: AddNextStepIdsToWorkflowVersionTriggers,
|
||||
protected readonly addNextStepIdsToWorkflowRunsTrigger: AddNextStepIdsToWorkflowRunsTrigger,
|
||||
) {
|
||||
super(
|
||||
workspaceRepository,
|
||||
@@ -216,7 +219,10 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
};
|
||||
|
||||
const commands_130: VersionCommands = {
|
||||
beforeSyncMetadata: [],
|
||||
beforeSyncMetadata: [
|
||||
this.addNextStepIdsToWorkflowVersionTriggers, // We add that command again because nextStepIds where not added on freshly created triggers. It will be done in 1.3
|
||||
this.addNextStepIdsToWorkflowRunsTrigger,
|
||||
],
|
||||
afterSyncMetadata: [],
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ export enum FeatureFlagKey {
|
||||
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
|
||||
IS_WORKFLOW_FILTERING_ENABLED = 'IS_WORKFLOW_FILTERING_ENABLED',
|
||||
IS_WORKFLOW_BRANCH_ENABLED = 'IS_WORKFLOW_BRANCH_ENABLED',
|
||||
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
|
||||
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED = 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED',
|
||||
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
@InputType()
|
||||
export class CreateWorkflowVersionEdgeInput {
|
||||
@Field(() => String, {
|
||||
description: 'Workflow version ID',
|
||||
nullable: false,
|
||||
})
|
||||
workflowVersionId: string;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'Workflow version source step ID',
|
||||
nullable: false,
|
||||
})
|
||||
source: string;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'Workflow version target step ID',
|
||||
nullable: false,
|
||||
})
|
||||
target: string;
|
||||
}
|
||||
+9
-1
@@ -2,6 +2,7 @@ import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-input.dto';
|
||||
|
||||
@InputType()
|
||||
export class CreateWorkflowVersionStepInput {
|
||||
@@ -17,7 +18,8 @@ export class CreateWorkflowVersionStepInput {
|
||||
})
|
||||
stepType: WorkflowActionType;
|
||||
|
||||
@Field(() => UUIDScalarType, {
|
||||
// Typed String as it can be 'trigger'
|
||||
@Field(() => String, {
|
||||
description: 'Parent step ID',
|
||||
nullable: true,
|
||||
})
|
||||
@@ -28,4 +30,10 @@ export class CreateWorkflowVersionStepInput {
|
||||
nullable: true,
|
||||
})
|
||||
nextStepId?: string;
|
||||
|
||||
@Field(() => WorkflowStepPositionInput, {
|
||||
description: 'Step position',
|
||||
nullable: true,
|
||||
})
|
||||
position?: WorkflowStepPositionInput;
|
||||
}
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
@InputType()
|
||||
export class WorkflowStepPositionInput {
|
||||
@Field(() => Number)
|
||||
x: number;
|
||||
|
||||
@Field(() => Number)
|
||||
y: number;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user