Skip to content

Commit 7d1ecad

Browse files
authored
Merge pull request #198 from loneil/feature/shareSubmission
Manage Submission Users
2 parents e068eda + 5597637 commit 7d1ecad

File tree

13 files changed

+774
-6
lines changed

13 files changed

+774
-6
lines changed

app/frontend/src/components/base/BaseNotificationContainer.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default {
1818

1919
<style scoped>
2020
.notification-container {
21-
z-index: 99;
21+
z-index: 999;
2222
position: fixed;
2323
top: 0;
2424
left: 25%;

app/frontend/src/components/designer/FormViewer.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<FormViewerActions
66
:draftEnabled="form.enableSubmitterDraft"
77
:formId="form.id"
8+
:isDraft="submissionRecord.draft"
89
:permissions="permissions"
910
:readOnly="readOnly"
1011
:submissionId="submissionId"

app/frontend/src/components/designer/FormViewerActions.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,24 @@
4646
</v-tooltip>
4747
</router-link>
4848
</span>
49+
50+
<!-- Go to draft edit -->
51+
<span v-if="submissionId" class="ml-2">
52+
<ManageSubmissionUsers :isDraft="isDraft" :submissionId="submissionId" />
53+
</span>
4954
</v-col>
5055
</v-row>
5156
</template>
5257

5358
<script>
5459
import { FormPermissions } from '@/utils/constants';
60+
import ManageSubmissionUsers from '@/components/forms/submission/ManageSubmissionUsers.vue';
5561
5662
export default {
5763
name: 'MySubmissionsActions',
64+
components: {
65+
ManageSubmissionUsers,
66+
},
5867
props: {
5968
draftEnabled: {
6069
type: Boolean,
@@ -64,6 +73,10 @@ export default {
6473
type: String,
6574
default: undefined,
6675
},
76+
isDraft: {
77+
type: Boolean,
78+
default: false,
79+
},
6780
permissions: {
6881
type: Array,
6982
},
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
<template>
2+
<span>
3+
<v-tooltip bottom>
4+
<template #activator="{ on, attrs }">
5+
<v-btn
6+
color="primary"
7+
@click="dialog = true"
8+
icon
9+
v-bind="attrs"
10+
v-on="on"
11+
>
12+
<v-icon>group</v-icon>
13+
</v-btn>
14+
</template>
15+
<span>Manage Team Members</span>
16+
</v-tooltip>
17+
<v-dialog v-model="dialog" width="600">
18+
<v-card>
19+
<v-card-title class="headline pb-0">
20+
Manage Team Members
21+
</v-card-title>
22+
23+
<v-card-text>
24+
<hr />
25+
26+
<v-row v-if="isDraft">
27+
<v-col cols="9">
28+
<form autocomplete="off">
29+
<v-autocomplete
30+
v-model="userSearchSelection"
31+
clearable
32+
dense
33+
:filter="filterObject"
34+
hide-details
35+
:items="userSearchResults"
36+
label="Enter a name, e-mail, or username"
37+
:loading="isLoadingDropdown"
38+
return-object
39+
:search-input.sync="findUsers"
40+
>
41+
<!-- no data -->
42+
<template #no-data>
43+
<div class="px-2">
44+
Can't find someone? They may not have joined the site.<br />
45+
Kindly send them a link to the site and ask them to log
46+
in.
47+
</div>
48+
</template>
49+
<!-- selected user -->
50+
<template #selection="data">
51+
<span
52+
v-bind="data.attrs"
53+
:input-value="data.selected"
54+
close
55+
@click="data.select"
56+
@click:close="remove(data.item)"
57+
>
58+
{{ data.item.fullName }}
59+
</span>
60+
</template>
61+
<!-- users found in dropdown -->
62+
<template #item="data">
63+
<template v-if="typeof data.item !== 'object'">
64+
<v-list-item-content v-text="data.item" />
65+
</template>
66+
<template v-else>
67+
<v-list-item-content>
68+
<v-list-item-title v-html="data.item.fullName" />
69+
<v-list-item-subtitle v-html="data.item.username" />
70+
<v-list-item-subtitle v-html="data.item.email" />
71+
</v-list-item-content>
72+
</template>
73+
</template>
74+
</v-autocomplete>
75+
</form>
76+
</v-col>
77+
<v-col cols="3">
78+
<v-btn
79+
color="primary"
80+
:disabled="!userSearchSelection"
81+
:loading="isLoadingDropdown"
82+
@click="addUser"
83+
>
84+
<span>Add</span>
85+
</v-btn>
86+
</v-col>
87+
</v-row>
88+
<div v-else>
89+
You can only invite and manage team members while this form is a draft
90+
</div>
91+
92+
<p class="mt-5">
93+
<strong>Team members for this submission:</strong>
94+
</p>
95+
96+
<v-skeleton-loader :loading="isLoadingTable" type="table-row">
97+
<v-simple-table dense>
98+
<template>
99+
<thead>
100+
<tr>
101+
<th class="text-left">Name</th>
102+
<th class="text-left">Username</th>
103+
<th class="text-left">Email</th>
104+
<th class="text-left" v-if="isDraft">Actions</th>
105+
</tr>
106+
</thead>
107+
<tbody>
108+
<tr :key="item.userId" v-for="item in userTableList">
109+
<td>{{ item.fullName }}</td>
110+
<td>{{ item.username }}</td>
111+
<td>{{ item.email }}</td>
112+
<td v-if="isDraft">
113+
<v-btn
114+
color="red"
115+
icon
116+
:disabled="item.isOwner"
117+
@click="removeUser(item)"
118+
>
119+
<v-icon>remove_circle</v-icon>
120+
</v-btn>
121+
</td>
122+
</tr>
123+
</tbody>
124+
</template>
125+
</v-simple-table>
126+
</v-skeleton-loader>
127+
</v-card-text>
128+
129+
<v-card-actions class="justify-center">
130+
<v-btn class="mb-5 close-dlg" color="primary" @click="dialog = false">
131+
<span>Close</span>
132+
</v-btn>
133+
</v-card-actions>
134+
</v-card>
135+
136+
<BaseDialog
137+
v-model="showDeleteDialog"
138+
type="CONTINUE"
139+
@close-dialog="showDeleteDialog = false"
140+
@continue-dialog="modifyPermissions(userToDelete.id, []); showDeleteDialog = false"
141+
>
142+
<template #title>Remove {{ userToDelete.username }}</template>
143+
<template #text>
144+
Are you sure you wish to remove
145+
<strong>{{ userToDelete.username }}</strong
146+
>? They will no longer have permissions for this submission.
147+
</template>
148+
<template #button-text-continue>
149+
<span>Remove</span>
150+
</template>
151+
</BaseDialog>
152+
</v-dialog>
153+
</span>
154+
</template>
155+
156+
<script>
157+
import { mapActions } from 'vuex';
158+
159+
import { FormPermissions } from '@/utils/constants';
160+
import { rbacService, userService } from '@/services';
161+
162+
export default {
163+
name: 'ManageSubmissionUsers',
164+
props: {
165+
isDraft: {
166+
type: Boolean,
167+
required: true,
168+
},
169+
submissionId: {
170+
type: String,
171+
required: true,
172+
},
173+
},
174+
data() {
175+
return {
176+
dialog: false,
177+
isLoadingTable: true,
178+
showDeleteDialog: false,
179+
userTableList: [],
180+
userToDelete: {},
181+
182+
// search box
183+
findUsers: null,
184+
isLoadingDropdown: false,
185+
userSearchResults: [],
186+
userSearchSelection: null,
187+
};
188+
},
189+
methods: {
190+
...mapActions('notifications', ['addNotification']),
191+
// show users in dropdown that have a text match on multiple properties
192+
addUser() {
193+
if (this.userSearchSelection) {
194+
const id = this.userSearchSelection.id;
195+
if (this.userTableList.some((u) => u.id === id)) {
196+
this.addNotification({
197+
type: 'warning',
198+
message: `User ${this.userSearchSelection.username} is already in the list of team members.`,
199+
});
200+
} else {
201+
this.modifyPermissions(id, [
202+
FormPermissions.SUBMISSION_UPDATE,
203+
FormPermissions.SUBMISSION_READ,
204+
]);
205+
}
206+
}
207+
// reset search field
208+
this.userSearchSelection = null;
209+
},
210+
filterObject(item, queryText) {
211+
return Object.values(item).some((v) => v !== null && v.toLocaleLowerCase().includes(queryText.toLocaleLowerCase()));
212+
},
213+
async getSubmissionUsers() {
214+
this.isLoadingTable = true;
215+
try {
216+
const response = await rbacService.getSubmissionUsers({
217+
formSubmissionId: this.submissionId,
218+
});
219+
if (response.data) {
220+
this.userTableList = this.transformResponseToTable(response.data);
221+
}
222+
} catch (error) {
223+
this.addNotification({
224+
message:
225+
'An error occured while trying to fetch users for this submission.',
226+
consoleError: `Error getting users for ${this.submissionId}: ${error}`,
227+
});
228+
} finally {
229+
this.isLoadingTable = false;
230+
}
231+
},
232+
async modifyPermissions(userId, permissions) {
233+
this.isLoadingTable = true;
234+
try {
235+
// Add the selected user with read/update permissions on this submission
236+
const response = await rbacService.setSubmissionUserPermissions(
237+
{ permissions: permissions },
238+
{
239+
formSubmissionId: this.submissionId,
240+
userId: userId,
241+
}
242+
);
243+
if (response.data) {
244+
this.userTableList = this.transformResponseToTable(response.data);
245+
}
246+
} catch (error) {
247+
this.addNotification({
248+
message:
249+
'An error occured while trying to update users for this submission.',
250+
consoleError: `Error setting user permissions. Sub: ${this.submissionId} User: ${userId} Error: ${error}`,
251+
});
252+
} finally {
253+
this.isLoadingTable = false;
254+
}
255+
},
256+
removeUser(userRow) {
257+
this.userToDelete = userRow;
258+
this.showDeleteDialog = true;
259+
},
260+
transformResponseToTable(responseData) {
261+
return responseData
262+
.map((su) => {
263+
return {
264+
email: su.user.email,
265+
fullName: su.user.fullName,
266+
id: su.userId,
267+
isOwner: su.permissions.includes(
268+
FormPermissions.SUBMISSION_CREATE
269+
),
270+
username: su.user.username,
271+
};
272+
})
273+
.sort((a, b) => b.isOwner - a.isOwner);
274+
},
275+
},
276+
watch: {
277+
// Get a list of user objects from database
278+
async findUsers(input) {
279+
if (!input) return;
280+
this.isLoadingDropdown = true;
281+
try {
282+
const response = await userService.getUsers({ search: input });
283+
this.userSearchResults = response.data;
284+
} catch (error) {
285+
console.error(`Error getting users: ${error}`); // eslint-disable-line no-console
286+
} finally {
287+
this.isLoadingDropdown = false;
288+
}
289+
},
290+
},
291+
created() {
292+
this.getSubmissionUsers();
293+
},
294+
};
295+
</script>

app/frontend/src/components/forms/submission/NotesPanel.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,7 @@
7171
<script>
7272
import { mapActions, mapGetters } from 'vuex';
7373
74-
import formService from '@/services/formService';
75-
import { rbacService } from '@/services';
74+
import { formService, rbacService } from '@/services';
7675
7776
export default {
7877
name: 'NotesPanel',
@@ -101,7 +100,7 @@ export default {
101100
const user = await rbacService.getCurrentUser();
102101
const body = {
103102
note: this.newNote,
104-
userId: user.data.id
103+
userId: user.data.id,
105104
};
106105
const response = await formService.addNote(this.submissionId, body);
107106
if (!response.data) {

app/frontend/src/services/rbacService.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,30 @@ export default {
6969
*/
7070
setUserForms(requestBody, params = {}) {
7171
return appAxios().put(`${ApiRoutes.RBAC}/users`, requestBody, { params });
72-
}
72+
},
73+
74+
//
75+
// Submission Management calls
76+
//
77+
78+
/**
79+
* @function getSubmissionUsers
80+
* Get the list of associated users for a submission
81+
* @param {Object} [params={}] The query parameters
82+
* @returns {Promise} An axios response
83+
*/
84+
getSubmissionUsers(params = {}) {
85+
return appAxios().get(`${ApiRoutes.RBAC}/submissions`, { params });
86+
},
87+
88+
/**
89+
* @function setFormUsers
90+
* Set permissions for a user on the form
91+
* @param {Object} requestBody The request body containing the permissions list
92+
* @param {Object} [params={}] The query parameters
93+
* @returns {Promise} An axios response
94+
*/
95+
setSubmissionUserPermissions(requestBody, params = {}) {
96+
return appAxios().put(`${ApiRoutes.RBAC}/submissions`, requestBody, { params });
97+
},
7398
};

0 commit comments

Comments
 (0)