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:
-126
@@ -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>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+29
-1
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+25
-7
@@ -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>
|
||||
Reference in New Issue
Block a user