mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 10:31:15 +00:00
feat(editor): Add settings icons to the node on canvas (#15467)
This commit is contained in:
123
packages/frontend/editor-ui/src/components/NodeSettingsHint.vue
Normal file
123
packages/frontend/editor-ui/src/components/NodeSettingsHint.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { IconName } from '@n8n/design-system/src/components/N8nIcon/icons';
|
||||
|
||||
interface Props {
|
||||
node: INodeUi | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const i18n = useI18n();
|
||||
|
||||
const activeSettings = computed(() => {
|
||||
if (!props.node || props.node.disabled) {
|
||||
return props.node?.disabled
|
||||
? [
|
||||
{
|
||||
key: 'disabled',
|
||||
message: i18n.baseText('ndv.nodeHints.disabled'),
|
||||
icon: 'power-off' as IconName,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
const settings = [];
|
||||
|
||||
if (props.node.alwaysOutputData) {
|
||||
settings.push({
|
||||
key: 'alwaysOutputData',
|
||||
message: i18n.baseText('ndv.nodeHints.alwaysOutputData'),
|
||||
icon: 'always-output-data' as IconName,
|
||||
});
|
||||
}
|
||||
|
||||
if (props.node.executeOnce) {
|
||||
settings.push({
|
||||
key: 'executeOnce',
|
||||
message: i18n.baseText('ndv.nodeHints.executeOnce'),
|
||||
icon: 'execute-once' as IconName,
|
||||
});
|
||||
}
|
||||
|
||||
if (props.node.retryOnFail) {
|
||||
settings.push({
|
||||
key: 'retryOnFail',
|
||||
message: i18n.baseText('ndv.nodeHints.retryOnFail'),
|
||||
icon: 'retry-on-fail' as IconName,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
props.node.onError === 'continueRegularOutput' ||
|
||||
props.node.onError === 'continueErrorOutput'
|
||||
) {
|
||||
settings.push({
|
||||
key: 'continueOnError',
|
||||
message: i18n.baseText('ndv.nodeHints.continueOnError'),
|
||||
icon: 'continue-on-error' as IconName,
|
||||
});
|
||||
}
|
||||
|
||||
return settings;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="activeSettings.length > 0" :class="$style.settingsHint">
|
||||
<div v-for="setting in activeSettings" :key="setting.key" :class="$style.settingItem">
|
||||
<div :class="$style.iconWrapper">
|
||||
<FontAwesomeIcon v-if="setting.icon === 'power'" icon="power" :class="$style.icon" />
|
||||
<N8nIcon v-else :icon="setting.icon" :class="$style.icon" />
|
||||
</div>
|
||||
<N8nText size="small" :class="$style.message">
|
||||
{{ setting.message }}
|
||||
</N8nText>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.settingsHint {
|
||||
background-color: var(--color-callout-info-background);
|
||||
border-radius: var(--border-radius-base);
|
||||
border: var(--border-width-base) var(--border-style-base);
|
||||
border-color: var(--color-callout-info-border);
|
||||
color: var(--color-callout-info-font);
|
||||
margin-top: var(--spacing-2xs);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
margin-left: var(--spacing-s);
|
||||
margin-right: var(--spacing-s);
|
||||
padding: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.settingItem {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-xs);
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--color-callout-info-icon);
|
||||
font-size: var(--font-size-s);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.message {
|
||||
line-height: var(--font-line-height-regular);
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ViewableMimeTypes } from '@n8n/api-types';
|
||||
import { useStorage } from '@/composables/useStorage';
|
||||
import { saveAs } from 'file-saver';
|
||||
import NodeSettingsHint from '@/components/NodeSettingsHint.vue';
|
||||
import type {
|
||||
IBinaryData,
|
||||
IConnectedNode,
|
||||
@@ -818,7 +819,6 @@ function getNodeHints(): NodeHint[] {
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function onItemHover(itemIndex: number | null) {
|
||||
if (itemIndex === null) {
|
||||
emit('itemHover', null);
|
||||
@@ -1575,7 +1575,7 @@ defineExpose({ enterEditMode });
|
||||
</slot>
|
||||
</N8nCallout>
|
||||
</div>
|
||||
|
||||
<NodeSettingsHint v-if="props.paneType === 'output'" :node="node" />
|
||||
<N8nCallout
|
||||
v-for="hint in getNodeHints()"
|
||||
:key="hint.message"
|
||||
@@ -2347,6 +2347,42 @@ defineExpose({ enterEditMode });
|
||||
padding: 0 var(--ndv-spacing);
|
||||
}
|
||||
|
||||
.messageSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.singleIcon {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.multipleIcons {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-2xs, 8px);
|
||||
}
|
||||
|
||||
.multipleIcons .iconStack {
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.iconStack {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4xs, 4px);
|
||||
flex-shrink: 0;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--color-callout-info-icon);
|
||||
line-height: 1;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.executingMessage {
|
||||
.compact & {
|
||||
color: var(--color-text-light);
|
||||
|
||||
@@ -165,6 +165,7 @@ exports[`InputPanel > should render 1`] = `
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
|
||||
|
||||
<!--v-if-->
|
||||
|
||||
@@ -4,12 +4,17 @@ import { createCanvasNodeProvide, createCanvasProvide } from '@/__tests__/data';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { CanvasNodeRenderType } from '@/types';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { createTestWorkflowObject } from '@/__tests__/mocks';
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasNodeRenderer);
|
||||
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow = vi.fn().mockReturnValue(workflowObject);
|
||||
});
|
||||
|
||||
describe('CanvasNodeRenderer', () => {
|
||||
|
||||
@@ -6,6 +6,8 @@ import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
||||
import { fireEvent } from '@testing-library/vue';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { createTestWorkflowObject } from '@/__tests__/mocks';
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasNodeDefault, {
|
||||
global: {
|
||||
@@ -18,6 +20,9 @@ const renderComponent = createComponentRenderer(CanvasNodeDefault, {
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow = vi.fn().mockReturnValue(workflowObject);
|
||||
});
|
||||
|
||||
describe('CanvasNodeDefault', () => {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useI18n } from '@n8n/i18n';
|
||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
import type { CanvasNodeDefaultRender } from '@/types';
|
||||
import { useCanvas } from '@/composables/useCanvas';
|
||||
import CanvasNodeSettingsIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeSettingsIcons.vue';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
import { calculateNodeSize } from '@/utils/nodeViewUtils';
|
||||
import ExperimentalInPlaceNodeSettings from '@/components/canvas/experimental/components/ExperimentalEmbeddedNodeDetails.vue';
|
||||
|
||||
@@ -43,6 +45,7 @@ const { mainOutputs, mainOutputConnections, mainInputs, mainInputConnections, no
|
||||
connections,
|
||||
});
|
||||
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
|
||||
|
||||
const classes = computed(() => {
|
||||
@@ -153,6 +156,13 @@ function onActivate(event: MouseEvent) {
|
||||
:disabled="isDisabled"
|
||||
:class="$style.icon"
|
||||
/>
|
||||
<CanvasNodeSettingsIcons
|
||||
v-if="
|
||||
!renderOptions.configuration &&
|
||||
!isDisabled &&
|
||||
!(hasPinnedData && !nodeHelpers.isProductionExecutionPreview.value)
|
||||
"
|
||||
/>
|
||||
<CanvasNodeDisabledStrikeThrough v-if="isStrikethroughVisible" />
|
||||
<div :class="$style.description">
|
||||
<div v-if="label" :class="$style.label">
|
||||
|
||||
@@ -25,6 +25,14 @@ exports[`CanvasNodeDefault > configurable > should render configurable node corr
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="settingsIcons"
|
||||
>
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div>
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="description"
|
||||
@@ -71,6 +79,7 @@ exports[`CanvasNodeDefault > configuration > should render configurable configur
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="description"
|
||||
>
|
||||
@@ -116,6 +125,7 @@ exports[`CanvasNodeDefault > configuration > should render configuration node co
|
||||
</div>
|
||||
</div>
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="description"
|
||||
>
|
||||
@@ -160,6 +170,14 @@ exports[`CanvasNodeDefault > should render node correctly 1`] = `
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="settingsIcons"
|
||||
>
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div>
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="description"
|
||||
@@ -205,6 +223,14 @@ exports[`CanvasNodeDefault > trigger > should render trigger node correctly 1`]
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="settingsIcons"
|
||||
>
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div>
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="description"
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { N8nIcon } from '@n8n/design-system';
|
||||
|
||||
const { name } = useCanvasNode();
|
||||
const i18n = useI18n();
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
|
||||
const workflow = computed(() => workflowHelpers.getCurrentWorkflow());
|
||||
const node = computed(() => workflow.value.getNode(name.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.settingsIcons">
|
||||
<N8nTooltip v-if="node?.alwaysOutputData">
|
||||
<template #content>
|
||||
<div :class="$style.tooltipHeader">
|
||||
<N8nIcon icon="always-output-data" />
|
||||
<strong :class="$style.tooltipTitle">{{
|
||||
i18n.baseText('nodeSettings.alwaysOutputData.displayName')
|
||||
}}</strong>
|
||||
</div>
|
||||
<div>
|
||||
{{ i18n.baseText('node.settings.alwaysOutputData') }}
|
||||
</div>
|
||||
</template>
|
||||
<div data-test-id="canvas-node-status-always-output-data">
|
||||
<N8nIcon icon="always-output-data" />
|
||||
</div>
|
||||
</N8nTooltip>
|
||||
|
||||
<N8nTooltip v-if="node?.executeOnce">
|
||||
<template #content>
|
||||
<div :class="$style.tooltipHeader">
|
||||
<N8nIcon icon="execute-once" />
|
||||
<strong :class="$style.tooltipTitle">{{
|
||||
i18n.baseText('nodeSettings.executeOnce.displayName')
|
||||
}}</strong>
|
||||
</div>
|
||||
<div>
|
||||
{{ i18n.baseText('node.settings.executeOnce') }}
|
||||
</div>
|
||||
</template>
|
||||
<div data-test-id="canvas-node-status-execute-once">
|
||||
<N8nIcon icon="execute-once" />
|
||||
</div>
|
||||
</N8nTooltip>
|
||||
|
||||
<N8nTooltip v-if="node?.retryOnFail">
|
||||
<template #content>
|
||||
<div :class="$style.tooltipHeader">
|
||||
<N8nIcon icon="retry-on-fail" />
|
||||
<strong :class="$style.tooltipTitle">{{
|
||||
i18n.baseText('nodeSettings.retryOnFail.displayName')
|
||||
}}</strong>
|
||||
</div>
|
||||
<div>
|
||||
{{ i18n.baseText('node.settings.retriesOnFailure') }}
|
||||
</div>
|
||||
</template>
|
||||
<div data-test-id="canvas-node-status-retry-on-fail">
|
||||
<N8nIcon icon="retry-on-fail" />
|
||||
</div>
|
||||
</N8nTooltip>
|
||||
|
||||
<N8nTooltip
|
||||
v-if="node?.onError === 'continueRegularOutput' || node?.onError === 'continueErrorOutput'"
|
||||
>
|
||||
<template #content>
|
||||
<div :class="$style.tooltipHeader">
|
||||
<N8nIcon icon="continue-on-error" />
|
||||
<strong :class="$style.tooltipTitle">{{
|
||||
i18n.baseText('node.settings.continuesOnError.title')
|
||||
}}</strong>
|
||||
</div>
|
||||
<div>
|
||||
{{ i18n.baseText('node.settings.continuesOnError') }}
|
||||
</div>
|
||||
</template>
|
||||
<div data-test-id="canvas-node-status-continue-on-error">
|
||||
<N8nIcon icon="continue-on-error" />
|
||||
</div>
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.settingsIcons {
|
||||
position: absolute;
|
||||
top: var(--canvas-node--status-icons-offset);
|
||||
right: var(--canvas-node--status-icons-offset);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.tooltipHeader {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.tooltipTitle {
|
||||
font-weight: 600;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user