mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
feat: Add sticky notes support to the new canvas (no-changelog) (#10031)
This commit is contained in:
parent
9302e33d55
commit
cd24c71a9e
|
@ -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:
|
||||
'<n8n-resizeable-sticky v-bind="args" @resize="onResize" @resizeend="onResizeEnd" @resizeStart="onResizeStart" @input="onInput"></n8n-resizeable-sticky>',
|
||||
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,
|
||||
};
|
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<N8nResizeWrapper
|
||||
:is-resizing-enabled="!readOnly"
|
||||
:height="height"
|
||||
:width="width"
|
||||
:min-height="minHeight"
|
||||
:min-width="minWidth"
|
||||
:scale="scale"
|
||||
:grid-size="gridSize"
|
||||
@resizeend="onResizeEnd"
|
||||
@resize="onResize"
|
||||
@resizestart="onResizeStart"
|
||||
>
|
||||
<N8nSticky v-bind="stickyBindings" />
|
||||
</N8nResizeWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, useAttrs } from 'vue';
|
||||
import N8nResizeWrapper, { type ResizeData } from '../N8nResizeWrapper/ResizeWrapper.vue';
|
||||
import N8nSticky from '../N8nSticky/Sticky.vue';
|
||||
import type { StickyProps } from '../N8nSticky/types';
|
||||
import { defaultStickyProps } from '../N8nSticky/constants';
|
||||
|
||||
type ResizeableStickyProps = StickyProps & {
|
||||
scale?: number;
|
||||
gridSize?: number;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<ResizeableStickyProps>(), {
|
||||
...defaultStickyProps,
|
||||
scale: 1,
|
||||
gridSize: 20,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
resize: [values: ResizeData];
|
||||
resizestart: [];
|
||||
resizeend: [];
|
||||
}>();
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
const stickyBindings = computed(() => ({ ...props, ...attrs }));
|
||||
|
||||
const isResizing = ref(false);
|
||||
|
||||
const onResize = (values: ResizeData) => {
|
||||
emit('resize', values);
|
||||
};
|
||||
|
||||
const onResizeStart = () => {
|
||||
isResizing.value = true;
|
||||
emit('resizestart');
|
||||
};
|
||||
|
||||
const onResizeEnd = () => {
|
||||
isResizing.value = false;
|
||||
emit('resizeend');
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,3 @@
|
|||
import ResizeableSticky from './ResizeableSticky.vue';
|
||||
|
||||
export default ResizeableSticky;
|
|
@ -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,
|
||||
|
|
|
@ -9,52 +9,40 @@
|
|||
:style="styles"
|
||||
@keydown.prevent
|
||||
>
|
||||
<N8nResizeWrapper
|
||||
:is-resizing-enabled="!readOnly"
|
||||
:height="height"
|
||||
:width="width"
|
||||
:min-height="minHeight"
|
||||
:min-width="minWidth"
|
||||
:scale="scale"
|
||||
:grid-size="gridSize"
|
||||
@resizeend="onResizeEnd"
|
||||
@resize="onResize"
|
||||
@resizestart="onResizeStart"
|
||||
<div v-show="!editMode" :class="$style.wrapper" @dblclick.stop="onDoubleClick">
|
||||
<N8nMarkdown
|
||||
theme="sticky"
|
||||
:content="modelValue"
|
||||
:with-multi-breaks="true"
|
||||
@markdown-click="onMarkdownClick"
|
||||
@update-content="onUpdateModelValue"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="editMode"
|
||||
:class="{ 'full-height': !shouldShowFooter, 'sticky-textarea': true }"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
@mouseup.stop
|
||||
@keydown.esc="onInputBlur"
|
||||
@keydown.stop
|
||||
>
|
||||
<div v-show="!editMode" :class="$style.wrapper" @dblclick.stop="onDoubleClick">
|
||||
<N8nMarkdown
|
||||
theme="sticky"
|
||||
:content="modelValue"
|
||||
:with-multi-breaks="true"
|
||||
@markdown-click="onMarkdownClick"
|
||||
@update-content="onUpdateModelValue"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="editMode"
|
||||
:class="{ 'full-height': !shouldShowFooter, 'sticky-textarea': true }"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
@mouseup.stop
|
||||
@keydown.esc="onInputBlur"
|
||||
@keydown.stop
|
||||
>
|
||||
<N8nInput
|
||||
ref="input"
|
||||
:model-value="modelValue"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
@blur="onInputBlur"
|
||||
@update:model-value="onUpdateModelValue"
|
||||
@wheel="onInputScroll"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="editMode && shouldShowFooter" :class="$style.footer">
|
||||
<N8nText size="xsmall" align="right">
|
||||
<span v-html="t('sticky.markdownHint')"></span>
|
||||
</N8nText>
|
||||
</div>
|
||||
</N8nResizeWrapper>
|
||||
<N8nInput
|
||||
ref="input"
|
||||
:model-value="modelValue"
|
||||
:name="inputName"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
@blur="onInputBlur"
|
||||
@update:model-value="onUpdateModelValue"
|
||||
@wheel="onInputScroll"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="editMode && shouldShowFooter" :class="$style.footer">
|
||||
<N8nText size="xsmall" align="right">
|
||||
<span v-html="t('sticky.markdownHint')"></span>
|
||||
</N8nText>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -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<StickyProps>(), {
|
||||
height: 180,
|
||||
width: 240,
|
||||
minHeight: 80,
|
||||
minWidth: 150,
|
||||
scale: 1,
|
||||
gridSize: 20,
|
||||
id: '0',
|
||||
editMode: false,
|
||||
readOnly: false,
|
||||
backgroundColor: 1,
|
||||
});
|
||||
const props = withDefaults(defineProps<StickyProps>(), 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) {
|
||||
|
|
10
packages/design-system/src/components/N8nSticky/constants.ts
Normal file
10
packages/design-system/src/components/N8nSticky/constants.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export const defaultStickyProps = {
|
||||
height: 180,
|
||||
width: 240,
|
||||
minHeight: 80,
|
||||
minWidth: 150,
|
||||
id: '0',
|
||||
editMode: false,
|
||||
readOnly: false,
|
||||
backgroundColor: 1,
|
||||
};
|
12
packages/design-system/src/components/N8nSticky/types.ts
Normal file
12
packages/design-system/src/components/N8nSticky/types.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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> = {}): CanvasNodeData {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
typeVersion,
|
||||
execution,
|
||||
issues,
|
||||
pinnedData,
|
||||
runData,
|
||||
id,
|
||||
type,
|
||||
typeVersion,
|
||||
disabled,
|
||||
inputs,
|
||||
outputs,
|
||||
|
|
|
@ -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<INodeUi>({ id, name, type, position, disabled, issues, typeVersion });
|
||||
parameters?: INodeUi['parameters'];
|
||||
}) => mock<INodeUi>({ 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 }),
|
||||
];
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
@click.left="mouseLeftClick"
|
||||
@contextmenu="onContextMenu"
|
||||
>
|
||||
<n8n-sticky
|
||||
<N8nResizeableSticky
|
||||
v-if="node"
|
||||
:id="node.id"
|
||||
:model-value="node.parameters.content"
|
||||
|
|
|
@ -19,6 +19,7 @@ const emit = defineEmits<{
|
|||
'update:node:active': [id: string];
|
||||
'update:node:enabled': [id: string];
|
||||
'update:node:selected': [id?: string];
|
||||
'update:node:parameters': [id: string, parameters: Record<string, unknown>];
|
||||
'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<string, unknown>) {
|
||||
emit('update:node:parameters', id, parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connections
|
||||
*/
|
||||
|
@ -222,6 +231,8 @@ onUnmounted(() => {
|
|||
@select="onSelectNode"
|
||||
@toggle="onToggleNodeEnabled"
|
||||
@activate="onSetNodeActive"
|
||||
@update="onUpdateNodeParameters"
|
||||
@move="onUpdateNodePosition"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@ defineEmits<{
|
|||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
waitingForWebhook: boolean;
|
||||
executing: boolean;
|
||||
waitingForWebhook?: boolean;
|
||||
executing?: boolean;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts" setup>
|
||||
import { Position } from '@vue-flow/core';
|
||||
import { computed, provide, toRef, watch } from 'vue';
|
||||
import type { CanvasNodeData, CanvasConnectionPort, CanvasElementPortWithPosition } from '@/types';
|
||||
import NodeIcon from '@/components/NodeIcon.vue';
|
||||
|
@ -9,7 +8,8 @@ import CanvasNodeRenderer from '@/components/canvas/elements/nodes/CanvasNodeRen
|
|||
import HandleRenderer from '@/components/canvas/elements/handles/HandleRenderer.vue';
|
||||
import { useNodeConnections } from '@/composables/useNodeConnections';
|
||||
import { CanvasNodeKey } from '@/constants';
|
||||
import type { NodeProps } from '@vue-flow/core';
|
||||
import { Position } from '@vue-flow/core';
|
||||
import type { XYPosition, NodeProps } from '@vue-flow/core';
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [id: string];
|
||||
|
@ -17,6 +17,8 @@ const emit = defineEmits<{
|
|||
select: [id: string, selected: boolean];
|
||||
toggle: [id: string];
|
||||
activate: [id: string];
|
||||
update: [id: string, parameters: Record<string, unknown>];
|
||||
move: [id: string, position: XYPosition];
|
||||
}>();
|
||||
const props = defineProps<NodeProps<CanvasNodeData>>();
|
||||
|
||||
|
@ -103,7 +105,9 @@ provide(CanvasNodeKey, {
|
|||
nodeType,
|
||||
});
|
||||
|
||||
const nodeIconSize = computed(() => (data.value.render.options.configuration ? 30 : 40));
|
||||
const nodeIconSize = computed(() =>
|
||||
'configuration' in data.value.render.options && data.value.render.options.configuration ? 30 : 40,
|
||||
);
|
||||
|
||||
function onDelete() {
|
||||
emit('delete', props.id);
|
||||
|
@ -120,6 +124,14 @@ function onDisabledToggle() {
|
|||
function onActivate() {
|
||||
emit('activate', props.id);
|
||||
}
|
||||
|
||||
function onUpdate(parameters: Record<string, unknown>) {
|
||||
emit('update', props.id, parameters);
|
||||
}
|
||||
|
||||
function onMove(position: XYPosition) {
|
||||
emit('move', props.id, position);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -157,7 +169,7 @@ function onActivate() {
|
|||
@run="onRun"
|
||||
/>
|
||||
|
||||
<CanvasNodeRenderer @dblclick="onActivate">
|
||||
<CanvasNodeRenderer @dblclick="onActivate" @move="onMove" @update="onUpdate">
|
||||
<NodeIcon
|
||||
v-if="nodeType"
|
||||
:node-type="nodeType"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
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;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { useCssModule } from 'vue';
|
||||
import { computed, useCssModule } from 'vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
import { CanvasNodeRenderType } from '@/types';
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [];
|
||||
|
@ -12,7 +13,7 @@ const emit = defineEmits<{
|
|||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
|
||||
const { renderOptions } = useCanvasNode();
|
||||
const { render } = useCanvasNode();
|
||||
|
||||
// @TODO
|
||||
const workflowRunning = false;
|
||||
|
@ -20,6 +21,18 @@ const workflowRunning = false;
|
|||
// @TODO
|
||||
const nodeDisabledTitle = 'Test';
|
||||
|
||||
const isExecuteNodeVisible = computed(() => {
|
||||
return (
|
||||
render.value.type === CanvasNodeRenderType.Default &&
|
||||
'configuration' in render.value.options &&
|
||||
!render.value.options.configuration
|
||||
);
|
||||
});
|
||||
|
||||
const isDisableNodeVisible = computed(() => {
|
||||
return render.value.type === CanvasNodeRenderType.Default;
|
||||
});
|
||||
|
||||
function executeNode() {
|
||||
emit('run');
|
||||
}
|
||||
|
@ -40,7 +53,7 @@ function openContextMenu(_e: MouseEvent, _type: string) {}
|
|||
<div :class="$style.canvasNodeToolbar">
|
||||
<div :class="$style.canvasNodeToolbarItems">
|
||||
<N8nIconButton
|
||||
v-if="!renderOptions.configuration"
|
||||
v-if="isExecuteNodeVisible"
|
||||
data-test-id="execute-node-button"
|
||||
type="tertiary"
|
||||
text
|
||||
|
@ -51,6 +64,7 @@ function openContextMenu(_e: MouseEvent, _type: string) {}
|
|||
@click="executeNode"
|
||||
/>
|
||||
<N8nIconButton
|
||||
v-if="isDisableNodeVisible"
|
||||
data-test-id="disable-node-button"
|
||||
type="tertiary"
|
||||
text
|
||||
|
|
|
@ -7,6 +7,7 @@ import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-typ
|
|||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
import { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
|
||||
import { N8nTooltip } from 'n8n-design-system';
|
||||
import type { CanvasNodeDefaultRender } from '@/types';
|
||||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
|
@ -22,7 +23,7 @@ const {
|
|||
executionRunning,
|
||||
hasRunData,
|
||||
hasIssues,
|
||||
renderOptions,
|
||||
render,
|
||||
} = useCanvasNode();
|
||||
const { mainOutputs, nonMainInputs, requiredNonMainInputs } = useNodeConnections({
|
||||
inputs,
|
||||
|
@ -30,6 +31,8 @@ const { mainOutputs, nonMainInputs, requiredNonMainInputs } = useNodeConnections
|
|||
connections,
|
||||
});
|
||||
|
||||
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
|
||||
|
||||
const classes = computed(() => {
|
||||
return {
|
||||
[$style.node]: true,
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import CanvasNodeStickyNote from '@/components/canvas/elements/nodes/render-types/CanvasNodeStickyNote.vue';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasNodeStickyNote);
|
||||
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
});
|
||||
|
||||
describe('CanvasNodeStickyNote', () => {
|
||||
it('should render node correctly', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
id: 'sticky',
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('canvas-sticky-note-node')).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-multiple-template-root */
|
||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
import type { CanvasNodeStickyNoteRender } from '@/types';
|
||||
import { ref, computed } from 'vue';
|
||||
import { NodeResizer } from '@vue-flow/node-resizer';
|
||||
import type { OnResize } from '@vue-flow/node-resizer/dist/types';
|
||||
import type { XYPosition } from '@vue-flow/core';
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [parameters: Record<string, unknown>];
|
||||
move: [position: XYPosition];
|
||||
dblclick: [event: MouseEvent];
|
||||
}>();
|
||||
|
||||
const { id, render } = useCanvasNode();
|
||||
|
||||
const renderOptions = computed(() => render.value.options as CanvasNodeStickyNoteRender['options']);
|
||||
|
||||
/**
|
||||
* Resizing
|
||||
*/
|
||||
|
||||
function onResize(event: OnResize) {
|
||||
emit('move', {
|
||||
x: event.params.x,
|
||||
y: event.params.y,
|
||||
});
|
||||
|
||||
emit('update', {
|
||||
...(event.params.width ? { width: event.params.width } : {}),
|
||||
...(event.params.height ? { height: event.params.height } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Content change
|
||||
*/
|
||||
|
||||
const isActive = ref(false);
|
||||
|
||||
function onInputChange(value: string) {
|
||||
emit('update', {
|
||||
content: value,
|
||||
});
|
||||
}
|
||||
|
||||
function onEdit(edit: boolean) {
|
||||
isActive.value = edit;
|
||||
}
|
||||
|
||||
function onDoubleClick(event: MouseEvent) {
|
||||
emit('dblclick', event);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<NodeResizer
|
||||
:min-height="80"
|
||||
:min-width="150"
|
||||
:height="renderOptions.height"
|
||||
:width="renderOptions.width"
|
||||
@resize="onResize"
|
||||
/>
|
||||
<N8nSticky
|
||||
:id="id"
|
||||
data-test-id="canvas-sticky-note-node"
|
||||
:height="renderOptions.height"
|
||||
:width="renderOptions.width"
|
||||
:class="$style.sticky"
|
||||
:model-value="renderOptions.content"
|
||||
:background="renderOptions.color"
|
||||
:edit-mode="isActive"
|
||||
@edit="onEdit"
|
||||
@dblclick="onDoubleClick"
|
||||
@update:model-value="onInputChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.sticky {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,45 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`CanvasNodeStickyNote > should render node correctly 1`] = `
|
||||
<div
|
||||
class="n8n-sticky sticky clickable color-1 sticky"
|
||||
data-test-id="canvas-sticky-note-node"
|
||||
style="height: 180px; width: 240px;"
|
||||
>
|
||||
<div
|
||||
class="wrapper"
|
||||
>
|
||||
<div
|
||||
class="n8n-markdown"
|
||||
>
|
||||
<div
|
||||
class="sticky"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="sticky-textarea"
|
||||
style="display: none;"
|
||||
>
|
||||
<div
|
||||
class="el-textarea el-input--large n8n-input"
|
||||
>
|
||||
<!-- input -->
|
||||
|
||||
<!-- textarea -->
|
||||
<textarea
|
||||
autocomplete="off"
|
||||
class="el-textarea__inner"
|
||||
name="sticky-input"
|
||||
placeholder=""
|
||||
rows="5"
|
||||
tabindex="0"
|
||||
title=""
|
||||
/>
|
||||
<!--v-if-->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
</div>
|
||||
`;
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
7
packages/editor-ui/src/styles/plugins/_vueflow.scss
Normal file
7
packages/editor-ui/src/styles/plugins/_vueflow.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
.vue-flow__resize-control.line {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.vue-flow__resize-control.handle {
|
||||
background-color: transparent;
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
@import "codemirror";
|
||||
@import "vueflow";
|
||||
|
|
|
@ -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<string, never>;
|
||||
};
|
||||
|
||||
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<CanvasNodeData>;
|
||||
|
|
|
@ -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<string, unknown>) {
|
||||
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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue