refactor(core): Delete all collaboration related code (no-changelog) (#9929)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2024-07-03 18:46:24 +02:00
committed by GitHub
parent 24091dfd9b
commit 22990342df
23 changed files with 57 additions and 911 deletions

View File

@@ -432,16 +432,6 @@ export interface IExecutionDeleteFilter {
ids?: string[];
}
export type PushDataUsersForWorkflow = {
workflowId: string;
activeUsers: Array<{ user: IUser; lastSeen: string }>;
};
type PushDataWorkflowUsersChanged = {
data: PushDataUsersForWorkflow;
type: 'activeWorkflowUsersChanged';
};
export type IPushData =
| PushDataExecutionFinished
| PushDataExecutionStarted
@@ -456,8 +446,7 @@ export type IPushData =
| PushDataWorkerStatusMessage
| PushDataActiveWorkflowAdded
| PushDataActiveWorkflowRemoved
| PushDataWorkflowFailedToActivate
| PushDataWorkflowUsersChanged;
| PushDataWorkflowFailedToActivate;
export type PushDataActiveWorkflowAdded = {
data: IActiveWorkflowAdded;

View File

@@ -1,82 +0,0 @@
<script setup lang="ts">
import { useUsersStore } from '@/stores/users.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useCollaborationStore } from '@/stores/collaboration.store';
import { onBeforeUnmount, onMounted, computed, ref } from 'vue';
import { TIME } from '@/constants';
import { isUserGlobalOwner } from '@/utils/userUtils';
const collaborationStore = useCollaborationStore();
const usersStore = useUsersStore();
const workflowsStore = useWorkflowsStore();
const HEARTBEAT_INTERVAL = 5 * TIME.MINUTE;
const heartbeatTimer = ref<number | null>(null);
const activeUsersSorted = computed(() => {
const currentWorkflowUsers = (collaborationStore.getUsersForCurrentWorkflow ?? []).map(
(userInfo) => userInfo.user,
);
const owner = currentWorkflowUsers.find(isUserGlobalOwner);
return {
defaultGroup: owner
? [owner, ...currentWorkflowUsers.filter((user) => user.id !== owner.id)]
: currentWorkflowUsers,
};
});
const currentUserEmail = computed(() => {
return usersStore.currentUser?.email;
});
const startHeartbeat = () => {
if (heartbeatTimer.value !== null) {
clearInterval(heartbeatTimer.value);
heartbeatTimer.value = null;
}
heartbeatTimer.value = window.setInterval(() => {
collaborationStore.notifyWorkflowOpened(workflowsStore.workflow.id);
}, HEARTBEAT_INTERVAL);
};
const stopHeartbeat = () => {
if (heartbeatTimer.value !== null) {
clearInterval(heartbeatTimer.value);
}
};
const onDocumentVisibilityChange = () => {
if (document.visibilityState === 'hidden') {
stopHeartbeat();
} else {
startHeartbeat();
}
};
onMounted(() => {
collaborationStore.initialize();
startHeartbeat();
document.addEventListener('visibilitychange', onDocumentVisibilityChange);
});
onBeforeUnmount(() => {
document.removeEventListener('visibilitychange', onDocumentVisibilityChange);
stopHeartbeat();
collaborationStore.terminate();
});
</script>
<template>
<div
:class="`collaboration-pane-container ${$style.container}`"
data-test-id="collaboration-pane"
>
<n8n-user-stack :users="activeUsersSorted" :current-user-email="currentUserEmail" />
</div>
</template>
<style lang="scss" module>
.container {
margin: 0 var(--spacing-4xs);
}
</style>

View File

@@ -22,7 +22,6 @@ import SaveButton from '@/components/SaveButton.vue';
import TagsDropdown from '@/components/TagsDropdown.vue';
import InlineTextEdit from '@/components/InlineTextEdit.vue';
import BreakpointsObserver from '@/components/BreakpointsObserver.vue';
import CollaborationPane from '@/components/MainHeader/CollaborationPane.vue';
import { useRootStore } from '@/stores/root.store';
import { useSettingsStore } from '@/stores/settings.store';
@@ -622,7 +621,6 @@ function showCreateWorkflowSuccessToast(id?: string) {
</span>
<EnterpriseEdition :features="[EnterpriseEditionFeature.Sharing]">
<div :class="$style.group">
<CollaborationPane />
<N8nButton
type="secondary"
data-test-id="workflow-share-button"

View File

@@ -1,110 +0,0 @@
import { merge } from 'lodash-es';
import { SETTINGS_STORE_DEFAULT_STATE, waitAllPromises } from '@/__tests__/utils';
import { ROLE, STORES } from '@/constants';
import { createTestingPinia } from '@pinia/testing';
import CollaborationPane from '@/components//MainHeader/CollaborationPane.vue';
import type { RenderOptions } from '@/__tests__/render';
import { createComponentRenderer } from '@/__tests__/render';
const OWNER_USER = {
createdAt: '2023-11-22T10:17:12.246Z',
id: 'aaaaaa',
email: 'owner@user.com',
firstName: 'Owner',
lastName: 'User',
role: ROLE.Owner,
disabled: false,
isPending: false,
fullName: 'Owner User',
};
const MEMBER_USER = {
createdAt: '2023-11-22T10:17:12.246Z',
id: 'aaabbb',
email: 'member@user.com',
firstName: 'Member',
lastName: 'User',
role: ROLE.Member,
disabled: false,
isPending: false,
fullName: 'Member User',
};
const MEMBER_USER_2 = {
createdAt: '2023-11-22T10:17:12.246Z',
id: 'aaaccc',
email: 'member2@user.com',
firstName: 'Another Member',
lastName: 'User',
role: ROLE.Member,
disabled: false,
isPending: false,
fullName: 'Another Member User',
};
const initialState = {
[STORES.SETTINGS]: {
settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings),
},
[STORES.WORKFLOWS]: {
workflow: {
id: 'w1',
},
},
[STORES.USERS]: {
currentUserId: 'aaaaaa',
users: {
aaaaaa: OWNER_USER,
aaabbb: MEMBER_USER,
aaaccc: MEMBER_USER_2,
},
},
[STORES.COLLABORATION]: {
usersForWorkflows: {
w1: [
{ lastSeen: '2023-11-22T10:17:12.246Z', user: MEMBER_USER },
{ lastSeen: '2023-11-22T10:17:12.246Z', user: OWNER_USER },
],
w2: [{ lastSeen: '2023-11-22T10:17:12.246Z', user: MEMBER_USER_2 }],
},
},
};
const defaultRenderOptions: RenderOptions = {
pinia: createTestingPinia({ initialState }),
};
const renderComponent = createComponentRenderer(CollaborationPane, defaultRenderOptions);
describe('CollaborationPane', () => {
afterEach(() => {
vi.clearAllMocks();
});
it('should show only current workflow users', async () => {
const { getByTestId, queryByTestId } = renderComponent();
await waitAllPromises();
expect(getByTestId('collaboration-pane')).toBeInTheDocument();
expect(getByTestId('user-stack-avatars')).toBeInTheDocument();
expect(getByTestId(`user-stack-avatar-${OWNER_USER.id}`)).toBeInTheDocument();
expect(getByTestId(`user-stack-avatar-${MEMBER_USER.id}`)).toBeInTheDocument();
expect(queryByTestId(`user-stack-avatar-${MEMBER_USER_2.id}`)).toBeNull();
});
it('should render current user correctly', async () => {
const { getByText, queryByText } = renderComponent();
await waitAllPromises();
expect(getByText(`${OWNER_USER.fullName} (you)`)).toBeInTheDocument();
expect(queryByText(`${MEMBER_USER.fullName} (you)`)).toBeNull();
expect(queryByText(`${MEMBER_USER.fullName}`)).toBeInTheDocument();
});
it('should always render owner first in the list', async () => {
const { getByTestId } = renderComponent();
await waitAllPromises();
const firstAvatar = getByTestId('user-stack-avatars').querySelector('.n8n-avatar');
// Owner is second in the store bur shourld be rendered first
expect(firstAvatar).toHaveAttribute('data-test-id', `user-stack-avatar-${OWNER_USER.id}`);
});
});

View File

@@ -2,7 +2,6 @@ import { usePushConnection } from '@/composables/usePushConnection';
import { useRouter } from 'vue-router';
import { createPinia, setActivePinia } from 'pinia';
import { usePushConnectionStore } from '@/stores/pushConnection.store';
import { useCollaborationStore } from '@/stores/collaboration.store';
import type { IPushData, PushDataWorkerStatusMessage } from '@/Interface';
import { useOrchestrationStore } from '@/stores/orchestration.store';
@@ -21,7 +20,6 @@ vi.useFakeTimers();
describe('usePushConnection()', () => {
let router: ReturnType<typeof useRouter>;
let pushStore: ReturnType<typeof usePushConnectionStore>;
let collaborationStore: ReturnType<typeof useCollaborationStore>;
let orchestrationStore: ReturnType<typeof useOrchestrationStore>;
let pushConnection: ReturnType<typeof usePushConnection>;
@@ -30,7 +28,6 @@ describe('usePushConnection()', () => {
router = vi.mocked(useRouter)();
pushStore = usePushConnectionStore();
collaborationStore = useCollaborationStore();
orchestrationStore = useOrchestrationStore();
pushConnection = usePushConnection({ router });
});
@@ -43,12 +40,6 @@ describe('usePushConnection()', () => {
expect(spy).toHaveBeenCalled();
});
it('should initialize collaborationStore', () => {
const spy = vi.spyOn(collaborationStore, 'initialize').mockImplementation(() => {});
pushConnection.initialize();
expect(spy).toHaveBeenCalled();
});
});
describe('terminate()', () => {
@@ -61,12 +52,6 @@ describe('usePushConnection()', () => {
expect(returnFn).toHaveBeenCalled();
});
it('should terminate collaborationStore', () => {
const spy = vi.spyOn(collaborationStore, 'terminate').mockImplementation(() => {});
pushConnection.terminate();
expect(spy).toHaveBeenCalled();
});
});
describe('queuePushMessage()', () => {

View File

@@ -35,7 +35,6 @@ import { parse } from 'flatted';
import { ref } from 'vue';
import { useOrchestrationStore } from '@/stores/orchestration.store';
import { usePushConnectionStore } from '@/stores/pushConnection.store';
import { useCollaborationStore } from '@/stores/collaboration.store';
import { useExternalHooks } from '@/composables/useExternalHooks';
import type { useRouter } from 'vue-router';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
@@ -51,7 +50,6 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
const i18n = useI18n();
const telemetry = useTelemetry();
const collaborationStore = useCollaborationStore();
const credentialsStore = useCredentialsStore();
const nodeTypesStore = useNodeTypesStore();
const orchestrationManagerStore = useOrchestrationStore();
@@ -68,11 +66,9 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
removeEventListener.value = pushStore.addEventListener((message) => {
void pushMessageReceived(message);
});
collaborationStore.initialize();
}
function terminate() {
collaborationStore.terminate();
if (typeof removeEventListener.value === 'function') {
removeEventListener.value();
}

View File

@@ -637,7 +637,6 @@ export const enum STORES {
HISTORY = 'history',
CLOUD_PLAN = 'cloudPlan',
RBAC = 'rbac',
COLLABORATION = 'collaboration',
PUSH = 'push',
BECOME_TEMPLATE_CREATOR = 'becomeTemplateCreator',
}

View File

@@ -1,64 +0,0 @@
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { usePushConnectionStore } from '@/stores/pushConnection.store';
import { STORES } from '@/constants';
import type { IUser } from '@/Interface';
type ActiveUsersForWorkflows = {
[workflowId: string]: Array<{ user: IUser; lastSeen: string }>;
};
export const useCollaborationStore = defineStore(STORES.COLLABORATION, () => {
const pushStore = usePushConnectionStore();
const workflowStore = useWorkflowsStore();
const usersForWorkflows = ref<ActiveUsersForWorkflows>({});
const pushStoreEventListenerRemovalFn = ref<(() => void) | null>(null);
const getUsersForCurrentWorkflow = computed(() => {
return usersForWorkflows.value[workflowStore.workflowId] ?? [];
});
function initialize() {
pushStoreEventListenerRemovalFn.value = pushStore.addEventListener((event) => {
if (event.type === 'activeWorkflowUsersChanged') {
const activeWorkflowId = workflowStore.workflowId;
if (event.data.workflowId === activeWorkflowId) {
usersForWorkflows.value[activeWorkflowId] = event.data.activeUsers;
}
}
});
}
function terminate() {
if (typeof pushStoreEventListenerRemovalFn.value === 'function') {
pushStoreEventListenerRemovalFn.value();
}
}
function workflowUsersUpdated(data: ActiveUsersForWorkflows) {
usersForWorkflows.value = data;
}
function notifyWorkflowOpened(workflowId: string) {
pushStore.send({
type: 'workflowOpened',
workflowId,
});
}
function notifyWorkflowClosed(workflowId: string) {
pushStore.send({ type: 'workflowClosed', workflowId });
}
return {
usersForWorkflows,
initialize,
terminate,
notifyWorkflowOpened,
notifyWorkflowClosed,
workflowUsersUpdated,
getUsersForCurrentWorkflow,
};
});

View File

@@ -36,7 +36,6 @@ import { useCredentialsStore } from '@/stores/credentials.store';
import useEnvironmentsStore from '@/stores/environments.ee.store';
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
import { useRootStore } from '@/stores/root.store';
import { useCollaborationStore } from '@/stores/collaboration.store';
import { historyBus } from '@/models/history';
import { useCanvasOperations } from '@/composables/useCanvasOperations';
import { useExecutionsStore } from '@/stores/executions.store';
@@ -79,7 +78,6 @@ const credentialsStore = useCredentialsStore();
const environmentsStore = useEnvironmentsStore();
const externalSecretsStore = useExternalSecretsStore();
const rootStore = useRootStore();
const collaborationStore = useCollaborationStore();
const executionsStore = useExecutionsStore();
const canvasStore = useCanvasStore();
const npsSurveyStore = useNpsSurveyStore();
@@ -173,7 +171,6 @@ async function initializeData() {
workflowId: workflowsStore.workflow.id,
workflowName: workflowsStore.workflow.name,
});
collaborationStore.notifyWorkflowOpened(workflowsStore.workflow.id);
const selectedExecution = executionsStore.activeExecution;
if (selectedExecution?.workflowId !== workflowsStore.workflow.id) {

View File

@@ -246,7 +246,6 @@ import {
AI_NODE_CREATOR_VIEW,
DRAG_EVENT_DATA_KEY,
UPDATE_WEBHOOK_ID_NODE_TYPES,
TIME,
AI_ASSISTANT_LOCAL_STORAGE_KEY,
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT,
} from '@/constants';
@@ -322,7 +321,6 @@ import type {
import { type RouteLocation, useRouter } from 'vue-router';
import { dataPinningEventBus, nodeViewEventBus } from '@/event-bus';
import { useCanvasStore } from '@/stores/canvas.store';
import { useCollaborationStore } from '@/stores/collaboration.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
@@ -471,18 +469,15 @@ export default defineComponent({
await this.$router.push(to);
} else {
this.collaborationStore.notifyWorkflowClosed(this.currentWorkflow);
next();
}
} else if (confirmModal === MODAL_CANCEL) {
this.collaborationStore.notifyWorkflowClosed(this.currentWorkflow);
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
this.resetWorkspace();
this.uiStore.stateIsDirty = false;
next();
}
} else {
this.collaborationStore.notifyWorkflowClosed(this.currentWorkflow);
next();
}
},
@@ -588,7 +583,6 @@ export default defineComponent({
useWorkflowsEEStore,
useHistoryStore,
useExternalSecretsStore,
useCollaborationStore,
usePushConnectionStore,
useSourceControlStore,
useExecutionsStore,
@@ -1035,7 +1029,6 @@ export default defineComponent({
if (!this.isDemo) {
this.pushStore.pushConnect();
}
this.collaborationStore.initialize();
},
beforeUnmount() {
// Make sure the event listeners get removed again else we
@@ -1049,7 +1042,6 @@ export default defineComponent({
if (!this.isDemo) {
this.pushStore.pushDisconnect();
}
this.collaborationStore.terminate();
this.resetWorkspace();
this.instance.unbind();
@@ -1506,7 +1498,6 @@ export default defineComponent({
this.executionsStore.activeExecution = selectedExecution;
}
this.canvasStore.stopLoading();
this.collaborationStore.notifyWorkflowOpened(workflow.id);
},
touchTap(e: MouseEvent | TouchEvent) {
if (this.deviceSupport.isTouchDevice) {
@@ -3681,18 +3672,10 @@ export default defineComponent({
if (this.isDemo || window.preventNodeViewBeforeUnload) {
return;
} else if (this.uiStore.stateIsDirty) {
// A bit hacky solution to detecting users leaving the page after prompt:
// 1. Notify that workflow is closed straight away
this.collaborationStore.notifyWorkflowClosed(this.workflowsStore.workflowId);
// 2. If user decided to stay on the page we notify that the workflow is opened again
this.unloadTimeout = setTimeout(() => {
this.collaborationStore.notifyWorkflowOpened(this.workflowsStore.workflowId);
}, 5 * TIME.SECOND);
e.returnValue = true; //Gecko + IE
return true; //Gecko + Webkit, Safari, Chrome etc.
} else {
this.canvasStore.startLoading(this.$locale.baseText('nodeView.redirecting'));
this.collaborationStore.notifyWorkflowClosed(this.workflowsStore.workflowId);
return;
}
},