Set steps output schema in a recoil family state (#10688)

- Create a workflow version component family state for each workflow
version : `stepId` => `StepOutputSchema`
- Populate this state when reaching the workflow visualizer of the
workflow version
- Wrap the right drawer when in edit mode with the context. It is the
only one who needs this schema

Next step:
- read this state from the variables
This commit is contained in:
Thomas Trompette
2025-03-06 14:31:35 +01:00
committed by GitHub
parent 17b488dd3b
commit 5ddf7c6475
8 changed files with 195 additions and 81 deletions

View File

@ -0,0 +1,76 @@
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { stepsOutputSchemaComponentFamilyState } from '@/workflow/states/stepsOutputSchemaFamilyState';
import { WorkflowVersion } from '@/workflow/types/Workflow';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import {
OutputSchema,
StepOutputSchema,
} from '@/workflow/workflow-variables/types/StepOutputSchema';
import { getTriggerStepName } from '@/workflow/workflow-variables/utils/getTriggerStepName';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared';
export const usePopulateStepsOutputSchema = ({
workflowVersionId,
}: {
workflowVersionId: string;
}) => {
const stepsOutputSchemaFamilyState = useRecoilComponentCallbackStateV2(
stepsOutputSchemaComponentFamilyState,
workflowVersionId,
);
const populateStepsOutputSchema = useRecoilCallback(
({ set }) =>
(workflowVersion: WorkflowVersion) => {
workflowVersion?.steps?.forEach((step) => {
const stepOutputSchema: StepOutputSchema = {
id: step.id,
name: step.name,
icon: getActionIcon(step.type),
outputSchema: step.settings.outputSchema as OutputSchema,
};
set(stepsOutputSchemaFamilyState(step.id), stepOutputSchema);
});
const trigger = workflowVersion.trigger;
if (isDefined(trigger)) {
const triggerIconKey =
trigger.type === 'DATABASE_EVENT'
? getTriggerIcon({
type: trigger.type,
eventName: splitWorkflowTriggerEventName(
trigger.settings?.eventName,
).event,
})
: getTriggerIcon({
type: trigger.type,
});
const triggerOutputSchema: StepOutputSchema = {
id: TRIGGER_STEP_ID,
name: isDefined(trigger.name)
? trigger.name
: getTriggerStepName(trigger),
icon: triggerIconKey,
outputSchema: trigger.settings.outputSchema as OutputSchema,
};
set(
stepsOutputSchemaFamilyState(TRIGGER_STEP_ID),
triggerOutputSchema,
);
}
},
[stepsOutputSchemaFamilyState],
);
return {
populateStepsOutputSchema,
};
};

View File

@ -0,0 +1,40 @@
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { WorkflowVersionComponentInstanceContext } from '@/workflow/states/context/WorkflowVersionComponentInstanceContext';
import { stepsOutputSchemaComponentFamilyState } from '@/workflow/states/stepsOutputSchemaFamilyState';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared';
export const useStepsOutputSchema = ({
instanceIdFromProps,
}: {
instanceIdFromProps?: string;
}) => {
const instanceId = useAvailableComponentInstanceIdOrThrow(
WorkflowVersionComponentInstanceContext,
instanceIdFromProps,
);
const stepsOutputSchemaFamilyState = useRecoilComponentCallbackStateV2(
stepsOutputSchemaComponentFamilyState,
instanceId,
);
const getStepsOutputSchema = useRecoilCallback(
({ snapshot }) =>
(stepIds: string[]) => {
const stepsOutputSchema = stepIds
.map((stepId) =>
snapshot
.getLoadable(stepsOutputSchemaFamilyState(stepId))
.getValue(),
)
.filter(isDefined);
return stepsOutputSchema;
},
[stepsOutputSchemaFamilyState],
);
return { getStepsOutputSchema };
};

View File

@ -0,0 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const WorkflowVersionComponentInstanceContext =
createComponentInstanceContext();

View File

@ -0,0 +1,10 @@
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
import { WorkflowVersionComponentInstanceContext } from '@/workflow/states/context/WorkflowVersionComponentInstanceContext';
import { StepOutputSchema } from '@/workflow/workflow-variables/types/StepOutputSchema';
export const stepsOutputSchemaComponentFamilyState =
createComponentFamilyStateV2<StepOutputSchema | null, string>({
key: 'stepsOutputSchemaComponentFamilyState',
defaultValue: null,
componentInstanceContext: WorkflowVersionComponentInstanceContext,
});

View File

@ -0,0 +1,19 @@
import { usePopulateStepsOutputSchema } from '@/workflow/hooks/usePopulateStepsOutputSchema';
import { WorkflowVersion } from '@/workflow/types/Workflow';
import { useEffect } from 'react';
export const WorkflowVersionOutputSchemaEffect = ({
workflowVersion,
}: {
workflowVersion: WorkflowVersion;
}) => {
const { populateStepsOutputSchema } = usePopulateStepsOutputSchema({
workflowVersionId: workflowVersion.id,
});
useEffect(() => {
populateStepsOutputSchema(workflowVersion);
}, [populateStepsOutputSchema, workflowVersion]);
return null;
};

View File

@ -1,17 +1,22 @@
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { WorkflowDiagramCanvasEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable'; import { WorkflowDiagramCanvasEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable';
import { WorkflowDiagramEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramEffect'; import { WorkflowDiagramEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramEffect';
import { WorkflowVersionOutputSchemaEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionOutputSchemaEffect';
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
export const WorkflowVisualizer = ({ workflowId }: { workflowId: string }) => { export const WorkflowVisualizer = ({ workflowId }: { workflowId: string }) => {
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId); const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
const workflowVersion = workflowWithCurrentVersion?.currentVersion;
return ( return (
<> <>
<WorkflowDiagramEffect <WorkflowDiagramEffect
workflowWithCurrentVersion={workflowWithCurrentVersion} workflowWithCurrentVersion={workflowWithCurrentVersion}
/> />
{isDefined(workflowVersion) && (
<WorkflowVersionOutputSchemaEffect workflowVersion={workflowVersion} />
)}
{isDefined(workflowWithCurrentVersion) ? ( {isDefined(workflowWithCurrentVersion) ? (
<WorkflowDiagramCanvasEditable <WorkflowDiagramCanvasEditable

View File

@ -1,4 +1,5 @@
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { WorkflowVersionComponentInstanceContext } from '@/workflow/states/context/WorkflowVersionComponentInstanceContext';
import { workflowIdState } from '@/workflow/states/workflowIdState'; import { workflowIdState } from '@/workflow/states/workflowIdState';
import { RightDrawerWorkflowEditStepContent } from '@/workflow/workflow-steps/components/RightDrawerWorkflowEditStepContent'; import { RightDrawerWorkflowEditStepContent } from '@/workflow/workflow-steps/components/RightDrawerWorkflowEditStepContent';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -12,5 +13,11 @@ export const RightDrawerWorkflowEditStep = () => {
return null; return null;
} }
return <RightDrawerWorkflowEditStepContent workflow={workflow} />; return (
<WorkflowVersionComponentInstanceContext.Provider
value={{ instanceId: workflow.currentVersion.id }}
>
<RightDrawerWorkflowEditStepContent workflow={workflow} />
</WorkflowVersionComponentInstanceContext.Provider>
);
}; };

View File

@ -1,111 +1,64 @@
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow'; import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { useStepsOutputSchema } from '@/workflow/hooks/useStepsOutputSchema';
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow'; import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { import {
OutputSchema, OutputSchema,
StepOutputSchema, StepOutputSchema,
} from '@/workflow/workflow-variables/types/StepOutputSchema'; } from '@/workflow/workflow-variables/types/StepOutputSchema';
import { filterOutputSchema } from '@/workflow/workflow-variables/utils/filterOutputSchema'; import { filterOutputSchema } from '@/workflow/workflow-variables/utils/filterOutputSchema';
import { getTriggerStepName } from '@/workflow/workflow-variables/utils/getTriggerStepName'; import { isEmptyObject } from '@tiptap/core';
import isEmpty from 'lodash.isempty';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
import { isEmptyObject } from '~/utils/isEmptyObject';
export const useAvailableVariablesInWorkflowStep = ({ export const useAvailableVariablesInWorkflowStep = ({
objectNameSingularToSelect, objectNameSingularToSelect,
}: { }: {
objectNameSingularToSelect?: string; objectNameSingularToSelect?: string;
}): StepOutputSchema[] => { }): StepOutputSchema[] => {
const workflowId = useRecoilValue(workflowIdState);
const workflow = useWorkflowWithCurrentVersion(workflowId);
const workflowSelectedNode = useWorkflowSelectedNodeOrThrow(); const workflowSelectedNode = useWorkflowSelectedNodeOrThrow();
const flow = useFlowOrThrow(); const flow = useFlowOrThrow();
const { getStepsOutputSchema } = useStepsOutputSchema({});
if (!isDefined(workflow)) { const steps = flow.steps ?? [];
return [];
}
const trigger = flow.trigger; const previousStepIds: string[] = [];
const steps = flow.steps;
const stepDefinition = getStepDefinitionOrThrow({
stepId: workflowSelectedNode,
trigger,
steps,
});
if (
!isDefined(stepDefinition) ||
stepDefinition.type === 'trigger' ||
!isDefined(steps)
) {
return [];
}
const previousSteps = [];
for (const step of steps) { for (const step of steps) {
if (step.id === workflowSelectedNode) { if (step.id === workflowSelectedNode) {
break; break;
} }
previousSteps.push(step); previousStepIds.push(step.id);
} }
const result = []; const availableStepsOutputSchema: StepOutputSchema[] =
getStepsOutputSchema(previousStepIds).filter(isDefined);
const filteredTriggerOutputSchema = filterOutputSchema( const triggersOutputSchema: StepOutputSchema[] = getStepsOutputSchema([
trigger?.settings?.outputSchema as OutputSchema | undefined, TRIGGER_STEP_ID,
objectNameSingularToSelect, ]).filter(isDefined);
);
if ( const availableVariablesInWorkflowStep = [
isDefined(trigger) && ...availableStepsOutputSchema,
isDefined(filteredTriggerOutputSchema) && ...triggersOutputSchema,
!isEmptyObject(filteredTriggerOutputSchema) ]
) { .map((stepOutputSchema) => {
const triggerIconKey = const outputSchema = filterOutputSchema(
trigger.type === 'DATABASE_EVENT' stepOutputSchema.outputSchema,
? getTriggerIcon({ objectNameSingularToSelect,
type: trigger.type, ) as OutputSchema;
eventName: splitWorkflowTriggerEventName(
trigger.settings?.eventName,
).event,
})
: getTriggerIcon({
type: trigger.type,
});
result.push({ if (!isDefined(outputSchema) || isEmptyObject(outputSchema)) {
id: 'trigger', return undefined;
name: isDefined(trigger.name) }
? trigger.name
: getTriggerStepName(trigger),
icon: triggerIconKey,
outputSchema: filteredTriggerOutputSchema,
});
}
previousSteps.forEach((previousStep) => { return {
const filteredOutputSchema = filterOutputSchema( id: stepOutputSchema.id,
previousStep.settings.outputSchema as OutputSchema, name: stepOutputSchema.name,
objectNameSingularToSelect, icon: stepOutputSchema.icon,
); outputSchema,
};
})
.filter(isDefined);
if (isDefined(filteredOutputSchema) && !isEmpty(filteredOutputSchema)) { return availableVariablesInWorkflowStep;
result.push({
id: previousStep.id,
name: previousStep.name,
icon: getActionIcon(previousStep.type),
outputSchema: filteredOutputSchema,
});
}
});
return result;
}; };