diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index b07d44b765..bc95e7d596 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -367,6 +367,7 @@ export type Query = { isMaintenanceModeBannerDismissed: Scalars['Boolean']['output']; search: SearchResultConnection; workflowStepConnectedAccountHandle?: Maybe; + workflowVersionContent: WorkflowVersionContent; }; @@ -442,6 +443,11 @@ export type QueryWorkflowStepConnectedAccountHandleArgs = { connectedAccountId: Scalars['UUID']['input']; }; + +export type QueryWorkflowVersionContentArgs = { + workflowVersionId: Scalars['UUID']['input']; +}; + export type RunWorkflowVersion = { __typename?: 'RunWorkflowVersion'; workflowRunId: Scalars['UUID']['output']; @@ -705,6 +711,13 @@ export type WorkflowStepPositionUpdateInput = { position: WorkflowStepPositionInput; }; +export type WorkflowVersionContent = { + __typename?: 'WorkflowVersionContent'; + steps?: Maybe; + trigger?: Maybe; + workflowVersionId: Scalars['UUID']['output']; +}; + export type WorkflowVersionDto = { __typename?: 'WorkflowVersionDTO'; createdAt: Scalars['String']['output']; @@ -916,6 +929,13 @@ export type UpdateWorkflowVersionPositionsMutationVariables = Exact<{ export type UpdateWorkflowVersionPositionsMutation = { __typename?: 'Mutation', updateWorkflowVersionPositions: boolean }; +export type GetWorkflowVersionContentQueryVariables = Exact<{ + workflowVersionId: Scalars['UUID']['input']; +}>; + + +export type GetWorkflowVersionContentQuery = { __typename?: 'Query', workflowVersionContent: { __typename?: 'WorkflowVersionContent', workflowVersionId: any, trigger?: any | null, steps?: any | null } }; + export const TimelineCalendarEventParticipantFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventParticipantFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEventParticipant"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceMemberId"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}}]} as unknown as DocumentNode; export const TimelineCalendarEventCallRecordingFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventCallRecordingFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEventCallRecording"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}}]}}]} as unknown as DocumentNode; export const TimelineCalendarEventFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"location"}},{"kind":"Field","name":{"kind":"Name","value":"startsAt"}},{"kind":"Field","name":{"kind":"Name","value":"endsAt"}},{"kind":"Field","name":{"kind":"Name","value":"isFullDay"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"participants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineCalendarEventParticipantFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"callRecordings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TimelineCalendarEventCallRecordingFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventParticipantFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEventParticipant"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceMemberId"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TimelineCalendarEventCallRecordingFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"TimelineCalendarEventCallRecording"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}}]}}]} as unknown as DocumentNode; @@ -946,4 +966,5 @@ export const UpdateWorkflowVersionTriggerDocument = {"kind":"Document","definiti export const WorkflowStepConnectedAccountHandleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"WorkflowStepConnectedAccountHandle"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workflowStepConnectedAccountHandle"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}}]}}]}}]} as unknown as DocumentNode; export const SubmitFormStepDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SubmitFormStep"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubmitFormStepInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"submitFormStep"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode; export const TestHttpRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TestHttpRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"TestHttpRequestInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"testHttpRequest"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"result"}},{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"statusText"}},{"kind":"Field","name":{"kind":"Name","value":"headers"}}]}}]}}]} as unknown as DocumentNode; -export const UpdateWorkflowVersionPositionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkflowVersionPositions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateWorkflowVersionPositionsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateWorkflowVersionPositions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const UpdateWorkflowVersionPositionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkflowVersionPositions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateWorkflowVersionPositionsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateWorkflowVersionPositions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode; +export const GetWorkflowVersionContentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkflowVersionContent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workflowVersionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workflowVersionContent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workflowVersionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workflowVersionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workflowVersionId"}},{"kind":"Field","name":{"kind":"Name","value":"trigger"}},{"kind":"Field","name":{"kind":"Name","value":"steps"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/TidyUpWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/TidyUpWorkflowSingleRecordCommand.tsx index cd4abf430b..0acad20391 100644 --- a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/TidyUpWorkflowSingleRecordCommand.tsx +++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/TidyUpWorkflowSingleRecordCommand.tsx @@ -12,10 +12,10 @@ export const TidyUpWorkflowSingleRecordCommand = () => { const { selectedRecords } = useHeadlessCommandContextApi(); const recordId = selectedRecords[0]?.id; - const { tidyUpWorkflowVersion } = useTidyUpWorkflowVersion(); const instanceId = getWorkflowVisualizerComponentInstanceId({ recordId: recordId ?? '', }); + const { tidyUpWorkflowVersion } = useTidyUpWorkflowVersion(instanceId); const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersionOrThrow(instanceId); @@ -35,7 +35,14 @@ export const TidyUpWorkflowSingleRecordCommand = () => { const workflowVersionId = await getUpdatableWorkflowVersion(); - await tidyUpWorkflowVersion(workflowVersionId, workflowDiagram); + const tidiedUpDiagram = await tidyUpWorkflowVersion( + workflowVersionId, + workflowDiagram, + ); + + if (isDefined(tidiedUpDiagram)) { + store.set(workflowDiagramAtom, tidiedUpDiagram); + } }; return ; diff --git a/packages/twenty-front/src/modules/side-panel/components/SidePanelWorkflowStepInfo.tsx b/packages/twenty-front/src/modules/side-panel/components/SidePanelWorkflowStepInfo.tsx index 137a278511..685b1a0c19 100644 --- a/packages/twenty-front/src/modules/side-panel/components/SidePanelWorkflowStepInfo.tsx +++ b/packages/twenty-front/src/modules/side-panel/components/SidePanelWorkflowStepInfo.tsx @@ -59,7 +59,8 @@ export const SidePanelWorkflowStepInfo = ({ const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersionOrThrow(instanceId); - const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep(); + const { updateWorkflowVersionStep } = + useUpdateWorkflowVersionStep(instanceId); const { updateTrigger } = useUpdateWorkflowVersionTrigger(instanceId); const { diff --git a/packages/twenty-front/src/modules/ui/utilities/state/jotai/hooks/useAtomFamilyState.ts b/packages/twenty-front/src/modules/ui/utilities/state/jotai/hooks/useAtomFamilyState.ts new file mode 100644 index 0000000000..82079e3ee4 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/state/jotai/hooks/useAtomFamilyState.ts @@ -0,0 +1,13 @@ +import { useAtom } from 'jotai'; + +import { type FamilyState } from '@/ui/utilities/state/jotai/types/FamilyState'; + +export const useAtomFamilyState = ( + familyState: FamilyState, + familyKey: FamilyKey, +): [ + ValueType, + (value: ValueType | ((prev: ValueType) => ValueType)) => void, +] => { + return useAtom(familyState.atomFamily(familyKey)); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx index 90454cee61..7ffe7a8590 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramEffect.tsx @@ -1,15 +1,21 @@ import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { useAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyState'; import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { flowComponentState } from '@/workflow/states/flowComponentState'; +import { shouldWorkflowRefetchRequestFamilyState } from '@/workflow/states/shouldWorkflowRefetchRequestFamilyState'; import { workflowLastCreatedStepIdComponentState } from '@/workflow/states/workflowLastCreatedStepIdComponentState'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; -import { type WorkflowVersion } from '@/workflow/types/Workflow'; +import { + type WorkflowAction, + type WorkflowTrigger, +} from '@/workflow/types/Workflow'; import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; +import { useWorkflowVersionContent } from '@/workflow/workflow-version/hooks/useWorkflowVersionContent'; import { useStepsOutputSchema } from '@/workflow/workflow-variables/hooks/useStepsOutputSchema'; -import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram'; +import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram'; import { mergeWorkflowDiagrams } from '@/workflow/workflow-diagram/utils/mergeWorkflowDiagrams'; import { useStore } from 'jotai'; import { useCallback, useEffect, useState } from 'react'; @@ -28,6 +34,7 @@ export const WorkflowDiagramEffect = () => { workflowDiagramComponentState, ); const setFlow = useSetAtomComponentState(flowComponentState); + const flow = useAtomComponentStateValue(flowComponentState); const { populateStepsOutputSchema } = useStepsOutputSchema(); const workflowLastCreatedStepId = useAtomComponentStateCallbackState( @@ -37,14 +44,33 @@ export const WorkflowDiagramEffect = () => { const store = useStore(); const currentVersion = workflowWithCurrentVersion?.currentVersion; - const [previousVersionId, setPreviousVersionId] = useState(); + const { content, refetchContent } = useWorkflowVersionContent( + currentVersion?.id, + ); + + const [shouldWorkflowRefetchRequest, setShouldWorkflowRefetchRequest] = + useAtomFamilyState( + shouldWorkflowRefetchRequestFamilyState, + workflowVisualizerWorkflowId ?? '', + ); + + const [seededVersionId, setSeededVersionId] = useState(); + const [previousDiagramVersionId, setPreviousDiagramVersionId] = + useState(); const computeAndMergeNewWorkflowDiagram = useCallback( - (version: WorkflowVersion, preservePositions: boolean) => { + ( + flowToRender: { + trigger: WorkflowTrigger | null; + steps: WorkflowAction[] | null; + }, + preservePositions: boolean, + ) => { const previousWorkflowDiagram = store.get(workflowDiagram); - const nextWorkflowDiagram = getWorkflowVersionDiagram({ - workflowVersion: version, + const nextWorkflowDiagram = generateWorkflowDiagram({ + trigger: flowToRender.trigger ?? undefined, + steps: flowToRender.steps ?? [], workflowContext: 'workflow', }); @@ -79,33 +105,75 @@ export const WorkflowDiagramEffect = () => { ); useEffect(() => { - if (!isDefined(currentVersion)) { + if (!shouldWorkflowRefetchRequest) { return; } - const isSameVersion = previousVersionId === currentVersion.id; - const isTransitionToDraft = currentVersion.status === 'DRAFT'; - const shouldPreservePositions = isSameVersion || isTransitionToDraft; + setShouldWorkflowRefetchRequest(false); - setPreviousVersionId(currentVersion.id); + void refetchContent() + .then(() => { + setSeededVersionId(undefined); + }) + .catch(() => {}); + }, [ + shouldWorkflowRefetchRequest, + setShouldWorkflowRefetchRequest, + refetchContent, + ]); + + useEffect(() => { + if (!isDefined(currentVersion) || !isDefined(content)) { + return; + } + + if (content.workflowVersionId !== currentVersion.id) { + return; + } + + if (seededVersionId === currentVersion.id) { + return; + } + + setSeededVersionId(currentVersion.id); setFlow({ workflowVersionId: currentVersion.id, - trigger: currentVersion.trigger, - steps: currentVersion.steps, + trigger: content.trigger, + steps: content.steps, }); - - computeAndMergeNewWorkflowDiagram(currentVersion, shouldPreservePositions); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [computeAndMergeNewWorkflowDiagram, setFlow, currentVersion]); + }, [content, currentVersion, seededVersionId, setFlow]); useEffect(() => { - if (!isDefined(currentVersion)) { + if (!isDefined(flow)) { return; } - populateStepsOutputSchema(currentVersion); - }, [currentVersion, populateStepsOutputSchema]); + const isSameVersion = previousDiagramVersionId === flow.workflowVersionId; + const isTransitionToDraft = currentVersion?.status === 'DRAFT'; + const shouldPreservePositions = isSameVersion || isTransitionToDraft; + + setPreviousDiagramVersionId(flow.workflowVersionId); + + computeAndMergeNewWorkflowDiagram(flow, shouldPreservePositions); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [computeAndMergeNewWorkflowDiagram, flow]); + + useEffect(() => { + if (!isDefined(currentVersion) || !isDefined(flow)) { + return; + } + + if (flow.workflowVersionId !== currentVersion.id) { + return; + } + + populateStepsOutputSchema({ + ...currentVersion, + trigger: flow.trigger, + steps: flow.steps, + }); + }, [currentVersion, flow, populateStepsOutputSchema]); return null; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx index 142f6834c6..fbb9d3718e 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect.tsx @@ -4,7 +4,8 @@ import { flowComponentState } from '@/workflow/states/flowComponentState'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState'; import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; -import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram'; +import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram'; +import { useWorkflowVersionContent } from '@/workflow/workflow-version/hooks/useWorkflowVersionContent'; import { useStepsOutputSchema } from '@/workflow/workflow-variables/hooks/useStepsOutputSchema'; import { useEffect } from 'react'; import { isDefined } from 'twenty-shared/utils'; @@ -15,6 +16,7 @@ export const WorkflowVersionVisualizerEffect = ({ workflowVersionId: string; }) => { const workflowVersion = useWorkflowVersion(workflowVersionId); + const { content } = useWorkflowVersionContent(workflowVersionId); const setFlow = useSetAtomComponentState(flowComponentState); const setWorkflowDiagram = useSetAtomComponentState( @@ -30,7 +32,11 @@ export const WorkflowVersionVisualizerEffect = ({ const { populateStepsOutputSchema } = useStepsOutputSchema(); useEffect(() => { - if (!isDefined(workflowVersion)) { + if ( + !isDefined(workflowVersion) || + !isDefined(content) || + content.workflowVersionId !== workflowVersion.id + ) { setFlow(undefined); return; @@ -38,13 +44,14 @@ export const WorkflowVersionVisualizerEffect = ({ setFlow({ workflowVersionId: workflowVersion.id, - trigger: workflowVersion.trigger, - steps: workflowVersion.steps, + trigger: content.trigger, + steps: content.steps, }); setWorkflowVisualizerWorkflowId(workflowVersion.workflowId); setWorkflowVisualizerWorkflowVersionId(workflowVersion.id); }, [ + content, setFlow, setWorkflowVisualizerWorkflowId, setWorkflowVisualizerWorkflowVersionId, @@ -52,27 +59,39 @@ export const WorkflowVersionVisualizerEffect = ({ ]); useEffect(() => { - if (!isDefined(workflowVersion)) { + if ( + !isDefined(content) || + content.workflowVersionId !== workflowVersionId + ) { setWorkflowDiagram(undefined); return; } - const nextWorkflowDiagram = getWorkflowVersionDiagram({ - workflowVersion, + const nextWorkflowDiagram = generateWorkflowDiagram({ + trigger: content.trigger ?? undefined, + steps: content.steps ?? [], workflowContext: 'workflow-version', }); setWorkflowDiagram(nextWorkflowDiagram); - }, [setWorkflowDiagram, workflowVersion]); + }, [content, setWorkflowDiagram, workflowVersionId]); useEffect(() => { - if (!isDefined(workflowVersion)) { + if ( + !isDefined(workflowVersion) || + !isDefined(content) || + content.workflowVersionId !== workflowVersion.id + ) { return; } - populateStepsOutputSchema(workflowVersion); - }, [populateStepsOutputSchema, workflowVersion]); + populateStepsOutputSchema({ + ...workflowVersion, + trigger: content.trigger, + steps: content.steps, + }); + }, [content, populateStepsOutputSchema, workflowVersion]); return null; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useUpdateWorkflowVersionStep.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useUpdateWorkflowVersionStep.test.ts index 8dae10b4f9..0747e5d19f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useUpdateWorkflowVersionStep.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useUpdateWorkflowVersionStep.test.ts @@ -1,3 +1,5 @@ +import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; +import { createElement, type ReactNode } from 'react'; import { useUpdateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep'; import { act, renderHook } from '@testing-library/react'; @@ -44,6 +46,13 @@ jest.mock('@apollo/client/react', () => ({ useMutation: () => [mockMutate], })); +const Wrapper = ({ children }: { children: ReactNode }) => + createElement( + WorkflowVisualizerComponentInstanceContext.Provider, + { value: { instanceId: 'workflow-visualizer-test' } }, + children, + ); + describe('useUpdateWorkflowVersionStep', () => { beforeEach(() => { jest.clearAllMocks(); @@ -65,7 +74,9 @@ describe('useUpdateWorkflowVersionStep', () => { steps: [{ id: 'step-1', name: 'Create Record', type: 'CREATE_RECORD' }], }); - const { result } = renderHook(() => useUpdateWorkflowVersionStep()); + const { result } = renderHook(() => useUpdateWorkflowVersionStep(), { + wrapper: Wrapper, + }); await act(async () => { await result.current.updateWorkflowVersionStep({ @@ -83,7 +94,9 @@ describe('useUpdateWorkflowVersionStep', () => { it('should not mark step for recomputation when mutation returns no data', async () => { mockMutate.mockResolvedValue({ data: null }); - const { result } = renderHook(() => useUpdateWorkflowVersionStep()); + const { result } = renderHook(() => useUpdateWorkflowVersionStep(), { + wrapper: Wrapper, + }); await act(async () => { await result.current.updateWorkflowVersionStep({ @@ -108,7 +121,9 @@ describe('useUpdateWorkflowVersionStep', () => { mockGetRecordFromCache.mockReturnValue(undefined); - const { result } = renderHook(() => useUpdateWorkflowVersionStep()); + const { result } = renderHook(() => useUpdateWorkflowVersionStep(), { + wrapper: Wrapper, + }); await act(async () => { await result.current.updateWorkflowVersionStep({ diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useApplyWorkflowVersionStepChanges.ts similarity index 75% rename from packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache.ts rename to packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useApplyWorkflowVersionStepChanges.ts index df26b17f68..6e3636bc9a 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useApplyWorkflowVersionStepChanges.ts @@ -5,11 +5,13 @@ import { CoreObjectNameSingular } from 'twenty-shared/types'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; +import { flowComponentState } from '@/workflow/states/flowComponentState'; import { type WorkflowVersion } from '@/workflow/types/Workflow'; import { applyDiff, isDefined } from 'twenty-shared/utils'; import { type WorkflowVersionStepChanges } from '~/generated/graphql'; -export const useUpdateWorkflowVersionCache = () => { +export const useApplyWorkflowVersionStepChanges = (instanceId?: string) => { const apolloCoreClient = useApolloCoreClient(); const { objectMetadataItems } = useObjectMetadataItems(); @@ -24,7 +26,9 @@ export const useUpdateWorkflowVersionCache = () => { objectNameSingular: CoreObjectNameSingular.WorkflowVersion, }); - const updateWorkflowVersionCache = ({ + const setFlow = useSetAtomComponentState(flowComponentState, instanceId); + + const applyWorkflowVersionStepChanges = ({ workflowVersionStepChanges, workflowVersionId, }: { @@ -35,14 +39,27 @@ export const useUpdateWorkflowVersionCache = () => { return; } + const { triggerDiff, stepsDiff } = workflowVersionStepChanges; + + setFlow((currentFlow) => { + if (!isDefined(currentFlow)) { + return currentFlow; + } + + return { + workflowVersionId, + trigger: applyDiff({ trigger: currentFlow.trigger }, triggerDiff) + .trigger, + steps: applyDiff({ steps: currentFlow.steps }, stepsDiff).steps, + }; + }); + const cachedRecord = getRecordFromCache(workflowVersionId); if (!isDefined(cachedRecord)) { return; } - const { triggerDiff, stepsDiff } = workflowVersionStepChanges; - const newCachedRecord = { ...cachedRecord, steps: applyDiff({ steps: cachedRecord.steps }, stepsDiff).steps, @@ -67,5 +84,5 @@ export const useUpdateWorkflowVersionCache = () => { return newCachedRecord; }; - return { updateWorkflowVersionCache }; + return { applyWorkflowVersionStepChanges }; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateWorkflowVersionEdge.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateWorkflowVersionEdge.ts index 656499430a..0ff125cca6 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateWorkflowVersionEdge.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateWorkflowVersionEdge.ts @@ -7,12 +7,13 @@ import { type CreateWorkflowVersionEdgeInput, } from '~/generated/graphql'; import { CREATE_WORKFLOW_VERSION_EDGE } from '@/workflow/graphql/mutations/createWorkflowVersionEdge'; -import { useUpdateWorkflowVersionCache } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache'; +import { useApplyWorkflowVersionStepChanges } from '@/workflow/workflow-steps/hooks/useApplyWorkflowVersionStepChanges'; export const useCreateWorkflowVersionEdge = () => { const apolloCoreClient = useApolloCoreClient(); - const { updateWorkflowVersionCache } = useUpdateWorkflowVersionCache(); + const { applyWorkflowVersionStepChanges } = + useApplyWorkflowVersionStepChanges(); const { enqueueErrorSnackBar } = useSnackBar(); const [mutate] = useMutation< @@ -32,7 +33,7 @@ export const useCreateWorkflowVersionEdge = () => { const workflowVersionStepChanges = result?.data?.createWorkflowVersionEdge; - updateWorkflowVersionCache({ + applyWorkflowVersionStepChanges({ workflowVersionStepChanges, workflowVersionId: input.workflowVersionId, }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep.ts index 134302452f..a21d2359c1 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep.ts @@ -7,17 +7,14 @@ import { type CreateWorkflowVersionStepMutation, type CreateWorkflowVersionStepMutationVariables, } from '~/generated/graphql'; -import { useUpdateWorkflowVersionCache } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache'; -import { flowComponentState } from '@/workflow/states/flowComponentState'; -import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; -import { isDefined } from 'twenty-shared/utils'; +import { useApplyWorkflowVersionStepChanges } from '@/workflow/workflow-steps/hooks/useApplyWorkflowVersionStepChanges'; export const useCreateWorkflowVersionStep = () => { const apolloCoreClient = useApolloCoreClient(); - const { updateWorkflowVersionCache } = useUpdateWorkflowVersionCache(); + const { applyWorkflowVersionStepChanges } = + useApplyWorkflowVersionStepChanges(); - const setFlow = useSetAtomComponentState(flowComponentState); const { enqueueErrorSnackBar } = useSnackBar(); const [mutate] = useMutation< @@ -39,19 +36,11 @@ export const useCreateWorkflowVersionStep = () => { const workflowVersionStepChanges = result?.data?.createWorkflowVersionStep; - const updatedWorkflowVersion = updateWorkflowVersionCache({ + applyWorkflowVersionStepChanges({ workflowVersionStepChanges, workflowVersionId: input.workflowVersionId, }); - if (isDefined(updatedWorkflowVersion)) { - setFlow({ - workflowVersionId: updatedWorkflowVersion.id, - trigger: updatedWorkflowVersion.trigger, - steps: updatedWorkflowVersion.steps, - }); - } - return result; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteWorkflowVersionEdge.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteWorkflowVersionEdge.ts index 6e2d88069b..9316716441 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteWorkflowVersionEdge.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteWorkflowVersionEdge.ts @@ -6,12 +6,13 @@ import { type DeleteWorkflowVersionEdgeMutationVariables, } from '~/generated/graphql'; import { DELETE_WORKFLOW_VERSION_EDGE } from '@/workflow/graphql/mutations/deleteWorkflowVersionEdge'; -import { useUpdateWorkflowVersionCache } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache'; +import { useApplyWorkflowVersionStepChanges } from '@/workflow/workflow-steps/hooks/useApplyWorkflowVersionStepChanges'; export const useDeleteWorkflowVersionEdge = () => { const apolloCoreClient = useApolloCoreClient(); - const { updateWorkflowVersionCache } = useUpdateWorkflowVersionCache(); + const { applyWorkflowVersionStepChanges } = + useApplyWorkflowVersionStepChanges(); const [mutate] = useMutation< DeleteWorkflowVersionEdgeMutation, @@ -25,7 +26,7 @@ export const useDeleteWorkflowVersionEdge = () => { const workflowVersionStepChanges = result?.data?.deleteWorkflowVersionEdge; - updateWorkflowVersionCache({ + applyWorkflowVersionStepChanges({ workflowVersionStepChanges, workflowVersionId: input.workflowVersionId, }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep.ts index fa2dd27209..73b2bcf317 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep.ts @@ -2,7 +2,7 @@ import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient import { CoreObjectNameSingular } from 'twenty-shared/types'; import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQuery'; import { DELETE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/deleteWorkflowVersionStep'; -import { useUpdateWorkflowVersionCache } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache'; +import { useApplyWorkflowVersionStepChanges } from '@/workflow/workflow-steps/hooks/useApplyWorkflowVersionStepChanges'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useMutation } from '@apollo/client/react'; import { @@ -14,7 +14,8 @@ import { export const useDeleteWorkflowVersionStep = () => { const apolloCoreClient = useApolloCoreClient(); - const { updateWorkflowVersionCache } = useUpdateWorkflowVersionCache(); + const { applyWorkflowVersionStepChanges } = + useApplyWorkflowVersionStepChanges(); const { enqueueErrorSnackBar } = useSnackBar(); const { findOneRecordQuery: findOneWorkflowVersionQuery } = @@ -48,7 +49,7 @@ export const useDeleteWorkflowVersionStep = () => { const workflowVersionStepChanges = result?.data?.deleteWorkflowVersionStep; - updateWorkflowVersionCache({ + applyWorkflowVersionStepChanges({ workflowVersionStepChanges, workflowVersionId: input.workflowVersionId, }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDuplicateWorkflowVersionStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDuplicateWorkflowVersionStep.ts index efa64f4e67..73a0539421 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDuplicateWorkflowVersionStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDuplicateWorkflowVersionStep.ts @@ -1,11 +1,8 @@ import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; import { DUPLICATE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/duplicateWorkflowVersionStep'; -import { flowComponentState } from '@/workflow/states/flowComponentState'; -import { useUpdateWorkflowVersionCache } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache'; +import { useApplyWorkflowVersionStepChanges } from '@/workflow/workflow-steps/hooks/useApplyWorkflowVersionStepChanges'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useMutation } from '@apollo/client/react'; -import { isDefined } from 'twenty-shared/utils'; import { type DuplicateWorkflowVersionStepInput, type DuplicateWorkflowVersionStepMutation, @@ -15,9 +12,9 @@ import { export const useDuplicateWorkflowVersionStep = () => { const apolloCoreClient = useApolloCoreClient(); - const { updateWorkflowVersionCache } = useUpdateWorkflowVersionCache(); + const { applyWorkflowVersionStepChanges } = + useApplyWorkflowVersionStepChanges(); - const setFlow = useSetAtomComponentState(flowComponentState); const { enqueueErrorSnackBar } = useSnackBar(); const [mutate] = useMutation< @@ -40,19 +37,11 @@ export const useDuplicateWorkflowVersionStep = () => { const workflowVersionStepChanges = result?.data?.duplicateWorkflowVersionStep; - const updatedWorkflowVersion = updateWorkflowVersionCache({ + applyWorkflowVersionStepChanges({ workflowVersionStepChanges, workflowVersionId: input.workflowVersionId, }); - if (isDefined(updatedWorkflowVersion)) { - setFlow({ - workflowVersionId: updatedWorkflowVersion.id, - trigger: updatedWorkflowVersion.trigger, - steps: updatedWorkflowVersion.steps, - }); - } - return result; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep.ts index 6adeaf3716..0b338cb6a4 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep.ts @@ -5,6 +5,8 @@ import { CoreObjectNameSingular } from 'twenty-shared/types'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; +import { flowComponentState } from '@/workflow/states/flowComponentState'; import { UPDATE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/updateWorkflowVersionStep'; import { type WorkflowVersion, @@ -20,12 +22,13 @@ import { type UpdateWorkflowVersionStepMutationVariables, } from '~/generated/graphql'; -export const useUpdateWorkflowVersionStep = () => { +export const useUpdateWorkflowVersionStep = (instanceId?: string) => { const apolloCoreClient = useApolloCoreClient(); const { objectMetadataItems } = useObjectMetadataItems(); const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); const { enqueueErrorSnackBar } = useSnackBar(); const { markStepForRecomputation } = useStepsOutputSchema(); + const setFlow = useSetAtomComponentState(flowComponentState, instanceId); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular: CoreObjectNameSingular.WorkflowVersion, @@ -59,6 +62,20 @@ export const useUpdateWorkflowVersionStep = () => { workflowVersionId: input.workflowVersionId, }); + setFlow((currentFlow) => { + if (!isDefined(currentFlow)) { + return currentFlow; + } + + return { + ...currentFlow, + workflowVersionId: input.workflowVersionId, + steps: (currentFlow.steps ?? []).map((step) => + step.id === updatedStep.id ? updatedStep : step, + ), + }; + }); + const cachedRecord = getRecordFromCache( input.workflowVersionId, ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/hooks/__tests__/useUpdateWorkflowVersionTrigger.test.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/hooks/__tests__/useUpdateWorkflowVersionTrigger.test.ts index 03d055d86f..85d367b2bf 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/hooks/__tests__/useUpdateWorkflowVersionTrigger.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/hooks/__tests__/useUpdateWorkflowVersionTrigger.test.ts @@ -1,3 +1,5 @@ +import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; +import { createElement, type ReactNode } from 'react'; import { type WorkflowTrigger } from '@/workflow/types/Workflow'; import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger'; import { act, renderHook } from '@testing-library/react'; @@ -53,6 +55,13 @@ jest.mock('@apollo/client/react', () => ({ useMutation: () => [mockMutate], })); +const Wrapper = ({ children }: { children: ReactNode }) => + createElement( + WorkflowVisualizerComponentInstanceContext.Provider, + { value: { instanceId: 'workflow-visualizer-test' } }, + children, + ); + describe('useUpdateWorkflowVersionTrigger', () => { const trigger: WorkflowTrigger = { name: 'Company created', @@ -75,7 +84,9 @@ describe('useUpdateWorkflowVersionTrigger', () => { it('updates the trigger via the dedicated mutation and marks it for recomputation', async () => { mockGetUpdatableWorkflowVersion.mockResolvedValue('version-id'); - const { result } = renderHook(() => useUpdateWorkflowVersionTrigger()); + const { result } = renderHook(() => useUpdateWorkflowVersionTrigger(), { + wrapper: Wrapper, + }); await act(async () => { await result.current.updateTrigger(trigger); @@ -114,7 +125,9 @@ describe('useUpdateWorkflowVersionTrigger', () => { nextStepIds: [], } as unknown as WorkflowTrigger; - const { result } = renderHook(() => useUpdateWorkflowVersionTrigger()); + const { result } = renderHook(() => useUpdateWorkflowVersionTrigger(), { + wrapper: Wrapper, + }); await act(async () => { await result.current.updateTrigger(testTrigger); diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger.ts index be3ae856ed..fb35a88dc6 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger.ts @@ -4,6 +4,8 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; +import { flowComponentState } from '@/workflow/states/flowComponentState'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { UPDATE_WORKFLOW_VERSION_TRIGGER } from '@/workflow/graphql/mutations/updateWorkflowVersionTrigger'; import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow'; @@ -39,6 +41,8 @@ export const useUpdateWorkflowVersionTrigger = (instanceId?: string) => { objectNameSingular: CoreObjectNameSingular.WorkflowVersion, }); + const setFlow = useSetAtomComponentState(flowComponentState, instanceId); + const [mutate] = useMutation< UpdateWorkflowVersionTriggerMutation, UpdateWorkflowVersionTriggerMutationVariables @@ -70,6 +74,14 @@ export const useUpdateWorkflowVersionTrigger = (instanceId?: string) => { workflowVersionId, }); + setFlow((currentFlow) => { + if (!isDefined(currentFlow)) { + return currentFlow; + } + + return { ...currentFlow, workflowVersionId, trigger: updatedTrigger }; + }); + const cachedRecord = getRecordFromCache(workflowVersionId); if (!isDefined(cachedRecord)) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-version/graphql/queries/getWorkflowVersionContent.ts b/packages/twenty-front/src/modules/workflow/workflow-version/graphql/queries/getWorkflowVersionContent.ts new file mode 100644 index 0000000000..952f258f7c --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-version/graphql/queries/getWorkflowVersionContent.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; + +export const GET_WORKFLOW_VERSION_CONTENT = gql` + query GetWorkflowVersionContent($workflowVersionId: UUID!) { + workflowVersionContent(workflowVersionId: $workflowVersionId) { + workflowVersionId + trigger + steps + } + } +`; diff --git a/packages/twenty-front/src/modules/workflow/workflow-version/hooks/useTidyUpWorkflowVersion.ts b/packages/twenty-front/src/modules/workflow/workflow-version/hooks/useTidyUpWorkflowVersion.ts index 9973734c5f..45a191ac50 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-version/hooks/useTidyUpWorkflowVersion.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-version/hooks/useTidyUpWorkflowVersion.ts @@ -14,10 +14,12 @@ import { CoreObjectNameSingular } from 'twenty-shared/types'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; +import { flowComponentState } from '@/workflow/states/flowComponentState'; import { getOrganizedDiagram } from '@/workflow/workflow-diagram/utils/getOrganizedDiagram'; import { UPDATE_WORKFLOW_VERSION_POSITIONS } from '@/workflow/workflow-version/graphql/mutations/updateWorkflowVersionPositions'; -export const useTidyUpWorkflowVersion = () => { +export const useTidyUpWorkflowVersion = (instanceId?: string) => { const apolloCoreClient = useApolloCoreClient(); const { objectMetadataItems } = useObjectMetadataItems(); @@ -30,6 +32,8 @@ export const useTidyUpWorkflowVersion = () => { objectNameSingular: CoreObjectNameSingular.WorkflowVersion, }); + const setFlow = useSetAtomComponentState(flowComponentState, instanceId); + const [mutate] = useMutation< UpdateWorkflowVersionPositionsMutation, UpdateWorkflowVersionPositionsMutationVariables @@ -41,6 +45,38 @@ export const useTidyUpWorkflowVersion = () => { ) => { await mutate({ variables: { input: { workflowVersionId, positions } } }); + setFlow((currentFlow) => { + if (!isDefined(currentFlow)) { + return currentFlow; + } + + const triggerPositionInFlow = positions.find( + (position) => position.id === 'trigger', + ); + + return { + ...currentFlow, + workflowVersionId, + trigger: + isDefined(triggerPositionInFlow) && isDefined(currentFlow.trigger) + ? { + ...currentFlow.trigger, + position: triggerPositionInFlow.position, + } + : currentFlow.trigger, + steps: + currentFlow.steps?.map((step) => { + const stepPosition = positions.find( + (position) => position.id === step.id, + ); + + return isDefined(stepPosition) + ? { ...step, position: stepPosition.position } + : step; + }) ?? null, + }; + }); + const cachedRecord = getRecordFromCache(workflowVersionId); if (!isDefined(cachedRecord)) { diff --git a/packages/twenty-front/src/modules/workflow/workflow-version/hooks/useWorkflowVersionContent.ts b/packages/twenty-front/src/modules/workflow/workflow-version/hooks/useWorkflowVersionContent.ts new file mode 100644 index 0000000000..050c676f9b --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-version/hooks/useWorkflowVersionContent.ts @@ -0,0 +1,71 @@ +import { useQuery } from '@apollo/client/react'; +import { CoreObjectNameSingular } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; +import { type WorkflowVersion } from '@/workflow/types/Workflow'; +import { GET_WORKFLOW_VERSION_CONTENT } from '@/workflow/workflow-version/graphql/queries/getWorkflowVersionContent'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { FeatureFlagKey } from '~/generated-metadata/graphql'; + +export type WorkflowVersionContent = { + workflowVersionId: string; + trigger: WorkflowVersion['trigger']; + steps: WorkflowVersion['steps']; +}; + +export const useWorkflowVersionContent = (workflowVersionId?: string) => { + const apolloCoreClient = useApolloCoreClient(); + const isWorkflowVersionInCoreEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_WORKFLOW_VERSION_IN_CORE_ENABLED, + ); + + const { + data: coreData, + loading: coreLoading, + refetch: refetchCore, + } = useQuery<{ + workflowVersionContent: WorkflowVersionContent; + }>(GET_WORKFLOW_VERSION_CONTENT, { + client: apolloCoreClient, + variables: { workflowVersionId }, + skip: !isWorkflowVersionInCoreEnabled || !isDefined(workflowVersionId), + }); + + const { + record: workflowVersionRecord, + loading: recordLoading, + refetch: refetchRecord, + } = useFindOneRecord({ + objectNameSingular: CoreObjectNameSingular.WorkflowVersion, + objectRecordId: workflowVersionId, + recordGqlFields: { + id: true, + trigger: true, + steps: true, + }, + skip: isWorkflowVersionInCoreEnabled || !isDefined(workflowVersionId), + }); + + const content: WorkflowVersionContent | undefined = + isWorkflowVersionInCoreEnabled + ? coreData?.workflowVersionContent + : isDefined(workflowVersionRecord) + ? { + workflowVersionId: workflowVersionRecord.id, + trigger: workflowVersionRecord.trigger, + steps: workflowVersionRecord.steps, + } + : undefined; + + const refetchContent = isWorkflowVersionInCoreEnabled + ? refetchCore + : refetchRecord; + + return { + content, + loading: isWorkflowVersionInCoreEnabled ? coreLoading : recordLoading, + refetchContent, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-version-content.dto.ts b/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-version-content.dto.ts new file mode 100644 index 0000000000..9cd2b32ca9 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/dtos/workflow-version-content.dto.ts @@ -0,0 +1,19 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import GraphQLJSON from 'graphql-type-json'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; +import { WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; + +@ObjectType('WorkflowVersionContent') +export class WorkflowVersionContentDTO { + @Field(() => UUIDScalarType) + workflowVersionId: string; + + @Field(() => GraphQLJSON, { nullable: true }) + trigger: WorkflowTrigger | null; + + @Field(() => GraphQLJSON, { nullable: true }) + steps: WorkflowAction[] | null; +} diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version.resolver.ts index 256faa835e..1df26c4b75 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version.resolver.ts @@ -1,14 +1,16 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; -import { Args, Mutation } from '@nestjs/graphql'; +import { Args, Mutation, Query } from '@nestjs/graphql'; import { PermissionFlagType } from 'twenty-shared/constants'; import { CoreResolver } from 'src/engine/api/graphql/graphql-config/decorators/core-resolver.decorator'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; import { CreateDraftFromWorkflowVersionInput } from 'src/engine/core-modules/workflow/dtos/create-draft-from-workflow-version.input'; import { DuplicateWorkflowInput } from 'src/engine/core-modules/workflow/dtos/duplicate-workflow.input'; import { UpdateWorkflowVersionPositionsInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-positions.input'; +import { WorkflowVersionContentDTO } from 'src/engine/core-modules/workflow/dtos/workflow-version-content.dto'; import { WorkflowVersionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-version.dto'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; @@ -16,6 +18,7 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; +import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowVersionWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service'; @CoreResolver() @@ -32,8 +35,28 @@ import { WorkflowVersionWorkspaceService } from 'src/modules/workflow/workflow-b export class WorkflowVersionResolver { constructor( private readonly workflowVersionWorkspaceService: WorkflowVersionWorkspaceService, + private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} + @Query(() => WorkflowVersionContentDTO) + async workflowVersionContent( + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, + @Args('workflowVersionId', { type: () => UUIDScalarType }) + workflowVersionId: string, + ): Promise { + const workflowVersion = + await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ + workspaceId, + workflowVersionId, + }); + + return { + workflowVersionId, + trigger: workflowVersion.trigger, + steps: workflowVersion.steps, + }; + } + @Mutation(() => WorkflowVersionDTO) async createDraftFromWorkflowVersion( @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, diff --git a/packages/twenty-server/test/integration/graphql/suites/workflow/workflow-version-content.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/workflow/workflow-version-content.integration-spec.ts new file mode 100644 index 0000000000..3f0b5e4f8f --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/workflow/workflow-version-content.integration-spec.ts @@ -0,0 +1,97 @@ +import request from 'supertest'; +import { updateWorkflowVersionTrigger } from 'test/integration/graphql/suites/workflow/utils/update-workflow-version-trigger.util'; + +const client = request(`http://localhost:${APP_PORT}`); + +const graphql = (query: string, variables?: object) => + client + .post('/graphql') + .set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`) + .send({ query, variables }); + +describe('workflowVersionContent (e2e)', () => { + let workflowId: string; + let workflowVersionId: string; + + beforeAll(async () => { + const createResponse = await graphql(` + mutation { + createWorkflow(data: { name: "Version Content Query" }) { + id + } + } + `); + + expect(createResponse.body.errors).toBeUndefined(); + workflowId = createResponse.body.data.createWorkflow.id; + + const getResponse = await graphql( + ` + query GetWorkflow($id: UUID!) { + workflow(filter: { id: { eq: $id } }) { + versions { + edges { + node { + id + } + } + } + } + } + `, + { id: workflowId }, + ); + + workflowVersionId = + getResponse.body.data.workflow.versions.edges[0].node.id; + + await updateWorkflowVersionTrigger({ + workflowVersionId, + trigger: { + name: 'Content Query Trigger', + type: 'MANUAL', + settings: { outputSchema: {} }, + nextStepIds: [], + position: { x: 0, y: 0 }, + }, + }); + }); + + afterAll(async () => { + if (workflowId) { + await graphql( + ` + mutation DestroyWorkflow($id: ID!) { + destroyWorkflow(id: $id) { + id + } + } + `, + { id: workflowId }, + ); + } + }); + + it('returns the version trigger and steps', async () => { + const contentResponse = await graphql( + ` + query GetWorkflowVersionContent($workflowVersionId: UUID!) { + workflowVersionContent(workflowVersionId: $workflowVersionId) { + workflowVersionId + trigger + steps + } + } + `, + { workflowVersionId }, + ); + + expect(contentResponse.body.errors).toBeUndefined(); + + const content = contentResponse.body.data.workflowVersionContent; + + expect(content.workflowVersionId).toBe(workflowVersionId); + expect(content.trigger.name).toBe('Content Query Trigger'); + expect(content.trigger.type).toBe('MANUAL'); + }); +});