Visualize iterator sub step's output by iteration index (#14747)
## Demo https://github.com/user-attachments/assets/27afff85-4f33-4e91-b28a-3dc868bb9ad7 ## With several sub steps > [!NOTE] > Please note that as I created the component state, the iteration index will be different for every step of a workflow run. We might have one component state instance per iterator node. We can implement it in another PR if we think it's the way to go. https://github.com/user-attachments/assets/6b8cd06d-9051-474d-b438-dd6155cc3810
This commit is contained in:
committed by
GitHub
parent
358ab54690
commit
9b88cee1ce
+47
-37
@@ -1,3 +1,4 @@
|
||||
import { CommandMenuWorkflowRunStepContentComponentInstanceContext } from '@/command-menu/pages/workflow/step/view-run/states/contexts/CommandMenuWorkflowRunStepContentComponentInstanceContext';
|
||||
import { getIsInputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsInputTabDisabled';
|
||||
import { getIsOutputTabDisabled } from '@/command-menu/pages/workflow/step/view-run/utils/getIsOutputTabDisabled';
|
||||
import { getShouldFocusNodeTab } from '@/command-menu/pages/workflow/step/view-run/utils/getShouldFocusNodeTab';
|
||||
@@ -12,6 +13,7 @@ import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { WorkflowIteratorSubStepSwitcher } from '@/workflow/workflow-steps/components/WorkflowIteratorSubStepSwitcher';
|
||||
import { WorkflowRunStepInputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepInputDetail';
|
||||
import { WorkflowRunStepNodeDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepNodeDetail';
|
||||
import { WorkflowRunStepOutputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepOutputDetail';
|
||||
@@ -112,46 +114,54 @@ export const CommandMenuWorkflowRunViewStepContent = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{shouldFocusNodeTab ? (
|
||||
<WorkflowRunStepNodeDetail
|
||||
stepId={workflowSelectedNode}
|
||||
trigger={flow.trigger}
|
||||
steps={flow.steps}
|
||||
stepExecutionStatus={stepExecutionStatus}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<StyledTabList
|
||||
tabs={tabs}
|
||||
behaveAsLinks={false}
|
||||
componentInstanceId={commandMenuPageComponentInstance.instanceId}
|
||||
<CommandMenuWorkflowRunStepContentComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: `${workflowRunId}_${workflowSelectedNode}`,
|
||||
}}
|
||||
>
|
||||
<StyledContainer>
|
||||
{shouldFocusNodeTab ? (
|
||||
<WorkflowRunStepNodeDetail
|
||||
stepId={workflowSelectedNode}
|
||||
trigger={flow.trigger}
|
||||
steps={flow.steps}
|
||||
stepExecutionStatus={stepExecutionStatus}
|
||||
/>
|
||||
|
||||
{activeTabId === WorkflowRunTabId.OUTPUT ? (
|
||||
<WorkflowRunStepOutputDetail
|
||||
key={workflowSelectedNode}
|
||||
stepId={workflowSelectedNode}
|
||||
) : (
|
||||
<>
|
||||
<StyledTabList
|
||||
tabs={tabs}
|
||||
behaveAsLinks={false}
|
||||
componentInstanceId={commandMenuPageComponentInstance.instanceId}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{activeTabId === WorkflowRunTabId.NODE ? (
|
||||
<WorkflowRunStepNodeDetail
|
||||
stepId={workflowSelectedNode}
|
||||
trigger={flow.trigger}
|
||||
steps={flow.steps}
|
||||
stepExecutionStatus={stepExecutionStatus}
|
||||
/>
|
||||
) : null}
|
||||
{activeTabId === WorkflowRunTabId.OUTPUT ? (
|
||||
<WorkflowRunStepOutputDetail
|
||||
key={workflowSelectedNode}
|
||||
stepId={workflowSelectedNode}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{activeTabId === WorkflowRunTabId.INPUT ? (
|
||||
<WorkflowRunStepInputDetail
|
||||
key={workflowSelectedNode}
|
||||
stepId={workflowSelectedNode}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</StyledContainer>
|
||||
{activeTabId === WorkflowRunTabId.NODE ? (
|
||||
<WorkflowRunStepNodeDetail
|
||||
stepId={workflowSelectedNode}
|
||||
trigger={flow.trigger}
|
||||
steps={flow.steps}
|
||||
stepExecutionStatus={stepExecutionStatus}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{activeTabId === WorkflowRunTabId.INPUT ? (
|
||||
<WorkflowRunStepInputDetail
|
||||
key={workflowSelectedNode}
|
||||
stepId={workflowSelectedNode}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<WorkflowIteratorSubStepSwitcher stepId={workflowSelectedNode} />
|
||||
</>
|
||||
)}
|
||||
</StyledContainer>
|
||||
</CommandMenuWorkflowRunStepContentComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const CommandMenuWorkflowRunStepContentComponentInstanceContext =
|
||||
createComponentInstanceContext({
|
||||
instanceId: '',
|
||||
});
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { CommandMenuWorkflowRunStepContentComponentInstanceContext } from '@/command-menu/pages/workflow/step/view-run/states/contexts/CommandMenuWorkflowRunStepContentComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const workflowRunIteratorSubStepIterationIndexComponentState =
|
||||
createComponentState<number>({
|
||||
key: 'workflowRunIteratorSubStepIterationIndexComponentState',
|
||||
defaultValue: 0,
|
||||
componentInstanceContext:
|
||||
CommandMenuWorkflowRunStepContentComponentInstanceContext,
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { type WorkflowStep } from '@/workflow/types/Workflow';
|
||||
import { isLastStepOfLoop } from '@/workflow/workflow-diagram/utils/isLastStepOfLoop';
|
||||
|
||||
export const isParentStep = ({
|
||||
currentStep,
|
||||
potentialParentStep,
|
||||
steps,
|
||||
}: {
|
||||
currentStep: WorkflowStep;
|
||||
potentialParentStep: WorkflowStep;
|
||||
steps: WorkflowStep[];
|
||||
}): boolean => {
|
||||
if (potentialParentStep.type === 'ITERATOR') {
|
||||
return !!(
|
||||
potentialParentStep.settings.input.initialLoopStepIds?.includes(
|
||||
currentStep.id,
|
||||
) || potentialParentStep.nextStepIds?.includes(currentStep.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (currentStep.type === 'ITERATOR') {
|
||||
return !!(
|
||||
potentialParentStep.nextStepIds?.includes(currentStep.id) &&
|
||||
!isLastStepOfLoop({
|
||||
iterator: currentStep,
|
||||
stepId: potentialParentStep.id,
|
||||
steps,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return !!potentialParentStep.nextStepIds?.includes(currentStep.id);
|
||||
};
|
||||
+44
-49
@@ -30,58 +30,53 @@ export const WorkflowActionFooter = ({
|
||||
const workflowId = useCommandMenuWorkflowIdOrThrow();
|
||||
const { openWorkflowEditStepTypeInCommandMenu } = useWorkflowCommandMenu();
|
||||
|
||||
const OptionsDropdown = () => {
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
data-select-disable
|
||||
clickableComponent={
|
||||
<Button title="Options" hotkeys={[getOsControlSymbol(), 'O']} />
|
||||
}
|
||||
dropdownPlacement="top-end"
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }}
|
||||
globalHotkeysConfig={{
|
||||
enableGlobalHotkeysWithModifiers: true,
|
||||
enableGlobalHotkeysConflictingWithKeyboard: false,
|
||||
}}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={['change-node-type', 'duplicate']}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
closeDropdown(dropdownId);
|
||||
openWorkflowEditStepTypeInCommandMenu(workflowId);
|
||||
}}
|
||||
text={t`Change node type`}
|
||||
LeftIcon={IconPencil}
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
closeDropdown(dropdownId);
|
||||
duplicateStep({ stepId });
|
||||
}}
|
||||
text={t`Duplicate node`}
|
||||
LeftIcon={IconCopyPlus}
|
||||
/>
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const OptionsDropdown = (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
data-select-disable
|
||||
clickableComponent={
|
||||
<Button title="Options" hotkeys={[getOsControlSymbol(), 'O']} />
|
||||
}
|
||||
dropdownPlacement="top-end"
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }}
|
||||
globalHotkeysConfig={{
|
||||
enableGlobalHotkeysWithModifiers: true,
|
||||
enableGlobalHotkeysConflictingWithKeyboard: false,
|
||||
}}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={['change-node-type', 'duplicate']}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
closeDropdown(dropdownId);
|
||||
openWorkflowEditStepTypeInCommandMenu(workflowId);
|
||||
}}
|
||||
text={t`Change node type`}
|
||||
LeftIcon={IconPencil}
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
closeDropdown(dropdownId);
|
||||
duplicateStep({ stepId });
|
||||
}}
|
||||
text={t`Duplicate node`}
|
||||
LeftIcon={IconCopyPlus}
|
||||
/>
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<RightDrawerFooter
|
||||
actions={[
|
||||
<OptionsDropdown key="options" />,
|
||||
...(additionalActions ?? []),
|
||||
]}
|
||||
actions={[OptionsDropdown, ...(additionalActions ?? [])]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { workflowRunIteratorSubStepIterationIndexComponentState } from '@/command-menu/pages/workflow/step/view-run/states/workflowRunIteratorSubStepIterationIndexComponentState';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { getIsDescendantOfIterator } from '@/workflow/workflow-steps/utils/getIsDescendantOfIterator';
|
||||
import { getWorkflowRunAllStepInfoHistory } from '@/workflow/workflow-steps/utils/getWorkflowRunAllStepInfoHistory';
|
||||
import styled from '@emotion/styled';
|
||||
import { plural } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronLeft, IconChevronRight } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-block: ${({ theme }) => theme.spacing(2)};
|
||||
padding-inline: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledCounter = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
export const WorkflowIteratorSubStepSwitcher = ({
|
||||
stepId,
|
||||
}: {
|
||||
stepId: string;
|
||||
}) => {
|
||||
const flow = useFlowOrThrow();
|
||||
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||
|
||||
const [
|
||||
workflowRunIteratorSubStepIterationIndex,
|
||||
setWorkflowRunIteratorSubStepIterationIndex,
|
||||
] = useRecoilComponentState(
|
||||
workflowRunIteratorSubStepIterationIndexComponentState,
|
||||
);
|
||||
|
||||
const stepDefinition = getStepDefinitionOrThrow({
|
||||
stepId,
|
||||
trigger: flow.trigger,
|
||||
steps: flow.steps,
|
||||
});
|
||||
|
||||
const stepInfo = workflowRun?.state?.stepInfos[stepId];
|
||||
|
||||
if (
|
||||
!isDefined(stepInfo) ||
|
||||
!isDefined(workflowRun?.state) ||
|
||||
!isDefined(flow.steps) ||
|
||||
stepDefinition?.type !== 'action'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allStepInfos = getWorkflowRunAllStepInfoHistory({
|
||||
stepInfo,
|
||||
});
|
||||
|
||||
const isDescendantOfIterator = getIsDescendantOfIterator({
|
||||
stepId,
|
||||
steps: flow.steps,
|
||||
});
|
||||
|
||||
const workflowRunIteratorSubStepIterationsCount = allStepInfos.length;
|
||||
|
||||
const canGoToPreviousIndex = workflowRunIteratorSubStepIterationIndex > 0;
|
||||
const canGoToNextIndex =
|
||||
workflowRunIteratorSubStepIterationIndex <
|
||||
workflowRunIteratorSubStepIterationsCount - 1;
|
||||
|
||||
const handleDecrementIndex = () => {
|
||||
if (!canGoToPreviousIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
setWorkflowRunIteratorSubStepIterationIndex(
|
||||
workflowRunIteratorSubStepIterationIndex - 1,
|
||||
);
|
||||
};
|
||||
|
||||
const handleIncrementIndex = () => {
|
||||
if (!canGoToNextIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
setWorkflowRunIteratorSubStepIterationIndex(
|
||||
workflowRunIteratorSubStepIterationIndex + 1,
|
||||
);
|
||||
};
|
||||
|
||||
if (!isDescendantOfIterator) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<IconButton
|
||||
Icon={IconChevronLeft}
|
||||
size="small"
|
||||
disabled={!canGoToPreviousIndex}
|
||||
onClick={handleDecrementIndex}
|
||||
/>
|
||||
|
||||
<StyledCounter>
|
||||
{workflowRunIteratorSubStepIterationIndex + 1}/
|
||||
{plural(workflowRunIteratorSubStepIterationsCount, {
|
||||
one: '# item',
|
||||
other: '# items',
|
||||
})}
|
||||
</StyledCounter>
|
||||
|
||||
<IconButton
|
||||
Icon={IconChevronRight}
|
||||
size="small"
|
||||
disabled={!canGoToNextIndex}
|
||||
onClick={handleIncrementIndex}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+9
-5
@@ -3,6 +3,8 @@ import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThro
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useWorkflowRunStepInfo } from '@/workflow/workflow-steps/hooks/useWorkflowRunStepInfo';
|
||||
import { getWorkflowRunStepInfoToDisplayAsOutput } from '@/workflow/workflow-steps/utils/getWorkflowRunStepInfoToDisplayAsOutput';
|
||||
import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
|
||||
@@ -29,13 +31,15 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||
|
||||
if (!isDefined(workflowRun?.state?.stepInfos)) {
|
||||
const stepInfo = useWorkflowRunStepInfo({ stepId });
|
||||
|
||||
if (!isDefined(workflowRun?.state) || !isDefined(stepInfo)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stepInfo = workflowRun.state.stepInfos[stepId];
|
||||
|
||||
const { status: _, ...stepInfoWithoutStatus } = stepInfo ?? {};
|
||||
const stepInfoToDisplay = getWorkflowRunStepInfoToDisplayAsOutput({
|
||||
stepInfo,
|
||||
});
|
||||
|
||||
const stepDefinition = getStepDefinitionOrThrow({
|
||||
stepId,
|
||||
@@ -89,7 +93,7 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
|
||||
<WorkflowRunStepJsonContainer>
|
||||
<JsonTree
|
||||
value={stepInfoWithoutStatus ?? t`No output available`}
|
||||
value={stepInfoToDisplay ?? t`No output available`}
|
||||
shouldExpandNodeInitially={isTwoFirstDepths}
|
||||
emptyArrayLabel={t`Empty Array`}
|
||||
emptyObjectLabel={t`Empty Object`}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { workflowRunIteratorSubStepIterationIndexComponentState } from '@/command-menu/pages/workflow/step/view-run/states/workflowRunIteratorSubStepIterationIndexComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { getWorkflowRunAllStepInfoHistory } from '@/workflow/workflow-steps/utils/getWorkflowRunAllStepInfoHistory';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useWorkflowRunStepInfo = ({ stepId }: { stepId: string }) => {
|
||||
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||
|
||||
const workflowRunIteratorSubStepIterationIndex = useRecoilComponentValue(
|
||||
workflowRunIteratorSubStepIterationIndexComponentState,
|
||||
);
|
||||
|
||||
const stepInfo = workflowRun?.state?.stepInfos[stepId];
|
||||
|
||||
if (!isDefined(stepInfo)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const allStepInfoHistory = getWorkflowRunAllStepInfoHistory({ stepInfo });
|
||||
|
||||
return allStepInfoHistory[workflowRunIteratorSubStepIterationIndex];
|
||||
};
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import { type WorkflowStep } from '@/workflow/types/Workflow';
|
||||
import { getIsDescendantOfIterator } from '../getIsDescendantOfIterator';
|
||||
|
||||
describe('getIsDescendantOfIterator', () => {
|
||||
const iteratorStep: WorkflowStep = {
|
||||
id: 'iterator1',
|
||||
name: 'Iterator Step',
|
||||
type: 'ITERATOR',
|
||||
valid: true,
|
||||
nextStepIds: ['step3'],
|
||||
settings: {
|
||||
input: {
|
||||
initialLoopStepIds: ['step2'],
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const codeStep2: WorkflowStep = {
|
||||
id: 'step2',
|
||||
name: 'Second Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: ['iterator1'],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func2',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const codeStep3: WorkflowStep = {
|
||||
id: 'step3',
|
||||
name: 'Third Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: [],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func3',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const workflow: WorkflowStep[] = [iteratorStep, codeStep2, codeStep3];
|
||||
|
||||
it('returns true for direct loop step descendant', () => {
|
||||
expect(
|
||||
getIsDescendantOfIterator({ stepId: codeStep2.id, steps: workflow }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for indirect descendant', () => {
|
||||
// step3 is pointed to by iterator1.nextStepIds
|
||||
expect(
|
||||
getIsDescendantOfIterator({ stepId: codeStep3.id, steps: workflow }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for iterator itself', () => {
|
||||
expect(
|
||||
getIsDescendantOfIterator({ stepId: iteratorStep.id, steps: workflow }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for step not connected to iterator', () => {
|
||||
const unrelatedStep: WorkflowStep = {
|
||||
id: 'step4',
|
||||
name: 'Unrelated Step',
|
||||
type: 'CODE',
|
||||
valid: true,
|
||||
nextStepIds: [],
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: 'func4',
|
||||
serverlessFunctionVersion: '1.0.0',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: true },
|
||||
continueOnFailure: { value: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(
|
||||
getIsDescendantOfIterator({
|
||||
stepId: unrelatedStep.id,
|
||||
steps: [...workflow, unrelatedStep],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ...existing test code...
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { type WorkflowStep } from '@/workflow/types/Workflow';
|
||||
import { isParentStep } from '@/workflow/workflow-diagram/utils/isParentStep';
|
||||
|
||||
export const getIsDescendantOfIterator = ({
|
||||
stepId,
|
||||
steps,
|
||||
}: {
|
||||
stepId: string;
|
||||
steps: WorkflowStep[];
|
||||
}): boolean => {
|
||||
const hasIteratorAncestor = (
|
||||
currentStepId: string,
|
||||
visited = new Set<string>(),
|
||||
): boolean => {
|
||||
const currentStep = steps.find((step) => step.id === currentStepId);
|
||||
if (!currentStep) {
|
||||
throw new Error(`Step with ID ${currentStepId} not found`);
|
||||
}
|
||||
|
||||
const parentSteps = steps
|
||||
.filter((step) =>
|
||||
isParentStep({
|
||||
currentStep,
|
||||
potentialParentStep: step,
|
||||
steps,
|
||||
}),
|
||||
)
|
||||
.filter((step) => !visited.has(step.id));
|
||||
|
||||
for (const parent of parentSteps) {
|
||||
visited.add(currentStepId);
|
||||
|
||||
if (parent.type === 'ITERATOR') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasIteratorAncestor(parent.id, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return hasIteratorAncestor(stepId);
|
||||
};
|
||||
+1
-32
@@ -1,36 +1,5 @@
|
||||
import { type WorkflowStep } from '@/workflow/types/Workflow';
|
||||
import { isLastStepOfLoop } from '@/workflow/workflow-diagram/utils/isLastStepOfLoop';
|
||||
|
||||
const isParentStep = ({
|
||||
currentStep,
|
||||
potentialParentStep,
|
||||
steps,
|
||||
}: {
|
||||
currentStep: WorkflowStep;
|
||||
potentialParentStep: WorkflowStep;
|
||||
steps: WorkflowStep[];
|
||||
}) => {
|
||||
if (potentialParentStep.type === 'ITERATOR') {
|
||||
return (
|
||||
potentialParentStep.settings.input.initialLoopStepIds?.includes(
|
||||
currentStep.id,
|
||||
) || potentialParentStep.nextStepIds?.includes(currentStep.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (currentStep.type === 'ITERATOR') {
|
||||
return (
|
||||
potentialParentStep.nextStepIds?.includes(currentStep.id) &&
|
||||
!isLastStepOfLoop({
|
||||
iterator: currentStep,
|
||||
stepId: potentialParentStep.id,
|
||||
steps,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return potentialParentStep.nextStepIds?.includes(currentStep.id);
|
||||
};
|
||||
import { isParentStep } from '@/workflow/workflow-diagram/utils/isParentStep';
|
||||
|
||||
export const getPreviousSteps = ({
|
||||
steps,
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { isArray } from '@sniptt/guards';
|
||||
import { type WorkflowRunStepInfo } from 'twenty-shared/workflow';
|
||||
|
||||
export const getWorkflowRunAllStepInfoHistory = ({
|
||||
stepInfo,
|
||||
}: {
|
||||
stepInfo: WorkflowRunStepInfo;
|
||||
}) => {
|
||||
const allStepInfoHistory: WorkflowRunStepInfo[] = [
|
||||
...(isArray(stepInfo?.history) ? stepInfo.history : []),
|
||||
stepInfo,
|
||||
];
|
||||
|
||||
return allStepInfoHistory;
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { type WorkflowRunStepInfo } from 'twenty-shared/workflow';
|
||||
|
||||
export const getWorkflowRunStepInfoToDisplayAsOutput = ({
|
||||
stepInfo,
|
||||
}: {
|
||||
stepInfo: WorkflowRunStepInfo;
|
||||
}) => {
|
||||
const { status: _status, history: _history, ...infoToDisplay } = stepInfo;
|
||||
|
||||
return infoToDisplay;
|
||||
};
|
||||
@@ -3,6 +3,15 @@ import { workflowRunStepStatusSchema } from './workflow-run-step-status-schema';
|
||||
|
||||
export const workflowRunStateStepInfoSchema = z.object({
|
||||
result: z.any().optional(),
|
||||
error: z.any().optional(),
|
||||
error: z.string().optional(),
|
||||
status: workflowRunStepStatusSchema,
|
||||
get history() {
|
||||
return z.array(
|
||||
workflowRunStateStepInfoSchema.pick({
|
||||
result: true,
|
||||
status: true,
|
||||
error: true,
|
||||
})
|
||||
).optional();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { type workflowRunStateStepInfoSchema } from "@/workflow/schemas/workflow-run-state-step-info-schema";
|
||||
import { type workflowRunStateStepInfosSchema } from "@/workflow/schemas/workflow-run-state-step-infos-schema";
|
||||
import type z from "zod";
|
||||
|
||||
export enum StepStatus {
|
||||
NOT_STARTED = 'NOT_STARTED',
|
||||
RUNNING = 'RUNNING',
|
||||
@@ -7,15 +11,6 @@ export enum StepStatus {
|
||||
PENDING = 'PENDING',
|
||||
}
|
||||
|
||||
export type WorkflowRunStepInfo = {
|
||||
result?: object;
|
||||
error?: string;
|
||||
status: StepStatus;
|
||||
history?: {
|
||||
status: StepStatus;
|
||||
result?: object;
|
||||
error?: string;
|
||||
}[];
|
||||
};
|
||||
export type WorkflowRunStepInfo = z.infer<typeof workflowRunStateStepInfoSchema>
|
||||
|
||||
export type WorkflowRunStepInfos = Record<string, WorkflowRunStepInfo>;
|
||||
export type WorkflowRunStepInfos = z.infer<typeof workflowRunStateStepInfosSchema>;
|
||||
|
||||
Reference in New Issue
Block a user