diff --git a/kolibri/plugins/coach/assets/src/app.js b/kolibri/plugins/coach/assets/src/app.js index becf7454fc3..e09918bd9e5 100644 --- a/kolibri/plugins/coach/assets/src/app.js +++ b/kolibri/plugins/coach/assets/src/app.js @@ -65,6 +65,13 @@ class CoachToolsModule extends KolibriApp { PageNames.QUIZ_REPLACE_QUESTIONS, PageNames.QUIZ_SELECT_PRACTICE_QUIZ, PageNames.QUIZ_SELECT_RESOURCES, + PageNames.QUIZ_SELECT_RESOURCES_INDEX, + PageNames.QUIZ_SELECT_RESOURCES_BOOKMARKS, + PageNames.QUIZ_SELECT_RESOURCES_TOPIC_TREE, + PageNames.QUIZ_PREVIEW_SELECTED_RESOURCES, + PageNames.QUIZ_SELECT_RESOURCES_SETTINGS, + PageNames.QUIZ_PREVIEW_RESOURCE, + PageNames.QUIZ_SELECT_RESOURCES_LANDING_SETTINGS, PageNames.QUIZ_SECTION_ORDER, PageNames.QUIZ_BOOK_MARKED_RESOURCES, PageNames.QUIZ_LEARNER_REPORT, diff --git a/kolibri/plugins/coach/assets/src/composables/useResourceSelection.js b/kolibri/plugins/coach/assets/src/composables/useResourceSelection.js index 3746c98fa6a..74b314b4f3e 100644 --- a/kolibri/plugins/coach/assets/src/composables/useResourceSelection.js +++ b/kolibri/plugins/coach/assets/src/composables/useResourceSelection.js @@ -15,6 +15,12 @@ import useFetch from './useFetch'; * and topic trees, and offers methods to add, remove, or override selected resources. * * @param {Object} options + * @param {Object} options.bookmarks Configuration object for bookmarks fetch. It can contain + * `filters` an object with extra query params, and `annotator` a function to annotate the results. + * @param {Object} options.channels Configuration object for channels fetch. It can contain + * `filters` an object with extra query params, and `annotator` a function to annotate the results. + * @param {Object} options.topicTree Configuration object for topic tree fetch. It can contain + * `filters` an object with extra query params, and `annotator` a function to annotate the results. * @param {string} options.searchResultsRouteName The name of the route where the search results * will be displayed so that we can redirect to it when the search terms are updated. * @@ -51,7 +57,12 @@ import useFetch from './useFetch'; * * @returns {UseResourceSelectionResponse} */ -export default function useResourceSelection({ searchResultsRouteName } = {}) { +export default function useResourceSelection({ + searchResultsRouteName, + bookmarks, + channels, + topicTree, +} = {}) { const store = getCurrentInstance().proxy.$store; const route = computed(() => store.state.route); const topicId = computed(() => route.value.query.topicId); @@ -60,10 +71,21 @@ export default function useResourceSelection({ searchResultsRouteName } = {}) { const selectedResources = ref([]); const topic = ref(null); + const fetchBookmarks = async params => { + const response = await ContentNodeResource.fetchBookmarks(params); + if (bookmarks?.annotator) { + const annotatedResults = await bookmarks.annotator(response.results); + return { + ...response, + results: annotatedResults, + }; + } + return response; + }; const bookmarksFetch = useFetch({ fetchMethod: () => - ContentNodeResource.fetchBookmarks({ - params: { limit: 25, available: true }, + fetchBookmarks({ + params: { limit: 25, available: true, ...bookmarks?.filters }, }), fetchMoreMethod: more => ContentNodeResource.fetchBookmarks({ @@ -71,13 +93,20 @@ export default function useResourceSelection({ searchResultsRouteName } = {}) { }), }); + const fetchChannels = async () => { + const result = await ChannelResource.fetchCollection({ + getParams: { + available: true, + ...channels?.filters, + }, + }); + if (channels?.annotator) { + return channels.annotator(result); + } + return result; + }; const channelsFetch = useFetch({ - fetchMethod: () => - ChannelResource.fetchCollection({ - getParams: { - available: true, - }, - }), + fetchMethod: fetchChannels, }); const waitForTopicLoad = () => { @@ -123,11 +152,22 @@ export default function useResourceSelection({ searchResultsRouteName } = {}) { if (topic.value?.id !== newTopic.id) { topic.value = newTopic; } + if (topicTree?.annotator) { + const annotatedResults = await topicTree.annotator(topic.value.children.results); + return { + ...topic.value.children, + results: annotatedResults, + }; + } return topic.value.children; }; const treeFetch = useFetch({ - fetchMethod: () => fetchTree({ id: topicId.value, params: { include_coach_content: true } }), + fetchMethod: () => + fetchTree({ + id: topicId.value, + params: { include_coach_content: true, ...topicTree?.filters }, + }), fetchMoreMethod: more => fetchTree(more), }); diff --git a/kolibri/plugins/coach/assets/src/constants/index.js b/kolibri/plugins/coach/assets/src/constants/index.js index 0e8e256bd70..f3ae450fdc3 100644 --- a/kolibri/plugins/coach/assets/src/constants/index.js +++ b/kolibri/plugins/coach/assets/src/constants/index.js @@ -11,12 +11,23 @@ export const PageNames = { QUIZ_LEARNER_REPORT: 'QUIZ_LEARNER_REPORT', QUIZ_SECTION_EDITOR: 'QUIZ_SECTION_EDITOR', QUIZ_REPLACE_QUESTIONS: 'QUIZ_REPLACE_QUESTIONS', - QUIZ_SELECT_RESOURCES: 'QUIZ_SELECT_RESOURCES', + QUIZ_SELECT_RESOURCES_OLD: 'QUIZ_SELECT_RESOURCES_OLD', QUIZ_SELECT_PRACTICE_QUIZ: 'QUIZ_SELECT_PRACTICE_QUIZ', QUIZ_SECTION_ORDER: 'QUIZ_SECTION_ORDER', QUIZ_QUESTION_PAGE_ROOT: 'QUIZ_QUESTION_PAGE_ROOT', QUIZ_QUESTION_REPORT: 'QUIZ_QUESTION_REPORT', QUIZ_BOOK_MARKED_RESOURCES: 'QUIZ_BOOK_MARKED_RESOURCES', + QUIZ_SECTION_SIDE_PANEL: 'QUIZ_SECTION_SIDE_PANEL', + QUIZ_SELECT_RESOURCES: 'QUIZ_SELECT_RESOURCES', + QUIZ_PREVIEW_RESOURCE: 'QUIZ_PREVIEW_RESOURCE', + QUIZ_PREVIEW_SELECTED_RESOURCES: 'QUIZ_PREVIEW_SELECTED_RESOURCES', + QUIZ_SELECT_RESOURCES_INDEX: 'QUIZ_SELECT_RESOURCES_INDEX', + QUIZ_SELECT_RESOURCES_SEARCH: 'QUIZ_SELECT_RESOURCES_SEARCH', + QUIZ_SELECT_RESOURCES_BOOKMARKS: 'QUIZ_SELECT_RESOURCES_BOOKMARKS', + QUIZ_SELECT_RESOURCES_TOPIC_TREE: 'QUIZ_SELECT_RESOURCES_TOPIC_TREE', + QUIZ_SELECT_RESOURCES_SEARCH_RESULTS: 'QUIZ_SELECT_RESOURCES_SEARCH_RESULTS', + QUIZ_SELECT_RESOURCES_SETTINGS: 'QUIZ_SELECT_RESOURCES_SETTINGS', + QUIZ_SELECT_RESOURCES_LANDING_SETTINGS: 'QUIZ_SELECT_RESOURCES_LANDING_SETTINGS', /* Lessons */ LESSONS_ROOT: 'LESSONS_ROOT', diff --git a/kolibri/plugins/coach/assets/src/routes/examRoutes.js b/kolibri/plugins/coach/assets/src/routes/examRoutes.js index ec43607cf6f..3ab7773bbdd 100644 --- a/kolibri/plugins/coach/assets/src/routes/examRoutes.js +++ b/kolibri/plugins/coach/assets/src/routes/examRoutes.js @@ -1,20 +1,27 @@ import store from 'kolibri/store'; import { PageNames } from '../constants'; import CreateExamPage from '../views/quizzes/CreateExamPage'; -import SectionEditor from '../views/quizzes/CreateExamPage/SectionEditor.vue'; -import ResourceSelection from '../views/quizzes/CreateExamPage/ResourceSelection.vue'; -import ReplaceQuestions from '../views/quizzes/CreateExamPage/ReplaceQuestions.vue'; +import SectionEditor from '../views/quizzes/CreateExamPage/sidePanels/SectionSidePanel/SectionEditor.vue'; +import ReplaceQuestions from '../views/quizzes/CreateExamPage/sidePanels/SectionSidePanel/ReplaceQuestions.vue'; import ExamsRootPage from '../views/quizzes/ExamsRootPage'; import QuizSummaryPage from '../views/quizzes/QuizSummaryPage'; -import SectionOrder from '../views/quizzes/CreateExamPage/SectionOrder'; +import SectionOrder from '../views/quizzes/CreateExamPage/sidePanels/SectionSidePanel/SectionOrder.vue'; +import SectionSidePanel from '../views/quizzes/CreateExamPage/sidePanels/SectionSidePanel/index.vue'; +import QuizResourceSelection from '../views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/index.vue'; import LearnerQuizPage from '../views/common/reports/LearnerQuizPage.vue'; import QuizPreviewPage from '../views/quizzes/reports/QuizPreviewPage.vue'; import { generateExamReportDetailHandler } from '../modules/examReportDetail/handlers'; import QuestionLearnersPage from '../views/common/reports/QuestionLearnersPage.vue'; +import SelectionIndex from '../views/common/resourceSelection/subPages/SelectionIndex.vue'; +import SelectFromChannels from '../views/common/resourceSelection/subPages/SelectFromTopicTree.vue'; +import SelectFromBookmarks from '../views/common/resourceSelection/subPages/SelectFromBookmarks.vue'; +import ManageSelectedResources from '../views/common/resourceSelection/subPages/ManageSelectedResources.vue'; +import PreviewSelectedResources from '../views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue'; import { generateQuestionDetailHandler, questionRootRedirectHandler, } from '../modules/questionDetail/handlers'; +import QuestionsSettings from '../views/quizzes/CreateExamPage/sidePanels/QuizResourceSelection/subPages/QuestionsSettings.vue'; import { classIdParamRequiredGuard, RouteSegments } from './utils'; const { @@ -52,31 +59,91 @@ export default [ }, children: [ { - name: PageNames.QUIZ_SECTION_EDITOR, - path: 'edit', - component: SectionEditor, - }, - { - name: PageNames.QUIZ_REPLACE_QUESTIONS, - path: 'replace-questions', - component: ReplaceQuestions, + name: PageNames.QUIZ_SECTION_SIDE_PANEL, + path: 'details', + component: SectionSidePanel, + children: [ + { + name: PageNames.QUIZ_SECTION_EDITOR, + path: 'edit', + component: SectionEditor, + }, + { + name: PageNames.QUIZ_REPLACE_QUESTIONS, + path: 'replace-questions', + component: ReplaceQuestions, + }, + { + name: PageNames.QUIZ_SECTION_ORDER, + path: 'section-order', + component: SectionOrder, + }, + ], }, { name: PageNames.QUIZ_SELECT_RESOURCES, - path: 'select-resources/:topic_id?', - component: ResourceSelection, - }, - { - name: PageNames.QUIZ_SECTION_ORDER, - path: 'section-order', - component: SectionOrder, + path: 'select-resources', + component: QuizResourceSelection, + redirect: 'select-resources/landing-settings', + children: [ + { + name: PageNames.QUIZ_SELECT_RESOURCES_LANDING_SETTINGS, + path: 'landing-settings', + component: QuestionsSettings, + props: { + isLanding: true, + }, + }, + { + name: PageNames.QUIZ_SELECT_RESOURCES_INDEX, + path: 'index', + component: SelectionIndex, + }, + { + name: PageNames.QUIZ_SELECT_RESOURCES_BOOKMARKS, + path: 'bookmarks', + component: SelectFromBookmarks, + }, + { + name: PageNames.QUIZ_SELECT_RESOURCES_TOPIC_TREE, + path: 'channels', + component: SelectFromChannels, + }, + { + name: PageNames.QUIZ_PREVIEW_SELECTED_RESOURCES, + path: 'preview-resources', + component: ManageSelectedResources, + }, + { + name: PageNames.QUIZ_SELECT_RESOURCES_SETTINGS, + path: 'settings', + component: QuestionsSettings, + }, + { + name: PageNames.QUIZ_PREVIEW_RESOURCE, + path: 'preview', + component: PreviewSelectedResources, + props: toRoute => { + const contentId = toRoute.query.contentId; + return { + contentId, + }; + }, + }, + ], }, { name: PageNames.QUIZ_SELECT_PRACTICE_QUIZ, - path: 'select-quiz/:topic_id?', - component: ResourceSelection, - props: { - selectPracticeQuiz: true, + path: 'select-quiz', + redirect: to => { + const { params } = to; + return { + name: PageNames.QUIZ_SELECT_RESOURCES_INDEX, + params, + query: { + selectPracticeQuiz: true, + }, + }; }, }, ], diff --git a/kolibri/plugins/coach/assets/src/routes/lessonsRoutes.js b/kolibri/plugins/coach/assets/src/routes/lessonsRoutes.js index 82a87013979..7a43b3c1594 100644 --- a/kolibri/plugins/coach/assets/src/routes/lessonsRoutes.js +++ b/kolibri/plugins/coach/assets/src/routes/lessonsRoutes.js @@ -37,14 +37,14 @@ import { import LessonLearnerExercisePage from '../views/lessons/reports/LessonLearnerExercisePage.vue'; import QuestionLearnersPage from '../views/common/reports/QuestionLearnersPage.vue'; import EditLessonDetails from '../views/lessons/LessonSummaryPage/sidePanels/EditLessonDetails'; -import PreviewSelectedResources from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/PreviewSelectedResources'; +import SelectionIndex from '../views/common/resourceSelection/subPages/SelectionIndex.vue'; +import SelectFromBookmarks from '../views/common/resourceSelection/subPages/SelectFromBookmarks.vue'; +import SelectFromTopicTree from '../views/common/resourceSelection/subPages/SelectFromTopicTree.vue'; +import ManageSelectedResources from '../views/common/resourceSelection/subPages/ManageSelectedResources.vue'; +import PreviewSelectedResources from '../views/common/resourceSelection/subPages/PreviewSelectedResources/index.vue'; import LessonResourceSelection from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/index.vue'; import SearchFilters from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SearchFilters.vue'; -import SelectionIndex from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectionIndex.vue'; -import SelectFromBookmarks from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromBookmarks.vue'; -import SelectFromTopicTree from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromTopicTree.vue'; import SelectFromSearchResults from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromSearchResults.vue'; -import ManageSelectedResources from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/ManageSelectedResources.vue'; import { classIdParamRequiredGuard, RouteSegments } from './utils'; const { @@ -185,11 +185,6 @@ export default [ }, ], }, - { - name: PageNames.LESSON_PREVIEW_RESOURCE, - path: 'preview-resources/:nodeId', - component: PreviewSelectedResources, - }, ], }, { diff --git a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/QuizResourceSelectionHeader.vue b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/QuizResourceSelectionHeader.vue new file mode 100644 index 00000000000..b03f58417c0 --- /dev/null +++ b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/QuizResourceSelectionHeader.vue @@ -0,0 +1,98 @@ + + + + + + + diff --git a/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/UpdatedResourceSelection.vue b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/UpdatedResourceSelection.vue similarity index 67% rename from kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/UpdatedResourceSelection.vue rename to kolibri/plugins/coach/assets/src/views/common/resourceSelection/UpdatedResourceSelection.vue index e22f7e01109..2790f99bf3f 100644 --- a/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/UpdatedResourceSelection.vue +++ b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/UpdatedResourceSelection.vue @@ -12,6 +12,7 @@ [], + validator: rules => + validateObject( + { rules }, + { + rules: { + type: Array, + spec: { + type: Function, + }, + default: () => [], + }, + }, + ), + }, + /** + * Array of functions that take a list of selectable resources and + * return true if select all should be enabled. + */ + selectAllRules: { + type: Array, + required: false, + default: () => [], + validator: rules => + validateObject( + { rules }, + { + rules: { + type: Array, + spec: { + type: Function, + }, + default: () => [], + }, + }, + ), }, selectedResources: { type: Array, required: true, }, + /** + * Array of resource ids that already belongs to the target model (quiz/lessons), + * and should not be selectable. + */ unselectableResourceIds: { type: Array, required: false, default: null, }, + /** + * Route object for the channels page to be rendered in the breadcrumbs. + * If null, the breadcrumbs will not render a link to the channels page. + */ + channelsLink: { + type: Object, + required: false, + default: null, + }, disabled: { type: Boolean, default: false, }, + /** + * Function that returns a message to be displayed based in the content + * passed as argument. + */ + contentCardMessage: { + type: Function, + required: false, + default: () => '', + }, cardsHeadingLevel: { type: Number, default: 3, }, - channelsLink: { - type: Object, - required: false, - default: null, - }, getTopicLink: { type: Function, required: false, default: () => {}, }, + /** + * Function that receives a resourceId and returns a link to the resource. + */ + getResourceLink: { + type: Function, + required: true, + }, hideBreadcrumbs: { type: Boolean, default: false, @@ -136,6 +208,15 @@ showSelectAll() { return this.canSelectAll && this.multi && this.selectableContentList.length > 0; }, + isSelectAllDisabled() { + if (this.disabled) { + return true; + } + const deselectedResources = this.selectableContentList.filter( + resource => !this.selectedResources.some(res => res.id === resource.id), + ); + return !this.selectAllRules.every(rule => rule(deselectedResources)); + }, viewMoreButtonState() { if (this.loadingMore) { return ViewMoreButtonStates.LOADING; @@ -148,18 +229,10 @@ }, methods: { contentLink(content) { - const { params, query } = this.$route; if (!content.is_leaf) { return this.topicsLink(content.id); } - return { - name: PageNames.LESSON_PREVIEW_RESOURCE, - params: params, - query: { - ...query, - contentId: content.id, - }, - }; + return this.getResourceLink(content.id); }, topicsLink(topicId) { const route = this.getTopicLink?.(topicId); @@ -188,6 +261,9 @@ if (this.disabled || this.unselectableResourceIds?.includes(resource.id)) { return true; } + if (this.selectedResources.some(res => res.id === resource.id)) { + return false; + } return !this.selectionRules.every(rule => rule(resource) === true); }, contentIsChecked(resource) { diff --git a/kolibri/plugins/coach/assets/src/views/common/resourceSelection/contants.js b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/contants.js new file mode 100644 index 00000000000..2c635f07eff --- /dev/null +++ b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/contants.js @@ -0,0 +1,4 @@ +export const SelectionTarget = { + QUIZ: 'quiz', + LESSON: 'lesson', +}; diff --git a/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/ManageSelectedResources.vue b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/ManageSelectedResources.vue similarity index 77% rename from kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/ManageSelectedResources.vue rename to kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/ManageSelectedResources.vue index 083e6baf7df..7e0a493b42a 100644 --- a/kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/ManageSelectedResources.vue +++ b/kolibri/plugins/coach/assets/src/views/common/resourceSelection/subPages/ManageSelectedResources.vue @@ -1,8 +1,11 @@