mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat(editor): Update styling of embedded NDV (no-changelog) (#17366)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -138,6 +138,7 @@ export function createCanvasProvide({
|
||||
isExecuting: ref(isExecuting),
|
||||
connectingHandle: ref(connectingHandle),
|
||||
viewport: ref(viewport),
|
||||
isExperimentalNdvActive: computed(() => false),
|
||||
} satisfies CanvasInjectionData,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,38 +86,39 @@ const onClaimCreditsClicked = async () => {
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="mt-xs">
|
||||
<n8n-callout
|
||||
v-if="userCanClaimOpenAiCredits && !showSuccessCallout"
|
||||
theme="secondary"
|
||||
icon="circle-alert"
|
||||
>
|
||||
<N8nCallout
|
||||
v-if="userCanClaimOpenAiCredits && !showSuccessCallout"
|
||||
theme="secondary"
|
||||
icon="circle-alert"
|
||||
class="mt-xs"
|
||||
>
|
||||
{{
|
||||
i18n.baseText('freeAi.credits.callout.claim.title', {
|
||||
interpolate: { credits: settingsStore.aiCreditsQuota },
|
||||
})
|
||||
}}
|
||||
<template #trailingContent>
|
||||
<n8n-button
|
||||
type="tertiary"
|
||||
size="small"
|
||||
:label="i18n.baseText('freeAi.credits.callout.claim.button.label')"
|
||||
:loading="claimingCredits"
|
||||
@click="onClaimCreditsClicked"
|
||||
/>
|
||||
</template>
|
||||
</N8nCallout>
|
||||
<N8nCallout v-else-if="showSuccessCallout" theme="success" icon="circle-check" class="mt-xs">
|
||||
<N8nText size="small">
|
||||
{{
|
||||
i18n.baseText('freeAi.credits.callout.claim.title', {
|
||||
i18n.baseText('freeAi.credits.callout.success.title.part1', {
|
||||
interpolate: { credits: settingsStore.aiCreditsQuota },
|
||||
})
|
||||
}}
|
||||
<template #trailingContent>
|
||||
<n8n-button
|
||||
type="tertiary"
|
||||
size="small"
|
||||
:label="i18n.baseText('freeAi.credits.callout.claim.button.label')"
|
||||
:loading="claimingCredits"
|
||||
@click="onClaimCreditsClicked"
|
||||
/>
|
||||
</template>
|
||||
</n8n-callout>
|
||||
<n8n-callout v-else-if="showSuccessCallout" theme="success" icon="circle-check">
|
||||
<n8n-text size="small">
|
||||
{{
|
||||
i18n.baseText('freeAi.credits.callout.success.title.part1', {
|
||||
interpolate: { credits: settingsStore.aiCreditsQuota },
|
||||
})
|
||||
}}</n8n-text
|
||||
>
|
||||
<n8n-text size="small" :bold="true">
|
||||
{{ i18n.baseText('freeAi.credits.callout.success.title.part2') }}</n8n-text
|
||||
>
|
||||
</n8n-callout>
|
||||
</div>
|
||||
</N8nText>
|
||||
|
||||
<N8nText size="small" :bold="true">
|
||||
{{ i18n.baseText('freeAi.credits.callout.success.title.part2') }}
|
||||
</N8nText>
|
||||
</N8nCallout>
|
||||
<div v-else />
|
||||
</template>
|
||||
|
||||
@@ -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,21 +856,18 @@ 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"
|
||||
:node-name="node.name"
|
||||
:disabled="outputPanelEditMode.enabled && !isTriggerNode"
|
||||
:tooltip="executeButtonTooltip"
|
||||
size="small"
|
||||
telemetry-source="parameters"
|
||||
@execute="onNodeExecute"
|
||||
@stop-execution="onStopExecution"
|
||||
@value-changed="valueChanged"
|
||||
/>
|
||||
<slot name="actions" />
|
||||
</template>
|
||||
<NodeExecuteButton
|
||||
v-if="isExecutable && !blockUI && node && nodeValid"
|
||||
data-test-id="node-execute-button"
|
||||
:node-name="node.name"
|
||||
:disabled="outputPanelEditMode.enabled && !isTriggerNode"
|
||||
:tooltip="executeButtonTooltip"
|
||||
size="small"
|
||||
telemetry-source="parameters"
|
||||
@execute="onNodeExecute"
|
||||
@stop-execution="onStopExecution"
|
||||
@value-changed="valueChanged"
|
||||
/>
|
||||
</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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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')"
|
||||
>
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
<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" />
|
||||
</ExperimentalEmbeddedNdvMapper>
|
||||
<div v-else role="button" :class="$style.collapsedContent" @click="handleToggleExpand">
|
||||
<NodeIcon :node-type="nodeType" :size="18" />
|
||||
<div :class="$style.collapsedNodeName">
|
||||
<N8nText bold>
|
||||
{{ node.name }}
|
||||
</N8nText>
|
||||
<N8nText bold size="small" color="text-light">
|
||||
{{ subTitle }}
|
||||
</N8nText>
|
||||
</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
|
||||
</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>
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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
3
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user