mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-21 11:49:59 +00:00
feat(editor): Add support for changing sticky notes color in new canvas (no-changelog) (#10593)
This commit is contained in:
@@ -3,11 +3,14 @@ import { ref } from 'vue';
|
|||||||
import type {
|
import type {
|
||||||
CanvasNode,
|
CanvasNode,
|
||||||
CanvasNodeData,
|
CanvasNodeData,
|
||||||
|
CanvasNodeEventBusEvents,
|
||||||
CanvasNodeHandleInjectionData,
|
CanvasNodeHandleInjectionData,
|
||||||
CanvasNodeInjectionData,
|
CanvasNodeInjectionData,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
import type { EventBus } from 'n8n-design-system';
|
||||||
|
import { createEventBus } from 'n8n-design-system';
|
||||||
|
|
||||||
export function createCanvasNodeData({
|
export function createCanvasNodeData({
|
||||||
id = 'node',
|
id = 'node',
|
||||||
@@ -89,11 +92,13 @@ export function createCanvasNodeProvide({
|
|||||||
label = 'Test Node',
|
label = 'Test Node',
|
||||||
selected = false,
|
selected = false,
|
||||||
data = {},
|
data = {},
|
||||||
|
eventBus = createEventBus<CanvasNodeEventBusEvents>(),
|
||||||
}: {
|
}: {
|
||||||
id?: string;
|
id?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
data?: Partial<CanvasNodeData>;
|
data?: Partial<CanvasNodeData>;
|
||||||
|
eventBus?: EventBus<CanvasNodeEventBusEvents>;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const props = createCanvasNodeProps({ id, label, selected, data });
|
const props = createCanvasNodeProps({ id, label, selected, data });
|
||||||
return {
|
return {
|
||||||
@@ -102,6 +107,7 @@ export function createCanvasNodeProvide({
|
|||||||
label: ref(props.label),
|
label: ref(props.label),
|
||||||
selected: ref(props.selected),
|
selected: ref(props.selected),
|
||||||
data: ref(props.data),
|
data: ref(props.data),
|
||||||
|
eventBus: ref(eventBus),
|
||||||
} satisfies CanvasNodeInjectionData,
|
} satisfies CanvasNodeInjectionData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { CanvasConnection, CanvasNode, CanvasNodeMoveEvent, ConnectStartEvent } from '@/types';
|
import type {
|
||||||
|
CanvasConnection,
|
||||||
|
CanvasNode,
|
||||||
|
CanvasNodeMoveEvent,
|
||||||
|
CanvasEventBusEvents,
|
||||||
|
ConnectStartEvent,
|
||||||
|
} from '@/types';
|
||||||
import type {
|
import type {
|
||||||
EdgeMouseEvent,
|
EdgeMouseEvent,
|
||||||
Connection,
|
Connection,
|
||||||
@@ -65,7 +71,7 @@ const props = withDefaults(
|
|||||||
nodes: CanvasNode[];
|
nodes: CanvasNode[];
|
||||||
connections: CanvasConnection[];
|
connections: CanvasConnection[];
|
||||||
controlsPosition?: PanelPosition;
|
controlsPosition?: PanelPosition;
|
||||||
eventBus?: EventBus;
|
eventBus?: EventBus<CanvasEventBusEvents>;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
@@ -102,8 +108,8 @@ useKeybindings({
|
|||||||
ctrl_d: emitWithSelectedNodes((ids) => emit('duplicate:nodes', ids)),
|
ctrl_d: emitWithSelectedNodes((ids) => emit('duplicate:nodes', ids)),
|
||||||
d: emitWithSelectedNodes((ids) => emit('update:nodes:enabled', ids)),
|
d: emitWithSelectedNodes((ids) => emit('update:nodes:enabled', ids)),
|
||||||
p: emitWithSelectedNodes((ids) => emit('update:nodes:pin', ids, 'keyboard-shortcut')),
|
p: emitWithSelectedNodes((ids) => emit('update:nodes:pin', ids, 'keyboard-shortcut')),
|
||||||
enter: () => emitWithLastSelectedNode((id) => emit('update:node:active', id)),
|
enter: emitWithLastSelectedNode((id) => onSetNodeActive(id)),
|
||||||
f2: () => emitWithLastSelectedNode((id) => emit('update:node:name', id)),
|
f2: emitWithLastSelectedNode((id) => emit('update:node:name', id)),
|
||||||
tab: () => emit('create:node', 'tab'),
|
tab: () => emit('create:node', 'tab'),
|
||||||
shift_s: () => emit('create:sticky'),
|
shift_s: () => emit('create:sticky'),
|
||||||
ctrl_alt_n: () => emit('create:workflow'),
|
ctrl_alt_n: () => emit('create:workflow'),
|
||||||
@@ -154,6 +160,7 @@ function onNodesChange(events: NodeChange[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSetNodeActive(id: string) {
|
function onSetNodeActive(id: string) {
|
||||||
|
props.eventBus.emit('nodes:action', { ids: [id], action: 'update:node:active' });
|
||||||
emit('update:node:active', id);
|
emit('update:node:active', id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +173,7 @@ function onSelectNode() {
|
|||||||
emit('update:node:selected', lastSelectedNode.value.id);
|
emit('update:node:selected', lastSelectedNode.value.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSelectNodes(ids: string[]) {
|
function onSelectNodes({ ids }: CanvasEventBusEvents['nodes:select']) {
|
||||||
clearSelectedNodes();
|
clearSelectedNodes();
|
||||||
addSelectedNodes(ids.map(findNode).filter(isPresent));
|
addSelectedNodes(ids.map(findNode).filter(isPresent));
|
||||||
}
|
}
|
||||||
@@ -358,9 +365,11 @@ function onContextMenuAction(action: ContextMenuAction, nodeIds: string[]) {
|
|||||||
case 'toggle_activation':
|
case 'toggle_activation':
|
||||||
return emit('update:nodes:enabled', nodeIds);
|
return emit('update:nodes:enabled', nodeIds);
|
||||||
case 'open':
|
case 'open':
|
||||||
return emit('update:node:active', nodeIds[0]);
|
return onSetNodeActive(nodeIds[0]);
|
||||||
case 'rename':
|
case 'rename':
|
||||||
return emit('update:node:name', nodeIds[0]);
|
return emit('update:node:name', nodeIds[0]);
|
||||||
|
case 'change_color':
|
||||||
|
return props.eventBus.emit('nodes:action', { ids: nodeIds, action: 'update:sticky:color' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,12 +387,12 @@ function minimapNodeClassnameFn(node: CanvasNode) {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
props.eventBus.on('fitView', onFitView);
|
props.eventBus.on('fitView', onFitView);
|
||||||
props.eventBus.on('selectNodes', onSelectNodes);
|
props.eventBus.on('nodes:select', onSelectNodes);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
props.eventBus.off('fitView', onFitView);
|
props.eventBus.off('fitView', onFitView);
|
||||||
props.eventBus.off('selectNodes', onSelectNodes);
|
props.eventBus.off('nodes:select', onSelectNodes);
|
||||||
});
|
});
|
||||||
|
|
||||||
onPaneReady(async () => {
|
onPaneReady(async () => {
|
||||||
@@ -431,6 +440,7 @@ provide(CanvasKey, {
|
|||||||
<Node
|
<Node
|
||||||
v-bind="canvasNodeProps"
|
v-bind="canvasNodeProps"
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
|
:event-bus="eventBus"
|
||||||
@delete="onDeleteNode"
|
@delete="onDeleteNode"
|
||||||
@run="onRunNode"
|
@run="onRunNode"
|
||||||
@select="onSelectNode"
|
@select="onSelectNode"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { IWorkflowDb } from '@/Interface';
|
|||||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||||
import type { EventBus } from 'n8n-design-system';
|
import type { EventBus } from 'n8n-design-system';
|
||||||
import { createEventBus } from 'n8n-design-system';
|
import { createEventBus } from 'n8n-design-system';
|
||||||
|
import type { CanvasEventBusEvents } from '@/types';
|
||||||
import { STICKY_NODE_TYPE } from '@/constants';
|
import { STICKY_NODE_TYPE } from '@/constants';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -18,12 +19,12 @@ const props = withDefaults(
|
|||||||
workflow: IWorkflowDb;
|
workflow: IWorkflowDb;
|
||||||
workflowObject: Workflow;
|
workflowObject: Workflow;
|
||||||
fallbackNodes?: IWorkflowDb['nodes'];
|
fallbackNodes?: IWorkflowDb['nodes'];
|
||||||
eventBus?: EventBus;
|
eventBus?: EventBus<CanvasEventBusEvents>;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
id: 'canvas',
|
id: 'canvas',
|
||||||
eventBus: () => createEventBus(),
|
eventBus: () => createEventBus<CanvasEventBusEvents>(),
|
||||||
fallbackNodes: () => [],
|
fallbackNodes: () => [],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, provide, toRef, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, provide, ref, toRef, watch } from 'vue';
|
||||||
import type {
|
import type {
|
||||||
CanvasConnectionPort,
|
CanvasConnectionPort,
|
||||||
CanvasElementPortWithRenderData,
|
CanvasElementPortWithRenderData,
|
||||||
CanvasNodeData,
|
CanvasNodeData,
|
||||||
|
CanvasNodeEventBusEvents,
|
||||||
|
CanvasEventBusEvents,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import { CanvasConnectionMode } from '@/types';
|
import { CanvasConnectionMode } from '@/types';
|
||||||
import NodeIcon from '@/components/NodeIcon.vue';
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
@@ -18,9 +20,12 @@ import type { NodeProps, XYPosition } from '@vue-flow/core';
|
|||||||
import { Position } from '@vue-flow/core';
|
import { Position } from '@vue-flow/core';
|
||||||
import { useCanvas } from '@/composables/useCanvas';
|
import { useCanvas } from '@/composables/useCanvas';
|
||||||
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
||||||
|
import type { EventBus } from 'n8n-design-system';
|
||||||
|
import { createEventBus } from 'n8n-design-system';
|
||||||
|
|
||||||
type Props = NodeProps<CanvasNodeData> & {
|
type Props = NodeProps<CanvasNodeData> & {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
eventBus?: EventBus<CanvasEventBusEvents>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -58,6 +63,18 @@ const nodeTypeDescription = computed(() => {
|
|||||||
return nodeTypesStore.getNodeType(props.data.type, props.data.typeVersion);
|
return nodeTypesStore.getNodeType(props.data.type, props.data.typeVersion);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event bus
|
||||||
|
*/
|
||||||
|
|
||||||
|
const canvasNodeEventBus = ref(createEventBus<CanvasNodeEventBusEvents>());
|
||||||
|
|
||||||
|
function emitCanvasNodeEvent(event: CanvasEventBusEvents['nodes:action']) {
|
||||||
|
if (event.ids.includes(props.id)) {
|
||||||
|
canvasNodeEventBus.value.emit(event.action, event.payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inputs
|
* Inputs
|
||||||
*/
|
*/
|
||||||
@@ -208,6 +225,7 @@ provide(CanvasNodeKey, {
|
|||||||
data,
|
data,
|
||||||
label,
|
label,
|
||||||
selected,
|
selected,
|
||||||
|
eventBus: canvasNodeEventBus,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showToolbar = computed(() => {
|
const showToolbar = computed(() => {
|
||||||
@@ -225,6 +243,14 @@ watch(
|
|||||||
emit('select', props.id, value);
|
emit('select', props.id, value);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
props.eventBus?.on('nodes:action', emitCanvasNodeEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
props.eventBus?.off('nodes:action', emitCanvasNodeEvent);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -272,6 +298,7 @@ watch(
|
|||||||
@delete="onDelete"
|
@delete="onDelete"
|
||||||
@toggle="onDisabledToggle"
|
@toggle="onDisabledToggle"
|
||||||
@run="onRun"
|
@run="onRun"
|
||||||
|
@update="onUpdate"
|
||||||
@open:contextmenu="onOpenContextMenuFromToolbar"
|
@open:contextmenu="onOpenContextMenuFromToolbar"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -296,6 +323,7 @@ watch(
|
|||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.canvasNode {
|
.canvasNode {
|
||||||
&:hover,
|
&:hover,
|
||||||
|
&:focus-within,
|
||||||
&.showToolbar {
|
&.showToolbar {
|
||||||
.canvasNodeToolbar {
|
.canvasNodeToolbar {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -311,5 +339,10 @@ watch(
|
|||||||
transform: translate(-50%, -100%);
|
transform: translate(-50%, -100%);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
|
&:focus-within,
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const emit = defineEmits<{
|
|||||||
delete: [];
|
delete: [];
|
||||||
toggle: [];
|
toggle: [];
|
||||||
run: [];
|
run: [];
|
||||||
|
update: [parameters: Record<string, unknown>];
|
||||||
'open:contextmenu': [event: MouseEvent];
|
'open:contextmenu': [event: MouseEvent];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -46,6 +47,8 @@ const isDisableNodeVisible = computed(() => {
|
|||||||
|
|
||||||
const isDeleteNodeVisible = computed(() => !props.readOnly);
|
const isDeleteNodeVisible = computed(() => !props.readOnly);
|
||||||
|
|
||||||
|
const isStickyNoteNodeType = computed(() => render.value.type === CanvasNodeRenderType.StickyNote);
|
||||||
|
|
||||||
function executeNode() {
|
function executeNode() {
|
||||||
emit('run');
|
emit('run');
|
||||||
}
|
}
|
||||||
@@ -58,6 +61,12 @@ function onDeleteNode() {
|
|||||||
emit('delete');
|
emit('delete');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onChangeStickyColor(color: number) {
|
||||||
|
emit('update', {
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function onOpenContextMenu(event: MouseEvent) {
|
function onOpenContextMenu(event: MouseEvent) {
|
||||||
emit('open:contextmenu', event);
|
emit('open:contextmenu', event);
|
||||||
}
|
}
|
||||||
@@ -97,6 +106,7 @@ function onOpenContextMenu(event: MouseEvent) {
|
|||||||
:title="i18n.baseText('node.delete')"
|
:title="i18n.baseText('node.delete')"
|
||||||
@click="onDeleteNode"
|
@click="onDeleteNode"
|
||||||
/>
|
/>
|
||||||
|
<CanvasNodeStickyColorSelector v-if="isStickyNoteNodeType" @update="onChangeStickyColor" />
|
||||||
<N8nIconButton
|
<N8nIconButton
|
||||||
data-test-id="overflow-node-button"
|
data-test-id="overflow-node-button"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
/* eslint-disable vue/no-multiple-template-root */
|
/* eslint-disable vue/no-multiple-template-root */
|
||||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
import type { CanvasNodeStickyNoteRender } from '@/types';
|
import type { CanvasNodeStickyNoteRender } from '@/types';
|
||||||
import { ref, computed, useCssModule } from 'vue';
|
import { ref, computed, useCssModule, onMounted, onBeforeUnmount } from 'vue';
|
||||||
import { NodeResizer } from '@vue-flow/node-resizer';
|
import { NodeResizer } from '@vue-flow/node-resizer';
|
||||||
import type { OnResize } from '@vue-flow/node-resizer/dist/types';
|
import type { OnResize } from '@vue-flow/node-resizer/dist/types';
|
||||||
import type { XYPosition } from '@vue-flow/core';
|
import type { XYPosition } from '@vue-flow/core';
|
||||||
@@ -15,11 +15,12 @@ const emit = defineEmits<{
|
|||||||
update: [parameters: Record<string, unknown>];
|
update: [parameters: Record<string, unknown>];
|
||||||
move: [position: XYPosition];
|
move: [position: XYPosition];
|
||||||
dblclick: [event: MouseEvent];
|
dblclick: [event: MouseEvent];
|
||||||
|
'open:contextmenu': [event: MouseEvent];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
const { id, isSelected, render } = useCanvasNode();
|
const { id, isSelected, render, eventBus } = useCanvasNode();
|
||||||
|
|
||||||
const renderOptions = computed(() => render.value.options as CanvasNodeStickyNoteRender['options']);
|
const renderOptions = computed(() => render.value.options as CanvasNodeStickyNoteRender['options']);
|
||||||
|
|
||||||
@@ -63,6 +64,30 @@ function onEdit(edit: boolean) {
|
|||||||
function onDoubleClick(event: MouseEvent) {
|
function onDoubleClick(event: MouseEvent) {
|
||||||
emit('dblclick', event);
|
emit('dblclick', event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onActivate() {
|
||||||
|
onEdit(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context menu
|
||||||
|
*/
|
||||||
|
|
||||||
|
function openContextMenu(event: MouseEvent) {
|
||||||
|
emit('open:contextmenu', event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle
|
||||||
|
*/
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
eventBus.value?.on('update:node:active', onActivate);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
eventBus.value?.off('update:node:active', onActivate);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<NodeResizer
|
<NodeResizer
|
||||||
@@ -80,11 +105,12 @@ function onDoubleClick(event: MouseEvent) {
|
|||||||
:height="renderOptions.height"
|
:height="renderOptions.height"
|
||||||
:width="renderOptions.width"
|
:width="renderOptions.width"
|
||||||
:model-value="renderOptions.content"
|
:model-value="renderOptions.content"
|
||||||
:background="renderOptions.color"
|
:background-color="renderOptions.color"
|
||||||
:edit-mode="isActive"
|
:edit-mode="isActive"
|
||||||
@edit="onEdit"
|
@edit="onEdit"
|
||||||
@dblclick="onDoubleClick"
|
@dblclick="onDoubleClick"
|
||||||
@update:model-value="onInputChange"
|
@update:model-value="onInputChange"
|
||||||
|
@contextmenu="openContextMenu"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { fireEvent } from '@testing-library/vue';
|
||||||
|
import CanvasNodeStickyColorSelector from '@/components/canvas/elements/nodes/toolbar/CanvasNodeStickyColorSelector.vue';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(CanvasNodeStickyColorSelector);
|
||||||
|
|
||||||
|
describe('CanvasNodeStickyColorSelector', () => {
|
||||||
|
it('should render trigger correctly', () => {
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
global: {
|
||||||
|
provide: {
|
||||||
|
...createCanvasNodeProvide(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const colorSelector = getByTestId('change-sticky-color');
|
||||||
|
expect(colorSelector).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render all colors and apply selected color correctly', async () => {
|
||||||
|
const { getByTestId, getAllByTestId, emitted } = renderComponent({
|
||||||
|
global: {
|
||||||
|
provide: {
|
||||||
|
...createCanvasNodeProvide(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const colorSelector = getByTestId('change-sticky-color');
|
||||||
|
|
||||||
|
await fireEvent.click(colorSelector);
|
||||||
|
|
||||||
|
const colorOption = getAllByTestId('color');
|
||||||
|
const selectedIndex = 2;
|
||||||
|
|
||||||
|
await fireEvent.click(colorOption[selectedIndex]);
|
||||||
|
|
||||||
|
expect(colorOption).toHaveLength(7);
|
||||||
|
expect(emitted()).toHaveProperty('update');
|
||||||
|
expect(emitted().update[0]).toEqual([selectedIndex + 1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
|
import type { CanvasNodeStickyNoteRender } from '@/types';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
update: [color: number];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const { render, eventBus } = useCanvasNode();
|
||||||
|
const renderOptions = computed(() => render.value.options as CanvasNodeStickyNoteRender['options']);
|
||||||
|
|
||||||
|
const autoHideTimeout = ref<NodeJS.Timeout | null>(null);
|
||||||
|
const isPopoverVisible = ref(false);
|
||||||
|
|
||||||
|
const colors = computed(() => Array.from({ length: 7 }).map((_, index) => index + 1));
|
||||||
|
|
||||||
|
function togglePopover() {
|
||||||
|
isPopoverVisible.value = !isPopoverVisible.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePopover() {
|
||||||
|
isPopoverVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPopover() {
|
||||||
|
isPopoverVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeColor(index: number) {
|
||||||
|
emit('update', index);
|
||||||
|
hidePopover();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseEnter() {
|
||||||
|
if (autoHideTimeout.value) {
|
||||||
|
clearTimeout(autoHideTimeout.value);
|
||||||
|
autoHideTimeout.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseLeave() {
|
||||||
|
autoHideTimeout.value = setTimeout(() => {
|
||||||
|
hidePopover();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
eventBus.value?.on('update:sticky:color', showPopover);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
eventBus.value?.off('update:sticky:color', showPopover);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<N8nPopover
|
||||||
|
effect="dark"
|
||||||
|
trigger="click"
|
||||||
|
placement="top"
|
||||||
|
:popper-class="$style.popover"
|
||||||
|
:popper-style="{ width: '208px' }"
|
||||||
|
:visible="isPopoverVisible"
|
||||||
|
:teleported="false"
|
||||||
|
@mouseenter="onMouseEnter"
|
||||||
|
@mouseleave="onMouseLeave"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<div
|
||||||
|
:class="$style.option"
|
||||||
|
data-test-id="change-sticky-color"
|
||||||
|
:title="i18n.baseText('node.changeColor')"
|
||||||
|
@click.stop="togglePopover"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon="palette" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div :class="$style.content">
|
||||||
|
<div
|
||||||
|
v-for="color in colors"
|
||||||
|
:key="color"
|
||||||
|
data-test-id="color"
|
||||||
|
:class="[
|
||||||
|
$style.color,
|
||||||
|
$style[`sticky-color-${color}`],
|
||||||
|
renderOptions.color === color ? $style.selected : '',
|
||||||
|
]"
|
||||||
|
@click="changeColor(color)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</N8nPopover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.popover {
|
||||||
|
min-width: 208px;
|
||||||
|
margin-bottom: -8px;
|
||||||
|
margin-left: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: fit-content;
|
||||||
|
gap: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--color-foreground-xdark);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-sticky-background);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
box-shadow: 0 0 0 1px var(--color-sticky-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sticky-color-1 {
|
||||||
|
--color-sticky-background: var(--color-sticky-background-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sticky-color-2 {
|
||||||
|
--color-sticky-background: var(--color-sticky-background-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sticky-color-3 {
|
||||||
|
--color-sticky-background: var(--color-sticky-background-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sticky-color-4 {
|
||||||
|
--color-sticky-background: var(--color-sticky-background-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sticky-color-5 {
|
||||||
|
--color-sticky-background: var(--color-sticky-background-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sticky-color-6 {
|
||||||
|
--color-sticky-background: var(--color-sticky-background-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sticky-color-7 {
|
||||||
|
--color-sticky-background: var(--color-sticky-background-7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
display: inline-block;
|
||||||
|
padding: var(--spacing-3xs);
|
||||||
|
color: var(--color-text-light);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: var(--font-size-s) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -63,6 +63,8 @@ export function useCanvasNode() {
|
|||||||
|
|
||||||
const render = computed(() => data.value.render);
|
const render = computed(() => data.value.render);
|
||||||
|
|
||||||
|
const eventBus = computed(() => node?.eventBus.value);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
id,
|
id,
|
||||||
@@ -84,5 +86,6 @@ export function useCanvasNode() {
|
|||||||
executionWaiting,
|
executionWaiting,
|
||||||
executionRunning,
|
executionRunning,
|
||||||
render,
|
render,
|
||||||
|
eventBus,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import type {
|
|||||||
NodeConnectionType,
|
NodeConnectionType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core';
|
import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { IExecutionResponse, INodeUi } from '@/Interface';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import type { PartialBy } from '@/utils/typeHelpers';
|
import type { PartialBy } from '@/utils/typeHelpers';
|
||||||
|
import type { EventBus } from 'n8n-design-system';
|
||||||
|
|
||||||
export type CanvasConnectionPortType = NodeConnectionType;
|
export type CanvasConnectionPortType = NodeConnectionType;
|
||||||
|
|
||||||
@@ -124,11 +125,29 @@ export interface CanvasInjectionData {
|
|||||||
connectingHandle: Ref<ConnectStartEvent | undefined>;
|
connectingHandle: Ref<ConnectStartEvent | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CanvasNodeEventBusEvents = {
|
||||||
|
'update:sticky:color': never;
|
||||||
|
'update:node:active': never;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CanvasEventBusEvents = {
|
||||||
|
fitView: never;
|
||||||
|
'saved:workflow': never;
|
||||||
|
'open:execution': IExecutionResponse;
|
||||||
|
'nodes:select': { ids: string[] };
|
||||||
|
'nodes:action': {
|
||||||
|
ids: string[];
|
||||||
|
action: keyof CanvasNodeEventBusEvents;
|
||||||
|
payload?: CanvasNodeEventBusEvents[keyof CanvasNodeEventBusEvents];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export interface CanvasNodeInjectionData {
|
export interface CanvasNodeInjectionData {
|
||||||
id: Ref<string>;
|
id: Ref<string>;
|
||||||
data: Ref<CanvasNodeData>;
|
data: Ref<CanvasNodeData>;
|
||||||
label: Ref<NodeProps['label']>;
|
label: Ref<NodeProps['label']>;
|
||||||
selected: Ref<NodeProps['selected']>;
|
selected: Ref<NodeProps['selected']>;
|
||||||
|
eventBus: Ref<EventBus<CanvasNodeEventBusEvents>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CanvasNodeHandleInjectionData {
|
export interface CanvasNodeHandleInjectionData {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import type {
|
|||||||
import type { Connection, ViewportTransform } from '@vue-flow/core';
|
import type { Connection, ViewportTransform } from '@vue-flow/core';
|
||||||
import type {
|
import type {
|
||||||
CanvasConnectionCreateData,
|
CanvasConnectionCreateData,
|
||||||
|
CanvasEventBusEvents,
|
||||||
CanvasNode,
|
CanvasNode,
|
||||||
CanvasNodeMoveEvent,
|
CanvasNodeMoveEvent,
|
||||||
ConnectStartEvent,
|
ConnectStartEvent,
|
||||||
@@ -137,7 +138,7 @@ const pushConnectionStore = usePushConnectionStore();
|
|||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
const templatesStore = useTemplatesStore();
|
const templatesStore = useTemplatesStore();
|
||||||
|
|
||||||
const canvasEventBus = createEventBus();
|
const canvasEventBus = createEventBus<CanvasEventBusEvents>();
|
||||||
|
|
||||||
const { addBeforeUnloadEventBindings, removeBeforeUnloadEventBindings } = useBeforeUnload({
|
const { addBeforeUnloadEventBindings, removeBeforeUnloadEventBindings } = useBeforeUnload({
|
||||||
route,
|
route,
|
||||||
@@ -1346,7 +1347,7 @@ function fitView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectNodes(ids: string[]) {
|
function selectNodes(ids: string[]) {
|
||||||
setTimeout(() => canvasEventBus.emit('selectNodes', ids));
|
setTimeout(() => canvasEventBus.emit('nodes:select', { ids }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user