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
+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`}
|
||||
|
||||
Reference in New Issue
Block a user