Files
n8n-enterprise-unlocked/packages/editor-ui/src/stores/ui.ts
Omar Ajoue 25e9f0817a refactor: Workflow sharing bug bash fixes (#4888)
* fix: Prevent workflows with only manual trigger from being activated

* fix: Fix workflow id when sharing from workflows list

* fix: Update sharing modal translations

* fix: Allow sharees to disable workflows and fix issue with unique key when removing a user

* refactor: Improve error messages and change logging level to be less verbose

* fix: Broken user removal transfer issue

* feat: Implement workflow sharing BE telemetry

* chore: temporarily add sharing env vars

* feat: Implement BE telemetry for workflow sharing

* fix: Prevent issues with possibly missing workflow id

* feat: Replace WorkflowSharing flag references (no-changelog) (#4918)

* ci: Block all external network calls in tests (no-changelog) (#4930)

* setup nock to prevent tests from making any external requests

* mock all calls to posthog sdk

* feat: Replace WorkflowSharing flag references (no-changelog)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <netroy@users.noreply.github.com>

* refactor: Remove temporary feature flag for workflow sharing

* refactor: add sharing_role to both manual and node executions

* refactor: Allow changing name, position and disabled of read only nodes

* feat: Overhaul dynamic translations for local and cloud (#4943)

* feat: Overhaul dynamic translations for local and cloud

* fix: remove type casting

* chore: remove unused translations

* fix: fix workflow sharing translation

* test: Fix broken test

* refactor: remove unnecessary import

* refactor: Minor code improvements

* refactor: rename dynamicTranslations to contextBasedTranslationKeys

* fix: fix type imports

* refactor: Consolidate sharing feature check

* feat: update cred sharing unavailable translations

* feat: update upgrade message when user management not available

* fix: rename plan names to Pro and Power

* feat: update translations to no longer contain plan names

* wip: subworkflow permissions

* feat: add workflowsFromSameOwner caller policy

* feat: Fix subworkflow permissions

* shared entites should check for role when deleting users

* refactor: remove circular dependency

* role filter shouldn't be an array

* fixed role issue

* fix: Corrected behavior when removing users

* feat: show instance owner credential sharing message only if isnt sharee

* feat: update workflow caller policy caller ids labels

* feat: update upgrade plan links to contain instance ids

* fix: show check errors below creds message only to owner

* fix(editor): Hide usage page on cloud

* fix: update credential validation error message for sharee

* fix(core): Remove duplicate import

* fix(editor): Extending deployment types

* feat: Overhaul contextual translations (#4992)

feat: update how contextual translations work

* refactor: improve messageing for subworkflow permissions

* test: Fix issue with user deletion and transfer

* fix: Explicitly throw error message so it can be displayed in UI

Co-authored-by: Alex Grozav <alex@grozav.com>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <netroy@users.noreply.github.com>
Co-authored-by: freyamade <freya@n8n.io>
Co-authored-by: Csaba Tuncsik <csaba@n8n.io>
2022-12-21 16:42:07 +01:00

428 lines
12 KiB
TypeScript

import {
applyForOnboardingCall,
fetchNextOnboardingPrompt,
submitEmailOnSignup,
} from '@/api/workflow-webhooks';
import {
ABOUT_MODAL_KEY,
CHANGE_PASSWORD_MODAL_KEY,
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
COMMUNITY_PACKAGE_MANAGE_ACTIONS,
CONTACT_PROMPT_MODAL_KEY,
CREDENTIAL_EDIT_MODAL_KEY,
CREDENTIAL_SELECT_MODAL_KEY,
DELETE_USER_MODAL_KEY,
DUPLICATE_MODAL_KEY,
EXECUTIONS_MODAL_KEY,
FAKE_DOOR_FEATURES,
IMPORT_CURL_MODAL_KEY,
INVITE_USER_MODAL_KEY,
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
PERSONALIZATION_MODAL_KEY,
STORES,
TAGS_MANAGER_MODAL_KEY,
VALUE_SURVEY_MODAL_KEY,
VERSIONS_MODAL_KEY,
VIEWS,
WORKFLOW_ACTIVE_MODAL_KEY,
WORKFLOW_SETTINGS_MODAL_KEY,
WORKFLOW_SHARE_MODAL_KEY,
} from '@/constants';
import {
CurlToJSONResponse,
IFakeDoorLocation,
IMenuItem,
INodeUi,
IOnboardingCallPrompt,
IUser,
UIState,
XYPosition,
} from '@/Interface';
import Vue from 'vue';
import { defineStore } from 'pinia';
import { useRootStore } from './n8nRootStore';
import { getCurlToJson } from '@/api/curlHelper';
import { useWorkflowsStore } from './workflows';
import { useSettingsStore } from './settings';
export const useUIStore = defineStore(STORES.UI, {
state: (): UIState => ({
activeActions: [],
activeCredentialType: null,
modals: {
[ABOUT_MODAL_KEY]: {
open: false,
},
[CHANGE_PASSWORD_MODAL_KEY]: {
open: false,
},
[CONTACT_PROMPT_MODAL_KEY]: {
open: false,
},
[CREDENTIAL_EDIT_MODAL_KEY]: {
open: false,
mode: '',
activeId: null,
},
[CREDENTIAL_SELECT_MODAL_KEY]: {
open: false,
},
[DELETE_USER_MODAL_KEY]: {
open: false,
activeId: null,
},
[DUPLICATE_MODAL_KEY]: {
open: false,
},
[ONBOARDING_CALL_SIGNUP_MODAL_KEY]: {
open: false,
},
[PERSONALIZATION_MODAL_KEY]: {
open: false,
},
[INVITE_USER_MODAL_KEY]: {
open: false,
},
[TAGS_MANAGER_MODAL_KEY]: {
open: false,
},
[VALUE_SURVEY_MODAL_KEY]: {
open: false,
},
[VERSIONS_MODAL_KEY]: {
open: false,
},
[WORKFLOW_SETTINGS_MODAL_KEY]: {
open: false,
},
[EXECUTIONS_MODAL_KEY]: {
open: false,
},
[WORKFLOW_SHARE_MODAL_KEY]: {
open: false,
},
[WORKFLOW_ACTIVE_MODAL_KEY]: {
open: false,
},
[COMMUNITY_PACKAGE_INSTALL_MODAL_KEY]: {
open: false,
},
[COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY]: {
open: false,
mode: '',
activeId: null,
},
[IMPORT_CURL_MODAL_KEY]: {
open: false,
curlCommand: '',
httpNodeParameters: '',
},
},
modalStack: [],
sidebarMenuCollapsed: true,
isPageLoading: true,
currentView: '',
mainPanelPosition: 0.5,
fakeDoorFeatures: [
{
id: FAKE_DOOR_FEATURES.ENVIRONMENTS,
featureName: 'fakeDoor.settings.environments.name',
icon: 'server',
infoText: 'fakeDoor.settings.environments.infoText',
actionBoxTitle: 'fakeDoor.settings.environments.actionBox.title',
actionBoxDescription: 'fakeDoor.settings.environments.actionBox.description',
linkURL: 'https://n8n-community.typeform.com/to/l7QOrERN#f=environments',
uiLocations: ['settings'],
},
{
id: FAKE_DOOR_FEATURES.LOGGING,
featureName: 'fakeDoor.settings.logging.name',
icon: 'sign-in-alt',
infoText: 'fakeDoor.settings.logging.infoText',
actionBoxTitle: 'fakeDoor.settings.logging.actionBox.title',
actionBoxDescription: 'fakeDoor.settings.logging.actionBox.description',
linkURL: 'https://n8n-community.typeform.com/to/l7QOrERN#f=logging',
uiLocations: ['settings'],
},
],
draggable: {
isDragging: false,
type: '',
data: '',
canDrop: false,
stickyPosition: null,
},
stateIsDirty: false,
lastSelectedNode: null,
lastSelectedNodeOutputIndex: null,
nodeViewOffsetPosition: [0, 0],
nodeViewMoveInProgress: false,
selectedNodes: [],
sidebarMenuItems: [],
nodeViewInitialized: false,
addFirstStepOnLoad: false,
executionSidebarAutoRefresh: true,
}),
getters: {
contextBasedTranslationKeys() {
const settingsStore = useSettingsStore();
const deploymentType = settingsStore.deploymentType;
let contextKey = '';
if (deploymentType === 'cloud') {
contextKey = '.cloud';
} else if (deploymentType === 'desktop_mac' || deploymentType === 'desktop_win') {
contextKey = '.desktop';
}
return {
upgradeLinkUrl: `contextual.upgradeLinkUrl${contextKey}`,
credentials: {
sharing: {
unavailable: {
title: `contextual.credentials.sharing.unavailable.title${contextKey}`,
description: `contextual.credentials.sharing.unavailable.description${contextKey}`,
action: `contextual.credentials.sharing.unavailable.action${contextKey}`,
button: `contextual.credentials.sharing.unavailable.button${contextKey}`,
},
},
},
workflows: {
sharing: {
title: 'contextual.workflows.sharing.title',
unavailable: {
title: `contextual.workflows.sharing.unavailable.title${contextKey}`,
description: {
modal: `contextual.workflows.sharing.unavailable.description.modal${contextKey}`,
tooltip: `contextual.workflows.sharing.unavailable.description.tooltip${contextKey}`,
},
action: `contextual.workflows.sharing.unavailable.action${contextKey}`,
button: `contextual.workflows.sharing.unavailable.button${contextKey}`,
},
},
},
};
},
getLastSelectedNode(): INodeUi | null {
const workflowsStore = useWorkflowsStore();
if (this.lastSelectedNode) {
return workflowsStore.getNodeByName(this.lastSelectedNode);
}
return null;
},
getCurlCommand(): string | undefined {
return this.modals[IMPORT_CURL_MODAL_KEY].curlCommand;
},
getHttpNodeParameters(): string | undefined {
return this.modals[IMPORT_CURL_MODAL_KEY].httpNodeParameters;
},
areExpressionsDisabled(): boolean {
return this.currentView === VIEWS.DEMO;
},
isVersionsOpen(): boolean {
return this.modals[VERSIONS_MODAL_KEY].open;
},
isModalOpen() {
return (name: string) => this.modals[name].open;
},
isModalActive() {
return (name: string) => this.modalStack.length > 0 && name === this.modalStack[0];
},
getModalActiveId() {
return (name: string) => this.modals[name].activeId;
},
getModalMode() {
return (name: string) => this.modals[name].mode;
},
getModalData() {
return (name: string) => this.modals[name].data;
},
getFakeDoorByLocation() {
return (location: IFakeDoorLocation) =>
this.fakeDoorFeatures.filter((fakeDoor) => fakeDoor.uiLocations.includes(location));
},
getFakeDoorById() {
return (id: string) =>
this.fakeDoorFeatures.find((fakeDoor) => fakeDoor.id.toString() === id);
},
isNodeView(): boolean {
return [
VIEWS.NEW_WORKFLOW.toString(),
VIEWS.WORKFLOW.toString(),
VIEWS.EXECUTION.toString(),
].includes(this.currentView);
},
isActionActive() {
return (action: string) => this.activeActions.includes(action);
},
getSelectedNodes(): INodeUi[] {
const seen = new Set();
return this.selectedNodes.filter((node: INodeUi) => {
// dedupe for instances when same node is selected in different ways
if (!seen.has(node.id)) {
seen.add(node.id);
return true;
}
return false;
});
},
isNodeSelected() {
return (nodeName: string): boolean => {
let index;
for (index in this.selectedNodes) {
if (this.selectedNodes[index].name === nodeName) {
return true;
}
}
return false;
};
},
},
actions: {
setMode(name: string, mode: string): void {
Vue.set(this.modals[name], 'mode', mode);
},
setActiveId(name: string, id: string): void {
Vue.set(this.modals[name], 'activeId', id);
},
setModalData(payload: { name: string; data: Record<string, unknown> }) {
Vue.set(this.modals[payload.name], 'data', payload.data);
},
openModal(name: string): void {
Vue.set(this.modals[name], 'open', true);
this.modalStack = [name].concat(this.modalStack);
},
openModalWithData(payload: { name: string; data: Record<string, unknown> }): void {
this.setModalData(payload);
this.openModal(payload.name);
},
closeModal(name: string): void {
Vue.set(this.modals[name], 'open', false);
this.modalStack = this.modalStack.filter((openModalName: string) => {
return name !== openModalName;
});
},
closeAllModals(): void {
Object.keys(this.modals).forEach((name: string) => {
if (this.modals[name].open) {
Vue.set(this.modals[name], 'open', false);
}
});
this.modalStack = [];
},
draggableStartDragging(type: string, data: string): void {
this.draggable = {
isDragging: true,
type,
data,
canDrop: false,
stickyPosition: null,
};
},
draggableStopDragging(): void {
this.draggable = {
isDragging: false,
type: '',
data: '',
canDrop: false,
stickyPosition: null,
};
},
setDraggableStickyPos(position: XYPosition): void {
Vue.set(this.draggable, 'stickyPosition', position);
},
setDraggableCanDrop(canDrop: boolean): void {
Vue.set(this.draggable, 'canDrop', canDrop);
},
openDeleteUserModal(id: string): void {
this.setActiveId(DELETE_USER_MODAL_KEY, id);
this.openModal(DELETE_USER_MODAL_KEY);
},
openExistingCredential(id: string): void {
this.setActiveId(CREDENTIAL_EDIT_MODAL_KEY, id);
this.setMode(CREDENTIAL_EDIT_MODAL_KEY, 'edit');
this.openModal(CREDENTIAL_EDIT_MODAL_KEY);
},
openNewCredential(type: string): void {
this.setActiveId(CREDENTIAL_EDIT_MODAL_KEY, type);
this.setMode(CREDENTIAL_EDIT_MODAL_KEY, 'new');
this.openModal(CREDENTIAL_EDIT_MODAL_KEY);
},
async getNextOnboardingPrompt(): Promise<IOnboardingCallPrompt> {
const rootStore = useRootStore();
const instanceId = rootStore.instanceId;
// TODO: current USER
const currentUser = {} as IUser;
return await fetchNextOnboardingPrompt(instanceId, currentUser);
},
async applyForOnboardingCall(email: string): Promise<string> {
const rootStore = useRootStore();
const instanceId = rootStore.instanceId;
// TODO: current USER
const currentUser = {} as IUser;
return await applyForOnboardingCall(instanceId, currentUser, email);
},
async submitContactEmail(email: string, agree: boolean): Promise<string> {
const rootStore = useRootStore();
const instanceId = rootStore.instanceId;
// TODO: current USER
const currentUser = {} as IUser;
return await submitEmailOnSignup(instanceId, currentUser, email || currentUser.email, agree);
},
openCommunityPackageUninstallConfirmModal(packageName: string) {
this.setActiveId(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, packageName);
this.setMode(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, COMMUNITY_PACKAGE_MANAGE_ACTIONS.UNINSTALL);
this.openModal(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY);
},
openCommunityPackageUpdateConfirmModal(packageName: string) {
this.setActiveId(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, packageName);
this.setMode(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, COMMUNITY_PACKAGE_MANAGE_ACTIONS.UPDATE);
this.openModal(COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY);
},
addActiveAction(action: string): void {
if (!this.activeActions.includes(action)) {
this.activeActions.push(action);
}
},
removeActiveAction(action: string): void {
const actionIndex = this.activeActions.indexOf(action);
if (actionIndex !== -1) {
this.activeActions.splice(actionIndex, 1);
}
},
addSelectedNode(node: INodeUi): void {
this.selectedNodes.push(node);
},
removeNodeFromSelection(node: INodeUi): void {
let index;
for (index in this.selectedNodes) {
if (this.selectedNodes[index].name === node.name) {
this.selectedNodes.splice(parseInt(index, 10), 1);
break;
}
}
},
resetSelectedNodes(): void {
Vue.set(this, 'selectedNodes', []);
},
addSidebarMenuItems(menuItems: IMenuItem[]) {
const updated = this.sidebarMenuItems.concat(menuItems);
Vue.set(this, 'sidebarMenuItems', updated);
},
setCurlCommand(payload: { name: string; command: string }): void {
Vue.set(this.modals[payload.name], 'curlCommand', payload.command);
},
setHttpNodeParameters(payload: { name: string; parameters: string }): void {
Vue.set(this.modals[payload.name], 'httpNodeParameters', payload.parameters);
},
toggleSidebarMenuCollapse(): void {
this.sidebarMenuCollapsed = !this.sidebarMenuCollapsed;
},
async getCurlToJson(curlCommand: string): Promise<CurlToJSONResponse> {
const rootStore = useRootStore();
return await getCurlToJson(rootStore.getRestApiContext, curlCommand);
},
},
});