mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
chore(editor): The zoomed view misc updates (#17865)
This commit is contained in:
@@ -1161,6 +1161,8 @@
|
|||||||
"ndv.input.noOutputData.hint.tooltip": "From the earliest node which is unexecuted, or is executed but has since been changed",
|
"ndv.input.noOutputData.hint.tooltip": "From the earliest node which is unexecuted, or is executed but has since been changed",
|
||||||
"ndv.input.noOutputData.schemaPreviewHint": "switch to {schema} to use the schema preview",
|
"ndv.input.noOutputData.schemaPreviewHint": "switch to {schema} to use the schema preview",
|
||||||
"ndv.input.noOutputData.or": "or",
|
"ndv.input.noOutputData.or": "or",
|
||||||
|
"ndv.input.noOutputData.embeddedNdv.link": "Execute previous nodes",
|
||||||
|
"ndv.input.noOutputData.embeddedNdv.description": "{link} to use their data here",
|
||||||
"ndv.input.executingPrevious": "Executing previous nodes...",
|
"ndv.input.executingPrevious": "Executing previous nodes...",
|
||||||
"ndv.input.notConnected.title": "Wire me up",
|
"ndv.input.notConnected.title": "Wire me up",
|
||||||
"ndv.input.notConnected.v2.title": "No input connected",
|
"ndv.input.notConnected.v2.title": "No input connected",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue';
|
import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue';
|
||||||
import { onClickOutside } from '@vueuse/core';
|
import { onClickOutside } from '@vueuse/core';
|
||||||
|
|
||||||
import ExpressionFunctionIcon from '@/components/ExpressionFunctionIcon.vue';
|
import ExpressionFunctionIcon from '@/components/ExpressionFunctionIcon.vue';
|
||||||
@@ -16,6 +16,7 @@ import { startCompletion } from '@codemirror/autocomplete';
|
|||||||
import type { EditorState, SelectionRange } from '@codemirror/state';
|
import type { EditorState, SelectionRange } from '@codemirror/state';
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
import type { IDataObject } from 'n8n-workflow';
|
||||||
import { createEventBus, type EventBus } from '@n8n/utils/event-bus';
|
import { createEventBus, type EventBus } from '@n8n/utils/event-bus';
|
||||||
|
import { CanvasKey, ExpressionLocalResolveContextSymbol } from '@/constants';
|
||||||
|
|
||||||
const isFocused = ref(false);
|
const isFocused = ref(false);
|
||||||
const segments = ref<Segment[]>([]);
|
const segments = ref<Segment[]>([]);
|
||||||
@@ -23,6 +24,7 @@ const editorState = ref<EditorState>();
|
|||||||
const selection = ref<SelectionRange>();
|
const selection = ref<SelectionRange>();
|
||||||
const inlineInput = ref<InstanceType<typeof InlineExpressionEditorInput>>();
|
const inlineInput = ref<InstanceType<typeof InlineExpressionEditorInput>>();
|
||||||
const container = ref<HTMLDivElement>();
|
const container = ref<HTMLDivElement>();
|
||||||
|
const outputPopover = ref<InstanceType<typeof InlineExpressionEditorOutput>>();
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -46,14 +48,21 @@ const emit = defineEmits<{
|
|||||||
'modal-opener-click': [];
|
'modal-opener-click': [];
|
||||||
'update:model-value': [value: string];
|
'update:model-value': [value: string];
|
||||||
focus: [];
|
focus: [];
|
||||||
blur: [];
|
blur: [FocusEvent | KeyboardEvent | undefined];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
const canvas = inject(CanvasKey, undefined);
|
||||||
|
const expressionLocalResolveCtx = inject(ExpressionLocalResolveContextSymbol, undefined);
|
||||||
|
const isInExperimentalNdv = computed(() => expressionLocalResolveCtx?.value !== undefined);
|
||||||
|
|
||||||
const isDragging = computed(() => ndvStore.isDraggableDragging);
|
const isDragging = computed(() => ndvStore.isDraggableDragging);
|
||||||
|
const isOutputPopoverVisible = computed(
|
||||||
|
() => isFocused.value && (!isInExperimentalNdv.value || !canvas?.isPaneMoving.value),
|
||||||
|
);
|
||||||
|
|
||||||
function select() {
|
function select() {
|
||||||
if (inlineInput.value) {
|
if (inlineInput.value) {
|
||||||
@@ -80,12 +89,16 @@ function onBlur(event?: FocusEvent | KeyboardEvent) {
|
|||||||
return; // prevent blur on resizing
|
return; // prevent blur on resizing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event?.target instanceof Element && outputPopover.value?.contentRef?.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const wasFocused = isFocused.value;
|
const wasFocused = isFocused.value;
|
||||||
|
|
||||||
isFocused.value = false;
|
isFocused.value = false;
|
||||||
|
|
||||||
if (wasFocused) {
|
if (wasFocused) {
|
||||||
emit('blur');
|
emit('blur', event);
|
||||||
|
|
||||||
const telemetryPayload = createExpressionTelemetryPayload(
|
const telemetryPayload = createExpressionTelemetryPayload(
|
||||||
segments.value,
|
segments.value,
|
||||||
@@ -161,7 +174,8 @@ onBeforeUnmount(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(isDragging, (newIsDragging) => {
|
watch(isDragging, (newIsDragging) => {
|
||||||
if (newIsDragging) {
|
// The input must stay focused in experimental NDV so that the input panel popover is open while dragging
|
||||||
|
if (newIsDragging && !isInExperimentalNdv.value) {
|
||||||
onBlur();
|
onBlur();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -212,13 +226,17 @@ defineExpose({ focus, select });
|
|||||||
@click="emit('modal-opener-click')"
|
@click="emit('modal-opener-click')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<InlineExpressionEditorOutput
|
<InlineExpressionEditorOutput
|
||||||
|
ref="outputPopover"
|
||||||
|
:visible="isOutputPopoverVisible"
|
||||||
:unresolved-expression="modelValue"
|
:unresolved-expression="modelValue"
|
||||||
:selection="selection"
|
:selection="selection"
|
||||||
:editor-state="editorState"
|
:editor-state="editorState"
|
||||||
:segments="segments"
|
:segments="segments"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:visible="isFocused"
|
:virtual-ref="container"
|
||||||
|
:append-to="isInExperimentalNdv ? '#canvas' : undefined"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ const trackWorkflowInputFieldAdded = () => {
|
|||||||
/>
|
/>
|
||||||
<div v-if="multipleValues">
|
<div v-if="multipleValues">
|
||||||
<Draggable
|
<Draggable
|
||||||
|
:item-key="property.name"
|
||||||
v-model="mutableValues[property.name]"
|
v-model="mutableValues[property.name]"
|
||||||
handle=".drag-handle"
|
handle=".drag-handle"
|
||||||
drag-class="dragging"
|
drag-class="dragging"
|
||||||
|
|||||||
@@ -4,23 +4,26 @@ import type { EditorState, SelectionRange } from '@codemirror/state';
|
|||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import type { Segment } from '@/types/expressions';
|
import type { Segment } from '@/types/expressions';
|
||||||
import { onBeforeUnmount } from 'vue';
|
import { computed, onBeforeUnmount, useTemplateRef } from 'vue';
|
||||||
import ExpressionOutput from './ExpressionOutput.vue';
|
import ExpressionOutput from './ExpressionOutput.vue';
|
||||||
import OutputItemSelect from './OutputItemSelect.vue';
|
import OutputItemSelect from './OutputItemSelect.vue';
|
||||||
import InlineExpressionTip from './InlineExpressionTip.vue';
|
import InlineExpressionTip from './InlineExpressionTip.vue';
|
||||||
import { outputTheme } from './theme';
|
import { outputTheme } from './theme';
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
import { N8nPopover } from '@n8n/design-system';
|
||||||
|
|
||||||
interface InlineExpressionEditorOutputProps {
|
interface InlineExpressionEditorOutputProps {
|
||||||
segments: Segment[];
|
segments: Segment[];
|
||||||
unresolvedExpression?: string;
|
unresolvedExpression?: string;
|
||||||
editorState?: EditorState;
|
editorState?: EditorState;
|
||||||
selection?: SelectionRange;
|
selection?: SelectionRange;
|
||||||
visible?: boolean;
|
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
|
visible: boolean;
|
||||||
|
virtualRef?: HTMLElement;
|
||||||
|
appendTo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<InlineExpressionEditorOutputProps>(), {
|
const props = withDefaults(defineProps<InlineExpressionEditorOutputProps>(), {
|
||||||
visible: false,
|
|
||||||
editorState: undefined,
|
editorState: undefined,
|
||||||
selection: undefined,
|
selection: undefined,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
@@ -30,45 +33,81 @@ withDefaults(defineProps<InlineExpressionEditorOutputProps>(), {
|
|||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const theme = outputTheme();
|
const theme = outputTheme();
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
|
const contentRef = useTemplateRef('content');
|
||||||
|
const virtualRefSize = useElementSize(computed(() => props.virtualRef));
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
ndvStore.expressionOutputItemIndex = 0;
|
ndvStore.expressionOutputItemIndex = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
contentRef,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="visible" :class="$style.dropdown" title="">
|
<N8nPopover
|
||||||
<div :class="$style.header">
|
:visible="visible"
|
||||||
<n8n-text bold size="small" compact>
|
placement="bottom"
|
||||||
{{ i18n.baseText('parameterInput.result') }}
|
:show-arrow="false"
|
||||||
</n8n-text>
|
:offset="0"
|
||||||
|
:persistent="false"
|
||||||
|
:virtual-triggering="virtualRef !== undefined"
|
||||||
|
:virtual-ref="virtualRef"
|
||||||
|
:width="virtualRefSize.width.value"
|
||||||
|
:popper-class="$style.popper"
|
||||||
|
:popper-options="{
|
||||||
|
modifiers: [
|
||||||
|
{ name: 'flip', enabled: false },
|
||||||
|
{
|
||||||
|
// Ensures that the popover is re-positioned when the reference element is resized
|
||||||
|
name: 'custom modifier',
|
||||||
|
options: {
|
||||||
|
width: virtualRefSize.width.value,
|
||||||
|
height: virtualRefSize.height.value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}"
|
||||||
|
:append-to="appendTo"
|
||||||
|
>
|
||||||
|
<div ref="content" :class="$style.dropdown">
|
||||||
|
<div :class="$style.header">
|
||||||
|
<n8n-text bold size="small" compact>
|
||||||
|
{{ i18n.baseText('parameterInput.result') }}
|
||||||
|
</n8n-text>
|
||||||
|
|
||||||
<OutputItemSelect />
|
<OutputItemSelect />
|
||||||
|
</div>
|
||||||
|
<n8n-text :class="$style.body">
|
||||||
|
<ExpressionOutput
|
||||||
|
data-test-id="inline-expression-editor-output"
|
||||||
|
:segments="segments"
|
||||||
|
:extensions="theme"
|
||||||
|
>
|
||||||
|
</ExpressionOutput>
|
||||||
|
</n8n-text>
|
||||||
|
<div v-if="!isReadOnly" :class="$style.footer">
|
||||||
|
<InlineExpressionTip
|
||||||
|
:editor-state="editorState"
|
||||||
|
:selection="selection"
|
||||||
|
:unresolved-expression="unresolvedExpression"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<n8n-text :class="$style.body">
|
</N8nPopover>
|
||||||
<ExpressionOutput
|
|
||||||
data-test-id="inline-expression-editor-output"
|
|
||||||
:segments="segments"
|
|
||||||
:extensions="theme"
|
|
||||||
>
|
|
||||||
</ExpressionOutput>
|
|
||||||
</n8n-text>
|
|
||||||
<div v-if="!isReadOnly" :class="$style.footer">
|
|
||||||
<InlineExpressionTip
|
|
||||||
:editor-state="editorState"
|
|
||||||
:selection="selection"
|
|
||||||
:unresolved-expression="unresolvedExpression"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
.popper {
|
||||||
|
background-color: transparent !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: absolute;
|
|
||||||
z-index: 2; // cover tooltips
|
|
||||||
background: var(--color-code-background);
|
background: var(--color-code-background);
|
||||||
border: var(--border-base);
|
border: var(--border-base);
|
||||||
border-top: none;
|
border-top: none;
|
||||||
|
|||||||
@@ -163,9 +163,10 @@ watchDebounced(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tipText {
|
.tipText {
|
||||||
display: inline;
|
|
||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
|
white-space: nowrap;
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag .tipText {
|
.drag .tipText {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type Props = {
|
|||||||
disableDisplayModeSelection?: boolean;
|
disableDisplayModeSelection?: boolean;
|
||||||
focusedMappableInput: string;
|
focusedMappableInput: string;
|
||||||
isMappingOnboarded: boolean;
|
isMappingOnboarded: boolean;
|
||||||
|
nodeNotRunMessageVariant?: 'default' | 'simple';
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -57,6 +58,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
readOnly: false,
|
readOnly: false,
|
||||||
isProductionExecutionPreview: false,
|
isProductionExecutionPreview: false,
|
||||||
isPaneActive: false,
|
isPaneActive: false,
|
||||||
|
nodeNotRunMessageVariant: 'default',
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -249,6 +251,10 @@ const isNDVV2 = computed(() =>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const nodeNameToExecute = computed(
|
||||||
|
() => (isActiveNodeConfig.value ? rootNode.value : activeNode.value?.name) ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
inputMode,
|
inputMode,
|
||||||
(mode) => {
|
(mode) => {
|
||||||
@@ -455,7 +461,23 @@ function handleChangeCollapsingColumn(columnName: string | null) {
|
|||||||
v-if="(isActiveNodeConfig && rootNode) || parentNodes.length"
|
v-if="(isActiveNodeConfig && rootNode) || parentNodes.length"
|
||||||
:class="$style.noOutputData"
|
:class="$style.noOutputData"
|
||||||
>
|
>
|
||||||
<template v-if="isNDVV2">
|
<N8nText v-if="nodeNotRunMessageVariant === 'simple'" color="text-base" size="small">
|
||||||
|
<I18nT scope="global" keypath="ndv.input.noOutputData.embeddedNdv.description">
|
||||||
|
<template #link>
|
||||||
|
<NodeExecuteButton
|
||||||
|
:class="$style.executeButton"
|
||||||
|
size="medium"
|
||||||
|
:node-name="nodeNameToExecute"
|
||||||
|
:label="i18n.baseText('ndv.input.noOutputData.embeddedNdv.link')"
|
||||||
|
text
|
||||||
|
telemetry-source="inputs"
|
||||||
|
hide-icon
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</I18nT>
|
||||||
|
</N8nText>
|
||||||
|
|
||||||
|
<template v-else-if="isNDVV2">
|
||||||
<NDVEmptyState
|
<NDVEmptyState
|
||||||
v-if="isMappingEnabled || hasRootNodeRun"
|
v-if="isMappingEnabled || hasRootNodeRun"
|
||||||
:title="i18n.baseText('ndv.input.noOutputData.v2.title')"
|
:title="i18n.baseText('ndv.input.noOutputData.v2.title')"
|
||||||
@@ -470,7 +492,7 @@ function handleChangeCollapsingColumn(columnName: string | null) {
|
|||||||
hide-icon
|
hide-icon
|
||||||
transparent
|
transparent
|
||||||
type="secondary"
|
type="secondary"
|
||||||
:node-name="(isActiveNodeConfig ? rootNode : activeNode?.name) ?? ''"
|
:node-name="nodeNameToExecute"
|
||||||
:label="i18n.baseText('ndv.input.noOutputData.v2.action')"
|
:label="i18n.baseText('ndv.input.noOutputData.v2.action')"
|
||||||
:tooltip="i18n.baseText('ndv.input.noOutputData.v2.tooltip')"
|
:tooltip="i18n.baseText('ndv.input.noOutputData.v2.tooltip')"
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
@@ -537,7 +559,7 @@ function handleChangeCollapsingColumn(columnName: string | null) {
|
|||||||
type="secondary"
|
type="secondary"
|
||||||
hide-icon
|
hide-icon
|
||||||
:transparent="true"
|
:transparent="true"
|
||||||
:node-name="(isActiveNodeConfig ? rootNode : activeNode?.name) ?? ''"
|
:node-name="nodeNameToExecute"
|
||||||
:label="i18n.baseText('ndv.input.noOutputData.executePrevious')"
|
:label="i18n.baseText('ndv.input.noOutputData.executePrevious')"
|
||||||
class="mt-m"
|
class="mt-m"
|
||||||
telemetry-source="inputs"
|
telemetry-source="inputs"
|
||||||
@@ -707,4 +729,8 @@ function handleChangeCollapsingColumn(columnName: string | null) {
|
|||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.executeButton {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ const props = withDefaults(
|
|||||||
activeNode?: INodeUi;
|
activeNode?: INodeUi;
|
||||||
isEmbeddedInCanvas?: boolean;
|
isEmbeddedInCanvas?: boolean;
|
||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
|
extraTabsClassName?: string;
|
||||||
|
extraParameterWrapperClassName?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
inputSize: 0,
|
inputSize: 0,
|
||||||
@@ -94,6 +96,7 @@ const emit = defineEmits<{
|
|||||||
activate: [];
|
activate: [];
|
||||||
execute: [];
|
execute: [];
|
||||||
captureWheelBody: [WheelEvent];
|
captureWheelBody: [WheelEvent];
|
||||||
|
dblclickHeader: [MouseEvent];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const slots = defineSlots<{ actions?: {} }>();
|
const slots = defineSlots<{ actions?: {} }>();
|
||||||
@@ -596,11 +599,13 @@ function handleSelectAction(params: INodeParameters) {
|
|||||||
:node-type="nodeType"
|
:node-type="nodeType"
|
||||||
:push-ref="pushRef"
|
:push-ref="pushRef"
|
||||||
:sub-title="subTitle"
|
:sub-title="subTitle"
|
||||||
|
:extra-tabs-class-name="extraTabsClassName"
|
||||||
:include-action="parametersByTab.action.length > 0"
|
:include-action="parametersByTab.action.length > 0"
|
||||||
:include-credential="isDisplayingCredentials"
|
:include-credential="isDisplayingCredentials"
|
||||||
:has-credential-issue="!areAllCredentialsSet"
|
:has-credential-issue="!areAllCredentialsSet"
|
||||||
@name-changed="nameChanged"
|
@name-changed="nameChanged"
|
||||||
@tab-changed="onTabSelect"
|
@tab-changed="onTabSelect"
|
||||||
|
@dblclick-title="emit('dblclickHeader', $event)"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
@@ -662,6 +667,7 @@ function handleSelectAction(params: INodeParameters) {
|
|||||||
'node-parameters-wrapper',
|
'node-parameters-wrapper',
|
||||||
shouldShowStaticScrollbar ? 'with-static-scrollbar' : '',
|
shouldShowStaticScrollbar ? 'with-static-scrollbar' : '',
|
||||||
{ 'ndv-v2': isNDVV2 },
|
{ 'ndv-v2': isNDVV2 },
|
||||||
|
extraParameterWrapperClassName ?? '',
|
||||||
]"
|
]"
|
||||||
data-test-id="node-parameters"
|
data-test-id="node-parameters"
|
||||||
@wheel.capture="emit('captureWheelBody', $event)"
|
@wheel.capture="emit('captureWheelBody', $event)"
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ function getNdvStateMock(): Partial<ReturnType<typeof useNDVStore>> {
|
|||||||
},
|
},
|
||||||
isInputPanelEmpty: false,
|
isInputPanelEmpty: false,
|
||||||
isOutputPanelEmpty: false,
|
isOutputPanelEmpty: false,
|
||||||
|
ndvInputDataWithPinnedData: [],
|
||||||
|
getHoveringItem: undefined,
|
||||||
|
expressionOutputItemIndex: 0,
|
||||||
|
isTableHoverOnboarded: false,
|
||||||
|
setHighlightDraggables: vi.fn(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +105,11 @@ describe('ParameterInput.vue', () => {
|
|||||||
},
|
},
|
||||||
isInputPanelEmpty: false,
|
isInputPanelEmpty: false,
|
||||||
isOutputPanelEmpty: false,
|
isOutputPanelEmpty: false,
|
||||||
|
ndvInputDataWithPinnedData: [],
|
||||||
|
getHoveringItem: undefined,
|
||||||
|
expressionOutputItemIndex: 0,
|
||||||
|
isTableHoverOnboarded: false,
|
||||||
|
setHighlightDraggables: vi.fn(),
|
||||||
};
|
};
|
||||||
mockNodeTypesState = {
|
mockNodeTypesState = {
|
||||||
allNodeTypes: [],
|
allNodeTypes: [],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, onUpdated, ref, watch } from 'vue';
|
import { computed, inject, nextTick, onBeforeUnmount, onMounted, onUpdated, ref, watch } from 'vue';
|
||||||
|
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
|
||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
APP_MODALS_ELEMENT_ID,
|
APP_MODALS_ELEMENT_ID,
|
||||||
CORE_NODES_CATEGORY,
|
CORE_NODES_CATEGORY,
|
||||||
CUSTOM_API_CALL_KEY,
|
CUSTOM_API_CALL_KEY,
|
||||||
|
ExpressionLocalResolveContextSymbol,
|
||||||
HTML_NODE_TYPE,
|
HTML_NODE_TYPE,
|
||||||
NODES_USING_CODE_NODE_EDITOR,
|
NODES_USING_CODE_NODE_EDITOR,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
@@ -72,13 +73,14 @@ import { useUIStore } from '@/stores/ui.store';
|
|||||||
import { N8nIcon, N8nInput, N8nInputNumber, N8nOption, N8nSelect } from '@n8n/design-system';
|
import { N8nIcon, N8nInput, N8nInputNumber, N8nOption, N8nSelect } from '@n8n/design-system';
|
||||||
import type { EventBus } from '@n8n/utils/event-bus';
|
import type { EventBus } from '@n8n/utils/event-bus';
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
import { useElementSize } from '@vueuse/core';
|
import { onClickOutside, useElementSize } from '@vueuse/core';
|
||||||
import { captureMessage } from '@sentry/vue';
|
import { captureMessage } from '@sentry/vue';
|
||||||
import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
|
import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
|
||||||
import { hasFocusOnInput, isBlurrableEl, isFocusableEl, isSelectableEl } from '@/utils/typesUtils';
|
import { hasFocusOnInput, isBlurrableEl, isFocusableEl, isSelectableEl } from '@/utils/typesUtils';
|
||||||
import { completeExpressionSyntax, shouldConvertToExpression } from '@/utils/expressions';
|
import { completeExpressionSyntax, shouldConvertToExpression } from '@/utils/expressions';
|
||||||
import CssEditor from './CssEditor/CssEditor.vue';
|
import CssEditor from './CssEditor/CssEditor.vue';
|
||||||
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
||||||
|
import ExperimentalEmbeddedNdvMapper from '@/components/canvas/experimental/components/ExperimentalEmbeddedNdvMapper.vue';
|
||||||
|
|
||||||
type Picker = { $emit: (arg0: string, arg1: Date) => void };
|
type Picker = { $emit: (arg0: string, arg1: Date) => void };
|
||||||
|
|
||||||
@@ -144,10 +146,13 @@ const nodeTypesStore = useNodeTypesStore();
|
|||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const focusPanelStore = useFocusPanelStore();
|
const focusPanelStore = useFocusPanelStore();
|
||||||
|
|
||||||
|
const expressionLocalResolveCtx = inject(ExpressionLocalResolveContextSymbol, undefined);
|
||||||
|
|
||||||
// ESLint: false positive
|
// ESLint: false positive
|
||||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||||
const inputField = ref<InstanceType<typeof N8nInput | typeof N8nSelect> | HTMLElement>();
|
const inputField = ref<InstanceType<typeof N8nInput | typeof N8nSelect> | HTMLElement>();
|
||||||
const wrapper = ref<HTMLDivElement>();
|
const wrapper = ref<HTMLDivElement>();
|
||||||
|
const mapperRef = ref<InstanceType<typeof ExperimentalEmbeddedNdvMapper>>();
|
||||||
|
|
||||||
const nodeName = ref('');
|
const nodeName = ref('');
|
||||||
const codeEditDialogVisible = ref(false);
|
const codeEditDialogVisible = ref(false);
|
||||||
@@ -190,7 +195,10 @@ const dateTimePickerOptions = ref({
|
|||||||
});
|
});
|
||||||
const isFocused = ref(false);
|
const isFocused = ref(false);
|
||||||
|
|
||||||
const node = computed(() => ndvStore.activeNode ?? undefined);
|
const contextNode = expressionLocalResolveCtx?.value?.workflow.getNode(
|
||||||
|
expressionLocalResolveCtx.value.nodeName,
|
||||||
|
);
|
||||||
|
const node = computed(() => contextNode ?? ndvStore.activeNode ?? undefined);
|
||||||
const nodeType = computed(
|
const nodeType = computed(
|
||||||
() => node.value && nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion),
|
() => node.value && nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion),
|
||||||
);
|
);
|
||||||
@@ -592,6 +600,12 @@ const showDragnDropTip = computed(
|
|||||||
|
|
||||||
const shouldCaptureForPosthog = computed(() => node.value?.type === AI_TRANSFORM_NODE_TYPE);
|
const shouldCaptureForPosthog = computed(() => node.value?.type === AI_TRANSFORM_NODE_TYPE);
|
||||||
|
|
||||||
|
const shouldShowMapper = computed(
|
||||||
|
() =>
|
||||||
|
isFocused.value &&
|
||||||
|
(isModelValueExpression.value || props.forceShowExpression || props.modelValue === ''),
|
||||||
|
);
|
||||||
|
|
||||||
function isRemoteParameterOption(option: INodePropertyOptions) {
|
function isRemoteParameterOption(option: INodePropertyOptions) {
|
||||||
return remoteParameterOptionsKeys.value.includes(option.name);
|
return remoteParameterOptionsKeys.value.includes(option.name);
|
||||||
}
|
}
|
||||||
@@ -916,7 +930,16 @@ function expressionUpdated(value: string) {
|
|||||||
valueChanged(val);
|
valueChanged(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBlur() {
|
function onBlur(event?: FocusEvent | KeyboardEvent) {
|
||||||
|
if (
|
||||||
|
event?.target instanceof HTMLElement &&
|
||||||
|
mapperRef.value?.contentRef &&
|
||||||
|
(event.target === mapperRef.value.contentRef ||
|
||||||
|
mapperRef.value.contentRef.contains(event.target))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
emit('blur');
|
emit('blur');
|
||||||
isFocused.value = false;
|
isFocused.value = false;
|
||||||
}
|
}
|
||||||
@@ -1166,6 +1189,8 @@ onUpdated(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onClickOutside(wrapper, onBlur);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -1186,7 +1211,17 @@ onUpdated(async () => {
|
|||||||
:redact-values="shouldRedactValue"
|
:redact-values="shouldRedactValue"
|
||||||
@close-dialog="closeExpressionEditDialog"
|
@close-dialog="closeExpressionEditDialog"
|
||||||
@update:model-value="expressionUpdated"
|
@update:model-value="expressionUpdated"
|
||||||
></ExpressionEditModal>
|
/>
|
||||||
|
|
||||||
|
<ExperimentalEmbeddedNdvMapper
|
||||||
|
v-if="node && expressionLocalResolveCtx?.inputNode"
|
||||||
|
ref="mapperRef"
|
||||||
|
:workflow="expressionLocalResolveCtx?.workflow"
|
||||||
|
:node="node"
|
||||||
|
:input-node-name="expressionLocalResolveCtx?.inputNode?.name"
|
||||||
|
:visible="shouldShowMapper"
|
||||||
|
:virtual-ref="wrapper"
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ beforeEach(() => {
|
|||||||
},
|
},
|
||||||
isInputPanelEmpty: false,
|
isInputPanelEmpty: false,
|
||||||
isOutputPanelEmpty: false,
|
isOutputPanelEmpty: false,
|
||||||
|
ndvInputDataWithPinnedData: [],
|
||||||
|
getHoveringItem: undefined,
|
||||||
|
expressionOutputItemIndex: 0,
|
||||||
|
isTableHoverOnboarded: false,
|
||||||
|
setHighlightDraggables: vi.fn(),
|
||||||
};
|
};
|
||||||
mockNodeTypesState = {
|
mockNodeTypesState = {
|
||||||
allNodeTypes: [],
|
allNodeTypes: [],
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ const emit = defineEmits<{
|
|||||||
];
|
];
|
||||||
displayModeChange: [IRunDataDisplayMode];
|
displayModeChange: [IRunDataDisplayMode];
|
||||||
collapsingTableColumnChanged: [columnName: string | null];
|
collapsingTableColumnChanged: [columnName: string | null];
|
||||||
|
captureWheelDataContainer: [WheelEvent];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const connectionType = ref<NodeConnectionType>(NodeConnectionTypes.Main);
|
const connectionType = ref<NodeConnectionType>(NodeConnectionTypes.Main);
|
||||||
@@ -1609,7 +1610,12 @@ defineExpose({ enterEditMode });
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="dataContainerRef" :class="$style.dataContainer" data-test-id="ndv-data-container">
|
<div
|
||||||
|
ref="dataContainerRef"
|
||||||
|
:class="$style.dataContainer"
|
||||||
|
data-test-id="ndv-data-container"
|
||||||
|
@wheel.capture="emit('captureWheelDataContainer', $event)"
|
||||||
|
>
|
||||||
<BinaryDataDisplay
|
<BinaryDataDisplay
|
||||||
v-if="binaryDataDisplayData"
|
v-if="binaryDataDisplayData"
|
||||||
:window-visible="binaryDataDisplayVisible"
|
:window-visible="binaryDataDisplayVisible"
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ describe('SqlEditor.vue', () => {
|
|||||||
},
|
},
|
||||||
[STORES.NDV]: {
|
[STORES.NDV]: {
|
||||||
activeNodeName: 'Test Node',
|
activeNodeName: 'Test Node',
|
||||||
|
hasInputData: true,
|
||||||
|
isInputPanelEmpty: false,
|
||||||
|
isOutputPanelEmpty: false,
|
||||||
|
ndvInputDataWithPinnedData: [],
|
||||||
|
getHoveringItem: undefined,
|
||||||
|
expressionOutputItemIndex: 0,
|
||||||
|
isTableHoverOnboarded: false,
|
||||||
|
setHighlightDraggables: vi.fn(),
|
||||||
},
|
},
|
||||||
[STORES.WORKFLOWS]: {
|
[STORES.WORKFLOWS]: {
|
||||||
workflow: {
|
workflow: {
|
||||||
@@ -185,10 +193,22 @@ describe('SqlEditor.vue', () => {
|
|||||||
// Does not hide output when clicking inside the output
|
// Does not hide output when clicking inside the output
|
||||||
await focusEditor(container);
|
await focusEditor(container);
|
||||||
await userEvent.click(getByTestId(EXPRESSION_OUTPUT_TEST_ID));
|
await userEvent.click(getByTestId(EXPRESSION_OUTPUT_TEST_ID));
|
||||||
await waitFor(() => expect(queryByTestId(EXPRESSION_OUTPUT_TEST_ID)).toBeInTheDocument());
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
queryByTestId(EXPRESSION_OUTPUT_TEST_ID)?.closest('[aria-hidden=false]'),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
|
||||||
// Does hide output when clicking outside the container
|
// Does hide output when clicking outside the container
|
||||||
await userEvent.click(baseElement);
|
await userEvent.click(baseElement);
|
||||||
await waitFor(() => expect(queryByTestId(EXPRESSION_OUTPUT_TEST_ID)).not.toBeInTheDocument());
|
|
||||||
|
// NOTE: in testing, popover persists regardless of persist option.
|
||||||
|
// See https://github.com/element-plus/element-plus/blob/2.4.3/packages/components/tooltip/src/content.vue#L83-L90
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
queryByTestId(EXPRESSION_OUTPUT_TEST_ID)?.closest('[aria-hidden=true]'),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ const emit = defineEmits<{
|
|||||||
const container = ref<HTMLDivElement>();
|
const container = ref<HTMLDivElement>();
|
||||||
const sqlEditor = ref<HTMLDivElement>();
|
const sqlEditor = ref<HTMLDivElement>();
|
||||||
const isFocused = ref(false);
|
const isFocused = ref(false);
|
||||||
|
const outputPopover = ref<InstanceType<typeof InlineExpressionEditorOutput>>();
|
||||||
|
|
||||||
const extensions = computed(() => {
|
const extensions = computed(() => {
|
||||||
const dialect = SQL_DIALECTS[props.dialect] ?? SQL_DIALECTS.StandardSQL;
|
const dialect = SQL_DIALECTS[props.dialect] ?? SQL_DIALECTS.StandardSQL;
|
||||||
@@ -156,9 +157,10 @@ onClickOutside(container, (event) => onBlur(event));
|
|||||||
function onBlur(event: FocusEvent | KeyboardEvent) {
|
function onBlur(event: FocusEvent | KeyboardEvent) {
|
||||||
if (
|
if (
|
||||||
event?.target instanceof Element &&
|
event?.target instanceof Element &&
|
||||||
Array.from(event.target.classList).some((_class) => _class.includes('resizer'))
|
(Array.from(event.target.classList).some((_class) => _class.includes('resizer')) ||
|
||||||
|
outputPopover.value?.contentRef?.contains(event.target))
|
||||||
) {
|
) {
|
||||||
return; // prevent blur on resizing
|
return; // prevent blur on resizing or interacting with output popover
|
||||||
}
|
}
|
||||||
|
|
||||||
isFocused.value = false;
|
isFocused.value = false;
|
||||||
@@ -219,9 +221,11 @@ defineExpose({
|
|||||||
<slot name="suffix" />
|
<slot name="suffix" />
|
||||||
<InlineExpressionEditorOutput
|
<InlineExpressionEditorOutput
|
||||||
v-if="!fullscreen"
|
v-if="!fullscreen"
|
||||||
|
ref="outputPopover"
|
||||||
:segments="segments"
|
:segments="segments"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:visible="isFocused"
|
:visible="isFocused"
|
||||||
|
:virtual-ref="container"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -137,6 +137,9 @@ const props = withDefaults(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { isMobileDevice, controlKeyCode } = useDeviceSupport();
|
const { isMobileDevice, controlKeyCode } = useDeviceSupport();
|
||||||
|
const experimentalNdvStore = useExperimentalNdvStore();
|
||||||
|
|
||||||
|
const isExperimentalNdvActive = computed(() => experimentalNdvStore.isActive(viewport.value.zoom));
|
||||||
|
|
||||||
const vueFlow = useVueFlow(props.id);
|
const vueFlow = useVueFlow(props.id);
|
||||||
const {
|
const {
|
||||||
@@ -174,14 +177,10 @@ const {
|
|||||||
getDownstreamNodes,
|
getDownstreamNodes,
|
||||||
getUpstreamNodes,
|
getUpstreamNodes,
|
||||||
} = useCanvasTraversal(vueFlow);
|
} = useCanvasTraversal(vueFlow);
|
||||||
const { layout } = useCanvasLayout({ id: props.id });
|
const { layout } = useCanvasLayout(props.id, isExperimentalNdvActive);
|
||||||
|
|
||||||
const experimentalNdvStore = useExperimentalNdvStore();
|
|
||||||
|
|
||||||
const isPaneReady = ref(false);
|
const isPaneReady = ref(false);
|
||||||
|
|
||||||
const isExperimentalNdvActive = computed(() => experimentalNdvStore.isActive(viewport.value.zoom));
|
|
||||||
|
|
||||||
const classes = computed(() => ({
|
const classes = computed(() => ({
|
||||||
[$style.canvas]: true,
|
[$style.canvas]: true,
|
||||||
[$style.ready]: !props.loading && isPaneReady.value,
|
[$style.ready]: !props.loading && isPaneReady.value,
|
||||||
@@ -886,7 +885,6 @@ watch([vueFlow.nodes, () => experimentalNdvStore.nodeNameToBeFocused], ([nodes,
|
|||||||
// setTimeout() so that this happens after layout recalculation with the node to be focused
|
// setTimeout() so that this happens after layout recalculation with the node to be focused
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
experimentalNdvStore.focusNode(toFocusNode, {
|
experimentalNdvStore.focusNode(toFocusNode, {
|
||||||
collapseOthers: false,
|
|
||||||
canvasViewport: viewport.value,
|
canvasViewport: viewport.value,
|
||||||
canvasDimensions: dimensions.value,
|
canvasDimensions: dimensions.value,
|
||||||
setCenter,
|
setCenter,
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const mappedConnectionsThrottled = throttledRef(mappedConnections, 200);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.wrapper" data-test-id="canvas-wrapper">
|
<div :class="$style.wrapper" data-test-id="canvas-wrapper">
|
||||||
<div :class="$style.canvas">
|
<div id="canvas" :class="$style.canvas">
|
||||||
<Canvas
|
<Canvas
|
||||||
v-if="workflow"
|
v-if="workflow"
|
||||||
:id="id"
|
:id="id"
|
||||||
|
|||||||
@@ -397,6 +397,8 @@ onBeforeUnmount(() => {
|
|||||||
data-test-id="canvas-node-toolbar"
|
data-test-id="canvas-node-toolbar"
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
:class="$style.canvasNodeToolbar"
|
:class="$style.canvasNodeToolbar"
|
||||||
|
:show-status-icons="isExperimentalNdvActive"
|
||||||
|
:items-class="$style.canvasNodeToolbarItems"
|
||||||
@delete="onDelete"
|
@delete="onDelete"
|
||||||
@toggle="onDisabledToggle"
|
@toggle="onDisabledToggle"
|
||||||
@run="onRun"
|
@run="onRun"
|
||||||
@@ -431,27 +433,24 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.canvasNode {
|
.canvasNode {
|
||||||
|
.canvasNodeToolbarItems {
|
||||||
|
transition: opacity 0.1s ease-in;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover:not(:has(> .trigger:hover)), // exclude .trigger which has extended hit zone
|
&:hover:not(:has(> .trigger:hover)), // exclude .trigger which has extended hit zone
|
||||||
&:focus-within,
|
&:focus-within,
|
||||||
&.showToolbar {
|
&.showToolbar {
|
||||||
.canvasNodeToolbar {
|
.canvasNodeToolbarItems {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvasNodeToolbar {
|
.canvasNodeToolbar {
|
||||||
transition: opacity 0.1s ease-in;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
bottom: 100%;
|
||||||
left: 50%;
|
left: 0;
|
||||||
transform: translate(-50%, -100%) scale(var(--canvas-zoom-compensation-factor, 1));
|
|
||||||
opacity: 0;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
&:focus-within,
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useCanvas } from '@/composables/useCanvas';
|
|||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useExperimentalNdvStore } from '../../experimental/experimentalNdv.store';
|
import { useExperimentalNdvStore } from '../../experimental/experimentalNdv.store';
|
||||||
|
import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
delete: [];
|
delete: [];
|
||||||
@@ -19,6 +20,8 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
showStatusIcons: boolean;
|
||||||
|
itemsClass: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
@@ -63,12 +66,7 @@ const isDisableNodeVisible = computed(() => {
|
|||||||
|
|
||||||
const isDeleteNodeVisible = computed(() => !props.readOnly);
|
const isDeleteNodeVisible = computed(() => !props.readOnly);
|
||||||
|
|
||||||
const isFocusNodeVisible = computed(
|
const isFocusNodeVisible = computed(() => experimentalNdvStore.isEnabled);
|
||||||
() =>
|
|
||||||
experimentalNdvStore.isEnabled &&
|
|
||||||
node.value !== null &&
|
|
||||||
experimentalNdvStore.collapsedNodes[node.value.id] !== false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isStickyNoteChangeColorVisible = computed(
|
const isStickyNoteChangeColorVisible = computed(
|
||||||
() => !props.readOnly && render.value.type === CanvasNodeRenderType.StickyNote,
|
() => !props.readOnly && render.value.type === CanvasNodeRenderType.StickyNote,
|
||||||
@@ -118,7 +116,7 @@ function onFocusNode() {
|
|||||||
@mouseenter="onMouseEnter"
|
@mouseenter="onMouseEnter"
|
||||||
@mouseleave="onMouseLeave"
|
@mouseleave="onMouseLeave"
|
||||||
>
|
>
|
||||||
<div :class="$style.canvasNodeToolbarItems">
|
<div :class="[$style.canvasNodeToolbarItems, itemsClass]">
|
||||||
<N8nTooltip
|
<N8nTooltip
|
||||||
v-if="isExecuteNodeVisible"
|
v-if="isExecuteNodeVisible"
|
||||||
placement="top"
|
placement="top"
|
||||||
@@ -178,6 +176,11 @@ function onFocusNode() {
|
|||||||
@click="onOpenContextMenu"
|
@click="onOpenContextMenu"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<CanvasNodeStatusIcons
|
||||||
|
v-if="showStatusIcons"
|
||||||
|
:class="$style.statusIcons"
|
||||||
|
spinner-layout="static"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -189,8 +192,11 @@ function onFocusNode() {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&.isExperimentalNdvActive {
|
&.isExperimentalNdvActive {
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
padding-bottom: var(--spacing-3xs);
|
padding-bottom: var(--spacing-3xs);
|
||||||
|
zoom: var(--canvas-zoom-compensation-factor, 1);
|
||||||
|
margin-bottom: var(--spacing-2xs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,4 +215,8 @@ function onFocusNode() {
|
|||||||
.forceVisible {
|
.forceVisible {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statusIcons {
|
||||||
|
margin-inline-end: var(--spacing-3xs);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ const nodeSize = computed(() =>
|
|||||||
mainInputs.value.length,
|
mainInputs.value.length,
|
||||||
mainOutputs.value.length,
|
mainOutputs.value.length,
|
||||||
nonMainInputs.value.length,
|
nonMainInputs.value.length,
|
||||||
|
isExperimentalNdvActive.value,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ import { CanvasNodeDirtiness, CanvasNodeRenderType } from '@/types';
|
|||||||
import { N8nTooltip } from '@n8n/design-system';
|
import { N8nTooltip } from '@n8n/design-system';
|
||||||
import { useCanvas } from '@/composables/useCanvas';
|
import { useCanvas } from '@/composables/useCanvas';
|
||||||
|
|
||||||
const { size = 'large', spinnerScrim = false } = defineProps<{
|
const {
|
||||||
|
size = 'large',
|
||||||
|
spinnerScrim = false,
|
||||||
|
spinnerLayout = 'absolute',
|
||||||
|
} = defineProps<{
|
||||||
size?: 'small' | 'medium' | 'large';
|
size?: 'small' | 'medium' | 'large';
|
||||||
spinnerScrim?: boolean;
|
spinnerScrim?: boolean;
|
||||||
|
spinnerLayout?: 'absolute' | 'static';
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
@@ -44,7 +49,11 @@ const isNodeExecuting = computed(() => {
|
|||||||
executionRunning.value || executionWaitingForNext.value || executionStatus.value === 'running' // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
|
executionRunning.value || executionWaitingForNext.value || executionStatus.value === 'running' // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const commonClasses = computed(() => [$style.status, spinnerScrim ? $style.spinnerScrim : '']);
|
const commonClasses = computed(() => [
|
||||||
|
$style.status,
|
||||||
|
spinnerScrim ? $style.spinnerScrim : '',
|
||||||
|
spinnerLayout === 'absolute' ? $style.absoluteSpinner : '',
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -57,7 +66,10 @@ const commonClasses = computed(() => [$style.status, spinnerScrim ? $style.spinn
|
|||||||
<N8nIcon icon="clock" :size="size" />
|
<N8nIcon icon="clock" :size="size" />
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div :class="[...commonClasses, $style['node-waiting-spinner']]">
|
<div
|
||||||
|
v-if="spinnerLayout === 'absolute'"
|
||||||
|
:class="[...commonClasses, $style['node-waiting-spinner']]"
|
||||||
|
>
|
||||||
<N8nIcon icon="refresh-cw" spin />
|
<N8nIcon icon="refresh-cw" spin />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,17 +154,21 @@ const commonClasses = computed(() => [$style.status, spinnerScrim ? $style.spinn
|
|||||||
|
|
||||||
.node-waiting-spinner,
|
.node-waiting-spinner,
|
||||||
.running {
|
.running {
|
||||||
width: 100%;
|
color: hsl(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l));
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
&.absoluteSpinner {
|
||||||
align-items: center;
|
width: 100%;
|
||||||
justify-content: center;
|
height: 100%;
|
||||||
font-size: 3.75em;
|
display: flex;
|
||||||
color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
|
align-items: center;
|
||||||
position: absolute;
|
justify-content: center;
|
||||||
left: 0;
|
font-size: 3.75em;
|
||||||
top: 0;
|
color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
|
||||||
padding: var(--canvas-node--status-icons-offset);
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
padding: var(--canvas-node--status-icons-offset);
|
||||||
|
}
|
||||||
|
|
||||||
&.spinnerScrim {
|
&.spinnerScrim {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ function handleValueChanged(parameterData: IUpdateInformation) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDoubleClickHeader() {
|
||||||
|
if (activeNode.value) {
|
||||||
|
ndvStore.setActiveNodeName(activeNode.value.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleCaptureWheelEvent(event: WheelEvent) {
|
function handleCaptureWheelEvent(event: WheelEvent) {
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
// If the event is pinch, let it propagate and zoom canvas
|
// If the event is pinch, let it propagate and zoom canvas
|
||||||
@@ -55,7 +61,7 @@ function handleCaptureWheelEvent(event: WheelEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, let it scroll the settings pane
|
// Otherwise, let it scroll the pane
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -71,8 +77,11 @@ function handleCaptureWheelEvent(event: WheelEvent) {
|
|||||||
:executable="!isReadOnly"
|
:executable="!isReadOnly"
|
||||||
is-embedded-in-canvas
|
is-embedded-in-canvas
|
||||||
:sub-title="subTitle"
|
:sub-title="subTitle"
|
||||||
|
extra-tabs-class-name="nodrag"
|
||||||
|
extra-parameter-wrapper-class-name="nodrag"
|
||||||
@value-changed="handleValueChanged"
|
@value-changed="handleValueChanged"
|
||||||
@capture-wheel-body="handleCaptureWheelEvent"
|
@capture-wheel-body="handleCaptureWheelEvent"
|
||||||
|
@dblclick-header="handleDoubleClickHeader"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue';
|
|
||||||
import { N8nIconButton } from '@n8n/design-system';
|
import { N8nIconButton } from '@n8n/design-system';
|
||||||
|
|
||||||
defineProps<{ isExpanded: boolean }>();
|
defineProps<{ isExpanded: boolean }>();
|
||||||
@@ -9,9 +8,6 @@ const emit = defineEmits<{ openNdv: []; toggleExpand: [] }>();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.actions">
|
<div :class="$style.actions">
|
||||||
<div :class="$style.icon">
|
|
||||||
<CanvasNodeStatusIcons size="small" spinner-scrim />
|
|
||||||
</div>
|
|
||||||
<N8nIconButton
|
<N8nIconButton
|
||||||
icon="maximize-2"
|
icon="maximize-2"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
@@ -43,8 +39,4 @@ const emit = defineEmits<{ openNdv: []; toggleExpand: [] }>();
|
|||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-inline: var(--spacing-2xs);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ defineProps<{
|
|||||||
nodeType?: INodeTypeDescription | null;
|
nodeType?: INodeTypeDescription | null;
|
||||||
pushRef: string;
|
pushRef: string;
|
||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
|
extraTabsClassName?: string;
|
||||||
selectedTab: NodeSettingsTab;
|
selectedTab: NodeSettingsTab;
|
||||||
includeAction: boolean;
|
includeAction: boolean;
|
||||||
includeCredential: boolean;
|
includeCredential: boolean;
|
||||||
@@ -19,6 +20,7 @@ defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'name-changed': [value: string];
|
'name-changed': [value: string];
|
||||||
|
'dblclick-title': [event: MouseEvent];
|
||||||
'tab-changed': [tab: NodeSettingsTab];
|
'tab-changed': [tab: NodeSettingsTab];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ defineSlots<{ actions?: {} }>();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.component, node.disabled ? $style.disabled : '']">
|
<div :class="[$style.component, node.disabled ? $style.disabled : '']">
|
||||||
<div :class="$style.title">
|
<div :class="$style.title" @dblclick="emit('dblclick-title', $event)">
|
||||||
<NodeIcon :node-type="nodeType" :size="16" />
|
<NodeIcon :node-type="nodeType" :size="16" />
|
||||||
<div :class="$style.titleText">
|
<div :class="$style.titleText">
|
||||||
<N8nInlineTextEdit
|
<N8nInlineTextEdit
|
||||||
@@ -44,6 +46,7 @@ defineSlots<{ actions?: {} }>();
|
|||||||
</div>
|
</div>
|
||||||
<div :class="$style.tabsContainer">
|
<div :class="$style.tabsContainer">
|
||||||
<NodeSettingsTabs
|
<NodeSettingsTabs
|
||||||
|
:class="extraTabsClassName"
|
||||||
:model-value="selectedTab"
|
:model-value="selectedTab"
|
||||||
:node-type="nodeType"
|
:node-type="nodeType"
|
||||||
:push-ref="pushRef"
|
:push-ref="pushRef"
|
||||||
@@ -68,8 +71,8 @@ defineSlots<{ actions?: {} }>();
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--spacing-2xs) var(--spacing-3xs) var(--spacing-2xs) var(--spacing-xs);
|
padding: var(--spacing-2xs) var(--spacing-3xs) var(--spacing-2xs) var(--spacing-xs);
|
||||||
border-bottom: var(--border-base);
|
border-bottom: var(--border-base);
|
||||||
margin-bottom: 14px; // to match bottom padding of tabs
|
|
||||||
gap: var(--spacing-4xs);
|
gap: var(--spacing-4xs);
|
||||||
|
cursor: grab;
|
||||||
|
|
||||||
.disabled & {
|
.disabled & {
|
||||||
background-color: var(--color-foreground-light);
|
background-color: var(--color-foreground-light);
|
||||||
@@ -99,6 +102,7 @@ defineSlots<{ actions?: {} }>();
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tabsContainer {
|
.tabsContainer {
|
||||||
|
padding-top: var(--spacing-xs);
|
||||||
padding-inline: var(--spacing-xs);
|
padding-inline: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,83 +1,61 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import InputPanel from '@/components/InputPanel.vue';
|
import InputPanel from '@/components/InputPanel.vue';
|
||||||
import { useCanvas } from '@/composables/useCanvas';
|
import { CanvasKey } from '@/constants';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { N8nText } from '@n8n/design-system';
|
import { N8nPopover } from '@n8n/design-system';
|
||||||
import { useVueFlow } from '@vue-flow/core';
|
import { useVueFlow } from '@vue-flow/core';
|
||||||
import { useActiveElement } from '@vueuse/core';
|
import { watchOnce } from '@vueuse/core';
|
||||||
import { ElPopover } from 'element-plus';
|
|
||||||
import type { Workflow } from 'n8n-workflow';
|
import type { Workflow } from 'n8n-workflow';
|
||||||
import { ref, useTemplateRef, watch } from 'vue';
|
import { computed, inject, ref, useTemplateRef } from 'vue';
|
||||||
|
|
||||||
const { node, container } = defineProps<{
|
const { node, inputNodeName, visible, virtualRef } = defineProps<{
|
||||||
workflow: Workflow;
|
workflow: Workflow;
|
||||||
node: INodeUi;
|
node: INodeUi;
|
||||||
container: HTMLDivElement | null;
|
inputNodeName: string;
|
||||||
inputNodeName?: string;
|
visible: boolean;
|
||||||
|
virtualRef: HTMLElement | undefined;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const contentRef = useTemplateRef('content');
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
const vf = useVueFlow();
|
const vf = useVueFlow();
|
||||||
const { isPaneMoving, viewport } = useCanvas();
|
const canvas = inject(CanvasKey, undefined);
|
||||||
const activeElement = useActiveElement();
|
const isVisible = computed(() => visible && !canvas?.isPaneMoving.value);
|
||||||
|
const isOnceVisible = ref(isVisible.value);
|
||||||
|
|
||||||
const inputPanelRef = useTemplateRef('inputPanel');
|
watchOnce(isVisible, (value) => {
|
||||||
const shouldShowInputPanel = ref(false);
|
isOnceVisible.value = isOnceVisible.value || value;
|
||||||
|
|
||||||
function getShouldShowInputPanel() {
|
|
||||||
const active = activeElement.value;
|
|
||||||
|
|
||||||
if (!active || !container || !container.contains(active)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: find a way to implement this without depending on test ID
|
|
||||||
return (
|
|
||||||
!!active.closest('[data-test-id=inline-expression-editor-input]') ||
|
|
||||||
!!inputPanelRef.value?.$el.contains(active)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
watch([activeElement, vf.getSelectedNodes], ([active, selected]) => {
|
|
||||||
if (active && container?.contains(active)) {
|
|
||||||
shouldShowInputPanel.value = getShouldShowInputPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selected.every((sel) => sel.id !== node.id)) {
|
|
||||||
shouldShowInputPanel.value = false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(isPaneMoving, (moving) => {
|
defineExpose({
|
||||||
shouldShowInputPanel.value = !moving && getShouldShowInputPanel();
|
contentRef: computed<HTMLElement>(() => contentRef.value?.$el ?? null),
|
||||||
});
|
|
||||||
|
|
||||||
watch(viewport, () => {
|
|
||||||
shouldShowInputPanel.value = false;
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElPopover
|
<N8nPopover
|
||||||
:visible="shouldShowInputPanel"
|
:visible="isVisible"
|
||||||
placement="left-start"
|
placement="left"
|
||||||
:show-arrow="false"
|
:show-arrow="false"
|
||||||
:popper-class="$style.component"
|
:popper-class="$style.component"
|
||||||
:width="360"
|
:width="360"
|
||||||
:offset="8"
|
:offset="8"
|
||||||
:append-to="vf.viewportRef?.value"
|
append-to="#canvas"
|
||||||
:popper-options="{
|
:popper-options="{
|
||||||
modifiers: [{ name: 'flip', enabled: false }],
|
modifiers: [{ name: 'flip', enabled: false }],
|
||||||
}"
|
}"
|
||||||
|
:persistent="isOnceVisible /* works like lazy initialization */"
|
||||||
|
virtual-triggering
|
||||||
|
:virtual-ref="virtualRef"
|
||||||
>
|
>
|
||||||
<template #reference>
|
|
||||||
<slot />
|
|
||||||
</template>
|
|
||||||
<InputPanel
|
<InputPanel
|
||||||
ref="inputPanel"
|
ref="content"
|
||||||
:tabindex="-1"
|
:tabindex="-1"
|
||||||
:class="$style.inputPanel"
|
:class="$style.inputPanel"
|
||||||
|
:style="{
|
||||||
|
maxHeight: `calc(${vf.viewportRef.value?.offsetHeight ?? 0}px - var(--spacing-s) * 2)`,
|
||||||
|
}"
|
||||||
:workflow-object="workflow"
|
:workflow-object="workflow"
|
||||||
:run-index="0"
|
:run-index="0"
|
||||||
compact
|
compact
|
||||||
@@ -88,21 +66,17 @@ watch(viewport, () => {
|
|||||||
:current-node-name="inputNodeName"
|
:current-node-name="inputNodeName"
|
||||||
:is-mapping-onboarded="ndvStore.isMappingOnboarded"
|
:is-mapping-onboarded="ndvStore.isMappingOnboarded"
|
||||||
:focused-mappable-input="ndvStore.focusedMappableInput"
|
:focused-mappable-input="ndvStore.focusedMappableInput"
|
||||||
>
|
node-not-run-message-variant="simple"
|
||||||
<template #header>
|
/>
|
||||||
<N8nText :class="$style.inputPanelTitle" :bold="true" color="text-light" size="small">
|
</N8nPopover>
|
||||||
Input
|
|
||||||
</N8nText>
|
|
||||||
</template>
|
|
||||||
</InputPanel>
|
|
||||||
</ElPopover>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.component {
|
.component {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
padding: 0 !important;
|
padding: var(--spacing-s) 0 !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
margin-top: -2px;
|
margin-top: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,10 +85,10 @@ watch(viewport, () => {
|
|||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
background-color: var(--color-background-light);
|
background-color: var(--color-background-light);
|
||||||
border-radius: var(--border-radius-large);
|
border-radius: var(--border-radius-large);
|
||||||
zoom: var(--zoom);
|
|
||||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.05);
|
||||||
padding: var(--spacing-2xs);
|
padding: var(--spacing-2xs);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputPanelTitle {
|
.inputPanelTitle {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { ExpressionLocalResolveContext } from '@/types/expressions';
|
|||||||
import { N8nText } from '@n8n/design-system';
|
import { N8nText } from '@n8n/design-system';
|
||||||
import { useVueFlow } from '@vue-flow/core';
|
import { useVueFlow } from '@vue-flow/core';
|
||||||
import { watchOnce } from '@vueuse/core';
|
import { watchOnce } from '@vueuse/core';
|
||||||
import { computed, provide, ref, useTemplateRef } from 'vue';
|
import { computed, provide, ref } from 'vue';
|
||||||
import { useExperimentalNdvStore } from '../experimentalNdv.store';
|
import { useExperimentalNdvStore } from '../experimentalNdv.store';
|
||||||
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
|
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
@@ -18,10 +18,9 @@ import { getNodeSubTitleText } from '@/components/canvas/experimental/experiment
|
|||||||
import ExperimentalEmbeddedNdvActions from '@/components/canvas/experimental/components/ExperimentalEmbeddedNdvActions.vue';
|
import ExperimentalEmbeddedNdvActions from '@/components/canvas/experimental/components/ExperimentalEmbeddedNdvActions.vue';
|
||||||
import { useCanvas } from '@/composables/useCanvas';
|
import { useCanvas } from '@/composables/useCanvas';
|
||||||
|
|
||||||
const { nodeId, isReadOnly, isConfigurable } = defineProps<{
|
const { nodeId, isReadOnly } = defineProps<{
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
isConfigurable: boolean;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -52,8 +51,6 @@ const isVisible = computed(() =>
|
|||||||
);
|
);
|
||||||
const isOnceVisible = ref(isVisible.value);
|
const isOnceVisible = ref(isVisible.value);
|
||||||
|
|
||||||
const containerRef = useTemplateRef('container');
|
|
||||||
|
|
||||||
const subTitle = computed(() =>
|
const subTitle = computed(() =>
|
||||||
node.value && nodeType.value
|
node.value && nodeType.value
|
||||||
? getNodeSubTitleText(node.value, nodeType.value, !isExpanded.value, i18n)
|
? getNodeSubTitleText(node.value, nodeType.value, !isExpanded.value, i18n)
|
||||||
@@ -105,6 +102,7 @@ const expressionResolveCtx = computed<ExpressionLocalResolveContext | undefined>
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const maxHeightOnFocus = computed(() => vf.dimensions.value.height * 0.8);
|
||||||
const workflowObject = computed(() => workflowsStore.workflowObject as Workflow);
|
const workflowObject = computed(() => workflowsStore.workflowObject as Workflow);
|
||||||
|
|
||||||
function handleToggleExpand() {
|
function handleToggleExpand() {
|
||||||
@@ -126,44 +124,34 @@ watchOnce(isVisible, (visible) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="container"
|
|
||||||
:class="[
|
:class="[
|
||||||
$style.component,
|
$style.component,
|
||||||
isExpanded ? $style.expanded : $style.collapsed,
|
isExpanded ? $style.expanded : $style.collapsed,
|
||||||
node?.disabled ? $style.disabled : '',
|
node?.disabled ? $style.disabled : '',
|
||||||
isExpanded ? 'nodrag' : '',
|
|
||||||
]"
|
]"
|
||||||
:style="{
|
:style="{
|
||||||
'--zoom': `${1 / experimentalNdvStore.maxCanvasZoom}`,
|
'--max-height-on-focus': `${maxHeightOnFocus / experimentalNdvStore.maxCanvasZoom}px`,
|
||||||
'--node-width-scaler': isConfigurable ? 1 : 1.5,
|
|
||||||
'--max-height-on-focus': `${(vf.dimensions.value.height * 0.8) / experimentalNdvStore.maxCanvasZoom}px`,
|
|
||||||
pointerEvents: isPaneMoving ? 'none' : 'auto', // Don't interrupt canvas panning
|
pointerEvents: isPaneMoving ? 'none' : 'auto', // Don't interrupt canvas panning
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template v-if="!node || !isOnceVisible" />
|
<template v-if="!node || !isOnceVisible" />
|
||||||
<ExperimentalEmbeddedNdvMapper
|
<ExperimentalCanvasNodeSettings
|
||||||
v-else-if="isExpanded"
|
v-else-if="isExpanded"
|
||||||
:workflow="workflowObject"
|
tabindex="-1"
|
||||||
:node="node"
|
:node-id="nodeId"
|
||||||
|
:class="$style.settingsView"
|
||||||
|
:is-read-only="isReadOnly"
|
||||||
|
:sub-title="subTitle"
|
||||||
:input-node-name="expressionResolveCtx?.inputNode?.name"
|
:input-node-name="expressionResolveCtx?.inputNode?.name"
|
||||||
:container="containerRef"
|
|
||||||
>
|
>
|
||||||
<ExperimentalCanvasNodeSettings
|
<template #actions>
|
||||||
tabindex="-1"
|
<ExperimentalEmbeddedNdvActions
|
||||||
:node-id="nodeId"
|
:is-expanded="isExpanded"
|
||||||
:class="$style.settingsView"
|
@open-ndv="handleOpenNdv"
|
||||||
:is-read-only="isReadOnly"
|
@toggle-expand="handleToggleExpand"
|
||||||
:sub-title="subTitle"
|
/>
|
||||||
>
|
</template>
|
||||||
<template #actions>
|
</ExperimentalCanvasNodeSettings>
|
||||||
<ExperimentalEmbeddedNdvActions
|
|
||||||
:is-expanded="isExpanded"
|
|
||||||
@open-ndv="handleOpenNdv"
|
|
||||||
@toggle-expand="handleToggleExpand"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</ExperimentalCanvasNodeSettings>
|
|
||||||
</ExperimentalEmbeddedNdvMapper>
|
|
||||||
<div v-else role="button" :class="$style.collapsedContent" @click="handleToggleExpand">
|
<div v-else role="button" :class="$style.collapsedContent" @click="handleToggleExpand">
|
||||||
<NodeIcon :node-type="nodeType" :size="18" />
|
<NodeIcon :node-type="nodeType" :size="18" />
|
||||||
<div :class="$style.collapsedNodeName">
|
<div :class="$style.collapsedNodeName">
|
||||||
@@ -189,7 +177,6 @@ watchOnce(isVisible, (visible) => {
|
|||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
border-width: 1px !important;
|
border-width: 1px !important;
|
||||||
border-radius: var(--border-radius-base) !important;
|
border-radius: var(--border-radius-base) !important;
|
||||||
width: calc(var(--canvas-node--width) * var(--node-width-scaler)) !important;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
--canvas-node--border-color: var(--color-text-lighter);
|
--canvas-node--border-color: var(--color-text-lighter);
|
||||||
@@ -253,7 +240,7 @@ watchOnce(isVisible, (visible) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
zoom: var(--zoom);
|
zoom: var(--canvas-zoom-compensation-factor, 1);
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@@ -275,7 +262,7 @@ watchOnce(isVisible, (visible) => {
|
|||||||
|
|
||||||
.settingsView {
|
.settingsView {
|
||||||
& > * {
|
& > * {
|
||||||
zoom: var(--zoom);
|
zoom: var(--canvas-zoom-compensation-factor, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { computed, ref, shallowRef } from 'vue';
|
import { computed, ref, shallowRef } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
|
||||||
import {
|
import {
|
||||||
type Dimensions,
|
type Dimensions,
|
||||||
type FitView,
|
type FitView,
|
||||||
@@ -12,18 +11,18 @@ import {
|
|||||||
type ZoomTo,
|
type ZoomTo,
|
||||||
} from '@vue-flow/core';
|
} from '@vue-flow/core';
|
||||||
import { CanvasNodeRenderType, type CanvasNodeData } from '@/types';
|
import { CanvasNodeRenderType, type CanvasNodeData } from '@/types';
|
||||||
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
|
import { CANVAS_ZOOMED_VIEW_EXPERIMENT } from '@/constants';
|
||||||
|
|
||||||
export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
|
export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
|
||||||
const workflowStore = useWorkflowsStore();
|
const workflowStore = useWorkflowsStore();
|
||||||
const settingsStore = useSettingsStore();
|
const postHogStore = usePostHog();
|
||||||
const isEnabled = computed(
|
const isEnabled = computed(
|
||||||
() =>
|
() =>
|
||||||
!Number.isNaN(settingsStore.experimental__minZoomNodeSettingsInCanvas) &&
|
postHogStore.getVariant(CANVAS_ZOOMED_VIEW_EXPERIMENT.name) ===
|
||||||
settingsStore.experimental__minZoomNodeSettingsInCanvas > 0,
|
CANVAS_ZOOMED_VIEW_EXPERIMENT.variant,
|
||||||
);
|
|
||||||
const maxCanvasZoom = computed(() =>
|
|
||||||
isEnabled.value ? settingsStore.experimental__minZoomNodeSettingsInCanvas : 4,
|
|
||||||
);
|
);
|
||||||
|
const maxCanvasZoom = computed(() => (isEnabled.value ? 2 : 4));
|
||||||
|
|
||||||
const previousViewport = ref<ViewportTransform>();
|
const previousViewport = ref<ViewportTransform>();
|
||||||
const collapsedNodes = shallowRef<Partial<Record<string, boolean>>>({});
|
const collapsedNodes = shallowRef<Partial<Record<string, boolean>>>({});
|
||||||
@@ -59,7 +58,6 @@ export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FocusNodeOptions {
|
interface FocusNodeOptions {
|
||||||
collapseOthers?: boolean;
|
|
||||||
canvasViewport: ViewportTransform;
|
canvasViewport: ViewportTransform;
|
||||||
canvasDimensions: Dimensions;
|
canvasDimensions: Dimensions;
|
||||||
setCenter: SetCenter;
|
setCenter: SetCenter;
|
||||||
@@ -67,14 +65,9 @@ export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
|
|||||||
|
|
||||||
function focusNode(
|
function focusNode(
|
||||||
node: GraphNode<CanvasNodeData>,
|
node: GraphNode<CanvasNodeData>,
|
||||||
{ collapseOthers = true, canvasDimensions, canvasViewport, setCenter }: FocusNodeOptions,
|
{ canvasDimensions, canvasViewport, setCenter }: FocusNodeOptions,
|
||||||
) {
|
) {
|
||||||
collapsedNodes.value = collapseOthers
|
collapsedNodes.value = { ...collapsedNodes.value, [node.id]: false };
|
||||||
? workflowStore.allNodes.reduce<Partial<Record<string, boolean>>>((acc, n) => {
|
|
||||||
acc[n.id] = n.id !== node.id;
|
|
||||||
return acc;
|
|
||||||
}, {})
|
|
||||||
: { ...collapsedNodes.value, [node.id]: false };
|
|
||||||
|
|
||||||
const topMargin = 80; // pixels
|
const topMargin = 80; // pixels
|
||||||
const nodeWidth = node.dimensions.width * (isActive(canvasViewport.zoom) ? 1 : 1.5);
|
const nodeWidth = node.dimensions.width * (isActive(canvasViewport.zoom) ? 1 : 1.5);
|
||||||
@@ -134,7 +127,7 @@ export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
|
|||||||
)[0];
|
)[0];
|
||||||
|
|
||||||
if (toFocus) {
|
if (toFocus) {
|
||||||
focusNode(toFocus, { ...options, collapseOthers: false });
|
focusNode(toFocus, options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useVueFlow, type GraphNode, type VueFlowStore } from '@vue-flow/core';
|
import { useVueFlow, type GraphNode, type VueFlowStore } from '@vue-flow/core';
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { createCanvasGraphEdge, createCanvasGraphNode } from '../__tests__/data';
|
import { createCanvasGraphEdge, createCanvasGraphNode } from '../__tests__/data';
|
||||||
import { CanvasNodeRenderType, type CanvasNodeData } from '../types';
|
import { CanvasNodeRenderType, type CanvasNodeData } from '../types';
|
||||||
import { useCanvasLayout, type CanvasLayoutResult } from './useCanvasLayout';
|
import { useCanvasLayout, type CanvasLayoutResult } from './useCanvasLayout';
|
||||||
@@ -36,7 +36,10 @@ describe('useCanvasLayout', () => {
|
|||||||
|
|
||||||
vi.mocked(useVueFlow).mockReturnValue(vueFlowStoreMock);
|
vi.mocked(useVueFlow).mockReturnValue(vueFlowStoreMock);
|
||||||
|
|
||||||
const { layout } = useCanvasLayout();
|
const { layout } = useCanvasLayout(
|
||||||
|
'test-canvas-id',
|
||||||
|
computed(() => false),
|
||||||
|
);
|
||||||
|
|
||||||
return { layout };
|
return { layout };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
} from '../types';
|
} from '../types';
|
||||||
import { isPresent } from '../utils/typesUtils';
|
import { isPresent } from '../utils/typesUtils';
|
||||||
import { DEFAULT_NODE_SIZE, GRID_SIZE, calculateNodeSize } from '../utils/nodeViewUtils';
|
import { DEFAULT_NODE_SIZE, GRID_SIZE, calculateNodeSize } from '../utils/nodeViewUtils';
|
||||||
|
import type { ComputedRef } from 'vue';
|
||||||
|
|
||||||
export type CanvasLayoutOptions = { id?: string };
|
|
||||||
export type CanvasLayoutTarget = 'selection' | 'all';
|
export type CanvasLayoutTarget = 'selection' | 'all';
|
||||||
export type CanvasLayoutSource =
|
export type CanvasLayoutSource =
|
||||||
| 'keyboard-shortcut'
|
| 'keyboard-shortcut'
|
||||||
@@ -47,7 +47,7 @@ const AI_X_SPACING = GRID_SIZE * 3;
|
|||||||
const AI_Y_SPACING = GRID_SIZE * 8;
|
const AI_Y_SPACING = GRID_SIZE * 8;
|
||||||
const STICKY_BOTTOM_PADDING = GRID_SIZE * 4;
|
const STICKY_BOTTOM_PADDING = GRID_SIZE * 4;
|
||||||
|
|
||||||
export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
|
export function useCanvasLayout(canvasId: string, isEmbeddedNdvActive: ComputedRef<boolean>) {
|
||||||
const {
|
const {
|
||||||
findNode,
|
findNode,
|
||||||
findEdge,
|
findEdge,
|
||||||
@@ -120,6 +120,7 @@ export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
|
|||||||
mainInputCount,
|
mainInputCount,
|
||||||
mainOutputCount,
|
mainOutputCount,
|
||||||
nonMainInputCount,
|
nonMainInputCount,
|
||||||
|
isEmbeddedNdvActive.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -497,8 +497,6 @@ export const LOCAL_STORAGE_LOGS_SYNC_SELECTION = 'N8N_LOGS_SYNC_SELECTION_ENABLE
|
|||||||
export const LOCAL_STORAGE_LOGS_PANEL_DETAILS_PANEL = 'N8N_LOGS_DETAILS_PANEL';
|
export const LOCAL_STORAGE_LOGS_PANEL_DETAILS_PANEL = 'N8N_LOGS_DETAILS_PANEL';
|
||||||
export const LOCAL_STORAGE_LOGS_PANEL_DETAILS_PANEL_SUB_NODE = 'N8N_LOGS_DETAILS_PANEL_SUB_NODE';
|
export const LOCAL_STORAGE_LOGS_PANEL_DETAILS_PANEL_SUB_NODE = 'N8N_LOGS_DETAILS_PANEL_SUB_NODE';
|
||||||
export const LOCAL_STORAGE_WORKFLOW_LIST_PREFERENCES_KEY = 'N8N_WORKFLOWS_LIST_PREFERENCES';
|
export const LOCAL_STORAGE_WORKFLOW_LIST_PREFERENCES_KEY = 'N8N_WORKFLOWS_LIST_PREFERENCES';
|
||||||
export const LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS =
|
|
||||||
'N8N_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS';
|
|
||||||
export const LOCAL_STORAGE_EXPERIMENTAL_DOCKED_NODE_SETTINGS =
|
export const LOCAL_STORAGE_EXPERIMENTAL_DOCKED_NODE_SETTINGS =
|
||||||
'N8N_EXPERIMENTAL_DOCKED_NODE_SETTINGS';
|
'N8N_EXPERIMENTAL_DOCKED_NODE_SETTINGS';
|
||||||
export const LOCAL_STORAGE_READ_WHATS_NEW_ARTICLES = 'N8N_READ_WHATS_NEW_ARTICLES';
|
export const LOCAL_STORAGE_READ_WHATS_NEW_ARTICLES = 'N8N_READ_WHATS_NEW_ARTICLES';
|
||||||
@@ -743,6 +741,12 @@ export const KEEP_AUTH_IN_NDV_FOR_NODES = [
|
|||||||
export const MAIN_AUTH_FIELD_NAME = 'authentication';
|
export const MAIN_AUTH_FIELD_NAME = 'authentication';
|
||||||
export const NODE_RESOURCE_FIELD_NAME = 'resource';
|
export const NODE_RESOURCE_FIELD_NAME = 'resource';
|
||||||
|
|
||||||
|
export const CANVAS_ZOOMED_VIEW_EXPERIMENT = {
|
||||||
|
name: 'canvas_zoomed_view',
|
||||||
|
control: 'control',
|
||||||
|
variant: 'variant',
|
||||||
|
};
|
||||||
|
|
||||||
export const NDV_UI_OVERHAUL_EXPERIMENT = {
|
export const NDV_UI_OVERHAUL_EXPERIMENT = {
|
||||||
name: '029_ndv_ui_overhaul',
|
name: '029_ndv_ui_overhaul',
|
||||||
control: 'control',
|
control: 'control',
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { testHealthEndpoint } from '@n8n/rest-api-client/api/templates';
|
|||||||
import {
|
import {
|
||||||
INSECURE_CONNECTION_WARNING,
|
INSECURE_CONNECTION_WARNING,
|
||||||
LOCAL_STORAGE_EXPERIMENTAL_DOCKED_NODE_SETTINGS,
|
LOCAL_STORAGE_EXPERIMENTAL_DOCKED_NODE_SETTINGS,
|
||||||
LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS,
|
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { STORES } from '@n8n/stores';
|
import { STORES } from '@n8n/stores';
|
||||||
import { UserManagementAuthenticationMethod } from '@/Interface';
|
import { UserManagementAuthenticationMethod } from '@/Interface';
|
||||||
@@ -314,15 +313,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
|||||||
moduleSettings.value = fetched;
|
moduleSettings.value = fetched;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* (Experimental) Minimum zoom level of the canvas to render node settings in place of nodes, without opening NDV
|
|
||||||
*/
|
|
||||||
const experimental__minZoomNodeSettingsInCanvas = useLocalStorage(
|
|
||||||
LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS,
|
|
||||||
0,
|
|
||||||
{ writeDefaults: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (Experimental) If set to true, show node settings for a selected node in docked pane
|
* (Experimental) If set to true, show node settings for a selected node in docked pane
|
||||||
*/
|
*/
|
||||||
@@ -388,7 +378,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
|||||||
isAskAiEnabled,
|
isAskAiEnabled,
|
||||||
isAiCreditsEnabled,
|
isAiCreditsEnabled,
|
||||||
aiCreditsQuota,
|
aiCreditsQuota,
|
||||||
experimental__minZoomNodeSettingsInCanvas,
|
|
||||||
experimental__dockedNodeSettingsEnabled,
|
experimental__dockedNodeSettingsEnabled,
|
||||||
partialExecutionVersion,
|
partialExecutionVersion,
|
||||||
reset,
|
reset,
|
||||||
|
|||||||
@@ -484,6 +484,7 @@ describe('calculateNodeSize', () => {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
0,
|
0,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
// width = GRID_SIZE * 5 = 16 * 5 = 80
|
// width = GRID_SIZE * 5 = 16 * 5 = 80
|
||||||
// height = GRID_SIZE * 5 = 16 * 5 = 80
|
// height = GRID_SIZE * 5 = 16 * 5 = 80
|
||||||
@@ -499,7 +500,7 @@ describe('calculateNodeSize', () => {
|
|||||||
// maxVerticalHandles = 3
|
// maxVerticalHandles = 3
|
||||||
// height = 96 + (3 - 2) * 32 = 96 + 32 = 128
|
// height = 96 + (3 - 2) * 32 = 96 + 32 = 128
|
||||||
expect(
|
expect(
|
||||||
calculateNodeSize(false, true, mainInputCount, mainOutputCount, nonMainInputCount),
|
calculateNodeSize(false, true, mainInputCount, mainOutputCount, nonMainInputCount, false),
|
||||||
).toEqual({ width: 272, height: 128 });
|
).toEqual({ width: 272, height: 128 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -507,7 +508,7 @@ describe('calculateNodeSize', () => {
|
|||||||
const nonMainInputCount = 2;
|
const nonMainInputCount = 2;
|
||||||
// width = 80 + 16 + (max(4, 2) - 1) * 16 * 3 = 240
|
// width = 80 + 16 + (max(4, 2) - 1) * 16 * 3 = 240
|
||||||
// height = CONFIGURATION_NODE_SIZE[1] = 16 * 5 = 80
|
// height = CONFIGURATION_NODE_SIZE[1] = 16 * 5 = 80
|
||||||
expect(calculateNodeSize(true, true, 1, 1, nonMainInputCount)).toEqual({
|
expect(calculateNodeSize(true, true, 1, 1, nonMainInputCount, false)).toEqual({
|
||||||
width: 240,
|
width: 240,
|
||||||
height: 80,
|
height: 80,
|
||||||
});
|
});
|
||||||
@@ -519,7 +520,7 @@ describe('calculateNodeSize', () => {
|
|||||||
// width = 96
|
// width = 96
|
||||||
// maxVerticalHandles = 3
|
// maxVerticalHandles = 3
|
||||||
// height = 96 + (3 - 2) * 32 = 128
|
// height = 96 + (3 - 2) * 32 = 128
|
||||||
expect(calculateNodeSize(false, false, mainInputCount, mainOutputCount, 0)).toEqual({
|
expect(calculateNodeSize(false, false, mainInputCount, mainOutputCount, 0, false)).toEqual({
|
||||||
width: 96,
|
width: 96,
|
||||||
height: 128,
|
height: 128,
|
||||||
});
|
});
|
||||||
@@ -530,7 +531,9 @@ describe('calculateNodeSize', () => {
|
|||||||
const mainOutputCount = 4;
|
const mainOutputCount = 4;
|
||||||
// maxVerticalHandles = 6
|
// maxVerticalHandles = 6
|
||||||
// height = 96 + (6 - 2) * 32 = 96 + 128 = 224
|
// height = 96 + (6 - 2) * 32 = 96 + 128 = 224
|
||||||
expect(calculateNodeSize(false, false, mainInputCount, mainOutputCount, 0).height).toBe(224);
|
expect(calculateNodeSize(false, false, mainInputCount, mainOutputCount, 0, false).height).toBe(
|
||||||
|
224,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should respect the minimum width for configurable nodes', () => {
|
it('should respect the minimum width for configurable nodes', () => {
|
||||||
@@ -539,7 +542,7 @@ describe('calculateNodeSize', () => {
|
|||||||
// height = default path, mainInputCount = 1, mainOutputCount = 1
|
// height = default path, mainInputCount = 1, mainOutputCount = 1
|
||||||
// maxVerticalHandles = 1
|
// maxVerticalHandles = 1
|
||||||
// height = 96 + (1 - 2) * 32 = 96 + 0 = 96
|
// height = 96 + (1 - 2) * 32 = 96 + 0 = 96
|
||||||
expect(calculateNodeSize(false, true, 1, 1, nonMainInputCount)).toEqual({
|
expect(calculateNodeSize(false, true, 1, 1, nonMainInputCount, false)).toEqual({
|
||||||
width: 224,
|
width: 224,
|
||||||
height: 96,
|
height: 96,
|
||||||
});
|
});
|
||||||
@@ -548,7 +551,7 @@ describe('calculateNodeSize', () => {
|
|||||||
it('should handle edge case when mainInputCount and mainOutputCount are 0', () => {
|
it('should handle edge case when mainInputCount and mainOutputCount are 0', () => {
|
||||||
// maxVerticalHandles = max(0,0,1) = 1
|
// maxVerticalHandles = max(0,0,1) = 1
|
||||||
// height = 96 + (1 - 2) * 32 = 96 + 0 = 96
|
// height = 96 + (1 - 2) * 32 = 96 + 0 = 96
|
||||||
expect(calculateNodeSize(false, false, 0, 0, 0).height).toBe(96);
|
expect(calculateNodeSize(false, false, 0, 0, 0, false).height).toBe(96);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -617,9 +617,11 @@ export function calculateNodeSize(
|
|||||||
mainInputCount: number,
|
mainInputCount: number,
|
||||||
mainOutputCount: number,
|
mainOutputCount: number,
|
||||||
nonMainInputCount: number,
|
nonMainInputCount: number,
|
||||||
|
isExperimentalNdvActive: boolean,
|
||||||
): { width: number; height: number } {
|
): { width: number; height: number } {
|
||||||
const maxVerticalHandles = Math.max(mainInputCount, mainOutputCount, 1);
|
const maxVerticalHandles = Math.max(mainInputCount, mainOutputCount, 1);
|
||||||
const height = DEFAULT_NODE_SIZE[1] + Math.max(0, maxVerticalHandles - 2) * GRID_SIZE * 2;
|
const height = DEFAULT_NODE_SIZE[1] + Math.max(0, maxVerticalHandles - 2) * GRID_SIZE * 2;
|
||||||
|
const widthScale = isExperimentalNdvActive ? 1.5 : 1;
|
||||||
|
|
||||||
if (isConfigurable) {
|
if (isConfigurable) {
|
||||||
const portCount = Math.max(NODE_MIN_INPUT_ITEMS_COUNT, nonMainInputCount);
|
const portCount = Math.max(NODE_MIN_INPUT_ITEMS_COUNT, nonMainInputCount);
|
||||||
@@ -627,15 +629,16 @@ export function calculateNodeSize(
|
|||||||
return {
|
return {
|
||||||
// Configuration node has extra width so that its centered port aligns to the grid
|
// Configuration node has extra width so that its centered port aligns to the grid
|
||||||
width:
|
width:
|
||||||
CONFIGURATION_NODE_RADIUS * 2 +
|
(CONFIGURATION_NODE_RADIUS * 2 +
|
||||||
GRID_SIZE * ((isConfiguration ? 1 : 0) + (portCount - 1) * 3),
|
GRID_SIZE * ((isConfiguration ? 1 : 0) + (portCount - 1) * 3)) *
|
||||||
|
widthScale,
|
||||||
height: isConfiguration ? CONFIGURATION_NODE_SIZE[1] : height,
|
height: isConfiguration ? CONFIGURATION_NODE_SIZE[1] : height,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isConfiguration) {
|
if (isConfiguration) {
|
||||||
return { width: CONFIGURATION_NODE_SIZE[0], height: CONFIGURATION_NODE_SIZE[1] };
|
return { width: CONFIGURATION_NODE_SIZE[0] * widthScale, height: CONFIGURATION_NODE_SIZE[1] };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { width: DEFAULT_NODE_SIZE[0], height };
|
return { width: DEFAULT_NODE_SIZE[0] * widthScale, height };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user