From 5ddf7c6475cc846434f16b40c31e3c5f1d239aa9 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 6 Mar 2025 14:31:35 +0100 Subject: [PATCH] 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 --- .../hooks/usePopulateStepsOutputSchema.ts | 76 ++++++++++++ .../workflow/hooks/useStepsOutputSchema.ts | 40 +++++++ ...WorkflowVersionComponentInstanceContext.ts | 4 + .../states/stepsOutputSchemaFamilyState.ts | 10 ++ .../WorkflowVersionOutputSchemaEffect.tsx | 19 +++ .../components/WorkflowVisualizer.tsx | 5 + .../RightDrawerWorkflowEditStep.tsx | 9 +- .../useAvailableVariablesInWorkflowStep.ts | 113 +++++------------- 8 files changed, 195 insertions(+), 81 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/hooks/usePopulateStepsOutputSchema.ts create mode 100644 packages/twenty-front/src/modules/workflow/hooks/useStepsOutputSchema.ts create mode 100644 packages/twenty-front/src/modules/workflow/states/context/WorkflowVersionComponentInstanceContext.ts create mode 100644 packages/twenty-front/src/modules/workflow/states/stepsOutputSchemaFamilyState.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionOutputSchemaEffect.tsx diff --git a/packages/twenty-front/src/modules/workflow/hooks/usePopulateStepsOutputSchema.ts b/packages/twenty-front/src/modules/workflow/hooks/usePopulateStepsOutputSchema.ts new file mode 100644 index 000000000..590735ebb --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/usePopulateStepsOutputSchema.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useStepsOutputSchema.ts b/packages/twenty-front/src/modules/workflow/hooks/useStepsOutputSchema.ts new file mode 100644 index 000000000..f7a7e0dd4 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/useStepsOutputSchema.ts @@ -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 }; +}; diff --git a/packages/twenty-front/src/modules/workflow/states/context/WorkflowVersionComponentInstanceContext.ts b/packages/twenty-front/src/modules/workflow/states/context/WorkflowVersionComponentInstanceContext.ts new file mode 100644 index 000000000..dfa548cc9 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/states/context/WorkflowVersionComponentInstanceContext.ts @@ -0,0 +1,4 @@ +import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext'; + +export const WorkflowVersionComponentInstanceContext = + createComponentInstanceContext(); diff --git a/packages/twenty-front/src/modules/workflow/states/stepsOutputSchemaFamilyState.ts b/packages/twenty-front/src/modules/workflow/states/stepsOutputSchemaFamilyState.ts new file mode 100644 index 000000000..2713dc708 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/states/stepsOutputSchemaFamilyState.ts @@ -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({ + key: 'stepsOutputSchemaComponentFamilyState', + defaultValue: null, + componentInstanceContext: WorkflowVersionComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionOutputSchemaEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionOutputSchemaEffect.tsx new file mode 100644 index 000000000..53e86178c --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionOutputSchemaEffect.tsx @@ -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; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVisualizer.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVisualizer.tsx index 901c22ed5..8bbb4032f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVisualizer.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVisualizer.tsx @@ -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 ( <> + {isDefined(workflowVersion) && ( + + )} {isDefined(workflowWithCurrentVersion) ? ( { return null; } - return ; + return ( + + + + ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts index be4909ace..eb722ce89 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep.ts @@ -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; };