mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Implement some quick improvements on NDV in canvas experiment (no-changelog) (#16717)
Co-authored-by: Milorad FIlipović <milorad@n8n.io>
This commit is contained in:
@@ -20,6 +20,7 @@ import type {
|
||||
import { useActions } from './NodeCreator/composables/useActions';
|
||||
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useExperimentalNdvStore } from '../canvas/experimental/experimentalNdv.store';
|
||||
|
||||
type Props = {
|
||||
nodeViewScale: number;
|
||||
@@ -44,6 +45,7 @@ const uiStore = useUIStore();
|
||||
const focusPanelStore = useFocusPanelStore();
|
||||
const posthogStore = usePostHog();
|
||||
const i18n = useI18n();
|
||||
const experimentalNdvStore = useExperimentalNdvStore();
|
||||
|
||||
const { getAddedNodesAndConnections } = useActions();
|
||||
|
||||
@@ -125,6 +127,20 @@ function nodeTypeSelected(value: NodeTypeSelectedPayload[]) {
|
||||
@click="focusPanelStore.toggleFocusPanel"
|
||||
/>
|
||||
</KeyboardShortcutTooltip>
|
||||
<n8n-icon-button
|
||||
v-if="experimentalNdvStore.isEnabled"
|
||||
type="tertiary"
|
||||
size="large"
|
||||
icon="expand"
|
||||
@click="experimentalNdvStore.expandAllNodes"
|
||||
/>
|
||||
<n8n-icon-button
|
||||
v-if="experimentalNdvStore.isEnabled"
|
||||
type="tertiary"
|
||||
size="large"
|
||||
icon="compress"
|
||||
@click="experimentalNdvStore.collapseAllNodes"
|
||||
/>
|
||||
</div>
|
||||
<Suspense>
|
||||
<LazyNodeCreator
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useTemplateRef, computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import type {
|
||||
INodeTypeDescription,
|
||||
INodeParameters,
|
||||
@@ -18,10 +18,8 @@ import type {
|
||||
|
||||
import { COMMUNITY_NODES_INSTALLATION_DOCS_URL, CUSTOM_NODES_DOCS_URL } from '@/constants';
|
||||
|
||||
import NodeTitle from '@/components/NodeTitle.vue';
|
||||
import ParameterInputList from '@/components/ParameterInputList.vue';
|
||||
import NodeCredentials from '@/components/NodeCredentials.vue';
|
||||
import NodeSettingsTabs from '@/components/NodeSettingsTabs.vue';
|
||||
import NodeWebhooks from '@/components/NodeWebhooks.vue';
|
||||
import NDVSubConnections from '@/components/NDVSubConnections.vue';
|
||||
import get from 'lodash/get';
|
||||
@@ -42,8 +40,9 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { importCurlEventBus, ndvEventBus } from '@/event-bus';
|
||||
import { ProjectTypes } from '@/types/projects.types';
|
||||
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
|
||||
import { shouldShowParameter } from './canvas/experimental/experimentalNdv.utils';
|
||||
import { useResizeObserver } from '@vueuse/core';
|
||||
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
||||
import { N8nIconButton } from '@n8n/design-system';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -57,8 +56,8 @@ const props = withDefaults(
|
||||
executable: boolean;
|
||||
inputSize: number;
|
||||
activeNode?: INodeUi;
|
||||
canExpand?: boolean;
|
||||
hideConnections?: boolean;
|
||||
isEmbeddedInCanvas?: boolean;
|
||||
noWheel?: boolean;
|
||||
}>(),
|
||||
{
|
||||
foreignCredentials: () => [],
|
||||
@@ -67,8 +66,8 @@ const props = withDefaults(
|
||||
inputSize: 0,
|
||||
blockUI: false,
|
||||
activeNode: undefined,
|
||||
canExpand: false,
|
||||
hideConnections: false,
|
||||
isEmbeddedInCanvas: false,
|
||||
noWheel: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -84,9 +83,10 @@ const emit = defineEmits<{
|
||||
];
|
||||
activate: [];
|
||||
execute: [];
|
||||
expand: [];
|
||||
}>();
|
||||
|
||||
const slots = defineSlots<{ actions?: {} }>();
|
||||
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const ndvStore = useNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
@@ -100,6 +100,17 @@ const i18n = useI18n();
|
||||
const nodeSettingsParameters = useNodeSettingsParameters();
|
||||
const nodeValues = nodeSettingsParameters.nodeValues;
|
||||
|
||||
const nodeParameterWrapper = useTemplateRef('nodeParameterWrapper');
|
||||
const shouldShowStaticScrollbar = ref(false);
|
||||
|
||||
if (props.isEmbeddedInCanvas) {
|
||||
useResizeObserver(nodeParameterWrapper, () => {
|
||||
shouldShowStaticScrollbar.value =
|
||||
(nodeParameterWrapper.value?.scrollHeight ?? 0) >
|
||||
(nodeParameterWrapper.value?.offsetHeight ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
const nodeValid = ref(true);
|
||||
const openPanel = ref<'params' | 'settings'>('params');
|
||||
|
||||
@@ -198,10 +209,12 @@ const parameters = computed(() => {
|
||||
|
||||
const parametersSetting = computed(() => parameters.value.filter((item) => item.isNodeSetting));
|
||||
|
||||
const parametersNoneSetting = computed(() =>
|
||||
const parametersNoneSetting = computed(() => {
|
||||
// The connection hint notice is visually hidden via CSS in NodeDetails.vue when the node has output connections
|
||||
parameters.value.filter((item) => !item.isNodeSetting),
|
||||
);
|
||||
const paramsToShow = parameters.value.filter((item) => !item.isNodeSetting);
|
||||
|
||||
return props.isEmbeddedInCanvas ? parameters.value.filter(shouldShowParameter) : paramsToShow;
|
||||
});
|
||||
|
||||
const isDisplayingCredentials = computed(
|
||||
() =>
|
||||
@@ -735,6 +748,12 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
||||
nodeHelpers.displayParameter(node.value.parameters, credentialTypeDescription, '', node.value)
|
||||
);
|
||||
}
|
||||
|
||||
function handleWheelEvent(event: WheelEvent) {
|
||||
if (event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -742,6 +761,7 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
||||
:class="{
|
||||
'node-settings': true,
|
||||
dragging: dragging,
|
||||
embedded: props.isEmbeddedInCanvas,
|
||||
}"
|
||||
@keydown.stop
|
||||
>
|
||||
@@ -754,8 +774,8 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
||||
:node-type="nodeType"
|
||||
:read-only="isReadOnly"
|
||||
@update:model-value="nameChanged"
|
||||
></NodeTitle>
|
||||
<div v-if="isExecutable || props.canExpand" :class="$style.headerActions">
|
||||
/>
|
||||
<template v-if="isExecutable || slots.actions">
|
||||
<NodeExecuteButton
|
||||
v-if="isExecutable && !blockUI && node && nodeValid"
|
||||
data-test-id="node-execute-button"
|
||||
@@ -768,17 +788,8 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
||||
@stop-execution="onStopExecution"
|
||||
@value-changed="valueChanged"
|
||||
/>
|
||||
<N8nIconButton
|
||||
v-if="props.canExpand"
|
||||
icon="expand"
|
||||
type="secondary"
|
||||
text
|
||||
size="mini"
|
||||
icon-size="large"
|
||||
aria-label="Expand"
|
||||
@click="emit('expand')"
|
||||
/>
|
||||
</div>
|
||||
<slot name="actions" />
|
||||
</template>
|
||||
</div>
|
||||
<NodeSettingsTabs
|
||||
v-if="node && nodeValid"
|
||||
@@ -830,7 +841,17 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div v-if="node && nodeValid" class="node-parameters-wrapper" data-test-id="node-parameters">
|
||||
<div
|
||||
v-if="node && nodeValid"
|
||||
ref="nodeParameterWrapper"
|
||||
:class="[
|
||||
'node-parameters-wrapper',
|
||||
shouldShowStaticScrollbar ? 'with-static-scrollbar' : '',
|
||||
noWheel && shouldShowStaticScrollbar ? 'nowheel' : '',
|
||||
]"
|
||||
data-test-id="node-parameters"
|
||||
@wheel="noWheel ? handleWheelEvent : undefined"
|
||||
>
|
||||
<n8n-notice
|
||||
v-if="hasForeignCredential && !isHomeProjectTeam"
|
||||
:content="
|
||||
@@ -851,11 +872,13 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
||||
:is-read-only="isReadOnly"
|
||||
:hidden-issues-inputs="hiddenIssuesInputs"
|
||||
path="parameters"
|
||||
:node="props.activeNode"
|
||||
@value-changed="valueChanged"
|
||||
@activate="onWorkflowActivate"
|
||||
@parameter-blur="onParameterBlur"
|
||||
>
|
||||
<NodeCredentials
|
||||
v-if="!isEmbeddedInCanvas"
|
||||
:node="node"
|
||||
:readonly="isReadOnly"
|
||||
:show-all="true"
|
||||
@@ -920,7 +943,7 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
||||
</div>
|
||||
</div>
|
||||
<NDVSubConnections
|
||||
v-if="node && !props.hideConnections"
|
||||
v-if="node && !props.isEmbeddedInCanvas"
|
||||
ref="subConnections"
|
||||
:root-node="node"
|
||||
@switch-selected-node="onSwitchSelectedNode"
|
||||
@@ -935,12 +958,6 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
||||
background-color: var(--color-background-base);
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
display: flex;
|
||||
gap: var(--spacing-4xs);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.warningIcon {
|
||||
color: var(--color-text-lighter);
|
||||
font-size: var(--font-size-2xl);
|
||||
@@ -976,6 +993,10 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
||||
}
|
||||
}
|
||||
|
||||
&.embedded .header-side-menu {
|
||||
padding: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.node-is-not-valid {
|
||||
height: 75%;
|
||||
padding: 10px;
|
||||
@@ -993,6 +1014,28 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.embedded .node-parameters-wrapper {
|
||||
padding: 0 var(--spacing-xs) var(--spacing-xs) var(--spacing-xs);
|
||||
}
|
||||
|
||||
&.embedded .node-parameters-wrapper.with-static-scrollbar {
|
||||
padding: 0 var(--spacing-2xs) var(--spacing-xs) var(--spacing-xs);
|
||||
|
||||
@supports not (selector(::-webkit-scrollbar)) {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
@supports selector(::-webkit-scrollbar) {
|
||||
&::-webkit-scrollbar {
|
||||
width: var(--spacing-2xs);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: var(--spacing-2xs);
|
||||
background: var(--color-foreground-dark);
|
||||
border: var(--spacing-5xs) solid white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0px 6px 16px rgba(255, 74, 51, 0.15);
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
import { ADD_FORM_NOTICE, deepCopy, NodeHelpers } from 'n8n-workflow';
|
||||
import { computed, defineAsyncComponent, onErrorCaptured, ref, watch, type WatchSource } from 'vue';
|
||||
|
||||
import type { IUpdateInformation } from '@/Interface';
|
||||
import type { INodeUi, IUpdateInformation } from '@/Interface';
|
||||
|
||||
import AssignmentCollection from '@/components/AssignmentCollection/AssignmentCollection.vue';
|
||||
import ButtonParameter from '@/components/ButtonParameter/ButtonParameter.vue';
|
||||
@@ -64,6 +64,7 @@ const LazyCollectionParameter = defineAsyncComponent(
|
||||
const showIssuesInLabelFor = ['fixedCollection'];
|
||||
|
||||
type Props = {
|
||||
node?: INodeUi;
|
||||
nodeValues: INodeParameters;
|
||||
parameters: INodeProperties[];
|
||||
path?: string;
|
||||
@@ -120,6 +121,8 @@ const nodeType = computed(() => {
|
||||
return null;
|
||||
});
|
||||
|
||||
const node = computed(() => props.node ?? ndvStore.activeNode);
|
||||
|
||||
const filteredParameters = computedWithControl(
|
||||
[() => props.parameters, () => props.nodeValues] as WatchSource[],
|
||||
() => {
|
||||
@@ -127,22 +130,20 @@ const filteredParameters = computedWithControl(
|
||||
displayNodeParameter(parameter),
|
||||
);
|
||||
|
||||
const activeNode = ndvStore.activeNode;
|
||||
|
||||
if (activeNode && activeNode.type === FORM_TRIGGER_NODE_TYPE) {
|
||||
return updateFormTriggerParameters(parameters, activeNode.name);
|
||||
if (node.value && node.value.type === FORM_TRIGGER_NODE_TYPE) {
|
||||
return updateFormTriggerParameters(parameters, node.value.name);
|
||||
}
|
||||
|
||||
if (activeNode && activeNode.type === FORM_NODE_TYPE) {
|
||||
return updateFormParameters(parameters, activeNode.name);
|
||||
if (node.value && node.value.type === FORM_NODE_TYPE) {
|
||||
return updateFormParameters(parameters, node.value.name);
|
||||
}
|
||||
|
||||
if (
|
||||
activeNode &&
|
||||
activeNode.type === WAIT_NODE_TYPE &&
|
||||
activeNode.parameters.resume === 'form'
|
||||
node.value &&
|
||||
node.value.type === WAIT_NODE_TYPE &&
|
||||
node.value.parameters.resume === 'form'
|
||||
) {
|
||||
return updateWaitParameters(parameters, activeNode.name);
|
||||
return updateWaitParameters(parameters, node.value.name);
|
||||
}
|
||||
|
||||
return parameters;
|
||||
@@ -153,8 +154,6 @@ const filteredParameterNames = computed(() => {
|
||||
return filteredParameters.value.map((parameter) => parameter.name);
|
||||
});
|
||||
|
||||
const node = computed(() => ndvStore.activeNode);
|
||||
|
||||
const nodeAuthFields = computed(() => {
|
||||
return getNodeAuthFields(nodeType.value);
|
||||
});
|
||||
|
||||
@@ -55,6 +55,7 @@ import Edge from './elements/edges/CanvasEdge.vue';
|
||||
import Node from './elements/nodes/CanvasNode.vue';
|
||||
import { useViewportAutoAdjust } from './composables/useViewportAutoAdjust';
|
||||
import { isOutsideSelected } from '@/utils/htmlUtils';
|
||||
import { useExperimentalNdvStore } from './experimental/experimentalNdv.store';
|
||||
|
||||
const $style = useCssModule();
|
||||
|
||||
@@ -174,6 +175,8 @@ const {
|
||||
} = useCanvasTraversal(vueFlow);
|
||||
const { layout } = useCanvasLayout({ id: props.id });
|
||||
|
||||
const experimentalNdvStore = useExperimentalNdvStore();
|
||||
|
||||
const isPaneReady = ref(false);
|
||||
|
||||
const classes = computed(() => ({
|
||||
@@ -854,7 +857,7 @@ provide(CanvasKey, {
|
||||
snap-to-grid
|
||||
:snap-grid="[GRID_SIZE, GRID_SIZE]"
|
||||
:min-zoom="0"
|
||||
:max-zoom="4"
|
||||
:max-zoom="experimentalNdvStore.isEnabled ? experimentalNdvStore.maxCanvasZoom : 4"
|
||||
:selection-key-code="selectionKeyCode"
|
||||
:zoom-activation-key-code="panningKeyCode"
|
||||
:pan-activation-key-code="panningKeyCode"
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { CanvasEventBusEvents } from '@/types';
|
||||
import { useVueFlow } from '@vue-flow/core';
|
||||
import { throttledRef } from '@vueuse/core';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import ExperimentalNodeDetailsDrawer from './components/ExperimentalNodeDetailsDrawer.vue';
|
||||
import ExperimentalNodeDetailsDrawer from './experimental/components/ExperimentalNodeDetailsDrawer.vue';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useVueFlow } from '@vue-flow/core';
|
||||
import { useDebounce } from '@vueuse/core';
|
||||
import { computed, type ComputedRef } from 'vue';
|
||||
|
||||
export function useNodeSettingsInCanvas(): ComputedRef<number | undefined> {
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
if (
|
||||
Number.isNaN(settingsStore.experimental__minZoomNodeSettingsInCanvas) ||
|
||||
settingsStore.experimental__minZoomNodeSettingsInCanvas <= 0
|
||||
) {
|
||||
return computed(() => undefined);
|
||||
}
|
||||
|
||||
const { editableWorkflow } = useCanvasOperations();
|
||||
const viewFlow = useVueFlow({ id: editableWorkflow.value.id });
|
||||
const zoom = computed(() => viewFlow.viewport.value.zoom);
|
||||
const debouncedZoom = useDebounce(zoom, 100);
|
||||
|
||||
return computed(() =>
|
||||
debouncedZoom.value > settingsStore.experimental__minZoomNodeSettingsInCanvas
|
||||
? debouncedZoom.value
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import { useI18n } from '@n8n/i18n';
|
||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
import type { CanvasNodeDefaultRender } from '@/types';
|
||||
import { useCanvas } from '@/composables/useCanvas';
|
||||
import { useNodeSettingsInCanvas } from '@/components/canvas/composables/useNodeSettingsInCanvas';
|
||||
import { calculateNodeSize } from '@/utils/nodeViewUtils';
|
||||
import ExperimentalCanvasNodeSettings from '../../../components/ExperimentalCanvasNodeSettings.vue';
|
||||
import ExperimentalInPlaceNodeSettings from '@/components/canvas/experimental/components/ExperimentalEmbeddedNodeDetails.vue';
|
||||
import { useExperimentalNdvStore } from '@/components/canvas/experimental/experimentalNdv.store';
|
||||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
@@ -45,7 +45,7 @@ const { mainOutputs, mainOutputConnections, mainInputs, mainInputConnections, no
|
||||
|
||||
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
|
||||
|
||||
const nodeSettingsZoom = useNodeSettingsInCanvas();
|
||||
const experimentalNdvStore = useExperimentalNdvStore();
|
||||
|
||||
const classes = computed(() => {
|
||||
return {
|
||||
@@ -61,7 +61,6 @@ const classes = computed(() => {
|
||||
[$style.configuration]: renderOptions.value.configuration,
|
||||
[$style.trigger]: renderOptions.value.trigger,
|
||||
[$style.warning]: renderOptions.value.dirtiness !== undefined,
|
||||
[$style.settingsView]: nodeSettingsZoom.value !== undefined,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -81,7 +80,6 @@ const styles = computed(() => ({
|
||||
'--canvas-node--width': `${nodeSize.value.width}px`,
|
||||
'--canvas-node--height': `${nodeSize.value.height}px`,
|
||||
'--node-icon-size': `${iconSize.value}px`,
|
||||
...(nodeSettingsZoom.value === undefined ? {} : { '--zoom': nodeSettingsZoom.value }),
|
||||
}));
|
||||
|
||||
const dataTestId = computed(() => {
|
||||
@@ -133,35 +131,39 @@ function onActivate(event: MouseEvent) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ExperimentalInPlaceNodeSettings
|
||||
v-if="experimentalNdvStore.isActive(viewport.zoom)"
|
||||
:node-id="id"
|
||||
:class="classes"
|
||||
:style="styles"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
:class="classes"
|
||||
:style="styles"
|
||||
:data-test-id="dataTestId"
|
||||
@contextmenu="openContextMenu"
|
||||
@dblclick.stop="onActivate"
|
||||
>
|
||||
<ExperimentalCanvasNodeSettings v-if="nodeSettingsZoom !== undefined" :node-id="id" />
|
||||
<template v-else>
|
||||
<CanvasNodeTooltip v-if="renderOptions.tooltip" :visible="showTooltip" />
|
||||
<NodeIcon
|
||||
:icon-source="iconSource"
|
||||
:size="iconSize"
|
||||
:shrink="false"
|
||||
:disabled="isDisabled"
|
||||
:class="$style.icon"
|
||||
/>
|
||||
<CanvasNodeDisabledStrikeThrough v-if="isStrikethroughVisible" />
|
||||
<div :class="$style.description">
|
||||
<div v-if="label" :class="$style.label">
|
||||
{{ label }}
|
||||
</div>
|
||||
<div v-if="isDisabled" :class="$style.disabledLabel">
|
||||
({{ i18n.baseText('node.disabled') }})
|
||||
</div>
|
||||
<div v-if="subtitle" :class="$style.subtitle">{{ subtitle }}</div>
|
||||
<CanvasNodeTooltip v-if="renderOptions.tooltip" :visible="showTooltip" />
|
||||
<NodeIcon
|
||||
:icon-source="iconSource"
|
||||
:size="iconSize"
|
||||
:shrink="false"
|
||||
:disabled="isDisabled"
|
||||
:class="$style.icon"
|
||||
/>
|
||||
<CanvasNodeDisabledStrikeThrough v-if="isStrikethroughVisible" />
|
||||
<div :class="$style.description">
|
||||
<div v-if="label" :class="$style.label">
|
||||
{{ label }}
|
||||
</div>
|
||||
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
|
||||
</template>
|
||||
<div v-if="isDisabled" :class="$style.disabledLabel">
|
||||
({{ i18n.baseText('node.disabled') }})
|
||||
</div>
|
||||
<div v-if="subtitle" :class="$style.subtitle">{{ subtitle }}</div>
|
||||
</div>
|
||||
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -188,20 +190,6 @@ function onActivate(event: MouseEvent) {
|
||||
var(--border-radius-large) var(--trigger-node--border-radius);
|
||||
}
|
||||
|
||||
&.settingsView {
|
||||
height: calc(var(--canvas-node--height) * 2.4) !important;
|
||||
width: calc(var(--canvas-node--width) * 1.6) !important;
|
||||
align-items: flex-start;
|
||||
justify-content: stretch;
|
||||
overflow: auto;
|
||||
border-radius: var(--border-radius-large) !important;
|
||||
|
||||
& > * {
|
||||
zoom: calc(1 / var(--zoom, 1));
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Node types
|
||||
*/
|
||||
@@ -272,32 +260,47 @@ function onActivate(event: MouseEvent) {
|
||||
}
|
||||
|
||||
&.success {
|
||||
border-color: var(--color-canvas-node-success-border-color, var(--color-success));
|
||||
--canvas-node--border-color: var(
|
||||
--color-canvas-node-success-border-color,
|
||||
var(--color-success)
|
||||
);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
border-color: var(--color-warning);
|
||||
--canvas-node--border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: var(--color-canvas-node-error-border-color, var(--color-danger));
|
||||
--canvas-node--border-color: var(--color-canvas-node-error-border-color, var(--color-danger));
|
||||
}
|
||||
|
||||
&.pinned {
|
||||
border-color: var(--color-canvas-node-pinned-border-color, var(--color-node-pinned-border));
|
||||
--canvas-node--border-color: var(
|
||||
--color-canvas-node-pinned-border-color,
|
||||
var(--color-node-pinned-border)
|
||||
);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
border-color: var(--color-canvas-node-disabled-border-color, var(--color-foreground-base));
|
||||
--canvas-node--border-color: var(
|
||||
--color-canvas-node-disabled-border-color,
|
||||
var(--color-foreground-base)
|
||||
);
|
||||
}
|
||||
|
||||
&.running {
|
||||
background-color: var(--color-node-executing-background);
|
||||
border-color: var(--color-canvas-node-running-border-color, var(--color-node-running-border));
|
||||
--canvas-node--border-color: var(
|
||||
--color-canvas-node-running-border-color,
|
||||
var(--color-node-running-border)
|
||||
);
|
||||
}
|
||||
|
||||
&.waiting {
|
||||
border-color: var(--color-canvas-node-waiting-border-color, var(--color-secondary));
|
||||
--canvas-node--border-color: var(
|
||||
--color-canvas-node-waiting-border-color,
|
||||
var(--color-secondary)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr
|
||||
data-test-id="canvas-configurable-node"
|
||||
style="--canvas-node--width: 240px; --canvas-node--height: 100px; --node-icon-size: 40px;"
|
||||
>
|
||||
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="n8n-node-icon icon icon"
|
||||
@@ -43,7 +42,6 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -53,7 +51,6 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur
|
||||
data-test-id="canvas-configurable-node"
|
||||
style="--canvas-node--width: 240px; --canvas-node--height: 75px; --node-icon-size: 30px;"
|
||||
>
|
||||
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="n8n-node-icon icon icon"
|
||||
@@ -90,7 +87,6 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -100,7 +96,6 @@ exports[`CanvasNodeDefault > configuration > should render configuration node co
|
||||
data-test-id="canvas-configuration-node"
|
||||
style="--canvas-node--width: 80px; --canvas-node--height: 80px; --node-icon-size: 30px;"
|
||||
>
|
||||
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="n8n-node-icon icon icon"
|
||||
@@ -137,7 +132,6 @@ exports[`CanvasNodeDefault > configuration > should render configuration node co
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -147,7 +141,6 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
|
||||
data-test-id="canvas-default-node"
|
||||
style="--canvas-node--width: 100px; --canvas-node--height: 100px; --node-icon-size: 40px;"
|
||||
>
|
||||
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="n8n-node-icon icon icon"
|
||||
@@ -184,7 +177,6 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -194,7 +186,6 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`]
|
||||
data-test-id="canvas-trigger-node"
|
||||
style="--canvas-node--width: 100px; --canvas-node--height: 100px; --node-icon-size: 40px;"
|
||||
>
|
||||
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="n8n-node-icon icon icon"
|
||||
@@ -231,6 +222,5 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`]
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
import NodeSettings from '@/components/NodeSettings.vue';
|
||||
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||
import { type IUpdateInformation } from '@/Interface';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const { nodeId, canOpenNdv } = defineProps<{ nodeId: string; canOpenNdv?: boolean }>();
|
||||
const { nodeId, noWheel } = defineProps<{ nodeId: string; noWheel?: boolean }>();
|
||||
|
||||
defineSlots<{ actions?: {} }>();
|
||||
|
||||
const settingsEventBus = createEventBus();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const { setActiveNodeName } = useNDVStore();
|
||||
const { renameNode } = useCanvasOperations();
|
||||
|
||||
const activeNode = computed(() => workflowsStore.getNodeById(nodeId));
|
||||
@@ -24,12 +24,6 @@ const activeNodeType = computed(() => {
|
||||
return null;
|
||||
});
|
||||
|
||||
function handleOpenNdv() {
|
||||
if (activeNode.value) {
|
||||
setActiveNodeName(activeNode.value.name);
|
||||
}
|
||||
}
|
||||
|
||||
function handleValueChanged(parameterData: IUpdateInformation) {
|
||||
if (parameterData.name === 'name' && parameterData.oldValue) {
|
||||
void renameNode(parameterData.oldValue as string, parameterData.value as string);
|
||||
@@ -39,7 +33,6 @@ function handleValueChanged(parameterData: IUpdateInformation) {
|
||||
|
||||
<template>
|
||||
<NodeSettings
|
||||
:can-expand="canOpenNdv"
|
||||
:event-bus="settingsEventBus"
|
||||
:dragging="false"
|
||||
:active-node="activeNode"
|
||||
@@ -50,8 +43,12 @@ function handleValueChanged(parameterData: IUpdateInformation) {
|
||||
:block-u-i="false"
|
||||
:executable="false"
|
||||
:input-size="0"
|
||||
hide-connections
|
||||
@expand="handleOpenNdv"
|
||||
is-embedded-in-canvas
|
||||
:no-wheel="noWheel"
|
||||
@value-changed="handleValueChanged"
|
||||
/>
|
||||
>
|
||||
<template #actions>
|
||||
<slot name="actions" />
|
||||
</template>
|
||||
</NodeSettings>
|
||||
</template>
|
||||
@@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
|
||||
import { onBeforeUnmount, ref, computed } from 'vue';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useExperimentalNdvStore } from '../experimentalNdv.store';
|
||||
import NodeTitle from '@/components/NodeTitle.vue';
|
||||
import { N8nIcon, N8nIconButton } from '@n8n/design-system';
|
||||
import { useVueFlow } from '@vue-flow/core';
|
||||
import { watchOnce } from '@vueuse/core';
|
||||
|
||||
const { nodeId } = defineProps<{ nodeId: string }>();
|
||||
|
||||
const experimentalNdvStore = useExperimentalNdvStore();
|
||||
const isExpanded = computed(() => !experimentalNdvStore.collapsedNodes[nodeId]);
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const node = computed(() => workflowsStore.getNodeById(nodeId) ?? null);
|
||||
const nodeType = computed(() => {
|
||||
if (node.value) {
|
||||
return nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const vf = useVueFlow(workflowsStore.workflowId);
|
||||
|
||||
const isMoving = ref(false);
|
||||
|
||||
const moveStartListener = vf.onMoveStart(() => {
|
||||
isMoving.value = true;
|
||||
});
|
||||
|
||||
const moveEndListener = vf.onMoveEnd(() => {
|
||||
isMoving.value = false;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
moveStartListener.off();
|
||||
moveEndListener.off();
|
||||
});
|
||||
|
||||
const isVisible = computed(() =>
|
||||
vf.isNodeIntersecting(
|
||||
{ id: nodeId },
|
||||
{
|
||||
x: -vf.viewport.value.x / vf.viewport.value.zoom,
|
||||
y: -vf.viewport.value.y / vf.viewport.value.zoom,
|
||||
width: vf.viewportRef.value?.offsetWidth ?? 0,
|
||||
height: vf.viewportRef.value?.offsetHeight ?? 0,
|
||||
},
|
||||
),
|
||||
);
|
||||
const isOnceVisible = ref(isVisible.value);
|
||||
|
||||
watchOnce(isVisible, (visible) => {
|
||||
isOnceVisible.value = isOnceVisible.value || visible;
|
||||
});
|
||||
|
||||
function handleToggleExpand() {
|
||||
experimentalNdvStore.setNodeExpanded(nodeId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
:class="[$style.component, isExpanded ? $style.expanded : $style.collapsed]"
|
||||
:style="{ '--zoom': `${1 / experimentalNdvStore.maxCanvasZoom}` }"
|
||||
>
|
||||
<template v-if="isOnceVisible">
|
||||
<ExperimentalCanvasNodeSettings
|
||||
v-if="isExpanded"
|
||||
:node-id="nodeId"
|
||||
:class="$style.settingsView"
|
||||
:no-wheel="
|
||||
!isMoving /* to not interrupt panning while allowing scroll of the settings pane, allow wheel event while panning */
|
||||
"
|
||||
>
|
||||
<template #actions>
|
||||
<N8nIconButton
|
||||
icon="compress"
|
||||
type="secondary"
|
||||
text
|
||||
size="mini"
|
||||
icon-size="large"
|
||||
aria-label="Toggle expand"
|
||||
@click="handleToggleExpand"
|
||||
/>
|
||||
</template>
|
||||
</ExperimentalCanvasNodeSettings>
|
||||
<div v-else role="button " :class="$style.collapsedContent" @click="handleToggleExpand">
|
||||
<NodeTitle
|
||||
v-if="node"
|
||||
class="node-name"
|
||||
:model-value="node.name"
|
||||
:node-type="nodeType"
|
||||
read-only
|
||||
/>
|
||||
<N8nIcon icon="expand" size="large" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
:root .component {
|
||||
position: relative;
|
||||
align-items: flex-start;
|
||||
justify-content: stretch;
|
||||
overflow: visible;
|
||||
border-width: 0 !important;
|
||||
outline: none;
|
||||
box-shadow: none !important;
|
||||
background-color: transparent;
|
||||
width: calc(var(--canvas-node--width) * 1.5);
|
||||
|
||||
&.expanded {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
height: 50px;
|
||||
margin-block: calc(var(--canvas-node--width) * 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
:root :global(.vue-flow__node):has(.component) {
|
||||
z-index: 10;
|
||||
|
||||
:global(.selected) & {
|
||||
z-index: 11;
|
||||
}
|
||||
}
|
||||
|
||||
:root .collapsedContent,
|
||||
:root .settingsView {
|
||||
border-radius: var(--border-radius-base);
|
||||
border: 1px solid var(--canvas-node--border-color, var(--color-foreground-xdark));
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
||||
:global(.selected) & {
|
||||
box-shadow: 0 0 0 4px var(--color-canvas-selected-transparent);
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
& > * {
|
||||
zoom: var(--zoom);
|
||||
}
|
||||
}
|
||||
|
||||
:root .settingsView {
|
||||
height: auto;
|
||||
max-height: min(200%, 300px);
|
||||
top: -10%;
|
||||
min-height: 120%;
|
||||
}
|
||||
|
||||
.collapsedContent {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-s);
|
||||
background-color: white;
|
||||
padding: var(--spacing-2xs);
|
||||
background-color: var(--color-background-xlight);
|
||||
color: var(--color-text-base);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import { type CanvasNode } from '@/types';
|
||||
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
|
||||
import { N8nText } from '@n8n/design-system';
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
|
||||
const { selectedNodes } = defineProps<{ selectedNodes: CanvasNode[] }>();
|
||||
|
||||
@@ -14,6 +15,13 @@ const content = computed(() =>
|
||||
: undefined,
|
||||
);
|
||||
const lastContent = ref<string | CanvasNode | undefined>();
|
||||
const { setActiveNodeName } = useNDVStore();
|
||||
|
||||
function handleOpenNdv() {
|
||||
if (typeof content.value === 'object' && content.value.data) {
|
||||
setActiveNodeName(content.value.data.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Sync lastContent to be "last defined content" (for drawer animation)
|
||||
watch(
|
||||
@@ -36,8 +44,19 @@ watch(
|
||||
v-else-if="lastContent !== undefined"
|
||||
:key="lastContent.id"
|
||||
:node-id="lastContent.id"
|
||||
can-open-ndv
|
||||
/>
|
||||
>
|
||||
<template #actions>
|
||||
<N8nIconButton
|
||||
icon="expand"
|
||||
type="secondary"
|
||||
text
|
||||
size="mini"
|
||||
icon-size="large"
|
||||
aria-label="Expand"
|
||||
@click="handleOpenNdv"
|
||||
/>
|
||||
</template>
|
||||
</ExperimentalCanvasNodeSettings>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
|
||||
export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
|
||||
const workflowStore = useWorkflowsStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const isEnabled = computed(
|
||||
() =>
|
||||
!Number.isNaN(settingsStore.experimental__minZoomNodeSettingsInCanvas) &&
|
||||
settingsStore.experimental__minZoomNodeSettingsInCanvas > 0,
|
||||
);
|
||||
const maxCanvasZoom = computed(() =>
|
||||
isEnabled.value ? settingsStore.experimental__minZoomNodeSettingsInCanvas : 4,
|
||||
);
|
||||
|
||||
const collapsedNodes = shallowRef<Partial<Record<string, boolean>>>({});
|
||||
|
||||
function setNodeExpanded(nodeId: string, isExpanded?: boolean) {
|
||||
collapsedNodes.value = {
|
||||
...collapsedNodes.value,
|
||||
[nodeId]: isExpanded ?? !collapsedNodes.value[nodeId],
|
||||
};
|
||||
}
|
||||
|
||||
function collapseAllNodes() {
|
||||
collapsedNodes.value = workflowStore.allNodes.reduce<Partial<Record<string, boolean>>>(
|
||||
(acc, node) => {
|
||||
acc[node.id] = true;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
function expandAllNodes() {
|
||||
collapsedNodes.value = {};
|
||||
}
|
||||
|
||||
function isActive(canvasZoom: number) {
|
||||
return isEnabled.value && canvasZoom === maxCanvasZoom.value;
|
||||
}
|
||||
|
||||
return {
|
||||
isEnabled,
|
||||
maxCanvasZoom,
|
||||
collapsedNodes: computed(() => collapsedNodes.value),
|
||||
isActive,
|
||||
setNodeExpanded,
|
||||
expandAllNodes,
|
||||
collapseAllNodes,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export function shouldShowParameter(item: INodeProperties): boolean {
|
||||
return item.name.match(/resource|authentication|operation/i) === null;
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
faCogs,
|
||||
faComment,
|
||||
faComments,
|
||||
faCompress,
|
||||
faClipboardList,
|
||||
faClock,
|
||||
faClone,
|
||||
@@ -247,6 +248,7 @@ export const FontAwesomePlugin: Plugin = {
|
||||
addIcon(faCogs);
|
||||
addIcon(faComment);
|
||||
addIcon(faComments);
|
||||
addIcon(faCompress);
|
||||
addIcon(faClipboardList);
|
||||
addIcon(faClock);
|
||||
addIcon(faClone);
|
||||
|
||||
Reference in New Issue
Block a user