diff --git a/packages/design-system/src/components/N8nResizeableSticky/ResizeableSticky.stories.ts b/packages/design-system/src/components/N8nResizeableSticky/ResizeableSticky.stories.ts
new file mode 100644
index 0000000000..fe344ed483
--- /dev/null
+++ b/packages/design-system/src/components/N8nResizeableSticky/ResizeableSticky.stories.ts
@@ -0,0 +1,69 @@
+import { action } from '@storybook/addon-actions';
+import type { StoryFn } from '@storybook/vue3';
+import N8nResizeableSticky from './ResizeableSticky.vue';
+
+export default {
+ title: 'Atoms/ResizeableSticky',
+ component: N8nResizeableSticky,
+ argTypes: {
+ content: {
+ control: {
+ control: 'text',
+ },
+ },
+ height: {
+ control: {
+ control: 'number',
+ },
+ },
+ minHeight: {
+ control: {
+ control: 'number',
+ },
+ },
+ minWidth: {
+ control: {
+ control: 'number',
+ },
+ },
+ readOnly: {
+ control: {
+ control: 'Boolean',
+ },
+ },
+ width: {
+ control: {
+ control: 'number',
+ },
+ },
+ },
+};
+
+const methods = {
+ onInput: action('update:modelValue'),
+ onResize: action('resize'),
+ onResizeEnd: action('resizeend'),
+ onResizeStart: action('resizestart'),
+};
+
+const Template: StoryFn = (args, { argTypes }) => ({
+ setup: () => ({ args }),
+ props: Object.keys(argTypes),
+ components: {
+ N8nResizeableSticky,
+ },
+ template:
+ '',
+ methods,
+});
+
+export const ResizeableSticky = Template.bind({});
+ResizeableSticky.args = {
+ height: 160,
+ width: 150,
+ modelValue:
+ "## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/sticky-notes/)",
+ minHeight: 80,
+ minWidth: 150,
+ readOnly: false,
+};
diff --git a/packages/design-system/src/components/N8nResizeableSticky/ResizeableSticky.vue b/packages/design-system/src/components/N8nResizeableSticky/ResizeableSticky.vue
new file mode 100644
index 0000000000..c70dfe89e8
--- /dev/null
+++ b/packages/design-system/src/components/N8nResizeableSticky/ResizeableSticky.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
diff --git a/packages/design-system/src/components/N8nResizeableSticky/index.ts b/packages/design-system/src/components/N8nResizeableSticky/index.ts
new file mode 100644
index 0000000000..a0346c10f5
--- /dev/null
+++ b/packages/design-system/src/components/N8nResizeableSticky/index.ts
@@ -0,0 +1,3 @@
+import ResizeableSticky from './ResizeableSticky.vue';
+
+export default ResizeableSticky;
diff --git a/packages/design-system/src/components/N8nSticky/Sticky.stories.ts b/packages/design-system/src/components/N8nSticky/Sticky.stories.ts
index 6a4e05d830..1411f2c05e 100644
--- a/packages/design-system/src/components/N8nSticky/Sticky.stories.ts
+++ b/packages/design-system/src/components/N8nSticky/Sticky.stories.ts
@@ -61,9 +61,7 @@ export const Sticky = Template.bind({});
Sticky.args = {
height: 160,
width: 150,
- content:
- "## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/sticky-notes/)",
- defaultText:
+ modelValue:
"## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/sticky-notes/)",
minHeight: 80,
minWidth: 150,
diff --git a/packages/design-system/src/components/N8nSticky/Sticky.vue b/packages/design-system/src/components/N8nSticky/Sticky.vue
index b258171fda..c53fb012f9 100644
--- a/packages/design-system/src/components/N8nSticky/Sticky.vue
+++ b/packages/design-system/src/components/N8nSticky/Sticky.vue
@@ -9,52 +9,40 @@
:style="styles"
@keydown.prevent
>
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -62,45 +50,17 @@
import { computed, ref, watch } from 'vue';
import N8nInput from '../N8nInput';
import N8nMarkdown from '../N8nMarkdown';
-import N8nResizeWrapper, { type ResizeData } from '../N8nResizeWrapper/ResizeWrapper.vue';
import N8nText from '../N8nText';
import { useI18n } from '../../composables/useI18n';
+import { defaultStickyProps } from './constants';
+import type { StickyProps } from './types';
-interface StickyProps {
- modelValue?: string;
- height?: number;
- width?: number;
- minHeight?: number;
- minWidth?: number;
- scale?: number;
- gridSize?: number;
- id?: string;
- defaultText?: string;
- editMode?: boolean;
- readOnly?: boolean;
- backgroundColor?: number | string;
-}
-
-const props = withDefaults(defineProps(), {
- height: 180,
- width: 240,
- minHeight: 80,
- minWidth: 150,
- scale: 1,
- gridSize: 20,
- id: '0',
- editMode: false,
- readOnly: false,
- backgroundColor: 1,
-});
+const props = withDefaults(defineProps(), defaultStickyProps);
const emit = defineEmits<{
edit: [editing: boolean];
'update:modelValue': [value: string];
'markdown-click': [link: string, e: Event];
- resize: [values: ResizeData];
- resizestart: [];
- resizeend: [];
}>();
const { t } = useI18n();
@@ -115,6 +75,8 @@ const resWidth = computed((): number => {
return props.width < props.minWidth ? props.minWidth : props.width;
});
+const inputName = computed(() => (props.id ? `${props.id}-input` : undefined));
+
const styles = computed((): { height: string; width: string } => ({
height: `${resHeight.value}px`,
width: `${resWidth.value}px`,
@@ -152,20 +114,6 @@ const onMarkdownClick = (link: string, event: Event) => {
emit('markdown-click', link, event);
};
-const onResize = (values: ResizeData) => {
- emit('resize', values);
-};
-
-const onResizeStart = () => {
- isResizing.value = true;
- emit('resizestart');
-};
-
-const onResizeEnd = () => {
- isResizing.value = false;
- emit('resizeend');
-};
-
const onInputScroll = (event: WheelEvent) => {
// Pass through zoom events but hold regular scrolling
if (!event.ctrlKey && !event.metaKey) {
diff --git a/packages/design-system/src/components/N8nSticky/constants.ts b/packages/design-system/src/components/N8nSticky/constants.ts
new file mode 100644
index 0000000000..274d51a63f
--- /dev/null
+++ b/packages/design-system/src/components/N8nSticky/constants.ts
@@ -0,0 +1,10 @@
+export const defaultStickyProps = {
+ height: 180,
+ width: 240,
+ minHeight: 80,
+ minWidth: 150,
+ id: '0',
+ editMode: false,
+ readOnly: false,
+ backgroundColor: 1,
+};
diff --git a/packages/design-system/src/components/N8nSticky/types.ts b/packages/design-system/src/components/N8nSticky/types.ts
new file mode 100644
index 0000000000..276a785a95
--- /dev/null
+++ b/packages/design-system/src/components/N8nSticky/types.ts
@@ -0,0 +1,12 @@
+export interface StickyProps {
+ modelValue?: string;
+ height?: number;
+ width?: number;
+ minHeight?: number;
+ minWidth?: number;
+ id?: string;
+ defaultText?: string;
+ editMode?: boolean;
+ readOnly?: boolean;
+ backgroundColor?: number | string;
+}
diff --git a/packages/design-system/src/components/index.ts b/packages/design-system/src/components/index.ts
index d917339b5e..d4f44016f7 100644
--- a/packages/design-system/src/components/index.ts
+++ b/packages/design-system/src/components/index.ts
@@ -40,6 +40,7 @@ export { default as N8nResizeWrapper } from './N8nResizeWrapper';
export { default as N8nSelect } from './N8nSelect';
export { default as N8nSpinner } from './N8nSpinner';
export { default as N8nSticky } from './N8nSticky';
+export { default as N8nResizeableSticky } from './N8nResizeableSticky';
export { default as N8nTabs } from './N8nTabs';
export { default as N8nTag } from './N8nTag';
export { default as N8nTags } from './N8nTags';
diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json
index da463b11d3..e06a493039 100644
--- a/packages/editor-ui/package.json
+++ b/packages/editor-ui/package.json
@@ -40,6 +40,7 @@
"@vue-flow/controls": "^1.1.1",
"@vue-flow/core": "^1.33.5",
"@vue-flow/minimap": "^1.4.0",
+ "@vue-flow/node-resizer": "^1.4.0",
"@vueuse/components": "^10.11.0",
"@vueuse/core": "^10.11.0",
"axios": "1.6.7",
diff --git a/packages/editor-ui/src/__tests__/data/canvas.ts b/packages/editor-ui/src/__tests__/data/canvas.ts
index b7ac64fedc..40f0060fe2 100644
--- a/packages/editor-ui/src/__tests__/data/canvas.ts
+++ b/packages/editor-ui/src/__tests__/data/canvas.ts
@@ -5,6 +5,7 @@ import { CanvasNodeRenderType } from '@/types';
export function createCanvasNodeData({
id = 'node',
+ name = 'Test Node',
type = 'test',
typeVersion = 1,
disabled = false,
@@ -21,13 +22,14 @@ export function createCanvasNodeData({
},
}: Partial = {}): CanvasNodeData {
return {
+ id,
+ name,
+ type,
+ typeVersion,
execution,
issues,
pinnedData,
runData,
- id,
- type,
- typeVersion,
disabled,
inputs,
outputs,
diff --git a/packages/editor-ui/src/__tests__/mocks.ts b/packages/editor-ui/src/__tests__/mocks.ts
index 5b7bac23af..dad814f6eb 100644
--- a/packages/editor-ui/src/__tests__/mocks.ts
+++ b/packages/editor-ui/src/__tests__/mocks.ts
@@ -23,6 +23,7 @@ import {
MANUAL_TRIGGER_NODE_TYPE,
NO_OP_NODE_TYPE,
SET_NODE_TYPE,
+ STICKY_NODE_TYPE,
} from '@/constants';
import type { INodeUi } from '@/Interface';
@@ -34,6 +35,7 @@ export const mockNode = ({
disabled = false,
issues = undefined,
typeVersion = 1,
+ parameters = {},
}: {
id?: INodeUi['id'];
name: INodeUi['name'];
@@ -42,7 +44,8 @@ export const mockNode = ({
disabled?: INodeUi['disabled'];
issues?: INodeIssues;
typeVersion?: INodeUi['typeVersion'];
-}) => mock({ id, name, type, position, disabled, issues, typeVersion });
+ parameters?: INodeUi['parameters'];
+}) => mock({ id, name, type, position, disabled, issues, typeVersion, parameters });
export const mockNodeTypeDescription = ({
name,
@@ -90,6 +93,7 @@ export const mockNodes = [
mockNode({ name: 'Rename', type: SET_NODE_TYPE }),
mockNode({ name: 'Chat Trigger', type: CHAT_TRIGGER_NODE_TYPE }),
mockNode({ name: 'Agent', type: AGENT_NODE_TYPE }),
+ mockNode({ name: 'Sticky', type: STICKY_NODE_TYPE }),
mockNode({ name: 'End', type: NO_OP_NODE_TYPE }),
];
diff --git a/packages/editor-ui/src/components/Sticky.vue b/packages/editor-ui/src/components/Sticky.vue
index 38b3cc1620..31627984d7 100644
--- a/packages/editor-ui/src/components/Sticky.vue
+++ b/packages/editor-ui/src/components/Sticky.vue
@@ -24,7 +24,7 @@
@click.left="mouseLeftClick"
@contextmenu="onContextMenu"
>
- ];
'run:node': [id: string];
'delete:node': [id: string];
'delete:connection': [connection: Connection];
@@ -57,10 +58,14 @@ const { getSelectedEdges, getSelectedNodes, viewportRef, fitView, project } = us
function onNodeDragStop(e: NodeDragEvent) {
e.nodes.forEach((node) => {
- emit('update:node:position', node.id, node.position);
+ onUpdateNodePosition(node.id, node.position);
});
}
+function onUpdateNodePosition(id: string, position: XYPosition) {
+ emit('update:node:position', id, position);
+}
+
function onSelectionDragStop(e: NodeDragEvent) {
onNodeDragStop(e);
}
@@ -82,6 +87,10 @@ function onDeleteNode(id: string) {
emit('delete:node', id);
}
+function onUpdateNodeParameters(id: string, parameters: Record) {
+ emit('update:node:parameters', id, parameters);
+}
+
/**
* Connections
*/
@@ -222,6 +231,8 @@ onUnmounted(() => {
@select="onSelectNode"
@toggle="onToggleNodeEnabled"
@activate="onSetNodeActive"
+ @update="onUpdateNodeParameters"
+ @move="onUpdateNodePosition"
/>
diff --git a/packages/editor-ui/src/components/canvas/elements/buttons/CanvasRunWorkflowButton.vue b/packages/editor-ui/src/components/canvas/elements/buttons/CanvasRunWorkflowButton.vue
index f68f69ecac..8f05b5a2b4 100644
--- a/packages/editor-ui/src/components/canvas/elements/buttons/CanvasRunWorkflowButton.vue
+++ b/packages/editor-ui/src/components/canvas/elements/buttons/CanvasRunWorkflowButton.vue
@@ -10,8 +10,8 @@ defineEmits<{
}>();
const props = defineProps<{
- waitingForWebhook: boolean;
- executing: boolean;
+ waitingForWebhook?: boolean;
+ executing?: boolean;
disabled?: boolean;
}>();
diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue
index 831f27d5bb..5f419e511b 100644
--- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue
+++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue
@@ -1,5 +1,4 @@
@@ -157,7 +169,7 @@ function onActivate() {
@run="onRun"
/>
-
+
import { h, inject } from 'vue';
import CanvasNodeDefault from '@/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue';
+import CanvasNodeStickyNote from '@/components/canvas/elements/nodes/render-types/CanvasNodeStickyNote.vue';
import CanvasNodeAddNodes from '@/components/canvas/elements/nodes/render-types/CanvasNodeAddNodes.vue';
import { CanvasNodeKey } from '@/constants';
import { CanvasNodeRenderType } from '@/types';
@@ -14,7 +15,9 @@ const slots = defineSlots<{
const Render = () => {
let Component;
switch (node?.data.value.render.type) {
- // @TODO Add support for sticky notes here
+ case CanvasNodeRenderType.StickyNote:
+ Component = CanvasNodeStickyNote;
+ break;
case CanvasNodeRenderType.AddNodes:
Component = CanvasNodeAddNodes;
break;
diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNodeToolbar.vue b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNodeToolbar.vue
index 2dbd218e7a..69b9862ab0 100644
--- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNodeToolbar.vue
+++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNodeToolbar.vue
@@ -1,7 +1,8 @@
+
+
+
+
+
+
diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeStickyNote.spec.ts.snap b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeStickyNote.spec.ts.snap
new file mode 100644
index 0000000000..d5b40b411f
--- /dev/null
+++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeStickyNote.spec.ts.snap
@@ -0,0 +1,45 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`CanvasNodeStickyNote > should render node correctly 1`] = `
+
+`;
diff --git a/packages/editor-ui/src/composables/useCanvasMapping.spec.ts b/packages/editor-ui/src/composables/useCanvasMapping.spec.ts
index a2d87e5be0..79efa76f53 100644
--- a/packages/editor-ui/src/composables/useCanvasMapping.spec.ts
+++ b/packages/editor-ui/src/composables/useCanvasMapping.spec.ts
@@ -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,
+ });
+
+ 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,
+ });
+
+ 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,
+ });
+
+ expect(mappedNodes.value[0]?.data?.render).toEqual({
+ type: CanvasNodeRenderType.StickyNote,
+ options: stickyNoteNode.parameters,
+ });
+ });
+ });
});
describe('connections', () => {
diff --git a/packages/editor-ui/src/composables/useCanvasMapping.ts b/packages/editor-ui/src/composables/useCanvasMapping.ts
index c35365c96e..b20e57ae92 100644
--- a/packages/editor-ui/src/composables/useCanvasMapping.ts
+++ b/packages/editor-ui/src/composables/useCanvasMapping.ts
@@ -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>((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>>((acc, node) => {
+ if (node.type === STICKY_NODE_TYPE) {
+ acc[node.id] = {
+ style: {
+ zIndex: -1,
+ },
+ };
+ }
+
+ return acc;
+ }, {});
+ });
+
const mappedNodes = computed(() => [
...nodes.value.map((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],
};
}),
]);
diff --git a/packages/editor-ui/src/composables/useCanvasNode.spec.ts b/packages/editor-ui/src/composables/useCanvasNode.spec.ts
index 1988f67699..9386c2906e 100644
--- a/packages/editor-ui/src/composables/useCanvasNode.spec.ts
+++ b/packages/editor-ui/src/composables/useCanvasNode.spec.ts
@@ -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);
});
});
diff --git a/packages/editor-ui/src/composables/useCanvasNode.ts b/packages/editor-ui/src/composables/useCanvasNode.ts
index 9fe4cb81d8..20d47d83f6 100644
--- a/packages/editor-ui/src/composables/useCanvasNode.ts
+++ b/packages/editor-ui/src/composables/useCanvasNode.ts
@@ -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,
};
}
diff --git a/packages/editor-ui/src/composables/useCanvasOperations.ts b/packages/editor-ui/src/composables/useCanvasOperations.ts
index ef5ccf4cd4..60107e7c8c 100644
--- a/packages/editor-ui/src/composables/useCanvasOperations.ts
+++ b/packages/editor-ui/src/composables/useCanvasOperations.ts
@@ -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) {
+ 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,
diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts
index 9a51382f7e..66b7d6e279 100644
--- a/packages/editor-ui/src/main.ts
+++ b/packages/editor-ui/src/main.ts
@@ -4,6 +4,7 @@ import '@vue-flow/core/dist/style.css';
import '@vue-flow/core/dist/theme-default.css';
import '@vue-flow/controls/dist/style.css';
import '@vue-flow/minimap/dist/style.css';
+import '@vue-flow/node-resizer/dist/style.css';
import 'vue-json-pretty/lib/styles.css';
import '@jsplumb/browser-ui/css/jsplumbtoolkit.css';
diff --git a/packages/editor-ui/src/styles/plugins/_vueflow.scss b/packages/editor-ui/src/styles/plugins/_vueflow.scss
new file mode 100644
index 0000000000..b10e9f4f9d
--- /dev/null
+++ b/packages/editor-ui/src/styles/plugins/_vueflow.scss
@@ -0,0 +1,7 @@
+.vue-flow__resize-control.line {
+ border-color: transparent;
+}
+
+.vue-flow__resize-control.handle {
+ background-color: transparent;
+}
diff --git a/packages/editor-ui/src/styles/plugins/index.scss b/packages/editor-ui/src/styles/plugins/index.scss
index fda7b1823c..c5d099d5d1 100644
--- a/packages/editor-ui/src/styles/plugins/index.scss
+++ b/packages/editor-ui/src/styles/plugins/index.scss
@@ -1 +1,2 @@
@import "codemirror";
+@import "vueflow";
diff --git a/packages/editor-ui/src/types/canvas.ts b/packages/editor-ui/src/types/canvas.ts
index cf62b7c97f..d7c6e8cb62 100644
--- a/packages/editor-ui/src/types/canvas.ts
+++ b/packages/editor-ui/src/types/canvas.ts
@@ -36,6 +36,7 @@ export interface CanvasElementPortWithPosition extends CanvasConnectionPort {
export const enum CanvasNodeRenderType {
Default = 'default',
+ StickyNote = 'n8n-nodes-base.stickyNote',
AddNodes = 'n8n-nodes-internal.addNodes',
}
@@ -53,8 +54,19 @@ export type CanvasNodeAddNodesRender = {
options: Record;
};
+export type CanvasNodeStickyNoteRender = {
+ type: CanvasNodeRenderType.StickyNote;
+ options: Partial<{
+ width: number;
+ height: number;
+ color: number;
+ content: string;
+ }>;
+};
+
export interface CanvasNodeData {
id: INodeUi['id'];
+ name: INodeUi['name'];
type: INodeUi['type'];
typeVersion: INodeUi['typeVersion'];
disabled: INodeUi['disabled'];
@@ -81,7 +93,7 @@ export interface CanvasNodeData {
count: number;
visible: boolean;
};
- render: CanvasNodeDefaultRender | CanvasNodeAddNodesRender;
+ render: CanvasNodeDefaultRender | CanvasNodeStickyNoteRender | CanvasNodeAddNodesRender;
}
export type CanvasNode = Node;
diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue
index 3cf0f29836..a5b9e3f867 100644
--- a/packages/editor-ui/src/views/NodeView.v2.vue
+++ b/packages/editor-ui/src/views/NodeView.v2.vue
@@ -144,6 +144,7 @@ const {
setNodeActive,
setNodeSelected,
toggleNodeDisabled,
+ setNodeParameters,
deleteNode,
revertDeleteNode,
addNodes,
@@ -310,6 +311,7 @@ async function initializeView() {
nodeHelpers.updateNodesParameterIssues();
await loadCredentials();
+ canvasEventBus.emit('fitView');
uiStore.nodeViewInitialized = true;
@@ -553,7 +555,6 @@ function onSetNodeSelected(id?: string) {
}
function onRenameNode(parameterData: IUpdateInformation) {
- // The name changed. Do not forget to change the connections as well
if (parameterData.name === 'name' && parameterData.oldValue) {
void renameNode(parameterData.oldValue as string, parameterData.value as string);
}
@@ -569,6 +570,10 @@ async function onRevertRenameNode({
await revertRenameNode(currentName, newName);
}
+function onUpdateNodeParameters(id: string, parameters: Record) {
+ setNodeParameters(id, parameters);
+}
+
/**
* Credentials
*/
@@ -1157,6 +1162,7 @@ onBeforeUnmount(() => {
@update:node:active="onSetNodeActive"
@update:node:selected="onSetNodeSelected"
@update:node:enabled="onToggleNodeDisabled"
+ @update:node:parameters="onUpdateNodeParameters"
@run:node="onRunWorkflowToNode"
@delete:node="onDeleteNode"
@create:connection="onCreateConnection"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 62c6a8ffe6..f590908f12 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1147,6 +1147,9 @@ importers:
'@vue-flow/minimap':
specifier: ^1.4.0
version: 1.4.0(@vue-flow/core@1.33.5(vue@3.4.21(typescript@5.5.2)))(vue@3.4.21(typescript@5.5.2))
+ '@vue-flow/node-resizer':
+ specifier: ^1.4.0
+ version: 1.4.0(@vue-flow/core@1.33.5(vue@3.4.21(typescript@5.5.2)))(vue@3.4.21(typescript@5.5.2))
'@vueuse/components':
specifier: ^10.11.0
version: 10.11.0(vue@3.4.21(typescript@5.5.2))
@@ -5696,6 +5699,12 @@ packages:
'@vue-flow/core': ^1.23.0
vue: ^3.3.0
+ '@vue-flow/node-resizer@1.4.0':
+ resolution: {integrity: sha512-S52MRcSpd6asza8Cl0bKM2sHGrbq7vBydKHDuPdoTD+cvjNX6XF4LSiPZOuzExePI6b+O6dg2EZ1378oOLGFpA==}
+ peerDependencies:
+ '@vue-flow/core': ^1.23.0
+ vue: ^3.3.0
+
'@vue/compiler-core@3.4.21':
resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==}
@@ -19069,6 +19078,13 @@ snapshots:
d3-zoom: 3.0.0
vue: 3.4.21(typescript@5.5.2)
+ '@vue-flow/node-resizer@1.4.0(@vue-flow/core@1.33.5(vue@3.4.21(typescript@5.5.2)))(vue@3.4.21(typescript@5.5.2))':
+ dependencies:
+ '@vue-flow/core': 1.33.5(vue@3.4.21(typescript@5.5.2))
+ d3-drag: 3.0.0
+ d3-selection: 3.0.0
+ vue: 3.4.21(typescript@5.5.2)
+
'@vue/compiler-core@3.4.21':
dependencies:
'@babel/parser': 7.24.0
@@ -21201,7 +21217,7 @@ snapshots:
eslint-import-resolver-node@0.3.9:
dependencies:
- debug: 3.2.7(supports-color@5.5.0)
+ debug: 3.2.7(supports-color@8.1.1)
is-core-module: 2.13.1
resolve: 1.22.8
transitivePeerDependencies:
@@ -21226,7 +21242,7 @@ snapshots:
eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
dependencies:
- debug: 3.2.7(supports-color@5.5.0)
+ debug: 3.2.7(supports-color@8.1.1)
optionalDependencies:
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.5.2)
eslint: 8.57.0
@@ -21246,7 +21262,7 @@ snapshots:
array.prototype.findlastindex: 1.2.3
array.prototype.flat: 1.3.2
array.prototype.flatmap: 1.3.2
- debug: 3.2.7(supports-color@5.5.0)
+ debug: 3.2.7(supports-color@8.1.1)
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
@@ -21776,7 +21792,7 @@ snapshots:
follow-redirects@1.15.6(debug@3.2.7):
optionalDependencies:
- debug: 3.2.7(supports-color@5.5.0)
+ debug: 3.2.7(supports-color@8.1.1)
follow-redirects@1.15.6(debug@4.3.4):
optionalDependencies:
@@ -22117,7 +22133,7 @@ snapshots:
array-parallel: 0.1.3
array-series: 0.1.5
cross-spawn: 4.0.2
- debug: 3.2.7(supports-color@5.5.0)
+ debug: 3.2.7(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@@ -24882,7 +24898,7 @@ snapshots:
pdf-parse@1.1.1:
dependencies:
- debug: 3.2.7(supports-color@5.5.0)
+ debug: 3.2.7(supports-color@8.1.1)
node-ensure: 0.0.0
transitivePeerDependencies:
- supports-color
@@ -25764,7 +25780,7 @@ snapshots:
rhea@1.0.24:
dependencies:
- debug: 3.2.7(supports-color@5.5.0)
+ debug: 3.2.7(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@@ -26138,7 +26154,7 @@ snapshots:
binascii: 0.0.2
bn.js: 5.2.1
browser-request: 0.3.3
- debug: 3.2.7(supports-color@5.5.0)
+ debug: 3.2.7(supports-color@8.1.1)
expand-tilde: 2.0.2
extend: 3.0.2
fast-xml-parser: 4.2.7