feat(editor): Update styling of embedded NDV (no-changelog) (#17366)

This commit is contained in:
Suguru Inoue
2025-07-22 11:39:14 +02:00
committed by GitHub
parent c1aae67a04
commit ee67e9e354
31 changed files with 623 additions and 265 deletions

View File

@@ -10,12 +10,14 @@ interface TabsProps {
modelValue?: Value;
options?: Array<TabOptions<Value>>;
size?: 'small' | 'medium';
variant?: 'modern' | 'legacy';
}
withDefaults(defineProps<TabsProps>(), {
modelValue: undefined,
options: () => [],
size: 'medium',
variant: 'legacy',
});
const scrollPosition = ref(0);
@@ -69,7 +71,14 @@ const scrollRight = () => scroll(50);
</script>
<template>
<div :class="['n8n-tabs', $style.container, size === 'small' ? $style.small : '']">
<div
:class="[
'n8n-tabs',
$style.container,
size === 'small' ? $style.small : '',
variant === 'modern' ? $style.modern : '',
]"
>
<div v-if="scrollPosition > 0" :class="$style.back" @click="scrollLeft">
<N8nIcon icon="chevron-left" size="small" />
</div>
@@ -133,6 +142,11 @@ const scrollRight = () => scroll(50);
height: 24px;
min-height: 24px;
width: 100%;
&.modern {
height: 26px;
min-height: 26px;
}
}
.tabs {
@@ -158,11 +172,9 @@ const scrollRight = () => scroll(50);
--active-tab-border-width: 2px;
display: block;
padding: 0 var(--spacing-s);
padding-bottom: calc(
var(--spacing-bottom-tab, var(--spacing-2xs)) + var(--active-tab-border-width)
);
font-size: var(--font-size-tab, var(--font-size-s));
font-weight: var(--font-weight-tab, var(--font-weight-regular));
padding-bottom: calc(var(--spacing-2xs) + var(--active-tab-border-width));
font-size: var(--font-size-s);
cursor: pointer;
white-space: nowrap;
color: var(--color-text-base);
@@ -174,6 +186,12 @@ const scrollRight = () => scroll(50);
margin-left: var(--spacing-4xs);
}
.modern & {
padding-bottom: calc(var(--spacing-xs) + var(--active-tab-border-width));
font-size: var(--font-size-2xs);
font-weight: var(--font-weight-bold);
}
.small & {
font-size: var(--font-size-2xs);
}
@@ -181,8 +199,12 @@ const scrollRight = () => scroll(50);
.activeTab {
color: var(--color-primary);
padding-bottom: var(--spacing-bottom-tab, var(--spacing-2xs));
padding-bottom: var(--spacing-2xs);
border-bottom: var(--color-primary) var(--active-tab-border-width) solid;
.modern & {
padding-bottom: var(--spacing-xs);
}
}
.alignRight:not(.alignRight + .alignRight) {

View File

@@ -109,6 +109,7 @@
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@pinia/testing": "^0.1.6",
"@testing-library/vue": "catalog:frontend",
"@types/dateformat": "^3.0.0",
"@types/file-saver": "^2.0.1",
"@types/humanize-duration": "^3.27.1",

View File

@@ -138,6 +138,7 @@ export function createCanvasProvide({
isExecuting: ref(isExecuting),
connectingHandle: ref(connectingHandle),
viewport: ref(viewport),
isExperimentalNdvActive: computed(() => false),
} satisfies CanvasInjectionData,
};
}

View File

