mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
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:
293
packages/editor-ui/src/components/ExpressionParameterInput.vue
Normal file
293
packages/editor-ui/src/components/ExpressionParameterInput.vue
Normal 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>
|
||||
Reference in New Issue
Block a user