feat(workflow): route all version content readers through the flag-aware sources (#23583)

## What

Follow-up to #23499. Migrates every remaining reader of
`record.trigger`/`record.steps` so all version content flows through the
flag-aware sources, then removes `trigger`/`steps` from the record field
sets. The record CRUD path no longer carries version content anywhere in
the app.

Reading is still entirely behind `IS_WORKFLOW_VERSION_IN_CORE_ENABLED`:
this PR changes who asks, never where the answer comes from. Flag off
remains record reads (via the content hook's record branch), flag on the
core query.

## Per reader

| reader | now reads |
| --- | --- |
| `WorkflowDiagramCanvasEditable` (connect, drag-stop) | flow atom |
| `useDeleteStep` | flow atom |
| `WorkflowEditActionIfElseBody` (branch cleanup) | flow atom |
| `SidePanelWorkflowCreateStepContent` (parent-step lookup) | flow atom
|
| `SidePanelWorkflowStepInfo` | flow atom (explicit instance id),
falling back to `useWorkflowVersionContent` when the visualizer is not
mounted |
| `TestWorkflowSingleRecordCommand` | `useWorkflowVersionContent`;
`ready` gates on content being loaded |
| headless enrichment hook (imperative) | core content query when the
flag is on, record otherwise |
| `WorkflowRunVisualizerEffect` (step output schemas) | the run snapshot
(`state.flow`), which is what a run should show anyway |

## Field-set slimming

`useWorkflowVersion` and `useWorkflowWithCurrentVersion` stop fetching
`trigger`/`steps` (identity fields only). Three call sites lost their
only reason to call `useWorkflowWithCurrentVersion` and were dropped
entirely. Verified by grep that no `currentVersion.trigger/steps` reads
remain; the only remaining `.trigger`/`.steps` accesses are
argument-taking utils whose callers now pass flow/content-sourced
objects.

## Verification

- `nx typecheck twenty-front` green
- Front tests: 1058 green (the enrichment test gained mocks for the
apollo client and flag its hook now uses)
- Full-tree `oxfmt` + `oxlint --type-aware` green (3 remaining warnings
are pre-existing in unrelated record-field files)
- Live click-through pending, flag off and on

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23583?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 18:57:11 +02:00
committed by GitHub
parent 42de0482d0
commit 53a18d7528
11 changed files with 91 additions and 41 deletions
@@ -37,6 +37,14 @@ const buildBaseContextApi = (
...overrides,
});
jest.mock('@/object-metadata/hooks/useApolloCoreClient', () => ({
useApolloCoreClient: () => ({ query: jest.fn() }),
}));
jest.mock('@/workspace/hooks/useIsFeatureEnabled', () => ({
useIsFeatureEnabled: () => false,
}));
describe('useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -4,11 +4,18 @@ import {
type HeadlessCommandContextApi,
type HeadlessEngineCommandContextApi,
} from '@/command-menu-item/engine-command/types/HeadlessCommandContextApi';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useLazyFindOneRecord } from '@/object-record/hooks/useLazyFindOneRecord';
import { type WorkflowVersion } from '@/workflow/types/Workflow';
import { GET_WORKFLOW_VERSION_CONTENT } from '@/workflow/workflow-version/graphql/queries/getWorkflowVersionContent';
import { type WorkflowVersionContent } from '@/workflow/workflow-version/hooks/useWorkflowVersionContent';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type CommandMenuItemAvailabilityType } from '~/generated-metadata/graphql';
import {
type CommandMenuItemAvailabilityType,
FeatureFlagKey,
} from '~/generated-metadata/graphql';
type WorkflowVersionRecord = Pick<
WorkflowVersion,
@@ -24,12 +31,31 @@ type EnrichParams = {
export const useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation =
() => {
const apolloCoreClient = useApolloCoreClient();
const isWorkflowVersionInCoreEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_VERSION_IN_CORE_ENABLED,
);
const { findOneRecord: findOneWorkflowVersion } =
useLazyFindOneRecord<WorkflowVersionRecord>({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
recordGqlFields: { id: true, workflowId: true, trigger: true },
});
const fetchTriggerFromCore = useCallback(
async (versionId: string) => {
const { data } = await apolloCoreClient.query<{
workflowVersionContent: WorkflowVersionContent;
}>({
query: GET_WORKFLOW_VERSION_CONTENT,
variables: { workflowVersionId: versionId },
});
return data?.workflowVersionContent.trigger ?? null;
},
[apolloCoreClient],
);
const fetchWorkflowVersion = useCallback(
async (versionId: string): Promise<WorkflowVersionRecord | undefined> => {
let record: WorkflowVersionRecord | undefined;
@@ -60,16 +86,24 @@ export const useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInforma
return undefined;
}
const trigger = isWorkflowVersionInCoreEnabled
? await fetchTriggerFromCore(workflowVersionId)
: workflowVersion.trigger;
return {
...headlessEngineCommandContextApi,
workflowId: workflowVersion.workflowId,
workflowVersionId: workflowVersion.id,
trigger: workflowVersion.trigger,
trigger,
availabilityType,
availabilityObjectMetadataId,
};
},
[fetchWorkflowVersion],
[
fetchWorkflowVersion,
fetchTriggerFromCore,
isWorkflowVersionInCoreEnabled,
],
);
return {
@@ -1,6 +1,7 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
import { useWorkflowVersionContent } from '@/workflow/workflow-version/hooks/useWorkflowVersionContent';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { getTestPayloadFromTrigger } from '@/workflow/workflow-trigger/utils/getTestPayloadFromTrigger';
import { isDefined } from 'twenty-shared/utils';
@@ -13,6 +14,9 @@ export const TestWorkflowSingleRecordCommand = () => {
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
recordId ?? '',
);
const { content } = useWorkflowVersionContent(
workflowWithCurrentVersion?.currentVersion.id,
);
if (!isDefined(recordId)) {
throw new Error('Record ID is required to test workflow');
@@ -25,21 +29,21 @@ export const TestWorkflowSingleRecordCommand = () => {
const { currentVersion } = workflowWithCurrentVersion;
if (!isDefined(currentVersion.trigger)) {
if (!isDefined(content?.trigger)) {
return;
}
runWorkflowVersion({
workflowVersionId: currentVersion.id,
workflowId: workflowWithCurrentVersion.id,
payload: getTestPayloadFromTrigger(currentVersion.trigger),
payload: getTestPayloadFromTrigger(content.trigger),
});
};
return (
<HeadlessEngineCommandWrapperEffect
execute={handleExecute}
ready={isDefined(workflowWithCurrentVersion)}
ready={isDefined(workflowWithCurrentVersion) && isDefined(content)}
/>
);
};
@@ -8,6 +8,7 @@ import { TitleInput } from '@/ui/input/components/TitleInput';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { flowComponentState } from '@/workflow/states/flowComponentState';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { getAgentIdFromStep } from '@/workflow/utils/getAgentIdFromStep';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
@@ -15,6 +16,7 @@ import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWo
import { useUpdateAgentLabel } from '@/workflow/workflow-steps/hooks/useUpdateAgentLabel';
import { useUpdateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep';
import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger';
import { useWorkflowVersionContent } from '@/workflow/workflow-version/hooks/useWorkflowVersionContent';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
@@ -63,15 +65,13 @@ export const SidePanelWorkflowStepInfo = ({
useUpdateWorkflowVersionStep(instanceId);
const { updateTrigger } = useUpdateWorkflowVersionTrigger(instanceId);
const {
trigger,
steps,
id: workflowVersionId,
} = workflowWithCurrentVersion?.currentVersion ?? {
trigger: null,
steps: null,
id: undefined,
};
const workflowVersionId = workflowWithCurrentVersion?.currentVersion?.id;
const flow = useAtomComponentStateValue(flowComponentState, instanceId);
const { content } = useWorkflowVersionContent(workflowVersionId);
const trigger = flow?.trigger ?? content?.trigger ?? null;
const steps = flow?.steps ?? content?.steps ?? null;
const isTriggerStep = sidePanelWorkflowStepId === TRIGGER_STEP_ID;
@@ -6,7 +6,7 @@ import {
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { flowComponentState } from '@/workflow/states/flowComponentState';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import {
type WorkflowActionType,
@@ -30,9 +30,7 @@ export const SidePanelWorkflowCreateStepContent = () => {
const { createStep } = useCreateStep();
const { updateStep } = useUpdateStep();
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
workflowVisualizerWorkflowId,
);
const flow = useAtomComponentStateValue(flowComponentState);
const { openWorkflowEditStepInSidePanel } = useSidePanelWorkflowNavigation();
const { closeRightClickMenu } = useCloseRightClickMenu();
@@ -81,7 +79,7 @@ export const SidePanelWorkflowCreateStepContent = () => {
return;
}
const steps = workflowWithCurrentVersion?.currentVersion?.steps;
const steps = flow?.steps;
const parentStep =
isDefined(parentStepId) && isDefined(steps) && isDefined(position)
? steps.find((step) => step.id === parentStepId)
@@ -14,8 +14,6 @@ export const useWorkflowVersion = (workflowVersionId?: string) => {
createdAt: true,
updatedAt: true,
workflowId: true,
trigger: true,
steps: true,
status: true,
workflow: {
id: true,
@@ -55,6 +55,14 @@ export const useWorkflowWithCurrentVersion = (
{
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
objectRecordId: currentVersionId,
recordGqlFields: {
id: true,
name: true,
status: true,
workflowId: true,
createdAt: true,
updatedAt: true,
},
skip: !isDefined(currentVersionId),
},
);
@@ -1,5 +1,6 @@
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { flowComponentState } from '@/workflow/states/flowComponentState';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
@@ -62,8 +63,10 @@ export const WorkflowDiagramCanvasEditable = () => {
const { startNodeCreation } = useStartNodeCreation();
const flow = useAtomComponentStateValue(flowComponentState);
const onConnect = async (edgeConnect: WorkflowConnection) => {
const steps = workflowWithCurrentVersion?.currentVersion?.steps;
const steps = flow?.steps;
const sourceStep = isDefined(steps)
? steps.find((step) => step.id === edgeConnect.source)
: undefined;
@@ -135,10 +138,7 @@ export const WorkflowDiagramCanvasEditable = () => {
};
const onNodeDragStop: OnNodeDrag<WorkflowDiagramNode> = async (_, node) => {
const stepToUpdate =
workflowWithCurrentVersion?.currentVersion?.steps?.find(
(step) => step.id === node.id,
);
const stepToUpdate = flow?.steps?.find((step) => step.id === node.id);
if (isDefined(stepToUpdate)) {
await updateStep({
@@ -149,7 +149,7 @@ export const WorkflowDiagramCanvasEditable = () => {
return;
}
const triggerToUpdate = workflowWithCurrentVersion?.currentVersion?.trigger;
const triggerToUpdate = flow?.trigger;
if (isDefined(triggerToUpdate)) {
await updateTrigger({
@@ -246,12 +246,16 @@ export const WorkflowRunVisualizerEffect = ({
]);
useEffect(() => {
if (!isDefined(workflowVersion)) {
if (!isDefined(workflowVersion) || !isDefined(workflowRun?.state)) {
return;
}
populateStepsOutputSchema(workflowVersion);
}, [populateStepsOutputSchema, workflowVersion]);
populateStepsOutputSchema({
...workflowVersion,
trigger: workflowRun.state.flow.trigger,
steps: workflowRun.state.flow.steps,
});
}, [populateStepsOutputSchema, workflowRun?.state, workflowVersion]);
return null;
};
@@ -1,7 +1,7 @@
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { flowComponentState } from '@/workflow/states/flowComponentState';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { useDeleteWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep';
import { useResetWorkflowAiAgentPermissionsStateOnSidePanelClose } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useResetWorkflowAiAgentPermissionsStateOnSidePanelClose';
@@ -20,12 +20,12 @@ export const useDeleteStep = () => {
const workflowVisualizerWorkflowId = useAtomComponentStateValue(
workflowVisualizerWorkflowIdComponentState,
);
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
const flow = useAtomComponentStateValue(flowComponentState);
const deleteStep = async (stepId: string) => {
const workflowVersionId = await getUpdatableWorkflowVersion();
const steps = workflow?.currentVersion?.steps;
const steps = flow?.steps;
const stepToDelete = isDefined(steps)
? steps.find((step) => step.id === stepId)
: undefined;
@@ -2,8 +2,7 @@ import { InputLabel } from '@/ui/input/components/InputLabel';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { flowComponentState } from '@/workflow/states/flowComponentState';
import {
type WorkflowIfElseAction,
type WorkflowStep,
@@ -66,10 +65,7 @@ export const WorkflowEditActionIfElseBody = ({
const { updateWorkflowVersionPosition } = useTidyUpWorkflowVersion();
const { deleteWorkflowVersionStep } = useDeleteWorkflowVersionStep();
const { deleteStepsOutputSchema } = useStepsOutputSchema();
const workflowVisualizerWorkflowId = useAtomComponentStateValue(
workflowVisualizerWorkflowIdComponentState,
);
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
const flow = useAtomComponentStateValue(flowComponentState);
const currentStepFilters = useAtomComponentStateValue(
currentStepFiltersComponentState,
@@ -146,7 +142,7 @@ export const WorkflowEditActionIfElseBody = ({
await cleanupEmptyChildStepsFromDeletedBranches({
branchesToDelete,
allSteps: workflow?.currentVersion?.steps ?? undefined,
allSteps: flow?.steps ?? undefined,
});
setCurrentStepFilterGroups(updatedStepFilterGroups);