-
Notifications
You must be signed in to change notification settings - Fork 10.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(editor): Show avatars for users currently working on the same wo…
…rkflow (#7763) This PR introduces the following changes: - New Vue stores: `collaborationStore` and `pushConnectionStore` - Front-end push connection handling overhaul: Keep only a singe connection open and handle it from the new store - Add user avatars in the editor header when there are multiple users working on the same workflow - Sending a heartbeat event to back-end service periodically to confirm user is still active - Back-end overhauls (authored by @tomi): - Implementing a cleanup procedure that removes inactive users - Refactoring collaboration service current implementation --------- Co-authored-by: Tomi Turtiainen <[email protected]>
- Loading branch information
1 parent
99a9ea4
commit 77bc8ec
Showing
18 changed files
with
654 additions
and
148 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
61 changes: 61 additions & 0 deletions
61
packages/cli/test/unit/collaboration/collaboration.state.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { TIME } from '@/constants'; | ||
import { CollaborationState } from '@/collaboration/collaboration.state'; | ||
|
||
const origDate = global.Date; | ||
|
||
const mockDateFactory = (currentDate: string) => { | ||
return class CustomDate extends origDate { | ||
constructor() { | ||
super(currentDate); | ||
} | ||
} as DateConstructor; | ||
}; | ||
|
||
describe('CollaborationState', () => { | ||
let collaborationState: CollaborationState; | ||
|
||
beforeEach(() => { | ||
collaborationState = new CollaborationState(); | ||
}); | ||
|
||
describe('cleanInactiveUsers', () => { | ||
const workflowId = 'workflow'; | ||
|
||
it('should remove inactive users', () => { | ||
// Setup | ||
global.Date = mockDateFactory('2023-01-01T00:00:00.000Z'); | ||
collaborationState.addActiveWorkflowUser(workflowId, 'inactiveUser'); | ||
|
||
global.Date = mockDateFactory('2023-01-01T00:30:00.000Z'); | ||
collaborationState.addActiveWorkflowUser(workflowId, 'activeUser'); | ||
|
||
// Act: Clean inactive users | ||
jest | ||
.spyOn(global.Date, 'now') | ||
.mockReturnValue(new origDate('2023-01-01T00:35:00.000Z').getTime()); | ||
collaborationState.cleanInactiveUsers(workflowId, 10 * TIME.MINUTE); | ||
|
||
// Assert: The inactive user should be removed | ||
expect(collaborationState.getActiveWorkflowUsers(workflowId)).toEqual([ | ||
{ userId: 'activeUser', lastSeen: new origDate('2023-01-01T00:30:00.000Z') }, | ||
]); | ||
}); | ||
|
||
it('should not remove active users', () => { | ||
// Setup: Add an active user to the state | ||
global.Date = mockDateFactory('2023-01-01T00:30:00.000Z'); | ||
collaborationState.addActiveWorkflowUser(workflowId, 'activeUser'); | ||
|
||
// Act: Clean inactive users | ||
jest | ||
.spyOn(global.Date, 'now') | ||
.mockReturnValue(new origDate('2023-01-01T00:35:00.000Z').getTime()); | ||
collaborationState.cleanInactiveUsers(workflowId, 10 * TIME.MINUTE); | ||
|
||
// Assert: The active user should still be present | ||
expect(collaborationState.getActiveWorkflowUsers(workflowId)).toEqual([ | ||
{ userId: 'activeUser', lastSeen: new origDate('2023-01-01T00:30:00.000Z') }, | ||
]); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
packages/editor-ui/src/components/MainHeader/CollaborationPane.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
<script setup lang="ts"> | ||
import { useUsersStore } from '@/stores/users.store'; | ||
import { useWorkflowsStore } from '@/stores/workflows.store'; | ||
import { useCollaborationStore } from '@/stores/collaboration.store'; | ||
import { onBeforeUnmount } from 'vue'; | ||
import { onMounted } from 'vue'; | ||
import { computed, ref } from 'vue'; | ||
import { TIME } from '@/constants'; | ||
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((user) => user.globalRoleId === 1); | ||
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(() => { | ||
startHeartbeat(); | ||
document.addEventListener('visibilitychange', onDocumentVisibilityChange); | ||
}); | ||
onBeforeUnmount(() => { | ||
document.removeEventListener('visibilitychange', onDocumentVisibilityChange); | ||
stopHeartbeat(); | ||
}); | ||
</script> | ||
|
||
<template> | ||
<div | ||
:class="`collaboration-pane-container ${$style.container}`" | ||
data-test-id="collaboration-pane" | ||
> | ||
<n8n-user-stack :users="activeUsersSorted" :currentUserEmail="currentUserEmail" /> | ||
</div> | ||
</template> | ||
|
||
<style lang="scss" module> | ||
.container { | ||
margin: 0 var(--spacing-4xs); | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.