feat(editor): Inline expression editor (#4814)

* WIP

* 🔥 Remove unneeded watch

*  Further setup

*  Fix import

*  Minor tweaks

* 🔥 Remove logging

* 🎨 Add some styling

* 🎨 More styling changes

* 🐛 Fix wrong marking of stale data

* 🎨 Prevent fx on dragging

* 🔥 Remove logging

*  Refine draggable target offsets

* refactor(editor): Consolidate expression management logic (#4836)

*  Extract `ExpressionFunctionIcon`

*  Simplify syntax

*  Move to mixin

* 🎨 Format

* 📘 Unify types

*  Dedup double brace handler

*  Consolidate resolvable highlighter

* 🎨 Format

*  Consolidate language pack

* ✏️ Add comment

*  Move completions to plugins

*  Partially deduplicate themes

* refactor(editor): Apply styling feedback to inline expression editor (#4846)

* 🎨 Adjust styling for expression parameter input

* 🎨 Style outputs differently

*  Set single line for RLC

* 🎨 Style both openers identically

* 🐛 Prevent defocus on resize

*  Adjust line height

* 🎨 Adjust border with for expression input

*  Fix font family for inline output

*  Set up telemetry

*  Complete telemetry

*  Simplify event source

*  Set monospaced font for inline output

* 🎨 Hide cursor on schema pill drop

* 🧪 Update snapshots

*  Consolidate editor styles

* ✏️ Add tech debt comments

*  Improve naming

*  Improve inside resolvable detection

*  Improve var naming

* 🔥 Remove outdated comment

* 🚚 Move constant to data

* ✏️ Clarify comments

* 🔥 Remove outdated comments

* 🔥 Remove unneeded try-catch

* 🔥 Remove unneeded method

* 🔥 Remove unneeded check

* 🔥 Remove `openExpression` check

* 🔥 Remove unused timeout

* 🔥 Remove commented out sections

*  Use Pinia naming convention

*  Re-evaluate on change of `ndvInputData`

* 🐛 Fix handling of `0` in number-type input

* 🐛 Surface focus and blur for mapping hints

* 🔥 Remove logging

* ✏️ Reword error

*  Change kebab-case to PascalCase

*  Refactor state fields for clarity

*  Support double bracing on selection

* 🎨 More styling

*  Miscellaneous cleanup

*  Disregard error on drop

* 🎨 Fix schema pill styling

* 🎨 More `background` to `background-color` fixes

* 🧪 Update snapshots

* 🎨 Replace non-existing var with white

* 🧪 Update snapshot

* 📦 Integrate `codemirror-lang-n8n-expression`

* 🎨 Fix formatting

* 🧪 Re-update test snapshots

* 🧪 Update selectors for inline editor

* 🔥 Remove unused test ID

* 📘 Add type for `currentNodePaneType`

*  Refactor mixin to util

*  Use `:global`

* 🔥 Remove comment

*  Add watch

*  Change import style

* 👕 Fix lint

*  Refactor preventing blur on resize

* 🔥 Remove comment

* 🧪 Re-update snapshots

* 🎨 Prettify

* 👕 Fix lint

* 🔥 Remove comment

Co-authored-by: Mutasem <mutdmour@gmail.com>
This commit is contained in:
Iván Ovejero
2022-12-14 14:43:02 +01:00
committed by GitHub
parent f73267ffa5
commit a1259898c0
36 changed files with 1285 additions and 593 deletions

View File

@@ -0,0 +1,293 @@
<template>
<div :class="$style['expression-parameter-input']" v-click-outside="onBlur">
<div :class="[$style['all-sections'], { [$style['focused']]: isFocused }]">
<div
:class="[
$style['prepend-section'],
'el-input-group__prepend',
{ [$style['squared']]: isForRecordLocator },
]"
>
<ExpressionFunctionIcon />
</div>
<InlineExpressionEditorInput
:value="value"
:isReadOnly="isReadOnly"
:targetItem="hoveringItem"
:isSingleLine="isForRecordLocator"
@focus="onFocus"
@blur="onBlur"
@change="onChange"
ref="inlineInput"
/>
<n8n-icon
v-if="!isDragging"
icon="external-link-alt"
size="xsmall"
:class="$style['expression-editor-modal-opener']"
@click="$emit('modalOpenerClick')"
data-test-id="expander"
/>
</div>
<div :class="isFocused ? $style.dropdown : $style.hidden">
<n8n-text size="small" compact :class="$style.header">
{{ $locale.baseText('parameterInput.resultForItem') }} {{ hoveringItemNumber }}
</n8n-text>
<n8n-text :class="$style.body">
<InlineExpressionEditorOutput
:value="value"
:isReadOnly="isReadOnly"
:segments="segments"
/>
</n8n-text>
<div :class="$style.footer">
<n8n-text size="small" compact>
{{ $locale.baseText('parameterInput.anythingInside') }}
</n8n-text>
<div :class="$style['expression-syntax-example']" v-text="`{{ }}`"></div>
<n8n-text size="small" compact>
{{ $locale.baseText('parameterInput.isJavaScript') }}
</n8n-text>
<n8n-link
:class="$style['learn-more']"
size="small"
underline
theme="text"
:to="expressionsDocsUrl"
>
{{ $locale.baseText('parameterInput.learnMore') }}
</n8n-link>
</div>
</div>
</div>
</template>
<script lang="ts">
import { mapStores } from 'pinia';
import Vue from 'vue';
import { useNDVStore } from '@/stores/ndv';
import { useWorkflowsStore } from '@/stores/workflows';
import InlineExpressionEditorInput from '@/components/InlineExpressionEditor/InlineExpressionEditorInput.vue';
import InlineExpressionEditorOutput from '@/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue';
import ExpressionFunctionIcon from '@/components/ExpressionFunctionIcon.vue';
import { createExpressionTelemetryPayload } from '@/utils/telemetryUtils';
import { EXPRESSIONS_DOCS_URL } from '@/constants';
import type { Segment } from '@/types/expressions';
import type { TargetItem } from '@/Interface';
export default Vue.extend({
name: 'ExpressionParameterInput',
components: {
InlineExpressionEditorInput,
InlineExpressionEditorOutput,
ExpressionFunctionIcon,
},
data() {
return {
isFocused: false,
segments: [] as Segment[],
expressionsDocsUrl: EXPRESSIONS_DOCS_URL,
};
},
props: {
value: {
type: String,
},
isReadOnly: {
type: Boolean,
default: false,
},
isForRecordLocator: {
type: Boolean,
default: false,
},
},
computed: {
...mapStores(useNDVStore, useWorkflowsStore),
hoveringItemNumber(): number {
return (this.hoveringItem?.itemIndex ?? 0) + 1;
},
hoveringItem(): TargetItem | null {
return this.ndvStore.hoveringItem;
},
isDragging(): boolean {
return this.ndvStore.isDraggableDragging;
},
},
methods: {
focus() {
const inlineInput = this.$refs.inlineInput as (Vue & HTMLElement) | undefined;
if (inlineInput?.$el) inlineInput.focus();
},
onFocus() {
this.isFocused = true;
this.$emit('focus');
},
onBlur(event: FocusEvent) {
if (
event.target instanceof Element &&
Array.from(event.target.classList).some((_class) => _class.includes('resizer'))
) {
return; // prevent blur on resizing
}
if (this.isDragging) return; // prevent blur on dragging
const wasFocused = this.isFocused;
this.isFocused = false;
this.$emit('blur');
if (wasFocused) {
const telemetryPayload = createExpressionTelemetryPayload(
this.segments,
this.value,
this.workflowsStore.workflowId,
this.ndvStore.sessionId,
this.ndvStore.activeNode?.type ?? '',
);
this.$telemetry.track('User closed Expression Editor', telemetryPayload);
}
},
onChange({ value, segments }: { value: string; segments: Segment[] }) {
if (this.isDragging) return;
this.segments = segments;
if (value === '=' + this.value) return; // prevent report on change of target item
this.$emit('valueChanged', value);
},
},
});
</script>
<style lang="scss" module>
.expression-parameter-input {
position: relative;
.all-sections {
height: 30px;
display: flex;
flex-direction: row;
display: inline-table;
width: 100%;
}
.prepend-section {
padding: 0;
padding-top: 2px;
width: 22px;
text-align: center;
}
.squared {
border-radius: 0;
}
}
.expression-editor-modal-opener {
position: absolute;
right: 0;
bottom: 0;
background-color: white;
padding: 3px;
line-height: 9px;
border: var(--border-base);
border-top-left-radius: var(--border-radius-base);
border-bottom-right-radius: var(--border-radius-base);
cursor: pointer;
svg {
width: 9px !important;
height: 9px;
transform: rotate(270deg);
&:hover {
color: var(--color-primary);
}
}
}
.focused > .prepend-section {
border-color: var(--color-secondary);
border-bottom-left-radius: 0;
}
.focused :global(.cm-editor) {
border-color: var(--color-secondary);
}
.focused > .expression-editor-modal-opener {
border-color: var(--color-secondary);
border-bottom-right-radius: 0;
background-color: white;
}
.hidden {
display: none;
}
.dropdown {
display: flex;
flex-direction: column;
position: absolute;
z-index: 2; // cover tooltips
background: white;
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;
.header,
.body,
.footer {
padding: var(--spacing-3xs);
}
.header {
color: var(--color-text-dark);
font-weight: var(--font-weight-bold);
padding-left: var(--spacing-2xs);
padding-top: var(--spacing-2xs);
}
.body {
padding-top: 0;
padding-left: var(--spacing-2xs);
color: var(--color-text-dark);
}
.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: #f0f0f0;
margin-left: var(--spacing-5xs);
margin-right: var(--spacing-5xs);
}
.learn-more {
line-height: 1;
white-space: nowrap;
}
}
}
</style>