mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 12:44:07 -08:00
feat(editor): Add drag and drop data mapping (#3708)
* commit package lock * refactor param options out * use action toggle * handle click on toggle * update color toggle * fix toggle * show options * update expression color * update pointer * fix readonly * fix readonly * fix expression spacing * refactor input label * show icon for headers * center icon * fix multi params * add credential options * increase spacing * update expression view * update transition * update el padding * rename side to options * fix label overflow * fix bug with unnessary lines * add overlay * fix bug affecting other pages * clean up spacing * rename * update icon size * fix toggle in users * clean up func * clean up css * use css var * fix overlay bug * clean up input * clean up input * clean up unnessary css * revert * update quotes * rename method * remove console errors * refactor data table * add drag button * make hoverable cells * add drag hint * disabel for output panel * add drag * disable for readonly * Add dragging * add draggable pill * add mapping targets * remove font color * Transferable * fix linting issue * teleport component * fix line * disable for readonly * fix position of data pill * fix position of data pill * ignore import * add droppable state * remove draggable key * update bg color * add value drop * use direct input * remove transition * add animation * shorten name * handle empty value * fix switch bug * fix up animation * add notification * add hint * add tooltip * show draggable hintm * fix multiple expre * fix hoverable * keep options on focus * increase timeouts * fix bug in set node * add transition on hover out * fix tooltip onboarding bug * only update expression if changes * add open delay * fix header highlight issue * update text * dont show tooltip always * update docs url * update ee border * add sticky behav * hide error highlight if dropping * switch out grip icon * increase timeout * add delay * show hint on execprev * add telemetry event * add telemetry event * add telemetry event * fire event on hint showing * fix telemetry event * add path * fix drag hint issue * decrease bottom margin * update mapping keys * remove file * hide overflow * sort params * add space * prevent scrolling * remove dropshadow * force cursor * address some comments * add thead tbody * add size opt
This commit is contained in:
parent
2997711e00
commit
577c73ee25
|
@ -7,7 +7,11 @@ export default {
|
|||
argTypes: {
|
||||
placement: {
|
||||
type: 'select',
|
||||
options: ['top', 'bottom'],
|
||||
options: ['top', 'top-start', 'top-end', 'bottom', 'bottom-end'],
|
||||
},
|
||||
size: {
|
||||
type: 'select',
|
||||
options: ['mini', 'small', 'medium'],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<template>
|
||||
<span :class="$style.container">
|
||||
<el-dropdown :placement="placement" trigger="click" @command="onCommand">
|
||||
<span :class="$style.button">
|
||||
<el-dropdown :placement="placement" :size="size" trigger="click" @command="onCommand" @visible-change="onVisibleChange">
|
||||
<span :class="{[$style.button]: true, [$style[theme]]: !!theme}">
|
||||
<component :is="$options.components.N8nIcon"
|
||||
icon="ellipsis-v"
|
||||
:size="iconSize"
|
||||
/>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
|
@ -11,6 +12,7 @@
|
|||
v-for="action in actions"
|
||||
:key="action.value"
|
||||
:command="action.value"
|
||||
:disabled="action.disabled"
|
||||
>
|
||||
{{action.label}}
|
||||
</el-dropdown-item>
|
||||
|
@ -42,12 +44,30 @@ export default {
|
|||
type: String,
|
||||
default: 'bottom',
|
||||
validator: (value: string): boolean =>
|
||||
['top', 'bottom'].includes(value),
|
||||
['top', 'top-end', 'top-start', 'bottom', 'bottom-end', 'bottom-start'].includes(value),
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value: string): boolean =>
|
||||
['mini', 'small', 'medium'].includes(value),
|
||||
},
|
||||
iconSize: {
|
||||
type: String,
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (value: string): boolean =>
|
||||
['default', 'dark'].includes(value),
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onCommand(value: string) {
|
||||
this.$emit('action', value) ;
|
||||
this.$emit('action', value);
|
||||
},
|
||||
onVisibleChange(value: boolean) {
|
||||
this.$emit('visible-change', value);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -62,6 +82,18 @@ export default {
|
|||
cursor: pointer;
|
||||
padding: var(--spacing-4xs);
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
color: var(--color-text-dark);
|
||||
|
||||
&:focus {
|
||||
|
|
|
@ -1,16 +1,26 @@
|
|||
<template functional>
|
||||
<div :class="{[$style.inputLabelContainer]: !props.labelHoverableOnly}">
|
||||
<div :class="$options.methods.getLabelClass(props, $style)">
|
||||
<component v-if="props.label" :is="$options.components.N8nText" :bold="props.bold" :size="props.size" :compact="!props.underline">
|
||||
{{ props.label }}
|
||||
<component :is="$options.components.N8nText" color="primary" :bold="props.bold" :size="props.size" v-if="props.required">*</component>
|
||||
</component>
|
||||
<span :class="[$style.infoIcon, props.showTooltip ? $style.showIcon: $style.hiddenIcon]" v-if="props.tooltipText">
|
||||
<component :is="$options.components.N8nTooltip" placement="top" :popper-class="$style.tooltipPopper">
|
||||
<component :is="$options.components.N8nIcon" icon="question-circle" size="small" />
|
||||
<div slot="content" v-html="$options.methods.addTargetBlank(props.tooltipText)"></div>
|
||||
</component>
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<div :class="{
|
||||
[this.$style.label]: !!this.label,
|
||||
[this.$style.underline]: this.underline,
|
||||
[this.$style[this.size]]: true,
|
||||
}">
|
||||
<div :class="$style.title" v-if="label">
|
||||
<n8n-text :bold="bold" :size="size" :compact="!underline">
|
||||
{{ label }}
|
||||
<n8n-text color="primary" :bold="bold" :size="size" v-if="required">*</n8n-text>
|
||||
</n8n-text>
|
||||
</div>
|
||||
<span :class="[$style.infoIcon, showTooltip ? $style.visible: $style.hidden]" v-if="tooltipText && label">
|
||||
<n8n-tooltip placement="top" :popper-class="$style.tooltipPopper">
|
||||
<n8n-icon icon="question-circle" size="small" />
|
||||
<div slot="content" v-html="addTargetBlank(tooltipText)"></div>
|
||||
</n8n-tooltip>
|
||||
</span>
|
||||
<div v-if="$slots.options && label" :class="{[$style.overlay]: true, [$style.visible]: showOptions}"><div></div></div>
|
||||
<div v-if="$slots.options" :class="{[$style.options]: true, [$style.visible]: showOptions}">
|
||||
<slot name="options"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
@ -56,74 +66,104 @@ export default {
|
|||
showTooltip: {
|
||||
type: Boolean,
|
||||
},
|
||||
labelHoverableOnly: {
|
||||
showOptions: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addTargetBlank,
|
||||
getLabelClass(props: {label: string, size: string, underline: boolean}, $style: any) {
|
||||
if (!props.label) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const classes = [];
|
||||
if (props.underline) {
|
||||
classes.push($style[`label-${props.size}-underline`]);
|
||||
}
|
||||
else {
|
||||
classes.push($style[`label-${props.size}`]);
|
||||
}
|
||||
|
||||
if (props.labelHoverableOnly) {
|
||||
classes.push($style.inputLabel);
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.inputLabelContainer:hover {
|
||||
> div > .infoIcon {
|
||||
display: inline-block;
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container:hover,.inputLabel:hover {
|
||||
.infoIcon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.options {
|
||||
opacity: 1;
|
||||
transition: opacity 100ms ease-in; // transition on hover in
|
||||
}
|
||||
|
||||
.overlay {
|
||||
opacity: 1;
|
||||
transition: opacity 100ms ease-in; // transition on hover in
|
||||
}
|
||||
}
|
||||
|
||||
.inputLabel:hover {
|
||||
> .infoIcon {
|
||||
display: inline-block;
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
|
||||
> * {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-text-light);
|
||||
padding-left: var(--spacing-4xs);
|
||||
background-color: var(--color-background-xlight);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.showIcon {
|
||||
display: inline-block;
|
||||
}
|
||||
.options {
|
||||
opacity: 0;
|
||||
background-color: var(--color-background-xlight);
|
||||
transition: opacity 250ms cubic-bezier(.98,-0.06,.49,-0.2); // transition on hover out
|
||||
|
||||
.hiddenIcon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
* {
|
||||
margin-right: var(--spacing-5xs);
|
||||
> * {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.label-small {
|
||||
composes: label;
|
||||
margin-bottom: var(--spacing-4xs);
|
||||
.overlay {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 250ms cubic-bezier(.98,-0.06,.49,-0.2); // transition on hover out
|
||||
|
||||
> div {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
height: 19px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 0;
|
||||
|
||||
background: linear-gradient(270deg, var(--color-foreground-xlight) 72.19%, rgba(255, 255, 255, 0) 107.45%);
|
||||
}
|
||||
}
|
||||
|
||||
.label-medium {
|
||||
composes: label;
|
||||
.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
overflow-x: hidden;
|
||||
overflow-y: clip;
|
||||
}
|
||||
|
||||
.small {
|
||||
margin-bottom: var(--spacing-5xs);
|
||||
}
|
||||
|
||||
.medium {
|
||||
margin-bottom: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
|
@ -131,16 +171,6 @@ export default {
|
|||
border-bottom: var(--border-base);
|
||||
}
|
||||
|
||||
.label-small-underline {
|
||||
composes: label-small;
|
||||
composes: underline;
|
||||
}
|
||||
|
||||
.label-medium-underline {
|
||||
composes: label-medium;
|
||||
composes: underline;
|
||||
}
|
||||
|
||||
.tooltipPopper {
|
||||
max-width: 400px;
|
||||
|
||||
|
@ -148,4 +178,5 @@ export default {
|
|||
margin-left: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<label role="radio" tabindex="-1" :class="$style.container" aria-checked="true">
|
||||
<label role="radio" tabindex="-1" :class="{[$style.container]: true, [$style.hoverable]: !this.disabled}" aria-checked="true">
|
||||
<input type="radio" tabindex="-1" autocomplete="off" :class="$style.input" :value="value">
|
||||
<div :class="{[$style.button]: true, [$style.active]: active}" @click="$emit('click')">{{ label }}</div>
|
||||
<div :class="{[$style.button]: true, [$style.active]: active, [$style[size]]: true, [$style.disabled]: disabled}" @click="$emit('click')">{{ label }}</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
|
@ -21,6 +21,15 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value: string): boolean =>
|
||||
['small', 'medium'].includes(value),
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -30,11 +39,11 @@ export default {
|
|||
display: inline-block;
|
||||
outline: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.button:not(.active) {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.hoverable:hover {
|
||||
.button:not(.active) {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,16 +56,29 @@ export default {
|
|||
|
||||
.button {
|
||||
border-radius: 0;
|
||||
padding: 0 var(--spacing-xs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 26px;
|
||||
font-size: var(--font-size-2xs);
|
||||
border-radius: var(--border-radius-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-base);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.medium {
|
||||
height: 26px;
|
||||
font-size: var(--font-size-2xs);
|
||||
padding: 0 var(--spacing-xs);
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: var(--font-size-3xs);
|
||||
height: 15px;
|
||||
padding: 0 var(--spacing-4xs);
|
||||
}
|
||||
|
||||
.active {
|
||||
|
|
|
@ -6,6 +6,10 @@ export default {
|
|||
title: 'Atoms/RadioButtons',
|
||||
component: N8nRadioButtons,
|
||||
argTypes: {
|
||||
size: {
|
||||
type: 'select',
|
||||
options: ['small', 'medium'],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
backgrounds: { default: '--color-background-xlight' },
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<template>
|
||||
<div role="radiogroup" :class="$style.radioGroup">
|
||||
<div role="radiogroup" :class="{[$style.radioGroup]: true, [$style.disabled]: disabled}">
|
||||
<RadioButton
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
v-bind="option"
|
||||
:active="value === option.value"
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
@click="(e) => onClick(option.value, e)"
|
||||
/>
|
||||
</div>
|
||||
|
@ -21,12 +23,21 @@ export default {
|
|||
},
|
||||
options: {
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
RadioButton,
|
||||
},
|
||||
methods: {
|
||||
onClick(value) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
this.$emit('input', value);
|
||||
},
|
||||
},
|
||||
|
@ -45,5 +56,9 @@ export default {
|
|||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
v-if="!user.isOwner"
|
||||
placement="bottom"
|
||||
:actions="getActions(user)"
|
||||
theme="dark"
|
||||
@action="(action) => onUserAction(user, action)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,7 @@ import VariableTable from './VariableTable.vue';
|
|||
<Canvas>
|
||||
<Story name="font-size">
|
||||
{{
|
||||
template: `<sizes :variables="['--font-size-2xs','--font-size-xs','--font-size-s','--font-size-m','--font-size-l','--font-size-xl','--font-size-2xl']" attr="font-size" />`,
|
||||
template: `<sizes :variables="['--font-size-3xs', '--font-size-2xs','--font-size-xs','--font-size-s','--font-size-m','--font-size-l','--font-size-xl','--font-size-2xl']" attr="font-size" />`,
|
||||
components: {
|
||||
Sizes,
|
||||
},
|
||||
|
|
|
@ -70,6 +70,23 @@
|
|||
var(--color-secondary-l)
|
||||
);
|
||||
|
||||
--color-secondary-tint-1-s: 45%;
|
||||
--color-secondary-tint-1-l: 70%;
|
||||
--color-secondary-tint-1: hsl(
|
||||
var(--color-secondary-h),
|
||||
var(--color-secondary-tint-1-s),
|
||||
var(--color-secondary-tint-1-l)
|
||||
);
|
||||
|
||||
--color-secondary-tint-2-s: 46.7%;
|
||||
--color-secondary-tint-2-l: 97%;
|
||||
--color-secondary-tint-2: hsl(
|
||||
var(--color-secondary-h),
|
||||
var(--color-secondary-tint-2-s),
|
||||
var(--color-secondary-tint-2-l)
|
||||
);
|
||||
|
||||
|
||||
--color-success-h: 150.4;
|
||||
--color-success-s: 60%;
|
||||
--color-success-l: 40.4%;
|
||||
|
|
|
@ -114,7 +114,6 @@ video {
|
|||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
vertical-align: baseline;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
"vue-fragment": "1.5.1",
|
||||
"vue-i18n": "^8.26.7",
|
||||
"vue2-boring-avatars": "0.3.4",
|
||||
"vue2-teleport": "^1.0.1",
|
||||
"xss": "^1.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -870,8 +870,17 @@ export interface IUiState {
|
|||
output: {
|
||||
displayMode: IRunDataDisplayMode;
|
||||
};
|
||||
focusedMappableInput: string;
|
||||
mappingTelemetry: {[key: string]: string | number | boolean};
|
||||
};
|
||||
mainPanelPosition: number;
|
||||
draggable: {
|
||||
isDragging: boolean;
|
||||
type: string;
|
||||
data: string;
|
||||
canDrop: boolean;
|
||||
stickyPosition: null | XYPosition;
|
||||
};
|
||||
}
|
||||
|
||||
export type ILogLevel = 'info' | 'debug' | 'warn' | 'error' | 'verbose';
|
||||
|
|
|
@ -4,37 +4,106 @@
|
|||
@mousedown="onDragStart"
|
||||
>
|
||||
<slot :isDragging="isDragging"></slot>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
ref="draggable"
|
||||
:class="$style.draggable"
|
||||
:style="draggableStyle"
|
||||
v-show="isDragging"
|
||||
>
|
||||
<slot name="preview" :canDrop="canDrop"></slot>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { XYPosition } from '@/Interface';
|
||||
import Vue from 'vue';
|
||||
|
||||
// @ts-ignore
|
||||
import Teleport from 'vue2-teleport';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
Teleport,
|
||||
},
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
},
|
||||
data: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDragging: false,
|
||||
draggablePosition: {
|
||||
x: -100,
|
||||
y: -100,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
draggableStyle(): { top: string; left: string; } {
|
||||
return {
|
||||
top: `${this.draggablePosition.y}px`,
|
||||
left: `${this.draggablePosition.x}px`,
|
||||
};
|
||||
},
|
||||
canDrop(): boolean {
|
||||
return this.$store.getters['ui/canDraggableDrop'];
|
||||
},
|
||||
stickyPosition(): XYPosition | null {
|
||||
return this.$store.getters['ui/draggableStickyPos'];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onDragStart(e: MouseEvent) {
|
||||
onDragStart(e: DragEvent) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.isDragging = true;
|
||||
this.$store.commit('ui/draggableStartDragging', {type: this.type, data: this.data || ''});
|
||||
|
||||
this.$emit('dragstart');
|
||||
document.body.style.cursor = 'grabbing';
|
||||
|
||||
window.addEventListener('mousemove', this.onDrag);
|
||||
window.addEventListener('mouseup', this.onDragEnd);
|
||||
|
||||
this.draggablePosition = { x: e.pageX, y: e.pageY };
|
||||
},
|
||||
onDrag(e: MouseEvent) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.$emit('drag', {x: e.pageX, y: e.pageY});
|
||||
if (this.canDrop && this.stickyPosition) {
|
||||
this.draggablePosition = { x: this.stickyPosition[0], y: this.stickyPosition[1]};
|
||||
}
|
||||
else {
|
||||
this.draggablePosition = { x: e.pageX, y: e.pageY };
|
||||
}
|
||||
|
||||
this.$emit('drag', this.draggablePosition);
|
||||
},
|
||||
onDragEnd(e: MouseEvent) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
@ -45,6 +114,7 @@ export default Vue.extend({
|
|||
setTimeout(() => {
|
||||
this.$emit('dragend');
|
||||
this.isDragging = false;
|
||||
this.$store.commit('ui/draggableStopDragging');
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
|
@ -56,4 +126,14 @@ export default Vue.extend({
|
|||
visibility: visible;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.draggable {
|
||||
position: fixed;
|
||||
z-index: 9999999;
|
||||
}
|
||||
|
||||
.draggable-data-transfer {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
</style>
|
||||
|
|
80
packages/editor-ui/src/components/DraggableTarget.vue
Normal file
80
packages/editor-ui/src/components/DraggableTarget.vue
Normal file
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<div ref="target">
|
||||
<slot :droppable="droppable" :activeDrop="activeDrop"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
sticky: {
|
||||
type: Boolean,
|
||||
},
|
||||
stickyOffset: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hovering: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('mousemove', this.onMouseMove);
|
||||
window.addEventListener('mouseup', this.onMouseUp);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('mousemove', this.onMouseMove);
|
||||
window.removeEventListener('mouseup', this.onMouseUp);
|
||||
},
|
||||
computed: {
|
||||
isDragging(): boolean {
|
||||
return this.$store.getters['ui/isDraggableDragging'];
|
||||
},
|
||||
draggableType(): string {
|
||||
return this.$store.getters['ui/draggableType'];
|
||||
},
|
||||
droppable(): boolean {
|
||||
return !this.disabled && this.isDragging && this.draggableType === this.type;
|
||||
},
|
||||
activeDrop(): boolean {
|
||||
return this.droppable && this.hovering;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onMouseMove(e: MouseEvent) {
|
||||
const target = this.$refs.target as HTMLElement;
|
||||
|
||||
if (target) {
|
||||
const dim = target.getBoundingClientRect();
|
||||
|
||||
this.hovering = e.clientX >= dim.left && e.clientX <= dim.right && e.clientY >= dim.top && e.clientY <= dim.bottom;
|
||||
|
||||
if (this.sticky && this.hovering) {
|
||||
this.$store.commit('ui/setDraggableStickyPos', [dim.left + this.stickyOffset, dim.top + this.stickyOffset]);
|
||||
}
|
||||
}
|
||||
},
|
||||
onMouseUp(e: MouseEvent) {
|
||||
if (this.activeDrop) {
|
||||
const data = this.$store.getters['ui/draggableData'];
|
||||
this.$emit('drop', data);
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeDrop(active) {
|
||||
this.$store.commit('ui/setDraggableCanDrop', active);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -50,8 +50,7 @@ import { externalHooks } from '@/components/mixins/externalHooks';
|
|||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
const MAPPING_PARAMS = [`$evaluateExpression`, `$item`, `$jmespath`, `$node`, `$binary`, `$data`, `$env`, `$json`, `$now`, `$parameters`, `$position`, `$resumeWebhookUrl`, `$runIndex`, `$today`, `$workflow`];
|
||||
import { hasExpressionMapping } from './helpers';
|
||||
|
||||
export default mixins(
|
||||
externalHooks,
|
||||
|
@ -92,9 +91,11 @@ export default mixins(
|
|||
},
|
||||
|
||||
closeDialog () {
|
||||
// Handle the close externally as the visible parameter is an external prop
|
||||
// and is so not allowed to be changed here.
|
||||
this.$emit('valueChanged', this.latestValue);
|
||||
if (this.latestValue !== this.value) {
|
||||
// Handle the close externally as the visible parameter is an external prop
|
||||
// and is so not allowed to be changed here.
|
||||
this.$emit('valueChanged', this.latestValue);
|
||||
}
|
||||
this.$emit('closeDialog');
|
||||
return false;
|
||||
},
|
||||
|
@ -172,7 +173,7 @@ export default mixins(
|
|||
source: this.eventSource,
|
||||
session_id: this.$store.getters['ui/ndvSessionId'],
|
||||
has_parameter: this.value.includes('$parameter'),
|
||||
has_mapping: !!MAPPING_PARAMS.find((param) => this.value.includes(param)),
|
||||
has_mapping: hasExpressionMapping(this.value),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -354,7 +354,7 @@ export default mixins(
|
|||
|
||||
.ql-disabled .ql-editor {
|
||||
border-width: 1px;
|
||||
border: 1px dashed $--custom-expression-text;
|
||||
border: 1px solid $--custom-expression-text;
|
||||
color: $--custom-expression-text;
|
||||
background-color: $--custom-expression-background;
|
||||
cursor: not-allowed;
|
||||
|
|
|
@ -10,73 +10,72 @@
|
|||
class="fixed-collection-parameter-property"
|
||||
>
|
||||
<n8n-input-label
|
||||
:label="property.displayName === '' || parameter.options.length === 1 ? '' : $locale.nodeText().inputLabelDisplayName(property, path)"
|
||||
v-if="property.displayName !== '' && (parameter.options && parameter.options.length !== 1)"
|
||||
:label="$locale.nodeText().inputLabelDisplayName(property, path)"
|
||||
:underline="true"
|
||||
:labelHoverableOnly="true"
|
||||
size="small"
|
||||
>
|
||||
<div v-if="multipleValues === true">
|
||||
<div
|
||||
v-for="(value, index) in values[property.name]"
|
||||
:key="property.name + index"
|
||||
class="parameter-item"
|
||||
>
|
||||
<div class="parameter-item-wrapper">
|
||||
<div class="delete-option" v-if="!isReadOnly">
|
||||
<font-awesome-icon
|
||||
icon="trash"
|
||||
class="reset-icon clickable"
|
||||
:title="$locale.baseText('fixedCollectionParameter.deleteItem')"
|
||||
@click="deleteOption(property.name, index)"
|
||||
/>
|
||||
<div v-if="sortable" class="sort-icon">
|
||||
<font-awesome-icon
|
||||
v-if="index !== 0"
|
||||
icon="angle-up"
|
||||
class="clickable"
|
||||
:title="$locale.baseText('fixedCollectionParameter.moveUp')"
|
||||
@click="moveOptionUp(property.name, index)"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
v-if="index !== (values[property.name].length - 1)"
|
||||
icon="angle-down"
|
||||
class="clickable"
|
||||
:title="$locale.baseText('fixedCollectionParameter.moveDown')"
|
||||
@click="moveOptionDown(property.name, index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<parameter-input-list
|
||||
:parameters="property.values"
|
||||
:nodeValues="nodeValues"
|
||||
:path="getPropertyPath(property.name, index)"
|
||||
:hideDelete="true"
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="parameter-item">
|
||||
/>
|
||||
<div v-if="multipleValues === true">
|
||||
<div
|
||||
v-for="(value, index) in values[property.name]"
|
||||
:key="property.name + index"
|
||||
class="parameter-item"
|
||||
>
|
||||
<div class="parameter-item-wrapper">
|
||||
<div class="delete-option" v-if="!isReadOnly">
|
||||
<font-awesome-icon
|
||||
icon="trash"
|
||||
class="reset-icon clickable"
|
||||
:title="$locale.baseText('fixedCollectionParameter.deleteItem')"
|
||||
@click="deleteOption(property.name)"
|
||||
@click="deleteOption(property.name, index)"
|
||||
/>
|
||||
<div v-if="sortable" class="sort-icon">
|
||||
<font-awesome-icon
|
||||
v-if="index !== 0"
|
||||
icon="angle-up"
|
||||
class="clickable"
|
||||
:title="$locale.baseText('fixedCollectionParameter.moveUp')"
|
||||
@click="moveOptionUp(property.name, index)"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
v-if="index !== (values[property.name].length - 1)"
|
||||
icon="angle-down"
|
||||
class="clickable"
|
||||
:title="$locale.baseText('fixedCollectionParameter.moveDown')"
|
||||
@click="moveOptionDown(property.name, index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<parameter-input-list
|
||||
:parameters="property.values"
|
||||
:nodeValues="nodeValues"
|
||||
:path="getPropertyPath(property.name)"
|
||||
class="parameter-item"
|
||||
@valueChanged="valueChanged"
|
||||
:path="getPropertyPath(property.name, index)"
|
||||
:hideDelete="true"
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</n8n-input-label>
|
||||
</div>
|
||||
<div v-else class="parameter-item">
|
||||
<div class="parameter-item-wrapper">
|
||||
<div class="delete-option" v-if="!isReadOnly">
|
||||
<font-awesome-icon
|
||||
icon="trash"
|
||||
class="reset-icon clickable"
|
||||
:title="$locale.baseText('fixedCollectionParameter.deleteItem')"
|
||||
@click="deleteOption(property.name)"
|
||||
/>
|
||||
</div>
|
||||
<parameter-input-list
|
||||
:parameters="property.values"
|
||||
:nodeValues="nodeValues"
|
||||
:path="getPropertyPath(property.name)"
|
||||
class="parameter-item"
|
||||
@valueChanged="valueChanged"
|
||||
:hideDelete="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="parameterOptions.length > 0 && !isReadOnly">
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
:executingMessage="$locale.baseText('ndv.input.executingPrevious')"
|
||||
:sessionId="sessionId"
|
||||
:overrideOutputs="connectedCurrentNodeOutputs"
|
||||
:mappingEnabled="!readOnly"
|
||||
:showMappingHint="draggableHintShown"
|
||||
:distanceFromActive="currentNodeDepth"
|
||||
paneType="input"
|
||||
@linkRun="onLinkRun"
|
||||
@unlinkRun="onUnlinkRun"
|
||||
|
@ -32,8 +35,11 @@
|
|||
<template v-slot:node-not-run>
|
||||
<div :class="$style.noOutputData" v-if="parentNodes.length">
|
||||
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{ $locale.baseText('ndv.input.noOutputData.title') }}</n8n-text>
|
||||
<NodeExecuteButton v-if="!readOnly" type="outline" :transparent="true" :nodeName="currentNodeName" :label="$locale.baseText('ndv.input.noOutputData.executePrevious')" @execute="onNodeExecute" telemetrySource="inputs" />
|
||||
<n8n-text v-if="!readOnly" tag="div" size="small">
|
||||
<n8n-tooltip v-if="!readOnly" :manual="true" :value="showDraggableHint && showDraggableHintWithDelay">
|
||||
<div slot="content" v-html="$locale.baseText('dataMapping.dragFromPreviousHint', { interpolate: { name: focusedMappableInput } })"></div>
|
||||
<NodeExecuteButton type="outline" :transparent="true" :nodeName="currentNodeName" :label="$locale.baseText('ndv.input.noOutputData.executePrevious')" @execute="onNodeExecute" telemetrySource="inputs" />
|
||||
</n8n-tooltip>
|
||||
<n8n-text v-if="!readOnly" tag="div" size="small">
|
||||
{{ $locale.baseText('ndv.input.noOutputData.hint') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
|
@ -65,6 +71,7 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
|||
import mixins from 'vue-typed-mixins';
|
||||
import NodeExecuteButton from './NodeExecuteButton.vue';
|
||||
import WireMeUp from './WireMeUp.vue';
|
||||
import { CRON_NODE_TYPE, INTERVAL_NODE_TYPE, LOCAL_STORAGE_MAPPING_FLAG, START_NODE_TYPE } from '@/constants';
|
||||
|
||||
export default mixins(
|
||||
workflowHelpers,
|
||||
|
@ -93,7 +100,27 @@ export default mixins(
|
|||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDraggableHintWithDelay: false,
|
||||
draggableHintShown: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
focusedMappableInput(): string {
|
||||
return this.$store.getters['ui/focusedMappableInput'];
|
||||
},
|
||||
isUserOnboarded(): boolean {
|
||||
return window.localStorage.getItem(LOCAL_STORAGE_MAPPING_FLAG) === 'true';
|
||||
},
|
||||
showDraggableHint(): boolean {
|
||||
const toIgnore = [START_NODE_TYPE, CRON_NODE_TYPE, INTERVAL_NODE_TYPE];
|
||||
if (toIgnore.includes(this.currentNode.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!this.focusedMappableInput && !this.isUserOnboarded;
|
||||
},
|
||||
isExecutingPrevious(): boolean {
|
||||
if (!this.workflowRunning) {
|
||||
return false;
|
||||
|
@ -136,6 +163,10 @@ export default mixins(
|
|||
|
||||
return nodes.filter(({name}, i) => (this.activeNode && (name !== this.activeNode.name)) && nodes.findIndex((node) => node.name === name) === i);
|
||||
},
|
||||
currentNodeDepth (): number {
|
||||
const node = this.parentNodes.find((node) => node.name === this.currentNode.name);
|
||||
return node ? node.depth: -1;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onNodeExecute() {
|
||||
|
@ -182,6 +213,26 @@ export default mixins(
|
|||
return truncated;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
showDraggableHint(curr: boolean, prev: boolean) {
|
||||
if (curr && !prev) {
|
||||
setTimeout(() => {
|
||||
if (this.draggableHintShown) {
|
||||
return;
|
||||
}
|
||||
this.showDraggableHintWithDelay = this.showDraggableHint;
|
||||
if (this.showDraggableHintWithDelay) {
|
||||
this.draggableHintShown = true;
|
||||
|
||||
this.$telemetry.track('User viewed data mapping tooltip', { type: 'unexecuted input pane' });
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
else if (!curr) {
|
||||
this.showDraggableHintWithDelay = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -4,34 +4,31 @@
|
|||
:label="$locale.nodeText().inputLabelDisplayName(parameter, path)"
|
||||
:tooltipText="$locale.nodeText().inputLabelDescription(parameter, path)"
|
||||
:underline="true"
|
||||
:labelHoverableOnly="true"
|
||||
size="small"
|
||||
>
|
||||
/>
|
||||
|
||||
<div v-for="(value, index) in values" :key="index" class="duplicate-parameter-item" :class="parameter.type">
|
||||
<div class="delete-item clickable" v-if="!isReadOnly">
|
||||
<font-awesome-icon icon="trash" :title="$locale.baseText('multipleParameter.deleteItem')" @click="deleteItem(index)" />
|
||||
<div v-if="sortable">
|
||||
<font-awesome-icon v-if="index !== 0" icon="angle-up" class="clickable" :title="$locale.baseText('multipleParameter.moveUp')" @click="moveOptionUp(index)" />
|
||||
<font-awesome-icon v-if="index !== (values.length -1)" icon="angle-down" class="clickable" :title="$locale.baseText('multipleParameter.moveDown')" @click="moveOptionDown(index)" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="parameter.type === 'collection'">
|
||||
<collection-parameter :parameter="parameter" :values="value" :nodeValues="nodeValues" :path="getPath(index)" :hideDelete="hideDelete" @valueChanged="valueChanged" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<parameter-input class="duplicate-parameter-input-item" :parameter="parameter" :value="value" :displayOptions="true" :path="getPath(index)" @valueChanged="valueChanged" inputSize="small" :isReadOnly="isReadOnly" />
|
||||
<div v-for="(value, index) in values" :key="index" class="duplicate-parameter-item" :class="parameter.type">
|
||||
<div class="delete-item clickable" v-if="!isReadOnly">
|
||||
<font-awesome-icon icon="trash" :title="$locale.baseText('multipleParameter.deleteItem')" @click="deleteItem(index)" />
|
||||
<div v-if="sortable">
|
||||
<font-awesome-icon v-if="index !== 0" icon="angle-up" class="clickable" :title="$locale.baseText('multipleParameter.moveUp')" @click="moveOptionUp(index)" />
|
||||
<font-awesome-icon v-if="index !== (values.length -1)" icon="angle-down" class="clickable" :title="$locale.baseText('multipleParameter.moveDown')" @click="moveOptionDown(index)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="add-item-wrapper">
|
||||
<div v-if="values && Object.keys(values).length === 0 || isReadOnly" class="no-items-exist">
|
||||
<n8n-text size="small">{{ $locale.baseText('multipleParameter.currentlyNoItemsExist') }}</n8n-text>
|
||||
</div>
|
||||
<n8n-button v-if="!isReadOnly" type="tertiary" fullWidth @click="addItem()" :label="addButtonText" />
|
||||
<div v-if="parameter.type === 'collection'">
|
||||
<collection-parameter :parameter="parameter" :values="value" :nodeValues="nodeValues" :path="getPath(index)" :hideDelete="hideDelete" @valueChanged="valueChanged" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<parameter-input-full class="duplicate-parameter-input-item" :parameter="parameter" :value="value" :displayOptions="true" :hideLabel="true" :path="getPath(index)" @valueChanged="valueChanged" inputSize="small" :isReadOnly="isReadOnly" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</n8n-input-label>
|
||||
<div class="add-item-wrapper">
|
||||
<div v-if="values && Object.keys(values).length === 0 || isReadOnly" class="no-items-exist">
|
||||
<n8n-text size="small">{{ $locale.baseText('multipleParameter.currentlyNoItemsExist') }}</n8n-text>
|
||||
</div>
|
||||
<n8n-button v-if="!isReadOnly" type="tertiary" fullWidth @click="addItem()" :label="addButtonText" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -41,7 +38,7 @@ import {
|
|||
} from '@/Interface';
|
||||
|
||||
import CollectionParameter from '@/components/CollectionParameter.vue';
|
||||
import ParameterInput from '@/components/ParameterInput.vue';
|
||||
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||
|
||||
import { get } from 'lodash';
|
||||
|
||||
|
@ -54,7 +51,7 @@ export default mixins(genericHelpers)
|
|||
name: 'MultipleParameter',
|
||||
components: {
|
||||
CollectionParameter,
|
||||
ParameterInput,
|
||||
ParameterInputFull,
|
||||
},
|
||||
props: [
|
||||
'nodeValues', // NodeParameters
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<Draggable @drag="onDrag" @dragstart="onDragStart" @dragend="onDragEnd">
|
||||
<Draggable type="panel-resize" @drag="onDrag" @dragstart="onDragStart" @dragend="onDragEnd">
|
||||
<template v-slot="{ isDragging }">
|
||||
<div
|
||||
:class="{ [$style.dragButton]: true }"
|
||||
|
|
|
@ -1,220 +1,262 @@
|
|||
<template>
|
||||
<div @keydown.stop :class="parameterInputClasses">
|
||||
<expression-edit :dialogVisible="expressionEditDialogVisible" :value="value" :parameter="parameter" :path="path" :eventSource="eventSource || 'ndv'" @closeDialog="closeExpressionEditDialog" @valueChanged="expressionUpdated"></expression-edit>
|
||||
<div class="parameter-input ignore-key-press" :style="parameterInputWrapperStyle" @click="openExpressionEdit">
|
||||
|
||||
<n8n-input
|
||||
v-if="isValueExpression && showExpressionAsTextInput"
|
||||
:size="inputSize"
|
||||
:value="expressionDisplayValue"
|
||||
:disabled="isReadOnly"
|
||||
:title="displayTitle"
|
||||
@keydown.stop
|
||||
/>
|
||||
|
||||
<div v-else-if="['json', 'string'].includes(parameter.type) || remoteParameterOptionsLoadingIssues !== null">
|
||||
<code-edit v-if="codeEditDialogVisible" :value="value" :parameter="parameter" :type="editorType" :codeAutocomplete="codeAutocomplete" :path="path" @closeDialog="closeCodeEditDialog" @valueChanged="expressionUpdated"></code-edit>
|
||||
<text-edit :dialogVisible="textEditDialogVisible" :value="value" :parameter="parameter" :path="path" @closeDialog="closeTextEditDialog" @valueChanged="expressionUpdated"></text-edit>
|
||||
|
||||
<div v-if="isEditor === true" class="code-edit clickable" @click="displayEditDialog()">
|
||||
<prism-editor v-if="!codeEditDialogVisible" :lineNumbers="true" :readonly="true" :code="displayValue" language="js"></prism-editor>
|
||||
</div>
|
||||
|
||||
<expression-edit
|
||||
:dialogVisible="expressionEditDialogVisible"
|
||||
:value="value"
|
||||
:parameter="parameter"
|
||||
:path="path"
|
||||
:eventSource="eventSource || 'ndv'"
|
||||
@closeDialog="closeExpressionEditDialog"
|
||||
@valueChanged="expressionUpdated"
|
||||
></expression-edit>
|
||||
<div
|
||||
class="parameter-input ignore-key-press"
|
||||
:style="parameterInputWrapperStyle"
|
||||
@click="openExpressionEdit"
|
||||
>
|
||||
<n8n-input
|
||||
v-else
|
||||
v-model="tempValue"
|
||||
ref="inputField"
|
||||
v-if="isValueExpression || droppable || forceShowExpression"
|
||||
:size="inputSize"
|
||||
:type="getStringInputType"
|
||||
:rows="getArgument('rows')"
|
||||
:value="displayValue"
|
||||
:disabled="isReadOnly"
|
||||
@input="onTextInputChange"
|
||||
@change="valueChanged"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
:value="activeDrop || forceShowExpression? '': expressionDisplayValue"
|
||||
:title="displayTitle"
|
||||
:placeholder="isValueExpression ? '' : getPlaceholder()"
|
||||
@keydown.stop
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else-if="
|
||||
['json', 'string'].includes(parameter.type) ||
|
||||
remoteParameterOptionsLoadingIssues !== null
|
||||
"
|
||||
>
|
||||
<div slot="suffix" class="expand-input-icon-container">
|
||||
<font-awesome-icon v-if="!isValueExpression && !isReadOnly" icon="external-link-alt" class="edit-window-button clickable" :title="$locale.baseText('parameterInput.openEditWindow')" @click="displayEditDialog()" />
|
||||
</div>
|
||||
</n8n-input>
|
||||
</div>
|
||||
|
||||
<div v-else-if="parameter.type === 'color'" ref="inputField" class="color-input">
|
||||
<el-color-picker
|
||||
size="small"
|
||||
class="color-picker"
|
||||
:value="displayValue"
|
||||
:disabled="isReadOnly"
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
@change="valueChanged"
|
||||
:title="displayTitle"
|
||||
:show-alpha="getArgument('showAlpha')"
|
||||
/>
|
||||
<n8n-input
|
||||
v-model="tempValue"
|
||||
:size="inputSize"
|
||||
type="text"
|
||||
:value="tempValue"
|
||||
:disabled="isReadOnly"
|
||||
@change="valueChanged"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
:title="displayTitle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-date-picker
|
||||
v-else-if="parameter.type === 'dateTime'"
|
||||
v-model="tempValue"
|
||||
ref="inputField"
|
||||
type="datetime"
|
||||
:size="inputSize"
|
||||
:value="displayValue"
|
||||
:title="displayTitle"
|
||||
:disabled="isReadOnly"
|
||||
:placeholder="parameter.placeholder ? getPlaceholder() : $locale.baseText('parameterInput.selectDateAndTime')"
|
||||
:picker-options="dateTimePickerOptions"
|
||||
@change="valueChanged"
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
@keydown.stop
|
||||
/>
|
||||
|
||||
<n8n-input-number
|
||||
v-else-if="parameter.type === 'number'"
|
||||
ref="inputField" :size="inputSize"
|
||||
:value="displayValue"
|
||||
:controls="false"
|
||||
:max="getArgument('maxValue')"
|
||||
:min="getArgument('minValue')"
|
||||
:precision="getArgument('numberPrecision')"
|
||||
:disabled="isReadOnly"
|
||||
@change="valueChanged"
|
||||
@input="onTextInputChange"
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
@keydown.stop
|
||||
:title="displayTitle"
|
||||
:placeholder="parameter.placeholder"
|
||||
/>
|
||||
|
||||
<credentials-select
|
||||
v-else-if="parameter.type === 'credentialsSelect' || (parameter.name === 'genericAuthType')"
|
||||
ref="inputField"
|
||||
:parameter="parameter"
|
||||
:node="node"
|
||||
:activeCredentialType="activeCredentialType"
|
||||
:inputSize="inputSize"
|
||||
:displayValue="displayValue"
|
||||
:isReadOnly="isReadOnly"
|
||||
:displayTitle="displayTitle"
|
||||
@credentialSelected="credentialSelected"
|
||||
@valueChanged="valueChanged"
|
||||
@setFocus="setFocus"
|
||||
@onBlur="onBlur"
|
||||
>
|
||||
<template v-slot:issues-and-options>
|
||||
<parameter-issues
|
||||
:issues="getIssues"
|
||||
/>
|
||||
<parameter-options
|
||||
v-if="displayOptionsComputed"
|
||||
:displayOptionsComputed="displayOptionsComputed"
|
||||
<code-edit
|
||||
v-if="codeEditDialogVisible"
|
||||
:value="value"
|
||||
:parameter="parameter"
|
||||
:isValueExpression="isValueExpression"
|
||||
:isDefault="isDefault"
|
||||
:hasRemoteMethod="hasRemoteMethod"
|
||||
@optionSelected="optionSelected"
|
||||
/>
|
||||
</template>
|
||||
</credentials-select>
|
||||
:type="editorType"
|
||||
:codeAutocomplete="codeAutocomplete"
|
||||
:path="path"
|
||||
@closeDialog="closeCodeEditDialog"
|
||||
@valueChanged="expressionUpdated"
|
||||
></code-edit>
|
||||
<text-edit
|
||||
:dialogVisible="textEditDialogVisible"
|
||||
:value="value"
|
||||
:parameter="parameter"
|
||||
:path="path"
|
||||
@closeDialog="closeTextEditDialog"
|
||||
@valueChanged="expressionUpdated"
|
||||
></text-edit>
|
||||
|
||||
<n8n-select
|
||||
v-else-if="parameter.type === 'options'"
|
||||
ref="inputField"
|
||||
:size="inputSize"
|
||||
filterable
|
||||
:value="displayValue"
|
||||
:placeholder="parameter.placeholder ? getPlaceholder() : $locale.baseText('parameterInput.select')"
|
||||
:loading="remoteParameterOptionsLoading"
|
||||
:disabled="isReadOnly || remoteParameterOptionsLoading"
|
||||
:title="displayTitle"
|
||||
@change="valueChanged"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
>
|
||||
<n8n-option
|
||||
v-for="option in parameterOptions"
|
||||
:value="option.value"
|
||||
:key="option.value"
|
||||
:label="getOptionsOptionDisplayName(option)"
|
||||
>
|
||||
<div class="list-option">
|
||||
<div class="option-headline">
|
||||
{{ getOptionsOptionDisplayName(option) }}
|
||||
<div v-if="isEditor === true" class="code-edit clickable" @click="displayEditDialog()">
|
||||
<prism-editor
|
||||
v-if="!codeEditDialogVisible"
|
||||
:lineNumbers="true"
|
||||
:readonly="true"
|
||||
:code="displayValue"
|
||||
language="js"
|
||||
></prism-editor>
|
||||
</div>
|
||||
|
||||
<n8n-input
|
||||
v-else
|
||||
v-model="tempValue"
|
||||
ref="inputField"
|
||||
:size="inputSize"
|
||||
:type="getStringInputType"
|
||||
:rows="getArgument('rows')"
|
||||
:value="displayValue"
|
||||
:disabled="isReadOnly"
|
||||
@input="onTextInputChange"
|
||||
@change="valueChanged"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
:title="displayTitle"
|
||||
:placeholder="getPlaceholder()"
|
||||
>
|
||||
<div slot="suffix" class="expand-input-icon-container">
|
||||
<font-awesome-icon
|
||||
v-if="!isReadOnly"
|
||||
icon="external-link-alt"
|
||||
class="edit-window-button clickable"
|
||||
:title="$locale.baseText('parameterInput.openEditWindow')"
|
||||
@click="displayEditDialog()"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="option.description" class="option-description" v-html="getOptionsOptionDescription(option)"></div>
|
||||
</div>
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
</n8n-input>
|
||||
</div>
|
||||
|
||||
<n8n-select
|
||||
v-else-if="parameter.type === 'multiOptions'"
|
||||
ref="inputField"
|
||||
:size="inputSize"
|
||||
filterable
|
||||
multiple
|
||||
:value="displayValue"
|
||||
:loading="remoteParameterOptionsLoading"
|
||||
:disabled="isReadOnly || remoteParameterOptionsLoading"
|
||||
:title="displayTitle"
|
||||
:placeholder="$locale.baseText('parameterInput.select')"
|
||||
@change="valueChanged"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
>
|
||||
<n8n-option v-for="option in parameterOptions" :value="option.value" :key="option.value" :label="getOptionsOptionDisplayName(option)">
|
||||
<div class="list-option">
|
||||
<div class="option-headline">{{ getOptionsOptionDisplayName(option) }}</div>
|
||||
<div v-if="option.description" class="option-description" v-html="getOptionsOptionDescription(option)"></div>
|
||||
</div>
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
<div v-else-if="parameter.type === 'color'" ref="inputField" class="color-input">
|
||||
<el-color-picker
|
||||
size="small"
|
||||
class="color-picker"
|
||||
:value="displayValue"
|
||||
:disabled="isReadOnly"
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
@change="valueChanged"
|
||||
:title="displayTitle"
|
||||
:show-alpha="getArgument('showAlpha')"
|
||||
/>
|
||||
<n8n-input
|
||||
v-model="tempValue"
|
||||
:size="inputSize"
|
||||
type="text"
|
||||
:value="tempValue"
|
||||
:disabled="isReadOnly"
|
||||
@change="valueChanged"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
:title="displayTitle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-switch
|
||||
v-else-if="parameter.type === 'boolean'"
|
||||
class="switch-input"
|
||||
ref="inputField"
|
||||
active-color="#13ce66"
|
||||
:value="displayValue"
|
||||
:disabled="isReadOnly"
|
||||
@change="valueChanged"
|
||||
/>
|
||||
</div>
|
||||
<el-date-picker
|
||||
v-else-if="parameter.type === 'dateTime'"
|
||||
v-model="tempValue"
|
||||
ref="inputField"
|
||||
type="datetime"
|
||||
:size="inputSize"
|
||||
:value="displayValue"
|
||||
:title="displayTitle"
|
||||
:disabled="isReadOnly"
|
||||
:placeholder="
|
||||
parameter.placeholder
|
||||
? getPlaceholder()
|
||||
: $locale.baseText('parameterInput.selectDateAndTime')
|
||||
"
|
||||
:picker-options="dateTimePickerOptions"
|
||||
@change="valueChanged"
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
@keydown.stop
|
||||
/>
|
||||
|
||||
<parameter-issues
|
||||
v-if="parameter.type !== 'credentialsSelect'"
|
||||
:issues="getIssues"
|
||||
/>
|
||||
<n8n-input-number
|
||||
v-else-if="parameter.type === 'number'"
|
||||
ref="inputField"
|
||||
:size="inputSize"
|
||||
:value="displayValue"
|
||||
:controls="false"
|
||||
:max="getArgument('maxValue')"
|
||||
:min="getArgument('minValue')"
|
||||
:precision="getArgument('numberPrecision')"
|
||||
:disabled="isReadOnly"
|
||||
@change="valueChanged"
|
||||
@input="onTextInputChange"
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
@keydown.stop
|
||||
:title="displayTitle"
|
||||
:placeholder="parameter.placeholder"
|
||||
/>
|
||||
|
||||
<parameter-options
|
||||
v-if="displayOptionsComputed && parameter.type !== 'credentialsSelect'"
|
||||
:displayOptionsComputed="displayOptionsComputed"
|
||||
:parameter="parameter"
|
||||
:isValueExpression="isValueExpression"
|
||||
:isDefault="isDefault"
|
||||
:hasRemoteMethod="hasRemoteMethod"
|
||||
@optionSelected="optionSelected"
|
||||
/>
|
||||
<credentials-select
|
||||
v-else-if="
|
||||
parameter.type === 'credentialsSelect' || parameter.name === 'genericAuthType'
|
||||
"
|
||||
ref="inputField"
|
||||
:parameter="parameter"
|
||||
:node="node"
|
||||
:activeCredentialType="activeCredentialType"
|
||||
:inputSize="inputSize"
|
||||
:displayValue="displayValue"
|
||||
:isReadOnly="isReadOnly"
|
||||
:displayTitle="displayTitle"
|
||||
@credentialSelected="credentialSelected"
|
||||
@valueChanged="valueChanged"
|
||||
@setFocus="setFocus"
|
||||
@onBlur="onBlur"
|
||||
>
|
||||
<template v-slot:issues-and-options>
|
||||
<parameter-issues :issues="getIssues" />
|
||||
</template>
|
||||
</credentials-select>
|
||||
|
||||
<n8n-select
|
||||
v-else-if="parameter.type === 'options'"
|
||||
ref="inputField"
|
||||
:size="inputSize"
|
||||
filterable
|
||||
:value="displayValue"
|
||||
:placeholder="
|
||||
parameter.placeholder ? getPlaceholder() : $locale.baseText('parameterInput.select')
|
||||
"
|
||||
:loading="remoteParameterOptionsLoading"
|
||||
:disabled="isReadOnly || remoteParameterOptionsLoading"
|
||||
:title="displayTitle"
|
||||
@change="valueChanged"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
>
|
||||
<n8n-option
|
||||
v-for="option in parameterOptions"
|
||||
:value="option.value"
|
||||
:key="option.value"
|
||||
:label="getOptionsOptionDisplayName(option)"
|
||||
>
|
||||
<div class="list-option">
|
||||
<div class="option-headline">
|
||||
{{ getOptionsOptionDisplayName(option) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="option.description"
|
||||
class="option-description"
|
||||
v-html="getOptionsOptionDescription(option)"
|
||||
></div>
|
||||
</div>
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
|
||||
<n8n-select
|
||||
v-else-if="parameter.type === 'multiOptions'"
|
||||
ref="inputField"
|
||||
:size="inputSize"
|
||||
filterable
|
||||
multiple
|
||||
:value="displayValue"
|
||||
:loading="remoteParameterOptionsLoading"
|
||||
:disabled="isReadOnly || remoteParameterOptionsLoading"
|
||||
:title="displayTitle"
|
||||
:placeholder="$locale.baseText('parameterInput.select')"
|
||||
@change="valueChanged"
|
||||
@keydown.stop
|
||||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
>
|
||||
<n8n-option
|
||||
v-for="option in parameterOptions"
|
||||
:value="option.value"
|
||||
:key="option.value"
|
||||
:label="getOptionsOptionDisplayName(option)"
|
||||
>
|
||||
<div class="list-option">
|
||||
<div class="option-headline">{{ getOptionsOptionDisplayName(option) }}</div>
|
||||
<div
|
||||
v-if="option.description"
|
||||
class="option-description"
|
||||
v-html="getOptionsOptionDescription(option)"
|
||||
></div>
|
||||
</div>
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
|
||||
<el-switch
|
||||
v-else-if="parameter.type === 'boolean'"
|
||||
class="switch-input"
|
||||
ref="inputField"
|
||||
active-color="#13ce66"
|
||||
:value="displayValue"
|
||||
:disabled="isReadOnly"
|
||||
@change="valueChanged"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<parameter-issues v-if="parameter.type !== 'credentialsSelect'" :issues="getIssues" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -253,6 +295,7 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
|||
import mixins from 'vue-typed-mixins';
|
||||
import { CUSTOM_API_CALL_KEY } from '@/constants';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { hasExpressionMapping } from './helpers';
|
||||
|
||||
export default mixins(
|
||||
externalHooks,
|
||||
|
@ -274,7 +317,6 @@ export default mixins(
|
|||
TextEdit,
|
||||
},
|
||||
props: [
|
||||
'displayOptions', // boolean
|
||||
'inputSize',
|
||||
'isReadOnly',
|
||||
'documentationUrl',
|
||||
|
@ -285,6 +327,9 @@ export default mixins(
|
|||
'errorHighlight',
|
||||
'isForCredential', // boolean
|
||||
'eventSource', // string
|
||||
'activeDrop',
|
||||
'droppable',
|
||||
'forceShowExpression',
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
@ -352,11 +397,6 @@ export default mixins(
|
|||
codeAutocomplete (): string | undefined {
|
||||
return this.getArgument('codeAutocomplete') as string | undefined;
|
||||
},
|
||||
showExpressionAsTextInput(): boolean {
|
||||
const types = ['number', 'boolean', 'dateTime', 'options', 'multiOptions'];
|
||||
|
||||
return types.includes(this.parameter.type);
|
||||
},
|
||||
dependentParametersValues (): string | null {
|
||||
const loadOptionsDependsOn = this.getArgument('loadOptionsDependsOn') as string[] | undefined;
|
||||
|
||||
|
@ -462,20 +502,6 @@ export default mixins(
|
|||
|
||||
return value;
|
||||
},
|
||||
displayOptionsComputed (): boolean {
|
||||
if (this.isReadOnly === true) {
|
||||
return false;
|
||||
}
|
||||
if (this.parameter.type === 'collection') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.displayOptions === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
expressionValueComputed (): NodeParameterValue | string[] | null {
|
||||
if (this.areExpressionsDisabled) {
|
||||
return this.value;
|
||||
|
@ -513,6 +539,10 @@ export default mixins(
|
|||
return 'textarea';
|
||||
}
|
||||
|
||||
if (this.parameter.type === 'code') {
|
||||
return 'textarea';
|
||||
}
|
||||
|
||||
return 'text';
|
||||
},
|
||||
getIssues (): string[] {
|
||||
|
@ -581,9 +611,6 @@ export default mixins(
|
|||
|
||||
return [];
|
||||
},
|
||||
isDefault (): boolean {
|
||||
return this.parameter.default === this.value;
|
||||
},
|
||||
isEditor (): boolean {
|
||||
return ['code', 'json'].includes(this.editorType);
|
||||
},
|
||||
|
@ -609,20 +636,26 @@ export default mixins(
|
|||
return this.remoteParameterOptions;
|
||||
},
|
||||
parameterInputClasses () {
|
||||
const classes = [];
|
||||
const classes: {[c: string]: boolean} = {
|
||||
droppable: this.droppable,
|
||||
activeDrop: this.activeDrop,
|
||||
};
|
||||
|
||||
const rows = this.getArgument('rows');
|
||||
const isTextarea = this.parameter.type === 'string' && rows !== undefined;
|
||||
const isSwitch = this.parameter.type === 'boolean' && !this.isValueExpression;
|
||||
|
||||
if (!isTextarea && !isSwitch) {
|
||||
classes.push('parameter-value-container');
|
||||
classes['parameter-value-container'] = true;
|
||||
}
|
||||
if (this.isValueExpression) {
|
||||
classes.push('expression');
|
||||
|
||||
if (this.isValueExpression || this.forceShowExpression) {
|
||||
classes['expression'] = true;
|
||||
}
|
||||
if (this.getIssues.length || this.errorHighlight) {
|
||||
classes.push('has-issues');
|
||||
if (!this.droppable && !this.activeDrop && (this.getIssues.length || this.errorHighlight)) {
|
||||
classes['has-issues'] = true;
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
parameterInputWrapperStyle () {
|
||||
|
@ -633,9 +666,6 @@ export default mixins(
|
|||
if (this.parameter.type === 'credentialsSelect') {
|
||||
return styles;
|
||||
}
|
||||
if (this.displayOptionsComputed === true) {
|
||||
deductWidth += 25;
|
||||
}
|
||||
if (this.getIssues.length) {
|
||||
deductWidth += 20;
|
||||
}
|
||||
|
@ -866,8 +896,12 @@ export default mixins(
|
|||
}
|
||||
},
|
||||
optionSelected (command: string) {
|
||||
const prevValue = this.value;
|
||||
|
||||
if (command === 'resetValue') {
|
||||
this.valueChanged(this.parameter.default);
|
||||
} else if (command === 'openExpression') {
|
||||
this.expressionEditDialogVisible = true;
|
||||
} else if (command === 'addExpression') {
|
||||
if (this.parameter.type === 'number' || this.parameter.type === 'boolean') {
|
||||
this.valueChanged(`={{${this.value}}}`);
|
||||
|
@ -876,8 +910,10 @@ export default mixins(
|
|||
this.valueChanged(`=${this.value}`);
|
||||
}
|
||||
|
||||
this.expressionEditDialogVisible = true;
|
||||
this.trackExpressionEditOpen();
|
||||
setTimeout(() => {
|
||||
this.expressionEditDialogVisible = true;
|
||||
this.trackExpressionEditOpen();
|
||||
}, 375);
|
||||
} else if (command === 'removeExpression') {
|
||||
let value = this.expressionValueComputed;
|
||||
|
||||
|
@ -890,9 +926,23 @@ export default mixins(
|
|||
} else if (command === 'refreshOptions') {
|
||||
this.loadRemoteParameterOptions();
|
||||
}
|
||||
|
||||
if (this.node && (command === 'addExpression' || command === 'removeExpression')) {
|
||||
this.$telemetry.track('User switched parameter mode', {
|
||||
node_type: this.node.type,
|
||||
parameter: this.path,
|
||||
old_mode: command === 'addExpression' ? 'fixed': 'expression',
|
||||
new_mode: command === 'removeExpression' ? 'fixed': 'expression',
|
||||
was_parameter_empty: prevValue === '' || prevValue === undefined,
|
||||
had_mapping: hasExpressionMapping(prevValue),
|
||||
had_parameter: typeof prevValue === 'string' && prevValue.includes('$parameter'),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$on('optionSelected', this.optionSelected);
|
||||
|
||||
this.tempValue = this.displayValue as string;
|
||||
if (this.node !== null) {
|
||||
this.nodeName = this.node.name;
|
||||
|
@ -986,18 +1036,30 @@ export default mixins(
|
|||
}
|
||||
|
||||
.expression {
|
||||
textarea[disabled], input[disabled] {
|
||||
textarea, input {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.el-switch__core {
|
||||
border: 1px dashed $--custom-expression-text;
|
||||
}
|
||||
--input-border-color: var(--color-secondary-tint-1);
|
||||
--input-background-color: var(--color-secondary-tint-2);
|
||||
--input-font-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
--input-border-color: #{$--custom-expression-text};
|
||||
|
||||
.droppable {
|
||||
--input-border-color: var(--color-secondary-tint-1);
|
||||
--input-background-color: var(--color-secondary-tint-2);
|
||||
--input-border-style: dashed;
|
||||
--input-background-color: #{$--custom-expression-background};
|
||||
--disabled-border: #{$--custom-expression-text};
|
||||
}
|
||||
|
||||
.activeDrop {
|
||||
--input-border-color: var(--color-success);
|
||||
--input-background-color: var(--color-success-tint-2);
|
||||
--input-border-style: solid;
|
||||
|
||||
textarea, input {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
}
|
||||
|
||||
.has-issues {
|
||||
|
|
|
@ -4,38 +4,53 @@
|
|||
:tooltipText="$locale.credText().inputLabelDescription(parameter)"
|
||||
:required="parameter.required"
|
||||
:showTooltip="focused"
|
||||
:showOptions="menuExpanded"
|
||||
>
|
||||
<parameter-input
|
||||
:parameter="parameter"
|
||||
:value="value"
|
||||
:path="parameter.name"
|
||||
:hideIssues="true"
|
||||
:displayOptions="true"
|
||||
:documentationUrl="documentationUrl"
|
||||
:errorHighlight="showRequiredErrors"
|
||||
:isForCredential="true"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@textInput="valueChanged"
|
||||
@valueChanged="valueChanged"
|
||||
inputSize="large"
|
||||
:eventSource="eventSource"
|
||||
/>
|
||||
<div :class="$style.errors" v-if="showRequiredErrors">
|
||||
<n8n-text color="danger" size="small">
|
||||
{{ $locale.baseText('parameterInputExpanded.thisFieldIsRequired') }}
|
||||
<n8n-link v-if="documentationUrl" :to="documentationUrl" size="small" :underline="true" @click="onDocumentationUrlClick">
|
||||
{{ $locale.baseText('parameterInputExpanded.openDocs') }}
|
||||
</n8n-link>
|
||||
</n8n-text>
|
||||
</div>
|
||||
<input-hint :class="$style.hint" :hint="$locale.credText().hint(parameter)" />
|
||||
<template #options>
|
||||
<parameter-options
|
||||
:parameter="parameter"
|
||||
:value="value"
|
||||
:isReadOnly="false"
|
||||
:showOptions="true"
|
||||
@optionSelected="optionSelected"
|
||||
@menu-expanded="onMenuExpanded"
|
||||
/>
|
||||
</template>
|
||||
<template>
|
||||
<parameter-input
|
||||
ref="param"
|
||||
inputSize="large"
|
||||
:parameter="parameter"
|
||||
:value="value"
|
||||
:path="parameter.name"
|
||||
:hideIssues="true"
|
||||
:displayOptions="true"
|
||||
:documentationUrl="documentationUrl"
|
||||
:errorHighlight="showRequiredErrors"
|
||||
:isForCredential="true"
|
||||
:eventSource="eventSource"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@textInput="valueChanged"
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
<div :class="$style.errors" v-if="showRequiredErrors">
|
||||
<n8n-text color="danger" size="small">
|
||||
{{ $locale.baseText('parameterInputExpanded.thisFieldIsRequired') }}
|
||||
<n8n-link v-if="documentationUrl" :to="documentationUrl" size="small" :underline="true" @click="onDocumentationUrlClick">
|
||||
{{ $locale.baseText('parameterInputExpanded.openDocs') }}
|
||||
</n8n-link>
|
||||
</n8n-text>
|
||||
</div>
|
||||
<input-hint :class="$style.hint" :hint="$locale.credText().hint(parameter)" />
|
||||
</template>
|
||||
</n8n-input-label>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { IUpdateInformation } from '@/Interface';
|
||||
import ParameterInput from './ParameterInput.vue';
|
||||
import ParameterOptions from './ParameterOptions.vue';
|
||||
import InputHint from './ParameterInputHint.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
|
@ -44,6 +59,7 @@ export default Vue.extend({
|
|||
components: {
|
||||
ParameterInput,
|
||||
InputHint,
|
||||
ParameterOptions,
|
||||
},
|
||||
props: {
|
||||
parameter: {
|
||||
|
@ -64,6 +80,7 @@ export default Vue.extend({
|
|||
return {
|
||||
focused: false,
|
||||
blurredEver: false,
|
||||
menuExpanded: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -93,6 +110,14 @@ export default Vue.extend({
|
|||
this.blurredEver = true;
|
||||
this.focused = false;
|
||||
},
|
||||
onMenuExpanded(expanded: boolean) {
|
||||
this.menuExpanded = expanded;
|
||||
},
|
||||
optionSelected (command: string) {
|
||||
if (this.$refs.param) {
|
||||
(this.$refs.param as Vue).$emit('optionSelected', command);
|
||||
}
|
||||
},
|
||||
valueChanged(parameterData: IUpdateInformation) {
|
||||
this.$emit('change', parameterData);
|
||||
},
|
||||
|
|
|
@ -1,22 +1,43 @@
|
|||
<template>
|
||||
<n8n-input-label
|
||||
:label="$locale.nodeText().inputLabelDisplayName(parameter, path)"
|
||||
:tooltipText="$locale.nodeText().inputLabelDescription(parameter, path)"
|
||||
:label="hideLabel? '': $locale.nodeText().inputLabelDisplayName(parameter, path)"
|
||||
:tooltipText="hideLabel? '': $locale.nodeText().inputLabelDescription(parameter, path)"
|
||||
:showTooltip="focused"
|
||||
:showOptions="menuExpanded || focused || forceShowExpression"
|
||||
:bold="false"
|
||||
size="small"
|
||||
>
|
||||
<parameter-input
|
||||
:parameter="parameter"
|
||||
:value="value"
|
||||
:displayOptions="displayOptions"
|
||||
:path="path"
|
||||
:isReadOnly="isReadOnly"
|
||||
@valueChanged="valueChanged"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
inputSize="small" />
|
||||
<input-hint :class="$style.hint" :hint="$locale.nodeText().hint(parameter, path)" />
|
||||
<template #options>
|
||||
<parameter-options
|
||||
:parameter="parameter"
|
||||
:value="value"
|
||||
:isReadOnly="isReadOnly"
|
||||
:showOptions="displayOptions"
|
||||
@optionSelected="optionSelected"
|
||||
@menu-expanded="onMenuExpanded"
|
||||
/>
|
||||
</template>
|
||||
<template>
|
||||
<DraggableTarget type="mapping" :disabled="parameter.noDataExpression || isReadOnly" :sticky="true" :stickyOffset="4" @drop="onDrop">
|
||||
<template v-slot="{ droppable, activeDrop }">
|
||||
<parameter-input
|
||||
ref="param"
|
||||
:parameter="parameter"
|
||||
:value="value"
|
||||
:displayOptions="displayOptions"
|
||||
:path="path"
|
||||
:isReadOnly="isReadOnly"
|
||||
:droppable="droppable"
|
||||
:activeDrop="activeDrop"
|
||||
:forceShowExpression="forceShowExpression"
|
||||
@valueChanged="valueChanged"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
inputSize="small" />
|
||||
</template>
|
||||
</DraggableTarget>
|
||||
<input-hint :class="$style.hint" :hint="$locale.nodeText().hint(parameter, path)" />
|
||||
</template>
|
||||
</n8n-input-label>
|
||||
</template>
|
||||
|
||||
|
@ -24,22 +45,35 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import {
|
||||
INodeUi,
|
||||
IUpdateInformation,
|
||||
} from '@/Interface';
|
||||
|
||||
import ParameterInput from '@/components/ParameterInput.vue';
|
||||
import InputHint from './ParameterInputHint.vue';
|
||||
import ParameterOptions from './ParameterOptions.vue';
|
||||
import DraggableTarget from '@/components/DraggableTarget.vue';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { showMessage } from './mixins/showMessage';
|
||||
import { LOCAL_STORAGE_MAPPING_FLAG } from '@/constants';
|
||||
import { hasExpressionMapping } from './helpers';
|
||||
|
||||
export default Vue
|
||||
export default mixins(
|
||||
showMessage,
|
||||
)
|
||||
.extend({
|
||||
name: 'ParameterInputFull',
|
||||
components: {
|
||||
ParameterInput,
|
||||
InputHint,
|
||||
ParameterOptions,
|
||||
DraggableTarget,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
focused: false,
|
||||
menuExpanded: false,
|
||||
forceShowExpression: false,
|
||||
};
|
||||
},
|
||||
props: [
|
||||
|
@ -48,22 +82,80 @@ export default Vue
|
|||
'parameter',
|
||||
'path',
|
||||
'value',
|
||||
'hideLabel',
|
||||
],
|
||||
computed: {
|
||||
node (): INodeUi | null {
|
||||
return this.$store.getters.activeNode;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getArgument (argumentName: string): string | number | boolean | undefined {
|
||||
if (this.parameter.typeOptions === undefined) {
|
||||
return undefined;
|
||||
onFocus() {
|
||||
this.focused = true;
|
||||
if (!this.parameter.noDataExpression) {
|
||||
this.$store.commit('ui/setMappableNDVInputFocus', this.parameter.displayName);
|
||||
}
|
||||
|
||||
if (this.parameter.typeOptions[argumentName] === undefined) {
|
||||
return undefined;
|
||||
},
|
||||
onBlur() {
|
||||
this.focused = false;
|
||||
if (!this.parameter.noDataExpression) {
|
||||
this.$store.commit('ui/setMappableNDVInputFocus', '');
|
||||
}
|
||||
},
|
||||
onMenuExpanded(expanded: boolean) {
|
||||
this.menuExpanded = expanded;
|
||||
},
|
||||
optionSelected (command: string) {
|
||||
if (this.$refs.param) {
|
||||
(this.$refs.param as Vue).$emit('optionSelected', command);
|
||||
}
|
||||
|
||||
return this.parameter.typeOptions[argumentName];
|
||||
},
|
||||
valueChanged (parameterData: IUpdateInformation) {
|
||||
this.$emit('valueChanged', parameterData);
|
||||
},
|
||||
onDrop(data: string) {
|
||||
this.forceShowExpression = true;
|
||||
setTimeout(() => {
|
||||
if (this.node) {
|
||||
const prevValue = this.value;
|
||||
let updatedValue: string;
|
||||
if (typeof prevValue === 'string' && prevValue.startsWith('=') && prevValue.length > 1) {
|
||||
updatedValue = `${prevValue} ${data}`;
|
||||
}
|
||||
else {
|
||||
updatedValue = `=${data}`;
|
||||
}
|
||||
|
||||
const parameterData = {
|
||||
node: this.node.name,
|
||||
name: this.path,
|
||||
value: updatedValue,
|
||||
};
|
||||
|
||||
this.$emit('valueChanged', parameterData);
|
||||
|
||||
if (window.localStorage.getItem(LOCAL_STORAGE_MAPPING_FLAG) !== 'true') {
|
||||
this.$showMessage({
|
||||
title: this.$locale.baseText('dataMapping.success.title'),
|
||||
message: this.$locale.baseText('dataMapping.success.moreInfo'),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
window.localStorage.setItem(LOCAL_STORAGE_MAPPING_FLAG, 'true');
|
||||
}
|
||||
|
||||
this.$store.commit('ui/setMappingTelemetry', {
|
||||
dest_node_type: this.node.type,
|
||||
dest_parameter: this.path,
|
||||
dest_parameter_mode: typeof prevValue === 'string' && prevValue.startsWith('=')? 'expression': 'fixed',
|
||||
dest_parameter_empty: prevValue === '' || prevValue === undefined,
|
||||
dest_parameter_had_mapping: typeof prevValue === 'string' && prevValue.startsWith('=') && hasExpressionMapping(prevValue),
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
this.forceShowExpression = false;
|
||||
}, 200);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -40,25 +40,23 @@
|
|||
:tooltipText="$locale.nodeText().inputLabelDescription(parameter, path)"
|
||||
size="small"
|
||||
:underline="true"
|
||||
:labelHoverableOnly="true"
|
||||
>
|
||||
<collection-parameter
|
||||
v-if="parameter.type === 'collection'"
|
||||
:parameter="parameter"
|
||||
:values="getParameterValue(nodeValues, parameter.name, path)"
|
||||
:nodeValues="nodeValues"
|
||||
:path="getPath(parameter.name)"
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
<fixed-collection-parameter
|
||||
v-else-if="parameter.type === 'fixedCollection'"
|
||||
:parameter="parameter"
|
||||
:values="getParameterValue(nodeValues, parameter.name, path)"
|
||||
:nodeValues="nodeValues"
|
||||
:path="getPath(parameter.name)"
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
</n8n-input-label>
|
||||
/>
|
||||
<collection-parameter
|
||||
v-if="parameter.type === 'collection'"
|
||||
:parameter="parameter"
|
||||
:values="getParameterValue(nodeValues, parameter.name, path)"
|
||||
:nodeValues="nodeValues"
|
||||
:path="getPath(parameter.name)"
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
<fixed-collection-parameter
|
||||
v-else-if="parameter.type === 'fixedCollection'"
|
||||
:parameter="parameter"
|
||||
:values="getParameterValue(nodeValues, parameter.name, path)"
|
||||
:nodeValues="nodeValues"
|
||||
:path="getPath(parameter.name)"
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="displayNodeParameter(parameter)" class="parameter-item">
|
||||
|
|
|
@ -1,45 +1,26 @@
|
|||
<template>
|
||||
<div :class="$style['parameter-options']">
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
@command="(opt) => $emit('optionSelected', opt)"
|
||||
size="mini"
|
||||
>
|
||||
<span class="el-dropdown-link">
|
||||
<font-awesome-icon
|
||||
icon="cogs"
|
||||
class="reset-icon clickable"
|
||||
:title="$locale.baseText('parameterInput.parameterOptions')"
|
||||
/>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item
|
||||
v-if="parameter.noDataExpression !== true && !isValueExpression"
|
||||
command="addExpression"
|
||||
>
|
||||
{{ $locale.baseText('parameterInput.addExpression') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="parameter.noDataExpression !== true && isValueExpression"
|
||||
command="removeExpression"
|
||||
>
|
||||
{{ $locale.baseText('parameterInput.removeExpression') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="hasRemoteMethod"
|
||||
command="refreshOptions"
|
||||
>
|
||||
{{ $locale.baseText('parameterInput.refreshList') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
command="resetValue"
|
||||
:disabled="isDefault"
|
||||
divided
|
||||
>
|
||||
{{ $locale.baseText('parameterInput.resetValue') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<div :class="$style.container">
|
||||
<n8n-action-toggle
|
||||
v-if="shouldShowOptions"
|
||||
placement="bottom-end"
|
||||
size="small"
|
||||
color="foreground-xdark"
|
||||
iconSize="small"
|
||||
:actions="actions"
|
||||
@action="(action) => $emit('optionSelected', action)"
|
||||
@visible-change="onMenuToggle"
|
||||
/>
|
||||
<n8n-radio-buttons
|
||||
v-if="parameter.noDataExpression !== true"
|
||||
size="small"
|
||||
:value="selectedView"
|
||||
:disabled="isReadOnly"
|
||||
@input="onViewSelected"
|
||||
:options="[
|
||||
{ label: $locale.baseText('parameterInput.fixed'), value: 'fixed'},
|
||||
{ label: $locale.baseText('parameterInput.expression'), value: 'expression'},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -49,20 +30,101 @@ import Vue from 'vue';
|
|||
export default Vue.extend({
|
||||
name: 'ParameterOptions',
|
||||
props: [
|
||||
'displayOptionsComputed',
|
||||
'optionSelected',
|
||||
'parameter',
|
||||
'isValueExpression',
|
||||
'isDefault',
|
||||
'hasRemoteMethod',
|
||||
'isReadOnly',
|
||||
'value',
|
||||
'showOptions',
|
||||
],
|
||||
computed: {
|
||||
isDefault (): boolean {
|
||||
return this.parameter.default === this.value;
|
||||
},
|
||||
shouldShowOptions (): boolean {
|
||||
if (this.isReadOnly === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.parameter.type === 'collection' || this.parameter.type === 'credentialsSelect') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.showOptions === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
selectedView () {
|
||||
if (this.isValueExpression) {
|
||||
return 'expression';
|
||||
}
|
||||
|
||||
return 'fixed';
|
||||
},
|
||||
isValueExpression () {
|
||||
if (this.parameter.noDataExpression === true) {
|
||||
return false;
|
||||
}
|
||||
if (typeof this.value === 'string' && this.value.charAt(0) === '=') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
hasRemoteMethod (): boolean {
|
||||
return !!this.getArgument('loadOptionsMethod') || !!this.getArgument('loadOptions');
|
||||
},
|
||||
actions (): Array<{label: string, value: string, disabled?: boolean}> {
|
||||
const actions = [
|
||||
{
|
||||
label: this.$locale.baseText('parameterInput.resetValue'),
|
||||
value: 'resetValue',
|
||||
disabled: this.isDefault,
|
||||
},
|
||||
];
|
||||
|
||||
if (this.hasRemoteMethod) {
|
||||
return [
|
||||
{
|
||||
label: this.$locale.baseText('parameterInput.refreshList'),
|
||||
value: 'refreshOptions',
|
||||
},
|
||||
...actions,
|
||||
];
|
||||
}
|
||||
|
||||
return actions;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onMenuToggle(visible: boolean) {
|
||||
this.$emit('menu-expanded', visible);
|
||||
},
|
||||
onViewSelected(selected: string) {
|
||||
if (selected === 'expression' ) {
|
||||
this.$emit('optionSelected', this.isValueExpression? 'openExpression': 'addExpression');
|
||||
}
|
||||
|
||||
if (selected === 'fixed' && this.isValueExpression) {
|
||||
this.$emit('optionSelected', 'removeExpression');
|
||||
}
|
||||
},
|
||||
getArgument (argumentName: string): string | number | boolean | undefined {
|
||||
if (this.parameter.typeOptions === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.parameter.typeOptions[argumentName] === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.parameter.typeOptions[argumentName];
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.parameter-options {
|
||||
width: 25px;
|
||||
text-align: right;
|
||||
float: right;
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -102,28 +102,8 @@
|
|||
</n8n-text>
|
||||
</div>
|
||||
|
||||
<div v-else-if="hasNodeRun && displayMode === 'table' && tableData && tableData.columns && tableData.columns.length === 0" :class="$style.dataDisplay">
|
||||
<table :class="$style.table">
|
||||
<tr>
|
||||
<th :class="$style.emptyCell"></th>
|
||||
</tr>
|
||||
<tr v-for="(row, index1) in tableData.data" :key="index1">
|
||||
<td>
|
||||
<n8n-text>{{ $locale.baseText('runData.emptyItemHint') }}</n8n-text>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-else-if="hasNodeRun && displayMode === 'table' && tableData" :class="$style.dataDisplay">
|
||||
<table :class="$style.table">
|
||||
<tr>
|
||||
<th v-for="column in (tableData.columns || [])" :key="column">{{column}}</th>
|
||||
</tr>
|
||||
<tr v-for="(row, index1) in tableData.data" :key="index1">
|
||||
<td v-for="(data, index2) in row" :key="index2">{{ [null, undefined].includes(data) ? ' ' : data }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<RunDataTable :node="node" :tableData="tableData" :mappingEnabled="mappingEnabled" :distanceFromActive="distanceFromActive" :showMappingHint="showMappingHint" :runIndex="runIndex" :totalRuns="maxRunIndex" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="hasNodeRun && displayMode === 'json'" :class="$style.jsonDisplay">
|
||||
|
@ -261,6 +241,7 @@ import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
|||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
import RunDataTable from './RunDataTable.vue';
|
||||
|
||||
// A path that does not exist so that nothing is selected by default
|
||||
const deselectedPlaceholder = '_!^&*';
|
||||
|
@ -278,6 +259,7 @@ export default mixins(
|
|||
NodeErrorView,
|
||||
VueJsonPretty,
|
||||
WarningTooltip,
|
||||
RunDataTable,
|
||||
},
|
||||
props: {
|
||||
nodeUi: {
|
||||
|
@ -312,6 +294,15 @@ export default mixins(
|
|||
overrideOutputs: {
|
||||
type: Array,
|
||||
},
|
||||
mappingEnabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
distanceFromActive: {
|
||||
type: Number,
|
||||
},
|
||||
showMappingHint: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -924,41 +915,6 @@ export default mixins(
|
|||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.table {
|
||||
border-collapse: separate;
|
||||
text-align: left;
|
||||
width: calc(100% - var(--spacing-s));
|
||||
margin-right: var(--spacing-s);
|
||||
font-size: var(--font-size-s);
|
||||
|
||||
th {
|
||||
padding: var(--spacing-2xs);
|
||||
background-color: var(--color-background-base);
|
||||
border-top: var(--border-base);
|
||||
border-bottom: var(--border-base);
|
||||
border-left: var(--border-base);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: var(--spacing-2xs);
|
||||
border-bottom: var(--border-base);
|
||||
border-left: var(--border-base);
|
||||
overflow-wrap: break-word;
|
||||
max-width: 300px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
th:last-child, td:last-child {
|
||||
border-right: var(--border-base);
|
||||
}
|
||||
}
|
||||
|
||||
.emptyCell {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.itemsCount {
|
||||
margin-left: var(--spacing-s);
|
||||
margin-bottom: var(--spacing-s);
|
||||
|
|
287
packages/editor-ui/src/components/RunDataTable.vue
Normal file
287
packages/editor-ui/src/components/RunDataTable.vue
Normal file
|
@ -0,0 +1,287 @@
|
|||
<template>
|
||||
<div>
|
||||
<table :class="$style.table" v-if="tableData.columns && tableData.columns.length === 0">
|
||||
<tr>
|
||||
<th :class="$style.emptyCell"></th>
|
||||
</tr>
|
||||
<tr v-for="(row, index1) in tableData.data" :key="index1">
|
||||
<td>
|
||||
<n8n-text>{{ $locale.baseText('runData.emptyItemHint') }}</n8n-text>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table :class="$style.table" v-else>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(column, i) in tableData.columns || []" :key="column">
|
||||
<n8n-tooltip placement="bottom-start" :disabled="!mappingEnabled || showHintWithDelay" :open-delay="1000">
|
||||
<div slot="content">{{ $locale.baseText('dataMapping.dragColumnToFieldHint') }}</div>
|
||||
<Draggable type="mapping" :data="getExpression(column)" :disabled="!mappingEnabled" @dragstart="onDragStart" @dragend="(column) => onDragEnd(column)">
|
||||
<template v-slot:preview="{ canDrop }">
|
||||
<div :class="[$style.dragPill, canDrop ? $style.droppablePill: $style.defaultPill]">
|
||||
{{ $locale.baseText('dataMapping.mapSpecificColumnToField', { interpolate: { name: shorten(column, 16, 2) } }) }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot="{ isDragging }">
|
||||
<div
|
||||
:class="{
|
||||
[$style.header]: true,
|
||||
[$style.draggableHeader]: mappingEnabled,
|
||||
[$style.activeHeader]: (i === activeColumn || forceShowGrip) && mappingEnabled,
|
||||
[$style.draggingHeader]: isDragging,
|
||||
}"
|
||||
>
|
||||
<span>{{ column || " " }}</span>
|
||||
<n8n-tooltip v-if="mappingEnabled" placement="bottom-start" :manual="true" :value="i === 0 && showHintWithDelay">
|
||||
<div v-if="focusedMappableInput" slot="content" v-html="$locale.baseText('dataMapping.tableHint', { interpolate: { name: focusedMappableInput } })"></div>
|
||||
<div v-else slot="content" v-html="$locale.baseText('dataMapping.dragColumnToFieldHint')"></div>
|
||||
<div :class="$style.dragButton">
|
||||
<font-awesome-icon icon="grip-vertical" />
|
||||
</div>
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
</n8n-tooltip>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index1) in tableData.data" :key="index1">
|
||||
<td
|
||||
v-for="(data, index2) in row"
|
||||
:key="index2"
|
||||
:data-col="index2"
|
||||
@mouseenter="onMouseEnterCell"
|
||||
@mouseleave="onMouseLeaveCell"
|
||||
>
|
||||
{{ [null, undefined].includes(data) ? ' ' : data }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { LOCAL_STORAGE_MAPPING_FLAG } from '@/constants';
|
||||
import { INodeUi, ITableData } from '@/Interface';
|
||||
import Vue from 'vue';
|
||||
import Draggable from './Draggable.vue';
|
||||
import { shorten } from './helpers';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'RunDataTable',
|
||||
components: { Draggable },
|
||||
props: {
|
||||
node: {
|
||||
type: Object as () => INodeUi,
|
||||
},
|
||||
tableData: {
|
||||
type: Object as () => ITableData,
|
||||
},
|
||||
mappingEnabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
distanceFromActive: {
|
||||
type: Number,
|
||||
},
|
||||
showMappingHint: {
|
||||
type: Boolean,
|
||||
},
|
||||
runIndex: {
|
||||
type: Number,
|
||||
},
|
||||
totalRuns: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeColumn: -1,
|
||||
showHintWithDelay: false,
|
||||
forceShowGrip: false,
|
||||
draggedColumn: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.showMappingHint && this.showHint) {
|
||||
setTimeout(() => {
|
||||
this.showHintWithDelay = this.showHint;
|
||||
this.$telemetry.track('User viewed data mapping tooltip', { type: 'param focus' });
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
focusedMappableInput (): string {
|
||||
return this.$store.getters['ui/focusedMappableInput'];
|
||||
},
|
||||
showHint (): boolean {
|
||||
return !this.draggedColumn && (this.showMappingHint || (!!this.focusedMappableInput && window.localStorage.getItem(LOCAL_STORAGE_MAPPING_FLAG) !== 'true'));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
shorten,
|
||||
onMouseEnterCell(e: MouseEvent) {
|
||||
const target = e.target;
|
||||
if (target && this.mappingEnabled) {
|
||||
const col = (target as HTMLElement).dataset.col;
|
||||
if (col && !isNaN(parseInt(col, 10))) {
|
||||
this.activeColumn = parseInt(col, 10);
|
||||
}
|
||||
}
|
||||
},
|
||||
onMouseLeaveCell() {
|
||||
this.activeColumn = -1;
|
||||
},
|
||||
getExpression(column: string) {
|
||||
if (!this.node) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (this.distanceFromActive === 1) {
|
||||
return `{{ $json["${column}"] }}`;
|
||||
}
|
||||
|
||||
return `{{ $node["${this.node.name}"].json["${column}"] }}`;
|
||||
},
|
||||
onDragStart() {
|
||||
this.draggedColumn = true;
|
||||
|
||||
this.$store.commit('ui/resetMappingTelemetry');
|
||||
},
|
||||
onDragEnd(column: string) {
|
||||
setTimeout(() => {
|
||||
const mappingTelemetry = this.$store.getters['ui/mappingTelemetry'];
|
||||
this.$telemetry.track('User dragged data for mapping', {
|
||||
src_node_type: this.node.type,
|
||||
src_field_name: column,
|
||||
src_nodes_back: this.distanceFromActive,
|
||||
src_run_index: this.runIndex,
|
||||
src_runs_total: this.totalRuns,
|
||||
src_view: 'table',
|
||||
src_element: 'column',
|
||||
success: false,
|
||||
...mappingTelemetry,
|
||||
});
|
||||
}, 1000); // ensure dest data gets set if drop
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
focusedMappableInput (curr: boolean) {
|
||||
setTimeout(() => {
|
||||
this.forceShowGrip = !!this.focusedMappableInput;
|
||||
}, curr? 300: 150);
|
||||
},
|
||||
showHint (curr: boolean, prev: boolean) {
|
||||
if (curr) {
|
||||
setTimeout(() => {
|
||||
this.showHintWithDelay = this.showHint;
|
||||
if (this.showHintWithDelay) {
|
||||
this.$telemetry.track('User viewed data mapping tooltip', { type: 'param focus' });
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
else {
|
||||
this.showHintWithDelay = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.table {
|
||||
border-collapse: separate;
|
||||
text-align: left;
|
||||
width: calc(100% - var(--spacing-s));
|
||||
margin-right: var(--spacing-s);
|
||||
font-size: var(--font-size-s);
|
||||
|
||||
th {
|
||||
background-color: var(--color-background-base);
|
||||
border-top: var(--border-base);
|
||||
border-bottom: var(--border-base);
|
||||
border-left: var(--border-base);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: var(--spacing-2xs);
|
||||
border-bottom: var(--border-base);
|
||||
border-left: var(--border-base);
|
||||
overflow-wrap: break-word;
|
||||
max-width: 300px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
border-right: var(--border-base);
|
||||
}
|
||||
}
|
||||
|
||||
.emptyCell {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-2xs);
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.draggableHeader {
|
||||
&:hover {
|
||||
cursor: grab;
|
||||
background-color: var(--color-foreground-base);
|
||||
|
||||
.dragButton {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.draggingHeader {
|
||||
background-color: var(--color-primary-tint-2);
|
||||
}
|
||||
|
||||
.activeHeader {
|
||||
.dragButton {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dragButton {
|
||||
opacity: 0;
|
||||
margin-left: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.dragPill {
|
||||
padding: var(--spacing-4xs) var(--spacing-4xs) var(--spacing-3xs) var(--spacing-4xs);
|
||||
color: var(--color-text-xlight);
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-2xs);
|
||||
border-radius: var(--border-radius-base);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.droppablePill {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
|
||||
.defaultPill {
|
||||
background-color: var(--color-primary);
|
||||
transform: translate(-50%, -100%);
|
||||
box-shadow: 0px 2px 6px rgba(68, 28, 23, 0.2);
|
||||
}
|
||||
</style>
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { shorten } from "./helpers";
|
||||
|
||||
const DEFAULT_WORKFLOW_NAME_LIMIT = 25;
|
||||
const WORKFLOW_NAME_END_COUNT_TO_KEEP = 4;
|
||||
|
@ -15,17 +16,7 @@ export default Vue.extend({
|
|||
props: ["name", "limit"],
|
||||
computed: {
|
||||
shortenedName(): string {
|
||||
const name = this.$props.name;
|
||||
|
||||
const limit = this.$props.limit || DEFAULT_WORKFLOW_NAME_LIMIT;
|
||||
if (name.length <= limit) {
|
||||
return name;
|
||||
}
|
||||
|
||||
const first = name.slice(0, limit - WORKFLOW_NAME_END_COUNT_TO_KEEP);
|
||||
const last = name.slice(name.length - WORKFLOW_NAME_END_COUNT_TO_KEEP, name.length);
|
||||
|
||||
return `${first}...${last}`;
|
||||
return shorten(this.name, this.limit || DEFAULT_WORKFLOW_NAME_LIMIT, WORKFLOW_NAME_END_COUNT_TO_KEEP);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CORE_NODES_CATEGORY, ERROR_TRIGGER_NODE_TYPE, TEMPLATES_NODES_FILTER } from '@/constants';
|
||||
import { CORE_NODES_CATEGORY, ERROR_TRIGGER_NODE_TYPE, MAPPING_PARAMS, TEMPLATES_NODES_FILTER } from '@/constants';
|
||||
import { INodeUi, ITemplatesNode } from '@/Interface';
|
||||
import dateformat from 'dateformat';
|
||||
import { INodeTypeDescription } from 'n8n-workflow';
|
||||
|
@ -68,3 +68,18 @@ export function isString(value: unknown): value is string {
|
|||
export function isNumber(value: unknown): value is number {
|
||||
return typeof value === 'number';
|
||||
}
|
||||
|
||||
export function shorten(s: string, limit: number, keep: number) {
|
||||
if (s.length <= limit) {
|
||||
return s;
|
||||
}
|
||||
|
||||
const first = s.slice(0, limit - keep);
|
||||
const last = s.slice(s.length - keep, s.length);
|
||||
|
||||
return `${first}...${last}`;
|
||||
}
|
||||
|
||||
export function hasExpressionMapping(value: unknown) {
|
||||
return typeof value === 'string' && !!MAPPING_PARAMS.find((param) => value.includes(param));
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ export const EXECUTE_COMMAND_NODE_TYPE = 'n8n-nodes-base.executeCommand';
|
|||
export const HTTP_REQUEST_NODE_TYPE = 'n8n-nodes-base.httpRequest';
|
||||
export const HUBSPOT_TRIGGER_NODE_TYPE = 'n8n-nodes-base.hubspotTrigger';
|
||||
export const IF_NODE_TYPE = 'n8n-nodes-base.if';
|
||||
export const INTERVAL_NODE_TYPE = 'n8n-nodes-base.interval';
|
||||
export const ITEM_LISTS_NODE_TYPE = 'n8n-nodes-base.itemLists';
|
||||
export const JIRA_NODE_TYPE = 'n8n-nodes-base.jira';
|
||||
export const JIRA_TRIGGER_NODE_TYPE = 'n8n-nodes-base.jiraTrigger';
|
||||
|
@ -193,6 +194,7 @@ export const MODAL_CONFIRMED = 'confirmed';
|
|||
|
||||
export const VALID_EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
export const LOCAL_STORAGE_ACTIVATION_FLAG = 'N8N_HIDE_ACTIVATION_ALERT';
|
||||
export const LOCAL_STORAGE_MAPPING_FLAG = 'N8N_MAPPING_ONBOARDED';
|
||||
export const BASE_NODE_SURVEY_URL = 'https://n8n-community.typeform.com/to/BvmzxqYv#nodename=';
|
||||
|
||||
export const HIRING_BANNER = `
|
||||
|
@ -242,3 +244,5 @@ export enum VIEWS {
|
|||
API_SETTINGS = "APISettings",
|
||||
NOT_FOUND = "NotFoundView",
|
||||
}
|
||||
|
||||
export const MAPPING_PARAMS = [`$evaluateExpression`, `$item`, `$jmespath`, `$node`, `$binary`, `$data`, `$env`, `$json`, `$now`, `$parameters`, `$position`, `$resumeWebhookUrl`, `$runIndex`, `$today`, `$workflow`, '$parameter'];
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
IRootState,
|
||||
IRunDataDisplayMode,
|
||||
IUiState,
|
||||
XYPosition,
|
||||
} from '../Interface';
|
||||
|
||||
const module: Module<IUiState, IRootState> = {
|
||||
|
@ -97,8 +98,17 @@ const module: Module<IUiState, IRootState> = {
|
|||
output: {
|
||||
displayMode: 'table',
|
||||
},
|
||||
focusedMappableInput: '',
|
||||
mappingTelemetry: {},
|
||||
},
|
||||
mainPanelPosition: 0.5,
|
||||
draggable: {
|
||||
isDragging: false,
|
||||
type: '',
|
||||
data: '',
|
||||
canDrop: false,
|
||||
stickyPosition: null,
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
areExpressionsDisabled(state: IUiState) {
|
||||
|
@ -127,6 +137,13 @@ const module: Module<IUiState, IRootState> = {
|
|||
inputPanelDispalyMode: (state: IUiState) => state.ndv.input.displayMode,
|
||||
outputPanelDispalyMode: (state: IUiState) => state.ndv.output.displayMode,
|
||||
mainPanelPosition: (state: IUiState) => state.mainPanelPosition,
|
||||
focusedMappableInput: (state: IUiState) => state.ndv.focusedMappableInput,
|
||||
isDraggableDragging: (state: IUiState) => state.draggable.isDragging,
|
||||
draggableType: (state: IUiState) => state.draggable.type,
|
||||
draggableData: (state: IUiState) => state.draggable.data,
|
||||
canDraggableDrop: (state: IUiState) => state.draggable.canDrop,
|
||||
draggableStickyPos: (state: IUiState) => state.draggable.stickyPosition,
|
||||
mappingTelemetry: (state: IUiState) => state.ndv.mappingTelemetry,
|
||||
},
|
||||
mutations: {
|
||||
setMode: (state: IUiState, params: {name: string, mode: string}) => {
|
||||
|
@ -173,7 +190,39 @@ const module: Module<IUiState, IRootState> = {
|
|||
setMainPanelRelativePosition(state: IUiState, relativePosition: number) {
|
||||
state.mainPanelPosition = relativePosition;
|
||||
},
|
||||
|
||||
setMappableNDVInputFocus(state: IUiState, paramName: string) {
|
||||
Vue.set(state.ndv, 'focusedMappableInput', paramName);
|
||||
},
|
||||
draggableStartDragging(state: IUiState, {type, data}: {type: string, data: string}) {
|
||||
state.draggable = {
|
||||
isDragging: true,
|
||||
type,
|
||||
data,
|
||||
canDrop: false,
|
||||
stickyPosition: null,
|
||||
};
|
||||
},
|
||||
draggableStopDragging(state: IUiState) {
|
||||
state.draggable = {
|
||||
isDragging: false,
|
||||
type: '',
|
||||
data: '',
|
||||
canDrop: false,
|
||||
stickyPosition: null,
|
||||
};
|
||||
},
|
||||
setDraggableStickyPos(state: IUiState, position: XYPosition | null) {
|
||||
Vue.set(state.draggable, 'stickyPosition', position);
|
||||
},
|
||||
setDraggableCanDrop(state: IUiState, canDrop: boolean) {
|
||||
Vue.set(state.draggable, 'canDrop', canDrop);
|
||||
},
|
||||
setMappingTelemetry(state: IUiState, telemetery: {[key: string]: string | number | boolean}) {
|
||||
state.ndv.mappingTelemetry = {...state.ndv.mappingTelemetry, ...telemetery};
|
||||
},
|
||||
resetMappingTelemetry(state: IUiState) {
|
||||
state.ndv.mappingTelemetry = {};
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
openModal: async (context: ActionContext<IUiState, IRootState>, modalKey: string) => {
|
||||
|
|
|
@ -152,6 +152,12 @@
|
|||
"dataDisplay.needHelp": "Need help?",
|
||||
"dataDisplay.nodeDocumentation": "Node Documentation",
|
||||
"dataDisplay.openDocumentationFor": "Open {nodeTypeDisplayName} documentation",
|
||||
"dataMapping.dragColumnToFieldHint": "Drag onto a field to map column to that field",
|
||||
"dataMapping.dragFromPreviousHint": "Map data from previous nodes to <b>{name}</b><br/> by first clicking this button",
|
||||
"dataMapping.success.title": "You just mapped some data!",
|
||||
"dataMapping.success.moreInfo": "Check out our <a href=\"https://docs.n8n.io/data/data-mapping\" target=\"_blank\">docs</a> for more details on mapping data in n8n",
|
||||
"dataMapping.tableHint": "Drag a column onto <b>{name}</b> to map it",
|
||||
"dataMapping.mapSpecificColumnToField": "Map {name} to field",
|
||||
"displayWithChange.cancelEdit": "Cancel Edit",
|
||||
"displayWithChange.clickToChange": "Click to Change",
|
||||
"displayWithChange.setValue": "Set Value",
|
||||
|
@ -521,9 +527,10 @@
|
|||
"onboardingWorkflow.stickyContent": "## 👇 Get started faster \nLightning tour of the key concepts \n\n[![n8n quickstart video](/static/quickstart_thumbnail.png#full-width)](https://www.youtube.com/watch?v=RpjQTGKm-ok)",
|
||||
"openWorkflow.workflowImportError": "Could not import workflow",
|
||||
"openWorkflow.workflowNotFoundError": "Could not find workflow",
|
||||
"parameterInput.addExpression": "Add Expression",
|
||||
"parameterInput.customApiCall": "Custom API Call",
|
||||
"parameterInput.error": "ERROR",
|
||||
"parameterInput.expression": "Expression",
|
||||
"parameterInput.fixed": "Fixed",
|
||||
"parameterInput.issues": "Issues",
|
||||
"parameterInput.loadingOptions": "Loading options...",
|
||||
"parameterInput.openEditWindow": "Open Edit Window",
|
||||
|
@ -531,9 +538,7 @@
|
|||
"parameterInput.parameterHasExpression": "Parameter: \"{shortPath}\" has an expression",
|
||||
"parameterInput.parameterHasIssues": "Parameter: \"{shortPath}\" has issues",
|
||||
"parameterInput.parameterHasIssuesAndExpression": "Parameter: \"{shortPath}\" has issues and an expression",
|
||||
"parameterInput.parameterOptions": "Parameter Options",
|
||||
"parameterInput.refreshList": "Refresh List",
|
||||
"parameterInput.removeExpression": "Remove Expression",
|
||||
"parameterInput.resetValue": "Reset Value",
|
||||
"parameterInput.select": "Select",
|
||||
"parameterInput.selectDateAndTime": "Select date and time",
|
||||
|
|
|
@ -48,6 +48,7 @@ import {
|
|||
faFolderOpen,
|
||||
faGift,
|
||||
faGraduationCap,
|
||||
faGripVertical,
|
||||
faHdd,
|
||||
faHome,
|
||||
faHourglass,
|
||||
|
@ -137,6 +138,7 @@ addIcon(faCloudDownloadAlt);
|
|||
addIcon(faCopy);
|
||||
addIcon(faCut);
|
||||
addIcon(faDotCircle);
|
||||
addIcon(faGripVertical);
|
||||
addIcon(faEdit);
|
||||
addIcon(faEllipsisV);
|
||||
addIcon(faEnvelope);
|
||||
|
|
Loading…
Reference in a new issue