Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finishing up the Quiz Root Page #11434

Merged
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
274ba2f
set not loading when quiz creator comes up
nucleogenesis Oct 31, 2023
b02fdb3
add DEBUG option to useQuizCreation; bootstrap basic accordion setup
nucleogenesis Oct 12, 2023
dba27e4
uuid for test data ID
nucleogenesis Oct 16, 2023
d14514f
remove unneded prop
nucleogenesis Oct 16, 2023
27fb976
drag-and-drop scaffolding function & design
nucleogenesis Oct 16, 2023
7d386d5
styles on the accordion headers
nucleogenesis Oct 17, 2023
4787a7f
section tabs better design parity w/out regression
nucleogenesis Oct 19, 2023
5f90049
dragStart event allows dynamic styles when dragging; disallow highlig…
nucleogenesis Oct 19, 2023
786b66f
design polish
nucleogenesis Oct 20, 2023
2125d3e
helpers for select all checkbox props
nucleogenesis Oct 24, 2023
85a5ac0
enhanced quiz messages updates
nucleogenesis Oct 24, 2023
96b927c
pass click event through tabswithoverflow; a11y improvements
nucleogenesis Oct 24, 2023
d60e666
edit & delete buttons work for section tabs
nucleogenesis Oct 24, 2023
0e33376
expandcollapse icon;
nucleogenesis Oct 25, 2023
11fc970
root page mostly feature complete; polish; a11y updates
nucleogenesis Oct 26, 2023
f166476
handle quiz title field
nucleogenesis Oct 26, 2023
1b08d9c
Ensure [Tab]-nav flow works properly when leaving Options button
nucleogenesis Nov 16, 2023
877d26f
not debug by default
nucleogenesis Nov 16, 2023
a86be79
fix eternal loading state when navigation routes
nucleogenesis Nov 16, 2023
51d0e6a
KDS Update
nucleogenesis Nov 29, 2023
1a65873
lint
nucleogenesis Nov 30, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions kolibri/core/assets/src/views/sortable/DragContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
handleStart() {
// handle cancelation of drags
// document.addEventListener('keyup', this.triggerMouseUpOnESC);
this.$emit('dragStart');
},
handleStop(event) {
const { oldIndex, newIndex } = event.data;
Expand Down
2 changes: 1 addition & 1 deletion kolibri/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
142 changes: 125 additions & 17 deletions kolibri/plugins/coach/assets/src/composables/useQuizCreation.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand All @@ -30,7 +31,7 @@ function isExercise(o) {
/**
* Composable function presenting primary interface for Quiz Creation
*/
export default () => {
export default (DEBUG = false) => {
// -----------
// Local state
// -----------
Expand All @@ -43,16 +44,50 @@ export default () => {
* The section that is currently selected for editing */
const _activeSectionId = ref(null);

/** @type {ref<QuizQuestion[]>}
* The questions that are currently selected for action in the active section */
const _selectedQuestions = ref([]);
/** @type {ref<String[]>}
* The question_ids that are currently selected for action in the active section */
const _selectedQuestionIds = ref([]);

/** @type {ref<Array>} A list of all channels available which have exercises */
const _channels = ref([]);

/** @type {ref<Number>} 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
// ------------------
Expand All @@ -64,6 +99,7 @@ export default () => {
* @throws {TypeError} if section is not a valid QuizSection
**/
function updateSection({ section_id, ...updates }) {
console.log('updating with...', section_id, updates);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very small but can you either get rid of this or add a TODO to cleanup console logs later?

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.`);
Expand Down Expand Up @@ -103,10 +139,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;
Expand Down Expand Up @@ -162,8 +198,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();
}

Expand Down Expand Up @@ -195,21 +235,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() {
Expand Down Expand Up @@ -271,15 +331,56 @@ export default () => {
/** @type {ComputedRef<QuizQuestion[]>} 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<QuizQuestion[]>} All questions the user has selected for the active
* section */
const selectedActiveQuestions = computed(() => get(_selectedQuestions));
/** @type {ComputedRef<String[]>} All question_ids the user has selected for the active section */
const selectedActiveQuestions = computed(() => get(_selectedQuestionIds));
/** @type {ComputedRef<QuizQuestion[]>} Questions in the active section's `resource_pool` that
* are not in `questions` */
const replacementQuestionPool = computed(() => {});
/** @type {ComputedRef<Array>} 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<Boolean>} 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<String>} 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<Boolean>} Whether the select all checkbox should be indeterminate */
const selectAllIsIndeterminate = computed(() => {
return !get(allQuestionsSelected) && !get(noQuestionsSelected);
});

return {
// Methods
saveQuiz,
Expand All @@ -290,8 +391,11 @@ export default () => {
setActiveSection,
initializeQuiz,
updateQuiz,
deleteActiveSelectedQuestions,
addQuestionToSelection,
removeQuestionFromSelection,
toggleQuestionInSelection,
selectAllQuestions,

// Computed
channels,
Expand All @@ -304,5 +408,9 @@ export default () => {
activeQuestions,
selectedActiveQuestions,
replacementQuestionPool,
selectAllIsIndeterminate,
selectAllLabel,
allQuestionsSelected,
noQuestionsSelected,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
name="list"
class="wrapper"
>
<slot
name="top"
:expandAll="expandAll"
:collapseAll="collapseAll"
></slot>
<slot
:toggleItemState="toggleItemState"
:isItemExpanded="isItemExpanded"
Expand All @@ -26,7 +31,18 @@
expandedItemIds: [],
};
},
watch: {
expandedItemIds() {
this.$emit('toggled', this.expandedItemIds);
},
},
methods: {
expandAll(ids = []) {
this.expandedItemIds = ids;
},
collapseAll() {
this.expandedItemIds = [];
},
toggleItemState(id) {
const index = this.expandedItemIds.indexOf(id);
if (index === -1) {
Expand All @@ -43,6 +59,7 @@
const index = this.expandedItemIds.indexOf(id);
this.expandedItemIds.splice(index, 1);
}
this.$emit('toggled', this.expandedItemIds);
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
<slot
:id="id"
name="content"
:answers="title"
>
</slot>
</div>
Expand All @@ -32,7 +31,7 @@
required: true,
},
id: {
type: Number,
type: String,
required: true,
},
},
Expand Down
Loading
Loading