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:
@@ -0,0 +1,58 @@
|
||||
/* tslint:disable:variable-name */
|
||||
import N8nNodeCreatorNode from './NodeCreatorNode.vue';
|
||||
import { StoryFn } from '@storybook/vue';
|
||||
|
||||
export default {
|
||||
title: 'Modules/Node Creator Node',
|
||||
component: N8nNodeCreatorNode,
|
||||
};
|
||||
|
||||
const DefaultTemplate: StoryFn = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nNodeCreatorNode,
|
||||
},
|
||||
template: `
|
||||
<n8n-node-creator-node v-bind="$props">
|
||||
<template v-slot:icon>
|
||||
<img src="https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/cartman.svg" />
|
||||
</template>
|
||||
</n8n-node-creator-node>
|
||||
`,
|
||||
});
|
||||
|
||||
export const WithTitle = DefaultTemplate.bind({});
|
||||
WithTitle.args = {
|
||||
title: 'Node with title',
|
||||
tooltipHtml: '<b>Bold</b> tooltip',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean et vehicula ipsum, eu facilisis lacus. Aliquam commodo vel elit eget mollis. Quisque ac elit non purus iaculis placerat. Quisque fringilla ultrices nisi sed porta.',
|
||||
};
|
||||
|
||||
const PanelTemplate: StoryFn = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nNodeCreatorNode,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isPanelActive: false,
|
||||
};
|
||||
},
|
||||
template: `
|
||||
<n8n-node-creator-node v-bind="$props" :isPanelActive="isPanelActive" @click.capture="isPanelActive = true">
|
||||
<template v-slot:icon>
|
||||
<img src="https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/cartman.svg" />
|
||||
</template>
|
||||
<template v-slot:panel>
|
||||
<p style="width: 100%; height: 300px; background: white">Lorem ipsum dolor sit amet</p>
|
||||
<button @click="isPanelActive = false">Close</button>
|
||||
</template>
|
||||
</n8n-node-creator-node>
|
||||
`,
|
||||
});
|
||||
export const WithPanel = PanelTemplate.bind({});
|
||||
WithPanel.args = {
|
||||
title: 'Node with panel',
|
||||
isTrigger: true,
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
[$style.creatorNode]: true,
|
||||
[$style.hasAction]: !showActionArrow,
|
||||
}"
|
||||
v-on="$listeners"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<div :class="$style.nodeIcon">
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
<div>
|
||||
<div :class="$style.details">
|
||||
<span :class="$style.name" v-text="title" data-test-id="node-creator-item-name" />
|
||||
<trigger-icon v-if="isTrigger" :class="$style.triggerIcon" />
|
||||
<n8n-tooltip
|
||||
v-if="!!$slots.tooltip"
|
||||
placement="top"
|
||||
data-test-id="node-creator-item-tooltip"
|
||||
>
|
||||
<template #content>
|
||||
<slot name="tooltip" />
|
||||
</template>
|
||||
<n8n-icon :class="$style.tooltipIcon" icon="cube" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
<p :class="$style.description" v-if="description" v-text="description" />
|
||||
</div>
|
||||
<slot name="dragContent" />
|
||||
<button :class="$style.panelIcon" v-if="showActionArrow">
|
||||
<font-awesome-icon :class="$style.panelArrow" icon="arrow-right" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
import TriggerIcon from './TriggerIcon.vue';
|
||||
import N8nTooltip from '../N8nTooltip';
|
||||
|
||||
export interface Props {
|
||||
active?: boolean;
|
||||
isTrigger?: boolean;
|
||||
description?: string;
|
||||
title: string;
|
||||
showActionArrow?: boolean;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
defineEmits<{
|
||||
(event: 'tooltipClick', $e: MouseEvent): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.creatorNode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
padding: 11px 8px 11px 0;
|
||||
|
||||
&.hasAction {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
.creatorNode:hover .panelIcon {
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.panelIcon {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-left: var(--spacing-2xs);
|
||||
color: var(--color-text-lighter);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
.tooltipIcon {
|
||||
margin-left: var(--spacing-3xs);
|
||||
}
|
||||
.panelArrow {
|
||||
font-size: var(--font-size-2xs);
|
||||
width: 12px;
|
||||
}
|
||||
.details {
|
||||
align-items: center;
|
||||
}
|
||||
.nodeIcon {
|
||||
display: flex;
|
||||
margin-right: var(--spacing-s);
|
||||
|
||||
& > :global(*) {
|
||||
min-width: 25px;
|
||||
max-width: 25px;
|
||||
}
|
||||
}
|
||||
.name {
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-s);
|
||||
line-height: 1.115rem;
|
||||
}
|
||||
.description {
|
||||
margin-top: var(--spacing-5xs);
|
||||
font-size: var(--font-size-2xs);
|
||||
line-height: 1rem;
|
||||
font-weight: 400;
|
||||
color: var(--node-creator-description-colo, var(--color-text-base));
|
||||
}
|
||||
|
||||
.triggerIcon {
|
||||
margin-left: var(--spacing-2xs);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-tooltip svg {
|
||||
color: var(--color-foreground-xdark);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<span :class="$style.trigger">
|
||||
<svg
|
||||
width="36px"
|
||||
height="36px"
|
||||
viewBox="0 0 36 36"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<title>Trigger node</title>
|
||||
<g
|
||||
id="/integrations-(V1-feature)"
|
||||
stroke="none"
|
||||
stroke-width="1"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
>
|
||||
<g
|
||||
id="Individual-node-view"
|
||||
transform="translate(-304.000000, -137.000000)"
|
||||
fill-rule="nonzero"
|
||||
>
|
||||
<g id="left-column" transform="translate(120.000000, 131.000000)">
|
||||
<g id="trigger-badge" transform="translate(178.000000, 0.000000)">
|
||||
<g id="trigger-icon" transform="translate(6.857143, 6.857143)">
|
||||
<g id="Icon" transform="translate(8.571429, 0.000000)" fill="#FF6150">
|
||||
<polygon
|
||||
id="Icon-Path"
|
||||
points="7.14285714 21.4285714 0 21.4285714 10 1.42857143 10 12.8571429 17.1428571 12.8571429 7.14285714 32.8571429"
|
||||
></polygon>
|
||||
</g>
|
||||
<rect id="ViewBox" x="0" y="0" width="34.2857143" height="34.2857143"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TriggerIcon',
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.trigger {
|
||||
background-color: var(--trigger-icon-background-color, var(--color-background-xlight));
|
||||
border: 1px solid var(--trigger-icon-border-color, var(--color-background-xlight));
|
||||
border-radius: 4px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 16px;
|
||||
|
||||
> svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
import NodeCreatorNode from './NodeCreatorNode.vue';
|
||||
|
||||
export default NodeCreatorNode;
|
||||
@@ -2,24 +2,35 @@
|
||||
<div class="n8n-node-icon">
|
||||
<div
|
||||
:class="{
|
||||
[$style['node-icon-wrapper']]: true,
|
||||
[$style['circle']]: this.circle,
|
||||
[$style['disabled']]: this.disabled,
|
||||
[$style.nodeIconWrapper]: true,
|
||||
[$style.circle]: circle,
|
||||
[$style.disabled]: disabled,
|
||||
}"
|
||||
:style="iconStyleData"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<n8n-tooltip placement="top" :disabled="!showTooltip">
|
||||
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
|
||||
<n8n-tooltip placement="top" :disabled="!showTooltip" v-if="showTooltip">
|
||||
<template #content>{{ nodeTypeName }}</template>
|
||||
<div v-if="type !== 'unknown'" :class="$style['icon']">
|
||||
<img v-if="type === 'file'" :src="src" :class="$style['node-icon-image']" />
|
||||
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
|
||||
<font-awesome-icon v-else :icon="name" :style="fontStyleData" />
|
||||
</div>
|
||||
<div v-else :class="$style['node-icon-placeholder']">
|
||||
<div v-else :class="$style.nodeIconPlaceholder">
|
||||
{{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
|
||||
?
|
||||
</div>
|
||||
</n8n-tooltip>
|
||||
<template v-else>
|
||||
<div v-if="type !== 'unknown'" :class="$style.icon">
|
||||
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
|
||||
<font-awesome-icon v-else :icon="name" :style="fontStyleData" />
|
||||
</div>
|
||||
<div v-else :class="$style.nodeIconPlaceholder">
|
||||
{{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
|
||||
?
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -91,7 +102,7 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.node-icon-wrapper {
|
||||
.nodeIconWrapper {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: var(--border-radius-small);
|
||||
@@ -110,13 +121,12 @@ export default Vue.extend({
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.node-icon-placeholder {
|
||||
.nodeIconPlaceholder {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.node-icon-image {
|
||||
.nodeIconImage {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
@@ -25,6 +25,7 @@ import N8nLoading from '../components/N8nLoading';
|
||||
import N8nMarkdown from '../components/N8nMarkdown';
|
||||
import N8nMenu from '../components/N8nMenu';
|
||||
import N8nMenuItem from '../components/N8nMenuItem';
|
||||
import N8nNodeCreatorNode from '../components/N8nNodeCreatorNode';
|
||||
import N8nNodeIcon from '../components/N8nNodeIcon';
|
||||
import N8nNotice from '../components/N8nNotice';
|
||||
import N8nOption from '../components/N8nOption';
|
||||
@@ -73,6 +74,7 @@ export default {
|
||||
app.component('n8n-markdown', N8nMarkdown);
|
||||
app.component('n8n-menu', N8nMenu);
|
||||
app.component('n8n-menu-item', N8nMenuItem);
|
||||
app.component('n8n-node-creator-node', N8nNodeCreatorNode);
|
||||
app.component('n8n-node-icon', N8nNodeIcon);
|
||||
app.component('n8n-notice', N8nNotice);
|
||||
app.component('n8n-option', N8nOption);
|
||||
|
||||
Reference in New Issue
Block a user