diff --git a/kolibri/core/assets/src/views/CoreTable.vue b/kolibri/core/assets/src/views/CoreTable.vue index 15663d0a0a6..26ecb05a184 100644 --- a/kolibri/core/assets/src/views/CoreTable.vue +++ b/kolibri/core/assets/src/views/CoreTable.vue @@ -84,7 +84,7 @@ }); // If we have loaded the data, but have no empty message and no rows, we log an error. - if (!this.dataLoading && this.emptyMessage && !tableHasRows) { + if (!this.dataLoading && !this.emptyMessage && !tableHasRows) { logging.error('CoreTable: No rows in table, but no empty message provided.'); } @@ -93,7 +93,7 @@ * empty message if there are no rows in the table. If we have loaded data, have * an emptyMessage and have no rows. If we have rows, then we show the table alone */ - var dataStatusEl = this.dataLoading + const dataStatusEl = this.dataLoading ? createElement('p', [createElement(KCircularLoader)]) : !tableHasRows && createElement('p', this.emptyMessage); // Only show message if no rows diff --git a/kolibri/plugins/coach/assets/src/composables/__mocks__/useGroups.js b/kolibri/plugins/coach/assets/src/composables/__mocks__/useGroups.js new file mode 100644 index 00000000000..f1fcb9dde7e --- /dev/null +++ b/kolibri/plugins/coach/assets/src/composables/__mocks__/useGroups.js @@ -0,0 +1,46 @@ +/** + * `useGroups` composable function mock. + * + * If default values are sufficient for tests, + * you only need call `jest.mock('')` + * at the top of a test file. + * + * If you need to override some default values from some tests, + * you can import a helper function `useGroupsMock` that accepts + * an object with values to be overriden and use it together + * with `mockImplementation` as follows: + * + * ``` + * // eslint-disable-next-line import/named + * import useGroups, { useGroupsMock } from ''; + * + * jest.mock('') + * + * it('test', () => { + * useGroups.mockImplementation( + * () => useGroupsMock({ groupsAreLoading: true }) + * ); + * }) + * ``` + * + * You can reset your mock implementation back to default values + * for other tests by calling the following in `beforeEach`: + * + * ``` + * useGroups.mockImplementation(() => useGroupsMock()) + * ``` + */ + +const MOCK_DEFAULTS = { + groupsAreLoading: false, + showGroupsPage: jest.fn(), +}; + +export function useGroupsMock(overrides = {}) { + return { + ...MOCK_DEFAULTS, + ...overrides, + }; +} + +export default jest.fn(() => useGroupsMock()); diff --git a/kolibri/plugins/coach/assets/src/composables/__mocks__/useLessons.js b/kolibri/plugins/coach/assets/src/composables/__mocks__/useLessons.js new file mode 100644 index 00000000000..3a95e46cc0f --- /dev/null +++ b/kolibri/plugins/coach/assets/src/composables/__mocks__/useLessons.js @@ -0,0 +1,46 @@ +/** + * `useLessons` composable function mock. + * + * If default values are sufficient for tests, + * you only need call `jest.mock('')` + * at the top of a test file. + * + * If you need to override some default values from some tests, + * you can import a helper function `useLessonsMock` that accepts + * an object with values to be overriden and use it together + * with `mockImplementation` as follows: + * + * ``` + * // eslint-disable-next-line import/named + * import useLessons, { useLessonsMock } from ''; + * + * jest.mock('') + * + * it('test', () => { + * useLessons.mockImplementation( + * () => useLessonsMock({ lessonsAreLoading: true }) + * ); + * }) + * ``` + * + * You can reset your mock implementation back to default values + * for other tests by calling the following in `beforeEach`: + * + * ``` + * useLessons.mockImplementation(() => useLessonsMock()) + * ``` + */ + +const MOCK_DEFAULTS = { + lessonsAreLoading: false, + showLessonsRootPage: jest.fn(), +}; + +export function useLessonsMock(overrides = {}) { + return { + ...MOCK_DEFAULTS, + ...overrides, + }; +} + +export default jest.fn(() => useLessonsMock()); diff --git a/kolibri/plugins/coach/assets/src/composables/useGroups.js b/kolibri/plugins/coach/assets/src/composables/useGroups.js new file mode 100644 index 00000000000..595c9930712 --- /dev/null +++ b/kolibri/plugins/coach/assets/src/composables/useGroups.js @@ -0,0 +1,71 @@ +import { ref } from 'kolibri.lib.vueCompositionApi'; +import samePageCheckGenerator from 'kolibri.utils.samePageCheckGenerator'; +import { LearnerGroupResource, FacilityUserResource } from 'kolibri.resources'; + +// Place outside the function to keep the state +const groupsAreLoading = ref(false); + +export function useGroups() { + function setGroupsLoading(loading) { + groupsAreLoading.value = loading; + } + + function showGroupsPage(store, classId) { + // On this page, handle loading state locally + // TODO: Open follow-up so that we don't need to do this + store.dispatch('notLoading'); + + setGroupsLoading(true); + + const promises = [ + FacilityUserResource.fetchCollection({ + getParams: { member_of: classId }, + force: true, + }), + LearnerGroupResource.fetchCollection({ + getParams: { parent: classId }, + force: true, + }), + ]; + const shouldResolve = samePageCheckGenerator(store); + return Promise.all(promises).then( + ([classUsers, groupsCollection]) => { + if (shouldResolve()) { + const groups = groupsCollection.map(group => ({ ...group, users: [] })); + const groupUsersPromises = groups.map(group => + FacilityUserResource.fetchCollection({ + getParams: { member_of: group.id }, + force: true, + }) + ); + + Promise.all(groupUsersPromises).then( + groupsUsersCollection => { + if (shouldResolve()) { + groupsUsersCollection.forEach((groupUsers, index) => { + groups[index].users = [...groupUsers]; + }); + store.commit('groups/SET_STATE', { + classUsers: [...classUsers], + groups, + groupModalShown: false, + }); + setGroupsLoading(false); + store.dispatch('clearError'); + } + }, + error => (shouldResolve() ? store.dispatch('handleError', error) : null) + ); + } + }, + error => { + shouldResolve() ? store.dispatch('handleError', error) : null; + } + ); + } + + return { + groupsAreLoading, + showGroupsPage, + }; +} diff --git a/kolibri/plugins/coach/assets/src/composables/useLessons.js b/kolibri/plugins/coach/assets/src/composables/useLessons.js new file mode 100644 index 00000000000..270bc929ca0 --- /dev/null +++ b/kolibri/plugins/coach/assets/src/composables/useLessons.js @@ -0,0 +1,46 @@ +import { ref } from 'kolibri.lib.vueCompositionApi'; +import { LearnerGroupResource } from 'kolibri.resources'; +import { LessonsPageNames } from '../constants/lessonsConstants'; + +// Place outside the function to keep the state +const lessonsAreLoading = ref(false); + +export function useLessons() { + function setLessonsLoading(loading) { + lessonsAreLoading.value = loading; + } + + // Show the Lessons Root Page, where all the Lessons are listed for a given Classroom + function showLessonsRootPage(store, classId) { + // on this page, don't handle loading state globally so we can do it locally + store.dispatch('notLoading'); + + setLessonsLoading(true); + store.commit('lessonsRoot/SET_STATE', { + lessons: [], + learnerGroups: [], + }); + const loadRequirements = [ + // Fetch learner groups for the New Lesson Modal + LearnerGroupResource.fetchCollection({ getParams: { parent: classId } }), + store.dispatch('lessonsRoot/refreshClassLessons', classId), + ]; + + return Promise.all(loadRequirements).then( + ([learnerGroups]) => { + store.commit('lessonsRoot/SET_LEARNER_GROUPS', learnerGroups); + store.commit('SET_PAGE_NAME', LessonsPageNames.PLAN_LESSONS_ROOT); + setLessonsLoading(false); + }, + error => { + store.dispatch('handleApiError', error); + setLessonsLoading(false); + } + ); + } + + return { + lessonsAreLoading, + showLessonsRootPage, + }; +} diff --git a/kolibri/plugins/coach/assets/src/modules/groups/handlers.js b/kolibri/plugins/coach/assets/src/modules/groups/handlers.js deleted file mode 100644 index 918030e533d..00000000000 --- a/kolibri/plugins/coach/assets/src/modules/groups/handlers.js +++ /dev/null @@ -1,51 +0,0 @@ -import samePageCheckGenerator from 'kolibri.utils.samePageCheckGenerator'; -import { LearnerGroupResource, FacilityUserResource } from 'kolibri.resources'; - -export function showGroupsPage(store, classId) { - store.dispatch('loading'); - const promises = [ - FacilityUserResource.fetchCollection({ - getParams: { member_of: classId }, - force: true, - }), - LearnerGroupResource.fetchCollection({ - getParams: { parent: classId }, - force: true, - }), - ]; - const shouldResolve = samePageCheckGenerator(store); - return Promise.all(promises).then( - ([classUsers, groupsCollection]) => { - if (shouldResolve()) { - const groups = groupsCollection.map(group => ({ ...group, users: [] })); - const groupUsersPromises = groups.map(group => - FacilityUserResource.fetchCollection({ - getParams: { member_of: group.id }, - force: true, - }) - ); - - Promise.all(groupUsersPromises).then( - groupsUsersCollection => { - if (shouldResolve()) { - groupsUsersCollection.forEach((groupUsers, index) => { - groups[index].users = [...groupUsers]; - }); - store.commit('groups/SET_STATE', { - classUsers: [...classUsers], - groups, - groupModalShown: false, - }); - store.dispatch('notLoading'); - store.dispatch('clearError'); - } - }, - error => (shouldResolve() ? store.dispatch('handleError', error) : null) - ); - } - }, - error => { - shouldResolve() ? store.dispatch('handleError', error) : null; - } - ); -} diff --git a/kolibri/plugins/coach/assets/src/modules/lessonsRoot/handlers.js b/kolibri/plugins/coach/assets/src/modules/lessonsRoot/handlers.js deleted file mode 100644 index f12a57fb869..00000000000 --- a/kolibri/plugins/coach/assets/src/modules/lessonsRoot/handlers.js +++ /dev/null @@ -1,28 +0,0 @@ -import { LearnerGroupResource } from 'kolibri.resources'; -import { LessonsPageNames } from '../../constants/lessonsConstants'; - -// Show the Lessons Root Page, where all the Lessons are listed for a given Classroom -export function showLessonsRootPage(store, classId) { - return store.dispatch('loading').then(() => { - store.commit('lessonsRoot/SET_STATE', { - lessons: [], - learnerGroups: [], - }); - const loadRequirements = [ - // Fetch learner groups for the New Lesson Modal - LearnerGroupResource.fetchCollection({ getParams: { parent: classId } }), - store.dispatch('lessonsRoot/refreshClassLessons', classId), - ]; - return Promise.all(loadRequirements).then( - ([learnerGroups]) => { - store.commit('lessonsRoot/SET_LEARNER_GROUPS', learnerGroups); - store.commit('SET_PAGE_NAME', LessonsPageNames.PLAN_LESSONS_ROOT); - store.dispatch('notLoading'); - }, - error => { - store.dispatch('handleApiError', error); - store.dispatch('notLoading'); - } - ); - }); -} diff --git a/kolibri/plugins/coach/assets/src/routes/planLessonsRoutes.js b/kolibri/plugins/coach/assets/src/routes/planLessonsRoutes.js index ccb25166230..5bb0a742c38 100644 --- a/kolibri/plugins/coach/assets/src/routes/planLessonsRoutes.js +++ b/kolibri/plugins/coach/assets/src/routes/planLessonsRoutes.js @@ -8,10 +8,11 @@ import { showLessonResourceBookmarks, showLessonResourceBookmarksMain, } from '../modules/lessonResources/handlers'; -import { showLessonsRootPage } from '../modules/lessonsRoot/handlers'; import { showLessonSummaryPage } from '../modules/lessonSummary/handlers'; import { LessonsPageNames } from '../constants/lessonsConstants'; +import { useLessons } from '../composables/useLessons'; + import LessonsRootPage from '../views/plan/LessonsRootPage'; import LessonSummaryPage from '../views/plan/LessonSummaryPage'; import LessonResourceSelectionPage from '../views/plan/LessonResourceSelectionPage'; @@ -31,6 +32,8 @@ function path(...args) { return args.join(''); } +const { showLessonsRootPage } = useLessons(); + export default [ { name: LessonsPageNames.PLAN_LESSONS_ROOT, diff --git a/kolibri/plugins/coach/assets/src/routes/planRoutes.js b/kolibri/plugins/coach/assets/src/routes/planRoutes.js index 918bd3d0d23..05f1b5a2c3d 100644 --- a/kolibri/plugins/coach/assets/src/routes/planRoutes.js +++ b/kolibri/plugins/coach/assets/src/routes/planRoutes.js @@ -1,12 +1,14 @@ import store from 'kolibri.coreVue.vuex.store'; import { PageNames } from '../constants'; +import { useGroups } from '../composables/useGroups'; import GroupsPage from '../views/plan/GroupsPage'; import GroupMembersPage from '../views/plan/GroupMembersPage'; import GroupEnrollPage from '../views/plan/GroupEnrollPage'; -import { showGroupsPage } from '../modules/groups/handlers'; import planLessonsRoutes from './planLessonsRoutes'; import planExamRoutes from './planExamRoutes'; +const { showGroupsPage } = useGroups(); + export default [ ...planLessonsRoutes, ...planExamRoutes, diff --git a/kolibri/plugins/coach/assets/src/views/plan/GroupsPage/index.vue b/kolibri/plugins/coach/assets/src/views/plan/GroupsPage/index.vue index daa81efcba6..bab3c09b1ef 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/GroupsPage/index.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/GroupsPage/index.vue @@ -19,7 +19,10 @@ /> - + -

- {{ $tr('noGroups') }} -

- - + -

- {{ $tr('noLessons') }} -

-

+

{{ coreString('noResultsLabel') }}

+