n8n/packages/editor-ui/src/components/NDVDraggablePanels.vue
Ricardo Espinoza 38fefff348
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
refactor(editor): Migrate ndv.store to composition API (#11574)
2024-11-08 08:11:36 -05:00

480 lines
13 KiB
Vue

<script lang="ts" setup>
import { useStorage } from '@/composables/useStorage';
import type { INodeTypeDescription } from 'n8n-workflow';
import PanelDragButton from './PanelDragButton.vue';
import { LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH, MAIN_NODE_PANEL_WIDTH } from '@/constants';
import { useNDVStore } from '@/stores/ndv.store';
import { ndvEventBus } from '@/event-bus';
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
import { useDebounce } from '@/composables/useDebounce';
import type { MainPanelType, XYPosition } from '@/Interface';
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
import { useUIStore } from '@/stores/ui.store';
const SIDE_MARGIN = 24;
const SIDE_PANELS_MARGIN = 80;
const MIN_PANEL_WIDTH = 310;
const PANEL_WIDTH = 350;
const PANEL_WIDTH_LARGE = 420;
const MIN_WINDOW_WIDTH = 2 * (SIDE_MARGIN + SIDE_PANELS_MARGIN) + MIN_PANEL_WIDTH;
const initialMainPanelWidth: Record<MainPanelType, 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,
};
interface Props {
isDraggable: boolean;
hideInputAndOutput: boolean;
nodeType: INodeTypeDescription | null;
}
const { callDebounced } = useDebounce();
const ndvStore = useNDVStore();
const uiStore = useUIStore();
const props = defineProps<Props>();
const isDragging = ref<boolean>(false);
const initialized = ref<boolean>(false);
const emit = defineEmits<{
init: [{ position: number }];
dragstart: [{ position: number }];
dragend: [{ position: number; windowWidth: number }];
switchSelectedNode: [string];
close: [];
}>();
const slots = defineSlots<{
input: unknown;
output: unknown;
main: unknown;
}>();
onMounted(() => {
/*
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 (
mainPanelDimensions.value.relativeLeft === 1 &&
mainPanelDimensions.value.relativeRight === 1
) {
setMainPanelWidth();
setPositions(getInitialLeftPosition(mainPanelDimensions.value.relativeWidth));
restorePositionData();
}
emit('init', { position: mainPanelDimensions.value.relativeLeft });
setTimeout(() => {
initialized.value = true;
}, 0);
ndvEventBus.on('setPositionByName', setPositionByName);
});
onBeforeUnmount(() => {
ndvEventBus.off('setPositionByName', setPositionByName);
});
const containerWidth = computed(() => uiStore.appGridWidth);
watch(containerWidth, (width) => {
const minRelativeWidth = pxToRelativeWidth(MIN_PANEL_WIDTH);
const isBelowMinWidthMainPanel = mainPanelDimensions.value.relativeWidth < minRelativeWidth;
// Prevent the panel resizing below MIN_PANEL_WIDTH whhile maintaing position
if (isBelowMinWidthMainPanel) {
setMainPanelWidth(minRelativeWidth);
}
const isBelowMinLeft = minimumLeftPosition.value > mainPanelDimensions.value.relativeLeft;
const isMaxRight = maximumRightPosition.value > mainPanelDimensions.value.relativeRight;
// When user is resizing from non-supported view(sub ~488px) we need to refit the panels
if (width > MIN_WINDOW_WIDTH && isBelowMinLeft && isMaxRight) {
setMainPanelWidth(minRelativeWidth);
setPositions(getInitialLeftPosition(mainPanelDimensions.value.relativeWidth));
}
setPositions(mainPanelDimensions.value.relativeLeft);
});
const currentNodePaneType = computed((): MainPanelType => {
if (!hasInputSlot.value) return 'inputless';
if (!props.isDraggable) return 'dragless';
if (props.nodeType === null) return 'unknown';
return props.nodeType.parameterPane ?? 'regular';
});
const mainPanelDimensions = computed(() => {
return ndvStore.mainPanelDimensions[currentNodePaneType.value];
});
const calculatedPositions = computed(
(): { inputPanelRelativeRight: number; outputPanelRelativeLeft: number } => {
const hasInput = slots.input !== undefined;
const outputPanelRelativeLeft =
mainPanelDimensions.value.relativeLeft + mainPanelDimensions.value.relativeWidth;
const inputPanelRelativeRight = hasInput
? 1 - outputPanelRelativeLeft + mainPanelDimensions.value.relativeWidth
: 1 - pxToRelativeWidth(SIDE_MARGIN);
return {
inputPanelRelativeRight,
outputPanelRelativeLeft,
};
},
);
const outputPanelRelativeTranslate = computed((): number => {
const panelMinLeft = 1 - pxToRelativeWidth(MIN_PANEL_WIDTH + SIDE_MARGIN);
const currentRelativeLeftDelta = calculatedPositions.value.outputPanelRelativeLeft - panelMinLeft;
return currentRelativeLeftDelta > 0 ? currentRelativeLeftDelta : 0;
});
const supportedResizeDirections = computed((): string[] => {
const supportedDirections = ['right'];
if (props.isDraggable) supportedDirections.push('left');
return supportedDirections;
});
const hasInputSlot = computed((): boolean => {
return slots.input !== undefined;
});
const inputPanelMargin = computed(() => pxToRelativeWidth(SIDE_PANELS_MARGIN));
const minimumLeftPosition = computed((): number => {
if (containerWidth.value < MIN_WINDOW_WIDTH) return pxToRelativeWidth(1);
if (!hasInputSlot.value) return pxToRelativeWidth(SIDE_MARGIN);
return pxToRelativeWidth(SIDE_MARGIN + 20) + inputPanelMargin.value;
});
const maximumRightPosition = computed((): number => {
if (containerWidth.value < MIN_WINDOW_WIDTH) return pxToRelativeWidth(1);
return pxToRelativeWidth(SIDE_MARGIN + 20) + inputPanelMargin.value;
});
const canMoveLeft = computed((): boolean => {
return mainPanelDimensions.value.relativeLeft > minimumLeftPosition.value;
});
const canMoveRight = computed((): boolean => {
return mainPanelDimensions.value.relativeRight > maximumRightPosition.value;
});
const mainPanelStyles = computed((): { left: string; right: string } => {
return {
left: `${relativeWidthToPx(mainPanelDimensions.value.relativeLeft)}px`,
right: `${relativeWidthToPx(mainPanelDimensions.value.relativeRight)}px`,
};
});
const inputPanelStyles = computed((): { right: string } => {
return {
right: `${relativeWidthToPx(calculatedPositions.value.inputPanelRelativeRight)}px`,
};
});
const outputPanelStyles = computed((): { left: string; transform: string } => {
return {
left: `${relativeWidthToPx(calculatedPositions.value.outputPanelRelativeLeft)}px`,
transform: `translateX(-${relativeWidthToPx(outputPanelRelativeTranslate.value)}px)`,
};
});
const hasDoubleWidth = computed((): boolean => {
return props.nodeType?.parameterPane === 'wide';
});
const fixedPanelWidth = computed((): number => {
const multiplier = hasDoubleWidth.value ? 2 : 1;
if (containerWidth.value > 1700) {
return PANEL_WIDTH_LARGE * multiplier;
}
return PANEL_WIDTH * multiplier;
});
const onSwitchSelectedNode = (node: string) => emit('switchSelectedNode', node);
function getInitialLeftPosition(width: number): number {
if (currentNodePaneType.value === 'dragless')
return pxToRelativeWidth(SIDE_MARGIN + 1 + fixedPanelWidth.value);
return hasInputSlot.value ? 0.5 - width / 2 : minimumLeftPosition.value;
}
function setMainPanelWidth(relativeWidth?: number): void {
const mainPanelRelativeWidth =
relativeWidth || pxToRelativeWidth(initialMainPanelWidth[currentNodePaneType.value]);
ndvStore.setMainPanelDimensions({
panelType: currentNodePaneType.value,
dimensions: {
relativeWidth: mainPanelRelativeWidth,
},
});
}
function setPositions(relativeLeft: number): void {
const mainPanelRelativeLeft =
relativeLeft || 1 - calculatedPositions.value.inputPanelRelativeRight;
const mainPanelRelativeRight =
1 - mainPanelRelativeLeft - mainPanelDimensions.value.relativeWidth;
const isMaxRight = maximumRightPosition.value > mainPanelRelativeRight;
const isMinLeft = minimumLeftPosition.value > mainPanelRelativeLeft;
const isInputless = currentNodePaneType.value === 'inputless';
if (isMinLeft) {
ndvStore.setMainPanelDimensions({
panelType: currentNodePaneType.value,
dimensions: {
relativeLeft: minimumLeftPosition.value,
relativeRight: 1 - mainPanelDimensions.value.relativeWidth - minimumLeftPosition.value,
},
});
return;
}
if (isMaxRight) {
ndvStore.setMainPanelDimensions({
panelType: currentNodePaneType.value,
dimensions: {
relativeLeft: 1 - mainPanelDimensions.value.relativeWidth - maximumRightPosition.value,
relativeRight: maximumRightPosition.value,
},
});
return;
}
ndvStore.setMainPanelDimensions({
panelType: currentNodePaneType.value,
dimensions: {
relativeLeft: isInputless ? minimumLeftPosition.value : mainPanelRelativeLeft,
relativeRight: mainPanelRelativeRight,
},
});
}
function setPositionByName(position: 'minLeft' | 'maxRight' | 'initial') {
const positionByName: Record<string, number> = {
minLeft: minimumLeftPosition.value,
maxRight: maximumRightPosition.value,
initial: getInitialLeftPosition(mainPanelDimensions.value.relativeWidth),
};
setPositions(positionByName[position]);
}
function pxToRelativeWidth(px: number): number {
return px / containerWidth.value;
}
function relativeWidthToPx(relativeWidth: number) {
return relativeWidth * containerWidth.value;
}
function onResizeEnd() {
storePositionData();
}
function onResizeDebounced(data: { direction: string; x: number; width: number }) {
if (initialized.value) {
void callDebounced(onResize, { debounceTime: 10, trailing: true }, data);
}
}
function onResize({ direction, x, width }: { direction: string; x: number; width: number }) {
const relativeDistance = pxToRelativeWidth(x);
const relativeWidth = pxToRelativeWidth(width);
if (direction === 'left' && relativeDistance <= minimumLeftPosition.value) return;
if (direction === 'right' && 1 - relativeDistance <= maximumRightPosition.value) return;
if (width <= MIN_PANEL_WIDTH) return;
setMainPanelWidth(relativeWidth);
setPositions(direction === 'left' ? relativeDistance : mainPanelDimensions.value.relativeLeft);
}
function restorePositionData() {
const storedPanelWidthData = useStorage(
`${LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH}_${currentNodePaneType.value}`,
).value;
if (storedPanelWidthData) {
const parsedWidth = parseFloat(storedPanelWidthData);
setMainPanelWidth(parsedWidth);
const initialPosition = getInitialLeftPosition(parsedWidth);
setPositions(initialPosition);
return true;
}
return false;
}
function storePositionData() {
useStorage(`${LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH}_${currentNodePaneType.value}`).value =
mainPanelDimensions.value.relativeWidth.toString();
}
function onDragStart() {
isDragging.value = true;
emit('dragstart', { position: mainPanelDimensions.value.relativeLeft });
}
function onDrag(position: XYPosition) {
const relativeLeft = pxToRelativeWidth(position[0]) - mainPanelDimensions.value.relativeWidth / 2;
setPositions(relativeLeft);
}
function onDragEnd() {
setTimeout(() => {
isDragging.value = false;
emit('dragend', {
windowWidth: containerWidth.value,
position: mainPanelDimensions.value.relativeLeft,
});
}, 0);
storePositionData();
}
</script>
<template>
<div>
<NDVFloatingNodes
v-if="ndvStore.activeNode"
:root-node="ndvStore.activeNode"
@switch-selected-node="onSwitchSelectedNode"
/>
<div v-if="!hideInputAndOutput" :class="$style.inputPanel" :style="inputPanelStyles">
<slot name="input"></slot>
</div>
<div v-if="!hideInputAndOutput" :class="$style.outputPanel" :style="outputPanelStyles">
<slot name="output"></slot>
</div>
<div :class="$style.mainPanel" :style="mainPanelStyles">
<n8n-resize-wrapper
:is-resizing-enabled="currentNodePaneType !== 'unknown'"
:width="relativeWidthToPx(mainPanelDimensions.relativeWidth)"
:min-width="MIN_PANEL_WIDTH"
:grid-size="20"
:supported-directions="supportedResizeDirections"
@resize="onResizeDebounced"
@resizeend="onResizeEnd"
>
<div :class="$style.dragButtonContainer">
<PanelDragButton
v-if="!hideInputAndOutput && isDraggable"
:class="{ [$style.draggable]: true, [$style.visible]: isDragging }"
:can-move-left="canMoveLeft"
:can-move-right="canMoveRight"
@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>
<style lang="scss" module>
.dataPanel {
position: absolute;
height: calc(100% - 2 * var(--spacing-l));
position: absolute;
top: var(--spacing-l);
z-index: 0;
min-width: 280px;
}
.inputPanel {
composes: dataPanel;
left: var(--spacing-l);
> * {
border-radius: var(--border-radius-large) 0 0 var(--border-radius-large);
}
}
.outputPanel {
composes: dataPanel;
right: var(--spacing-l);
> * {
border-radius: 0 var(--border-radius-large) var(--border-radius-large) 0;
}
}
.mainPanel {
position: absolute;
height: 100%;
&:hover {
.draggable {
visibility: visible;
}
}
}
.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 {
visibility: hidden;
}
.double-width {
left: 90%;
}
.dragButtonContainer {
position: absolute;
top: -12px;
width: 100%;
height: 12px;
display: flex;
justify-content: center;
pointer-events: none;
.draggable {
pointer-events: all;
}
&:hover .draggable {
visibility: visible;
}
}
.visible {
visibility: visible;
}
</style>