Files
Automata/packages/editor-ui/src/components/ExpressionInput.vue
Alex Grozav 27e2ce0470 feat: migrate editor-ui to Vite.js and various DX improvements (N8N-2277) (#4061)
* feat: Added vite.js dependencies.

* chore: Removed tests folder to follow same structure as design-system

* chore: Removed unused testing config.

* chore: Created vite.js index.html

* refactor: Updated scss structure and imports.

* refactor: Updated workflow building.

* fix: Cleared up all workflow dependency cycles. Added proper package.json imports config.

* feat: Got a working build using Vite. Need to fix issues next.

* fix: Progress! Getting process.env error.

* fix: Changed process.env to import.meta.env.

* fix: Fixed circular imports that used require(). Fixed monaco editor.

* chore: Removed commented code.

* chore: Cleaned up package.json

* feat: Made necessary changes to replace base path in css files.

* feat: Serve CSS files for `editor-ui` Vite migration (#4069)

 Serve CSS files for Vite migration

* chore: Fixed package-lock.json.

* fix: Fixed build after centralized tsconfig update.

* fix: Removed lodash-es replacement.

* fix: Commented out vitest test command.

* style: Fixed linting issues.

* fix: Added lodash-es hotfix back.

* chore: Updated package-lock.json

* refactor: Renamed all n8n scss variables to no longer be defined as private.

* feat(editor): add application-wide el-button replacement.

* fix(editor): Fix import in page alert after merge.

* chore(editor): update package-lock.json.

* fix: Case sensitive lodash-es replacement for vue-agile.

* fix: add alias for lodash-es camelcase import.

* fix: add patch-package support for fixing quill

* feat: add patch-package on postinstall

* fix: update quill patch path.

* refactor: rename quill patch

* fix: update quill version.

* fix: update quill patch

* fix: fix linting rules after installing eslint in design-system

* fix: update date picker button to have primary color

* test: update callout component snapshots

* fix(editor): fix linting issues in editor after enabling eslint

* fix(cli): add /assets/* to auth ignore endpoints in server

* chore: update package-lock.json

* chore: update package-lock.json

* fix(editor): fix linting issues

* feat: add vite-legacy support

* fix: update workflow package interface imports to type imports.

* chore: update package-lock.json

* fix(editor) fix importing translations other than english

* fix(editor): remove test command until vitest is added

* fix: increase memory allocation for vite build

* fix: add patch-package patches to n8n-custom docker build

* fix: add performance and load time improvements

* fix: add proper typing to setNodeType

* chore: update package-lock.json

* style: use generic type for reduce in setNodeType

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
2022-09-23 17:14:28 +03:00

374 lines
8.6 KiB
Vue

<template>
<div>
<div ref="expression-editor" :style="editorStyle" class="ignore-key-press" @keydown.stop></div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import 'quill/dist/quill.core.css';
// @ts-ignore
import Quill from 'quill';
import DeltaOperation from 'quill-delta';
// @ts-ignore
import AutoFormat from 'quill-autoformat';
import {
NodeParameterValue,
Workflow,
} from 'n8n-workflow';
import {
IVariableItemSelected,
} from '@/Interface';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import mixins from 'vue-typed-mixins';
export default mixins(
genericHelpers,
workflowHelpers,
)
.extend({
name: 'ExpressionInput',
props: [
'rows',
'value',
'parameter',
'path',
'resolvedValue',
],
data () {
return {
editor: null as null | Quill,
};
},
computed: {
editorStyle () {
let rows = 1;
if (this.rows) {
rows = parseInt(this.rows, 10);
}
return {
'height': Math.max((rows * 26 + 10), 40) + 'px',
};
},
workflow (): Workflow {
return this.getCurrentWorkflow();
},
},
watch: {
value () {
if (this.resolvedValue) {
// When resolved value gets displayed update the input automatically
this.initValue();
}
},
},
mounted () {
const that = this;
// tslint:disable-next-line
const Inline = Quill.import('blots/inline');
class VariableField extends Inline {
static create (value: string) {
const node = super.create(value);
node.setAttribute('data-value', value);
node.setAttribute('class', 'variable');
return node;
}
static formats (domNode: HTMLElement) {
// For the not resolved one the value can be read directly from the dom
let variableName = domNode.innerHTML.trim();
if (that.resolvedValue) {
// For the resolve done it has to get the one from creation.
// It will not update on change but because the init runs on every change it does not really matter
variableName = domNode.getAttribute('data-value') as string;
}
const newClasses = that.getPlaceholderClasses(variableName);
if (domNode.getAttribute('class') !== newClasses) {
// Only update when it changed else we get an endless loop!
domNode.setAttribute('class', newClasses);
}
return true;
}
}
// @ts-ignore
VariableField.blotName = 'variable';
// @ts-ignore
VariableField.className = 'variable';
// @ts-ignore
VariableField.tagName = 'span';
AutoFormat.DEFAULTS = {
expression: {
trigger: /\B[\w\s]/,
find: /\{\{[^\s,;:!?}]+\}\}/i,
format: 'variable',
},
};
Quill.register({
'modules/autoformat': AutoFormat,
'formats/variable': VariableField,
});
this.editor = new Quill(this.$refs['expression-editor'] as Element, {
readOnly: !!this.resolvedValue || this.isReadOnly,
modules: {
autoformat: {},
keyboard: {
bindings: {
'list autofill': null,
},
},
},
});
this.editor.root.addEventListener('blur', (event: Event) => {
this.$emit('blur', event);
});
this.initValue();
if (!this.resolvedValue) {
// Only call update when not resolved value gets displayed
this.setFocus();
this.editor.on('text-change', () => this.update());
}
},
methods: {
// ------------------------------- EDITOR -------------------------------
customizeVariable (variableName: string) {
const returnData = {
classes: [] as string[],
message: variableName as string,
};
let value;
try {
value = this.resolveExpression(`=${variableName}`);
if (value !== undefined) {
returnData.classes.push('valid');
} else {
returnData.classes.push('invalid');
}
} catch (e) {
returnData.classes.push('invalid');
}
return returnData;
},
// Resolves the given variable. If it is not valid it will return
// an error-string.
resolveParameterString (variableName: string) {
let returnValue;
try {
returnValue = this.resolveExpression(`=${variableName}`);
} catch (error) {
return `[invalid (${error.message})]`;
}
if (returnValue === undefined) {
return '[not found]';
}
return returnValue;
},
getPlaceholderClasses (variableName: string) {
const customizeData = this.customizeVariable(variableName);
return 'variable ' + customizeData.classes.join(' ');
},
getValue () {
if (!this.editor) {
return '';
}
const content = this.editor.getContents();
if (!content || !content.ops) {
return '';
}
let returnValue = '';
// Convert the editor operations into a string
content.ops.forEach((item: DeltaOperation) => {
if (!item.insert) {
return;
}
returnValue += item.insert;
});
// For some unknown reason does the Quill always return a "\n"
// at the end. Remove it here manually
return '=' + returnValue.replace(/\s+$/g, '');
},
setFocus () {
// TODO: There is a bug that when opening ExpressionEditor and typing directly it shows the first letter and
// then adds the second letter in from of the first on
this.editor!.focus();
},
itemSelected (eventData: IVariableItemSelected) {
// We can only get the selection if editor is in focus so make
// sure it is
this.editor!.focus();
const selection = this.editor!.getSelection();
let addIndex = null;
if (selection) {
addIndex = selection.index;
}
if (addIndex) {
// If we have a location to add variable to add it there
this.editor!.insertText(addIndex, `{{${eventData.variable}}}`, 'variable', true);
this.update();
} else {
// If no position got found add it to end
let newValue = this.getValue();
if (newValue === '=' || newValue === '=0') {
newValue = `{{${eventData.variable}}}\n`;
} else {
newValue += ` {{${eventData.variable}}}\n`;
}
this.$emit('change', newValue, true);
if (!this.resolvedValue) {
Vue.nextTick(() => {
this.initValue();
});
}
}
},
initValue () {
if (!this.value) {
return;
}
let currentValue = this.value;
if (currentValue.charAt(0) === '=') {
currentValue = currentValue.slice(1);
}
// Convert the expression string into a Quill Operations
const editorOperations: DeltaOperation[] = [];
currentValue.replace(/\{\{(.*?)\}\}/ig, '*%%#_@^$1*%%#_@').split('*%%#_@').forEach((value: string) => {
if (value && value.charAt(0) === '^') {
// Is variable
let displayValue = `{{${value.slice(1)}}}` as string | number | boolean | null | undefined;
if (this.resolvedValue) {
displayValue = [null, undefined].includes(displayValue as null | undefined) ? '' : displayValue;
displayValue = this.resolveParameterString((displayValue as string).toString()) as NodeParameterValue;
}
displayValue = [null, undefined].includes(displayValue as null | undefined) ? '' : displayValue;
editorOperations.push({
attributes: {
variable: `{{${value.slice(1)}}}`,
},
insert: (displayValue as string).toString(),
});
} else {
// Is text
editorOperations.push({
insert: value,
});
}
});
// @ts-ignore
this.editor!.setContents(editorOperations);
},
update () {
this.$emit('input', this.getValue());
this.$emit('change', this.getValue());
},
},
});
</script>
<style lang="scss">
.variable-wrapper {
text-decoration: none;
}
.variable-value {
font-weight: bold;
color: var(--color-text-dark);
background-color: var(--color-text-base);
padding: 3px;
border-radius: 3px;
}
.variable-delete {
position: relative;
left: -3px;
top: -8px;
display: none;
color: var(--color-text-xlight);
font-weight: bold;
padding: 2px 4px;
}
.variable-wrapper:hover .variable-delete {
display: inline;
background-color: var(--color-danger);
border-radius: 5px;
}
.variable {
font-weight: bold;
color: #000;
background-color: var(--color-text-base);
padding: 3px;
border-radius: 3px;
margin: 0 2px;
&:first-child {
margin-left: 0;
}
&.invalid {
background-color: var(--color-danger);
}
&.valid {
background-color: var(--color-success);
}
}
.ql-editor {
padding: 0.5em 1em;
}
.ql-disabled .ql-editor {
border-width: 1px;
border: 1px solid $custom-expression-text;
color: $custom-expression-text;
background-color: $custom-expression-background;
cursor: not-allowed;
}
.ql-disabled .ql-editor .variable {
color: #303030;
}
</style>