-
-
+
+ {{ $locale.baseText('workflowActivator.thisWorkflowHasNoTriggerNodes') }}
+
+
+
- {{ $locale.baseText('workflowActivator.theWorkflowIsSetToBeActiveBut') }}
+
@@ -27,13 +30,17 @@ import { genericHelpers } from '@/components/mixins/genericHelpers';
import { restApi } from '@/components/mixins/restApi';
import { showMessage } from '@/components/mixins/showMessage';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
-import {
- IWorkflowDataUpdate,
-} from '../Interface';
import mixins from 'vue-typed-mixins';
import { mapGetters } from "vuex";
+import {
+ WORKFLOW_ACTIVE_MODAL_KEY,
+ LOCAL_STORAGE_ACTIVATION_FLAG,
+} from '@/constants';
+import { getActivatableTriggerNodes } from './helpers';
+
+
export default mixins(
externalHooks,
genericHelpers,
@@ -45,7 +52,6 @@ export default mixins(
{
name: 'WorkflowActivator',
props: [
- 'disabled',
'workflowActive',
'workflowId',
],
@@ -74,59 +80,47 @@ export default mixins(
}
return '#13ce66';
},
+ isCurrentWorkflow(): boolean {
+ return this.$store.getters['workflowId'] === this.workflowId;
+ },
+ disabled(): boolean {
+ const isNewWorkflow = !this.workflowId;
+ if (isNewWorkflow || this.isCurrentWorkflow) {
+ return !this.workflowActive && !this.containsTrigger;
+ }
+
+ return false;
+ },
+ containsTrigger(): boolean {
+ const foundTriggers = getActivatableTriggerNodes(this.$store.getters.workflowTriggerNodes);
+ return foundTriggers.length > 0;
+ },
},
methods: {
async activeChanged (newActiveState: boolean) {
- if (this.workflowId === undefined) {
- this.$showMessage({
- title: this.$locale.baseText('workflowActivator.showMessage.activeChangedWorkflowIdUndefined.title'),
- message: this.$locale.baseText('workflowActivator.showMessage.activeChangedWorkflowIdUndefined.message'),
- type: 'error',
- });
- return;
- }
-
- if (this.nodesIssuesExist === true) {
- this.$showMessage({
- title: this.$locale.baseText('workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.title'),
- message: this.$locale.baseText('workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.message'),
- type: 'error',
- });
- return;
- }
-
- // Set that the active state should be changed
- let data: IWorkflowDataUpdate = {};
-
- const activeWorkflowId = this.$store.getters.workflowId;
- if (newActiveState === true && this.workflowId === activeWorkflowId) {
- // If the currently active workflow gets activated save the whole
- // workflow. If that would not happen then it could be quite confusing
- // for people because it would activate a different version of the workflow
- // than the one they can currently see.
- if (this.dirtyState) {
- const importConfirm = await this.confirmMessage(
- this.$locale.baseText('workflowActivator.confirmMessage.message'),
- this.$locale.baseText('workflowActivator.confirmMessage.headline'),
- 'warning',
- this.$locale.baseText('workflowActivator.confirmMessage.confirmButtonText'),
- this.$locale.baseText('workflowActivator.confirmMessage.cancelButtonText'),
- );
- if (importConfirm === false) {
- return;
- }
- }
-
- // Get the current workflow data that it gets saved together with the activation
- data = await this.getWorkflowDataToSave();
- }
-
- data.active = newActiveState;
-
this.loading = true;
+ if (!this.workflowId) {
+ const saved = await this.saveCurrentWorkflow();
+ if (!saved) {
+ this.loading = false;
+ return;
+ }
+ }
+
try {
- await this.restApi().updateWorkflow(this.workflowId, data);
+ if (this.isCurrentWorkflow && this.nodesIssuesExist) {
+ this.$showMessage({
+ title: this.$locale.baseText('workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.title'),
+ message: this.$locale.baseText('workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.message'),
+ type: 'error',
+ });
+
+ this.loading = false;
+ return;
+ }
+
+ await this.updateWorkflow({workflowId: this.workflowId, active: newActiveState});
} catch (error) {
const newStateName = newActiveState === true ? 'activated' : 'deactivated';
this.$showError(
@@ -141,27 +135,21 @@ export default mixins(
return;
}
- const currentWorkflowId = this.$store.getters.workflowId;
- let activationEventName = 'workflow.activeChange';
- if (currentWorkflowId === this.workflowId) {
- // If the status of the current workflow got changed
- // commit it specifically
- this.$store.commit('setActive', newActiveState);
- activationEventName = 'workflow.activeChangeCurrent';
- }
-
- if (newActiveState === true) {
- this.$store.commit('setWorkflowActive', this.workflowId);
- } else {
- this.$store.commit('setWorkflowInactive', this.workflowId);
- }
-
+ const activationEventName = this.isCurrentWorkflow ? 'workflow.activeChangeCurrent' : 'workflow.activeChange';
this.$externalHooks().run(activationEventName, { workflowId: this.workflowId, active: newActiveState });
this.$telemetry.track('User set workflow active status', { workflow_id: this.workflowId, is_active: newActiveState });
this.$emit('workflowActiveChanged', { id: this.workflowId, active: newActiveState });
this.loading = false;
- this.$store.dispatch('settings/fetchPromptsData');
+
+ if (this.isCurrentWorkflow) {
+ if (newActiveState && window.localStorage.getItem(LOCAL_STORAGE_ACTIVATION_FLAG) !== 'true') {
+ this.$store.dispatch('ui/openModal', WORKFLOW_ACTIVE_MODAL_KEY);
+ }
+ else {
+ this.$store.dispatch('settings/fetchPromptsData');
+ }
+ }
},
async displayActivationError () {
let errorMessage: string;
@@ -192,7 +180,8 @@ export default mixins(
);
-
diff --git a/packages/editor-ui/src/components/WorkflowOpen.vue b/packages/editor-ui/src/components/WorkflowOpen.vue
index 235c36230b..fcb91454ec 100644
--- a/packages/editor-ui/src/components/WorkflowOpen.vue
+++ b/packages/editor-ui/src/components/WorkflowOpen.vue
@@ -183,7 +183,7 @@ export default mixins(
params: { name: data.id },
});
}
- this.$store.commit('ui/closeTopModal');
+ this.$store.commit('ui/closeAllModals');
}
},
openDialog () {
diff --git a/packages/editor-ui/src/components/helpers.ts b/packages/editor-ui/src/components/helpers.ts
index 506b3de618..a15a7396ae 100644
--- a/packages/editor-ui/src/components/helpers.ts
+++ b/packages/editor-ui/src/components/helpers.ts
@@ -1,3 +1,5 @@
+import { ERROR_TRIGGER_NODE_TYPE } from '@/constants';
+import { INodeUi } from '@/Interface';
import dateformat from 'dateformat';
const KEYWORDS_TO_FILTER = ['API', 'OAuth1', 'OAuth2'];
@@ -18,3 +20,14 @@ export function getStyleTokenValue(name: string): string {
const style = getComputedStyle(document.body);
return style.getPropertyValue(name);
}
+
+export function getTriggerNodeServiceName(nodeName: string) {
+ return nodeName.replace(/ trigger/i, '');
+}
+
+export function getActivatableTriggerNodes(nodes: INodeUi[]) {
+ return nodes.filter((node: INodeUi) => {
+ // Error Trigger does not behave like other triggers and workflows using it can not be activated
+ return !node.disabled && node.type !== ERROR_TRIGGER_NODE_TYPE;
+ });
+}
diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts
index fc963b54ca..dd328010a0 100644
--- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts
+++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts
@@ -437,6 +437,32 @@ export const workflowHelpers = mixins(
return returnData['__xxxxxxx__'];
},
+ async updateWorkflow({workflowId, active}: {workflowId: string, active?: boolean}) {
+ let data: IWorkflowDataUpdate = {};
+
+ const isCurrentWorkflow = workflowId === this.$store.getters.workflowId;
+ if (isCurrentWorkflow) {
+ data = await this.getWorkflowDataToSave();
+ }
+
+ if (active !== undefined) {
+ data.active = active;
+ }
+
+ const workflow = await this.restApi().updateWorkflow(workflowId, data);
+
+ if (isCurrentWorkflow) {
+ this.$store.commit('setActive', !!workflow.active);
+ this.$store.commit('setStateDirty', false);
+ }
+
+ if (workflow.active) {
+ this.$store.commit('setWorkflowActive', workflowId);
+ } else {
+ this.$store.commit('setWorkflowInactive', workflowId);
+ }
+ },
+
async saveCurrentWorkflow({name, tags}: {name?: string, tags?: string[]} = {}): Promise
{
const currentWorkflow = this.$route.params.name;
if (!currentWorkflow) {
diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts
index 065712d967..a4001cef03 100644
--- a/packages/editor-ui/src/constants.ts
+++ b/packages/editor-ui/src/constants.ts
@@ -28,6 +28,8 @@ export const CREDENTIAL_LIST_MODAL_KEY = 'credentialsList';
export const PERSONALIZATION_MODAL_KEY = 'personalization';
export const CONTACT_PROMPT_MODAL_KEY = 'contactPrompt';
export const VALUE_SURVEY_MODAL_KEY = 'valueSurvey';
+export const EXECUTIONS_MODAL_KEY = 'executions';
+export const WORKFLOW_ACTIVE_MODAL_KEY = 'activation';
// breakpoints
export const BREAKPOINT_SM = 768;
@@ -135,4 +137,5 @@ export const OTHER_WORK_AREA_KEY = 'otherWorkArea';
export const OTHER_COMPANY_INDUSTRY_KEY = 'otherCompanyIndustry';
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';
diff --git a/packages/editor-ui/src/modules/ui.ts b/packages/editor-ui/src/modules/ui.ts
index 7a5a25cd10..331828bbe1 100644
--- a/packages/editor-ui/src/modules/ui.ts
+++ b/packages/editor-ui/src/modules/ui.ts
@@ -1,4 +1,4 @@
-import { CONTACT_PROMPT_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY, DUPLICATE_MODAL_KEY, PERSONALIZATION_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, VERSIONS_MODAL_KEY, WORKFLOW_OPEN_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY, CREDENTIAL_LIST_MODAL_KEY, VALUE_SURVEY_MODAL_KEY } from '@/constants';
+import { CONTACT_PROMPT_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY, DUPLICATE_MODAL_KEY, PERSONALIZATION_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, VERSIONS_MODAL_KEY, WORKFLOW_OPEN_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY, CREDENTIAL_LIST_MODAL_KEY, VALUE_SURVEY_MODAL_KEY, EXECUTIONS_MODAL_KEY, WORKFLOW_ACTIVE_MODAL_KEY } from '@/constants';
import Vue from 'vue';
import { ActionContext, Module } from 'vuex';
import {
@@ -45,6 +45,12 @@ const module: Module = {
[WORKFLOW_SETTINGS_MODAL_KEY]: {
open: false,
},
+ [EXECUTIONS_MODAL_KEY]: {
+ open: false,
+ },
+ [WORKFLOW_ACTIVE_MODAL_KEY]: {
+ open: false,
+ },
},
modalStack: [],
sidebarMenuCollapsed: true,
@@ -81,17 +87,19 @@ const module: Module = {
Vue.set(state.modals[name], 'open', true);
state.modalStack = [name].concat(state.modalStack);
},
- closeTopModal: (state: IUiState) => {
- const name = state.modalStack[0];
+ closeModal: (state: IUiState, name: string) => {
Vue.set(state.modals[name], 'open', false);
- if (state.modals.mode) {
- Vue.set(state.modals[name], 'mode', '');
- }
- if (state.modals.activeId) {
- Vue.set(state.modals[name], 'activeId', '');
- }
-
- state.modalStack = state.modalStack.slice(1);
+ state.modalStack = state.modalStack.filter((openModalName: string) => {
+ return name !== openModalName;
+ });
+ },
+ closeAllModals: (state: IUiState) => {
+ Object.keys(state.modals).forEach((name: string) => {
+ if (state.modals[name].open) {
+ Vue.set(state.modals[name], 'open', false);
+ }
+ });
+ state.modalStack = [];
},
toggleSidebarMenuCollapse: (state: IUiState) => {
state.sidebarMenuCollapsed = !state.sidebarMenuCollapsed;
diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json
index bd034dc036..42ed83157f 100644
--- a/packages/editor-ui/src/plugins/i18n/locales/en.json
+++ b/packages/editor-ui/src/plugins/i18n/locales/en.json
@@ -472,7 +472,7 @@
},
"clickOnTheQuestionMarkIcon": "Click the '?' icon to open this node on n8n.io",
"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.",
+ "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.",
"displayName": "Continue On Fail"
},
"executeOnce": {
@@ -891,12 +891,6 @@
},
"workflowActivator": {
"activateWorkflow": "Activate workflow",
- "confirmMessage": {
- "cancelButtonText": "",
- "confirmButtonText": "Yes, activate and save!",
- "headline": "Activate and save?",
- "message": "When you activate the workflow all currently unsaved changes of the workflow will be saved."
- },
"deactivateWorkflow": "Deactivate workflow",
"showError": {
"message": "There was a problem and the workflow could not be {newStateName}",
@@ -920,7 +914,8 @@
"title": "Problem activating workflow"
}
},
- "theWorkflowIsSetToBeActiveBut": "The workflow is set to be active but could not be started.
Click to display error message."
+ "theWorkflowIsSetToBeActiveBut": "The workflow is set to be active but could not be started.
Click to display error message.",
+ "thisWorkflowHasNoTriggerNodes": "This workflow has no trigger nodes that require activation"
},
"workflowDetails": {
"active": "Active",
@@ -1040,5 +1035,19 @@
"timeoutAfter": "Timeout After",
"timeoutWorkflow": "Timeout Workflow",
"timezone": "Timezone"
+ },
+ "activationModal": {
+ "workflowActivated": "Workflow activated",
+ "theseExecutionsWillNotShowUp": "These executions will not show up immediately in the editor,",
+ "butYouCanSeeThem": "but you can see them in the",
+ "executionList": "execution list",
+ "ifYouChooseTo": "if you choose to",
+ "saveExecutions": "save executions.",
+ "dontShowAgain": "Don't show again",
+ "yourTriggersWillNowFire": "Your triggers will now fire production executions automatically.",
+ "yourTriggerWillNowFire": "Your trigger will now fire production executions automatically.",
+ "yourWorkflowWillNowRegularlyCheck": "Your workflow will now regularly check {serviceName} for events and trigger executions for them.",
+ "yourWorkflowWillNowListenForEvents": "Your workflow will now listen for events from {serviceName} and trigger executions.",
+ "gotIt": "Got it"
}
}
diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts
index 3944e57f0d..e659587710 100644
--- a/packages/editor-ui/src/store.ts
+++ b/packages/editor-ui/src/store.ts
@@ -761,6 +761,7 @@ export const store = new Vuex.Store({
return getters.nodeType(node.type).group.includes('trigger');
});
},
+
// Node-Index
getNodeIndex: (state) => (nodeName: string): number => {
return state.nodeIndex.indexOf(nodeName);
diff --git a/packages/nodes-base/nodes/Cron/Cron.node.ts b/packages/nodes-base/nodes/Cron/Cron.node.ts
index bf1a19e1c8..4a6cfa7d02 100644
--- a/packages/nodes-base/nodes/Cron/Cron.node.ts
+++ b/packages/nodes-base/nodes/Cron/Cron.node.ts
@@ -26,6 +26,7 @@ export class Cron implements INodeType {
version: 1,
description: 'Triggers the workflow at a specific time',
eventTriggerDescription: '',
+ activationMessage: 'Your cron trigger will now trigger executions on the schedule you have defined.',
defaults: {
name: 'Cron',
color: '#00FF00',
diff --git a/packages/nodes-base/nodes/Interval/Interval.node.ts b/packages/nodes-base/nodes/Interval/Interval.node.ts
index b41d65c5d6..c71317bdd0 100644
--- a/packages/nodes-base/nodes/Interval/Interval.node.ts
+++ b/packages/nodes-base/nodes/Interval/Interval.node.ts
@@ -16,6 +16,7 @@ export class Interval implements INodeType {
version: 1,
description: 'Triggers the workflow in a given interval',
eventTriggerDescription: '',
+ activationMessage: 'Your interval trigger will now trigger executions on the schedule you have defined.',
defaults: {
name: 'Interval',
color: '#00FF00',
diff --git a/packages/nodes-base/nodes/SseTrigger/SseTrigger.node.ts b/packages/nodes-base/nodes/SseTrigger/SseTrigger.node.ts
index 1edbad5a7f..979b480c09 100644
--- a/packages/nodes-base/nodes/SseTrigger/SseTrigger.node.ts
+++ b/packages/nodes-base/nodes/SseTrigger/SseTrigger.node.ts
@@ -16,6 +16,7 @@ export class SseTrigger implements INodeType {
version: 1,
description: 'Triggers the workflow when Server-Sent Events occur',
eventTriggerDescription: '',
+ activationMessage: 'You can now make calls to your SSE URL to trigger executions.',
defaults: {
name: 'SSE Trigger',
color: '#225577',
diff --git a/packages/nodes-base/nodes/Webhook/Webhook.node.ts b/packages/nodes-base/nodes/Webhook/Webhook.node.ts
index ad6b6cc280..5e72c37666 100644
--- a/packages/nodes-base/nodes/Webhook/Webhook.node.ts
+++ b/packages/nodes-base/nodes/Webhook/Webhook.node.ts
@@ -48,6 +48,7 @@ export class Webhook implements INodeType {
version: 1,
description: 'Starts the workflow when a webhook is called',
eventTriggerDescription: 'Waiting for you to call the Test URL',
+ activationMessage: 'You can now make calls to your production webhook URL.',
defaults: {
name: 'Webhook',
},
diff --git a/packages/nodes-base/nodes/WorkflowTrigger/WorkflowTrigger.node.ts b/packages/nodes-base/nodes/WorkflowTrigger/WorkflowTrigger.node.ts
index 243c203d83..025fdf0433 100644
--- a/packages/nodes-base/nodes/WorkflowTrigger/WorkflowTrigger.node.ts
+++ b/packages/nodes-base/nodes/WorkflowTrigger/WorkflowTrigger.node.ts
@@ -17,6 +17,7 @@ export class WorkflowTrigger implements INodeType {
version: 1,
description: 'Triggers based on various lifecycle events, like when a workflow is activated',
eventTriggerDescription: '',
+ activationMessage: 'Your workflow will now trigger executions on the event you have defined.',
defaults: {
name: 'Workflow Trigger',
color: '#ff6d5a',
diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts
index a82d734126..f695d6da79 100644
--- a/packages/workflow/src/Interfaces.ts
+++ b/packages/workflow/src/Interfaces.ts
@@ -807,6 +807,7 @@ export interface INodeTypeDescription extends INodeTypeBaseDescription {
version: number;
defaults: INodeParameters;
eventTriggerDescription?: string;
+ activationMessage?: string;
inputs: string[];
inputNames?: string[];
outputs: string[];