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:
@@ -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",
|
||||
],
|
||||
};
|
||||
@@ -1,34 +1,24 @@
|
||||
<template>
|
||||
<div :class="$style.resize">
|
||||
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="right" :class="[$style.resizer, $style.right]" />
|
||||
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="left" :class="[$style.resizer, $style.left]" />
|
||||
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top" :class="[$style.resizer, $style.top]" />
|
||||
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="bottom" :class="[$style.resizer, $style.bottom]" />
|
||||
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top-left" :class="[$style.resizer, $style.topLeft]" />
|
||||
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top-right" :class="[$style.resizer, $style.topRight]" />
|
||||
<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]" />
|
||||
<div
|
||||
v-for="direction in enabledDirections"
|
||||
:key="direction"
|
||||
:data-dir="direction"
|
||||
:class="[$style.resizer, $style[direction]]"
|
||||
@mousedown="resizerMove"
|
||||
/>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
const cursorMap: { [key: string]: string } = {
|
||||
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',
|
||||
};
|
||||
import Vue from 'vue';
|
||||
|
||||
function closestNumber(value: number, divisor: number): number {
|
||||
let q = value / divisor;
|
||||
let n1 = divisor * q;
|
||||
const q = value / divisor;
|
||||
const n1 = divisor * q;
|
||||
|
||||
let n2 = (value * divisor) > 0 ?
|
||||
const n2 = (value * divisor) > 0 ?
|
||||
(divisor * (q + 1)) : (divisor * (q - 1));
|
||||
|
||||
if (Math.abs(value - n1) < Math.abs(value - n2))
|
||||
@@ -37,7 +27,7 @@ function closestNumber(value: number, divisor: number): number {
|
||||
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);
|
||||
if (target >= min && virtual > 0) {
|
||||
return target;
|
||||
@@ -46,7 +36,16 @@ function getSize(delta: number, min: number, virtual: number, gridSize: number):
|
||||
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({
|
||||
name: 'n8n-resize',
|
||||
@@ -74,9 +73,14 @@ export default Vue.extend({
|
||||
gridSize: {
|
||||
type: Number,
|
||||
},
|
||||
supportedDirections: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
directionsCursorMaps,
|
||||
dir: '',
|
||||
dHeight: 0,
|
||||
dWidth: 0,
|
||||
@@ -86,6 +90,16 @@ export default Vue.extend({
|
||||
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: {
|
||||
resizerMove(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
@@ -93,10 +107,10 @@ export default Vue.extend({
|
||||
|
||||
const targetResizer = event.target as { dataset: { dir: string } } | null;
|
||||
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.y = event.pageY;
|
||||
@@ -137,17 +151,20 @@ export default Vue.extend({
|
||||
|
||||
this.vHeight = this.vHeight + deltaHeight;
|
||||
this.vWidth = this.vWidth + deltaWidth;
|
||||
const height = getSize(deltaHeight, this.minHeight, this.vHeight, this.gridSize);
|
||||
const width = getSize(deltaWidth, this.minWidth, this.vWidth, this.gridSize);
|
||||
const height = getSize(this.minHeight, this.vHeight, this.gridSize);
|
||||
const width = getSize(this.minWidth, this.vWidth, this.gridSize);
|
||||
|
||||
const dX = left && width !== this.width ? -1 * (width - this.width) : 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.dWidth = dWidth;
|
||||
},
|
||||
mouseUp(event: Event) {
|
||||
mouseUp(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.$emit('resizeend');
|
||||
@@ -162,7 +179,7 @@ export default Vue.extend({
|
||||
|
||||
<style lang="scss" module>
|
||||
.resize {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
@@ -170,7 +187,7 @@ export default Vue.extend({
|
||||
|
||||
.resizer {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.right {
|
||||
@@ -211,7 +228,6 @@ export default Vue.extend({
|
||||
top: -3px;
|
||||
left: -3px;
|
||||
cursor: nw-resize;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.topRight {
|
||||
@@ -220,7 +236,6 @@ export default Vue.extend({
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
cursor: ne-resize;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.bottomLeft {
|
||||
@@ -229,7 +244,6 @@ export default Vue.extend({
|
||||
bottom: -3px;
|
||||
left: -3px;
|
||||
cursor: sw-resize;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.bottomRight {
|
||||
@@ -238,6 +252,5 @@ export default Vue.extend({
|
||||
bottom: -3px;
|
||||
right: -3px;
|
||||
cursor: se-resize;
|
||||
z-index: 3;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
import ResizeWrapper from './ResizeWrapper.vue';
|
||||
|
||||
export default ResizeWrapper;
|
||||
@@ -4,7 +4,7 @@
|
||||
:style="styles"
|
||||
@keydown.prevent
|
||||
>
|
||||
<resize
|
||||
<n8n-resize-wrapper
|
||||
:isResizingEnabled="!readOnly"
|
||||
:height="height"
|
||||
:width="width"
|
||||
@@ -60,14 +60,14 @@
|
||||
</n8n-text>
|
||||
</div>
|
||||
</template>
|
||||
</resize>
|
||||
</n8n-resize-wrapper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import N8nInput from '../N8nInput';
|
||||
import N8nMarkdown from '../N8nMarkdown';
|
||||
import Resize from './Resize.vue';
|
||||
import N8nResizeWrapper from '../N8nResizeWrapper';
|
||||
import N8nText from '../N8nText';
|
||||
import Locale from '../../mixins/locale';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
@@ -121,7 +121,7 @@ export default mixins(Locale).extend({
|
||||
components: {
|
||||
N8nInput,
|
||||
N8nMarkdown,
|
||||
Resize,
|
||||
N8nResizeWrapper,
|
||||
N8nText,
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -40,6 +40,7 @@ import N8nTree from '../components/N8nTree';
|
||||
import N8nUserInfo from '../components/N8nUserInfo';
|
||||
import N8nUserSelect from '../components/N8nUserSelect';
|
||||
import N8nUsersList from '../components/N8nUsersList';
|
||||
import N8nResizeWrapper from '../components/N8nResizeWrapper';
|
||||
|
||||
export default {
|
||||
install: (app: typeof Vue, options?: {}) => {
|
||||
@@ -84,5 +85,6 @@ export default {
|
||||
app.component('n8n-tree', N8nTree);
|
||||
app.component('n8n-users-list', N8nUsersList);
|
||||
app.component('n8n-user-select', N8nUserSelect);
|
||||
app.component('n8n-resize-wrapper', N8nResizeWrapper);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user