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;
|
modelValue?: Value;
|
||||||
options?: Array<TabOptions<Value>>;
|
options?: Array<TabOptions<Value>>;
|
||||||
size?: 'small' | 'medium';
|
size?: 'small' | 'medium';
|
||||||
|
variant?: 'modern' | 'legacy';
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<TabsProps>(), {
|
withDefaults(defineProps<TabsProps>(), {
|
||||||
modelValue: undefined,
|
modelValue: undefined,
|
||||||
options: () => [],
|
options: () => [],
|
||||||
size: 'medium',
|
size: 'medium',
|
||||||
|
variant: 'legacy',
|
||||||
});
|
});
|
||||||
|
|
||||||
const scrollPosition = ref(0);
|
const scrollPosition = ref(0);
|
||||||
@@ -69,7 +71,14 @@ const scrollRight = () => scroll(50);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<div v-if="scrollPosition > 0" :class="$style.back" @click="scrollLeft">
|
||||||
<N8nIcon icon="chevron-left" size="small" />
|
<N8nIcon icon="chevron-left" size="small" />
|
||||||
</div>
|
</div>
|
||||||
@@ -133,6 +142,11 @@ const scrollRight = () => scroll(50);
|
|||||||
height: 24px;
|
height: 24px;
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&.modern {
|
||||||
|
height: 26px;
|
||||||
|
min-height: 26px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
@@ -158,11 +172,9 @@ const scrollRight = () => scroll(50);
|
|||||||
--active-tab-border-width: 2px;
|
--active-tab-border-width: 2px;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0 var(--spacing-s);
|
padding: 0 var(--spacing-s);
|
||||||
padding-bottom: calc(
|
padding-bottom: calc(var(--spacing-2xs) + var(--active-tab-border-width));
|
||||||
var(--spacing-bottom-tab, var(--spacing-2xs)) + var(--active-tab-border-width)
|
font-size: var(--font-size-s);
|
||||||
);
|
|
||||||
font-size: var(--font-size-tab, var(--font-size-s));
|
|
||||||
font-weight: var(--font-weight-tab, var(--font-weight-regular));
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
@@ -174,6 +186,12 @@ const scrollRight = () => scroll(50);
|
|||||||
margin-left: var(--spacing-4xs);
|
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 & {
|
.small & {
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
}
|
}
|
||||||
@@ -181,8 +199,12 @@ const scrollRight = () => scroll(50);
|
|||||||
|
|
||||||
.activeTab {
|
.activeTab {
|
||||||
color: var(--color-primary);
|
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;
|
border-bottom: var(--color-primary) var(--active-tab-border-width) solid;
|
||||||
|
|
||||||
|
.modern & {
|
||||||
|
padding-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.alignRight:not(.alignRight + .alignRight) {
|
.alignRight:not(.alignRight + .alignRight) {
|
||||||
|
|||||||
@@ -109,6 +109,7 @@
|
|||||||
"@n8n/typescript-config": "workspace:*",
|
"@n8n/typescript-config": "workspace:*",
|
||||||
"@n8n/vitest-config": "workspace:*",
|
"@n8n/vitest-config": "workspace:*",
|
||||||
"@pinia/testing": "^0.1.6",
|
"@pinia/testing": "^0.1.6",
|
||||||
|
"@testing-library/vue": "catalog:frontend",
|
||||||
"@types/dateformat": "^3.0.0",
|
"@types/dateformat": "^3.0.0",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"@types/humanize-duration": "^3.27.1",
|
"@types/humanize-duration": "^3.27.1",
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export function createCanvasProvide({
|
|||||||
isExecuting: ref(isExecuting),
|
isExecuting: ref(isExecuting),
|
||||||
connectingHandle: ref(connectingHandle),
|
connectingHandle: ref(connectingHandle),
|
||||||
viewport: ref(viewport),
|
viewport: ref(viewport),
|
||||||
|
isExperimentalNdvActive: computed(() => false),
|
||||||
} satisfies CanvasInjectionData,
|
} satisfies CanvasInjectionData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useSettingsStore } from '@/stores/settings.store';
|
|||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { OPEN_AI_API_CREDENTIAL_TYPE } from 'n8n-workflow';
|
import { OPEN_AI_API_CREDENTIAL_TYPE } from 'n8n-workflow';
|
||||||
|
import { N8nCallout, N8nText } from '@n8n/design-system';
|
||||||
|
|
||||||
const LANGCHAIN_NODES_PREFIX = '@n8n/n8n-nodes-langchain.';
|
const LANGCHAIN_NODES_PREFIX = '@n8n/n8n-nodes-langchain.';
|
||||||
|
|
||||||
@@ -85,11 +86,11 @@ const onClaimCreditsClicked = async () => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="mt-xs">
|
<N8nCallout
|
||||||
<n8n-callout
|
|
||||||
v-if="userCanClaimOpenAiCredits && !showSuccessCallout"
|
v-if="userCanClaimOpenAiCredits && !showSuccessCallout"
|
||||||
theme="secondary"
|
theme="secondary"
|
||||||
icon="circle-alert"
|
icon="circle-alert"
|
||||||
|
class="mt-xs"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
i18n.baseText('freeAi.credits.callout.claim.title', {
|
i18n.baseText('freeAi.credits.callout.claim.title', {
|
||||||
@@ -105,18 +106,19 @@ const onClaimCreditsClicked = async () => {
|
|||||||
@click="onClaimCreditsClicked"
|
@click="onClaimCreditsClicked"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</n8n-callout>
|
</N8nCallout>
|
||||||
<n8n-callout v-else-if="showSuccessCallout" theme="success" icon="circle-check">
|
<N8nCallout v-else-if="showSuccessCallout" theme="success" icon="circle-check" class="mt-xs">
|
||||||
<n8n-text size="small">
|
<N8nText size="small">
|
||||||
{{
|
{{
|
||||||
i18n.baseText('freeAi.credits.callout.success.title.part1', {
|
i18n.baseText('freeAi.credits.callout.success.title.part1', {
|
||||||
interpolate: { credits: settingsStore.aiCreditsQuota },
|
interpolate: { credits: settingsStore.aiCreditsQuota },
|
||||||
})
|
})
|
||||||
}}</n8n-text
|
}}
|
||||||
>
|
</N8nText>
|
||||||
<n8n-text size="small" :bold="true">
|
|
||||||
{{ i18n.baseText('freeAi.credits.callout.success.title.part2') }}</n8n-text
|
<N8nText size="small" :bold="true">
|
||||||
>
|
{{ i18n.baseText('freeAi.credits.callout.success.title.part2') }}
|
||||||
</n8n-callout>
|
</N8nText>
|
||||||
</div>
|
</N8nCallout>
|
||||||
|
<div v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ import { shouldShowParameter } from './canvas/experimental/experimentalNdv.utils
|
|||||||
import { useResizeObserver } from '@vueuse/core';
|
import { useResizeObserver } from '@vueuse/core';
|
||||||
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
||||||
import { I18nT } from 'vue-i18n';
|
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(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -69,6 +71,7 @@ const props = withDefaults(
|
|||||||
activeNode?: INodeUi;
|
activeNode?: INodeUi;
|
||||||
isEmbeddedInCanvas?: boolean;
|
isEmbeddedInCanvas?: boolean;
|
||||||
noWheel?: boolean;
|
noWheel?: boolean;
|
||||||
|
subTitle?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
foreignCredentials: () => [],
|
foreignCredentials: () => [],
|
||||||
@@ -79,6 +82,7 @@ const props = withDefaults(
|
|||||||
activeNode: undefined,
|
activeNode: undefined,
|
||||||
isEmbeddedInCanvas: false,
|
isEmbeddedInCanvas: false,
|
||||||
noWheel: false,
|
noWheel: false,
|
||||||
|
subTitle: undefined,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -827,7 +831,22 @@ function handleWheelEvent(event: WheelEvent) {
|
|||||||
}"
|
}"
|
||||||
@keydown.stop
|
@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">
|
<div class="header-side-menu">
|
||||||
<NodeTitle
|
<NodeTitle
|
||||||
v-if="node"
|
v-if="node"
|
||||||
@@ -837,7 +856,6 @@ function handleWheelEvent(event: WheelEvent) {
|
|||||||
:read-only="isReadOnly"
|
:read-only="isReadOnly"
|
||||||
@update:model-value="nameChanged"
|
@update:model-value="nameChanged"
|
||||||
/>
|
/>
|
||||||
<template v-if="isExecutable || slots.actions">
|
|
||||||
<NodeExecuteButton
|
<NodeExecuteButton
|
||||||
v-if="isExecutable && !blockUI && node && nodeValid"
|
v-if="isExecutable && !blockUI && node && nodeValid"
|
||||||
data-test-id="node-execute-button"
|
data-test-id="node-execute-button"
|
||||||
@@ -850,8 +868,6 @@ function handleWheelEvent(event: WheelEvent) {
|
|||||||
@stop-execution="onStopExecution"
|
@stop-execution="onStopExecution"
|
||||||
@value-changed="valueChanged"
|
@value-changed="valueChanged"
|
||||||
/>
|
/>
|
||||||
<slot name="actions" />
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
<NodeSettingsTabs
|
<NodeSettingsTabs
|
||||||
v-if="node && nodeValid"
|
v-if="node && nodeValid"
|
||||||
@@ -878,12 +894,12 @@ function handleWheelEvent(event: WheelEvent) {
|
|||||||
/>
|
/>
|
||||||
<div v-if="node && !nodeValid" class="node-is-not-valid">
|
<div v-if="node && !nodeValid" class="node-is-not-valid">
|
||||||
<p :class="$style.warningIcon">
|
<p :class="$style.warningIcon">
|
||||||
<n8n-icon icon="triangle-alert" />
|
<N8nIcon icon="triangle-alert" />
|
||||||
</p>
|
</p>
|
||||||
<div class="missingNodeTitleContainer mt-s mb-xs">
|
<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') }}
|
{{ i18n.baseText('nodeSettings.communityNodeUnknown.title') }}
|
||||||
</n8n-text>
|
</N8nText>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isCommunityNode" :class="$style.descriptionContainer">
|
<div v-if="isCommunityNode" :class="$style.descriptionContainer">
|
||||||
<div class="mb-l">
|
<div class="mb-l">
|
||||||
@@ -902,12 +918,12 @@ function handleWheelEvent(event: WheelEvent) {
|
|||||||
</template>
|
</template>
|
||||||
</I18nT>
|
</I18nT>
|
||||||
</div>
|
</div>
|
||||||
<n8n-link
|
<N8nLink
|
||||||
:to="COMMUNITY_NODES_INSTALLATION_DOCS_URL"
|
:to="COMMUNITY_NODES_INSTALLATION_DOCS_URL"
|
||||||
@click="onMissingNodeLearnMoreLinkClick"
|
@click="onMissingNodeLearnMoreLinkClick"
|
||||||
>
|
>
|
||||||
{{ i18n.baseText('nodeSettings.communityNodeUnknown.installLink.text') }}
|
{{ i18n.baseText('nodeSettings.communityNodeUnknown.installLink.text') }}
|
||||||
</n8n-link>
|
</N8nLink>
|
||||||
</div>
|
</div>
|
||||||
<I18nT v-else keypath="nodeSettings.nodeTypeUnknown.description" tag="span" scope="global">
|
<I18nT v-else keypath="nodeSettings.nodeTypeUnknown.description" tag="span" scope="global">
|
||||||
<template #action>
|
<template #action>
|
||||||
@@ -931,7 +947,7 @@ function handleWheelEvent(event: WheelEvent) {
|
|||||||
data-test-id="node-parameters"
|
data-test-id="node-parameters"
|
||||||
@wheel="noWheel ? handleWheelEvent : undefined"
|
@wheel="noWheel ? handleWheelEvent : undefined"
|
||||||
>
|
>
|
||||||
<n8n-notice
|
<N8nNotice
|
||||||
v-if="hasForeignCredential && !isHomeProjectTeam"
|
v-if="hasForeignCredential && !isHomeProjectTeam"
|
||||||
:content="
|
:content="
|
||||||
i18n.baseText('nodeSettings.hasForeignCredential', {
|
i18n.baseText('nodeSettings.hasForeignCredential', {
|
||||||
@@ -968,9 +984,9 @@ function handleWheelEvent(event: WheelEvent) {
|
|||||||
/>
|
/>
|
||||||
</ParameterInputList>
|
</ParameterInputList>
|
||||||
<div v-if="showNoParametersNotice" class="no-parameters">
|
<div v-if="showNoParametersNotice" class="no-parameters">
|
||||||
<n8n-text>
|
<N8nText>
|
||||||
{{ i18n.baseText('nodeSettings.thisNodeDoesNotHaveAnyParameters') }}
|
{{ i18n.baseText('nodeSettings.thisNodeDoesNotHaveAnyParameters') }}
|
||||||
</n8n-text>
|
</N8nText>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -978,7 +994,7 @@ function handleWheelEvent(event: WheelEvent) {
|
|||||||
class="parameter-item parameter-notice"
|
class="parameter-item parameter-notice"
|
||||||
data-test-id="node-parameters-http-notice"
|
data-test-id="node-parameters-http-notice"
|
||||||
>
|
>
|
||||||
<n8n-notice
|
<N8nNotice
|
||||||
:content="
|
:content="
|
||||||
i18n.baseText('nodeSettings.useTheHttpRequestNode', {
|
i18n.baseText('nodeSettings.useTheHttpRequestNode', {
|
||||||
interpolate: { nodeTypeDisplayName: nodeType?.displayName ?? '' },
|
interpolate: { nodeTypeDisplayName: nodeType?.displayName ?? '' },
|
||||||
@@ -1038,7 +1054,7 @@ function handleWheelEvent(event: WheelEvent) {
|
|||||||
@switch-selected-node="onSwitchSelectedNode"
|
@switch-selected-node="onSwitchSelectedNode"
|
||||||
@open-connection-node-creator="onOpenConnectionNodeCreator"
|
@open-connection-node-creator="onOpenConnectionNodeCreator"
|
||||||
/>
|
/>
|
||||||
<n8n-block-ui :show="blockUI" />
|
<N8nBlockUi :show="blockUI" />
|
||||||
<CommunityNodeFooter
|
<CommunityNodeFooter
|
||||||
v-if="openPanel === 'settings' && isCommunityNode"
|
v-if="openPanel === 'settings' && isCommunityNode"
|
||||||
:package-name="packageName"
|
:package-name="packageName"
|
||||||
@@ -1100,13 +1116,10 @@ function handleWheelEvent(event: WheelEvent) {
|
|||||||
|
|
||||||
.node-name {
|
.node-name {
|
||||||
padding-top: var(--spacing-5xs);
|
padding-top: var(--spacing-5xs);
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.embedded .header-side-menu {
|
|
||||||
padding: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-is-not-valid {
|
.node-is-not-valid {
|
||||||
height: 75%;
|
height: 75%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const emit = defineEmits<{
|
|||||||
:node-type="nodeType"
|
:node-type="nodeType"
|
||||||
:push-ref="pushRef"
|
:push-ref="pushRef"
|
||||||
:class="$style.tabs"
|
:class="$style.tabs"
|
||||||
|
tabs-variant="modern"
|
||||||
@update:model-value="emit('tab-changed', $event)"
|
@update:model-value="emit('tab-changed', $event)"
|
||||||
/>
|
/>
|
||||||
<NodeExecuteButton
|
<NodeExecuteButton
|
||||||
@@ -54,10 +55,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.header {
|
.header {
|
||||||
--spacing-bottom-tab: calc(var(--spacing-xs));
|
|
||||||
--font-size-tab: var(--font-size-2xs);
|
|
||||||
--color-tabs-arrow-buttons: var(--color-background-xlight);
|
--color-tabs-arrow-buttons: var(--color-background-xlight);
|
||||||
--font-weight-tab: var(--font-weight-bold);
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -68,7 +66,6 @@ const emit = defineEmits<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
padding-top: calc(var(--spacing-xs) + 1px);
|
align-self: flex-end;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ type Props = {
|
|||||||
nodeType?: INodeTypeDescription | null;
|
nodeType?: INodeTypeDescription | null;
|
||||||
pushRef?: string;
|
pushRef?: string;
|
||||||
hideDocs?: boolean;
|
hideDocs?: boolean;
|
||||||
|
tabsVariant?: 'modern' | 'legacy';
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
modelValue: 'params',
|
modelValue: 'params',
|
||||||
nodeType: undefined,
|
nodeType: undefined,
|
||||||
pushRef: '',
|
pushRef: '',
|
||||||
|
tabsVariant: undefined,
|
||||||
});
|
});
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:model-value': [tab: Tab];
|
'update:model-value': [tab: Tab];
|
||||||
@@ -147,6 +149,7 @@ onMounted(async () => {
|
|||||||
<N8nTabs
|
<N8nTabs
|
||||||
:options="options"
|
:options="options"
|
||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
|
:variant="tabsVariant"
|
||||||
@update:model-value="onTabSelect"
|
@update:model-value="onTabSelect"
|
||||||
@tooltip-click="onTooltipClick"
|
@tooltip-click="onTooltipClick"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -179,9 +179,12 @@ const experimentalNdvStore = useExperimentalNdvStore();
|
|||||||
|
|
||||||
const isPaneReady = ref(false);
|
const isPaneReady = ref(false);
|
||||||
|
|
||||||
|
const isExperimentalNdvActive = computed(() => experimentalNdvStore.isActive(viewport.value.zoom));
|
||||||
|
|
||||||
const classes = computed(() => ({
|
const classes = computed(() => ({
|
||||||
[$style.canvas]: true,
|
[$style.canvas]: true,
|
||||||
[$style.ready]: !props.loading && isPaneReady.value,
|
[$style.ready]: !props.loading && isPaneReady.value,
|
||||||
|
[$style.isExperimentalNdvActive]: isExperimentalNdvActive.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -844,6 +847,7 @@ provide(CanvasKey, {
|
|||||||
isExecuting,
|
isExecuting,
|
||||||
initialized,
|
initialized,
|
||||||
viewport,
|
viewport,
|
||||||
|
isExperimentalNdvActive,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -892,6 +896,7 @@ provide(CanvasKey, {
|
|||||||
:event-bus="eventBus"
|
:event-bus="eventBus"
|
||||||
:hovered="nodesHoveredById[nodeProps.id]"
|
:hovered="nodesHoveredById[nodeProps.id]"
|
||||||
:nearby-hovered="nodeProps.id === hoveredTriggerNode.id.value"
|
:nearby-hovered="nodeProps.id === hoveredTriggerNode.id.value"
|
||||||
|
:is-experimental-ndv-active="isExperimentalNdvActive"
|
||||||
@delete="onDeleteNode"
|
@delete="onDeleteNode"
|
||||||
@run="onRunNode"
|
@run="onRunNode"
|
||||||
@select="onSelectNode"
|
@select="onSelectNode"
|
||||||
@@ -957,6 +962,7 @@ provide(CanvasKey, {
|
|||||||
:show-interactive="false"
|
:show-interactive="false"
|
||||||
:zoom="viewport.zoom"
|
:zoom="viewport.zoom"
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
|
:is-experimental-ndv-active="isExperimentalNdvActive"
|
||||||
@zoom-to-fit="onFitView"
|
@zoom-to-fit="onFitView"
|
||||||
@zoom-in="onZoomIn"
|
@zoom-in="onZoomIn"
|
||||||
@zoom-out="onZoomOut"
|
@zoom-out="onZoomOut"
|
||||||
@@ -992,6 +998,10 @@ provide(CanvasKey, {
|
|||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.isExperimentalNdvActive {
|
||||||
|
--canvas-zoom-compensation-factor: 0.67;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const props = withDefaults(
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
zoom?: number;
|
zoom?: number;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
isExperimentalNdvActive: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
@@ -113,7 +114,7 @@ function onTidyUp() {
|
|||||||
</N8nButton>
|
</N8nButton>
|
||||||
</KeyboardShortcutTooltip>
|
</KeyboardShortcutTooltip>
|
||||||
<N8nTooltip
|
<N8nTooltip
|
||||||
v-if="experimentalNdvStore.isActive(props.zoom)"
|
v-if="isExperimentalNdvActive"
|
||||||
placement="top"
|
placement="top"
|
||||||
:content="i18n.baseText('nodeView.expandAllNodes')"
|
:content="i18n.baseText('nodeView.expandAllNodes')"
|
||||||
>
|
>
|
||||||
@@ -125,7 +126,7 @@ function onTidyUp() {
|
|||||||
/>
|
/>
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
<N8nTooltip
|
<N8nTooltip
|
||||||
v-if="experimentalNdvStore.isActive(props.zoom)"
|
v-if="isExperimentalNdvActive"
|
||||||
placement="top"
|
placement="top"
|
||||||
:content="i18n.baseText('nodeView.collapseAllNodes')"
|
: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", () => {
|
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(() => ({
|
const edgeStyle = computed(() => ({
|
||||||
...props.style,
|
...props.style,
|
||||||
...(isMainConnection.value ? {} : { strokeDasharray: '8,8' }),
|
...(isMainConnection.value ? {} : { strokeDasharray: '8,8' }),
|
||||||
strokeWidth: 2,
|
|
||||||
stroke: delayedHovered.value ? 'var(--color-primary)' : edgeColor.value,
|
stroke: delayedHovered.value ? 'var(--color-primary)' : edgeColor.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -85,13 +84,6 @@ const edgeClasses = computed(() => ({
|
|||||||
'bring-to-front': props.bringToFront,
|
'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(() => ({
|
const edgeToolbarStyle = computed(() => ({
|
||||||
transform: `translate(-50%, -50%) translate(${labelPosition.value[0]}px, ${labelPosition.value[1]}px)`,
|
transform: `translate(-50%, -50%) translate(${labelPosition.value[0]}px, ${labelPosition.value[1]}px)`,
|
||||||
...(delayedHovered.value ? { zIndex: 1 } : {}),
|
...(delayedHovered.value ? { zIndex: 1 } : {}),
|
||||||
@@ -101,6 +93,7 @@ const edgeToolbarClasses = computed(() => ({
|
|||||||
[$style.edgeLabelWrapper]: true,
|
[$style.edgeLabelWrapper]: true,
|
||||||
'vue-flow__edge-label': true,
|
'vue-flow__edge-label': true,
|
||||||
selected: props.selected,
|
selected: props.selected,
|
||||||
|
[$style.straight]: renderData.value.isConnectorStraight,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const renderData = computed(() =>
|
const renderData = computed(() =>
|
||||||
@@ -172,7 +165,7 @@ function onEdgeLabelMouseLeave() {
|
|||||||
@add="onAdd"
|
@add="onAdd"
|
||||||
@delete="onDelete"
|
@delete="onDelete"
|
||||||
/>
|
/>
|
||||||
<div v-else :style="edgeLabelStyle" :class="$style.edgeLabel">{{ label }}</div>
|
<div v-else :class="$style.edgeLabel">{{ label }}</div>
|
||||||
</div>
|
</div>
|
||||||
</EdgeLabelRenderer>
|
</EdgeLabelRenderer>
|
||||||
</template>
|
</template>
|
||||||
@@ -182,14 +175,24 @@ function onEdgeLabelMouseLeave() {
|
|||||||
transition:
|
transition:
|
||||||
stroke 0.3s ease,
|
stroke 0.3s ease,
|
||||||
fill 0.3s ease;
|
fill 0.3s ease;
|
||||||
|
stroke-width: calc(2 * var(--canvas-zoom-compensation-factor, 1));
|
||||||
|
stroke-linecap: square;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edgeLabelWrapper {
|
.edgeLabelWrapper {
|
||||||
transform: translateY(calc(var(--spacing-xs) * -1));
|
transform: translateY(calc(var(--spacing-xs) * -1));
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
|
--label-translate-y: 0;
|
||||||
|
|
||||||
|
&.straight {
|
||||||
|
--label-translate-y: -100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.edgeLabel {
|
.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);
|
font-size: var(--font-size-xs);
|
||||||
background-color: hsla(
|
background-color: hsla(
|
||||||
var(--color-canvas-background-h),
|
var(--color-canvas-background-h),
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ function onDelete() {
|
|||||||
gap: var(--spacing-2xs);
|
gap: var(--spacing-2xs);
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
padding: var(--spacing-2xs);
|
padding: var(--spacing-2xs);
|
||||||
|
transform: scale(var(--canvas-zoom-compensation-factor, 1));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -166,8 +166,8 @@ provide(CanvasNodeHandleKey, {
|
|||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.handle {
|
.handle {
|
||||||
--handle--indicator--width: 16px;
|
--handle--indicator--width: calc(16px * var(--canvas-zoom-compensation-factor, 1));
|
||||||
--handle--indicator--height: 16px;
|
--handle--indicator--height: calc(16px * var(--canvas-zoom-compensation-factor, 1));
|
||||||
|
|
||||||
width: var(--handle--indicator--width);
|
width: var(--handle--indicator--width);
|
||||||
height: var(--handle--indicator--height);
|
height: var(--handle--indicator--height);
|
||||||
@@ -181,7 +181,7 @@ provide(CanvasNodeHandleKey, {
|
|||||||
|
|
||||||
&.inputs {
|
&.inputs {
|
||||||
&.main {
|
&.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;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: calc(var(--spacing-xs) * -1);
|
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);
|
font-size: var(--font-size-2xs);
|
||||||
color: var(--color-foreground-xdark);
|
color: var(--color-foreground-xdark);
|
||||||
background: var(--color-canvas-label-background);
|
background: var(--color-canvas-label-background);
|
||||||
|
|||||||
@@ -125,7 +125,8 @@ function onClickAdd() {
|
|||||||
.outputLabel {
|
.outputLabel {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: var(--spacing-m);
|
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);
|
font-size: var(--font-size-2xs);
|
||||||
color: var(--color-foreground-xdark);
|
color: var(--color-foreground-xdark);
|
||||||
}
|
}
|
||||||
@@ -133,8 +134,9 @@ function onClickAdd() {
|
|||||||
.runDataLabel {
|
.runDataLabel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: calc(50% * var(--canvas-zoom-compensation-factor, 1));
|
||||||
transform: translate(-50%, -150%);
|
transform: translate(-50%, -50%) scale(var(--canvas-zoom-compensation-factor, 1))
|
||||||
|
translate(0, -100%);
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function onClickAdd() {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, 0);
|
transform: translate(-50%, 0) scale(var(--canvas-zoom-compensation-factor, 1));
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
color: var(--node-type-supplemental-color);
|
color: var(--node-type-supplemental-color);
|
||||||
background: var(--color-canvas-label-background);
|
background: var(--color-canvas-label-background);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const classes = computed(() => ({
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: -20px;
|
top: -20px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, 0);
|
transform: translate(-50%, 0) scale(var(--canvas-zoom-compensation-factor, 1));
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
color: var(--node-type-supplemental-color);
|
color: var(--node-type-supplemental-color);
|
||||||
background: var(--color-canvas-label-background);
|
background: var(--color-canvas-label-background);
|
||||||
|
|||||||
@@ -143,6 +143,15 @@ function onClick(event: MouseEvent) {
|
|||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.wrapper {
|
.wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
transform: scale(var(--canvas-zoom-compensation-factor, 1));
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
transform-origin: center left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom {
|
||||||
|
transform-origin: top center;
|
||||||
|
}
|
||||||
|
|
||||||
&.secondary {
|
&.secondary {
|
||||||
.line {
|
.line {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import { createEventBus } from '@n8n/utils/event-bus';
|
|||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
import CanvasNodeTrigger from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeTrigger.vue';
|
import CanvasNodeTrigger from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeTrigger.vue';
|
||||||
import { CONFIGURATION_NODE_OFFSET, GRID_SIZE } from '@/utils/nodeViewUtils';
|
import { CONFIGURATION_NODE_OFFSET, GRID_SIZE } from '@/utils/nodeViewUtils';
|
||||||
import { useExperimentalNdvStore } from '../../experimental/experimentalNdv.store';
|
|
||||||
|
|
||||||
type Props = NodeProps<CanvasNodeData> & {
|
type Props = NodeProps<CanvasNodeData> & {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@@ -73,9 +72,7 @@ const props = defineProps<Props>();
|
|||||||
|
|
||||||
const contextMenu = useContextMenu();
|
const contextMenu = useContextMenu();
|
||||||
|
|
||||||
const { connectingHandle, viewport } = useCanvas();
|
const { connectingHandle, isExperimentalNdvActive } = useCanvas();
|
||||||
|
|
||||||
const experimentalNdvStore = useExperimentalNdvStore();
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Toolbar slot classes
|
Toolbar slot classes
|
||||||
@@ -99,10 +96,6 @@ const {
|
|||||||
|
|
||||||
const isDisabled = computed(() => props.data.disabled);
|
const isDisabled = computed(() => props.data.disabled);
|
||||||
|
|
||||||
const isExperimentalEmbeddedNdvShown = computed(() =>
|
|
||||||
experimentalNdvStore.isActive(viewport.value.zoom),
|
|
||||||
);
|
|
||||||
|
|
||||||
const classes = computed(() => ({
|
const classes = computed(() => ({
|
||||||
[style.canvasNode]: true,
|
[style.canvasNode]: true,
|
||||||
[style.showToolbar]: showToolbar.value,
|
[style.showToolbar]: showToolbar.value,
|
||||||
@@ -194,8 +187,8 @@ const createEndpointMappingFn =
|
|||||||
const offsetValue =
|
const offsetValue =
|
||||||
position === Position.Bottom
|
position === Position.Bottom
|
||||||
? `${GRID_SIZE * 2 * (1 + index * 2) + CONFIGURATION_NODE_OFFSET}px`
|
? `${GRID_SIZE * 2 * (1 + index * 2) + CONFIGURATION_NODE_OFFSET}px`
|
||||||
: isExperimentalEmbeddedNdvShown.value && endpoints.length === 1
|
: isExperimentalNdvActive.value && endpoints.length === 1
|
||||||
? `${(1 + index) * (GRID_SIZE * 2)}px`
|
? `${(1 + index) * (GRID_SIZE * 1.5)}px`
|
||||||
: `${(100 / (endpoints.length + 1)) * (index + 1)}%`;
|
: `${(100 / (endpoints.length + 1)) * (index + 1)}%`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -421,6 +414,7 @@ onBeforeUnmount(() => {
|
|||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
:class="$style.trigger"
|
:class="$style.trigger"
|
||||||
|
:is-experimental-ndv-active="isExperimentalNdvActive"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -441,7 +435,7 @@ onBeforeUnmount(() => {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -100%);
|
transform: translate(-50%, -100%) scale(var(--canvas-zoom-compensation-factor, 1));
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const props = defineProps<{
|
|||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const { isExecuting } = useCanvas();
|
const { isExecuting, isExperimentalNdvActive } = useCanvas();
|
||||||
const { isDisabled, render, name } = useCanvasNode();
|
const { isDisabled, render, name } = useCanvasNode();
|
||||||
|
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
@@ -44,6 +44,7 @@ const classes = computed(() => ({
|
|||||||
[$style.canvasNodeToolbar]: true,
|
[$style.canvasNodeToolbar]: true,
|
||||||
[$style.readOnly]: props.readOnly,
|
[$style.readOnly]: props.readOnly,
|
||||||
[$style.forceVisible]: isHovered.value || isStickyColorSelectorOpen.value,
|
[$style.forceVisible]: isHovered.value || isStickyColorSelectorOpen.value,
|
||||||
|
[$style.isExperimentalNdvActive]: isExperimentalNdvActive.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const isExecuteNodeVisible = computed(() => {
|
const isExecuteNodeVisible = computed(() => {
|
||||||
@@ -185,6 +186,10 @@ function onFocusNode() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&.isExperimentalNdvActive {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvasNodeToolbarItems {
|
.canvasNodeToolbarItems {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import type { CanvasNodeDefaultRender } from '@/types';
|
|||||||
import { useCanvas } from '@/composables/useCanvas';
|
import { useCanvas } from '@/composables/useCanvas';
|
||||||
import { calculateNodeSize } from '@/utils/nodeViewUtils';
|
import { calculateNodeSize } from '@/utils/nodeViewUtils';
|
||||||
import ExperimentalInPlaceNodeSettings from '@/components/canvas/experimental/components/ExperimentalEmbeddedNodeDetails.vue';
|
import ExperimentalInPlaceNodeSettings from '@/components/canvas/experimental/components/ExperimentalEmbeddedNodeDetails.vue';
|
||||||
import { useExperimentalNdvStore } from '@/components/canvas/experimental/experimentalNdv.store';
|
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -17,7 +16,7 @@ const emit = defineEmits<{
|
|||||||
activate: [id: string, event: MouseEvent];
|
activate: [id: string, event: MouseEvent];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { initialized, viewport } = useCanvas();
|
const { initialized, viewport, isExperimentalNdvActive } = useCanvas();
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
@@ -46,8 +45,6 @@ const { mainOutputs, mainOutputConnections, mainInputs, mainInputConnections, no
|
|||||||
|
|
||||||
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
|
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
|
||||||
|
|
||||||
const experimentalNdvStore = useExperimentalNdvStore();
|
|
||||||
|
|
||||||
const classes = computed(() => {
|
const classes = computed(() => {
|
||||||
return {
|
return {
|
||||||
[$style.node]: true,
|
[$style.node]: true,
|
||||||
@@ -133,7 +130,7 @@ function onActivate(event: MouseEvent) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ExperimentalInPlaceNodeSettings
|
<ExperimentalInPlaceNodeSettings
|
||||||
v-if="experimentalNdvStore.isActive(viewport.zoom)"
|
v-if="isExperimentalNdvActive"
|
||||||
:node-id="id"
|
:node-id="id"
|
||||||
:class="classes"
|
:class="classes"
|
||||||
:style="styles"
|
:style="styles"
|
||||||
@@ -260,7 +257,8 @@ function onActivate(event: MouseEvent) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
&.selected {
|
&.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 {
|
&.success {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, useCssModule } from 'vue';
|
||||||
import TitledList from '@/components/TitledList.vue';
|
import TitledList from '@/components/TitledList.vue';
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
@@ -8,8 +8,14 @@ import { CanvasNodeDirtiness, CanvasNodeRenderType } from '@/types';
|
|||||||
import { N8nTooltip } from '@n8n/design-system';
|
import { N8nTooltip } from '@n8n/design-system';
|
||||||
import { useCanvas } from '@/composables/useCanvas';
|
import { useCanvas } from '@/composables/useCanvas';
|
||||||
|
|
||||||
|
const { size = 'medium', spinnerScrim = false } = defineProps<{
|
||||||
|
size?: 'small' | 'medium';
|
||||||
|
spinnerScrim?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const $style = useCssModule();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hasPinnedData,
|
hasPinnedData,
|
||||||
@@ -38,31 +44,35 @@ const isNodeExecuting = computed(() => {
|
|||||||
executionRunning.value || executionWaitingForNext.value || executionStatus.value === 'running' // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
|
executionRunning.value || executionWaitingForNext.value || executionStatus.value === 'running' // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
const commonClasses = computed(() => [$style.status, spinnerScrim ? $style.spinnerScrim : '']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div v-if="isDisabled" :class="[...commonClasses, $style.disabled]">
|
||||||
|
<N8nIcon icon="power" :size="size" />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="hasIssues && !hideNodeIssues"
|
v-else-if="hasIssues && !hideNodeIssues"
|
||||||
:class="[$style.status, $style.issues]"
|
:class="[...commonClasses, $style.issues]"
|
||||||
data-test-id="node-issues"
|
data-test-id="node-issues"
|
||||||
>
|
>
|
||||||
<N8nTooltip :show-after="500" placement="bottom">
|
<N8nTooltip :show-after="500" placement="bottom">
|
||||||
<template #content>
|
<template #content>
|
||||||
<TitledList :title="`${i18n.baseText('node.issues')}:`" :items="issues" />
|
<TitledList :title="`${i18n.baseText('node.issues')}:`" :items="issues" />
|
||||||
</template>
|
</template>
|
||||||
<N8nIcon icon="triangle-alert" />
|
<N8nIcon icon="triangle-alert" :size="size" />
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="executionWaiting || executionStatus === 'waiting'">
|
<div v-else-if="executionWaiting || executionStatus === 'waiting'">
|
||||||
<div :class="[$style.status, $style.waiting]">
|
<div :class="[...commonClasses, $style.waiting]">
|
||||||
<N8nTooltip placement="bottom">
|
<N8nTooltip placement="bottom">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div v-text="executionWaiting"></div>
|
<div v-text="executionWaiting"></div>
|
||||||
</template>
|
</template>
|
||||||
<N8nIcon icon="clock" />
|
<N8nIcon icon="clock" :size="size" />
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div :class="[$style.status, $style['node-waiting-spinner']]">
|
<div :class="[...commonClasses, $style['node-waiting-spinner']]">
|
||||||
<N8nIcon icon="refresh-cw" spin />
|
<N8nIcon icon="refresh-cw" spin />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,16 +82,16 @@ const isNodeExecuting = computed(() => {
|
|||||||
<div
|
<div
|
||||||
v-else-if="isNodeExecuting"
|
v-else-if="isNodeExecuting"
|
||||||
data-test-id="canvas-node-status-running"
|
data-test-id="canvas-node-status-running"
|
||||||
:class="[$style.status, $style.running]"
|
:class="[...commonClasses, $style.running]"
|
||||||
>
|
>
|
||||||
<N8nIcon icon="refresh-cw" spin />
|
<N8nIcon icon="refresh-cw" spin />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="hasPinnedData && !nodeHelpers.isProductionExecutionPreview.value && !isDisabled"
|
v-else-if="hasPinnedData && !nodeHelpers.isProductionExecutionPreview.value"
|
||||||
data-test-id="canvas-node-status-pinned"
|
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>
|
||||||
<div v-else-if="dirtiness !== undefined">
|
<div v-else-if="dirtiness !== undefined">
|
||||||
<N8nTooltip :show-after="500" placement="bottom">
|
<N8nTooltip :show-after="500" placement="bottom">
|
||||||
@@ -94,8 +104,8 @@ const isNodeExecuting = computed(() => {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</template>
|
</template>
|
||||||
<div data-test-id="canvas-node-status-warning" :class="[$style.status, $style.warning]">
|
<div data-test-id="canvas-node-status-warning" :class="[...commonClasses, $style.warning]">
|
||||||
<N8nIcon icon="triangle" />
|
<N8nIcon icon="triangle" :size="size" />
|
||||||
<span v-if="runDataIterations > 1" :class="$style.count"> {{ runDataIterations }}</span>
|
<span v-if="runDataIterations > 1" :class="$style.count"> {{ runDataIterations }}</span>
|
||||||
</div>
|
</div>
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
@@ -103,9 +113,9 @@ const isNodeExecuting = computed(() => {
|
|||||||
<div
|
<div
|
||||||
v-else-if="hasRunData"
|
v-else-if="hasRunData"
|
||||||
data-test-id="canvas-node-status-success"
|
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>
|
<span v-if="runDataIterations > 1" :class="$style.count"> {{ runDataIterations }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -130,26 +140,25 @@ const isNodeExecuting = computed(() => {
|
|||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-waiting-spinner,
|
||||||
.running {
|
.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%;
|
width: 100%;
|
||||||
height: 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;
|
position: absolute;
|
||||||
left: -34px;
|
left: 0;
|
||||||
top: -34px;
|
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 {
|
.issues {
|
||||||
@@ -164,4 +173,8 @@ const isNodeExecuting = computed(() => {
|
|||||||
.warning {
|
.warning {
|
||||||
color: var(--color-warning);
|
color: var(--color-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
color: var(--color-foreground-xdark);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const {
|
|||||||
disabled,
|
disabled,
|
||||||
readOnly,
|
readOnly,
|
||||||
class: cls,
|
class: cls,
|
||||||
|
isExperimentalNdvActive,
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -24,6 +25,7 @@ const {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
class?: string;
|
class?: string;
|
||||||
|
isExperimentalNdvActive: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const style = useCssModule();
|
const style = useCssModule();
|
||||||
@@ -32,6 +34,7 @@ const containerClass = computed(() => ({
|
|||||||
[style.container]: true,
|
[style.container]: true,
|
||||||
[style.interactive]: !disabled && !readOnly,
|
[style.interactive]: !disabled && !readOnly,
|
||||||
[style.hovered]: !!hovered,
|
[style.hovered]: !!hovered,
|
||||||
|
[style.isExperimentalNdvActive]: isExperimentalNdvActive,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -126,6 +129,8 @@ async function handleClickExecute() {
|
|||||||
transition:
|
transition:
|
||||||
translate 0.1s ease-in,
|
translate 0.1s ease-in,
|
||||||
opacity 0.1s ease-in;
|
opacity 0.1s ease-in;
|
||||||
|
transform: scale(var(--canvas-zoom-compensation-factor, 1));
|
||||||
|
transform-origin: center right;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.interactive.hovered button {
|
&.interactive.hovered button {
|
||||||
@@ -133,6 +138,10 @@ async function handleClickExecute() {
|
|||||||
translate: 0 0;
|
translate: 0 0;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.isExperimentalNdvActive {
|
||||||
|
height: var(--spacing-2xl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bolt {
|
.bolt {
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
|||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const { nodeId, noWheel, isReadOnly } = defineProps<{
|
const { nodeId, noWheel, isReadOnly, subTitle } = defineProps<{
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
noWheel?: boolean;
|
noWheel?: boolean;
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
|
subTitle?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineSlots<{ actions?: {} }>();
|
defineSlots<{ actions?: {} }>();
|
||||||
@@ -40,6 +41,7 @@ function handleValueChanged(parameterData: IUpdateInformation) {
|
|||||||
:input-size="0"
|
:input-size="0"
|
||||||
is-embedded-in-canvas
|
is-embedded-in-canvas
|
||||||
:no-wheel="noWheel"
|
:no-wheel="noWheel"
|
||||||
|
:sub-title="subTitle"
|
||||||
@value-changed="handleValueChanged"
|
@value-changed="handleValueChanged"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<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">
|
<script setup lang="ts">
|
||||||
import InputPanel from '@/components/InputPanel.vue';
|
|
||||||
import NodeTitle from '@/components/NodeTitle.vue';
|
|
||||||
import { ExpressionLocalResolveContextSymbol } from '@/constants';
|
import { ExpressionLocalResolveContextSymbol } from '@/constants';
|
||||||
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
|
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import type { ExpressionLocalResolveContext } from '@/types/expressions';
|
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 { useVueFlow } from '@vue-flow/core';
|
||||||
import { useActiveElement, watchOnce } from '@vueuse/core';
|
import { watchOnce } from '@vueuse/core';
|
||||||
import { computed, onBeforeUnmount, provide, ref, useTemplateRef, watch } from 'vue';
|
import { computed, onBeforeUnmount, provide, ref, useTemplateRef } from 'vue';
|
||||||
import { useExperimentalNdvStore } from '../experimentalNdv.store';
|
import { useExperimentalNdvStore } from '../experimentalNdv.store';
|
||||||
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
|
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import 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<{
|
const { nodeId, isReadOnly, isConfigurable } = defineProps<{
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@@ -20,6 +22,7 @@ const { nodeId, isReadOnly, isConfigurable } = defineProps<{
|
|||||||
isConfigurable: boolean;
|
isConfigurable: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
const experimentalNdvStore = useExperimentalNdvStore();
|
const experimentalNdvStore = useExperimentalNdvStore();
|
||||||
const isExpanded = computed(() => !experimentalNdvStore.collapsedNodes[nodeId]);
|
const isExpanded = computed(() => !experimentalNdvStore.collapsedNodes[nodeId]);
|
||||||
@@ -61,12 +64,14 @@ const isVisible = computed(() =>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
const isOnceVisible = ref(isVisible.value);
|
const isOnceVisible = ref(isVisible.value);
|
||||||
const shouldShowInputPanel = ref(false);
|
|
||||||
|
|
||||||
const containerRef = useTemplateRef('container');
|
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>(() => {
|
const expressionResolveCtx = computed<ExpressionLocalResolveContext | undefined>(() => {
|
||||||
if (!node.value) {
|
if (!node.value) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -118,38 +123,42 @@ function handleToggleExpand() {
|
|||||||
experimentalNdvStore.setNodeExpanded(nodeId);
|
experimentalNdvStore.setNodeExpanded(nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleOpenNdv() {
|
||||||
|
if (node.value) {
|
||||||
|
ndvStore.setActiveNodeName(node.value.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
provide(ExpressionLocalResolveContextSymbol, expressionResolveCtx);
|
provide(ExpressionLocalResolveContextSymbol, expressionResolveCtx);
|
||||||
|
|
||||||
watchOnce(isVisible, (visible) => {
|
watchOnce(isVisible, (visible) => {
|
||||||
isOnceVisible.value = isOnceVisible.value || 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="container"
|
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="{
|
:style="{
|
||||||
'--zoom': `${1 / experimentalNdvStore.maxCanvasZoom}`,
|
'--zoom': `${1 / experimentalNdvStore.maxCanvasZoom}`,
|
||||||
'--node-width-scaler': isConfigurable ? 1 : 1.5,
|
'--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
|
<ExperimentalCanvasNodeSettings
|
||||||
v-if="isExpanded"
|
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
:node-id="nodeId"
|
:node-id="nodeId"
|
||||||
:class="$style.settingsView"
|
: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 */
|
!isMoving /* to not interrupt panning while allowing scroll of the settings pane, allow wheel event while panning */
|
||||||
"
|
"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
|
:sub-title="subTitle"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<N8nIconButton
|
<ExperimentalEmbeddedNdvActions
|
||||||
icon="minimize-2"
|
:is-expanded="isExpanded"
|
||||||
type="secondary"
|
@open-ndv="handleOpenNdv"
|
||||||
text
|
@toggle-expand="handleToggleExpand"
|
||||||
size="mini"
|
|
||||||
icon-size="large"
|
|
||||||
aria-label="Toggle expand"
|
|
||||||
@click="handleToggleExpand"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ExperimentalCanvasNodeSettings>
|
</ExperimentalCanvasNodeSettings>
|
||||||
<div v-else role="button " :class="$style.collapsedContent" @click="handleToggleExpand">
|
</ExperimentalEmbeddedNdvMapper>
|
||||||
<NodeTitle
|
<div v-else role="button" :class="$style.collapsedContent" @click="handleToggleExpand">
|
||||||
v-if="node"
|
<NodeIcon :node-type="nodeType" :size="18" />
|
||||||
class="node-name"
|
<div :class="$style.collapsedNodeName">
|
||||||
:model-value="node.name"
|
<N8nText bold>
|
||||||
:node-type="nodeType"
|
{{ node.name }}
|
||||||
read-only
|
</N8nText>
|
||||||
/>
|
<N8nText bold size="small" color="text-light">
|
||||||
<N8nIcon icon="maximize-2" size="large" />
|
{{ subTitle }}
|
||||||
</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>
|
</N8nText>
|
||||||
</template>
|
|
||||||
</InputPanel>
|
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
<ExperimentalEmbeddedNdvActions
|
||||||
</template>
|
:is-expanded="isExpanded"
|
||||||
|
@open-ndv="handleOpenNdv"
|
||||||
|
@toggle-expand="handleToggleExpand"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -220,16 +204,20 @@ watch([activeElement, vf.getSelectedNodes], ([active, selected]) => {
|
|||||||
border-width: 1px !important;
|
border-width: 1px !important;
|
||||||
border-radius: var(--border-radius-base) !important;
|
border-radius: var(--border-radius-base) !important;
|
||||||
width: calc(var(--canvas-node--width) * var(--node-width-scaler));
|
width: calc(var(--canvas-node--width) * var(--node-width-scaler));
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
--canvas-node--border-color: var(--color-text-lighter);
|
||||||
|
|
||||||
&.expanded {
|
&.expanded {
|
||||||
cursor: default;
|
user-select: text;
|
||||||
|
cursor: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: min(calc(var(--canvas-node--height) * 2), 300px);
|
max-height: min(calc(var(--canvas-node--height) * 2), 300px);
|
||||||
min-height: var(--spacing-3xl);
|
min-height: var(--spacing-3xl);
|
||||||
}
|
}
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: var(--spacing-3xl);
|
height: var(--spacing-2xl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,27 +233,44 @@ watch([activeElement, vf.getSelectedNodes], ([active, selected]) => {
|
|||||||
:root .settingsView {
|
:root .settingsView {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: var(--border-radius-base);
|
|
||||||
|
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: calc(min(calc(var(--canvas-node--height) * 2), 300px) - var(--border-width-base) * 2);
|
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 {
|
.collapsedContent {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: var(--spacing-4xs);
|
||||||
gap: var(--spacing-s);
|
|
||||||
background-color: white;
|
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);
|
background-color: var(--color-background-xlight);
|
||||||
color: var(--color-text-base);
|
|
||||||
cursor: pointer;
|
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);
|
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>
|
</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 {
|
export function shouldShowParameter(item: INodeProperties): boolean {
|
||||||
return item.name.match(/resource|authentication|operation/i) === null;
|
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>;
|
isExecuting: Ref<boolean | undefined>;
|
||||||
connectingHandle: Ref<ConnectStartEvent | undefined>;
|
connectingHandle: Ref<ConnectStartEvent | undefined>;
|
||||||
viewport: Ref<ViewportTransform>;
|
viewport: Ref<ViewportTransform>;
|
||||||
|
isExperimentalNdvActive: ComputedRef<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CanvasNodeEventBusEvents = {
|
export type CanvasNodeEventBusEvents = {
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -2533,6 +2533,9 @@ importers:
|
|||||||
'@pinia/testing':
|
'@pinia/testing':
|
||||||
specifier: ^0.1.6
|
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))
|
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':
|
'@types/dateformat':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
|
|||||||
Reference in New Issue
Block a user