From a156dfaf8fc0e91114fcb66a5faa27278a2838a0 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Thu, 26 May 2022 18:06:00 +0300 Subject: [PATCH 01/42] feat: Design system color improvements and button component redesign. --- .../components/N8nButton/Button.stories.js | 122 -------- .../components/N8nButton/Button.stories.ts | 192 ++++++++++++ .../src/components/N8nButton/Button.vue | 279 +++++++++--------- .../src/components/N8nButton/index.d.ts | 43 --- packages/design-system/theme/src/_tokens.scss | 22 +- packages/design-system/theme/src/button.scss | 16 +- .../design-system/theme/src/common/var.scss | 26 +- 7 files changed, 393 insertions(+), 307 deletions(-) delete mode 100644 packages/design-system/src/components/N8nButton/Button.stories.js create mode 100644 packages/design-system/src/components/N8nButton/Button.stories.ts delete mode 100644 packages/design-system/src/components/N8nButton/index.d.ts diff --git a/packages/design-system/src/components/N8nButton/Button.stories.js b/packages/design-system/src/components/N8nButton/Button.stories.js deleted file mode 100644 index 31a8b28d61f47..0000000000000 --- a/packages/design-system/src/components/N8nButton/Button.stories.js +++ /dev/null @@ -1,122 +0,0 @@ -import N8nButton from './Button.vue'; -import { action } from '@storybook/addon-actions'; - -export default { - title: 'Atoms/Button', - component: N8nButton, - argTypes: { - label: { - control: 'text', - }, - title: { - control: 'text', - }, - type: { - control: 'select', - options: ['primary', 'outline', 'light', 'text', 'tertiary'], - }, - size: { - control: { - type: 'select', - options: ['mini', 'small', 'medium', 'large', 'xlarge'], - }, - }, - loading: { - control: { - type: 'boolean', - }, - }, - icon: { - control: { - type: 'text', - }, - }, - circle: { - control: { - type: 'boolean', - }, - }, - fullWidth: { - type: 'boolean', - }, - theme: { - type: 'select', - options: ['success', 'danger', 'warning'], - }, - float: { - type: 'select', - options: ['left', 'right'], - }, - }, - parameters: { - design: { - type: 'figma', - url: 'https://www.figma.com/file/DxLbnIyMK8X0uLkUguFV4n/n8n-design-system_v1?node-id=5%3A1147', - }, - }, -}; - -const methods = { - onClick: action('click'), -}; - -const Template = (args, { argTypes }) => ({ - props: Object.keys(argTypes), - components: { - N8nButton, - }, - template: '', - methods, -}); - -export const Button = Template.bind({}); -Button.args = { - label: 'Button', -}; - -const ManyTemplate = (args, { argTypes }) => ({ - props: Object.keys(argTypes), - components: { - N8nButton, - }, - template: - '
', - methods, -}); - -export const Primary = ManyTemplate.bind({}); -Primary.args = { - type: 'primary', - label: 'Button', -}; - -export const Outline = ManyTemplate.bind({}); -Outline.args = { - type: 'outline', - label: 'Button', -}; - -export const Tertiary = ManyTemplate.bind({}); -Tertiary.args = { - type: 'tertiary', - label: 'Button', -}; - -export const Light = ManyTemplate.bind({}); -Light.args = { - type: 'light', - label: 'Button', -}; - -export const WithIcon = ManyTemplate.bind({}); -WithIcon.args = { - label: 'Button', - icon: 'plus-circle', -}; - -export const Text = ManyTemplate.bind({}); -Text.args = { - type: 'text', - label: 'Button', - icon: 'plus-circle', -}; diff --git a/packages/design-system/src/components/N8nButton/Button.stories.ts b/packages/design-system/src/components/N8nButton/Button.stories.ts new file mode 100644 index 0000000000000..ecf6d6734aba3 --- /dev/null +++ b/packages/design-system/src/components/N8nButton/Button.stories.ts @@ -0,0 +1,192 @@ +/* tslint:disable:variable-name */ +import N8nButton from './Button.vue'; +import { action } from '@storybook/addon-actions'; +import { StoryFn } from "@storybook/vue"; + +export default { + title: 'Atoms/Button', + component: N8nButton, + argTypes: { + label: { + control: 'text', + }, + title: { + control: 'text', + }, + type: { + control: 'select', + options: ['primary', 'outline', 'light', 'text', 'tertiary'], + }, + size: { + control: { + type: 'select', + options: ['mini', 'small', 'medium', 'large', 'xlarge'], + }, + }, + loading: { + control: { + type: 'boolean', + }, + }, + icon: { + control: { + type: 'text', + }, + }, + circle: { + control: { + type: 'boolean', + }, + }, + fullWidth: { + type: 'boolean', + }, + float: { + type: 'select', + options: ['left', 'right'], + }, + }, + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/DxLbnIyMK8X0uLkUguFV4n/n8n-design-system_v1?node-id=5%3A1147', + }, + }, +}; + +const methods = { + onClick: action('click'), +}; + +const Template: StoryFn = (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { + N8nButton, + }, + template: '', + methods, +}); + +export const Button = Template.bind({}); +Button.args = { + label: 'Button', +}; + +const AllSizesTemplate: StoryFn = (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { + N8nButton, + }, + template: `
+ + + + + +
`, + methods, +}); + +const AllColorsTemplate: StoryFn = (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { + N8nButton, + }, + template: `
+ + + + + + +
`, + methods, +}); + +const AllColorsAndSizesTemplate: StoryFn = (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { + N8nButton, + }, + template: `
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + +
`, + methods, +}); + +export const Primary = AllSizesTemplate.bind({}); +Primary.args = { + type: 'primary', + label: 'Button', +}; + +export const Secondary = AllSizesTemplate.bind({}); +Secondary.args = { + type: 'secondary', + label: 'Button', +}; + +export const Tertiary = AllSizesTemplate.bind({}); +Tertiary.args = { + type: 'tertiary', + label: 'Button', +}; + +export const Success = AllSizesTemplate.bind({}); +Success.args = { + type: 'success', + label: 'Button', +}; + +export const Warning = AllSizesTemplate.bind({}); +Warning.args = { + type: 'warning', + label: 'Button', +}; + +export const Danger = AllSizesTemplate.bind({}); +Danger.args = { + type: 'danger', + label: 'Button', +}; + +export const Outline = AllColorsAndSizesTemplate.bind({}); +Outline.args = { + outline: true, + label: 'Button', +}; + +export const Text = AllColorsAndSizesTemplate.bind({}); +Text.args = { + type: 'primary', + text: true, + label: 'Button', +}; + +export const WithIcon = AllSizesTemplate.bind({}); +WithIcon.args = { + label: 'Button', + icon: 'plus-circle', +}; + diff --git a/packages/design-system/src/components/N8nButton/Button.vue b/packages/design-system/src/components/N8nButton/Button.vue index 2e2c1fd3cd61a..c6561ac720e0e 100644 --- a/packages/design-system/src/components/N8nButton/Button.vue +++ b/packages/design-system/src/components/N8nButton/Button.vue @@ -10,6 +10,7 @@ :round="!props.circle && props.round" :circle="props.circle" :style="$options.styles(props)" + tabindex="0" @click="(e) => listeners.click && listeners.click(e)" > @@ -47,12 +48,7 @@ export default { type: String, default: 'primary', validator: (value: string): boolean => - ['primary', 'outline', 'light', 'text', 'tertiary'].includes(value), - }, - theme: { - type: String, - validator: (value: string): boolean => - ['success', 'warning', 'danger'].includes(value), + ['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger'].includes(value), }, size: { type: String, @@ -68,11 +64,19 @@ export default { type: Boolean, default: false, }, + outline: { + type: Boolean, + default: false, + }, + text: { + type: Boolean, + default: false, + }, icon: { }, round: { type: Boolean, - default: true, + default: false, }, circle: { type: Boolean, @@ -106,16 +110,11 @@ export default { ...(props.fullWidth ? { width: '100%' } : {}), }; }, - getClass(props: { type: string; theme?: string, transparentBackground: boolean }, $style: any): string { - const theme = props.type === 'text' || props.type === 'tertiary' - ? props.type - : `${props.type}-${props.theme || 'primary'}`; - - if (props.transparentBackground) { - return `${$style[theme]} ${$style['transparent']}`; - } - - return $style[theme]; + getClass(props: { type: string; outline: boolean; text: boolean; transparentBackground: boolean }, $style: any): string { + return `${$style['button']} ${$style[props.type]}` + + `${props.transparentBackground ? ` ${$style['transparent']}` : ''}` + + `${props.outline ? ` ${$style['outline']}` : ''}` + + `${props.text ? ` ${$style['text']}` : ''}`; }, }; @@ -123,22 +122,6 @@ export default { diff --git a/packages/design-system/src/components/N8nIconButton/IconButton.stories.js b/packages/design-system/src/components/N8nIconButton/IconButton.stories.ts similarity index 84% rename from packages/design-system/src/components/N8nIconButton/IconButton.stories.js rename to packages/design-system/src/components/N8nIconButton/IconButton.stories.ts index fe99328783644..1d66494061521 100644 --- a/packages/design-system/src/components/N8nIconButton/IconButton.stories.js +++ b/packages/design-system/src/components/N8nIconButton/IconButton.stories.ts @@ -1,5 +1,7 @@ +/* tslint:disable:variable-name */ import N8nIconButton from './IconButton.vue'; import { action } from '@storybook/addon-actions'; +import { StoryFn } from "@storybook/vue"; export default { title: 'Atoms/Icon Button', @@ -7,17 +9,17 @@ export default { argTypes: { type: { control: 'select', - options: ['primary', 'outline', 'light', 'text'], - }, - title: { - control: 'text', + options: ['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger'], }, size: { control: { type: 'select', - options: ['small', 'medium', 'large', 'xlarge'], + options: ['mini', 'small', 'medium', 'large', 'xlarge'], }, }, + title: { + control: 'text', + }, loading: { control: { type: 'boolean', @@ -28,12 +30,6 @@ export default { type: 'text', }, }, - theme: { - control: { - type: 'select', - options: ['success', 'warning', 'danger'], - }, - }, }, parameters: { backgrounds: { default: '--color-background-light' }, @@ -44,7 +40,7 @@ const methods = { onClick: action('click'), }; -const Template = (args, { argTypes }) => ({ +const Template: StoryFn = (args, { argTypes }) => ({ props: Object.keys(argTypes), components: { N8nIconButton, @@ -59,7 +55,7 @@ Button.args = { title: 'my title', }; -const ManyTemplate = (args, { argTypes }) => ({ +const ManyTemplate: StoryFn = (args, { argTypes }) => ({ props: Object.keys(argTypes), components: { N8nIconButton, diff --git a/packages/design-system/src/components/N8nIconButton/IconButton.vue b/packages/design-system/src/components/N8nIconButton/IconButton.vue index 987941bc60be9..58f5787196e80 100644 --- a/packages/design-system/src/components/N8nIconButton/IconButton.vue +++ b/packages/design-system/src/components/N8nIconButton/IconButton.vue @@ -1,17 +1,5 @@ - diff --git a/packages/editor-ui/src/components/TriggerPanel.vue b/packages/editor-ui/src/components/TriggerPanel.vue index 8dcdf14c5d4ef..ca403b87fe15c 100644 --- a/packages/editor-ui/src/components/TriggerPanel.vue +++ b/packages/editor-ui/src/components/TriggerPanel.vue @@ -10,8 +10,8 @@ {{ $locale.baseText('ndv.trigger.webhookNode.listening') }} -
- +
+ {{ $locale.baseText('ndv.trigger.webhookNode.requestHint', { interpolate: { type: this.webhookHttpMethod }, @@ -33,8 +33,8 @@ {{ $locale.baseText('ndv.trigger.webhookBasedNode.listening') }} -
- +
+ {{ $locale.baseText('ndv.trigger.webhookBasedNode.serviceHint', { interpolate: { service: serviceName }, diff --git a/packages/editor-ui/src/components/forms/CodeEditor.vue b/packages/editor-ui/src/components/forms/CodeEditor.vue new file mode 100644 index 0000000000000..cbede9326ac34 --- /dev/null +++ b/packages/editor-ui/src/components/forms/CodeEditor.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/packages/editor-ui/src/components/forms/index.ts b/packages/editor-ui/src/components/forms/index.ts new file mode 100644 index 0000000000000..345b928ff05d8 --- /dev/null +++ b/packages/editor-ui/src/components/forms/index.ts @@ -0,0 +1 @@ +export { default as CodeEditor } from './CodeEditor.vue'; diff --git a/packages/editor-ui/src/components/helpers.ts b/packages/editor-ui/src/components/helpers.ts index df3429756fe93..0817ba759a737 100644 --- a/packages/editor-ui/src/components/helpers.ts +++ b/packages/editor-ui/src/components/helpers.ts @@ -1,7 +1,7 @@ import { CORE_NODES_CATEGORY, ERROR_TRIGGER_NODE_TYPE, TEMPLATES_NODES_FILTER } from '@/constants'; import { INodeUi, ITemplatesNode } from '@/Interface'; import dateformat from 'dateformat'; -import { INodeTypeDescription } from 'n8n-workflow'; +import {IDataObject, INodeTypeDescription} from 'n8n-workflow'; const KEYWORDS_TO_FILTER = ['API', 'OAuth1', 'OAuth2']; const SI_SYMBOL = ['', 'k', 'M', 'G', 'T', 'P', 'E']; @@ -68,3 +68,9 @@ export function isString(value: unknown): value is string { export function isNumber(value: unknown): value is number { return typeof value === 'number'; } + +export function stringSizeInBytes(input: string | IDataObject | IDataObject[] | undefined): number { + if (input === undefined) return 0; + + return new Blob([typeof input === 'string' ? input : JSON.stringify(input)]).size; +} diff --git a/packages/editor-ui/src/components/mixins/nodeHelpers.ts b/packages/editor-ui/src/components/mixins/nodeHelpers.ts index 75c57cfc5f365..6fae0e4a9bf4c 100644 --- a/packages/editor-ui/src/components/mixins/nodeHelpers.ts +++ b/packages/editor-ui/src/components/mixins/nodeHelpers.ts @@ -47,10 +47,14 @@ export const nodeHelpers = mixins( return Object.keys(node.parameters).includes('nodeCredentialType'); }, + isObjectLiteral(maybeObject: unknown): maybeObject is { [key: string]: string } { + return typeof maybeObject === 'object' && maybeObject !== null && !Array.isArray(maybeObject); + }, + isCustomApiCallSelected (nodeValues: INodeParameters): boolean { const { parameters } = nodeValues; - if (!isObjectLiteral(parameters)) return false; + if (!this.isObjectLiteral(parameters)) return false; return ( parameters.resource !== undefined && parameters.resource.includes(CUSTOM_API_CALL_KEY) || @@ -73,11 +77,13 @@ export const nodeHelpers = mixins( // Returns all the issues of the node getNodeIssues (nodeType: INodeTypeDescription | null, node: INodeUi, ignoreIssues?: string[]): INodeIssues | null { + const pinDataNodeNames = Object.keys(this.$store.getters.pinData || {}); + let nodeIssues: INodeIssues | null = null; ignoreIssues = ignoreIssues || []; - if (node.disabled === true) { - // Ignore issues on disabled nodes + if (node.disabled === true || pinDataNodeNames.includes(node.name)) { + // Ignore issues on disabled and pindata nodes return null; } @@ -510,7 +516,3 @@ declare namespace HttpRequestNode { }; } } - -function isObjectLiteral(maybeObject: unknown): maybeObject is { [key: string]: string } { - return typeof maybeObject === 'object' && maybeObject !== null && !Array.isArray(maybeObject); -} diff --git a/packages/editor-ui/src/components/mixins/pinData.ts b/packages/editor-ui/src/components/mixins/pinData.ts new file mode 100644 index 0000000000000..d558bd7d84306 --- /dev/null +++ b/packages/editor-ui/src/components/mixins/pinData.ts @@ -0,0 +1,85 @@ +import Vue from 'vue'; +import { INodeUi } from "@/Interface"; +import {IDataObject, PinData} from "n8n-workflow"; +import {stringSizeInBytes} from "@/components/helpers"; +import {MAX_WORKFLOW_PINNED_DATA_SIZE, PIN_DATA_NODE_TYPES_DENYLIST} from "@/constants"; + +interface PinDataContext { + node: INodeUi; + $showError(error: Error, title: string): void; +} + +export const pinData = (Vue as Vue.VueConstructor).extend({ + computed: { + pinData (): PinData[string] | undefined { + return this.node ? this.$store.getters['pinDataByNodeName'](this.node!.name) : undefined; + }, + hasPinData (): boolean { + return !!this.node && typeof this.pinData !== 'undefined'; + }, + isPinDataNodeType(): boolean { + return !!this.node && !PIN_DATA_NODE_TYPES_DENYLIST.includes(this.node.type); + }, + }, + methods: { + isValidPinDataJSON(data: string): boolean { + try { + JSON.parse(data); + + return true; + } catch (error) { + const title = this.$locale.baseText('runData.editOutputInvalid'); + + const toRemove = new RegExp(/JSON\.parse:|of the JSON data/, 'g'); + const message = error.message.replace(toRemove, '').trim(); + const positionMatchRegEx = /at position (\d+)/; + const positionMatch = error.message.match(positionMatchRegEx); + + error.message = message.charAt(0).toUpperCase() + message.slice(1); + error.message = error.message.replace( + 'Unexpected token \' in JSON', + this.$locale.baseText('runData.editOutputInvalid.singleQuote'), + ); + + if (positionMatch) { + const position = parseInt(positionMatch[1], 10); + const lineBreaksUpToPosition = (data.slice(0, position).match(/\n/g) || []).length; + + error.message = error.message.replace(positionMatchRegEx, + this.$locale.baseText('runData.editOutputInvalid.atPosition', { + interpolate: { + position: `${position}`, + }, + }), + ); + + error.message = `${ + this.$locale.baseText('runData.editOutputInvalid.onLine', { + interpolate: { + line: `${lineBreaksUpToPosition + 1}`, + }, + }) + } ${error.message}`; + } + + this.$showError(error, title); + + return false; + } + }, + isValidPinDataSize(data: string | object): boolean { + if (typeof data === 'object') data = JSON.stringify(data); + + if (this.$store.getters['pinDataSize'] + stringSizeInBytes(data) > MAX_WORKFLOW_PINNED_DATA_SIZE) { + this.$showError( + new Error(this.$locale.baseText('ndv.pinData.error.tooLarge.description')), + this.$locale.baseText('ndv.pinData.error.tooLarge.title'), + ); + + return false; + } + + return true; + }, + }, +}); diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index 6fc7e5f83e425..cb21cdd8f1105 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -328,6 +328,7 @@ export const workflowHelpers = mixins( const data: IWorkflowData = { name: this.$store.getters.workflowName, nodes, + pinData: this.$store.getters.pinData, connections: workflowConnections, active: this.$store.getters.isActive, settings: this.$store.getters.workflowSettings, diff --git a/packages/editor-ui/src/components/mixins/workflowRun.ts b/packages/editor-ui/src/components/mixins/workflowRun.ts index ee243467ce1d9..135823446d2be 100644 --- a/packages/editor-ui/src/components/mixins/workflowRun.ts +++ b/packages/editor-ui/src/components/mixins/workflowRun.ts @@ -188,6 +188,7 @@ export const workflowRun = mixins( const startRunData: IStartRunData = { workflowData, runData: newRunData, + pinData: workflowData.pinData, startNodes, }; if (nodeName) { @@ -208,6 +209,7 @@ export const workflowRun = mixins( data: { resultData: { runData: newRunData || {}, + pinData: workflowData.pinData, startNodes, workflowData, }, diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 56bab6a9d7396..6986ba81a5f68 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -1,3 +1,5 @@ +export const MAX_WORKFLOW_SIZE = 16777216; // Workflow size limit in bytes +export const MAX_WORKFLOW_PINNED_DATA_SIZE = 12582912; // Workflow pinned data size limit in bytes export const MAX_DISPLAY_DATA_SIZE = 204800; export const MAX_DISPLAY_ITEMS_AUTO_ALL = 250; export const NODE_NAME_PREFIX = 'node-'; @@ -47,6 +49,7 @@ export const BREAKPOINT_XL = 1920; export const N8N_IO_BASE_URL = `https://api.n8n.io/api/`; +export const DATA_PINNING_DOCS_URL = 'https://docs.n8n.io/data/data-pinning/'; // node types export const BAMBOO_HR_NODE_TYPE = 'n8n-nodes-base.bambooHr'; @@ -78,6 +81,7 @@ export const SET_NODE_TYPE = 'n8n-nodes-base.set'; export const SERVICENOW_NODE_TYPE = 'n8n-nodes-base.serviceNow'; export const SLACK_NODE_TYPE = 'n8n-nodes-base.slack'; export const SPREADSHEET_FILE_NODE_TYPE = 'n8n-nodes-base.spreadsheetFile'; +export const SPLIT_IN_BATCHES_NODE_TYPE = 'n8n-nodes-base.splitInBatches'; export const START_NODE_TYPE = 'n8n-nodes-base.start'; export const SWITCH_NODE_TYPE = 'n8n-nodes-base.switch'; export const THE_HIVE_TRIGGER_NODE_TYPE = 'n8n-nodes-base.theHiveTrigger'; @@ -89,6 +93,16 @@ export const XERO_NODE_TYPE = 'n8n-nodes-base.xero'; export const ZENDESK_NODE_TYPE = 'n8n-nodes-base.zendesk'; export const ZENDESK_TRIGGER_NODE_TYPE = 'n8n-nodes-base.zendeskTrigger'; +export const MULTIPLE_OUTPUT_NODE_TYPES = [ + IF_NODE_TYPE, + SWITCH_NODE_TYPE, +]; + +export const PIN_DATA_NODE_TYPES_DENYLIST = [ + ...MULTIPLE_OUTPUT_NODE_TYPES, + SPLIT_IN_BATCHES_NODE_TYPE, +]; + // Node creator export const CORE_NODES_CATEGORY = 'Core Nodes'; export const CUSTOM_NODES_CATEGORY = 'Custom Nodes'; @@ -193,6 +207,8 @@ export const MODAL_CONFIRMED = 'confirmed'; export const VALID_EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; export const LOCAL_STORAGE_ACTIVATION_FLAG = 'N8N_HIDE_ACTIVATION_ALERT'; +export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG = 'N8N_PIN_DATA_DISCOVERY_NDV'; +export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG = 'N8N_PIN_DATA_DISCOVERY_CANVAS'; export const BASE_NODE_SURVEY_URL = 'https://n8n-community.typeform.com/to/BvmzxqYv#nodename='; export const HIRING_BANNER = ` @@ -242,3 +258,14 @@ export enum VIEWS { API_SETTINGS = "APISettings", NOT_FOUND = "NotFoundView", } + +export const TEST_PIN_DATA = [ + { + name: "First item", + code: 1, + }, + { + name: "Second item", + code: 2, + }, +]; diff --git a/packages/editor-ui/src/event-bus/data-pinning-event-bus.ts b/packages/editor-ui/src/event-bus/data-pinning-event-bus.ts new file mode 100644 index 0000000000000..7eccde2710e4e --- /dev/null +++ b/packages/editor-ui/src/event-bus/data-pinning-event-bus.ts @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export const dataPinningEventBus = new Vue(); diff --git a/packages/editor-ui/src/modules/ui.ts b/packages/editor-ui/src/modules/ui.ts index 60a0891c0a4c5..1994083feb745 100644 --- a/packages/editor-ui/src/modules/ui.ts +++ b/packages/editor-ui/src/modules/ui.ts @@ -96,6 +96,10 @@ const module: Module = { }, output: { displayMode: 'table', + editMode: { + enabled: false, + value: '', + }, }, }, mainPanelPosition: 0.5, @@ -126,6 +130,7 @@ const module: Module = { }, inputPanelDispalyMode: (state: IUiState) => state.ndv.input.displayMode, outputPanelDispalyMode: (state: IUiState) => state.ndv.output.displayMode, + outputPanelEditMode: (state: IUiState): IUiState['ndv']['output']['editMode'] => state.ndv.output.editMode, mainPanelPosition: (state: IUiState) => state.mainPanelPosition, }, mutations: { @@ -170,6 +175,12 @@ const module: Module = { setPanelDisplayMode: (state: IUiState, params: {pane: 'input' | 'output', mode: IRunDataDisplayMode}) => { Vue.set(state.ndv[params.pane], 'displayMode', params.mode); }, + setOutputPanelEditModeEnabled: (state: IUiState, payload: boolean) => { + Vue.set(state.ndv.output.editMode, 'enabled', payload); + }, + setOutputPanelEditModeValue: (state: IUiState, payload: string) => { + Vue.set(state.ndv.output.editMode, 'value', payload); + }, setMainPanelRelativePosition(state: IUiState, relativePosition: number) { state.mainPanelPosition = relativePosition; }, diff --git a/packages/editor-ui/src/plugins/components.ts b/packages/editor-ui/src/plugins/components.ts index 1618c58392159..3b688763b3a64 100644 --- a/packages/editor-ui/src/plugins/components.ts +++ b/packages/editor-ui/src/plugins/components.ts @@ -50,6 +50,7 @@ import { N8nActionToggle, N8nButton, N8nElButton, + N8nCallout, N8nCard, N8nIcon, N8nIconButton, @@ -90,6 +91,7 @@ Vue.use(N8nActionToggle); Vue.use(N8nAvatar); Vue.component('n8n-button', N8nButton); Vue.component('el-button', N8nElButton); +Vue.component('n8n-callout', N8nCallout); Vue.component('n8n-card', N8nCard); Vue.component('n8n-form-box', N8nFormBox); Vue.component('n8n-form-inputs', N8nFormInputs); diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 5aaaec4135b31..3b91115711473 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -9,6 +9,7 @@ "generic.cancel": "Cancel", "generic.delete": "Delete", "generic.copy": "Copy", + "generic.or": "or", "generic.clickToCopy": "Click to copy", "generic.copiedToClipboard": "Copied to clipboard", "generic.beta": "beta", @@ -332,7 +333,10 @@ "ndv.input.notConnected.title": "Wire me up", "ndv.input.notConnected.message": "This node can only receive input data if you connect it to another node.", "ndv.input.notConnected.learnMore": "Learn more", + "ndv.input.disabled": "The '{nodeName}' node is disabled and won’t execute.", + "ndv.input.disabled.cta": "Enable it", "ndv.output": "Output", + "ndv.output.edit": "Edit Output", "ndv.output.all": "all", "ndv.output.branch": "Branch", "ndv.output.executing": "Executing node...", @@ -346,7 +350,9 @@ "ndv.output.pageSize": "Page Size", "ndv.output.run": "Run", "ndv.output.runNodeHint": "Execute this node to output data", - "ndv.output.staleDataWarning": "Node parameters have changed.
Execute node again to refresh output.", + "ndv.output.insertTestData": "insert test data", + "ndv.output.staleDataWarning.regular": "Node parameters have changed.
Execute node again to refresh output.", + "ndv.output.staleDataWarning.pinData": "Node parameter changes will not affect pinned output data.", "ndv.output.tooMuchData.message": "The node contains {size} MB of data. Displaying it may cause problems.
If you do decide to display it, avoid the JSON view.", "ndv.output.tooMuchData.showDataAnyway": "Show data anyway", "ndv.output.tooMuchData.title": "Output data is huge!", @@ -354,6 +360,20 @@ "ndv.title.cancel": "Cancel", "ndv.title.rename": "Rename", "ndv.title.renameNode": "Rename node", + "ndv.pinData.pin.title": "Pin data", + "ndv.pinData.pin.description": "Node will always output this data instead of executing. You can also pin data from previous executions.", + "ndv.pinData.pin.link": "More info", + "ndv.pinData.pin.multipleRuns.title": "Run #{index} was pinned", + "ndv.pinData.pin.multipleRuns.description": "This run will be outputted each time the node is run.", + "ndv.pinData.unpinAndExecute.title": "Unpin output data?", + "ndv.pinData.unpinAndExecute.description": "Executing a node overwrites pinned data.", + "ndv.pinData.unpinAndExecute.cancel": "Cancel", + "ndv.pinData.unpinAndExecute.confirm": "Unpin and execute", + "ndv.pinData.beforeClosing.title": "Save output changes before closing?", + "ndv.pinData.beforeClosing.cancel": "Discard", + "ndv.pinData.beforeClosing.confirm": "Save", + "ndv.pinData.error.tooLarge.title": "Output data is too large to pin", + "ndv.pinData.error.tooLarge.description": "You can pin at most 12MB of output per workflow.", "noTagsView.readyToOrganizeYourWorkflows": "Ready to organize your workflows?", "noTagsView.withWorkflowTagsYouReFree": "With workflow tags, you're free to create the perfect tagging system for your flows", "node.activateDeactivateNode": "Activate/Deactivate Node", @@ -367,6 +387,8 @@ "node.nodeIsWaitingTill": "Node is waiting until {date} {time}", "node.theNodeIsWaitingIndefinitelyForAnIncomingWebhookCall": "The node is waiting for an incoming webhook call (indefinitely)", "node.waitingForYouToCreateAnEventIn": "Waiting for you to create an event in {nodeType}", + "node.discovery.pinData.canvas": "You can pin this output instead of waiting for a test event. Open node to do so.", + "node.discovery.pinData.ndv": "You can pin this output instead of waiting for a test event.", "nodeBase.clickToAddNodeOrDragToConnect": "Click to add node
or drag to connect", "nodeCreator.categoryNames.analytics": "Analytics", "nodeCreator.categoryNames.communication": "Communication", @@ -629,14 +651,25 @@ "runData.unlinking.hint": "Unlink displayed input and output runs", "runData.binary": "Binary", "runData.copyItemPath": "Copy Item Path", + "runData.copyItemPath.toast": "Item path copied", "runData.copyParameterPath": "Copy Parameter Path", + "runData.copyParameterPath.toast": "Parameter path copied", + "runData.copyValue": "Copy Selection", + "runData.copyValue.toast": "Output data copied", "runData.copyToClipboard": "Copy to Clipboard", - "runData.copyValue": "Copy Value", + "runData.copyDisabled": "First click on the output data you want to copy, then click this button.", + "runData.editOutput": "Edit Output", + "runData.editOutputInvalid": "Problem with output data", + "runData.editOutputInvalid.singleQuote": "Unexpected single quote. Please use double quotes (\") instead", + "runData.editOutputInvalid.onLine": "On line {line}:", + "runData.editOutputInvalid.atPosition": "(at position {position})", + "runData.editValue": "Edit Value", "runData.downloadBinaryData": "Download", "runData.executeNode": "Execute Node", "runData.executionTime": "Execution Time", "runData.fileExtension": "File Extension", "runData.fileName": "File Name", + "runData.invalidPinnedData": "Invalid pinned data", "runData.items": "Items", "runData.json": "JSON", "runData.mimeType": "Mime Type", @@ -649,6 +682,12 @@ "runData.showBinaryData": "View", "runData.startTime": "Start Time", "runData.table": "Table", + "runData.pindata.learnMore": "Learn more", + "runData.pindata.thisDataIsPinned": "This data is pinned.", + "runData.pindata.unpin": "Unpin", + "runData.editor.save": "Save", + "runData.editor.cancel": "Cancel", + "runData.editor.copyDataInfo": "You can copy data from previous executions and paste it above.", "saveButton.save": "@:_reusableBaseText.save", "saveButton.saved": "Saved", "saveButton.saving": "Saving", diff --git a/packages/editor-ui/src/plugins/icons.ts b/packages/editor-ui/src/plugins/icons.ts index c7d46714f1d79..61ea7d1830366 100644 --- a/packages/editor-ui/src/plugins/icons.ts +++ b/packages/editor-ui/src/plugins/icons.ts @@ -90,6 +90,7 @@ import { faTasks, faTerminal, faThLarge, + faThumbtack, faTimes, faTrash, faUndo, @@ -199,6 +200,7 @@ addIcon(faTable); addIcon(faTasks); addIcon(faTerminal); addIcon(faThLarge); +addIcon(faThumbtack); addIcon(faTimes); addIcon(faTrash); addIcon(faUndo); diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 26ac7bf79cfdb..4c1563f23e743 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -2,7 +2,10 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import { PLACEHOLDER_EMPTY_WORKFLOW_ID, DEFAULT_NODETYPE_VERSION } from '@/constants'; +import { + PLACEHOLDER_EMPTY_WORKFLOW_ID, + DEFAULT_NODETYPE_VERSION, +} from '@/constants'; import { IConnection, @@ -14,6 +17,7 @@ import { IRunData, ITaskData, IWorkflowSettings, + PinData, } from 'n8n-workflow'; import { @@ -39,6 +43,7 @@ import users from './modules/users'; import workflows from './modules/workflows'; import versions from './modules/versions'; import templates from './modules/templates'; +import {stringSizeInBytes} from "@/components/helpers"; Vue.use(Vuex); @@ -87,6 +92,7 @@ const state: IRootState = { nodes: [], settings: {}, tags: [], + pinData: {}, }, sidebarMenuItems: [], instanceId: '', @@ -202,6 +208,23 @@ export const store = new Vuex.Store({ Vue.set(state, 'selectedNodes', []); }, + // Pin data + pinData(state, payload: { node: INodeUi, data: IDataObject }) { + if (state.workflow.pinData) { + Vue.set(state.workflow.pinData, payload.node.name, payload.data); + } + + state.stateIsDirty = true; + }, + unpinData(state, payload: { node: INodeUi }) { + if (state.workflow.pinData) { + Vue.set(state.workflow.pinData, payload.node.name, undefined); + delete state.workflow.pinData[payload.node.name]; + } + + state.stateIsDirty = true; + }, + // Active setActive (state, newActive: boolean) { state.workflow.active = newActive; @@ -332,6 +355,11 @@ export const store = new Vuex.Store({ Vue.set(state.nodeMetadata, nameData.new, state.nodeMetadata[nameData.old]); Vue.delete(state.nodeMetadata, nameData.old); + + if (state.workflow.pinData && state.workflow.pinData.hasOwnProperty(nameData.old)) { + Vue.set(state.workflow.pinData, nameData.new, state.workflow.pinData[nameData.old]); + Vue.delete(state.workflow.pinData, nameData.old); + } }, resetAllNodesIssues (state) { @@ -424,6 +452,10 @@ export const store = new Vuex.Store({ removeNode (state, node: INodeUi) { Vue.delete(state.nodeMetadata, node.name); + if (state.workflow.pinData && state.workflow.pinData.hasOwnProperty(node.name)) { + Vue.delete(state.workflow.pinData, node.name); + } + for (let i = 0; i < state.workflow.nodes.length; i++) { if (state.workflow.nodes[i].name === node.name) { state.workflow.nodes.splice(i, 1); @@ -436,6 +468,11 @@ export const store = new Vuex.Store({ if (data.setStateDirty === true) { state.stateIsDirty = true; } + + if (data.removePinData) { + state.workflow.pinData = {}; + } + state.workflow.nodes.splice(0, state.workflow.nodes.length); state.nodeMetadata = {}; }, @@ -607,6 +644,10 @@ export const store = new Vuex.Store({ Vue.set(state.workflow, 'settings', workflowSettings); }, + setWorkflowPinData (state, pinData: Record) { + Vue.set(state.workflow, 'pinData', pinData); + }, + setWorkflowTagIds (state, tags: string[]) { Vue.set(state.workflow, 'tags', tags); }, @@ -844,6 +885,27 @@ export const store = new Vuex.Store({ return state.nodeTypes; }, + /** + * Pin data + */ + + pinData: (state): PinData | undefined => { + return state.workflow.pinData; + }, + pinDataByNodeName: (state) => (nodeName: string) => { + return state.workflow.pinData && state.workflow.pinData[nodeName]; + }, + pinDataSize: (state) => { + return state.workflow.nodes + .reduce((acc, node) => { + if (typeof node.pinData !== 'undefined' && node.name !== state.activeNode) { + acc += stringSizeInBytes(node.pinData); + } + + return acc; + }, 0); + }, + /** * Getter for node default names ending with a number: `'S3'`, `'Magento 2'`, etc. */ diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 4f3c20a74bdfe..50fad1352da81 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -548,6 +548,7 @@ export default mixins( this.$store.commit('setWorkflowId', PLACEHOLDER_EMPTY_WORKFLOW_ID); this.$store.commit('setWorkflowExecutionData', data); + this.$store.commit('setWorkflowPinData', data.workflowData.pinData); await this.addNodes(JSON.parse(JSON.stringify(data.workflowData.nodes)), JSON.parse(JSON.stringify(data.workflowData.connections))); this.$nextTick(() => { @@ -555,6 +556,7 @@ export default mixins( this.$store.commit('setStateDirty', false); }); + this.$externalHooks().run('execution.open', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId }); this.$telemetry.track('User opened read-only execution', { workflow_id: data.workflowData.id, execution_mode: data.mode, execution_finished: data.finished }); @@ -609,6 +611,11 @@ export default mixins( this.resetWorkspace(); data.workflow.nodes = CanvasHelpers.getFixedNodesList(data.workflow.nodes); await this.addNodes(data.workflow.nodes, data.workflow.connections); + + if (data.workflow.pinData) { + this.$store.commit('setWorkflowPinData', data.workflow.pinData); + } + this.$nextTick(() => { this.zoomToFit(); }); @@ -677,6 +684,7 @@ export default mixins( this.$store.commit('setWorkflowId', workflowId); this.$store.commit('setWorkflowName', {newName: data.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', data.settings || {}); + this.$store.commit('setWorkflowPinData', data.pinData || {}); const tags = (data.tags || []) as ITag[]; this.$store.commit('tags/upsertTags', tags); @@ -1311,6 +1319,10 @@ export default mixins( }); }); + if (workflowData.pinData) { + this.$store.commit('setWorkflowPinData', workflowData.pinData); + } + const tagsEnabled = this.$store.getters['settings/areTagsEnabled']; if (importTags && tagsEnabled && Array.isArray(workflowData.tags)) { const allTags: ITag[] = await this.$store.dispatch('tags/fetchAll'); @@ -2167,6 +2179,14 @@ export default mixins( await this.addNodes([newNodeData]); + const pinData = this.$store.getters['pinDataByNodeName'](nodeName); + if (pinData) { + this.$store.commit('pinData', { + node: newNodeData, + data: pinData, + }); + } + this.$store.commit('setStateDirty', true); // Automatically deselect all nodes and select the current one and also active @@ -2831,7 +2851,7 @@ export default mixins( } this.$store.commit('removeAllConnections', {setStateDirty: false}); - this.$store.commit('removeAllNodes', {setStateDirty: false}); + this.$store.commit('removeAllNodes', { setStateDirty: false, removePinData: true }); // Reset workflow execution data this.$store.commit('setWorkflowExecutionData', null); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 4a87b614ba914..8908a082c31c4 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -798,6 +798,11 @@ export interface INode { parameters: INodeParameters; credentials?: INodeCredentials; webhookId?: string; + pinData?: IDataObject; +} + +export interface PinData { + [nodeName: string]: IDataObject[]; } export interface INodes { @@ -1299,6 +1304,7 @@ export interface IRunExecutionData { resultData: { error?: ExecutionError; runData: IRunData; + pinData?: PinData; lastNodeExecuted?: string; }; executionData?: { @@ -1372,6 +1378,7 @@ export interface IWorkflowBase { connections: IConnections; settings?: IWorkflowSettings; staticData?: IDataObject; + pinData?: PinData; } export interface IWorkflowCredentials { @@ -1492,6 +1499,7 @@ export interface INodesGraph { node_connections: IDataObject[]; nodes: INodesGraphNode; notes: INotesGraphNode; + is_pinned: boolean; } export interface INodesGraphNode { diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index 723709e9f6cdd..366f760d7fe67 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -1060,12 +1060,13 @@ export function getNodeWebhookUrl( export function getNodeParametersIssues( nodePropertiesArray: INodeProperties[], node: INode, + pinDataNodeNames?: string[], ): INodeIssues | null { const foundIssues: INodeIssues = {}; let propertyIssues: INodeIssues; - if (node.disabled === true) { - // Ignore issues on disabled nodes + if (node.disabled === true || pinDataNodeNames?.includes(node.name)) { + // Ignore issues on disabled and pindata nodes return null; } diff --git a/packages/workflow/src/TelemetryHelpers.ts b/packages/workflow/src/TelemetryHelpers.ts index 72f9054cfc944..3c29e5fa15342 100644 --- a/packages/workflow/src/TelemetryHelpers.ts +++ b/packages/workflow/src/TelemetryHelpers.ts @@ -120,6 +120,7 @@ export function generateNodesGraph( node_connections: [], nodes: {}, notes: {}, + is_pinned: Object.keys(workflow.pinData ?? {}).length > 0, }; const nodeNameAndIndex: INodeNameIndex = {}; const webhookNodeNames: string[] = []; diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index 1492ee8d85985..2205c23b2f82e 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -252,6 +252,7 @@ export class Workflow { checkReadyForExecution(inputData: { startNode?: string; destinationNode?: string; + pinDataNodeNames?: string[]; }): IWorfklowIssues | null { let node: INode; let nodeType: INodeType | undefined; @@ -287,7 +288,11 @@ export class Workflow { typeUnknown: true, }; } else { - nodeIssues = NodeHelpers.getNodeParametersIssues(nodeType.description.properties, node); + nodeIssues = NodeHelpers.getNodeParametersIssues( + nodeType.description.properties, + node, + inputData.pinDataNodeNames, + ); } if (nodeIssues !== null) { From ed3dceb9a595d3b24f139b7daca03b02ed5170d6 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Mon, 18 Jul 2022 16:33:21 +0300 Subject: [PATCH 29/42] fix: Showing pin data without executing the node only in output pane. --- packages/editor-ui/src/components/RunData.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index 7eda024845e4a..691ed06e29d90 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -624,7 +624,7 @@ export default mixins( inputData (): INodeExecutionData[] { let inputData = this.rawInputData; - if (this.node && this.pinData) { + if (this.paneType === 'output' && this.node && this.pinData) { inputData = Array.isArray(this.pinData) ? this.pinData.map((value) => ({ json: value, From 7d8e39f1918696d4065f382c47013d67ad6283fa Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 19 Jul 2022 13:00:15 +0300 Subject: [PATCH 30/42] fix: Updated no data message when previous node not executed. --- packages/editor-ui/src/components/RunData.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index 691ed06e29d90..51c4003901c80 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -543,7 +543,7 @@ export default mixins( return defaults; }, hasNodeRun(): boolean { - return Boolean(!this.isExecuting && this.node && (this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name) || this.hasPinData)); + return Boolean(!this.isExecuting && this.node && (this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name) || (this.paneType === 'output' && this.hasPinData))); }, hasRunError(): boolean { return Boolean(this.node && this.workflowRunData && this.workflowRunData[this.node.name] && this.workflowRunData[this.node.name][this.runIndex] && this.workflowRunData[this.node.name][this.runIndex].error); From 84c598a92838d0dc4181d4549a949c54aca897cb Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 20 Jul 2022 11:11:11 +0300 Subject: [PATCH 31/42] feat: Added expression input and evaluation for pin data nodes without execution. --- package-lock.json | 59 ++++- .../src/components/NodeDetailsView.vue | 2 +- packages/editor-ui/src/components/RunData.vue | 8 +- .../src/components/VariableSelector.vue | 85 +++++++- .../src/components/mixins/workflowHelpers.ts | 56 ++++- packages/editor-ui/src/store.ts | 202 ++++++++++++------ packages/editor-ui/src/views/NodeView.vue | 10 +- packages/workflow/src/WorkflowDataProxy.ts | 1 + 8 files changed, 324 insertions(+), 99 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6b6ab6e6e1976..61eec016ac367 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "n8n", - "version": "0.185.0", + "version": "0.186.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "n8n", - "version": "0.185.0", + "version": "0.186.1", "dependencies": { "@apidevtools/swagger-cli": "4.0.0", "@babel/core": "^7.14.6", @@ -22909,6 +22909,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bonjour": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", @@ -31045,6 +31059,20 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/express/node_modules/qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ext": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", @@ -44023,9 +44051,9 @@ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "engines": { "node": ">= 0.8" } @@ -79256,6 +79284,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "requires": { + "side-channel": "^1.0.4" + } } } }, @@ -85540,6 +85576,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "requires": { + "side-channel": "^1.0.4" + } } } }, @@ -95765,8 +95809,9 @@ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "media-typer": { - "version": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" }, "memfs": { "version": "3.4.7", diff --git a/packages/editor-ui/src/components/NodeDetailsView.vue b/packages/editor-ui/src/components/NodeDetailsView.vue index 944aa6353cbc2..32403397c9056 100644 --- a/packages/editor-ui/src/components/NodeDetailsView.vue +++ b/packages/editor-ui/src/components/NodeDetailsView.vue @@ -489,7 +489,7 @@ export default mixins( return; } - this.$store.commit('pinData', { node: this.activeNode, data: JSON.parse(value) }); + this.$store.dispatch('pinData', { node: this.activeNode, data: JSON.parse(value) }); } this.$store.commit('ui/setOutputPanelEditModeEnabled', false); diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index 51c4003901c80..f312c35f620de 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -543,7 +543,7 @@ export default mixins( return defaults; }, hasNodeRun(): boolean { - return Boolean(!this.isExecuting && this.node && (this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name) || (this.paneType === 'output' && this.hasPinData))); + return Boolean(!this.isExecuting && this.node && (this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name) || this.hasPinData)); }, hasRunError(): boolean { return Boolean(this.node && this.workflowRunData && this.workflowRunData[this.node.name] && this.workflowRunData[this.node.name][this.runIndex] && this.workflowRunData[this.node.name][this.runIndex].error); @@ -624,7 +624,7 @@ export default mixins( inputData (): INodeExecutionData[] { let inputData = this.rawInputData; - if (this.paneType === 'output' && this.node && this.pinData) { + if (this.node && this.pinData) { inputData = Array.isArray(this.pinData) ? this.pinData.map((value) => ({ json: value, @@ -767,7 +767,7 @@ export default mixins( } this.$store.commit('ui/setOutputPanelEditModeEnabled', false); - this.$store.commit('pinData', { node: this.node, data: this.removeJsonKeys(value) }); + this.$store.dispatch('pinData', { node: this.node, data: this.removeJsonKeys(value) }); this.onDataPinningSuccess({ source: 'save-edit' }); @@ -862,7 +862,7 @@ export default mixins( this.onDataPinningSuccess({ source: 'save-edit' }); - this.$store.commit('pinData', { node: this.node, data }); + this.$store.dispatch('pinData', { node: this.node, data }); if (this.maxRunIndex > 0) { this.$showToast({ diff --git a/packages/editor-ui/src/components/VariableSelector.vue b/packages/editor-ui/src/components/VariableSelector.vue index 659da19ea068b..08a7cdfc552ee 100644 --- a/packages/editor-ui/src/components/VariableSelector.vue +++ b/packages/editor-ui/src/components/VariableSelector.vue @@ -19,10 +19,10 @@ import { import { GenericValue, IContextObject, - IDataObject, + IDataObject, INodeExecutionData, IRunData, IRunExecutionData, - IWorkflowDataProxyAdditionalKeys, + IWorkflowDataProxyAdditionalKeys, PinData, Workflow, WorkflowDataProxy, } from 'n8n-workflow'; @@ -248,19 +248,20 @@ export default mixins( }, /** - * Returns the data the a node does output + * Get the node's output using runData * - * @param {IRunData} runData The data of the run to get the data of * @param {string} nodeName The name of the node to get the data of + * @param {IRunData} runData The data of the run to get the data of * @param {string} filterText Filter text for parameters * @param {number} [itemIndex=0] The index of the item * @param {number} [runIndex=0] The index of the run * @param {string} [inputName='main'] The name of the input * @param {number} [outputIndex=0] The index of the output + * @param {boolean} [useShort=false] Use short notation * @returns * @memberof Workflow */ - getNodeOutputData (runData: IRunData, nodeName: string, filterText: string, itemIndex = 0, runIndex = 0, inputName = 'main', outputIndex = 0, useShort = false): IVariableSelectorOption[] | null { + getNodeRunDataOutput(nodeName: string, runData: IRunData, filterText: string, itemIndex = 0, runIndex = 0, inputName = 'main', outputIndex = 0, useShort = false): IVariableSelectorOption[] | null { if (!runData.hasOwnProperty(nodeName)) { // No data found for node return null; @@ -297,6 +298,32 @@ export default mixins( const outputData = runData[nodeName][runIndex].data![inputName][outputIndex]![itemIndex]; + return this.getNodeOutput(nodeName, outputData, filterText, useShort); + }, + + /** + * Get the node's output using pinData + * + * @param {string} nodeName The name of the node to get the data of + * @param {PinData[string]} pinData The node's pin data + * @param {string} filterText Filter text for parameters + * @param {boolean} [useShort=false] Use short notation + */ + getNodePinDataOutput(nodeName: string, pinData: PinData[string], filterText: string, useShort = false): IVariableSelectorOption[] | null { + const outputData = pinData.map((data) => ({ json: data } as INodeExecutionData))[0]; + + return this.getNodeOutput(nodeName, outputData, filterText, useShort); + }, + + /** + * Returns the node's output data + * + * @param {string} nodeName The name of the node to get the data of + * @param {INodeExecutionData} outputData The data of the run to get the data of + * @param {string} filterText Filter text for parameters + * @param {boolean} [useShort=false] Use short notation + */ + getNodeOutput (nodeName: string, outputData: INodeExecutionData, filterText: string, useShort = false): IVariableSelectorOption[] | null { const returnData: IVariableSelectorOption[] = []; // Get json data @@ -388,6 +415,8 @@ export default mixins( const nodeConnection = this.workflow.getNodeConnectionIndexes(activeNode.name, parentNode[0], 'main'); const connectionInputData = this.connectionInputData(parentNode, nodeName, inputName, runIndex, nodeConnection); + console.log('getNodeContext', {activeNode, connectionInputData}); + if (connectionInputData === null) { return returnData; } @@ -416,6 +445,8 @@ export default mixins( }); } + console.log('getNodeContext', returnData); + return returnData; }, /** @@ -491,7 +522,7 @@ export default mixins( } } - let tempOutputData; + let tempOutputData: IVariableSelectorOption[] | null; if (parentNode.length) { // If the node has an input node add the input data @@ -502,7 +533,41 @@ export default mixins( const nodeConnection = this.workflow.getNodeConnectionIndexes(activeNode.name, parentNode[0], 'main'); const outputIndex = nodeConnection === undefined ? 0: nodeConnection.sourceIndex; - tempOutputData = this.getNodeOutputData(runData, parentNode[0], filterText, itemIndex, 0, 'main', outputIndex, true) as IVariableSelectorOption[]; + tempOutputData = this.getNodeRunDataOutput(parentNode[0], runData, filterText, itemIndex, 0, 'main', outputIndex, true) as IVariableSelectorOption[]; + + let pinDataOptions: IVariableSelectorOption[] = []; + parentNode.forEach((parentNodeName) => { + const pinData = this.$store.getters['pinDataByNodeName'](parentNodeName); + + if (pinData) { + pinDataOptions = pinDataOptions.concat(this.getNodePinDataOutput(parentNodeName, pinData, filterText, true) || []); + } + }); + + if (pinDataOptions.length > 0) { + if (tempOutputData) { + const jsonTempOutputData = tempOutputData.find((tempData) => tempData.name === 'JSON'); + + if (jsonTempOutputData) { + if (!jsonTempOutputData.options) { + jsonTempOutputData.options = []; + } + + (pinDataOptions[0].options || []).forEach((pinDataOption) => { + const existingOptionIndex = jsonTempOutputData.options!.findIndex((option) => option.name === pinDataOption.name); + if (existingOptionIndex !== -1) { + jsonTempOutputData.options![existingOptionIndex] = pinDataOption; + } else { + jsonTempOutputData.options!.push(pinDataOption); + } + }); + } else { + tempOutputData.push(pinDataOptions[0]); + } + } else { + tempOutputData = pinDataOptions; + } + } if (tempOutputData) { if (JSON.stringify(tempOutputData).length < 102400) { @@ -602,7 +667,11 @@ export default mixins( if (upstreamNodes.includes(nodeName)) { // If the node is an upstream node add also the output data which can be referenced - tempOutputData = this.getNodeOutputData(runData, nodeName, filterText, itemIndex); + const pinData = this.$store.getters['pinDataByNodeName'](nodeName); + tempOutputData = pinData + ? this.getNodePinDataOutput(nodeName, pinData, filterText) + : this.getNodeRunDataOutput(nodeName, runData, filterText, itemIndex); + if (tempOutputData) { nodeOptions.push( { diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index cb21cdd8f1105..9633e39fc616d 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -30,6 +30,7 @@ import { IExecuteData, INodeConnection, IWebhookDescription, + PinData, } from 'n8n-workflow'; import { @@ -41,7 +42,7 @@ import { IWorkflowDataUpdate, XYPosition, ITag, - IUpdateInformation, + IUpdateInformation, IVariableSelectorOption, } from '../../Interface'; import { externalHooks } from '@/components/mixins/externalHooks'; @@ -69,7 +70,7 @@ export const workflowHelpers = mixins( source: null, } as IExecuteData; - if (parentNode.length) { + if (parentNode.length) { // Add the input data to be able to also resolve the short expression format // which does not use the node name const parentNodeName = parentNode[0]; @@ -78,6 +79,7 @@ export const workflowHelpers = mixins( if (workflowRunData === null) { return executeData; } + if (!workflowRunData[parentNodeName] || workflowRunData[parentNodeName].length <= runIndex || !workflowRunData[parentNodeName][runIndex].hasOwnProperty('data') || @@ -108,7 +110,7 @@ export const workflowHelpers = mixins( }, // Returns connectionInputData to be able to execute an expression. connectionInputData (parentNode: string[], currentNode: string, inputName: string, runIndex: number, nodeConnection: INodeConnection = { sourceIndex: 0, destinationIndex: 0 }): INodeExecutionData[] | null { - let connectionInputData = null; + let connectionInputData: INodeExecutionData[] | null = null; const executeData = this.executeData(parentNode, currentNode, inputName, runIndex); if (parentNode.length) { if (!Object.keys(executeData.data).length || executeData.data[inputName].length <= nodeConnection.sourceIndex) { @@ -131,6 +133,35 @@ export const workflowHelpers = mixins( } } + const parentPinData = parentNode.reduce((acc: INodeExecutionData[], parentNodeName, index) => { + const pinData = this.$store.getters['pinDataByNodeName'](parentNodeName); + + if (pinData) { + acc.push({ + json: pinData[0], + pairedItem: { + item: index, + input: 1, + }, + }); + } + + return acc; + }, []); + + if (parentPinData.length > 0) { + if (connectionInputData && connectionInputData.length > 0) { + parentPinData.forEach((parentPinDataEntry) => { + connectionInputData![0].json = { + ...connectionInputData![0].json, + ...parentPinDataEntry.json, + }; + }); + } else { + connectionInputData = parentPinData; + } + } + return connectionInputData; }, @@ -491,6 +522,25 @@ export const workflowHelpers = mixins( runExecutionData = executionData.data; } + parentNode.forEach((parentNodeName) => { + const pinData: PinData[string] = this.$store.getters['pinDataByNodeName'](parentNodeName); + + if (pinData) { + runExecutionData.resultData.runData[parentNodeName] = [ + { + startTime: new Date().valueOf(), + executionTime: 0, + source: [], + data: { + main: [ + pinData.map((data) => ({ json: data })), + ], + }, + }, + ]; + } + }); + if (connectionInputData === null) { connectionInputData = []; } diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 4c1563f23e743..657f77c376603 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -1,4 +1,3 @@ - import Vue from 'vue'; import Vuex from 'vuex'; @@ -10,7 +9,7 @@ import { import { IConnection, IConnections, - IDataObject, + IDataObject, IExecuteData, INodeConnections, INodeIssueData, INodeTypeDescription, @@ -114,15 +113,27 @@ export const store = new Vuex.Store({ strict: process.env.NODE_ENV !== 'production', modules, state, + actions: { + pinData({commit, state}, payload: { node: INodeUi, data: PinData[string] }) { + commit('pinData', payload); + commit('setPinDataAsExecutionData', { + [payload.node.name]: payload.data, + }); + }, + setWorkflowPinData({commit, state}, payload: PinData) { + commit('setWorkflowPinData', payload); + commit('setPinDataAsExecutionData', payload); + }, + }, mutations: { // Active Actions - addActiveAction (state, action: string) { + addActiveAction(state, action: string) { if (!state.activeActions.includes(action)) { state.activeActions.push(action); } }, - removeActiveAction (state, action: string) { + removeActiveAction(state, action: string) { const actionIndex = state.activeActions.indexOf(action); if (actionIndex !== -1) { state.activeActions.splice(actionIndex, 1); @@ -130,7 +141,7 @@ export const store = new Vuex.Store({ }, // Active Executions - addActiveExecution (state, newActiveExecution: IExecutionsCurrentSummaryExtended) { + addActiveExecution(state, newActiveExecution: IExecutionsCurrentSummaryExtended) { // Check if the execution exists already const activeExecution = state.activeExecutions.find(execution => { return execution.id === newActiveExecution.id; @@ -146,7 +157,7 @@ export const store = new Vuex.Store({ state.activeExecutions.unshift(newActiveExecution); }, - finishActiveExecution (state, finishedActiveExecution: IPushDataExecutionFinished) { + finishActiveExecution(state, finishedActiveExecution: IPushDataExecutionFinished) { // Find the execution to set to finished const activeExecution = state.activeExecutions.find(execution => { return execution.id === finishedActiveExecution.executionId; @@ -164,22 +175,22 @@ export const store = new Vuex.Store({ Vue.set(activeExecution, 'finished', finishedActiveExecution.data.finished); Vue.set(activeExecution, 'stoppedAt', finishedActiveExecution.data.stoppedAt); }, - setActiveExecutions (state, newActiveExecutions: IExecutionsCurrentSummaryExtended[]) { + setActiveExecutions(state, newActiveExecutions: IExecutionsCurrentSummaryExtended[]) { Vue.set(state, 'activeExecutions', newActiveExecutions); }, // Active Workflows - setActiveWorkflows (state, newActiveWorkflows: string[]) { + setActiveWorkflows(state, newActiveWorkflows: string[]) { state.activeWorkflows = newActiveWorkflows; }, - setWorkflowActive (state, workflowId: string) { + setWorkflowActive(state, workflowId: string) { state.stateIsDirty = false; const index = state.activeWorkflows.indexOf(workflowId); if (index === -1) { state.activeWorkflows.push(workflowId); } }, - setWorkflowInactive (state, workflowId: string) { + setWorkflowInactive(state, workflowId: string) { const index = state.activeWorkflows.indexOf(workflowId); if (index !== -1) { state.activeWorkflows.splice(index, 1); @@ -187,15 +198,15 @@ export const store = new Vuex.Store({ }, // Set state condition dirty or not // ** Dirty: if current workflow state has been synchronized with database AKA has it been saved - setStateDirty (state, dirty : boolean) { + setStateDirty(state, dirty: boolean) { state.stateIsDirty = dirty; }, // Selected Nodes - addSelectedNode (state, node: INodeUi) { + addSelectedNode(state, node: INodeUi) { state.selectedNodes.push(node); }, - removeNodeFromSelection (state, node: INodeUi) { + removeNodeFromSelection(state, node: INodeUi) { let index; for (index in state.selectedNodes) { if (state.selectedNodes[index].name === node.name) { @@ -204,12 +215,12 @@ export const store = new Vuex.Store({ } } }, - resetSelectedNodes (state) { + resetSelectedNodes(state) { Vue.set(state, 'selectedNodes', []); }, // Pin data - pinData(state, payload: { node: INodeUi, data: IDataObject }) { + pinData(state, payload: { node: INodeUi, data: PinData[string] }) { if (state.workflow.pinData) { Vue.set(state.workflow.pinData, payload.node.name, payload.data); } @@ -224,14 +235,63 @@ export const store = new Vuex.Store({ state.stateIsDirty = true; }, + setPinDataAsExecutionData(state, payload: PinData) { + let executionData: IExecutionResponse | null = state.workflowExecutionData; + + if (!executionData) { + executionData = { + id: '', + workflowData: state.workflow, + data: { + startData: {}, + resultData: { + runData: {}, + lastNodeExecuted: '', + }, + executionData: { + contextData: {}, + nodeExecutionStack: [], + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }, + mode: "manual", + startedAt: new Date(), + stoppedAt: new Date(), + finished: true, + }; + } + + Object.keys(payload).forEach((nodeName) => { + executionData!.data!.resultData.runData[nodeName] = [ + { + startTime: 0, + executionTime: 0, + source: [], + data: { + main: [ + payload[nodeName].map((data, index) => ({ + json: data, + pairedItem: { item: index }, + })), + ], + }, + }, + ]; + + executionData!.data!.resultData.lastNodeExecuted = nodeName; + }); + + // Vue.set(state, 'workflowExecutionData', executionData); + }, // Active - setActive (state, newActive: boolean) { + setActive(state, newActive: boolean) { state.workflow.active = newActive; }, // Connections - addConnection (state, data) { + addConnection(state, data) { if (data.connection.length !== 2) { // All connections need two entries // TODO: Check if there is an error or whatever that is supposed to be returned @@ -279,7 +339,7 @@ export const store = new Vuex.Store({ } }, - removeConnection (state, data) { + removeConnection(state, data) { const sourceData = data.connection[0]; const destinationData = data.connection[1]; @@ -304,13 +364,13 @@ export const store = new Vuex.Store({ } }, - removeAllConnections (state, data) { + removeAllConnections(state, data) { if (data && data.setStateDirty === true) { state.stateIsDirty = true; } state.workflow.connections = {}; }, - removeAllNodeConnection (state, node: INodeUi) { + removeAllNodeConnection(state, node: INodeUi) { state.stateIsDirty = true; // Remove all source connections if (state.workflow.connections.hasOwnProperty(node.name)) { @@ -339,7 +399,7 @@ export const store = new Vuex.Store({ } }, - renameNodeSelectedAndExecution (state, nameData) { + renameNodeSelectedAndExecution(state, nameData) { state.stateIsDirty = true; // If node has any WorkflowResultData rename also that one that the data // does still get displayed also after node got renamed @@ -362,7 +422,7 @@ export const store = new Vuex.Store({ } }, - resetAllNodesIssues (state) { + resetAllNodesIssues(state) { state.workflow.nodes.forEach((node) => { node.issues = undefined; }); @@ -370,7 +430,7 @@ export const store = new Vuex.Store({ return true; }, - setNodeIssue (state, nodeIssueData: INodeIssueData) { + setNodeIssue(state, nodeIssueData: INodeIssueData) { const node = state.workflow.nodes.find(node => { return node.name === nodeIssueData.node; @@ -406,7 +466,7 @@ export const store = new Vuex.Store({ }, // Name - setWorkflowName (state, data) { + setWorkflowName(state, data) { if (data.setStateDirty === true) { state.stateIsDirty = true; } @@ -414,7 +474,7 @@ export const store = new Vuex.Store({ }, // replace invalid credentials in workflow - replaceInvalidWorkflowCredentials(state, {credentials, invalid, type }) { + replaceInvalidWorkflowCredentials(state, {credentials, invalid, type}) { state.workflow.nodes.forEach((node) => { if (!node.credentials || !node.credentials[type]) { return; @@ -427,7 +487,7 @@ export const store = new Vuex.Store({ } if (nodeCredentials.id === null) { - if (nodeCredentials.name === invalid.name){ + if (nodeCredentials.name === invalid.name) { node.credentials[type] = credentials; } return; @@ -440,7 +500,7 @@ export const store = new Vuex.Store({ }, // Nodes - addNode (state, nodeData: INodeUi) { + addNode(state, nodeData: INodeUi) { if (!nodeData.hasOwnProperty('name')) { // All nodes have to have a name // TODO: Check if there is an error or whatever that is supposed to be returned @@ -449,7 +509,7 @@ export const store = new Vuex.Store({ state.workflow.nodes.push(nodeData); }, - removeNode (state, node: INodeUi) { + removeNode(state, node: INodeUi) { Vue.delete(state.nodeMetadata, node.name); if (state.workflow.pinData && state.workflow.pinData.hasOwnProperty(node.name)) { @@ -464,7 +524,7 @@ export const store = new Vuex.Store({ } } }, - removeAllNodes (state, data) { + removeAllNodes(state, data) { if (data.setStateDirty === true) { state.stateIsDirty = true; } @@ -476,7 +536,7 @@ export const store = new Vuex.Store({ state.workflow.nodes.splice(0, state.workflow.nodes.length); state.nodeMetadata = {}; }, - updateNodeProperties (state, updateInformation: INodeUpdatePropertiesInformation) { + updateNodeProperties(state, updateInformation: INodeUpdatePropertiesInformation) { // Find the node that should be updated const node = state.workflow.nodes.find(node => { return node.name === updateInformation.name; @@ -489,7 +549,7 @@ export const store = new Vuex.Store({ } } }, - setNodeValue (state, updateInformation: IUpdateInformation) { + setNodeValue(state, updateInformation: IUpdateInformation) { // Find the node that should be updated const node = state.workflow.nodes.find(node => { return node.name === updateInformation.name; @@ -502,7 +562,7 @@ export const store = new Vuex.Store({ state.stateIsDirty = true; Vue.set(node, updateInformation.key, updateInformation.value); }, - setNodeParameters (state, updateInformation: IUpdateInformation) { + setNodeParameters(state, updateInformation: IUpdateInformation) { // Find the node that should be updated const node = state.workflow.nodes.find(node => { return node.name === updateInformation.name; @@ -522,72 +582,72 @@ export const store = new Vuex.Store({ }, // Node-Index - addToNodeIndex (state, nodeName: string) { + addToNodeIndex(state, nodeName: string) { state.nodeIndex.push(nodeName); }, - setNodeIndex (state, newData: { index: number, name: string | null}) { + setNodeIndex(state, newData: { index: number, name: string | null }) { state.nodeIndex[newData.index] = newData.name; }, - resetNodeIndex (state) { + resetNodeIndex(state) { Vue.set(state, 'nodeIndex', []); }, // Node-View - setNodeViewMoveInProgress (state, value: boolean) { + setNodeViewMoveInProgress(state, value: boolean) { state.nodeViewMoveInProgress = value; }, - setNodeViewOffsetPosition (state, data) { + setNodeViewOffsetPosition(state, data) { state.nodeViewOffsetPosition = data.newOffset; }, // Node-Types - setNodeTypes (state, nodeTypes: INodeTypeDescription[]) { + setNodeTypes(state, nodeTypes: INodeTypeDescription[]) { Vue.set(state, 'nodeTypes', nodeTypes); }, // Active Execution - setExecutingNode (state, executingNode: string) { + setExecutingNode(state, executingNode: string) { state.executingNode = executingNode; }, - setExecutionWaitingForWebhook (state, newWaiting: boolean) { + setExecutionWaitingForWebhook(state, newWaiting: boolean) { state.executionWaitingForWebhook = newWaiting; }, - setActiveExecutionId (state, executionId: string | null) { + setActiveExecutionId(state, executionId: string | null) { state.executionId = executionId; }, // Push Connection - setPushConnectionActive (state, newActive: boolean) { + setPushConnectionActive(state, newActive: boolean) { state.pushConnectionActive = newActive; }, // Webhooks - setUrlBaseWebhook (state, urlBaseWebhook: string) { + setUrlBaseWebhook(state, urlBaseWebhook: string) { Vue.set(state, 'urlBaseWebhook', urlBaseWebhook); }, - setEndpointWebhook (state, endpointWebhook: string) { + setEndpointWebhook(state, endpointWebhook: string) { Vue.set(state, 'endpointWebhook', endpointWebhook); }, - setEndpointWebhookTest (state, endpointWebhookTest: string) { + setEndpointWebhookTest(state, endpointWebhookTest: string) { Vue.set(state, 'endpointWebhookTest', endpointWebhookTest); }, - setSaveDataErrorExecution (state, newValue: string) { + setSaveDataErrorExecution(state, newValue: string) { Vue.set(state, 'saveDataErrorExecution', newValue); }, - setSaveDataSuccessExecution (state, newValue: string) { + setSaveDataSuccessExecution(state, newValue: string) { Vue.set(state, 'saveDataSuccessExecution', newValue); }, - setSaveManualExecutions (state, saveManualExecutions: boolean) { + setSaveManualExecutions(state, saveManualExecutions: boolean) { Vue.set(state, 'saveManualExecutions', saveManualExecutions); }, - setTimezone (state, timezone: string) { + setTimezone(state, timezone: string) { Vue.set(state, 'timezone', timezone); }, - setExecutionTimeout (state, executionTimeout: number) { + setExecutionTimeout(state, executionTimeout: number) { Vue.set(state, 'executionTimeout', executionTimeout); }, - setMaxExecutionTimeout (state, maxExecutionTimeout: number) { + setMaxExecutionTimeout(state, maxExecutionTimeout: number) { Vue.set(state, 'maxExecutionTimeout', maxExecutionTimeout); }, setVersionCli(state, version: string) { @@ -605,25 +665,25 @@ export const store = new Vuex.Store({ setDefaultLocale(state, locale: string) { Vue.set(state, 'defaultLocale', locale); }, - setActiveNode (state, nodeName: string) { + setActiveNode(state, nodeName: string) { state.activeNode = nodeName; }, - setActiveCredentialType (state, activeCredentialType: string) { + setActiveCredentialType(state, activeCredentialType: string) { state.activeCredentialType = activeCredentialType; }, - setLastSelectedNode (state, nodeName: string) { + setLastSelectedNode(state, nodeName: string) { state.lastSelectedNode = nodeName; }, - setLastSelectedNodeOutputIndex (state, outputIndex: number | null) { + setLastSelectedNodeOutputIndex(state, outputIndex: number | null) { state.lastSelectedNodeOutputIndex = outputIndex; }, - setWorkflowExecutionData (state, workflowResultData: IExecutionResponse | null) { + setWorkflowExecutionData(state, workflowResultData: IExecutionResponse | null) { state.workflowExecutionData = workflowResultData; }, - addNodeExecutionData (state, pushData: IPushDataNodeExecuteAfter): void { + addNodeExecutionData(state, pushData: IPushDataNodeExecuteAfter): void { if (state.workflowExecutionData === null) { throw new Error('The "workflowExecutionData" is not initialized!'); } @@ -632,7 +692,7 @@ export const store = new Vuex.Store({ } state.workflowExecutionData.data.resultData.runData[pushData.nodeName].push(pushData.data); }, - clearNodeExecutionData (state, nodeName: string): void { + clearNodeExecutionData(state, nodeName: string): void { if (state.workflowExecutionData === null) { return; } @@ -640,23 +700,23 @@ export const store = new Vuex.Store({ Vue.delete(state.workflowExecutionData.data.resultData.runData, nodeName); }, - setWorkflowSettings (state, workflowSettings: IWorkflowSettings) { + setWorkflowSettings(state, workflowSettings: IWorkflowSettings) { Vue.set(state.workflow, 'settings', workflowSettings); }, - setWorkflowPinData (state, pinData: Record) { + setWorkflowPinData(state, pinData: PinData) { Vue.set(state.workflow, 'pinData', pinData); }, - setWorkflowTagIds (state, tags: string[]) { + setWorkflowTagIds(state, tags: string[]) { Vue.set(state.workflow, 'tags', tags); }, - addWorkflowTagIds (state, tags: string[]) { + addWorkflowTagIds(state, tags: string[]) { Vue.set(state.workflow, 'tags', [...new Set([...(state.workflow.tags || []), ...tags])]); }, - removeWorkflowTagId (state, tagId: string) { + removeWorkflowTagId(state, tagId: string) { const tags = state.workflow.tags as string[]; const updated = tags.filter((id: string) => id !== tagId); @@ -664,7 +724,7 @@ export const store = new Vuex.Store({ }, // Workflow - setWorkflow (state, workflow: IWorkflowDb) { + setWorkflow(state, workflow: IWorkflowDb) { Vue.set(state, 'workflow', workflow); if (!state.workflow.hasOwnProperty('active')) { @@ -690,21 +750,21 @@ export const store = new Vuex.Store({ } }, - updateNodeTypes (state, nodeTypes: INodeTypeDescription[]) { + updateNodeTypes(state, nodeTypes: INodeTypeDescription[]) { const oldNodesToKeep = state.nodeTypes.filter(node => !nodeTypes.find(n => n.name === node.name && n.version.toString() === node.version.toString())); const newNodesState = [...oldNodesToKeep, ...nodeTypes]; Vue.set(state, 'nodeTypes', newNodesState); state.nodeTypes = newNodesState; }, - addSidebarMenuItems (state, menuItems: IMenuItem[]) { + addSidebarMenuItems(state, menuItems: IMenuItem[]) { const updated = state.sidebarMenuItems.concat(menuItems); Vue.set(state, 'sidebarMenuItems', updated); }, }, getters: { executedNode: (state): string | undefined => { - return state.workflowExecutionData? state.workflowExecutionData.executedNode: undefined; + return state.workflowExecutionData ? state.workflowExecutionData.executedNode : undefined; }, activeCredentialType: (state): string | null => { return state.activeCredentialType; @@ -760,7 +820,7 @@ export const store = new Vuex.Store({ return `${state.urlBaseWebhook}${state.endpointWebhookTest}`; }, - getStateIsDirty: (state) : boolean => { + getStateIsDirty: (state): boolean => { return state.stateIsDirty; }, @@ -863,8 +923,8 @@ export const store = new Vuex.Store({ allNodes: (state): INodeUi[] => { return state.workflow.nodes; }, - nodesByName: (state: IRootState): {[name: string]: INodeUi} => { - return state.workflow.nodes.reduce((accu: {[name: string]: INodeUi}, node) => { + nodesByName: (state: IRootState): { [name: string]: INodeUi } => { + return state.workflow.nodes.reduce((accu: { [name: string]: INodeUi }, node) => { accu[node.name] = node; return accu; }, {}); @@ -910,7 +970,7 @@ export const store = new Vuex.Store({ * Getter for node default names ending with a number: `'S3'`, `'Magento 2'`, etc. */ nativelyNumberSuffixedDefaults: (_, getters): string[] => { - const { allNodeTypes } = getters as { + const {allNodeTypes} = getters as { allNodeTypes: Array; }; diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 50fad1352da81..dc26d940305c8 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -548,7 +548,7 @@ export default mixins( this.$store.commit('setWorkflowId', PLACEHOLDER_EMPTY_WORKFLOW_ID); this.$store.commit('setWorkflowExecutionData', data); - this.$store.commit('setWorkflowPinData', data.workflowData.pinData); + this.$store.dispatch('setWorkflowPinData', data.workflowData.pinData); await this.addNodes(JSON.parse(JSON.stringify(data.workflowData.nodes)), JSON.parse(JSON.stringify(data.workflowData.connections))); this.$nextTick(() => { @@ -613,7 +613,7 @@ export default mixins( await this.addNodes(data.workflow.nodes, data.workflow.connections); if (data.workflow.pinData) { - this.$store.commit('setWorkflowPinData', data.workflow.pinData); + this.$store.dispatch('setWorkflowPinData', data.workflow.pinData); } this.$nextTick(() => { @@ -684,7 +684,7 @@ export default mixins( this.$store.commit('setWorkflowId', workflowId); this.$store.commit('setWorkflowName', {newName: data.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', data.settings || {}); - this.$store.commit('setWorkflowPinData', data.pinData || {}); + this.$store.dispatch('setWorkflowPinData', data.pinData || {}); const tags = (data.tags || []) as ITag[]; this.$store.commit('tags/upsertTags', tags); @@ -1320,7 +1320,7 @@ export default mixins( }); if (workflowData.pinData) { - this.$store.commit('setWorkflowPinData', workflowData.pinData); + this.$store.dispatch('setWorkflowPinData', workflowData.pinData); } const tagsEnabled = this.$store.getters['settings/areTagsEnabled']; @@ -2181,7 +2181,7 @@ export default mixins( const pinData = this.$store.getters['pinDataByNodeName'](nodeName); if (pinData) { - this.$store.commit('pinData', { + this.$store.dispatch('pinData', { node: newNodeData, data: pinData, }); diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts index bf243815610e6..3ee09923edd32 100644 --- a/packages/workflow/src/WorkflowDataProxy.ts +++ b/packages/workflow/src/WorkflowDataProxy.ts @@ -102,6 +102,7 @@ export class WorkflowDataProxy { const that = this; const node = this.workflow.nodes[nodeName]; + console.log({ that }); return new Proxy( {}, { From 8d1ee92512f7542961737233b5e3333bed0f2570 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 20 Jul 2022 11:24:38 +0300 Subject: [PATCH 32/42] chore: Fixed linting issues and removed remnant console.log(). --- packages/editor-ui/src/components/VariableSelector.vue | 10 ++++------ .../editor-ui/src/components/mixins/workflowHelpers.ts | 2 +- packages/editor-ui/src/store.ts | 2 +- packages/workflow/src/WorkflowDataProxy.ts | 1 - 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/editor-ui/src/components/VariableSelector.vue b/packages/editor-ui/src/components/VariableSelector.vue index 08a7cdfc552ee..8eed40da60290 100644 --- a/packages/editor-ui/src/components/VariableSelector.vue +++ b/packages/editor-ui/src/components/VariableSelector.vue @@ -19,10 +19,12 @@ import { import { GenericValue, IContextObject, - IDataObject, INodeExecutionData, + IDataObject, + INodeExecutionData, IRunData, IRunExecutionData, - IWorkflowDataProxyAdditionalKeys, PinData, + IWorkflowDataProxyAdditionalKeys, + PinData, Workflow, WorkflowDataProxy, } from 'n8n-workflow'; @@ -415,8 +417,6 @@ export default mixins( const nodeConnection = this.workflow.getNodeConnectionIndexes(activeNode.name, parentNode[0], 'main'); const connectionInputData = this.connectionInputData(parentNode, nodeName, inputName, runIndex, nodeConnection); - console.log('getNodeContext', {activeNode, connectionInputData}); - if (connectionInputData === null) { return returnData; } @@ -445,8 +445,6 @@ export default mixins( }); } - console.log('getNodeContext', returnData); - return returnData; }, /** diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index 9633e39fc616d..bf7d9520a1d1d 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -42,7 +42,7 @@ import { IWorkflowDataUpdate, XYPosition, ITag, - IUpdateInformation, IVariableSelectorOption, + IUpdateInformation, } from '../../Interface'; import { externalHooks } from '@/components/mixins/externalHooks'; diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 657f77c376603..864fabbce1244 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -9,7 +9,7 @@ import { import { IConnection, IConnections, - IDataObject, IExecuteData, + IDataObject, INodeConnections, INodeIssueData, INodeTypeDescription, diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts index 3ee09923edd32..bf243815610e6 100644 --- a/packages/workflow/src/WorkflowDataProxy.ts +++ b/packages/workflow/src/WorkflowDataProxy.ts @@ -102,7 +102,6 @@ export class WorkflowDataProxy { const that = this; const node = this.workflow.nodes[nodeName]; - console.log({ that }); return new Proxy( {}, { From eb7fa9b730eeaf60229d2b12357778e0dfae7fbc Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 20 Jul 2022 11:25:49 +0300 Subject: [PATCH 33/42] chore: Undone package-lock changes. --- package-lock.json | 59 ++++++----------------------------------------- 1 file changed, 7 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index 61eec016ac367..6b6ab6e6e1976 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "n8n", - "version": "0.186.1", + "version": "0.185.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "n8n", - "version": "0.186.1", + "version": "0.185.0", "dependencies": { "@apidevtools/swagger-cli": "4.0.0", "@babel/core": "^7.14.6", @@ -22909,20 +22909,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/bonjour": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", @@ -31059,20 +31045,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/express/node_modules/qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/ext": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", @@ -44051,9 +44023,9 @@ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "engines": { "node": ">= 0.8" } @@ -79284,14 +79256,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", - "requires": { - "side-channel": "^1.0.4" - } } } }, @@ -85576,14 +85540,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", - "requires": { - "side-channel": "^1.0.4" - } } } }, @@ -95809,9 +95765,8 @@ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" + "version": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "memfs": { "version": "3.4.7", From fdf23928e8617c0b80a668d66a3a99e581112c9d Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 20 Jul 2022 11:30:16 +0300 Subject: [PATCH 34/42] fix: Removed pin data store changes. --- .../src/components/NodeDetailsView.vue | 2 +- packages/editor-ui/src/components/RunData.vue | 4 +- packages/editor-ui/src/store.ts | 61 ------------------- packages/editor-ui/src/views/NodeView.vue | 10 +-- 4 files changed, 8 insertions(+), 69 deletions(-) diff --git a/packages/editor-ui/src/components/NodeDetailsView.vue b/packages/editor-ui/src/components/NodeDetailsView.vue index 32403397c9056..944aa6353cbc2 100644 --- a/packages/editor-ui/src/components/NodeDetailsView.vue +++ b/packages/editor-ui/src/components/NodeDetailsView.vue @@ -489,7 +489,7 @@ export default mixins( return; } - this.$store.dispatch('pinData', { node: this.activeNode, data: JSON.parse(value) }); + this.$store.commit('pinData', { node: this.activeNode, data: JSON.parse(value) }); } this.$store.commit('ui/setOutputPanelEditModeEnabled', false); diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index f312c35f620de..7eda024845e4a 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -767,7 +767,7 @@ export default mixins( } this.$store.commit('ui/setOutputPanelEditModeEnabled', false); - this.$store.dispatch('pinData', { node: this.node, data: this.removeJsonKeys(value) }); + this.$store.commit('pinData', { node: this.node, data: this.removeJsonKeys(value) }); this.onDataPinningSuccess({ source: 'save-edit' }); @@ -862,7 +862,7 @@ export default mixins( this.onDataPinningSuccess({ source: 'save-edit' }); - this.$store.dispatch('pinData', { node: this.node, data }); + this.$store.commit('pinData', { node: this.node, data }); if (this.maxRunIndex > 0) { this.$showToast({ diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 864fabbce1244..359fac6282ce7 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -113,18 +113,6 @@ export const store = new Vuex.Store({ strict: process.env.NODE_ENV !== 'production', modules, state, - actions: { - pinData({commit, state}, payload: { node: INodeUi, data: PinData[string] }) { - commit('pinData', payload); - commit('setPinDataAsExecutionData', { - [payload.node.name]: payload.data, - }); - }, - setWorkflowPinData({commit, state}, payload: PinData) { - commit('setWorkflowPinData', payload); - commit('setPinDataAsExecutionData', payload); - }, - }, mutations: { // Active Actions addActiveAction(state, action: string) { @@ -235,55 +223,6 @@ export const store = new Vuex.Store({ state.stateIsDirty = true; }, - setPinDataAsExecutionData(state, payload: PinData) { - let executionData: IExecutionResponse | null = state.workflowExecutionData; - - if (!executionData) { - executionData = { - id: '', - workflowData: state.workflow, - data: { - startData: {}, - resultData: { - runData: {}, - lastNodeExecuted: '', - }, - executionData: { - contextData: {}, - nodeExecutionStack: [], - waitingExecution: {}, - waitingExecutionSource: {}, - }, - }, - mode: "manual", - startedAt: new Date(), - stoppedAt: new Date(), - finished: true, - }; - } - - Object.keys(payload).forEach((nodeName) => { - executionData!.data!.resultData.runData[nodeName] = [ - { - startTime: 0, - executionTime: 0, - source: [], - data: { - main: [ - payload[nodeName].map((data, index) => ({ - json: data, - pairedItem: { item: index }, - })), - ], - }, - }, - ]; - - executionData!.data!.resultData.lastNodeExecuted = nodeName; - }); - - // Vue.set(state, 'workflowExecutionData', executionData); - }, // Active setActive(state, newActive: boolean) { diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index dc26d940305c8..50fad1352da81 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -548,7 +548,7 @@ export default mixins( this.$store.commit('setWorkflowId', PLACEHOLDER_EMPTY_WORKFLOW_ID); this.$store.commit('setWorkflowExecutionData', data); - this.$store.dispatch('setWorkflowPinData', data.workflowData.pinData); + this.$store.commit('setWorkflowPinData', data.workflowData.pinData); await this.addNodes(JSON.parse(JSON.stringify(data.workflowData.nodes)), JSON.parse(JSON.stringify(data.workflowData.connections))); this.$nextTick(() => { @@ -613,7 +613,7 @@ export default mixins( await this.addNodes(data.workflow.nodes, data.workflow.connections); if (data.workflow.pinData) { - this.$store.dispatch('setWorkflowPinData', data.workflow.pinData); + this.$store.commit('setWorkflowPinData', data.workflow.pinData); } this.$nextTick(() => { @@ -684,7 +684,7 @@ export default mixins( this.$store.commit('setWorkflowId', workflowId); this.$store.commit('setWorkflowName', {newName: data.name, setStateDirty: false}); this.$store.commit('setWorkflowSettings', data.settings || {}); - this.$store.dispatch('setWorkflowPinData', data.pinData || {}); + this.$store.commit('setWorkflowPinData', data.pinData || {}); const tags = (data.tags || []) as ITag[]; this.$store.commit('tags/upsertTags', tags); @@ -1320,7 +1320,7 @@ export default mixins( }); if (workflowData.pinData) { - this.$store.dispatch('setWorkflowPinData', workflowData.pinData); + this.$store.commit('setWorkflowPinData', workflowData.pinData); } const tagsEnabled = this.$store.getters['settings/areTagsEnabled']; @@ -2181,7 +2181,7 @@ export default mixins( const pinData = this.$store.getters['pinDataByNodeName'](nodeName); if (pinData) { - this.$store.dispatch('pinData', { + this.$store.commit('pinData', { node: newNodeData, data: pinData, }); From e51d12621c1e0bad8d076bce8c977bac6e7dfcf6 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 20 Jul 2022 11:34:51 +0300 Subject: [PATCH 35/42] fix: Created a new object using vuex runExecutionData. --- .../src/components/mixins/workflowHelpers.ts | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index bf7d9520a1d1d..aedca43d9e6c9 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -526,18 +526,27 @@ export const workflowHelpers = mixins( const pinData: PinData[string] = this.$store.getters['pinDataByNodeName'](parentNodeName); if (pinData) { - runExecutionData.resultData.runData[parentNodeName] = [ - { - startTime: new Date().valueOf(), - executionTime: 0, - source: [], - data: { - main: [ - pinData.map((data) => ({ json: data })), + runExecutionData = { + ...runExecutionData, + resultData: { + ...runExecutionData.resultData, + runData: { + ...runExecutionData.resultData.runData, + [parentNodeName]: [ + { + startTime: new Date().valueOf(), + executionTime: 0, + source: [], + data: { + main: [ + pinData.map((data) => ({json: data})), + ], + }, + }, ], }, }, - ]; + }; } }); From 0f922137dcc9164561d82a2041b44ac6c0b8a60a Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 20 Jul 2022 13:27:06 +0300 Subject: [PATCH 36/42] fix: Fixed bug appearing when adding a new node after executing. --- .../src/components/VariableSelector.vue | 21 +++++++++++++------ .../src/components/mixins/workflowHelpers.ts | 8 ++++--- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/editor-ui/src/components/VariableSelector.vue b/packages/editor-ui/src/components/VariableSelector.vue index 8eed40da60290..8b265d47a8759 100644 --- a/packages/editor-ui/src/components/VariableSelector.vue +++ b/packages/editor-ui/src/components/VariableSelector.vue @@ -259,7 +259,7 @@ export default mixins( * @param {number} [runIndex=0] The index of the run * @param {string} [inputName='main'] The name of the input * @param {number} [outputIndex=0] The index of the output - * @param {boolean} [useShort=false] Use short notation + * @param {boolean} [useShort=false] Use short notation $json vs. $node[NodeName].json * @returns * @memberof Workflow */ @@ -309,7 +309,7 @@ export default mixins( * @param {string} nodeName The name of the node to get the data of * @param {PinData[string]} pinData The node's pin data * @param {string} filterText Filter text for parameters - * @param {boolean} [useShort=false] Use short notation + * @param {boolean} [useShort=false] Use short notation $json vs. $node[NodeName].json */ getNodePinDataOutput(nodeName: string, pinData: PinData[string], filterText: string, useShort = false): IVariableSelectorOption[] | null { const outputData = pinData.map((data) => ({ json: data } as INodeExecutionData))[0]; @@ -520,7 +520,7 @@ export default mixins( } } - let tempOutputData: IVariableSelectorOption[] | null; + let tempOutputData: IVariableSelectorOption[] | null | undefined; if (parentNode.length) { // If the node has an input node add the input data @@ -533,16 +533,25 @@ export default mixins( tempOutputData = this.getNodeRunDataOutput(parentNode[0], runData, filterText, itemIndex, 0, 'main', outputIndex, true) as IVariableSelectorOption[]; - let pinDataOptions: IVariableSelectorOption[] = []; + let pinDataOptions: IVariableSelectorOption[] = [ + { + name: 'JSON', + options: [], + }, + ]; parentNode.forEach((parentNodeName) => { const pinData = this.$store.getters['pinDataByNodeName'](parentNodeName); if (pinData) { - pinDataOptions = pinDataOptions.concat(this.getNodePinDataOutput(parentNodeName, pinData, filterText, true) || []); + const output = this.getNodePinDataOutput(parentNodeName, pinData, filterText, true); + + pinDataOptions[0].options = pinDataOptions[0].options!.concat( + output && output[0].options ? output[0].options : [], + ); } }); - if (pinDataOptions.length > 0) { + if (pinDataOptions[0].options!.length > 0) { if (tempOutputData) { const jsonTempOutputData = tempOutputData.find((tempData) => tempData.name === 'JSON'); diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index aedca43d9e6c9..0b1844da47e67 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -505,7 +505,10 @@ export const workflowHelpers = mixins( const workflowRunData = this.$store.getters.getWorkflowRunData as IRunData | null; let runIndexParent = 0; if (workflowRunData !== null && parentNode.length) { - runIndexParent = workflowRunData[parentNode[0]].length -1; + const firstParentWithWorkflowRunData = parentNode.find((parentNodeName) => workflowRunData[parentNodeName]); + if (firstParentWithWorkflowRunData) { + runIndexParent = workflowRunData[firstParentWithWorkflowRunData].length - 1; + } } const nodeConnection = workflow.getNodeConnectionIndexes(activeNode!.name, parentNode[0]); @@ -539,7 +542,7 @@ export const workflowHelpers = mixins( source: [], data: { main: [ - pinData.map((data) => ({json: data})), + pinData.map((data) => ({ json: data })), ], }, }, @@ -569,7 +572,6 @@ export const workflowHelpers = mixins( }, resolveExpression(expression: string, siblingParameters: INodeParameters = {}) { - const parameters = { '__xxxxxxx__': expression, ...siblingParameters, From 95365f37821cb9e1168b5954df3e6d0303aa3f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 20 Jul 2022 12:51:21 +0200 Subject: [PATCH 37/42] fix: Fix editor-ui build --- packages/editor-ui/src/components/VariableSelector.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/src/components/VariableSelector.vue b/packages/editor-ui/src/components/VariableSelector.vue index 8b265d47a8759..ec5e53e4b5ece 100644 --- a/packages/editor-ui/src/components/VariableSelector.vue +++ b/packages/editor-ui/src/components/VariableSelector.vue @@ -533,7 +533,7 @@ export default mixins( tempOutputData = this.getNodeRunDataOutput(parentNode[0], runData, filterText, itemIndex, 0, 'main', outputIndex, true) as IVariableSelectorOption[]; - let pinDataOptions: IVariableSelectorOption[] = [ + const pinDataOptions: IVariableSelectorOption[] = [ { name: 'JSON', options: [], From cf0568c6a119c46f235a682759dbd9549b0ce0cb Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 20 Jul 2022 15:16:50 +0300 Subject: [PATCH 38/42] feat: Added green node connectors when having pin data output. --- .../src/components/VariableSelector.vue | 2 +- packages/editor-ui/src/store.ts | 7 ++++ packages/editor-ui/src/views/NodeView.vue | 41 ++++++++++++++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/components/VariableSelector.vue b/packages/editor-ui/src/components/VariableSelector.vue index 8b265d47a8759..ec5e53e4b5ece 100644 --- a/packages/editor-ui/src/components/VariableSelector.vue +++ b/packages/editor-ui/src/components/VariableSelector.vue @@ -533,7 +533,7 @@ export default mixins( tempOutputData = this.getNodeRunDataOutput(parentNode[0], runData, filterText, itemIndex, 0, 'main', outputIndex, true) as IVariableSelectorOption[]; - let pinDataOptions: IVariableSelectorOption[] = [ + const pinDataOptions: IVariableSelectorOption[] = [ { name: 'JSON', options: [], diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 359fac6282ce7..bdb813c1024d8 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -43,6 +43,7 @@ import workflows from './modules/workflows'; import versions from './modules/versions'; import templates from './modules/templates'; import {stringSizeInBytes} from "@/components/helpers"; +import {dataPinningEventBus} from "@/event-bus/data-pinning-event-bus"; Vue.use(Vuex); @@ -214,6 +215,8 @@ export const store = new Vuex.Store({ } state.stateIsDirty = true; + + dataPinningEventBus.$emit('pin-data', { [payload.node.name]: payload.data }); }, unpinData(state, payload: { node: INodeUi }) { if (state.workflow.pinData) { @@ -222,6 +225,8 @@ export const store = new Vuex.Store({ } state.stateIsDirty = true; + + dataPinningEventBus.$emit('unpin-data', { [payload.node.name]: undefined }); }, // Active @@ -645,6 +650,8 @@ export const store = new Vuex.Store({ setWorkflowPinData(state, pinData: PinData) { Vue.set(state.workflow, 'pinData', pinData); + + dataPinningEventBus.$emit('pin-data', pinData); }, setWorkflowTagIds(state, tags: string[]) { diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 50fad1352da81..7e1e52cbbcddb 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -194,7 +194,7 @@ import { INodeCredentialsDetails, TelemetryHelpers, ITelemetryTrackProperties, - IWorkflowBase, + IWorkflowBase, PinData, } from 'n8n-workflow'; import { ICredentialsResponse, @@ -219,6 +219,8 @@ import { import '../plugins/N8nCustomConnectorType'; import '../plugins/PlusEndpointType'; +import {dataPinningEventBus} from "@/event-bus/data-pinning-event-bus"; +import {addConnectionOutputSuccess} from "./canvasHelpers"; interface AddNodeOptions { position?: XYPosition; @@ -2290,6 +2292,7 @@ export default mixins( if (connection) { const output = outputMap[sourceOutputIndex][targetNodeName][targetInputIndex]; + if (!output || !output.total) { CanvasHelpers.resetConnection(connection); } @@ -2956,6 +2959,33 @@ export default mixins( await this.importWorkflowData(workflowData); } }, + addPinDataConnections(pinData: PinData) { + Object.keys(pinData).forEach((nodeName) => { + // @ts-ignore + const connections = this.instance.getConnections({ + source: NODE_NAME_PREFIX + this.getNodeIndex(nodeName), + }) as Connection[]; + + connections.forEach((connection) => { + CanvasHelpers.addConnectionOutputSuccess(connection, { + total: pinData[nodeName].length, + iterations: 0, + }); + }); + }); + }, + removePinDataConnections(pinData: PinData) { + Object.keys(pinData).forEach((nodeName) => { + // @ts-ignore + const connections = this.instance.getConnections({ + source: NODE_NAME_PREFIX + this.getNodeIndex(nodeName), + }) as Connection[]; + + connections.forEach((connection) => { + CanvasHelpers.resetConnection(connection); + }); + }); + }, }, async mounted () { @@ -3004,10 +3034,19 @@ export default mixins( setTimeout(() => { this.$store.dispatch('users/showPersonalizationSurvey'); this.checkForNewVersions(); + this.addPinDataConnections(this.$store.getters.pinData); }, 0); }); this.$externalHooks().run('nodeView.mount'); + + dataPinningEventBus.$on('pin-data', (pinData: PinData) => { + this.addPinDataConnections(pinData); + }); + + dataPinningEventBus.$on('unpin-data', (pinData: PinData) => { + this.removePinDataConnections(pinData); + }); }, destroyed () { From 3cd16c3792e3e5273487c6b065b0dcadcceab28c Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 20 Jul 2022 16:16:28 +0300 Subject: [PATCH 39/42] chore: Fixed linting errors. --- packages/editor-ui/src/views/NodeView.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 7e1e52cbbcddb..a91715708121f 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -194,7 +194,8 @@ import { INodeCredentialsDetails, TelemetryHelpers, ITelemetryTrackProperties, - IWorkflowBase, PinData, + IWorkflowBase, + PinData, } from 'n8n-workflow'; import { ICredentialsResponse, @@ -220,7 +221,6 @@ import { import '../plugins/N8nCustomConnectorType'; import '../plugins/PlusEndpointType'; import {dataPinningEventBus} from "@/event-bus/data-pinning-event-bus"; -import {addConnectionOutputSuccess} from "./canvasHelpers"; interface AddNodeOptions { position?: XYPosition; From b18309f2a888f5ba75193778372378fbc9ce8859 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 20 Jul 2022 16:31:59 +0300 Subject: [PATCH 40/42] fix: Added pin data eventBus unsubscribe. --- packages/editor-ui/src/views/NodeView.vue | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index a91715708121f..32e3699624695 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -2981,9 +2981,7 @@ export default mixins( source: NODE_NAME_PREFIX + this.getNodeIndex(nodeName), }) as Connection[]; - connections.forEach((connection) => { - CanvasHelpers.resetConnection(connection); - }); + connections.forEach(CanvasHelpers.resetConnection); }); }, }, @@ -3040,13 +3038,8 @@ export default mixins( this.$externalHooks().run('nodeView.mount'); - dataPinningEventBus.$on('pin-data', (pinData: PinData) => { - this.addPinDataConnections(pinData); - }); - - dataPinningEventBus.$on('unpin-data', (pinData: PinData) => { - this.removePinDataConnections(pinData); - }); + dataPinningEventBus.$on('pin-data', this.addPinDataConnections); + dataPinningEventBus.$on('unpin-data', this.removePinDataConnections); }, destroyed () { @@ -3056,6 +3049,9 @@ export default mixins( this.$root.$off('newWorkflow', this.newWorkflow); this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent); this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent); + + dataPinningEventBus.$off('pin-data', this.addPinDataConnections); + dataPinningEventBus.$off('unpin-data', this.removePinDataConnections); }, }); From 030c9fa2d9ebe44b957cade502eaf2a9b5823baa Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 20 Jul 2022 16:57:38 +0300 Subject: [PATCH 41/42] fix: Added pin data color check after adding a connection. --- packages/editor-ui/src/views/NodeView.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 32e3699624695..1860cbb81441b 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -2090,6 +2090,10 @@ export default mixins( // so if we do not connect we have to save the connection manually this.$store.commit('addConnection', connectionProperties); } + + setTimeout(() => { + this.addPinDataConnections(this.$store.getters.pinData); + }); }, __removeConnection (connection: [IConnection, IConnection], removeVisualConnection = false) { if (removeVisualConnection === true) { From 9f4b37f73b5e7c1ff06cf0460e1aa74e434b3838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 20 Jul 2022 17:27:33 +0200 Subject: [PATCH 42/42] :art: Add pindata styles --- packages/design-system/theme/src/_tokens.scss | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/design-system/theme/src/_tokens.scss b/packages/design-system/theme/src/_tokens.scss index 0375b9460874e..525216e6c1b06 100644 --- a/packages/design-system/theme/src/_tokens.scss +++ b/packages/design-system/theme/src/_tokens.scss @@ -88,6 +88,14 @@ var(--color-secondary-tint-2-l) ); + --color-secondary-tint-3-h: 247; + --color-secondary-tint-3-s: 49%; + --color-secondary-tint-3-l: 95%; + --color-secondary-tint-3: hsl( + var(--color-secondary-tint-3-h), + var(--color-secondary-tint-3-s), + var(--color-secondary-tint-3-l) + ); --color-success-h: 150.4; --color-success-s: 60%; @@ -192,6 +200,33 @@ var(--color-info-tint-2-l) ); + --color-grey-h: 228; + --color-grey-s: 10%; + --color-grey-l: 80%; + --color-grey: hsl( + var(--color-grey-h), + var(--color-grey-s), + var(--color-grey-l) + ); + + --color-light-grey-h: 220; + --color-light-grey-s: 20%; + --color-light-grey-l: 88%; + --color-light-grey: hsl( + var(--color-light-grey-h), + var(--color-light-grey-s), + var(--color-light-grey-l) + ); + + --color-neutral-h: 228; + --color-neutral-s: 10%; + --color-neutral-l: 50%; + --color-neutral: hsl( + var(--color-neutral-h), + var(--color-neutral-s), + var(--color-neutral-l) + ); + --color-text-dark-h: 0; --color-text-dark-s: 0%; --color-text-dark-l: 33.3%; @@ -374,6 +409,19 @@ var(--color-sticky-default-border-l) ); + // Generated Color Shades from 50 to 950 + // Not yet used in design system + @each $color in ('neutral', 'success', 'warning', 'danger') { + @each $shade in (50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950) { + --color-#{$color}-#{$shade}-l: #{math.div($shade, 10)}#{'%'}; + --color-#{$color}-#{$shade}: hsl( + var(--color-#{$color}-h), + var(--color-#{$color}-s), + var(--color-#{$color}-#{$shade}-l) + ); + } + } + --border-radius-xlarge: 12px; --border-radius-large: 8px; --border-radius-base: 4px;