feat(editor): Help users discover expressions when using drag n drop (#8869)
This commit is contained in:
@@ -58,7 +58,7 @@ const assignmentTypeToNodeProperty = (
|
||||
|
||||
const nameParameter = computed<INodeProperties>(() => ({
|
||||
name: 'name',
|
||||
displayName: '',
|
||||
displayName: 'Name',
|
||||
default: '',
|
||||
requiresDataPath: 'single',
|
||||
placeholder: 'name',
|
||||
@@ -68,7 +68,7 @@ const nameParameter = computed<INodeProperties>(() => ({
|
||||
const valueParameter = computed<INodeProperties>(() => {
|
||||
return {
|
||||
name: 'value',
|
||||
displayName: '',
|
||||
displayName: 'Value',
|
||||
default: '',
|
||||
placeholder: 'value',
|
||||
...assignmentTypeToNodeProperty(assignment.value.type ?? 'string'),
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
:rows="rows"
|
||||
:additional-data="additionalExpressionData"
|
||||
:path="path"
|
||||
:event-bus="eventBus"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@change="onChange"
|
||||
@@ -64,6 +65,7 @@ import type { Segment } from '@/types/expressions';
|
||||
import type { TargetItem } from '@/Interface';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
import { type EventBus, createEventBus } from 'n8n-design-system/utils';
|
||||
|
||||
type InlineExpressionEditorInputRef = InstanceType<typeof InlineExpressionEditorInput>;
|
||||
|
||||
@@ -97,6 +99,10 @@ export default defineComponent({
|
||||
type: Object as PropType<IDataObject>,
|
||||
default: () => ({}),
|
||||
},
|
||||
eventBus: {
|
||||
type: Object as PropType<EventBus>,
|
||||
default: () => createEventBus(),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { callDebounced } = useDebounce();
|
||||
|
||||
@@ -3,27 +3,30 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { completionStatus, startCompletion } from '@codemirror/autocomplete';
|
||||
import { history } from '@codemirror/commands';
|
||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||
import { EditorView, keymap } from '@codemirror/view';
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { defineComponent, nextTick } from 'vue';
|
||||
|
||||
import { completionManager } from '@/mixins/completionManager';
|
||||
import { expressionManager } from '@/mixins/expressionManager';
|
||||
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
|
||||
import { n8nAutocompletion, n8nLang } from '@/plugins/codemirror/n8nLang';
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import { inputTheme } from './theme';
|
||||
import {
|
||||
autocompleteKeyMap,
|
||||
enterKeyMap,
|
||||
historyKeyMap,
|
||||
tabKeyMap,
|
||||
} from '@/plugins/codemirror/keymap';
|
||||
import { completionStatus } from '@codemirror/autocomplete';
|
||||
import { n8nAutocompletion, n8nLang } from '@/plugins/codemirror/n8nLang';
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { createEventBus, type EventBus } from 'n8n-design-system/utils';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import { inputTheme } from './theme';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { mapStores } from 'pinia';
|
||||
|
||||
const editableConf = new Compartment();
|
||||
|
||||
@@ -51,6 +54,13 @@ export default defineComponent({
|
||||
type: Object as PropType<IDataObject>,
|
||||
default: () => ({}),
|
||||
},
|
||||
eventBus: {
|
||||
type: Object as PropType<EventBus>,
|
||||
default: () => createEventBus(),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNDVStore),
|
||||
},
|
||||
watch: {
|
||||
isReadOnly(newValue: boolean) {
|
||||
@@ -132,14 +142,34 @@ export default defineComponent({
|
||||
this.editorState = this.editor.state;
|
||||
|
||||
highlighter.addColor(this.editor, this.resolvableSegments);
|
||||
|
||||
this.eventBus.on('drop', this.onDrop);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.editor?.destroy();
|
||||
this.eventBus.off('drop', this.onDrop);
|
||||
},
|
||||
methods: {
|
||||
focus() {
|
||||
this.editor?.focus();
|
||||
},
|
||||
setCursorPosition(pos: number) {
|
||||
this.editor.dispatch({ selection: { anchor: pos, head: pos } });
|
||||
},
|
||||
async onDrop() {
|
||||
await nextTick();
|
||||
this.focus();
|
||||
|
||||
const END_OF_EXPRESSION = ' }}';
|
||||
const value = this.editor.state.sliceDoc(0);
|
||||
const cursorPosition = Math.max(value.lastIndexOf(END_OF_EXPRESSION), 0);
|
||||
|
||||
this.setCursorPosition(cursorPosition);
|
||||
|
||||
if (!this.ndvStore.isAutocompleteOnboarded) {
|
||||
startCompletion(this.editor as EditorView);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,130 +7,111 @@
|
||||
<div ref="root" data-test-id="inline-expression-editor-output"></div>
|
||||
</n8n-text>
|
||||
<div :class="$style.footer">
|
||||
<n8n-text size="small" compact>
|
||||
{{ i18n.baseText('parameterInput.anythingInside') }}
|
||||
</n8n-text>
|
||||
<div :class="$style['expression-syntax-example']" v-text="`{{ }}`"></div>
|
||||
<n8n-text size="small" compact>
|
||||
{{ i18n.baseText('parameterInput.isJavaScript') }}
|
||||
</n8n-text>
|
||||
{{ ' ' }}
|
||||
<n8n-link
|
||||
:class="$style['learn-more']"
|
||||
size="small"
|
||||
underline
|
||||
theme="text"
|
||||
:to="expressionsDocsUrl"
|
||||
>
|
||||
{{ i18n.baseText('parameterInput.learnMore') }}
|
||||
</n8n-link>
|
||||
<InlineExpressionTip />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
import { outputTheme } from './theme';
|
||||
|
||||
import type { Plaintext, Resolved, Segment } from '@/types/expressions';
|
||||
import { EXPRESSIONS_DOCS_URL } from '@/constants';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import type { Plaintext, Resolved, Segment } from '@/types/expressions';
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
import { computed, onMounted, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { outputTheme } from './theme';
|
||||
import InlineExpressionTip from './InlineExpressionTip.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'InlineExpressionEditorOutput',
|
||||
props: {
|
||||
segments: {
|
||||
type: Array as PropType<Segment[]>,
|
||||
required: true,
|
||||
},
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
noInputData: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hoveringItemNumber: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const i18n = useI18n();
|
||||
interface InlineExpressionEditorOutputProps {
|
||||
segments: Segment[];
|
||||
hoveringItemNumber: number;
|
||||
isReadOnly?: boolean;
|
||||
visible?: boolean;
|
||||
noInputData?: boolean;
|
||||
}
|
||||
|
||||
return {
|
||||
i18n,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editor: null as EditorView | null,
|
||||
expressionsDocsUrl: EXPRESSIONS_DOCS_URL,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
resolvedExpression(): string {
|
||||
return this.segments.reduce((acc, segment) => {
|
||||
acc += segment.kind === 'resolvable' ? segment.resolved : segment.plaintext;
|
||||
return acc;
|
||||
}, '');
|
||||
},
|
||||
plaintextSegments(): Plaintext[] {
|
||||
return this.segments.filter((s): s is Plaintext => s.kind === 'plaintext');
|
||||
},
|
||||
resolvedSegments(): Resolved[] {
|
||||
let cursor = 0;
|
||||
const props = withDefaults(defineProps<InlineExpressionEditorOutputProps>(), {
|
||||
readOnly: false,
|
||||
visible: false,
|
||||
noInputData: false,
|
||||
});
|
||||
|
||||
return this.segments
|
||||
.map((segment) => {
|
||||
segment.from = cursor;
|
||||
cursor +=
|
||||
segment.kind === 'plaintext'
|
||||
? segment.plaintext.length
|
||||
: segment.resolved
|
||||
? segment.resolved.toString().length
|
||||
: 0;
|
||||
segment.to = cursor;
|
||||
return segment;
|
||||
})
|
||||
.filter((segment): segment is Resolved => segment.kind === 'resolvable');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
segments() {
|
||||
if (!this.editor) return;
|
||||
const i18n = useI18n();
|
||||
|
||||
this.editor.dispatch({
|
||||
changes: { from: 0, to: this.editor.state.doc.length, insert: this.resolvedExpression },
|
||||
});
|
||||
const editor = ref<EditorView | null>(null);
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
|
||||
highlighter.addColor(this.editor, this.resolvedSegments);
|
||||
highlighter.removeColor(this.editor, this.plaintextSegments);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.editor = new EditorView({
|
||||
parent: this.$refs.root as HTMLDivElement,
|
||||
state: EditorState.create({
|
||||
doc: this.resolvedExpression,
|
||||
extensions: [outputTheme(), EditorState.readOnly.of(true), EditorView.lineWrapping],
|
||||
}),
|
||||
const resolvedExpression = computed(() => {
|
||||
if (props.segments.length === 0) {
|
||||
return i18n.baseText('parameterInput.emptyString');
|
||||
}
|
||||
|
||||
return props.segments.reduce((acc, segment) => {
|
||||
acc += segment.kind === 'resolvable' ? (segment.resolved as string) : segment.plaintext;
|
||||
return acc;
|
||||
}, '');
|
||||
});
|
||||
|
||||
const plaintextSegments = computed<Plaintext[]>(() => {
|
||||
if (props.segments.length === 0) {
|
||||
return [
|
||||
{
|
||||
from: 0,
|
||||
to: resolvedExpression.value.length - 1,
|
||||
plaintext: resolvedExpression.value,
|
||||
kind: 'plaintext',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return props.segments.filter((s): s is Plaintext => s.kind === 'plaintext');
|
||||
});
|
||||
|
||||
const resolvedSegments = computed<Resolved[]>(() => {
|
||||
let cursor = 0;
|
||||
|
||||
return props.segments
|
||||
.map((segment) => {
|
||||
segment.from = cursor;
|
||||
cursor +=
|
||||
segment.kind === 'plaintext'
|
||||
? segment.plaintext.length
|
||||
: segment.resolved
|
||||
? (segment.resolved as string | number | boolean).toString().length
|
||||
: 0;
|
||||
segment.to = cursor;
|
||||
return segment;
|
||||
})
|
||||
.filter((segment): segment is Resolved => segment.kind === 'resolvable');
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.segments,
|
||||
() => {
|
||||
if (!editor.value) return;
|
||||
|
||||
editor.value.dispatch({
|
||||
changes: { from: 0, to: editor.value.state.doc.length, insert: resolvedExpression.value },
|
||||
});
|
||||
|
||||
highlighter.addColor(editor.value as EditorView, resolvedSegments.value);
|
||||
highlighter.removeColor(editor.value as EditorView, plaintextSegments.value);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.editor?.destroy();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
editor.value = new EditorView({
|
||||
parent: root.value as HTMLElement,
|
||||
state: EditorState.create({
|
||||
doc: resolvedExpression.value,
|
||||
extensions: [outputTheme(), EditorState.readOnly.of(true), EditorView.lineWrapping],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor.value?.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -157,11 +138,14 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.header,
|
||||
.body,
|
||||
.footer {
|
||||
.body {
|
||||
padding: var(--spacing-3xs);
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: var(--border-base);
|
||||
}
|
||||
|
||||
.header {
|
||||
color: var(--color-text-dark);
|
||||
font-weight: var(--font-weight-bold);
|
||||
@@ -178,28 +162,5 @@ export default defineComponent({
|
||||
padding-top: var(--spacing-2xs);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: var(--border-base);
|
||||
padding: var(--spacing-4xs);
|
||||
padding-left: var(--spacing-2xs);
|
||||
padding-top: 0;
|
||||
line-height: var(--font-line-height-regular);
|
||||
color: var(--color-text-base);
|
||||
|
||||
.expression-syntax-example {
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-2xs);
|
||||
height: var(--font-size-m);
|
||||
background-color: var(--color-expression-syntax-example);
|
||||
margin-left: var(--spacing-5xs);
|
||||
margin-right: var(--spacing-5xs);
|
||||
}
|
||||
|
||||
.learn-more {
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div v-if="tip === 'drag'" :class="$style.tip">
|
||||
<n8n-text size="small" :class="$style.tipText"
|
||||
>{{ $locale.baseText('parameterInput.tip') }}:
|
||||
</n8n-text>
|
||||
<n8n-text size="small" :class="$style.text">
|
||||
{{ $locale.baseText('parameterInput.dragTipBeforePill') }}
|
||||
</n8n-text>
|
||||
<div :class="[$style.pill, { [$style.highlight]: !ndvStore.isMappingOnboarded }]">
|
||||
{{ $locale.baseText('parameterInput.inputField') }}
|
||||
</div>
|
||||
<n8n-text size="small" :class="$style.text">
|
||||
{{ $locale.baseText('parameterInput.dragTipAfterPill') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tip === 'executePrevious'" :class="$style.tip">
|
||||
<n8n-text size="small" :class="$style.tipText"
|
||||
>{{ $locale.baseText('parameterInput.tip') }}:
|
||||
</n8n-text>
|
||||
<n8n-text size="small" :class="$style.text"
|
||||
>{{ $locale.baseText('expressionTip.noExecutionData') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
|
||||
<div v-else :class="$style.tip">
|
||||
<n8n-text size="small" :class="$style.tipText"
|
||||
>{{ $locale.baseText('parameterInput.tip') }}:
|
||||
</n8n-text>
|
||||
<n8n-text size="small" :class="$style.text">
|
||||
{{ i18n.baseText('parameterInput.anythingInside') }}
|
||||
</n8n-text>
|
||||
<code v-text="`{{ }}`"></code>
|
||||
<n8n-text size="small" :class="$style.text">
|
||||
{{ i18n.baseText('parameterInput.isJavaScript') }}
|
||||
</n8n-text>
|
||||
<n8n-link
|
||||
:class="$style['learn-more']"
|
||||
size="small"
|
||||
underline
|
||||
theme="text"
|
||||
:to="expressionsDocsUrl"
|
||||
>
|
||||
{{ i18n.baseText('parameterInput.learnMore') }}
|
||||
</n8n-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { computed } from 'vue';
|
||||
import { EXPRESSIONS_DOCS_URL } from '@/constants';
|
||||
|
||||
const i18n = useI18n();
|
||||
const ndvStore = useNDVStore();
|
||||
|
||||
const props = defineProps<{ tip?: 'drag' | 'default' }>();
|
||||
|
||||
const tip = computed(() => {
|
||||
if (!ndvStore.hasInputData) {
|
||||
return 'executePrevious';
|
||||
}
|
||||
|
||||
if (props.tip) return props.tip;
|
||||
|
||||
if (ndvStore.focusedMappableInput) return 'drag';
|
||||
|
||||
return 'default';
|
||||
});
|
||||
const expressionsDocsUrl = EXPRESSIONS_DOCS_URL;
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.tip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: var(--font-line-height-regular);
|
||||
color: var(--color-text-base);
|
||||
font-size: var(--font-size-2xs);
|
||||
padding: var(--spacing-2xs);
|
||||
gap: var(--spacing-4xs);
|
||||
|
||||
.tipText {
|
||||
color: var(--color-text-dark);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.text {
|
||||
flex-shrink: 0;
|
||||
|
||||
&:last-child {
|
||||
flex-shrink: 1;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: var(--font-size-3xs);
|
||||
background: var(--color-background-base);
|
||||
padding: var(--spacing-5xs);
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
.pill {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-text-dark);
|
||||
|
||||
border: var(--border-base);
|
||||
border-color: var(--color-foreground-light);
|
||||
background-color: var(--color-background-xlight);
|
||||
padding: var(--spacing-5xs) var(--spacing-3xs);
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: var(--color-primary);
|
||||
background-color: var(--color-primary-tint-3);
|
||||
border-color: var(--color-primary-tint-1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -51,6 +51,7 @@
|
||||
:path="path"
|
||||
:additional-expression-data="additionalExpressionData"
|
||||
:class="{ 'ph-no-capture': shouldRedactValue }"
|
||||
:event-bus="eventBus"
|
||||
@update:model-value="expressionUpdated"
|
||||
@modalOpenerClick="openExpressionEditorModal"
|
||||
@focus="setFocus"
|
||||
|
||||
@@ -29,46 +29,34 @@
|
||||
@drop="onDrop"
|
||||
>
|
||||
<template #default="{ droppable, activeDrop }">
|
||||
<n8n-tooltip
|
||||
placement="left"
|
||||
:visible="showMappingTooltip"
|
||||
:buttons="dataMappingTooltipButtons"
|
||||
>
|
||||
<template #content>
|
||||
<span
|
||||
v-html="
|
||||
i18n.baseText(`dataMapping.${displayMode}Hint`, {
|
||||
interpolate: { name: parameter.displayName },
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<ParameterInputWrapper
|
||||
ref="param"
|
||||
:parameter="parameter"
|
||||
:model-value="value"
|
||||
:path="path"
|
||||
:is-read-only="isReadOnly"
|
||||
:is-assignment="isAssignment"
|
||||
:rows="rows"
|
||||
:droppable="droppable"
|
||||
:active-drop="activeDrop"
|
||||
:force-show-expression="forceShowExpression"
|
||||
:hint="hint"
|
||||
:hide-hint="hideHint"
|
||||
:hide-issues="hideIssues"
|
||||
:label="label"
|
||||
:event-bus="eventBus"
|
||||
input-size="small"
|
||||
@update="valueChanged"
|
||||
@textInput="onTextInput"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@drop="onDrop"
|
||||
/>
|
||||
</n8n-tooltip>
|
||||
<ParameterInputWrapper
|
||||
ref="param"
|
||||
:parameter="parameter"
|
||||
:model-value="value"
|
||||
:path="path"
|
||||
:is-read-only="isReadOnly"
|
||||
:is-assignment="isAssignment"
|
||||
:rows="rows"
|
||||
:droppable="droppable"
|
||||
:active-drop="activeDrop"
|
||||
:force-show-expression="forceShowExpression"
|
||||
:hint="hint"
|
||||
:hide-hint="hideHint"
|
||||
:hide-issues="hideIssues"
|
||||
:label="label"
|
||||
:event-bus="eventBus"
|
||||
input-size="small"
|
||||
@update="valueChanged"
|
||||
@textInput="onTextInput"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@drop="onDrop"
|
||||
/>
|
||||
</template>
|
||||
</DraggableTarget>
|
||||
<div v-if="showDragnDropTip" :class="$style.tip">
|
||||
<InlineExpressionTip tip="drag" />
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
[$style.options]: true,
|
||||
@@ -94,7 +82,7 @@ import { defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
|
||||
import type { IN8nButton, INodeUi, IRunDataDisplayMode, IUpdateInformation } from '@/Interface';
|
||||
import type { INodeUi, IRunDataDisplayMode, IUpdateInformation } from '@/Interface';
|
||||
|
||||
import ParameterOptions from '@/components/ParameterOptions.vue';
|
||||
import DraggableTarget from '@/components/DraggableTarget.vue';
|
||||
@@ -109,13 +97,11 @@ import type {
|
||||
IParameterLabel,
|
||||
NodeParameterValueType,
|
||||
} from 'n8n-workflow';
|
||||
import type { BaseTextKey } from '@/plugins/i18n';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useSegment } from '@/stores/segment.store';
|
||||
import { getMappedResult } from '@/utils/mappingUtils';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
|
||||
const DISPLAY_MODES_WITH_DATA_MAPPING = ['table', 'json', 'schema'];
|
||||
import InlineExpressionTip from './InlineExpressionEditor/InlineExpressionTip.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ParameterInputFull',
|
||||
@@ -123,6 +109,7 @@ export default defineComponent({
|
||||
ParameterOptions,
|
||||
DraggableTarget,
|
||||
ParameterInputWrapper,
|
||||
InlineExpressionTip,
|
||||
},
|
||||
props: {
|
||||
displayOptions: {
|
||||
@@ -159,6 +146,7 @@ export default defineComponent({
|
||||
},
|
||||
parameter: {
|
||||
type: Object as PropType<INodeProperties>,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
@@ -192,24 +180,8 @@ export default defineComponent({
|
||||
focused: false,
|
||||
menuExpanded: false,
|
||||
forceShowExpression: false,
|
||||
dataMappingTooltipButtons: [] as IN8nButton[],
|
||||
mappingTooltipEnabled: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const mappingTooltipDismissHandler = this.onMappingTooltipDismissed.bind(this);
|
||||
this.dataMappingTooltipButtons = [
|
||||
{
|
||||
attrs: {
|
||||
label: this.i18n.baseText('_reusableBaseText.dismiss' as BaseTextKey),
|
||||
'data-test-id': 'dismiss-mapping-tooltip',
|
||||
},
|
||||
listeners: {
|
||||
onClick: mappingTooltipDismissHandler,
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNDVStore),
|
||||
node(): INodeUi | null {
|
||||
@@ -221,6 +193,9 @@ export default defineComponent({
|
||||
isInputTypeString(): boolean {
|
||||
return this.parameter.type === 'string';
|
||||
},
|
||||
isInputTypeNumber(): boolean {
|
||||
return this.parameter.type === 'number';
|
||||
},
|
||||
isResourceLocator(): boolean {
|
||||
return this.parameter.type === 'resourceLocator';
|
||||
},
|
||||
@@ -239,31 +214,29 @@ export default defineComponent({
|
||||
displayMode(): IRunDataDisplayMode {
|
||||
return this.ndvStore.inputPanelDisplayMode;
|
||||
},
|
||||
showMappingTooltip(): boolean {
|
||||
showDragnDropTip(): boolean {
|
||||
return (
|
||||
this.mappingTooltipEnabled &&
|
||||
!this.ndvStore.isMappingOnboarded &&
|
||||
this.focused &&
|
||||
this.isInputTypeString &&
|
||||
!this.isInputDataEmpty &&
|
||||
DISPLAY_MODES_WITH_DATA_MAPPING.includes(this.displayMode)
|
||||
(this.isInputTypeString || this.isInputTypeNumber) &&
|
||||
!this.isValueExpression &&
|
||||
!this.isDropDisabled &&
|
||||
!this.ndvStore.isMappingOnboarded
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onFocus() {
|
||||
this.focused = true;
|
||||
setTimeout(() => {
|
||||
this.mappingTooltipEnabled = true;
|
||||
}, 500);
|
||||
if (!this.parameter.noDataExpression) {
|
||||
this.ndvStore.setMappableNDVInputFocus(this.parameter.displayName);
|
||||
}
|
||||
},
|
||||
onBlur() {
|
||||
this.focused = false;
|
||||
this.mappingTooltipEnabled = false;
|
||||
if (!this.parameter.noDataExpression) {
|
||||
if (
|
||||
!this.parameter.noDataExpression &&
|
||||
this.ndvStore.focusedMappableInput === this.parameter.displayName
|
||||
) {
|
||||
this.ndvStore.setMappableNDVInputFocus('');
|
||||
}
|
||||
this.$emit('blur');
|
||||
@@ -333,6 +306,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
this.valueChanged(parameterData);
|
||||
this.eventBus.emit('drop', updatedValue);
|
||||
|
||||
if (!this.ndvStore.isMappingOnboarded) {
|
||||
this.showMessage({
|
||||
@@ -342,7 +316,7 @@ export default defineComponent({
|
||||
dangerouslyUseHTMLString: true,
|
||||
});
|
||||
|
||||
this.ndvStore.disableMappingHint();
|
||||
this.ndvStore.setMappingOnboarded();
|
||||
}
|
||||
|
||||
this.ndvStore.setMappingTelemetry({
|
||||
@@ -364,21 +338,11 @@ export default defineComponent({
|
||||
this.forceShowExpression = false;
|
||||
}, 200);
|
||||
},
|
||||
onMappingTooltipDismissed() {
|
||||
this.ndvStore.disableMappingHint(false);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
showMappingTooltip(newValue: boolean) {
|
||||
if (!newValue) {
|
||||
this.$telemetry.track('User viewed data mapping tooltip', { type: 'param focus' });
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
<style lang="scss" module>
|
||||
.wrapper {
|
||||
position: relative;
|
||||
|
||||
@@ -388,6 +352,20 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 100%;
|
||||
background: var(--color-code-background);
|
||||
border: var(--border-base);
|
||||
border-top: none;
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 6px 0 rgba(#441c17, 0.1);
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.options {
|
||||
position: absolute;
|
||||
bottom: -22px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="$style.jsonDisplay">
|
||||
<div :class="[$style.jsonDisplay, { [$style.highlight]: highlight }]">
|
||||
<Suspense>
|
||||
<RunDataJsonActions
|
||||
v-if="!editMode.enabled"
|
||||
@@ -157,6 +157,9 @@ export default defineComponent({
|
||||
jsonData(): IDataObject[] {
|
||||
return executionDataToJson(this.inputData);
|
||||
},
|
||||
highlight(): boolean {
|
||||
return !this.ndvStore.isMappingOnboarded && Boolean(this.ndvStore.focusedMappableInput);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getShortKey(el: HTMLElement): string {
|
||||
@@ -200,7 +203,9 @@ export default defineComponent({
|
||||
|
||||
setTimeout(() => {
|
||||
void this.externalHooks.run('runDataJson.onDragEnd', telemetryPayload);
|
||||
this.$telemetry.track('User dragged data for mapping', telemetryPayload);
|
||||
this.$telemetry.track('User dragged data for mapping', telemetryPayload, {
|
||||
withPostHog: true,
|
||||
});
|
||||
}, 1000); // ensure dest data gets set if drop
|
||||
},
|
||||
getContent(value: unknown): string {
|
||||
@@ -232,20 +237,22 @@ export default defineComponent({
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mappable {
|
||||
cursor: grab;
|
||||
.mappable {
|
||||
cursor: grab;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-json-highlight);
|
||||
&:hover {
|
||||
background-color: var(--color-json-highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dragged {
|
||||
&,
|
||||
&:hover {
|
||||
background-color: var(--color-primary-tint-2);
|
||||
&.highlight .mappable,
|
||||
.dragged {
|
||||
&,
|
||||
&:hover {
|
||||
background-color: var(--color-primary-tint-2);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -35,6 +35,10 @@ const schema = computed(() => getSchemaForExecutionData(props.data));
|
||||
|
||||
const isDataEmpty = computed(() => isEmpty(props.data));
|
||||
|
||||
const highlight = computed(() => {
|
||||
return !ndvStore.isMappingOnboarded && Boolean(ndvStore.focusedMappableInput);
|
||||
});
|
||||
|
||||
const onDragStart = (el: HTMLElement) => {
|
||||
if (el?.dataset?.path) {
|
||||
draggingPath.value = el.dataset.path;
|
||||
@@ -62,13 +66,13 @@ const onDragEnd = (el: HTMLElement) => {
|
||||
|
||||
void useExternalHooks().run('runDataJson.onDragEnd', telemetryPayload);
|
||||
|
||||
telemetry.track('User dragged data for mapping', telemetryPayload);
|
||||
telemetry.track('User dragged data for mapping', telemetryPayload, { withPostHog: true });
|
||||
}, 1000); // ensure dest data gets set if drop
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.schemaWrapper">
|
||||
<div :class="[$style.schemaWrapper, { highlightSchema: highlight }]">
|
||||
<n8n-info-tip v-if="isDataEmpty">{{
|
||||
i18n.baseText('dataMapping.schemaView.emptyData')
|
||||
}}</n8n-info-tip>
|
||||
|
||||
@@ -39,6 +39,8 @@ const text = computed(() =>
|
||||
Array.isArray(props.schema.value) ? '' : shorten(props.schema.value, 600, 0),
|
||||
);
|
||||
|
||||
const dragged = computed(() => props.draggingPath === props.schema.path);
|
||||
|
||||
const getJsonParameterPath = (path: string): string =>
|
||||
getMappedExpression({
|
||||
nodeName: props.node!.name,
|
||||
@@ -83,7 +85,7 @@ const getIconBySchemaType = (type: Schema['type']): string => {
|
||||
:class="{
|
||||
[$style.pill]: true,
|
||||
[$style.mappable]: mappingEnabled,
|
||||
[$style.dragged]: draggingPath === schema.path,
|
||||
[$style.highlight]: dragged,
|
||||
}"
|
||||
>
|
||||
<span
|
||||
@@ -203,6 +205,25 @@ const getIconBySchemaType = (type: Schema['type']): string => {
|
||||
}
|
||||
}
|
||||
|
||||
:global(.highlightSchema) {
|
||||
.pill.mappable {
|
||||
&,
|
||||
&:hover,
|
||||
span,
|
||||
&:hover span span {
|
||||
color: var(--color-primary);
|
||||
border-color: var(--color-primary-tint-1);
|
||||
background-color: var(--color-primary-tint-3);
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pill {
|
||||
float: left;
|
||||
display: inline-flex;
|
||||
@@ -237,22 +258,6 @@ const getIconBySchemaType = (type: Schema['type']): string => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.dragged {
|
||||
&,
|
||||
&:hover,
|
||||
span {
|
||||
color: var(--color-primary);
|
||||
border-color: var(--color-primary-tint-1);
|
||||
background-color: var(--color-primary-tint-3);
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="$style.dataDisplay">
|
||||
<div :class="[$style.dataDisplay, { [$style.highlight]: highlight }]">
|
||||
<table v-if="tableData.columns && tableData.columns.length === 0" :class="$style.table">
|
||||
<tr>
|
||||
<th :class="$style.emptyCell"></th>
|
||||
@@ -259,6 +259,9 @@ export default defineComponent({
|
||||
focusedMappableInput(): string {
|
||||
return this.ndvStore.focusedMappableInput;
|
||||
},
|
||||
highlight(): boolean {
|
||||
return !this.ndvStore.isMappingOnboarded && Boolean(this.ndvStore.focusedMappableInput);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
shorten,
|
||||
@@ -438,7 +441,9 @@ export default defineComponent({
|
||||
|
||||
void this.externalHooks.run('runDataTable.onDragEnd', telemetryPayload);
|
||||
|
||||
this.$telemetry.track('User dragged data for mapping', telemetryPayload);
|
||||
this.$telemetry.track('User dragged data for mapping', telemetryPayload, {
|
||||
withPostHog: true,
|
||||
});
|
||||
}, 1000); // ensure dest data gets set if drop
|
||||
},
|
||||
isSimple(data: unknown): boolean {
|
||||
@@ -642,7 +647,12 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.highlight .draggableHeader {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.draggingHeader {
|
||||
color: var(--color-primary);
|
||||
background-color: var(--color-primary-tint-2);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user