feat(workflow): make the flow atom authoritative for version content, read core behind a flag (#23499)

## What

The frontend half of the workflow-version read switch, plus the small
server query it consumes. Two ideas:

1. **One hook owns where content comes from.**
`useWorkflowVersionContent(workflowVersionId)` returns `{ trigger, steps
}` from the workspace record when `IS_WORKFLOW_VERSION_IN_CORE_ENABLED`
is off (default), and from the new `workflowVersionContent` core query
when on. Switching the source later (core-only, after the column drop)
is a change inside this one hook.
2. **`flowComponentState` becomes authoritative for the builder.** The
canvas, diagram and step output schemas derive from the jotai atom; the
atom is seeded once per version through the hook above; mutations keep
it up to date.

## Why the seeding change is required

Today `WorkflowDiagramEffect` re-seeds the atom from the Apollo record
on **every** `currentVersion` identity change. That has two
consequences:

- Three of the five step/edge hooks (`delete step`, `create edge`,
`delete edge`) never write the atom themselves; they only write the
record and the re-seed papers over it.
- The model breaks the moment content comes from a source mutations do
not write (i.e. core): the stale fetch would be re-applied over every
optimistic edit, and your just-added step would vanish from the canvas.

So the atom is now seeded **once per version**, and
`useUpdateWorkflowVersionCache` applies the mutation's
`stepsDiff`/`triggerDiff` to the atom directly. All five step/edge hooks
get that through their existing call, which closes the three-hook gap in
one move. The step-update, trigger and tidy-up hooks write the atom too.
The record-cache writes are all kept while `trigger`/`steps` still live
on the record (dropped later with the columns).

## The dead wire, now the refresh path

`shouldWorkflowRefetchRequestFamilyState` was set by
`WorkflowSSESubscribeEffect` (reconnect, other-tab create) and
**consumed by nothing**. It is now the external-refresh path: when set,
the builder refetches content and reseeds. Known trade-off: while
connected, another tab's edits no longer live-patch the canvas through
record cache updates (they arrive on reconnect, version switch or
reload). Given concurrent editing of one draft has no conflict handling
anyway, that seemed acceptable; easy to extend the SSE effect to set the
flag on update events if we want live propagation back.

## Untouched by design

- **Run visualizer**: feeds the same atom from the immutable
`workflowRun.state.flow` snapshot; that duality (version content or run
snapshot) is exactly why the atom stays separate from the record store.
- **Version visualizer** (read-only): reseeds on content change, safe
because nothing writes its instance optimistically.
- Peripheral readers of `currentVersion.trigger/steps` (test-workflow
command, headless command enrichment, if-else body, etc.) still read the
record. Correct while dual-writing continues; they move to the content
hook before workspace content writes stop (tracked in the migration
plan).

## Verification

- `nx typecheck` green on both packages; `oxfmt` + `oxlint --type-aware`
green on all 16 changed files
- Front unit tests: 134 suites / 993 tests green (the two hook tests
gained the visualizer instance context their hooks now require)
- New server integration test for `workflowVersionContent`
- **Live click-through pending**: step create/delete/duplicate, edge
create/delete, trigger edit, tidy-up, draft create/discard, activation,
version viewer, run viewer, with the flag off and on. The failure mode
this PR guards against (an edit vanishing from the canvas) does not show
up in typecheck or unit tests.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23499?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Thomas Trompette
2026-07-30 16:00:10 +02:00
committed by GitHub
parent c25ae72914
commit 6447b7f935
22 changed files with 528 additions and 87 deletions
+22 -1
View File
@@ -367,6 +367,7 @@ export type Query = {
isMaintenanceModeBannerDismissed: Scalars['Boolean']['output'];
search: SearchResultConnection;
workflowStepConnectedAccountHandle?: Maybe<ConnectedAccountHandleDto>;
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<Scalars['JSON']['output']>;
trigger?: Maybe<Scalars['JSON']['output']>;
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<TimelineCalendarEventParticipantFragmentFragment, unknown>;
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<TimelineCalendarEventCallRecordingFragmentFragment, unknown>;
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<TimelineCalendarEventFragmentFragment, unknown>;
@@ -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<WorkflowStepConnectedAccountHandleQuery, WorkflowStepConnectedAccountHandleQueryVariables>;
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<SubmitFormStepMutation, SubmitFormStepMutationVariables>;
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<TestHttpRequestMutation, TestHttpRequestMutationVariables>;
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<UpdateWorkflowVersionPositionsMutation, UpdateWorkflowVersionPositionsMutationVariables>;
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<UpdateWorkflowVersionPositionsMutation, UpdateWorkflowVersionPositionsMutationVariables>;
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<GetWorkflowVersionContentQuery, GetWorkflowVersionContentQueryVariables>;
@@ -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 <HeadlessEngineCommandWrapperEffect execute={handleExecute} />;
@@ -59,7 +59,8 @@ export const SidePanelWorkflowStepInfo = ({
const { getUpdatableWorkflowVersion } =
useGetUpdatableWorkflowVersionOrThrow(instanceId);
const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep();
const { updateWorkflowVersionStep } =
useUpdateWorkflowVersionStep(instanceId);
const { updateTrigger } = useUpdateWorkflowVersionTrigger(instanceId);
const {
@@ -0,0 +1,13 @@
import { useAtom } from 'jotai';
import { type FamilyState } from '@/ui/utilities/state/jotai/types/FamilyState';
export const useAtomFamilyState = <ValueType, FamilyKey>(
familyState: FamilyState<ValueType, FamilyKey>,
familyKey: FamilyKey,
): [
ValueType,
(value: ValueType | ((prev: ValueType) => ValueType)) => void,
] => {
return useAtom(familyState.atomFamily(familyKey));
};
@@ -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<string>();
const { content, refetchContent } = useWorkflowVersionContent(
currentVersion?.id,
);
const [shouldWorkflowRefetchRequest, setShouldWorkflowRefetchRequest] =
useAtomFamilyState(
shouldWorkflowRefetchRequestFamilyState,
workflowVisualizerWorkflowId ?? '',
);
const [seededVersionId, setSeededVersionId] = useState<string>();
const [previousDiagramVersionId, setPreviousDiagramVersionId] =
useState<string>();
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;
};
@@ -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;
};
@@ -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({
@@ -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<WorkflowVersion>(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 };
};
@@ -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,
});
@@ -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;
};
@@ -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,
});
@@ -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,
});
@@ -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;
};
@@ -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<WorkflowVersion>(
input.workflowVersionId,
);
@@ -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);
@@ -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<WorkflowVersion>(workflowVersionId);
if (!isDefined(cachedRecord)) {
return;
@@ -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
}
}
`;
@@ -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<WorkflowVersion>(workflowVersionId);
if (!isDefined(cachedRecord)) {
@@ -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<WorkflowVersion>({
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,
};
};
@@ -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;
}
@@ -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<WorkflowVersionContentDTO> {
const workflowVersion =
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
workspaceId,
workflowVersionId,
});
return {
workflowVersionId,
trigger: workflowVersion.trigger,
steps: workflowVersion.steps,
};
}
@Mutation(() => WorkflowVersionDTO)
async createDraftFromWorkflowVersion(
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
@@ -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');
});
});