feat(editor): Help users discover expressions when using drag n drop (#8869)
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user