feat: add duplicate flow functionality
This commit is contained in:
@@ -9,6 +9,7 @@ import updateFlow from './mutations/update-flow';
|
|||||||
import updateFlowStatus from './mutations/update-flow-status';
|
import updateFlowStatus from './mutations/update-flow-status';
|
||||||
import executeFlow from './mutations/execute-flow';
|
import executeFlow from './mutations/execute-flow';
|
||||||
import deleteFlow from './mutations/delete-flow';
|
import deleteFlow from './mutations/delete-flow';
|
||||||
|
import duplicateFlow from './mutations/duplicate-flow';
|
||||||
import createStep from './mutations/create-step';
|
import createStep from './mutations/create-step';
|
||||||
import updateStep from './mutations/update-step';
|
import updateStep from './mutations/update-step';
|
||||||
import deleteStep from './mutations/delete-step';
|
import deleteStep from './mutations/delete-step';
|
||||||
@@ -31,6 +32,7 @@ const mutationResolvers = {
|
|||||||
updateFlowStatus,
|
updateFlowStatus,
|
||||||
executeFlow,
|
executeFlow,
|
||||||
deleteFlow,
|
deleteFlow,
|
||||||
|
duplicateFlow,
|
||||||
createStep,
|
createStep,
|
||||||
updateStep,
|
updateStep,
|
||||||
deleteStep,
|
deleteStep,
|
||||||
|
88
packages/backend/src/graphql/mutations/duplicate-flow.ts
Normal file
88
packages/backend/src/graphql/mutations/duplicate-flow.ts
Normal 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;
|
@@ -56,6 +56,7 @@ type Mutation {
|
|||||||
updateFlowStatus(input: UpdateFlowStatusInput): Flow
|
updateFlowStatus(input: UpdateFlowStatusInput): Flow
|
||||||
executeFlow(input: ExecuteFlowInput): executeFlowType
|
executeFlow(input: ExecuteFlowInput): executeFlowType
|
||||||
deleteFlow(input: DeleteFlowInput): Boolean
|
deleteFlow(input: DeleteFlowInput): Boolean
|
||||||
|
duplicateFlow(input: DuplicateFlowInput): Flow
|
||||||
createStep(input: CreateStepInput): Step
|
createStep(input: CreateStepInput): Step
|
||||||
updateStep(input: UpdateStepInput): Step
|
updateStep(input: UpdateStepInput): Step
|
||||||
deleteStep(input: DeleteStepInput): Step
|
deleteStep(input: DeleteStepInput): Step
|
||||||
@@ -324,6 +325,10 @@ input DeleteFlowInput {
|
|||||||
id: String!
|
id: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input DuplicateFlowInput {
|
||||||
|
id: String!
|
||||||
|
}
|
||||||
|
|
||||||
input CreateStepInput {
|
input CreateStepInput {
|
||||||
id: String
|
id: String
|
||||||
previousStepId: String
|
previousStepId: String
|
||||||
|
@@ -7,6 +7,7 @@ import MenuItem from '@mui/material/MenuItem';
|
|||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
import { DELETE_FLOW } from 'graphql/mutations/delete-flow';
|
import { DELETE_FLOW } from 'graphql/mutations/delete-flow';
|
||||||
|
import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
@@ -22,8 +23,26 @@ export default function ContextMenu(
|
|||||||
const { flowId, onClose, anchorEl } = props;
|
const { flowId, onClose, anchorEl } = props;
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
const [deleteFlow] = useMutation(DELETE_FLOW);
|
const [deleteFlow] = useMutation(DELETE_FLOW);
|
||||||
|
const [duplicateFlow] = useMutation(
|
||||||
|
DUPLICATE_FLOW,
|
||||||
|
{
|
||||||
|
refetchQueries: ['GetFlows'],
|
||||||
|
}
|
||||||
|
);
|
||||||
const formatMessage = useFormatMessage();
|
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 () => {
|
const onFlowDelete = React.useCallback(async () => {
|
||||||
await deleteFlow({
|
await deleteFlow({
|
||||||
variables: { input: { id: flowId } },
|
variables: { input: { id: flowId } },
|
||||||
@@ -42,7 +61,9 @@ export default function ContextMenu(
|
|||||||
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
|
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
});
|
});
|
||||||
}, [flowId, deleteFlow]);
|
|
||||||
|
onClose();
|
||||||
|
}, [flowId, onClose, deleteFlow]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
@@ -55,6 +76,8 @@ export default function ContextMenu(
|
|||||||
{formatMessage('flow.view')}
|
{formatMessage('flow.view')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem onClick={onFlowDuplicate}>{formatMessage('flow.duplicate')}</MenuItem>
|
||||||
|
|
||||||
<MenuItem onClick={onFlowDelete}>{formatMessage('flow.delete')}</MenuItem>
|
<MenuItem onClick={onFlowDelete}>{formatMessage('flow.delete')}</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
28
packages/web/src/graphql/mutations/duplicate-flow.ts
Normal file
28
packages/web/src/graphql/mutations/duplicate-flow.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
@@ -46,6 +46,7 @@
|
|||||||
"flow.paused": "Paused",
|
"flow.paused": "Paused",
|
||||||
"flow.draft": "Draft",
|
"flow.draft": "Draft",
|
||||||
"flow.successfullyDeleted": "The flow and associated executions have been deleted.",
|
"flow.successfullyDeleted": "The flow and associated executions have been deleted.",
|
||||||
|
"flow.successfullyDuplicated": "The flow has been successfully duplicated.",
|
||||||
"flowEditor.publish": "PUBLISH",
|
"flowEditor.publish": "PUBLISH",
|
||||||
"flowEditor.unpublish": "UNPUBLISH",
|
"flowEditor.unpublish": "UNPUBLISH",
|
||||||
"flowEditor.publishedFlowCannotBeUpdated": "To edit this flow, you must first unpublish it.",
|
"flowEditor.publishedFlowCannotBeUpdated": "To edit this flow, you must first unpublish it.",
|
||||||
@@ -68,6 +69,7 @@
|
|||||||
"flow.createdAt": "created {datetime}",
|
"flow.createdAt": "created {datetime}",
|
||||||
"flow.updatedAt": "updated {datetime}",
|
"flow.updatedAt": "updated {datetime}",
|
||||||
"flow.view": "View",
|
"flow.view": "View",
|
||||||
|
"flow.duplicate": "Duplicate",
|
||||||
"flow.delete": "Delete",
|
"flow.delete": "Delete",
|
||||||
"flowStep.triggerType": "Trigger",
|
"flowStep.triggerType": "Trigger",
|
||||||
"flowStep.actionType": "Action",
|
"flowStep.actionType": "Action",
|
||||||
|
Reference in New Issue
Block a user