diff --git a/kolibri/core/assets/src/views/sortable/DragContainer.vue b/kolibri/core/assets/src/views/sortable/DragContainer.vue index 7fb1e54585c..10698966044 100644 --- a/kolibri/core/assets/src/views/sortable/DragContainer.vue +++ b/kolibri/core/assets/src/views/sortable/DragContainer.vue @@ -56,6 +56,7 @@ handleStart() { // handle cancelation of drags // document.addEventListener('keyup', this.triggerMouseUpOnESC); + this.$emit('dragStart'); }, handleStop(event) { const { oldIndex, newIndex } = event.data; diff --git a/kolibri/core/package.json b/kolibri/core/package.json index fa362410689..d1d1535726d 100644 --- a/kolibri/core/package.json +++ b/kolibri/core/package.json @@ -21,7 +21,7 @@ "js-cookie": "^3.0.5", "knuth-shuffle-seeded": "^1.0.6", "kolibri-constants": "0.2.0", - "kolibri-design-system": "https://github.com/learningequality/kolibri-design-system#v2.0.0-beta1", + "kolibri-design-system": "https://github.com/learningequality/kolibri-design-system#f19ca3fc5677fc7f3b4cdbfee33950f5adde5290", "lockr": "0.8.5", "lodash": "^4.17.21", "loglevel": "^1.8.1", diff --git a/kolibri/plugins/coach/assets/src/composables/useExerciseResources.js b/kolibri/plugins/coach/assets/src/composables/useExerciseResources.js new file mode 100644 index 00000000000..32b57ee5bf7 --- /dev/null +++ b/kolibri/plugins/coach/assets/src/composables/useExerciseResources.js @@ -0,0 +1,198 @@ +import { ref, onMounted } from 'kolibri.lib.vueCompositionApi'; +import { ChannelResource, ContentNodeResource, ContentNodeSearchResource } from 'kolibri.resources'; +import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants'; +import { getContentNodeThumbnail } from 'kolibri.utils.contentNode'; +import { set } from '@vueuse/core'; +// import pickBy from 'lodash/pickBy'; +import uniq from 'lodash/uniq'; +// import store from 'kolibri.coreVue.vuex.store'; + +export function useExerciseResources() { + const resources = ref(null); + const channels = ref([]); + const bookmarks = ref([]); + const channelTopics = ref([]); + const contentList = ref([]); + const ancestors = ref([]); + const currentTopicId = ref(null); + const currentTopic = ref(null); + const currentTopicResource = ref(null); + + function fetchChannelResource() { + ChannelResource.fetchCollection({ params: { has_exercises: true, available: true } }).then( + response => { + set( + channels, + response.map(chnl => { + return { + ...chnl, + id: chnl.root, + title: chnl.name, + kind: ContentNodeKinds.CHANNEL, + is_leaf: false, + }; + }) + ); + } + ); + } + + function fetchBookMarkedResource() { + ContentNodeResource.fetchBookmarks({ params: { limit: 25, available: true } }).then(data => { + bookmarks.value = data.results ? data.results : []; + }); + } + + function _getTopicsWithExerciseDescendants(topicIds = []) { + return new Promise(resolve => { + if (!topicIds.length) { + resolve([]); + return; + } + const topicsNumAssessmentDescendantsPromise = ContentNodeResource.fetchDescendantsAssessments( + topicIds + ); + + topicsNumAssessmentDescendantsPromise.then(response => { + const topicsWithExerciseDescendants = []; + response.data.forEach(descendantAssessments => { + if (descendantAssessments.num_assessments > 0) { + topicsWithExerciseDescendants.push({ + id: descendantAssessments.id, + numAssessments: descendantAssessments.num_assessments, + exercises: [], + }); + } + }); + + ContentNodeResource.fetchDescendants( + topicsWithExerciseDescendants.map(topic => topic.id), + { + descendant_kind: ContentNodeKinds.EXERCISE, + } + ).then(response => { + response.data.forEach(exercise => { + channelTopics.value.push(exercise); + const topic = topicsWithExerciseDescendants.find(t => t.id === exercise.ancestor_id); + topic.exercises.push(exercise); + }); + channels.value = channelTopics.value; + resolve(topicsWithExerciseDescendants); + }); + }); + }); + } + + function fetchTopicResource(topicId) { + const topicNodePromise = ContentNodeResource.fetchModel({ id: topicId }); + const childNodesPromise = ContentNodeResource.fetchCollection({ + getParams: { + parent: topicId, + kind_in: [ContentNodeKinds.TOPIC, ContentNodeKinds.EXERCISE], + }, + }); + const loadRequirements = [topicNodePromise, childNodesPromise]; + + return Promise.all(loadRequirements).then(([topicNode, childNodes]) => { + return filterAndAnnotateContentList(childNodes).then(contentList => { + // set(topicId, topicNode.id); + ancestors.value = [...topicNode.ancestors, topicNode]; + return { + ...topicNode, + ...contentList, + thumbnail: getContentNodeThumbnail(topicNode), + }; + }); + }); + } + + function filterAndAnnotateContentList(childNodes) { + return new Promise(resolve => { + if (childNodes) { + const childTopics = childNodes.filter(({ kind }) => kind === ContentNodeKinds.TOPIC); + const topicIds = childTopics.map(({ id }) => id); + const topicsThatHaveExerciseDescendants = _getTopicsWithExerciseDescendants(topicIds); + topicsThatHaveExerciseDescendants.then(topics => { + const childNodesWithExerciseDescendants = childNodes + .map(childNode => { + const index = topics.findIndex(topic => topic.id === childNode.id); + if (index !== -1) { + return { ...childNode, ...topics[index] }; + } + return childNode; + }) + .filter(childNode => { + if ( + childNode.kind === ContentNodeKinds.TOPIC && + (childNode.numAssessments || 0) < 1 + ) { + return false; + } + return true; + }); + contentList.value = childNodesWithExerciseDescendants.map(node => ({ + ...node, + thumbnail: getContentNodeThumbnail(node), + })); + channels.value = contentList.value; + resolve(contentList); + }); + } + }); + } + + function showChannelLevel(store, params, query = {}) { + let kinds; + if (query.kind) { + kinds = [query.kind]; + } else { + kinds = [ContentNodeKinds.EXERCISE, ContentNodeKinds.TOPIC]; + } + + ContentNodeSearchResource.fetchCollection({ + getParams: { + search: '', + kind_in: kinds, + // ...pickBy({ channel_id: query.channel }), + }, + }).then(results => { + return filterAndAnnotateContentList(results.results).then(contentList => { + const searchResults = { + ...results, + results: contentList, + content_kinds: results.content_kinds.filter(kind => + [ContentNodeKinds.TOPIC, ContentNodeKinds.EXERCISE].includes(kind) + ), + contentIdsFetched: uniq(results.results.map(({ content_id }) => content_id)), + }; + + this.channels.value = searchResults.results; + console.log(searchResults.results); + }); + }); + } + + onMounted(() => { + fetchChannelResource(); + fetchBookMarkedResource(); + filterAndAnnotateContentList(); + _getTopicsWithExerciseDescendants([]); + }); + + return { + resources, + channels, + bookmarks, + contentList, + channelTopics, + currentTopicId, + currentTopic, + currentTopicResource, + ancestors, + fetchChannelResource, + filterAndAnnotateContentList, + _getTopicsWithExerciseDescendants, + showChannelLevel, + fetchTopicResource, + }; +} diff --git a/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js b/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js index 67e547dc9a3..0d245f19d2e 100644 --- a/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js +++ b/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js @@ -1,14 +1,21 @@ import { v4 as uuidv4 } from 'uuid'; +import isEqual from 'lodash/isEqual'; import { enhancedQuizManagementStrings } from 'kolibri-common/strings/enhancedQuizManagementStrings'; import uniq from 'lodash/uniq'; import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants'; -import { ChannelResource, ExamResource } from 'kolibri.resources'; +import { + ChannelResource, + ExamResource, + BookmarksResource, + ContentNodeResource, +} from 'kolibri.resources'; +import chunk from 'lodash/chunk'; import { validateObject, objectWithDefaults } from 'kolibri.utils.objectSpecs'; import { get, set } from '@vueuse/core'; import { computed, ref } from 'kolibri.lib.vueCompositionApi'; // TODO: Probably move this to this file's local dir import selectQuestions from '../modules/examCreation/selectQuestions.js'; -import { Quiz, QuizSection } from './quizCreationSpecs.js'; +import { Quiz, QuizSection, QuizQuestion } from './quizCreationSpecs.js'; /** Validators **/ /* objectSpecs expects every property to be available -- but we don't want to have to make an @@ -30,7 +37,7 @@ function isExercise(o) { /** * Composable function presenting primary interface for Quiz Creation */ -export default () => { +export default (DEBUG = false) => { // ----------- // Local state // ----------- @@ -43,16 +50,51 @@ export default () => { * The section that is currently selected for editing */ const _activeSectionId = ref(null); - /** @type {ref} - * The questions that are currently selected for action in the active section */ - const _selectedQuestions = ref([]); + /** @type {ref} + * The question_ids that are currently selected for action in the active section */ + const _selectedQuestionIds = ref([]); /** @type {ref} A list of all channels available which have exercises */ const _channels = ref([]); + const _bookmarks = ref([]); /** @type {ref} A counter for use in naming new sections */ const _sectionLabelCounter = ref(1); + //-- + // Debug Data Generators + //-- + function _quizQuestions(num = 5) { + const questions = []; + for (let i = 0; i <= num; i++) { + const overrides = { + title: `Quiz Question ${i}`, + question_id: uuidv4(), + }; + questions.push(objectWithDefaults(overrides, QuizQuestion)); + } + return questions; + } + + function _quizSections(num = 5, numQuestions = 5) { + const sections = []; + for (let i = 0; i <= num; i++) { + const overrides = { + section_id: uuidv4(), + section_title: `Test section ${i}`, + questions: _quizQuestions(numQuestions), + }; + sections.push(objectWithDefaults(overrides, QuizSection)); + } + return sections; + } + + function _generateTestData(numSections = 5, numQuestions = 5) { + const sections = _quizSections(numSections, numQuestions); + updateQuiz({ question_sources: sections }); + setActiveSection(sections[0].section_id); + } + // ------------------ // Section Management // ------------------ @@ -64,6 +106,7 @@ export default () => { * @throws {TypeError} if section is not a valid QuizSection **/ function updateSection({ section_id, ...updates }) { + console.log('updating with...', section_id, updates); const targetSection = get(allSections).find(section => section.section_id === section_id); if (!targetSection) { throw new TypeError(`Section with id ${section_id} not found; cannot be updated.`); @@ -103,10 +146,10 @@ export default () => { /** * @param {QuizQuestion[]} newQuestions * @affects _quiz - Updates the active section's `questions` property - * @affects _selectedQuestions - Clears this back to an empty array + * @affects _selectedQuestionIds - Clears this back to an empty array * @throws {TypeError} if newQuestions is not a valid array of QuizQuestions * Updates the active section's `questions` property with the given newQuestions, and clears - * _selectedQuestions from it. Then it resets _selectedQuestions to an empty array */ + * _selectedQuestionIds from it. Then it resets _selectedQuestionIds to an empty array */ // TODO WRITE THIS FUNCTION function replaceSelectedQuestions(newQuestions) { return newQuestions; @@ -141,8 +184,34 @@ export default () => { } updateQuiz({ question_sources: updatedSections }); } + /** + * @affects _bookmarks + */ + + function _fetchBookMarkedResources() { + BookmarksResource.fetchCollection() + .then(bookmarks => bookmarks.map(bookmark => bookmark.contentnode_id)) + .then(contentNodeIds => { + const chunkedContentNodeIds = chunk(contentNodeIds, 50); // Breaking contentNodeIds into lists no more than 50 in length + // Now we will create an array of promises, each of which queries for the 50-id chunk + chunkedContentNodeIds.forEach(idsChunk => { + ContentNodeResource.fetchCollection({ + getParams: { + ids: idsChunk, // This filters only the ids we want + }, + }).then(contentNodes => { + _updateBookmarks(contentNodes); + }); + }); + }); + } + + function _updateBookmarks(bookmarks = []) { + set(_bookmarks, [...get(_bookmarks), ...bookmarks]); + } /** + * * @param {string} [section_id] * @affects _activeSectionId * Sets the given section_id as the active section ID, however, if the ID is not found or is null @@ -162,9 +231,14 @@ export default () => { * use */ function initializeQuiz() { set(_quiz, objectWithDefaults({}, Quiz)); - const newSection = addSection(); - setActiveSection(newSection.section_id); + if (DEBUG) { + _generateTestData(); + } else { + const newSection = addSection(); + setActiveSection(newSection.section_id); + } _fetchChannels(); + _fetchBookMarkedResources(); } /** @@ -195,21 +269,41 @@ export default () => { // -------------------------------- /** @param {QuizQuestion} question - * @affects _selectedQuestions - Adds question to _selectedQuestions if it isn't there already */ + * @affects _selectedQuestionIds - Adds question to _selectedQuestionIds if it isn't + * there already */ function addQuestionToSelection(question_id) { - set(_selectedQuestions, uniq([...get(_selectedQuestions), question_id])); + set(_selectedQuestionIds, uniq([...get(_selectedQuestionIds), question_id])); } /** * @param {QuizQuestion} question - * @affects _selectedQuestions - Removes question from _selectedQuestions if it is there */ + * @affects _selectedQuestionIds - Removes question from _selectedQuestionIds if it is there */ function removeQuestionFromSelection(question_id) { set( - _selectedQuestions, - get(_selectedQuestions).filter(id => id !== question_id) + _selectedQuestionIds, + get(_selectedQuestionIds).filter(id => id !== question_id) ); } + function toggleQuestionInSelection(question_id) { + if (get(_selectedQuestionIds).includes(question_id)) { + removeQuestionFromSelection(question_id); + } else { + addQuestionToSelection(question_id); + } + } + + function selectAllQuestions() { + if (get(allQuestionsSelected)) { + set(_selectedQuestionIds, []); + } else { + set( + _selectedQuestionIds, + get(activeQuestions).map(q => q.question_id) + ); + } + } + /** * @affects _channels - Fetches all channels with exercises and sets them to _channels */ function _fetchChannels() { @@ -271,14 +365,57 @@ export default () => { /** @type {ComputedRef} All questions in the active section's `questions` property * those which are currently set to be used in the section */ const activeQuestions = computed(() => get(activeSection).questions); - /** @type {ComputedRef} All questions the user has selected for the active - * section */ - const selectedActiveQuestions = computed(() => get(_selectedQuestions)); + /** @type {ComputedRef} All question_ids the user has selected for the active section */ + const selectedActiveQuestions = computed(() => get(_selectedQuestionIds)); /** @type {ComputedRef} Questions in the active section's `resource_pool` that * are not in `questions` */ const replacementQuestionPool = computed(() => {}); /** @type {ComputedRef} A list of all channels available which have exercises */ const channels = computed(() => get(_channels)); + /** @type {ComputedRef} A list of all bookmarks available which have exercises */ + const bookmarks = computed(() => get(_bookmarks)); + + /** Handling the Select All Checkbox + * See: remove/toggleQuestionFromSelection() & selectAllQuestions() for more */ + + /** @type {ComputedRef} Whether all active questions are selected */ + const allQuestionsSelected = computed(() => { + return isEqual( + get(selectedActiveQuestions).sort(), + get(activeQuestions) + .map(q => q.question_id) + .sort() + ); + }); + + /** + * Deletes and clears the selected questions from the active section + */ + function deleteActiveSelectedQuestions() { + const { section_id, questions } = get(activeSection); + const selectedIds = get(selectedActiveQuestions); + const newQuestions = questions.filter(q => !selectedIds.includes(q.question_id)); + updateSection({ section_id, questions: newQuestions }); + set(_selectedQuestionIds, []); + } + + const noQuestionsSelected = computed(() => get(selectedActiveQuestions).length === 0); + /** @type {ComputedRef} The label that should be shown alongside the "Select all" checkbox + */ + const selectAllLabel = computed(() => { + if (get(noQuestionsSelected)) { + const { selectAllLabel$ } = enhancedQuizManagementStrings; + return selectAllLabel$(); + } else { + const { numberOfSelectedQuestions$ } = enhancedQuizManagementStrings; + return numberOfSelectedQuestions$({ count: get(selectedActiveQuestions).length }); + } + }); + + /** @type {ComputedRef} Whether the select all checkbox should be indeterminate */ + const selectAllIsIndeterminate = computed(() => { + return !get(allQuestionsSelected) && !get(noQuestionsSelected); + }); return { // Methods @@ -290,8 +427,11 @@ export default () => { setActiveSection, initializeQuiz, updateQuiz, + deleteActiveSelectedQuestions, addQuestionToSelection, removeQuestionFromSelection, + toggleQuestionInSelection, + selectAllQuestions, // Computed channels, @@ -304,5 +444,10 @@ export default () => { activeQuestions, selectedActiveQuestions, replacementQuestionPool, + selectAllIsIndeterminate, + selectAllLabel, + allQuestionsSelected, + noQuestionsSelected, + bookmarks, }; }; diff --git a/kolibri/plugins/coach/assets/src/constants/index.js b/kolibri/plugins/coach/assets/src/constants/index.js index a3dd22e8b41..d67e422fa9a 100644 --- a/kolibri/plugins/coach/assets/src/constants/index.js +++ b/kolibri/plugins/coach/assets/src/constants/index.js @@ -16,6 +16,9 @@ export const PageNames = { QUIZ_SECTION_EDITOR: 'QUIZ_SECTION_EDITOR', QUIZ_REPLACE_QUESTIONS: 'QUIZ_REPLACE_QUESTIONS', QUIZ_SELECT_RESOURCES: 'QUIZ_SELECT_RESOURCES', + SELECT_FROM_RESOURCE: 'SELECT_FROM_RESOURCE', + BOOK_MARKED_RESOURCES: 'BOOK_MARKED_RESOURCES', + SELECTED_BOOKMARKS: 'SELECTED_BOOKMARKS', /** TODO Remove unused */ EXAM_CREATION_TOPIC: 'EXAM_CREATION_TOPIC', diff --git a/kolibri/plugins/coach/assets/src/routes/planExamRoutes.js b/kolibri/plugins/coach/assets/src/routes/planExamRoutes.js index d76eef2f605..1b7a824f9df 100644 --- a/kolibri/plugins/coach/assets/src/routes/planExamRoutes.js +++ b/kolibri/plugins/coach/assets/src/routes/planExamRoutes.js @@ -49,6 +49,23 @@ export default [ { name: PageNames.QUIZ_SELECT_RESOURCES, path: ':section_id/select-resources', + + children: [ + { + name: PageNames.SELECT_FROM_RESOURCE, + path: ':topic_id/resource', + }, + ], + }, + { + name: PageNames.BOOK_MARKED_RESOURCES, + path: ':section_id/book-marked-resources', + children: [ + { + name: PageNames.SELECTED_BOOKMARKS, + path: ':topic_id', + }, + ], }, ], }, diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/AccordionContainer.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/AccordionContainer.vue index b07998825aa..0f5d53e88cd 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/AccordionContainer.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/AccordionContainer.vue @@ -6,6 +6,11 @@ name="list" class="wrapper" > + @@ -32,7 +31,7 @@ required: true, }, id: { - type: Number, + type: String, required: true, }, }, diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/BookMarkedResource.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/BookMarkedResource.vue new file mode 100644 index 00000000000..569dc6c76ca --- /dev/null +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/BookMarkedResource.vue @@ -0,0 +1,122 @@ + + + + + + + diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreateQuizSection.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreateQuizSection.vue index 6f9dc0adbb4..e7b78407e30 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreateQuizSection.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreateQuizSection.vue @@ -1,9 +1,7 @@ @@ -131,12 +85,15 @@ {{ addSectionLabel$() }} @@ -145,37 +102,225 @@ -
- - -

{{ quizForge.activeSection.value.section_id }}

-
- ? -
+
+ + + + + + + +
+ ? +
-

- {{ noQuestionsInSection$() }} -

+

+ {{ noQuestionsInSection$() }} +

-

{{ addQuizSectionQuestionsInstructions$() }}

+

{{ addQuizSectionQuestionsInstructions$() }}

- - {{ addQuestionsLabel$() }} - + + {{ addQuestionsLabel$() }} + +
+
+ + +

+ {{ questionList$() }} +

+
+ + + + + +
+ + + + + +
+
@@ -186,17 +331,29 @@ + + + diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ReplaceQuestions.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ReplaceQuestions.vue index 7116850ebe9..4357c1e4ee1 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ReplaceQuestions.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ReplaceQuestions.vue @@ -2,6 +2,9 @@

Replace questions

+

+ {{ JSON.stringify(question) }} +

Select resources @@ -16,6 +19,7 @@ export default { name: 'ReplaceQuestions', + inject: ['quizForge'], }; diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ResourceSelection.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ResourceSelection.vue index 86e9b8d5f06..f2eebb53a00 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ResourceSelection.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ResourceSelection.vue @@ -1,20 +1,538 @@ + + + diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/SectionSidePanel.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/SectionSidePanel.vue index c3ed566db20..a2938e3b7ce 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/SectionSidePanel.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/SectionSidePanel.vue @@ -19,19 +19,32 @@ import SidePanelModal from 'kolibri-common/components/SidePanelModal'; import { PageNames } from '../../../constants'; + import ResourceSelectionBreadcrumbs from '../../plan/LessonResourceSelectionPage/SearchTools/ResourceSelectionBreadcrumbs'; import SectionEditor from './SectionEditor'; import ReplaceQuestions from './ReplaceQuestions'; import ResourceSelection from './ResourceSelection'; + import ShowBookMarkedResources from './ShowBookMarkedResources.vue'; + // import SelectedChannel from './SelectedChannel.vue'; const pageNameComponentMap = { [PageNames.QUIZ_SECTION_EDITOR]: SectionEditor, [PageNames.QUIZ_REPLACE_QUESTIONS]: ReplaceQuestions, [PageNames.QUIZ_SELECT_RESOURCES]: ResourceSelection, + [PageNames.BOOK_MARKED_RESOURCES]: ShowBookMarkedResources, + [PageNames.SELECT_FROM_RESOURCE]: ResourceSelection, }; export default { name: 'SectionSidePanel', - components: { SidePanelModal, SectionEditor, ReplaceQuestions, ResourceSelection }, + components: { + SidePanelModal, + SectionEditor, + ReplaceQuestions, + ResourceSelection, + // SelectedChannel, + ResourceSelectionBreadcrumbs, + ShowBookMarkedResources, + }, inject: ['quizForge'], data() { return { diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/SelectedChannel.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/SelectedChannel.vue new file mode 100644 index 00000000000..0e30dece1f1 --- /dev/null +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/SelectedChannel.vue @@ -0,0 +1,202 @@ + + + + diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ShowBookMarkedResources.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ShowBookMarkedResources.vue new file mode 100644 index 00000000000..8215886d9eb --- /dev/null +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ShowBookMarkedResources.vue @@ -0,0 +1,150 @@ + + + + diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/TabsWithOverflow.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/TabsWithOverflow.vue index 9808153c587..8375fa0dbc8 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/TabsWithOverflow.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/TabsWithOverflow.vue @@ -7,6 +7,7 @@ v-bind="$attrs" :activeTabId="activeTabId" :tabs="tabs" + @click="id => $emit('click', id)" >