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..b237aa97759 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#0ed2f274b1bc3808218a4d3f526c181b96b32c6d", "lockr": "0.8.5", "lodash": "^4.17.21", "loglevel": "^1.8.1", diff --git a/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js b/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js index 67e547dc9a3..41aa7b3bcea 100644 --- a/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js +++ b/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js @@ -1,4 +1,5 @@ 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'; @@ -8,7 +9,7 @@ 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 +31,7 @@ function isExercise(o) { /** * Composable function presenting primary interface for Quiz Creation */ -export default () => { +export default (DEBUG = false) => { // ----------- // Local state // ----------- @@ -43,9 +44,9 @@ 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([]); @@ -53,6 +54,40 @@ export default () => { /** @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 // ------------------ @@ -103,10 +138,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; @@ -162,8 +197,12 @@ 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(); } @@ -195,21 +234,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,15 +330,56 @@ 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)); + /** 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 saveQuiz, @@ -290,8 +390,11 @@ export default () => { setActiveSection, initializeQuiz, updateQuiz, + deleteActiveSelectedQuestions, addQuestionToSelection, removeQuestionFromSelection, + toggleQuestionInSelection, + selectAllQuestions, // Computed channels, @@ -304,5 +407,9 @@ export default () => { activeQuestions, selectedActiveQuestions, replacementQuestionPool, + selectAllIsIndeterminate, + selectAllLabel, + allQuestionsSelected, + noQuestionsSelected, }; }; 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/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/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)" >