feat: Add workflow sharing functionality and permissions (#4370)
* feat(editor): extract credentials view into reusable layout components for workflows view * feat(editor): add workflow card and start work on empty state * feat: add hoverable card and finish workflows empty state * fix: undo workflows response interface changes * chore: fix linting issues. * fix: remove enterprise sharing env schema * fix(editor): fix workflows resource view when sharing is enabled * fix: change owner tag design and order * feat: add personalization survey on workflows page * fix: update component snapshots * feat: refactored workflow card to use workflow-activator properly * fix: fix workflow activator and proptypes * fix: hide owner tag for workflow card until sharing is available * fix: fixed ownedBy and sharedWith appearing for workflows list * feat: update tags component design * refactor: change resource filter select to n8n-user-select * fix: made telemetry messages reusable * chore: remove unused import * refactor: fix component name casing * refactor: use Vue.set to make workflow property reactive * feat: add support for clicking on tags for filtering * chore: fix tags linting issues * fix: fix resources list layout when title words are very long * refactor: add active and inactive status text to workflow activator * fix: fix credentials and workflows sorting when name contains leading whitespace * fix: remove wrongfully added style tag * feat: add translations and storybook examples for truncated tags * fix: remove enterprise sharing env from schema * refactor: fix workflows module and workflows field store naming conflict * feat: add workflow share button and open dummy modal * feat: add workflow sharing modal (in progress) * feat: add message when sharing disabled * feat: add sharing messages based on flags * feat: add workflow sharing api integration and readonly state handling * fix: change how foreign credentials are handled * refactor: migrate newly added workflow sharing store methods to pinia * fix: update foreign credentials handler and add executable prop to node-settings * fix: fix credentials display issue caused by addCredentials override * fix: fix various issues when sharing from empty state * fix: update node duplication credentials * fix: revert defautl values for sharing env * feat: hide share button behind feature flag * chore: add env variable for sharing feature (testing only) * fix: change enterprise-edition component casing
This commit is contained in:
@@ -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<ICredentialsResponse[]> {
|
||||
const rootStore = useRootStore();
|
||||
const credentials = await getForeignCredentials(rootStore.getRestApiContext);
|
||||
this.setForeignCredentials(credentials);
|
||||
return credentials;
|
||||
},
|
||||
async getCredentialData({ id }: {id: string}): Promise<ICredentialsResponse | ICredentialsDecryptedResponse | undefined> {
|
||||
const rootStore = useRootStore();
|
||||
return await getCredentialData(rootStore.getRestApiContext, id);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 || '';
|
||||
|
||||
67
packages/editor-ui/src/stores/workflows.ee.ts
Normal file
67
packages/editor-ui/src/stores/workflows.ee.ts
Normal file
@@ -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<IUser> }): 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<Partial<IUser>> }): 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<IUser> }): 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<IUser> }): 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<Partial<IUser>>; workflowId: string; }): Promise<void> {
|
||||
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;
|
||||
@@ -290,7 +290,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||
if (this.workflowsById[workflowId]) {
|
||||
this.workflowsById[workflowId].active = true;
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
|
||||
setWorkflowInactive(workflowId: string): void {
|
||||
|
||||
Reference in New Issue
Block a user