mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
## Summary Provide details about your pull request and what it adds, fixes, or changes. Photos and videos are recommended. As part of NodeView refactor, this PR migrates all externalHooks calls to `useExternalHooks` composable. #### How to test the change: 1. Run using env `export N8N_DEPLOYMENT_TYPE=cloud` 2. Hooks should still run as expected ## Issues fixed Include links to Github issue or Community forum post or **Linear ticket**: > Important in order to close automatically and provide context to reviewers https://linear.app/n8n/issue/N8N-6349/externalhooks ## Review / Merge checklist - [x] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [x] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [x] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. A feature is not complete without tests. > > *(internal)* You can use Slack commands to trigger [e2e tests](https://www.notion.so/n8n/How-to-use-Test-Instances-d65f49dfc51f441ea44367fb6f67eb0a?pvs=4#a39f9e5ba64a48b58a71d81c837e8227) or [deploy test instance](https://www.notion.so/n8n/How-to-use-Test-Instances-d65f49dfc51f441ea44367fb6f67eb0a?pvs=4#f6a177d32bde4b57ae2da0b8e454bfce) or [deploy early access version on Cloud](https://www.notion.so/n8n/Cloudbot-3dbe779836004972b7057bc989526998?pvs=4#fef2d36ab02247e1a0f65a74f6fb534e).
428 lines
10 KiB
Vue
428 lines
10 KiB
Vue
<template>
|
|
<div
|
|
class="sticky-wrapper"
|
|
:id="nodeId"
|
|
:ref="data.name"
|
|
:style="stickyPosition"
|
|
:data-name="data.name"
|
|
data-test-id="sticky"
|
|
>
|
|
<div
|
|
:class="{
|
|
'sticky-default': true,
|
|
'touch-active': isTouchActive,
|
|
'is-touch-device': isTouchDevice,
|
|
}"
|
|
:style="stickySize"
|
|
>
|
|
<div class="select-sticky-background" v-show="isSelected" />
|
|
<div
|
|
class="sticky-box"
|
|
@click.left="mouseLeftClick"
|
|
@contextmenu="onContextMenu"
|
|
v-touch:start="touchStart"
|
|
v-touch:end="touchEnd"
|
|
>
|
|
<n8n-sticky
|
|
:modelValue="node.parameters.content"
|
|
:height="node.parameters.height"
|
|
:width="node.parameters.width"
|
|
:scale="nodeViewScale"
|
|
:backgroundColor="node.parameters.color"
|
|
:id="node.id"
|
|
:readOnly="isReadOnly"
|
|
:defaultText="defaultText"
|
|
:editMode="isActive && !isReadOnly"
|
|
:gridSize="gridSize"
|
|
@edit="onEdit"
|
|
@resizestart="onResizeStart"
|
|
@resize="onResize"
|
|
@resizeend="onResizeEnd"
|
|
@markdown-click="onMarkdownClick"
|
|
@update:modelValue="onInputChange"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-show="showActions"
|
|
:class="{ 'sticky-options': true, 'no-select-on-click': true, 'force-show': forceActions }"
|
|
>
|
|
<div
|
|
v-touch:tap="deleteNode"
|
|
class="option"
|
|
data-test-id="delete-sticky"
|
|
:title="$locale.baseText('node.deleteNode')"
|
|
>
|
|
<font-awesome-icon icon="trash" />
|
|
</div>
|
|
<n8n-popover
|
|
effect="dark"
|
|
:popper-style="{ width: '208px' }"
|
|
trigger="click"
|
|
placement="top"
|
|
@show="onShowPopover"
|
|
@hide="onHidePopover"
|
|
>
|
|
<template #reference>
|
|
<div
|
|
ref="colorPopoverTrigger"
|
|
class="option"
|
|
data-test-id="change-sticky-color"
|
|
:title="$locale.baseText('node.changeColor')"
|
|
>
|
|
<font-awesome-icon icon="palette" />
|
|
</div>
|
|
</template>
|
|
<div class="content">
|
|
<div
|
|
class="color"
|
|
data-test-id="color"
|
|
v-for="(_, index) in Array.from({ length: 7 })"
|
|
:key="index"
|
|
v-on:click="changeColor(index + 1)"
|
|
:class="`sticky-color-${index + 1}`"
|
|
:style="{
|
|
'border-width': '1px',
|
|
'border-style': 'solid',
|
|
'border-color': 'var(--color-foreground-xdark)',
|
|
'background-color': `var(--color-sticky-background-${index + 1})`,
|
|
'box-shadow':
|
|
(index === 0 && node?.parameters.color === '') ||
|
|
index + 1 === node?.parameters.color
|
|
? `0 0 0 1px var(--color-sticky-background-${index + 1})`
|
|
: 'none',
|
|
}"
|
|
></div>
|
|
</div>
|
|
</n8n-popover>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { defineComponent, ref } from 'vue';
|
|
import { mapStores } from 'pinia';
|
|
|
|
import { nodeBase } from '@/mixins/nodeBase';
|
|
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
|
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
|
import { isNumber, isString } from '@/utils/typeGuards';
|
|
import type {
|
|
INodeUi,
|
|
INodeUpdatePropertiesInformation,
|
|
IUpdateInformation,
|
|
XYPosition,
|
|
} from '@/Interface';
|
|
|
|
import type { INodeTypeDescription } from 'n8n-workflow';
|
|
import { QUICKSTART_NOTE_NAME } from '@/constants';
|
|
import { useUIStore } from '@/stores/ui.store';
|
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
import { useNDVStore } from '@/stores/ndv.store';
|
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|
import { useContextMenu } from '@/composables/useContextMenu';
|
|
|
|
export default defineComponent({
|
|
name: 'Sticky',
|
|
mixins: [nodeBase, nodeHelpers, workflowHelpers],
|
|
setup() {
|
|
const colorPopoverTrigger = ref<HTMLDivElement>();
|
|
const forceActions = ref(false);
|
|
const setForceActions = (value: boolean) => {
|
|
forceActions.value = value;
|
|
};
|
|
const contextMenu = useContextMenu((action) => {
|
|
if (action === 'change_color') {
|
|
setForceActions(true);
|
|
colorPopoverTrigger.value?.click();
|
|
}
|
|
});
|
|
|
|
return { colorPopoverTrigger, contextMenu, forceActions, setForceActions };
|
|
},
|
|
props: {
|
|
nodeViewScale: {
|
|
type: Number,
|
|
},
|
|
gridSize: {
|
|
type: Number,
|
|
},
|
|
},
|
|
computed: {
|
|
...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore),
|
|
defaultText(): string {
|
|
if (!this.nodeType) {
|
|
return '';
|
|
}
|
|
const properties = this.nodeType.properties;
|
|
const content = properties.find((property) => property.name === 'content');
|
|
|
|
return content && isString(content.default) ? content.default : '';
|
|
},
|
|
isSelected(): boolean {
|
|
return (
|
|
this.uiStore.getSelectedNodes.find((node: INodeUi) => node.name === this.data.name) !==
|
|
undefined
|
|
);
|
|
},
|
|
nodeType(): INodeTypeDescription | null {
|
|
return this.data && this.nodeTypesStore.getNodeType(this.data.type, this.data.typeVersion);
|
|
},
|
|
node(): INodeUi | null {
|
|
// same as this.data but reactive..
|
|
return this.workflowsStore.getNodeByName(this.name);
|
|
},
|
|
position(): XYPosition {
|
|
if (this.node) {
|
|
return this.node.position;
|
|
} else {
|
|
return [0, 0];
|
|
}
|
|
},
|
|
height(): number {
|
|
return this.node && isNumber(this.node.parameters.height) ? this.node.parameters.height : 0;
|
|
},
|
|
width(): number {
|
|
return this.node && isNumber(this.node.parameters.width) ? this.node.parameters.width : 0;
|
|
},
|
|
stickySize(): object {
|
|
const returnStyles: {
|
|
[key: string]: string | number;
|
|
} = {
|
|
height: this.height + 'px',
|
|
width: this.width + 'px',
|
|
};
|
|
|
|
return returnStyles;
|
|
},
|
|
stickyPosition(): object {
|
|
const returnStyles: {
|
|
[key: string]: string | number;
|
|
} = {
|
|
left: this.position[0] + 'px',
|
|
top: this.position[1] + 'px',
|
|
zIndex: this.isActive ? 9999999 : -1 * Math.floor((this.height * this.width) / 1000),
|
|
};
|
|
|
|
return returnStyles;
|
|
},
|
|
showActions(): boolean {
|
|
return (
|
|
!(this.hideActions || this.isReadOnly || this.workflowRunning || this.isResizing) ||
|
|
this.forceActions
|
|
);
|
|
},
|
|
workflowRunning(): boolean {
|
|
return this.uiStore.isActionActive('workflowRunning');
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
isResizing: false,
|
|
isTouchActive: false,
|
|
};
|
|
},
|
|
methods: {
|
|
onShowPopover() {
|
|
this.setForceActions(true);
|
|
},
|
|
onHidePopover() {
|
|
this.setForceActions(false);
|
|
},
|
|
async deleteNode() {
|
|
// Wait a tick else vue causes problems because the data is gone
|
|
await this.$nextTick();
|
|
this.$emit('removeNode', this.data.name);
|
|
},
|
|
changeColor(index: number) {
|
|
this.workflowsStore.updateNodeProperties({
|
|
name: this.name,
|
|
properties: { parameters: { ...this.node.parameters, color: index } },
|
|
});
|
|
},
|
|
onEdit(edit: boolean) {
|
|
if (edit && !this.isActive && this.node) {
|
|
this.ndvStore.activeNodeName = this.node.name;
|
|
} else if (this.isActive && !edit) {
|
|
this.ndvStore.activeNodeName = null;
|
|
}
|
|
},
|
|
onMarkdownClick(link: HTMLAnchorElement, event: Event) {
|
|
if (link) {
|
|
const isOnboardingNote = this.name === QUICKSTART_NOTE_NAME;
|
|
const isWelcomeVideo = link.querySelector('img[alt="n8n quickstart video"');
|
|
const type =
|
|
isOnboardingNote && isWelcomeVideo
|
|
? 'welcome_video'
|
|
: isOnboardingNote && link.getAttribute('href') === '/templates'
|
|
? 'templates'
|
|
: 'other';
|
|
|
|
this.$telemetry.track('User clicked note link', { type });
|
|
}
|
|
},
|
|
onInputChange(content: string) {
|
|
this.node.parameters.content = content;
|
|
this.setParameters({ content });
|
|
},
|
|
onResizeStart() {
|
|
this.isResizing = true;
|
|
if (!this.isSelected && this.node) {
|
|
this.$emit('nodeSelected', this.node.name, false, true);
|
|
}
|
|
},
|
|
onResize({ height, width, dX, dY }: { width: number; height: number; dX: number; dY: number }) {
|
|
if (!this.node) {
|
|
return;
|
|
}
|
|
if (dX !== 0 || dY !== 0) {
|
|
this.setPosition([this.node.position[0] + (dX || 0), this.node.position[1] + (dY || 0)]);
|
|
}
|
|
|
|
this.setParameters({ height, width });
|
|
},
|
|
onResizeEnd() {
|
|
this.isResizing = false;
|
|
},
|
|
setParameters(params: { content?: string; height?: number; width?: number; color?: string }) {
|
|
if (this.node) {
|
|
const nodeParameters = {
|
|
content: isString(params.content) ? params.content : this.node.parameters.content,
|
|
height: isNumber(params.height) ? params.height : this.node.parameters.height,
|
|
width: isNumber(params.width) ? params.width : this.node.parameters.width,
|
|
color: isString(params.color) ? params.color : this.node.parameters.color,
|
|
};
|
|
|
|
const updateInformation: IUpdateInformation = {
|
|
key: this.node.id,
|
|
name: this.node.name,
|
|
value: nodeParameters,
|
|
};
|
|
|
|
this.workflowsStore.setNodeParameters(updateInformation);
|
|
}
|
|
},
|
|
setPosition(position: XYPosition) {
|
|
if (!this.node) {
|
|
return;
|
|
}
|
|
|
|
const updateInformation: INodeUpdatePropertiesInformation = {
|
|
name: this.node.name,
|
|
properties: {
|
|
position,
|
|
},
|
|
};
|
|
|
|
this.workflowsStore.updateNodeProperties(updateInformation);
|
|
},
|
|
touchStart() {
|
|
if (this.isTouchDevice === true && !this.isMacOs && !this.isTouchActive) {
|
|
this.isTouchActive = true;
|
|
setTimeout(() => {
|
|
this.isTouchActive = false;
|
|
}, 2000);
|
|
}
|
|
},
|
|
onContextMenu(e: MouseEvent): void {
|
|
if (this.node) {
|
|
this.contextMenu.open(e, { source: 'node-right-click', node: this.node });
|
|
}
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.sticky-wrapper {
|
|
position: absolute;
|
|
|
|
.sticky-default {
|
|
.sticky-box {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
&.touch-active,
|
|
&:hover {
|
|
.sticky-options {
|
|
display: flex;
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
|
|
.sticky-options {
|
|
display: none;
|
|
justify-content: flex-start;
|
|
position: absolute;
|
|
top: -25px;
|
|
left: -8px;
|
|
height: 26px;
|
|
font-size: 0.9em;
|
|
text-align: left;
|
|
z-index: 10;
|
|
color: #aaa;
|
|
text-align: center;
|
|
|
|
.option {
|
|
width: 28px;
|
|
display: inline-block;
|
|
|
|
&.touch {
|
|
display: none;
|
|
}
|
|
|
|
&:hover {
|
|
color: $color-primary;
|
|
}
|
|
}
|
|
}
|
|
|
|
.force-show {
|
|
display: flex;
|
|
}
|
|
|
|
&.is-touch-device .sticky-options {
|
|
left: -25px;
|
|
width: 150px;
|
|
|
|
.option.touch {
|
|
display: initial;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.select-sticky-background {
|
|
display: block;
|
|
position: absolute;
|
|
background-color: var(--color-canvas-selected);
|
|
border-radius: var(--border-radius-xlarge);
|
|
overflow: hidden;
|
|
height: calc(100% + 16px);
|
|
width: calc(100% + 16px);
|
|
left: -8px;
|
|
top: -8px;
|
|
z-index: 0;
|
|
}
|
|
|
|
.content {
|
|
display: flex;
|
|
flex-direction: row;
|
|
width: fit-content;
|
|
gap: var(--spacing-2xs);
|
|
}
|
|
|
|
.color {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
border-color: var(--color-primary-shade-1);
|
|
|
|
&:hover {
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
</style>
|