mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-26 05:04:05 -08:00
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:
parent
d1ffc58aa4
commit
898c25fd7e
|
@ -892,6 +892,7 @@ export const schema = {
|
||||||
sharing: {
|
sharing: {
|
||||||
format: Boolean,
|
format: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
env: 'N8N_SHARING_ENABLED',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// This is a temporary flag (acting as feature toggle)
|
// This is a temporary flag (acting as feature toggle)
|
||||||
|
@ -899,6 +900,7 @@ export const schema = {
|
||||||
workflowSharingEnabled: {
|
workflowSharingEnabled: {
|
||||||
format: Boolean,
|
format: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
env: 'N8N_WORKFLOW_SHARING_ENABLED',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,13 @@
|
||||||
>
|
>
|
||||||
{{ t('nds.auth.roles.owner') }}
|
{{ t('nds.auth.roles.owner') }}
|
||||||
</n8n-badge>
|
</n8n-badge>
|
||||||
|
<slot
|
||||||
|
v-if="!user.isOwner && !readonly"
|
||||||
|
name="actions"
|
||||||
|
:user="user"
|
||||||
|
/>
|
||||||
<n8n-action-toggle
|
<n8n-action-toggle
|
||||||
v-if="!user.isOwner && !readonly && getActions(user).length > 0"
|
v-if="!user.isOwner && !readonly && getActions(user).length > 0 && actions.length > 0"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
:actions="getActions(user)"
|
:actions="getActions(user)"
|
||||||
theme="dark"
|
theme="dark"
|
||||||
|
@ -35,6 +40,7 @@ import N8nUserInfo from '../N8nUserInfo';
|
||||||
import Locale from '../../mixins/locale';
|
import Locale from '../../mixins/locale';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { t } from '../../locale';
|
import { t } from '../../locale';
|
||||||
|
import {PropType} from "vue";
|
||||||
|
|
||||||
export interface IUserListAction {
|
export interface IUserListAction {
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -71,6 +77,10 @@ export default mixins(Locale).extend({
|
||||||
type: String,
|
type: String,
|
||||||
default: () => t('nds.usersList.reinviteUser'),
|
default: () => t('nds.usersList.reinviteUser'),
|
||||||
},
|
},
|
||||||
|
actions: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default: () => ['delete', 'reinvite'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
sortedUsers(): IUser[] {
|
sortedUsers(): IUser[] {
|
||||||
|
@ -113,6 +123,7 @@ export default mixins(Locale).extend({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getActions(user: IUser): IUserListAction[] {
|
getActions(user: IUser): IUserListAction[] {
|
||||||
|
const actions = [];
|
||||||
const DELETE: IUserListAction = {
|
const DELETE: IUserListAction = {
|
||||||
label: this.deleteLabel as string,
|
label: this.deleteLabel as string,
|
||||||
value: 'delete',
|
value: 'delete',
|
||||||
|
@ -127,16 +138,17 @@ export default mixins(Locale).extend({
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.firstName) {
|
if (!user.firstName) {
|
||||||
return [
|
if (this.actions.includes('reinvite')) {
|
||||||
DELETE,
|
actions.push(REINVITE);
|
||||||
];
|
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
REINVITE,
|
|
||||||
DELETE,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.actions.includes('delete')) {
|
||||||
|
actions.push(DELETE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
},
|
},
|
||||||
onUserAction(user: IUser, action: string): void {
|
onUserAction(user: IUser, action: string): void {
|
||||||
if (action === 'delete' || action === 'reinvite') {
|
if (action === 'delete' || action === 'reinvite') {
|
||||||
|
|
|
@ -37,6 +37,7 @@ import {
|
||||||
NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { FAKE_DOOR_FEATURES } from './constants';
|
import { FAKE_DOOR_FEATURES } from './constants';
|
||||||
|
import {ICredentialsDb} from "n8n";
|
||||||
|
|
||||||
export * from 'n8n-design-system/src/types';
|
export * from 'n8n-design-system/src/types';
|
||||||
|
|
||||||
|
@ -320,6 +321,7 @@ export interface IWorkflowDb {
|
||||||
sharedWith?: Array<Partial<IUser>>;
|
sharedWith?: Array<Partial<IUser>>;
|
||||||
ownedBy?: Partial<IUser>;
|
ownedBy?: Partial<IUser>;
|
||||||
hash: string;
|
hash: string;
|
||||||
|
usedCredentials?: Array<Partial<ICredentialsDb>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identical to cli.Interfaces.ts
|
// Identical to cli.Interfaces.ts
|
||||||
|
@ -332,6 +334,14 @@ export interface IWorkflowShortResponse {
|
||||||
tags: ITag[];
|
tags: ITag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IWorkflowsShareResponse {
|
||||||
|
id: string;
|
||||||
|
createdAt: number | string;
|
||||||
|
updatedAt: number | string;
|
||||||
|
sharedWith?: Array<Partial<IUser>>;
|
||||||
|
ownedBy?: Partial<IUser>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Identical or almost identical to cli.Interfaces.ts
|
// Identical or almost identical to cli.Interfaces.ts
|
||||||
|
|
||||||
|
@ -346,12 +356,17 @@ export interface IShareCredentialsPayload {
|
||||||
shareWithIds: string[];
|
shareWithIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IShareWorkflowsPayload {
|
||||||
|
shareWithIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ICredentialsResponse extends ICredentialsEncrypted {
|
export interface ICredentialsResponse extends ICredentialsEncrypted {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: number | string;
|
createdAt: number | string;
|
||||||
updatedAt: number | string;
|
updatedAt: number | string;
|
||||||
sharedWith?: Array<Partial<IUser>>;
|
sharedWith?: Array<Partial<IUser>>;
|
||||||
ownedBy?: Partial<IUser>;
|
ownedBy?: Partial<IUser>;
|
||||||
|
currentUserHasAccess?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICredentialsBase {
|
export interface ICredentialsBase {
|
||||||
|
@ -987,7 +1002,6 @@ export interface ICredentialMap {
|
||||||
export interface ICredentialsState {
|
export interface ICredentialsState {
|
||||||
credentialTypes: ICredentialTypeMap;
|
credentialTypes: ICredentialTypeMap;
|
||||||
credentials: ICredentialMap;
|
credentials: ICredentialMap;
|
||||||
foreignCredentials?: ICredentialMap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITagsState {
|
export interface ITagsState {
|
||||||
|
@ -1112,7 +1126,7 @@ export type IFakeDoor = {
|
||||||
uiLocations: IFakeDoorLocation[],
|
uiLocations: IFakeDoorLocation[],
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IFakeDoorLocation = 'settings' | 'credentialsModal';
|
export type IFakeDoorLocation = 'settings' | 'credentialsModal' | 'workflowShareModal';
|
||||||
|
|
||||||
export type INodeFilterType = "Regular" | "Trigger" | "All";
|
export type INodeFilterType = "Regular" | "Trigger" | "All";
|
||||||
|
|
||||||
|
|
|
@ -51,9 +51,3 @@ export async function oAuth2CredentialAuthorize(context: IRestApiContext, data:
|
||||||
export async function testCredential(context: IRestApiContext, data: INodeCredentialTestRequest): Promise<INodeCredentialTestResult> {
|
export async function testCredential(context: IRestApiContext, data: INodeCredentialTestRequest): Promise<INodeCredentialTestResult> {
|
||||||
return makeRestApiRequest(context, 'POST', '/credentials/test', data as unknown as IDataObject);
|
return makeRestApiRequest(context, 'POST', '/credentials/test', data as unknown as IDataObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getForeignCredentials(context: IRestApiContext): Promise<ICredentialsResponse[]> {
|
|
||||||
// TODO: Get foreign credentials
|
|
||||||
//return await makeRestApiRequest(context, 'GET', '/foreign-credentials');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
13
packages/editor-ui/src/api/workflows.ee.ts
Normal file
13
packages/editor-ui/src/api/workflows.ee.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {
|
||||||
|
IRestApiContext,
|
||||||
|
IShareWorkflowsPayload,
|
||||||
|
IWorkflowsShareResponse,
|
||||||
|
} from '@/Interface';
|
||||||
|
import { makeRestApiRequest } from './helpers';
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export async function setWorkflowSharedWith(context: IRestApiContext, id: string, data: IShareWorkflowsPayload): Promise<IWorkflowsShareResponse> {
|
||||||
|
return makeRestApiRequest(context, 'PUT', `/workflows/${id}/share`, data as unknown as IDataObject);
|
||||||
|
}
|
|
@ -73,7 +73,7 @@ export default mixins(
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async onAddSharee(userId: string) {
|
async onAddSharee(userId: string) {
|
||||||
const sharee = this.usersStore.getUserById(userId);
|
const sharee = { ...this.usersStore.getUserById(userId), isOwner: false };
|
||||||
this.$emit('change', (this.credentialData.sharedWith || []).concat(sharee));
|
this.$emit('change', (this.credentialData.sharedWith || []).concat(sharee));
|
||||||
},
|
},
|
||||||
async onRemoveSharee(userId: string) {
|
async onRemoveSharee(userId: string) {
|
||||||
|
|
|
@ -67,6 +67,15 @@
|
||||||
<span class="activator">
|
<span class="activator">
|
||||||
<WorkflowActivator :workflow-active="isWorkflowActive" :workflow-id="currentWorkflowId" />
|
<WorkflowActivator :workflow-active="isWorkflowActive" :workflow-id="currentWorkflowId" />
|
||||||
</span>
|
</span>
|
||||||
|
<enterprise-edition :features="[EnterpriseEditionFeature.WorkflowSharing]">
|
||||||
|
<n8n-button
|
||||||
|
type="tertiary"
|
||||||
|
class="mr-2xs"
|
||||||
|
@click="onShareButtonClick"
|
||||||
|
>
|
||||||
|
{{ $locale.baseText('workflowDetails.share') }}
|
||||||
|
</n8n-button>
|
||||||
|
</enterprise-edition>
|
||||||
<SaveButton
|
<SaveButton
|
||||||
type="secondary"
|
type="secondary"
|
||||||
:saved="!this.isDirty && !this.isNewWorkflow"
|
:saved="!this.isDirty && !this.isNewWorkflow"
|
||||||
|
@ -87,10 +96,12 @@ import Vue from "vue";
|
||||||
import mixins from "vue-typed-mixins";
|
import mixins from "vue-typed-mixins";
|
||||||
import {
|
import {
|
||||||
DUPLICATE_MODAL_KEY,
|
DUPLICATE_MODAL_KEY,
|
||||||
|
EnterpriseEditionFeature,
|
||||||
MAX_WORKFLOW_NAME_LENGTH,
|
MAX_WORKFLOW_NAME_LENGTH,
|
||||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||||
VIEWS, WORKFLOW_MENU_ACTIONS,
|
VIEWS, WORKFLOW_MENU_ACTIONS,
|
||||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||||
|
WORKFLOW_SHARE_MODAL_KEY,
|
||||||
} from "@/constants";
|
} from "@/constants";
|
||||||
|
|
||||||
import ShortenName from "@/components/ShortenName.vue";
|
import ShortenName from "@/components/ShortenName.vue";
|
||||||
|
@ -143,6 +154,7 @@ export default mixins(workflowHelpers, titleChange).extend({
|
||||||
tagsEditBus: new Vue(),
|
tagsEditBus: new Vue(),
|
||||||
MAX_WORKFLOW_NAME_LENGTH,
|
MAX_WORKFLOW_NAME_LENGTH,
|
||||||
tagsSaving: false,
|
tagsSaving: false,
|
||||||
|
EnterpriseEditionFeature,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -228,6 +240,9 @@ export default mixins(workflowHelpers, titleChange).extend({
|
||||||
const saved = await this.saveCurrentWorkflow({ id: currentId, name: this.workflowName, tags: this.currentWorkflowTagIds });
|
const saved = await this.saveCurrentWorkflow({ id: currentId, name: this.workflowName, tags: this.currentWorkflowTagIds });
|
||||||
if (saved) await this.settingsStore.fetchPromptsData();
|
if (saved) await this.settingsStore.fetchPromptsData();
|
||||||
},
|
},
|
||||||
|
onShareButtonClick() {
|
||||||
|
this.uiStore.openModal(WORKFLOW_SHARE_MODAL_KEY);
|
||||||
|
},
|
||||||
onTagsEditEnable() {
|
onTagsEditEnable() {
|
||||||
this.$data.appliedTagIds = this.currentWorkflowTagIds;
|
this.$data.appliedTagIds = this.currentWorkflowTagIds;
|
||||||
this.$data.isTagsEditEnabled = true;
|
this.$data.isTagsEditEnabled = true;
|
||||||
|
|
|
@ -82,6 +82,10 @@
|
||||||
<ActivationModal />
|
<ActivationModal />
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
|
||||||
|
<ModalRoot :name="WORKFLOW_SHARE_MODAL_KEY">
|
||||||
|
<WorkflowShareModal />
|
||||||
|
</ModalRoot>
|
||||||
|
|
||||||
<ModalRoot :name="ONBOARDING_CALL_SIGNUP_MODAL_KEY">
|
<ModalRoot :name="ONBOARDING_CALL_SIGNUP_MODAL_KEY">
|
||||||
<OnboardingCallSignupModal />
|
<OnboardingCallSignupModal />
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
@ -128,6 +132,7 @@ import {
|
||||||
VERSIONS_MODAL_KEY,
|
VERSIONS_MODAL_KEY,
|
||||||
WORKFLOW_ACTIVE_MODAL_KEY,
|
WORKFLOW_ACTIVE_MODAL_KEY,
|
||||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||||
|
WORKFLOW_SHARE_MODAL_KEY,
|
||||||
IMPORT_CURL_MODAL_KEY,
|
IMPORT_CURL_MODAL_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
|
||||||
|
@ -151,6 +156,7 @@ import DeleteUserModal from "./DeleteUserModal.vue";
|
||||||
import ExecutionsList from "./ExecutionsList.vue";
|
import ExecutionsList from "./ExecutionsList.vue";
|
||||||
import ActivationModal from "./ActivationModal.vue";
|
import ActivationModal from "./ActivationModal.vue";
|
||||||
import ImportCurlModal from './ImportCurlModal.vue';
|
import ImportCurlModal from './ImportCurlModal.vue';
|
||||||
|
import WorkflowShareModal from './WorkflowShareModal.ee.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: "Modals",
|
name: "Modals",
|
||||||
|
@ -174,6 +180,7 @@ export default Vue.extend({
|
||||||
UpdatesPanel,
|
UpdatesPanel,
|
||||||
ValueSurvey,
|
ValueSurvey,
|
||||||
WorkflowSettings,
|
WorkflowSettings,
|
||||||
|
WorkflowShareModal,
|
||||||
ImportCurlModal,
|
ImportCurlModal,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
@ -192,6 +199,7 @@ export default Vue.extend({
|
||||||
TAGS_MANAGER_MODAL_KEY,
|
TAGS_MANAGER_MODAL_KEY,
|
||||||
VERSIONS_MODAL_KEY,
|
VERSIONS_MODAL_KEY,
|
||||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||||
|
WORKFLOW_SHARE_MODAL_KEY,
|
||||||
VALUE_SURVEY_MODAL_KEY,
|
VALUE_SURVEY_MODAL_KEY,
|
||||||
EXECUTIONS_MODAL_KEY,
|
EXECUTIONS_MODAL_KEY,
|
||||||
WORKFLOW_ACTIVE_MODAL_KEY,
|
WORKFLOW_ACTIVE_MODAL_KEY,
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
size="small"
|
size="small"
|
||||||
color="text-dark"
|
color="text-dark"
|
||||||
>
|
>
|
||||||
<div v-if="isReadOnly">
|
<div v-if="readonly || isReadOnly">
|
||||||
<n8n-input
|
<n8n-input
|
||||||
:value="selected && selected[credentialTypeDescription.name] && selected[credentialTypeDescription.name].name"
|
:value="selected && selected[credentialTypeDescription.name] && selected[credentialTypeDescription.name].name"
|
||||||
disabled
|
disabled
|
||||||
|
@ -94,6 +94,7 @@ export default mixins(
|
||||||
).extend({
|
).extend({
|
||||||
name: 'NodeCredentials',
|
name: 'NodeCredentials',
|
||||||
props: [
|
props: [
|
||||||
|
'readonly',
|
||||||
'node', // INodeUi
|
'node', // INodeUi
|
||||||
'overrideCredType', // cred type
|
'overrideCredType', // cred type
|
||||||
],
|
],
|
||||||
|
|
|
@ -91,6 +91,7 @@
|
||||||
:nodeType="activeNodeType"
|
:nodeType="activeNodeType"
|
||||||
:isReadOnly="readOnly || hasForeignCredential"
|
:isReadOnly="readOnly || hasForeignCredential"
|
||||||
:blockUI="blockUi && showTriggerPanel"
|
:blockUI="blockUi && showTriggerPanel"
|
||||||
|
:executable="!readOnly || hasForeignCredential"
|
||||||
@valueChanged="valueChanged"
|
@valueChanged="valueChanged"
|
||||||
@execute="onNodeExecute"
|
@execute="onNodeExecute"
|
||||||
@stopExecution="onStopExecution"
|
@stopExecution="onStopExecution"
|
||||||
|
@ -136,6 +137,7 @@ import InputPanel from './InputPanel.vue';
|
||||||
import TriggerPanel from './TriggerPanel.vue';
|
import TriggerPanel from './TriggerPanel.vue';
|
||||||
import {
|
import {
|
||||||
BASE_NODE_SURVEY_URL,
|
BASE_NODE_SURVEY_URL,
|
||||||
|
EnterpriseEditionFeature,
|
||||||
START_NODE_TYPE,
|
START_NODE_TYPE,
|
||||||
STICKY_NODE_TYPE,
|
STICKY_NODE_TYPE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
@ -147,6 +149,7 @@ import { useWorkflowsStore } from '@/stores/workflows';
|
||||||
import { useNDVStore } from '@/stores/ndv';
|
import { useNDVStore } from '@/stores/ndv';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
import { useNodeTypesStore } from '@/stores/nodeTypes';
|
||||||
import { useUIStore } from '@/stores/ui';
|
import { useUIStore } from '@/stores/ui';
|
||||||
|
import {useSettingsStore} from "@/stores/settings";
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
externalHooks,
|
externalHooks,
|
||||||
|
@ -188,7 +191,6 @@ export default mixins(
|
||||||
pinDataDiscoveryTooltipVisible: false,
|
pinDataDiscoveryTooltipVisible: false,
|
||||||
avgInputRowHeight: 0,
|
avgInputRowHeight: 0,
|
||||||
avgOutputRowHeight: 0,
|
avgOutputRowHeight: 0,
|
||||||
hasForeignCredential: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -206,6 +208,7 @@ export default mixins(
|
||||||
useNDVStore,
|
useNDVStore,
|
||||||
useUIStore,
|
useUIStore,
|
||||||
useWorkflowsStore,
|
useWorkflowsStore,
|
||||||
|
useSettingsStore,
|
||||||
),
|
),
|
||||||
sessionId(): string {
|
sessionId(): string {
|
||||||
return this.ndvStore.sessionId;
|
return this.ndvStore.sessionId;
|
||||||
|
@ -365,6 +368,21 @@ export default mixins(
|
||||||
blockUi(): boolean {
|
blockUi(): boolean {
|
||||||
return this.isWorkflowRunning || this.isExecutionWaitingForWebhook;
|
return this.isWorkflowRunning || this.isExecutionWaitingForWebhook;
|
||||||
},
|
},
|
||||||
|
hasForeignCredential(): boolean {
|
||||||
|
const credentials = (this.activeNode || {}).credentials;
|
||||||
|
const foreignCredentials = this.credentialsStore.foreignCredentialsById;
|
||||||
|
|
||||||
|
let hasForeignCredential = false;
|
||||||
|
if (credentials && this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowSharing)) {
|
||||||
|
Object.values(credentials).forEach((credential) => {
|
||||||
|
if (credential.id && foreignCredentials[credential.id] && !foreignCredentials[credential.id].currentUserHasAccess) {
|
||||||
|
hasForeignCredential = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasForeignCredential;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
activeNode(node: INodeUi | null) {
|
activeNode(node: INodeUi | null) {
|
||||||
|
@ -384,8 +402,6 @@ export default mixins(
|
||||||
nodeSubtitle: this.getNodeSubtitle(node, this.activeNodeType, this.getCurrentWorkflow()),
|
nodeSubtitle: this.getNodeSubtitle(node, this.activeNodeType, this.getCurrentWorkflow()),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.checkForeignCredentials();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.activeNode) {
|
if (this.activeNode) {
|
||||||
const outgoingConnections = this.workflowsStore.outgoingConnectionsByNodeName(
|
const outgoingConnections = this.workflowsStore.outgoingConnectionsByNodeName(
|
||||||
|
@ -631,12 +647,6 @@ export default mixins(
|
||||||
input_node_type: this.inputNode ? this.inputNode.type : '',
|
input_node_type: this.inputNode ? this.inputNode.type : '',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
checkForeignCredentials() {
|
|
||||||
if(this.activeNode){
|
|
||||||
const issues = this.getNodeCredentialIssues(this.activeNode);
|
|
||||||
this.hasForeignCredential = !!issues?.credentials?.foreign;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onStopExecution(){
|
onStopExecution(){
|
||||||
this.$emit('stopExecution');
|
this.$emit('stopExecution');
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
:isReadOnly="isReadOnly"
|
:isReadOnly="isReadOnly"
|
||||||
@input="nameChanged"
|
@input="nameChanged"
|
||||||
></NodeTitle>
|
></NodeTitle>
|
||||||
<div v-if="!isReadOnly">
|
<div v-if="executable">
|
||||||
<NodeExecuteButton
|
<NodeExecuteButton
|
||||||
v-if="!blockUI"
|
v-if="!blockUI"
|
||||||
:nodeName="node.name"
|
:nodeName="node.name"
|
||||||
|
@ -83,7 +83,7 @@
|
||||||
@valueChanged="valueChanged"
|
@valueChanged="valueChanged"
|
||||||
@activate="onWorkflowActivate"
|
@activate="onWorkflowActivate"
|
||||||
>
|
>
|
||||||
<node-credentials :node="node" @credentialSelected="credentialSelected" />
|
<node-credentials :node="node" :readonly="isReadOnly" @credentialSelected="credentialSelected" />
|
||||||
</parameter-input-list>
|
</parameter-input-list>
|
||||||
<div v-if="parametersNoneSetting.length === 0" class="no-parameters">
|
<div v-if="parametersNoneSetting.length === 0" class="no-parameters">
|
||||||
<n8n-text>
|
<n8n-text>
|
||||||
|
@ -264,6 +264,10 @@ export default mixins(externalHooks, nodeHelpers).extend({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
executable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -26,9 +26,9 @@
|
||||||
</div>
|
</div>
|
||||||
<template #append>
|
<template #append>
|
||||||
<div :class="$style.cardActions">
|
<div :class="$style.cardActions">
|
||||||
<enterprise-edition :features="[EnterpriseEditionFeature.Sharing]" v-show="false">
|
<enterprise-edition :features="[EnterpriseEditionFeature.WorkflowSharing]">
|
||||||
<n8n-badge
|
<n8n-badge
|
||||||
v-if="credentialPermissions.isOwner"
|
v-if="workflowPermissions.isOwner"
|
||||||
class="mr-xs"
|
class="mr-xs"
|
||||||
theme="tertiary"
|
theme="tertiary"
|
||||||
bold
|
bold
|
||||||
|
@ -122,7 +122,7 @@ export default mixins(
|
||||||
currentUser (): IUser {
|
currentUser (): IUser {
|
||||||
return this.usersStore.currentUser || {} as IUser;
|
return this.usersStore.currentUser || {} as IUser;
|
||||||
},
|
},
|
||||||
credentialPermissions(): IPermissions {
|
workflowPermissions(): IPermissions {
|
||||||
return getWorkflowPermissions(this.currentUser, this.data);
|
return getWorkflowPermissions(this.currentUser, this.data);
|
||||||
},
|
},
|
||||||
actions(): Array<{ label: string; value: string; }> {
|
actions(): Array<{ label: string; value: string; }> {
|
||||||
|
@ -135,7 +135,7 @@ export default mixins(
|
||||||
label: this.$locale.baseText('workflows.item.duplicate'),
|
label: this.$locale.baseText('workflows.item.duplicate'),
|
||||||
value: WORKFLOW_LIST_ITEM_ACTIONS.DUPLICATE,
|
value: WORKFLOW_LIST_ITEM_ACTIONS.DUPLICATE,
|
||||||
},
|
},
|
||||||
].concat(this.credentialPermissions.delete ? [{
|
].concat(this.workflowPermissions.delete ? [{
|
||||||
label: this.$locale.baseText('workflows.item.delete'),
|
label: this.$locale.baseText('workflows.item.delete'),
|
||||||
value: WORKFLOW_LIST_ITEM_ACTIONS.DELETE,
|
value: WORKFLOW_LIST_ITEM_ACTIONS.DELETE,
|
||||||
}]: []);
|
}]: []);
|
||||||
|
|
265
packages/editor-ui/src/components/WorkflowShareModal.ee.vue
Normal file
265
packages/editor-ui/src/components/WorkflowShareModal.ee.vue
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
width="460px"
|
||||||
|
:title="$locale.baseText(fakeDoor.actionBoxTitle, { interpolate: { name: workflow.name } })"
|
||||||
|
:eventBus="modalBus"
|
||||||
|
:name="WORKFLOW_SHARE_MODAL_KEY"
|
||||||
|
:center="true"
|
||||||
|
>
|
||||||
|
<template slot="content">
|
||||||
|
<div :class="$style.container">
|
||||||
|
<enterprise-edition :features="[EnterpriseEditionFeature.WorkflowSharing]">
|
||||||
|
<n8n-user-select
|
||||||
|
v-if="workflowPermissions.updateSharing"
|
||||||
|
size="large"
|
||||||
|
:users="usersList"
|
||||||
|
:currentUserId="currentUser.id"
|
||||||
|
:placeholder="$locale.baseText('workflows.shareModal.select.placeholder')"
|
||||||
|
@input="onAddSharee"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<n8n-icon icon="search" />
|
||||||
|
</template>
|
||||||
|
</n8n-user-select>
|
||||||
|
<n8n-users-list
|
||||||
|
:actions="[]"
|
||||||
|
:users="sharedWithList"
|
||||||
|
:currentUserId="currentUser.id"
|
||||||
|
:delete-label="$locale.baseText('workflows.shareModal.list.delete')"
|
||||||
|
:readonly="!workflowPermissions.updateSharing"
|
||||||
|
@delete="onRemoveSharee"
|
||||||
|
>
|
||||||
|
<template v-slot:actions="{ user }">
|
||||||
|
<n8n-select
|
||||||
|
:class="$style.roleSelect"
|
||||||
|
value="editor"
|
||||||
|
size="small"
|
||||||
|
@change="onRoleAction(user, $event)"
|
||||||
|
>
|
||||||
|
<n8n-option
|
||||||
|
:label="$locale.baseText('workflows.roles.editor')"
|
||||||
|
value="editor" />
|
||||||
|
<n8n-option
|
||||||
|
:class="$style.roleSelectRemoveOption"
|
||||||
|
value="remove"
|
||||||
|
>
|
||||||
|
<n8n-text color="danger">{{ $locale.baseText('workflows.shareModal.list.delete') }}</n8n-text>
|
||||||
|
</n8n-option>
|
||||||
|
</n8n-select>
|
||||||
|
</template>
|
||||||
|
</n8n-users-list>
|
||||||
|
<template #fallback>
|
||||||
|
<n8n-text>
|
||||||
|
{{ $locale.baseText(fakeDoor.actionBoxDescription) }}
|
||||||
|
</n8n-text>
|
||||||
|
</template>
|
||||||
|
</enterprise-edition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template slot="footer">
|
||||||
|
<enterprise-edition :features="[EnterpriseEditionFeature.WorkflowSharing]" :class="$style.actionButtons">
|
||||||
|
<n8n-text
|
||||||
|
v-show="isDirty"
|
||||||
|
color="text-light"
|
||||||
|
size="small"
|
||||||
|
class="mr-xs"
|
||||||
|
>
|
||||||
|
{{ $locale.baseText('workflows.shareModal.changesHint') }}
|
||||||
|
</n8n-text>
|
||||||
|
<n8n-button
|
||||||
|
v-show="workflowPermissions.updateSharing"
|
||||||
|
@click="onSave"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!isDirty"
|
||||||
|
size="medium"
|
||||||
|
>
|
||||||
|
{{ $locale.baseText('workflows.shareModal.save') }}
|
||||||
|
</n8n-button>
|
||||||
|
|
||||||
|
<template #fallback>
|
||||||
|
<n8n-link :to="fakeDoor.linkURL">
|
||||||
|
<n8n-button
|
||||||
|
:loading="loading"
|
||||||
|
size="medium"
|
||||||
|
>
|
||||||
|
{{ $locale.baseText(fakeDoor.actionBoxButtonLabel) }}
|
||||||
|
</n8n-button>
|
||||||
|
</n8n-link>
|
||||||
|
</template>
|
||||||
|
</enterprise-edition>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Modal from './Modal.vue';
|
||||||
|
import {
|
||||||
|
EnterpriseEditionFeature,
|
||||||
|
FAKE_DOOR_FEATURES,
|
||||||
|
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||||
|
WORKFLOW_SHARE_MODAL_KEY
|
||||||
|
} from '../constants';
|
||||||
|
import {IFakeDoor, IUser, IWorkflowDb} from "@/Interface";
|
||||||
|
import { getWorkflowPermissions, IPermissions } from "@/permissions";
|
||||||
|
import mixins from "vue-typed-mixins";
|
||||||
|
import {showMessage} from "@/components/mixins/showMessage";
|
||||||
|
import {nodeViewEventBus} from "@/event-bus/node-view-event-bus";
|
||||||
|
import {mapStores} from "pinia";
|
||||||
|
import {useSettingsStore} from "@/stores/settings";
|
||||||
|
import {useUIStore} from "@/stores/ui";
|
||||||
|
import {useUsersStore} from "@/stores/users";
|
||||||
|
import {useWorkflowsStore} from "@/stores/workflows";
|
||||||
|
import useWorkflowsEEStore from "@/stores/workflows.ee";
|
||||||
|
|
||||||
|
export default mixins(
|
||||||
|
showMessage,
|
||||||
|
).extend({
|
||||||
|
name: 'workflow-share-modal',
|
||||||
|
components: {
|
||||||
|
Modal,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
WORKFLOW_SHARE_MODAL_KEY,
|
||||||
|
loading: false,
|
||||||
|
modalBus: new Vue(),
|
||||||
|
sharedWith: [...(workflowsStore.workflow.sharedWith || [])] as Array<Partial<IUser>>,
|
||||||
|
EnterpriseEditionFeature,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useSettingsStore, useUIStore, useUsersStore, useWorkflowsStore, useWorkflowsEEStore),
|
||||||
|
usersList(): IUser[] {
|
||||||
|
return this.usersStore.allUsers.filter((user: IUser) => {
|
||||||
|
const isCurrentUser = user.id === this.usersStore.currentUser?.id;
|
||||||
|
const isAlreadySharedWithUser = (this.sharedWith || []).find((sharee) => sharee.id === user.id);
|
||||||
|
|
||||||
|
return !isCurrentUser && !isAlreadySharedWithUser;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
sharedWithList(): Array<Partial<IUser>> {
|
||||||
|
return ([
|
||||||
|
{
|
||||||
|
...(this.workflow && this.workflow.ownedBy ? this.workflow.ownedBy : this.usersStore.currentUser),
|
||||||
|
isOwner: true,
|
||||||
|
},
|
||||||
|
] as Array<Partial<IUser>>).concat(this.sharedWith || []);
|
||||||
|
},
|
||||||
|
workflow(): IWorkflowDb {
|
||||||
|
return this.workflowsStore.workflow;
|
||||||
|
},
|
||||||
|
currentUser(): IUser | null {
|
||||||
|
return this.usersStore.currentUser;
|
||||||
|
},
|
||||||
|
workflowPermissions(): IPermissions {
|
||||||
|
return getWorkflowPermissions(this.usersStore.currentUser, this.workflow);
|
||||||
|
},
|
||||||
|
isSharingAvailable(): boolean {
|
||||||
|
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowSharing) === true;
|
||||||
|
},
|
||||||
|
fakeDoor(): IFakeDoor | undefined {
|
||||||
|
return this.uiStore.getFakeDoorById(FAKE_DOOR_FEATURES.WORKFLOWS_SHARING);
|
||||||
|
},
|
||||||
|
isDirty(): boolean {
|
||||||
|
const previousSharedWith = this.workflow.sharedWith || [];
|
||||||
|
|
||||||
|
return this.sharedWith.length !== previousSharedWith.length ||
|
||||||
|
this.sharedWith.some(
|
||||||
|
(sharee) => !previousSharedWith.find((previousSharee) => sharee.id === previousSharee.id),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async onSave() {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
const saveWorkflowPromise = () => {
|
||||||
|
return new Promise<string>((resolve) => {
|
||||||
|
if (this.workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
|
||||||
|
nodeViewEventBus.$emit('saveWorkflow', () => {
|
||||||
|
resolve(this.workflowsStore.workflowId);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(this.workflowsStore.workflowId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const workflowId = await saveWorkflowPromise();
|
||||||
|
await this.workflowsEEStore.saveWorkflowSharedWith({ workflowId, sharedWith: this.sharedWith });
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
async onAddSharee(userId: string) {
|
||||||
|
const { id, firstName, lastName, email } = this.usersStore.getUserById(userId)!;
|
||||||
|
const sharee = { id, firstName, lastName, email };
|
||||||
|
|
||||||
|
this.sharedWith = this.sharedWith.concat(sharee);
|
||||||
|
},
|
||||||
|
async onRemoveSharee(userId: string) {
|
||||||
|
const user = this.usersStore.getUserById(userId)!;
|
||||||
|
const isNewSharee = !(this.workflow.sharedWith || []).find((sharee) => sharee.id === userId);
|
||||||
|
|
||||||
|
let confirm = true;
|
||||||
|
if (!isNewSharee) {
|
||||||
|
confirm = await this.confirmMessage(
|
||||||
|
this.$locale.baseText('workflows.shareModal.list.delete.confirm.message', {
|
||||||
|
interpolate: { name: user.fullName as string, workflow: this.workflow.name },
|
||||||
|
}),
|
||||||
|
this.$locale.baseText('workflows.shareModal.list.delete.confirm.title', { interpolate: { name: user.fullName } }),
|
||||||
|
null,
|
||||||
|
this.$locale.baseText('workflows.shareModal.list.delete.confirm.confirmButtonText'),
|
||||||
|
this.$locale.baseText('workflows.shareModal.list.delete.confirm.cancelButtonText'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirm) {
|
||||||
|
this.sharedWith = this.sharedWith.filter((sharee: IUser) => {
|
||||||
|
return sharee.id !== user.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRoleAction(user: IUser, action: string) {
|
||||||
|
if (action === 'remove') {
|
||||||
|
this.onRemoveSharee(user.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadUsers() {
|
||||||
|
await this.usersStore.fetchUsers();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.isSharingAvailable) {
|
||||||
|
this.loadUsers();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.container > * {
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButtons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roleSelect {
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roleSelectRemoveOption {
|
||||||
|
border-top: 1px solid var(--color-foreground-base);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -245,16 +245,6 @@ export const nodeHelpers = mixins(
|
||||||
let credentialType: ICredentialType | null;
|
let credentialType: ICredentialType | null;
|
||||||
let credentialDisplayName: string;
|
let credentialDisplayName: string;
|
||||||
let selectedCredentials: INodeCredentialsDetails;
|
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 {
|
const {
|
||||||
authentication,
|
authentication,
|
||||||
|
@ -350,9 +340,7 @@ export const nodeHelpers = mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameMatches.length === 0) {
|
if (nameMatches.length === 0) {
|
||||||
if (this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) {
|
if (!this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowSharing)) {
|
||||||
foundIssues[credentialTypeDescription.name] = [this.$locale.baseText('nodeIssues.credentials.notAvailable')];
|
|
||||||
} else {
|
|
||||||
foundIssues[credentialTypeDescription.name] = [this.$locale.baseText('nodeIssues.credentials.doNotExist', { interpolate: { name: selectedCredentials.name, type: credentialDisplayName } }), this.$locale.baseText('nodeIssues.credentials.doNotExist.hint')];
|
foundIssues[credentialTypeDescription.name] = [this.$locale.baseText('nodeIssues.credentials.doNotExist', { interpolate: { name: selectedCredentials.name, type: credentialDisplayName } }), this.$locale.baseText('nodeIssues.credentials.doNotExist.hint')];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -383,9 +383,10 @@ export const pushConnection = mixins(
|
||||||
// it can be displayed in the node-view
|
// it can be displayed in the node-view
|
||||||
this.updateNodesExecutionIssues();
|
this.updateNodesExecutionIssues();
|
||||||
|
|
||||||
|
const lastNodeExecuted: string | undefined = runDataExecuted.data.resultData.lastNodeExecuted;
|
||||||
let itemsCount = 0;
|
let itemsCount = 0;
|
||||||
if(runDataExecuted.data.resultData.lastNodeExecuted && !runDataExecutedErrorMessage) {
|
if(lastNodeExecuted && runDataExecuted.data.resultData.runData[lastNodeExecuted as string] && !runDataExecutedErrorMessage) {
|
||||||
itemsCount = runDataExecuted.data.resultData.runData[runDataExecuted.data.resultData.lastNodeExecuted][0].data!.main[0]!.length;
|
itemsCount = runDataExecuted.data.resultData.runData[lastNodeExecuted as string][0].data!.main[0]!.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$externalHooks().run('pushConnection.executionFinished', {
|
this.$externalHooks().run('pushConnection.executionFinished', {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ElNotificationComponent, ElNotificationOptions } from 'element-ui/types
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
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 { ElMessageBoxOptions } from 'element-ui/types/message-box';
|
||||||
import type { ElMessageComponent, ElMessageOptions, MessageType } from 'element-ui/types/message';
|
import type { ElMessageComponent, ElMessageOptions, MessageType } from 'element-ui/types/message';
|
||||||
import { sanitizeHtml } from '@/utils';
|
import { sanitizeHtml } from '@/utils';
|
||||||
|
@ -89,13 +89,13 @@ export const showMessage = mixins(externalHooks).extend({
|
||||||
return this.$message(config);
|
return this.$message(config);
|
||||||
},
|
},
|
||||||
|
|
||||||
$getExecutionError(data: IRunExecutionData) {
|
$getExecutionError(data: IRunExecutionData | IExecuteContextData) {
|
||||||
const error = data.resultData.error;
|
const error = data.resultData.error;
|
||||||
|
|
||||||
let errorMessage: string;
|
let errorMessage: string;
|
||||||
|
|
||||||
if (data.resultData.lastNodeExecuted && error) {
|
if (data.resultData.lastNodeExecuted && error) {
|
||||||
errorMessage = error.message;
|
errorMessage = error.message || error.description;
|
||||||
} else {
|
} else {
|
||||||
errorMessage = 'There was a problem executing the workflow!';
|
errorMessage = 'There was a problem executing the workflow!';
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ export const DUPLICATE_MODAL_KEY = 'duplicate';
|
||||||
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';
|
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';
|
||||||
export const VERSIONS_MODAL_KEY = 'versions';
|
export const VERSIONS_MODAL_KEY = 'versions';
|
||||||
export const WORKFLOW_SETTINGS_MODAL_KEY = 'settings';
|
export const WORKFLOW_SETTINGS_MODAL_KEY = 'settings';
|
||||||
|
export const WORKFLOW_SHARE_MODAL_KEY = 'workflowShare';
|
||||||
export const PERSONALIZATION_MODAL_KEY = 'personalization';
|
export const PERSONALIZATION_MODAL_KEY = 'personalization';
|
||||||
export const CONTACT_PROMPT_MODAL_KEY = 'contactPrompt';
|
export const CONTACT_PROMPT_MODAL_KEY = 'contactPrompt';
|
||||||
export const VALUE_SURVEY_MODAL_KEY = 'valueSurvey';
|
export const VALUE_SURVEY_MODAL_KEY = 'valueSurvey';
|
||||||
|
@ -315,7 +316,8 @@ export enum VIEWS {
|
||||||
export enum FAKE_DOOR_FEATURES {
|
export enum FAKE_DOOR_FEATURES {
|
||||||
ENVIRONMENTS = 'environments',
|
ENVIRONMENTS = 'environments',
|
||||||
LOGGING = 'logging',
|
LOGGING = 'logging',
|
||||||
SHARING = 'sharing',
|
CREDENTIALS_SHARING = 'credentialsSharing',
|
||||||
|
WORKFLOWS_SHARING = 'workflowsSharing',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ONBOARDING_PROMPT_TIMEBOX = 14;
|
export const ONBOARDING_PROMPT_TIMEBOX = 14;
|
||||||
|
@ -370,6 +372,7 @@ export enum WORKFLOW_MENU_ACTIONS {
|
||||||
*/
|
*/
|
||||||
export enum EnterpriseEditionFeature {
|
export enum EnterpriseEditionFeature {
|
||||||
Sharing = 'sharing',
|
Sharing = 'sharing',
|
||||||
|
WorkflowSharing = 'workflowSharing',
|
||||||
}
|
}
|
||||||
export const MAIN_NODE_PANEL_WIDTH = 360;
|
export const MAIN_NODE_PANEL_WIDTH = 360;
|
||||||
|
|
||||||
|
@ -418,6 +421,7 @@ export enum STORES {
|
||||||
UI = 'ui',
|
UI = 'ui',
|
||||||
USERS = 'users',
|
USERS = 'users',
|
||||||
WORKFLOWS = 'workflows',
|
WORKFLOWS = 'workflows',
|
||||||
|
WORKFLOWS_EE = 'workflowsEE',
|
||||||
NDV = 'ndv',
|
NDV = 'ndv',
|
||||||
TEMPLATES = 'templates',
|
TEMPLATES = 'templates',
|
||||||
NODE_TYPES = 'nodeTypes',
|
NODE_TYPES = 'nodeTypes',
|
||||||
|
|
3
packages/editor-ui/src/event-bus/node-view-event-bus.ts
Normal file
3
packages/editor-ui/src/event-bus/node-view-event-bus.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export const nodeViewEventBus = new Vue();
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {IUser, ICredentialsResponse, IRootState, IWorkflowDb} from "@/Interface";
|
import {IUser, ICredentialsResponse, IRootState, IWorkflowDb} from "@/Interface";
|
||||||
import {EnterpriseEditionFeature} from "@/constants";
|
import {EnterpriseEditionFeature, PLACEHOLDER_EMPTY_WORKFLOW_ID} from "@/constants";
|
||||||
import { useSettingsStore } from "./stores/settings";
|
import { useSettingsStore } from "./stores/settings";
|
||||||
|
|
||||||
export enum UserRole {
|
export enum UserRole {
|
||||||
|
@ -32,9 +32,9 @@ export type IPermissionsTable = IPermissionsTableRow[];
|
||||||
* @param user
|
* @param user
|
||||||
* @param table
|
* @param table
|
||||||
*/
|
*/
|
||||||
export const parsePermissionsTable = (user: IUser, table: IPermissionsTable): IPermissions => {
|
export const parsePermissionsTable = (user: IUser | null, table: IPermissionsTable): IPermissions => {
|
||||||
const genericTable = [
|
const genericTable = [
|
||||||
{ name: UserRole.InstanceOwner, test: () => user.isOwner },
|
{ name: UserRole.InstanceOwner, test: () => user?.isOwner },
|
||||||
];
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -53,11 +53,11 @@ export const parsePermissionsTable = (user: IUser, table: IPermissionsTable): IP
|
||||||
* User permissions definition
|
* User permissions definition
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const getCredentialPermissions = (user: IUser, credential: ICredentialsResponse) => {
|
export const getCredentialPermissions = (user: IUser | null, credential: ICredentialsResponse) => {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const table: IPermissionsTable = [
|
const table: IPermissionsTable = [
|
||||||
{ name: UserRole.ResourceOwner, test: () => !!(credential && credential.ownedBy && credential.ownedBy.id === user.id) || !settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing) },
|
{ 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.ResourceReader, test: () => !!(credential && credential.sharedWith && credential.sharedWith.find((sharee) => sharee.id === user?.id)) },
|
||||||
{ name: 'read', test: [UserRole.ResourceOwner, UserRole.InstanceOwner, UserRole.ResourceReader] },
|
{ name: 'read', test: [UserRole.ResourceOwner, UserRole.InstanceOwner, UserRole.ResourceReader] },
|
||||||
{ name: 'save', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
{ name: 'save', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
||||||
{ name: 'updateName', 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);
|
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 = [
|
const table: IPermissionsTable = [
|
||||||
// { name: UserRole.ResourceOwner, test: () => !!(workflow && workflow.ownedBy && workflow.ownedBy.id === user.id) || !useSettingsStore().isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing) },
|
{ name: UserRole.ResourceOwner, test: () => !!(isNewWorkflow || workflow && workflow.ownedBy && workflow.ownedBy.id === user?.id) || !settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowSharing) },
|
||||||
{ name: UserRole.ResourceOwner, test: () => true },
|
{ name: UserRole.ResourceReader, test: () => !!(workflow && workflow.sharedWith && workflow.sharedWith.find((sharee) => sharee.id === user?.id)) },
|
||||||
// { name: UserRole.ResourceReader, test: () => !!(workflow && workflow.sharedWith && workflow.sharedWith.find((sharee) => sharee.id === user.id)) },
|
|
||||||
{ name: UserRole.ResourceReader, test: () => true },
|
|
||||||
{ name: 'read', test: [UserRole.ResourceOwner, UserRole.InstanceOwner, UserRole.ResourceReader] },
|
{ name: 'read', test: [UserRole.ResourceOwner, UserRole.InstanceOwner, UserRole.ResourceReader] },
|
||||||
{ name: 'save', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
{ name: 'save', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
||||||
{ name: 'updateName', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
{ name: 'updateName', test: [UserRole.ResourceOwner, UserRole.InstanceOwner] },
|
||||||
|
|
|
@ -486,6 +486,11 @@
|
||||||
"fakeDoor.credentialEdit.sharing.actionBox.title.cloud.upgrade": "Upgrade to add users",
|
"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.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.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.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.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)",
|
"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.showMessage.displayActivationError.title": "Problem activating workflow",
|
||||||
"workflowActivator.theWorkflowIsSetToBeActiveBut": "The workflow is activated but could not be started.<br />Click to display error message.",
|
"workflowActivator.theWorkflowIsSetToBeActiveBut": "The workflow is activated but could not be started.<br />Click to display error message.",
|
||||||
"workflowActivator.thisWorkflowHasNoTriggerNodes": "This workflow has no trigger nodes that require activation",
|
"workflowActivator.thisWorkflowHasNoTriggerNodes": "This workflow has no trigger nodes that require activation",
|
||||||
|
"workflowDetails.share": "Share",
|
||||||
"workflowDetails.active": "Active",
|
"workflowDetails.active": "Active",
|
||||||
"workflowDetails.addTag": "Add tag",
|
"workflowDetails.addTag": "Add tag",
|
||||||
"workflowDetails.chooseOrCreateATag": "Choose or create a tag",
|
"workflowDetails.chooseOrCreateATag": "Choose or create a tag",
|
||||||
|
@ -1361,6 +1367,18 @@
|
||||||
"workflows.empty.description": "Create your first workflow",
|
"workflows.empty.description": "Create your first workflow",
|
||||||
"workflows.empty.startFromScratch": "Start from scratch",
|
"workflows.empty.startFromScratch": "Start from scratch",
|
||||||
"workflows.empty.browseTemplates": "Browse templates",
|
"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": "<strong>This might cause the workflow to stop working:</strong> 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.title": "Import cURL command",
|
||||||
"importCurlModal.input.label": "cURL Command",
|
"importCurlModal.input.label": "cURL Command",
|
||||||
"importCurlModal.input.placeholder": "Paste the cURL command here",
|
"importCurlModal.input.placeholder": "Paste the cURL command here",
|
||||||
|
|
|
@ -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 { setCredentialSharedWith } from "@/api/credentials.ee";
|
||||||
import { getAppNameFromCredType } from "@/components/helpers";
|
import { getAppNameFromCredType } from "@/components/helpers";
|
||||||
import { EnterpriseEditionFeature, STORES } from "@/constants";
|
import { EnterpriseEditionFeature, STORES } from "@/constants";
|
||||||
|
@ -33,10 +33,6 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
|
||||||
return Object.values(this.credentials)
|
return Object.values(this.credentials)
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.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[]} {
|
allCredentialsByType(): {[type: string]: ICredentialsResponse[]} {
|
||||||
const credentials = this.allCredentials;
|
const credentials = this.allCredentials;
|
||||||
const types = this.allCredentialTypes;
|
const types = this.allCredentialTypes;
|
||||||
|
@ -53,6 +49,9 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
|
||||||
getCredentialById() {
|
getCredentialById() {
|
||||||
return (id: string): ICredentialsResponse => this.credentials[id];
|
return (id: string): ICredentialsResponse => this.credentials[id];
|
||||||
},
|
},
|
||||||
|
foreignCredentialsById(): ICredentialMap {
|
||||||
|
return Object.fromEntries(Object.entries(this.credentials).filter(([_, credential]) => credential.hasOwnProperty('currentUserHasAccess')));
|
||||||
|
},
|
||||||
getCredentialByIdAndType() {
|
getCredentialByIdAndType() {
|
||||||
return (id: string, type: string): ICredentialsResponse | undefined => {
|
return (id: string, type: string): ICredentialsResponse | undefined => {
|
||||||
const credential = this.credentials[id];
|
const credential = this.credentials[id];
|
||||||
|
@ -138,13 +137,12 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
|
||||||
return accu;
|
return accu;
|
||||||
}, {});
|
}, {});
|
||||||
},
|
},
|
||||||
setForeignCredentials(credentials: ICredentialsResponse[]): void {
|
addCredentials(credentials: ICredentialsResponse[]): void {
|
||||||
this.foreignCredentials = credentials.reduce((accu: ICredentialMap, cred: ICredentialsResponse) => {
|
credentials.forEach((cred: ICredentialsResponse) => {
|
||||||
if (cred.id) {
|
if (cred.id) {
|
||||||
accu[cred.id] = cred;
|
this.credentials[cred.id] = { ...this.credentials[cred.id], ...cred };
|
||||||
}
|
}
|
||||||
return accu;
|
});
|
||||||
}, {});
|
|
||||||
},
|
},
|
||||||
upsertCredential(credential: ICredentialsResponse): void {
|
upsertCredential(credential: ICredentialsResponse): void {
|
||||||
if (credential.id) {
|
if (credential.id) {
|
||||||
|
@ -168,12 +166,6 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
|
||||||
this.setCredentials(credentials);
|
this.setCredentials(credentials);
|
||||||
return 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> {
|
async getCredentialData({ id }: {id: string}): Promise<ICredentialsResponse | ICredentialsDecryptedResponse | undefined> {
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
return await getCredentialData(rootStore.getRestApiContext, id);
|
return await getCredentialData(rootStore.getRestApiContext, id);
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
VERSIONS_MODAL_KEY,
|
VERSIONS_MODAL_KEY,
|
||||||
VIEWS,
|
VIEWS,
|
||||||
WORKFLOW_ACTIVE_MODAL_KEY,
|
WORKFLOW_ACTIVE_MODAL_KEY,
|
||||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
WORKFLOW_SETTINGS_MODAL_KEY, WORKFLOW_SHARE_MODAL_KEY,
|
||||||
} from "@/constants";
|
} from "@/constants";
|
||||||
import {
|
import {
|
||||||
CurlToJSONResponse,
|
CurlToJSONResponse,
|
||||||
|
@ -97,6 +97,9 @@ export const useUIStore = defineStore(STORES.UI, {
|
||||||
[EXECUTIONS_MODAL_KEY]: {
|
[EXECUTIONS_MODAL_KEY]: {
|
||||||
open: false,
|
open: false,
|
||||||
},
|
},
|
||||||
|
[WORKFLOW_SHARE_MODAL_KEY]: {
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
[WORKFLOW_ACTIVE_MODAL_KEY]: {
|
[WORKFLOW_ACTIVE_MODAL_KEY]: {
|
||||||
open: false,
|
open: false,
|
||||||
},
|
},
|
||||||
|
@ -141,13 +144,22 @@ export const useUIStore = defineStore(STORES.UI, {
|
||||||
uiLocations: ['settings'],
|
uiLocations: ['settings'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: FAKE_DOOR_FEATURES.SHARING,
|
id: FAKE_DOOR_FEATURES.CREDENTIALS_SHARING,
|
||||||
featureName: 'fakeDoor.credentialEdit.sharing.name',
|
featureName: 'fakeDoor.credentialEdit.sharing.name',
|
||||||
actionBoxTitle: 'fakeDoor.credentialEdit.sharing.actionBox.title',
|
actionBoxTitle: 'fakeDoor.credentialEdit.sharing.actionBox.title',
|
||||||
actionBoxDescription: 'fakeDoor.credentialEdit.sharing.actionBox.description',
|
actionBoxDescription: 'fakeDoor.credentialEdit.sharing.actionBox.description',
|
||||||
linkURL: 'https://n8n-community.typeform.com/to/l7QOrERN#f=sharing',
|
linkURL: 'https://n8n-community.typeform.com/to/l7QOrERN#f=sharing',
|
||||||
uiLocations: ['credentialsModal'],
|
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: {
|
draggable: {
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
|
|
|
@ -23,8 +23,8 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
||||||
currentUser(): IUser | null {
|
currentUser(): IUser | null {
|
||||||
return this.currentUserId ? this.users[this.currentUserId] : null;
|
return this.currentUserId ? this.users[this.currentUserId] : null;
|
||||||
},
|
},
|
||||||
getUserById(): (userId: string) => IUser | null {
|
getUserById(state) {
|
||||||
return (userId: string): IUser | null => this.users[userId];
|
return (userId: string): IUser | null => state.users[userId];
|
||||||
},
|
},
|
||||||
globalRoleName(): string {
|
globalRoleName(): string {
|
||||||
return this.currentUser?.globalRole?.name || '';
|
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;
|
|
@ -149,7 +149,7 @@ import {
|
||||||
STICKY_NODE_TYPE,
|
STICKY_NODE_TYPE,
|
||||||
VIEWS,
|
VIEWS,
|
||||||
WEBHOOK_NODE_TYPE,
|
WEBHOOK_NODE_TYPE,
|
||||||
TRIGGER_NODE_FILTER,
|
TRIGGER_NODE_FILTER, EnterpriseEditionFeature,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
|
@ -221,6 +221,7 @@ import { useSettingsStore } from '@/stores/settings';
|
||||||
import { useUsersStore } from '@/stores/users';
|
import { useUsersStore } from '@/stores/users';
|
||||||
import { getNodeViewTab } from '@/components/helpers';
|
import { getNodeViewTab } from '@/components/helpers';
|
||||||
import { Route, RawLocation } from 'vue-router';
|
import { Route, RawLocation } from 'vue-router';
|
||||||
|
import { nodeViewEventBus } from '@/event-bus/node-view-event-bus';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows';
|
import { useWorkflowsStore } from '@/stores/workflows';
|
||||||
import { useRootStore } from '@/stores/n8nRootStore';
|
import { useRootStore } from '@/stores/n8nRootStore';
|
||||||
import { useNDVStore } from '@/stores/ndv';
|
import { useNDVStore } from '@/stores/ndv';
|
||||||
|
@ -231,6 +232,7 @@ import { useTagsStore } from '@/stores/tags';
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator';
|
import { useNodeCreatorStore } from '@/stores/nodeCreator';
|
||||||
import { dataPinningEventBus } from '@/event-bus/data-pinning-event-bus';
|
import { dataPinningEventBus } from '@/event-bus/data-pinning-event-bus';
|
||||||
import { useCanvasStore } from '@/stores/canvas';
|
import { useCanvasStore } from '@/stores/canvas';
|
||||||
|
import useWorkflowsEEStore from "@/stores/workflows.ee";
|
||||||
|
|
||||||
interface AddNodeOptions {
|
interface AddNodeOptions {
|
||||||
position?: XYPosition;
|
position?: XYPosition;
|
||||||
|
@ -395,6 +397,7 @@ export default mixins(
|
||||||
useUIStore,
|
useUIStore,
|
||||||
useUsersStore,
|
useUsersStore,
|
||||||
useWorkflowsStore,
|
useWorkflowsStore,
|
||||||
|
useWorkflowsEEStore,
|
||||||
),
|
),
|
||||||
nativelyNumberSuffixedDefaults(): string[] {
|
nativelyNumberSuffixedDefaults(): string[] {
|
||||||
return this.rootStore.nativelyNumberSuffixedDefaults;
|
return this.rootStore.nativelyNumberSuffixedDefaults;
|
||||||
|
@ -830,6 +833,7 @@ export default mixins(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.workflowsStore.setActive(data.active || false);
|
this.workflowsStore.setActive(data.active || false);
|
||||||
this.workflowsStore.setWorkflowId(workflowId);
|
this.workflowsStore.setWorkflowId(workflowId);
|
||||||
this.workflowsStore.setWorkflowName({ newName: data.name, setStateDirty: false });
|
this.workflowsStore.setWorkflowName({ newName: data.name, setStateDirty: false });
|
||||||
|
@ -837,6 +841,32 @@ export default mixins(
|
||||||
this.workflowsStore.setWorkflowPinData(data.pinData || {});
|
this.workflowsStore.setWorkflowPinData(data.pinData || {});
|
||||||
this.workflowsStore.setWorkflowHash(data.hash);
|
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 tags = (data.tags || []) as ITag[];
|
||||||
const tagIds = tags.map((tag) => tag.id);
|
const tagIds = tags.map((tag) => tag.id);
|
||||||
this.workflowsStore.setWorkflowTagIds(tagIds || []);
|
this.workflowsStore.setWorkflowTagIds(tagIds || []);
|
||||||
|
@ -2372,6 +2402,15 @@ export default mixins(
|
||||||
newNodeData.webhookId = uuid();
|
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]);
|
await this.addNodes([newNodeData]);
|
||||||
|
|
||||||
const pinData = this.workflowsStore.pinDataByNodeName(nodeName);
|
const pinData = this.workflowsStore.pinDataByNodeName(nodeName);
|
||||||
|
@ -3065,6 +3104,7 @@ export default mixins(
|
||||||
this.workflowsStore.resetAllNodesIssues();
|
this.workflowsStore.resetAllNodesIssues();
|
||||||
// vm.$forceUpdate();
|
// vm.$forceUpdate();
|
||||||
|
|
||||||
|
this.workflowsStore.$patch({ workflow: {} });
|
||||||
this.workflowsStore.setActive(false);
|
this.workflowsStore.setActive(false);
|
||||||
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
|
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
|
||||||
this.workflowsStore.setWorkflowName({ newName: '', setStateDirty: false });
|
this.workflowsStore.setWorkflowName({ newName: '', setStateDirty: false });
|
||||||
|
@ -3094,7 +3134,6 @@ export default mixins(
|
||||||
},
|
},
|
||||||
async loadCredentials(): Promise<void> {
|
async loadCredentials(): Promise<void> {
|
||||||
await this.credentialsStore.fetchAllCredentials();
|
await this.credentialsStore.fetchAllCredentials();
|
||||||
await this.credentialsStore.fetchForeignCredentials();
|
|
||||||
},
|
},
|
||||||
async loadNodesProperties(nodeInfos: INodeTypeNameVersion[]): Promise<void> {
|
async loadNodesProperties(nodeInfos: INodeTypeNameVersion[]): Promise<void> {
|
||||||
const allNodes: INodeTypeDescription[] = this.nodeTypesStore.allNodeTypes;
|
const allNodes: INodeTypeDescription[] = this.nodeTypesStore.allNodeTypes;
|
||||||
|
@ -3217,6 +3256,10 @@ export default mixins(
|
||||||
onAddNode({ nodeTypeName, position }: { nodeTypeName: string; position?: [number, number] }) {
|
onAddNode({ nodeTypeName, position }: { nodeTypeName: string; position?: [number, number] }) {
|
||||||
this.addNode(nodeTypeName, { position });
|
this.addNode(nodeTypeName, { position });
|
||||||
},
|
},
|
||||||
|
async saveCurrentWorkflowExternal(callback: () => void) {
|
||||||
|
await this.saveCurrentWorkflow();
|
||||||
|
callback?.();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.$titleReset();
|
this.$titleReset();
|
||||||
|
@ -3304,8 +3347,6 @@ export default mixins(
|
||||||
}, promptTimeout);
|
}, promptTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataPinningEventBus.$on('pin-data', this.addPinDataConnections);
|
|
||||||
dataPinningEventBus.$on('unpin-data', this.removePinDataConnections);
|
|
||||||
},
|
},
|
||||||
activated() {
|
activated() {
|
||||||
const openSideMenu = this.uiStore.addFirstStepOnLoad;
|
const openSideMenu = this.uiStore.addFirstStepOnLoad;
|
||||||
|
@ -3317,23 +3358,29 @@ export default mixins(
|
||||||
document.addEventListener('keydown', this.keyDown);
|
document.addEventListener('keydown', this.keyDown);
|
||||||
document.addEventListener('keyup', this.keyUp);
|
document.addEventListener('keyup', this.keyUp);
|
||||||
window.addEventListener('message', this.onPostMessageReceived);
|
window.addEventListener('message', this.onPostMessageReceived);
|
||||||
|
|
||||||
this.$root.$on('newWorkflow', this.newWorkflow);
|
this.$root.$on('newWorkflow', this.newWorkflow);
|
||||||
this.$root.$on('importWorkflowData', this.onImportWorkflowDataEvent);
|
this.$root.$on('importWorkflowData', this.onImportWorkflowDataEvent);
|
||||||
this.$root.$on('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
this.$root.$on('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
||||||
|
|
||||||
dataPinningEventBus.$on('pin-data', this.addPinDataConnections);
|
dataPinningEventBus.$on('pin-data', this.addPinDataConnections);
|
||||||
dataPinningEventBus.$on('unpin-data', this.removePinDataConnections);
|
dataPinningEventBus.$on('unpin-data', this.removePinDataConnections);
|
||||||
|
nodeViewEventBus.$on('saveWorkflow', this.saveCurrentWorkflowExternal);
|
||||||
|
|
||||||
this.canvasStore.isDemo = this.isDemo;
|
this.canvasStore.isDemo = this.isDemo;
|
||||||
},
|
},
|
||||||
deactivated () {
|
deactivated () {
|
||||||
document.removeEventListener('keydown', this.keyDown);
|
document.removeEventListener('keydown', this.keyDown);
|
||||||
document.removeEventListener('keyup', this.keyUp);
|
document.removeEventListener('keyup', this.keyUp);
|
||||||
window.removeEventListener('message', this.onPostMessageReceived);
|
window.removeEventListener('message', this.onPostMessageReceived);
|
||||||
|
|
||||||
this.$root.$off('newWorkflow', this.newWorkflow);
|
this.$root.$off('newWorkflow', this.newWorkflow);
|
||||||
this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent);
|
this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent);
|
||||||
this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
||||||
|
|
||||||
dataPinningEventBus.$off('pin-data', this.addPinDataConnections);
|
dataPinningEventBus.$off('pin-data', this.addPinDataConnections);
|
||||||
dataPinningEventBus.$off('unpin-data', this.removePinDataConnections);
|
dataPinningEventBus.$off('unpin-data', this.removePinDataConnections);
|
||||||
|
nodeViewEventBus.$off('saveWorkflow', this.saveCurrentWorkflowExternal);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
this.resetWorkspace();
|
this.resetWorkspace();
|
||||||
|
@ -3342,9 +3389,6 @@ export default mixins(
|
||||||
this.$root.$off('newWorkflow', this.newWorkflow);
|
this.$root.$off('newWorkflow', this.newWorkflow);
|
||||||
this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent);
|
this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent);
|
||||||
this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
||||||
|
|
||||||
dataPinningEventBus.$off('pin-data', this.addPinDataConnections);
|
|
||||||
dataPinningEventBus.$off('unpin-data', this.removePinDataConnections);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
:filters="filters"
|
:filters="filters"
|
||||||
:additional-filters-handler="onFilter"
|
:additional-filters-handler="onFilter"
|
||||||
:show-aside="allWorkflows.length > 0"
|
:show-aside="allWorkflows.length > 0"
|
||||||
:shareable="false"
|
:shareable="isShareable"
|
||||||
@click:add="addWorkflow"
|
@click:add="addWorkflow"
|
||||||
@update:filters="filters = $event"
|
@update:filters="filters = $event"
|
||||||
>
|
>
|
||||||
|
@ -69,7 +69,7 @@ import PageViewLayoutList from "@/components/layouts/PageViewLayoutList.vue";
|
||||||
import WorkflowCard from "@/components/WorkflowCard.vue";
|
import WorkflowCard from "@/components/WorkflowCard.vue";
|
||||||
import TemplateCard from "@/components/TemplateCard.vue";
|
import TemplateCard from "@/components/TemplateCard.vue";
|
||||||
import { debounceHelper } from '@/components/mixins/debounce';
|
import { debounceHelper } from '@/components/mixins/debounce';
|
||||||
import {VIEWS} from '@/constants';
|
import {EnterpriseEditionFeature, VIEWS} from '@/constants';
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import {ITag, IUser, IWorkflowDb} from "@/Interface";
|
import {ITag, IUser, IWorkflowDb} from "@/Interface";
|
||||||
import TagsDropdown from "@/components/TagsDropdown.vue";
|
import TagsDropdown from "@/components/TagsDropdown.vue";
|
||||||
|
@ -118,6 +118,9 @@ export default mixins(
|
||||||
allWorkflows(): IWorkflowDb[] {
|
allWorkflows(): IWorkflowDb[] {
|
||||||
return this.workflowsStore.allWorkflows;
|
return this.workflowsStore.allWorkflows;
|
||||||
},
|
},
|
||||||
|
isShareable(): boolean {
|
||||||
|
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowSharing);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addWorkflow() {
|
addWorkflow() {
|
||||||
|
@ -188,8 +191,7 @@ export default mixins(
|
||||||
svg {
|
svg {
|
||||||
width: 48px!important;
|
width: 48px!important;
|
||||||
color: var(--color-foreground-dark);
|
color: var(--color-foreground-dark);
|
||||||
transition: color 0.3s ease;
|
transition: color 0.3s ease;}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue