mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
⚡ Introduce FE external hooks (#1332)
* ⚡ Introduce FE external hooks * ⚡ update hooks * ⚡ add data from frontend settings to hooks * re-organize and update * cleanup * 👌 * ⚡ cleanup workflowSave mixin, add events * avoid alert on new workflow save as * ⚡ update workflow active events * rename externalhooks method * ⚡ Rename frontend hooks Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
parent
fd1f60bbbe
commit
4d446229c3
|
@ -301,6 +301,9 @@ export interface IN8nUISettings {
|
|||
timezone: string;
|
||||
urlBaseWebhook: string;
|
||||
versionCli: string;
|
||||
n8nMetadata?: {
|
||||
[key: string]: string | number | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IPackageVersions {
|
||||
|
|
|
@ -117,6 +117,10 @@ export interface INodeTypesMaxCount {
|
|||
};
|
||||
}
|
||||
|
||||
export interface IExternalHooks {
|
||||
run(eventName: string, metadata?: IDataObject): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IRestApi {
|
||||
getActiveWorkflows(): Promise<string[]>;
|
||||
getActivationError(id: string): Promise<IActivationError | undefined >;
|
||||
|
@ -406,6 +410,9 @@ export interface IN8nUISettings {
|
|||
};
|
||||
urlBaseWebhook: string;
|
||||
versionCli: string;
|
||||
n8nMetadata?: {
|
||||
[key: string]: string | number | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
|
||||
|
|
|
@ -119,6 +119,7 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
|
@ -147,6 +148,7 @@ import mixins from 'vue-typed-mixins';
|
|||
|
||||
export default mixins(
|
||||
copyPaste,
|
||||
externalHooks,
|
||||
nodeHelpers,
|
||||
restApi,
|
||||
showMessage,
|
||||
|
@ -336,6 +338,8 @@ export default mixins(
|
|||
|
||||
this.$emit('credentialsCreated', {data: result, options: { closeDialog }});
|
||||
|
||||
this.$externalHooks().run('credentials.create', { credentialTypeData: this.credentialTypeData });
|
||||
|
||||
return result;
|
||||
},
|
||||
async oAuthCredentialAuthorize () {
|
||||
|
|
|
@ -181,7 +181,6 @@ import { restApi } from '@/components/mixins/restApi';
|
|||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { titleChange } from '@/components/mixins/titleChange';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
import { workflowSave } from '@/components/mixins/workflowSave';
|
||||
import { workflowRun } from '@/components/mixins/workflowRun';
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
|
@ -195,7 +194,6 @@ export default mixins(
|
|||
titleChange,
|
||||
workflowHelpers,
|
||||
workflowRun,
|
||||
workflowSave,
|
||||
)
|
||||
.extend({
|
||||
name: 'MainHeader',
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
|
@ -33,6 +34,7 @@ import {
|
|||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default mixins(
|
||||
externalHooks,
|
||||
genericHelpers,
|
||||
restApi,
|
||||
showMessage,
|
||||
|
@ -121,10 +123,12 @@ export default mixins(
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -133,6 +137,8 @@ export default mixins(
|
|||
this.$store.commit('setWorkflowInactive', this.workflowId);
|
||||
}
|
||||
|
||||
this.$externalHooks().run(activationEventName, { workflowId: this.workflowId, active: newActiveState });
|
||||
|
||||
this.$emit('workflowActiveChanged', { id: this.workflowId, active: newActiveState });
|
||||
this.loading = false;
|
||||
},
|
||||
|
|
39
packages/editor-ui/src/components/mixins/externalHooks.ts
Normal file
39
packages/editor-ui/src/components/mixins/externalHooks.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { IExternalHooks } from '@/Interface';
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
import Vue from 'vue';
|
||||
import { Store } from 'vuex';
|
||||
|
||||
export async function runExternalHook(
|
||||
eventName: string,
|
||||
store: Store<IDataObject>,
|
||||
metadata?: IDataObject,
|
||||
) {
|
||||
// @ts-ignore
|
||||
if (!window.n8nExternalHooks) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [resource, operator] = eventName.split('.');
|
||||
|
||||
// @ts-ignore
|
||||
if (window.n8nExternalHooks[resource] && window.n8nExternalHooks[resource][operator]) {
|
||||
// @ts-ignore
|
||||
const hookMethods = window.n8nExternalHooks[resource][operator];
|
||||
|
||||
for (const hookmethod of hookMethods) {
|
||||
await hookmethod(store, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const externalHooks = Vue.extend({
|
||||
methods: {
|
||||
$externalHooks(): IExternalHooks {
|
||||
return {
|
||||
run: async (eventName: string, metadata?: IDataObject): Promise<void> => {
|
||||
await runExternalHook.call(this, eventName, this.$store, metadata);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
|
@ -27,6 +27,7 @@ import {
|
|||
XYPositon,
|
||||
} from '../../Interface';
|
||||
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
|
@ -36,6 +37,7 @@ import { isEqual } from 'lodash';
|
|||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export const workflowHelpers = mixins(
|
||||
externalHooks,
|
||||
nodeHelpers,
|
||||
restApi,
|
||||
showMessage,
|
||||
|
@ -422,6 +424,7 @@ export const workflowHelpers = mixins(
|
|||
this.$store.commit('setWorkflowId', workflowData.id);
|
||||
this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false});
|
||||
this.$store.commit('setWorkflowSettings', workflowData.settings || {});
|
||||
this.$store.commit('setStateDirty', false);
|
||||
} else {
|
||||
// Workflow exists already so update it
|
||||
await this.restApi().updateWorkflow(currentWorkflow, workflowData);
|
||||
|
@ -441,6 +444,7 @@ export const workflowHelpers = mixins(
|
|||
message: `The workflow "${workflowData.name}" got saved!`,
|
||||
type: 'success',
|
||||
});
|
||||
this.$externalHooks().run('workflow.afterUpdate', { workflowData });
|
||||
} catch (e) {
|
||||
this.$store.commit('removeActiveAction', 'workflowSaving');
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
NodeHelpers,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
|
||||
|
@ -17,6 +18,7 @@ import mixins from 'vue-typed-mixins';
|
|||
import { titleChange } from './titleChange';
|
||||
|
||||
export const workflowRun = mixins(
|
||||
externalHooks,
|
||||
restApi,
|
||||
workflowHelpers,
|
||||
titleChange,
|
||||
|
@ -82,6 +84,7 @@ export const workflowRun = mixins(
|
|||
duration: 0,
|
||||
});
|
||||
this.$titleSet(workflow.name as string, 'ERROR');
|
||||
this.$externalHooks().run('workflow.runError', { errorMessages });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
import {
|
||||
IWorkflowData,
|
||||
} from '../../Interface';
|
||||
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export const workflowSave = mixins(
|
||||
nodeHelpers,
|
||||
restApi,
|
||||
showMessage,
|
||||
workflowHelpers,
|
||||
)
|
||||
.extend({
|
||||
methods: {
|
||||
// Saves the currently loaded workflow to the database.
|
||||
async saveCurrentWorkflow (withNewName = false) {
|
||||
const currentWorkflow = this.$route.params.name;
|
||||
let workflowName: string | null | undefined = '';
|
||||
if (currentWorkflow === undefined || withNewName === true) {
|
||||
// Currently no workflow name is set to get it from user
|
||||
workflowName = await this.$prompt(
|
||||
'Enter workflow name',
|
||||
'Name',
|
||||
{
|
||||
confirmButtonText: 'Save',
|
||||
cancelButtonText: 'Cancel',
|
||||
},
|
||||
)
|
||||
.then((data) => {
|
||||
// @ts-ignore
|
||||
return data.value;
|
||||
})
|
||||
.catch(() => {
|
||||
// User did cancel
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (workflowName === undefined) {
|
||||
// User did cancel
|
||||
return;
|
||||
} else if (['', null].includes(workflowName)) {
|
||||
// User did not enter a name
|
||||
this.$showMessage({
|
||||
title: 'Name missing',
|
||||
message: `No name for the workflow got entered and could so not be saved!`,
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.$store.commit('addActiveAction', 'workflowSaving');
|
||||
|
||||
let workflowData: IWorkflowData = await this.getWorkflowDataToSave();
|
||||
|
||||
if (currentWorkflow === undefined || withNewName === true) {
|
||||
// Workflow is new or is supposed to get saved under a new name
|
||||
// so create a new entry in database
|
||||
workflowData.name = workflowName!.trim() as string;
|
||||
|
||||
if (withNewName === true) {
|
||||
// If an existing workflow gets resaved with a new name
|
||||
// make sure that the new ones is not active
|
||||
workflowData.active = false;
|
||||
}
|
||||
|
||||
workflowData = await this.restApi().createNewWorkflow(workflowData);
|
||||
|
||||
this.$store.commit('setActive', workflowData.active || false);
|
||||
this.$store.commit('setWorkflowId', workflowData.id);
|
||||
this.$store.commit('setWorkflowName', {newName: workflowData.name, setStateDirty: false});
|
||||
this.$store.commit('setWorkflowSettings', workflowData.settings || {});
|
||||
} else {
|
||||
// Workflow exists already so update it
|
||||
await this.restApi().updateWorkflow(currentWorkflow, workflowData);
|
||||
}
|
||||
// Set dirty = false before pushing route so unsaved changes message doesnt trigger.
|
||||
this.$store.commit('setStateDirty', false);
|
||||
|
||||
if (this.$route.params.name !== workflowData.id) {
|
||||
this.$router.push({
|
||||
name: 'NodeViewExisting',
|
||||
params: { name: workflowData.id as string, action: 'workflowSave' },
|
||||
});
|
||||
}
|
||||
|
||||
this.$store.commit('removeActiveAction', 'workflowSaving');
|
||||
this.$showMessage({
|
||||
title: 'Workflow saved',
|
||||
message: `The workflow "${workflowData.name}" got saved!`,
|
||||
type: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
this.$store.commit('removeActiveAction', 'workflowSaving');
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Problem saving workflow',
|
||||
message: `There was a problem saving the workflow: "${e.message}"`,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -17,6 +17,8 @@ import './n8n-theme.scss';
|
|||
import App from '@/App.vue';
|
||||
import router from './router';
|
||||
|
||||
import { runExternalHook } from './components/mixins/externalHooks';
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import {
|
||||
faAngleDoubleLeft,
|
||||
|
@ -172,6 +174,9 @@ library.add(faUsers);
|
|||
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
router.afterEach((to, from) => {
|
||||
runExternalHook('main.routeChange', store, { from, to });
|
||||
});
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
|
|
|
@ -58,6 +58,7 @@ export const store = new Vuex.Store({
|
|||
maxExecutionTimeout: Number.MAX_SAFE_INTEGER,
|
||||
versionCli: '0.0.0',
|
||||
oauthCallbackUrls: {},
|
||||
n8nMetadata: {},
|
||||
workflowExecutionData: null as IExecutionResponse | null,
|
||||
lastSelectedNode: null as string | null,
|
||||
lastSelectedNodeOutputIndex: null as number | null,
|
||||
|
@ -530,7 +531,9 @@ export const store = new Vuex.Store({
|
|||
setOauthCallbackUrls(state, urls: IDataObject) {
|
||||
Vue.set(state, 'oauthCallbackUrls', urls);
|
||||
},
|
||||
|
||||
setN8nMetadata(state, metadata: IDataObject) {
|
||||
Vue.set(state, 'n8nMetadata', metadata);
|
||||
},
|
||||
setActiveNode (state, nodeName: string) {
|
||||
state.activeNode = nodeName;
|
||||
},
|
||||
|
@ -653,6 +656,9 @@ export const store = new Vuex.Store({
|
|||
oauthCallbackUrls: (state): object => {
|
||||
return state.oauthCallbackUrls;
|
||||
},
|
||||
n8nMetadata: (state): object => {
|
||||
return state.n8nMetadata;
|
||||
},
|
||||
|
||||
// Push Connection
|
||||
pushConnectionActive: (state): boolean => {
|
||||
|
|
|
@ -114,6 +114,7 @@ import { MessageBoxInputData } from 'element-ui/types/message-box';
|
|||
import { jsPlumb, Endpoint, OnConnectionBindInfo } from 'jsplumb';
|
||||
import { NODE_NAME_PREFIX, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
|
||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { mouseSelect } from '@/components/mixins/mouseSelect';
|
||||
import { moveNodeWorkflow } from '@/components/mixins/moveNodeWorkflow';
|
||||
|
@ -164,6 +165,7 @@ import {
|
|||
|
||||
export default mixins(
|
||||
copyPaste,
|
||||
externalHooks,
|
||||
genericHelpers,
|
||||
mouseSelect,
|
||||
moveNodeWorkflow,
|
||||
|
@ -375,6 +377,8 @@ export default mixins(
|
|||
|
||||
this.$store.commit('setStateDirty', false);
|
||||
|
||||
this.$externalHooks().run('workflow.open', { workflowId, workflowName: data.name });
|
||||
|
||||
return data;
|
||||
},
|
||||
touchTap (e: MouseEvent | TouchEvent) {
|
||||
|
@ -1969,6 +1973,7 @@ export default mixins(
|
|||
this.$store.commit('setMaxExecutionTimeout', settings.maxExecutionTimeout);
|
||||
this.$store.commit('setVersionCli', settings.versionCli);
|
||||
this.$store.commit('setOauthCallbackUrls', settings.oauthCallbackUrls);
|
||||
this.$store.commit('setN8nMetadata', settings.n8nMetadata || {});
|
||||
},
|
||||
async loadNodeTypes (): Promise<void> {
|
||||
const nodeTypes = await this.restApi().getNodeTypes();
|
||||
|
@ -2033,6 +2038,8 @@ export default mixins(
|
|||
}
|
||||
this.stopLoading();
|
||||
});
|
||||
|
||||
this.$externalHooks().run('nodeView.mount');
|
||||
},
|
||||
|
||||
destroyed () {
|
||||
|
|
Loading…
Reference in a new issue