feat: add duplicate flow functionality

This commit is contained in:
Ali BARIN
2023-05-12 15:58:05 +00:00
parent a5dbac9817
commit 9776c9f5a4
6 changed files with 149 additions and 1 deletions

View File

@@ -9,6 +9,7 @@ import updateFlow from './mutations/update-flow';
import updateFlowStatus from './mutations/update-flow-status';
import executeFlow from './mutations/execute-flow';
import deleteFlow from './mutations/delete-flow';
import duplicateFlow from './mutations/duplicate-flow';
import createStep from './mutations/create-step';
import updateStep from './mutations/update-step';
import deleteStep from './mutations/delete-step';
@@ -31,6 +32,7 @@ const mutationResolvers = {
updateFlowStatus,
executeFlow,
deleteFlow,
duplicateFlow,
createStep,
updateStep,
deleteStep,

View File

@@ -0,0 +1,88 @@
import Context from '../../types/express/context';
import Step from '../../models/step';
type Params = {
input: {
id: string;
};
};
type NewStepIds = Record<string, string>;
function updateStepId(value: string, newStepIds: NewStepIds) {
let newValue = value;
const stepIdEntries = Object.entries(newStepIds);
for (const stepIdEntry of stepIdEntries) {
const [oldStepId, newStepId] = stepIdEntry;
const partialOldVariable = `{{step.${oldStepId}.`;
const partialNewVariable = `{{step.${newStepId}.`;
newValue = newValue.replace(partialOldVariable, partialNewVariable);
}
return newValue;
}
function updateStepVariables(parameters: Step['parameters'], newStepIds: NewStepIds): Step['parameters'] {
const entries = Object.entries(parameters);
return entries.reduce((result, [key, value]: [string, unknown]) => {
if (typeof value === 'string') {
return {
...result,
[key]: updateStepId(value, newStepIds),
};
}
if (Array.isArray(value)) {
return {
...result,
[key]: value.map(item => updateStepVariables(item, newStepIds)),
};
}
return {
...result,
[key]: value,
};
}, {});
}
const duplicateFlow = async (
_parent: unknown,
params: Params,
context: Context
) => {
const flow = await context.currentUser
.$relatedQuery('flows')
.withGraphJoined('[steps]')
.orderBy('steps.position', 'asc')
.findOne({ 'flows.id': params.input.id })
.throwIfNotFound();
const duplicatedFlow = await context.currentUser
.$relatedQuery('flows')
.insert({
name: `Copy of ${flow.name}`,
active: false,
});
const newStepIds: NewStepIds = {};
for (const step of flow.steps) {
const duplicatedStep = await duplicatedFlow.$relatedQuery('steps')
.insert({
key: step.key,
appKey: step.appKey,
type: step.type,
connectionId: step.connectionId,
position: step.position,
parameters: updateStepVariables(step.parameters, newStepIds),
});
newStepIds[step.id] = duplicatedStep.id;
}
return duplicatedFlow;
};
export default duplicateFlow;

View File

@@ -56,6 +56,7 @@ type Mutation {
updateFlowStatus(input: UpdateFlowStatusInput): Flow
executeFlow(input: ExecuteFlowInput): executeFlowType
deleteFlow(input: DeleteFlowInput): Boolean
duplicateFlow(input: DuplicateFlowInput): Flow
createStep(input: CreateStepInput): Step
updateStep(input: UpdateStepInput): Step
deleteStep(input: DeleteStepInput): Step
@@ -324,6 +325,10 @@ input DeleteFlowInput {
id: String!
}
input DuplicateFlowInput {
id: String!
}
input CreateStepInput {
id: String
previousStepId: String

View File

@@ -7,6 +7,7 @@ import MenuItem from '@mui/material/MenuItem';
import { useSnackbar } from 'notistack';
import { DELETE_FLOW } from 'graphql/mutations/delete-flow';
import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
@@ -22,8 +23,26 @@ export default function ContextMenu(
const { flowId, onClose, anchorEl } = props;
const { enqueueSnackbar } = useSnackbar();
const [deleteFlow] = useMutation(DELETE_FLOW);
const [duplicateFlow] = useMutation(
DUPLICATE_FLOW,
{
refetchQueries: ['GetFlows'],
}
);
const formatMessage = useFormatMessage();
const onFlowDuplicate = React.useCallback(async () => {
await duplicateFlow({
variables: { input: { id: flowId } },
});
enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), {
variant: 'success',
});
onClose();
}, [flowId, onClose, duplicateFlow]);
const onFlowDelete = React.useCallback(async () => {
await deleteFlow({
variables: { input: { id: flowId } },
@@ -42,7 +61,9 @@ export default function ContextMenu(
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
variant: 'success',
});
}, [flowId, deleteFlow]);
onClose();
}, [flowId, onClose, deleteFlow]);
return (
<Menu
@@ -55,6 +76,8 @@ export default function ContextMenu(
{formatMessage('flow.view')}
</MenuItem>
<MenuItem onClick={onFlowDuplicate}>{formatMessage('flow.duplicate')}</MenuItem>
<MenuItem onClick={onFlowDelete}>{formatMessage('flow.delete')}</MenuItem>
</Menu>
);

View File

@@ -0,0 +1,28 @@
import { gql } from '@apollo/client';
export const DUPLICATE_FLOW = gql`
mutation DuplicateFlow($input: DuplicateFlowInput) {
duplicateFlow(input: $input) {
id
name
active
status
steps {
id
type
key
appKey
iconUrl
webhookUrl
status
position
connection {
id
verified
createdAt
}
parameters
}
}
}
`;

View File

@@ -46,6 +46,7 @@
"flow.paused": "Paused",
"flow.draft": "Draft",
"flow.successfullyDeleted": "The flow and associated executions have been deleted.",
"flow.successfullyDuplicated": "The flow has been successfully duplicated.",
"flowEditor.publish": "PUBLISH",
"flowEditor.unpublish": "UNPUBLISH",
"flowEditor.publishedFlowCannotBeUpdated": "To edit this flow, you must first unpublish it.",
@@ -68,6 +69,7 @@
"flow.createdAt": "created {datetime}",
"flow.updatedAt": "updated {datetime}",
"flow.view": "View",
"flow.duplicate": "Duplicate",
"flow.delete": "Delete",
"flowStep.triggerType": "Trigger",
"flowStep.actionType": "Action",