Skip to content

Commit 3fd9afa

Browse files
authored
Merge pull request #342 from codex-team/feat/remove-team-member
Feat: remove team member
2 parents 113dd0b + ccfe732 commit 3fd9afa

File tree

8 files changed

+191
-1
lines changed

8 files changed

+191
-1
lines changed

src/application/i18n/messages/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@
6767
"roles": {
6868
"Read": "Reader",
6969
"Write": "Writer"
70+
},
71+
"removeMemberConfirmationTitle": "Remove member",
72+
"removeMemberConfirmationBody": "Are you sure you want to remove '{username}' from the team?",
73+
"contextMenu": {
74+
"title": "More actions",
75+
"remove": "Remove"
7076
}
7177
}
7278
},

src/application/services/useNoteSettings.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ interface UseNoteSettingsComposableState {
6666
* @param newParentURL - New parent note URL
6767
*/
6868
setParent: (id: NoteId, newParentURL: string) => Promise<void>;
69+
70+
/**
71+
* Delete team member by user id
72+
* @param id - Note id
73+
* @param userId - User id
74+
* @returns true if user was removed
75+
*/
76+
removeMemberByUserId: (id: NoteId, userId: UserId) => Promise<boolean>;
6977
}
7078

7179
/**
@@ -188,6 +196,16 @@ export default function (): UseNoteSettingsComposableState {
188196
}
189197
};
190198

199+
/**
200+
* Delete team member by user id
201+
* @param id - Note id
202+
* @param userId - User id
203+
* @returns true if user was removed
204+
*/
205+
const removeMemberByUserId = async (id: NoteId, userId: UserId): Promise<boolean> => {
206+
return await noteSettingsService.removeMemberByUserId(id, userId);
207+
};
208+
191209
return {
192210
updateCover,
193211
setParent,
@@ -198,5 +216,6 @@ export default function (): UseNoteSettingsComposableState {
198216
revokeHash,
199217
changeRole,
200218
deleteNoteById,
219+
removeMemberByUserId,
201220
};
202221
}

src/domain/noteSettings.repository.interface.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,12 @@ export default interface NoteSettingsRepositoryInterface {
4343
* @param id - Note id
4444
*/
4545
deleteNote(id: NoteId): Promise<void>;
46+
47+
/**
48+
* Delete team member by user id
49+
* @param id - Note id
50+
* @param userId - User id
51+
* @returns true if user was removed
52+
*/
53+
removeMemberByUserId(id: NoteId, userId: UserId): Promise<boolean>;
4654
}

src/domain/noteSettings.service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,14 @@ export default class NoteSettingsService {
118118
public async deleteNote(id: NoteId): Promise<void> {
119119
return await this.noteSettingsRepository.deleteNote(id);
120120
}
121+
122+
/**
123+
* Delete team member by user id
124+
* @param id - Note id
125+
* @param userId - User id
126+
* @returns true if user was removed
127+
*/
128+
public async removeMemberByUserId(id: NoteId, userId: UserId): Promise<boolean> {
129+
return await this.noteSettingsRepository.removeMemberByUserId(id, userId);
130+
}
121131
}

src/infrastructure/noteSettings.repository.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,17 @@ export default class NoteSettingsRepository implements NoteSettingsRepositoryInt
6969
public async deleteNote(id: NoteId): Promise<void> {
7070
await this.transport.delete<boolean>(`/note/` + id);
7171
}
72+
73+
/**
74+
* Delete team member by user id
75+
* @param id - Note id
76+
* @param userId - User id
77+
* @returns true if user was removed
78+
*/
79+
public async removeMemberByUserId(id: NoteId, userId: UserId): Promise<boolean> {
80+
const data = { userId };
81+
const response = await this.transport.delete<number>(`/note-settings/${id}/team`, data);
82+
83+
return response === userId;
84+
}
7285
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<template>
2+
<button
3+
ref="triggerButton"
4+
:title="t('noteSettings.team.contextMenu.title')"
5+
class="more-actions-button"
6+
@click="handleButtonClick"
7+
>
8+
<Icon
9+
name="EtcVertical"
10+
/>
11+
</button>
12+
</template>
13+
14+
<script setup lang="ts">
15+
import { ref } from 'vue';
16+
import { Icon, ContextMenu, usePopover, useConfirm, type ContextMenuItem } from '@codexteam/ui/vue';
17+
import { type TeamMember } from '@/domain/entities/Team';
18+
import { useI18n } from 'vue-i18n';
19+
import { NoteId } from '@/domain/entities/Note';
20+
import useNoteSettings from '@/application/services/useNoteSettings';
21+
22+
const { removeMemberByUserId } = useNoteSettings();
23+
24+
const props = defineProps<{
25+
/**
26+
* Team member data
27+
*/
28+
teamMember: TeamMember;
29+
/**
30+
* Id of the current note
31+
*/
32+
noteId: NoteId;
33+
}>();
34+
35+
const { t } = useI18n();
36+
const { showPopover, hide } = usePopover();
37+
const { confirm } = useConfirm();
38+
39+
const triggerButton = ref<HTMLButtonElement>();
40+
41+
const menuItems: ContextMenuItem[] = [
42+
{
43+
title: t('noteSettings.team.contextMenu.remove'),
44+
onActivate: async () => {
45+
hide();
46+
await handleRemove(props.teamMember);
47+
},
48+
},
49+
];
50+
51+
const emit = defineEmits<{
52+
teamMemberRemoved: [userId: TeamMember['user']['id']];
53+
}>();
54+
55+
const handleButtonClick = (): void => {
56+
if (triggerButton.value) {
57+
showPopover({
58+
targetEl: triggerButton.value,
59+
with: {
60+
component: ContextMenu,
61+
props: {
62+
items: menuItems,
63+
},
64+
},
65+
align: {
66+
vertically: 'below',
67+
horizontally: 'right',
68+
},
69+
width: 'auto',
70+
});
71+
}
72+
};
73+
74+
/**
75+
* Remove team member by user id
76+
*
77+
* @param member - team member to remove
78+
*/
79+
const handleRemove = async (member: TeamMember): Promise<void> => {
80+
const shouldRemove = await confirm(
81+
t('noteSettings.team.removeMemberConfirmationTitle'),
82+
t('noteSettings.team.removeMemberConfirmationBody', { username: member.user.name })
83+
);
84+
85+
if (shouldRemove) {
86+
const isDeleted = await removeMemberByUserId(props.noteId, member.user.id);
87+
88+
if (isDeleted) {
89+
emit('teamMemberRemoved', member.user.id);
90+
}
91+
}
92+
};
93+
</script>
94+
95+
<style scoped>
96+
.more-actions-button {
97+
color: var(--ct-text-color-primary);
98+
display: flex;
99+
align-items: center;
100+
justify-content: center;
101+
border: none;
102+
cursor: pointer;
103+
}
104+
</style>

