feat(editor-ui): Resizable main panel (#3980)

* Introduce node deprecation (#3930)

 Introduce node deprecation

* 🚧 Scaffold out Code node

* 👕 Fix lint

* 📘 Create types file

* 🚚 Rename theme

* 🔥 Remove unneeded prop

*  Override keybindings

*  Expand lintings

*  Create editor content getter

* 🚚 Ensure all helpers use `$`

*  Add autocompletion

* ♻️ Refactore Resize UI lib component, allow to use it in different than n8n-sticky context

* 🚧 Use variable width for node settings and allow for resizing

*  Use store to keep track of wide and regular main panel widths

* ♻️ Extract Resize wrapper from the Sticky and create a story for it

* 🐛 Fixed cherry-pick conflicts

*  Filter out welcome note node

*  Convey error line number

*  Highlight error line

*  Restore logging from node

*  More autocompletions

*  Streamline completions

* 💄 Fix drag-button border

* ✏️ Update placeholders

*  Update linter to new methods

*  Preserve main panel width in local storage

* 🐛 Fallback to max size size if window is too big

* 🔥 Remove `$nodeItem` completions

*  Re-update placeholders

* 🎨 Fix formatting

* 📦 Update `package-lock.json`

*  Refresh with multi-line empty string

* ♻️ Refactored DraggablePanels to use relative units and implemented independent resizing, cleaned store

* 🐛 Re-implement dragging indicators and move border styles to NDVDraggablePanels component

* 🚨 Fix semis

* 🚨 Remove unsused UI state props

* ♻️ Use only relative left position and calculate right based on it, fix quirks

* 🚨Fix linting error

* ♻️ Store and retrieve main panel dimensions from store to make them persistable in the same app mount session

* 🐛 Prevent resizing of unknown nodes

* ♻️ Add typings for `nodeType` prop, remove unused `convertRemToPixels` import

* 🏷️ Add typings for `nodeType` prop in NodeSettings.vue

* 🐛 Prevent the main panel resize below 280px

* 🐛 Fix inputless panel left position

*  Resize resource locator on main panel size change

* 🐛 Resize resource locator on window resize

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
OlegIvaniv 2022-09-22 17:41:15 +02:00 committed by GitHub
parent 8eeed77edb
commit d01f7d4d93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 510 additions and 182 deletions

62
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "n8n", "name": "n8n",
"version": "0.194.0", "version": "0.195.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "n8n", "name": "n8n",
"version": "0.194.0", "version": "0.195.3",
"workspaces": [ "workspaces": [
"packages/*", "packages/*",
"packages/@n8n_io/*" "packages/@n8n_io/*"
@ -52142,7 +52142,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "n8n", "name": "n8n",
"version": "0.194.0", "version": "0.195.3",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"@oclif/command": "^1.5.18", "@oclif/command": "^1.5.18",
@ -52186,10 +52186,10 @@
"lodash.split": "^4.4.2", "lodash.split": "^4.4.2",
"lodash.unset": "^4.5.2", "lodash.unset": "^4.5.2",
"mysql2": "~2.3.0", "mysql2": "~2.3.0",
"n8n-core": "~0.134.0", "n8n-core": "~0.135.0",
"n8n-editor-ui": "~0.160.0", "n8n-editor-ui": "~0.161.1",
"n8n-nodes-base": "~0.192.0", "n8n-nodes-base": "~0.193.1",
"n8n-workflow": "~0.116.0", "n8n-workflow": "~0.117.0",
"nodemailer": "^6.7.1", "nodemailer": "^6.7.1",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"open": "^7.0.0", "open": "^7.0.0",
@ -52268,7 +52268,7 @@
}, },
"packages/core": { "packages/core": {
"name": "n8n-core", "name": "n8n-core",
"version": "0.134.0", "version": "0.135.0",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
@ -52280,7 +52280,7 @@
"form-data": "^4.0.0", "form-data": "^4.0.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"n8n-workflow": "~0.116.0", "n8n-workflow": "~0.117.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0", "p-cancelable": "^2.0.0",
"qs": "^6.10.1", "qs": "^6.10.1",
@ -52367,7 +52367,7 @@
}, },
"packages/design-system": { "packages/design-system": {
"name": "n8n-design-system", "name": "n8n-design-system",
"version": "0.34.0", "version": "0.35.0",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"element-ui": "~2.15.7", "element-ui": "~2.15.7",
@ -52434,14 +52434,14 @@
}, },
"packages/editor-ui": { "packages/editor-ui": {
"name": "n8n-editor-ui", "name": "n8n-editor-ui",
"version": "0.160.0", "version": "0.161.1",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"@fontsource/open-sans": "^4.5.0", "@fontsource/open-sans": "^4.5.0",
"@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-regular-svg-icons": "^6.1.1",
"luxon": "^2.3.0", "luxon": "^2.3.0",
"monaco-editor": "^0.30.1", "monaco-editor": "^0.30.1",
"n8n-design-system": "~0.34.0", "n8n-design-system": "~0.35.0",
"timeago.js": "^4.0.2", "timeago.js": "^4.0.2",
"v-click-outside": "^3.1.2", "v-click-outside": "^3.1.2",
"vue-fragment": "1.5.1", "vue-fragment": "1.5.1",
@ -52487,7 +52487,7 @@
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",
"monaco-editor-webpack-plugin": "^5.0.0", "monaco-editor-webpack-plugin": "^5.0.0",
"n8n-workflow": "~0.116.0", "n8n-workflow": "~0.117.0",
"normalize-wheel": "^1.0.1", "normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1", "prismjs": "^1.17.1",
"quill": "^2.0.0-dev.3", "quill": "^2.0.0-dev.3",
@ -52513,7 +52513,7 @@
}, },
"packages/node-dev": { "packages/node-dev": {
"name": "n8n-node-dev", "name": "n8n-node-dev",
"version": "0.73.0", "version": "0.74.0",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"@oclif/command": "^1.5.18", "@oclif/command": "^1.5.18",
@ -52521,8 +52521,8 @@
"change-case": "^4.1.1", "change-case": "^4.1.1",
"fast-glob": "^3.2.5", "fast-glob": "^3.2.5",
"inquirer": "^7.0.1", "inquirer": "^7.0.1",
"n8n-core": "~0.134.0", "n8n-core": "~0.135.0",
"n8n-workflow": "~0.116.0", "n8n-workflow": "~0.117.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0", "replace-in-file": "^6.0.0",
"request": "^2.88.2", "request": "^2.88.2",
@ -52544,7 +52544,7 @@
}, },
"packages/nodes-base": { "packages/nodes-base": {
"name": "n8n-nodes-base", "name": "n8n-nodes-base",
"version": "0.192.0", "version": "0.193.1",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"@kafkajs/confluent-schema-registry": "1.0.6", "@kafkajs/confluent-schema-registry": "1.0.6",
@ -52579,7 +52579,7 @@
"mqtt": "4.2.6", "mqtt": "4.2.6",
"mssql": "^8.1.2", "mssql": "^8.1.2",
"mysql2": "~2.3.0", "mysql2": "~2.3.0",
"n8n-core": "~0.134.0", "n8n-core": "~0.135.0",
"node-html-markdown": "^1.1.3", "node-html-markdown": "^1.1.3",
"node-ssh": "^12.0.0", "node-ssh": "^12.0.0",
"nodemailer": "^6.7.1", "nodemailer": "^6.7.1",
@ -52636,7 +52636,7 @@
"eslint-plugin-n8n-nodes-base": "^1.9.3", "eslint-plugin-n8n-nodes-base": "^1.9.3",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"jest": "^27.4.7", "jest": "^27.4.7",
"n8n-workflow": "~0.116.0", "n8n-workflow": "~0.117.0",
"ts-jest": "^27.1.3", "ts-jest": "^27.1.3",
"tslint": "^6.1.2", "tslint": "^6.1.2",
"typescript": "~4.6.0" "typescript": "~4.6.0"
@ -52686,7 +52686,7 @@
}, },
"packages/workflow": { "packages/workflow": {
"name": "n8n-workflow", "name": "n8n-workflow",
"version": "0.116.0", "version": "0.117.0",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"@n8n_io/riot-tmpl": "^1.0.1", "@n8n_io/riot-tmpl": "^1.0.1",
@ -81496,10 +81496,10 @@
"lodash.split": "^4.4.2", "lodash.split": "^4.4.2",
"lodash.unset": "^4.5.2", "lodash.unset": "^4.5.2",
"mysql2": "~2.3.0", "mysql2": "~2.3.0",
"n8n-core": "~0.134.0", "n8n-core": "~0.135.0",
"n8n-editor-ui": "~0.160.0", "n8n-editor-ui": "~0.161.1",
"n8n-nodes-base": "~0.192.0", "n8n-nodes-base": "~0.193.1",
"n8n-workflow": "~0.116.0", "n8n-workflow": "~0.117.0",
"nodemailer": "^6.7.1", "nodemailer": "^6.7.1",
"nodemon": "^2.0.2", "nodemon": "^2.0.2",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
@ -81553,7 +81553,7 @@
"jest": "^27.4.7", "jest": "^27.4.7",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"n8n-workflow": "~0.116.0", "n8n-workflow": "~0.117.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0", "p-cancelable": "^2.0.0",
"qs": "^6.10.1", "qs": "^6.10.1",
@ -81699,8 +81699,8 @@
"luxon": "^2.3.0", "luxon": "^2.3.0",
"monaco-editor": "^0.30.1", "monaco-editor": "^0.30.1",
"monaco-editor-webpack-plugin": "^5.0.0", "monaco-editor-webpack-plugin": "^5.0.0",
"n8n-design-system": "~0.34.0", "n8n-design-system": "~0.35.0",
"n8n-workflow": "~0.116.0", "n8n-workflow": "~0.117.0",
"normalize-wheel": "^1.0.1", "normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1", "prismjs": "^1.17.1",
"quill": "^2.0.0-dev.3", "quill": "^2.0.0-dev.3",
@ -81746,8 +81746,8 @@
"change-case": "^4.1.1", "change-case": "^4.1.1",
"fast-glob": "^3.2.5", "fast-glob": "^3.2.5",
"inquirer": "^7.0.1", "inquirer": "^7.0.1",
"n8n-core": "~0.134.0", "n8n-core": "~0.135.0",
"n8n-workflow": "~0.116.0", "n8n-workflow": "~0.117.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0", "replace-in-file": "^6.0.0",
"request": "^2.88.2", "request": "^2.88.2",
@ -81824,8 +81824,8 @@
"mqtt": "4.2.6", "mqtt": "4.2.6",
"mssql": "^8.1.2", "mssql": "^8.1.2",
"mysql2": "~2.3.0", "mysql2": "~2.3.0",
"n8n-core": "~0.134.0", "n8n-core": "~0.135.0",
"n8n-workflow": "~0.116.0", "n8n-workflow": "~0.117.0",
"node-html-markdown": "^1.1.3", "node-html-markdown": "^1.1.3",
"node-ssh": "^12.0.0", "node-ssh": "^12.0.0",
"nodemailer": "^6.7.1", "nodemailer": "^6.7.1",

View file

@ -0,0 +1,77 @@
import { action } from '@storybook/addon-actions';
import N8nResizeWrapper from './ResizeWrapper.vue';
export default {
title: 'Atoms/ResizeWrapper',
component: N8nResizeWrapper,
};
const methods = {
onInput: action('input'),
onResize(resizeData) {
action('resize', resizeData);
this.newHeight = resizeData.height;
this.newWidth = resizeData.width;
},
onResizeEnd: action('resizeend'),
onResizeStart: action('resizestart'),
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nResizeWrapper,
},
data() {
return {
newWidth: this.width,
newHeight: this.height,
background: "linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(255,154,0,1) 10%, rgba(208,222,33,1) 20%, rgba(79,220,74,1) 30%, rgba(63,218,216,1) 40%, rgba(47,201,226,1) 50%, rgba(28,127,238,1) 60%, rgba(95,21,242,1) 70%, rgba(186,12,248,1) 80%, rgba(251,7,217,1) 90%, rgba(255,0,0,1) 100%)",
};
},
computed: {
containerStyles() {
return {
width: `${this.newWidth}px`,
height: `${this.newHeight}px`,
background: this.background,
};
},
},
template:
`<div style="width: fit-content; height: fit-content">
<n8n-resize-wrapper
v-bind="$props"
@resize="onResize"
@resizeend="onResizeEnd"
@resizestart="onResizeStart"
@input="onInput"
:width="newWidth"
:height="newHeight"
>
<div :style="containerStyles" />
</n8n-resize-wrapper>
</div>`,
methods,
});
export const Resize = Template.bind({});
Resize.args = {
width: 200,
height: 200,
minWidth: 200,
minHeight: 200,
scale: 1,
gridSize: 20,
isResizingEnabled: true,
supportedDirections: [
"right",
"top",
"bottom",
"left",
"topLeft",
"topRight",
"bottomLeft",
"bottomRight",
],
};

View file

@ -1,34 +1,24 @@
<template> <template>
<div :class="$style.resize"> <div :class="$style.resize">
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="right" :class="[$style.resizer, $style.right]" /> <div
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="left" :class="[$style.resizer, $style.left]" /> v-for="direction in enabledDirections"
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top" :class="[$style.resizer, $style.top]" /> :key="direction"
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="bottom" :class="[$style.resizer, $style.bottom]" /> :data-dir="direction"
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top-left" :class="[$style.resizer, $style.topLeft]" /> :class="[$style.resizer, $style[direction]]"
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top-right" :class="[$style.resizer, $style.topRight]" /> @mousedown="resizerMove"
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="bottom-left" :class="[$style.resizer, $style.bottomLeft]" /> />
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="bottom-right" :class="[$style.resizer, $style.bottomRight]" />
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
const cursorMap: { [key: string]: string } = { import Vue from 'vue';
right: 'ew-resize',
top: 'ns-resize',
bottom: 'ns-resize',
left: 'ew-resize',
'top-left': 'nw-resize',
'top-right' : 'ne-resize',
'bottom-left': 'sw-resize',
'bottom-right': 'se-resize',
};
function closestNumber(value: number, divisor: number): number { function closestNumber(value: number, divisor: number): number {
let q = value / divisor; const q = value / divisor;
let n1 = divisor * q; const n1 = divisor * q;
let n2 = (value * divisor) > 0 ? const n2 = (value * divisor) > 0 ?
(divisor * (q + 1)) : (divisor * (q - 1)); (divisor * (q + 1)) : (divisor * (q - 1));
if (Math.abs(value - n1) < Math.abs(value - n2)) if (Math.abs(value - n1) < Math.abs(value - n2))
@ -37,7 +27,7 @@ function closestNumber(value: number, divisor: number): number {
return n2; return n2;
} }
function getSize(delta: number, min: number, virtual: number, gridSize: number): number { function getSize(min: number, virtual: number, gridSize: number): number {
const target = closestNumber(virtual, gridSize); const target = closestNumber(virtual, gridSize);
if (target >= min && virtual > 0) { if (target >= min && virtual > 0) {
return target; return target;
@ -46,7 +36,16 @@ function getSize(delta: number, min: number, virtual: number, gridSize: number):
return min; return min;
}; };
import Vue from 'vue'; const directionsCursorMaps: { [key: string]: string } = {
right: 'ew-resize',
top: 'ns-resize',
bottom: 'ns-resize',
left: 'ew-resize',
topLeft: 'nw-resize',
topRight : 'ne-resize',
bottomLeft: 'sw-resize',
bottomRight: 'se-resize',
};
export default Vue.extend({ export default Vue.extend({
name: 'n8n-resize', name: 'n8n-resize',
@ -74,9 +73,14 @@ export default Vue.extend({
gridSize: { gridSize: {
type: Number, type: Number,
}, },
supportedDirections: {
type: Array,
default: () => [],
},
}, },
data() { data() {
return { return {
directionsCursorMaps,
dir: '', dir: '',
dHeight: 0, dHeight: 0,
dWidth: 0, dWidth: 0,
@ -86,6 +90,16 @@ export default Vue.extend({
y: 0, y: 0,
}; };
}, },
computed: {
enabledDirections() {
const availableDirections = Object.keys(directionsCursorMaps);
if(this.isResizingEnabled === false) return [];
if(this.supportedDirections.length === 0) return availableDirections;
return this.supportedDirections;
},
},
methods: { methods: {
resizerMove(event: MouseEvent) { resizerMove(event: MouseEvent) {
event.preventDefault(); event.preventDefault();
@ -93,10 +107,10 @@ export default Vue.extend({
const targetResizer = event.target as { dataset: { dir: string } } | null; const targetResizer = event.target as { dataset: { dir: string } } | null;
if (targetResizer) { if (targetResizer) {
this.dir = targetResizer.dataset.dir; this.dir = targetResizer.dataset.dir.toLocaleLowerCase();
} }
document.body.style.cursor = cursorMap[this.dir]; document.body.style.cursor = directionsCursorMaps[this.dir];
this.x = event.pageX; this.x = event.pageX;
this.y = event.pageY; this.y = event.pageY;
@ -137,17 +151,20 @@ export default Vue.extend({
this.vHeight = this.vHeight + deltaHeight; this.vHeight = this.vHeight + deltaHeight;
this.vWidth = this.vWidth + deltaWidth; this.vWidth = this.vWidth + deltaWidth;
const height = getSize(deltaHeight, this.minHeight, this.vHeight, this.gridSize); const height = getSize(this.minHeight, this.vHeight, this.gridSize);
const width = getSize(deltaWidth, this.minWidth, this.vWidth, this.gridSize); const width = getSize(this.minWidth, this.vWidth, this.gridSize);
const dX = left && width !== this.width ? -1 * (width - this.width) : 0; const dX = left && width !== this.width ? -1 * (width - this.width) : 0;
const dY = top && height !== this.height ? -1 * (height - this.height): 0; const dY = top && height !== this.height ? -1 * (height - this.height): 0;
const x = event.x;
const y = event.y;
const direction = this.dir;
this.$emit('resize', { height, width, dX, dY }); this.$emit('resize', { height, width, dX, dY, x, y, direction });
this.dHeight = dHeight; this.dHeight = dHeight;
this.dWidth = dWidth; this.dWidth = dWidth;
}, },
mouseUp(event: Event) { mouseUp(event: MouseEvent) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.$emit('resizeend'); this.$emit('resizeend');
@ -162,7 +179,7 @@ export default Vue.extend({
<style lang="scss" module> <style lang="scss" module>
.resize { .resize {
position: absolute; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 2; z-index: 2;
@ -170,7 +187,7 @@ export default Vue.extend({
.resizer { .resizer {
position: absolute; position: absolute;
z-index: 2; z-index: 3;
} }
.right { .right {
@ -211,7 +228,6 @@ export default Vue.extend({
top: -3px; top: -3px;
left: -3px; left: -3px;
cursor: nw-resize; cursor: nw-resize;
z-index: 3;
} }
.topRight { .topRight {
@ -220,7 +236,6 @@ export default Vue.extend({
top: -3px; top: -3px;
right: -3px; right: -3px;
cursor: ne-resize; cursor: ne-resize;
z-index: 3;
} }
.bottomLeft { .bottomLeft {
@ -229,7 +244,6 @@ export default Vue.extend({
bottom: -3px; bottom: -3px;
left: -3px; left: -3px;
cursor: sw-resize; cursor: sw-resize;
z-index: 3;
} }
.bottomRight { .bottomRight {
@ -238,6 +252,5 @@ export default Vue.extend({
bottom: -3px; bottom: -3px;
right: -3px; right: -3px;
cursor: se-resize; cursor: se-resize;
z-index: 3;
} }
</style> </style>

View file

@ -0,0 +1,3 @@
import ResizeWrapper from './ResizeWrapper.vue';
export default ResizeWrapper;

View file

@ -4,7 +4,7 @@
:style="styles" :style="styles"
@keydown.prevent @keydown.prevent
> >
<resize <n8n-resize-wrapper
:isResizingEnabled="!readOnly" :isResizingEnabled="!readOnly"
:height="height" :height="height"
:width="width" :width="width"
@ -60,14 +60,14 @@
</n8n-text> </n8n-text>
</div> </div>
</template> </template>
</resize> </n8n-resize-wrapper>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import N8nInput from '../N8nInput'; import N8nInput from '../N8nInput';
import N8nMarkdown from '../N8nMarkdown'; import N8nMarkdown from '../N8nMarkdown';
import Resize from './Resize.vue'; import N8nResizeWrapper from '../N8nResizeWrapper';
import N8nText from '../N8nText'; import N8nText from '../N8nText';
import Locale from '../../mixins/locale'; import Locale from '../../mixins/locale';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
@ -121,7 +121,7 @@ export default mixins(Locale).extend({
components: { components: {
N8nInput, N8nInput,
N8nMarkdown, N8nMarkdown,
Resize, N8nResizeWrapper,
N8nText, N8nText,
}, },
data() { data() {

View file

@ -40,6 +40,7 @@ import N8nTree from '../components/N8nTree';
import N8nUserInfo from '../components/N8nUserInfo'; import N8nUserInfo from '../components/N8nUserInfo';
import N8nUserSelect from '../components/N8nUserSelect'; import N8nUserSelect from '../components/N8nUserSelect';
import N8nUsersList from '../components/N8nUsersList'; import N8nUsersList from '../components/N8nUsersList';
import N8nResizeWrapper from '../components/N8nResizeWrapper';
export default { export default {
install: (app: typeof Vue, options?: {}) => { install: (app: typeof Vue, options?: {}) => {
@ -84,5 +85,6 @@ export default {
app.component('n8n-tree', N8nTree); app.component('n8n-tree', N8nTree);
app.component('n8n-users-list', N8nUsersList); app.component('n8n-users-list', N8nUsersList);
app.component('n8n-user-select', N8nUserSelect); app.component('n8n-user-select', N8nUserSelect);
app.component('n8n-resize-wrapper', N8nResizeWrapper);
}, },
}; };

View file

@ -934,6 +934,7 @@ export interface IUiState {
modals: { modals: {
[key: string]: IModalState; [key: string]: IModalState;
}; };
mainPanelDimensions: {[key: string]: {[key: string]: number}};
isPageLoading: boolean; isPageLoading: boolean;
currentView: string; currentView: string;
ndv: { ndv: {

View file

@ -7,31 +7,61 @@
<slot name="output"></slot> <slot name="output"></slot>
</div> </div>
<div :class="$style.mainPanel" :style="mainPanelStyles"> <div :class="$style.mainPanel" :style="mainPanelStyles">
<div :class="$style.dragButtonContainer" @click="close"> <n8n-resize-wrapper
<PanelDragButton :isResizingEnabled="currentNodePaneType !== 'unknown'"
:class="{ [$style.draggable]: true, [$style.visible]: isDragging }" :width="relativeWidthToPx(mainPanelDimensions.relativeWidth)"
v-if="!hideInputAndOutput && isDraggable" :minWidth="MIN_PANEL_WIDTH"
:canMoveLeft="canMoveLeft" :gridSize="20"
:canMoveRight="canMoveRight" @resize="onResize"
@dragstart="onDragStart" @resizestart="onResizeStart"
@drag="onDrag" @resizeend="onResizeEnd"
@dragend="onDragEnd" :supportedDirections="supportedResizeDirections"
/> >
</div> <div :class="$style.dragButtonContainer">
<slot name="main"></slot> <PanelDragButton
:class="{ [$style.draggable]: true, [$style.visible]: isDragging }"
:canMoveLeft="canMoveLeft"
:canMoveRight="canMoveRight"
v-if="!hideInputAndOutput && isDraggable"
@dragstart="onDragStart"
@drag="onDrag"
@dragend="onDragEnd"
/>
</div>
<div :class="{ [$style.mainPanelInner]: true, [$style.dragging]: isDragging }">
<slot name="main" />
</div>
</n8n-resize-wrapper>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue, { PropType } from 'vue';
import { get } from 'lodash';
import { INodeTypeDescription } from 'n8n-workflow';
import PanelDragButton from './PanelDragButton.vue'; import PanelDragButton from './PanelDragButton.vue';
const MAIN_PANEL_WIDTH = 360; import {
LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH,
MAIN_NODE_PANEL_WIDTH,
} from '@/constants';
const SIDE_MARGIN = 24; const SIDE_MARGIN = 24;
const FIXED_PANEL_WIDTH = 320; const SIDE_PANELS_MARGIN = 80;
const FIXED_PANEL_WIDTH_LARGE = 420; const MIN_PANEL_WIDTH = 280;
const MINIMUM_INPUT_PANEL_WIDTH = 320; const PANEL_WIDTH = 320;
const PANEL_WIDTH_LARGE = 420;
const initialMainPanelWidth:{ [key: string]: number } = {
regular: MAIN_NODE_PANEL_WIDTH,
dragless: MAIN_NODE_PANEL_WIDTH,
unknown: MAIN_NODE_PANEL_WIDTH,
inputless: MAIN_NODE_PANEL_WIDTH,
wide: MAIN_NODE_PANEL_WIDTH * 2,
};
export default Vue.extend({ export default Vue.extend({
name: 'NDVDraggablePanels', name: 'NDVDraggablePanels',
@ -48,124 +78,271 @@ export default Vue.extend({
position: { position: {
type: Number, type: Number,
}, },
nodeType: {
type: Object as PropType<INodeTypeDescription>,
default: () => ({}),
},
}, },
data() { data(): { windowWidth: number, isDragging: boolean, MIN_PANEL_WIDTH: number} {
return { return {
windowWidth: 0, windowWidth: 1,
isDragging: false, isDragging: false,
MIN_PANEL_WIDTH,
}; };
}, },
mounted() { mounted() {
this.setTotalWidth(); this.setTotalWidth();
/*
Only set(or restore) initial position if `mainPanelDimensions`
is at the default state({relativeLeft:1, relativeRight: 1, relativeWidth: 1}) to make sure we use store values if they are set
*/
if(this.mainPanelDimensions.relativeLeft === 1 && this.mainPanelDimensions.relativeRight === 1) {
this.setMainPanelWidth();
this.setPositions(this.getInitialLeftPosition(this.mainPanelDimensions.relativeWidth));
this.restorePositionData();
}
window.addEventListener('resize', this.setTotalWidth); window.addEventListener('resize', this.setTotalWidth);
this.$emit('init', { position: this.getRelativePosition() }); this.$emit('init', { position: this.mainPanelDimensions.relativeLeft });
}, },
destroyed() { destroyed() {
window.removeEventListener('resize', this.setTotalWidth); window.removeEventListener('resize', this.setTotalWidth);
}, },
computed: { computed: {
fixedPanelWidth() { mainPanelDimensions(): {
if (this.windowWidth > 1700) { relativeWidth: number,
return FIXED_PANEL_WIDTH_LARGE; relativeLeft: number,
} relativeRight: number
} {
return FIXED_PANEL_WIDTH; return this.$store.getters['ui/mainPanelDimensions'](this.currentNodePaneType);
}, },
mainPanelPosition(): number { supportedResizeDirections() {
if (typeof this.position === 'number') { const supportedDirections = ['right'];
return this.position;
}
if (!this.isDraggable) { if(this.isDraggable) supportedDirections.push('left');
return this.fixedPanelWidth + MAIN_PANEL_WIDTH / 2 + SIDE_MARGIN; return supportedDirections;
} },
currentNodePaneType() {
const relativePosition = this.$store.getters['ui/mainPanelPosition'] as number; if(!this.hasInputSlot) return 'inputless';
if(!this.isDraggable) return 'dragless';
return relativePosition * this.windowWidth; if(this.nodeType === null) return 'unknown';
return get(this, 'nodeType.parameterPane') || 'regular';
},
hasInputSlot() {
return this.$slots.input !== undefined;
}, },
inputPanelMargin(): number { inputPanelMargin(): number {
return !this.isDraggable? 0 : 80; return this.pxToRelativeWidth(SIDE_PANELS_MARGIN);
},
minWindowWidth() {
return 2 * (SIDE_MARGIN + SIDE_PANELS_MARGIN) + MIN_PANEL_WIDTH;
}, },
minimumLeftPosition(): number { minimumLeftPosition(): number {
return SIDE_MARGIN + this.inputPanelMargin; if(this.windowWidth < this.minWindowWidth) return this.pxToRelativeWidth(1);
if(!this.hasInputSlot) return this.pxToRelativeWidth(SIDE_MARGIN);
return this.pxToRelativeWidth(SIDE_MARGIN + 20) + this.inputPanelMargin;
}, },
maximumRightPosition(): number { maximumRightPosition(): number {
return this.windowWidth - MAIN_PANEL_WIDTH - this.minimumLeftPosition; if(this.windowWidth < this.minWindowWidth) return this.pxToRelativeWidth(1);
},
mainPanelFinalPositionPx(): number {
const padding = this.minimumLeftPosition;
let pos = this.mainPanelPosition + MAIN_PANEL_WIDTH / 2;
pos = Math.max(padding, pos - MAIN_PANEL_WIDTH);
pos = Math.min(pos, this.maximumRightPosition);
return pos; return this.pxToRelativeWidth(SIDE_MARGIN + 20) + this.inputPanelMargin;
}, },
canMoveLeft(): boolean { canMoveLeft(): boolean {
return this.mainPanelFinalPositionPx > this.minimumLeftPosition; return this.mainPanelDimensions.relativeLeft > this.minimumLeftPosition;
}, },
canMoveRight(): boolean { canMoveRight(): boolean {
return this.mainPanelFinalPositionPx < this.maximumRightPosition; return this.mainPanelDimensions.relativeRight > this.maximumRightPosition;
}, },
mainPanelStyles(): { left: string } { mainPanelStyles(): { left: string, right: string } {
return { return {
left: `${this.mainPanelFinalPositionPx}px`, 'left': `${this.relativeWidthToPx(this.mainPanelDimensions.relativeLeft)}px`,
'right': `${this.relativeWidthToPx(this.mainPanelDimensions.relativeRight)}px`,
}; };
}, },
inputPanelStyles(): { width: string } { inputPanelStyles():{ right: string } {
if (!this.isDraggable) { return {
return { right: `${this.relativeWidthToPx(this.calculatedPositions.inputPanelRelativeRight)}px`,
width: `${this.fixedPanelWidth}px`, };
}; },
outputPanelStyles(): { left: string, transform: string} {
return {
left: `${this.relativeWidthToPx(this.calculatedPositions.outputPanelRelativeLeft)}px`,
transform: `translateX(-${this.relativeWidthToPx(this.outputPanelRelativeTranslate)}px)`,
};
},
calculatedPositions():{ inputPanelRelativeRight: number, outputPanelRelativeLeft: number } {
const hasInput = this.$slots.input !== undefined;
const outputPanelRelativeLeft = this.mainPanelDimensions.relativeLeft + this.mainPanelDimensions.relativeWidth;
const inputPanelRelativeRight = hasInput
? 1 - outputPanelRelativeLeft + this.mainPanelDimensions.relativeWidth
: (1 - this.pxToRelativeWidth(SIDE_MARGIN));
return {
inputPanelRelativeRight,
outputPanelRelativeLeft,
};
},
outputPanelRelativeTranslate():number {
const panelMinLeft = 1 - this.pxToRelativeWidth(MIN_PANEL_WIDTH + SIDE_MARGIN);
const currentRelativeLeftDelta = this.calculatedPositions.outputPanelRelativeLeft - panelMinLeft;
return currentRelativeLeftDelta > 0 ? currentRelativeLeftDelta : 0;
},
hasDoubleWidth() {
return get(this, 'nodeType.parameterPane') === 'wide';
},
fixedPanelWidth(): number {
const multiplier = this.hasDoubleWidth ? 2 : 1;
if (this.windowWidth > 1700) {
return PANEL_WIDTH_LARGE * multiplier;
} }
let width = this.mainPanelPosition - MAIN_PANEL_WIDTH / 2 - SIDE_MARGIN; return PANEL_WIDTH * multiplier;
width = Math.min(
width,
this.windowWidth - SIDE_MARGIN * 2 - this.inputPanelMargin - MAIN_PANEL_WIDTH,
);
width = Math.max(320, width);
return {
width: `${width}px`,
};
}, },
outputPanelStyles(): { width: string } { isBelowMinWidthMainPanel(): boolean {
let width = this.windowWidth - this.mainPanelPosition - MAIN_PANEL_WIDTH / 2 - SIDE_MARGIN; const minRelativeWidth = this.pxToRelativeWidth(MIN_PANEL_WIDTH);
width = Math.min( return this.mainPanelDimensions.relativeWidth < minRelativeWidth;
width, },
this.windowWidth - SIDE_MARGIN * 2 - this.inputPanelMargin - MAIN_PANEL_WIDTH, },
); watch: {
width = Math.max(MINIMUM_INPUT_PANEL_WIDTH, width); windowWidth(windowWidth) {
return { const minRelativeWidth = this.pxToRelativeWidth(MIN_PANEL_WIDTH);
width: `${width}px`, // Prevent the panel resizing below MIN_PANEL_WIDTH whhile maintaing position
}; if(this.isBelowMinWidthMainPanel) {
this.setMainPanelWidth(minRelativeWidth);
}
const isBelowMinLeft = this.minimumLeftPosition > this.mainPanelDimensions.relativeLeft;
const isMaxRight = this.maximumRightPosition > this.mainPanelDimensions.relativeRight;
// When user is resizing from non-supported view(sub ~488px) we need to refit the panels
if((windowWidth > this.minWindowWidth) && isBelowMinLeft && isMaxRight) {
this.setMainPanelWidth(minRelativeWidth);
this.setPositions(this.getInitialLeftPosition(this.mainPanelDimensions.relativeWidth));
}
this.setPositions(this.mainPanelDimensions.relativeLeft);
}, },
}, },
methods: { methods: {
getRelativePosition() { getInitialLeftPosition(width: number) {
const current = this.mainPanelFinalPositionPx + MAIN_PANEL_WIDTH / 2 - this.windowWidth / 2; if(this.currentNodePaneType === 'dragless') return this.pxToRelativeWidth(SIDE_MARGIN + 1 + this.fixedPanelWidth);
const pos = Math.floor( return this.hasInputSlot
(current / ((this.maximumRightPosition - this.minimumLeftPosition) / 2)) * 100, ? 0.5 - (width / 2)
: this.minimumLeftPosition;
},
setMainPanelWidth(relativeWidth?: number) {
const mainPanelRelativeWidth = relativeWidth || this.pxToRelativeWidth(initialMainPanelWidth[this.currentNodePaneType]);
this.$store.commit('ui/setMainPanelDimensions', {
panelType: this.currentNodePaneType,
dimensions: {
relativeWidth: mainPanelRelativeWidth,
},
});
},
setPositions(relativeLeft: number) {
const mainPanelRelativeLeft = relativeLeft || 1 - this.calculatedPositions.inputPanelRelativeRight;
const mainPanelRelativeRight = 1 - mainPanelRelativeLeft - this.mainPanelDimensions.relativeWidth;
const isMaxRight = this.maximumRightPosition > mainPanelRelativeRight;
const isMinLeft = this.minimumLeftPosition > mainPanelRelativeLeft;
const isInputless = this.currentNodePaneType === 'inputless';
if(isMinLeft) {
this.$store.commit('ui/setMainPanelDimensions', {
panelType: this.currentNodePaneType,
dimensions: {
relativeLeft: this.minimumLeftPosition,
relativeRight: 1 - this.mainPanelDimensions.relativeWidth - this.minimumLeftPosition,
},
});
return;
}
if(isMaxRight) {
this.$store.commit('ui/setMainPanelDimensions', {
panelType: this.currentNodePaneType,
dimensions: {
relativeLeft: 1 - this.mainPanelDimensions.relativeWidth - this.maximumRightPosition,
relativeRight: this.maximumRightPosition,
},
});
return;
}
this.$store.commit('ui/setMainPanelDimensions', {
panelType: this.currentNodePaneType,
dimensions: {
relativeLeft: isInputless ? this.minimumLeftPosition : mainPanelRelativeLeft,
relativeRight: mainPanelRelativeRight,
},
});
},
pxToRelativeWidth(px: number) {
return px / this.windowWidth;
},
relativeWidthToPx(relativeWidth: number) {
return relativeWidth * this.windowWidth;
},
onResizeStart() {
this.setTotalWidth();
},
onResizeEnd() {
this.storePositionData();
},
onResize({ direction, x, width }: { direction: string, x: number, width: number}) {
const relativeDistance = this.pxToRelativeWidth(x);
const relativeWidth = this.pxToRelativeWidth(width);
if(direction === "left" && relativeDistance <= this.minimumLeftPosition) return;
if(direction === "right" && (1 - relativeDistance) <= this.maximumRightPosition) return;
if(width <= MIN_PANEL_WIDTH) return;
this.setMainPanelWidth(relativeWidth);
this.setPositions(direction === 'left'
? relativeDistance
: this.mainPanelDimensions.relativeLeft,
); );
return pos; },
restorePositionData() {
const storedPanelWidthData = window.localStorage.getItem(`${LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH}_${this.currentNodePaneType}`);
if(storedPanelWidthData) {
const parsedWidth = parseFloat(storedPanelWidthData);
this.setMainPanelWidth(parsedWidth);
const initialPosition = this.getInitialLeftPosition(parsedWidth);
this.setPositions(initialPosition);
return true;
}
return false;
},
storePositionData() {
window.localStorage.setItem(`${LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH}_${this.currentNodePaneType}`, this.mainPanelDimensions.relativeWidth.toString());
}, },
onDragStart() { onDragStart() {
this.isDragging = true; this.isDragging = true;
this.$emit('dragstart', { position: this.getRelativePosition() }); this.$emit('dragstart', { position: this.mainPanelDimensions.relativeLeft });
}, },
onDrag(e: {x: number, y: number}) { onDrag(e: {x: number, y: number}) {
const relativePosition = e.x / this.windowWidth; const relativeLeft = this.pxToRelativeWidth(e.x) - (this.mainPanelDimensions.relativeWidth / 2);
this.$store.commit('ui/setMainPanelRelativePosition', relativePosition);
this.setPositions(relativeLeft);
}, },
onDragEnd() { onDragEnd() {
setTimeout(() => { setTimeout(() => {
this.isDragging = false; this.isDragging = false;
this.$emit('dragend', { this.$emit('dragend', {
windowWidth: this.windowWidth, windowWidth: this.windowWidth,
position: this.getRelativePosition(), position: this.mainPanelDimensions.relativeLeft,
}); });
}, 0); }, 0);
this.storePositionData();
}, },
setTotalWidth() { setTotalWidth() {
this.windowWidth = window.innerWidth; this.windowWidth = window.innerWidth;
@ -178,14 +355,13 @@ export default Vue.extend({
</script> </script>
<style lang="scss" module> <style lang="scss" module>
$--main-panel-width: 360px;
.dataPanel { .dataPanel {
position: absolute; position: absolute;
height: calc(100% - 2 * var(--spacing-l)); height: calc(100% - 2 * var(--spacing-l));
position: absolute; position: absolute;
top: var(--spacing-l); top: var(--spacing-l);
z-index: 0; z-index: 0;
min-width: 280px;
} }
.inputPanel { .inputPanel {
@ -200,7 +376,6 @@ $--main-panel-width: 360px;
.outputPanel { .outputPanel {
composes: dataPanel; composes: dataPanel;
right: var(--spacing-l); right: var(--spacing-l);
width: $--main-panel-width;
> * { > * {
border-radius: 0 var(--border-radius-large) var(--border-radius-large) 0; border-radius: 0 var(--border-radius-large) var(--border-radius-large) 0;
@ -218,18 +393,35 @@ $--main-panel-width: 360px;
} }
} }
.mainPanelInner {
height: 100%;
border: var(--border-base);
border-radius: var(--border-radius-large);
box-shadow: 0 4px 16px rgb(50 61 85 / 10%);
overflow: hidden;
&.dragging {
border-color: var(--color-primary);
box-shadow: 0px 6px 16px rgba(255, 74, 51, 0.15);
}
}
.draggable { .draggable {
position: absolute;
left: 40%;
visibility: hidden; visibility: hidden;
} }
.dragButtonContainer { .dragButtonContainer {
position: absolute; position: absolute;
top: -12px; top: -12px;
width: $--main-panel-width; width: 100%;
height: 12px; height: 12px;
display: flex;
justify-content: center;
pointer-events: none;
.draggable {
pointer-events: all;
}
&:hover .draggable { &:hover .draggable {
visibility: visible; visibility: visible;
} }

View file

@ -32,6 +32,7 @@
:hideInputAndOutput="activeNodeType === null" :hideInputAndOutput="activeNodeType === null"
:position="isTriggerNode && !showTriggerPanel ? 0 : undefined" :position="isTriggerNode && !showTriggerPanel ? 0 : undefined"
:isDraggable="!isTriggerNode" :isDraggable="!isTriggerNode"
:nodeType="activeNodeType"
@close="close" @close="close"
@init="onPanelsInit" @init="onPanelsInit"
@dragstart="onDragStart" @dragstart="onDragStart"
@ -82,6 +83,7 @@
:eventBus="settingsEventBus" :eventBus="settingsEventBus"
:dragging="isDragging" :dragging="isDragging"
:sessionId="sessionId" :sessionId="sessionId"
:nodeType="activeNodeType"
@valueChanged="valueChanged" @valueChanged="valueChanged"
@execute="onNodeExecute" @execute="onNodeExecute"
@activate="onWorkflowActivate" @activate="onWorkflowActivate"

View file

@ -1,5 +1,6 @@
<template> <template>
<div :class="{'node-settings': true, 'dragging': dragging}" @keydown.stop> <div :class="{
'node-settings': true, 'dragging': dragging }" @keydown.stop>
<div :class="$style.header"> <div :class="$style.header">
<div class="header-side-menu"> <div class="header-side-menu">
<NodeTitle class="node-name" :value="node && node.name" :nodeType="nodeType" @input="nameChanged" :readOnly="isReadOnly"></NodeTitle> <NodeTitle class="node-name" :value="node && node.name" :nodeType="nodeType" @input="nameChanged" :readOnly="isReadOnly"></NodeTitle>
@ -96,7 +97,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue, { PropType } from 'vue';
import { import {
INodeTypeDescription, INodeTypeDescription,
INodeParameters, INodeParameters,
@ -113,7 +114,8 @@ import {
import { import {
COMMUNITY_NODES_INSTALLATION_DOCS_URL, COMMUNITY_NODES_INSTALLATION_DOCS_URL,
CUSTOM_NODES_DOCS_URL, CUSTOM_NODES_DOCS_URL,
} from '../constants'; MAIN_NODE_PANEL_WIDTH,
} from '@/constants';
import NodeTitle from '@/components/NodeTitle.vue'; import NodeTitle from '@/components/NodeTitle.vue';
import ParameterInputFull from '@/components/ParameterInputFull.vue'; import ParameterInputFull from '@/components/ParameterInputFull.vue';
@ -148,13 +150,6 @@ export default mixins(
NodeExecuteButton, NodeExecuteButton,
}, },
computed: { computed: {
nodeType (): INodeTypeDescription | null {
if (this.node) {
return this.$store.getters['nodeTypes/getNodeType'](this.node.type, this.node.typeVersion);
}
return null;
},
nodeTypeName(): string { nodeTypeName(): string {
if (this.nodeType) { if (this.nodeType) {
const shortNodeType = this.$locale.shortNodeType(this.nodeType.name); const shortNodeType = this.$locale.shortNodeType(this.nodeType.name);
@ -224,6 +219,9 @@ export default mixins(
sessionId: { sessionId: {
type: String, type: String,
}, },
nodeType: {
type: Object as PropType<INodeTypeDescription>,
},
}, },
data () { data () {
return { return {
@ -336,6 +334,7 @@ export default mixins(
] as INodeProperties[], ] as INodeProperties[],
COMMUNITY_NODES_INSTALLATION_DOCS_URL, COMMUNITY_NODES_INSTALLATION_DOCS_URL,
CUSTOM_NODES_DOCS_URL, CUSTOM_NODES_DOCS_URL,
MAIN_NODE_PANEL_WIDTH,
}; };
}, },
watch: { watch: {
@ -641,13 +640,9 @@ export default mixins(
<style lang="scss"> <style lang="scss">
.node-settings { .node-settings {
overflow: hidden; overflow: hidden;
min-width: 360px;
max-width: 360px;
background-color: var(--color-background-xlight); background-color: var(--color-background-xlight);
height: 100%; height: 100%;
border: var(--border-base); width: 100%;
border-radius: var(--border-radius-large);
box-shadow: 0 4px 16px rgb(50 61 85 / 10%);
.no-parameters { .no-parameters {
margin-top: var(--spacing-xs); margin-top: var(--spacing-xs);

View file

@ -1,5 +1,5 @@
<template> <template>
<Draggable type="panel-resize" @drag="onDrag" @dragstart="onDragStart" @dragend="onDragEnd"> <Draggable type="panel-resize" @drag="onDrag" @dragstart="onDragStart" @dragend="onDragEnd" :class="$style.dragContainer">
<template v-slot="{ isDragging }"> <template v-slot="{ isDragging }">
<div <div
:class="{ [$style.dragButton]: true }" :class="{ [$style.dragButton]: true }"
@ -63,6 +63,9 @@ export default mixins(dragging).extend({
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.dragContainer {
pointer-events: all;
}
.dragButton { .dragButton {
background-color: var(--color-background-base); background-color: var(--color-background-base);
width: 64px; width: 64px;
@ -74,6 +77,8 @@ export default mixins(dragging).extend({
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: visible; overflow: visible;
position: relative;
z-index: 3;
&:hover { &:hover {
.leftArrow, .rightArrow { .leftArrow, .rightArrow {

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="resource-locator"> <div class="resource-locator" ref="container">
<resource-locator-dropdown <resource-locator-dropdown
:value="value ? value.value: ''" :value="value ? value.value: ''"
:show="showResourceDropdown" :show="showResourceDropdown"
@ -10,6 +10,7 @@
:filter="searchFilter" :filter="searchFilter"
:hasMore="currentQueryHasMore" :hasMore="currentQueryHasMore"
:errorView="currentQueryError" :errorView="currentQueryError"
:width="width"
@input="onListItemSelected" @input="onListItemSelected"
@hide="onDropdownHide" @hide="onDropdownHide"
@filter="onSearchFilter" @filter="onSearchFilter"
@ -251,10 +252,12 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({
}, },
data() { data() {
return { return {
mainPanelMutationSubscription: () => {},
showResourceDropdown: false, showResourceDropdown: false,
searchFilter: '', searchFilter: '',
cachedResponses: {} as { [key: string]: IResourceLocatorQuery }, cachedResponses: {} as { [key: string]: IResourceLocatorQuery },
hasCompletedASearch: false, hasCompletedASearch: false,
width: 0,
}; };
}, },
computed: { computed: {
@ -423,8 +426,23 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({
}, },
mounted() { mounted() {
this.$on('refreshList', this.refreshList); this.$on('refreshList', this.refreshList);
this.setWidth();
window.addEventListener('resize', this.setWidth);
this.mainPanelMutationSubscription = this.$store.subscribe(this.setWidthOnMainPanelResize);
},
beforeDestroy() {
// Unsubscribe
this.mainPanelMutationSubscription();
window.removeEventListener('resize', this.setWidth);
}, },
methods: { methods: {
setWidth() {
this.width = (this.$refs.container as HTMLElement).offsetWidth;
},
setWidthOnMainPanelResize(mutation: { type: string }) {
// Update the width when main panel dimension change
if(mutation.type === 'ui/setMainPanelDimensions') this.setWidth();
},
getLinkAlt(entity: string) { getLinkAlt(entity: string) {
if (this.selectedMode === 'list' && entity) { if (this.selectedMode === 'list' && entity) {
return this.$locale.baseText('resourceLocator.openSpecificResource', { interpolate: { entity, appName: this.appName } }); return this.$locale.baseText('resourceLocator.openSpecificResource', { interpolate: { entity, appName: this.appName } });

View file

@ -1,7 +1,7 @@
<template> <template>
<n8n-popover <n8n-popover
placement="bottom" placement="bottom"
width="318" :width="width"
:popper-class="$style.popover" :popper-class="$style.popover"
:value="show" :value="show"
trigger="manual" trigger="manual"
@ -90,6 +90,9 @@ export default Vue.extend({
filterRequired: { filterRequired: {
type: Boolean, type: Boolean,
}, },
width: {
type: Number,
},
}, },
data() { data() {
return { return {
@ -254,7 +257,7 @@ export default Vue.extend({
--input-font-size: var(--font-size-2xs); --input-font-size: var(--font-size-2xs);
position: absolute; position: absolute;
top: 0; top: 0;
width: 316px; width: 100%;
z-index: 1; z-index: 1;
} }

View file

@ -118,3 +118,7 @@ export function isValueExpression (parameter: INodeProperties, paramValue: NodeP
} }
return false; return false;
} }
export function convertRemToPixels(rem: string) {
return parseInt(rem, 10) * parseFloat(getComputedStyle(document.documentElement).fontSize);
}

View file

@ -233,6 +233,7 @@ export const LOCAL_STORAGE_ACTIVATION_FLAG = 'N8N_HIDE_ACTIVATION_ALERT';
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG = 'N8N_PIN_DATA_DISCOVERY_NDV'; export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG = 'N8N_PIN_DATA_DISCOVERY_NDV';
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG = 'N8N_PIN_DATA_DISCOVERY_CANVAS'; export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG = 'N8N_PIN_DATA_DISCOVERY_CANVAS';
export const LOCAL_STORAGE_MAPPING_FLAG = 'N8N_MAPPING_ONBOARDED'; export const LOCAL_STORAGE_MAPPING_FLAG = 'N8N_MAPPING_ONBOARDED';
export const LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH = 'N8N_MAIN_PANEL_RELATIVE_WIDTH';
export const BASE_NODE_SURVEY_URL = 'https://n8n-community.typeform.com/to/BvmzxqYv#nodename='; export const BASE_NODE_SURVEY_URL = 'https://n8n-community.typeform.com/to/BvmzxqYv#nodename=';
export const HIRING_BANNER = ` export const HIRING_BANNER = `
@ -317,3 +318,4 @@ export const DEFAULT_STICKY_WIDTH = 240;
export enum EnterpriseEditionFeature { export enum EnterpriseEditionFeature {
Sharing = 'sharing', Sharing = 'sharing',
} }
export const MAIN_NODE_PANEL_WIDTH = 360;

View file

@ -23,6 +23,7 @@ import {
ONBOARDING_CALL_SIGNUP_MODAL_KEY, ONBOARDING_CALL_SIGNUP_MODAL_KEY,
FAKE_DOOR_FEATURES, FAKE_DOOR_FEATURES,
COMMUNITY_PACKAGE_MANAGE_ACTIONS, COMMUNITY_PACKAGE_MANAGE_ACTIONS,
MAIN_NODE_PANEL_WIDTH,
} from '@/constants'; } from '@/constants';
import Vue from 'vue'; import Vue from 'vue';
import { ActionContext, Module } from 'vuex'; import { ActionContext, Module } from 'vuex';
@ -109,6 +110,7 @@ const module: Module<IUiState, IRootState> = {
sidebarMenuCollapsed: true, sidebarMenuCollapsed: true,
isPageLoading: true, isPageLoading: true,
currentView: '', currentView: '',
mainPanelDimensions: {},
ndv: { ndv: {
sessionId: '', sessionId: '',
input: { input: {
@ -205,10 +207,22 @@ const module: Module<IUiState, IRootState> = {
draggableType: (state: IUiState) => state.draggable.type, draggableType: (state: IUiState) => state.draggable.type,
draggableData: (state: IUiState) => state.draggable.data, draggableData: (state: IUiState) => state.draggable.data,
canDraggableDrop: (state: IUiState) => state.draggable.canDrop, canDraggableDrop: (state: IUiState) => state.draggable.canDrop,
mainPanelDimensions: (state: IUiState) => (panelType: string) => {
const defaults = { relativeRight: 1, relativeLeft: 1, relativeWidth: 1 };
return {...defaults, ...state.mainPanelDimensions[panelType]};
},
draggableStickyPos: (state: IUiState) => state.draggable.stickyPosition, draggableStickyPos: (state: IUiState) => state.draggable.stickyPosition,
mappingTelemetry: (state: IUiState) => state.ndv.mappingTelemetry, mappingTelemetry: (state: IUiState) => state.ndv.mappingTelemetry,
}, },
mutations: { mutations: {
setMainPanelDimensions: (state: IUiState, params: { panelType:string, dimensions: { relativeLeft?: number, relativeRight?: number, relativeWidth?: number }}) => {
Vue.set(
state.mainPanelDimensions,
params.panelType,
{...state.mainPanelDimensions[params.panelType], ...params.dimensions },
);
},
setMode: (state: IUiState, params: {name: string, mode: string}) => { setMode: (state: IUiState, params: {name: string, mode: string}) => {
const { name, mode } = params; const { name, mode } = params;
Vue.set(state.modals[name], 'mode', mode); Vue.set(state.modals[name], 'mode', mode);
@ -259,9 +273,6 @@ const module: Module<IUiState, IRootState> = {
setOutputPanelEditModeValue: (state: IUiState, payload: string) => { setOutputPanelEditModeValue: (state: IUiState, payload: string) => {
Vue.set(state.ndv.output.editMode, 'value', payload); Vue.set(state.ndv.output.editMode, 'value', payload);
}, },
setMainPanelRelativePosition(state: IUiState, relativePosition: number) {
state.mainPanelPosition = relativePosition;
},
setMappableNDVInputFocus(state: IUiState, paramName: string) { setMappableNDVInputFocus(state: IUiState, paramName: string) {
Vue.set(state.ndv, 'focusedMappableInput', paramName); Vue.set(state.ndv, 'focusedMappableInput', paramName);
}, },