mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat: Rewrite Front End cloud and posthog hooks using TypeScript (no-changelog) (#5491)
This commit is contained in:
parent
3dfabc37d8
commit
a262c450f7
|
@ -16,7 +16,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"build": "cross-env VUE_APP_PUBLIC_PATH=\"/{{BASE_PATH}}/\" NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
"build": "cross-env VUE_APP_PUBLIC_PATH=\"/{{BASE_PATH}}/\" NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
||||||
"typecheck": "vue-tsc --emitDeclarationOnly",
|
"typecheck": "vue-tsc",
|
||||||
"dev": "pnpm serve",
|
"dev": "pnpm serve",
|
||||||
"lint": "eslint src --ext .js,.ts,.vue --quiet",
|
"lint": "eslint src --ext .js,.ts,.vue --quiet",
|
||||||
"lintfix": "eslint src --ext .js,.ts,.vue --fix",
|
"lintfix": "eslint src --ext .js,.ts,.vue --fix",
|
||||||
|
|
|
@ -35,6 +35,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
|
import { extendExternalHooks } from '@/mixins/externalHooks';
|
||||||
|
import { newVersions } from '@/mixins/newVersions';
|
||||||
|
|
||||||
import BannerStack from '@/components/banners/BannerStack.vue';
|
import BannerStack from '@/components/banners/BannerStack.vue';
|
||||||
import Modals from '@/components/Modals.vue';
|
import Modals from '@/components/Modals.vue';
|
||||||
|
@ -57,8 +59,10 @@ import {
|
||||||
useUsageStore,
|
useUsageStore,
|
||||||
} from '@/stores';
|
} from '@/stores';
|
||||||
import { useHistoryHelper } from '@/composables/useHistoryHelper';
|
import { useHistoryHelper } from '@/composables/useHistoryHelper';
|
||||||
import { newVersions } from '@/mixins/newVersions';
|
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
import type { ExternalHooks } from '@/types';
|
||||||
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
import { runExternalHook } from '@/utils';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'App',
|
name: 'App',
|
||||||
|
@ -131,6 +135,21 @@ export default defineComponent({
|
||||||
await this.nodeTypesStore.getNodeTranslationHeaders();
|
await this.nodeTypesStore.getNodeTranslationHeaders();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async initializeHooks(): Promise<void> {
|
||||||
|
const hooksImports = [];
|
||||||
|
|
||||||
|
if (this.settingsStore.isCloudDeployment) {
|
||||||
|
hooksImports.push(
|
||||||
|
import('./hooks/cloud').then(
|
||||||
|
({ n8nCloudHooks }: { n8nCloudHooks: PartialDeep<ExternalHooks> }) => {
|
||||||
|
extendExternalHooks(n8nCloudHooks);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled(hooksImports);
|
||||||
|
},
|
||||||
async onAfterAuthenticate() {
|
async onAfterAuthenticate() {
|
||||||
if (this.onAfterAuthenticateInitialized) {
|
if (this.onAfterAuthenticateInitialized) {
|
||||||
return;
|
return;
|
||||||
|
@ -153,10 +172,12 @@ export default defineComponent({
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.logHiringBanner();
|
this.logHiringBanner();
|
||||||
|
|
||||||
|
await this.initializeHooks();
|
||||||
|
|
||||||
void this.checkForNewVersions();
|
void this.checkForNewVersions();
|
||||||
void this.onAfterAuthenticate();
|
void this.onAfterAuthenticate();
|
||||||
|
|
||||||
void this.externalHooks.run('app.mount');
|
void runExternalHook('app.mount');
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
@ -7,7 +7,6 @@ import type {
|
||||||
REGULAR_NODE_CREATOR_VIEW,
|
REGULAR_NODE_CREATOR_VIEW,
|
||||||
AI_OTHERS_NODE_CREATOR_VIEW,
|
AI_OTHERS_NODE_CREATOR_VIEW,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
import type { IMenuItem } from 'n8n-design-system';
|
import type { IMenuItem } from 'n8n-design-system';
|
||||||
import type {
|
import type {
|
||||||
GenericValue,
|
GenericValue,
|
||||||
|
@ -50,6 +49,7 @@ import type {
|
||||||
import type { BulkCommand, Undoable } from '@/models/history';
|
import type { BulkCommand, Undoable } from '@/models/history';
|
||||||
import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers';
|
import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers';
|
||||||
import type { Component } from 'vue';
|
import type { Component } from 'vue';
|
||||||
|
import type { runExternalHook } from '@/utils';
|
||||||
|
|
||||||
export * from 'n8n-design-system/types';
|
export * from 'n8n-design-system/types';
|
||||||
|
|
||||||
|
@ -92,7 +92,9 @@ declare global {
|
||||||
debug?(): void;
|
debug?(): void;
|
||||||
};
|
};
|
||||||
analytics?: {
|
analytics?: {
|
||||||
track(event: string, proeprties?: ITelemetryTrackProperties): void;
|
identify(userId: string): void;
|
||||||
|
track(event: string, properties?: ITelemetryTrackProperties): void;
|
||||||
|
page(category: string, name: string, properties?: ITelemetryTrackProperties): void;
|
||||||
};
|
};
|
||||||
featureFlags?: {
|
featureFlags?: {
|
||||||
getAll: () => FeatureFlags;
|
getAll: () => FeatureFlags;
|
||||||
|
@ -158,7 +160,7 @@ export interface INodeTypesMaxCount {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExternalHooks {
|
export interface IExternalHooks {
|
||||||
run(eventName: string, metadata?: IDataObject): Promise<void>;
|
run: typeof runExternalHook;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INodeTranslationHeaders {
|
export interface INodeTranslationHeaders {
|
||||||
|
|
|
@ -219,10 +219,8 @@ export default defineComponent({
|
||||||
for (const item of injectedItems) {
|
for (const item of injectedItems) {
|
||||||
items.push({
|
items.push({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
// @ts-ignore
|
icon: item.icon || '',
|
||||||
icon: item.properties ? item.properties.icon : '',
|
label: item.label || '',
|
||||||
// @ts-ignore
|
|
||||||
label: item.properties ? item.properties.title : '',
|
|
||||||
position: item.position,
|
position: item.position,
|
||||||
type: item.properties?.href ? 'link' : 'regular',
|
type: item.properties?.href ? 'link' : 'regular',
|
||||||
properties: item.properties,
|
properties: item.properties,
|
||||||
|
@ -353,7 +351,9 @@ export default defineComponent({
|
||||||
mounted() {
|
mounted() {
|
||||||
this.basePath = this.rootStore.baseUrl;
|
this.basePath = this.rootStore.baseUrl;
|
||||||
if (this.$refs.user) {
|
if (this.$refs.user) {
|
||||||
void this.$externalHooks().run('mainSidebar.mounted', { userRef: this.$refs.user });
|
void this.$externalHooks().run('mainSidebar.mounted', {
|
||||||
|
userRef: this.$refs.user as Element,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void this.$nextTick(() => {
|
void this.$nextTick(() => {
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { useWebhooksStore } from '@/stores/webhooks.store';
|
|
||||||
import { runExternalHook } from '@/utils';
|
import { runExternalHook } from '@/utils';
|
||||||
|
|
||||||
import { useActions } from '../composables/useActions';
|
import { useActions } from '../composables/useActions';
|
||||||
|
@ -171,7 +170,7 @@ function trackActionsView() {
|
||||||
trigger_action_count,
|
trigger_action_count,
|
||||||
};
|
};
|
||||||
|
|
||||||
void runExternalHook('nodeCreateList.onViewActions', useWebhooksStore(), trackingPayload);
|
void runExternalHook('nodeCreateList.onViewActions', trackingPayload);
|
||||||
telemetry?.trackNodesPanel('nodeCreateList.onViewActions', trackingPayload);
|
telemetry?.trackNodesPanel('nodeCreateList.onViewActions', trackingPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,7 +191,7 @@ function addHttpNode() {
|
||||||
if (telemetry) setAddedNodeActionParameters(updateData);
|
if (telemetry) setAddedNodeActionParameters(updateData);
|
||||||
|
|
||||||
const app_identifier = actions.value[0].key;
|
const app_identifier = actions.value[0].key;
|
||||||
void runExternalHook('nodeCreateList.onActionsCustmAPIClicked', useWebhooksStore(), {
|
void runExternalHook('nodeCreateList.onActionsCustmAPIClicked', {
|
||||||
app_identifier,
|
app_identifier,
|
||||||
});
|
});
|
||||||
telemetry?.trackNodesPanel('nodeCreateList.onActionsCustmAPIClicked', { app_identifier });
|
telemetry?.trackNodesPanel('nodeCreateList.onActionsCustmAPIClicked', { app_identifier });
|
||||||
|
|
|
@ -25,8 +25,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, reactive, toRefs, onBeforeUnmount } from 'vue';
|
import { onMounted, reactive, toRefs, onBeforeUnmount } from 'vue';
|
||||||
import { useWebhooksStore } from '@/stores/webhooks.store';
|
import { useExternalHooks } from '@/composables';
|
||||||
import { runExternalHook } from '@/utils';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
|
@ -46,6 +45,8 @@ const state = reactive({
|
||||||
inputRef: null as HTMLInputElement | null,
|
inputRef: null as HTMLInputElement | null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const externalHooks = useExternalHooks();
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
state.inputRef?.focus();
|
state.inputRef?.focus();
|
||||||
}
|
}
|
||||||
|
@ -60,9 +61,7 @@ function clear() {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void runExternalHook('nodeCreator_searchBar.mount', useWebhooksStore(), {
|
void externalHooks.run('nodeCreatorSearchBar.mount', { inputRef: state.inputRef });
|
||||||
inputRef: state.inputRef,
|
|
||||||
});
|
|
||||||
setTimeout(focus, 0);
|
setTimeout(focus, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,6 @@ import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { runExternalHook } from '@/utils';
|
import { runExternalHook } from '@/utils';
|
||||||
import { useWebhooksStore } from '@/stores/webhooks.store';
|
|
||||||
|
|
||||||
import { sortNodeCreateElements, transformNodeType } from '../utils';
|
import { sortNodeCreateElements, transformNodeType } from '../utils';
|
||||||
|
|
||||||
|
@ -256,7 +255,7 @@ export const useActions = () => {
|
||||||
source_mode: rootView.toLowerCase(),
|
source_mode: rootView.toLowerCase(),
|
||||||
resource: (action.value as INodeParameters).resource || '',
|
resource: (action.value as INodeParameters).resource || '',
|
||||||
};
|
};
|
||||||
void runExternalHook('nodeCreateList.addAction', useWebhooksStore(), payload);
|
void runExternalHook('nodeCreateList.addAction', payload);
|
||||||
telemetry?.trackNodesPanel('nodeCreateList.addAction', payload);
|
telemetry?.trackNodesPanel('nodeCreateList.addAction', payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1254,7 +1254,7 @@ export default defineComponent({
|
||||||
|
|
||||||
void this.$externalHooks().run('parameterInput.mount', {
|
void this.$externalHooks().run('parameterInput.mount', {
|
||||||
parameter: this.parameter,
|
parameter: this.parameter,
|
||||||
inputFieldRef: this.$refs.inputField,
|
inputFieldRef: this.$refs.inputField as InstanceType<typeof N8nInput>,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
|
|
|
@ -642,7 +642,10 @@ export default defineComponent({
|
||||||
personalization_survey_n8n_version: this.rootStore.versionCli,
|
personalization_survey_n8n_version: this.rootStore.versionCli,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.$externalHooks().run('personalizationModal.onSubmit', survey);
|
await this.$externalHooks().run(
|
||||||
|
'personalizationModal.onSubmit',
|
||||||
|
survey as IPersonalizationLatestVersion,
|
||||||
|
);
|
||||||
|
|
||||||
await this.usersStore.submitPersonalizationSurvey(survey as IPersonalizationLatestVersion);
|
await this.usersStore.submitPersonalizationSurvey(survey as IPersonalizationLatestVersion);
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import type { INodeUi } from '@/Interface';
|
||||||
import RunDataSchemaItem from '@/components/RunDataSchemaItem.vue';
|
import RunDataSchemaItem from '@/components/RunDataSchemaItem.vue';
|
||||||
import Draggable from '@/components/Draggable.vue';
|
import Draggable from '@/components/Draggable.vue';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { useWebhooksStore } from '@/stores/webhooks.store';
|
|
||||||
import { telemetry } from '@/plugins/telemetry';
|
import { telemetry } from '@/plugins/telemetry';
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
import type { IDataObject } from 'n8n-workflow';
|
||||||
import { isEmpty, runExternalHook } from '@/utils';
|
import { isEmpty, runExternalHook } from '@/utils';
|
||||||
|
@ -27,7 +26,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const draggingPath = ref<string>('');
|
const draggingPath = ref<string>('');
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
const webhooksStore = useWebhooksStore();
|
|
||||||
const { getSchemaForExecutionData } = useDataSchema();
|
const { getSchemaForExecutionData } = useDataSchema();
|
||||||
|
|
||||||
const schema = computed(() => getSchemaForExecutionData(props.data));
|
const schema = computed(() => getSchemaForExecutionData(props.data));
|
||||||
|
@ -59,7 +57,7 @@ const onDragEnd = (el: HTMLElement) => {
|
||||||
...mappingTelemetry,
|
...mappingTelemetry,
|
||||||
};
|
};
|
||||||
|
|
||||||
void runExternalHook('runDataJson.onDragEnd', webhooksStore, telemetryPayload);
|
void runExternalHook('runDataJson.onDragEnd', telemetryPayload);
|
||||||
|
|
||||||
telemetry.track('User dragged data for mapping', telemetryPayload);
|
telemetry.track('User dragged data for mapping', telemetryPayload);
|
||||||
}, 1000); // ensure dest data gets set if drop
|
}, 1000); // ensure dest data gets set if drop
|
||||||
|
|
|
@ -4,7 +4,8 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
import type { EnvironmentVariable, Rule, RuleGroup } from '@/Interface';
|
import type { EnvironmentVariable, Rule, RuleGroup } from '@/Interface';
|
||||||
import { useI18n, useToast, useCopyToClipboard } from '@/composables';
|
import { useI18n, useToast, useCopyToClipboard } from '@/composables';
|
||||||
import { EnterpriseEditionFeature } from '@/constants';
|
import { EnterpriseEditionFeature } from '@/constants';
|
||||||
import { useSettingsStore, useUsersStore } from '@/stores';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { getVariablesPermissions } from '@/permissions';
|
import { getVariablesPermissions } from '@/permissions';
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
|
@ -6,14 +6,13 @@ import { STORES } from '@/constants';
|
||||||
|
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import BannerStack from '@/components/banners/BannerStack.vue';
|
import BannerStack from '@/components/banners/BannerStack.vue';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
|
||||||
import type { RenderOptions } from '@/__tests__/render';
|
import type { RenderOptions } from '@/__tests__/render';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { waitFor } from '@testing-library/vue';
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
|
||||||
let uiStore: ReturnType<typeof useUIStore>;
|
let uiStore: ReturnType<typeof useUIStore>;
|
||||||
let usersStore: ReturnType<typeof useUsersStore>;
|
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
[STORES.SETTINGS]: {
|
[STORES.SETTINGS]: {
|
||||||
|
@ -55,7 +54,6 @@ const renderComponent = createComponentRenderer(BannerStack, defaultRenderOption
|
||||||
describe('BannerStack', () => {
|
describe('BannerStack', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
uiStore = useUIStore();
|
uiStore = useUIStore();
|
||||||
usersStore = useUsersStore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -75,7 +73,7 @@ describe('BannerStack', () => {
|
||||||
it('should dismiss banner on click', async () => {
|
it('should dismiss banner on click', async () => {
|
||||||
const { getByTestId } = renderComponent();
|
const { getByTestId } = renderComponent();
|
||||||
const dismissBannerSpy = vi
|
const dismissBannerSpy = vi
|
||||||
.spyOn(useUIStore(), 'dismissBanner')
|
.spyOn(uiStore, 'dismissBanner')
|
||||||
.mockImplementation(async (banner, mode) => {});
|
.mockImplementation(async (banner, mode) => {});
|
||||||
expect(getByTestId('banners-V1')).toBeInTheDocument();
|
expect(getByTestId('banners-V1')).toBeInTheDocument();
|
||||||
const closeTrialBannerButton = getByTestId('banner-V1-close');
|
const closeTrialBannerButton = getByTestId('banner-V1-close');
|
||||||
|
@ -87,7 +85,7 @@ describe('BannerStack', () => {
|
||||||
it('should permanently dismiss banner on click', async () => {
|
it('should permanently dismiss banner on click', async () => {
|
||||||
const { getByTestId } = renderComponent();
|
const { getByTestId } = renderComponent();
|
||||||
const dismissBannerSpy = vi
|
const dismissBannerSpy = vi
|
||||||
.spyOn(useUIStore(), 'dismissBanner')
|
.spyOn(uiStore, 'dismissBanner')
|
||||||
.mockImplementation(async (banner, mode) => {});
|
.mockImplementation(async (banner, mode) => {});
|
||||||
|
|
||||||
const permanentlyDismissBannerLink = getByTestId('banner-confirm-v1');
|
const permanentlyDismissBannerLink = getByTestId('banner-confirm-v1');
|
||||||
|
|
|
@ -3,7 +3,8 @@ import type { EnvironmentVariable } from '@/Interface';
|
||||||
import { fireEvent } from '@testing-library/vue';
|
import { fireEvent } from '@testing-library/vue';
|
||||||
import { setupServer } from '@/__tests__/server';
|
import { setupServer } from '@/__tests__/server';
|
||||||
import { afterAll, beforeAll } from 'vitest';
|
import { afterAll, beforeAll } from 'vitest';
|
||||||
import { useSettingsStore, useUsersStore } from '@/stores';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { STORES } from '@/constants';
|
import { STORES } from '@/constants';
|
||||||
|
|
|
@ -3,7 +3,7 @@ import BaseBanner from '@/components/banners/BaseBanner.vue';
|
||||||
import { i18n as locale } from '@/plugins/i18n';
|
import { i18n as locale } from '@/plugins/i18n';
|
||||||
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useUIStore } from '@/stores';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
|
||||||
const trialDaysLeft = computed(() => {
|
const trialDaysLeft = computed(() => {
|
||||||
const { trialDaysLeft } = useCloudPlanStore();
|
const { trialDaysLeft } = useCloudPlanStore();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import BaseBanner from '@/components/banners/BaseBanner.vue';
|
import BaseBanner from '@/components/banners/BaseBanner.vue';
|
||||||
import { i18n as locale } from '@/plugins/i18n';
|
import { i18n as locale } from '@/plugins/i18n';
|
||||||
import { useUIStore } from '@/stores';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
|
||||||
function onUpdatePlanClick() {
|
function onUpdatePlanClick() {
|
||||||
void useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect');
|
void useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect');
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import BaseBanner from '@/components/banners/BaseBanner.vue';
|
import BaseBanner from '@/components/banners/BaseBanner.vue';
|
||||||
import { i18n as locale } from '@/plugins/i18n';
|
import { i18n as locale } from '@/plugins/i18n';
|
||||||
import { useUsersStore } from '@/stores';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import type { IExternalHooks } from '@/Interface';
|
import type { IExternalHooks } from '@/Interface';
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
|
||||||
import { useWebhooksStore } from '@/stores/webhooks.store';
|
|
||||||
import { runExternalHook } from '@/utils';
|
import { runExternalHook } from '@/utils';
|
||||||
|
|
||||||
export function useExternalHooks(): IExternalHooks {
|
export function useExternalHooks(): IExternalHooks {
|
||||||
return {
|
return {
|
||||||
async run(eventName: string, metadata?: IDataObject): Promise<void> {
|
run: runExternalHook,
|
||||||
return runExternalHook(eventName, useWebhooksStore(), metadata);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
513
packages/editor-ui/src/hooks/cloud.ts
Normal file
513
packages/editor-ui/src/hooks/cloud.ts
Normal file
|
@ -0,0 +1,513 @@
|
||||||
|
import { hooksAddAdminIcon, hooksAddFakeDoorFeatures } from '@/hooks/utils';
|
||||||
|
import {
|
||||||
|
getAuthenticationModalEventData,
|
||||||
|
getExpressionEditorEventsData,
|
||||||
|
getInsertedItemFromExpEditorEventData,
|
||||||
|
getNodeTypeChangedEventData,
|
||||||
|
getOpenWorkflowSettingsEventData,
|
||||||
|
getOutputModeChangedEventData,
|
||||||
|
getUpdatedWorkflowSettingsEventData,
|
||||||
|
getUserSavedCredentialsEventData,
|
||||||
|
getExecutionFinishedEventData,
|
||||||
|
getNodeRemovedEventData,
|
||||||
|
getNodeEditingFinishedEventData,
|
||||||
|
getExecutionStartedEventData,
|
||||||
|
} from '@/hooks/segment';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import {
|
||||||
|
hooksGenerateNodesPanelEvent,
|
||||||
|
hooksResetNodesPanelSession,
|
||||||
|
nodesPanelSession,
|
||||||
|
} from '@/hooks/utils/hooksNodesPanel';
|
||||||
|
import { useSegment } from '@/stores/segment.store';
|
||||||
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
import type { IDataObject } from 'n8n-workflow';
|
||||||
|
import type { INodeUi } from '@/Interface';
|
||||||
|
import type { ExternalHooks } from '@/types';
|
||||||
|
|
||||||
|
export const n8nCloudHooks: PartialDeep<ExternalHooks> = {
|
||||||
|
app: {
|
||||||
|
mount: [
|
||||||
|
() => {
|
||||||
|
hooksAddAdminIcon();
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
hooksAddFakeDoorFeatures();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
nodeView: {
|
||||||
|
mount: [
|
||||||
|
() => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
segmentStore.identify();
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
hooksAddAdminIcon();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
createNodeActiveChanged: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = {
|
||||||
|
source: meta.source,
|
||||||
|
nodes_panel_session_id: nodesPanelSession.sessionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
hooksResetNodesPanelSession();
|
||||||
|
segmentStore.track('User opened nodes panel', eventData);
|
||||||
|
segmentStore.page('Cloud instance', 'Nodes panel', eventData);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
addNodeButton: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = {
|
||||||
|
eventName: 'User added node to workflow canvas',
|
||||||
|
properties: {
|
||||||
|
node_type: meta.nodeTypeName.split('.')[1],
|
||||||
|
nodes_panel_session_id: nodesPanelSession.sessionId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
routeChange: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const splitPath = meta.to.path.split('/');
|
||||||
|
if (meta.from.path !== '/' && splitPath[1] === 'workflow') {
|
||||||
|
const eventData = {
|
||||||
|
workflow_id: splitPath[2],
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.page('Cloud instance', 'Workflow editor', eventData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
credential: {
|
||||||
|
saved: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = getUserSavedCredentialsEventData(meta);
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
credentialsEdit: {
|
||||||
|
credentialTypeChanged: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
if (meta.newValue) {
|
||||||
|
const eventData = {
|
||||||
|
eventName: 'User opened Credentials modal',
|
||||||
|
properties: {
|
||||||
|
source: meta.setCredentialType === meta.credentialType ? 'node' : 'primary_menu',
|
||||||
|
new_credential: !meta.editCredentials,
|
||||||
|
credential_type: meta.credentialType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
segmentStore.page('Cloud instance', 'Credentials modal', eventData.properties);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
credentialModalOpened: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = {
|
||||||
|
eventName: 'User opened Credentials modal',
|
||||||
|
properties: {
|
||||||
|
source: meta.activeNode ? 'node' : 'primary_menu',
|
||||||
|
new_credential: !meta.isEditingCredential,
|
||||||
|
credential_type: meta.credentialType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
segmentStore.page('Cloud instance', 'Credentials modal', eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
credentialsList: {
|
||||||
|
mounted: [
|
||||||
|
() => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = {
|
||||||
|
eventName: 'User opened global Credentials panel',
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName);
|
||||||
|
segmentStore.page('Cloud instance', 'Credentials panel');
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dialogVisibleChanged: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
if (meta.dialogVisible) {
|
||||||
|
const eventData = {
|
||||||
|
eventName: 'User opened global Credentials panel',
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName);
|
||||||
|
segmentStore.page('Cloud instance', 'Credentials panel');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
workflowSettings: {
|
||||||
|
dialogVisibleChanged: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
if (meta.dialogVisible) {
|
||||||
|
const eventData = getOpenWorkflowSettingsEventData();
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
saveSettings: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = getUpdatedWorkflowSettingsEventData(meta);
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
dataDisplay: {
|
||||||
|
onDocumentationUrlClick: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = {
|
||||||
|
eventName: 'User clicked node modal docs link',
|
||||||
|
properties: {
|
||||||
|
node_type: meta.nodeType.name.split('.')[1],
|
||||||
|
docs_link: meta.documentationUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nodeTypeChanged: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
const eventData = getNodeTypeChangedEventData(meta);
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
segmentStore.page('Cloud instance', 'Node modal', {
|
||||||
|
node: ndvStore.activeNode?.name,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nodeEditingFinished: [
|
||||||
|
() => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
const eventData = getNodeEditingFinishedEventData(ndvStore.activeNode);
|
||||||
|
if (eventData) {
|
||||||
|
eventData.properties!.workflow_id = workflowsStore.workflowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData) {
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
executionsList: {
|
||||||
|
openDialog: [
|
||||||
|
() => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = {
|
||||||
|
eventName: 'User opened Executions log',
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName);
|
||||||
|
segmentStore.page('Cloud instance', 'Executions log');
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
showMessage: {
|
||||||
|
showError: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = {
|
||||||
|
eventName: 'Instance FE emitted error',
|
||||||
|
properties: {
|
||||||
|
error_title: meta.title,
|
||||||
|
error_description: meta.message,
|
||||||
|
error_message: meta.errorMessage,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
expressionEdit: {
|
||||||
|
itemSelected: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = getInsertedItemFromExpEditorEventData(meta);
|
||||||
|
|
||||||
|
if (meta.selectedItem.variable.startsWith('Object.keys')) {
|
||||||
|
eventData.properties!.variable_type = 'Keys';
|
||||||
|
} else if (meta.selectedItem.variable.startsWith('Object.values')) {
|
||||||
|
eventData.properties!.variable_type = 'Values';
|
||||||
|
} else {
|
||||||
|
eventData.properties!.variable_type = 'Raw value';
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dialogVisibleChanged: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const currentValue = meta.value.slice(1);
|
||||||
|
let isValueDefault = false;
|
||||||
|
|
||||||
|
switch (typeof meta.parameter.default) {
|
||||||
|
case 'boolean':
|
||||||
|
isValueDefault =
|
||||||
|
(currentValue === 'true' && meta.parameter.default) ||
|
||||||
|
(currentValue === 'false' && !meta.parameter.default);
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
isValueDefault = currentValue === meta.parameter.default;
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
isValueDefault = currentValue === meta.parameter.default.toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventData = getExpressionEditorEventsData(meta, isValueDefault);
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
nodeSettings: {
|
||||||
|
valueChanged: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
if (meta.parameterPath !== 'authentication') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventData = getAuthenticationModalEventData(meta);
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
credentialSelected: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const creds = Object.keys(meta.updateInformation.properties.credentials || {});
|
||||||
|
if (creds.length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventData = {
|
||||||
|
eventName: 'User selected credential from node modal',
|
||||||
|
properties: {
|
||||||
|
credential_name: (meta.updateInformation.properties.credentials as IDataObject)[
|
||||||
|
creds[0]
|
||||||
|
],
|
||||||
|
credential_type: creds[0],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
workflowRun: {
|
||||||
|
runWorkflow: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = getExecutionStartedEventData(meta);
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
runError: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = {
|
||||||
|
eventName: meta.nodeName
|
||||||
|
? 'Node execution finished'
|
||||||
|
: 'Manual workflow execution finished',
|
||||||
|
properties: {
|
||||||
|
preflight: 'true',
|
||||||
|
status: 'failed',
|
||||||
|
error_message: meta.errorMessages.join('<br /> - '),
|
||||||
|
error_timestamp: new Date(),
|
||||||
|
node_name: meta.nodeName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
runData: {
|
||||||
|
displayModeChanged: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = getOutputModeChangedEventData(meta);
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
pushConnection: {
|
||||||
|
executionFinished: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = getExecutionFinishedEventData(meta);
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
deleteNode: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = getNodeRemovedEventData(meta);
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
workflow: {
|
||||||
|
activeChange: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = {
|
||||||
|
eventName: (meta.active && 'User activated workflow') || 'User deactivated workflow',
|
||||||
|
properties: {
|
||||||
|
workflow_id: meta.workflowId,
|
||||||
|
source: 'workflow_modal',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
activeChangeCurrent: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
const eventData = {
|
||||||
|
eventName: (meta.active && 'User activated workflow') || 'User deactivated workflow',
|
||||||
|
properties: {
|
||||||
|
source: 'main nav',
|
||||||
|
workflow_id: meta.workflowId,
|
||||||
|
workflow_name: workflowsStore.workflowName,
|
||||||
|
workflow_nodes: workflowsStore.allNodes.map((n) => n.type.split('.')[1]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
afterUpdate: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = {
|
||||||
|
eventName: 'User saved workflow',
|
||||||
|
properties: {
|
||||||
|
workflow_id: meta.workflowData.id,
|
||||||
|
workflow_name: meta.workflowData.name,
|
||||||
|
workflow_nodes: meta.workflowData.nodes.map((n) => n.type.split('.')[1]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
execution: {
|
||||||
|
open: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = {
|
||||||
|
eventName: 'User opened read-only execution',
|
||||||
|
properties: {
|
||||||
|
workflow_id: meta.workflowId,
|
||||||
|
workflow_name: meta.workflowName,
|
||||||
|
execution_id: meta.executionId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
nodeCreateList: {
|
||||||
|
destroyed: [
|
||||||
|
() => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
if (
|
||||||
|
nodesPanelSession.data.nodeFilter.length > 0 &&
|
||||||
|
nodesPanelSession.data.nodeFilter !== ''
|
||||||
|
) {
|
||||||
|
const eventData = hooksGenerateNodesPanelEvent();
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectedTypeChanged: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
const eventData = {
|
||||||
|
eventName: 'User changed nodes panel filter',
|
||||||
|
properties: {
|
||||||
|
old_filter: meta.oldValue,
|
||||||
|
new_filter: meta.newValue,
|
||||||
|
nodes_panel_session_id: nodesPanelSession.sessionId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
nodesPanelSession.data.filterMode = meta.newValue;
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nodeFilterChanged: [
|
||||||
|
(_, meta) => {
|
||||||
|
const segmentStore = useSegment();
|
||||||
|
if (meta.newValue.length === 0 && nodesPanelSession.data.nodeFilter.length > 0) {
|
||||||
|
const eventData = hooksGenerateNodesPanelEvent();
|
||||||
|
|
||||||
|
segmentStore.track(eventData.eventName, eventData.properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta.newValue.length > meta.oldValue.length) {
|
||||||
|
nodesPanelSession.data.nodeFilter = meta.newValue;
|
||||||
|
nodesPanelSession.data.resultsNodes = meta.filteredNodes.map((node) => {
|
||||||
|
if ((node as unknown as INodeUi).name) {
|
||||||
|
return (node as unknown as INodeUi).name.split('.')[1];
|
||||||
|
} else if (node.key) {
|
||||||
|
return node.key.split('.')[1];
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
4
packages/editor-ui/src/hooks/index.ts
Normal file
4
packages/editor-ui/src/hooks/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './cloud';
|
||||||
|
export * from './segment';
|
||||||
|
export * from './types';
|
||||||
|
export * from './utils';
|
1
packages/editor-ui/src/hooks/init.ts
Normal file
1
packages/editor-ui/src/hooks/init.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
window.n8nHooksNext = true;
|
351
packages/editor-ui/src/hooks/segment/getters.ts
Normal file
351
packages/editor-ui/src/hooks/segment/getters.ts
Normal file
|
@ -0,0 +1,351 @@
|
||||||
|
import { deepCopy } from 'n8n-workflow';
|
||||||
|
import type {
|
||||||
|
ExecutionError,
|
||||||
|
GenericValue,
|
||||||
|
INodeParameters,
|
||||||
|
INodeProperties,
|
||||||
|
ITelemetryTrackProperties,
|
||||||
|
NodeParameterValue,
|
||||||
|
INode,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import type { TelemetryEventData } from '@/hooks/types';
|
||||||
|
import type { INodeUi } from '@/Interface';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
|
|
||||||
|
export interface UserSavedCredentialsEventData {
|
||||||
|
credential_type: string;
|
||||||
|
credential_id: string;
|
||||||
|
is_new: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserSavedCredentialsEventData = (meta: UserSavedCredentialsEventData) => {
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventName: 'User saved credentials',
|
||||||
|
properties: {
|
||||||
|
instance_id: rootStore.instanceId,
|
||||||
|
credential_type: meta.credential_type,
|
||||||
|
credential_id: meta.credential_id,
|
||||||
|
workflow_id: workflowsStore.workflowId,
|
||||||
|
node_type: workflowsStore.activeNode?.name,
|
||||||
|
is_new: meta.is_new,
|
||||||
|
// is_complete: true,
|
||||||
|
// is_valid: true,
|
||||||
|
// error_message: ''
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOpenWorkflowSettingsEventData = (): TelemetryEventData => {
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventName: 'User opened workflow settings',
|
||||||
|
properties: {
|
||||||
|
workflow_id: workflowsStore.workflowId,
|
||||||
|
workflow_name: workflowsStore.workflowName,
|
||||||
|
current_settings: deepCopy(workflowsStore.workflowSettings),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface UpdatedWorkflowSettingsEventData {
|
||||||
|
oldSettings: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUpdatedWorkflowSettingsEventData = (
|
||||||
|
meta: UpdatedWorkflowSettingsEventData,
|
||||||
|
): TelemetryEventData => {
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventName: 'User updated workflow settings',
|
||||||
|
properties: {
|
||||||
|
workflow_id: workflowsStore.workflowId,
|
||||||
|
workflow_name: workflowsStore.workflowName,
|
||||||
|
new_settings: deepCopy(workflowsStore.workflowSettings),
|
||||||
|
old_settings: meta.oldSettings,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface NodeTypeChangedEventData {
|
||||||
|
nodeSubtitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getNodeTypeChangedEventData = (meta: NodeTypeChangedEventData): TelemetryEventData => {
|
||||||
|
const store = useNDVStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventName: 'User opened node modal',
|
||||||
|
properties: {
|
||||||
|
node_name: store.activeNode?.name,
|
||||||
|
node_subtitle: meta.nodeSubtitle,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface InsertedItemFromExpEditorEventData {
|
||||||
|
parameter: {
|
||||||
|
displayName: string;
|
||||||
|
};
|
||||||
|
value: string;
|
||||||
|
selectedItem: {
|
||||||
|
variable: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getInsertedItemFromExpEditorEventData = (
|
||||||
|
meta: InsertedItemFromExpEditorEventData,
|
||||||
|
): TelemetryEventData => {
|
||||||
|
const store = useNDVStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventName: 'User inserted item from Expression Editor variable selector',
|
||||||
|
properties: {
|
||||||
|
node_name: store.activeNode?.name,
|
||||||
|
node_type: store.activeNode?.type.split('.')[1],
|
||||||
|
parameter_name: meta.parameter.displayName,
|
||||||
|
variable_expression: meta.selectedItem.variable,
|
||||||
|
} as ITelemetryTrackProperties,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ExpressionEditorEventsData {
|
||||||
|
dialogVisible: boolean;
|
||||||
|
value: string;
|
||||||
|
resolvedExpressionValue: string;
|
||||||
|
parameter: INodeParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getExpressionEditorEventsData = (
|
||||||
|
meta: ExpressionEditorEventsData,
|
||||||
|
isValueDefault: boolean,
|
||||||
|
): TelemetryEventData => {
|
||||||
|
const store = useNDVStore();
|
||||||
|
const eventData: TelemetryEventData = {
|
||||||
|
eventName: '',
|
||||||
|
properties: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!meta.dialogVisible) {
|
||||||
|
eventData.eventName = 'User closed Expression Editor';
|
||||||
|
eventData.properties = {
|
||||||
|
empty_expression: isValueDefault,
|
||||||
|
expression_value: meta.value,
|
||||||
|
expression_result: meta.resolvedExpressionValue.slice(1),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
eventData.eventName = 'User opened Expression Editor';
|
||||||
|
eventData.properties = {
|
||||||
|
node_name: store.activeNode?.name,
|
||||||
|
node_type: store.activeNode?.type.split('.')[1],
|
||||||
|
parameter_name: meta.parameter.displayName,
|
||||||
|
parameter_field_type: meta.parameter.type,
|
||||||
|
new_expression: isValueDefault,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return eventData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AuthenticationModalEventData {
|
||||||
|
parameterPath: string;
|
||||||
|
oldNodeParameters: Record<string, GenericValue>;
|
||||||
|
parameters: INodeProperties[];
|
||||||
|
newValue: NodeParameterValue;
|
||||||
|
}
|
||||||
|
export const getAuthenticationModalEventData = (
|
||||||
|
meta: AuthenticationModalEventData,
|
||||||
|
): TelemetryEventData => {
|
||||||
|
const store = useNDVStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventName: 'User changed Authentication type from node modal',
|
||||||
|
properties: {
|
||||||
|
node_name: store.activeNode?.name,
|
||||||
|
node_type: store.activeNode?.type.split('.')[1],
|
||||||
|
old_mode:
|
||||||
|
meta.oldNodeParameters.authentication ||
|
||||||
|
(
|
||||||
|
meta.parameters.find((param) => param.name === 'authentication') || {
|
||||||
|
default: 'default',
|
||||||
|
}
|
||||||
|
).default,
|
||||||
|
new_mode: meta.newValue,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface OutputModeChangedEventData {
|
||||||
|
oldValue: string;
|
||||||
|
newValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOutputModeChangedEventData = (
|
||||||
|
meta: OutputModeChangedEventData,
|
||||||
|
): TelemetryEventData => {
|
||||||
|
const store = useNDVStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventName: 'User changed node output view mode',
|
||||||
|
properties: {
|
||||||
|
old_mode: meta.oldValue,
|
||||||
|
new_mode: meta.newValue,
|
||||||
|
node_name: store.activeNode?.name,
|
||||||
|
node_type: store.activeNode?.type.split('.')[1],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ExecutionFinishedEventData {
|
||||||
|
runDataExecutedStartData:
|
||||||
|
| { destinationNode?: string | undefined; runNodeFilter?: string[] | undefined }
|
||||||
|
| undefined;
|
||||||
|
nodeName?: string;
|
||||||
|
errorMessage: string;
|
||||||
|
resultDataError: ExecutionError | undefined;
|
||||||
|
itemsCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getExecutionFinishedEventData = (
|
||||||
|
meta: ExecutionFinishedEventData,
|
||||||
|
): TelemetryEventData => {
|
||||||
|
const store = useWorkflowsStore();
|
||||||
|
|
||||||
|
const eventData: TelemetryEventData = {
|
||||||
|
eventName: '',
|
||||||
|
properties: {
|
||||||
|
execution_id: store.activeExecutionId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (meta.runDataExecutedStartData?.destinationNode) {
|
||||||
|
eventData.eventName = 'Node execution finished';
|
||||||
|
eventData.properties!.node_type = store.getNodeByName(meta.nodeName || '')?.type.split('.')[1];
|
||||||
|
eventData.properties!.node_name = meta.nodeName;
|
||||||
|
} else {
|
||||||
|
eventData.eventName = 'Manual workflow execution finished';
|
||||||
|
eventData.properties!.workflow_id = store.workflowId;
|
||||||
|
eventData.properties!.workflow_name = store.workflowName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta.errorMessage || meta.resultDataError) {
|
||||||
|
eventData.properties!.status = 'failed';
|
||||||
|
eventData.properties!.error_message =
|
||||||
|
(meta.resultDataError && meta.resultDataError.message) || '';
|
||||||
|
eventData.properties!.error_stack = (meta.resultDataError && meta.resultDataError.stack) || '';
|
||||||
|
eventData.properties!.error_ui_message = meta.errorMessage || '';
|
||||||
|
eventData.properties!.error_timestamp = new Date();
|
||||||
|
|
||||||
|
if (meta.resultDataError && (meta.resultDataError as unknown as { node: INodeUi })?.node) {
|
||||||
|
eventData.properties!.error_node =
|
||||||
|
typeof (meta.resultDataError as unknown as { node: string })?.node === 'string'
|
||||||
|
? (meta.resultDataError as unknown as { node: string })?.node
|
||||||
|
: (meta.resultDataError as unknown as { node: INodeUi })?.node?.name;
|
||||||
|
} else {
|
||||||
|
eventData.properties!.error_node = meta.nodeName;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eventData.properties!.status = 'success';
|
||||||
|
if (meta.runDataExecutedStartData?.destinationNode) {
|
||||||
|
// Node execution finished
|
||||||
|
eventData.properties!.items_count = meta.itemsCount || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return eventData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface NodeRemovedEventData {
|
||||||
|
node: INodeUi;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getNodeRemovedEventData = (meta: NodeRemovedEventData): TelemetryEventData => {
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventName: 'User removed node from workflow canvas',
|
||||||
|
properties: {
|
||||||
|
node_name: meta.node.name,
|
||||||
|
node_type: meta.node.type,
|
||||||
|
node_disabled: meta.node.disabled,
|
||||||
|
workflow_id: workflowsStore.workflowId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNodeEditingFinishedEventData = (
|
||||||
|
activeNode: INode | null,
|
||||||
|
): TelemetryEventData | undefined => {
|
||||||
|
switch (activeNode?.type) {
|
||||||
|
case 'n8n-nodes-base.httpRequest':
|
||||||
|
const domain = (activeNode.parameters.url as string).split('/')[2];
|
||||||
|
return {
|
||||||
|
eventName: 'User finished httpRequest node editing',
|
||||||
|
properties: {
|
||||||
|
method: activeNode.parameters.method,
|
||||||
|
domain,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case 'n8n-nodes-base.function':
|
||||||
|
return {
|
||||||
|
eventName: 'User finished function node editing',
|
||||||
|
properties: {
|
||||||
|
node_name: activeNode.name,
|
||||||
|
code: activeNode.parameters.functionCode,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case 'n8n-nodes-base.functionItem':
|
||||||
|
return {
|
||||||
|
eventName: 'User finished functionItem node editing',
|
||||||
|
properties: {
|
||||||
|
node_name: activeNode.name,
|
||||||
|
code: activeNode.parameters.functionCode,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ExecutionStartedEventData {
|
||||||
|
nodeName?: string;
|
||||||
|
source?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getExecutionStartedEventData = (
|
||||||
|
meta: ExecutionStartedEventData,
|
||||||
|
): TelemetryEventData => {
|
||||||
|
const store = useWorkflowsStore();
|
||||||
|
|
||||||
|
const eventData: TelemetryEventData = {
|
||||||
|
eventName: '',
|
||||||
|
properties: {
|
||||||
|
execution_id: store.activeExecutionId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// node execution
|
||||||
|
if (meta.nodeName) {
|
||||||
|
eventData.eventName = 'User started node execution';
|
||||||
|
eventData.properties!.source = 'unknown';
|
||||||
|
eventData.properties!.node_type = store.getNodeByName(meta.nodeName)?.type.split('.')[1];
|
||||||
|
eventData.properties!.node_name = meta.nodeName;
|
||||||
|
|
||||||
|
if (meta.source === 'RunData.ExecuteNodeButton') {
|
||||||
|
eventData.properties!.source = 'node_modal';
|
||||||
|
} else if (meta.source === 'Node.executeNode') {
|
||||||
|
eventData.properties!.source = 'workflow_canvas';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// workflow execution
|
||||||
|
eventData.eventName = 'User started manual workflow execution';
|
||||||
|
eventData.properties!.workflow_id = store.workflowId;
|
||||||
|
eventData.properties!.workflow_name = store.workflowName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventData;
|
||||||
|
};
|
1
packages/editor-ui/src/hooks/segment/index.ts
Normal file
1
packages/editor-ui/src/hooks/segment/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './getters';
|
6
packages/editor-ui/src/hooks/types.ts
Normal file
6
packages/editor-ui/src/hooks/types.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import type { ITelemetryTrackProperties } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export interface TelemetryEventData {
|
||||||
|
eventName: string;
|
||||||
|
properties?: ITelemetryTrackProperties;
|
||||||
|
}
|
38
packages/editor-ui/src/hooks/utils/hooksAddAdminIcon.ts
Normal file
38
packages/editor-ui/src/hooks/utils/hooksAddAdminIcon.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import type { IMenuItem } from 'n8n-design-system/types';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { addAutoLoginToAdminPanelButton } from '@/hooks/utils/hooksAddAutoLoginToAdminPanelButton';
|
||||||
|
|
||||||
|
let adminIconAdded = false;
|
||||||
|
|
||||||
|
export const hooksAddAdminIcon = () => {
|
||||||
|
if (adminIconAdded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uiStore = useUIStore();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
|
||||||
|
if (usersStore?.globalRoleName !== 'owner') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems: IMenuItem[] = [
|
||||||
|
{
|
||||||
|
id: 'admin',
|
||||||
|
type: 'link',
|
||||||
|
position: 'bottom',
|
||||||
|
label: 'Admin Panel',
|
||||||
|
icon: 'home',
|
||||||
|
properties: {
|
||||||
|
href: 'https://app.n8n.cloud',
|
||||||
|
newWindow: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
addAutoLoginToAdminPanelButton();
|
||||||
|
|
||||||
|
uiStore.sidebarMenuItems = [...uiStore.sidebarMenuItems, ...menuItems] as IMenuItem[];
|
||||||
|
adminIconAdded = true;
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
export function addAutoLoginToAdminPanelButton() {
|
||||||
|
const adminPanelHost = new URL(window.location.href).host.split('.').slice(1).join('.');
|
||||||
|
|
||||||
|
document.body?.addEventListener('click', async (e) => {
|
||||||
|
if (!e.target || !(e.target instanceof Element)) return;
|
||||||
|
if (e.target.getAttribute('id') !== 'admin' && !e.target.closest('#admin')) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const restPath = window.REST_ENDPOINT ?? 'rest';
|
||||||
|
const response = await fetch(`/${restPath}/cloud/proxy/login/code`);
|
||||||
|
const { code } = await response.json();
|
||||||
|
window.location.href = `https://${adminPanelHost}/login?code=${code}`;
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import type { IFakeDoor } from '@/Interface';
|
||||||
|
import { FAKE_DOOR_FEATURES } from '@/constants';
|
||||||
|
|
||||||
|
export function compileFakeDoorFeatures(): IFakeDoor[] {
|
||||||
|
const store = useUIStore();
|
||||||
|
const fakeDoorFeatures: IFakeDoor[] = store.fakeDoorFeatures.map((feature) => ({ ...feature }));
|
||||||
|
|
||||||
|
const environmentsFeature = fakeDoorFeatures.find(
|
||||||
|
(feature) => feature.id === FAKE_DOOR_FEATURES.ENVIRONMENTS,
|
||||||
|
);
|
||||||
|
if (environmentsFeature) {
|
||||||
|
environmentsFeature.actionBoxTitle += '.cloud';
|
||||||
|
environmentsFeature.linkURL += '&edition=cloud';
|
||||||
|
}
|
||||||
|
|
||||||
|
const loggingFeature = fakeDoorFeatures.find(
|
||||||
|
(feature) => feature.id === FAKE_DOOR_FEATURES.LOGGING,
|
||||||
|
);
|
||||||
|
if (loggingFeature) {
|
||||||
|
loggingFeature.actionBoxTitle += '.cloud';
|
||||||
|
loggingFeature.linkURL += '&edition=cloud';
|
||||||
|
loggingFeature.infoText = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return fakeDoorFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hooksAddFakeDoorFeatures = () => {
|
||||||
|
const store = useUIStore();
|
||||||
|
|
||||||
|
store.fakeDoorFeatures = compileFakeDoorFeatures();
|
||||||
|
};
|
30
packages/editor-ui/src/hooks/utils/hooksNodesPanel.ts
Normal file
30
packages/editor-ui/src/hooks/utils/hooksNodesPanel.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
export const nodesPanelSession = {
|
||||||
|
sessionId: '',
|
||||||
|
data: {
|
||||||
|
nodeFilter: '',
|
||||||
|
resultsNodes: [] as string[],
|
||||||
|
filterMode: 'Regular',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hooksGenerateNodesPanelEvent = () => {
|
||||||
|
return {
|
||||||
|
eventName: 'User entered nodes panel search term',
|
||||||
|
properties: {
|
||||||
|
search_string: nodesPanelSession.data.nodeFilter,
|
||||||
|
results_count: nodesPanelSession.data.resultsNodes.length,
|
||||||
|
results_nodes: nodesPanelSession.data.resultsNodes,
|
||||||
|
filter_mode: nodesPanelSession.data.filterMode,
|
||||||
|
nodes_panel_session_id: nodesPanelSession.sessionId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hooksResetNodesPanelSession = () => {
|
||||||
|
nodesPanelSession.sessionId = `nodes_panel_session_${new Date().valueOf()}`;
|
||||||
|
nodesPanelSession.data = {
|
||||||
|
nodeFilter: '',
|
||||||
|
resultsNodes: [],
|
||||||
|
filterMode: 'Regular',
|
||||||
|
};
|
||||||
|
};
|
4
packages/editor-ui/src/hooks/utils/index.ts
Normal file
4
packages/editor-ui/src/hooks/utils/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './hooksAddAdminIcon';
|
||||||
|
export * from './hooksAddAutoLoginToAdminPanelButton';
|
||||||
|
export * from './hooksAddFakeDoorFeatures';
|
||||||
|
export * from './hooksNodesPanel';
|
|
@ -11,6 +11,8 @@ import '@fontsource/open-sans/latin-400.css';
|
||||||
import '@fontsource/open-sans/latin-600.css';
|
import '@fontsource/open-sans/latin-600.css';
|
||||||
import '@fontsource/open-sans/latin-700.css';
|
import '@fontsource/open-sans/latin-700.css';
|
||||||
|
|
||||||
|
import './hooks/init';
|
||||||
|
|
||||||
import App from '@/App.vue';
|
import App from '@/App.vue';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,36 @@
|
||||||
import type { IExternalHooks } from '@/Interface';
|
import type { IExternalHooks } from '@/Interface';
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
import type { PartialDeep } from 'type-fest';
|
||||||
import { useWebhooksStore } from '@/stores/webhooks.store';
|
import type { ExternalHooks, ExternalHooksGenericContext } from '@/types';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { runExternalHook } from '@/utils';
|
import { runExternalHook } from '@/utils';
|
||||||
|
|
||||||
|
export function extendExternalHooks(hooksExtension: PartialDeep<ExternalHooks>) {
|
||||||
|
if (typeof window.n8nExternalHooks === 'undefined') {
|
||||||
|
window.n8nExternalHooks = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const resource of Object.keys(hooksExtension) as Array<keyof ExternalHooks>) {
|
||||||
|
if (typeof window.n8nExternalHooks[resource] === 'undefined') {
|
||||||
|
window.n8nExternalHooks[resource] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionContext = hooksExtension[resource] as ExternalHooksGenericContext;
|
||||||
|
const context = window.n8nExternalHooks[resource] as ExternalHooksGenericContext;
|
||||||
|
for (const operator of Object.keys(extensionContext)) {
|
||||||
|
if (typeof context[operator] === 'undefined') {
|
||||||
|
context[operator] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
context[operator].push(...extensionContext[operator]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const externalHooks = defineComponent({
|
export const externalHooks = defineComponent({
|
||||||
methods: {
|
methods: {
|
||||||
$externalHooks(): IExternalHooks {
|
$externalHooks(): IExternalHooks {
|
||||||
return {
|
return {
|
||||||
run: async (eventName: string, metadata?: IDataObject): Promise<void> => {
|
run: runExternalHook,
|
||||||
await runExternalHook.call(this, eventName, useWebhooksStore(), metadata);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { useUsersStore } from './stores/users.store';
|
||||||
import { useTemplatesStore } from './stores/templates.store';
|
import { useTemplatesStore } from './stores/templates.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useSSOStore } from './stores/sso.store';
|
import { useSSOStore } from './stores/sso.store';
|
||||||
import { useWebhooksStore } from '@/stores/webhooks.store';
|
|
||||||
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
|
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
|
||||||
import { useTelemetry } from '@/composables';
|
import { useTelemetry } from '@/composables';
|
||||||
|
|
||||||
|
@ -963,7 +962,7 @@ router.afterEach((to, from) => {
|
||||||
* Run external hooks
|
* Run external hooks
|
||||||
*/
|
*/
|
||||||
|
|
||||||
void runExternalHook('main.routeChange', useWebhooksStore(), { from, to });
|
void runExternalHook('main.routeChange', { from, to });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Track current view for telemetry
|
* Track current view for telemetry
|
||||||
|
|
10
packages/editor-ui/src/shims.d.ts
vendored
10
packages/editor-ui/src/shims.d.ts
vendored
|
@ -1,6 +1,6 @@
|
||||||
import { VNode, ComponentPublicInstance } from 'vue';
|
import { VNode, ComponentPublicInstance } from 'vue';
|
||||||
import type { Store } from 'pinia';
|
import { PartialDeep } from 'type-fest';
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
import { ExternalHooks } from '@/types/externalHooks';
|
||||||
|
|
||||||
declare module 'markdown-it-link-attributes';
|
declare module 'markdown-it-link-attributes';
|
||||||
declare module 'markdown-it-emoji';
|
declare module 'markdown-it-emoji';
|
||||||
|
@ -20,10 +20,8 @@ declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
BASE_PATH: string;
|
BASE_PATH: string;
|
||||||
REST_ENDPOINT: string;
|
REST_ENDPOINT: string;
|
||||||
n8nExternalHooks?: Record<
|
n8nHooksNext: boolean;
|
||||||
string,
|
n8nExternalHooks?: PartialDeep<ExternalHooks>;
|
||||||
Record<string, Array<(store: Store, metadata?: IDataObject) => Promise<void>>>
|
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
|
|
|
@ -184,6 +184,7 @@ export const usePostHog = defineStore('posthog', () => {
|
||||||
isVariantEnabled,
|
isVariantEnabled,
|
||||||
getVariant,
|
getVariant,
|
||||||
reset,
|
reset,
|
||||||
|
identify,
|
||||||
capture,
|
capture,
|
||||||
setMetadata,
|
setMetadata,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,10 +7,12 @@ import {
|
||||||
WEBHOOK_NODE_TYPE,
|
WEBHOOK_NODE_TYPE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import type { INodeTypeDescription, IRun, ITelemetryTrackProperties } from 'n8n-workflow';
|
import type { INodeTypeDescription, IRun, ITelemetryTrackProperties } from 'n8n-workflow';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
ADDED_MANUAL_TRIGGER: 'User added manual trigger',
|
ADDED_MANUAL_TRIGGER: 'User added manual trigger',
|
||||||
|
@ -27,6 +29,7 @@ export const useSegment = defineStore('segment', () => {
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
|
||||||
const track = (eventName: string, properties?: ITelemetryTrackProperties) => {
|
const track = (eventName: string, properties?: ITelemetryTrackProperties) => {
|
||||||
if (settingsStore.telemetry.enabled) {
|
if (settingsStore.telemetry.enabled) {
|
||||||
|
@ -34,6 +37,20 @@ export const useSegment = defineStore('segment', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const page = (category: string, name: string, properties?: ITelemetryTrackProperties) => {
|
||||||
|
if (settingsStore.telemetry.enabled) {
|
||||||
|
window.analytics?.page(category, name, properties);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const identify = () => {
|
||||||
|
const userId = usersStore.currentUserId;
|
||||||
|
|
||||||
|
if (settingsStore.telemetry.enabled && userId) {
|
||||||
|
window.analytics?.identify(userId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const trackAddedTrigger = (nodeTypeName: string) => {
|
const trackAddedTrigger = (nodeTypeName: string) => {
|
||||||
if (!nodeTypesStore.isTriggerNode(nodeTypeName)) {
|
if (!nodeTypesStore.isTriggerNode(nodeTypeName)) {
|
||||||
return;
|
return;
|
||||||
|
@ -119,6 +136,8 @@ export const useSegment = defineStore('segment', () => {
|
||||||
track,
|
track,
|
||||||
trackAddedTrigger,
|
trackAddedTrigger,
|
||||||
trackSuccessfulWorkflowExecution,
|
trackSuccessfulWorkflowExecution,
|
||||||
|
identify,
|
||||||
|
page,
|
||||||
EVENTS,
|
EVENTS,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -115,10 +115,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
||||||
return this.settings.deployment?.type.startsWith('desktop_');
|
return this.settings.deployment?.type.startsWith('desktop_');
|
||||||
},
|
},
|
||||||
isCloudDeployment(): boolean {
|
isCloudDeployment(): boolean {
|
||||||
if (!this.settings.deployment) {
|
return this.settings.deployment?.type === 'cloud';
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.settings.deployment.type === 'cloud';
|
|
||||||
},
|
},
|
||||||
isSmtpSetup(): boolean {
|
isSmtpSetup(): boolean {
|
||||||
return this.userManagement.smtpSetup;
|
return this.userManagement.smtpSetup;
|
||||||
|
|
|
@ -1,69 +1,19 @@
|
||||||
import { STORES } from '@/constants';
|
import { STORES } from '@/constants';
|
||||||
import type { IFakeDoor, INodeUi, IRootState, NestedRecord } from '@/Interface';
|
|
||||||
import type { IMenuItem } from 'n8n-design-system';
|
|
||||||
import type { IWorkflowSettings } from 'n8n-workflow';
|
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useRootStore } from './n8nRoot.store';
|
import { useRootStore } from './n8nRoot.store';
|
||||||
import { useNDVStore } from './ndv.store';
|
import { useNDVStore } from './ndv.store';
|
||||||
import { useUIStore } from './ui.store';
|
import { useUIStore } from './ui.store';
|
||||||
import { useUsersStore } from './users.store';
|
import { useUsersStore } from './users.store';
|
||||||
import { useWorkflowsStore } from './workflows.store';
|
import { useWorkflowsStore } from './workflows.store';
|
||||||
|
import { useSettingsStore } from './settings.store';
|
||||||
|
|
||||||
export const useWebhooksStore = defineStore(STORES.WEBHOOKS, {
|
export const useWebhooksStore = defineStore(STORES.WEBHOOKS, () => {
|
||||||
getters: {
|
return {
|
||||||
globalRoleName(): string {
|
...useRootStore(),
|
||||||
return useUsersStore().globalRoleName;
|
...useWorkflowsStore(),
|
||||||
},
|
...useUIStore(),
|
||||||
getContextBasedTranslationKeys() {
|
...useUsersStore(),
|
||||||
return useUIStore().contextBasedTranslationKeys;
|
...useNDVStore(),
|
||||||
},
|
...useSettingsStore(),
|
||||||
getFakeDoorFeatures() {
|
};
|
||||||
return useUIStore().fakeDoorFeatures;
|
|
||||||
},
|
|
||||||
getFakeDoorItems(): IFakeDoor[] {
|
|
||||||
return useUIStore().fakeDoorFeatures;
|
|
||||||
},
|
|
||||||
n8nMetadata(): IRootState['n8nMetadata'] {
|
|
||||||
return useRootStore().n8nMetadata;
|
|
||||||
},
|
|
||||||
instanceId(): string {
|
|
||||||
return useRootStore().instanceId;
|
|
||||||
},
|
|
||||||
workflowId(): string {
|
|
||||||
return useWorkflowsStore().workflowId;
|
|
||||||
},
|
|
||||||
workflowName(): string {
|
|
||||||
return useWorkflowsStore().workflowName;
|
|
||||||
},
|
|
||||||
activeNode(): INodeUi | null {
|
|
||||||
return useNDVStore().activeNode;
|
|
||||||
},
|
|
||||||
workflowSettings(): IWorkflowSettings {
|
|
||||||
return useWorkflowsStore().workflowSettings;
|
|
||||||
},
|
|
||||||
activeExecutionId(): string {
|
|
||||||
return useWorkflowsStore().activeExecutionId || '';
|
|
||||||
},
|
|
||||||
nodeByName:
|
|
||||||
(state: IRootState) =>
|
|
||||||
(nodeName: string): INodeUi | null => {
|
|
||||||
return useWorkflowsStore().getNodeByName(nodeName);
|
|
||||||
},
|
|
||||||
allNodes(): INodeUi[] {
|
|
||||||
return useWorkflowsStore().allNodes;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
addSidebarMenuItems(menuItems: IMenuItem[]) {
|
|
||||||
const uiStore = useUIStore();
|
|
||||||
const updated = uiStore.sidebarMenuItems.concat(menuItems);
|
|
||||||
uiStore.sidebarMenuItems = updated;
|
|
||||||
},
|
|
||||||
setFakeDoorFeatures(fakeDoors: IFakeDoor[]): void {
|
|
||||||
useUIStore().fakeDoorFeatures = fakeDoors;
|
|
||||||
},
|
|
||||||
setContextBasedTranslationKeys(translations: NestedRecord<string>): void {
|
|
||||||
useUIStore().contextBasedTranslationKeys = translations;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
294
packages/editor-ui/src/types/externalHooks.ts
Normal file
294
packages/editor-ui/src/types/externalHooks.ts
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
import type { N8nInput } from 'n8n-design-system';
|
||||||
|
import type {
|
||||||
|
IConnections,
|
||||||
|
INodeParameters,
|
||||||
|
INodeProperties,
|
||||||
|
INodeTypeDescription,
|
||||||
|
ITelemetryTrackProperties,
|
||||||
|
INodeParameterResourceLocator,
|
||||||
|
NodeParameterValue,
|
||||||
|
ResourceMapperValue,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import type { RouteLocation } from 'vue-router';
|
||||||
|
import type {
|
||||||
|
AuthenticationModalEventData,
|
||||||
|
ExecutionFinishedEventData,
|
||||||
|
ExecutionStartedEventData,
|
||||||
|
ExpressionEditorEventsData,
|
||||||
|
InsertedItemFromExpEditorEventData,
|
||||||
|
NodeRemovedEventData,
|
||||||
|
NodeTypeChangedEventData,
|
||||||
|
OutputModeChangedEventData,
|
||||||
|
UpdatedWorkflowSettingsEventData,
|
||||||
|
UserSavedCredentialsEventData,
|
||||||
|
} from '@/hooks';
|
||||||
|
import type {
|
||||||
|
INodeCreateElement,
|
||||||
|
INodeUi,
|
||||||
|
INodeUpdatePropertiesInformation,
|
||||||
|
IPersonalizationLatestVersion,
|
||||||
|
IWorkflowDb,
|
||||||
|
NodeFilterType,
|
||||||
|
} from '@/Interface';
|
||||||
|
import type { ComponentPublicInstance } from 'vue/dist/vue';
|
||||||
|
import type { useWebhooksStore } from '@/stores';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export interface ExternalHooksMethod<T = any, R = void> {
|
||||||
|
(store: ReturnType<typeof useWebhooksStore>, metadata: T): R | Promise<R>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExternalHooksGenericContext {
|
||||||
|
[key: string]: ExternalHooksMethod[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExternalHooks {
|
||||||
|
parameterInput: {
|
||||||
|
mount: Array<
|
||||||
|
ExternalHooksMethod<{
|
||||||
|
inputFieldRef?: InstanceType<typeof N8nInput>;
|
||||||
|
parameter?: INodeProperties;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
modeSwitch: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
updated: Array<ExternalHooksMethod<{ remoteParameterOptions: NodeListOf<Element> }>>;
|
||||||
|
};
|
||||||
|
nodeCreatorSearchBar: {
|
||||||
|
mount: Array<ExternalHooksMethod<{ inputRef: HTMLElement | null }>>;
|
||||||
|
};
|
||||||
|
app: {
|
||||||
|
mount: Array<ExternalHooksMethod<{}>>;
|
||||||
|
};
|
||||||
|
nodeView: {
|
||||||
|
mount: Array<ExternalHooksMethod<{}>>;
|
||||||
|
createNodeActiveChanged: Array<
|
||||||
|
ExternalHooksMethod<{
|
||||||
|
source?: string;
|
||||||
|
mode: string;
|
||||||
|
createNodeActive: boolean;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
addNodeButton: Array<ExternalHooksMethod<{ nodeTypeName: string }>>;
|
||||||
|
onRunNode: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
onRunWorkflow: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
};
|
||||||
|
main: {
|
||||||
|
routeChange: Array<ExternalHooksMethod<{ to: RouteLocation; from: RouteLocation }>>;
|
||||||
|
};
|
||||||
|
credential: {
|
||||||
|
saved: Array<ExternalHooksMethod<UserSavedCredentialsEventData>>;
|
||||||
|
};
|
||||||
|
copyInput: {
|
||||||
|
mounted: Array<ExternalHooksMethod<{ copyInputValueRef: HTMLElement }>>;
|
||||||
|
};
|
||||||
|
credentialsEdit: {
|
||||||
|
credentialTypeChanged: Array<
|
||||||
|
ExternalHooksMethod<{
|
||||||
|
newValue: string;
|
||||||
|
setCredentialType: string;
|
||||||
|
credentialType: string;
|
||||||
|
editCredentials: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
credentialModalOpened: Array<
|
||||||
|
ExternalHooksMethod<{
|
||||||
|
activeNode: INodeUi | null;
|
||||||
|
isEditingCredential: boolean;
|
||||||
|
credentialType: string | null;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
credentialsList: {
|
||||||
|
mounted: Array<ExternalHooksMethod<{ tableRef: ComponentPublicInstance }>>;
|
||||||
|
dialogVisibleChanged: Array<ExternalHooksMethod<{ dialogVisible: boolean }>>;
|
||||||
|
};
|
||||||
|
credentialsSelectModal: {
|
||||||
|
openCredentialType: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
};
|
||||||
|
credentialEdit: {
|
||||||
|
saveCredential: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
};
|
||||||
|
workflowSettings: {
|
||||||
|
dialogVisibleChanged: Array<ExternalHooksMethod<{ dialogVisible: boolean }>>;
|
||||||
|
saveSettings: Array<ExternalHooksMethod<UpdatedWorkflowSettingsEventData>>;
|
||||||
|
};
|
||||||
|
dataDisplay: {
|
||||||
|
onDocumentationUrlClick: Array<
|
||||||
|
ExternalHooksMethod<{
|
||||||
|
nodeType: INodeTypeDescription;
|
||||||
|
documentationUrl: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
nodeTypeChanged: Array<ExternalHooksMethod<NodeTypeChangedEventData>>;
|
||||||
|
nodeEditingFinished: Array<ExternalHooksMethod<{}>>;
|
||||||
|
};
|
||||||
|
executionsList: {
|
||||||
|
created: Array<
|
||||||
|
ExternalHooksMethod<{ filtersRef: HTMLElement; tableRef: ComponentPublicInstance }>
|
||||||
|
>;
|
||||||
|
openDialog: Array<ExternalHooksMethod<{}>>;
|
||||||
|
};
|
||||||
|
showMessage: {
|
||||||
|
showError: Array<
|
||||||
|
ExternalHooksMethod<{ title: string; message?: string; errorMessage: string }>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
expressionEdit: {
|
||||||
|
itemSelected: Array<ExternalHooksMethod<InsertedItemFromExpEditorEventData>>;
|
||||||
|
dialogVisibleChanged: Array<ExternalHooksMethod<ExpressionEditorEventsData>>;
|
||||||
|
closeDialog: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
mounted: Array<
|
||||||
|
ExternalHooksMethod<{
|
||||||
|
expressionInputRef: HTMLElement;
|
||||||
|
expressionOutputRef: HTMLElement;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
nodeSettings: {
|
||||||
|
valueChanged: Array<ExternalHooksMethod<AuthenticationModalEventData>>;
|
||||||
|
credentialSelected: Array<
|
||||||
|
ExternalHooksMethod<{
|
||||||
|
updateInformation: INodeUpdatePropertiesInformation;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
workflowRun: {
|
||||||
|
runWorkflow: Array<ExternalHooksMethod<ExecutionStartedEventData>>;
|
||||||
|
runError: Array<ExternalHooksMethod<{ errorMessages: string[]; nodeName: string | undefined }>>;
|
||||||
|
};
|
||||||
|
runData: {
|
||||||
|
updated: Array<ExternalHooksMethod<{ elements: HTMLElement[] }>>;
|
||||||
|
onTogglePinData: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
onDataPinningSuccess: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
displayModeChanged: Array<ExternalHooksMethod<OutputModeChangedEventData>>;
|
||||||
|
};
|
||||||
|
pushConnection: {
|
||||||
|
executionFinished: Array<ExternalHooksMethod<ExecutionFinishedEventData>>;
|
||||||
|
};
|
||||||
|
node: {
|
||||||
|
deleteNode: Array<ExternalHooksMethod<NodeRemovedEventData>>;
|
||||||
|
};
|
||||||
|
nodeExecuteButton: {
|
||||||
|
onClick: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
};
|
||||||
|
workflow: {
|
||||||
|
activeChange: Array<ExternalHooksMethod<{ active: boolean; workflowId: string }>>;
|
||||||
|
activeChangeCurrent: Array<ExternalHooksMethod<{ workflowId: string; active: boolean }>>;
|
||||||
|
afterUpdate: Array<ExternalHooksMethod<{ workflowData: IWorkflowDb }>>;
|
||||||
|
open: Array<ExternalHooksMethod<{ workflowId: string; workflowName: string }>>;
|
||||||
|
};
|
||||||
|
execution: {
|
||||||
|
open: Array<
|
||||||
|
ExternalHooksMethod<{ workflowId: string; workflowName: string; executionId: string }>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
userInfo: {
|
||||||
|
mounted: Array<ExternalHooksMethod<{ userInfoRef: HTMLElement }>>;
|
||||||
|
};
|
||||||
|
variableSelectorItem: {
|
||||||
|
mounted: Array<ExternalHooksMethod<{ variableSelectorItemRef: HTMLElement }>>;
|
||||||
|
};
|
||||||
|
mainSidebar: {
|
||||||
|
mounted: Array<ExternalHooksMethod<{ userRef: Element }>>;
|
||||||
|
};
|
||||||
|
nodeCreateList: {
|
||||||
|
destroyed: Array<ExternalHooksMethod<{}>>;
|
||||||
|
addAction: Array<
|
||||||
|
ExternalHooksMethod<{
|
||||||
|
node_type?: string;
|
||||||
|
action: string;
|
||||||
|
resource:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| true
|
||||||
|
| INodeParameters
|
||||||
|
| INodeParameterResourceLocator
|
||||||
|
| NodeParameterValue[]
|
||||||
|
| INodeParameters[]
|
||||||
|
| INodeParameterResourceLocator[]
|
||||||
|
| ResourceMapperValue[];
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
selectedTypeChanged: Array<ExternalHooksMethod<{ oldValue: string; newValue: string }>>;
|
||||||
|
filteredNodeTypesComputed: Array<
|
||||||
|
ExternalHooksMethod<{
|
||||||
|
nodeFilter: string;
|
||||||
|
result: INodeCreateElement[];
|
||||||
|
selectedType: NodeFilterType;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
nodeFilterChanged: Array<
|
||||||
|
ExternalHooksMethod<{
|
||||||
|
oldValue: string;
|
||||||
|
newValue: string;
|
||||||
|
selectedType: NodeFilterType;
|
||||||
|
filteredNodes: INodeCreateElement[];
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
onActionsCustmAPIClicked: Array<ExternalHooksMethod<{ app_identifier?: string }>>;
|
||||||
|
onViewActions: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
};
|
||||||
|
personalizationModal: {
|
||||||
|
onSubmit: Array<ExternalHooksMethod<IPersonalizationLatestVersion>>;
|
||||||
|
};
|
||||||
|
settingsPersonalView: {
|
||||||
|
mounted: Array<ExternalHooksMethod<{ userRef: HTMLElement }>>;
|
||||||
|
};
|
||||||
|
workflowOpen: {
|
||||||
|
mounted: Array<ExternalHooksMethod<{ tableRef: ComponentPublicInstance }>>;
|
||||||
|
};
|
||||||
|
workflowActivate: {
|
||||||
|
updateWorkflowActivation: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
};
|
||||||
|
runDataTable: {
|
||||||
|
onDragEnd: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
};
|
||||||
|
runDataJson: {
|
||||||
|
onDragEnd: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
};
|
||||||
|
sticky: {
|
||||||
|
mounted: Array<ExternalHooksMethod<{ stickyRef: HTMLElement }>>;
|
||||||
|
};
|
||||||
|
telemetry: {
|
||||||
|
currentUserIdChanged: Array<ExternalHooksMethod<{}>>;
|
||||||
|
};
|
||||||
|
settingsCommunityNodesView: {
|
||||||
|
openInstallModal: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
};
|
||||||
|
templatesWorkflowView: {
|
||||||
|
openWorkflow: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
};
|
||||||
|
templatesCollectionView: {
|
||||||
|
onUseWorkflow: Array<ExternalHooksMethod<ITelemetryTrackProperties>>;
|
||||||
|
};
|
||||||
|
template: {
|
||||||
|
requested: Array<ExternalHooksMethod<{ templateId: string }>>;
|
||||||
|
open: Array<
|
||||||
|
ExternalHooksMethod<{
|
||||||
|
templateId: string;
|
||||||
|
templateName: string;
|
||||||
|
workflow: { nodes: INodeUi[]; connections: IConnections };
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExternalHooksKey = {
|
||||||
|
[K in keyof ExternalHooks]: `${K}.${Extract<keyof ExternalHooks[K], string>}`;
|
||||||
|
}[keyof ExternalHooks];
|
||||||
|
|
||||||
|
type ExtractHookMethodArray<
|
||||||
|
P extends keyof ExternalHooks,
|
||||||
|
S extends keyof ExternalHooks[P],
|
||||||
|
> = ExternalHooks[P][S] extends Array<infer U> ? U : never;
|
||||||
|
|
||||||
|
type ExtractHookMethodFunction<T> = T extends ExternalHooksMethod ? T : never;
|
||||||
|
|
||||||
|
export type ExtractExternalHooksMethodPayloadFromKey<T extends ExternalHooksKey> =
|
||||||
|
T extends `${infer P}.${infer S}`
|
||||||
|
? P extends keyof ExternalHooks
|
||||||
|
? S extends keyof ExternalHooks[P]
|
||||||
|
? Parameters<ExtractHookMethodFunction<ExtractHookMethodArray<P, S>>>[1]
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never;
|
1
packages/editor-ui/src/types/index.ts
Normal file
1
packages/editor-ui/src/types/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './externalHooks';
|
|
@ -1,18 +1,33 @@
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
import type { IDataObject } from 'n8n-workflow';
|
||||||
import type { Store } from 'pinia';
|
import type {
|
||||||
|
ExternalHooks,
|
||||||
|
ExternalHooksKey,
|
||||||
|
ExternalHooksGenericContext,
|
||||||
|
ExtractExternalHooksMethodPayloadFromKey,
|
||||||
|
} from '@/types/externalHooks';
|
||||||
|
import { useWebhooksStore } from '@/stores/webhooks.store';
|
||||||
|
|
||||||
export async function runExternalHook(eventName: string, store: Store, metadata?: IDataObject) {
|
export async function runExternalHook<T extends ExternalHooksKey>(
|
||||||
|
eventName: T,
|
||||||
|
metadata?: ExtractExternalHooksMethodPayloadFromKey<T>,
|
||||||
|
) {
|
||||||
if (!window.n8nExternalHooks) {
|
if (!window.n8nExternalHooks) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [resource, operator] = eventName.split('.');
|
const store = useWebhooksStore();
|
||||||
|
|
||||||
if (window.n8nExternalHooks[resource]?.[operator]) {
|
const [resource, operator] = eventName.split('.') as [
|
||||||
const hookMethods = window.n8nExternalHooks[resource][operator];
|
keyof ExternalHooks,
|
||||||
|
keyof ExternalHooks[keyof ExternalHooks],
|
||||||
|
];
|
||||||
|
|
||||||
|
const context = window.n8nExternalHooks[resource] as ExternalHooksGenericContext;
|
||||||
|
if (context?.[operator]) {
|
||||||
|
const hookMethods = context[operator];
|
||||||
|
|
||||||
for (const hookMethod of hookMethods) {
|
for (const hookMethod of hookMethods) {
|
||||||
await hookMethod(store, metadata);
|
await hookMethod(store, metadata as IDataObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||||
import SettingsExternalSecrets from '@/views/SettingsExternalSecrets.vue';
|
import SettingsExternalSecrets from '@/views/SettingsExternalSecrets.vue';
|
||||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { useSettingsStore } from '@/stores';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { setupServer } from '@/__tests__/server';
|
import { setupServer } from '@/__tests__/server';
|
||||||
|
|
||||||
let pinia: ReturnType<typeof createTestingPinia>;
|
let pinia: ReturnType<typeof createTestingPinia>;
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
import type { IN8nUISettings } from 'n8n-workflow';
|
import { waitAllPromises } from '@/__tests__/utils';
|
||||||
import { SETTINGS_STORE_DEFAULT_STATE, waitAllPromises } from '@/__tests__/utils';
|
|
||||||
import SettingsPersonalView from '@/views/SettingsPersonalView.vue';
|
import SettingsPersonalView from '@/views/SettingsPersonalView.vue';
|
||||||
import { useSettingsStore } from '@/stores';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { setupServer } from '@/__tests__/server';
|
import { setupServer } from '@/__tests__/server';
|
||||||
|
@ -12,8 +11,6 @@ let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||||
let usersStore: ReturnType<typeof useUsersStore>;
|
let usersStore: ReturnType<typeof useUsersStore>;
|
||||||
let server: ReturnType<typeof setupServer>;
|
let server: ReturnType<typeof setupServer>;
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: IN8nUISettings = SETTINGS_STORE_DEFAULT_STATE.settings;
|
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(SettingsPersonalView);
|
const renderComponent = createComponentRenderer(SettingsPersonalView);
|
||||||
|
|
||||||
const currentUser = {
|
const currentUser = {
|
||||||
|
@ -26,6 +23,8 @@ const currentUser = {
|
||||||
isDefaultUser: false,
|
isDefaultUser: false,
|
||||||
isPendingUser: false,
|
isPendingUser: false,
|
||||||
isPending: false,
|
isPending: false,
|
||||||
|
hasRecoveryCodesLeft: false,
|
||||||
|
mfaEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('SettingsPersonalView', () => {
|
describe('SettingsPersonalView', () => {
|
||||||
|
|
Loading…
Reference in a new issue