refactor(editor): Migrate part of the vuex store to pinia (#4484)
* ✨ Added pinia support. Migrated community nodes module. * ✨ Added ui pinia store, moved some data from root store to it, updated modals to work with pinia stores * ✨ Added ui pinia store and migrated a part of the root store * ✨ Migrated `settings` store to pinia * ✨ Removing vuex store refs from router * ✨ Migrated `users` module to pinia store * ⚡ Fixing errors after sync with master * ⚡ One more error after merge * ⚡ Created `workflows` pinia store. Moved large part of root store to it. Started updating references. * ✨ Finished migrating workflows store to pinia * ⚡ Renaming some getters and actions to make more sense * ✨ Finished migrating the root store to pinia * ✨ Migrated ndv store to pinia * ⚡ Renaming main panel dimensions getter so it doesn't clash with data prop name * ✔️ Fixing lint errors * ✨ Migrated `templates` store to pinia * ✨ Migrated the `nodeTypes`store * ⚡ Removed unused pieces of code and oold vuex modules * ✨ Adding vuex calls to pinia store, fi xing wrong references * 💄 Removing leftover $store refs * ⚡ Added legacy getters and mutations to store to support webhooks * ⚡ Added missing front-end hooks, updated vuex state subscriptions to pinia * ✔️ Fixing linting errors * ⚡ Removing vue composition api plugin * ⚡ Fixing main sidebar state when loading node view * 🐛 Fixing an error when activating workflows * 🐛 Fixing isses with workflow settings and executions auto-refresh * 🐛 Removing duplicate listeners which cause import error * 🐛 Fixing route authentication * ⚡ Updating freshly pulled $store refs * Adding deleted const * ⚡ Updating store references in ee features. Reseting NodeView credentials update flag when resetting workspace * ⚡ Adding return type to email submission modal * ⚡ Making NodeView only react to paste event when active * 🐛 Fixing signup view errors * 👌 Addressing PR review comments * 👌 Addressing new PR comments * 👌 Updating invite id logic in signup view
This commit is contained in:
committed by
GitHub
parent
c2c7927414
commit
40e413d958
82
packages/editor-ui/src/stores/communityNodes.ts
Normal file
82
packages/editor-ui/src/stores/communityNodes.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { getInstalledCommunityNodes, installNewPackage, uninstallPackage, updatePackage } from "@/api/communityNodes";
|
||||
import { getAvailableCommunityPackageCount } from "@/api/settings";
|
||||
import { defineStore } from "pinia";
|
||||
import { useRootStore } from "./n8nRootStore";
|
||||
import { PublicInstalledPackage } from 'n8n-workflow';
|
||||
import Vue from "vue";
|
||||
import { CommunityNodesState, CommunityPackageMap } from "@/Interface";
|
||||
import { STORES } from "@/constants";
|
||||
|
||||
const LOADER_DELAY = 300;
|
||||
|
||||
export const useCommunityNodesStore = defineStore(STORES.COMMUNITY_NODES, {
|
||||
state: (): CommunityNodesState => ({
|
||||
// -1 means that package count has not been fetched yet
|
||||
availablePackageCount: -1,
|
||||
installedPackages: {},
|
||||
}),
|
||||
getters: {
|
||||
getInstalledPackages() : PublicInstalledPackage[] {
|
||||
return Object.values(this.installedPackages).sort((a, b) => a.packageName.localeCompare(b.packageName));
|
||||
},
|
||||
getInstalledPackageByName() {
|
||||
return (name: string): PublicInstalledPackage => this.installedPackages[name];
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async fetchAvailableCommunityPackageCount(): Promise<void> {
|
||||
if (this.availablePackageCount === -1) {
|
||||
this.availablePackageCount = await getAvailableCommunityPackageCount();
|
||||
}
|
||||
},
|
||||
async fetchInstalledPackages(): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
const installedPackages = await getInstalledCommunityNodes(rootStore.getRestApiContext);
|
||||
this.setInstalledPackages(installedPackages);
|
||||
const timeout = installedPackages.length > 0 ? 0: LOADER_DELAY;
|
||||
setTimeout(() => {
|
||||
return;
|
||||
}, timeout);
|
||||
},
|
||||
async installPackage(packageName: string): Promise<void> {
|
||||
try {
|
||||
const rootStore = useRootStore();
|
||||
await installNewPackage(rootStore.getRestApiContext, packageName);
|
||||
await this.fetchInstalledPackages();
|
||||
} catch (error) {
|
||||
throw (error);
|
||||
}
|
||||
},
|
||||
async uninstallPackage(packageName: string): Promise<void> {
|
||||
try {
|
||||
const rootStore = useRootStore();
|
||||
await uninstallPackage(rootStore.getRestApiContext, packageName);
|
||||
this.removePackageByName(packageName);
|
||||
} catch (error) {
|
||||
throw (error);
|
||||
}
|
||||
},
|
||||
async updatePackage(packageName: string): Promise<void> {
|
||||
try {
|
||||
const rootStore = useRootStore();
|
||||
const packageToUpdate: PublicInstalledPackage = this.getInstalledPackageByName(packageName);
|
||||
const updatedPackage: PublicInstalledPackage = await updatePackage(rootStore.getRestApiContext, packageToUpdate.packageName);
|
||||
this.updatePackageObject(updatedPackage);
|
||||
} catch (error) {
|
||||
throw (error);
|
||||
}
|
||||
},
|
||||
setInstalledPackages(packages: PublicInstalledPackage[]) {
|
||||
this.installedPackages = packages.reduce((packageMap: CommunityPackageMap, pack: PublicInstalledPackage) => {
|
||||
packageMap[pack.packageName] = pack;
|
||||
return packageMap;
|
||||
}, {});
|
||||
},
|
||||
removePackageByName(name: string): void {
|
||||
Vue.delete(this.installedPackages, name);
|
||||
},
|
||||
updatePackageObject(newPackage: PublicInstalledPackage) {
|
||||
this.installedPackages[newPackage.packageName] = newPackage;
|
||||
},
|
||||
},
|
||||
});
|
||||
109
packages/editor-ui/src/stores/n8nRootStore.ts
Normal file
109
packages/editor-ui/src/stores/n8nRootStore.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { STORES } from '@/constants';
|
||||
import { INodeUi, IRestApiContext, RootState } from '@/Interface';
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
import { defineStore } from 'pinia';
|
||||
import Vue from 'vue';
|
||||
import { useNodeTypesStore } from './nodeTypes';
|
||||
|
||||
export const useRootStore = defineStore(STORES.ROOT, {
|
||||
state: (): RootState => ({
|
||||
// @ts-ignore
|
||||
baseUrl: import.meta.env.VUE_APP_URL_BASE_API ? import.meta.env.VUE_APP_URL_BASE_API : (window.BASE_PATH === '/%BASE_PATH%/' ? '/' : window.BASE_PATH),
|
||||
defaultLocale: 'en',
|
||||
endpointWebhook: 'webhook',
|
||||
endpointWebhookTest: 'webhook-test',
|
||||
pushConnectionActive: true,
|
||||
timezone: 'America/New_York',
|
||||
executionTimeout: -1,
|
||||
maxExecutionTimeout: Number.MAX_SAFE_INTEGER,
|
||||
versionCli: '0.0.0',
|
||||
oauthCallbackUrls: {},
|
||||
n8nMetadata: {},
|
||||
sessionId: Math.random().toString(36).substring(2, 15),
|
||||
urlBaseWebhook: 'http://localhost:5678/',
|
||||
urlBaseEditor: 'http://localhost:5678',
|
||||
isNpmAvailable: false,
|
||||
instanceId: '',
|
||||
}),
|
||||
getters: {
|
||||
getWebhookUrl(): string {
|
||||
return `${this.urlBaseWebhook}${this.endpointWebhook}`;
|
||||
},
|
||||
|
||||
getWebhookTestUrl(): string {
|
||||
return `${this.urlBaseEditor}${this.endpointWebhookTest}`;
|
||||
},
|
||||
|
||||
getRestUrl(): string {
|
||||
let endpoint = 'rest';
|
||||
if (import.meta.env.VUE_APP_ENDPOINT_REST) {
|
||||
endpoint = import.meta.env.VUE_APP_ENDPOINT_REST;
|
||||
}
|
||||
return `${this.baseUrl}${endpoint}`;
|
||||
},
|
||||
|
||||
getRestApiContext(): IRestApiContext {
|
||||
let endpoint = 'rest';
|
||||
if (import.meta.env.VUE_APP_ENDPOINT_REST) {
|
||||
endpoint = import.meta.env.VUE_APP_ENDPOINT_REST;
|
||||
}
|
||||
return {
|
||||
baseUrl: `${this.baseUrl}${endpoint}`,
|
||||
sessionId: this.sessionId,
|
||||
};
|
||||
},
|
||||
// TODO: Waiting for nodeTypes store
|
||||
/**
|
||||
* Getter for node default names ending with a number: `'S3'`, `'Magento 2'`, etc.
|
||||
*/
|
||||
nativelyNumberSuffixedDefaults: (): string[] => {
|
||||
return useNodeTypesStore().allNodeTypes.reduce<string[]>((acc, cur) => {
|
||||
if (/\d$/.test(cur.defaults.name as string)) acc.push(cur.defaults.name as string);
|
||||
return acc;
|
||||
}, []);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setUrlBaseWebhook(urlBaseWebhook: string): void {
|
||||
const url = urlBaseWebhook.endsWith('/') ? urlBaseWebhook : `${urlBaseWebhook}/`;
|
||||
Vue.set(this, 'urlBaseWebhook', url);
|
||||
},
|
||||
setUrlBaseEditor(urlBaseEditor: string): void {
|
||||
const url = urlBaseEditor.endsWith('/') ? urlBaseEditor : `${urlBaseEditor}/`;
|
||||
Vue.set(this, 'urlBaseEditor', url);
|
||||
},
|
||||
setEndpointWebhook(endpointWebhook: string): void {
|
||||
Vue.set(this, 'endpointWebhook', endpointWebhook);
|
||||
},
|
||||
setEndpointWebhookTest(endpointWebhookTest: string): void {
|
||||
Vue.set(this, 'endpointWebhookTest', endpointWebhookTest);
|
||||
},
|
||||
setTimezone(timezone: string): void {
|
||||
Vue.set(this, 'timezone', timezone);
|
||||
},
|
||||
setExecutionTimeout(executionTimeout: number): void {
|
||||
Vue.set(this, 'executionTimeout', executionTimeout);
|
||||
},
|
||||
setMaxExecutionTimeout(maxExecutionTimeout: number): void {
|
||||
Vue.set(this, 'maxExecutionTimeout', maxExecutionTimeout);
|
||||
},
|
||||
setVersionCli(version: string): void {
|
||||
Vue.set(this, 'versionCli', version);
|
||||
},
|
||||
setInstanceId(instanceId: string): void {
|
||||
Vue.set(this, 'instanceId', instanceId);
|
||||
},
|
||||
setOauthCallbackUrls(urls: IDataObject): void {
|
||||
Vue.set(this, 'oauthCallbackUrls', urls);
|
||||
},
|
||||
setN8nMetadata(metadata: IDataObject): void {
|
||||
Vue.set(this, 'n8nMetadata', metadata);
|
||||
},
|
||||
setDefaultLocale(locale: string): void {
|
||||
Vue.set(this, 'defaultLocale', locale);
|
||||
},
|
||||
setIsNpmAvailable(isNpmAvailable: boolean): void {
|
||||
Vue.set(this, 'isNpmAvailable', isNpmAvailable);
|
||||
},
|
||||
},
|
||||
});
|
||||
180
packages/editor-ui/src/stores/ndv.ts
Normal file
180
packages/editor-ui/src/stores/ndv.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { STORES } from "@/constants";
|
||||
import { INodeUi, IRunDataDisplayMode, NDVState, XYPosition } from "@/Interface";
|
||||
import { IRunData } from "n8n-workflow";
|
||||
import { defineStore } from "pinia";
|
||||
import Vue from "vue";
|
||||
import { useWorkflowsStore } from "./workflows";
|
||||
|
||||
export const useNDVStore = defineStore(STORES.NDV, {
|
||||
state: (): NDVState => ({
|
||||
activeNodeName: null,
|
||||
mainPanelDimensions: {},
|
||||
sessionId: '',
|
||||
input: {
|
||||
displayMode: 'table',
|
||||
nodeName: undefined,
|
||||
run: undefined,
|
||||
branch: undefined,
|
||||
data: {
|
||||
isEmpty: true,
|
||||
},
|
||||
},
|
||||
output: {
|
||||
displayMode: 'table',
|
||||
branch: undefined,
|
||||
data: {
|
||||
isEmpty: true,
|
||||
},
|
||||
editMode: {
|
||||
enabled: false,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
focusedMappableInput: '',
|
||||
mappingTelemetry: {},
|
||||
hoveringItem: null,
|
||||
draggable: {
|
||||
isDragging: false,
|
||||
type: '',
|
||||
data: '',
|
||||
canDrop: false,
|
||||
stickyPosition: null,
|
||||
},
|
||||
}),
|
||||
getters: {
|
||||
activeNode(): INodeUi | null {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
return workflowsStore.getNodeByName(this.activeNodeName || '');
|
||||
},
|
||||
ndvInputData(): IRunData[] {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const executionData = workflowsStore.getWorkflowExecution;
|
||||
const inputNodeName: string | undefined = this.input.nodeName;
|
||||
const inputRunIndex: number = this.input.run ?? 0;
|
||||
const inputBranchIndex: number = this.input.branch?? 0;
|
||||
|
||||
if (!executionData || !inputNodeName || inputRunIndex === undefined || inputBranchIndex === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return executionData.data?.resultData?.runData?.[inputNodeName]?.[inputRunIndex]?.data?.main?.[inputBranchIndex];
|
||||
},
|
||||
getPanelDisplayMode() {
|
||||
return (panel: 'input' | 'output') => this[panel].displayMode;
|
||||
},
|
||||
inputPanelDisplayMode(): IRunDataDisplayMode {
|
||||
return this.input.displayMode;
|
||||
},
|
||||
outputPanelDisplayMode(): IRunDataDisplayMode {
|
||||
return this.output.displayMode;
|
||||
},
|
||||
isDraggableDragging(): boolean {
|
||||
return this.draggable.isDragging;
|
||||
},
|
||||
draggableType(): string {
|
||||
return this.draggable.type;
|
||||
},
|
||||
draggableData(): string {
|
||||
return this.draggable.data;
|
||||
},
|
||||
canDraggableDrop(): boolean {
|
||||
return this.draggable.canDrop;
|
||||
},
|
||||
outputPanelEditMode(): NDVState['output']['editMode'] {
|
||||
return this.output.editMode;
|
||||
},
|
||||
getMainPanelDimensions() {
|
||||
return (panelType: string) => {
|
||||
const defaults = { relativeRight: 1, relativeLeft: 1, relativeWidth: 1 };
|
||||
return {...defaults, ...this.mainPanelDimensions[panelType]};
|
||||
};
|
||||
},
|
||||
draggableStickyPos(): XYPosition | null {
|
||||
return this.draggable.stickyPosition;
|
||||
},
|
||||
ndvInputNodeName(): string | undefined {
|
||||
return this.input.nodeName;
|
||||
},
|
||||
ndvInputRunIndex(): number | undefined {
|
||||
return this.input.run;
|
||||
},
|
||||
ndvInputBranchIndex(): number | undefined {
|
||||
return this.input.branch;
|
||||
},
|
||||
isDNVDataEmpty() {
|
||||
return (panel: 'input' | 'output'): boolean => this[panel].data.isEmpty;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setInputNodeName(name: string | undefined): void {
|
||||
Vue.set(this.input, 'nodeName', name);
|
||||
},
|
||||
setInputRunIndex(run?: string): void {
|
||||
Vue.set(this.input, 'run', run);
|
||||
},
|
||||
setMainPanelDimensions(params: { panelType:string, dimensions: { relativeLeft?: number, relativeRight?: number, relativeWidth?: number }}): void {
|
||||
Vue.set(
|
||||
this.mainPanelDimensions,
|
||||
params.panelType,
|
||||
{...this.mainPanelDimensions[params.panelType], ...params.dimensions },
|
||||
);
|
||||
},
|
||||
setNDVSessionId(): void {
|
||||
Vue.set(this, 'sessionId', `ndv-${Math.random().toString(36).slice(-8)}`);
|
||||
},
|
||||
resetNDVSessionId(): void {
|
||||
Vue.set(this, 'sessionId', '');
|
||||
},
|
||||
setPanelDisplayMode(params: {pane: 'input' | 'output', mode: IRunDataDisplayMode}): void {
|
||||
Vue.set(this[params.pane], 'displayMode', params.mode);
|
||||
},
|
||||
setOutputPanelEditModeEnabled(isEnabled: boolean): void {
|
||||
Vue.set(this.output.editMode, 'enabled', isEnabled);
|
||||
},
|
||||
setOutputPanelEditModeValue(payload: string): void {
|
||||
Vue.set(this.output.editMode, 'value', payload);
|
||||
},
|
||||
setMappableNDVInputFocus(paramName: string): void {
|
||||
Vue.set(this, 'focusedMappableInput', paramName);
|
||||
},
|
||||
draggableStartDragging({type, data}: {type: string, data: string}): void {
|
||||
this.draggable = {
|
||||
isDragging: true,
|
||||
type,
|
||||
data,
|
||||
canDrop: false,
|
||||
stickyPosition: null,
|
||||
};
|
||||
},
|
||||
draggableStopDragging(): void {
|
||||
this.draggable = {
|
||||
isDragging: false,
|
||||
type: '',
|
||||
data: '',
|
||||
canDrop: false,
|
||||
stickyPosition: null,
|
||||
};
|
||||
},
|
||||
setDraggableStickyPos(position: XYPosition | null): void {
|
||||
Vue.set(this.draggable, 'stickyPosition', position);
|
||||
},
|
||||
setDraggableCanDrop(canDrop: boolean): void {
|
||||
Vue.set(this.draggable, 'canDrop', canDrop);
|
||||
},
|
||||
setMappingTelemetry(telemetry: {[key: string]: string | number | boolean}): void {
|
||||
this.mappingTelemetry = {...this.mappingTelemetry, ...telemetry};
|
||||
},
|
||||
resetMappingTelemetry(): void {
|
||||
this.mappingTelemetry = {};
|
||||
},
|
||||
setHoveringItem(item: null | NDVState['hoveringItem']): void {
|
||||
Vue.set(this, 'hoveringItem', item);
|
||||
},
|
||||
setNDVBranchIndex(e: {pane: 'input' | 'output', branchIndex: number}): void {
|
||||
Vue.set(this[e.pane], 'branch', e.branchIndex);
|
||||
},
|
||||
setNDVPanelDataIsEmpty(payload: {panel: 'input' | 'output', isEmpty: boolean}): void {
|
||||
Vue.set(this[payload.panel].data, 'isEmpty', payload.isEmpty);
|
||||
},
|
||||
},
|
||||
});
|
||||
157
packages/editor-ui/src/stores/nodeTypes.ts
Normal file
157
packages/editor-ui/src/stores/nodeTypes.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { getNodeParameterOptions, getNodesInformation, getNodeTranslationHeaders, getNodeTypes, getResourceLocatorResults } from "@/api/nodeTypes";
|
||||
import { DEFAULT_NODETYPE_VERSION, STORES } from "@/constants";
|
||||
import { ICategoriesWithNodes, INodeCreateElement, INodeTypesState, IResourceLocatorReqParams } from "@/Interface";
|
||||
import { getCategoriesWithNodes, getCategorizedList } from "@/modules/nodeTypesHelpers";
|
||||
import { addHeaders, addNodeTranslation } from "@/plugins/i18n";
|
||||
import { store } from "@/store";
|
||||
import { omit } from "@/utils";
|
||||
import { ILoadOptions, INodeCredentials, INodeListSearchResult, INodeParameters, INodePropertyOptions, INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow';
|
||||
import { defineStore } from "pinia";
|
||||
import Vue from "vue";
|
||||
import { useRootStore } from "./n8nRootStore";
|
||||
import { useUsersStore } from "./users";
|
||||
|
||||
function getNodeVersions(nodeType: INodeTypeDescription) {
|
||||
return Array.isArray(nodeType.version) ? nodeType.version : [nodeType.version];
|
||||
}
|
||||
|
||||
export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
|
||||
state: (): INodeTypesState => ({
|
||||
nodeTypes: {},
|
||||
}),
|
||||
getters: {
|
||||
allNodeTypes(): INodeTypeDescription[] {
|
||||
return Object.values(this.nodeTypes).reduce<INodeTypeDescription[]>((allNodeTypes, nodeType) => {
|
||||
const versionNumbers = Object.keys(nodeType).map(Number);
|
||||
const allNodeVersions = versionNumbers.map(version => nodeType[version]);
|
||||
|
||||
return [...allNodeTypes, ...allNodeVersions];
|
||||
}, []);
|
||||
},
|
||||
allLatestNodeTypes(): INodeTypeDescription[] {
|
||||
return Object.values(this.nodeTypes).reduce<INodeTypeDescription[]>((allLatestNodeTypes, nodeVersions) => {
|
||||
const versionNumbers = Object.keys(nodeVersions).map(Number);
|
||||
const latestNodeVersion = nodeVersions[Math.max(...versionNumbers)];
|
||||
|
||||
if (!latestNodeVersion) return allLatestNodeTypes;
|
||||
|
||||
return [...allLatestNodeTypes, latestNodeVersion];
|
||||
}, []);
|
||||
},
|
||||
getNodeType() {
|
||||
return (nodeTypeName: string, version?: number): INodeTypeDescription | null => {
|
||||
const nodeVersions = this.nodeTypes[nodeTypeName];
|
||||
|
||||
if (!nodeVersions) return null;
|
||||
|
||||
const versionNumbers = Object.keys(nodeVersions).map(Number);
|
||||
const nodeType = nodeVersions[version || Math.max(...versionNumbers)];
|
||||
|
||||
return nodeType || null;
|
||||
};
|
||||
},
|
||||
isTriggerNode() {
|
||||
return (nodeTypeName: string) => {
|
||||
const nodeType = this.getNodeType(nodeTypeName);
|
||||
return !!(nodeType && nodeType.group.includes('trigger'));
|
||||
};
|
||||
},
|
||||
visibleNodeTypes(): INodeTypeDescription[] {
|
||||
return this.allLatestNodeTypes.filter((nodeType: INodeTypeDescription) => !nodeType.hidden);
|
||||
},
|
||||
categoriesWithNodes(): ICategoriesWithNodes {
|
||||
const usersStore = useUsersStore();
|
||||
return getCategoriesWithNodes(this.visibleNodeTypes, usersStore.personalizedNodeTypes);
|
||||
},
|
||||
categorizedItems(): INodeCreateElement[] {
|
||||
return getCategorizedList(this.categoriesWithNodes);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setNodeTypes(newNodeTypes: INodeTypeDescription[] = []): void {
|
||||
const nodeTypes = newNodeTypes.reduce<Record<string, Record<string, INodeTypeDescription>>>((acc, newNodeType) => {
|
||||
const newNodeVersions = getNodeVersions(newNodeType);
|
||||
|
||||
if (newNodeVersions.length === 0) {
|
||||
const singleVersion = { [DEFAULT_NODETYPE_VERSION]: newNodeType };
|
||||
|
||||
acc[newNodeType.name] = singleVersion;
|
||||
return acc;
|
||||
}
|
||||
|
||||
for (const version of newNodeVersions) {
|
||||
if (acc[newNodeType.name]) {
|
||||
acc[newNodeType.name][version] = newNodeType;
|
||||
} else {
|
||||
acc[newNodeType.name] = { [version]: newNodeType };
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, { ...this.nodeTypes });
|
||||
|
||||
Vue.set(this, 'nodeTypes', nodeTypes);
|
||||
},
|
||||
removeNodeTypes(nodeTypesToRemove: INodeTypeDescription[]): void {
|
||||
this.nodeTypes = nodeTypesToRemove.reduce(
|
||||
(oldNodes, newNodeType) => omit(newNodeType.name, oldNodes),
|
||||
this.nodeTypes,
|
||||
);
|
||||
},
|
||||
async getNodesInformation(nodeInfos: INodeTypeNameVersion[]): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
const nodesInformation = await getNodesInformation(rootStore.getRestApiContext, nodeInfos);
|
||||
|
||||
nodesInformation.forEach(nodeInformation => {
|
||||
if (nodeInformation.translation) {
|
||||
const nodeType = nodeInformation.name.replace('n8n-nodes-base.', '');
|
||||
|
||||
addNodeTranslation(
|
||||
{ [nodeType]: nodeInformation.translation },
|
||||
rootStore.defaultLocale,
|
||||
);
|
||||
}
|
||||
});
|
||||
this.setNodeTypes(nodesInformation);
|
||||
},
|
||||
async getFullNodesProperties(nodesToBeFetched: INodeTypeNameVersion[]): Promise<void> {
|
||||
const vuexStore = store;
|
||||
vuexStore.dispatch('credentials/fetchCredentialTypes', true);
|
||||
await this.getNodesInformation(nodesToBeFetched);
|
||||
},
|
||||
async getNodeTypes(): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
const nodeTypes = await getNodeTypes(rootStore.getRestApiContext);
|
||||
if (nodeTypes.length) {
|
||||
this.setNodeTypes(nodeTypes);
|
||||
}
|
||||
},
|
||||
async getNodeTranslationHeaders(): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
const headers = await getNodeTranslationHeaders(rootStore.getRestApiContext);
|
||||
|
||||
if (headers) {
|
||||
addHeaders(headers, rootStore.defaultLocale);
|
||||
}
|
||||
},
|
||||
async getNodeParameterOptions(
|
||||
sendData: {
|
||||
nodeTypeAndVersion: INodeTypeNameVersion,
|
||||
path: string,
|
||||
methodName?: string,
|
||||
loadOptions?: ILoadOptions,
|
||||
currentNodeParameters: INodeParameters,
|
||||
credentials?: INodeCredentials,
|
||||
},
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const rootStore = useRootStore();
|
||||
return getNodeParameterOptions(rootStore.getRestApiContext, sendData);
|
||||
},
|
||||
async getResourceLocatorResults(
|
||||
sendData: IResourceLocatorReqParams,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const rootStore = useRootStore();
|
||||
return getResourceLocatorResults(rootStore.getRestApiContext, sendData);
|
||||
},
|
||||
},
|
||||
});
|
||||
235
packages/editor-ui/src/stores/settings.ts
Normal file
235
packages/editor-ui/src/stores/settings.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { createApiKey, deleteApiKey, getApiKey } from "@/api/api-keys";
|
||||
import { getPromptsData, getSettings, submitContactInfo, submitValueSurvey } from "@/api/settings";
|
||||
import { testHealthEndpoint } from "@/api/templates";
|
||||
import { CONTACT_PROMPT_MODAL_KEY, EnterpriseEditionFeature, STORES, VALUE_SURVEY_MODAL_KEY } from "@/constants";
|
||||
import { ILogLevel, IN8nPromptResponse, IN8nPrompts, IN8nUISettings, IN8nValueSurveyData, ISettingsState, WorkflowCallerPolicyDefaultOption } from "@/Interface";
|
||||
import { store } from "@/store";
|
||||
import { ITelemetrySettings } from "n8n-workflow";
|
||||
import { defineStore } from "pinia";
|
||||
import Vue from "vue";
|
||||
import { useRootStore } from "./n8nRootStore";
|
||||
import { useUIStore } from "./ui";
|
||||
import { useUsersStore } from "./users";
|
||||
|
||||
export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
||||
state: (): ISettingsState => ({
|
||||
settings: {} as IN8nUISettings,
|
||||
promptsData: {} as IN8nPrompts,
|
||||
userManagement: {
|
||||
enabled: false,
|
||||
showSetupOnFirstLoad: false,
|
||||
smtpSetup: false,
|
||||
},
|
||||
templatesEndpointHealthy: false,
|
||||
api: {
|
||||
enabled: false,
|
||||
latestVersion: 0,
|
||||
path: '/',
|
||||
},
|
||||
onboardingCallPromptEnabled: false,
|
||||
saveDataErrorExecution: 'all',
|
||||
saveDataSuccessExecution: 'all',
|
||||
saveManualExecutions: false,
|
||||
}),
|
||||
getters: {
|
||||
isEnterpriseFeatureEnabled() {
|
||||
return (feature: EnterpriseEditionFeature) : boolean => this.settings.enterprise[feature];
|
||||
},
|
||||
versionCli(): string {
|
||||
return this.settings.versionCli;
|
||||
},
|
||||
isUserManagementEnabled(): boolean {
|
||||
return this.userManagement.enabled;
|
||||
},
|
||||
isPublicApiEnabled(): boolean {
|
||||
return this.api.enabled;
|
||||
},
|
||||
publicApiLatestVersion(): number {
|
||||
return this.api.latestVersion;
|
||||
},
|
||||
publicApiPath(): string {
|
||||
return this.api.path;
|
||||
},
|
||||
showSetupPage() : boolean {
|
||||
return this.userManagement.showSetupOnFirstLoad === true;
|
||||
},
|
||||
isDesktopDeployment() : boolean {
|
||||
if (!this.settings.deployment) {
|
||||
return false;
|
||||
}
|
||||
return this.settings.deployment?.type.startsWith('desktop_');
|
||||
},
|
||||
isCloudDeployment() : boolean {
|
||||
if (!this.settings.deployment) {
|
||||
return false;
|
||||
}
|
||||
return this.settings.deployment.type === 'cloud';
|
||||
},
|
||||
isSmtpSetup() : boolean {
|
||||
return this.userManagement.smtpSetup;
|
||||
},
|
||||
isPersonalizationSurveyEnabled() : boolean {
|
||||
return (this.settings.telemetry && this.settings.telemetry.enabled) && this.settings.personalizationSurveyEnabled;
|
||||
},
|
||||
telemetry() : ITelemetrySettings {
|
||||
return this.settings.telemetry;
|
||||
},
|
||||
logLevel() : ILogLevel {
|
||||
return this.settings.logLevel;
|
||||
},
|
||||
isTelemetryEnabled() : boolean {
|
||||
return this.settings.telemetry && this.settings.telemetry.enabled;
|
||||
},
|
||||
areTagsEnabled() : boolean {
|
||||
return this.settings.workflowTagsDisabled !== undefined ? !this.settings.workflowTagsDisabled : true;
|
||||
},
|
||||
isHiringBannerEnabled() : boolean {
|
||||
return this.settings.hiringBannerEnabled;
|
||||
},
|
||||
isTemplatesEnabled(): boolean {
|
||||
return Boolean(this.settings.templates && this.settings.templates.enabled);
|
||||
},
|
||||
isTemplatesEndpointReachable() : boolean {
|
||||
return this.templatesEndpointHealthy;
|
||||
},
|
||||
templatesHost() : string {
|
||||
return this.settings.templates.host;
|
||||
},
|
||||
isCommunityNodesFeatureEnabled() : boolean {
|
||||
return this.settings.communityNodesEnabled;
|
||||
},
|
||||
isNpmAvailable() : boolean {
|
||||
return this.settings.isNpmAvailable;
|
||||
},
|
||||
allowedModules() : { builtIn?: string[]; external?: string[] } {
|
||||
return this.settings.allowedModules;
|
||||
},
|
||||
isQueueModeEnabled(): boolean {
|
||||
return this.settings.executionMode === 'queue';
|
||||
},
|
||||
isWorkflowSharingEnabled(): boolean {
|
||||
return this.settings.isWorkflowSharingEnabled;
|
||||
},
|
||||
workflowCallerPolicyDefaultOption(): WorkflowCallerPolicyDefaultOption {
|
||||
return this.settings.workflowCallerPolicyDefaultOption;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setSettings(settings: IN8nUISettings): void {
|
||||
|
||||
this.settings = settings;
|
||||
this.userManagement.enabled = settings.userManagement.enabled;
|
||||
this.userManagement.showSetupOnFirstLoad = !!settings.userManagement.showSetupOnFirstLoad;
|
||||
this.userManagement.smtpSetup = settings.userManagement.smtpSetup;
|
||||
this.api.enabled = settings.publicApi.enabled;
|
||||
this.api.latestVersion = settings.publicApi.latestVersion;
|
||||
this.api.path = settings.publicApi.path;
|
||||
this.onboardingCallPromptEnabled = settings.onboardingCallPromptEnabled;
|
||||
},
|
||||
async getSettings(): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
const settings = await getSettings(rootStore.getRestApiContext);
|
||||
const vuexStore = store;
|
||||
|
||||
this.setSettings(settings);
|
||||
this.settings.communityNodesEnabled = settings.communityNodesEnabled;
|
||||
this.setAllowedModules(settings.allowedModules as { builtIn?: string, external?: string });
|
||||
this.setSaveDataErrorExecution(settings.saveDataErrorExecution);
|
||||
this.setSaveDataSuccessExecution(settings.saveDataSuccessExecution);
|
||||
this.setSaveManualExecutions(settings.saveManualExecutions);
|
||||
|
||||
rootStore.setUrlBaseWebhook(settings.urlBaseWebhook);
|
||||
rootStore.setUrlBaseEditor(settings.urlBaseEditor);
|
||||
rootStore.setEndpointWebhook(settings.endpointWebhook);
|
||||
rootStore.setEndpointWebhookTest(settings.endpointWebhookTest);
|
||||
rootStore.setTimezone(settings.timezone);
|
||||
rootStore.setExecutionTimeout(settings.executionTimeout);
|
||||
rootStore.setMaxExecutionTimeout(settings.maxExecutionTimeout);
|
||||
rootStore.setVersionCli(settings.versionCli);
|
||||
rootStore.setInstanceId(settings.instanceId);
|
||||
rootStore.setOauthCallbackUrls(settings.oauthCallbackUrls);
|
||||
rootStore.setN8nMetadata(settings.n8nMetadata || {});
|
||||
rootStore.setDefaultLocale(settings.defaultLocale);
|
||||
rootStore.setIsNpmAvailable(settings.isNpmAvailable);
|
||||
vuexStore.commit('versions/setVersionNotificationSettings', settings.versionNotifications, {root: true});
|
||||
},
|
||||
stopShowingSetupPage(): void {
|
||||
Vue.set(this.userManagement, 'showSetupOnFirstLoad', false);
|
||||
|
||||
},
|
||||
setPromptsData(promptsData: IN8nPrompts): void {
|
||||
Vue.set(this, 'promptsData', promptsData);
|
||||
},
|
||||
setAllowedModules(allowedModules: { builtIn?: string, external?: string }): void {
|
||||
this.settings.allowedModules = {
|
||||
...(allowedModules.builtIn && { builtIn: allowedModules.builtIn.split(',') }),
|
||||
...(allowedModules.external && { external: allowedModules.external.split(',') }),
|
||||
};
|
||||
},
|
||||
async fetchPromptsData(): Promise<void> {
|
||||
if (!this.isTelemetryEnabled) {
|
||||
Promise.resolve();
|
||||
}
|
||||
try {
|
||||
const uiStore = useUIStore();
|
||||
const usersStore = useUsersStore();
|
||||
const promptsData: IN8nPrompts = await getPromptsData(this.settings.instanceId, usersStore.currentUserId || '');
|
||||
|
||||
if (promptsData && promptsData.showContactPrompt) {
|
||||
uiStore.openModal(CONTACT_PROMPT_MODAL_KEY);
|
||||
} else if (promptsData && promptsData.showValueSurvey) {
|
||||
uiStore.openModal(VALUE_SURVEY_MODAL_KEY);
|
||||
}
|
||||
|
||||
this.setPromptsData(promptsData);
|
||||
Promise.resolve();
|
||||
} catch (error) {
|
||||
Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async submitContactInfo(email: string): Promise<IN8nPromptResponse | undefined> {
|
||||
try {
|
||||
const usersStore = useUsersStore();
|
||||
return await submitContactInfo(this.settings.instanceId, usersStore.currentUserId || '', email);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
async submitValueSurvey(params: IN8nValueSurveyData): Promise<IN8nPromptResponse | undefined> {
|
||||
try {
|
||||
const usersStore = useUsersStore();
|
||||
return await submitValueSurvey(this.settings.instanceId, usersStore.currentUserId || '', params);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
async testTemplatesEndpoint(): Promise<void> {
|
||||
const timeout = new Promise((_, reject) => setTimeout(() => reject(), 2000));
|
||||
await Promise.race([testHealthEndpoint(this.templatesHost), timeout]);
|
||||
this.templatesEndpointHealthy = true;
|
||||
},
|
||||
async getApiKey(): Promise<string | null> {
|
||||
const rootStore = useRootStore();
|
||||
const { apiKey } = await getApiKey(rootStore.getRestApiContext);
|
||||
return apiKey;
|
||||
},
|
||||
async createApiKey(): Promise<string | null> {
|
||||
const rootStore = useRootStore();
|
||||
const { apiKey } = await createApiKey(rootStore.getRestApiContext);
|
||||
return apiKey;
|
||||
},
|
||||
async deleteApiKey(): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
await deleteApiKey(rootStore.getRestApiContext);
|
||||
},
|
||||
setSaveDataErrorExecution(newValue: string) {
|
||||
Vue.set(this, 'saveDataErrorExecution', newValue);
|
||||
},
|
||||
setSaveDataSuccessExecution(newValue: string) {
|
||||
Vue.set(this, 'saveDataSuccessExecution', newValue);
|
||||
},
|
||||
setSaveManualExecutions(saveManualExecutions: boolean) {
|
||||
Vue.set(this, 'saveManualExecutions', saveManualExecutions);
|
||||
},
|
||||
},
|
||||
});
|
||||
267
packages/editor-ui/src/stores/templates.ts
Normal file
267
packages/editor-ui/src/stores/templates.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { STORES } from '@/constants';
|
||||
import { ITemplatesCategory, ITemplatesCollection, ITemplatesCollectionFull, ITemplatesQuery, ITemplateState, ITemplatesWorkflow, ITemplatesWorkflowFull, IWorkflowTemplate } from "@/Interface";
|
||||
import Vue from "vue";
|
||||
import { useSettingsStore } from "./settings";
|
||||
import { getCategories, getCollectionById, getCollections, getTemplateById, getWorkflows, getWorkflowTemplate } from "@/api/templates";
|
||||
|
||||
const TEMPLATES_PAGE_SIZE = 10;
|
||||
|
||||
function getSearchKey(query: ITemplatesQuery): string {
|
||||
return JSON.stringify([query.search || '', [...query.categories].sort()]);
|
||||
}
|
||||
|
||||
export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
|
||||
state: (): ITemplateState => ({
|
||||
categories: {},
|
||||
collections: {},
|
||||
workflows: {},
|
||||
collectionSearches: {},
|
||||
workflowSearches: {},
|
||||
currentSessionId: '',
|
||||
previousSessionId: '',
|
||||
}),
|
||||
getters: {
|
||||
allCategories(): ITemplatesCategory[] {
|
||||
return Object.values(this.categories).sort((a: ITemplatesCategory, b: ITemplatesCategory) => a.name > b.name ? 1: -1);
|
||||
},
|
||||
getTemplateById() {
|
||||
return (id: string): null | ITemplatesWorkflow => this.workflows[id];
|
||||
},
|
||||
getCollectionById() {
|
||||
return (id: string): null | ITemplatesCollection => this.collections[id];
|
||||
},
|
||||
getCategoryById() {
|
||||
return (id: string): null | ITemplatesCategory => this.categories[id];
|
||||
},
|
||||
getSearchedCollections() {
|
||||
return (query: ITemplatesQuery) => {
|
||||
const searchKey = getSearchKey(query);
|
||||
const search = this.collectionSearches[searchKey];
|
||||
if (!search) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return search.collectionIds.map((collectionId: string) => this.collections[collectionId]);
|
||||
};
|
||||
},
|
||||
getSearchedWorkflows() {
|
||||
return (query: ITemplatesQuery) => {
|
||||
const searchKey = getSearchKey(query);
|
||||
const search = this.workflowSearches[searchKey];
|
||||
if (!search) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return search.workflowIds.map((workflowId: string) => this.workflows[workflowId]);
|
||||
};
|
||||
},
|
||||
getSearchedWorkflowsTotal() {
|
||||
return (query: ITemplatesQuery) => {
|
||||
const searchKey = getSearchKey(query);
|
||||
const search = this.workflowSearches[searchKey];
|
||||
|
||||
return search ? search.totalWorkflows : 0;
|
||||
};
|
||||
},
|
||||
isSearchLoadingMore() {
|
||||
return (query: ITemplatesQuery) => {
|
||||
const searchKey = getSearchKey(query);
|
||||
const search = this.workflowSearches[searchKey];
|
||||
|
||||
return Boolean(search && search.loadingMore);
|
||||
};
|
||||
},
|
||||
isSearchFinished() {
|
||||
return (query: ITemplatesQuery) => {
|
||||
const searchKey = getSearchKey(query);
|
||||
const search = this.workflowSearches[searchKey];
|
||||
|
||||
return Boolean(search && !search.loadingMore && search.totalWorkflows === search.workflowIds.length);
|
||||
};
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
addCategories(categories: ITemplatesCategory[]): void {
|
||||
categories.forEach((category: ITemplatesCategory) => {
|
||||
Vue.set(this.categories, category.id, category);
|
||||
});
|
||||
},
|
||||
addCollections(collections: Array<ITemplatesCollection | ITemplatesCollectionFull>): void {
|
||||
collections.forEach((collection) => {
|
||||
const workflows = (collection.workflows || []).map((workflow) => ({id: workflow.id}));
|
||||
const cachedCollection = this.collections[collection.id] || {};
|
||||
Vue.set(this.collections, collection.id, {
|
||||
...cachedCollection,
|
||||
...collection,
|
||||
workflows,
|
||||
});
|
||||
});
|
||||
},
|
||||
addWorkflows(workflows: Array<ITemplatesWorkflow | ITemplatesWorkflowFull>): void {
|
||||
workflows.forEach((workflow: ITemplatesWorkflow) => {
|
||||
const cachedWorkflow = this.workflows[workflow.id] || {};
|
||||
Vue.set(this.workflows, workflow.id, {
|
||||
...cachedWorkflow,
|
||||
...workflow,
|
||||
});
|
||||
});
|
||||
},
|
||||
addCollectionSearch(data: {collections: ITemplatesCollection[], query: ITemplatesQuery}): void {
|
||||
const collectionIds = data.collections.map((collection) => collection.id);
|
||||
const searchKey = getSearchKey(data.query);
|
||||
Vue.set(this.collectionSearches, searchKey, {
|
||||
collectionIds,
|
||||
});
|
||||
},
|
||||
addWorkflowsSearch(data: {totalWorkflows: number; workflows: ITemplatesWorkflow[], query: ITemplatesQuery}): void {
|
||||
const workflowIds = data.workflows.map((workflow) => workflow.id);
|
||||
const searchKey = getSearchKey(data.query);
|
||||
const cachedResults = this.workflowSearches[searchKey];
|
||||
if (!cachedResults) {
|
||||
Vue.set(this.workflowSearches, searchKey, {
|
||||
workflowIds,
|
||||
totalWorkflows: data.totalWorkflows,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Vue.set(this.workflowSearches, searchKey, {
|
||||
workflowIds: [...cachedResults.workflowIds, ...workflowIds],
|
||||
totalWorkflows: data.totalWorkflows,
|
||||
});
|
||||
},
|
||||
setWorkflowSearchLoading(query: ITemplatesQuery): void {
|
||||
const searchKey = getSearchKey(query);
|
||||
const cachedResults = this.workflowSearches[searchKey];
|
||||
if (!cachedResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vue.set(this.workflowSearches[searchKey], 'loadingMore', true);
|
||||
},
|
||||
setWorkflowSearchLoaded(query: ITemplatesQuery): void {
|
||||
const searchKey = getSearchKey(query);
|
||||
const cachedResults = this.workflowSearches[searchKey];
|
||||
if (!cachedResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vue.set(this.workflowSearches[searchKey], 'loadingMore', false);
|
||||
},
|
||||
resetSessionId(): void {
|
||||
this.previousSessionId = this.currentSessionId;
|
||||
this.currentSessionId = '';
|
||||
},
|
||||
setSessionId(): void {
|
||||
if (!this.currentSessionId) {
|
||||
this.currentSessionId = `templates-${Date.now()}`;
|
||||
}
|
||||
},
|
||||
async fetchTemplateById(templateId: string): Promise<ITemplatesWorkflow | ITemplatesWorkflowFull> {
|
||||
const settingsStore = useSettingsStore();
|
||||
const apiEndpoint: string = settingsStore.templatesHost;
|
||||
const versionCli: string = settingsStore.versionCli;
|
||||
const response = await getTemplateById(apiEndpoint, templateId, { 'n8n-version': versionCli });
|
||||
|
||||
const template: ITemplatesWorkflowFull = {
|
||||
...response.workflow,
|
||||
full: true,
|
||||
};
|
||||
this.addWorkflows([template]);
|
||||
|
||||
return template;
|
||||
},
|
||||
async fetchCollectionById(collectionId: string): Promise<ITemplatesCollection | null> {
|
||||
const settingsStore = useSettingsStore();
|
||||
const apiEndpoint: string = settingsStore.templatesHost;
|
||||
const versionCli: string = settingsStore.versionCli;
|
||||
const response = await getCollectionById(apiEndpoint, collectionId, { 'n8n-version': versionCli });
|
||||
const collection: ITemplatesCollectionFull = {
|
||||
...response.collection,
|
||||
full: true,
|
||||
};
|
||||
|
||||
this.addCollections([collection]);
|
||||
this.addWorkflows(response.collection.workflows);
|
||||
return this.getCollectionById(collectionId);
|
||||
},
|
||||
async getCategories(): Promise<ITemplatesCategory[]> {
|
||||
const cachedCategories = this.allCategories;
|
||||
if (cachedCategories.length) {
|
||||
return cachedCategories;
|
||||
}
|
||||
const settingsStore = useSettingsStore();
|
||||
const apiEndpoint: string = settingsStore.templatesHost;
|
||||
const versionCli: string = settingsStore.versionCli;
|
||||
const response = await getCategories(apiEndpoint, { 'n8n-version': versionCli });
|
||||
const categories = response.categories;
|
||||
|
||||
this.addCategories(categories);
|
||||
return categories;
|
||||
},
|
||||
async getCollections(query: ITemplatesQuery): Promise<ITemplatesCollection[]> {
|
||||
const cachedResults = this.getSearchedCollections(query);
|
||||
if (cachedResults) {
|
||||
return cachedResults;
|
||||
}
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const apiEndpoint: string = settingsStore.templatesHost;
|
||||
const versionCli: string = settingsStore.versionCli;
|
||||
const response = await getCollections(apiEndpoint, query, { 'n8n-version': versionCli });
|
||||
const collections = response.collections;
|
||||
|
||||
this.addCollections(collections);
|
||||
this.addCollectionSearch({query, collections});
|
||||
collections.forEach(collection => this.addWorkflows(collection.workflows as ITemplatesWorkflowFull[]));
|
||||
|
||||
return collections;
|
||||
},
|
||||
async getWorkflows(query: ITemplatesQuery): Promise<ITemplatesWorkflow[]> {
|
||||
const cachedResults = this.getSearchedWorkflows(query);
|
||||
if (cachedResults) {
|
||||
return cachedResults;
|
||||
}
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const apiEndpoint: string = settingsStore.templatesHost;
|
||||
const versionCli: string = settingsStore.versionCli;
|
||||
|
||||
const payload = await getWorkflows(apiEndpoint, {...query, skip: 0, limit: TEMPLATES_PAGE_SIZE}, { 'n8n-version': versionCli });
|
||||
|
||||
this.addWorkflows(payload.workflows);
|
||||
this.addWorkflowsSearch({...payload, query});
|
||||
return this.getSearchedWorkflows(query) || [];
|
||||
},
|
||||
async getMoreWorkflows(query: ITemplatesQuery): Promise<ITemplatesWorkflow[]> {
|
||||
if (this.isSearchLoadingMore(query) && !this.isSearchFinished(query)) {
|
||||
return [];
|
||||
}
|
||||
const cachedResults = this.getSearchedWorkflows(query) || [];
|
||||
const settingsStore = useSettingsStore();
|
||||
const apiEndpoint: string = settingsStore.templatesHost;
|
||||
|
||||
this.setWorkflowSearchLoading(query);
|
||||
try {
|
||||
const payload = await getWorkflows(apiEndpoint, {...query, skip: cachedResults.length, limit: TEMPLATES_PAGE_SIZE});
|
||||
|
||||
this.setWorkflowSearchLoaded(query);
|
||||
this.addWorkflows(payload.workflows);
|
||||
this.addWorkflowsSearch({...payload, query});
|
||||
|
||||
return this.getSearchedWorkflows(query) || [];
|
||||
} catch (e) {
|
||||
this.setWorkflowSearchLoaded(query);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
async getWorkflowTemplate(templateId: string): Promise<IWorkflowTemplate> {
|
||||
const settingsStore = useSettingsStore();
|
||||
const apiEndpoint: string = settingsStore.templatesHost;
|
||||
const versionCli: string = settingsStore.versionCli;
|
||||
return await getWorkflowTemplate(apiEndpoint, templateId, { 'n8n-version': versionCli });
|
||||
},
|
||||
},
|
||||
});
|
||||
385
packages/editor-ui/src/stores/ui.ts
Normal file
385
packages/editor-ui/src/stores/ui.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
import {
|
||||
applyForOnboardingCall,
|
||||
fetchNextOnboardingPrompt,
|
||||
submitEmailOnSignup,
|
||||
} from "@/api/workflow-webhooks";
|
||||
import {
|
||||
ABOUT_MODAL_KEY,
|
||||
CHANGE_PASSWORD_MODAL_KEY,
|
||||
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
|
||||
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
|
||||
COMMUNITY_PACKAGE_MANAGE_ACTIONS,
|
||||
CONTACT_PROMPT_MODAL_KEY,
|
||||
CREDENTIAL_EDIT_MODAL_KEY,
|
||||
CREDENTIAL_SELECT_MODAL_KEY,
|
||||
DELETE_USER_MODAL_KEY,
|
||||
DUPLICATE_MODAL_KEY,
|
||||
EXECUTIONS_MODAL_KEY,
|
||||
FAKE_DOOR_FEATURES,
|
||||
IMPORT_CURL_MODAL_KEY,
|
||||
INVITE_USER_MODAL_KEY,
|
||||
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
||||
PERSONALIZATION_MODAL_KEY,
|
||||
STORES,
|
||||
TAGS_MANAGER_MODAL_KEY,
|
||||
VALUE_SURVEY_MODAL_KEY,
|
||||
VERSIONS_MODAL_KEY,
|
||||
VIEWS,
|
||||
WORKFLOW_ACTIVE_MODAL_KEY,
|
||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||
} from "@/constants";
|
||||
import {
|
||||
CurlToJSONResponse,
|
||||
IFakeDoorLocation,
|
||||
IMenuItem,
|
||||
INodeUi,
|
||||
IOnboardingCallPrompt,
|
||||
IUser,
|
||||
UIState,
|
||||
XYPosition,
|
||||
} from "@/Interface";
|
||||
import Vue from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { useRootStore } from "./n8nRootStore";
|
||||
import { getCurlToJson } from "@/api/curlHelper";
|
||||
import { useWorkflowsStore } from "./workflows";
|
||||
|
||||
export const useUIStore = defineStore(STORES.UI, {
|
||||
state: (): UIState => ({
|
||||
activeActions: [],
|
||||
activeCredentialType: null,
|
||||
modals: {
|
||||
[ABOUT_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[CHANGE_PASSWORD_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[CONTACT_PROMPT_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[CREDENTIAL_EDIT_MODAL_KEY]: {
|
||||
open: false,
|
||||
mode: '',
|
||||
activeId: null,
|
||||
},
|
||||
[CREDENTIAL_SELECT_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[DELETE_USER_MODAL_KEY]: {
|
||||
open: false,
|
||||
activeId: null,
|
||||
},
|
||||
[DUPLICATE_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[ONBOARDING_CALL_SIGNUP_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[PERSONALIZATION_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[INVITE_USER_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[TAGS_MANAGER_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[VALUE_SURVEY_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[VERSIONS_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[WORKFLOW_SETTINGS_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[EXECUTIONS_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[WORKFLOW_ACTIVE_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[COMMUNITY_PACKAGE_INSTALL_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY]: {
|
||||
open: false,
|
||||
mode: '',
|
||||
activeId: null,
|
||||
},
|
||||
[IMPORT_CURL_MODAL_KEY]: {
|
||||
open: false,
|
||||
curlCommand: '',
|
||||
httpNodeParameters: '',
|
||||
},
|
||||
},
|
||||
modalStack: [],
|
||||
sidebarMenuCollapsed: true,
|
||||
isPageLoading: true,
|
||||
currentView: '',
|
||||
mainPanelPosition: 0.5,
|
||||
fakeDoorFeatures: [
|
||||
{
|
||||
id: FAKE_DOOR_FEATURES.ENVIRONMENTS,
|
||||
featureName: 'fakeDoor.settings.environments.name',
|
||||
icon: 'server',
|
||||
infoText: 'fakeDoor.settings.environments.infoText',
|
||||
actionBoxTitle: 'fakeDoor.settings.environments.actionBox.title',
|
||||
actionBoxDescription: 'fakeDoor.settings.environments.actionBox.description',
|
||||
linkURL: 'https://n8n-community.typeform.com/to/l7QOrERN#f=environments',
|
||||
uiLocations: ['settings'],
|
||||
},
|
||||
{
|
||||
id: FAKE_DOOR_FEATURES.LOGGING,
|
||||
featureName: 'fakeDoor.settings.logging.name',
|
||||
icon: 'sign-in-alt',
|
||||
infoText: 'fakeDoor.settings.logging.infoText',
|
||||
actionBoxTitle: 'fakeDoor.settings.logging.actionBox.title',
|
||||
actionBoxDescription: 'fakeDoor.settings.logging.actionBox.description',
|
||||
linkURL: 'https://n8n-community.typeform.com/to/l7QOrERN#f=logging',
|
||||
uiLocations: ['settings'],
|
||||
},
|
||||
{
|
||||
id: FAKE_DOOR_FEATURES.SHARING,
|
||||
featureName: 'fakeDoor.credentialEdit.sharing.name',
|
||||
actionBoxTitle: 'fakeDoor.credentialEdit.sharing.actionBox.title',
|
||||
actionBoxDescription: 'fakeDoor.credentialEdit.sharing.actionBox.description',
|
||||
linkURL: 'https://n8n-community.typeform.com/to/l7QOrERN#f=sharing',
|
||||
uiLocations: ['credentialsModal'],
|
||||
},
|
||||
],
|
||||
draggable: {
|
||||
isDragging: false,
|
||||
type: '',
|
||||
data: '',
|
||||
canDrop: false,
|
||||
stickyPosition: null,
|
||||
},
|
||||
stateIsDirty: false,
|
||||
lastSelectedNode: null,
|
||||
lastSelectedNodeOutputIndex: null,
|
||||
nodeViewOffsetPosition: [0, 0],
|
||||
nodeViewMoveInProgress: false,
|
||||
selectedNodes: [],
|
||||
sidebarMenuItems: [],
|
||||
nodeViewInitialized: false,
|
||||
addFirstStepOnLoad: false,
|
||||
executionSidebarAutoRefresh: true,
|
||||
}),
|
||||
getters: {
|
||||
getLastSelectedNode(): INodeUi | null {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
if (this.lastSelectedNode) {
|
||||
return workflowsStore.getNodeByName(this.lastSelectedNode);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getCurlCommand() : string|undefined {
|
||||
return this.modals[IMPORT_CURL_MODAL_KEY].curlCommand;
|
||||
},
|
||||
getHttpNodeParameters() : string|undefined {
|
||||
return this.modals[IMPORT_CURL_MODAL_KEY].httpNodeParameters;
|
||||
},
|
||||
areExpressionsDisabled() : boolean {
|
||||
return this.currentView === VIEWS.DEMO;
|
||||
},
|
||||
isVersionsOpen() : boolean {
|
||||
return this.modals[VERSIONS_MODAL_KEY].open;
|
||||
},
|
||||
isModalOpen() {
|
||||
return (name: string) => this.modals[name].open;
|
||||
},
|
||||
isModalActive() {
|
||||
return (name: string) => this.modalStack.length > 0 && name === this.modalStack[0];
|
||||
},
|
||||
getModalActiveId() {
|
||||
return (name: string) => this.modals[name].activeId;
|
||||
},
|
||||
getModalMode() {
|
||||
return (name: string) => this.modals[name].mode;
|
||||
},
|
||||
getModalData() {
|
||||
return (name: string) => this.modals[name].data;
|
||||
},
|
||||
getFakeDoorByLocation() {
|
||||
return (location: IFakeDoorLocation) => this.fakeDoorFeatures.filter(fakeDoor => fakeDoor.uiLocations.includes(location));
|
||||
},
|
||||
getFakeDoorById() {
|
||||
return (id: string) => this.fakeDoorFeatures.find(fakeDoor => fakeDoor.id.toString() === id);
|
||||
},
|
||||
isNodeView() : boolean {
|
||||
return [VIEWS.NEW_WORKFLOW.toString(), VIEWS.WORKFLOW.toString(), VIEWS.EXECUTION.toString()].includes(this.currentView);
|
||||
},
|
||||
isActionActive() {
|
||||
return (action: string) => this.activeActions.includes(action);
|
||||
},
|
||||
getSelectedNodes() : INodeUi[] {
|
||||
const seen = new Set();
|
||||
return this.selectedNodes.filter((node: INodeUi) => {
|
||||
// dedupe for instances when same node is selected in different ways
|
||||
if (!seen.has(node.id)) {
|
||||
seen.add(node.id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
},
|
||||
isNodeSelected() {
|
||||
return (nodeName: string): boolean => {
|
||||
let index;
|
||||
for (index in this.selectedNodes) {
|
||||
if (this.selectedNodes[index].name === nodeName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setMode(name: string, mode: string): void {
|
||||
Vue.set(this.modals[name], 'mode', mode);
|
||||
},
|
||||
setActiveId(name: string, id: string): void {
|
||||
Vue.set(this.modals[name], 'activeId', id);
|
||||
},
|
||||
setModalData (payload: { name: string, data: Record<string, unknown> }) {
|
||||
Vue.set(this.modals[payload.name], 'data', payload.data);
|
||||
},
|
||||
openModal(name: string): void {
|
||||
Vue.set(this.modals[name], 'open', true);
|
||||
this.modalStack = [name].concat(this.modalStack);
|
||||
},
|
||||
openModalWithData (payload: { name: string, data: Record<string, unknown> }): void {
|
||||
this.setModalData(payload);
|
||||
this.openModal(payload.name);
|
||||
},
|
||||
closeModal(name: string): void {
|
||||
Vue.set(this.modals[name], 'open', false);
|
||||
this.modalStack = this.modalStack.filter((openModalName: string) => {
|
||||
return name !== openModalName;
|
||||
});
|
||||
},
|
||||
closeAllModals(): void {
|
||||
Object.keys(this.modals).forEach((name: string) => {
|
||||
if (this.modals[name].open) {
|
||||
Vue.set(this.modals[name], 'open', false);
|
||||
}
|
||||
});
|
||||
this.modalStack = [];
|
||||
},
|
||||
draggableStartDragging(type: string, data: string): void {
|
||||
this.draggable = {
|
||||
isDragging: true,
|
||||
type,
|
||||
data,
|
||||
canDrop: false,
|
||||
stickyPosition: null,
|
||||
};
|
||||
},
|
||||
draggableStopDragging(): void {
|
||||
this.draggable = {
|
||||
isDragging: false,
|
||||
type: '',
|
||||
data: '',
|
||||
canDrop: false,
|
||||
stickyPosition: null,
|
||||
};
|
||||
},
|
||||
setDraggableStickyPos(position: XYPosition): void {
|
||||
Vue.set(this.draggable, 'stickyPosition', position);
|
||||
},
|
||||
setDraggableCanDrop(canDrop: boolean): void {
|
||||
Vue.set(this.draggable, 'canDrop', canDrop);
|
||||
},
|
||||
openDeleteUserModal(id: string): void {
|
||||
this.setActiveId(DELETE_USER_MODAL_KEY, id);
|
||||
this.openModal(DELETE_USER_MODAL_KEY);
|
||||
},
|
||||
openExistingCredential(id: string): void {
|
||||
this.setActiveId(CREDENTIAL_EDIT_MODAL_KEY, id);
|
||||
this.setMode(CREDENTIAL_EDIT_MODAL_KEY, 'edit');
|
||||
this.openModal(CREDENTIAL_EDIT_MODAL_KEY);
|
||||
},
|
||||
openNewCredential(type: string): void {
|
||||
this.setActiveId(CREDENTIAL_EDIT_MODAL_KEY, type);
|
||||
this.setMode(CREDENTIAL_EDIT_MODAL_KEY, 'new');
|
||||
this.openModal(CREDENTIAL_EDIT_MODAL_KEY);
|
||||
},
|
||||
async getNextOnboardingPrompt(): Promise<IOnboardingCallPrompt> {
|
||||
const rootStore = useRootStore();
|
||||
const instanceId = rootStore.instanceId;
|
||||
// TODO: current USER
|
||||
const currentUser = {} as IUser;
|
||||
return await fetchNextOnboardingPrompt(instanceId, currentUser);
|
||||
},
|
||||
async applyForOnboardingCall(email: string): Promise<string> {
|
||||
const rootStore = useRootStore();
|
||||
const instanceId = rootStore.instanceId;
|
||||
// TODO: current USER
|
||||
const currentUser = {} as IUser;
|
||||
return await applyForOnboardingCall(instanceId, currentUser, email);
|
||||
},
|
||||
async submitContactEmail(email: string, agree: boolean): Promise<string> {
|
||||
const rootStore = useRootStore();
|
||||
const instanceId = rootStore.instanceId;
|
||||
// TODO: current USER
|
||||
const currentUser = {} as IUser;
|
||||
return await submitEmailOnSignup(instanceId, currentUser, email || currentUser.email, agree);
|
||||
},
|
||||
openCommunityPackageUninstallConfirmModal(packageName: string) {
|
||||
this.setActiveId(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, packageName);
|
||||
this.setMode(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, COMMUNITY_PACKAGE_MANAGE_ACTIONS.UNINSTALL);
|
||||
this.openModal(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY);
|
||||
},
|
||||
openCommunityPackageUpdateConfirmModal(packageName: string) {
|
||||
this.setActiveId(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, packageName);
|
||||
this.setMode(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, COMMUNITY_PACKAGE_MANAGE_ACTIONS.UPDATE);
|
||||
this.openModal(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY);
|
||||
},
|
||||
addActiveAction(action: string): void {
|
||||
if (!this.activeActions.includes(action)) {
|
||||
this.activeActions.push(action);
|
||||
}
|
||||
},
|
||||
removeActiveAction(action: string): void {
|
||||
const actionIndex = this.activeActions.indexOf(action);
|
||||
if (actionIndex !== -1) {
|
||||
this.activeActions.splice(actionIndex, 1);
|
||||
}
|
||||
},
|
||||
addSelectedNode(node: INodeUi): void {
|
||||
this.selectedNodes.push(node);
|
||||
},
|
||||
removeNodeFromSelection(node: INodeUi): void {
|
||||
let index;
|
||||
for (index in this.selectedNodes) {
|
||||
if (this.selectedNodes[index].name === node.name) {
|
||||
this.selectedNodes.splice(parseInt(index, 10), 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
resetSelectedNodes(): void {
|
||||
Vue.set(this, 'selectedNodes', []);
|
||||
},
|
||||
addSidebarMenuItems(menuItems: IMenuItem[]) {
|
||||
const updated = this.sidebarMenuItems.concat(menuItems);
|
||||
Vue.set(this, 'sidebarMenuItems', updated);
|
||||
},
|
||||
setCurlCommand (payload: { name: string, command: string }): void {
|
||||
Vue.set(this.modals[payload.name], 'curlCommand', payload.command);
|
||||
},
|
||||
setHttpNodeParameters (payload: { name: string, parameters: string }): void {
|
||||
Vue.set(this.modals[payload.name], 'httpNodeParameters', payload.parameters);
|
||||
},
|
||||
toggleSidebarMenuCollapse (): void {
|
||||
this.sidebarMenuCollapsed = !this.sidebarMenuCollapsed;
|
||||
},
|
||||
async getCurlToJson (curlCommand: string): Promise<CurlToJSONResponse> {
|
||||
const rootStore = useRootStore();
|
||||
return await getCurlToJson(rootStore.getRestApiContext, curlCommand);
|
||||
},
|
||||
},
|
||||
});
|
||||
204
packages/editor-ui/src/stores/users.ts
Normal file
204
packages/editor-ui/src/stores/users.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { changePassword, deleteUser, getCurrentUser, getUsers, inviteUsers, login, loginCurrentUser, logout, reinvite, sendForgotPasswordEmail, setupOwner, signup, skipOwnerSetup, submitPersonalizationSurvey, updateCurrentUser, updateCurrentUserPassword, validatePasswordToken, validateSignupToken } from "@/api/users";
|
||||
import { PERSONALIZATION_MODAL_KEY, STORES } from "@/constants";
|
||||
import { IInviteResponse, IPersonalizationLatestVersion, IUser, IUserResponse, IUsersState } from "@/Interface";
|
||||
import { getPersonalizedNodeTypes, isAuthorized, PERMISSIONS, ROLE } from "@/modules/userHelpers";
|
||||
import { defineStore } from "pinia";
|
||||
import Vue from "vue";
|
||||
import { useRootStore } from "./n8nRootStore";
|
||||
import { useSettingsStore } from "./settings";
|
||||
import { useUIStore } from "./ui";
|
||||
|
||||
const isDefaultUser = (user: IUserResponse | null) => Boolean(user && user.isPending && user.globalRole && user.globalRole.name === ROLE.Owner);
|
||||
const isPendingUser = (user: IUserResponse | null) => Boolean(user && user.isPending);
|
||||
|
||||
export const useUsersStore = defineStore(STORES.USERS, {
|
||||
state: (): IUsersState => ({
|
||||
currentUserId: null,
|
||||
users: {},
|
||||
}),
|
||||
getters: {
|
||||
allUsers(): IUser[] {
|
||||
return Object.values(this.users);
|
||||
},
|
||||
currentUser(): IUser | null {
|
||||
return this.currentUserId ? this.users[this.currentUserId] : null;
|
||||
},
|
||||
getUserById(): (userId: string) => IUser | null {
|
||||
return (userId: string): IUser | null => this.users[userId];
|
||||
},
|
||||
globalRoleName(): string {
|
||||
return this.currentUser?.globalRole?.name || '';
|
||||
},
|
||||
canUserDeleteTags(): boolean {
|
||||
return isAuthorized(PERMISSIONS.TAGS.CAN_DELETE_TAGS, this.currentUser);
|
||||
},
|
||||
canUserAccessSidebarUserInfo() {
|
||||
if (this.currentUser) {
|
||||
const currentUser: IUser = this.currentUser;
|
||||
return isAuthorized(PERMISSIONS.PRIMARY_MENU.CAN_ACCESS_USER_INFO, currentUser);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
showUMSetupWarning() {
|
||||
if (this.currentUser) {
|
||||
const currentUser: IUser = this.currentUser;
|
||||
return isAuthorized(PERMISSIONS.USER_SETTINGS.VIEW_UM_SETUP_WARNING, currentUser);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
personalizedNodeTypes(): string[] {
|
||||
const user = this.currentUser as IUser | null;
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const answers = user.personalizationAnswers;
|
||||
if (!answers) {
|
||||
return [];
|
||||
}
|
||||
return getPersonalizedNodeTypes(answers);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
addUsers(users: IUserResponse[]) {
|
||||
users.forEach((userResponse: IUserResponse) => {
|
||||
const prevUser = this.users[userResponse.id] || {};
|
||||
const updatedUser = {
|
||||
...prevUser,
|
||||
...userResponse,
|
||||
};
|
||||
const user: IUser = {
|
||||
...updatedUser,
|
||||
fullName: userResponse.firstName? `${updatedUser.firstName} ${updatedUser.lastName || ''}`: undefined,
|
||||
isDefaultUser: isDefaultUser(updatedUser),
|
||||
isPendingUser: isPendingUser(updatedUser),
|
||||
isOwner: Boolean(updatedUser.globalRole && updatedUser.globalRole.name === ROLE.Owner),
|
||||
};
|
||||
Vue.set(this.users, user.id, user);
|
||||
});
|
||||
},
|
||||
deleteUserById(userId: string): void {
|
||||
Vue.delete(this.users, userId);
|
||||
},
|
||||
setPersonalizationAnswers(answers: IPersonalizationLatestVersion): void {
|
||||
if (!this.currentUser) {
|
||||
return;
|
||||
}
|
||||
Vue.set(this.currentUser, 'personalizationAnswers', answers);
|
||||
},
|
||||
async getCurrentUser(): void {
|
||||
const rootStore = useRootStore();
|
||||
const user = await getCurrentUser(rootStore.getRestApiContext);
|
||||
if (user) {
|
||||
this.addUsers([user]);
|
||||
this.currentUserId = user.id;
|
||||
}
|
||||
},
|
||||
async loginWithCookie(): void {
|
||||
const rootStore = useRootStore();
|
||||
const user = await loginCurrentUser(rootStore.getRestApiContext);
|
||||
if (user) {
|
||||
this.addUsers([user]);
|
||||
this.currentUserId = user.id;
|
||||
}
|
||||
},
|
||||
async loginWithCreds(params: {email: string, password: string}): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
const user = await login(rootStore.getRestApiContext, params);
|
||||
if (user) {
|
||||
this.addUsers([user]);
|
||||
this.currentUserId = user.id;
|
||||
}
|
||||
},
|
||||
async logout(): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
await logout(rootStore.getRestApiContext);
|
||||
this.currentUserId = null;
|
||||
},
|
||||
async createOwner(params: { firstName: string; lastName: string; email: string; password: string;}): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
const user = await setupOwner(rootStore.getRestApiContext, params);
|
||||
const settingsStore = useSettingsStore();
|
||||
if (user) {
|
||||
this.addUsers([user]);
|
||||
this.currentUserId = user.id;
|
||||
settingsStore.stopShowingSetupPage();
|
||||
}
|
||||
},
|
||||
async validateSignupToken(params: {inviteeId: string, inviterId: string}): Promise<{ inviter: { firstName: string, lastName: string } }> {
|
||||
const rootStore = useRootStore();
|
||||
return await validateSignupToken(rootStore.getRestApiContext, params);
|
||||
},
|
||||
async signup(params: { inviteeId: string; inviterId: string; firstName: string; lastName: string; password: string;}): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
const user = await signup(rootStore.getRestApiContext, params);
|
||||
if (user) {
|
||||
this.addUsers([user]);
|
||||
this.currentUserId = user.id;
|
||||
}
|
||||
},
|
||||
async sendForgotPasswordEmail( params: {email: string}): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
await sendForgotPasswordEmail(rootStore.getRestApiContext, params);
|
||||
},
|
||||
async validatePasswordToken(params: {token: string, userId: string}): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
await validatePasswordToken(rootStore.getRestApiContext, params);
|
||||
},
|
||||
async changePassword(params: {token: string, password: string, userId: string}): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
await changePassword(rootStore.getRestApiContext, params);
|
||||
},
|
||||
async updateUser(params: {id: string, firstName: string, lastName: string, email: string}): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
const user = await updateCurrentUser(rootStore.getRestApiContext, params);
|
||||
this.addUsers([user]);
|
||||
},
|
||||
async updateCurrentUserPassword({password, currentPassword}: {password: string, currentPassword: string}): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
await updateCurrentUserPassword(rootStore.getRestApiContext, {newPassword: password, currentPassword});
|
||||
},
|
||||
async deleteUser(params: { id: string, transferId?: string}): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
await deleteUser(rootStore.getRestApiContext, params);
|
||||
this.deleteUserById(params.id);
|
||||
},
|
||||
async fetchUsers(): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
const users = await getUsers(rootStore.getRestApiContext);
|
||||
this.addUsers(users);
|
||||
},
|
||||
async inviteUsers(params: Array<{email: string}>): Promise<IInviteResponse[]> {
|
||||
const rootStore = useRootStore();
|
||||
const users = await inviteUsers(rootStore.getRestApiContext, params);
|
||||
this.addUsers(users.map(({user}) => ({ isPending: true, ...user })));
|
||||
return users;
|
||||
},
|
||||
async reinviteUser(params: {id: string}): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
await reinvite(rootStore.getRestApiContext, params);
|
||||
},
|
||||
async submitPersonalizationSurvey(results: IPersonalizationLatestVersion): Promise<void> {
|
||||
const rootStore = useRootStore();
|
||||
await submitPersonalizationSurvey(rootStore.getRestApiContext, results);
|
||||
this.setPersonalizationAnswers(results);
|
||||
},
|
||||
async showPersonalizationSurvey(): Promise<void> {
|
||||
const settingsStore = useSettingsStore();
|
||||
const surveyEnabled = settingsStore.isPersonalizationSurveyEnabled;
|
||||
const currentUser = this.currentUser;
|
||||
if (surveyEnabled && currentUser && !currentUser.personalizationAnswers) {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.openModal(PERSONALIZATION_MODAL_KEY);
|
||||
}
|
||||
},
|
||||
async skipOwnerSetup(): Promise<void> {
|
||||
try {
|
||||
const rootStore = useRootStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
settingsStore.stopShowingSetupPage();
|
||||
await skipOwnerSetup(rootStore.getRestApiContext);
|
||||
} catch (error) {}
|
||||
},
|
||||
},
|
||||
});
|
||||
773
packages/editor-ui/src/stores/workflows.ts
Normal file
773
packages/editor-ui/src/stores/workflows.ts
Normal file
@@ -0,0 +1,773 @@
|
||||
import { DEFAULT_NEW_WORKFLOW_NAME, DUPLICATE_POSTFFIX, MAX_WORKFLOW_NAME_LENGTH, PLACEHOLDER_EMPTY_WORKFLOW_ID, STORES } from "@/constants";
|
||||
import {
|
||||
IExecutionResponse,
|
||||
IExecutionsCurrentSummaryExtended,
|
||||
IExecutionsSummary,
|
||||
INewWorkflowData,
|
||||
INodeUi,
|
||||
INodeUpdatePropertiesInformation,
|
||||
IPushDataExecutionFinished,
|
||||
IPushDataNodeExecuteAfter,
|
||||
IUpdateInformation,
|
||||
IWorkflowDb,
|
||||
IWorkflowsMap,
|
||||
WorkflowsState,
|
||||
} from "@/Interface";
|
||||
import { defineStore } from "pinia";
|
||||
import { IConnection, IConnections, IDataObject, INode, INodeConnections, INodeCredentials, INodeCredentialsDetails, INodeExecutionData, INodeIssueData, IPinData, IRunData, ITaskData, IWorkflowSettings } from 'n8n-workflow';
|
||||
import Vue from "vue";
|
||||
import { useRootStore } from "./n8nRootStore";
|
||||
import { getActiveWorkflows, getCurrentExecutions, getFinishedExecutions, getNewWorkflow, getWorkflows } from "@/api/workflows";
|
||||
import { useUIStore } from "./ui";
|
||||
import { getPairedItemsMapping } from "@/pairedItemUtils";
|
||||
import { dataPinningEventBus } from "@/event-bus/data-pinning-event-bus";
|
||||
import { isJsonKeyObject } from "@/utils";
|
||||
import { stringSizeInBytes } from "@/components/helpers";
|
||||
import { useNDVStore } from "./ndv";
|
||||
import { useNodeTypesStore } from "./nodeTypes";
|
||||
|
||||
export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||
state: (): WorkflowsState => ({
|
||||
workflow: {
|
||||
id: PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||
name: '',
|
||||
active: false,
|
||||
createdAt: -1,
|
||||
updatedAt: -1,
|
||||
connections: {},
|
||||
nodes: [],
|
||||
settings: {},
|
||||
tags: [],
|
||||
pinData: {},
|
||||
hash: '',
|
||||
},
|
||||
activeWorkflows: [],
|
||||
activeExecutions: [],
|
||||
currentWorkflowExecutions: [],
|
||||
activeWorkflowExecution: null,
|
||||
finishedExecutionsCount: 0,
|
||||
workflowExecutionData: null,
|
||||
workflowExecutionPairedItemMappings: {},
|
||||
workflowsById: {},
|
||||
subWorkflowExecutionError: null,
|
||||
activeExecutionId: null,
|
||||
executingNode: null,
|
||||
executionWaitingForWebhook: false,
|
||||
nodeMetadata: {},
|
||||
}),
|
||||
getters: {
|
||||
// Workflow getters
|
||||
workflowName(): string {
|
||||
return this.workflow.name;
|
||||
},
|
||||
workflowId(): string {
|
||||
return this.workflow.id;
|
||||
},
|
||||
workflowHash(): string | undefined {
|
||||
return this.workflow.hash;
|
||||
},
|
||||
workflowSettings() : IWorkflowSettings {
|
||||
if (this.workflow.settings === undefined) {
|
||||
return {};
|
||||
}
|
||||
return this.workflow.settings;
|
||||
},
|
||||
workflowTags() : string[] {
|
||||
return this.workflow.tags as string[];
|
||||
},
|
||||
allWorkflows() : IWorkflowDb[] {
|
||||
return Object.values(this.workflowsById)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
},
|
||||
isNewWorkflow() : boolean {
|
||||
return this.workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID;
|
||||
},
|
||||
isWorkflowActive(): boolean {
|
||||
return this.workflow.active;
|
||||
},
|
||||
workflowTriggerNodes() : INodeUi[] {
|
||||
return this.workflow.nodes.filter((node: INodeUi) => {
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const nodeType = nodeTypesStore.getNodeType(node.type as string, node.typeVersion as number);
|
||||
return nodeType && nodeType.group.includes('trigger');
|
||||
});
|
||||
},
|
||||
currentWorkflowHasWebhookNode(): boolean {
|
||||
return !!this.workflow.nodes.find((node: INodeUi) => !!node.webhookId);
|
||||
},
|
||||
getWorkflowRunData(): IRunData | null {
|
||||
if (!this.workflowExecutionData || !this.workflowExecutionData.data || !this.workflowExecutionData.data.resultData) {
|
||||
return null;
|
||||
}
|
||||
return this.workflowExecutionData.data.resultData.runData;
|
||||
},
|
||||
getWorkflowResultDataByNodeName() {
|
||||
return (nodeName: string): ITaskData[] | null => {
|
||||
const workflowRunData = this.getWorkflowRunData;
|
||||
|
||||
if (workflowRunData === null) {
|
||||
return null;
|
||||
}
|
||||
if (!workflowRunData.hasOwnProperty(nodeName)) {
|
||||
return null;
|
||||
}
|
||||
return workflowRunData[nodeName];
|
||||
};
|
||||
},
|
||||
|
||||
// Node getters
|
||||
allConnections() : IConnections {
|
||||
return this.workflow.connections;
|
||||
},
|
||||
outgoingConnectionsByNodeName() {
|
||||
return (nodeName: string): INodeConnections => {
|
||||
if (this.workflow.connections.hasOwnProperty(nodeName)) {
|
||||
return this.workflow.connections[nodeName];
|
||||
}
|
||||
return {};
|
||||
};
|
||||
},
|
||||
allNodes() : INodeUi[] {
|
||||
return this.workflow.nodes;
|
||||
},
|
||||
nodesByName() : { [name: string]: INodeUi } {
|
||||
return this.workflow.nodes.reduce((accu: { [name: string]: INodeUi }, node) => {
|
||||
accu[node.name] = node;
|
||||
return accu;
|
||||
}, {});
|
||||
},
|
||||
getNodeByName() {
|
||||
return (nodeName: string): INodeUi | null => this.nodesByName[nodeName] || null;
|
||||
},
|
||||
getNodeById() {
|
||||
return (nodeId: string): INodeUi | undefined => this.workflow.nodes.find((node: INodeUi) => node.id === nodeId);
|
||||
},
|
||||
nodesIssuesExist(): boolean {
|
||||
for (const node of this.workflow.nodes) {
|
||||
if (node.issues === undefined || Object.keys(node.issues).length === 0) {
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
getPinData(): IPinData | undefined {
|
||||
return this.workflow.pinData;
|
||||
},
|
||||
pinDataSize(): number {
|
||||
const ndvStore = useNDVStore();
|
||||
const activeNode = ndvStore.activeNodeName;
|
||||
return this.workflow.nodes
|
||||
.reduce((acc, node) => {
|
||||
if (typeof node.pinData !== 'undefined' && node.name !== activeNode) {
|
||||
acc += stringSizeInBytes(node.pinData);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, 0);
|
||||
},
|
||||
executedNode(): string | undefined {
|
||||
return this.workflowExecutionData ? this.workflowExecutionData.executedNode : undefined;
|
||||
},
|
||||
getParametersLastUpdate(): ((name: string) => number | undefined) {
|
||||
return (nodeName: string) => this.nodeMetadata[nodeName] && this.nodeMetadata[nodeName].parametersLastUpdatedAt;
|
||||
},
|
||||
|
||||
// Executions getters
|
||||
getExecutionDataById(): (id: string) => IExecutionsSummary | undefined {
|
||||
return (id: string): IExecutionsSummary | undefined => this.currentWorkflowExecutions.find(execution => execution.id === id);
|
||||
},
|
||||
getAllLoadedFinishedExecutions(): IExecutionsSummary[] {
|
||||
return this.currentWorkflowExecutions.filter(ex => ex.finished === true || ex.stoppedAt !== undefined);
|
||||
},
|
||||
getWorkflowExecution(): IExecutionResponse | null {
|
||||
return this.workflowExecutionData;
|
||||
},
|
||||
getTotalFinishedExecutionsCount(): number {
|
||||
return this.finishedExecutionsCount;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
|
||||
// Workflow actions
|
||||
|
||||
async fetchAllWorkflows(): Promise<IWorkflowDb[]> {
|
||||
const rootStore = useRootStore();
|
||||
const workflows = await getWorkflows(rootStore.getRestApiContext);
|
||||
this.setWorkflows(workflows);
|
||||
return workflows;
|
||||
},
|
||||
|
||||
async getNewWorkflowData(name?: string): Promise<INewWorkflowData> {
|
||||
let workflowData = {
|
||||
name: '',
|
||||
onboardingFlowEnabled: false,
|
||||
};
|
||||
try {
|
||||
const rootStore = useRootStore();
|
||||
workflowData = await getNewWorkflow(rootStore.getRestApiContext, name);
|
||||
}
|
||||
catch (e) {
|
||||
// in case of error, default to original name
|
||||
workflowData.name = name || DEFAULT_NEW_WORKFLOW_NAME;
|
||||
}
|
||||
|
||||
this.setWorkflowName({ newName: workflowData.name, setStateDirty: false });
|
||||
return workflowData;
|
||||
},
|
||||
|
||||
setWorkflowId (id: string): void {
|
||||
this.workflow.id = id === 'new' ? PLACEHOLDER_EMPTY_WORKFLOW_ID : id;
|
||||
},
|
||||
|
||||
setWorkflowName(data: { newName: string, setStateDirty: boolean }): void {
|
||||
if (data.setStateDirty === true) {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
}
|
||||
this.workflow.name = data.newName;
|
||||
},
|
||||
|
||||
setWorkflowHash(hash: string): void {
|
||||
this.workflow.hash = hash;
|
||||
},
|
||||
|
||||
// replace invalid credentials in workflow
|
||||
replaceInvalidWorkflowCredentials(data: {credentials: INodeCredentialsDetails, invalid: INodeCredentialsDetails, type: string}): void {
|
||||
this.workflow.nodes.forEach((node : INodeUi) => {
|
||||
const nodeCredentials: INodeCredentials | undefined = (node as unknown as INode).credentials;
|
||||
|
||||
if (!nodeCredentials || !nodeCredentials[data.type]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeCredentialDetails: INodeCredentialsDetails | string = nodeCredentials[data.type];
|
||||
|
||||
if (typeof nodeCredentialDetails === 'string' && nodeCredentialDetails === data.invalid.name) {
|
||||
(node.credentials as INodeCredentials)[data.type] = data.credentials;
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeCredentialDetails.id === null) {
|
||||
if (nodeCredentialDetails.name === data.invalid.name) {
|
||||
(node.credentials as INodeCredentials)[data.type] = data.credentials;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeCredentialDetails.id === data.invalid.id) {
|
||||
(node.credentials as INodeCredentials)[data.type] = data.credentials;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setWorkflows(workflows: IWorkflowDb[]) : void {
|
||||
this.workflowsById = workflows.reduce<IWorkflowsMap>((acc, workflow: IWorkflowDb) => {
|
||||
if (workflow.id) {
|
||||
acc[workflow.id] = workflow;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
},
|
||||
|
||||
deleteWorkflow(id: string) : void {
|
||||
const { [id]: deletedWorkflow, ...workflows } = this.workflowsById;
|
||||
this.workflowsById = workflows;
|
||||
},
|
||||
|
||||
addWorkflow(workflow: IWorkflowDb) : void {
|
||||
Vue.set(this.workflowsById, workflow.id, workflow);
|
||||
},
|
||||
|
||||
setWorkflowActive(workflowId: string): void {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = false;
|
||||
const index = this.activeWorkflows.indexOf(workflowId);
|
||||
if (index === -1) {
|
||||
this.activeWorkflows.push(workflowId);
|
||||
}
|
||||
},
|
||||
|
||||
setWorkflowInactive(workflowId: string): void {
|
||||
const index = this.activeWorkflows.indexOf(workflowId);
|
||||
if (index !== -1) {
|
||||
this.activeWorkflows.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchActiveWorkflows(): Promise<string[]> {
|
||||
const rootStore = useRootStore();
|
||||
const activeWorkflows = await getActiveWorkflows(rootStore.getRestApiContext);
|
||||
this.activeWorkflows = activeWorkflows;
|
||||
return activeWorkflows;
|
||||
},
|
||||
|
||||
setActive(newActive: boolean) : void {
|
||||
this.workflow.active = newActive;
|
||||
},
|
||||
|
||||
async getDuplicateCurrentWorkflowName(currentWorkflowName: string): Promise<string> {
|
||||
if (currentWorkflowName && (currentWorkflowName.length + DUPLICATE_POSTFFIX.length) >= MAX_WORKFLOW_NAME_LENGTH) {
|
||||
return currentWorkflowName;
|
||||
}
|
||||
|
||||
let newName = `${currentWorkflowName}${DUPLICATE_POSTFFIX}`;
|
||||
try {
|
||||
const rootStore = useRootStore();
|
||||
const newWorkflow = await getNewWorkflow(rootStore.getRestApiContext, newName );
|
||||
newName = newWorkflow.name;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
return newName;
|
||||
},
|
||||
|
||||
|
||||
// Node actions
|
||||
setWorkflowExecutionData(workflowResultData: IExecutionResponse | null): void {
|
||||
this.workflowExecutionData = workflowResultData;
|
||||
this.workflowExecutionPairedItemMappings = getPairedItemsMapping(this.workflowExecutionData);
|
||||
},
|
||||
|
||||
setWorkflowSettings(workflowSettings: IWorkflowSettings): void {
|
||||
Vue.set(this.workflow, 'settings', workflowSettings);
|
||||
},
|
||||
|
||||
setWorkflowPinData(pinData: IPinData): void {
|
||||
Vue.set(this.workflow, 'pinData', pinData || {});
|
||||
dataPinningEventBus.$emit('pin-data', pinData || {});
|
||||
},
|
||||
|
||||
setWorkflowTagIds(tags: string[]): void {
|
||||
Vue.set(this.workflow, 'tags', tags);
|
||||
},
|
||||
|
||||
addWorkflowTagIds(tags: string[]): void {
|
||||
Vue.set(this.workflow, 'tags', [...new Set([...(this.workflow.tags || []), ...tags])]);
|
||||
},
|
||||
|
||||
removeWorkflowTagId(tagId: string): void {
|
||||
const tags = this.workflow.tags as string[];
|
||||
const updated = tags.filter((id: string) => id !== tagId);
|
||||
Vue.set(this.workflow, 'tags', updated);
|
||||
},
|
||||
|
||||
setWorkflow(workflow: IWorkflowDb): void {
|
||||
Vue.set(this, 'workflow', workflow);
|
||||
|
||||
if (!this.workflow.hasOwnProperty('active')) {
|
||||
Vue.set(this.workflow, 'active', false);
|
||||
}
|
||||
if (!this.workflow.hasOwnProperty('connections')) {
|
||||
Vue.set(this.workflow, 'connections', {});
|
||||
}
|
||||
if (!this.workflow.hasOwnProperty('createdAt')) {
|
||||
Vue.set(this.workflow, 'createdAt', -1);
|
||||
}
|
||||
if (!this.workflow.hasOwnProperty('updatedAt')) {
|
||||
Vue.set(this.workflow, 'updatedAt', -1);
|
||||
}
|
||||
if (!this.workflow.hasOwnProperty('id')) {
|
||||
Vue.set(this.workflow, 'id', PLACEHOLDER_EMPTY_WORKFLOW_ID);
|
||||
}
|
||||
if (!this.workflow.hasOwnProperty('nodes')) {
|
||||
Vue.set(this.workflow, 'nodes', []);
|
||||
}
|
||||
if (!this.workflow.hasOwnProperty('settings')) {
|
||||
Vue.set(this.workflow, 'settings', {});
|
||||
}
|
||||
},
|
||||
|
||||
pinData(payload: { node: INodeUi, data: INodeExecutionData[] }): void {
|
||||
if (!this.workflow.pinData) {
|
||||
Vue.set(this.workflow, 'pinData', {});
|
||||
}
|
||||
|
||||
if (!Array.isArray(payload.data)) {
|
||||
payload.data = [payload.data];
|
||||
}
|
||||
|
||||
const storedPinData = payload.data.map(item => isJsonKeyObject(item) ? item : { json: item });
|
||||
|
||||
Vue.set(this.workflow.pinData!, payload.node.name, storedPinData);
|
||||
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
|
||||
dataPinningEventBus.$emit('pin-data', { [payload.node.name]: storedPinData });
|
||||
},
|
||||
|
||||
unpinData(payload: { node: INodeUi }): void {
|
||||
if (!this.workflow.pinData) {
|
||||
Vue.set(this.workflow, 'pinData', {});
|
||||
}
|
||||
|
||||
Vue.set(this.workflow.pinData!, payload.node.name, undefined);
|
||||
delete this.workflow.pinData![payload.node.name];
|
||||
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
|
||||
dataPinningEventBus.$emit('unpin-data', { [payload.node.name]: undefined });
|
||||
},
|
||||
|
||||
addConnection(data: { connection: IConnection[], setStateDirty: boolean }): void {
|
||||
if (data.connection.length !== 2) {
|
||||
// All connections need two entries
|
||||
// TODO: Check if there is an error or whatever that is supposed to be returned
|
||||
return;
|
||||
}
|
||||
const uiStore = useUIStore();
|
||||
if (data.setStateDirty === true) {
|
||||
uiStore.stateIsDirty = true;
|
||||
}
|
||||
|
||||
const sourceData: IConnection = data.connection[0];
|
||||
const destinationData: IConnection = data.connection[1];
|
||||
|
||||
// Check if source node and type exist already and if not add them
|
||||
if (!this.workflow.connections.hasOwnProperty(sourceData.node)) {
|
||||
Vue.set(this.workflow.connections, sourceData.node, {});
|
||||
}
|
||||
if (!this.workflow.connections[sourceData.node].hasOwnProperty(sourceData.type)) {
|
||||
Vue.set(this.workflow.connections[sourceData.node], sourceData.type, []);
|
||||
}
|
||||
if (this.workflow.connections[sourceData.node][sourceData.type].length < (sourceData.index + 1)) {
|
||||
for (let i = this.workflow.connections[sourceData.node][sourceData.type].length; i <= sourceData.index; i++) {
|
||||
this.workflow.connections[sourceData.node][sourceData.type].push([]);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the same connection exists already
|
||||
const checkProperties = ['index', 'node', 'type'];
|
||||
let propertyName: string;
|
||||
let connectionExists = false;
|
||||
connectionLoop:
|
||||
for (const existingConnection of this.workflow.connections[sourceData.node][sourceData.type][sourceData.index]) {
|
||||
for (propertyName of checkProperties) {
|
||||
if ((existingConnection as any)[propertyName] !== (destinationData as any)[propertyName]) { // tslint:disable-line:no-any
|
||||
continue connectionLoop;
|
||||
}
|
||||
}
|
||||
connectionExists = true;
|
||||
break;
|
||||
}
|
||||
// Add the new connection if it does not exist already
|
||||
if (connectionExists === false) {
|
||||
this.workflow.connections[sourceData.node][sourceData.type][sourceData.index].push(destinationData);
|
||||
}
|
||||
},
|
||||
|
||||
removeConnection(data: { connection: IConnection[] }): void {
|
||||
const sourceData = data.connection[0];
|
||||
const destinationData = data.connection[1];
|
||||
|
||||
if (!this.workflow.connections.hasOwnProperty(sourceData.node)) {
|
||||
return;
|
||||
}
|
||||
if (!this.workflow.connections[sourceData.node].hasOwnProperty(sourceData.type)) {
|
||||
return;
|
||||
}
|
||||
if (this.workflow.connections[sourceData.node][sourceData.type].length < (sourceData.index + 1)) {
|
||||
return;
|
||||
}
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
|
||||
const connections = this.workflow.connections[sourceData.node][sourceData.type][sourceData.index];
|
||||
for (const index in connections) {
|
||||
if (connections[index].node === destinationData.node && connections[index].type === destinationData.type && connections[index].index === destinationData.index) {
|
||||
// Found the connection to remove
|
||||
connections.splice(parseInt(index, 10), 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
removeAllConnections(data: { setStateDirty: boolean }): void {
|
||||
if (data && data.setStateDirty === true) {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
}
|
||||
this.workflow.connections = {};
|
||||
},
|
||||
|
||||
removeAllNodeConnection(node: INodeUi): void {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
// Remove all source connections
|
||||
if (this.workflow.connections.hasOwnProperty(node.name)) {
|
||||
delete this.workflow.connections[node.name];
|
||||
}
|
||||
|
||||
// Remove all destination connections
|
||||
const indexesToRemove = [];
|
||||
let sourceNode: string, type: string, sourceIndex: string, connectionIndex: string, connectionData: IConnection;
|
||||
for (sourceNode of Object.keys(this.workflow.connections)) {
|
||||
for (type of Object.keys(this.workflow.connections[sourceNode])) {
|
||||
for (sourceIndex of Object.keys(this.workflow.connections[sourceNode][type])) {
|
||||
indexesToRemove.length = 0;
|
||||
for (connectionIndex of Object.keys(this.workflow.connections[sourceNode][type][parseInt(sourceIndex, 10)])) {
|
||||
connectionData = this.workflow.connections[sourceNode][type][parseInt(sourceIndex, 10)][parseInt(connectionIndex, 10)];
|
||||
if (connectionData.node === node.name) {
|
||||
indexesToRemove.push(connectionIndex);
|
||||
}
|
||||
}
|
||||
|
||||
indexesToRemove.forEach((index) => {
|
||||
this.workflow.connections[sourceNode][type][parseInt(sourceIndex, 10)].splice(parseInt(index, 10), 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
renameNodeSelectedAndExecution(nameData: { old: string, new: string }): void {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
// If node has any WorkflowResultData rename also that one that the data
|
||||
// does still get displayed also after node got renamed
|
||||
if (this.workflowExecutionData !== null && this.workflowExecutionData.data && this.workflowExecutionData.data.resultData.runData.hasOwnProperty(nameData.old)) {
|
||||
this.workflowExecutionData.data.resultData.runData[nameData.new] = this.workflowExecutionData.data.resultData.runData[nameData.old];
|
||||
delete this.workflowExecutionData.data.resultData.runData[nameData.old];
|
||||
}
|
||||
|
||||
// In case the renamed node was last selected set it also there with the new name
|
||||
if (uiStore.lastSelectedNode === nameData.old) {
|
||||
uiStore.lastSelectedNode = nameData.new;
|
||||
}
|
||||
|
||||
Vue.set(this.nodeMetadata, nameData.new, this.nodeMetadata[nameData.old]);
|
||||
Vue.delete(this.nodeMetadata, nameData.old);
|
||||
|
||||
if (this.workflow.pinData && this.workflow.pinData.hasOwnProperty(nameData.old)) {
|
||||
Vue.set(this.workflow.pinData, nameData.new, this.workflow.pinData[nameData.old]);
|
||||
Vue.delete(this.workflow.pinData, nameData.old);
|
||||
}
|
||||
|
||||
this.workflowExecutionPairedItemMappings = getPairedItemsMapping(this.workflowExecutionData);
|
||||
},
|
||||
|
||||
resetAllNodesIssues(): boolean {
|
||||
this.workflow.nodes.forEach((node) => {
|
||||
node.issues = undefined;
|
||||
});
|
||||
return true;
|
||||
},
|
||||
|
||||
setNodeIssue(nodeIssueData: INodeIssueData): boolean {
|
||||
const node = this.workflow.nodes.find(node => {
|
||||
return node.name === nodeIssueData.node;
|
||||
});
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
if (nodeIssueData.value === null) {
|
||||
// Remove the value if one exists
|
||||
if (node.issues === undefined || node.issues[nodeIssueData.type] === undefined) {
|
||||
// No values for type exist so nothing has to get removed
|
||||
return true;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
Vue.delete(node.issues, nodeIssueData.type);
|
||||
} else {
|
||||
if (node.issues === undefined) {
|
||||
Vue.set(node, 'issues', {});
|
||||
}
|
||||
// Set/Overwrite the value
|
||||
Vue.set(node.issues!, nodeIssueData.type, nodeIssueData.value);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
addNode(nodeData: INodeUi): void {
|
||||
if (!nodeData.hasOwnProperty('name')) {
|
||||
// All nodes have to have a name
|
||||
// TODO: Check if there is an error or whatever that is supposed to be returned
|
||||
return;
|
||||
}
|
||||
this.workflow.nodes.push(nodeData);
|
||||
},
|
||||
|
||||
removeNode(node: INodeUi): void {
|
||||
Vue.delete(this.nodeMetadata, node.name);
|
||||
|
||||
if (this.workflow.pinData && this.workflow.pinData.hasOwnProperty(node.name)) {
|
||||
Vue.delete(this.workflow.pinData, node.name);
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.workflow.nodes.length; i++) {
|
||||
if (this.workflow.nodes[i].name === node.name) {
|
||||
this.workflow.nodes.splice(i, 1);
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
removeAllNodes(data: { setStateDirty: boolean, removePinData: boolean }): void {
|
||||
if (data.setStateDirty === true) {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
}
|
||||
|
||||
if (data.removePinData) {
|
||||
Vue.set(this.workflow, 'pinData', {});
|
||||
}
|
||||
|
||||
this.workflow.nodes.splice(0, this.workflow.nodes.length);
|
||||
this.nodeMetadata = {};
|
||||
},
|
||||
|
||||
updateNodeProperties(updateInformation: INodeUpdatePropertiesInformation): void {
|
||||
// Find the node that should be updated
|
||||
const node = this.workflow.nodes.find(node => {
|
||||
return node.name === updateInformation.name;
|
||||
});
|
||||
|
||||
if (node) {
|
||||
for (const key of Object.keys(updateInformation.properties)) {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
Vue.set(node, key, updateInformation.properties[key]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setNodeValue(updateInformation: IUpdateInformation): void {
|
||||
// Find the node that should be updated
|
||||
const node = this.workflow.nodes.find(node => {
|
||||
return node.name === updateInformation.name;
|
||||
});
|
||||
|
||||
if (node === undefined || node === null) {
|
||||
throw new Error(`Node with the name "${updateInformation.name}" could not be found to set parameter.`);
|
||||
}
|
||||
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
Vue.set(node, updateInformation.key, updateInformation.value);
|
||||
},
|
||||
|
||||
setNodeParameters(updateInformation: IUpdateInformation): void {
|
||||
// Find the node that should be updated
|
||||
const node = this.workflow.nodes.find(node => {
|
||||
return node.name === updateInformation.name;
|
||||
});
|
||||
|
||||
if (node === undefined || node === null) {
|
||||
throw new Error(`Node with the name "${updateInformation.name}" could not be found to set parameter.`);
|
||||
}
|
||||
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
Vue.set(node, 'parameters', updateInformation.value);
|
||||
|
||||
if (!this.nodeMetadata[node.name]) {
|
||||
Vue.set(this.nodeMetadata, node.name, {});
|
||||
}
|
||||
Vue.set(this.nodeMetadata[node.name], 'parametersLastUpdatedAt', Date.now());
|
||||
},
|
||||
|
||||
addNodeExecutionData(pushData: IPushDataNodeExecuteAfter): void {
|
||||
if (this.workflowExecutionData === null || !this.workflowExecutionData.data) {
|
||||
throw new Error('The "workflowExecutionData" is not initialized!');
|
||||
}
|
||||
if (this.workflowExecutionData.data.resultData.runData[pushData.nodeName] === undefined) {
|
||||
Vue.set(this.workflowExecutionData.data.resultData.runData, pushData.nodeName, []);
|
||||
}
|
||||
this.workflowExecutionData.data.resultData.runData[pushData.nodeName].push(pushData.data);
|
||||
this.workflowExecutionPairedItemMappings = getPairedItemsMapping(this.workflowExecutionData);
|
||||
},
|
||||
clearNodeExecutionData(nodeName: string): void {
|
||||
if (this.workflowExecutionData === null || !this.workflowExecutionData.data) {
|
||||
return;
|
||||
}
|
||||
Vue.delete(this.workflowExecutionData.data.resultData.runData, nodeName);
|
||||
},
|
||||
|
||||
pinDataByNodeName(nodeName: string): INodeExecutionData[] | undefined {
|
||||
if (!this.workflow.pinData || !this.workflow.pinData[nodeName]) return undefined;
|
||||
return this.workflow.pinData[nodeName].map(item => item.json) as INodeExecutionData[];
|
||||
},
|
||||
|
||||
activeNode(): INodeUi | null {
|
||||
// kept here for FE hooks
|
||||
const ndvStore = useNDVStore();
|
||||
return ndvStore.activeNode;
|
||||
},
|
||||
|
||||
// Executions actions
|
||||
|
||||
addActiveExecution(newActiveExecution: IExecutionsCurrentSummaryExtended) : void {
|
||||
// Check if the execution exists already
|
||||
const activeExecution = this.activeExecutions.find(execution => {
|
||||
return execution.id === newActiveExecution.id;
|
||||
});
|
||||
|
||||
if (activeExecution !== undefined) {
|
||||
// Exists already so no need to add it again
|
||||
if (activeExecution.workflowName === undefined) {
|
||||
activeExecution.workflowName = newActiveExecution.workflowName;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.activeExecutions.unshift(newActiveExecution);
|
||||
},
|
||||
finishActiveExecution(finishedActiveExecution: IPushDataExecutionFinished) : void {
|
||||
// Find the execution to set to finished
|
||||
const activeExecution = this.activeExecutions.find(execution => {
|
||||
return execution.id === finishedActiveExecution.executionId;
|
||||
});
|
||||
|
||||
if (activeExecution === undefined) {
|
||||
// The execution could not be found
|
||||
return;
|
||||
}
|
||||
|
||||
if (finishedActiveExecution.executionId !== undefined) {
|
||||
Vue.set(activeExecution, 'id', finishedActiveExecution.executionId);
|
||||
}
|
||||
|
||||
Vue.set(activeExecution, 'finished', finishedActiveExecution.data.finished);
|
||||
Vue.set(activeExecution, 'stoppedAt', finishedActiveExecution.data.stoppedAt);
|
||||
},
|
||||
|
||||
setActiveExecutions(newActiveExecutions: IExecutionsCurrentSummaryExtended[]) : void {
|
||||
Vue.set(this, 'activeExecutions', newActiveExecutions);
|
||||
},
|
||||
|
||||
async loadCurrentWorkflowExecutions (filter: { finished: boolean, status: string }): Promise<IExecutionsSummary[]> {
|
||||
let activeExecutions = [];
|
||||
let finishedExecutions = [];
|
||||
const requestFilter: IDataObject = { workflowId: this.workflowId };
|
||||
|
||||
if (!this.workflowId) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const rootStore = useRootStore();
|
||||
if (filter.status === ''|| !filter.finished) {
|
||||
activeExecutions = await getCurrentExecutions(rootStore.getRestApiContext, requestFilter);
|
||||
}
|
||||
if (filter.status === '' || filter.finished) {
|
||||
if (filter.status === 'waiting') {
|
||||
requestFilter.waitTill = true;
|
||||
} else if (filter.status !== '') {
|
||||
requestFilter.finished = filter.status === 'success';
|
||||
}
|
||||
finishedExecutions = await getFinishedExecutions(rootStore.getRestApiContext, requestFilter);
|
||||
}
|
||||
// context.commit('setTotalFinishedExecutionsCount', finishedExecutions.count);
|
||||
return [...activeExecutions, ...finishedExecutions.results || []];
|
||||
} catch (error) {
|
||||
throw(error);
|
||||
}
|
||||
},
|
||||
deleteExecution (execution: IExecutionsSummary): void {
|
||||
this.currentWorkflowExecutions.splice(this.currentWorkflowExecutions.indexOf(execution), 1);
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user