feat(editor): Make focus panel resizable (no-changelog) (#17289)

This commit is contained in:
Daria
2025-07-15 11:20:26 +03:00
committed by GitHub
parent 5bb5a65edf
commit c1b008090f
3 changed files with 189 additions and 138 deletions

View File

@@ -14,21 +14,31 @@ function closestNumber(value: number, divisor: number): number {
return n2; return n2;
} }
function getSize(min: number, virtual: number, gridSize: number): number { function getSize(min: number, virtual: number, gridSize: number, max: number): number {
const target = closestNumber(virtual, gridSize); if (virtual <= 0) {
if (target >= min && virtual > 0) { return min;
return target;
} }
const target = closestNumber(virtual, gridSize);
if (target <= min) {
return min; return min;
} }
if (target >= max) {
return max;
}
return target;
}
interface ResizeProps { interface ResizeProps {
isResizingEnabled?: boolean; isResizingEnabled?: boolean;
height?: number; height?: number;
width?: number; width?: number;
minHeight?: number; minHeight?: number;
maxHeight?: number;
minWidth?: number; minWidth?: number;
maxWidth?: number;
scale?: number; scale?: number;
gridSize?: number; gridSize?: number;
supportedDirections?: Direction[]; supportedDirections?: Direction[];
@@ -41,7 +51,9 @@ const props = withDefaults(defineProps<ResizeProps>(), {
height: 0, height: 0,
width: 0, width: 0,
minHeight: 0, minHeight: 0,
maxHeight: Number.POSITIVE_INFINITY,
minWidth: 0, minWidth: 0,
maxWidth: Number.POSITIVE_INFINITY,
scale: 1, scale: 1,
gridSize: 20, gridSize: 20,
outset: false, outset: false,
@@ -109,8 +121,8 @@ const mouseMove = (event: MouseEvent) => {
state.vHeight.value = state.vHeight.value + deltaHeight; state.vHeight.value = state.vHeight.value + deltaHeight;
state.vWidth.value = state.vWidth.value + deltaWidth; state.vWidth.value = state.vWidth.value + deltaWidth;
const height = getSize(props.minHeight, state.vHeight.value, props.gridSize); const height = getSize(props.minHeight, state.vHeight.value, props.gridSize, props.maxHeight);
const width = getSize(props.minWidth, state.vWidth.value, props.gridSize); const width = getSize(props.minWidth, state.vWidth.value, props.gridSize, props.maxWidth);
const dX = left && width !== props.width ? -1 * (width - props.width) : 0; const dX = left && width !== props.width ? -1 * (width - props.width) : 0;
const dY = top && height !== props.height ? -1 * (height - props.height) : 0; const dY = top && height !== props.height ? -1 * (height - props.height) : 0;

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useFocusPanelStore } from '@/stores/focusPanel.store'; import { useFocusPanelStore } from '@/stores/focusPanel.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { N8nText, N8nInput } from '@n8n/design-system'; import { N8nText, N8nInput, N8nResizeWrapper } from '@n8n/design-system';
import { computed, nextTick, ref } from 'vue'; import { computed, nextTick, ref } from 'vue';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { import {
@@ -26,7 +26,8 @@ import { useEnvironmentsStore } from '@/stores/environments.ee.store';
import { useDebounce } from '@/composables/useDebounce'; import { useDebounce } from '@/composables/useDebounce';
import { htmlEditorEventBus } from '@/event-bus'; import { htmlEditorEventBus } from '@/event-bus';
import { hasFocusOnInput, isFocusableEl } from '@/utils/typesUtils'; import { hasFocusOnInput, isFocusableEl } from '@/utils/typesUtils';
import type { TargetNodeParameterContext } from '@/Interface'; import type { ResizeData, TargetNodeParameterContext } from '@/Interface';
import { useThrottleFn } from '@vueuse/core';
defineOptions({ name: 'FocusPanel' }); defineOptions({ name: 'FocusPanel' });
@@ -58,6 +59,7 @@ const resolvedParameter = computed(() =>
); );
const focusPanelActive = computed(() => focusPanelStore.focusPanelActive); const focusPanelActive = computed(() => focusPanelStore.focusPanelActive);
const focusPanelWidth = computed(() => focusPanelStore.focusPanelWidth);
const isDisabled = computed(() => { const isDisabled = computed(() => {
if (!resolvedParameter.value) return false; if (!resolvedParameter.value) return false;
@@ -244,10 +246,26 @@ function optionSelected(command: string) {
} }
const valueChangedDebounced = debounce(valueChanged, { debounceTime: 0 }); const valueChangedDebounced = debounce(valueChanged, { debounceTime: 0 });
function onResize(event: ResizeData) {
focusPanelStore.updateWidth(event.width);
}
const onResizeThrottle = useThrottleFn(onResize, 10);
</script> </script>
<template> <template>
<div v-if="focusPanelActive" :class="$style.container" @keydown.stop> <div v-if="focusPanelActive" :class="$style.wrapper" @keydown.stop>
<N8nResizeWrapper
:width="focusPanelWidth"
:supported-directions="['left']"
:min-width="300"
:max-width="1000"
:grid-size="8"
:style="{ width: `${focusPanelWidth}px` }"
@resize="onResizeThrottle"
>
<div :class="$style.container">
<div :class="$style.header"> <div :class="$style.header">
<N8nText size="small" :bold="true"> <N8nText size="small" :bold="true">
{{ locale.baseText('nodeView.focusPanel.title') }} {{ locale.baseText('nodeView.focusPanel.title') }}
@@ -381,16 +399,24 @@ const valueChangedDebounced = debounce(valueChanged, { debounceTime: 0 });
</div> </div>
</div> </div>
</div> </div>
</N8nResizeWrapper>
</div>
</template> </template>
<style lang="scss" module> <style lang="scss" module>
.container { .wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: row nowrap;
width: 528px;
border-left: 1px solid var(--color-foreground-base); border-left: 1px solid var(--color-foreground-base);
background: var(--color-foreground-light); background: var(--color-foreground-light);
overflow-y: hidden; overflow-y: hidden;
height: 100%;
}
.container {
display: flex;
flex-direction: column;
height: 100%;
} }
.closeButton:hover { .closeButton:hover {

View File

@@ -13,6 +13,8 @@ import { useWorkflowsStore } from './workflows.store';
import { LOCAL_STORAGE_FOCUS_PANEL, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants'; import { LOCAL_STORAGE_FOCUS_PANEL, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import { useStorage } from '@/composables/useStorage'; import { useStorage } from '@/composables/useStorage';
const DEFAULT_PANEL_WIDTH = 528;
type FocusedNodeParameter = { type FocusedNodeParameter = {
nodeId: string; nodeId: string;
parameter: INodeProperties; parameter: INodeProperties;
@@ -27,6 +29,7 @@ export type RichFocusedNodeParameter = FocusedNodeParameter & {
type FocusPanelData = { type FocusPanelData = {
isActive: boolean; isActive: boolean;
parameters: FocusedNodeParameter[]; parameters: FocusedNodeParameter[];
width?: number;
}; };
type FocusPanelDataByWid = Record<string, FocusPanelData>; type FocusPanelDataByWid = Record<string, FocusPanelData>;
@@ -53,6 +56,7 @@ export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
); );
const focusPanelActive = computed(() => currentFocusPanelData.value.isActive); const focusPanelActive = computed(() => currentFocusPanelData.value.isActive);
const focusPanelWidth = computed(() => currentFocusPanelData.value.width ?? DEFAULT_PANEL_WIDTH);
const _focusedNodeParameters = computed(() => currentFocusPanelData.value.parameters); const _focusedNodeParameters = computed(() => currentFocusPanelData.value.parameters);
// An unenriched parameter indicates a missing nodeId // An unenriched parameter indicates a missing nodeId
@@ -74,11 +78,13 @@ export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
parameters, parameters,
isActive, isActive,
wid = workflowsStore.workflowId, wid = workflowsStore.workflowId,
width = undefined,
removeEmpty = false, removeEmpty = false,
}: { }: {
isActive?: boolean; isActive?: boolean;
parameters?: FocusedNodeParameter[]; parameters?: FocusedNodeParameter[];
wid?: string; wid?: string;
width?: number;
removeEmpty?: boolean; removeEmpty?: boolean;
}) { }) {
const focusPanelDataCurrent = focusPanelData.value; const focusPanelDataCurrent = focusPanelData.value;
@@ -92,6 +98,7 @@ export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
[wid]: { [wid]: {
isActive: isActive ?? focusPanelActive.value, isActive: isActive ?? focusPanelActive.value,
parameters: parameters ?? _focusedNodeParameters.value, parameters: parameters ?? _focusedNodeParameters.value,
width,
}, },
}); });
} }
@@ -127,6 +134,10 @@ export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
_setOptions({ isActive: !focusPanelActive.value }); _setOptions({ isActive: !focusPanelActive.value });
} }
function updateWidth(width: number) {
_setOptions({ width });
}
function isRichParameter( function isRichParameter(
p: RichFocusedNodeParameter | FocusedNodeParameter, p: RichFocusedNodeParameter | FocusedNodeParameter,
): p is RichFocusedNodeParameter { ): p is RichFocusedNodeParameter {
@@ -141,5 +152,7 @@ export const useFocusPanelStore = defineStore(STORES.FOCUS_PANEL, () => {
closeFocusPanel, closeFocusPanel,
toggleFocusPanel, toggleFocusPanel,
onNewWorkflowSave, onNewWorkflowSave,
updateWidth,
focusPanelWidth,
}; };
}); });