Add duplicate step action (#14392)
https://github.com/user-attachments/assets/ac89440c-6f84-4d2a-b88a-da7ddfb181eb
This commit is contained in:
@@ -961,6 +961,11 @@ export enum DistantTableUpdate {
|
||||
TABLE_DELETED = 'TABLE_DELETED'
|
||||
}
|
||||
|
||||
export type DuplicateWorkflowVersionStepInput = {
|
||||
stepId: Scalars['String'];
|
||||
workflowVersionId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type EditSsoInput = {
|
||||
id: Scalars['UUID'];
|
||||
status: SsoIdentityProviderStatus;
|
||||
@@ -1474,6 +1479,7 @@ export type Mutation = {
|
||||
destroyPageLayoutTab: Scalars['Boolean'];
|
||||
destroyPageLayoutWidget: Scalars['Boolean'];
|
||||
disablePostgresProxy: PostgresCredentials;
|
||||
duplicateWorkflowVersionStep: WorkflowVersionStepChanges;
|
||||
editSSOIdentityProvider: EditSsoOutput;
|
||||
emailPasswordResetLink: EmailPasswordResetLink;
|
||||
enablePostgresProxy: PostgresCredentials;
|
||||
@@ -1918,6 +1924,11 @@ export type MutationDestroyPageLayoutWidgetArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDuplicateWorkflowVersionStepArgs = {
|
||||
input: DuplicateWorkflowVersionStepInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationEditSsoIdentityProviderArgs = {
|
||||
input: EditSsoInput;
|
||||
};
|
||||
@@ -5289,6 +5300,13 @@ export type DeleteWorkflowVersionStepMutationVariables = Exact<{
|
||||
|
||||
export type DeleteWorkflowVersionStepMutation = { __typename?: 'Mutation', deleteWorkflowVersionStep: { __typename?: 'WorkflowVersionStepChanges', triggerNextStepIds?: Array<string> | null, stepsNextStepIds?: any | null, deletedStepIds?: Array<string> | null } };
|
||||
|
||||
export type DuplicateWorkflowVersionStepMutationVariables = Exact<{
|
||||
input: DuplicateWorkflowVersionStepInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type DuplicateWorkflowVersionStepMutation = { __typename?: 'Mutation', duplicateWorkflowVersionStep: { __typename?: 'WorkflowVersionStepChanges', triggerNextStepIds?: Array<string> | null, stepsNextStepIds?: any | null, createdStep?: { __typename?: 'WorkflowAction', id: string, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array<string> | null, position?: { __typename?: 'WorkflowStepPosition', x: number, y: number } | null } | null } };
|
||||
|
||||
export type RunWorkflowVersionMutationVariables = Exact<{
|
||||
input: RunWorkflowVersionInput;
|
||||
}>;
|
||||
@@ -12063,6 +12081,52 @@ export function useDeleteWorkflowVersionStepMutation(baseOptions?: Apollo.Mutati
|
||||
export type DeleteWorkflowVersionStepMutationHookResult = ReturnType<typeof useDeleteWorkflowVersionStepMutation>;
|
||||
export type DeleteWorkflowVersionStepMutationResult = Apollo.MutationResult<DeleteWorkflowVersionStepMutation>;
|
||||
export type DeleteWorkflowVersionStepMutationOptions = Apollo.BaseMutationOptions<DeleteWorkflowVersionStepMutation, DeleteWorkflowVersionStepMutationVariables>;
|
||||
export const DuplicateWorkflowVersionStepDocument = gql`
|
||||
mutation DuplicateWorkflowVersionStep($input: DuplicateWorkflowVersionStepInput!) {
|
||||
duplicateWorkflowVersionStep(input: $input) {
|
||||
triggerNextStepIds
|
||||
stepsNextStepIds
|
||||
createdStep {
|
||||
id
|
||||
name
|
||||
type
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
position {
|
||||
x
|
||||
y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type DuplicateWorkflowVersionStepMutationFn = Apollo.MutationFunction<DuplicateWorkflowVersionStepMutation, DuplicateWorkflowVersionStepMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useDuplicateWorkflowVersionStepMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useDuplicateWorkflowVersionStepMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useDuplicateWorkflowVersionStepMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [duplicateWorkflowVersionStepMutation, { data, loading, error }] = useDuplicateWorkflowVersionStepMutation({
|
||||
* variables: {
|
||||
* input: // value for 'input'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDuplicateWorkflowVersionStepMutation(baseOptions?: Apollo.MutationHookOptions<DuplicateWorkflowVersionStepMutation, DuplicateWorkflowVersionStepMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<DuplicateWorkflowVersionStepMutation, DuplicateWorkflowVersionStepMutationVariables>(DuplicateWorkflowVersionStepDocument, options);
|
||||
}
|
||||
export type DuplicateWorkflowVersionStepMutationHookResult = ReturnType<typeof useDuplicateWorkflowVersionStepMutation>;
|
||||
export type DuplicateWorkflowVersionStepMutationResult = Apollo.MutationResult<DuplicateWorkflowVersionStepMutation>;
|
||||
export type DuplicateWorkflowVersionStepMutationOptions = Apollo.BaseMutationOptions<DuplicateWorkflowVersionStepMutation, DuplicateWorkflowVersionStepMutationVariables>;
|
||||
export const RunWorkflowVersionDocument = gql`
|
||||
mutation RunWorkflowVersion($input: RunWorkflowVersionInput!) {
|
||||
runWorkflowVersion(input: $input) {
|
||||
|
||||
@@ -925,6 +925,11 @@ export enum DistantTableUpdate {
|
||||
TABLE_DELETED = 'TABLE_DELETED'
|
||||
}
|
||||
|
||||
export type DuplicateWorkflowVersionStepInput = {
|
||||
stepId: Scalars['String'];
|
||||
workflowVersionId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type EditSsoInput = {
|
||||
id: Scalars['UUID'];
|
||||
status: SsoIdentityProviderStatus;
|
||||
@@ -1429,6 +1434,7 @@ export type Mutation = {
|
||||
destroyPageLayoutTab: Scalars['Boolean'];
|
||||
destroyPageLayoutWidget: Scalars['Boolean'];
|
||||
disablePostgresProxy: PostgresCredentials;
|
||||
duplicateWorkflowVersionStep: WorkflowVersionStepChanges;
|
||||
editSSOIdentityProvider: EditSsoOutput;
|
||||
emailPasswordResetLink: EmailPasswordResetLink;
|
||||
enablePostgresProxy: PostgresCredentials;
|
||||
@@ -1849,6 +1855,11 @@ export type MutationDestroyPageLayoutWidgetArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDuplicateWorkflowVersionStepArgs = {
|
||||
input: DuplicateWorkflowVersionStepInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationEditSsoIdentityProviderArgs = {
|
||||
input: EditSsoInput;
|
||||
};
|
||||
|
||||
+1
-8
@@ -1,17 +1,10 @@
|
||||
import { CommandMenuWorkflowSelectActionContent } from '@/command-menu/pages/workflow/action/components/CommandMenuWorkflowSelectActionContent';
|
||||
import { useCommandMenuWorkflowIdOrThrow } from '@/command-menu/pages/workflow/hooks/useCommandMenuWorkflowIdOrThrow';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
|
||||
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const CommandMenuWorkflowSelectAction = () => {
|
||||
const workflowId = useCommandMenuWorkflowIdOrThrow();
|
||||
const workflow = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
if (!isDefined(workflow)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowVisualizerComponentInstanceContext.Provider
|
||||
@@ -21,7 +14,7 @@ export const CommandMenuWorkflowSelectAction = () => {
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<CommandMenuWorkflowSelectActionContent workflow={workflow} />
|
||||
<CommandMenuWorkflowSelectActionContent />
|
||||
</WorkflowVisualizerComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-12
@@ -4,10 +4,7 @@ import { commandMenuNavigationStackState } from '@/command-menu/states/commandMe
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import {
|
||||
type WorkflowActionType,
|
||||
type WorkflowWithCurrentVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
import { useCloseRightClickMenu } from '@/workflow/workflow-diagram/hooks/useCloseRightClickMenu';
|
||||
import { RightDrawerStepListContainer } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepContainer';
|
||||
import { RightDrawerWorkflowSelectStepTitle } from '@/workflow/workflow-steps/components/RightDrawerWorkflowSelectStepTitle';
|
||||
@@ -26,16 +23,10 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
export const CommandMenuWorkflowSelectActionContent = ({
|
||||
workflow,
|
||||
}: {
|
||||
workflow: WorkflowWithCurrentVersion;
|
||||
}) => {
|
||||
export const CommandMenuWorkflowSelectActionContent = () => {
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { createStep } = useCreateStep({
|
||||
workflow,
|
||||
});
|
||||
const { createStep } = useCreateStep();
|
||||
|
||||
const { closeRightClickMenu } = useCloseRightClickMenu();
|
||||
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DUPLICATE_WORKFLOW_VERSION_STEP = gql`
|
||||
mutation DuplicateWorkflowVersionStep(
|
||||
$input: DuplicateWorkflowVersionStepInput!
|
||||
) {
|
||||
duplicateWorkflowVersionStep(input: $input) {
|
||||
triggerNextStepIds
|
||||
stepsNextStepIds
|
||||
createdStep {
|
||||
id
|
||||
name
|
||||
type
|
||||
settings
|
||||
valid
|
||||
nextStepIds
|
||||
position {
|
||||
x
|
||||
y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
+1
-9
@@ -1,8 +1,5 @@
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu';
|
||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
@@ -53,12 +50,7 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
|
||||
targetY,
|
||||
});
|
||||
|
||||
const workflowVisualizerWorkflowId = useRecoilComponentValue(
|
||||
workflowVisualizerWorkflowIdComponentState,
|
||||
);
|
||||
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
|
||||
|
||||
const { createStep } = useCreateStep({ workflow });
|
||||
const { createStep } = useCreateStep();
|
||||
|
||||
const { deleteEdge } = useDeleteEdge();
|
||||
|
||||
|
||||
-1
@@ -30,7 +30,6 @@ export const WorkflowDiagramStepNodeEditable = ({
|
||||
);
|
||||
|
||||
const { deleteStep } = useDeleteStep();
|
||||
|
||||
const setWorkflowSelectedNode = useSetRecoilComponentState(
|
||||
workflowSelectedNodeComponentState,
|
||||
);
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
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 { useDuplicateStep } from '@/workflow/workflow-steps/hooks/useDuplicateStep';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useId } from 'react';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { getOsControlSymbol } from 'twenty-ui/utilities';
|
||||
|
||||
export const WorkflowActionFooter = ({
|
||||
stepId,
|
||||
additionalActions,
|
||||
}: {
|
||||
stepId: string;
|
||||
additionalActions?: React.ReactNode[];
|
||||
}) => {
|
||||
const dropdownId = useId();
|
||||
const theme = useTheme();
|
||||
const { duplicateStep } = useDuplicateStep();
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
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={['duplicate']}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
closeDropdown(dropdownId);
|
||||
duplicateStep({ stepId });
|
||||
}}
|
||||
text="Duplicate"
|
||||
/>
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<RightDrawerFooter
|
||||
actions={[
|
||||
<OptionsDropdown key="options" />,
|
||||
...(additionalActions ?? []),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+3
-21
@@ -1,4 +1,3 @@
|
||||
import { type WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { WorkflowVisualizerComponentInstanceContext } from '../../../workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
||||
@@ -44,17 +43,6 @@ const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
};
|
||||
|
||||
describe('useCreateStep', () => {
|
||||
const mockWorkflow = {
|
||||
id: '123',
|
||||
currentVersion: {
|
||||
id: '456',
|
||||
status: 'DRAFT',
|
||||
steps: [],
|
||||
trigger: { type: 'manual' },
|
||||
},
|
||||
versions: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -63,15 +51,9 @@ describe('useCreateStep', () => {
|
||||
const mockWorkflowVersionId = 'version-123';
|
||||
mockGetUpdatableWorkflowVersion.mockResolvedValue(mockWorkflowVersionId);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useCreateStep({
|
||||
workflow: mockWorkflow as unknown as WorkflowWithCurrentVersion,
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
},
|
||||
);
|
||||
const { result } = renderHook(() => useCreateStep(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.createStep({
|
||||
|
||||
+7
-4
@@ -6,11 +6,14 @@ const mockGetUpdatableWorkflowVersion = jest.fn();
|
||||
const mockDeleteStepsOutputSchema = jest.fn();
|
||||
const mockCloseCommandMenu = jest.fn();
|
||||
|
||||
jest.mock('@/workflow/hooks/useDeleteWorkflowVersionStep', () => ({
|
||||
useDeleteWorkflowVersionStep: () => ({
|
||||
deleteWorkflowVersionStep: mockDeleteWorkflowVersionStep,
|
||||
jest.mock(
|
||||
'@/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep',
|
||||
() => ({
|
||||
useDeleteWorkflowVersionStep: () => ({
|
||||
deleteWorkflowVersionStep: mockDeleteWorkflowVersionStep,
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
);
|
||||
|
||||
jest.mock('@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow', () => ({
|
||||
useGetUpdatableWorkflowVersionOrThrow: () => ({
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { useDuplicateStep } from '@/workflow/workflow-steps/hooks/useDuplicateStep';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { WorkflowVisualizerComponentInstanceContext } from '../../../workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
||||
|
||||
const mockGetUpdatableWorkflowVersion = jest.fn();
|
||||
const mockDuplicateWorkflowVersionStep = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
duplicateWorkflowVersionStep: { createdStep: { id: '2', type: 'CODE' } },
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
'@/workflow/workflow-steps/hooks/useDuplicateWorkflowVersionStep',
|
||||
() => ({
|
||||
useDuplicateWorkflowVersionStep: () => ({
|
||||
duplicateWorkflowVersionStep: mockDuplicateWorkflowVersionStep,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow', () => ({
|
||||
useGetUpdatableWorkflowVersionOrThrow: () => ({
|
||||
getUpdatableWorkflowVersion: mockGetUpdatableWorkflowVersion,
|
||||
}),
|
||||
}));
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const workflowVisualizerComponentInstanceId =
|
||||
'workflow-visualizer-instance-id';
|
||||
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<WorkflowVisualizerComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: workflowVisualizerComponentInstanceId,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</WorkflowVisualizerComponentInstanceContext.Provider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
};
|
||||
|
||||
describe('useDuplicateStep', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create step in workflow version', async () => {
|
||||
const mockWorkflowVersionId = 'version-123';
|
||||
mockGetUpdatableWorkflowVersion.mockResolvedValue(mockWorkflowVersionId);
|
||||
|
||||
const { result } = renderHook(() => useDuplicateStep(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.duplicateStep({
|
||||
stepId: 'step-1',
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockGetUpdatableWorkflowVersion).toHaveBeenCalled();
|
||||
expect(mockDuplicateWorkflowVersionStep).toHaveBeenCalledWith({
|
||||
stepId: 'step-1',
|
||||
workflowVersionId: mockWorkflowVersionId,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,13 @@
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
|
||||
import { workflowLastCreatedStepIdComponentState } from '@/workflow/states/workflowLastCreatedStepIdComponentState';
|
||||
import {
|
||||
type WorkflowStepType,
|
||||
type WorkflowWithCurrentVersion,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { type WorkflowStepType } from '@/workflow/types/Workflow';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { useCreateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useCreateWorkflowVersionStep';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useCreateStep = ({
|
||||
workflow,
|
||||
}: {
|
||||
workflow: WorkflowWithCurrentVersion | undefined;
|
||||
}) => {
|
||||
export const useCreateStep = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { createWorkflowVersionStep } = useCreateWorkflowVersionStep();
|
||||
const setWorkflowSelectedNode = useSetRecoilComponentState(
|
||||
@@ -27,12 +20,6 @@ export const useCreateStep = ({
|
||||
const { getUpdatableWorkflowVersion } =
|
||||
useGetUpdatableWorkflowVersionOrThrow();
|
||||
|
||||
if (!isDefined(workflow)) {
|
||||
return {
|
||||
createStep: async () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const createStep = async ({
|
||||
newStepType,
|
||||
parentStepId,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useDeleteWorkflowVersionStep } from '@/workflow/hooks/useDeleteWorkflowVersionStep';
|
||||
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
|
||||
import { useStepsOutputSchema } from '@/workflow/hooks/useStepsOutputSchema';
|
||||
import { useDeleteWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep';
|
||||
|
||||
export const useDeleteStep = () => {
|
||||
const { deleteWorkflowVersionStep } = useDeleteWorkflowVersionStep();
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
|
||||
import { workflowLastCreatedStepIdComponentState } from '@/workflow/states/workflowLastCreatedStepIdComponentState';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { useDuplicateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useDuplicateWorkflowVersionStep';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useDuplicateStep = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { duplicateWorkflowVersionStep } = useDuplicateWorkflowVersionStep();
|
||||
const setWorkflowSelectedNode = useSetRecoilComponentState(
|
||||
workflowSelectedNodeComponentState,
|
||||
);
|
||||
const setWorkflowLastCreatedStepId = useSetRecoilComponentState(
|
||||
workflowLastCreatedStepIdComponentState,
|
||||
);
|
||||
|
||||
const { getUpdatableWorkflowVersion } =
|
||||
useGetUpdatableWorkflowVersionOrThrow();
|
||||
|
||||
const duplicateStep = async ({ stepId }: { stepId: string }) => {
|
||||
if (isLoading === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const workflowVersionId = await getUpdatableWorkflowVersion();
|
||||
|
||||
const workflowVersionStepChanges = (
|
||||
await duplicateWorkflowVersionStep({
|
||||
workflowVersionId,
|
||||
stepId,
|
||||
})
|
||||
)?.data?.duplicateWorkflowVersionStep;
|
||||
|
||||
const createdStep = workflowVersionStepChanges?.createdStep;
|
||||
|
||||
if (!isDefined(createdStep)) {
|
||||
throw new Error("Couldn't duplicate step");
|
||||
}
|
||||
|
||||
setWorkflowSelectedNode(createdStep.id);
|
||||
setWorkflowLastCreatedStepId(createdStep.id);
|
||||
|
||||
return createdStep;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
duplicateStep,
|
||||
};
|
||||
};
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { DUPLICATE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/duplicateWorkflowVersionStep';
|
||||
import { flowComponentState } from '@/workflow/states/flowComponentState';
|
||||
import { useUpdateWorkflowVersionCache } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type DuplicateWorkflowVersionStepInput,
|
||||
type DuplicateWorkflowVersionStepMutation,
|
||||
type DuplicateWorkflowVersionStepMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useDuplicateWorkflowVersionStep = () => {
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const { updateWorkflowVersionCache } = useUpdateWorkflowVersionCache();
|
||||
|
||||
const setFlow = useSetRecoilComponentState(flowComponentState);
|
||||
|
||||
const [mutate] = useMutation<
|
||||
DuplicateWorkflowVersionStepMutation,
|
||||
DuplicateWorkflowVersionStepMutationVariables
|
||||
>(DUPLICATE_WORKFLOW_VERSION_STEP, {
|
||||
client: apolloCoreClient,
|
||||
});
|
||||
|
||||
const duplicateWorkflowVersionStep = async (
|
||||
input: DuplicateWorkflowVersionStepInput,
|
||||
) => {
|
||||
const result = await mutate({
|
||||
variables: { input },
|
||||
});
|
||||
|
||||
const workflowVersionStepChanges =
|
||||
result?.data?.duplicateWorkflowVersionStep;
|
||||
|
||||
const updatedWorkflowVersion = updateWorkflowVersionCache({
|
||||
workflowVersionStepChanges,
|
||||
workflowVersionId: input.workflowVersionId,
|
||||
});
|
||||
|
||||
if (isDefined(updatedWorkflowVersion)) {
|
||||
setFlow({
|
||||
workflowVersionId: updatedWorkflowVersion.id,
|
||||
trigger: updatedWorkflowVersion.trigger,
|
||||
steps: updatedWorkflowVersion.steps,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return { duplicateWorkflowVersionStep };
|
||||
};
|
||||
+2
@@ -3,6 +3,7 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { type WorkflowAiAgentAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
@@ -145,6 +146,7 @@ export const WorkflowEditActionAiAgent = ({
|
||||
readonly={actionOptions.readonly}
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowActionFooter stepId={action.id} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+15
-10
@@ -24,7 +24,6 @@ import { getFunctionOutputSchema } from '@/serverless-functions/utils/getFunctio
|
||||
import { mergeDefaultFunctionInputAndFunctionInput } from '@/serverless-functions/utils/mergeDefaultFunctionInputAndFunctionInput';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
@@ -44,6 +43,7 @@ import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { type Monaco } from '@monaco-editor/react';
|
||||
import { type editor } from 'monaco-editor';
|
||||
import { AutoTypings } from 'monaco-editor-auto-typings';
|
||||
@@ -515,15 +515,20 @@ export const WorkflowEditActionServerlessFunction = ({
|
||||
</>
|
||||
)}
|
||||
</WorkflowStepBody>
|
||||
{activeTabId === WorkflowServerlessFunctionTabId.TEST && (
|
||||
<RightDrawerFooter
|
||||
actions={[
|
||||
<CmdEnterActionButton
|
||||
title="Test"
|
||||
onClick={handleRunFunction}
|
||||
disabled={isTesting || actionOptions.readonly}
|
||||
/>,
|
||||
]}
|
||||
{!actionOptions.readonly && (
|
||||
<WorkflowActionFooter
|
||||
stepId={action.id}
|
||||
additionalActions={
|
||||
activeTabId === WorkflowServerlessFunctionTabId.TEST
|
||||
? [
|
||||
<CmdEnterActionButton
|
||||
title="Test"
|
||||
onClick={handleRunFunction}
|
||||
disabled={isTesting}
|
||||
/>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{fullScreenOverlay}
|
||||
|
||||
+2
@@ -7,6 +7,7 @@ import { Select } from '@/ui/input/components/Select';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOrDefaultViewFromPrefetchedViews';
|
||||
import { type WorkflowCreateRecordAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
@@ -271,6 +272,7 @@ export const WorkflowEditActionCreateRecord = ({
|
||||
);
|
||||
})}
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowActionFooter stepId={action.id} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+2
@@ -6,6 +6,7 @@ import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/Workflo
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
@@ -179,6 +180,7 @@ export const WorkflowEditActionDeleteRecord = ({
|
||||
/>
|
||||
)}
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowActionFooter stepId={action.id} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+2
@@ -11,6 +11,7 @@ import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/Gene
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { type WorkflowSendEmailAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
@@ -294,6 +295,7 @@ export const WorkflowEditActionSendEmail = ({
|
||||
multiline
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowActionFooter stepId={action.id} />}
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
+2
@@ -9,6 +9,7 @@ import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-typ
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
@@ -265,6 +266,7 @@ export const WorkflowEditActionUpdateRecord = ({
|
||||
);
|
||||
})}
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowActionFooter stepId={action.id} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+2
@@ -13,6 +13,7 @@ import { type RecordFilter } from '@/object-record/record-filter/types/RecordFil
|
||||
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useRecordIndexFieldMetadataDerivedStates } from '@/object-record/record-index/hooks/useRecordIndexFieldMetadataDerivedStates';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowFindRecordsFilters } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsFilters';
|
||||
import { WorkflowFindRecordsFiltersEffect } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsFiltersEffect';
|
||||
@@ -232,6 +233,7 @@ export const WorkflowEditActionFindRecords = ({
|
||||
readonly
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowActionFooter stepId={action.id} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+2
@@ -6,6 +6,7 @@ import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||
import { type WorkflowFormAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { WorkflowEditActionFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings';
|
||||
@@ -398,6 +399,7 @@ export const WorkflowEditActionFormBuilder = ({
|
||||
</StyledAddFieldButtonContainer>
|
||||
)}
|
||||
</StyledWorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowActionFooter stepId={action.id} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+4
@@ -5,7 +5,9 @@ import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
|
||||
|
||||
@@ -56,9 +58,11 @@ const meta: Meta<typeof WorkflowEditActionFormBuilder> = {
|
||||
},
|
||||
decorators: [
|
||||
WorkflowStepActionDrawerDecorator,
|
||||
WorkflowStepDecorator,
|
||||
ComponentDecorator,
|
||||
RouterDecorator,
|
||||
I18nFrontDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
+15
-10
@@ -8,10 +8,10 @@ import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-acti
|
||||
|
||||
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/WorkflowActionFooter';
|
||||
import { isMethodWithBody } from '@/workflow/workflow-steps/workflow-actions/http-request-action/utils/isMethodWithBody';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useTheme } from '@emotion/react';
|
||||
@@ -217,15 +217,20 @@ export const WorkflowEditActionHttpRequest = ({
|
||||
</StyledTestTabContent>
|
||||
)}
|
||||
</WorkflowStepBody>
|
||||
{activeTabId === WorkflowHttpRequestTabId.TEST && (
|
||||
<RightDrawerFooter
|
||||
actions={[
|
||||
<CmdEnterActionButton
|
||||
title="Test"
|
||||
onClick={handleTestRequest}
|
||||
disabled={isTesting || actionOptions.readonly}
|
||||
/>,
|
||||
]}
|
||||
{!actionOptions.readonly && (
|
||||
<WorkflowActionFooter
|
||||
stepId={action.id}
|
||||
additionalActions={
|
||||
activeTabId === WorkflowHttpRequestTabId.TEST
|
||||
? [
|
||||
<CmdEnterActionButton
|
||||
title="Test"
|
||||
onClick={handleTestRequest}
|
||||
disabled={isTesting}
|
||||
/>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
+2
@@ -3,6 +3,7 @@ import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { expect, fn, waitFor, within } from '@storybook/test';
|
||||
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||
@@ -84,6 +85,7 @@ const meta: Meta<typeof WorkflowEditActionHttpRequest> = {
|
||||
SnackBarDecorator,
|
||||
WorkspaceDecorator,
|
||||
I18nFrontDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
@InputType()
|
||||
export class DuplicateWorkflowVersionStepInput {
|
||||
@Field(() => String)
|
||||
stepId: string;
|
||||
|
||||
@Field(() => String)
|
||||
workflowVersionId: string;
|
||||
}
|
||||
+16
@@ -7,6 +7,7 @@ import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
|
||||
import { DeleteWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/delete-workflow-version-step-input.dto';
|
||||
import { DuplicateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/duplicate-workflow-version-step-input.dto';
|
||||
import { SubmitFormStepInput } from 'src/engine/core-modules/workflow/dtos/submit-form-step-input.dto';
|
||||
import { UpdateWorkflowRunStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-run-step-input.dto';
|
||||
import { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-step-input.dto';
|
||||
@@ -124,4 +125,19 @@ export class WorkflowVersionStepResolver {
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
@Mutation(() => WorkflowVersionStepChangesDTO)
|
||||
async duplicateWorkflowVersionStep(
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
@Args('input')
|
||||
{ stepId, workflowVersionId }: DuplicateWorkflowVersionStepInput,
|
||||
): Promise<WorkflowVersionStepChangesDTO> {
|
||||
return this.workflowVersionStepWorkspaceService.duplicateWorkflowVersionStep(
|
||||
{
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
stepId,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -10,4 +10,5 @@ export enum ServerlessFunctionExceptionCode {
|
||||
SERVERLESS_FUNCTION_BUILDING = 'SERVERLESS_FUNCTION_BUILDING',
|
||||
SERVERLESS_FUNCTION_CODE_UNCHANGED = 'SERVERLESS_FUNCTION_CODE_UNCHANGED',
|
||||
SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED = 'SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED',
|
||||
SERVERLESS_FUNCTION_CREATE_FAILED = 'SERVERLESS_FUNCTION_CREATE_FAILED',
|
||||
}
|
||||
|
||||
+52
-1
@@ -363,7 +363,7 @@ export class ServerlessFunctionService {
|
||||
});
|
||||
}
|
||||
|
||||
async usePublishedVersionAsDraft({
|
||||
async createDraftFromPublishedVersion({
|
||||
id,
|
||||
version,
|
||||
workspaceId,
|
||||
@@ -400,6 +400,57 @@ export class ServerlessFunctionService {
|
||||
});
|
||||
}
|
||||
|
||||
async duplicateServerlessFunction({
|
||||
id,
|
||||
version,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
version: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const serverlessFunctionToDuplicate =
|
||||
await this.serverlessFunctionRepository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const newServerlessFunction = await this.createOneServerlessFunction(
|
||||
{
|
||||
name: serverlessFunctionToDuplicate.name,
|
||||
description: serverlessFunctionToDuplicate.description,
|
||||
timeoutSeconds: serverlessFunctionToDuplicate.timeoutSeconds,
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isDefined(newServerlessFunction)) {
|
||||
throw new ServerlessFunctionException(
|
||||
'Failed to create new serverless function',
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CREATE_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
await this.fileStorageService.copy({
|
||||
from: {
|
||||
folderPath: getServerlessFolder({
|
||||
serverlessFunction: serverlessFunctionToDuplicate,
|
||||
version,
|
||||
}),
|
||||
},
|
||||
to: {
|
||||
folderPath: getServerlessFolder({
|
||||
serverlessFunction: newServerlessFunction,
|
||||
version: 'draft',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return newServerlessFunction;
|
||||
}
|
||||
|
||||
private async throttleExecution(workspaceId: string) {
|
||||
try {
|
||||
await this.throttlerService.throttle(
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ export const serverlessFunctionGraphQLApiExceptionHandler = (error: any) => {
|
||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED:
|
||||
throw new ForbiddenError(error);
|
||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CODE_UNCHANGED:
|
||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CREATE_FAILED:
|
||||
throw error;
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
|
||||
+141
-25
@@ -48,6 +48,8 @@ const BASE_STEP_DEFINITION: BaseWorkflowActionSettings = {
|
||||
},
|
||||
};
|
||||
|
||||
const DUPLICATED_STEP_POSITION_OFFSET = 50;
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowVersionStepWorkspaceService {
|
||||
constructor(
|
||||
@@ -279,36 +281,69 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
});
|
||||
}
|
||||
|
||||
async duplicateStep({
|
||||
step,
|
||||
async duplicateWorkflowVersionStep({
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
stepId,
|
||||
}: {
|
||||
step: WorkflowAction;
|
||||
workspaceId: string;
|
||||
}): Promise<WorkflowAction> {
|
||||
switch (step.type) {
|
||||
case WorkflowActionType.CODE: {
|
||||
await this.serverlessFunctionService.usePublishedVersionAsDraft({
|
||||
id: step.settings.input.serverlessFunctionId,
|
||||
version: step.settings.input.serverlessFunctionVersion,
|
||||
workspaceId,
|
||||
});
|
||||
workflowVersionId: string;
|
||||
stepId: string;
|
||||
}): Promise<WorkflowVersionStepChangesDTO> {
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
return {
|
||||
...step,
|
||||
settings: {
|
||||
...step.settings,
|
||||
input: {
|
||||
...step.settings.input,
|
||||
serverlessFunctionVersion: 'draft',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return step;
|
||||
}
|
||||
const workflowVersion = await workflowVersionRepository.findOne({
|
||||
where: {
|
||||
id: workflowVersionId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(workflowVersion)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
'WorkflowVersion not found',
|
||||
WorkflowVersionStepExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
assertWorkflowVersionIsDraft(workflowVersion);
|
||||
|
||||
const stepToDuplicate = workflowVersion.steps?.find(
|
||||
(step) => step.id === stepId,
|
||||
);
|
||||
|
||||
if (!isDefined(stepToDuplicate)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
'Step not found',
|
||||
WorkflowVersionStepExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const duplicatedStep = await this.createStepForDuplicate({
|
||||
step: stepToDuplicate,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const { updatedSteps, updatedInsertedStep, updatedTrigger } = insertStep({
|
||||
existingSteps: workflowVersion.steps ?? [],
|
||||
existingTrigger: workflowVersion.trigger,
|
||||
insertedStep: duplicatedStep,
|
||||
});
|
||||
|
||||
await workflowVersionRepository.update(workflowVersion.id, {
|
||||
steps: updatedSteps,
|
||||
trigger: updatedTrigger,
|
||||
});
|
||||
|
||||
return computeWorkflowVersionStepChanges({
|
||||
createdStep: updatedInsertedStep,
|
||||
trigger: updatedTrigger,
|
||||
steps: updatedSteps,
|
||||
});
|
||||
}
|
||||
|
||||
async submitFormStep({
|
||||
@@ -372,6 +407,38 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
});
|
||||
}
|
||||
|
||||
async createDraftStep({
|
||||
step,
|
||||
workspaceId,
|
||||
}: {
|
||||
step: WorkflowAction;
|
||||
workspaceId: string;
|
||||
}): Promise<WorkflowAction> {
|
||||
switch (step.type) {
|
||||
case WorkflowActionType.CODE: {
|
||||
await this.serverlessFunctionService.createDraftFromPublishedVersion({
|
||||
id: step.settings.input.serverlessFunctionId,
|
||||
version: step.settings.input.serverlessFunctionVersion,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
...step,
|
||||
settings: {
|
||||
...step.settings,
|
||||
input: {
|
||||
...step.settings.input,
|
||||
serverlessFunctionVersion: 'draft',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return step;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async enrichOutputSchema({
|
||||
step,
|
||||
workspaceId,
|
||||
@@ -745,4 +812,53 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
private async createStepForDuplicate({
|
||||
step,
|
||||
workspaceId,
|
||||
}: {
|
||||
step: WorkflowAction;
|
||||
workspaceId: string;
|
||||
}): Promise<WorkflowAction> {
|
||||
const duplicatedStepPosition = {
|
||||
x: (step.position?.x ?? 0) + DUPLICATED_STEP_POSITION_OFFSET,
|
||||
y: (step.position?.y ?? 0) + DUPLICATED_STEP_POSITION_OFFSET,
|
||||
};
|
||||
|
||||
switch (step.type) {
|
||||
case WorkflowActionType.CODE: {
|
||||
const newServerlessFunction =
|
||||
await this.serverlessFunctionService.duplicateServerlessFunction({
|
||||
id: step.settings.input.serverlessFunctionId,
|
||||
version: step.settings.input.serverlessFunctionVersion,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
...step,
|
||||
id: v4(),
|
||||
name: `${step.name} (Duplicate)`,
|
||||
nextStepIds: [],
|
||||
position: duplicatedStepPosition,
|
||||
settings: {
|
||||
...step.settings,
|
||||
input: {
|
||||
...step.settings.input,
|
||||
serverlessFunctionId: newServerlessFunction.id,
|
||||
serverlessFunctionVersion: 'draft',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
...step,
|
||||
id: v4(),
|
||||
name: `${step.name} (Duplicate)`,
|
||||
nextStepIds: [],
|
||||
position: duplicatedStepPosition,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ export class WorkflowVersionWorkspaceService {
|
||||
|
||||
for (const step of workflowVersionToCopy.steps) {
|
||||
const duplicatedStep =
|
||||
await this.workflowVersionStepWorkspaceService.duplicateStep({
|
||||
await this.workflowVersionStepWorkspaceService.createDraftStep({
|
||||
step,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user