mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -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: {
|
argTypes: {
|
||||||
placement: {
|
placement: {
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: ['top', 'bottom'],
|
options: ['top', 'top-start', 'top-end', 'bottom', 'bottom-end'],
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: 'select',
|
||||||
|
options: ['mini', 'small', 'medium'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<span :class="$style.container">
|
<span :class="$style.container">
|
||||||
<el-dropdown :placement="placement" trigger="click" @command="onCommand">
|
<el-dropdown :placement="placement" :size="size" trigger="click" @command="onCommand" @visible-change="onVisibleChange">
|
||||||
<span :class="$style.button">
|
<span :class="{[$style.button]: true, [$style[theme]]: !!theme}">
|
||||||
<component :is="$options.components.N8nIcon"
|
<component :is="$options.components.N8nIcon"
|
||||||
icon="ellipsis-v"
|
icon="ellipsis-v"
|
||||||
|
:size="iconSize"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<el-dropdown-menu slot="dropdown">
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
@ -11,6 +12,7 @@
|
||||||
v-for="action in actions"
|
v-for="action in actions"
|
||||||
:key="action.value"
|
:key="action.value"
|
||||||
:command="action.value"
|
:command="action.value"
|
||||||
|
:disabled="action.disabled"
|
||||||
>
|
>
|
||||||
{{action.label}}
|
{{action.label}}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
|
@ -42,12 +44,30 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'bottom',
|
default: 'bottom',
|
||||||
validator: (value: string): boolean =>
|
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: {
|
methods: {
|
||||||
onCommand(value: string) {
|
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;
|
cursor: pointer;
|
||||||
padding: var(--spacing-4xs);
|
padding: var(--spacing-4xs);
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
|
|
|
@ -1,16 +1,26 @@
|
||||||
<template functional>
|
<template>
|
||||||
<div :class="{[$style.inputLabelContainer]: !props.labelHoverableOnly}">
|
<div :class="$style.container">
|
||||||
<div :class="$options.methods.getLabelClass(props, $style)">
|
<div :class="{
|
||||||
<component v-if="props.label" :is="$options.components.N8nText" :bold="props.bold" :size="props.size" :compact="!props.underline">
|
[this.$style.label]: !!this.label,
|
||||||
{{ props.label }}
|
[this.$style.underline]: this.underline,
|
||||||
<component :is="$options.components.N8nText" color="primary" :bold="props.bold" :size="props.size" v-if="props.required">*</component>
|
[this.$style[this.size]]: true,
|
||||||
</component>
|
}">
|
||||||
<span :class="[$style.infoIcon, props.showTooltip ? $style.showIcon: $style.hiddenIcon]" v-if="props.tooltipText">
|
<div :class="$style.title" v-if="label">
|
||||||
<component :is="$options.components.N8nTooltip" placement="top" :popper-class="$style.tooltipPopper">
|
<n8n-text :bold="bold" :size="size" :compact="!underline">
|
||||||
<component :is="$options.components.N8nIcon" icon="question-circle" size="small" />
|
{{ label }}
|
||||||
<div slot="content" v-html="$options.methods.addTargetBlank(props.tooltipText)"></div>
|
<n8n-text color="primary" :bold="bold" :size="size" v-if="required">*</n8n-text>
|
||||||
</component>
|
</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>
|
</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>
|
</div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,74 +66,104 @@ export default {
|
||||||
showTooltip: {
|
showTooltip: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
labelHoverableOnly: {
|
showOptions: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addTargetBlank,
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.inputLabelContainer:hover {
|
.container {
|
||||||
> div > .infoIcon {
|
display: flex;
|
||||||
display: inline-block;
|
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 {
|
.title {
|
||||||
> .infoIcon {
|
display: flex;
|
||||||
display: inline-block;
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoIcon {
|
.infoIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
|
padding-left: var(--spacing-4xs);
|
||||||
|
background-color: var(--color-background-xlight);
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.showIcon {
|
.options {
|
||||||
display: inline-block;
|
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;
|
float: right;
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
* {
|
|
||||||
margin-right: var(--spacing-5xs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-small {
|
.overlay {
|
||||||
composes: label;
|
position: relative;
|
||||||
margin-bottom: var(--spacing-4xs);
|
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 {
|
.hidden {
|
||||||
composes: label;
|
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);
|
margin-bottom: var(--spacing-2xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,16 +171,6 @@ export default {
|
||||||
border-bottom: var(--border-base);
|
border-bottom: var(--border-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-small-underline {
|
|
||||||
composes: label-small;
|
|
||||||
composes: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-medium-underline {
|
|
||||||
composes: label-medium;
|
|
||||||
composes: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipPopper {
|
.tooltipPopper {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
|
||||||
|
@ -148,4 +178,5 @@ export default {
|
||||||
margin-left: var(--spacing-s);
|
margin-left: var(--spacing-s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<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">
|
<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>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -21,6 +21,15 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'medium',
|
||||||
|
validator: (value: string): boolean =>
|
||||||
|
['small', 'medium'].includes(value),
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -30,11 +39,11 @@ export default {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
.hoverable:hover {
|
||||||
.button:not(.active) {
|
.button:not(.active) {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,16 +56,29 @@ export default {
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 0 var(--spacing-xs);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 26px;
|
|
||||||
font-size: var(--font-size-2xs);
|
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s ease;
|
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 {
|
.active {
|
||||||
|
|
|
@ -6,6 +6,10 @@ export default {
|
||||||
title: 'Atoms/RadioButtons',
|
title: 'Atoms/RadioButtons',
|
||||||
component: N8nRadioButtons,
|
component: N8nRadioButtons,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
|
size: {
|
||||||
|
type: 'select',
|
||||||
|
options: ['small', 'medium'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
backgrounds: { default: '--color-background-xlight' },
|
backgrounds: { default: '--color-background-xlight' },
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div role="radiogroup" :class="$style.radioGroup">
|
<div role="radiogroup" :class="{[$style.radioGroup]: true, [$style.disabled]: disabled}">
|
||||||
<RadioButton
|
<RadioButton
|
||||||
v-for="option in options"
|
v-for="option in options"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
v-bind="option"
|
v-bind="option"
|
||||||
:active="value === option.value"
|
:active="value === option.value"
|
||||||
|
:size="size"
|
||||||
|
:disabled="disabled"
|
||||||
@click="(e) => onClick(option.value, e)"
|
@click="(e) => onClick(option.value, e)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,12 +23,21 @@ export default {
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
},
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
RadioButton,
|
RadioButton,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onClick(value) {
|
onClick(value) {
|
||||||
|
if (this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.$emit('input', value);
|
this.$emit('input', value);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -45,5 +56,9 @@ export default {
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
v-if="!user.isOwner"
|
v-if="!user.isOwner"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
:actions="getActions(user)"
|
:actions="getActions(user)"
|
||||||
|
theme="dark"
|
||||||
@action="(action) => onUserAction(user, action)"
|
@action="(action) => onUserAction(user, action)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import VariableTable from './VariableTable.vue';
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="font-size">
|
<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: {
|
components: {
|
||||||
Sizes,
|
Sizes,
|
||||||
},
|
},
|
||||||
|
|
|
@ -70,6 +70,23 @@
|
||||||
var(--color-secondary-l)
|
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-h: 150.4;
|
||||||
--color-success-s: 60%;
|
--color-success-s: 60%;
|
||||||
--color-success-l: 40.4%;
|
--color-success-l: 40.4%;
|
||||||
|
|
|
@ -114,7 +114,6 @@ video {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
vertical-align: baseline;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
"vue-fragment": "1.5.1",
|
"vue-fragment": "1.5.1",
|
||||||
"vue-i18n": "^8.26.7",
|
"vue-i18n": "^8.26.7",
|
||||||
"vue2-boring-avatars": "0.3.4",
|
"vue2-boring-avatars": "0.3.4",
|
||||||
|
"vue2-teleport": "^1.0.1",
|
||||||
"xss": "^1.0.10"
|
"xss": "^1.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -870,8 +870,17 @@ export interface IUiState {
|
||||||
output: {
|
output: {
|
||||||
displayMode: IRunDataDisplayMode;
|
displayMode: IRunDataDisplayMode;
|
||||||
};
|
};
|
||||||
|
focusedMappableInput: string;
|
||||||
|
mappingTelemetry: {[key: string]: string | number | boolean};
|
||||||
};
|
};
|
||||||
mainPanelPosition: number;
|
mainPanelPosition: number;
|
||||||
|
draggable: {
|
||||||
|
isDragging: boolean;
|
||||||
|
type: string;
|
||||||
|
data: string;
|
||||||
|
canDrop: boolean;
|
||||||
|
stickyPosition: null | XYPosition;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ILogLevel = 'info' | 'debug' | 'warn' | 'error' | 'verbose';
|
export type ILogLevel = 'info' | 'debug' | 'warn' | 'error' | 'verbose';
|
||||||
|
|
|
@ -4,37 +4,106 @@
|
||||||
@mousedown="onDragStart"
|
@mousedown="onDragStart"
|
||||||
>
|
>
|
||||||
<slot :isDragging="isDragging"></slot>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { XYPosition } from '@/Interface';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import Teleport from 'vue2-teleport';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
Teleport,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isDragging: false,
|
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: {
|
methods: {
|
||||||
onDragStart(e: MouseEvent) {
|
onDragStart(e: DragEvent) {
|
||||||
|
if (this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.isDragging = true;
|
this.isDragging = true;
|
||||||
|
this.$store.commit('ui/draggableStartDragging', {type: this.type, data: this.data || ''});
|
||||||
|
|
||||||
this.$emit('dragstart');
|
this.$emit('dragstart');
|
||||||
document.body.style.cursor = 'grabbing';
|
document.body.style.cursor = 'grabbing';
|
||||||
|
|
||||||
window.addEventListener('mousemove', this.onDrag);
|
window.addEventListener('mousemove', this.onDrag);
|
||||||
window.addEventListener('mouseup', this.onDragEnd);
|
window.addEventListener('mouseup', this.onDragEnd);
|
||||||
|
|
||||||
|
this.draggablePosition = { x: e.pageX, y: e.pageY };
|
||||||
},
|
},
|
||||||
onDrag(e: MouseEvent) {
|
onDrag(e: MouseEvent) {
|
||||||
|
if (this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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) {
|
onDragEnd(e: MouseEvent) {
|
||||||
|
if (this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
@ -45,6 +114,7 @@ export default Vue.extend({
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$emit('dragend');
|
this.$emit('dragend');
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
|
this.$store.commit('ui/draggableStopDragging');
|
||||||
}, 0);
|
}, 0);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -56,4 +126,14 @@ export default Vue.extend({
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.draggable {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-data-transfer {
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
}
|
||||||
</style>
|
</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 { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
import { hasExpressionMapping } from './helpers';
|
||||||
const MAPPING_PARAMS = [`$evaluateExpression`, `$item`, `$jmespath`, `$node`, `$binary`, `$data`, `$env`, `$json`, `$now`, `$parameters`, `$position`, `$resumeWebhookUrl`, `$runIndex`, `$today`, `$workflow`];
|
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
externalHooks,
|
externalHooks,
|
||||||
|
@ -92,9 +91,11 @@ export default mixins(
|
||||||
},
|
},
|
||||||
|
|
||||||
closeDialog () {
|
closeDialog () {
|
||||||
// Handle the close externally as the visible parameter is an external prop
|
if (this.latestValue !== this.value) {
|
||||||
// and is so not allowed to be changed here.
|
// Handle the close externally as the visible parameter is an external prop
|
||||||
this.$emit('valueChanged', this.latestValue);
|
// and is so not allowed to be changed here.
|
||||||
|
this.$emit('valueChanged', this.latestValue);
|
||||||
|
}
|
||||||
this.$emit('closeDialog');
|
this.$emit('closeDialog');
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
@ -172,7 +173,7 @@ export default mixins(
|
||||||
source: this.eventSource,
|
source: this.eventSource,
|
||||||
session_id: this.$store.getters['ui/ndvSessionId'],
|
session_id: this.$store.getters['ui/ndvSessionId'],
|
||||||
has_parameter: this.value.includes('$parameter'),
|
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 {
|
.ql-disabled .ql-editor {
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border: 1px dashed $--custom-expression-text;
|
border: 1px solid $--custom-expression-text;
|
||||||
color: $--custom-expression-text;
|
color: $--custom-expression-text;
|
||||||
background-color: $--custom-expression-background;
|
background-color: $--custom-expression-background;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|
|
@ -10,73 +10,72 @@
|
||||||
class="fixed-collection-parameter-property"
|
class="fixed-collection-parameter-property"
|
||||||
>
|
>
|
||||||
<n8n-input-label
|
<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"
|
:underline="true"
|
||||||
:labelHoverableOnly="true"
|
|
||||||
size="small"
|
size="small"
|
||||||
>
|
/>
|
||||||
<div v-if="multipleValues === true">
|
<div v-if="multipleValues === true">
|
||||||
<div
|
<div
|
||||||
v-for="(value, index) in values[property.name]"
|
v-for="(value, index) in values[property.name]"
|
||||||
:key="property.name + index"
|
:key="property.name + index"
|
||||||
class="parameter-item"
|
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 class="parameter-item-wrapper">
|
<div class="parameter-item-wrapper">
|
||||||
<div class="delete-option" v-if="!isReadOnly">
|
<div class="delete-option" v-if="!isReadOnly">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
icon="trash"
|
icon="trash"
|
||||||
class="reset-icon clickable"
|
class="reset-icon clickable"
|
||||||
:title="$locale.baseText('fixedCollectionParameter.deleteItem')"
|
: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>
|
</div>
|
||||||
<parameter-input-list
|
<parameter-input-list
|
||||||
:parameters="property.values"
|
:parameters="property.values"
|
||||||
:nodeValues="nodeValues"
|
:nodeValues="nodeValues"
|
||||||
:path="getPropertyPath(property.name)"
|
:path="getPropertyPath(property.name, index)"
|
||||||
class="parameter-item"
|
|
||||||
@valueChanged="valueChanged"
|
|
||||||
:hideDelete="true"
|
:hideDelete="true"
|
||||||
|
@valueChanged="valueChanged"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div v-if="parameterOptions.length > 0 && !isReadOnly">
|
<div v-if="parameterOptions.length > 0 && !isReadOnly">
|
||||||
|
|
|
@ -10,6 +10,9 @@
|
||||||
:executingMessage="$locale.baseText('ndv.input.executingPrevious')"
|
:executingMessage="$locale.baseText('ndv.input.executingPrevious')"
|
||||||
:sessionId="sessionId"
|
:sessionId="sessionId"
|
||||||
:overrideOutputs="connectedCurrentNodeOutputs"
|
:overrideOutputs="connectedCurrentNodeOutputs"
|
||||||
|
:mappingEnabled="!readOnly"
|
||||||
|
:showMappingHint="draggableHintShown"
|
||||||
|
:distanceFromActive="currentNodeDepth"
|
||||||
paneType="input"
|
paneType="input"
|
||||||
@linkRun="onLinkRun"
|
@linkRun="onLinkRun"
|
||||||
@unlinkRun="onUnlinkRun"
|
@unlinkRun="onUnlinkRun"
|
||||||
|
@ -32,8 +35,11 @@
|
||||||
<template v-slot:node-not-run>
|
<template v-slot:node-not-run>
|
||||||
<div :class="$style.noOutputData" v-if="parentNodes.length">
|
<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>
|
<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-tooltip v-if="!readOnly" :manual="true" :value="showDraggableHint && showDraggableHintWithDelay">
|
||||||
<n8n-text v-if="!readOnly" tag="div" size="small">
|
<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') }}
|
{{ $locale.baseText('ndv.input.noOutputData.hint') }}
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</div>
|
</div>
|
||||||
|
@ -65,6 +71,7 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import NodeExecuteButton from './NodeExecuteButton.vue';
|
import NodeExecuteButton from './NodeExecuteButton.vue';
|
||||||
import WireMeUp from './WireMeUp.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(
|
export default mixins(
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
|
@ -93,7 +100,27 @@ export default mixins(
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showDraggableHintWithDelay: false,
|
||||||
|
draggableHintShown: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
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 {
|
isExecutingPrevious(): boolean {
|
||||||
if (!this.workflowRunning) {
|
if (!this.workflowRunning) {
|
||||||
return false;
|
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);
|
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: {
|
methods: {
|
||||||
onNodeExecute() {
|
onNodeExecute() {
|
||||||
|
@ -182,6 +213,26 @@ export default mixins(
|
||||||
return truncated;
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -4,34 +4,31 @@
|
||||||
:label="$locale.nodeText().inputLabelDisplayName(parameter, path)"
|
:label="$locale.nodeText().inputLabelDisplayName(parameter, path)"
|
||||||
:tooltipText="$locale.nodeText().inputLabelDescription(parameter, path)"
|
:tooltipText="$locale.nodeText().inputLabelDescription(parameter, path)"
|
||||||
:underline="true"
|
:underline="true"
|
||||||
:labelHoverableOnly="true"
|
|
||||||
size="small"
|
size="small"
|
||||||
>
|
/>
|
||||||
|
|
||||||
<div v-for="(value, index) in values" :key="index" class="duplicate-parameter-item" :class="parameter.type">
|
<div v-for="(value, index) in values" :key="index" class="duplicate-parameter-item" :class="parameter.type">
|
||||||
<div class="delete-item clickable" v-if="!isReadOnly">
|
<div class="delete-item clickable" v-if="!isReadOnly">
|
||||||
<font-awesome-icon icon="trash" :title="$locale.baseText('multipleParameter.deleteItem')" @click="deleteItem(index)" />
|
<font-awesome-icon icon="trash" :title="$locale.baseText('multipleParameter.deleteItem')" @click="deleteItem(index)" />
|
||||||
<div v-if="sortable">
|
<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 !== 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)" />
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="parameter.type === 'collection'">
|
||||||
<div class="add-item-wrapper">
|
<collection-parameter :parameter="parameter" :values="value" :nodeValues="nodeValues" :path="getPath(index)" :hideDelete="hideDelete" @valueChanged="valueChanged" />
|
||||||
<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>
|
||||||
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -41,7 +38,7 @@ import {
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
|
|
||||||
import CollectionParameter from '@/components/CollectionParameter.vue';
|
import CollectionParameter from '@/components/CollectionParameter.vue';
|
||||||
import ParameterInput from '@/components/ParameterInput.vue';
|
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
|
|
||||||
|
@ -54,7 +51,7 @@ export default mixins(genericHelpers)
|
||||||
name: 'MultipleParameter',
|
name: 'MultipleParameter',
|
||||||
components: {
|
components: {
|
||||||
CollectionParameter,
|
CollectionParameter,
|
||||||
ParameterInput,
|
ParameterInputFull,
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
'nodeValues', // NodeParameters
|
'nodeValues', // NodeParameters
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Draggable @drag="onDrag" @dragstart="onDragStart" @dragend="onDragEnd">
|
<Draggable type="panel-resize" @drag="onDrag" @dragstart="onDragStart" @dragend="onDragEnd">
|
||||||
<template v-slot="{ isDragging }">
|
<template v-slot="{ isDragging }">
|
||||||
<div
|
<div
|
||||||
:class="{ [$style.dragButton]: true }"
|
:class="{ [$style.dragButton]: true }"
|
||||||
|
|
|
@ -1,220 +1,262 @@
|
||||||
<template>
|
<template>
|
||||||
<div @keydown.stop :class="parameterInputClasses">
|
<div @keydown.stop :class="parameterInputClasses">
|
||||||
<expression-edit :dialogVisible="expressionEditDialogVisible" :value="value" :parameter="parameter" :path="path" :eventSource="eventSource || 'ndv'" @closeDialog="closeExpressionEditDialog" @valueChanged="expressionUpdated"></expression-edit>
|
<expression-edit
|
||||||
<div class="parameter-input ignore-key-press" :style="parameterInputWrapperStyle" @click="openExpressionEdit">
|
:dialogVisible="expressionEditDialogVisible"
|
||||||
|
:value="value"
|
||||||
<n8n-input
|
:parameter="parameter"
|
||||||
v-if="isValueExpression && showExpressionAsTextInput"
|
:path="path"
|
||||||
:size="inputSize"
|
:eventSource="eventSource || 'ndv'"
|
||||||
:value="expressionDisplayValue"
|
@closeDialog="closeExpressionEditDialog"
|
||||||
:disabled="isReadOnly"
|
@valueChanged="expressionUpdated"
|
||||||
:title="displayTitle"
|
></expression-edit>
|
||||||
@keydown.stop
|
<div
|
||||||
/>
|
class="parameter-input ignore-key-press"
|
||||||
|
:style="parameterInputWrapperStyle"
|
||||||
<div v-else-if="['json', 'string'].includes(parameter.type) || remoteParameterOptionsLoadingIssues !== null">
|
@click="openExpressionEdit"
|
||||||
<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>
|
|
||||||
|
|
||||||
<n8n-input
|
<n8n-input
|
||||||
v-else
|
v-if="isValueExpression || droppable || forceShowExpression"
|
||||||
v-model="tempValue"
|
|
||||||
ref="inputField"
|
|
||||||
:size="inputSize"
|
:size="inputSize"
|
||||||
:type="getStringInputType"
|
:type="getStringInputType"
|
||||||
:rows="getArgument('rows')"
|
:rows="getArgument('rows')"
|
||||||
:value="displayValue"
|
:value="activeDrop || forceShowExpression? '': expressionDisplayValue"
|
||||||
:disabled="isReadOnly"
|
|
||||||
@input="onTextInputChange"
|
|
||||||
@change="valueChanged"
|
|
||||||
@keydown.stop
|
|
||||||
@focus="setFocus"
|
|
||||||
@blur="onBlur"
|
|
||||||
:title="displayTitle"
|
: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">
|
<code-edit
|
||||||
<font-awesome-icon v-if="!isValueExpression && !isReadOnly" icon="external-link-alt" class="edit-window-button clickable" :title="$locale.baseText('parameterInput.openEditWindow')" @click="displayEditDialog()" />
|
v-if="codeEditDialogVisible"
|
||||||
</div>
|
:value="value"
|
||||||
</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"
|
|
||||||
:parameter="parameter"
|
:parameter="parameter"
|
||||||
:isValueExpression="isValueExpression"
|
:type="editorType"
|
||||||
:isDefault="isDefault"
|
:codeAutocomplete="codeAutocomplete"
|
||||||
:hasRemoteMethod="hasRemoteMethod"
|
:path="path"
|
||||||
@optionSelected="optionSelected"
|
@closeDialog="closeCodeEditDialog"
|
||||||
/>
|
@valueChanged="expressionUpdated"
|
||||||
</template>
|
></code-edit>
|
||||||
</credentials-select>
|
<text-edit
|
||||||
|
:dialogVisible="textEditDialogVisible"
|
||||||
|
:value="value"
|
||||||
|
:parameter="parameter"
|
||||||
|
:path="path"
|
||||||
|
@closeDialog="closeTextEditDialog"
|
||||||
|
@valueChanged="expressionUpdated"
|
||||||
|
></text-edit>
|
||||||
|
|
||||||
<n8n-select
|
<div v-if="isEditor === true" class="code-edit clickable" @click="displayEditDialog()">
|
||||||
v-else-if="parameter.type === 'options'"
|
<prism-editor
|
||||||
ref="inputField"
|
v-if="!codeEditDialogVisible"
|
||||||
:size="inputSize"
|
:lineNumbers="true"
|
||||||
filterable
|
:readonly="true"
|
||||||
:value="displayValue"
|
:code="displayValue"
|
||||||
:placeholder="parameter.placeholder ? getPlaceholder() : $locale.baseText('parameterInput.select')"
|
language="js"
|
||||||
:loading="remoteParameterOptionsLoading"
|
></prism-editor>
|
||||||
:disabled="isReadOnly || remoteParameterOptionsLoading"
|
</div>
|
||||||
:title="displayTitle"
|
|
||||||
@change="valueChanged"
|
<n8n-input
|
||||||
@keydown.stop
|
v-else
|
||||||
@focus="setFocus"
|
v-model="tempValue"
|
||||||
@blur="onBlur"
|
ref="inputField"
|
||||||
>
|
:size="inputSize"
|
||||||
<n8n-option
|
:type="getStringInputType"
|
||||||
v-for="option in parameterOptions"
|
:rows="getArgument('rows')"
|
||||||
:value="option.value"
|
:value="displayValue"
|
||||||
:key="option.value"
|
:disabled="isReadOnly"
|
||||||
:label="getOptionsOptionDisplayName(option)"
|
@input="onTextInputChange"
|
||||||
>
|
@change="valueChanged"
|
||||||
<div class="list-option">
|
@keydown.stop
|
||||||
<div class="option-headline">
|
@focus="setFocus"
|
||||||
{{ getOptionsOptionDisplayName(option) }}
|
@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>
|
||||||
<div v-if="option.description" class="option-description" v-html="getOptionsOptionDescription(option)"></div>
|
</n8n-input>
|
||||||
</div>
|
</div>
|
||||||
</n8n-option>
|
|
||||||
</n8n-select>
|
|
||||||
|
|
||||||
<n8n-select
|
<div v-else-if="parameter.type === 'color'" ref="inputField" class="color-input">
|
||||||
v-else-if="parameter.type === 'multiOptions'"
|
<el-color-picker
|
||||||
ref="inputField"
|
size="small"
|
||||||
:size="inputSize"
|
class="color-picker"
|
||||||
filterable
|
:value="displayValue"
|
||||||
multiple
|
:disabled="isReadOnly"
|
||||||
:value="displayValue"
|
@focus="setFocus"
|
||||||
:loading="remoteParameterOptionsLoading"
|
@blur="onBlur"
|
||||||
:disabled="isReadOnly || remoteParameterOptionsLoading"
|
@change="valueChanged"
|
||||||
:title="displayTitle"
|
:title="displayTitle"
|
||||||
:placeholder="$locale.baseText('parameterInput.select')"
|
:show-alpha="getArgument('showAlpha')"
|
||||||
@change="valueChanged"
|
/>
|
||||||
@keydown.stop
|
<n8n-input
|
||||||
@focus="setFocus"
|
v-model="tempValue"
|
||||||
@blur="onBlur"
|
:size="inputSize"
|
||||||
>
|
type="text"
|
||||||
<n8n-option v-for="option in parameterOptions" :value="option.value" :key="option.value" :label="getOptionsOptionDisplayName(option)">
|
:value="tempValue"
|
||||||
<div class="list-option">
|
:disabled="isReadOnly"
|
||||||
<div class="option-headline">{{ getOptionsOptionDisplayName(option) }}</div>
|
@change="valueChanged"
|
||||||
<div v-if="option.description" class="option-description" v-html="getOptionsOptionDescription(option)"></div>
|
@keydown.stop
|
||||||
</div>
|
@focus="setFocus"
|
||||||
</n8n-option>
|
@blur="onBlur"
|
||||||
</n8n-select>
|
:title="displayTitle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-switch
|
<el-date-picker
|
||||||
v-else-if="parameter.type === 'boolean'"
|
v-else-if="parameter.type === 'dateTime'"
|
||||||
class="switch-input"
|
v-model="tempValue"
|
||||||
ref="inputField"
|
ref="inputField"
|
||||||
active-color="#13ce66"
|
type="datetime"
|
||||||
:value="displayValue"
|
:size="inputSize"
|
||||||
:disabled="isReadOnly"
|
:value="displayValue"
|
||||||
@change="valueChanged"
|
:title="displayTitle"
|
||||||
/>
|
:disabled="isReadOnly"
|
||||||
</div>
|
:placeholder="
|
||||||
|
parameter.placeholder
|
||||||
|
? getPlaceholder()
|
||||||
|
: $locale.baseText('parameterInput.selectDateAndTime')
|
||||||
|
"
|
||||||
|
:picker-options="dateTimePickerOptions"
|
||||||
|
@change="valueChanged"
|
||||||
|
@focus="setFocus"
|
||||||
|
@blur="onBlur"
|
||||||
|
@keydown.stop
|
||||||
|
/>
|
||||||
|
|
||||||
<parameter-issues
|
<n8n-input-number
|
||||||
v-if="parameter.type !== 'credentialsSelect'"
|
v-else-if="parameter.type === 'number'"
|
||||||
:issues="getIssues"
|
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
|
<credentials-select
|
||||||
v-if="displayOptionsComputed && parameter.type !== 'credentialsSelect'"
|
v-else-if="
|
||||||
:displayOptionsComputed="displayOptionsComputed"
|
parameter.type === 'credentialsSelect' || parameter.name === 'genericAuthType'
|
||||||
:parameter="parameter"
|
"
|
||||||
:isValueExpression="isValueExpression"
|
ref="inputField"
|
||||||
:isDefault="isDefault"
|
:parameter="parameter"
|
||||||
:hasRemoteMethod="hasRemoteMethod"
|
:node="node"
|
||||||
@optionSelected="optionSelected"
|
: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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -253,6 +295,7 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { CUSTOM_API_CALL_KEY } from '@/constants';
|
import { CUSTOM_API_CALL_KEY } from '@/constants';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { hasExpressionMapping } from './helpers';
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
externalHooks,
|
externalHooks,
|
||||||
|
@ -274,7 +317,6 @@ export default mixins(
|
||||||
TextEdit,
|
TextEdit,
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
'displayOptions', // boolean
|
|
||||||
'inputSize',
|
'inputSize',
|
||||||
'isReadOnly',
|
'isReadOnly',
|
||||||
'documentationUrl',
|
'documentationUrl',
|
||||||
|
@ -285,6 +327,9 @@ export default mixins(
|
||||||
'errorHighlight',
|
'errorHighlight',
|
||||||
'isForCredential', // boolean
|
'isForCredential', // boolean
|
||||||
'eventSource', // string
|
'eventSource', // string
|
||||||
|
'activeDrop',
|
||||||
|
'droppable',
|
||||||
|
'forceShowExpression',
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -352,11 +397,6 @@ export default mixins(
|
||||||
codeAutocomplete (): string | undefined {
|
codeAutocomplete (): string | undefined {
|
||||||
return this.getArgument('codeAutocomplete') as 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 {
|
dependentParametersValues (): string | null {
|
||||||
const loadOptionsDependsOn = this.getArgument('loadOptionsDependsOn') as string[] | undefined;
|
const loadOptionsDependsOn = this.getArgument('loadOptionsDependsOn') as string[] | undefined;
|
||||||
|
|
||||||
|
@ -462,20 +502,6 @@ export default mixins(
|
||||||
|
|
||||||
return value;
|
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 {
|
expressionValueComputed (): NodeParameterValue | string[] | null {
|
||||||
if (this.areExpressionsDisabled) {
|
if (this.areExpressionsDisabled) {
|
||||||
return this.value;
|
return this.value;
|
||||||
|
@ -513,6 +539,10 @@ export default mixins(
|
||||||
return 'textarea';
|
return 'textarea';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.parameter.type === 'code') {
|
||||||
|
return 'textarea';
|
||||||
|
}
|
||||||
|
|
||||||
return 'text';
|
return 'text';
|
||||||
},
|
},
|
||||||
getIssues (): string[] {
|
getIssues (): string[] {
|
||||||
|
@ -581,9 +611,6 @@ export default mixins(
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
isDefault (): boolean {
|
|
||||||
return this.parameter.default === this.value;
|
|
||||||
},
|
|
||||||
isEditor (): boolean {
|
isEditor (): boolean {
|
||||||
return ['code', 'json'].includes(this.editorType);
|
return ['code', 'json'].includes(this.editorType);
|
||||||
},
|
},
|
||||||
|
@ -609,20 +636,26 @@ export default mixins(
|
||||||
return this.remoteParameterOptions;
|
return this.remoteParameterOptions;
|
||||||
},
|
},
|
||||||
parameterInputClasses () {
|
parameterInputClasses () {
|
||||||
const classes = [];
|
const classes: {[c: string]: boolean} = {
|
||||||
|
droppable: this.droppable,
|
||||||
|
activeDrop: this.activeDrop,
|
||||||
|
};
|
||||||
|
|
||||||
const rows = this.getArgument('rows');
|
const rows = this.getArgument('rows');
|
||||||
const isTextarea = this.parameter.type === 'string' && rows !== undefined;
|
const isTextarea = this.parameter.type === 'string' && rows !== undefined;
|
||||||
const isSwitch = this.parameter.type === 'boolean' && !this.isValueExpression;
|
const isSwitch = this.parameter.type === 'boolean' && !this.isValueExpression;
|
||||||
|
|
||||||
if (!isTextarea && !isSwitch) {
|
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) {
|
if (!this.droppable && !this.activeDrop && (this.getIssues.length || this.errorHighlight)) {
|
||||||
classes.push('has-issues');
|
classes['has-issues'] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return classes;
|
return classes;
|
||||||
},
|
},
|
||||||
parameterInputWrapperStyle () {
|
parameterInputWrapperStyle () {
|
||||||
|
@ -633,9 +666,6 @@ export default mixins(
|
||||||
if (this.parameter.type === 'credentialsSelect') {
|
if (this.parameter.type === 'credentialsSelect') {
|
||||||
return styles;
|
return styles;
|
||||||
}
|
}
|
||||||
if (this.displayOptionsComputed === true) {
|
|
||||||
deductWidth += 25;
|
|
||||||
}
|
|
||||||
if (this.getIssues.length) {
|
if (this.getIssues.length) {
|
||||||
deductWidth += 20;
|
deductWidth += 20;
|
||||||
}
|
}
|
||||||
|
@ -866,8 +896,12 @@ export default mixins(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
optionSelected (command: string) {
|
optionSelected (command: string) {
|
||||||
|
const prevValue = this.value;
|
||||||
|
|
||||||
if (command === 'resetValue') {
|
if (command === 'resetValue') {
|
||||||
this.valueChanged(this.parameter.default);
|
this.valueChanged(this.parameter.default);
|
||||||
|
} else if (command === 'openExpression') {
|
||||||
|
this.expressionEditDialogVisible = true;
|
||||||
} else if (command === 'addExpression') {
|
} else if (command === 'addExpression') {
|
||||||
if (this.parameter.type === 'number' || this.parameter.type === 'boolean') {
|
if (this.parameter.type === 'number' || this.parameter.type === 'boolean') {
|
||||||
this.valueChanged(`={{${this.value}}}`);
|
this.valueChanged(`={{${this.value}}}`);
|
||||||
|
@ -876,8 +910,10 @@ export default mixins(
|
||||||
this.valueChanged(`=${this.value}`);
|
this.valueChanged(`=${this.value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.expressionEditDialogVisible = true;
|
setTimeout(() => {
|
||||||
this.trackExpressionEditOpen();
|
this.expressionEditDialogVisible = true;
|
||||||
|
this.trackExpressionEditOpen();
|
||||||
|
}, 375);
|
||||||
} else if (command === 'removeExpression') {
|
} else if (command === 'removeExpression') {
|
||||||
let value = this.expressionValueComputed;
|
let value = this.expressionValueComputed;
|
||||||
|
|
||||||
|
@ -890,9 +926,23 @@ export default mixins(
|
||||||
} else if (command === 'refreshOptions') {
|
} else if (command === 'refreshOptions') {
|
||||||
this.loadRemoteParameterOptions();
|
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 () {
|
mounted () {
|
||||||
|
this.$on('optionSelected', this.optionSelected);
|
||||||
|
|
||||||
this.tempValue = this.displayValue as string;
|
this.tempValue = this.displayValue as string;
|
||||||
if (this.node !== null) {
|
if (this.node !== null) {
|
||||||
this.nodeName = this.node.name;
|
this.nodeName = this.node.name;
|
||||||
|
@ -986,18 +1036,30 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
.expression {
|
.expression {
|
||||||
textarea[disabled], input[disabled] {
|
textarea, input {
|
||||||
cursor: pointer !important;
|
cursor: pointer !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-switch__core {
|
--input-border-color: var(--color-secondary-tint-1);
|
||||||
border: 1px dashed $--custom-expression-text;
|
--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-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 {
|
.has-issues {
|
||||||
|
|
|
@ -4,38 +4,53 @@
|
||||||
:tooltipText="$locale.credText().inputLabelDescription(parameter)"
|
:tooltipText="$locale.credText().inputLabelDescription(parameter)"
|
||||||
:required="parameter.required"
|
:required="parameter.required"
|
||||||
:showTooltip="focused"
|
:showTooltip="focused"
|
||||||
|
:showOptions="menuExpanded"
|
||||||
>
|
>
|
||||||
<parameter-input
|
<template #options>
|
||||||
:parameter="parameter"
|
<parameter-options
|
||||||
:value="value"
|
:parameter="parameter"
|
||||||
:path="parameter.name"
|
:value="value"
|
||||||
:hideIssues="true"
|
:isReadOnly="false"
|
||||||
:displayOptions="true"
|
:showOptions="true"
|
||||||
:documentationUrl="documentationUrl"
|
@optionSelected="optionSelected"
|
||||||
:errorHighlight="showRequiredErrors"
|
@menu-expanded="onMenuExpanded"
|
||||||
:isForCredential="true"
|
/>
|
||||||
@focus="onFocus"
|
</template>
|
||||||
@blur="onBlur"
|
<template>
|
||||||
@textInput="valueChanged"
|
<parameter-input
|
||||||
@valueChanged="valueChanged"
|
ref="param"
|
||||||
inputSize="large"
|
inputSize="large"
|
||||||
:eventSource="eventSource"
|
:parameter="parameter"
|
||||||
/>
|
:value="value"
|
||||||
<div :class="$style.errors" v-if="showRequiredErrors">
|
:path="parameter.name"
|
||||||
<n8n-text color="danger" size="small">
|
:hideIssues="true"
|
||||||
{{ $locale.baseText('parameterInputExpanded.thisFieldIsRequired') }}
|
:displayOptions="true"
|
||||||
<n8n-link v-if="documentationUrl" :to="documentationUrl" size="small" :underline="true" @click="onDocumentationUrlClick">
|
:documentationUrl="documentationUrl"
|
||||||
{{ $locale.baseText('parameterInputExpanded.openDocs') }}
|
:errorHighlight="showRequiredErrors"
|
||||||
</n8n-link>
|
:isForCredential="true"
|
||||||
</n8n-text>
|
:eventSource="eventSource"
|
||||||
</div>
|
@focus="onFocus"
|
||||||
<input-hint :class="$style.hint" :hint="$locale.credText().hint(parameter)" />
|
@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>
|
</n8n-input-label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { IUpdateInformation } from '@/Interface';
|
import { IUpdateInformation } from '@/Interface';
|
||||||
import ParameterInput from './ParameterInput.vue';
|
import ParameterInput from './ParameterInput.vue';
|
||||||
|
import ParameterOptions from './ParameterOptions.vue';
|
||||||
import InputHint from './ParameterInputHint.vue';
|
import InputHint from './ParameterInputHint.vue';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
@ -44,6 +59,7 @@ export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
ParameterInput,
|
ParameterInput,
|
||||||
InputHint,
|
InputHint,
|
||||||
|
ParameterOptions,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
parameter: {
|
parameter: {
|
||||||
|
@ -64,6 +80,7 @@ export default Vue.extend({
|
||||||
return {
|
return {
|
||||||
focused: false,
|
focused: false,
|
||||||
blurredEver: false,
|
blurredEver: false,
|
||||||
|
menuExpanded: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -93,6 +110,14 @@ export default Vue.extend({
|
||||||
this.blurredEver = true;
|
this.blurredEver = true;
|
||||||
this.focused = false;
|
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) {
|
valueChanged(parameterData: IUpdateInformation) {
|
||||||
this.$emit('change', parameterData);
|
this.$emit('change', parameterData);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,22 +1,43 @@
|
||||||
<template>
|
<template>
|
||||||
<n8n-input-label
|
<n8n-input-label
|
||||||
:label="$locale.nodeText().inputLabelDisplayName(parameter, path)"
|
:label="hideLabel? '': $locale.nodeText().inputLabelDisplayName(parameter, path)"
|
||||||
:tooltipText="$locale.nodeText().inputLabelDescription(parameter, path)"
|
:tooltipText="hideLabel? '': $locale.nodeText().inputLabelDescription(parameter, path)"
|
||||||
:showTooltip="focused"
|
:showTooltip="focused"
|
||||||
|
:showOptions="menuExpanded || focused || forceShowExpression"
|
||||||
:bold="false"
|
:bold="false"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
<parameter-input
|
<template #options>
|
||||||
:parameter="parameter"
|
<parameter-options
|
||||||
:value="value"
|
:parameter="parameter"
|
||||||
:displayOptions="displayOptions"
|
:value="value"
|
||||||
:path="path"
|
:isReadOnly="isReadOnly"
|
||||||
:isReadOnly="isReadOnly"
|
:showOptions="displayOptions"
|
||||||
@valueChanged="valueChanged"
|
@optionSelected="optionSelected"
|
||||||
@focus="focused = true"
|
@menu-expanded="onMenuExpanded"
|
||||||
@blur="focused = false"
|
/>
|
||||||
inputSize="small" />
|
</template>
|
||||||
<input-hint :class="$style.hint" :hint="$locale.nodeText().hint(parameter, path)" />
|
<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>
|
</n8n-input-label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -24,22 +45,35 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
INodeUi,
|
||||||
IUpdateInformation,
|
IUpdateInformation,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
|
|
||||||
import ParameterInput from '@/components/ParameterInput.vue';
|
import ParameterInput from '@/components/ParameterInput.vue';
|
||||||
import InputHint from './ParameterInputHint.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({
|
.extend({
|
||||||
name: 'ParameterInputFull',
|
name: 'ParameterInputFull',
|
||||||
components: {
|
components: {
|
||||||
ParameterInput,
|
ParameterInput,
|
||||||
InputHint,
|
InputHint,
|
||||||
|
ParameterOptions,
|
||||||
|
DraggableTarget,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
focused: false,
|
focused: false,
|
||||||
|
menuExpanded: false,
|
||||||
|
forceShowExpression: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
|
@ -48,22 +82,80 @@ export default Vue
|
||||||
'parameter',
|
'parameter',
|
||||||
'path',
|
'path',
|
||||||
'value',
|
'value',
|
||||||
|
'hideLabel',
|
||||||
],
|
],
|
||||||
|
computed: {
|
||||||
|
node (): INodeUi | null {
|
||||||
|
return this.$store.getters.activeNode;
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getArgument (argumentName: string): string | number | boolean | undefined {
|
onFocus() {
|
||||||
if (this.parameter.typeOptions === undefined) {
|
this.focused = true;
|
||||||
return undefined;
|
if (!this.parameter.noDataExpression) {
|
||||||
|
this.$store.commit('ui/setMappableNDVInputFocus', this.parameter.displayName);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
if (this.parameter.typeOptions[argumentName] === undefined) {
|
onBlur() {
|
||||||
return undefined;
|
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) {
|
valueChanged (parameterData: IUpdateInformation) {
|
||||||
this.$emit('valueChanged', parameterData);
|
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>
|
</script>
|
||||||
|
|
|
@ -40,25 +40,23 @@
|
||||||
:tooltipText="$locale.nodeText().inputLabelDescription(parameter, path)"
|
:tooltipText="$locale.nodeText().inputLabelDescription(parameter, path)"
|
||||||
size="small"
|
size="small"
|
||||||
:underline="true"
|
:underline="true"
|
||||||
:labelHoverableOnly="true"
|
/>
|
||||||
>
|
<collection-parameter
|
||||||
<collection-parameter
|
v-if="parameter.type === 'collection'"
|
||||||
v-if="parameter.type === 'collection'"
|
:parameter="parameter"
|
||||||
:parameter="parameter"
|
:values="getParameterValue(nodeValues, parameter.name, path)"
|
||||||
:values="getParameterValue(nodeValues, parameter.name, path)"
|
:nodeValues="nodeValues"
|
||||||
:nodeValues="nodeValues"
|
:path="getPath(parameter.name)"
|
||||||
:path="getPath(parameter.name)"
|
@valueChanged="valueChanged"
|
||||||
@valueChanged="valueChanged"
|
/>
|
||||||
/>
|
<fixed-collection-parameter
|
||||||
<fixed-collection-parameter
|
v-else-if="parameter.type === 'fixedCollection'"
|
||||||
v-else-if="parameter.type === 'fixedCollection'"
|
:parameter="parameter"
|
||||||
:parameter="parameter"
|
:values="getParameterValue(nodeValues, parameter.name, path)"
|
||||||
:values="getParameterValue(nodeValues, parameter.name, path)"
|
:nodeValues="nodeValues"
|
||||||
:nodeValues="nodeValues"
|
:path="getPath(parameter.name)"
|
||||||
:path="getPath(parameter.name)"
|
@valueChanged="valueChanged"
|
||||||
@valueChanged="valueChanged"
|
/>
|
||||||
/>
|
|
||||||
</n8n-input-label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="displayNodeParameter(parameter)" class="parameter-item">
|
<div v-else-if="displayNodeParameter(parameter)" class="parameter-item">
|
||||||
|
|
|
@ -1,45 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style['parameter-options']">
|
<div :class="$style.container">
|
||||||
<el-dropdown
|
<n8n-action-toggle
|
||||||
trigger="click"
|
v-if="shouldShowOptions"
|
||||||
@command="(opt) => $emit('optionSelected', opt)"
|
placement="bottom-end"
|
||||||
size="mini"
|
size="small"
|
||||||
>
|
color="foreground-xdark"
|
||||||
<span class="el-dropdown-link">
|
iconSize="small"
|
||||||
<font-awesome-icon
|
:actions="actions"
|
||||||
icon="cogs"
|
@action="(action) => $emit('optionSelected', action)"
|
||||||
class="reset-icon clickable"
|
@visible-change="onMenuToggle"
|
||||||
:title="$locale.baseText('parameterInput.parameterOptions')"
|
/>
|
||||||
/>
|
<n8n-radio-buttons
|
||||||
</span>
|
v-if="parameter.noDataExpression !== true"
|
||||||
<el-dropdown-menu slot="dropdown">
|
size="small"
|
||||||
<el-dropdown-item
|
:value="selectedView"
|
||||||
v-if="parameter.noDataExpression !== true && !isValueExpression"
|
:disabled="isReadOnly"
|
||||||
command="addExpression"
|
@input="onViewSelected"
|
||||||
>
|
:options="[
|
||||||
{{ $locale.baseText('parameterInput.addExpression') }}
|
{ label: $locale.baseText('parameterInput.fixed'), value: 'fixed'},
|
||||||
</el-dropdown-item>
|
{ label: $locale.baseText('parameterInput.expression'), value: 'expression'},
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -49,20 +30,101 @@ import Vue from 'vue';
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'ParameterOptions',
|
name: 'ParameterOptions',
|
||||||
props: [
|
props: [
|
||||||
'displayOptionsComputed',
|
|
||||||
'optionSelected',
|
|
||||||
'parameter',
|
'parameter',
|
||||||
'isValueExpression',
|
'isReadOnly',
|
||||||
'isDefault',
|
'value',
|
||||||
'hasRemoteMethod',
|
'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>
|
</script>
|
||||||
|
|
||||||
<style module lang="scss">
|
<style lang="scss" module>
|
||||||
.parameter-options {
|
.container {
|
||||||
width: 25px;
|
display: flex;
|
||||||
text-align: right;
|
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -102,28 +102,8 @@
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</div>
|
</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">
|
<div v-else-if="hasNodeRun && displayMode === 'table' && tableData" :class="$style.dataDisplay">
|
||||||
<table :class="$style.table">
|
<RunDataTable :node="node" :tableData="tableData" :mappingEnabled="mappingEnabled" :distanceFromActive="distanceFromActive" :showMappingHint="showMappingHint" :runIndex="runIndex" :totalRuns="maxRunIndex" />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="hasNodeRun && displayMode === 'json'" :class="$style.jsonDisplay">
|
<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 mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
|
import RunDataTable from './RunDataTable.vue';
|
||||||
|
|
||||||
// A path that does not exist so that nothing is selected by default
|
// A path that does not exist so that nothing is selected by default
|
||||||
const deselectedPlaceholder = '_!^&*';
|
const deselectedPlaceholder = '_!^&*';
|
||||||
|
@ -278,6 +259,7 @@ export default mixins(
|
||||||
NodeErrorView,
|
NodeErrorView,
|
||||||
VueJsonPretty,
|
VueJsonPretty,
|
||||||
WarningTooltip,
|
WarningTooltip,
|
||||||
|
RunDataTable,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
nodeUi: {
|
nodeUi: {
|
||||||
|
@ -312,6 +294,15 @@ export default mixins(
|
||||||
overrideOutputs: {
|
overrideOutputs: {
|
||||||
type: Array,
|
type: Array,
|
||||||
},
|
},
|
||||||
|
mappingEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
distanceFromActive: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
showMappingHint: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -924,41 +915,6 @@ export default mixins(
|
||||||
margin-bottom: var(--spacing-s);
|
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 {
|
.itemsCount {
|
||||||
margin-left: var(--spacing-s);
|
margin-left: var(--spacing-s);
|
||||||
margin-bottom: 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">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
|
import { shorten } from "./helpers";
|
||||||
|
|
||||||
const DEFAULT_WORKFLOW_NAME_LIMIT = 25;
|
const DEFAULT_WORKFLOW_NAME_LIMIT = 25;
|
||||||
const WORKFLOW_NAME_END_COUNT_TO_KEEP = 4;
|
const WORKFLOW_NAME_END_COUNT_TO_KEEP = 4;
|
||||||
|
@ -15,17 +16,7 @@ export default Vue.extend({
|
||||||
props: ["name", "limit"],
|
props: ["name", "limit"],
|
||||||
computed: {
|
computed: {
|
||||||
shortenedName(): string {
|
shortenedName(): string {
|
||||||
const name = this.$props.name;
|
return shorten(this.name, this.limit || DEFAULT_WORKFLOW_NAME_LIMIT, WORKFLOW_NAME_END_COUNT_TO_KEEP);
|
||||||
|
|
||||||
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}`;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 { INodeUi, ITemplatesNode } from '@/Interface';
|
||||||
import dateformat from 'dateformat';
|
import dateformat from 'dateformat';
|
||||||
import { INodeTypeDescription } from 'n8n-workflow';
|
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 {
|
export function isNumber(value: unknown): value is number {
|
||||||
return typeof value === '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 HTTP_REQUEST_NODE_TYPE = 'n8n-nodes-base.httpRequest';
|
||||||
export const HUBSPOT_TRIGGER_NODE_TYPE = 'n8n-nodes-base.hubspotTrigger';
|
export const HUBSPOT_TRIGGER_NODE_TYPE = 'n8n-nodes-base.hubspotTrigger';
|
||||||
export const IF_NODE_TYPE = 'n8n-nodes-base.if';
|
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 ITEM_LISTS_NODE_TYPE = 'n8n-nodes-base.itemLists';
|
||||||
export const JIRA_NODE_TYPE = 'n8n-nodes-base.jira';
|
export const JIRA_NODE_TYPE = 'n8n-nodes-base.jira';
|
||||||
export const JIRA_TRIGGER_NODE_TYPE = 'n8n-nodes-base.jiraTrigger';
|
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 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_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 BASE_NODE_SURVEY_URL = 'https://n8n-community.typeform.com/to/BvmzxqYv#nodename=';
|
||||||
|
|
||||||
export const HIRING_BANNER = `
|
export const HIRING_BANNER = `
|
||||||
|
@ -242,3 +244,5 @@ export enum VIEWS {
|
||||||
API_SETTINGS = "APISettings",
|
API_SETTINGS = "APISettings",
|
||||||
NOT_FOUND = "NotFoundView",
|
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,
|
IRootState,
|
||||||
IRunDataDisplayMode,
|
IRunDataDisplayMode,
|
||||||
IUiState,
|
IUiState,
|
||||||
|
XYPosition,
|
||||||
} from '../Interface';
|
} from '../Interface';
|
||||||
|
|
||||||
const module: Module<IUiState, IRootState> = {
|
const module: Module<IUiState, IRootState> = {
|
||||||
|
@ -97,8 +98,17 @@ const module: Module<IUiState, IRootState> = {
|
||||||
output: {
|
output: {
|
||||||
displayMode: 'table',
|
displayMode: 'table',
|
||||||
},
|
},
|
||||||
|
focusedMappableInput: '',
|
||||||
|
mappingTelemetry: {},
|
||||||
},
|
},
|
||||||
mainPanelPosition: 0.5,
|
mainPanelPosition: 0.5,
|
||||||
|
draggable: {
|
||||||
|
isDragging: false,
|
||||||
|
type: '',
|
||||||
|
data: '',
|
||||||
|
canDrop: false,
|
||||||
|
stickyPosition: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
areExpressionsDisabled(state: IUiState) {
|
areExpressionsDisabled(state: IUiState) {
|
||||||
|
@ -127,6 +137,13 @@ const module: Module<IUiState, IRootState> = {
|
||||||
inputPanelDispalyMode: (state: IUiState) => state.ndv.input.displayMode,
|
inputPanelDispalyMode: (state: IUiState) => state.ndv.input.displayMode,
|
||||||
outputPanelDispalyMode: (state: IUiState) => state.ndv.output.displayMode,
|
outputPanelDispalyMode: (state: IUiState) => state.ndv.output.displayMode,
|
||||||
mainPanelPosition: (state: IUiState) => state.mainPanelPosition,
|
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: {
|
mutations: {
|
||||||
setMode: (state: IUiState, params: {name: string, mode: string}) => {
|
setMode: (state: IUiState, params: {name: string, mode: string}) => {
|
||||||
|
@ -173,7 +190,39 @@ const module: Module<IUiState, IRootState> = {
|
||||||
setMainPanelRelativePosition(state: IUiState, relativePosition: number) {
|
setMainPanelRelativePosition(state: IUiState, relativePosition: number) {
|
||||||
state.mainPanelPosition = relativePosition;
|
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: {
|
actions: {
|
||||||
openModal: async (context: ActionContext<IUiState, IRootState>, modalKey: string) => {
|
openModal: async (context: ActionContext<IUiState, IRootState>, modalKey: string) => {
|
||||||
|
|
|
@ -152,6 +152,12 @@
|
||||||
"dataDisplay.needHelp": "Need help?",
|
"dataDisplay.needHelp": "Need help?",
|
||||||
"dataDisplay.nodeDocumentation": "Node Documentation",
|
"dataDisplay.nodeDocumentation": "Node Documentation",
|
||||||
"dataDisplay.openDocumentationFor": "Open {nodeTypeDisplayName} 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.cancelEdit": "Cancel Edit",
|
||||||
"displayWithChange.clickToChange": "Click to Change",
|
"displayWithChange.clickToChange": "Click to Change",
|
||||||
"displayWithChange.setValue": "Set Value",
|
"displayWithChange.setValue": "Set Value",
|
||||||
|
@ -521,9 +527,10 @@
|
||||||
"onboardingWorkflow.stickyContent": "## 👇 Get started faster \nLightning tour of the key concepts \n\n[](https://www.youtube.com/watch?v=RpjQTGKm-ok)",
|
"onboardingWorkflow.stickyContent": "## 👇 Get started faster \nLightning tour of the key concepts \n\n[](https://www.youtube.com/watch?v=RpjQTGKm-ok)",
|
||||||
"openWorkflow.workflowImportError": "Could not import workflow",
|
"openWorkflow.workflowImportError": "Could not import workflow",
|
||||||
"openWorkflow.workflowNotFoundError": "Could not find workflow",
|
"openWorkflow.workflowNotFoundError": "Could not find workflow",
|
||||||
"parameterInput.addExpression": "Add Expression",
|
|
||||||
"parameterInput.customApiCall": "Custom API Call",
|
"parameterInput.customApiCall": "Custom API Call",
|
||||||
"parameterInput.error": "ERROR",
|
"parameterInput.error": "ERROR",
|
||||||
|
"parameterInput.expression": "Expression",
|
||||||
|
"parameterInput.fixed": "Fixed",
|
||||||
"parameterInput.issues": "Issues",
|
"parameterInput.issues": "Issues",
|
||||||
"parameterInput.loadingOptions": "Loading options...",
|
"parameterInput.loadingOptions": "Loading options...",
|
||||||
"parameterInput.openEditWindow": "Open Edit Window",
|
"parameterInput.openEditWindow": "Open Edit Window",
|
||||||
|
@ -531,9 +538,7 @@
|
||||||
"parameterInput.parameterHasExpression": "Parameter: \"{shortPath}\" has an expression",
|
"parameterInput.parameterHasExpression": "Parameter: \"{shortPath}\" has an expression",
|
||||||
"parameterInput.parameterHasIssues": "Parameter: \"{shortPath}\" has issues",
|
"parameterInput.parameterHasIssues": "Parameter: \"{shortPath}\" has issues",
|
||||||
"parameterInput.parameterHasIssuesAndExpression": "Parameter: \"{shortPath}\" has issues and an expression",
|
"parameterInput.parameterHasIssuesAndExpression": "Parameter: \"{shortPath}\" has issues and an expression",
|
||||||
"parameterInput.parameterOptions": "Parameter Options",
|
|
||||||
"parameterInput.refreshList": "Refresh List",
|
"parameterInput.refreshList": "Refresh List",
|
||||||
"parameterInput.removeExpression": "Remove Expression",
|
|
||||||
"parameterInput.resetValue": "Reset Value",
|
"parameterInput.resetValue": "Reset Value",
|
||||||
"parameterInput.select": "Select",
|
"parameterInput.select": "Select",
|
||||||
"parameterInput.selectDateAndTime": "Select date and time",
|
"parameterInput.selectDateAndTime": "Select date and time",
|
||||||
|
|
|
@ -48,6 +48,7 @@ import {
|
||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
faGift,
|
faGift,
|
||||||
faGraduationCap,
|
faGraduationCap,
|
||||||
|
faGripVertical,
|
||||||
faHdd,
|
faHdd,
|
||||||
faHome,
|
faHome,
|
||||||
faHourglass,
|
faHourglass,
|
||||||
|
@ -137,6 +138,7 @@ addIcon(faCloudDownloadAlt);
|
||||||
addIcon(faCopy);
|
addIcon(faCopy);
|
||||||
addIcon(faCut);
|
addIcon(faCut);
|
||||||
addIcon(faDotCircle);
|
addIcon(faDotCircle);
|
||||||
|
addIcon(faGripVertical);
|
||||||
addIcon(faEdit);
|
addIcon(faEdit);
|
||||||
addIcon(faEllipsisV);
|
addIcon(faEllipsisV);
|
||||||
addIcon(faEnvelope);
|
addIcon(faEnvelope);
|
||||||
|
|
Loading…
Reference in a new issue