-
+
@@ -264,6 +264,10 @@ export default mixins(externalHooks, nodeHelpers).extend({
type: Boolean,
default: false,
},
+ executable: {
+ type: Boolean,
+ default: true,
+ },
},
data() {
return {
diff --git a/packages/editor-ui/src/components/WorkflowCard.vue b/packages/editor-ui/src/components/WorkflowCard.vue
index 82de96c28a..8b1e185efa 100644
--- a/packages/editor-ui/src/components/WorkflowCard.vue
+++ b/packages/editor-ui/src/components/WorkflowCard.vue
@@ -26,9 +26,9 @@
-
+
{
@@ -135,7 +135,7 @@ export default mixins(
label: this.$locale.baseText('workflows.item.duplicate'),
value: WORKFLOW_LIST_ITEM_ACTIONS.DUPLICATE,
},
- ].concat(this.credentialPermissions.delete ? [{
+ ].concat(this.workflowPermissions.delete ? [{
label: this.$locale.baseText('workflows.item.delete'),
value: WORKFLOW_LIST_ITEM_ACTIONS.DELETE,
}]: []);
diff --git a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue
new file mode 100644
index 0000000000..9df4e41b3c
--- /dev/null
+++ b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue
@@ -0,0 +1,265 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $locale.baseText('workflows.shareModal.list.delete') }}
+
+
+
+
+
+
+ {{ $locale.baseText(fakeDoor.actionBoxDescription) }}
+
+
+
+
+
+
+
+
+
+ {{ $locale.baseText('workflows.shareModal.changesHint') }}
+
+
+ {{ $locale.baseText('workflows.shareModal.save') }}
+
+
+
+
+
+ {{ $locale.baseText(fakeDoor.actionBoxButtonLabel) }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/editor-ui/src/components/mixins/nodeHelpers.ts b/packages/editor-ui/src/components/mixins/nodeHelpers.ts
index 6b8a5f3e59..433012d23c 100644
--- a/packages/editor-ui/src/components/mixins/nodeHelpers.ts
+++ b/packages/editor-ui/src/components/mixins/nodeHelpers.ts
@@ -245,16 +245,6 @@ export const nodeHelpers = mixins(
let credentialType: ICredentialType | null;
let credentialDisplayName: string;
let selectedCredentials: INodeCredentialsDetails;
- const foreignCredentials = this.credentialsStore.allForeignCredentials;
-
- // TODO: Check if any of the node credentials is found in foreign credentials
- if(foreignCredentials?.some(() => true)){
- return {
- credentials: {
- foreign: [],
- },
- };
- }
const {
authentication,
@@ -350,9 +340,7 @@ export const nodeHelpers = mixins(
}
if (nameMatches.length === 0) {
- if (this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) {
- foundIssues[credentialTypeDescription.name] = [this.$locale.baseText('nodeIssues.credentials.notAvailable')];
- } else {
+ if (!this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowSharing)) {
foundIssues[credentialTypeDescription.name] = [this.$locale.baseText('nodeIssues.credentials.doNotExist', { interpolate: { name: selectedCredentials.name, type: credentialDisplayName } }), this.$locale.baseText('nodeIssues.credentials.doNotExist.hint')];
}
}
diff --git a/packages/editor-ui/src/components/mixins/pushConnection.ts b/packages/editor-ui/src/components/mixins/pushConnection.ts
index 83782d76df..6cca0ab837 100644
--- a/packages/editor-ui/src/components/mixins/pushConnection.ts
+++ b/packages/editor-ui/src/components/mixins/pushConnection.ts
@@ -383,9 +383,10 @@ export const pushConnection = mixins(
// it can be displayed in the node-view
this.updateNodesExecutionIssues();
+ const lastNodeExecuted: string | undefined = runDataExecuted.data.resultData.lastNodeExecuted;
let itemsCount = 0;
- if(runDataExecuted.data.resultData.lastNodeExecuted && !runDataExecutedErrorMessage) {
- itemsCount = runDataExecuted.data.resultData.runData[runDataExecuted.data.resultData.lastNodeExecuted][0].data!.main[0]!.length;
+ if(lastNodeExecuted && runDataExecuted.data.resultData.runData[lastNodeExecuted as string] && !runDataExecutedErrorMessage) {
+ itemsCount = runDataExecuted.data.resultData.runData[lastNodeExecuted as string][0].data!.main[0]!.length;
}
this.$externalHooks().run('pushConnection.executionFinished', {
diff --git a/packages/editor-ui/src/components/mixins/showMessage.ts b/packages/editor-ui/src/components/mixins/showMessage.ts
index b76e0c88b0..e1d59a411d 100644
--- a/packages/editor-ui/src/components/mixins/showMessage.ts
+++ b/packages/editor-ui/src/components/mixins/showMessage.ts
@@ -3,7 +3,7 @@ import { ElNotificationComponent, ElNotificationOptions } from 'element-ui/types
import mixins from 'vue-typed-mixins';
import { externalHooks } from '@/components/mixins/externalHooks';
-import { IRunExecutionData } from 'n8n-workflow';
+import {IExecuteContextData, IRunExecutionData} from 'n8n-workflow';
import type { ElMessageBoxOptions } from 'element-ui/types/message-box';
import type { ElMessageComponent, ElMessageOptions, MessageType } from 'element-ui/types/message';
import { sanitizeHtml } from '@/utils';
@@ -89,13 +89,13 @@ export const showMessage = mixins(externalHooks).extend({
return this.$message(config);
},
- $getExecutionError(data: IRunExecutionData) {
+ $getExecutionError(data: IRunExecutionData | IExecuteContextData) {
const error = data.resultData.error;
let errorMessage: string;
if (data.resultData.lastNodeExecuted && error) {
- errorMessage = error.message;
+ errorMessage = error.message || error.description;
} else {
errorMessage = 'There was a problem executing the workflow!';
diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts
index 00480ca29c..490133391c 100644
--- a/packages/editor-ui/src/constants.ts
+++ b/packages/editor-ui/src/constants.ts
@@ -32,6 +32,7 @@ export const DUPLICATE_MODAL_KEY = 'duplicate';
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';
export const VERSIONS_MODAL_KEY = 'versions';
export const WORKFLOW_SETTINGS_MODAL_KEY = 'settings';
+export const WORKFLOW_SHARE_MODAL_KEY = 'workflowShare';
export const PERSONALIZATION_MODAL_KEY = 'personalization';
export const CONTACT_PROMPT_MODAL_KEY = 'contactPrompt';
export const VALUE_SURVEY_MODAL_KEY = 'valueSurvey';
@@ -315,7 +316,8 @@ export enum VIEWS {
export enum FAKE_DOOR_FEATURES {
ENVIRONMENTS = 'environments',
LOGGING = 'logging',
- SHARING = 'sharing',
+ CREDENTIALS_SHARING = 'credentialsSharing',
+ WORKFLOWS_SHARING = 'workflowsSharing',
}
export const ONBOARDING_PROMPT_TIMEBOX = 14;
@@ -370,6 +372,7 @@ export enum WORKFLOW_MENU_ACTIONS {
*/
export enum EnterpriseEditionFeature {
Sharing = 'sharing',
+ WorkflowSharing = 'workflowSharing',
}
export const MAIN_NODE_PANEL_WIDTH = 360;
@@ -418,6 +421,7 @@ export enum STORES {
UI = 'ui',
USERS = 'users',
WORKFLOWS = 'workflows',
+ WORKFLOWS_EE = 'workflowsEE',
NDV = 'ndv',
TEMPLATES = 'templates',
NODE_TYPES = 'nodeTypes',
diff --git a/packages/editor-ui/src/event-bus/node-view-event-bus.ts b/packages/editor-ui/src/event-bus/node-view-event-bus.ts
new file mode 100644
index 0000000000..cda6848a71
--- /dev/null
+++ b/packages/editor-ui/src/event-bus/node-view-event-bus.ts
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export const nodeViewEventBus = new Vue();
diff --git a/packages/editor-ui/src/permissions.ts b/packages/editor-ui/src/permissions.ts
index 2d26142c64..3e17f1ac3b 100644
--- a/packages/editor-ui/src/permissions.ts
+++ b/packages/editor-ui/src/permissions.ts
@@ -5,7 +5,7 @@
*/
import {IUser, ICredentialsResponse, IRootState, IWorkflowDb} from "@/Interface";
-import {EnterpriseEditionFeature} from "@/constants";
+import {EnterpriseEditionFeature, PLACEHOLDER_EMPTY_WORKFLOW_ID} from "@/constants";
import { useSettingsStore } from "./stores/settings";
export enum UserRole {
@@ -32,9 +32,9 @@ export type IPermissionsTable = IPermissionsTableRow[];
* @param user
* @param table
*/
-export const parsePermissionsTable = (user: IUser, table: IPermissionsTable): IPermissions => {
+export const parsePermissionsTable = (user: IUser | null, table: IPermissionsTable): IPermissions => {
const genericTable = [
- { name: UserRole.InstanceOwner, test: () => user.isOwner },
+ { name: UserRole.InstanceOwner, test: () => user?.isOwner },
];
return [
@@ -53,11 +53,11 @@ export const parsePermissionsTable = (user: IUser, table: IPermissionsTable): IP
* User permissions definition
*/
-export const getCredentialPermissions = (user: IUser, credential: ICredentialsResponse) => {
+export const getCredentialPermissions = (user: IUser | null, credential: ICredentialsResponse) => {
const settingsStore = useSettingsStore();
const table: IPermissionsTable = [
- { name: UserRole.ResourceOwner, test: () => !!(credential && credential.ownedBy && credential.ownedBy.id === user.id) || !settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing) },
- { name: UserRole.ResourceReader, test: () => !!(credential && credential.sharedWith && credential.sharedWith.find((sharee) => sharee.id === user.id)) },
+ { name: UserRole.ResourceOwner, test: () => !!(credential && credential.ownedBy && credential.ownedBy.id === user?.id) || !settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing) },
+ { name: UserRole.ResourceReader, test: () => !!(credential && credential.sharedWith && credential.sharedWith.find((sharee) => sharee.id === user?.id)) },
{ name: 'read', test: [UserRole.ResourceOwner, UserRole.InstanceOwner, UserRole.ResourceReader] },
{ name: 'save', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
{ name: 'updateName', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
@@ -71,12 +71,13 @@ export const getCredentialPermissions = (user: IUser, credential: ICredentialsRe
return parsePermissionsTable(user, table);
};
-export const getWorkflowPermissions = (user: IUser, workflow: IWorkflowDb) => {
+export const getWorkflowPermissions = (user: IUser | null, workflow: IWorkflowDb) => {
+ const settingsStore = useSettingsStore();
+ const isNewWorkflow = workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID;
+
const table: IPermissionsTable = [
- // { name: UserRole.ResourceOwner, test: () => !!(workflow && workflow.ownedBy && workflow.ownedBy.id === user.id) || !useSettingsStore().isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing) },
- { name: UserRole.ResourceOwner, test: () => true },
- // { name: UserRole.ResourceReader, test: () => !!(workflow && workflow.sharedWith && workflow.sharedWith.find((sharee) => sharee.id === user.id)) },
- { name: UserRole.ResourceReader, test: () => true },
+ { name: UserRole.ResourceOwner, test: () => !!(isNewWorkflow || workflow && workflow.ownedBy && workflow.ownedBy.id === user?.id) || !settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowSharing) },
+ { name: UserRole.ResourceReader, test: () => !!(workflow && workflow.sharedWith && workflow.sharedWith.find((sharee) => sharee.id === user?.id)) },
{ name: 'read', test: [UserRole.ResourceOwner, UserRole.InstanceOwner, UserRole.ResourceReader] },
{ name: 'save', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
{ name: 'updateName', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json
index 5df1906214..1be9eaca15 100644
--- a/packages/editor-ui/src/plugins/i18n/locales/en.json
+++ b/packages/editor-ui/src/plugins/i18n/locales/en.json
@@ -486,6 +486,11 @@
"fakeDoor.credentialEdit.sharing.actionBox.title.cloud.upgrade": "Upgrade to add users",
"fakeDoor.credentialEdit.sharing.actionBox.description.cloud.upgrade": "Power and Pro plan users can create multiple user accounts and share credentials. (Sharing workflows is coming soon)",
"fakeDoor.credentialEdit.sharing.actionBox.button.cloud.upgrade": "Upgrade",
+ "fakeDoor.workflowsSharing.title.cloud.upgrade": "Upgrade to add users",
+ "fakeDoor.workflowsSharing.description": "Sharing workflows with others is currently available only on n8n cloud, our hosted offering.",
+ "fakeDoor.workflowsSharing.description.cloud.upgrade": "Power and Pro plan users can create multiple user accounts and share workflows.",
+ "fakeDoor.workflowsSharing.button": "Explore n8n cloud",
+ "fakeDoor.workflowsSharing.button.cloud.upgrade": "Upgrade",
"fakeDoor.settings.environments.name": "Environments",
"fakeDoor.settings.environments.infoText": "Environments allow you to use different settings and credentials in a workflow when you're building it vs when it's running in production",
"fakeDoor.settings.environments.actionBox.title": "We’re working on environments (as a paid feature)",
@@ -1247,6 +1252,7 @@
"workflowActivator.showMessage.displayActivationError.title": "Problem activating workflow",
"workflowActivator.theWorkflowIsSetToBeActiveBut": "The workflow is activated but could not be started.
Click to display error message.",
"workflowActivator.thisWorkflowHasNoTriggerNodes": "This workflow has no trigger nodes that require activation",
+ "workflowDetails.share": "Share",
"workflowDetails.active": "Active",
"workflowDetails.addTag": "Add tag",
"workflowDetails.chooseOrCreateATag": "Choose or create a tag",
@@ -1361,6 +1367,18 @@
"workflows.empty.description": "Create your first workflow",
"workflows.empty.startFromScratch": "Start from scratch",
"workflows.empty.browseTemplates": "Browse templates",
+ "workflows.shareModal.title": "Share '{name}'",
+ "workflows.shareModal.select.placeholder": "Add people",
+ "workflows.shareModal.list.delete": "Remove access",
+ "workflows.shareModal.list.delete.confirm.title": "Remove {name}'s access?",
+ "workflows.shareModal.list.delete.confirm.message": "This might cause the workflow to stop working: if {name} is the only user with access to credentials used in this workflow, those credentials will also be removed from {workflow}.",
+ "workflows.shareModal.list.delete.confirm.confirmButtonText": "Remove access",
+ "workflows.shareModal.list.delete.confirm.cancelButtonText": "Cancel",
+ "workflows.shareModal.save": "Save",
+ "workflows.shareModal.changesHint": "You made changes",
+ "workflows.shareModal.notAvailable": "Sharing workflows with others is currently available only on n8n cloud, our hosted offering.",
+ "workflows.shareModal.notAvailable.button": "Explore n8n cloud",
+ "workflows.roles.editor": "Editor",
"importCurlModal.title": "Import cURL command",
"importCurlModal.input.label": "cURL Command",
"importCurlModal.input.placeholder": "Paste the cURL command here",
diff --git a/packages/editor-ui/src/stores/credentials.ts b/packages/editor-ui/src/stores/credentials.ts
index ead14f8b83..f862bdad3f 100644
--- a/packages/editor-ui/src/stores/credentials.ts
+++ b/packages/editor-ui/src/stores/credentials.ts
@@ -1,4 +1,4 @@
-import { createNewCredential, deleteCredential, getAllCredentials, getCredentialData, getCredentialsNewName, getCredentialTypes, getForeignCredentials, oAuth1CredentialAuthorize, oAuth2CredentialAuthorize, testCredential, updateCredential } from "@/api/credentials";
+import { createNewCredential, deleteCredential, getAllCredentials, getCredentialData, getCredentialsNewName, getCredentialTypes, oAuth1CredentialAuthorize, oAuth2CredentialAuthorize, testCredential, updateCredential } from "@/api/credentials";
import { setCredentialSharedWith } from "@/api/credentials.ee";
import { getAppNameFromCredType } from "@/components/helpers";
import { EnterpriseEditionFeature, STORES } from "@/constants";
@@ -33,10 +33,6 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
return Object.values(this.credentials)
.sort((a, b) => a.name.localeCompare(b.name));
},
- allForeignCredentials(): ICredentialsResponse[] {
- return Object.values(this.foreignCredentials || {})
- .sort((a, b) => a.name.localeCompare(b.name));
- },
allCredentialsByType(): {[type: string]: ICredentialsResponse[]} {
const credentials = this.allCredentials;
const types = this.allCredentialTypes;
@@ -53,6 +49,9 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
getCredentialById() {
return (id: string): ICredentialsResponse => this.credentials[id];
},
+ foreignCredentialsById(): ICredentialMap {
+ return Object.fromEntries(Object.entries(this.credentials).filter(([_, credential]) => credential.hasOwnProperty('currentUserHasAccess')));
+ },
getCredentialByIdAndType() {
return (id: string, type: string): ICredentialsResponse | undefined => {
const credential = this.credentials[id];
@@ -138,13 +137,12 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
return accu;
}, {});
},
- setForeignCredentials(credentials: ICredentialsResponse[]): void {
- this.foreignCredentials = credentials.reduce((accu: ICredentialMap, cred: ICredentialsResponse) => {
+ addCredentials(credentials: ICredentialsResponse[]): void {
+ credentials.forEach((cred: ICredentialsResponse) => {
if (cred.id) {
- accu[cred.id] = cred;
+ this.credentials[cred.id] = { ...this.credentials[cred.id], ...cred };
}
- return accu;
- }, {});
+ });
},
upsertCredential(credential: ICredentialsResponse): void {
if (credential.id) {
@@ -168,12 +166,6 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
this.setCredentials(credentials);
return credentials;
},
- async fetchForeignCredentials(): Promise {
- const rootStore = useRootStore();
- const credentials = await getForeignCredentials(rootStore.getRestApiContext);
- this.setForeignCredentials(credentials);
- return credentials;
- },
async getCredentialData({ id }: {id: string}): Promise {
const rootStore = useRootStore();
return await getCredentialData(rootStore.getRestApiContext, id);
diff --git a/packages/editor-ui/src/stores/ui.ts b/packages/editor-ui/src/stores/ui.ts
index 1bafccd2af..d8c4f6d2aa 100644
--- a/packages/editor-ui/src/stores/ui.ts
+++ b/packages/editor-ui/src/stores/ui.ts
@@ -26,7 +26,7 @@ import {
VERSIONS_MODAL_KEY,
VIEWS,
WORKFLOW_ACTIVE_MODAL_KEY,
- WORKFLOW_SETTINGS_MODAL_KEY,
+ WORKFLOW_SETTINGS_MODAL_KEY, WORKFLOW_SHARE_MODAL_KEY,
} from "@/constants";
import {
CurlToJSONResponse,
@@ -97,6 +97,9 @@ export const useUIStore = defineStore(STORES.UI, {
[EXECUTIONS_MODAL_KEY]: {
open: false,
},
+ [WORKFLOW_SHARE_MODAL_KEY]: {
+ open: false,
+ },
[WORKFLOW_ACTIVE_MODAL_KEY]: {
open: false,
},
@@ -141,13 +144,22 @@ export const useUIStore = defineStore(STORES.UI, {
uiLocations: ['settings'],
},
{
- id: FAKE_DOOR_FEATURES.SHARING,
+ id: FAKE_DOOR_FEATURES.CREDENTIALS_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'],
},
+ {
+ id: FAKE_DOOR_FEATURES.WORKFLOWS_SHARING,
+ featureName: 'fakeDoor.workflowsSharing.name',
+ actionBoxTitle: 'workflows.shareModal.title', // Use this translation in modal title when removing fakeDoor
+ actionBoxDescription: 'fakeDoor.workflowsSharing.description',
+ actionBoxButtonLabel: 'fakeDoor.workflowsSharing.button',
+ linkURL: 'https://n8n.cloud',
+ uiLocations: ['workflowShareModal'],
+ },
],
draggable: {
isDragging: false,
diff --git a/packages/editor-ui/src/stores/users.ts b/packages/editor-ui/src/stores/users.ts
index bbf658ebe1..514d2c919f 100644
--- a/packages/editor-ui/src/stores/users.ts
+++ b/packages/editor-ui/src/stores/users.ts
@@ -23,8 +23,8 @@ export const useUsersStore = defineStore(STORES.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];
+ getUserById(state) {
+ return (userId: string): IUser | null => state.users[userId];
},
globalRoleName(): string {
return this.currentUser?.globalRole?.name || '';
diff --git a/packages/editor-ui/src/stores/workflows.ee.ts b/packages/editor-ui/src/stores/workflows.ee.ts
new file mode 100644
index 0000000000..cbd33f072e
--- /dev/null
+++ b/packages/editor-ui/src/stores/workflows.ee.ts
@@ -0,0 +1,67 @@
+import Vue from 'vue';
+import {
+ IUser,
+} from '../Interface';
+import {setWorkflowSharedWith} from "@/api/workflows.ee";
+import {EnterpriseEditionFeature, STORES} from "@/constants";
+import {useRootStore} from "@/stores/n8nRootStore";
+import {useSettingsStore} from "@/stores/settings";
+import {defineStore} from "pinia";
+import {useWorkflowsStore} from "@/stores/workflows";
+
+// @TODO Move to workflows store as part of workflows store refactoring
+//
+export const useWorkflowsEEStore = defineStore(STORES.WORKFLOWS_EE, {
+ state() { return {}; },
+ actions: {
+ setWorkflowOwnedBy(payload: { workflowId: string, ownedBy: Partial }): void {
+ const workflowsStore = useWorkflowsStore();
+
+ Vue.set(workflowsStore.workflowsById[payload.workflowId], 'ownedBy', payload.ownedBy);
+ Vue.set(workflowsStore.workflow, 'ownedBy', payload.ownedBy);
+ },
+ setWorkflowSharedWith(payload: { workflowId: string, sharedWith: Array> }): void {
+ const workflowsStore = useWorkflowsStore();
+
+ Vue.set(workflowsStore.workflowsById[payload.workflowId], 'sharedWith', payload.sharedWith);
+ Vue.set(workflowsStore.workflow, 'sharedWith', payload.sharedWith);
+ },
+ addWorkflowSharee(payload: { workflowId: string, sharee: Partial }): void {
+ const workflowsStore = useWorkflowsStore();
+
+ Vue.set(
+ workflowsStore.workflowsById[payload.workflowId],
+ 'sharedWith',
+ (workflowsStore.workflowsById[payload.workflowId].sharedWith || []).concat([payload.sharee]),
+ );
+ },
+ removeWorkflowSharee(payload: { workflowId: string, sharee: Partial }): void {
+ const workflowsStore = useWorkflowsStore();
+
+ Vue.set(
+ workflowsStore.workflowsById[payload.workflowId],
+ 'sharedWith',
+ (workflowsStore.workflowsById[payload.workflowId].sharedWith || [])
+ .filter((sharee) => sharee.id !== payload.sharee.id),
+ );
+ },
+ async saveWorkflowSharedWith(payload: { sharedWith: Array>; workflowId: string; }): Promise {
+ const rootStore = useRootStore();
+ const settingsStore = useSettingsStore();
+
+ if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowSharing)) {
+ await setWorkflowSharedWith(
+ rootStore.getRestApiContext,
+ payload.workflowId,
+ {
+ shareWithIds: payload.sharedWith.map((sharee) => sharee.id as string),
+ },
+ );
+
+ this.setWorkflowSharedWith(payload);
+ }
+ },
+ },
+});
+
+export default useWorkflowsEEStore;
diff --git a/packages/editor-ui/src/stores/workflows.ts b/packages/editor-ui/src/stores/workflows.ts
index d8f98d2840..b5c045cc17 100644
--- a/packages/editor-ui/src/stores/workflows.ts
+++ b/packages/editor-ui/src/stores/workflows.ts
@@ -290,7 +290,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
if (this.workflowsById[workflowId]) {
this.workflowsById[workflowId].active = true;
}
-
+
},
setWorkflowInactive(workflowId: string): void {
diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue
index 81a5e5bd78..d74fae4b99 100644
--- a/packages/editor-ui/src/views/NodeView.vue
+++ b/packages/editor-ui/src/views/NodeView.vue
@@ -149,7 +149,7 @@ import {
STICKY_NODE_TYPE,
VIEWS,
WEBHOOK_NODE_TYPE,
- TRIGGER_NODE_FILTER,
+ TRIGGER_NODE_FILTER, EnterpriseEditionFeature,
} from '@/constants';
import { copyPaste } from '@/components/mixins/copyPaste';
import { externalHooks } from '@/components/mixins/externalHooks';
@@ -221,6 +221,7 @@ import { useSettingsStore } from '@/stores/settings';
import { useUsersStore } from '@/stores/users';
import { getNodeViewTab } from '@/components/helpers';
import { Route, RawLocation } from 'vue-router';
+import { nodeViewEventBus } from '@/event-bus/node-view-event-bus';
import { useWorkflowsStore } from '@/stores/workflows';
import { useRootStore } from '@/stores/n8nRootStore';
import { useNDVStore } from '@/stores/ndv';
@@ -231,6 +232,7 @@ import { useTagsStore } from '@/stores/tags';
import { useNodeCreatorStore } from '@/stores/nodeCreator';
import { dataPinningEventBus } from '@/event-bus/data-pinning-event-bus';
import { useCanvasStore } from '@/stores/canvas';
+import useWorkflowsEEStore from "@/stores/workflows.ee";
interface AddNodeOptions {
position?: XYPosition;
@@ -395,6 +397,7 @@ export default mixins(
useUIStore,
useUsersStore,
useWorkflowsStore,
+ useWorkflowsEEStore,
),
nativelyNumberSuffixedDefaults(): string[] {
return this.rootStore.nativelyNumberSuffixedDefaults;
@@ -830,6 +833,7 @@ export default mixins(
),
);
}
+
this.workflowsStore.setActive(data.active || false);
this.workflowsStore.setWorkflowId(workflowId);
this.workflowsStore.setWorkflowName({ newName: data.name, setStateDirty: false });
@@ -837,6 +841,32 @@ export default mixins(
this.workflowsStore.setWorkflowPinData(data.pinData || {});
this.workflowsStore.setWorkflowHash(data.hash);
+ // @TODO
+ this.workflowsStore.addWorkflow({
+ id: data.id,
+ name: data.name,
+ ownedBy: data.ownedBy,
+ sharedWith: data.sharedWith,
+ tags: data.tags || [],
+ active: data.active,
+ createdAt: data.createdAt,
+ updatedAt: data.updatedAt,
+ nodes: data.nodes,
+ connections: data.connections,
+ });
+ this.workflowsEEStore.setWorkflowOwnedBy({
+ workflowId: data.id,
+ ownedBy: data.ownedBy,
+ });
+ this.workflowsEEStore.setWorkflowSharedWith({
+ workflowId: data.id,
+ sharedWith: data.sharedWith,
+ });
+
+ if (data.usedCredentials) {
+ this.credentialsStore.addCredentials(data.usedCredentials);
+ }
+
const tags = (data.tags || []) as ITag[];
const tagIds = tags.map((tag) => tag.id);
this.workflowsStore.setWorkflowTagIds(tagIds || []);
@@ -2372,6 +2402,15 @@ export default mixins(
newNodeData.webhookId = uuid();
}
+ if (newNodeData.credentials && this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowSharing)) {
+ const foreignCredentials = this.credentialsStore.foreignCredentialsById;
+ newNodeData.credentials = Object.fromEntries(
+ Object.entries(newNodeData.credentials).filter(([_, credential]) => {
+ return credential.id && (!foreignCredentials[credential.id] || foreignCredentials[credential.id]?.currentUserHasAccess);
+ }),
+ );
+ }
+
await this.addNodes([newNodeData]);
const pinData = this.workflowsStore.pinDataByNodeName(nodeName);
@@ -3065,6 +3104,7 @@ export default mixins(
this.workflowsStore.resetAllNodesIssues();
// vm.$forceUpdate();
+ this.workflowsStore.$patch({ workflow: {} });
this.workflowsStore.setActive(false);
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
this.workflowsStore.setWorkflowName({ newName: '', setStateDirty: false });
@@ -3094,7 +3134,6 @@ export default mixins(
},
async loadCredentials(): Promise {
await this.credentialsStore.fetchAllCredentials();
- await this.credentialsStore.fetchForeignCredentials();
},
async loadNodesProperties(nodeInfos: INodeTypeNameVersion[]): Promise {
const allNodes: INodeTypeDescription[] = this.nodeTypesStore.allNodeTypes;
@@ -3217,6 +3256,10 @@ export default mixins(
onAddNode({ nodeTypeName, position }: { nodeTypeName: string; position?: [number, number] }) {
this.addNode(nodeTypeName, { position });
},
+ async saveCurrentWorkflowExternal(callback: () => void) {
+ await this.saveCurrentWorkflow();
+ callback?.();
+ },
},
async mounted() {
this.$titleReset();
@@ -3304,8 +3347,6 @@ export default mixins(
}, promptTimeout);
}
}
- dataPinningEventBus.$on('pin-data', this.addPinDataConnections);
- dataPinningEventBus.$on('unpin-data', this.removePinDataConnections);
},
activated() {
const openSideMenu = this.uiStore.addFirstStepOnLoad;
@@ -3317,23 +3358,29 @@ export default mixins(
document.addEventListener('keydown', this.keyDown);
document.addEventListener('keyup', this.keyUp);
window.addEventListener('message', this.onPostMessageReceived);
+
this.$root.$on('newWorkflow', this.newWorkflow);
this.$root.$on('importWorkflowData', this.onImportWorkflowDataEvent);
this.$root.$on('importWorkflowUrl', this.onImportWorkflowUrlEvent);
+
dataPinningEventBus.$on('pin-data', this.addPinDataConnections);
dataPinningEventBus.$on('unpin-data', this.removePinDataConnections);
+ nodeViewEventBus.$on('saveWorkflow', this.saveCurrentWorkflowExternal);
+
this.canvasStore.isDemo = this.isDemo;
},
deactivated () {
document.removeEventListener('keydown', this.keyDown);
document.removeEventListener('keyup', this.keyUp);
window.removeEventListener('message', this.onPostMessageReceived);
+
this.$root.$off('newWorkflow', this.newWorkflow);
this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent);
this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent);
dataPinningEventBus.$off('pin-data', this.addPinDataConnections);
dataPinningEventBus.$off('unpin-data', this.removePinDataConnections);
+ nodeViewEventBus.$off('saveWorkflow', this.saveCurrentWorkflowExternal);
},
destroyed() {
this.resetWorkspace();
@@ -3342,9 +3389,6 @@ export default mixins(
this.$root.$off('newWorkflow', this.newWorkflow);
this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent);
this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent);
-
- dataPinningEventBus.$off('pin-data', this.addPinDataConnections);
- dataPinningEventBus.$off('unpin-data', this.removePinDataConnections);
},
});
diff --git a/packages/editor-ui/src/views/WorkflowsView.vue b/packages/editor-ui/src/views/WorkflowsView.vue
index 998ad45133..f7e151d384 100644
--- a/packages/editor-ui/src/views/WorkflowsView.vue
+++ b/packages/editor-ui/src/views/WorkflowsView.vue
@@ -7,7 +7,7 @@
:filters="filters"
:additional-filters-handler="onFilter"
:show-aside="allWorkflows.length > 0"
- :shareable="false"
+ :shareable="isShareable"
@click:add="addWorkflow"
@update:filters="filters = $event"
>
@@ -69,7 +69,7 @@ import PageViewLayoutList from "@/components/layouts/PageViewLayoutList.vue";
import WorkflowCard from "@/components/WorkflowCard.vue";
import TemplateCard from "@/components/TemplateCard.vue";
import { debounceHelper } from '@/components/mixins/debounce';
-import {VIEWS} from '@/constants';
+import {EnterpriseEditionFeature, VIEWS} from '@/constants';
import Vue from "vue";
import {ITag, IUser, IWorkflowDb} from "@/Interface";
import TagsDropdown from "@/components/TagsDropdown.vue";
@@ -118,6 +118,9 @@ export default mixins(
allWorkflows(): IWorkflowDb[] {
return this.workflowsStore.allWorkflows;
},
+ isShareable(): boolean {
+ return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowSharing);
+ },
},
methods: {
addWorkflow() {
@@ -188,8 +191,7 @@ export default mixins(
svg {
width: 48px!important;
color: var(--color-foreground-dark);
- transition: color 0.3s ease;
- }
+ transition: color 0.3s ease;}
}