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
16 changed files with 510 additions and 182 deletions

View File

@@ -7,31 +7,61 @@
<slot name="output"></slot>
</div>
<div :class="$style.mainPanel" :style="mainPanelStyles">
<div :class="$style.dragButtonContainer" @click="close">
<PanelDragButton
:class="{ [$style.draggable]: true, [$style.visible]: isDragging }"
v-if="!hideInputAndOutput && isDraggable"
:canMoveLeft="canMoveLeft"
:canMoveRight="canMoveRight"
@dragstart="onDragStart"
@drag="onDrag"
@dragend="onDragEnd"
/>
</div>
<slot name="main"></slot>
<n8n-resize-wrapper
:isResizingEnabled="currentNodePaneType !== 'unknown'"
:width="relativeWidthToPx(mainPanelDimensions.relativeWidth)"
:minWidth="MIN_PANEL_WIDTH"
:gridSize="20"
@resize="onResize"
@resizestart="onResizeStart"
@resizeend="onResizeEnd"
:supportedDirections="supportedResizeDirections"
>
<div :class="$style.dragButtonContainer">
<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>
</template>
<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';
const MAIN_PANEL_WIDTH = 360;
import {
LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH,
MAIN_NODE_PANEL_WIDTH,
} from '@/constants';
const SIDE_MARGIN = 24;
const FIXED_PANEL_WIDTH = 320;
const FIXED_PANEL_WIDTH_LARGE = 420;
const MINIMUM_INPUT_PANEL_WIDTH = 320;
const SIDE_PANELS_MARGIN = 80;
const MIN_PANEL_WIDTH = 280;
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({
name: 'NDVDraggablePanels',
@@ -48,124 +78,271 @@ export default Vue.extend({
position: {
type: Number,
},
nodeType: {
type: Object as PropType<INodeTypeDescription>,
default: () => ({}),
},
},
data() {
data(): { windowWidth: number, isDragging: boolean, MIN_PANEL_WIDTH: number} {
return {
windowWidth: 0,
windowWidth: 1,
isDragging: false,
MIN_PANEL_WIDTH,
};
},
mounted() {
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);
this.$emit('init', { position: this.getRelativePosition() });
this.$emit('init', { position: this.mainPanelDimensions.relativeLeft });
},
destroyed() {
window.removeEventListener('resize', this.setTotalWidth);
},
computed: {
fixedPanelWidth() {
if (this.windowWidth > 1700) {
return FIXED_PANEL_WIDTH_LARGE;
}
return FIXED_PANEL_WIDTH;
mainPanelDimensions(): {
relativeWidth: number,
relativeLeft: number,
relativeRight: number
} {
return this.$store.getters['ui/mainPanelDimensions'](this.currentNodePaneType);
},
mainPanelPosition(): number {
if (typeof this.position === 'number') {
return this.position;
}
supportedResizeDirections() {
const supportedDirections = ['right'];
if (!this.isDraggable) {
return this.fixedPanelWidth + MAIN_PANEL_WIDTH / 2 + SIDE_MARGIN;
}
const relativePosition = this.$store.getters['ui/mainPanelPosition'] as number;
return relativePosition * this.windowWidth;
if(this.isDraggable) supportedDirections.push('left');
return supportedDirections;
},
currentNodePaneType() {
if(!this.hasInputSlot) return 'inputless';
if(!this.isDraggable) return 'dragless';
if(this.nodeType === null) return 'unknown';
return get(this, 'nodeType.parameterPane') || 'regular';
},
hasInputSlot() {
return this.$slots.input !== undefined;
},
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 {
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 {
return this.windowWidth - MAIN_PANEL_WIDTH - this.minimumLeftPosition;
},
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);
if(this.windowWidth < this.minWindowWidth) return this.pxToRelativeWidth(1);
return pos;
return this.pxToRelativeWidth(SIDE_MARGIN + 20) + this.inputPanelMargin;
},
canMoveLeft(): boolean {
return this.mainPanelFinalPositionPx > this.minimumLeftPosition;
return this.mainPanelDimensions.relativeLeft > this.minimumLeftPosition;
},
canMoveRight(): boolean {
return this.mainPanelFinalPositionPx < this.maximumRightPosition;
return this.mainPanelDimensions.relativeRight > this.maximumRightPosition;
},
mainPanelStyles(): { left: string } {
mainPanelStyles(): { left: string, right: string } {
return {
left: `${this.mainPanelFinalPositionPx}px`,
'left': `${this.relativeWidthToPx(this.mainPanelDimensions.relativeLeft)}px`,
'right': `${this.relativeWidthToPx(this.mainPanelDimensions.relativeRight)}px`,
};
},
inputPanelStyles(): { width: string } {
if (!this.isDraggable) {
return {
width: `${this.fixedPanelWidth}px`,
};
inputPanelStyles():{ right: string } {
return {
right: `${this.relativeWidthToPx(this.calculatedPositions.inputPanelRelativeRight)}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;
width = Math.min(
width,
this.windowWidth - SIDE_MARGIN * 2 - this.inputPanelMargin - MAIN_PANEL_WIDTH,
);
width = Math.max(320, width);
return {
width: `${width}px`,
};
return PANEL_WIDTH * multiplier;
},
outputPanelStyles(): { width: string } {
let width = this.windowWidth - this.mainPanelPosition - MAIN_PANEL_WIDTH / 2 - SIDE_MARGIN;
width = Math.min(
width,
this.windowWidth - SIDE_MARGIN * 2 - this.inputPanelMargin - MAIN_PANEL_WIDTH,
);
width = Math.max(MINIMUM_INPUT_PANEL_WIDTH, width);
return {
width: `${width}px`,
};
isBelowMinWidthMainPanel(): boolean {
const minRelativeWidth = this.pxToRelativeWidth(MIN_PANEL_WIDTH);
return this.mainPanelDimensions.relativeWidth < minRelativeWidth;
},
},
watch: {
windowWidth(windowWidth) {
const minRelativeWidth = this.pxToRelativeWidth(MIN_PANEL_WIDTH);
// 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: {
getRelativePosition() {
const current = this.mainPanelFinalPositionPx + MAIN_PANEL_WIDTH / 2 - this.windowWidth / 2;
getInitialLeftPosition(width: number) {
if(this.currentNodePaneType === 'dragless') return this.pxToRelativeWidth(SIDE_MARGIN + 1 + this.fixedPanelWidth);
const pos = Math.floor(
(current / ((this.maximumRightPosition - this.minimumLeftPosition) / 2)) * 100,
return this.hasInputSlot
? 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() {
this.isDragging = true;
this.$emit('dragstart', { position: this.getRelativePosition() });
this.$emit('dragstart', { position: this.mainPanelDimensions.relativeLeft });
},
onDrag(e: {x: number, y: number}) {
const relativePosition = e.x / this.windowWidth;
this.$store.commit('ui/setMainPanelRelativePosition', relativePosition);
const relativeLeft = this.pxToRelativeWidth(e.x) - (this.mainPanelDimensions.relativeWidth / 2);
this.setPositions(relativeLeft);
},
onDragEnd() {
setTimeout(() => {
this.isDragging = false;
this.$emit('dragend', {
windowWidth: this.windowWidth,
position: this.getRelativePosition(),
position: this.mainPanelDimensions.relativeLeft,
});
}, 0);
this.storePositionData();
},
setTotalWidth() {
this.windowWidth = window.innerWidth;
@@ -178,14 +355,13 @@ export default Vue.extend({
</script>
<style lang="scss" module>
$--main-panel-width: 360px;
.dataPanel {
position: absolute;
height: calc(100% - 2 * var(--spacing-l));
position: absolute;
top: var(--spacing-l);
z-index: 0;
min-width: 280px;
}
.inputPanel {
@@ -200,7 +376,6 @@ $--main-panel-width: 360px;
.outputPanel {
composes: dataPanel;
right: var(--spacing-l);
width: $--main-panel-width;
> * {
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 {
position: absolute;
left: 40%;
visibility: hidden;
}
.dragButtonContainer {
position: absolute;
top: -12px;
width: $--main-panel-width;
width: 100%;
height: 12px;
display: flex;
justify-content: center;
pointer-events: none;
.draggable {
pointer-events: all;
}
&:hover .draggable {
visibility: visible;
}