@@ -9,6 +9,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store';
import { computed, ref } from 'vue';
import { OPEN_AI_API_CREDENTIAL_TYPE } from 'n8n-workflow';
import { N8nCallout, N8nText } from '@n8n/design-system';
const LANGCHAIN_NODES_PREFIX = '@n8n/n8n-nodes-langchain.';
@@ -85,11 +86,11 @@ const onClaimCreditsClicked = async () => {
};
</script>
<template>
<div class="mt-xs">
<n8n-callout
<N8nCallout
v-if="userCanClaimOpenAiCredits && !showSuccessCallout"
theme="secondary"
icon="circle-alert"
class="mt-xs"
>
{{
i18n.baseText('freeAi.credits.callout.claim.title', {
@@ -105,18 +106,19 @@ const onClaimCreditsClicked = async () => {
@click="onClaimCreditsClicked"
/>
</template>
</n8n-callout>
<n8n-callout v-else-if="showSuccessCallout" theme="success" icon="circle-check">
<n8n-text size="small">
</N8nCallout>
<N8nCallout v-else-if="showSuccessCallout" theme="success" icon="circle-check" class="mt-xs">
<N8nText size="small">
{{
i18n.baseText('freeAi.credits.callout.success.title.part1', {
interpolate: { credits: settingsStore.aiCreditsQuota },
})
}}</n8n-text
>&nbsp;
<n8n-text size="small" :bold="true">
{{ i18n.baseText('freeAi.credits.callout.success.title.part2') }}</n8n-text
>
</n8n-callout>
</div>
}}
</N8nText>
&nbsp;
<N8nText size="small" :bold="true">
{{ i18n.baseText('freeAi.credits.callout.success.title.part2') }}
</N8nText>
</N8nCallout>
<div v-else />
</template>

View File

@@ -55,6 +55,8 @@ import { shouldShowParameter } from './canvas/experimental/experimentalNdv.utils
import { useResizeObserver } from '@vueuse/core';
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
import { I18nT } from 'vue-i18n';
import { N8nBlockUi, N8nIcon, N8nLink, N8nNotice, N8nText } from '@n8n/design-system';
import ExperimentalEmbeddedNdvHeader from '@/components/canvas/experimental/components/ExperimentalEmbeddedNdvHeader.vue';
const props = withDefaults(
defineProps<{
@@ -69,6 +71,7 @@ const props = withDefaults(
activeNode?: INodeUi;
isEmbeddedInCanvas?: boolean;
noWheel?: boolean;
subTitle?: string;
}>(),
{
foreignCredentials: () => [],
@@ -79,6 +82,7 @@ const props = withDefaults(
activeNode: undefined,
isEmbeddedInCanvas: false,
noWheel: false,
subTitle: undefined,
},
);
@@ -827,7 +831,22 @@ function handleWheelEvent(event: WheelEvent) {
}"
@keydown.stop
>
<div v-if="!isNDVV2" :class="$style.header">
<ExperimentalEmbeddedNdvHeader
v-if="isEmbeddedInCanvas && node"
:node="node"
:selected-tab="openPanel"
:read-only="readOnly"
:node-type="nodeType"
:push-ref="pushRef"
:sub-title="subTitle"
@name-changed="nameChanged"
@tab-changed="onTabSelect"
>
<template #actions>
<slot name="actions" />
</template>
</ExperimentalEmbeddedNdvHeader>
<div v-else-if="!isNDVV2" :class="$style.header">
<div class="header-side-menu">
<NodeTitle
v-if="node"
@@ -837,7 +856,6 @@ function handleWheelEvent(event: WheelEvent) {
:read-only="isReadOnly"
@update:model-value="nameChanged"
/>
<template v-if="isExecutable || slots.actions">
<NodeExecuteButton
v-if="isExecutable && !blockUI && node && nodeValid"
data-test-id="node-execute-button"
@@ -850,8 +868,6 @@ function handleWheelEvent(event: WheelEvent) {
@stop-execution="onStopExecution"
@value-changed="valueChanged"
/>
<slot name="actions" />
</template>
</div>
<NodeSettingsTabs
v-if="node && nodeValid"
@@ -878,12 +894,12 @@ function handleWheelEvent(event: WheelEvent) {
/>
<div v-if="node && !nodeValid" class="node-is-not-valid">
<p :class="$style.warningIcon">
<n8n-icon icon="triangle-alert" />
<N8nIcon icon="triangle-alert" />
</p>
<div class="missingNodeTitleContainer mt-s mb-xs">
<n8n-text size="large" color="text-dark" bold>
<N8nText size="large" color="text-dark" bold>
{{ i18n.baseText('nodeSettings.communityNodeUnknown.title') }}
</n8n-text>
</N8nText>
</div>
<div v-if="isCommunityNode" :class="$style.descriptionContainer">
<div class="mb-l">
@@ -902,12 +918,12 @@ function handleWheelEvent(event: WheelEvent) {
</template>
</I18nT>
</div>
<n8n-link
<N8nLink
:to="COMMUNITY_NODES_INSTALLATION_DOCS_URL"
@click="onMissingNodeLearnMoreLinkClick"
>
{{ i18n.baseText('nodeSettings.communityNodeUnknown.installLink.text') }}
</n8n-link>
</N8nLink>
</div>
<I18nT v-else keypath="nodeSettings.nodeTypeUnknown.description" tag="span" scope="global">
<template #action>
@@ -931,7 +947,7 @@ function handleWheelEvent(event: WheelEvent) {
data-test-id="node-parameters"
@wheel="noWheel ? handleWheelEvent : undefined"
>
<n8n-notice
<N8nNotice
v-if="hasForeignCredential && !isHomeProjectTeam"
:content="
i18n.baseText('nodeSettings.hasForeignCredential', {
@@ -968,9 +984,9 @@ function handleWheelEvent(event: WheelEvent) {
/>
</ParameterInputList>
<div v-if="showNoParametersNotice" class="no-parameters">
<n8n-text>
<N8nText>
{{ i18n.baseText('nodeSettings.thisNodeDoesNotHaveAnyParameters') }}
</n8n-text>
</N8nText>
</div>
<div
@@ -978,7 +994,7 @@ function handleWheelEvent(event: WheelEvent) {
class="parameter-item parameter-notice"
data-test-id="node-parameters-http-notice"
>
<n8n-notice
<N8nNotice
:content="
i18n.baseText('nodeSettings.useTheHttpRequestNode', {
interpolate: { nodeTypeDisplayName: nodeType?.displayName ?? '' },
@@ -1038,7 +1054,7 @@ function handleWheelEvent(event: WheelEvent) {
@switch-selected-node="onSwitchSelectedNode"
@open-connection-node-creator="onOpenConnectionNodeCreator"
/>
<n8n-block-ui :show="blockUI" />
<N8nBlockUi :show="blockUI" />
<CommunityNodeFooter
v-if="openPanel === 'settings' && isCommunityNode"
:package-name="packageName"
@@ -1100,13 +1116,10 @@ function handleWheelEvent(event: WheelEvent) {
.node-name {
padding-top: var(--spacing-5xs);
margin-right: var(--spacing-s);
}
}
&.embedded .header-side-menu {
padding: var(--spacing-xs);
}
.node-is-not-valid {
height: 75%;
padding: 10px;

View File

@@ -34,6 +34,7 @@ const emit = defineEmits<{
:node-type="nodeType"
:push-ref="pushRef"
:class="$style.tabs"
tabs-variant="modern"
@update:model-value="emit('tab-changed', $event)"
/>
<NodeExecuteButton
@@ -54,10 +55,7 @@ const emit = defineEmits<{
<style lang="scss" module>
.header {
--spacing-bottom-tab: calc(var(--spacing-xs));
--font-size-tab: var(--font-size-2xs);
--color-tabs-arrow-buttons: var(--color-background-xlight);
--font-weight-tab: var(--font-weight-bold);
display: flex;
align-items: center;
@@ -68,7 +66,6 @@ const emit = defineEmits<{
}
.tabs {
padding-top: calc(var(--spacing-xs) + 1px);
height: 100%;
align-self: flex-end;
}
</style>

View File

@@ -22,12 +22,14 @@ type Props = {
nodeType?: INodeTypeDescription | null;
pushRef?: string;
hideDocs?: boolean;
tabsVariant?: 'modern' | 'legacy';
};
const props = withDefaults(defineProps<Props>(), {
modelValue: 'params',
nodeType: undefined,
pushRef: '',
tabsVariant: undefined,
});
const emit = defineEmits<{
'update:model-value': [tab: Tab];
@@ -147,6 +149,7 @@ onMounted(async () => {
<N8nTabs
:options="options"
:model-value="modelValue"
:variant="tabsVariant"
@update:model-value="onTabSelect"
@tooltip-click="onTooltipClick"
/>

View File

@@ -179,9 +179,12 @@ const experimentalNdvStore = useExperimentalNdvStore();
const isPaneReady = ref(false);
const isExperimentalNdvActive = computed(() => experimentalNdvStore.isActive(viewport.value.zoom));
const classes = computed(() => ({
[$style.canvas]: true,
[$style.ready]: !props.loading && isPaneReady.value,
[$style.isExperimentalNdvActive]: isExperimentalNdvActive.value,
}));
/**
@@ -844,6 +847,7 @@ provide(CanvasKey, {
isExecuting,
initialized,
viewport,
isExperimentalNdvActive,
});
</script>
@@ -892,6 +896,7 @@ provide(CanvasKey, {
:event-bus="eventBus"
:hovered="nodesHoveredById[nodeProps.id]"
:nearby-hovered="nodeProps.id === hoveredTriggerNode.id.value"
:is-experimental-ndv-active="isExperimentalNdvActive"
@delete="onDeleteNode"
@run="onRunNode"
@select="onSelectNode"
@@ -957,6 +962,7 @@ provide(CanvasKey, {
:show-interactive="false"
:zoom="viewport.zoom"
:read-only="readOnly"
:is-experimental-ndv-active="isExperimentalNdvActive"
@zoom-to-fit="onFitView"
@zoom-in="onZoomIn"
@zoom-out="onZoomOut"
@@ -992,6 +998,10 @@ provide(CanvasKey, {
cursor: grabbing;
}
}
&.isExperimentalNdvActive {
--canvas-zoom-compensation-factor: 0.67;
}
}
</style>

View File

@@ -10,6 +10,7 @@ const props = withDefaults(
defineProps<{
zoom?: number;
readOnly?: boolean;
isExperimentalNdvActive: boolean;
}>(),
{
zoom: 1,
@@ -113,7 +114,7 @@ function onTidyUp() {
</N8nButton>
</KeyboardShortcutTooltip>
<N8nTooltip
v-if="experimentalNdvStore.isActive(props.zoom)"
v-if="isExperimentalNdvActive"
placement="top"
:content="i18n.baseText('nodeView.expandAllNodes')"
>
@@ -125,7 +126,7 @@ function onTidyUp() {
/>
</N8nTooltip>
<N8nTooltip
v-if="experimentalNdvStore.isActive(props.zoom)"
v-if="isExperimentalNdvActive"
placement="top"
:content="i18n.baseText('nodeView.collapseAllNodes')"
>

View File

@@ -185,9 +185,9 @@ describe('CanvasEdge', () => {
},
});
const label = container.querySelector('.vue-flow__edge-label')?.childNodes[0];
const labelWrapper = container.querySelector('.vue-flow__edge-label');
expect(label).toHaveAttribute('style', 'transform: translate(0, -100%);');
expect(labelWrapper).toHaveClass('straight');
});
it("should render a label in the middle of the connector when it isn't straight", () => {
@@ -199,8 +199,8 @@ describe('CanvasEdge', () => {
},
});
const label = container.querySelector('.vue-flow__edge-label')?.childNodes[0];
const labelWrapper = container.querySelector('.vue-flow__edge-label');
expect(label).toHaveAttribute('style', 'transform: translate(0, 0%);');
expect(labelWrapper).not.toHaveClass('straight');
});
});

View File

@@ -75,7 +75,6 @@ const edgeColor = computed(() => {
const edgeStyle = computed(() => ({
...props.style,
...(isMainConnection.value ? {} : { strokeDasharray: '8,8' }),
strokeWidth: 2,
stroke: delayedHovered.value ? 'var(--color-primary)' : edgeColor.value,
}));
@@ -85,13 +84,6 @@ const edgeClasses = computed(() => ({
'bring-to-front': props.bringToFront,
}));
const edgeLabelStyle = computed(() => ({
transform: `translate(0, ${isConnectorStraight.value ? '-100%' : '0%'})`,
color: 'var(--color-text-base)',
}));
const isConnectorStraight = computed(() => renderData.value.isConnectorStraight);
const edgeToolbarStyle = computed(() => ({
transform: `translate(-50%, -50%) translate(${labelPosition.value[0]}px, ${labelPosition.value[1]}px)`,
...(delayedHovered.value ? { zIndex: 1 } : {}),
@@ -101,6 +93,7 @@ const edgeToolbarClasses = computed(() => ({
[$style.edgeLabelWrapper]: true,
'vue-flow__edge-label': true,
selected: props.selected,
[$style.straight]: renderData.value.isConnectorStraight,
}));
const renderData = computed(() =>
@@ -172,7 +165,7 @@ function onEdgeLabelMouseLeave() {
@add="onAdd"
@delete="onDelete"
/>
<div v-else :style="edgeLabelStyle" :class="$style.edgeLabel">{{ label }}</div>
<div v-else :class="$style.edgeLabel">{{ label }}</div>
</div>
</EdgeLabelRenderer>
</template>
@@ -182,14 +175,24 @@ function onEdgeLabelMouseLeave() {
transition:
stroke 0.3s ease,
fill 0.3s ease;
stroke-width: calc(2 * var(--canvas-zoom-compensation-factor, 1));
stroke-linecap: square;
}
.edgeLabelWrapper {
transform: translateY(calc(var(--spacing-xs) * -1));
position: absolute;
--label-translate-y: 0;
&.straight {
--label-translate-y: -100%;
}
}
.edgeLabel {
transform: scale(var(--canvas-zoom-compensation-factor, 1)) translate(0, var(--label-translate-y));
color: var(--color-text-base);
font-size: var(--font-size-xs);
background-color: hsla(
var(--color-canvas-background-h),

View File

@@ -66,6 +66,7 @@ function onDelete() {
gap: var(--spacing-2xs);
pointer-events: all;
padding: var(--spacing-2xs);
transform: scale(var(--canvas-zoom-compensation-factor, 1));
}
</style>

View File

@@ -166,8 +166,8 @@ provide(CanvasNodeHandleKey, {
<style lang="scss" module>
.handle {
--handle--indicator--width: 16px;
--handle--indicator--height: 16px;
--handle--indicator--width: calc(16px * var(--canvas-zoom-compensation-factor, 1));
--handle--indicator--height: calc(16px * var(--canvas-zoom-compensation-factor, 1));
width: var(--handle--indicator--width);
height: var(--handle--indicator--height);
@@ -181,7 +181,7 @@ provide(CanvasNodeHandleKey, {
&.inputs {
&.main {
--handle--indicator--width: 8px;
--handle--indicator--width: calc(8px * var(--canvas-zoom-compensation-factor, 1));
}
}
}

View File

@@ -33,7 +33,8 @@ const handleClasses = 'target';
position: absolute;
top: 50%;
left: calc(var(--spacing-xs) * -1);
transform: translate(-100%, -50%);
transform: translate(0, -50%) scale(var(--canvas-zoom-compensation-factor, 1)) translate(-100%, 0);
transform-origin: center left;
font-size: var(--font-size-2xs);
color: var(--color-foreground-xdark);
background: var(--color-canvas-label-background);

View File

@@ -125,7 +125,8 @@ function onClickAdd() {
.outputLabel {
top: 50%;
left: var(--spacing-m);
transform: translate(0, -50%);
transform: translate(0, -50%) scale(var(--canvas-zoom-compensation-factor, 1));
transform-origin: center left;
font-size: var(--font-size-2xs);
color: var(--color-foreground-xdark);
}
@@ -133,8 +134,9 @@ function onClickAdd() {
.runDataLabel {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -150%);
left: calc(50% * var(--canvas-zoom-compensation-factor, 1));
transform: translate(-50%, -50%) scale(var(--canvas-zoom-compensation-factor, 1))
translate(0, -100%);
font-size: var(--font-size-xs);
color: var(--color-text-base);
}

View File

@@ -72,7 +72,7 @@ function onClickAdd() {
position: absolute;
top: 20px;
left: 50%;
transform: translate(-50%, 0);
transform: translate(-50%, 0) scale(var(--canvas-zoom-compensation-factor, 1));
font-size: var(--font-size-2xs);
color: var(--node-type-supplemental-color);
background: var(--color-canvas-label-background);

View File

@@ -32,7 +32,7 @@ const classes = computed(() => ({
position: absolute;
top: -20px;
left: 50%;
transform: translate(-50%, 0);
transform: translate(-50%, 0) scale(var(--canvas-zoom-compensation-factor, 1));
font-size: var(--font-size-2xs);
color: var(--node-type-supplemental-color);
background: var(--color-canvas-label-background);

View File

@@ -143,6 +143,15 @@ function onClick(event: MouseEvent) {
<style lang="scss" module>
.wrapper {
position: relative;
transform: scale(var(--canvas-zoom-compensation-factor, 1));
&.right {
transform-origin: center left;
}
&.bottom {
transform-origin: top center;
}
&.secondary {
.line {

View File

@@ -35,7 +35,6 @@ import { createEventBus } from '@n8n/utils/event-bus';
import isEqual from 'lodash/isEqual';
import CanvasNodeTrigger from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeTrigger.vue';
import { CONFIGURATION_NODE_OFFSET, GRID_SIZE } from '@/utils/nodeViewUtils';
import { useExperimentalNdvStore } from '../../experimental/experimentalNdv.store';
type Props = NodeProps<CanvasNodeData> & {
readOnly?: boolean;
@@ -73,9 +72,7 @@ const props = defineProps<Props>();
const contextMenu = useContextMenu();
const { connectingHandle, viewport } = useCanvas();
const experimentalNdvStore = useExperimentalNdvStore();
const { connectingHandle, isExperimentalNdvActive } = useCanvas();
/*
Toolbar slot classes
@@ -99,10 +96,6 @@ const {
const isDisabled = computed(() => props.data.disabled);
const isExperimentalEmbeddedNdvShown = computed(() =>
experimentalNdvStore.isActive(viewport.value.zoom),
);
const classes = computed(() => ({
[style.canvasNode]: true,
[style.showToolbar]: showToolbar.value,
@@ -194,8 +187,8 @@ const createEndpointMappingFn =
const offsetValue =
position === Position.Bottom
? `${GRID_SIZE * 2 * (1 + index * 2) + CONFIGURATION_NODE_OFFSET}px`
: isExperimentalEmbeddedNdvShown.value && endpoints.length === 1
? `${(1 + index) * (GRID_SIZE * 2)}px`
: isExperimentalNdvActive.value && endpoints.length === 1
? `${(1 + index) * (GRID_SIZE * 1.5)}px`
: `${(100 / (endpoints.length + 1)) * (index + 1)}%`;
return {
@@ -421,6 +414,7 @@ onBeforeUnmount(() => {
:disabled="isDisabled"
:read-only="readOnly"
:class="$style.trigger"
:is-experimental-ndv-active="isExperimentalNdvActive"
/>
</div>
</template>
@@ -441,7 +435,7 @@ onBeforeUnmount(() => {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -100%);
transform: translate(-50%, -100%) scale(var(--canvas-zoom-compensation-factor, 1));
opacity: 0;
z-index: 1;

View File

@@ -23,7 +23,7 @@ const props = defineProps<{
const $style = useCssModule();
const i18n = useI18n();
const { isExecuting } = useCanvas();
const { isExecuting, isExperimentalNdvActive } = useCanvas();
const { isDisabled, render, name } = useCanvasNode();
const workflowsStore = useWorkflowsStore();
@@ -44,6 +44,7 @@ const classes = computed(() => ({
[$style.canvasNodeToolbar]: true,
[$style.readOnly]: props.readOnly,
[$style.forceVisible]: isHovered.value || isStickyColorSelectorOpen.value,
[$style.isExperimentalNdvActive]: isExperimentalNdvActive.value,
}));
const isExecuteNodeVisible = computed(() => {
@@ -185,6 +186,10 @@ function onFocusNode() {
display: flex;
justify-content: flex-end;
width: 100%;
&.isExperimentalNdvActive {
justify-content: center;
}
}
.canvasNodeToolbarItems {

View File

@@ -7,7 +7,6 @@ import type { CanvasNodeDefaultRender } from '@/types';
import { useCanvas } from '@/composables/useCanvas';
import { calculateNodeSize } from '@/utils/nodeViewUtils';
import ExperimentalInPlaceNodeSettings from '@/components/canvas/experimental/components/ExperimentalEmbeddedNodeDetails.vue';
import { useExperimentalNdvStore } from '@/components/canvas/experimental/experimentalNdv.store';
const $style = useCssModule();
const i18n = useI18n();
@@ -17,7 +16,7 @@ const emit = defineEmits<{
activate: [id: string, event: MouseEvent];
}>();
const { initialized, viewport } = useCanvas();
const { initialized, viewport, isExperimentalNdvActive } = useCanvas();
const {
id,
label,
@@ -46,8 +45,6 @@ const { mainOutputs, mainOutputConnections, mainInputs, mainInputConnections, no
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
const experimentalNdvStore = useExperimentalNdvStore();
const classes = computed(() => {
return {
[$style.node]: true,
@@ -133,7 +130,7 @@ function onActivate(event: MouseEvent) {
<template>
<ExperimentalInPlaceNodeSettings
v-if="experimentalNdvStore.isActive(viewport.zoom)"
v-if="isExperimentalNdvActive"
:node-id="id"
:class="classes"
:style="styles"
@@ -260,7 +257,8 @@ function onActivate(event: MouseEvent) {
*/
&.selected {
box-shadow: 0 0 0 8px var(--color-canvas-selected-transparent);
box-shadow: 0 0 0 calc(8px * var(--canvas-zoom-compensation-factor, 1))
var(--color-canvas-selected-transparent);
}
&.success {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, useCssModule } from 'vue';
import TitledList from '@/components/TitledList.vue';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useCanvasNode } from '@/composables/useCanvasNode';
@@ -8,8 +8,14 @@ import { CanvasNodeDirtiness, CanvasNodeRenderType } from '@/types';
import { N8nTooltip } from '@n8n/design-system';
import { useCanvas } from '@/composables/useCanvas';
const { size = 'medium', spinnerScrim = false } = defineProps<{
size?: 'small' | 'medium';
spinnerScrim?: boolean;
}>();
const nodeHelpers = useNodeHelpers();
const i18n = useI18n();
const $style = useCssModule();
const {
hasPinnedData,
@@ -38,31 +44,35 @@ const isNodeExecuting = computed(() => {
executionRunning.value || executionWaitingForNext.value || executionStatus.value === 'running' // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
);
});
const commonClasses = computed(() => [$style.status, spinnerScrim ? $style.spinnerScrim : '']);
</script>
<template>
<div v-if="isDisabled" :class="[...commonClasses, $style.disabled]">
<N8nIcon icon="power" :size="size" />
</div>
<div
v-if="hasIssues && !hideNodeIssues"
:class="[$style.status, $style.issues]"
v-else-if="hasIssues && !hideNodeIssues"
:class="[...commonClasses, $style.issues]"
data-test-id="node-issues"
>
<N8nTooltip :show-after="500" placement="bottom">
<template #content>
<TitledList :title="`${i18n.baseText('node.issues')}:`" :items="issues" />
</template>
<N8nIcon icon="triangle-alert" />
<N8nIcon icon="triangle-alert" :size="size" />
</N8nTooltip>
</div>
<div v-else-if="executionWaiting || executionStatus === 'waiting'">
<div :class="[$style.status, $style.waiting]">
<div :class="[...commonClasses, $style.waiting]">
<N8nTooltip placement="bottom">
<template #content>
<div v-text="executionWaiting"></div>
</template>
<N8nIcon icon="clock" />
<N8nIcon icon="clock" :size="size" />
</N8nTooltip>
</div>
<div :class="[$style.status, $style['node-waiting-spinner']]">
<div :class="[...commonClasses, $style['node-waiting-spinner']]">
<N8nIcon icon="refresh-cw" spin />
</div>
</div>
@@ -72,16 +82,16 @@ const isNodeExecuting = computed(() => {
<div
v-else-if="isNodeExecuting"
data-test-id="canvas-node-status-running"
:class="[$style.status, $style.running]"
:class="[...commonClasses, $style.running]"
>
<N8nIcon icon="refresh-cw" spin />
</div>
<div
v-else-if="hasPinnedData && !nodeHelpers.isProductionExecutionPreview.value && !isDisabled"
v-else-if="hasPinnedData && !nodeHelpers.isProductionExecutionPreview.value"
data-test-id="canvas-node-status-pinned"
:class="[$style.status, $style.pinnedData]"
:class="[...commonClasses, $style.pinnedData]"
>
<N8nIcon icon="pin" />
<N8nIcon icon="pin" :size="size" />
</div>
<div v-else-if="dirtiness !== undefined">
<N8nTooltip :show-after="500" placement="bottom">
@@ -94,8 +104,8 @@ const isNodeExecuting = computed(() => {
)
}}
</template>
<div data-test-id="canvas-node-status-warning" :class="[$style.status, $style.warning]">
<N8nIcon icon="triangle" />
<div data-test-id="canvas-node-status-warning" :class="[...commonClasses, $style.warning]">
<N8nIcon icon="triangle" :size="size" />
<span v-if="runDataIterations > 1" :class="$style.count"> {{ runDataIterations }}</span>
</div>
</N8nTooltip>
@@ -103,9 +113,9 @@ const isNodeExecuting = computed(() => {
<div
v-else-if="hasRunData"
data-test-id="canvas-node-status-success"
:class="[$style.status, $style.runData]"
:class="[...commonClasses, $style.runData]"
>
<N8nIcon icon="check" />
<N8nIcon icon="check" :size="size" />
<span v-if="runDataIterations > 1" :class="$style.count"> {{ runDataIterations }}</span>
</div>
</template>
@@ -130,26 +140,25 @@ const isNodeExecuting = computed(() => {
color: var(--color-secondary);
}
.node-waiting-spinner,
.running {
width: calc(100% - 2 * var(--canvas-node--status-icons-offset));
height: calc(100% - 2 * var(--canvas-node--status-icons-offset));
display: flex;
align-items: center;
justify-content: center;
font-size: 3.75em;
color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
}
.node-waiting-spinner {
display: flex;
align-items: center;
justify-content: center;
font-size: 3.75em;
color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 3.75em;
color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
position: absolute;
left: -34px;
top: -34px;
left: 0;
top: 0;
padding: var(--canvas-node--status-icons-offset);
&.spinnerScrim {
z-index: 10;
background-color: rgba(255, 255, 255, 0.82);
border-radius: var(--border-radius-large);
}
}
.issues {
@@ -164,4 +173,8 @@ const isNodeExecuting = computed(() => {
.warning {
color: var(--color-warning);
}
.disabled {
color: var(--color-foreground-xdark);
}
</style>

View File

@@ -17,6 +17,7 @@ const {
disabled,
readOnly,
class: cls,
isExperimentalNdvActive,
} = defineProps<{
name: string;
type: string;
@@ -24,6 +25,7 @@ const {
disabled?: boolean;
readOnly?: boolean;
class?: string;
isExperimentalNdvActive: boolean;
}>();
const style = useCssModule();
@@ -32,6 +34,7 @@ const containerClass = computed(() => ({
[style.container]: true,
[style.interactive]: !disabled && !readOnly,
[style.hovered]: !!hovered,
[style.isExperimentalNdvActive]: isExperimentalNdvActive,
}));
const router = useRouter();
@@ -126,6 +129,8 @@ async function handleClickExecute() {
transition:
translate 0.1s ease-in,
opacity 0.1s ease-in;
transform: scale(var(--canvas-zoom-compensation-factor, 1));
transform-origin: center right;
}
&.interactive.hovered button {
@@ -133,6 +138,10 @@ async function handleClickExecute() {
translate: 0 0;
pointer-events: all;
}
&.isExperimentalNdvActive {
height: var(--spacing-2xl);
}
}
.bolt {

View File

@@ -6,10 +6,11 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { createEventBus } from '@n8n/utils/event-bus';
import { computed } from 'vue';
const { nodeId, noWheel, isReadOnly } = defineProps<{
const { nodeId, noWheel, isReadOnly, subTitle } = defineProps<{
nodeId: string;
noWheel?: boolean;
isReadOnly?: boolean;
subTitle?: string;
}>();
defineSlots<{ actions?: {} }>();
@@ -40,6 +41,7 @@ function handleValueChanged(parameterData: IUpdateInformation) {
:input-size="0"
is-embedded-in-canvas
:no-wheel="noWheel"
:sub-title="subTitle"
@value-changed="handleValueChanged"
>
<template #actions>

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue';
import { N8nIconButton } from '@n8n/design-system';
defineProps<{ isExpanded: boolean }>();
const emit = defineEmits<{ openNdv: []; toggleExpand: [] }>();
</script>
<template>
<div :class="$style.actions">
<div :class="$style.icon">
<CanvasNodeStatusIcons size="small" spinner-scrim />
</div>
<N8nIconButton
icon="maximize-2"
type="secondary"
text
size="mini"
icon-size="small"
aria-label="Open..."
@click.stop="emit('openNdv')"
/>
<N8nIconButton
:icon="isExpanded ? 'chevron-down' : 'chevron-up'"
type="secondary"
text
size="mini"
icon-size="medium"
aria-label="Toggle expand"
@click.stop="emit('toggleExpand')"
/>
</div>
</template>
<style lang="scss" module>
.actions {
display: flex;
align-items: center;
color: var(--color-text-base);
& > button {
color: var(--color-text-light);
}
}
.icon {
margin-inline: var(--spacing-2xs);
}
</style>

View File

@@ -0,0 +1,90 @@
<script setup lang="ts">
import NodeIcon from '@/components/NodeIcon.vue';
import NodeSettingsTabs, { type Tab } from '@/components/NodeSettingsTabs.vue';
import { N8nText } from '@n8n/design-system';
import type { INode, INodeTypeDescription } from 'n8n-workflow';
defineProps<{
node: INode;
readOnly: boolean;
nodeType?: INodeTypeDescription | null;
pushRef: string;
subTitle?: string;
selectedTab: Tab;
}>();
const emit = defineEmits<{
'name-changed': [value: string];
'tab-changed': [tab: Tab];
}>();
defineSlots<{ actions?: {} }>();
</script>
<template>
<div :class="[$style.component, node.disabled ? $style.disabled : '']">
<div :class="$style.title">
<NodeIcon :node-type="nodeType" :size="16" />
<div :class="$style.titleText">
<N8nInlineTextEdit
:min-width="0"
:model-value="node.name"
:read-only="readOnly"
@update:model-value="emit('name-changed', $event)"
/>
</div>
<N8nText bold size="small" color="text-light" :class="$style.subTitleText">
{{ subTitle }}
</N8nText>
<slot name="actions" />
</div>
<NodeSettingsTabs
:model-value="selectedTab"
:node-type="nodeType"
:push-ref="pushRef"
tabs-variant="modern"
@update:model-value="emit('tab-changed', $event)"
/>
</div>
</template>
<style lang="scss" module>
.component {
border-bottom: var(--border-base);
}
.title {
display: flex;
align-items: center;
padding: var(--spacing-2xs) var(--spacing-3xs) var(--spacing-2xs) var(--spacing-xs);
border-bottom: var(--border-base);
margin-bottom: var(--spacing-xs);
gap: var(--spacing-4xs);
.disabled & {
background-color: var(--color-foreground-light);
}
}
.titleText {
min-width: 0;
flex-grow: 1;
flex-shrink: 1;
font-weight: var(--font-weight-medium);
font-size: var(--font-size-s);
overflow: hidden;
/* Same amount of padding and negative margin for border to not be cut by overflow: hidden */
padding: var(--spacing-2xs);
margin: calc(-1 * var(--spacing-2xs));
}
.subTitleText {
width: 0;
flex-grow: 100;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-top: var(--spacing-5xs);
}
</style>

View File

@@ -0,0 +1,129 @@
<script setup lang="ts">
import InputPanel from '@/components/InputPanel.vue';
import type { INodeUi } from '@/Interface';
import { useNDVStore } from '@/stores/ndv.store';
import { N8nText } from '@n8n/design-system';
import { useVueFlow } from '@vue-flow/core';
import { useActiveElement } from '@vueuse/core';
import { ElPopover } from 'element-plus';
import type { Workflow } from 'n8n-workflow';
import { onBeforeUnmount, ref, useTemplateRef, watch } from 'vue';
const { node, container } = defineProps<{
workflow: Workflow;
node: INodeUi;
container: HTMLDivElement | null;
inputNodeName?: string;
}>();
const ndvStore = useNDVStore();
const vf = useVueFlow();
const activeElement = useActiveElement();
const inputPanelRef = useTemplateRef('inputPanel');
const shouldShowInputPanel = ref(false);
const moveStartListener = vf.onMoveStart(() => {
shouldShowInputPanel.value = false;
});
const moveEndListener = vf.onMoveEnd(() => {
shouldShowInputPanel.value = getShouldShowInputPanel();
});
const viewportChangeListener = vf.onViewportChange(() => {
shouldShowInputPanel.value = false;
});
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;
}
});
onBeforeUnmount(() => {
moveStartListener.off();
moveEndListener.off();
viewportChangeListener.off();
});
</script>
<template>
<ElPopover
:visible="shouldShowInputPanel"
placement="left-start"
:show-arrow="false"
:popper-class="$style.component"
:width="360"
:offset="8"
:append-to="vf.viewportRef?.value"
>
<template #reference>
<slot />
</template>
<InputPanel
ref="inputPanel"
:tabindex="-1"
:class="$style.inputPanel"
:workflow="workflow"
:run-index="0"
compact
push-ref=""
display-mode="schema"
disable-display-mode-selection
:active-node-name="node.name"
:current-node-name="inputNodeName"
:is-mapping-onboarded="ndvStore.isMappingOnboarded"
:focused-mappable-input="ndvStore.focusedMappableInput"
>
<template #header>
<N8nText :class="$style.inputPanelTitle" :bold="true" color="text-light" size="small">
Input
</N8nText>
</template>
</InputPanel>
</ElPopover>
</template>
<style lang="scss" module>
.component {
background-color: transparent !important;
padding: 0 !important;
border: none !important;
margin-top: -2px;
}
.inputPanel {
border: var(--border-base);
border-width: 1px;
background-color: var(--color-background-light);
border-radius: var(--border-radius-large);
zoom: var(--zoom);
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.05);
padding: var(--spacing-2xs);
height: 100%;
}
.inputPanelTitle {
text-transform: uppercase;
letter-spacing: 3px;
}
</style>

View File

@@ -1,18 +1,20 @@
<script setup lang="ts">
import InputPanel from '@/components/InputPanel.vue';
import NodeTitle from '@/components/NodeTitle.vue';
import { ExpressionLocalResolveContextSymbol } from '@/constants';
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { ExpressionLocalResolveContext } from '@/types/expressions';
import { N8nIcon, N8nIconButton } from '@n8n/design-system';
import { N8nText } from '@n8n/design-system';
import { useVueFlow } from '@vue-flow/core';
import { useActiveElement, watchOnce } from '@vueuse/core';
import { computed, onBeforeUnmount, provide, ref, useTemplateRef, watch } from 'vue';
import { watchOnce } from '@vueuse/core';
import { computed, onBeforeUnmount, provide, ref, useTemplateRef } from 'vue';
import { useExperimentalNdvStore } from '../experimentalNdv.store';
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
import { useI18n } from '@n8n/i18n';
import NodeIcon from '@/components/NodeIcon.vue';
import { getNodeSubTitleText } from '@/components/canvas/experimental/experimentalNdv.utils';
import ExperimentalEmbeddedNdvActions from '@/components/canvas/experimental/components/ExperimentalEmbeddedNdvActions.vue';
const { nodeId, isReadOnly, isConfigurable } = defineProps<{
nodeId: string;
@@ -20,6 +22,7 @@ const { nodeId, isReadOnly, isConfigurable } = defineProps<{
isConfigurable: boolean;
}>();
const i18n = useI18n();
const ndvStore = useNDVStore();
const experimentalNdvStore = useExperimentalNdvStore();
const isExpanded = computed(() => !experimentalNdvStore.collapsedNodes[nodeId]);
@@ -61,12 +64,14 @@ const isVisible = computed(() =>
),
);
const isOnceVisible = ref(isVisible.value);
const shouldShowInputPanel = ref(false);
const containerRef = useTemplateRef('container');
const inputPanelContainerRef = useTemplateRef('inputPanelContainer');
const activeElement = useActiveElement();
const subTitle = computed(() =>
node.value && nodeType.value
? getNodeSubTitleText(node.value, nodeType.value, !isExpanded.value, i18n)
: undefined,
);
const expressionResolveCtx = computed<ExpressionLocalResolveContext | undefined>(() => {
if (!node.value) {
return undefined;
@@ -118,38 +123,42 @@ function handleToggleExpand() {
experimentalNdvStore.setNodeExpanded(nodeId);
}
function handleOpenNdv() {
if (node.value) {
ndvStore.setActiveNodeName(node.value.name);
}
}
provide(ExpressionLocalResolveContextSymbol, expressionResolveCtx);
watchOnce(isVisible, (visible) => {
isOnceVisible.value = isOnceVisible.value || visible;
});
watch([activeElement, vf.getSelectedNodes], ([active, selected]) => {
if (active && containerRef.value?.contains(active)) {
// TODO: find a way to implement this without depending on test ID
shouldShowInputPanel.value =
!!active.closest('[data-test-id=inline-expression-editor-input]') ||
!!inputPanelContainerRef.value?.contains(active);
}
if (selected.every((sel) => sel.id !== node.value?.id)) {
shouldShowInputPanel.value = false;
}
});
</script>
<template>
<div
ref="container"
:class="[$style.component, isExpanded ? $style.expanded : $style.collapsed]"
:class="[
$style.component,
isExpanded ? $style.expanded : $style.collapsed,
node?.disabled ? $style.disabled : '',
isExpanded ? 'nodrag' : '',
]"
:style="{
'--zoom': `${1 / experimentalNdvStore.maxCanvasZoom}`,
'--node-width-scaler': isConfigurable ? 1 : 1.5,
}"
>
<template v-if="isOnceVisible">
<template v-if="!node || !isOnceVisible" />
<ExperimentalEmbeddedNdvMapper
v-else-if="isExpanded"
:workflow="workflow"
:node="node"
:input-node-name="expressionResolveCtx?.inputNode?.name"
:container="containerRef"
>
<ExperimentalCanvasNodeSettings
v-if="isExpanded"
tabindex="-1"
:node-id="nodeId"
:class="$style.settingsView"
@@ -157,58 +166,33 @@ watch([activeElement, vf.getSelectedNodes], ([active, selected]) => {
!isMoving /* to not interrupt panning while allowing scroll of the settings pane, allow wheel event while panning */
"
:is-read-only="isReadOnly"
:sub-title="subTitle"
>
<template #actions>
<N8nIconButton
icon="minimize-2"
type="secondary"
text
size="mini"
icon-size="large"
aria-label="Toggle expand"
@click="handleToggleExpand"
<ExperimentalEmbeddedNdvActions
:is-expanded="isExpanded"
@open-ndv="handleOpenNdv"
@toggle-expand="handleToggleExpand"
/>
</template>
</ExperimentalCanvasNodeSettings>
</ExperimentalEmbeddedNdvMapper>
<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="maximize-2" size="large" />
</div>
<Transition name="input">
<div
v-if="shouldShowInputPanel && node"
ref="inputPanelContainer"
:class="$style.inputPanelContainer"
:tabindex="-1"
>
<InputPanel
:class="$style.inputPanel"
:workflow="workflow"
:run-index="0"
compact
push-ref=""
display-mode="schema"
disable-display-mode-selection
:active-node-name="node.name"
:current-node-name="expressionResolveCtx?.inputNode?.name"
:is-mapping-onboarded="ndvStore.isMappingOnboarded"
:focused-mappable-input="ndvStore.focusedMappableInput"
>
<template #header>
<N8nText :class="$style.inputPanelTitle" :bold="true" color="text-light" size="small">
Input
<NodeIcon :node-type="nodeType" :size="18" />
<div :class="$style.collapsedNodeName">
<N8nText bold>
{{ node.name }}
</N8nText>
<N8nText bold size="small" color="text-light">
{{ subTitle }}
</N8nText>
</template>
</InputPanel>
</div>
</Transition>
</template>
<ExperimentalEmbeddedNdvActions
:is-expanded="isExpanded"
@open-ndv="handleOpenNdv"
@toggle-expand="handleToggleExpand"
/>
</div>
</div>
</template>
@@ -220,16 +204,20 @@ watch([activeElement, vf.getSelectedNodes], ([active, selected]) => {
border-width: 1px !important;
border-radius: var(--border-radius-base) !important;
width: calc(var(--canvas-node--width) * var(--node-width-scaler));
overflow: hidden;
--canvas-node--border-color: var(--color-text-lighter);
&.expanded {
cursor: default;
user-select: text;
cursor: auto;
height: auto;
max-height: min(calc(var(--canvas-node--height) * 2), 300px);
min-height: var(--spacing-3xl);
}
&.collapsed {
overflow: hidden;
height: var(--spacing-3xl);
height: var(--spacing-2xl);
}
}
@@ -245,27 +233,44 @@ watch([activeElement, vf.getSelectedNodes], ([active, selected]) => {
:root .settingsView {
z-index: 1000;
width: 100%;
border-radius: var(--border-radius-base);
height: auto;
max-height: calc(min(calc(var(--canvas-node--height) * 2), 300px) - var(--border-width-base) * 2);
min-height: var(--spacing-3xl); // should be multiple of GRID_SIZE
min-height: var(--spacing-2xl); // should be multiple of GRID_SIZE
}
.collapsedContent {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-s);
gap: var(--spacing-4xs);
background-color: white;
padding: var(--spacing-2xs);
padding: var(--spacing-2xs) var(--spacing-4xs) var(--spacing-2xs) var(--spacing-2xs);
background-color: var(--color-background-xlight);
color: var(--color-text-base);
cursor: pointer;
.disabled & {
background-color: var(--color-foreground-light);
}
& > * {
zoom: calc(var(--zoom) * 1.25);
zoom: var(--zoom);
flex-grow: 0;
flex-shrink: 0;
}
}
.collapsedNodeName {
width: 0;
flex-grow: 1;
display: flex;
flex-direction: column;
gap: var(--spacing-5xs);
& > * {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
@@ -274,43 +279,4 @@ watch([activeElement, vf.getSelectedNodes], ([active, selected]) => {
zoom: var(--zoom);
}
}
.inputPanelContainer {
position: absolute;
right: 100%;
top: 0;
padding-right: var(--spacing-4xs);
margin-top: calc(-1 * var(--border-width-base));
width: 180px;
z-index: 2000;
max-height: 80vh;
}
.inputPanel {
border: var(--border-base);
border-width: 1px;
background-color: var(--color-background-light);
border-radius: var(--border-radius-large);
zoom: var(--zoom);
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.05);
padding: var(--spacing-2xs);
height: 100%;
}
.inputPanelTitle {
text-transform: uppercase;
letter-spacing: 3px;
}
</style>
<style lang="scss" scoped>
.input-enter-active,
.input-leave-active {
transition: opacity 0.3s ease;
}
.input-enter-from,
.input-leave-to {
opacity: 0;
}
</style>

View File

@@ -1,5 +1,37 @@
import type { INodeProperties } from 'n8n-workflow';
import type { INodeUi } from '@/Interface';
import type { I18nClass } from '@n8n/i18n';
import type { INodeProperties, INodeTypeDescription } from 'n8n-workflow';
export function shouldShowParameter(item: INodeProperties): boolean {
return item.name.match(/resource|authentication|operation/i) === null;
}
export function getNodeSubTitleText(
node: INodeUi,
nodeType: INodeTypeDescription,
includeOperation: boolean,
t: I18nClass,
) {
if (node.disabled) {
return `(${t.baseText('node.disabled')})`;
}
const displayName = nodeType.displayName ?? '';
if (!includeOperation) {
return displayName;
}
const selectedOperation = node.parameters.operation;
const selectedOperationDisplayName =
selectedOperation &&
nodeType.properties
.find((p) => p.name === 'operation')
?.options?.find((o) => 'value' in o && o.value === selectedOperation)?.name;
if (!selectedOperationDisplayName) {
return displayName;
}
return `${displayName}: ${selectedOperationDisplayName}`;
}

View File

@@ -163,6 +163,7 @@ export interface CanvasInjectionData {
isExecuting: Ref<boolean | undefined>;
connectingHandle: Ref<ConnectStartEvent | undefined>;
viewport: Ref<ViewportTransform>;
isExperimentalNdvActive: ComputedRef<boolean>;
}
export type CanvasNodeEventBusEvents = {

3
pnpm-lock.yaml generated
View File

@@ -2533,6 +2533,9 @@ importers:
'@pinia/testing':
specifier: ^0.1.6
version: 0.1.6(pinia@2.2.4(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
'@testing-library/vue':
specifier: catalog:frontend
version: 8.1.0(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.3))
'@types/dateformat':
specifier: ^3.0.0
version: 3.0.1