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 { WorkflowDiagramCanvasEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable';
import { WorkflowDiagramEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramEffect';
import { WorkflowVersionOutputSchemaEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionOutputSchemaEffect';
import '@xyflow/react/dist/style.css';
import { isDefined } from 'twenty-shared';
export const WorkflowVisualizer = ({ workflowId }: { workflowId: string }) => {
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
const workflowVersion = workflowWithCurrentVersion?.currentVersion;
return (
<>
<WorkflowDiagramEffect
workflowWithCurrentVersion={workflowWithCurrentVersion}
/>
{isDefined(workflowVersion) && (
<WorkflowVersionOutputSchemaEffect workflowVersion={workflowVersion} />
)}
{isDefined(workflowWithCurrentVersion) ? (
<WorkflowDiagramCanvasEditable

View File

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