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:
+8
@@ -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();
|
||||
|
||||
+37
-3
@@ -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 {
|
||||
|
||||
+7
-3
@@ -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)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+9
-9
@@ -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;
|
||||
|
||||
|
||||
+3
-5
@@ -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),
|
||||
},
|
||||
);
|
||||
|
||||
+6
-6
@@ -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({
|
||||
|
||||
+7
-3
@@ -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;
|
||||
|
||||
+3
-7
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user