diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index ef5cb62190..5d13bc64bc 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -62,7 +62,8 @@ export class WorkflowPage extends BasePage { this.getters.canvasNodeByName(nodeTypeName).dblclick(); }, openExpressionEditor: () => { - cy.get('input[value="expression"]').parent('label').click(); + cy.contains('Expression').invoke('show').click(); + cy.getByTestId('expander').invoke('show').click(); }, typeIntoParameterInput: (parameterName: string, content: string) => { this.getters.ndvParameterInput(parameterName).type(content); diff --git a/packages/design-system/src/components/N8nIcon/Icon.vue b/packages/design-system/src/components/N8nIcon/Icon.vue index 1e6c9e4ae0..1329442190 100644 --- a/packages/design-system/src/components/N8nIcon/Icon.vue +++ b/packages/design-system/src/components/N8nIcon/Icon.vue @@ -1,5 +1,5 @@ diff --git a/packages/design-system/src/css/_tokens.scss b/packages/design-system/src/css/_tokens.scss index 8ec5bc0861..944bb55f54 100644 --- a/packages/design-system/src/css/_tokens.scss +++ b/packages/design-system/src/css/_tokens.scss @@ -461,6 +461,7 @@ --font-weight-regular: 400; --font-weight-bold: 600; --font-family: 'Open Sans', sans-serif; + --font-family-monospace: Menlo, Consolas, 'DejaVu Sans Mono', monospace; --spacing-5xs: 0.125rem; --spacing-4xs: 0.25rem; diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 2aa39a53c6..8b135558b9 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -40,6 +40,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/vue-fontawesome": "^2.0.2", "axios": "^0.21.1", + "codemirror-lang-n8n-expression": "^0.1.0", "dateformat": "^3.0.3", "esprima-next": "5.8.4", "fast-json-stable-stringify": "^2.1.0", diff --git a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue index 78daa5bda8..80b4dd392d 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue +++ b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue @@ -1,5 +1,5 @@ + + diff --git a/packages/editor-ui/src/components/ExpressionEditorModal/ExpressionModalOutput.vue b/packages/editor-ui/src/components/ExpressionEditorModal/ExpressionEditorModalOutput.vue similarity index 76% rename from packages/editor-ui/src/components/ExpressionEditorModal/ExpressionModalOutput.vue rename to packages/editor-ui/src/components/ExpressionEditorModal/ExpressionEditorModalOutput.vue index 6d40157df9..b6545b2bda 100644 --- a/packages/editor-ui/src/components/ExpressionEditorModal/ExpressionModalOutput.vue +++ b/packages/editor-ui/src/components/ExpressionEditorModal/ExpressionEditorModalOutput.vue @@ -1,5 +1,5 @@ + + diff --git a/packages/editor-ui/src/components/ExpressionParameterInput.vue b/packages/editor-ui/src/components/ExpressionParameterInput.vue new file mode 100644 index 0000000000..d2cc37b047 --- /dev/null +++ b/packages/editor-ui/src/components/ExpressionParameterInput.vue @@ -0,0 +1,293 @@ + + + + + diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorInput.vue b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorInput.vue new file mode 100644 index 0000000000..53468eb5d2 --- /dev/null +++ b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorInput.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue new file mode 100644 index 0000000000..bf5c111494 --- /dev/null +++ b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/theme.ts b/packages/editor-ui/src/components/InlineExpressionEditor/theme.ts new file mode 100644 index 0000000000..0e11f5e8c9 --- /dev/null +++ b/packages/editor-ui/src/components/InlineExpressionEditor/theme.ts @@ -0,0 +1,68 @@ +import { EditorView } from '@codemirror/view'; +import { highlighter } from '@/plugins/codemirror/resolvableHighlighter'; + +const commonThemeProps = { + '&.cm-focused': { + outline: '0 !important', + }, + '.cm-content': { + fontFamily: 'var(--font-family-monospace)', + color: 'var(--input-font-color, var(--color-text-dark))', + }, + '.cm-line': { + padding: '0', + }, +}; + +export const inputTheme = ({ isSingleLine } = { isSingleLine: false }) => { + const theme = EditorView.theme({ + ...commonThemeProps, + '&': { + maxHeight: isSingleLine ? '30px' : '112px', + minHeight: '30px', + width: '100%', + fontSize: 'var(--font-size-2xs)', + padding: '0 0 0 var(--spacing-2xs)', + borderWidth: 'var(--border-width-base)', + borderStyle: 'var(--input-border-style, var(--border-style-base))', + borderColor: 'var(--input-border-color, var(--border-color-base))', + borderRadius: 'var(--input-border-radius, var(--border-radius-base))', + borderTopLeftRadius: '0', + borderBottomLeftRadius: '0', + backgroundColor: 'white', + }, + '.cm-scroller': { + lineHeight: '1.68', + }, + }); + + return [theme, highlighter.resolvableStyle]; +}; + +export const outputTheme = () => { + const theme = EditorView.theme({ + ...commonThemeProps, + '&': { + maxHeight: '95px', + width: '100%', + fontSize: 'var(--font-size-2xs)', + padding: '0', + borderTopLeftRadius: '0', + borderBottomLeftRadius: '0', + backgroundColor: 'white', + }, + '.cm-scroller': { + lineHeight: '1.6', + }, + '.cm-valid-resolvable': { + padding: '0 2px', + borderRadius: '2px', + }, + '.cm-invalid-resolvable': { + padding: '0 2px', + borderRadius: '2px', + }, + }); + + return [theme, highlighter.resolvableStyle]; +}; diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 6a598a63e3..da565e27a7 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -12,11 +12,7 @@ @closeDialog="closeExpressionEditDialog" @valueChanged="expressionUpdated" > -
+
-
@@ -329,6 +332,7 @@ import ScopesNotice from '@/components/ScopesNotice.vue'; import ParameterOptions from '@/components/ParameterOptions.vue'; import ParameterIssues from '@/components/ParameterIssues.vue'; import ResourceLocator from '@/components/ResourceLocator/ResourceLocator.vue'; +import ExpressionParameterInput from '@/components/ExpressionParameterInput.vue'; // @ts-ignore import PrismEditor from 'vue-prism-editor'; import TextEdit from '@/components/TextEdit.vue'; @@ -362,6 +366,7 @@ export default mixins( CodeEdit, CodeNodeEditor, ExpressionEdit, + ExpressionParameterInput, NodeCredentials, CredentialsSelect, PrismEditor, @@ -464,6 +469,7 @@ export default mixins( }, ], }, + isFocused: false, }; }, watch: { @@ -718,10 +724,12 @@ export default mixins( classes['parameter-value-container'] = true; } - if (this.isValueExpression || this.forceShowExpression) { - classes['expression'] = true; - } - if (!this.droppable && !this.activeDrop && (this.getIssues.length || this.errorHighlight)) { + if ( + !this.droppable && + !this.activeDrop && + (this.getIssues.length > 0 || this.errorHighlight) && + !this.isValueExpression + ) { classes['has-issues'] = true; } @@ -890,26 +898,20 @@ export default mixins( : value; this.valueChanged(val); }, - openExpressionEdit() { - if (this.isValueExpression) { - this.expressionEditDialogVisible = true; - this.trackExpressionEditOpen(); - return; - } + openExpressionEditorModal() { + if (!this.isValueExpression) return; + + this.expressionEditDialogVisible = true; + this.trackExpressionEditOpen(); }, onBlur() { this.$emit('blur'); + this.isFocused = false; }, onResourceLocatorDrop(data: string) { this.$emit('drop', data); }, setFocus() { - if (this.isValueExpression) { - this.expressionEditDialogVisible = true; - this.trackExpressionEditOpen(); - return; - } - if (['json'].includes(this.parameter.type) && this.getArgument('alwaysOpenEditWindow')) { this.displayEditDialog(); return; @@ -931,6 +933,7 @@ export default mixins( if (this.$refs.inputField && this.$refs.inputField.$el) { // @ts-ignore this.$refs.inputField.focus(); + this.isFocused = true; } }); @@ -1014,8 +1017,6 @@ export default mixins( if (command === 'resetValue') { this.valueChanged(this.parameter.default); - } else if (command === 'openExpression') { - this.expressionEditDialogVisible = true; } else if (command === 'addExpression') { if (this.isResourceLocatorParameter) { if (isResourceLocatorValue(this.value)) { @@ -1023,19 +1024,23 @@ export default mixins( } else { this.valueChanged({ __rl: true, value: `=${this.value}`, mode: '' }); } + } else if ( + this.parameter.type === 'number' && + (!this.value || this.value === '[Object: null]') + ) { + this.valueChanged('={{ 0 }}'); } else if (this.parameter.type === 'number' || this.parameter.type === 'boolean') { - this.valueChanged(`={{${this.value}}}`); + this.valueChanged(`={{ ${this.value} }}`); } else { this.valueChanged(`=${this.value}`); } - setTimeout(() => { - this.expressionEditDialogVisible = true; - this.trackExpressionEditOpen(); - }, 375); + this.setFocus(); } else if (command === 'removeExpression') { let value: NodeParameterValueType = this.expressionEvaluated; + this.isFocused = false; + if (this.parameter.type === 'multiOptions' && typeof value === 'string') { value = (value || '') .split(',') @@ -1201,24 +1206,13 @@ export default mixins( background-color: #f0f0f0; } -.expression { - textarea, - input { - cursor: pointer !important; - } - - --input-border-color: var(--color-secondary-tint-1); - --input-background-color: var(--color-secondary-tint-3); - --input-font-color: var(--color-secondary); -} - .droppable { --input-border-color: var(--color-secondary); - --input-background-color: var(--color-foreground-xlight); --input-border-style: dashed; textarea, - input { + input, + .cm-editor { border-width: 1.5px; } } @@ -1276,4 +1270,39 @@ export default mixins( height: 100%; align-items: center; } + +.input-with-opener > .el-input__suffix { + right: 0; +} + +.textarea-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 { + border-color: var(--color-secondary); +} + +.invalid { + border-color: var(--color-danger); +} diff --git a/packages/editor-ui/src/components/ParameterInputFull.vue b/packages/editor-ui/src/components/ParameterInputFull.vue index de602f4911..7cf102f6af 100644 --- a/packages/editor-ui/src/components/ParameterInputFull.vue +++ b/packages/editor-ui/src/components/ParameterInputFull.vue @@ -24,7 +24,7 @@ type="mapping" :disabled="isDropDisabled" :sticky="true" - :stickyOffset="3" + :stickyOffset="isValueExpression ? [26, 3] : [3, 3]" @drop="onDrop" >