Skip to content

Commit 51db565

Browse files
committed
Refactoring error/edge-case handling in userstable
Adds better messaging and workflow for handling the edge cases where the selection of some users might partially succeed and fail - we now will show the user a list of their selected users who failed in one way or another
1 parent 8a2bf15 commit 51db565

File tree

11 files changed

+202
-61
lines changed

11 files changed

+202
-61
lines changed

kolibri/plugins/facility/assets/src/constants.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ export const PageNames = {
2525
ENROLL_LEARNERS_SIDE_PANEL__NEW_USERS: 'ENROLL_LEARNERS_SIDE_PANEL__NEW_USERS',
2626
};
2727

28+
29+
export const InvalidActionTypes = {
30+
ASSIGN: "assign",
31+
ENROLL: "enroll",
32+
};
33+
2834
// modal names
2935
export const Modals = {
3036
CREATE_CLASS: 'CREATE_CLASS',

kolibri/plugins/facility/assets/src/modules/classAssignMembers/actions.js

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
1-
import MembershipResource from 'kolibri-common/apiResources/MembershipResource';
2-
import RoleResource from 'kolibri-common/apiResources/RoleResource';
1+
import client from 'kolibri/client';
2+
import urls from 'kolibri/urls';
33
import { UserKinds } from 'kolibri/constants';
44
import uniq from 'lodash/uniq';
55

6-
export function enrollLearnersInClass(store, { classId, users }) {
7-
return MembershipResource.saveCollection({
8-
getParams: {
9-
collection: classId,
10-
},
6+
export async function enrollLearnersInClass(store, { classId, users }) {
7+
return client({
8+
url: urls['kolibri:core:membership_list'](),
9+
method: 'POST',
1110
data: uniq(users).map(userId => ({
1211
collection: classId,
1312
user: userId,
1413
})),
1514
});
1615
}
1716

18-
export function assignCoachesToClass(store, { classId, coaches }) {
19-
return RoleResource.saveCollection({
20-
getParams: {
21-
collection: classId,
22-
},
17+
export async function assignCoachesToClass(store, { classId, coaches }) {
18+
return client({
19+
url: urls['kolibri:core:role_list'](),
20+
method: 'POST',
2321
data: uniq(coaches).map(userId => ({
2422
collection: classId,
2523
user: userId,

kolibri/plugins/facility/assets/src/views/CoachClassAssignmentPage.vue

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import commonCoreStrings from 'kolibri/uiText/commonCoreStrings';
3030
import useSnackbar from 'kolibri/composables/useSnackbar';
3131
import ClassEnrollForm from './ClassEnrollForm';
32+
import { bulkUserManagementStrings } from 'kolibri-common/strings/bulkUserManagementStrings';
33+
import useActionWithUndo from '../composables/useActionWithUndo';
3234
3335
export default {
3436
name: 'CoachClassAssignmentPage',
@@ -44,7 +46,16 @@
4446
mixins: [commonCoreStrings],
4547
setup() {
4648
const { createSnackbar } = useSnackbar();
47-
return { createSnackbar };
49+
const {
50+
someLearnersEnrolledNotice$,
51+
coachesAllAssignedNotice$,
52+
someCoachesAssignedNotice$,
53+
} = bulkUserManagementStrings;
54+
return {
55+
someCoachesAssignedNotice$,
56+
coachesAllAssignedNotice$,
57+
createSnackbar,
58+
};
4859
},
4960
data() {
5061
return {
@@ -68,13 +79,17 @@
6879
assignCoaches(coaches) {
6980
this.formIsDisabled = true;
7081
this.assignCoachesToClass({ classId: this.class.id, coaches })
71-
.then(() => {
72-
// do this in action?
82+
.then(response => {
83+
const { created, invalid } = response.data;
84+
if(created?.length) {
85+
if(invalid?.length) {
86+
this.createSnackbar(this.someCoachesAssignedNotice$());
87+
} else {
88+
this.createSnackbar(this.coachesAllAssignedNotice$());
89+
}
90+
}
7391
this.$router
7492
.push(this.$store.getters.facilityPageLinks.ClassEditPage(this.class.id))
75-
.then(() => {
76-
this.showSnackbarNotification('coachesAssignedNoCount', { count: coaches.length });
77-
});
7893
})
7994
.catch(() => {
8095
this.formIsDisabled = false;

kolibri/plugins/facility/assets/src/views/LearnerClassEnrollmentPage.vue

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import commonCoreStrings from 'kolibri/uiText/commonCoreStrings';
2929
import ImmersivePage from 'kolibri/components/pages/ImmersivePage';
3030
import useSnackbar from 'kolibri/composables/useSnackbar';
31+
import { bulkUserManagementStrings } from 'kolibri-common/strings/bulkUserManagementStrings';
3132
import ClassEnrollForm from './ClassEnrollForm';
3233
3334
export default {
@@ -44,7 +45,15 @@
4445
mixins: [commonCoreStrings],
4546
setup() {
4647
const { createSnackbar } = useSnackbar();
47-
return { createSnackbar };
48+
const {
49+
usersEnrolledNotice$,
50+
someLearnersEnrolledNotice$,
51+
} = bulkUserManagementStrings;
52+
return {
53+
createSnackbar,
54+
usersEnrolledNotice$,
55+
someLearnersEnrolledNotice$,
56+
};
4857
},
4958
data() {
5059
return {
@@ -72,7 +81,15 @@
7281
window.localStorage.setItem(`${welcomeDismissalKey}-${id}`, false);
7382
});
7483
this.enrollLearnersInClass({ classId: this.class.id, users: selectedUsers })
75-
.then(() => {
84+
.then(response => {
85+
const { created, invalid } = response.data;
86+
if(created?.length) {
87+
if(invalid?.length) {
88+
this.createSnackbar(this.someLearnersEnrolledNotice$());
89+
} else {
90+
this.createSnackbar(this.usersEnrolledNotice$());
91+
}
92+
}
7693
this.$router
7794
.push(this.$store.getters.facilityPageLinks.ClassEditPage(this.class.id))
7895
.then(() => {

kolibri/plugins/facility/assets/src/views/users/UsersRootPage/index.vue

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
/>
100100
</template>
101101
<template #userActions>
102+
102103
<KIconButton
103104
ref="assignButton"
104105
icon="assignCoaches"
@@ -156,6 +157,15 @@
156157
:numFilteredItems="usersCount"
157158
/>
158159
</template>
160+
<template #alert>
161+
<UiAlert
162+
v-if="showingInvalidUsers"
163+
type="warning"
164+
:dismissible="false"
165+
>
166+
{{ warningMessage }}
167+
</UiAlert>
168+
</template>
159169
</UsersTableToolbar>
160170
</div>
161171
<UsersTable
@@ -200,14 +210,15 @@
200210
import commonCoreStrings from 'kolibri/uiText/commonCoreStrings';
201211
import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow';
202212
import useFacilities from 'kolibri-common/composables/useFacilities';
213+
import UiAlert from 'kolibri-design-system/lib/keen/UiAlert';
203214
import { bulkUserManagementStrings } from 'kolibri-common/strings/bulkUserManagementStrings';
204215
import useUser from 'kolibri/composables/useUser';
205216
import { UserKinds } from 'kolibri/constants';
206217
import usePreviousRoute from 'kolibri-common/composables/usePreviousRoute';
207218
import UsersTableToolbar from '../common/UsersTableToolbar/index.vue';
208219
import useUserManagement from '../../../composables/useUserManagement';
209220
import FacilityAppBarPage from '../../FacilityAppBarPage';
210-
import { PageNames } from '../../../constants';
221+
import { InvalidActionTypes, PageNames } from '../../../constants';
211222
import UsersTable from '../common/UsersTable.vue';
212223
import { overrideRoute } from '../../../utils';
213224
import MoveToTrashModal from '../common/MoveToTrashModal.vue';
@@ -227,6 +238,7 @@
227238
MoveToTrashModal,
228239
FacilityAppBarPage,
229240
FilterTextbox,
241+
UiAlert,
230242
PaginationActions,
231243
},
232244
mixins: [commonCoreStrings],
@@ -251,8 +263,11 @@
251263
filterLabel$,
252264
numUsersSelected$,
253265
clearFiltersLabel$,
266+
someFailedToAssign$,
267+
someFailedToEnroll$,
254268
} = bulkUserManagementStrings;
255269
270+
256271
const { $store, $router } = getCurrentInstance().proxy;
257272
const activeFacilityId =
258273
$router.currentRoute.params.facility_id || $store.getters.activeFacilityId;
@@ -343,7 +358,32 @@
343358
return {};
344359
});
345360
361+
362+
// Alerting users about enrollment/assignment errors
363+
const alertDismissed = ref(false)
364+
365+
function handleAlertDismissal() {
366+
alertDismissed.value = true
367+
}
368+
369+
const showingInvalidUsers = computed(() => !alertDismissed.value && Boolean(route.query.by_ids));
370+
const warningMessage = computed(
371+
() => {
372+
switch(route.query?.failedActionType) {
373+
case(InvalidActionTypes.ASSIGN):
374+
return someFailedToAssign$();
375+
case(InvalidActionTypes.ENROLL):
376+
return someFailedToEnroll$();
377+
default:
378+
return '';
379+
}
380+
}
381+
);
382+
346383
return {
384+
handleAlertDismissal,
385+
warningMessage,
386+
showingInvalidUsers,
347387
windowIsSmall,
348388
usersTableStyles,
349389
// Route utilities
@@ -486,6 +526,12 @@
486526
}
487527
},
488528
},
529+
beforeRouteLeave(to,__,next) {
530+
this.alertDismissed = false;
531+
const { query } = to;
532+
delete query.failedActionType;
533+
next(this.overrideRoute(to, { query }))
534+
}
489535
};
490536
491537
</script>

kolibri/plugins/facility/assets/src/views/users/common/UsersTableToolbar/NormalLayout.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
</div>
3333
<slot name="paginationControls"></slot>
3434
</div>
35+
<div><slot name="alert"></slot></div>
3536
</div>
3637

3738
</template>

kolibri/plugins/facility/assets/src/views/users/common/UsersTableToolbar/SmallWindowLayout.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
</div>
4444
<slot name="paginationControls"></slot>
4545
</div>
46+
<div><slot name="alert"></slot></div>
4647
<div
4748
v-if="showUsersTable"
4849
:style="{

kolibri/plugins/facility/assets/src/views/users/common/UsersTableToolbar/index.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
<template #paginationControls>
2828
<slot name="paginationControls"></slot>
2929
</template>
30+
<template #alert>
31+
<slot name="alert"></slot>
32+
</template>
3033
</component>
3134

3235
</template>

kolibri/plugins/facility/assets/src/views/users/sidePanels/AssignCoachesSidePanel.vue

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@
112112
<script>
113113
114114
import { ref, computed } from 'vue';
115-
import { useRoute } from 'vue-router/composables';
115+
import { useRouter, useRoute } from 'vue-router/composables';
116116
import SidePanelModal from 'kolibri-common/components/SidePanelModal';
117117
import { bulkUserManagementStrings } from 'kolibri-common/strings/bulkUserManagementStrings';
118118
import { UserKinds } from 'kolibri/constants';
@@ -124,7 +124,7 @@
124124
import FacilityUserResource from 'kolibri-common/apiResources/FacilityUserResource';
125125
import flatMap from 'lodash/flatMap';
126126
import CloseConfirmationGuard from '../common/CloseConfirmationGuard.vue';
127-
import { PageNames } from '../../../constants.js';
127+
import { InvalidActionTypes, PageNames } from '../../../constants.js';
128128
import { getRootRouteName, overrideRoute } from '../../../utils';
129129
import SelectableList from '../../common/SelectableList.vue';
130130
import { _userState } from '../../../modules/mappers';
@@ -145,6 +145,7 @@
145145
const invalidRoles = ref(null);
146146
const facilityUsers = ref([]);
147147
const route = useRoute();
148+
const router = useRouter();
148149
const closeConfirmationGuardRef = ref(null);
149150
150151
const goBack = useGoBack({
@@ -158,7 +159,8 @@
158159
const {
159160
coachesAllAssignedNotice$,
160161
coachesAllInvalidNotice$,
161-
coachesSomeInvalidNotice$,
162+
unableToAssignAlert$,
163+
someCoachesAssignedNotice$,
162164
assignCoachUndoneNotice$,
163165
usersInClassNotAffected$,
164166
assignAction$,
@@ -237,8 +239,24 @@
237239
affectedClasses: selectedClasses.value,
238240
resetSelection: true,
239241
});
240-
closeSidePanel();
241-
return true;
242+
if(invalidRoles.value.length) {
243+
router.push(
244+
overrideRoute(
245+
route,
246+
{
247+
name: getRootRouteName(route),
248+
query: {
249+
by_ids: invalidRoles.value.map(r => r.user).join(','),
250+
failedActionType: InvalidActionTypes.ASSIGN,
251+
},
252+
},
253+
)
254+
);
255+
return true;
256+
} else {
257+
closeSidePanel();
258+
return true;
259+
}
242260
} catch (error) {
243261
showErrorWarning.value = true;
244262
isLoading.value = false;
@@ -275,12 +293,15 @@
275293
276294
// Only add roles that were actually created (have an id)
277295
const actuallyCreatedRoles = created.filter(role => role.id);
296+
278297
createdRoles.value = actuallyCreatedRoles;
279298
invalidRoles.value = invalid || [];
280299
}
281300
301+
const canUndo = computed(() => createdRoles.value?.length)
302+
282303
async function handleUndoAssignments() {
283-
if (createdRoles.value.length > 0) {
304+
if (createdRoles.value?.length > 0) {
284305
const roleIds = createdRoles.value.map(role => role.id);
285306
await RoleResource.deleteCollection({ by_ids: roleIds });
286307
props.onChange({
@@ -290,14 +311,16 @@
290311
}
291312
292313
const snackbarMessage$ = () => {
293-
if(createdRoles.value.length) {
294-
if(invalidRoles.value.length) {
295-
return coachesSomeInvalidNotice$();
314+
if(createdRoles.value?.length) {
315+
if(invalidRoles.value?.length) {
316+
return someCoachesAssignedNotice$();
296317
} else {
297318
return coachesAllAssignedNotice$();
298319
}
299320
}
300-
return coachesAllInvalidNotice$();
321+
if(invalidRoles.value?.length) {
322+
return unableToAssignAlert$();
323+
}
301324
};
302325
303326
const { performAction: handleAssign } = useActionWithUndo({
@@ -306,7 +329,7 @@
306329
undoAction: handleUndoAssignments,
307330
undoActionNotice$: assignCoachUndoneNotice$,
308331
onBlur: props.onBlur,
309-
canUndo: computed(() => createdRoles.value.length),
332+
canUndo,
310333
});
311334
312335
function closeSidePanel() {

0 commit comments

Comments
 (0)