feat: Add sticky notes support to the new canvas (no-changelog) (#10031)

This commit is contained in:
Alex Grozav
2024-07-15 13:00:52 +03:00
committed by GitHub
parent 9302e33d55
commit cd24c71a9e
32 changed files with 653 additions and 147 deletions

View File

@@ -12,14 +12,14 @@ import {
mockNodes,
mockNodeTypeDescription,
} from '@/__tests__/mocks';
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE } from '@/constants';
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import {
createCanvasConnectionHandleString,
createCanvasConnectionId,
} from '@/utils/canvasUtilsV2';
import { CanvasConnectionMode } from '@/types';
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
beforeEach(() => {
const pinia = createPinia();
@@ -86,6 +86,7 @@ describe('useCanvasMapping', () => {
position: expect.anything(),
data: {
id: manualTriggerNode.id,
name: manualTriggerNode.name,
type: manualTriggerNode.type,
typeVersion: expect.anything(),
disabled: false,
@@ -224,6 +225,93 @@ describe('useCanvasMapping', () => {
}),
);
});
describe('render', () => {
it('should handle render options for default node type', () => {
const manualTriggerNode = mockNode({
name: 'Manual Trigger',
type: MANUAL_TRIGGER_NODE_TYPE,
disabled: false,
});
const nodes = [manualTriggerNode];
const connections = {};
const workflowObject = createTestWorkflowObject({
nodes,
connections,
});
const { nodes: mappedNodes } = useCanvasMapping({
nodes: ref(nodes),
connections: ref(connections),
workflowObject: ref(workflowObject) as Ref<Workflow>,
});
expect(mappedNodes.value[0]?.data?.render).toEqual({
type: CanvasNodeRenderType.Default,
options: {
configurable: false,
configuration: false,
trigger: true,
},
});
});
it('should handle render options for addNodes node type', () => {
const addNodesNode = mockNode({
name: CanvasNodeRenderType.AddNodes,
type: CanvasNodeRenderType.AddNodes,
disabled: false,
});
const nodes = [addNodesNode];
const connections = {};
const workflowObject = createTestWorkflowObject({
nodes: [],
connections,
});
const { nodes: mappedNodes } = useCanvasMapping({
nodes: ref(nodes),
connections: ref(connections),
workflowObject: ref(workflowObject) as Ref<Workflow>,
});
expect(mappedNodes.value[0]?.data?.render).toEqual({
type: CanvasNodeRenderType.AddNodes,
options: {},
});
});
it('should handle render options for stickyNote node type', () => {
const stickyNoteNode = mockNode({
name: 'Sticky',
type: STICKY_NODE_TYPE,
disabled: false,
parameters: {
width: 200,
height: 200,
color: 3,
content: '# Hello world',
},
});
const nodes = [stickyNoteNode];
const connections = {};
const workflowObject = createTestWorkflowObject({
nodes,
connections,
});
const { nodes: mappedNodes } = useCanvasMapping({
nodes: ref(nodes),
connections: ref(connections),
workflowObject: ref(workflowObject) as Ref<Workflow>,
});
expect(mappedNodes.value[0]?.data?.render).toEqual({
type: CanvasNodeRenderType.StickyNote,
options: stickyNoteNode.parameters,
});
});
});
});
describe('connections', () => {

View File

@@ -13,7 +13,10 @@ import type {
CanvasConnectionData,
CanvasConnectionPort,
CanvasNode,
CanvasNodeAddNodesRender,
CanvasNodeData,
CanvasNodeDefaultRender,
CanvasNodeStickyNoteRender,
} from '@/types';
import { CanvasNodeRenderType } from '@/types';
import {
@@ -30,7 +33,7 @@ import type {
} from 'n8n-workflow';
import { NodeHelpers } from 'n8n-workflow';
import type { INodeUi } from '@/Interface';
import { WAIT_TIME_UNLIMITED } from '@/constants';
import { STICKY_NODE_TYPE, WAIT_TIME_UNLIMITED } from '@/constants';
import { sanitizeHtml } from '@/utils/htmlUtils';
export function useCanvasMapping({
@@ -46,30 +49,48 @@ export function useCanvasMapping({
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const renderTypeByNodeType = computed(
function createStickyNoteRenderType(node: INodeUi): CanvasNodeStickyNoteRender {
return {
type: CanvasNodeRenderType.StickyNote,
options: {
width: node.parameters.width as number,
height: node.parameters.height as number,
color: node.parameters.color as number,
content: node.parameters.content as string,
},
};
}
function createAddNodesRenderType(): CanvasNodeAddNodesRender {
return {
type: CanvasNodeRenderType.AddNodes,
options: {},
};
}
function createDefaultNodeRenderType(node: INodeUi): CanvasNodeDefaultRender {
return {
type: CanvasNodeRenderType.Default,
options: {
trigger: nodeTypesStore.isTriggerNode(node.type),
configuration: nodeTypesStore.isConfigNode(workflowObject.value, node, node.type),
configurable: nodeTypesStore.isConfigurableNode(workflowObject.value, node, node.type),
},
};
}
const renderTypeByNodeId = computed(
() =>
nodes.value.reduce<Record<string, CanvasNodeData['render']>>((acc, node) => {
// @TODO Add support for sticky notes here
switch (node.type) {
case `${CanvasNodeRenderType.StickyNote}`:
acc[node.id] = createStickyNoteRenderType(node);
break;
case `${CanvasNodeRenderType.AddNodes}`:
acc[node.type] = {
type: CanvasNodeRenderType.AddNodes,
options: {},
};
acc[node.id] = createAddNodesRenderType();
break;
default:
acc[node.type] = {
type: CanvasNodeRenderType.Default,
options: {
trigger: nodeTypesStore.isTriggerNode(node.type),
configuration: nodeTypesStore.isConfigNode(workflowObject.value, node, node.type),
configurable: nodeTypesStore.isConfigurableNode(
workflowObject.value,
node,
node.type,
),
},
};
acc[node.id] = createDefaultNodeRenderType(node);
}
return acc;
@@ -214,6 +235,20 @@ export function useCanvasMapping({
}, {}),
);
const additionalNodePropertiesById = computed(() => {
return nodes.value.reduce<Record<string, Partial<CanvasNode>>>((acc, node) => {
if (node.type === STICKY_NODE_TYPE) {
acc[node.id] = {
style: {
zIndex: -1,
},
};
}
return acc;
}, {});
});
const mappedNodes = computed<CanvasNode[]>(() => [
...nodes.value.map<CanvasNode>((node) => {
const inputConnections = workflowObject.value.connectionsByDestinationNode[node.name] ?? {};
@@ -221,6 +256,7 @@ export function useCanvasMapping({
const data: CanvasNodeData = {
id: node.id,
name: node.name,
type: node.type,
typeVersion: node.typeVersion,
disabled: !!node.disabled,
@@ -247,7 +283,7 @@ export function useCanvasMapping({
count: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
visible: !!nodeExecutionRunDataById.value[node.id],
},
render: renderTypeByNodeType.value[node.type] ?? { type: 'default', options: {} },
render: renderTypeByNodeId.value[node.id] ?? { type: 'default', options: {} },
};
return {
@@ -256,6 +292,7 @@ export function useCanvasMapping({
type: 'canvas-node',
position: { x: node.position[0], y: node.position[1] },
data,
...additionalNodePropertiesById.value[node.id],
};
}),
]);

View File

@@ -30,13 +30,14 @@ describe('useCanvasNode', () => {
expect(result.executionStatus.value).toBeUndefined();
expect(result.executionWaiting.value).toBeUndefined();
expect(result.executionRunning.value).toBe(false);
expect(result.renderOptions.value).toEqual({});
expect(result.render.value).toEqual({ type: CanvasNodeRenderType.Default, options: {} });
});
it('should return node data when node is provided', () => {
const node = {
data: ref({
id: 'node1',
name: 'Node 1',
type: 'nodeType1',
typeVersion: 1,
disabled: true,
@@ -66,6 +67,7 @@ describe('useCanvasNode', () => {
const result = useCanvasNode();
expect(result.label.value).toBe('Node 1');
expect(result.name.value).toBe('Node 1');
expect(result.inputs.value).toEqual([{ type: 'main', index: 0 }]);
expect(result.outputs.value).toEqual([{ type: 'main', index: 0 }]);
expect(result.connections.value).toEqual({ input: { '0': [] }, output: {} });
@@ -80,6 +82,6 @@ describe('useCanvasNode', () => {
expect(result.executionStatus.value).toBe('running');
expect(result.executionWaiting.value).toBe('waiting');
expect(result.executionRunning.value).toBe(true);
expect(result.renderOptions.value).toBe(node.data.value.render.options);
expect(result.render.value).toBe(node.data.value.render);
});
});

View File

@@ -14,6 +14,7 @@ export function useCanvasNode() {
() =>
node?.data.value ?? {
id: '',
name: '',
type: '',
typeVersion: 1,
disabled: false,
@@ -33,8 +34,10 @@ export function useCanvasNode() {
},
);
const id = computed(() => node?.id.value ?? '');
const label = computed(() => node?.label.value ?? '');
const name = computed(() => data.value.name);
const inputs = computed(() => data.value.inputs);
const outputs = computed(() => data.value.outputs);
const connections = computed(() => data.value.connections);
@@ -56,10 +59,12 @@ export function useCanvasNode() {
const runDataCount = computed(() => data.value.runData.count);
const hasRunData = computed(() => data.value.runData.visible);
const renderOptions = computed(() => data.value.render.options);
const render = computed(() => data.value.render);
return {
node,
id,
name,
label,
inputs,
outputs,
@@ -75,6 +80,6 @@ export function useCanvasNode() {
executionStatus,
executionWaiting,
executionRunning,
renderOptions,
render,
};
}

View File

@@ -40,6 +40,7 @@ import type {
INodeTypeDescription,
INodeTypeNameVersion,
ITelemetryTrackProperties,
NodeParameterValueType,
} from 'n8n-workflow';
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
import { useNDVStore } from '@/stores/ndv.store';
@@ -229,6 +230,21 @@ export function useCanvasOperations({
ndvStore.activeNodeName = name;
}
function setNodeParameters(id: string, parameters: Record<string, unknown>) {
const node = workflowsStore.getNodeById(id);
if (!node) {
return;
}
workflowsStore.setNodeParameters(
{
name: node.name,
value: parameters as NodeParameterValueType,
},
true,
);
}
function setNodeSelected(id?: string) {
if (!id) {
uiStore.lastSelectedNode = '';
@@ -443,7 +459,7 @@ export function useCanvasOperations({
const nodeType = nodeTypesStore.getNodeType(newNodeData.type, newNodeData.typeVersion);
const nodeParameters = NodeHelpers.getNodeParameters(
nodeType?.properties ?? [],
{},
node.parameters ?? {},
true,
false,
newNodeData,
@@ -883,10 +899,16 @@ export function useCanvasOperations({
targetNode: INodeUi,
connectionType: NodeConnectionType,
): boolean {
const blocklist = [STICKY_NODE_TYPE];
if (sourceNode.id === targetNode.id) {
return false;
}
if (blocklist.includes(sourceNode.type) || blocklist.includes(targetNode.type)) {
return false;
}
const targetNodeType = nodeTypesStore.getNodeType(targetNode.type, targetNode.typeVersion);
if (targetNodeType?.inputs?.length) {
const workflowNode = editableWorkflowObject.value.getNode(targetNode.name);
@@ -958,6 +980,7 @@ export function useCanvasOperations({
setNodeActive,
setNodeActiveByName,
setNodeSelected,
setNodeParameters,
toggleNodeDisabled,
renameNode,
revertRenameNode,