Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion backend/src/baserow/contrib/automation/nodes/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ def undo(
action_to_undo: Action,
):
# Trash the duplicated node.
AutomationNodeService().delete_node(user, params.duplicated_node_id)
AutomationNodeService().delete_node(
user, params.duplicated_node_id, ignore_user_for_signal=True
)

@classmethod
def redo(
Expand Down
8 changes: 3 additions & 5 deletions backend/src/baserow/contrib/automation/nodes/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,7 @@ def update_node(
)

def delete_node(
self,
user: AbstractUser,
node_id: int,
self, user: AbstractUser | None, node_id: int, ignore_user_for_signal=False
) -> AutomationNode:
"""
Deletes the specified automation node.
Expand All @@ -268,7 +266,7 @@ def delete_node(
node.get_type().before_delete(node.specific)

TrashHandler.trash(
user,
user if not ignore_user_for_signal else None,
automation.workspace,
automation,
node,
Expand All @@ -278,7 +276,7 @@ def delete_node(
self,
workflow=workflow,
node_id=node.id,
user=user,
user=user if not ignore_user_for_signal else None,
)

return node
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
@remove-node="emit('remove-node', $event)"
@replace-node="emit('replace-node', $event)"
@move-node="emit('move-node', $event)"
@duplicate-node="emit('duplicate-node', $event)"
/>
</div>
</template>
Expand Down Expand Up @@ -90,7 +91,14 @@ const props = defineProps({
},
})

const emit = defineEmits(['add-node', 'select-node', 'move-node'])
const emit = defineEmits([
'add-node',
'select-node',
'move-node',
'remove-node',
'replace-node',
'duplicate-node',
])

const store = useStore()
const workflow = inject('workflow')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
@replace-node="emit('replace-node', $event)"
@select-node="emit('input', $event.id)"
@move-node="emit('move-node', $event)"
@duplicate-node="emit('duplicate-node', $event)"
/>
<template v-else>
<div class="workflow-editor__trigger-selector" @scroll.stop>
Expand Down Expand Up @@ -72,7 +73,13 @@ const props = defineProps({
})

const vueFlowEdges = []
const emit = defineEmits(['add-node', 'remove-node', 'input', 'move-node'])
const emit = defineEmits([
'add-node',
'remove-node',
'input',
'move-node',
'duplicate-node',
])

const { onPaneClick } = useVueFlow()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
@select-node="emit('select-node', $event)"
@remove-node="emit('remove-node', $event)"
@replace-node="emit('replace-node', $event)"
@duplicate-node="emit('duplicate-node', $event)"
/>
<div v-if="nodeType.isContainer" class="workflow-node__children">
<div ref="children" class="workflow-node__children-wrapper">
Expand Down Expand Up @@ -67,6 +68,7 @@
@remove-node="emit('remove-node', $event)"
@replace-node="emit('replace-node', $event)"
@move-node="emit('move-node', $event)"
@duplicate-node="emit('duplicate-node', $event)"
/>
</div>
</div>
Expand Down Expand Up @@ -111,6 +113,7 @@ const emit = defineEmits([
'remove-node',
'replace-node',
'move-node',
'duplicate-node',
])

const { app } = useContext()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,17 @@
{{ $t('workflowNode.moreReplace') }}
</a>
</li>
<li class="context__menu-item">
<li v-if="canBeDuplicated" class="context__menu-item">
<a
role="button"
class="context__menu-item-link"
@click="emit('duplicate-node', node.id)"
>
<i class="context__menu-item-icon iconoir-copy"></i>
{{ $t('workflowNode.moreDuplicate') }}
</a>
</li>
<li class="context__menu-item context__menu-item--with-separator">
<a
:key="getDeleteErrorMessage"
v-tooltip="getDeleteErrorMessage || null"
Expand Down Expand Up @@ -131,7 +141,12 @@ const props = defineProps({
},
})

const emit = defineEmits(['remove-node', 'replace-node', 'select-node'])
const emit = defineEmits([
'remove-node',
'replace-node',
'select-node',
'duplicate-node',
])

const isDragging = ref(false)

Expand Down Expand Up @@ -277,4 +292,11 @@ const getDataBeforeLabel = computed(() => {
output,
})
})

const canBeDuplicated = computed(() => {
return nodeType.value.isDuplicable({
workflow: workflow.value,
node: props.node,
})
})
</script>
4 changes: 3 additions & 1 deletion web-frontend/modules/automation/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@
"beforeLabelRepeat": "For each item",
"moreEdit": "Edit",
"moreReplace": "Replace",
"nodeOptions": "Node options"
"nodeOptions": "Node options",
"displayLabelDebug": "ID: {id} | Prev: {previousNodeId} | {outputUid}",
"moreDuplicate": "Duplicate"
},
"workflowAddNode": {
"displayTitle": "Create automation node"
Expand Down
4 changes: 4 additions & 0 deletions web-frontend/modules/automation/nodeTypeMixins.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export const TriggerNodeTypeMixin = (Base) =>
getDeleteErrorMessage({ workflow, node }) {
return this.app.i18n.t('nodeType.triggerDeletionError')
}

isDuplicable({ workflow, node }) {
return false
}
}

export const ActionNodeTypeMixin = (Base) =>
Expand Down
15 changes: 15 additions & 0 deletions web-frontend/modules/automation/nodeTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ export class NodeType extends Registerable {
return Boolean(this.getDeleteErrorMessage({ workflow, node }))
}

/**
* Returns whether the given node can be duplicated.
*/
isDuplicable({ workflow, node }) {
return true
}

/**
* Returns the error message we should show when a node cannot be deleted.
* By default, this method is empty, but can be overridden by the node type
Expand Down Expand Up @@ -655,6 +662,10 @@ export class CoreIteratorNodeType extends containerNodeTypeMixin(
}
return ''
}

isDuplicable({ workflow, node }) {
return false
}
}

export class CoreSMTPEmailNodeType extends ActionNodeTypeMixin(NodeType) {
Expand Down Expand Up @@ -805,6 +816,10 @@ export class CoreRouterNodeType extends ActionNodeTypeMixin(
},
]
}

isDuplicable({ workflow, node }) {
return false
}
}

export class AIAgentActionNodeType extends ActionNodeTypeMixin(NodeType) {
Expand Down
7 changes: 7 additions & 0 deletions web-frontend/modules/automation/pages/automationWorkflow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
@remove-node="handleRemoveNode"
@replace-node="handleReplaceNode"
@move-node="handleMoveNode"
@duplicate-node="handleDuplicateNode"
/>
</client-only>
</div>
Expand Down Expand Up @@ -265,6 +266,12 @@ export default {
notifyIf(err, 'automation')
}
},
async handleDuplicateNode(nodeId) {
await this.$store.dispatch('automationWorkflowNode/duplicate', {
workflow: this.workflow,
nodeId,
})
},
},
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,8 @@ export default (client) => {
simulateDispatch(nodeId) {
return client.post(`automation/node/${nodeId}/simulate-dispatch/`)
},
duplicate(nodeId) {
return client.post(`automation/node/${nodeId}/duplicate/`)
},
}
}
66 changes: 66 additions & 0 deletions web-frontend/modules/automation/store/automationWorkflowNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ const actions = {
type,
workflow: workflow.id,
})

commit('ADD_ITEM', { workflow, node: tempNode })

const initialGraph = clone(workflow.graph)
Expand Down Expand Up @@ -407,6 +408,71 @@ const actions = {
throw error
}
},
async duplicate({ commit, dispatch, getters }, { workflow, nodeId }) {
const nodeToDuplicate = getters.findById(workflow, nodeId)
if (!nodeToDuplicate) {
return
}

// Get the node type to properly initialize the node
const nodeType = this.$registry.get('node', nodeToDuplicate.type)

// Use getDefaultValues like in create, but override with duplicated node's data
const tempNode = nodeType.getDefaultValues({
...nodeToDuplicate, // Copy all properties from the original
id: uuid(), // But give it a new ID
workflow: workflow.id,
})

commit('ADD_ITEM', { workflow, node: tempNode })

const initialGraph = clone(workflow.graph)

// Insert the duplicated node after the original node using 'south' position
await dispatch('graphInsert', {
workflow,
node: tempNode,
referenceNode: nodeToDuplicate,
position: 'south',
output: '', // Default output for creating after a node
})

try {
const { data: node } = await AutomationWorkflowNodeService(
this.$client
).duplicate(nodeId)

commit('ADD_ITEM', { workflow, node })

await dispatch('graphReplace', {
workflow,
nodeToReplace: tempNode,
newNode: node,
})

// Remove temp node and add real one
commit('DELETE_ITEM', { workflow, nodeId: tempNode.id })

setTimeout(() => {
const populatedNode = getters.findById(workflow, node.id)
dispatch('select', { workflow, node: populatedNode })
})

return node
} catch (error) {
// If API fails, restore the initial graph
await dispatch(
'automationWorkflow/forceUpdate',
{
workflow,
values: { graph: initialGraph },
},
{ root: true }
)
commit('DELETE_ITEM', { workflow, nodeId: tempNode.id })
throw error
}
},
select({ commit, dispatch }, { workflow, node }) {
commit('SELECT_ITEM', { workflow, node })
dispatch(
Expand Down
Loading