feat(editor): Node creator actions (#4696)
* WIP: Node Actions List UI * WIP: Recommended Actions and preseting of fields * WIP: Resource category * 🎨 Moved actions categorisation to the server * 🏷️ Add missing INodeAction type * ✨ Improve SSR categorisation, fix adding of mixed actions * ♻️ Refactor CategorizedItems to composition api, style fixes * WIP: Adding multiple nodes * ♻️ Refactor rest of the NodeCreator component to composition API, conver globalLinkActions to composable * ✨ Allow actions dragging, fix search and refactor passing of actions to categorized items * 💄 Fix node actions title * Migrate to the pinia store, add posthog feature and various fixes * 🐛 Fix filtering of trigger actions when not merged * fix: N8N-5439 — Do not use simple node item when at NodeHelperPanel root * 🐛 Design review fixes * 🐛 Fix disabling of merged actions * Fix trigger root filtering * ✨ Allow for custom node actions parser, introduce hubspot parser * 🐛 Fix initial node params validation, fix position of second added node * 🐛 Introduce operations category, removed canvas node names overrride, fix API actions display and prevent dragging of action nodes * ✨ Prevent NDV auto-open feature flag * 🐛 Inject recommened action for trigger nodes without actions * Refactored NodeCreatorNode to Storybook, change filtering of merged nodes for the trigger helper panel, minor fixes * Improve rendering of app nodes and animation * Cleanup, any only enable accordion transition on triggerhelperpanel * Hide node creator scrollbars in Firefox * Minor styles fixes * Do not copy the array in rendering method * Removed unused props * Fix memory leak * Fix categorisation of regular nodes with a single resource * Implement telemetry calls for node actions * Move categorization to FE * Fix client side actions categorisation * Skip custom action show * Only load tooltip for NodeIcon if necessary * Fix lodash startCase import * Remove lodash.startcase * Cleanup * Fix node creator autofocus on "tab" * Prevent posthog getFeatureFlag from crashing * Debugging preview env search issues * Remove logs * Make sure the pre-filled params are update not overwritten * Get rid of transition in itemiterator * WIP: Rough version of NodeActions keyboard navigation, replace nodeCreator composable with Pinia store module * Rewrite to add support for ActionItem to ItemIterator and make CategorizedItems accept items props * Fix category item counter & cleanup * Add APIHint to actions search no-result, clean up NodeCreatorNode * Improve node actions no results message * Remove logging, fix filtering of recommended placeholder category * Remove unused NodeActions component and node merging feature falg * Do not show regular nodes without actions * Make sure to add manual trigger when adding http node via actions hint * Fixed api hint footer line height * Prevent pointer-events od NodeIcon img and remove "this" from template * Address PR points * Fix e2e specs * Make sure canvas ia loaded * Make sure canvas ia loaded before opening nodeCreator in e2e spec * Fix flaky workflows tags e2e getter * Imrpove node creator click outside UX, add manual node to regular nodes added from trigger panel * Add manual trigger node if dragging regular from trigger panel
This commit is contained in:
@@ -1,206 +1,180 @@
|
||||
<template>
|
||||
<div
|
||||
draggable
|
||||
<!-- Node Item is draggable only if it doesn't contain actions -->
|
||||
<n8n-node-creator-node
|
||||
:draggable="!showActionArrow"
|
||||
@dragstart="onDragStart"
|
||||
@dragend="onDragEnd"
|
||||
:class="{[$style['node-item']]: true}"
|
||||
@click.stop="onClick"
|
||||
:class="$style.nodeItem"
|
||||
:description="allowActions ? undefined : description"
|
||||
:title="displayName"
|
||||
:isTrigger="!allowActions && isTriggerNode"
|
||||
:show-action-arrow="showActionArrow"
|
||||
>
|
||||
<node-icon :class="$style['node-icon']" :nodeType="nodeType" />
|
||||
<div>
|
||||
<div :class="$style.details">
|
||||
<span :class="$style.name" data-test-id="node-item-name">
|
||||
{{ $locale.headerText({
|
||||
key: `headers.${shortNodeType}.displayName`,
|
||||
fallback: nodeType.displayName,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-if="isTrigger" :class="$style['trigger-icon']">
|
||||
<trigger-icon />
|
||||
</span>
|
||||
<n8n-tooltip v-if="isCommunityNode" placement="top" data-test-id="node-item-community-tooltip">
|
||||
<template #content>
|
||||
<div
|
||||
:class="$style['community-node-icon']"
|
||||
v-html="$locale.baseText('generic.communityNode.tooltip', { interpolate: { packageName: nodeType.name.split('.')[0], docURL: COMMUNITY_NODES_INSTALLATION_DOCS_URL } })"
|
||||
@click="onCommunityNodeTooltipClick"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<n8n-icon icon="cube" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
<div :class="$style.description">
|
||||
{{ $locale.headerText({
|
||||
key: `headers.${shortNodeType}.description`,
|
||||
fallback: nodeType.description,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<template #icon>
|
||||
<node-icon :nodeType="nodeType" />
|
||||
</template>
|
||||
|
||||
<div :class="$style['draggable-data-transfer']" ref="draggableDataTransfer" />
|
||||
<transition name="node-item-transition">
|
||||
<div
|
||||
:class="$style.draggable"
|
||||
:style="draggableStyle"
|
||||
ref="draggable"
|
||||
v-show="dragging"
|
||||
>
|
||||
<node-icon class="node-icon" :nodeType="nodeType" :size="40" :shrink="false" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<template #tooltip v-if="isCommunityNode">
|
||||
<p
|
||||
:class="$style.communityNodeIcon"
|
||||
v-html="$locale.baseText('generic.communityNode.tooltip', { interpolate: { packageName: nodeType.name.split('.')[0], docURL: COMMUNITY_NODES_INSTALLATION_DOCS_URL } })"
|
||||
@click="onCommunityNodeTooltipClick"
|
||||
/>
|
||||
</template>
|
||||
<template #dragContent>
|
||||
<div :class="$style.draggableDataTransfer" ref="draggableDataTransfer"/>
|
||||
<div
|
||||
:class="$style.draggable"
|
||||
:style="draggableStyle"
|
||||
v-show="dragging"
|
||||
>
|
||||
<node-icon :nodeType="nodeType" @click.capture.stop :size="40" :shrink="false" />
|
||||
</div>
|
||||
</template>
|
||||
</n8n-node-creator-node>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import Vue, { PropType } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed, toRefs, getCurrentInstance } from 'vue';
|
||||
import { INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
import { isCommunityPackageName } from '@/utils';
|
||||
import { getNewNodePosition, NODE_SIZE } from '@/utils/nodeViewUtils';
|
||||
import { isCommunityPackageName } from '@/utils';
|
||||
import { COMMUNITY_NODES_INSTALLATION_DOCS_URL } from '@/constants';
|
||||
import { useNodeCreatorStore } from '@/stores/nodeCreator';
|
||||
|
||||
import NodeIcon from '@/components/NodeIcon.vue';
|
||||
import TriggerIcon from '@/components/TriggerIcon.vue';
|
||||
|
||||
Vue.component('node-icon', NodeIcon);
|
||||
Vue.component('trigger-icon', TriggerIcon);
|
||||
export interface Props {
|
||||
nodeType: INodeTypeDescription;
|
||||
active?: boolean;
|
||||
allowActions?: boolean;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'NodeItem',
|
||||
props: {
|
||||
nodeType: {
|
||||
type: Object as PropType<INodeTypeDescription>,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragging: false,
|
||||
draggablePosition: {
|
||||
x: -100,
|
||||
y: -100,
|
||||
},
|
||||
COMMUNITY_NODES_INSTALLATION_DOCS_URL,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
shortNodeType(): string {
|
||||
return this.$locale.shortNodeType(this.nodeType.name);
|
||||
},
|
||||
isTrigger (): boolean {
|
||||
return this.nodeType.group.includes('trigger');
|
||||
},
|
||||
draggableStyle(): { top: string; left: string; } {
|
||||
return {
|
||||
top: `${this.draggablePosition.y}px`,
|
||||
left: `${this.draggablePosition.x}px`,
|
||||
};
|
||||
},
|
||||
isCommunityNode(): boolean {
|
||||
return isCommunityPackageName(this.nodeType.name);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onDragStart(event: DragEvent): void {
|
||||
/**
|
||||
* Workaround for firefox, that doesn't attach the pageX and pageY coordinates to "ondrag" event.
|
||||
* All browsers attach the correct page coordinates to the "dragover" event.
|
||||
* @bug https://bugzilla.mozilla.org/show_bug.cgi?id=505521
|
||||
*/
|
||||
document.body.addEventListener("dragover", this.onDragOver);
|
||||
const { pageX: x, pageY: y } = event;
|
||||
|
||||
this.$emit('dragstart', event);
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = "copy";
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
event.dataTransfer.setData('nodeTypeName', this.nodeType.name);
|
||||
event.dataTransfer.setDragImage(this.$refs.draggableDataTransfer as Element, 0, 0);
|
||||
}
|
||||
|
||||
this.dragging = true;
|
||||
this.draggablePosition = { x, y };
|
||||
},
|
||||
onDragOver(event: DragEvent): void {
|
||||
if (!this.dragging || event.pageX === 0 && event.pageY === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [x,y] = getNewNodePosition([], [event.pageX - NODE_SIZE / 2, event.pageY - NODE_SIZE / 2]);
|
||||
|
||||
this.draggablePosition = { x, y };
|
||||
},
|
||||
onDragEnd(event: DragEvent): void {
|
||||
document.body.removeEventListener("dragover", this.onDragOver);
|
||||
this.$emit('dragend', event);
|
||||
|
||||
this.dragging = false;
|
||||
setTimeout(() => {
|
||||
this.draggablePosition = { x: -100, y: -100 };
|
||||
}, 300);
|
||||
},
|
||||
onCommunityNodeTooltipClick(event: MouseEvent) {
|
||||
if ((event.target as Element).localName === 'a') {
|
||||
this.$telemetry.track('user clicked cnr docs link', { source: 'nodes panel node' });
|
||||
}
|
||||
},
|
||||
},
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
active: false,
|
||||
allowActions: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'dragstart', $e: DragEvent): void,
|
||||
(event: 'dragend', $e: DragEvent): void,
|
||||
(event: 'nodeTypeSelected', value: string[]): void,
|
||||
(event: 'actionsOpen', value: INodeTypeDescription): void,
|
||||
}>();
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const state = reactive({
|
||||
dragging: false,
|
||||
draggablePosition: {
|
||||
x: -100,
|
||||
y: -100,
|
||||
},
|
||||
draggableDataTransfer: null as Element | null,
|
||||
});
|
||||
const description = computed<string>(() => {
|
||||
return instance?.proxy.$locale.headerText({
|
||||
key: `headers.${shortNodeType.value}.description`,
|
||||
fallback: props.nodeType.description,
|
||||
}) as string;
|
||||
});
|
||||
const showActionArrow = computed(() => props.allowActions && hasActions.value);
|
||||
|
||||
const hasActions = computed<boolean>(() => (props.nodeType.actions?.length || 0) > 0);
|
||||
|
||||
const shortNodeType = computed<string>(() => instance?.proxy.$locale.shortNodeType(props.nodeType.name) || '');
|
||||
|
||||
const draggableStyle = computed<{ top: string; left: string; }>(() => ({
|
||||
top: `${state.draggablePosition.y}px`,
|
||||
left: `${state.draggablePosition.x}px`,
|
||||
}));
|
||||
|
||||
const isCommunityNode = computed<boolean>(() => isCommunityPackageName(props.nodeType.name));
|
||||
|
||||
const displayName = computed<any>(() => {
|
||||
const displayName = props.nodeType.displayName.trimEnd();
|
||||
|
||||
return instance?.proxy.$locale.headerText({
|
||||
key: `headers.${shortNodeType}.displayName`,
|
||||
fallback: props.allowActions ? displayName.replace('Trigger', '') : displayName,
|
||||
});
|
||||
});
|
||||
|
||||
const isTriggerNode = computed<boolean>(() => props.nodeType.displayName.toLowerCase().includes('trigger'));
|
||||
|
||||
function onClick() {
|
||||
if(hasActions.value && props.allowActions) emit('actionsOpen', props.nodeType);
|
||||
else emit('nodeTypeSelected', [props.nodeType.name]);
|
||||
}
|
||||
function onDragStart(event: DragEvent): void {
|
||||
/**
|
||||
* Workaround for firefox, that doesn't attach the pageX and pageY coordinates to "ondrag" event.
|
||||
* All browsers attach the correct page coordinates to the "dragover" event.
|
||||
* @bug https://bugzilla.mozilla.org/show_bug.cgi?id=505521
|
||||
*/
|
||||
document.body.addEventListener("dragover", onDragOver);
|
||||
|
||||
const { pageX: x, pageY: y } = event;
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = "copy";
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
event.dataTransfer.setDragImage(state.draggableDataTransfer as Element, 0, 0);
|
||||
event.dataTransfer.setData(
|
||||
'nodeTypeName',
|
||||
useNodeCreatorStore().getNodeTypesWithManualTrigger(props.nodeType.name).join(','),
|
||||
);
|
||||
}
|
||||
|
||||
state.dragging = true;
|
||||
state.draggablePosition = { x, y };
|
||||
emit('dragstart', event);
|
||||
}
|
||||
|
||||
function onDragOver(event: DragEvent): void {
|
||||
if (!state.dragging || event.pageX === 0 && event.pageY === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [x,y] = getNewNodePosition([], [event.pageX - NODE_SIZE / 2, event.pageY - NODE_SIZE / 2]);
|
||||
|
||||
state.draggablePosition = { x, y };
|
||||
}
|
||||
|
||||
function onDragEnd(event: DragEvent): void {
|
||||
document.body.removeEventListener("dragover", onDragOver);
|
||||
|
||||
emit('dragend', event);
|
||||
|
||||
state.dragging = false;
|
||||
setTimeout(() => {
|
||||
state.draggablePosition = { x: -100, y: -100 };
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function onCommunityNodeTooltipClick(event: MouseEvent) {
|
||||
if ((event.target as Element).localName === 'a') {
|
||||
instance?.proxy.$telemetry.track('user clicked cnr docs link', { source: 'nodes panel node' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
defineExpose({
|
||||
onClick,
|
||||
});
|
||||
const { dragging, draggableDataTransfer } = toRefs(state);
|
||||
</script>
|
||||
<style lang="scss" module>
|
||||
.node-item {
|
||||
padding: 11px 8px 11px 0;
|
||||
.nodeItem {
|
||||
--trigger-icon-background-color: #{$trigger-icon-background-color};
|
||||
--trigger-icon-border-color: #{$trigger-icon-border-color};
|
||||
margin-left: 15px;
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.details {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
min-width: 26px;
|
||||
max-width: 26px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.packageName {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 2px;
|
||||
font-size: var(--font-size-2xs);
|
||||
line-height: 16px;
|
||||
font-weight: 400;
|
||||
color: $node-creator-description-color;
|
||||
}
|
||||
|
||||
.trigger-icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
display: inline-block;
|
||||
margin-right: var(--spacing-3xs);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.community-node-icon {
|
||||
.communityNodeIcon {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@@ -218,29 +192,8 @@ export default Vue.extend({
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.draggable-data-transfer {
|
||||
.draggableDataTransfer {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.node-item-transition {
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
transition-property: opacity, transform;
|
||||
transition-duration: 300ms;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
&-enter,
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
.el-tooltip svg {
|
||||
color: var(--color-foreground-xdark);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user