From aaf259f1ca5211eeca974b5915cb7e7f4285d0da Mon Sep 17 00:00:00 2001 From: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Date: Mon, 11 Apr 2022 15:09:14 +0200 Subject: [PATCH] feat(editor): Refactor Output Panel + fix i18n issues (#3097) * update main panel * finish up tabs * fix docs link * add icon * update node settings * clean up settings * add rename modal * fix component styles * fix spacing * truncate name * remove mixin * fix spacing * fix spacing * hide docs url * fix bug * fix renaming * refactor tabs out * refactor execute button * refactor header * add more views * fix error view * fix workflow rename bug * rename component * fix small screen bug * move items, fix positions * add hover state * show selector on empty state * add empty run state * fix binary view * 1 item * add vjs styles * show empty row for every item * refactor tabs * add branch names * fix spacing * fix up spacing * add run selector * fix positioning * clean up * increase width of selector * fix up spacing * fix copy button * fix branch naming; type issues * fix docs in custom nodes * add type * hide items when run selector is shown * increase selector size * add select prepend * clean up a bit * Add pagination * add stale icon * enable stale data in execution run * Revert "enable stale data in execution run" 8edb68dbffa0aa0d8189117e1a53381cb2c27608 * move metadata to its own state * fix smaller size * add scroll buttons * update tabs on resize * update stale data on rename * remove metadata on delete * hide x * change title colors * binary data classes * remove duplicate css * add colors * delete unused keys * use event bus * update styles of pagination * fix ts issues * fix ts issues * use chevron icons * fix design with download button * add back to canvas button * add trigger warning disabled * show trigger warning tooltip * update button labels for triggers * update node output message * fix add-option bug * add page selector * fix pagination selector bug * fix executions bug * remove hint * add json colors * add colors for json * add color json keys * fix select options bug * update keys * address comments * update name limit * align pencil * update icon size * update radio buttons height * address comments * fix pencil bug * change buttons alignment * fully center * change order of buttons * add no output message in branch * scroll to top * change active state * fix page size * all items * update expression background * update naming * align pencil * update modal background * add schedule group * update schedule nodes messages * use ellpises for last chars * fix spacing * fix tabs issue * fix too far data bug * fix executions bug * fix table wrapping * fix rename bug * add padding * handle unkown errors * add sticky header * ignore empty input, trim node name * nudge lightness of color * center buttons * update pagination * set colors of title * increase table font, fix alignment * fix pencil bug * fix spacing * use date now * address pagination issues * delete unused keys * update keys sort * fix prepend * fix radio button position * Revert "fix radio button position" ae42781786f2e6dcfb00d1be770b19a67f533bdf --- .../src/components/N8nIcon/Icon.vue | 3 + .../components/N8nInfoTip/InfoTip.stories.js | 10 +- .../src/components/N8nInfoTip/InfoTip.vue | 63 +- .../N8nRadioButtons/RadioButton.vue | 66 ++ .../N8nRadioButtons/RadioButtons.stories.js | 51 + .../N8nRadioButtons/RadioButtons.vue | 49 + .../src/components/N8nRadioButtons/index.js | 3 + .../src/components/N8nSelect/Select.vue | 71 +- .../src/components/N8nTabs/Tabs.stories.js | 54 + .../src/components/N8nTabs/Tabs.vue | 192 ++++ .../src/components/N8nTabs/index.js | 3 + .../design-system/src/components/index.js | 8 + .../src/components/utils/helpers.ts | 2 +- .../src/styleguide/colors.stories.mdx | 12 + packages/design-system/theme/src/_tokens.scss | 15 +- .../design-system/theme/src/common/var.scss | 4 +- packages/design-system/theme/src/index.scss | 2 +- .../design-system/theme/src/pagination.scss | 20 +- packages/editor-ui/src/Interface.ts | 13 + .../editor-ui/src/components/DataDisplay.vue | 149 ++- .../src/components/DisplayWithChange.vue | 128 --- .../src/components/ExpressionEdit.vue | 2 +- .../ExecutionDetails/ExecutionDetails.vue | 8 +- .../components/MainHeader/WorkflowDetails.vue | 8 +- .../src/components/NodeExecuteButton.vue | 65 ++ .../editor-ui/src/components/NodeSettings.vue | 162 +-- .../editor-ui/src/components/NodeTabs.vue | 85 ++ .../editor-ui/src/components/NodeTitle.vue | 135 +++ packages/editor-ui/src/components/RunData.vue | 1011 ++++++++++------- ...{WorkflowNameShort.vue => ShortenName.vue} | 2 +- .../src/components/mixins/nodeHelpers.ts | 3 + packages/editor-ui/src/n8n-theme.scss | 30 +- packages/editor-ui/src/plugins/components.ts | 11 +- .../src/plugins/i18n/locales/en.json | 40 +- packages/editor-ui/src/plugins/icons.ts | 2 + packages/editor-ui/src/store.ts | 15 + packages/editor-ui/src/views/NodeView.vue | 15 +- packages/nodes-base/nodes/Cron/Cron.node.ts | 2 +- .../nodes/Interval/Interval.node.ts | 2 +- 39 files changed, 1739 insertions(+), 777 deletions(-) create mode 100644 packages/design-system/src/components/N8nRadioButtons/RadioButton.vue create mode 100644 packages/design-system/src/components/N8nRadioButtons/RadioButtons.stories.js create mode 100644 packages/design-system/src/components/N8nRadioButtons/RadioButtons.vue create mode 100644 packages/design-system/src/components/N8nRadioButtons/index.js create mode 100644 packages/design-system/src/components/N8nTabs/Tabs.stories.js create mode 100644 packages/design-system/src/components/N8nTabs/Tabs.vue create mode 100644 packages/design-system/src/components/N8nTabs/index.js delete mode 100644 packages/editor-ui/src/components/DisplayWithChange.vue create mode 100644 packages/editor-ui/src/components/NodeExecuteButton.vue create mode 100644 packages/editor-ui/src/components/NodeTabs.vue create mode 100644 packages/editor-ui/src/components/NodeTitle.vue rename packages/editor-ui/src/components/{WorkflowNameShort.vue => ShortenName.vue} (96%) diff --git a/packages/design-system/src/components/N8nIcon/Icon.vue b/packages/design-system/src/components/N8nIcon/Icon.vue index 8ffe65edeccf4..b89b43a57eee3 100644 --- a/packages/design-system/src/components/N8nIcon/Icon.vue +++ b/packages/design-system/src/components/N8nIcon/Icon.vue @@ -2,6 +2,7 @@ diff --git a/packages/design-system/src/components/N8nInfoTip/InfoTip.stories.js b/packages/design-system/src/components/N8nInfoTip/InfoTip.stories.js index 382a46dac6ffa..f61d1d9f9d17a 100644 --- a/packages/design-system/src/components/N8nInfoTip/InfoTip.stories.js +++ b/packages/design-system/src/components/N8nInfoTip/InfoTip.stories.js @@ -11,7 +11,13 @@ const Template = (args, { argTypes }) => ({ N8nInfoTip, }, template: - 'Need help doing something? Open docs', + 'Need help doing something? Open docs', }); -export const InputLabel = Template.bind({}); +export const Note = Template.bind({}); + +export const Tooltip = Template.bind({}); +Tooltip.args = { + type: 'tooltip', + tooltipPlacement: 'right', +}; diff --git a/packages/design-system/src/components/N8nInfoTip/InfoTip.vue b/packages/design-system/src/components/N8nInfoTip/InfoTip.vue index 75a56f49e4bfb..4d26093f473e5 100644 --- a/packages/design-system/src/components/N8nInfoTip/InfoTip.vue +++ b/packages/design-system/src/components/N8nInfoTip/InfoTip.vue @@ -1,23 +1,48 @@ - @@ -81,7 +81,7 @@ import mixins from "vue-typed-mixins"; import { mapGetters } from "vuex"; import { MAX_WORKFLOW_NAME_LENGTH } from "@/constants"; -import WorkflowNameShort from "@/components/WorkflowNameShort.vue"; +import ShortenName from "@/components/ShortenName.vue"; import TagsContainer from "@/components/TagsContainer.vue"; import PushConnectionTracker from "@/components/PushConnectionTracker.vue"; import WorkflowActivator from "@/components/WorkflowActivator.vue"; @@ -105,7 +105,7 @@ export default mixins(workflowHelpers).extend({ components: { TagsContainer, PushConnectionTracker, - WorkflowNameShort, + ShortenName, WorkflowActivator, SaveButton, TagsDropdown, diff --git a/packages/editor-ui/src/components/NodeExecuteButton.vue b/packages/editor-ui/src/components/NodeExecuteButton.vue new file mode 100644 index 0000000000000..e478da18e1991 --- /dev/null +++ b/packages/editor-ui/src/components/NodeExecuteButton.vue @@ -0,0 +1,65 @@ + + + diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index 0d59691cf9a62..5f4a76c0dc625 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -1,17 +1,15 @@ @@ -59,12 +55,11 @@ import { IUpdateInformation, } from '@/Interface'; -import { ElTabPane } from "element-ui/types/tab-pane"; - -import DisplayWithChange from '@/components/DisplayWithChange.vue'; +import NodeTitle from '@/components/NodeTitle.vue'; import ParameterInputFull from '@/components/ParameterInputFull.vue'; import ParameterInputList from '@/components/ParameterInputList.vue'; import NodeCredentials from '@/components/NodeCredentials.vue'; +import NodeTabs from '@/components/NodeTabs.vue'; import NodeWebhooks from '@/components/NodeWebhooks.vue'; import { get, set, unset } from 'lodash'; @@ -73,21 +68,23 @@ import { genericHelpers } from '@/components/mixins/genericHelpers'; import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import mixins from 'vue-typed-mixins'; +import NodeExecuteButton from './NodeExecuteButton.vue'; export default mixins( externalHooks, genericHelpers, nodeHelpers, ) - .extend({ name: 'NodeSettings', components: { - DisplayWithChange, + NodeTitle, NodeCredentials, ParameterInputFull, ParameterInputList, + NodeTabs, NodeWebhooks, + NodeExecuteButton, }, computed: { nodeType (): INodeTypeDescription | null { @@ -150,14 +147,16 @@ export default mixins( return this.nodeType.properties; }, - workflowRunning (): boolean { - return this.$store.getters.isActionActive('workflowRunning'); + }, + props: { + eventBus: { }, }, data () { return { nodeValid: true, nodeColor: null, + openPanel: 'params', nodeValues: { color: '#ff0000', alwaysOutputData: false, @@ -271,7 +270,9 @@ export default mixins( }, }, methods: { - noOp () {}, + onNodeExecute () { + this.$emit('execute'); + }, setValue (name: string, value: NodeParameterValue) { const nameParts = name.split('.'); let lastNamePart: string | undefined = nameParts.pop(); @@ -337,6 +338,13 @@ export default mixins( this.$externalHooks().run('nodeSettings.credentialSelected', { updateInformation }); }, + nameChanged(name: string) { + // @ts-ignore + this.valueChanged({ + value: name, + name: 'name', + }); + }, valueChanged (parameterData: IUpdateInformation) { let newValue: NodeParameterValue; if (parameterData.hasOwnProperty('value')) { @@ -362,7 +370,6 @@ export default mixins( }; this.$emit('valueChanged', sendData); - this.$store.commit('setActiveNode', newValue); } else if (parameterData.name.startsWith('parameters.')) { // A node parameter changed @@ -514,20 +521,25 @@ export default mixins( this.nodeValid = false; } }, - handleTabClick(tab: ElTabPane) { - if(tab.label === 'Settings') { - this.$telemetry.track('User viewed node settings', { node_type: this.node ? this.node.type : '', workflow_id: this.$store.getters.workflowId }); - } - }, }, mounted () { this.setNodeValues(); + if (this.eventBus) { + (this.eventBus as Vue).$on('openSettings', () => { + this.openPanel = 'settings'; + }); + } }, }); - + diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index 43091926db53c..ad2348b26687a 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -1,73 +1,52 @@ @@ -225,6 +241,7 @@ import { IBinaryDisplayData, IExecutionResponse, INodeUi, + ITab, ITableData, } from '@/Interface'; @@ -234,15 +251,16 @@ import { } from '@/constants'; import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue'; +import WarningTooltip from '@/components/WarningTooltip.vue'; import NodeErrorView from '@/components/Error/NodeErrorView.vue'; import { copyPaste } from '@/components/mixins/copyPaste'; import { externalHooks } from "@/components/mixins/externalHooks"; import { genericHelpers } from '@/components/mixins/genericHelpers'; import { nodeHelpers } from '@/components/mixins/nodeHelpers'; -import { workflowRun } from '@/components/mixins/workflowRun'; import mixins from 'vue-typed-mixins'; +import Vue from 'vue/types/umd'; import { saveAs } from 'file-saver'; @@ -254,7 +272,6 @@ export default mixins( externalHooks, genericHelpers, nodeHelpers, - workflowRun, ) .extend({ name: 'RunData', @@ -262,13 +279,14 @@ export default mixins( BinaryDataDisplay, NodeErrorView, VueJsonPretty, + WarningTooltip, }, data () { return { binaryDataPreviewActive: false, dataSize: 0, deselectedPlaceholder, - displayMode: this.$locale.baseText('runData.table'), + displayMode: 'table', state: { value: '' as object | number | string, path: deselectedPlaceholder, @@ -276,18 +294,48 @@ export default mixins( runIndex: 0, showData: false, outputIndex: 0, - maxDisplayItems: 25 as number | null, binaryDataDisplayVisible: false, binaryDataDisplayData: null as IBinaryDisplayData | null, MAX_DISPLAY_DATA_SIZE, MAX_DISPLAY_ITEMS_AUTO_ALL, + currentPage: 1, + pageSize: 10, + pageSizes: [10, 25, 50, 100], }; }, mounted() { this.init(); }, computed: { + nodeType (): INodeTypeDescription | null { + if (this.node) { + return this.$store.getters.nodeType(this.node.type, this.node.typeVersion); + } + return null; + }, + isTriggerNode (): boolean { + return !!(this.nodeType && this.nodeType.group.includes('trigger')); + }, + isPollingTypeNode (): boolean { + return !!(this.nodeType && this.nodeType.polling); + }, + isScheduleTrigger (): boolean { + return !!(this.nodeType && this.nodeType.group.includes('schedule')); + }, + buttons(): Array<{label: string, value: string}> { + const defaults = [ + { label: this.$locale.baseText('runData.table'), value: 'table'}, + { label: this.$locale.baseText('runData.json'), value: 'json'}, + ]; + if (this.binaryData.length) { + return [ ...defaults, + { label: this.$locale.baseText('runData.binary'), value: 'binary'}, + ]; + } + + return defaults; + }, hasNodeRun(): boolean { return Boolean(this.node && this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name)); }, @@ -305,19 +353,15 @@ export default mixins( return null; } const executionData: IRunExecutionData = this.workflowExecution.data; - return executionData.resultData.runData; - }, - maxDisplayItemsOptions (): number[] { - const options = [25, 50, 100, 250, 500, 1000].filter(option => option <= this.dataCount); - if (!options.includes(this.dataCount)) { - options.push(this.dataCount); + if (executionData && executionData.resultData) { + return executionData.resultData.runData; } - return options; + return null; }, node (): INodeUi | null { return this.$store.getters.activeNode; }, - runMetadata () { + runTaskData (): ITaskData | null { if (!this.node || this.workflowExecution === null) { return null; } @@ -332,40 +376,33 @@ export default mixins( return null; } - const taskData: ITaskData = runData[this.node.name][this.runIndex]; + return runData[this.node.name][this.runIndex]; + }, + runMetadata (): {executionTime: number, startTime: string} | null { + if (!this.runTaskData) { + return null; + } return { - executionTime: taskData.executionTime, - startTime: new Date(taskData.startTime).toLocaleString(), + executionTime: this.runTaskData.executionTime, + startTime: new Date(this.runTaskData.startTime).toLocaleString(), }; }, - dataCount (): number { - if (this.node === null) { - return 0; - } - - const runData: IRunData | null = this.workflowRunData; - - if (runData === null || !runData.hasOwnProperty(this.node.name)) { - return 0; - } - - if (runData[this.node.name].length <= this.runIndex) { - return 0; + staleData(): boolean { + if (!this.node) { + return false; } - - if (runData[this.node.name][this.runIndex].hasOwnProperty('error')) { - return 1; - } - - if (!runData[this.node.name][this.runIndex].hasOwnProperty('data') || - runData[this.node.name][this.runIndex].data === undefined - ) { - return 0; + const updatedAt = this.$store.getters.getParametersLastUpdated(this.node.name); + if (!updatedAt || !this.runTaskData) { + return false; } - - const inputData = this.getMainInputData(runData[this.node.name][this.runIndex].data!, this.outputIndex); - - return inputData.length; + const runAt = this.runTaskData.startTime; + return updatedAt > runAt; + }, + dataCount (): number { + return this.getDataCount(this.runIndex, this.outputIndex); + }, + dataSizeInMB(): string { + return (this.dataSize / 1024 / 1000).toLocaleString(); }, maxOutputIndex (): number { if (this.node === null) { @@ -407,29 +444,22 @@ export default mixins( return 0; }, - jsonData (): IDataObject[] { + inputData (): INodeExecutionData[] { let inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex); if (inputData.length === 0 || !Array.isArray(inputData)) { return []; } - if (this.maxDisplayItems !== null) { - inputData = inputData.slice(0, this.maxDisplayItems); - } + const offset = this.pageSize * (this.currentPage - 1); + inputData = inputData.slice(offset, offset + this.pageSize); - return this.convertToJson(inputData); + return inputData; + }, + jsonData (): IDataObject[] { + return this.convertToJson(this.inputData); }, tableData (): ITableData | undefined { - let inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex); - if (inputData.length === 0) { - return undefined; - } - - if (this.maxDisplayItems !== null) { - inputData = inputData.slice(0,this.maxDisplayItems); - } - - return this.convertToTable(inputData); + return this.convertToTable(this.inputData); }, binaryData (): IBinaryKeyData[] { if (this.node === null) { @@ -438,17 +468,106 @@ export default mixins( return this.getBinaryData(this.workflowRunData, this.node.name, this.runIndex, this.outputIndex); }, + branches (): ITab[] { + function capitalize(name: string) { + return name.charAt(0).toLocaleUpperCase() + name.slice(1); + } + const branches: ITab[] = []; + for (let i = 0; i <= this.maxOutputIndex; i++) { + const itemsCount = this.getDataCount(this.runIndex, i); + const items = this.$locale.baseText(itemsCount === 1 ? 'ndv.output.item': 'ndv.output.items'); + let outputName = this.getOutputName(i); + if (`${outputName}` === `${i}`) { + outputName = `${this.$locale.baseText('ndv.output')} ${outputName}`; + } + else { + outputName = capitalize(`${this.getOutputName(i)} ${this.$locale.baseText('ndv.output.branch')}`); + } + branches.push({ + label: itemsCount ? `${outputName} (${itemsCount} ${items})` : outputName, + value: i, + }); + } + return branches; + }, }, methods: { + onPageSizeChange(pageSize: number) { + this.pageSize = pageSize; + const maxPage = Math.ceil(this.dataCount / this.pageSize); + if (maxPage < this.currentPage) { + this.currentPage = maxPage; + } + }, + onDisplayModeChange(displayMode: string) { + const previous = this.displayMode; + this.displayMode = displayMode; + + const dataContainer = this.$refs.dataContainer; + if (dataContainer) { + const dataDisplay = (dataContainer as Element).children[0]; + + if (dataDisplay){ + dataDisplay.scrollTo(0, 0); + } + } + + this.closeBinaryDataDisplay(); + this.$externalHooks().run('runData.displayModeChanged', { newValue: displayMode, oldValue: previous }); + if(this.node) { + const nodeType = this.node ? this.node.type : ''; + this.$telemetry.track('User changed node output view mode', { old_mode: previous, new_mode: displayMode, node_type: nodeType, workflow_id: this.$store.getters.workflowId }); + } + }, + getRunLabel(option: number) { + let itemsCount = 0; + for (let i = 0; i <= this.maxOutputIndex; i++) { + itemsCount += this.getDataCount(option - 1, i); + } + const items = this.$locale.baseText(itemsCount === 1 ? 'ndv.output.item': 'ndv.output.items'); + const itemsLabel = itemsCount > 0 ? ` (${itemsCount} ${items})` : ''; + return option + this.$locale.baseText('ndv.output.of') + (this.maxRunIndex+1) + itemsLabel; + }, + getDataCount(runIndex: number, outputIndex: number) { + if (this.node === null) { + return 0; + } + + const runData: IRunData | null = this.workflowRunData; + + if (runData === null || !runData.hasOwnProperty(this.node.name)) { + return 0; + } + + if (runData[this.node.name].length <= runIndex) { + return 0; + } + + if (runData[this.node.name][runIndex].hasOwnProperty('error')) { + return 1; + } + + if (!runData[this.node.name][runIndex].hasOwnProperty('data') || + runData[this.node.name][runIndex].data === undefined + ) { + return 0; + } + + const inputData = this.getMainInputData(runData[this.node.name][runIndex].data!, outputIndex); + + return inputData.length; + }, + openSettings() { + this.$emit('openSettings'); + }, init() { // Reset the selected output index every time another node gets selected this.outputIndex = 0; - this.maxDisplayItems = 25; this.refreshDataSize(); - if (this.displayMode === this.$locale.baseText('runData.binary')) { + if (this.displayMode === 'binary') { this.closeBinaryDataDisplay(); if (this.binaryData.length === 0) { - this.displayMode = this.$locale.baseText('runData.table'); + this.displayMode = 'table'; } } }, @@ -562,7 +681,7 @@ export default mixins( return outputIndex + 1; } - const nodeType = this.$store.getters.nodeType(this.node.type, this.node.typeVersion) as INodeTypeDescription | null; + const nodeType = this.nodeType; if (!nodeType || !nodeType.outputNames || nodeType.outputNames.length <= outputIndex) { return outputIndex + 1; } @@ -643,7 +762,8 @@ export default mixins( // Check how much data there is to display const inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex); - const jsonItems = inputData.slice(0, this.maxDisplayItems || inputData.length).map(item => item.json); + const offset = this.pageSize * (this.currentPage - 1); + const jsonItems = inputData.slice(offset, offset + this.pageSize).map(item => item.json); this.dataSize = JSON.stringify(jsonItems).length; @@ -660,14 +780,6 @@ export default mixins( jsonData () { this.refreshDataSize(); }, - displayMode (newValue, oldValue) { - this.closeBinaryDataDisplay(); - this.$externalHooks().run('runData.displayModeChanged', { newValue, oldValue }); - if(this.node) { - const nodeType = this.node ? this.node.type : ''; - this.$telemetry.track('User changed node output view mode', { old_mode: oldValue, new_mode: newValue, node_type: nodeType, workflow_id: this.$store.getters.workflowId }); - } - }, maxRunIndex () { this.runIndex = Math.min(this.runIndex, this.maxRunIndex); }, @@ -675,188 +787,279 @@ export default mixins( }); - + + diff --git a/packages/editor-ui/src/components/WorkflowNameShort.vue b/packages/editor-ui/src/components/ShortenName.vue similarity index 96% rename from packages/editor-ui/src/components/WorkflowNameShort.vue rename to packages/editor-ui/src/components/ShortenName.vue index eb9b73c063752..dca88204b1b03 100644 --- a/packages/editor-ui/src/components/WorkflowNameShort.vue +++ b/packages/editor-ui/src/components/ShortenName.vue @@ -11,7 +11,7 @@ const DEFAULT_WORKFLOW_NAME_LIMIT = 25; const WORKFLOW_NAME_END_COUNT_TO_KEEP = 4; export default Vue.extend({ - name: "WorkflowNameShort", + name: "ShortenName", props: ["name", "limit"], computed: { shortenedName(): string { diff --git a/packages/editor-ui/src/components/mixins/nodeHelpers.ts b/packages/editor-ui/src/components/mixins/nodeHelpers.ts index f4c33408cbf05..6335a7e4c5b04 100644 --- a/packages/editor-ui/src/components/mixins/nodeHelpers.ts +++ b/packages/editor-ui/src/components/mixins/nodeHelpers.ts @@ -287,6 +287,9 @@ export const nodeHelpers = mixins( return []; } const executionData: IRunExecutionData = this.$store.getters.getWorkflowExecution.data; + if (!executionData || !executionData.resultData) { // unknown status + return []; + } const runData = executionData.resultData.runData; if (runData === null || runData[node.name] === undefined || diff --git a/packages/editor-ui/src/n8n-theme.scss b/packages/editor-ui/src/n8n-theme.scss index acd8ab1c75554..120c2cb8a8128 100644 --- a/packages/editor-ui/src/n8n-theme.scss +++ b/packages/editor-ui/src/n8n-theme.scss @@ -120,9 +120,6 @@ .el-tabs__nav:focus { outline: none; } -.el-tabs__item { - color: #555; -} .el-tabs__item.is-active { font-weight: bold; } @@ -185,3 +182,30 @@ } } } + +.add-option { + > * { + border: none; + } + + i.el-select__caret { + color: var(--color-foreground-xlight); + } + .el-input .el-input__inner { + &, + &:hover, + &:focus { + border-radius: 20px; + color: var(--color-foreground-xlight); + font-weight: 600; + background-color: var(--color-primary); + border-color: var(--color-primary); + text-align: center; + } + + &::placeholder { + color: var(--color-foreground-xlight); + opacity: 1; /** Firefox */ + } + } +} diff --git a/packages/editor-ui/src/plugins/components.ts b/packages/editor-ui/src/plugins/components.ts index 9a6cca9964ac8..e902e1580c45d 100644 --- a/packages/editor-ui/src/plugins/components.ts +++ b/packages/editor-ui/src/plugins/components.ts @@ -41,6 +41,8 @@ import { Message, Notification, CollapseTransition, + Pagination, + Popover, N8nActionBox, N8nAvatar, @@ -60,8 +62,10 @@ import { N8nMenu, N8nMenuItem, N8nOption, + N8nRadioButtons, N8nSelect, N8nSpinner, + N8nTabs, N8nFormInputs, N8nFormBox, N8nSquareButton, @@ -81,7 +85,7 @@ Vue.use(N8nAvatar); Vue.use(N8nButton); Vue.component('n8n-form-box', N8nFormBox); Vue.component('n8n-form-inputs', N8nFormInputs); -Vue.use('n8n-icon', N8nIcon); +Vue.component('n8n-icon', N8nIcon); Vue.use(N8nIconButton); Vue.use(N8nInfoTip); Vue.use(N8nInput); @@ -96,8 +100,10 @@ Vue.use(N8nMenuItem); Vue.use(N8nOption); Vue.use(N8nSelect); Vue.use(N8nSpinner); +Vue.use(N8nRadioButtons); Vue.component('n8n-square-button', N8nSquareButton); Vue.use(N8nTags); +Vue.component('n8n-tabs', N8nTabs); Vue.use(N8nTag); Vue.component('n8n-text', N8nText); Vue.use(N8nTooltip); @@ -130,6 +136,9 @@ Vue.use(Badge); Vue.use(Card); Vue.use(ColorPicker); Vue.use(Container); +Vue.use(Pagination); +Vue.use(Popover); + Vue.use(VueAgile); Vue.component(CollapseTransition.name, CollapseTransition); diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 6a715c9c3748b..b37ca0f446c38 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -288,6 +288,38 @@ "multipleParameter.deleteItem": "Delete item", "multipleParameter.moveDown": "Move down", "multipleParameter.moveUp": "Move up", + "ndv.backToCanvas": "Back to canvas", + "ndv.backToCanvas.waitingForTriggerWarning": "Waiting for a Trigger node to execute. Close this view to see the Workflow Canvas.", + "ndv.execute.executeNode": "Execute node", + "ndv.execute.executing": "Executing", + "ndv.execute.fetchEvent": "Fetch Event", + "ndv.execute.listenForEvent": "Listen For Event", + "ndv.output": "Output", + "ndv.output.all": "all", + "ndv.output.branch": "Branch", + "ndv.output.emptyInput": "This input item is empty. {name} will still execute when it recieves an empty item.", + "ndv.output.emptyOutput": "This output item is empty.", + "ndv.output.executing": "Executing node...", + "ndv.output.item": "item", + "ndv.output.items": "items", + "ndv.output.noOutputData.message": "n8n stops executing the workflow when a node has no output data. You can change this default behaviour via", + "ndv.output.noOutputData.message.settings": "Settings", + "ndv.output.noOutputData.message.settingsOption": "> “Always Output Data”.", + "ndv.output.noOutputData.title": "No output data returned", + "ndv.output.noOutputDataInBranch": "No output data in this branch", + "ndv.output.of": " of ", + "ndv.output.pageSize": "Page Size", + "ndv.output.pollEventNodeHint": "Fetch an event to output data", + "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.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!", + "ndv.output.triggerEventNodeHint": "Listen for an event to output data", + "ndv.title.cancel": "Cancel", + "ndv.title.rename": "Rename", + "ndv.title.renameNode": "Rename node", "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", @@ -357,6 +389,7 @@ "nodeSettings.clickOnTheQuestionMarkIcon": "Click the '?' icon to open this node on n8n.io", "nodeSettings.continueOnFail.description": "If active, the workflow continues even if this node's execution fails. When this occurs, the node passes along input data from previous nodes - so your workflow should account for unexpected output data.", "nodeSettings.continueOnFail.displayName": "Continue On Fail", + "nodeSettings.docs": "Docs", "nodeSettings.executeOnce.description": "If active, the node executes only once, with data from the first item it receives", "nodeSettings.executeOnce.displayName": "Execute Once", "nodeSettings.maxTries.description": "Number of times to attempt to execute the node before failing the execution", @@ -370,7 +403,6 @@ "nodeSettings.parameters": "Parameters", "nodeSettings.retryOnFail.description": "If active, the node tries to execute again when it fails", "nodeSettings.retryOnFail.displayName": "Retry On Fail", - "nodeSettings.settings": "Settings", "nodeSettings.theNodeIsNotValidAsItsTypeIsUnknown": "The node is not valid as its type ({nodeType}) is unknown", "nodeSettings.thisNodeDoesNotHaveAnyParameters": "This node does not have any parameters", "nodeSettings.waitBetweenTries.description": "How long to wait between each attempt (in milliseconds)", @@ -551,13 +583,8 @@ "runData.copyParameterPath": "Copy Parameter Path", "runData.copyToClipboard": "Copy to Clipboard", "runData.copyValue": "Copy Value", - "runData.dataOfExecution": "Data of Execution", - "runData.dataReturnedByThisNodeWillDisplayHere": "Data returned by this node will display here", - "runData.displayDataAnyway": "Display Data Anyway", "runData.downloadBinaryData": "Download", - "runData.entriesExistButThey": "Entries exist but they do not contain any JSON data", "runData.executeNode": "Execute Node", - "runData.executesThisNodeAfterExecuting": "Executes this {nodeName} node after executing any previous nodes that have not yet returned data", "runData.executionTime": "Execution Time", "runData.fileExtension": "File Extension", "runData.fileName": "File Name", @@ -573,7 +600,6 @@ "runData.showBinaryData": "View", "runData.startTime": "Start Time", "runData.table": "Table", - "runData.theNodeContains": "The node contains {numberOfKb} KB of data.
Displaying it could cause problems.

If you do decide to display it, consider avoiding the JSON view.", "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 22908d518dce0..f7c86d9ec4c8d 100644 --- a/packages/editor-ui/src/plugins/icons.ts +++ b/packages/editor-ui/src/plugins/icons.ts @@ -61,6 +61,7 @@ import { faPause, faPauseCircle, faPen, + faPencilAlt, faPlay, faPlayCircle, faPlus, @@ -158,6 +159,7 @@ addIcon(faNetworkWired); addIcon(faPause); addIcon(faPauseCircle); addIcon(faPen); +addIcon(faPencilAlt); addIcon(faPlay); addIcon(faPlayCircle); addIcon(faPlus); diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 418a22a33fe76..d465d9fce76b0 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -90,6 +90,7 @@ const state: IRootState = { }, sidebarMenuItems: [], instanceId: '', + nodeMetadata: {}, }; const modules = { @@ -328,6 +329,9 @@ export const store = new Vuex.Store({ if (state.lastSelectedNode === nameData.old) { state.lastSelectedNode = nameData.new; } + + Vue.set(state.nodeMetadata, nameData.new, state.nodeMetadata[nameData.old]); + Vue.delete(state.nodeMetadata, nameData.old); }, resetAllNodesIssues (state) { @@ -418,6 +422,8 @@ export const store = new Vuex.Store({ state.workflow.nodes.push(nodeData); }, removeNode (state, node: INodeUi) { + Vue.delete(state.nodeMetadata, 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); @@ -470,6 +476,11 @@ export const store = new Vuex.Store({ state.stateIsDirty = true; Vue.set(node, 'parameters', updateInformation.value); + + if (!state.nodeMetadata[node.name]) { + Vue.set(state.nodeMetadata, node.name, {}); + } + Vue.set(state.nodeMetadata[node.name], 'parametersLastUpdatedAt', Date.now()); }, // Node-Index @@ -666,6 +677,10 @@ export const store = new Vuex.Store({ return state.activeExecutions; }, + getParametersLastUpdated: (state): ((name: string) => number | undefined) => { + return (nodeName: string) => state.nodeMetadata[nodeName] && state.nodeMetadata[nodeName].parametersLastUpdatedAt; + }, + getBaseUrl: (state): string => { return state.baseUrl; }, diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index f6f8123ddd289..18c4b83c73974 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -33,7 +33,7 @@ > - +
@@ -344,6 +344,7 @@ export default mixins( pullConnActiveNodeName: null as string | null, pullConnActive: false, dropPrevented: false, + renamingActive: false, }; }, beforeDestroy () { @@ -2235,6 +2236,13 @@ export default mixins( if (currentName === newName) { return; } + + const activeNodeName = this.activeNode && this.activeNode.name; + const isActive = activeNodeName === currentName; + if (isActive) { + this.renamingActive = true; + } + // Check if node-name is unique else find one that is newName = this.getUniqueNodeName({ originalName: newName, @@ -2262,6 +2270,11 @@ export default mixins( // Make sure that the node is selected again this.deselectAllNodes(); this.nodeSelectedByName(newName); + + if (isActive) { + this.$store.commit('setActiveNode', newName); + this.renamingActive = false; + } }, deleteEveryEndpoint () { // Check as it does not exist on first load diff --git a/packages/nodes-base/nodes/Cron/Cron.node.ts b/packages/nodes-base/nodes/Cron/Cron.node.ts index 4a6cfa7d02669..54782b08f5fe6 100644 --- a/packages/nodes-base/nodes/Cron/Cron.node.ts +++ b/packages/nodes-base/nodes/Cron/Cron.node.ts @@ -22,7 +22,7 @@ export class Cron implements INodeType { displayName: 'Cron', name: 'cron', icon: 'fa:calendar', - group: ['trigger'], + group: ['trigger', 'schedule'], version: 1, description: 'Triggers the workflow at a specific time', eventTriggerDescription: '', diff --git a/packages/nodes-base/nodes/Interval/Interval.node.ts b/packages/nodes-base/nodes/Interval/Interval.node.ts index c71317bdd012d..61b78455b6471 100644 --- a/packages/nodes-base/nodes/Interval/Interval.node.ts +++ b/packages/nodes-base/nodes/Interval/Interval.node.ts @@ -12,7 +12,7 @@ export class Interval implements INodeType { displayName: 'Interval', name: 'interval', icon: 'fa:hourglass', - group: ['trigger'], + group: ['trigger', 'schedule'], version: 1, description: 'Triggers the workflow in a given interval', eventTriggerDescription: '',