feat: Add sticky notes support to the new canvas (no-changelog) (#10031)
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user