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 { useActions } from './NodeCreator/composables/useActions';
|
||||||
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import { useExperimentalNdvStore } from '../canvas/experimental/experimentalNdv.store';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
nodeViewScale: number;
|
nodeViewScale: number;
|
||||||
@@ -44,6 +45,7 @@ const uiStore = useUIStore();
|
|||||||
const focusPanelStore = useFocusPanelStore();
|
const focusPanelStore = useFocusPanelStore();
|
||||||
const posthogStore = usePostHog();
|
const posthogStore = usePostHog();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const experimentalNdvStore = useExperimentalNdvStore();
|
||||||
|
|
||||||
const { getAddedNodesAndConnections } = useActions();
|
const { getAddedNodesAndConnections } = useActions();
|
||||||
|
|
||||||
@@ -125,6 +127,20 @@ function nodeTypeSelected(value: NodeTypeSelectedPayload[]) {
|
|||||||
@click="focusPanelStore.toggleFocusPanel"
|
@click="focusPanelStore.toggleFocusPanel"
|
||||||
/>
|
/>
|
||||||
</KeyboardShortcutTooltip>
|
</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>
|
</div>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<LazyNodeCreator
|
<LazyNodeCreator
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { useTemplateRef, computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import type {
|
import type {
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
INodeParameters,
|
INodeParameters,
|
||||||
@@ -18,10 +18,8 @@ import type {
|
|||||||
|
|
||||||
import { COMMUNITY_NODES_INSTALLATION_DOCS_URL, CUSTOM_NODES_DOCS_URL } from '@/constants';
|
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 ParameterInputList from '@/components/ParameterInputList.vue';
|
||||||
import NodeCredentials from '@/components/NodeCredentials.vue';
|
import NodeCredentials from '@/components/NodeCredentials.vue';
|
||||||
import NodeSettingsTabs from '@/components/NodeSettingsTabs.vue';
|
|
||||||
import NodeWebhooks from '@/components/NodeWebhooks.vue';
|
import NodeWebhooks from '@/components/NodeWebhooks.vue';
|
||||||
import NDVSubConnections from '@/components/NDVSubConnections.vue';
|
import NDVSubConnections from '@/components/NDVSubConnections.vue';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
@@ -42,8 +40,9 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
|||||||
import { importCurlEventBus, ndvEventBus } from '@/event-bus';
|
import { importCurlEventBus, ndvEventBus } from '@/event-bus';
|
||||||
import { ProjectTypes } from '@/types/projects.types';
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
|
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
|
||||||
|
import { shouldShowParameter } from './canvas/experimental/experimentalNdv.utils';
|
||||||
|
import { useResizeObserver } from '@vueuse/core';
|
||||||
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
||||||
import { N8nIconButton } from '@n8n/design-system';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -57,8 +56,8 @@ const props = withDefaults(
|
|||||||
executable: boolean;
|
executable: boolean;
|
||||||
inputSize: number;
|
inputSize: number;
|
||||||
activeNode?: INodeUi;
|
activeNode?: INodeUi;
|
||||||
canExpand?: boolean;
|
isEmbeddedInCanvas?: boolean;
|
||||||
hideConnections?: boolean;
|
noWheel?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
foreignCredentials: () => [],
|
foreignCredentials: () => [],
|
||||||
@@ -67,8 +66,8 @@ const props = withDefaults(
|
|||||||
inputSize: 0,
|
inputSize: 0,
|
||||||
blockUI: false,
|
blockUI: false,
|
||||||
activeNode: undefined,
|
activeNode: undefined,
|
||||||
canExpand: false,
|
isEmbeddedInCanvas: false,
|
||||||
hideConnections: false,
|
noWheel: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -84,9 +83,10 @@ const emit = defineEmits<{
|
|||||||
];
|
];
|
||||||
activate: [];
|
activate: [];
|
||||||
execute: [];
|
execute: [];
|
||||||
expand: [];
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const slots = defineSlots<{ actions?: {} }>();
|
||||||
|
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
@@ -100,6 +100,17 @@ const i18n = useI18n();
|
|||||||
const nodeSettingsParameters = useNodeSettingsParameters();
|
const nodeSettingsParameters = useNodeSettingsParameters();
|
||||||
const nodeValues = nodeSettingsParameters.nodeValues;
|
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 nodeValid = ref(true);
|
||||||
const openPanel = ref<'params' | 'settings'>('params');
|
const openPanel = ref<'params' | 'settings'>('params');
|
||||||
|
|
||||||
@@ -198,10 +209,12 @@ const parameters = computed(() => {
|
|||||||
|
|
||||||
const parametersSetting = computed(() => parameters.value.filter((item) => item.isNodeSetting));
|
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
|
// 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(
|
const isDisplayingCredentials = computed(
|
||||||
() =>
|
() =>
|
||||||
@@ -735,6 +748,12 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
|||||||
nodeHelpers.displayParameter(node.value.parameters, credentialTypeDescription, '', node.value)
|
nodeHelpers.displayParameter(node.value.parameters, credentialTypeDescription, '', node.value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleWheelEvent(event: WheelEvent) {
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -742,6 +761,7 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
|||||||
:class="{
|
:class="{
|
||||||
'node-settings': true,
|
'node-settings': true,
|
||||||
dragging: dragging,
|
dragging: dragging,
|
||||||
|
embedded: props.isEmbeddedInCanvas,
|
||||||
}"
|
}"
|
||||||
@keydown.stop
|
@keydown.stop
|
||||||
>
|
>
|
||||||
@@ -754,8 +774,8 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
|||||||
:node-type="nodeType"
|
:node-type="nodeType"
|
||||||
:read-only="isReadOnly"
|
:read-only="isReadOnly"
|
||||||
@update:model-value="nameChanged"
|
@update:model-value="nameChanged"
|
||||||
></NodeTitle>
|
/>
|
||||||
<div v-if="isExecutable || props.canExpand" :class="$style.headerActions">
|
<template v-if="isExecutable || slots.actions">
|
||||||
<NodeExecuteButton
|
<NodeExecuteButton
|
||||||
v-if="isExecutable && !blockUI && node && nodeValid"
|
v-if="isExecutable && !blockUI && node && nodeValid"
|
||||||
data-test-id="node-execute-button"
|
data-test-id="node-execute-button"
|
||||||
@@ -768,17 +788,8 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
|||||||
@stop-execution="onStopExecution"
|
@stop-execution="onStopExecution"
|
||||||
@value-changed="valueChanged"
|
@value-changed="valueChanged"
|
||||||
/>
|
/>
|
||||||
<N8nIconButton
|
<slot name="actions" />
|
||||||
v-if="props.canExpand"
|
</template>
|
||||||
icon="expand"
|
|
||||||
type="secondary"
|
|
||||||
text
|
|
||||||
size="mini"
|
|
||||||
icon-size="large"
|
|
||||||
aria-label="Expand"
|
|
||||||
@click="emit('expand')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<NodeSettingsTabs
|
<NodeSettingsTabs
|
||||||
v-if="node && nodeValid"
|
v-if="node && nodeValid"
|
||||||
@@ -830,7 +841,17 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
|||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</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
|
<n8n-notice
|
||||||
v-if="hasForeignCredential && !isHomeProjectTeam"
|
v-if="hasForeignCredential && !isHomeProjectTeam"
|
||||||
:content="
|
:content="
|
||||||
@@ -851,11 +872,13 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
|||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:hidden-issues-inputs="hiddenIssuesInputs"
|
:hidden-issues-inputs="hiddenIssuesInputs"
|
||||||
path="parameters"
|
path="parameters"
|
||||||
|
:node="props.activeNode"
|
||||||
@value-changed="valueChanged"
|
@value-changed="valueChanged"
|
||||||
@activate="onWorkflowActivate"
|
@activate="onWorkflowActivate"
|
||||||
@parameter-blur="onParameterBlur"
|
@parameter-blur="onParameterBlur"
|
||||||
>
|
>
|
||||||
<NodeCredentials
|
<NodeCredentials
|
||||||
|
v-if="!isEmbeddedInCanvas"
|
||||||
:node="node"
|
:node="node"
|
||||||
:readonly="isReadOnly"
|
:readonly="isReadOnly"
|
||||||
:show-all="true"
|
:show-all="true"
|
||||||
@@ -920,7 +943,7 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NDVSubConnections
|
<NDVSubConnections
|
||||||
v-if="node && !props.hideConnections"
|
v-if="node && !props.isEmbeddedInCanvas"
|
||||||
ref="subConnections"
|
ref="subConnections"
|
||||||
:root-node="node"
|
:root-node="node"
|
||||||
@switch-selected-node="onSwitchSelectedNode"
|
@switch-selected-node="onSwitchSelectedNode"
|
||||||
@@ -935,12 +958,6 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
|||||||
background-color: var(--color-background-base);
|
background-color: var(--color-background-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerActions {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-4xs);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warningIcon {
|
.warningIcon {
|
||||||
color: var(--color-text-lighter);
|
color: var(--color-text-lighter);
|
||||||
font-size: var(--font-size-2xl);
|
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 {
|
.node-is-not-valid {
|
||||||
height: 75%;
|
height: 75%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -993,6 +1014,28 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
|
|||||||
flex-grow: 1;
|
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 {
|
&.dragging {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
box-shadow: 0px 6px 16px rgba(255, 74, 51, 0.15);
|
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 { ADD_FORM_NOTICE, deepCopy, NodeHelpers } from 'n8n-workflow';
|
||||||
import { computed, defineAsyncComponent, onErrorCaptured, ref, watch, type WatchSource } from 'vue';
|
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 AssignmentCollection from '@/components/AssignmentCollection/AssignmentCollection.vue';
|
||||||
import ButtonParameter from '@/components/ButtonParameter/ButtonParameter.vue';
|
import ButtonParameter from '@/components/ButtonParameter/ButtonParameter.vue';
|
||||||
@@ -64,6 +64,7 @@ const LazyCollectionParameter = defineAsyncComponent(
|
|||||||
const showIssuesInLabelFor = ['fixedCollection'];
|
const showIssuesInLabelFor = ['fixedCollection'];
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
node?: INodeUi;
|
||||||
nodeValues: INodeParameters;
|
nodeValues: INodeParameters;
|
||||||
parameters: INodeProperties[];
|
parameters: INodeProperties[];
|
||||||
path?: string;
|
path?: string;
|
||||||
@@ -120,6 +121,8 @@ const nodeType = computed(() => {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const node = computed(() => props.node ?? ndvStore.activeNode);
|
||||||
|
|
||||||
const filteredParameters = computedWithControl(
|
const filteredParameters = computedWithControl(
|
||||||
[() => props.parameters, () => props.nodeValues] as WatchSource[],
|
[() => props.parameters, () => props.nodeValues] as WatchSource[],
|
||||||
() => {
|
() => {
|
||||||
@@ -127,22 +130,20 @@ const filteredParameters = computedWithControl(
|
|||||||
displayNodeParameter(parameter),
|
displayNodeParameter(parameter),
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeNode = ndvStore.activeNode;
|
if (node.value && node.value.type === FORM_TRIGGER_NODE_TYPE) {
|
||||||
|
return updateFormTriggerParameters(parameters, node.value.name);
|
||||||
if (activeNode && activeNode.type === FORM_TRIGGER_NODE_TYPE) {
|
|
||||||
return updateFormTriggerParameters(parameters, activeNode.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeNode && activeNode.type === FORM_NODE_TYPE) {
|
if (node.value && node.value.type === FORM_NODE_TYPE) {
|
||||||
return updateFormParameters(parameters, activeNode.name);
|
return updateFormParameters(parameters, node.value.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
activeNode &&
|
node.value &&
|
||||||
activeNode.type === WAIT_NODE_TYPE &&
|
node.value.type === WAIT_NODE_TYPE &&
|
||||||
activeNode.parameters.resume === 'form'
|
node.value.parameters.resume === 'form'
|
||||||
) {
|
) {
|
||||||
return updateWaitParameters(parameters, activeNode.name);
|
return updateWaitParameters(parameters, node.value.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parameters;
|
return parameters;
|
||||||
@@ -153,8 +154,6 @@ const filteredParameterNames = computed(() => {
|
|||||||
return filteredParameters.value.map((parameter) => parameter.name);
|
return filteredParameters.value.map((parameter) => parameter.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
const node = computed(() => ndvStore.activeNode);
|
|
||||||
|
|
||||||
const nodeAuthFields = computed(() => {
|
const nodeAuthFields = computed(() => {
|
||||||
return getNodeAuthFields(nodeType.value);
|
return getNodeAuthFields(nodeType.value);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import Edge from './elements/edges/CanvasEdge.vue';
|
|||||||
import Node from './elements/nodes/CanvasNode.vue';
|
import Node from './elements/nodes/CanvasNode.vue';
|
||||||
import { useViewportAutoAdjust } from './composables/useViewportAutoAdjust';
|
import { useViewportAutoAdjust } from './composables/useViewportAutoAdjust';
|
||||||
import { isOutsideSelected } from '@/utils/htmlUtils';
|
import { isOutsideSelected } from '@/utils/htmlUtils';
|
||||||
|
import { useExperimentalNdvStore } from './experimental/experimentalNdv.store';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
@@ -174,6 +175,8 @@ const {
|
|||||||
} = useCanvasTraversal(vueFlow);
|
} = useCanvasTraversal(vueFlow);
|
||||||
const { layout } = useCanvasLayout({ id: props.id });
|
const { layout } = useCanvasLayout({ id: props.id });
|
||||||
|
|
||||||
|
const experimentalNdvStore = useExperimentalNdvStore();
|
||||||
|
|
||||||
const isPaneReady = ref(false);
|
const isPaneReady = ref(false);
|
||||||
|
|
||||||
const classes = computed(() => ({
|
const classes = computed(() => ({
|
||||||
@@ -854,7 +857,7 @@ provide(CanvasKey, {
|
|||||||
snap-to-grid
|
snap-to-grid
|
||||||
:snap-grid="[GRID_SIZE, GRID_SIZE]"
|
:snap-grid="[GRID_SIZE, GRID_SIZE]"
|
||||||
:min-zoom="0"
|
:min-zoom="0"
|
||||||
:max-zoom="4"
|
:max-zoom="experimentalNdvStore.isEnabled ? experimentalNdvStore.maxCanvasZoom : 4"
|
||||||
:selection-key-code="selectionKeyCode"
|
:selection-key-code="selectionKeyCode"
|
||||||
:zoom-activation-key-code="panningKeyCode"
|
:zoom-activation-key-code="panningKeyCode"
|
||||||
:pan-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 { useVueFlow } from '@vue-flow/core';
|
||||||
import { throttledRef } from '@vueuse/core';
|
import { throttledRef } from '@vueuse/core';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import ExperimentalNodeDetailsDrawer from './components/ExperimentalNodeDetailsDrawer.vue';
|
import ExperimentalNodeDetailsDrawer from './experimental/components/ExperimentalNodeDetailsDrawer.vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
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 { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
import type { CanvasNodeDefaultRender } from '@/types';
|
import type { CanvasNodeDefaultRender } from '@/types';
|
||||||
import { useCanvas } from '@/composables/useCanvas';
|
import { useCanvas } from '@/composables/useCanvas';
|
||||||
import { useNodeSettingsInCanvas } from '@/components/canvas/composables/useNodeSettingsInCanvas';
|
|
||||||
import { calculateNodeSize } from '@/utils/nodeViewUtils';
|
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 $style = useCssModule();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -45,7 +45,7 @@ const { mainOutputs, mainOutputConnections, mainInputs, mainInputConnections, no
|
|||||||
|
|
||||||
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
|
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
|
||||||
|
|
||||||
const nodeSettingsZoom = useNodeSettingsInCanvas();
|
const experimentalNdvStore = useExperimentalNdvStore();
|
||||||
|
|
||||||
const classes = computed(() => {
|
const classes = computed(() => {
|
||||||
return {
|
return {
|
||||||
@@ -61,7 +61,6 @@ const classes = computed(() => {
|
|||||||
[$style.configuration]: renderOptions.value.configuration,
|
[$style.configuration]: renderOptions.value.configuration,
|
||||||
[$style.trigger]: renderOptions.value.trigger,
|
[$style.trigger]: renderOptions.value.trigger,
|
||||||
[$style.warning]: renderOptions.value.dirtiness !== undefined,
|
[$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--width': `${nodeSize.value.width}px`,
|
||||||
'--canvas-node--height': `${nodeSize.value.height}px`,
|
'--canvas-node--height': `${nodeSize.value.height}px`,
|
||||||
'--node-icon-size': `${iconSize.value}px`,
|
'--node-icon-size': `${iconSize.value}px`,
|
||||||
...(nodeSettingsZoom.value === undefined ? {} : { '--zoom': nodeSettingsZoom.value }),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const dataTestId = computed(() => {
|
const dataTestId = computed(() => {
|
||||||
@@ -133,15 +131,20 @@ function onActivate(event: MouseEvent) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<ExperimentalInPlaceNodeSettings
|
||||||
|
v-if="experimentalNdvStore.isActive(viewport.zoom)"
|
||||||
|
:node-id="id"
|
||||||
|
:class="classes"
|
||||||
|
:style="styles"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
|
v-else
|
||||||
:class="classes"
|
:class="classes"
|
||||||
:style="styles"
|
:style="styles"
|
||||||
:data-test-id="dataTestId"
|
:data-test-id="dataTestId"
|
||||||
@contextmenu="openContextMenu"
|
@contextmenu="openContextMenu"
|
||||||
@dblclick.stop="onActivate"
|
@dblclick.stop="onActivate"
|
||||||
>
|
>
|
||||||
<ExperimentalCanvasNodeSettings v-if="nodeSettingsZoom !== undefined" :node-id="id" />
|
|
||||||
<template v-else>
|
|
||||||
<CanvasNodeTooltip v-if="renderOptions.tooltip" :visible="showTooltip" />
|
<CanvasNodeTooltip v-if="renderOptions.tooltip" :visible="showTooltip" />
|
||||||
<NodeIcon
|
<NodeIcon
|
||||||
:icon-source="iconSource"
|
:icon-source="iconSource"
|
||||||
@@ -161,7 +164,6 @@ function onActivate(event: MouseEvent) {
|
|||||||
<div v-if="subtitle" :class="$style.subtitle">{{ subtitle }}</div>
|
<div v-if="subtitle" :class="$style.subtitle">{{ subtitle }}</div>
|
||||||
</div>
|
</div>
|
||||||
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
|
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -188,20 +190,6 @@ function onActivate(event: MouseEvent) {
|
|||||||
var(--border-radius-large) var(--trigger-node--border-radius);
|
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
|
* Node types
|
||||||
*/
|
*/
|
||||||
@@ -272,32 +260,47 @@ function onActivate(event: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.success {
|
&.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 {
|
&.warning {
|
||||||
border-color: var(--color-warning);
|
--canvas-node--border-color: var(--color-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error {
|
&.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 {
|
&.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 {
|
&.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 {
|
&.running {
|
||||||
background-color: var(--color-node-executing-background);
|
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 {
|
&.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"
|
data-test-id="canvas-configurable-node"
|
||||||
style="--canvas-node--width: 240px; --canvas-node--height: 100px; --node-icon-size: 40px;"
|
style="--canvas-node--width: 240px; --canvas-node--height: 100px; --node-icon-size: 40px;"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<div
|
<div
|
||||||
class="n8n-node-icon icon icon"
|
class="n8n-node-icon icon icon"
|
||||||
@@ -43,7 +42,6 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -53,7 +51,6 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur
|
|||||||
data-test-id="canvas-configurable-node"
|
data-test-id="canvas-configurable-node"
|
||||||
style="--canvas-node--width: 240px; --canvas-node--height: 75px; --node-icon-size: 30px;"
|
style="--canvas-node--width: 240px; --canvas-node--height: 75px; --node-icon-size: 30px;"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<div
|
<div
|
||||||
class="n8n-node-icon icon icon"
|
class="n8n-node-icon icon icon"
|
||||||
@@ -90,7 +87,6 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -100,7 +96,6 @@ exports[`CanvasNodeDefault > configuration > should render configuration node co
|
|||||||
data-test-id="canvas-configuration-node"
|
data-test-id="canvas-configuration-node"
|
||||||
style="--canvas-node--width: 80px; --canvas-node--height: 80px; --node-icon-size: 30px;"
|
style="--canvas-node--width: 80px; --canvas-node--height: 80px; --node-icon-size: 30px;"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<div
|
<div
|
||||||
class="n8n-node-icon icon icon"
|
class="n8n-node-icon icon icon"
|
||||||
@@ -137,7 +132,6 @@ exports[`CanvasNodeDefault > configuration > should render configuration node co
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -147,7 +141,6 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
|
|||||||
data-test-id="canvas-default-node"
|
data-test-id="canvas-default-node"
|
||||||
style="--canvas-node--width: 100px; --canvas-node--height: 100px; --node-icon-size: 40px;"
|
style="--canvas-node--width: 100px; --canvas-node--height: 100px; --node-icon-size: 40px;"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<div
|
<div
|
||||||
class="n8n-node-icon icon icon"
|
class="n8n-node-icon icon icon"
|
||||||
@@ -184,7 +177,6 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -194,7 +186,6 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`]
|
|||||||
data-test-id="canvas-trigger-node"
|
data-test-id="canvas-trigger-node"
|
||||||
style="--canvas-node--width: 100px; --canvas-node--height: 100px; --node-icon-size: 40px;"
|
style="--canvas-node--width: 100px; --canvas-node--height: 100px; --node-icon-size: 40px;"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<div
|
<div
|
||||||
class="n8n-node-icon icon icon"
|
class="n8n-node-icon icon icon"
|
||||||
@@ -231,6 +222,5 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`]
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
import NodeSettings from '@/components/NodeSettings.vue';
|
import NodeSettings from '@/components/NodeSettings.vue';
|
||||||
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||||
import { type IUpdateInformation } from '@/Interface';
|
import { type IUpdateInformation } from '@/Interface';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
|
||||||
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 { createEventBus } from '@n8n/utils/event-bus';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
import { computed } from 'vue';
|
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 settingsEventBus = createEventBus();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const { setActiveNodeName } = useNDVStore();
|
|
||||||
const { renameNode } = useCanvasOperations();
|
const { renameNode } = useCanvasOperations();
|
||||||
|
|
||||||
const activeNode = computed(() => workflowsStore.getNodeById(nodeId));
|
const activeNode = computed(() => workflowsStore.getNodeById(nodeId));
|
||||||
@@ -24,12 +24,6 @@ const activeNodeType = computed(() => {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleOpenNdv() {
|
|
||||||
if (activeNode.value) {
|
|
||||||
setActiveNodeName(activeNode.value.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleValueChanged(parameterData: IUpdateInformation) {
|
function handleValueChanged(parameterData: IUpdateInformation) {
|
||||||
if (parameterData.name === 'name' && parameterData.oldValue) {
|
if (parameterData.name === 'name' && parameterData.oldValue) {
|
||||||
void renameNode(parameterData.oldValue as string, parameterData.value as string);
|
void renameNode(parameterData.oldValue as string, parameterData.value as string);
|
||||||
@@ -39,7 +33,6 @@ function handleValueChanged(parameterData: IUpdateInformation) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NodeSettings
|
<NodeSettings
|
||||||
:can-expand="canOpenNdv"
|
|
||||||
:event-bus="settingsEventBus"
|
:event-bus="settingsEventBus"
|
||||||
:dragging="false"
|
:dragging="false"
|
||||||
:active-node="activeNode"
|
:active-node="activeNode"
|
||||||
@@ -50,8 +43,12 @@ function handleValueChanged(parameterData: IUpdateInformation) {
|
|||||||
:block-u-i="false"
|
:block-u-i="false"
|
||||||
:executable="false"
|
:executable="false"
|
||||||
:input-size="0"
|
:input-size="0"
|
||||||
hide-connections
|
is-embedded-in-canvas
|
||||||
@expand="handleOpenNdv"
|
:no-wheel="noWheel"
|
||||||
@value-changed="handleValueChanged"
|
@value-changed="handleValueChanged"
|
||||||
/>
|
>
|
||||||
|
<template #actions>
|
||||||
|
<slot name="actions" />
|
||||||
|
</template>
|
||||||
|
</NodeSettings>
|
||||||
</template>
|
</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 ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
|
||||||
import { N8nText } from '@n8n/design-system';
|
import { N8nText } from '@n8n/design-system';
|
||||||
import { computed, watch, ref } from 'vue';
|
import { computed, watch, ref } from 'vue';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
|
||||||
const { selectedNodes } = defineProps<{ selectedNodes: CanvasNode[] }>();
|
const { selectedNodes } = defineProps<{ selectedNodes: CanvasNode[] }>();
|
||||||
|
|
||||||
@@ -14,6 +15,13 @@ const content = computed(() =>
|
|||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
const lastContent = ref<string | CanvasNode | 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)
|
// Sync lastContent to be "last defined content" (for drawer animation)
|
||||||
watch(
|
watch(
|
||||||
@@ -36,8 +44,19 @@ watch(
|
|||||||
v-else-if="lastContent !== undefined"
|
v-else-if="lastContent !== undefined"
|
||||||
:key="lastContent.id"
|
:key="lastContent.id"
|
||||||
:node-id="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>
|
</div>
|
||||||
</template>
|
</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,
|
faCogs,
|
||||||
faComment,
|
faComment,
|
||||||
faComments,
|
faComments,
|
||||||
|
faCompress,
|
||||||
faClipboardList,
|
faClipboardList,
|
||||||
faClock,
|
faClock,
|
||||||
faClone,
|
faClone,
|
||||||
@@ -247,6 +248,7 @@ export const FontAwesomePlugin: Plugin = {
|
|||||||
addIcon(faCogs);
|
addIcon(faCogs);
|
||||||
addIcon(faComment);
|
addIcon(faComment);
|
||||||
addIcon(faComments);
|
addIcon(faComments);
|
||||||
|
addIcon(faCompress);
|
||||||
addIcon(faClipboardList);
|
addIcon(faClipboardList);
|
||||||
addIcon(faClock);
|
addIcon(faClock);
|
||||||
addIcon(faClone);
|
addIcon(faClone);
|
||||||
|
|||||||
Reference in New Issue
Block a user