diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 7f7e445fde074..e869bb13cd748 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -850,6 +850,12 @@ export interface ITemplatesNode extends IVersionNode { export interface INodeMetadata { parametersLastUpdatedAt?: number; + /** + * UNIX timestamp of either when existing pinned data is modified or removed. + * + * Note that pinning data for the first time isn't supposed to set this field. + */ + pinnedDataUpdatedAt?: number; pristine: boolean; } diff --git a/packages/editor-ui/src/components/InputPanel.vue b/packages/editor-ui/src/components/InputPanel.vue index 41b85e33890cd..caa605f28f07c 100644 --- a/packages/editor-ui/src/components/InputPanel.vue +++ b/packages/editor-ui/src/components/InputPanel.vue @@ -22,6 +22,7 @@ import InputNodeSelect from './InputNodeSelect.vue'; import NodeExecuteButton from './NodeExecuteButton.vue'; import RunData from './RunData.vue'; import WireMeUp from './WireMeUp.vue'; +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; type MappingMode = 'debugging' | 'mapping'; @@ -464,7 +465,16 @@ function activatePane() { /> - {{ i18n.baseText('ndv.input.noOutputData.hint') }} + + +
diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index 49cb19ca71ae2..43ee007edb37a 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -765,7 +765,7 @@ const credentialSelected = (updateInformation: INodeUpdatePropertiesInformation) const nameChanged = (name: string) => { if (node.value) { - historyStore.pushCommandToUndo(new RenameNodeCommand(node.value.name, name)); + historyStore.pushCommandToUndo(new RenameNodeCommand(node.value.name, name, Date.now())); } valueChanged({ value: name, diff --git a/packages/editor-ui/src/components/canvas/elements/edges/CanvasEdge.vue b/packages/editor-ui/src/components/canvas/elements/edges/CanvasEdge.vue index 385ef7f834e1e..7600d0fb2cee9 100644 --- a/packages/editor-ui/src/components/canvas/elements/edges/CanvasEdge.vue +++ b/packages/editor-ui/src/components/canvas/elements/edges/CanvasEdge.vue @@ -67,6 +67,8 @@ const edgeColor = computed(() => { return 'var(--node-type-supplemental-color)'; } else if (props.selected) { return 'var(--color-background-dark)'; + } else if (status.value === 'warning') { + return 'var(--color-warning)'; } else { return 'var(--color-foreground-xdark)'; } diff --git a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue index 89c83b4887c54..9cc7cebf248c4 100644 --- a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue +++ b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue @@ -41,7 +41,13 @@ const runDataLabel = computed(() => const isHandlePlusVisible = computed(() => !isConnecting.value || isHovered.value); -const plusType = computed(() => (runDataTotal.value > 0 ? 'success' : 'default')); +const plusType = computed(() => + renderOptions.value.dirtiness !== undefined + ? 'warning' + : runDataTotal.value > 0 + ? 'success' + : 'default', +); const plusLineSize = computed( () => @@ -60,6 +66,7 @@ const outputLabelClasses = computed(() => ({ const runDataLabelClasses = computed(() => ({ [$style.label]: true, [$style.runDataLabel]: true, + [$style.dirty]: renderOptions.value.dirtiness !== undefined, })); function onMouseEnter() { @@ -137,6 +144,10 @@ function onClickAdd() { transform: translate(-50%, -150%); font-size: var(--font-size-xs); color: var(--color-success); + + &.dirty { + color: var(--color-warning); + } } diff --git a/packages/editor-ui/src/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.vue b/packages/editor-ui/src/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.vue index 880a9a04017b2..c18f41e3cbe49 100644 --- a/packages/editor-ui/src/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.vue +++ b/packages/editor-ui/src/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.vue @@ -7,7 +7,7 @@ const props = withDefaults( handleClasses?: string; plusSize?: number; lineSize?: number; - type?: 'success' | 'secondary' | 'default'; + type?: 'success' | 'warning' | 'secondary' | 'default'; }>(), { position: 'right', @@ -163,6 +163,12 @@ function onClick(event: MouseEvent) { } } + &.warning { + .line { + stroke: var(--color-warning); + } + } + .plus { &:hover { cursor: pointer; diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue index c6b8b15d9cd3c..780cc24347a0c 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue @@ -64,6 +64,7 @@ const classes = computed(() => { [$style.configurable]: renderOptions.value.configurable, [$style.configuration]: renderOptions.value.configuration, [$style.trigger]: renderOptions.value.trigger, + [$style.warning]: renderOptions.value.dirtiness !== undefined, }; }); @@ -257,6 +258,10 @@ function openContextMenu(event: MouseEvent) { border-color: var(--color-canvas-node-success-border-color, var(--color-success)); } + &.warning { + border-color: var(--color-warning); + } + &.error { border-color: var(--color-canvas-node-error-border-color, var(--color-danger)); } diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.vue b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.vue index d292e8f951ea3..89684afd2f555 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.vue @@ -1,15 +1,19 @@ @@ -31,4 +35,8 @@ const classes = computed(() => { .success { border-color: var(--color-success-light); } + +.warning { + border-color: var(--color-warning-tint-1); +} diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.test.ts b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.test.ts index 55baf764620ba..8bdd32386d7d0 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.test.ts +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.test.ts @@ -2,6 +2,7 @@ import CanvasNodeStatusIcons from './CanvasNodeStatusIcons.vue'; import { createComponentRenderer } from '@/__tests__/render'; import { createCanvasNodeProvide } from '@/__tests__/data'; import { createTestingPinia } from '@pinia/testing'; +import { CanvasNodeRenderType } from '@/types'; const renderComponent = createComponentRenderer(CanvasNodeStatusIcons, { pinia: createTestingPinia(), @@ -51,4 +52,22 @@ describe('CanvasNodeStatusIcons', () => { expect(getByTestId('canvas-node-status-success')).toHaveTextContent('15'); }); + + it('should render correctly for a dirty node that has run successfully', () => { + const { getByTestId } = renderComponent({ + global: { + provide: createCanvasNodeProvide({ + data: { + runData: { outputMap: {}, iterations: 15, visible: true }, + render: { + type: CanvasNodeRenderType.Default, + options: { dirtiness: 'parameters-updated' }, + }, + }, + }), + }, + }); + + expect(getByTestId('canvas-node-status-warning')).toBeInTheDocument(); + }); }); diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue index aa80598baa06b..d77c7198e54c3 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue @@ -4,6 +4,8 @@ import TitledList from '@/components/TitledList.vue'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useCanvasNode } from '@/composables/useCanvasNode'; import { useI18n } from '@/composables/useI18n'; +import { CanvasNodeRenderType } from '@/types'; +import { N8nTooltip } from 'n8n-design-system'; const nodeHelpers = useNodeHelpers(); const i18n = useI18n(); @@ -18,9 +20,15 @@ const { hasRunData, runDataIterations, isDisabled, + render, } = useCanvasNode(); const hideNodeIssues = computed(() => false); // @TODO Implement this +const isParameterChanged = computed( + () => + render.value.type === CanvasNodeRenderType.Default && + render.value.options.dirtiness === 'parameters-updated', +);