From 8bae1d7a3a2a13b4c8ad476cd5d985cb6810470e Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Tue, 11 Jun 2024 09:21:52 -0700 Subject: [PATCH 01/23] Allow any data type to define a functional default. --- kolibri/core/assets/src/objectSpecs.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kolibri/core/assets/src/objectSpecs.js b/kolibri/core/assets/src/objectSpecs.js index 1bfb6a764b1..95df4dfaaf1 100644 --- a/kolibri/core/assets/src/objectSpecs.js +++ b/kolibri/core/assets/src/objectSpecs.js @@ -186,8 +186,7 @@ export function objectWithDefaults(object, spec) { // set defaults if necessary if (isUndefined(cloned[dataKey]) && !isUndefined(options.default)) { // arrays and objects need to use a function to return defaults - const needsFunction = options.type === Array || options.type === Object; - if (needsFunction && options.default !== null) { + if (options.default instanceof Function) { cloned[dataKey] = options.default(); } // all other types can be assigned directly From 6e269b8626fa9ea4dc565c5195b3eca108fdc059 Mon Sep 17 00:00:00 2001 From: Richard Tibbles Date: Fri, 14 Jun 2024 15:24:02 -0700 Subject: [PATCH 02/23] Remove use of section_id for routing of sections. Prefer sectionIndex. --- kolibri/core/assets/src/exams/utils.js | 2 - kolibri/core/assets/test/exams/utils.spec.js | 4 - .../content/test/utils/test_assignment.py | 2 - kolibri/core/exams/models.py | 7 +- kolibri/core/exams/serializers.py | 1 - kolibri/core/exams/test/test_exam_api.py | 46 ------- .../src/composables/quizCreationSpecs.js | 6 +- .../assets/src/composables/useQuizCreation.js | 116 ++++++++---------- .../coach/assets/src/routes/planExamRoutes.js | 36 +++--- .../plan/CreateExamPage/CreateQuizSection.vue | 93 +++++++------- .../plan/CreateExamPage/ResourceSelection.vue | 6 +- .../plan/CreateExamPage/SectionEditor.vue | 25 ++-- .../plan/CreateExamPage/SectionSidePanel.vue | 21 +--- .../src/views/plan/CreateExamPage/index.vue | 8 +- .../coach/assets/test/useQuizCreation.spec.js | 28 ++--- 15 files changed, 156 insertions(+), 245 deletions(-) diff --git a/kolibri/core/assets/src/exams/utils.js b/kolibri/core/assets/src/exams/utils.js index 5b48f57dac1..b95465a8ba1 100644 --- a/kolibri/core/assets/src/exams/utils.js +++ b/kolibri/core/assets/src/exams/utils.js @@ -1,6 +1,5 @@ import uniq from 'lodash/uniq'; import some from 'lodash/some'; -import { v4 as uuidv4 } from 'uuid'; import { ExamResource, ContentNodeResource } from 'kolibri.resources'; /* @@ -99,7 +98,6 @@ export function convertExamQuestionSourcesV2toV3({ question_sources, learners_se const questions = question_sources; return [ { - section_id: uuidv4(), section_title: '', description: '', questions, diff --git a/kolibri/core/assets/test/exams/utils.spec.js b/kolibri/core/assets/test/exams/utils.spec.js index 57c4c1ad266..22559b314d7 100644 --- a/kolibri/core/assets/test/exams/utils.spec.js +++ b/kolibri/core/assets/test/exams/utils.spec.js @@ -139,11 +139,8 @@ describe('exam utils', () => { it('returns an array of newly structured objects with old question sources in questions', async () => { const converted = await convertExamQuestionSources(exam); - // The section id is randomly generated so just test that it is there and is set on the object - expect(converted.question_sources[0].section_id).toBeTruthy(); expect(converted.question_sources).toEqual([ { - section_id: converted.question_sources[0].section_id, section_title: '', description: '', questions: expectedSources.sort(), @@ -163,7 +160,6 @@ describe('exam utils', () => { const converted = await convertExamQuestionSources(exam); expect(converted.question_sources).toEqual([ { - section_id: converted.question_sources[0].section_id, section_title: '', description: '', questions: expectedSources.sort(), diff --git a/kolibri/core/content/test/utils/test_assignment.py b/kolibri/core/content/test/utils/test_assignment.py index 60b0d74c4e2..b16c32bc5ae 100644 --- a/kolibri/core/content/test/utils/test_assignment.py +++ b/kolibri/core/content/test/utils/test_assignment.py @@ -488,7 +488,6 @@ def test_on_downloadable_assignment__exam(self): ] resources = [ { - "section_id": uuid.uuid4().hex, "section_title": "Test Section Title", "description": "Test descripton for Section", "questions": questions, @@ -539,7 +538,6 @@ def test_on_downloadable_assignment__individual_syncable_exam(self): ] resources = [ { - "section_id": uuid.uuid4().hex, "section_title": "Test Section Title", "description": "Test descripton for Section", "questions": questions, diff --git a/kolibri/core/exams/models.py b/kolibri/core/exams/models.py index 567049165ef..1c72bc9e89c 100644 --- a/kolibri/core/exams/models.py +++ b/kolibri/core/exams/models.py @@ -23,13 +23,13 @@ def exam_assignment_lookup(question_sources): :return: a tuple of contentnode_id and metadata """ for question_source in question_sources: - if "section_id" in question_source: + if "exercise_id" in question_source: + yield (question_source["exercise_id"], None) + else: questions = question_source.get("questions") if questions is not None: for question in question_source["questions"]: yield (question["exercise_id"], None) - else: - yield (question_source["exercise_id"], None) class AbstractExam(models.Model): @@ -57,7 +57,6 @@ class Meta: [ # Section 1 { - "section_id": , "section_title":
, "description":
, "question_count": , diff --git a/kolibri/core/exams/serializers.py b/kolibri/core/exams/serializers.py index 873b3a08e8a..53af4044dcd 100644 --- a/kolibri/core/exams/serializers.py +++ b/kolibri/core/exams/serializers.py @@ -29,7 +29,6 @@ class QuestionSourceSerializer(Serializer): class QuizSectionSerializer(Serializer): - section_id = HexUUIDField(format="hex") description = CharField(required=False, allow_blank=True) section_title = CharField(allow_blank=True, required=False) question_count = IntegerField() diff --git a/kolibri/core/exams/test/test_exam_api.py b/kolibri/core/exams/test/test_exam_api.py index 0be21878e76..350dbb64346 100644 --- a/kolibri/core/exams/test/test_exam_api.py +++ b/kolibri/core/exams/test/test_exam_api.py @@ -72,7 +72,6 @@ def setUpTestData(cls): creator=cls.admin, question_sources=[ { - "section_id": uuid.uuid4().hex, "section_title": "Test Section Title", "description": "Test descripton for Section", "questions": [ @@ -108,7 +107,6 @@ def make_basic_sections(self, no_of_sec): questions = self.make_basic_questions(3) for i in range(1, no_of_sec + 1): section = { - "section_id": uuid.uuid4().hex, "section_title": "Test Section Title", "description": "Test descripton for Section", "questions": questions, @@ -369,43 +367,6 @@ def test_can_get_quiz_size(self): ) self.assertEqual(response.status_code, 200) - def test_quiz_section_with_no_section_id(self): - self.login_as_admin() - exam = self.make_basic_exam() - title = "no_section_id" - questions = self.make_basic_questions(1) - exam["title"] = title - exam["question_sources"].append( - { - "section_title": "Test Section Title", - "description": "Test descripton for Section", - "questions": questions, - "question_count": 0, - "learners_see_fixed_order": False, - } - ) - response = self.post_new_exam(exam) - self.assertEqual(response.status_code, 400) - self.assertExamNotExist(title=title) - - def test_quiz_section_with_invalid_section_id(self): - self.login_as_admin() - exam = self.make_basic_exam() - title = "invalid_section_sources" - exam["title"] = title - exam["question_sources"].append( - { - "section_id": "evil ID", - "section_title": "Test Section Title", - "description": "Test descripton for Section", - "question_count": 0, - "learners_see_fixed_order": False, - } - ) - response = self.post_new_exam(exam) - self.assertEqual(response.status_code, 400) - self.assertExamNotExist(title=title) - def test_quiz_section_with_no_question_count(self): self.login_as_admin() exam = self.make_basic_exam() @@ -414,7 +375,6 @@ def test_quiz_section_with_no_question_count(self): exam["title"] = title exam["question_sources"].append( { - "section_id": uuid.uuid4().hex, "section_title": "Test Section Title", "description": "Test descripton for Section", "questions": questions, @@ -432,7 +392,6 @@ def test_quiz_section_with_no_question_succeds(self): exam["title"] = title exam["question_sources"].append( { - "section_id": uuid.uuid4().hex, "section_title": "Test Section Title", "description": "Test descripton for Section", "question_count": 0, @@ -650,7 +609,6 @@ def test_logged_in_admin_exam_can_create_and_publish_remove_empty_sections(self) exam["draft"] = False exam["question_sources"].append( { - "section_id": uuid.uuid4().hex, "section_title": "Test Section Title", "description": "Test descripton for Section", "question_count": 0, @@ -678,7 +636,6 @@ def test_logged_in_admin_exam_cant_create_and_publish_empty_sections(self): exam["draft"] = False exam["question_sources"] = [ { - "section_id": uuid.uuid4().hex, "section_title": "Test Section Title", "description": "Test descripton for Section", "question_count": 0, @@ -686,7 +643,6 @@ def test_logged_in_admin_exam_cant_create_and_publish_empty_sections(self): "learners_see_fixed_order": False, }, { - "section_id": uuid.uuid4().hex, "section_title": "Test Section Title", "description": "Test descripton for Section", "question_count": 0, @@ -856,7 +812,6 @@ def test_logged_in_admin_exam_can_update_and_publish_remove_empty_sections(self) self.login_as_admin() self.exam.question_sources = self.make_basic_sections(1) + [ { - "section_id": uuid.uuid4().hex, "section_title": "Test Section Title", "description": "Test descripton for Section", "question_count": 0, @@ -875,7 +830,6 @@ def test_logged_in_admin_exam_cant_update_and_publish_empty_quiz(self): self.login_as_admin() self.exam.question_sources = [ { - "section_id": uuid.uuid4().hex, "section_title": "Test Section Title", "description": "Test descripton for Section", "question_count": 0, diff --git a/kolibri/plugins/coach/assets/src/composables/quizCreationSpecs.js b/kolibri/plugins/coach/assets/src/composables/quizCreationSpecs.js index f827b4be6e6..5d4c1a5ff5d 100644 --- a/kolibri/plugins/coach/assets/src/composables/quizCreationSpecs.js +++ b/kolibri/plugins/coach/assets/src/composables/quizCreationSpecs.js @@ -96,13 +96,13 @@ export const QuizQuestion = { * section is shown in the same order, or * randomized, to the learners * @property {QuizExercise[]} resource_pool An array of QuizExercise objects from - * which the questions in this section_id + * which the questions in this section * will be drawn */ export const QuizSection = { section_id: { type: String, - required: true, + default: () => String(performance.now()), }, section_title: { type: String, @@ -178,7 +178,7 @@ export const Quiz = { }, seed: { type: Number, - default: getRandomInt(), + default: getRandomInt, }, // Default to sections being shown in a fixed order learners_see_fixed_order: { diff --git a/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js b/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js index 8684bf788e8..1418e411b8c 100644 --- a/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js +++ b/kolibri/plugins/coach/assets/src/composables/useQuizCreation.js @@ -1,4 +1,3 @@ -import { v4 } from 'uuid'; import isEqual from 'lodash/isEqual'; import { enhancedQuizManagementStrings } from 'kolibri-common/strings/enhancedQuizManagementStrings'; import uniq from 'lodash/uniq'; @@ -6,16 +5,19 @@ import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants'; import { ChannelResource, ExamResource } from 'kolibri.resources'; import { validateObject, objectWithDefaults } from 'kolibri.utils.objectSpecs'; import { get, set } from '@vueuse/core'; -import { computed, ref, provide, inject } from 'kolibri.lib.vueCompositionApi'; +import { + computed, + ref, + provide, + inject, + getCurrentInstance, + watch, +} from 'kolibri.lib.vueCompositionApi'; import { fetchExamWithContent } from 'kolibri.utils.exams'; // TODO: Probably move this to this file's local dir import selectQuestions from '../utils/selectQuestions.js'; import { Quiz, QuizSection } from './quizCreationSpecs.js'; -function uuidv4() { - return v4().replace(/-/g, ''); -} - /** Validators **/ /* objectSpecs expects every property to be available -- but we don't want to have to make an * object with every property just to validate it. So we use these functions to validate subsets @@ -40,6 +42,7 @@ const fieldsToSave = [ * Composable function presenting primary interface for Quiz Creation */ export default function useQuizCreation() { + const store = getCurrentInstance()?.proxy?.$store; // ----------- // Local state // ----------- @@ -53,7 +56,7 @@ export default function useQuizCreation() { /** @type {ref} * The section that is currently selected for editing */ - const _activeSectionId = ref(null); + const activeSectionIndex = computed(() => Number(store?.state?.route?.params?.sectionIndex || 0)); /** @type {ref} * The QuizQuestion.id's that are currently selected for action in the active section */ @@ -72,14 +75,14 @@ export default function useQuizCreation() { /** * @param {QuizSection} section * @returns {QuizSection} - * @affects _quiz - Updates the section with the given section_id with the given param + * @affects _quiz - Updates the section with the given sectionIndex with the given param * @throws {TypeError} if section is not a valid QuizSection **/ - function updateSection({ section_id, ...updates }) { + function updateSection({ sectionIndex, ...updates }) { set(quizHasChanged, true); - const targetSection = get(allSections).find(section => section.section_id === section_id); + const targetSection = get(allSections)[sectionIndex]; if (!targetSection) { - throw new TypeError(`Section with id ${section_id} not found; cannot be updated.`); + throw new TypeError(`Section with id ${sectionIndex} not found; cannot be updated.`); } // original variables are the original values of the properties we're updating @@ -158,15 +161,16 @@ export default function useQuizCreation() { updates.questions = originalQuestions.slice(0, question_count); } + const _allSections = get(allSections); + set(_quiz, { ...get(quiz), // Update matching QuizSections with the updates object - question_sources: get(allSections).map(section => { - if (section.section_id === section_id) { - return { ...section, ...updates }; - } - return section; - }), + question_sources: [ + ..._allSections.slice(0, sectionIndex), + { ...targetSection, ...updates }, + ..._allSections.slice(sectionIndex + 1), + ], }); } @@ -178,7 +182,7 @@ export default function useQuizCreation() { return question; }); updateSection({ - section_id: activeSection.value.section_id, + sectionIndex: get(activeSectionIndex), questions, }); } @@ -227,46 +231,38 @@ export default function useQuizCreation() { /** @returns {QuizSection} * Adds a section to the quiz and returns it */ function addSection() { - const newSection = objectWithDefaults({ section_id: uuidv4() }, QuizSection); + const newSection = objectWithDefaults({}, QuizSection); updateQuiz({ question_sources: [...get(quiz).question_sources, newSection] }); - setActiveSection(newSection.section_id); return newSection; } /** * @throws {Error} if section not found - * Deletes the given section by section_id */ - function removeSection(section_id) { - const updatedSections = get(allSections).filter(section => section.section_id !== section_id); - if (updatedSections.length === get(allSections).length) { - throw new Error(`Section with id ${section_id} not found; cannot be removed.`); - } - if (updatedSections.length === 0) { - const newSection = addSection(); - setActiveSection(newSection.section_id); - updatedSections.push(newSection); - } else { - setActiveSection(get(updatedSections)[0].section_id); + * Deletes the given section by sectionIndex */ + function removeSection(sectionIndex) { + if (!get(allSections)[sectionIndex]) { + throw new Error(`Section with index ${sectionIndex} not found; cannot be removed.`); } + const updatedSections = get(allSections) + .slice(0, sectionIndex) + .concat(get(allSections).slice(sectionIndex + 1)); updateQuiz({ question_sources: updatedSections }); + if (get(allSections).length === 0) { + // Always need to have at least one section + addSection(); + } } - /** - * @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 - * it will set the activeId to the first section in _quiz.question_sources */ - function setActiveSection(section_id = null) { - set(_selectedQuestionIds, []); // Clear the selected questions when changing sections - set(_activeSectionId, section_id); - } - + watch(activeSectionIndex, () => { + // Clear the selected questions when changing sections + set(_selectedQuestionIds, []); + }); // ------------ // Quiz General // ------------ /** @affects _quiz - * @affects _activeSectionId + * @affects activeSectionIndex * @affects _channels - Calls _fetchChannels to bootstrap the list of needed channels * @param {string} collection - The collection (aka current class ID) to associate the exam with * Adds a new section to the quiz and sets the activeSectionID to it, preparing the module for @@ -277,8 +273,7 @@ export default function useQuizCreation() { if (quizId === 'new') { const assignments = [collection]; set(_quiz, objectWithDefaults({ collection, assignments }, Quiz)); - const newSection = addSection(); - setActiveSection(newSection.section_id); + addSection(); } else { const exam = await ExamResource.fetchModel({ id: quizId }); const { exam: quiz, exercises } = await fetchExamWithContent(exam); @@ -297,10 +292,7 @@ export default function useQuizCreation() { }); set(_quiz, objectWithDefaults(quiz, Quiz)); if (get(allSections).length === 0) { - const newSection = addSection(); - setActiveSection(newSection.section_id); - } else { - setActiveSection(get(allSections)[0].section_id); + addSection(); } } set(quizHasChanged, false); @@ -329,6 +321,7 @@ export default function useQuizCreation() { const questionSourcesWithoutResourcePool = get(allSections).map(section => { const sectionToSave = { ...section }; delete sectionToSave.resource_pool; + delete sectionToSave.section_id; sectionToSave.questions = section.questions.map(question => { const questionToSave = { ...question }; delete questionToSave.item; @@ -433,15 +426,12 @@ export default function useQuizCreation() { /** @type {ComputedRef} The value of _quiz's `question_sources` */ const allSections = computed(() => get(quiz).question_sources); /** @type {ComputedRef} The active section */ - const activeSection = computed(() => - get(allSections).find(s => s.section_id === get(_activeSectionId)) - ); - const activeSectionIndex = computed(() => - get(allSections).findIndex(s => isEqual(s.section_title === get(activeSection).section_title)) - ); + const activeSection = computed(() => get(allSections)[get(activeSectionIndex)]); /** @type {ComputedRef} The inactive sections */ const inactiveSections = computed(() => - get(allSections).filter(s => s.section_id !== get(_activeSectionId)) + get(allSections) + .slice(0, get(activeSectionIndex)) + .concat(get(allSections).slice(get(activeSectionIndex) + 1)) ); /** @type {ComputedRef} The active section's `resource_pool` */ const activeResourcePool = computed(() => get(activeSection).resource_pool); @@ -513,12 +503,13 @@ export default function useQuizCreation() { */ function deleteActiveSelectedQuestions() { - const { section_id, questions: section_questions } = get(activeSection); + const sectionIndex = get(activeSectionIndex); + const { questions: section_questions } = get(activeSection); const selectedIds = get(selectedActiveQuestions); const questions = section_questions.filter(q => !selectedIds.includes(q.item)); const question_count = questions.length; updateSection({ - section_id, + sectionIndex, questions, question_count, }); @@ -549,7 +540,6 @@ export default function useQuizCreation() { provide('replaceSelectedQuestions', replaceSelectedQuestions); provide('addSection', addSection); provide('removeSection', removeSection); - provide('setActiveSection', setActiveSection); provide('updateQuiz', updateQuiz); provide('addQuestionToSelection', addQuestionToSelection); provide('removeQuestionFromSelection', removeQuestionFromSelection); @@ -557,6 +547,7 @@ export default function useQuizCreation() { provide('channels', channels); provide('replacements', replacements); provide('allSections', allSections); + provide('activeSectionIndex', activeSectionIndex); provide('activeSection', activeSection); provide('activeSectionIndex', activeSectionIndex); provide('inactiveSections', inactiveSections); @@ -580,7 +571,6 @@ export default function useQuizCreation() { replaceSelectedQuestions, addSection, removeSection, - setActiveSection, initializeQuiz, updateQuiz, clearSelectedQuestions, @@ -618,7 +608,6 @@ export function injectQuizCreation() { const replaceSelectedQuestions = inject('replaceSelectedQuestions'); const addSection = inject('addSection'); const removeSection = inject('removeSection'); - const setActiveSection = inject('setActiveSection'); const updateQuiz = inject('updateQuiz'); const addQuestionToSelection = inject('addQuestionToSelection'); const removeQuestionFromSelection = inject('removeQuestionFromSelection'); @@ -626,8 +615,8 @@ export function injectQuizCreation() { const channels = inject('channels'); const replacements = inject('replacements'); const allSections = inject('allSections'); - const activeSection = inject('activeSection'); const activeSectionIndex = inject('activeSectionIndex'); + const activeSection = inject('activeSection'); const inactiveSections = inject('inactiveSections'); const activeResourcePool = inject('activeResourcePool'); const activeResourceMap = inject('activeResourceMap'); @@ -650,7 +639,6 @@ export function injectQuizCreation() { replaceSelectedQuestions, addSection, removeSection, - setActiveSection, updateQuiz, clearSelectedQuestions, addQuestionToSelection, @@ -664,8 +652,8 @@ export function injectQuizCreation() { channels, replacements, allSections, - activeSection, activeSectionIndex, + activeSection, inactiveSections, activeResourcePool, activeResourceMap, diff --git a/kolibri/plugins/coach/assets/src/routes/planExamRoutes.js b/kolibri/plugins/coach/assets/src/routes/planExamRoutes.js index e296a5a494b..086ea6a2ac0 100644 --- a/kolibri/plugins/coach/assets/src/routes/planExamRoutes.js +++ b/kolibri/plugins/coach/assets/src/routes/planExamRoutes.js @@ -14,7 +14,6 @@ import { import CreatePracticeQuizPage from '../views/plan/CreateExamPage/CreatePracticeQuizPage.vue'; import CreateExamPage from '../views/plan/CreateExamPage'; import CreateExamPreview from '../views/plan/CreateExamPage/CreateExamPreview.vue'; -import SectionSidePanel from '../views/plan/CreateExamPage/SectionSidePanel.vue'; import SectionEditor from '../views/plan/CreateExamPage/SectionEditor.vue'; import ResourceSelection from '../views/plan/CreateExamPage/ResourceSelection.vue'; import ReplaceQuestions from '../views/plan/CreateExamPage/ReplaceQuestions.vue'; @@ -34,30 +33,23 @@ export default [ }, { name: PageNames.EXAM_CREATION_ROOT, - path: '/:classId/plan/quizzes/:quizId/edit', + path: '/:classId/plan/quizzes/:quizId/edit/:sectionIndex', component: CreateExamPage, children: [ { - name: PageNames.EXAM_SIDE_PANEL, - path: ':section_id', - 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_SELECT_RESOURCES, - path: 'select-resources/:topic_id?', - component: ResourceSelection, - }, - ], + name: PageNames.QUIZ_SECTION_EDITOR, + path: 'edit', + component: SectionEditor, + }, + { + name: PageNames.QUIZ_REPLACE_QUESTIONS, + path: 'replace-questions', + component: ReplaceQuestions, + }, + { + name: PageNames.QUIZ_SELECT_RESOURCES, + path: 'select-resources/:topic_id?', + component: ResourceSelection, }, ], }, 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 0bd3171c0d0..6c5e887c6d7 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreateQuizSection.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreateQuizSection.vue @@ -15,9 +15,7 @@ tabsId="quizSectionTabs" class="section-tabs" :tabs="tabs" - :activeTabId="activeSection ? - activeSection.section_id : - '' " + :activeTabId="String(activeSectionIndex)" backgroundColor="transparent" hoverBackgroundColor="transparent" :aria-label="quizSectionsLabel$()" @@ -77,7 +75,7 @@ {{ addQuestionsLabel$() }} @@ -321,7 +319,7 @@ :title="deleteSectionLabel$()" :submitText="coreString('deleteAction')" :cancelText="coreString('cancelAction')" - @cancel="handleShowConfirmation" + @cancel="showDeleteConfirmation = true" @submit="handleConfirmDelete" >