mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 20:54:07 -08:00
feat(editor): Add duplicate and copy/paste to canvas v2 (no-changelog) (#10112)
This commit is contained in:
parent
fc4184773a
commit
2e5c548452
|
@ -7,6 +7,7 @@ import {
|
||||||
MODAL_CONFIRM,
|
MODAL_CONFIRM,
|
||||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||||
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
||||||
|
VALID_WORKFLOW_IMPORT_URL_REGEX,
|
||||||
VIEWS,
|
VIEWS,
|
||||||
WORKFLOW_MENU_ACTIONS,
|
WORKFLOW_MENU_ACTIONS,
|
||||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||||
|
@ -454,7 +455,7 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
|
||||||
confirmButtonText: locale.baseText('mainSidebar.prompt.import'),
|
confirmButtonText: locale.baseText('mainSidebar.prompt.import'),
|
||||||
cancelButtonText: locale.baseText('mainSidebar.prompt.cancel'),
|
cancelButtonText: locale.baseText('mainSidebar.prompt.cancel'),
|
||||||
inputErrorMessage: locale.baseText('mainSidebar.prompt.invalidUrl'),
|
inputErrorMessage: locale.baseText('mainSidebar.prompt.invalidUrl'),
|
||||||
inputPattern: /^http[s]?:\/\/.*\.json$/i,
|
inputPattern: VALID_WORKFLOW_IMPORT_URL_REGEX,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { useKeybindings } from '@/composables/useKeybindings';
|
||||||
import ContextMenu from '@/components/ContextMenu/ContextMenu.vue';
|
import ContextMenu from '@/components/ContextMenu/ContextMenu.vue';
|
||||||
import type { NodeCreatorOpenSource } from '@/Interface';
|
import type { NodeCreatorOpenSource } from '@/Interface';
|
||||||
import type { PinDataSource } from '@/composables/usePinnedData';
|
import type { PinDataSource } from '@/composables/usePinnedData';
|
||||||
|
import { isPresent } from '@/utils/typesUtils';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
|
@ -75,6 +76,7 @@ const {
|
||||||
project,
|
project,
|
||||||
nodes: graphNodes,
|
nodes: graphNodes,
|
||||||
onPaneReady,
|
onPaneReady,
|
||||||
|
findNode,
|
||||||
} = useVueFlow({ id: props.id, deleteKeyCode: null });
|
} = useVueFlow({ id: props.id, deleteKeyCode: null });
|
||||||
|
|
||||||
useKeybindings({
|
useKeybindings({
|
||||||
|
@ -131,11 +133,20 @@ function onSetNodeActive(id: string) {
|
||||||
emit('update:node:active', id);
|
emit('update:node:active', id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearSelectedNodes() {
|
||||||
|
removeSelectedNodes(selectedNodes.value);
|
||||||
|
}
|
||||||
|
|
||||||
function onSelectNode() {
|
function onSelectNode() {
|
||||||
if (!lastSelectedNode.value) return;
|
if (!lastSelectedNode.value) return;
|
||||||
emit('update:node:selected', lastSelectedNode.value.id);
|
emit('update:node:selected', lastSelectedNode.value.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSelectNodes(ids: string[]) {
|
||||||
|
clearSelectedNodes();
|
||||||
|
addSelectedNodes(ids.map(findNode).filter(isPresent));
|
||||||
|
}
|
||||||
|
|
||||||
function onToggleNodeEnabled(id: string) {
|
function onToggleNodeEnabled(id: string) {
|
||||||
emit('update:node:enabled', id);
|
emit('update:node:enabled', id);
|
||||||
}
|
}
|
||||||
|
@ -288,7 +299,7 @@ function onContextMenuAction(action: ContextMenuAction, nodeIds: string[]) {
|
||||||
case 'select_all':
|
case 'select_all':
|
||||||
return addSelectedNodes(graphNodes.value);
|
return addSelectedNodes(graphNodes.value);
|
||||||
case 'deselect_all':
|
case 'deselect_all':
|
||||||
return removeSelectedNodes(selectedNodes.value);
|
return clearSelectedNodes();
|
||||||
case 'duplicate':
|
case 'duplicate':
|
||||||
return emit('duplicate:nodes', nodeIds);
|
return emit('duplicate:nodes', nodeIds);
|
||||||
case 'toggle_pin':
|
case 'toggle_pin':
|
||||||
|
@ -310,10 +321,12 @@ function onContextMenuAction(action: ContextMenuAction, nodeIds: string[]) {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
props.eventBus.on('fitView', onFitView);
|
props.eventBus.on('fitView', onFitView);
|
||||||
|
props.eventBus.on('selectNodes', onSelectNodes);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
props.eventBus.off('fitView', onFitView);
|
props.eventBus.off('fitView', onFitView);
|
||||||
|
props.eventBus.off('selectNodes', onSelectNodes);
|
||||||
});
|
});
|
||||||
|
|
||||||
onPaneReady(async () => {
|
onPaneReady(async () => {
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`useCanvasOperations > copyNodes > should copy nodes 1`] = `
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "1",
|
||||||
|
"name": "Node 1",
|
||||||
|
"type": "type",
|
||||||
|
"position": [
|
||||||
|
40,
|
||||||
|
40
|
||||||
|
],
|
||||||
|
"typeVersion": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "2",
|
||||||
|
"name": "Node 2",
|
||||||
|
"type": "type",
|
||||||
|
"position": [
|
||||||
|
40,
|
||||||
|
40
|
||||||
|
],
|
||||||
|
"typeVersion": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {},
|
||||||
|
"pinData": {}
|
||||||
|
}",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`useCanvasOperations > cutNodes > should copy and delete nodes 1`] = `
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "1",
|
||||||
|
"name": "Node 1",
|
||||||
|
"type": "type",
|
||||||
|
"position": [
|
||||||
|
40,
|
||||||
|
40
|
||||||
|
],
|
||||||
|
"typeVersion": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "2",
|
||||||
|
"name": "Node 2",
|
||||||
|
"type": "type",
|
||||||
|
"position": [
|
||||||
|
40,
|
||||||
|
40
|
||||||
|
],
|
||||||
|
"typeVersion": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {},
|
||||||
|
"pinData": {}
|
||||||
|
}",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`;
|
|
@ -22,16 +22,22 @@ import { mock } from 'vitest-mock-extended';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
import { telemetry } from '@/plugins/telemetry';
|
||||||
|
import { useClipboard } from '@/composables/useClipboard';
|
||||||
|
|
||||||
vi.mock('vue-router', async () => {
|
vi.mock('vue-router', async (importOriginal) => {
|
||||||
const actual = await import('vue-router');
|
const actual = await importOriginal<{}>();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
useRouter: () => ({}),
|
useRouter: () => ({}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock('@/composables/useClipboard', async () => {
|
||||||
|
const copySpy = vi.fn();
|
||||||
|
return { useClipboard: vi.fn(() => ({ copy: copySpy })) };
|
||||||
|
});
|
||||||
|
|
||||||
describe('useCanvasOperations', () => {
|
describe('useCanvasOperations', () => {
|
||||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||||
let uiStore: ReturnType<typeof useUIStore>;
|
let uiStore: ReturnType<typeof useUIStore>;
|
||||||
|
@ -71,6 +77,7 @@ describe('useCanvasOperations', () => {
|
||||||
await workflowHelpers.initState(workflow);
|
await workflowHelpers.initState(workflow);
|
||||||
|
|
||||||
canvasOperations = useCanvasOperations({ router, lastClickPosition });
|
canvasOperations = useCanvasOperations({ router, lastClickPosition });
|
||||||
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addNode', () => {
|
describe('addNode', () => {
|
||||||
|
@ -904,4 +911,71 @@ describe('useCanvasOperations', () => {
|
||||||
expect(addConnectionSpy).toHaveBeenCalledWith({ connection });
|
expect(addConnectionSpy).toHaveBeenCalledWith({ connection });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('duplicateNodes', () => {
|
||||||
|
it('should duplicate nodes', async () => {
|
||||||
|
nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'type' })]);
|
||||||
|
const telemetrySpy = vi.spyOn(telemetry, 'track');
|
||||||
|
|
||||||
|
const nodes = buildImportNodes();
|
||||||
|
workflowsStore.setNodes(nodes);
|
||||||
|
|
||||||
|
const duplicatedNodeIds = await canvasOperations.duplicateNodes(['1', '2']);
|
||||||
|
expect(duplicatedNodeIds.length).toBe(2);
|
||||||
|
expect(duplicatedNodeIds).not.toContain('1');
|
||||||
|
expect(duplicatedNodeIds).not.toContain('2');
|
||||||
|
expect(workflowsStore.workflow.nodes.length).toEqual(4);
|
||||||
|
expect(telemetrySpy).toHaveBeenCalledWith(
|
||||||
|
'User duplicated nodes',
|
||||||
|
expect.objectContaining({ node_graph_string: expect.any(String), workflow_id: 'test' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('copyNodes', () => {
|
||||||
|
it('should copy nodes', async () => {
|
||||||
|
nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'type' })]);
|
||||||
|
const telemetrySpy = vi.spyOn(telemetry, 'track');
|
||||||
|
const nodes = buildImportNodes();
|
||||||
|
workflowsStore.setNodes(nodes);
|
||||||
|
|
||||||
|
await canvasOperations.copyNodes(['1', '2']);
|
||||||
|
expect(useClipboard().copy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(vi.mocked(useClipboard().copy).mock.calls).toMatchSnapshot();
|
||||||
|
expect(telemetrySpy).toHaveBeenCalledWith(
|
||||||
|
'User copied nodes',
|
||||||
|
expect.objectContaining({ node_types: ['type', 'type'], workflow_id: 'test' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cutNodes', () => {
|
||||||
|
it('should copy and delete nodes', async () => {
|
||||||
|
nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'type' })]);
|
||||||
|
const telemetrySpy = vi.spyOn(telemetry, 'track');
|
||||||
|
const nodes = buildImportNodes();
|
||||||
|
workflowsStore.setNodes(nodes);
|
||||||
|
|
||||||
|
await canvasOperations.cutNodes(['1', '2']);
|
||||||
|
expect(useClipboard().copy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(vi.mocked(useClipboard().copy).mock.calls).toMatchSnapshot();
|
||||||
|
expect(telemetrySpy).toHaveBeenCalledWith(
|
||||||
|
'User copied nodes',
|
||||||
|
expect.objectContaining({ node_types: ['type', 'type'], workflow_id: 'test' }),
|
||||||
|
);
|
||||||
|
expect(workflowsStore.getNodes().length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function buildImportNodes() {
|
||||||
|
return [
|
||||||
|
mockNode({ id: '1', name: 'Node 1', type: 'type' }),
|
||||||
|
mockNode({ id: '2', name: 'Node 2', type: 'type' }),
|
||||||
|
].map((node) => {
|
||||||
|
// Setting position in mockNode will wrap it in a Proxy
|
||||||
|
// This causes deepCopy to remove position -> set position after instead
|
||||||
|
node.position = [40, 40];
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import type {
|
||||||
AddedNodesAndConnections,
|
AddedNodesAndConnections,
|
||||||
INodeUi,
|
INodeUi,
|
||||||
ITag,
|
ITag,
|
||||||
|
IUsedCredential,
|
||||||
|
IWorkflowData,
|
||||||
IWorkflowDataUpdate,
|
IWorkflowDataUpdate,
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
XYPosition,
|
XYPosition,
|
||||||
|
@ -20,6 +22,7 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
import {
|
import {
|
||||||
|
EnterpriseEditionFeature,
|
||||||
FORM_TRIGGER_NODE_TYPE,
|
FORM_TRIGGER_NODE_TYPE,
|
||||||
QUICKSTART_NOTE_NAME,
|
QUICKSTART_NOTE_NAME,
|
||||||
STICKY_NODE_TYPE,
|
STICKY_NODE_TYPE,
|
||||||
|
@ -56,7 +59,6 @@ import {
|
||||||
} from '@/utils/canvasUtilsV2';
|
} from '@/utils/canvasUtilsV2';
|
||||||
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
||||||
import { isValidNodeConnectionType } from '@/utils/typeGuards';
|
import { isValidNodeConnectionType } from '@/utils/typeGuards';
|
||||||
import { isPresent } from '@/utils/typesUtils';
|
|
||||||
import type { Connection } from '@vue-flow/core';
|
import type { Connection } from '@vue-flow/core';
|
||||||
import type {
|
import type {
|
||||||
ConnectionTypes,
|
ConnectionTypes,
|
||||||
|
@ -64,20 +66,24 @@ import type {
|
||||||
IConnections,
|
IConnections,
|
||||||
INode,
|
INode,
|
||||||
INodeConnections,
|
INodeConnections,
|
||||||
|
INodeCredentials,
|
||||||
INodeInputConfiguration,
|
INodeInputConfiguration,
|
||||||
INodeOutputConfiguration,
|
INodeOutputConfiguration,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
INodeTypeNameVersion,
|
INodeTypeNameVersion,
|
||||||
|
IPinData,
|
||||||
ITelemetryTrackProperties,
|
ITelemetryTrackProperties,
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
Workflow,
|
Workflow,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeConnectionType, NodeHelpers, TelemetryHelpers } from 'n8n-workflow';
|
import { deepCopy, NodeConnectionType, NodeHelpers, TelemetryHelpers } from 'n8n-workflow';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { computed, nextTick } from 'vue';
|
import { computed, nextTick } from 'vue';
|
||||||
import type { useRouter } from 'vue-router';
|
import type { useRouter } from 'vue-router';
|
||||||
|
import { useClipboard } from '@/composables/useClipboard';
|
||||||
|
import { isPresent } from '../utils/typesUtils';
|
||||||
|
|
||||||
type AddNodeData = Partial<INodeUi> & {
|
type AddNodeData = Partial<INodeUi> & {
|
||||||
type: string;
|
type: string;
|
||||||
|
@ -116,6 +122,7 @@ export function useCanvasOperations({
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
|
const clipboard = useClipboard();
|
||||||
|
|
||||||
const editableWorkflow = computed(() => workflowsStore.workflow);
|
const editableWorkflow = computed(() => workflowsStore.workflow);
|
||||||
const editableWorkflowObject = computed(() => workflowsStore.getCurrentWorkflow());
|
const editableWorkflowObject = computed(() => workflowsStore.getCurrentWorkflow());
|
||||||
|
@ -296,14 +303,14 @@ export function useCanvasOperations({
|
||||||
ids: string[],
|
ids: string[],
|
||||||
{ trackHistory = true }: { trackHistory?: boolean } = {},
|
{ trackHistory = true }: { trackHistory?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
const nodes = ids.map((id) => workflowsStore.getNodeById(id)).filter(isPresent);
|
const nodes = workflowsStore.getNodesByIds(ids);
|
||||||
nodeHelpers.disableNodes(nodes, trackHistory);
|
nodeHelpers.disableNodes(nodes, trackHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleNodesPinned(ids: string[], source: PinDataSource) {
|
function toggleNodesPinned(ids: string[], source: PinDataSource) {
|
||||||
historyStore.startRecordingUndo();
|
historyStore.startRecordingUndo();
|
||||||
|
|
||||||
const nodes = ids.map((id) => workflowsStore.getNodeById(id)).filter(isPresent);
|
const nodes = workflowsStore.getNodesByIds(ids);
|
||||||
const nextStatePinned = nodes.some((node) => !workflowsStore.pinDataByNodeName(node.name));
|
const nextStatePinned = nodes.some((node) => !workflowsStore.pinDataByNodeName(node.name));
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
|
@ -1168,7 +1175,7 @@ export function useCanvasOperations({
|
||||||
|
|
||||||
// Get only the connections of the nodes that get created
|
// Get only the connections of the nodes that get created
|
||||||
const newConnections: IConnections = {};
|
const newConnections: IConnections = {};
|
||||||
const currentConnections = data.connections!;
|
const currentConnections = data.connections ?? {};
|
||||||
const createNodeNames = createNodes.map((node) => node.name);
|
const createNodeNames = createNodes.map((node) => node.name);
|
||||||
let sourceNode, type, sourceIndex, connectionIndex, connectionData;
|
let sourceNode, type, sourceIndex, connectionIndex, connectionData;
|
||||||
for (sourceNode of Object.keys(currentConnections)) {
|
for (sourceNode of Object.keys(currentConnections)) {
|
||||||
|
@ -1271,10 +1278,10 @@ export function useCanvasOperations({
|
||||||
workflowData: IWorkflowDataUpdate,
|
workflowData: IWorkflowDataUpdate,
|
||||||
source: string,
|
source: string,
|
||||||
importTags = true,
|
importTags = true,
|
||||||
): Promise<void> {
|
): Promise<IWorkflowDataUpdate> {
|
||||||
// If it is JSON check if it looks on the first look like data we can use
|
// If it is JSON check if it looks on the first look like data we can use
|
||||||
if (!workflowData.hasOwnProperty('nodes') || !workflowData.hasOwnProperty('connections')) {
|
if (!workflowData.hasOwnProperty('nodes') || !workflowData.hasOwnProperty('connections')) {
|
||||||
return;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -1346,10 +1353,6 @@ export function useCanvasOperations({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// By default we automatically deselect all the currently
|
|
||||||
// selected nodes and select the new ones
|
|
||||||
// this.deselectAllNodes();
|
|
||||||
|
|
||||||
// Fix the node position as it could be totally offscreen
|
// Fix the node position as it could be totally offscreen
|
||||||
// and the pasted nodes would so not be directly visible to
|
// and the pasted nodes would so not be directly visible to
|
||||||
// the user
|
// the user
|
||||||
|
@ -1360,17 +1363,14 @@ export function useCanvasOperations({
|
||||||
|
|
||||||
await addImportedNodesToWorkflow(workflowData);
|
await addImportedNodesToWorkflow(workflowData);
|
||||||
|
|
||||||
// setTimeout(() => {
|
|
||||||
// (data?.nodes ?? []).forEach((node: INodeUi) => {
|
|
||||||
// this.nodeSelectedByName(node.name);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
if (importTags && settingsStore.areTagsEnabled && Array.isArray(workflowData.tags)) {
|
if (importTags && settingsStore.areTagsEnabled && Array.isArray(workflowData.tags)) {
|
||||||
await importWorkflowTags(workflowData);
|
await importWorkflowTags(workflowData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return workflowData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.showError(error, i18n.baseText('nodeView.showError.importWorkflowData.title'));
|
toast.showError(error, i18n.baseText('nodeView.showError.importWorkflowData.title'));
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1383,9 +1383,9 @@ export function useCanvasOperations({
|
||||||
|
|
||||||
const creatingTagPromises: Array<Promise<ITag>> = [];
|
const creatingTagPromises: Array<Promise<ITag>> = [];
|
||||||
for (const tag of notFound) {
|
for (const tag of notFound) {
|
||||||
const creationPromise = tagsStore.create(tag.name).then((tag: ITag) => {
|
const creationPromise = tagsStore.create(tag.name).then((newTag: ITag) => {
|
||||||
allTags.push(tag);
|
allTags.push(newTag);
|
||||||
return tag;
|
return newTag;
|
||||||
});
|
});
|
||||||
|
|
||||||
creatingTagPromises.push(creationPromise);
|
creatingTagPromises.push(creationPromise);
|
||||||
|
@ -1394,7 +1394,7 @@ export function useCanvasOperations({
|
||||||
await Promise.all(creatingTagPromises);
|
await Promise.all(creatingTagPromises);
|
||||||
|
|
||||||
const tagIds = workflowTags.reduce((accu: string[], imported: ITag) => {
|
const tagIds = workflowTags.reduce((accu: string[], imported: ITag) => {
|
||||||
const tag = allTags.find((tag) => tag.name === imported.name);
|
const tag = allTags.find((t) => t.name === imported.name);
|
||||||
if (tag) {
|
if (tag) {
|
||||||
accu.push(tag.id);
|
accu.push(tag.id);
|
||||||
}
|
}
|
||||||
|
@ -1424,6 +1424,121 @@ export function useCanvasOperations({
|
||||||
return workflowData;
|
return workflowData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNodesToSave(nodes: INode[]): IWorkflowData {
|
||||||
|
const data = {
|
||||||
|
nodes: [] as INodeUi[],
|
||||||
|
connections: {} as IConnections,
|
||||||
|
pinData: {} as IPinData,
|
||||||
|
} satisfies IWorkflowData;
|
||||||
|
|
||||||
|
const exportedNodeNames = new Set<string>();
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
const nodeSaveData = workflowHelpers.getNodeDataToSave(node);
|
||||||
|
const pinDataForNode = workflowsStore.pinDataByNodeName(node.name);
|
||||||
|
|
||||||
|
if (pinDataForNode) {
|
||||||
|
data.pinData[node.name] = pinDataForNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
nodeSaveData.credentials &&
|
||||||
|
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)
|
||||||
|
) {
|
||||||
|
nodeSaveData.credentials = filterAllowedCredentials(
|
||||||
|
nodeSaveData.credentials,
|
||||||
|
workflowsStore.usedCredentials,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.nodes.push(nodeSaveData);
|
||||||
|
exportedNodeNames.add(node.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.connections = getConnectionsForNodes(data.nodes, exportedNodeNames);
|
||||||
|
|
||||||
|
workflowHelpers.removeForeignCredentialsFromWorkflow(data, credentialsStore.allCredentials);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterAllowedCredentials(
|
||||||
|
credentials: INodeCredentials,
|
||||||
|
usedCredentials: Record<string, IUsedCredential>,
|
||||||
|
): INodeCredentials {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(credentials).filter(([, credential]) => {
|
||||||
|
return (
|
||||||
|
credential.id &&
|
||||||
|
(!usedCredentials[credential.id] || usedCredentials[credential.id]?.currentUserHasAccess)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConnectionsForNodes(
|
||||||
|
nodes: INodeUi[],
|
||||||
|
includeNodeNames: Set<string>,
|
||||||
|
): Record<string, INodeConnections> {
|
||||||
|
const connections: Record<string, INodeConnections> = {};
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
const outgoingConnections = workflowsStore.outgoingConnectionsByNodeName(node.name);
|
||||||
|
if (!Object.keys(outgoingConnections).length) continue;
|
||||||
|
|
||||||
|
const filteredConnections = filterConnectionsByNodes(outgoingConnections, includeNodeNames);
|
||||||
|
if (Object.keys(filteredConnections).length) {
|
||||||
|
connections[node.name] = filteredConnections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return connections;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterConnectionsByNodes(
|
||||||
|
connections: Record<string, IConnection[][]>,
|
||||||
|
includeNodeNames: Set<string>,
|
||||||
|
): INodeConnections {
|
||||||
|
const filteredConnections: INodeConnections = {};
|
||||||
|
|
||||||
|
for (const [type, typeConnections] of Object.entries(connections)) {
|
||||||
|
const validConnections = typeConnections
|
||||||
|
.map((sourceConnections) =>
|
||||||
|
sourceConnections.filter((connection) => includeNodeNames.has(connection.node)),
|
||||||
|
)
|
||||||
|
.filter((sourceConnections) => sourceConnections.length > 0);
|
||||||
|
|
||||||
|
if (validConnections.length) {
|
||||||
|
filteredConnections[type] = validConnections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredConnections;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function duplicateNodes(ids: string[]) {
|
||||||
|
const workflowData = deepCopy(getNodesToSave(workflowsStore.getNodesByIds(ids)));
|
||||||
|
const result = await importWorkflowData(workflowData, 'duplicate', false);
|
||||||
|
|
||||||
|
return result.nodes?.map((node) => node.id).filter(isPresent) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyNodes(ids: string[]) {
|
||||||
|
const workflowData = deepCopy(getNodesToSave(workflowsStore.getNodesByIds(ids)));
|
||||||
|
|
||||||
|
await clipboard.copy(JSON.stringify(workflowData, null, 2));
|
||||||
|
|
||||||
|
telemetry.track('User copied nodes', {
|
||||||
|
node_types: workflowData.nodes.map((node) => node.type),
|
||||||
|
workflow_id: workflowsStore.workflowId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cutNodes(ids: string[]) {
|
||||||
|
await copyNodes(ids);
|
||||||
|
deleteNodes(ids);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
editableWorkflow,
|
editableWorkflow,
|
||||||
editableWorkflowObject,
|
editableWorkflowObject,
|
||||||
|
@ -1441,6 +1556,9 @@ export function useCanvasOperations({
|
||||||
revertRenameNode,
|
revertRenameNode,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
deleteNodes,
|
deleteNodes,
|
||||||
|
copyNodes,
|
||||||
|
cutNodes,
|
||||||
|
duplicateNodes,
|
||||||
revertDeleteNode,
|
revertDeleteNode,
|
||||||
addConnections,
|
addConnections,
|
||||||
createConnection,
|
createConnection,
|
||||||
|
|
|
@ -403,6 +403,7 @@ export const MODAL_CLOSE = 'close';
|
||||||
|
|
||||||
export const VALID_EMAIL_REGEX =
|
export const VALID_EMAIL_REGEX =
|
||||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
export const VALID_WORKFLOW_IMPORT_URL_REGEX = /^http[s]?:\/\/.*\.json$/i;
|
||||||
export const LOCAL_STORAGE_ACTIVATION_FLAG = 'N8N_HIDE_ACTIVATION_ALERT';
|
export const LOCAL_STORAGE_ACTIVATION_FLAG = 'N8N_HIDE_ACTIVATION_ALERT';
|
||||||
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG = 'N8N_PIN_DATA_DISCOVERY_NDV';
|
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG = 'N8N_PIN_DATA_DISCOVERY_NDV';
|
||||||
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG = 'N8N_PIN_DATA_DISCOVERY_CANVAS';
|
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG = 'N8N_PIN_DATA_DISCOVERY_CANVAS';
|
||||||
|
|
|
@ -64,7 +64,7 @@ import { useUIStore } from '@/stores/ui.store';
|
||||||
import { dataPinningEventBus } from '@/event-bus';
|
import { dataPinningEventBus } from '@/event-bus';
|
||||||
import { isObject } from '@/utils/objectUtils';
|
import { isObject } from '@/utils/objectUtils';
|
||||||
import { getPairedItemsMapping } from '@/utils/pairedItemUtils';
|
import { getPairedItemsMapping } from '@/utils/pairedItemUtils';
|
||||||
import { isJsonKeyObject, isEmpty, stringSizeInBytes } from '@/utils/typesUtils';
|
import { isJsonKeyObject, isEmpty, stringSizeInBytes, isPresent } from '@/utils/typesUtils';
|
||||||
import { makeRestApiRequest, unflattenExecutionData, ResponseError } from '@/utils/apiUtils';
|
import { makeRestApiRequest, unflattenExecutionData, ResponseError } from '@/utils/apiUtils';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
@ -250,6 +250,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
return workflow.value.nodes.find((node) => node.id === nodeId);
|
return workflow.value.nodes.find((node) => node.id === nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNodesByIds(nodeIds: string[]): INodeUi[] {
|
||||||
|
return nodeIds.map(getNodeById).filter(isPresent);
|
||||||
|
}
|
||||||
|
|
||||||
function getParametersLastUpdate(nodeName: string): number | undefined {
|
function getParametersLastUpdate(nodeName: string): number | undefined {
|
||||||
return nodeMetadata.value[nodeName]?.parametersLastUpdatedAt;
|
return nodeMetadata.value[nodeName]?.parametersLastUpdatedAt;
|
||||||
}
|
}
|
||||||
|
@ -1584,6 +1588,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
getWorkflowById,
|
getWorkflowById,
|
||||||
getNodeByName,
|
getNodeByName,
|
||||||
getNodeById,
|
getNodeById,
|
||||||
|
getNodesByIds,
|
||||||
getParametersLastUpdate,
|
getParametersLastUpdate,
|
||||||
isNodePristine,
|
isNodePristine,
|
||||||
isNodeExecuting,
|
isNodeExecuting,
|
||||||
|
|
|
@ -42,12 +42,13 @@ import {
|
||||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||||
START_NODE_TYPE,
|
START_NODE_TYPE,
|
||||||
STICKY_NODE_TYPE,
|
STICKY_NODE_TYPE,
|
||||||
|
VALID_WORKFLOW_IMPORT_URL_REGEX,
|
||||||
VIEWS,
|
VIEWS,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { TelemetryHelpers, NodeConnectionType } from 'n8n-workflow';
|
import { TelemetryHelpers, NodeConnectionType, jsonParse } from 'n8n-workflow';
|
||||||
import type { IDataObject, ExecutionSummary, IConnection, IWorkflowBase } from 'n8n-workflow';
|
import type { IDataObject, ExecutionSummary, IConnection, IWorkflowBase } from 'n8n-workflow';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
@ -83,6 +84,7 @@ import { tryToParseNumber } from '@/utils/typesUtils';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { createEventBus } from 'n8n-design-system';
|
import { createEventBus } from 'n8n-design-system';
|
||||||
import type { PinDataSource } from '@/composables/usePinnedData';
|
import type { PinDataSource } from '@/composables/usePinnedData';
|
||||||
|
import { useClipboard } from '@/composables/useClipboard';
|
||||||
|
|
||||||
const NodeCreation = defineAsyncComponent(
|
const NodeCreation = defineAsyncComponent(
|
||||||
async () => await import('@/components/Node/NodeCreation.vue'),
|
async () => await import('@/components/Node/NodeCreation.vue'),
|
||||||
|
@ -141,6 +143,9 @@ const {
|
||||||
setNodeParameters,
|
setNodeParameters,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
deleteNodes,
|
deleteNodes,
|
||||||
|
copyNodes,
|
||||||
|
cutNodes,
|
||||||
|
duplicateNodes,
|
||||||
revertDeleteNode,
|
revertDeleteNode,
|
||||||
addNodes,
|
addNodes,
|
||||||
createConnection,
|
createConnection,
|
||||||
|
@ -156,6 +161,7 @@ const {
|
||||||
editableWorkflowObject,
|
editableWorkflowObject,
|
||||||
} = useCanvasOperations({ router, lastClickPosition });
|
} = useCanvasOperations({ router, lastClickPosition });
|
||||||
const { applyExecutionData } = useExecutionDebugging();
|
const { applyExecutionData } = useExecutionDebugging();
|
||||||
|
useClipboard({ onPaste: onClipboardPaste });
|
||||||
|
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
const isBlankRedirect = ref(false);
|
const isBlankRedirect = ref(false);
|
||||||
|
@ -475,16 +481,76 @@ function onSetNodeSelected(id?: string) {
|
||||||
setNodeSelected(id);
|
setNodeSelected(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCopyNodes(_ids: string[]) {
|
async function onCopyNodes(ids: string[]) {
|
||||||
// @TODO: implement this
|
await copyNodes(ids);
|
||||||
|
|
||||||
|
toast.showMessage({ title: i18n.baseText('generic.copiedToClipboard'), type: 'success' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCutNodes(_ids: string[]) {
|
async function onClipboardPaste(plainTextData: string): Promise<void> {
|
||||||
// @TODO: implement this
|
if (getNodeViewTab(route) !== MAIN_HEADER_TABS.WORKFLOW) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkIfEditingIsAllowed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let workflowData: IWorkflowDataUpdate | null | undefined = null;
|
||||||
|
|
||||||
|
// Check if it is an URL which could contain workflow data
|
||||||
|
if (plainTextData.match(VALID_WORKFLOW_IMPORT_URL_REGEX)) {
|
||||||
|
const importConfirm = await message.confirm(
|
||||||
|
i18n.baseText('nodeView.confirmMessage.onClipboardPasteEvent.message', {
|
||||||
|
interpolate: { plainTextData },
|
||||||
|
}),
|
||||||
|
i18n.baseText('nodeView.confirmMessage.onClipboardPasteEvent.headline'),
|
||||||
|
{
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: i18n.baseText(
|
||||||
|
'nodeView.confirmMessage.onClipboardPasteEvent.confirmButtonText',
|
||||||
|
),
|
||||||
|
cancelButtonText: i18n.baseText(
|
||||||
|
'nodeView.confirmMessage.onClipboardPasteEvent.cancelButtonText',
|
||||||
|
),
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (importConfirm !== MODAL_CONFIRM) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
workflowData = await fetchWorkflowDataFromUrl(plainTextData);
|
||||||
|
} else {
|
||||||
|
// Pasted data is is possible workflow data
|
||||||
|
workflowData = jsonParse<IWorkflowDataUpdate | null>(plainTextData, { fallbackValue: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!workflowData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await importWorkflowData(workflowData, 'paste', false);
|
||||||
|
selectNodes(result.nodes?.map((node) => node.id) ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDuplicateNodes(_ids: string[]) {
|
async function onCutNodes(ids: string[]) {
|
||||||
// @TODO: implement this
|
if (isCanvasReadOnly.value) {
|
||||||
|
await copyNodes(ids);
|
||||||
|
} else {
|
||||||
|
await cutNodes(ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDuplicateNodes(ids: string[]) {
|
||||||
|
if (!checkIfEditingIsAllowed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newIds = await duplicateNodes(ids);
|
||||||
|
|
||||||
|
selectNodes(newIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPinNodes(ids: string[], source: PinDataSource) {
|
function onPinNodes(ids: string[], source: PinDataSource) {
|
||||||
|
@ -666,9 +732,11 @@ async function importWorkflowExact({ workflow: workflowData }: { workflow: IWork
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onImportWorkflowDataEvent(data: IDataObject) {
|
async function onImportWorkflowDataEvent(data: IDataObject) {
|
||||||
await importWorkflowData(data.data as IWorkflowDataUpdate, 'file');
|
const workflowData = data.data as IWorkflowDataUpdate;
|
||||||
|
await importWorkflowData(workflowData, 'file');
|
||||||
|
|
||||||
fitView();
|
fitView();
|
||||||
|
selectNodes(workflowData.nodes?.map((node) => node.id) ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onImportWorkflowUrlEvent(data: IDataObject) {
|
async function onImportWorkflowUrlEvent(data: IDataObject) {
|
||||||
|
@ -680,6 +748,7 @@ async function onImportWorkflowUrlEvent(data: IDataObject) {
|
||||||
await importWorkflowData(workflowData, 'url');
|
await importWorkflowData(workflowData, 'url');
|
||||||
|
|
||||||
fitView();
|
fitView();
|
||||||
|
selectNodes(workflowData.nodes?.map((node) => node.id) ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addImportEventBindings() {
|
function addImportEventBindings() {
|
||||||
|
@ -972,12 +1041,16 @@ function removePostMessageEventBindings() {
|
||||||
window.removeEventListener('message', onPostMessageReceived);
|
window.removeEventListener('message', onPostMessageReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onPostMessageReceived(message: MessageEvent) {
|
async function onPostMessageReceived(messageEvent: MessageEvent) {
|
||||||
if (!message || typeof message.data !== 'string' || !message.data?.includes?.('"command"')) {
|
if (
|
||||||
|
!messageEvent ||
|
||||||
|
typeof messageEvent.data !== 'string' ||
|
||||||
|
!messageEvent.data?.includes?.('"command"')
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(message.data);
|
const json = JSON.parse(messageEvent.data);
|
||||||
if (json && json.command === 'openWorkflow') {
|
if (json && json.command === 'openWorkflow') {
|
||||||
try {
|
try {
|
||||||
await importWorkflowExact(json);
|
await importWorkflowExact(json);
|
||||||
|
@ -1110,6 +1183,10 @@ function fitView() {
|
||||||
setTimeout(() => canvasEventBus.emit('fitView'));
|
setTimeout(() => canvasEventBus.emit('fitView'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectNodes(ids: string[]) {
|
||||||
|
setTimeout(() => canvasEventBus.emit('selectNodes', ids));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mouse events
|
* Mouse events
|
||||||
*/
|
*/
|
||||||
|
@ -1250,9 +1327,6 @@ onMounted(async () => {
|
||||||
|
|
||||||
registerCustomActions();
|
registerCustomActions();
|
||||||
|
|
||||||
// @TODO Implement this
|
|
||||||
// this.clipboard.onPaste.value = this.onClipboardPasteEvent;
|
|
||||||
|
|
||||||
// @TODO: This currently breaks since front-end hooks are still not updated to work with pinia store
|
// @TODO: This currently breaks since front-end hooks are still not updated to work with pinia store
|
||||||
void externalHooks.run('nodeView.mount').catch(() => {});
|
void externalHooks.run('nodeView.mount').catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
|
@ -241,6 +241,7 @@ import {
|
||||||
DRAG_EVENT_DATA_KEY,
|
DRAG_EVENT_DATA_KEY,
|
||||||
UPDATE_WEBHOOK_ID_NODE_TYPES,
|
UPDATE_WEBHOOK_ID_NODE_TYPES,
|
||||||
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT,
|
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT,
|
||||||
|
VALID_WORKFLOW_IMPORT_URL_REGEX,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
|
||||||
import useGlobalLinkActions from '@/composables/useGlobalLinkActions';
|
import useGlobalLinkActions from '@/composables/useGlobalLinkActions';
|
||||||
|
@ -2024,7 +2025,7 @@ export default defineComponent({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check if it is an URL which could contain workflow data
|
// Check if it is an URL which could contain workflow data
|
||||||
if (plainTextData.match(/^http[s]?:\/\/.*\.json$/i)) {
|
if (plainTextData.match(VALID_WORKFLOW_IMPORT_URL_REGEX)) {
|
||||||
// Pasted data points to a possible workflow JSON file
|
// Pasted data points to a possible workflow JSON file
|
||||||
|
|
||||||
if (!this.editAllowedCheck()) {
|
if (!this.editAllowedCheck()) {
|
||||||
|
|
Loading…
Reference in a new issue