Workflow command menu fixes (#15234)

- Move trash button to command menu footer
<img width="132" height="102" alt="Capture d’écran 2025-10-21 à 18 12
19"
src="https://github.com/user-attachments/assets/ad6a9374-a28f-4498-b8f3-ca576981693c"
/>

- Add footer to triggers + on missing steps
- Catch step body errors so the user can still delete the step when an
error happens
<img width="529" height="419" alt="Capture d’écran 2025-10-21 à 18 13
17"
src="https://github.com/user-attachments/assets/0ac07511-f4ad-40c4-98f1-afb53c0f7a89"
/>
This commit is contained in:
Thomas Trompette
2025-10-22 10:17:06 +02:00
committed by GitHub
parent c5564d9bd0
commit bf3c3fc5a5
32 changed files with 136 additions and 92 deletions
@@ -1,126 +0,0 @@
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>
);
};
@@ -17,7 +17,7 @@ import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-act
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords';
import { WorkflowEditActionFormFiller } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller';
import { WorkflowEditActionHttpRequest } from '@/workflow/workflow-steps/workflow-actions/http-request-action/components/WorkflowEditActionHttpRequest';
import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator';
import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/components/WorkflowEditActionIterator';
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
import { WorkflowEditTriggerManual } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManual';
@@ -1,3 +1,6 @@
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
import { AppErrorDisplay } from '@/error-handler/components/internal/AppErrorDisplay';
import { type AppErrorDisplayProps } from '@/error-handler/types/AppErrorDisplayProps';
import styled from '@emotion/styled';
const StyledWorkflowStepBody = styled.div`
@@ -12,4 +15,29 @@ const StyledWorkflowStepBody = styled.div`
row-gap: ${({ theme }) => theme.spacing(4)};
`;
export { StyledWorkflowStepBody as WorkflowStepBody };
export const WorkflowStepBody = ({
children,
}: {
children: React.ReactNode;
}) => {
return (
<StyledWorkflowStepBody>
<AppErrorBoundary
resetOnLocationChange={true}
FallbackComponent={({
error,
resetErrorBoundary,
title,
}: AppErrorDisplayProps) => (
<AppErrorDisplay
error={error}
resetErrorBoundary={resetErrorBoundary}
title={title}
/>
)}
>
{children}
</AppErrorBoundary>
</StyledWorkflowStepBody>
);
};
@@ -16,7 +16,7 @@ import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-act
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords';
import { WorkflowEditActionFormBuilder } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder';
import { WorkflowEditActionHttpRequest } from '@/workflow/workflow-steps/workflow-actions/http-request-action/components/WorkflowEditActionHttpRequest';
import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator';
import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/components/WorkflowEditActionIterator';
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
import { WorkflowEditTriggerManual } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManual';
@@ -6,16 +6,18 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
import { useDuplicateStep } from '@/workflow/workflow-steps/hooks/useDuplicateStep';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { useId } from 'react';
import { IconCopyPlus, IconPencil } from 'twenty-ui/display';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { IconCopyPlus, IconPencil, IconTrash } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { getOsControlSymbol } from 'twenty-ui/utilities';
export const WorkflowActionFooter = ({
export const WorkflowStepFooter = ({
stepId,
additionalActions,
}: {
@@ -28,7 +30,11 @@ export const WorkflowActionFooter = ({
const { duplicateStep } = useDuplicateStep();
const { closeDropdown } = useCloseDropdown();
const workflowId = useCommandMenuWorkflowIdOrThrow();
const { openWorkflowEditStepTypeInCommandMenu } = useWorkflowCommandMenu();
const {
openWorkflowEditStepTypeInCommandMenu,
openWorkflowTriggerTypeInCommandMenu,
} = useWorkflowCommandMenu();
const { deleteStep } = useDeleteStep();
const OptionsDropdown = (
<Dropdown
@@ -54,18 +60,30 @@ export const WorkflowActionFooter = ({
<MenuItem
onClick={() => {
closeDropdown(dropdownId);
openWorkflowEditStepTypeInCommandMenu(workflowId);
stepId === TRIGGER_STEP_ID
? openWorkflowTriggerTypeInCommandMenu(workflowId)
: openWorkflowEditStepTypeInCommandMenu(workflowId);
}}
text={t`Change node type`}
LeftIcon={IconPencil}
/>
{stepId !== TRIGGER_STEP_ID && (
<MenuItem
onClick={() => {
closeDropdown(dropdownId);
duplicateStep({ stepId });
}}
text={t`Duplicate node`}
LeftIcon={IconCopyPlus}
/>
)}
<MenuItem
onClick={() => {
closeDropdown(dropdownId);
duplicateStep({ stepId });
deleteStep(stepId);
}}
text={t`Duplicate node`}
LeftIcon={IconCopyPlus}
text={t`Delete node`}
LeftIcon={IconTrash}
/>
</SelectableList>
</DropdownMenuItemsContainer>