Skip to content

Commit 2b9caff

Browse files
authored
Add UI to add/remove members to/from projects (#83)
1 parent 2127f6e commit 2b9caff

File tree

3 files changed

+144
-1
lines changed

3 files changed

+144
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<template>
2+
<v-dialog v-model="openDialog" max-width="500px" scrollable>
3+
<v-card>
4+
<v-card-title>Project Members for {{ project && project.name }}</v-card-title>
5+
<v-card-text style="max-height: 600px">
6+
<div v-if="project && project.members">
7+
<v-container class="py-0">
8+
<v-row align="center" justify="start">
9+
<v-col v-for="(selection, i) in selections" :key="selection.id" class="shrink">
10+
<v-chip close @click:close="removeProjectMember(selection.id, i)">
11+
{{ selection.name }}
12+
</v-chip>
13+
</v-col>
14+
15+
<v-col v-if="!allSelected" cols="12">
16+
<v-text-field
17+
ref="search"
18+
v-model="search"
19+
full-width
20+
hide-details
21+
label="Search"
22+
single-line
23+
></v-text-field>
24+
</v-col>
25+
</v-row>
26+
</v-container>
27+
28+
<v-list>
29+
<template v-for="item in list">
30+
<v-list-item
31+
v-if="!selected.some((e) => e.id == item.id)"
32+
:key="item.name"
33+
@click="addProjectMember(item.id, item)"
34+
>
35+
<v-list-item-title v-text="item.name"></v-list-item-title>
36+
</v-list-item>
37+
</template>
38+
</v-list>
39+
</div>
40+
<div v-else class="d-flex justify-center">
41+
<v-progress-circular color="primary" indeterminate />
42+
</div>
43+
</v-card-text>
44+
</v-card>
45+
</v-dialog>
46+
</template>
47+
48+
<script>
49+
import { mapActions, mapGetters, mapState } from 'vuex';
50+
export default {
51+
model: {
52+
prop: 'projectId',
53+
event: 'change',
54+
},
55+
props: {
56+
projectId: { type: Number, default: null },
57+
},
58+
data: function () {
59+
return {
60+
search: '',
61+
selected: [],
62+
};
63+
},
64+
computed: {
65+
...mapState('members', ['members']),
66+
...mapGetters('projects', ['getProjectMembers']),
67+
openDialog: {
68+
get: function () {
69+
return this.projectId !== null;
70+
},
71+
set: function (newValue) {
72+
this.$emit('change', newValue ? newValue : null);
73+
},
74+
},
75+
project() {
76+
if (!this.projectId) return null;
77+
const project = this.getProjectMembers(this.projectId);
78+
// Fetch full project data if data is not in Vuex store
79+
if (!project.members) this.fetchProject(this.projectId);
80+
this.sel(project);
81+
return project;
82+
},
83+
allSelected() {
84+
return this.selected.length === this.members.length;
85+
},
86+
list() {
87+
const search = this.search.toLowerCase();
88+
89+
if (!search) return this.members;
90+
91+
return this.members.filter((item) => {
92+
const text = item.name.toLowerCase();
93+
94+
return text.indexOf(search) > -1;
95+
});
96+
},
97+
selections() {
98+
const selections = [];
99+
100+
for (const selection of this.selected) {
101+
selections.push(selection);
102+
}
103+
104+
return selections;
105+
},
106+
},
107+
watch: {
108+
selected() {
109+
this.search = '';
110+
},
111+
},
112+
methods: {
113+
...mapActions('projects', ['fetchProject', 'addMember', 'deleteMember']),
114+
sel(data) {
115+
this.selected = data.members;
116+
},
117+
removeProjectMember(memberId, i) {
118+
this.deleteMember({
119+
id: this.project.id,
120+
memberId,
121+
});
122+
this.selected.splice(i, 1);
123+
},
124+
addProjectMember(memberId, item) {
125+
this.selected.push(item);
126+
this.addMember({
127+
id: this.project.id,
128+
memberId,
129+
});
130+
},
131+
},
132+
};
133+
</script>
134+
135+
<style></style>

frontend/src/components/projects/ProjectsTable.vue

+8-1
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,11 @@
7979
</v-card-actions>
8080
</v-card>
8181
</v-dialog>
82+
<project-modal v-model="projectMembers" />
8283
</v-toolbar>
8384
</template>
8485
<template #[`item.actions`]="{ item }">
86+
<v-icon small class="mr-2" @click="editMembers(item)"> mdi-account-multiple </v-icon>
8587
<v-icon small class="mr-2" @click="editItem(item)"> mdi-pencil </v-icon>
8688
<v-icon small @click="deleteItem(item)"> mdi-delete </v-icon>
8789
</template>
@@ -95,11 +97,14 @@
9597

9698
<script>
9799
import { mapActions, mapState } from 'vuex';
100+
import ProjectModal from './ProjectModal.vue';
98101
99102
export default {
103+
components: { ProjectModal },
100104
data: () => ({
101105
dialog: false,
102106
dialogDelete: false,
107+
projectMembers: null,
103108
search: '',
104109
headers: [
105110
{ text: 'Project', value: 'name' },
@@ -201,7 +206,9 @@ export default {
201206
}
202207
this.close();
203208
},
204-
209+
editMembers(item) {
210+
this.projectMembers = item.id;
211+
},
205212
...mapActions('projects', ['updateProject', 'createProject', 'deleteProject']),
206213
},
207214
};

frontend/src/store/projects.js

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const getters = {
99
getProjectsSelect: (state) => {
1010
return state.projects.map((project) => ({ text: project.name, value: project.id }));
1111
},
12+
getProjectMembers: (state) => (id) => state.projects.find((project) => project.id == id),
1213
};
1314

1415
const actions = {

0 commit comments

Comments
 (0)