src/presentation/components/team/Team.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
:note-id="noteId"
1616
:team-member="member"
1717
/>
18+
<MoreActions
19+
:note-id="noteId"
20+
:team-member="member"
21+
@team-member-removed="handleMemberRemoved"
22+
/>
1823
</template>
1924

2025
<template #left>
@@ -34,12 +39,13 @@
3439

3540
<script setup lang="ts">
3641
import { computed } from 'vue';
37-
import { Team, MemberRole } from '@/domain/entities/Team';
42+
import { Team, MemberRole, TeamMember } from '@/domain/entities/Team';
3843
import { Note, NoteId } from '@/domain/entities/Note';
3944
import { Section, Row, Avatar } from '@codexteam/ui/vue';
4045
import RoleSelect from './RoleSelect.vue';
4146
import { useI18n } from 'vue-i18n';
4247
import useNote from '@/application/services/useNote.ts';
48+
import MoreActions from './MoreActions.vue';
4349
4450
const props = defineProps<{
4551
/**
@@ -52,6 +58,10 @@ const props = defineProps<{
5258
noteId: NoteId;
5359
}>();
5460
61+
const emit = defineEmits<{
62+
teamMemberRemoved: [id: TeamMember['user']['id']];
63+
}>();
64+
5565
const { t } = useI18n();
5666
const { note } = useNote({ id: props.noteId });
5767
@@ -79,6 +89,11 @@ const sortedTeam = computed(() => {
7989
return roleOrder[a.role] - roleOrder[b.role];
8090
});
8191
});
92+
93+
// Listen for teamMemberRemoved event from child component and bubble them up
94+
const handleMemberRemoved = (userId: TeamMember['user']['id']): void => {
95+
emit('teamMemberRemoved', userId);
96+
};
8297
</script>
8398

8499
<style scoped>

src/presentation/pages/NoteSettings.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
<Team
7474
:note-id="id"
7575
:team="noteSettings.team"
76+
@team-member-removed="handleTeamMemberRemoved"
7677
/>
7778
<InviteLink
7879
:id="props.id"
@@ -111,6 +112,7 @@ import { getTimeFromNow } from '@/infrastructure/utils/date';
111112
import InviteLink from '@/presentation/components/noteSettings/InviteLink.vue';
112113
import useNavbar from '@/application/services/useNavbar';
113114
import { useRoute } from 'vue-router';
115+
import { TeamMember } from '@/domain/entities/Team';
114116
115117
const { t } = useI18n();
116118
@@ -221,6 +223,19 @@ onMounted(async () => {
221223
parentURL.value = getParentURL(parentNote.value?.id);
222224
});
223225
226+
/**
227+
* Handle team member removal by refreshing the note settings and removing the member from the team
228+
*
229+
* @param userId - user id of the member to remove
230+
*/
231+
async function handleTeamMemberRemoved(userId: TeamMember['user']['id']) {
232+
if (noteSettings.value !== null) {
233+
noteSettings.value = {
234+
...noteSettings.value,
235+
team: noteSettings.value.team.filter(member => member.user.id !== userId),
236+
};
237+
}
238+
}
224239
</script>
225240

226241
<style setup lang="postcss" scoped>

0 commit comments

Comments
 (0)